mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-03-09 15:40:18 +00:00
Python pacakge structure and installing it as python packages
This commit is contained in:
parent
9001fee975
commit
f87702f638
19 changed files with 264 additions and 31 deletions
1
virtscreen/__init__.py
Normal file
1
virtscreen/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
__all__ = ['virtscreen']
|
16
virtscreen/data/config.default.json
Normal file
16
virtscreen/data/config.default.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"version": 0.1,
|
||||
"theme_color": 8,
|
||||
"virt": {
|
||||
"device": "VIRTUAL1",
|
||||
"width": 1368,
|
||||
"height": 1024,
|
||||
"portrait": false,
|
||||
"hidpi": false
|
||||
},
|
||||
"vnc": {
|
||||
"port": 5900,
|
||||
"autostart": false
|
||||
},
|
||||
"presets": []
|
||||
}
|
BIN
virtscreen/icon/gif_example.gif
Normal file
BIN
virtscreen/icon/gif_example.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.8 MiB |
BIN
virtscreen/icon/icon.png
Normal file
BIN
virtscreen/icon/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
virtscreen/icon/icon_tablet_off.png
Normal file
BIN
virtscreen/icon/icon_tablet_off.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
virtscreen/icon/icon_tablet_on.png
Normal file
BIN
virtscreen/icon/icon_tablet_on.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
294
virtscreen/qml/AppWindow.qml
Normal file
294
virtscreen/qml/AppWindow.qml
Normal file
|
@ -0,0 +1,294 @@
|
|||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Material 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import VirtScreen.Backend 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
id: window
|
||||
visible: false
|
||||
flags: Qt.FramelessWindowHint
|
||||
title: "Basic layouts"
|
||||
|
||||
property int theme_color: settings.theme_color
|
||||
Material.theme: Material.Light
|
||||
Material.primary: theme_color
|
||||
Material.accent: theme_color
|
||||
// Material.background: Material.Grey
|
||||
|
||||
width: 380
|
||||
height: 540
|
||||
property int margin: 10
|
||||
property int popupWidth: width - 26
|
||||
|
||||
// hide screen when loosing focus
|
||||
property bool autoClose: true
|
||||
property bool ignoreCloseOnce: false
|
||||
onAutoCloseChanged: {
|
||||
// When setting auto close disabled and then enabled again, we need to
|
||||
// ignore focus change once. Otherwise the window always is closed one time
|
||||
// even when the mouse is clicked in the window.
|
||||
if (!autoClose) {
|
||||
ignoreCloseOnce = true;
|
||||
}
|
||||
}
|
||||
onActiveFocusItemChanged: {
|
||||
if (autoClose && !ignoreCloseOnce && !activeFocusItem && !sysTrayIcon.clicked) {
|
||||
this.hide();
|
||||
}
|
||||
if (ignoreCloseOnce && autoClose) {
|
||||
ignoreCloseOnce = false;
|
||||
}
|
||||
}
|
||||
|
||||
menuBar: ToolBar {
|
||||
id: toolbar
|
||||
font.weight: Font.Medium
|
||||
font.pointSize: 11 //parent.font.pointSize + 1
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: margin + 10
|
||||
|
||||
Label {
|
||||
id: vncStateLabel
|
||||
color: "white"
|
||||
text: vncStateText.text
|
||||
}
|
||||
|
||||
ToolButton {
|
||||
id: menuButton
|
||||
anchors.right: parent.right
|
||||
text: qsTr("⋮")
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
font: parent.font
|
||||
color: "white"
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
onClicked: menu.open()
|
||||
|
||||
Menu {
|
||||
id: menu
|
||||
y: toolbar.height
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("&Preference")
|
||||
onTriggered: {
|
||||
preferenceLoader.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("&About")
|
||||
onTriggered: {
|
||||
aboutDialog.open();
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem {
|
||||
text: qsTr("&Quit")
|
||||
onTriggered: quitAction.onTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header: TabBar {
|
||||
id: tabBar
|
||||
position: TabBar.Footer
|
||||
// Material.primary: Material.Teal
|
||||
|
||||
currentIndex: 0
|
||||
|
||||
TabButton {
|
||||
text: qsTr("Display")
|
||||
}
|
||||
|
||||
TabButton {
|
||||
text: qsTr("VNC")
|
||||
}
|
||||
}
|
||||
|
||||
footer: ProgressBar {
|
||||
z: 1
|
||||
indeterminate: backend.vncState == Backend.WAITING
|
||||
value: backend.vncState == Backend.CONNECTED ? 1 : 0
|
||||
}
|
||||
|
||||
|
||||
Popup {
|
||||
id: busyDialog
|
||||
modal: true
|
||||
closePolicy: Popup.NoAutoClose
|
||||
x: (parent.width - width) / 2
|
||||
y: parent.height / 2 - height
|
||||
BusyIndicator {
|
||||
anchors.fill: parent
|
||||
running: true
|
||||
}
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
implicitWidth: 100
|
||||
implicitHeight: 100
|
||||
// border.color: "#444"
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: aboutDialog
|
||||
focus: true
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.width - height) / 2 //(window.height) / 2
|
||||
width: popupWidth
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font { weight: Font.Bold; pointSize: 15 }
|
||||
text: "VirtScreen"
|
||||
}
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "Make your iPad/tablet/computer<br/>as a secondary monitor.<br/>"
|
||||
}
|
||||
Text {
|
||||
text: "- <a href='https://github.com/kbumsik/VirtScreen'>Project Website</a>"
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
Text {
|
||||
text: "- <a href='https://github.com/kbumsik/VirtScreen/issues'>Issues & Bug Report</a>"
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
Text {
|
||||
font { pointSize: 10 }
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
lineHeight: 0.7
|
||||
text: "<br/>Copyright © 2018 Bumsik Kim <a href='https://kbumsik.io/'>Homepage</a><br/>"
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
Text {
|
||||
font { pointSize: 9 }
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "This program comes with absolutely no warranty.<br/>" +
|
||||
"See the <a href='https://github.com/kbumsik/VirtScreen/blob/master/LICENSE'>" +
|
||||
"GNU General Public License, version 3</a> for details."
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: passwordDialog
|
||||
title: "New password"
|
||||
focus: true
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok | Dialog.Cancel
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.width - height) / 2 //(window.height) / 2
|
||||
width: popupWidth
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
TextField {
|
||||
id: passwordFIeld
|
||||
focus: true
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
placeholderText: "New Password";
|
||||
echoMode: TextInput.Password;
|
||||
}
|
||||
Keys.onPressed: {
|
||||
event.accepted = true;
|
||||
if (event.key == Qt.Key_Return || event.key == Qt.Key_Enter) {
|
||||
passwordDialog.accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
backend.createVNCPassword(passwordFIeld.text);
|
||||
passwordFIeld.text = "";
|
||||
}
|
||||
onRejected: passwordFIeld.text = ""
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: errorDialog
|
||||
title: "Error"
|
||||
focus: true
|
||||
modal: true
|
||||
standardButtons: Dialog.Ok
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.width - height) / 2 //(window.height) / 2
|
||||
width: popupWidth
|
||||
height: 310
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
TextArea {
|
||||
// readOnly: true
|
||||
selectByMouse: true
|
||||
Layout.fillWidth: true
|
||||
// wrapMode: Text.WordWrap
|
||||
text: errorText.text
|
||||
onTextChanged: {
|
||||
if (text) {
|
||||
busyDialog.close();
|
||||
errorDialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
// parent: ipListView.parent
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
ScrollBar.horizontal: ScrollBar {
|
||||
// parent: ipListView.parent
|
||||
anchors.top: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: preferenceLoader
|
||||
active: false
|
||||
source: "preferenceDialog.qml"
|
||||
onLoaded: {
|
||||
item.onClosed.connect(function() {
|
||||
preferenceLoader.active = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SwipeView {
|
||||
anchors.top: tabBar.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: margin
|
||||
clip: true
|
||||
|
||||
currentIndex: tabBar.currentIndex
|
||||
|
||||
// in the same "qml" folder
|
||||
DisplayPage {}
|
||||
VncPage {}
|
||||
}
|
||||
}
|
138
virtscreen/qml/DisplayPage.qml
Normal file
138
virtscreen/qml/DisplayPage.qml
Normal file
|
@ -0,0 +1,138 @@
|
|||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import VirtScreen.Backend 1.0
|
||||
|
||||
ColumnLayout {
|
||||
GroupBox {
|
||||
title: "Virtual Display"
|
||||
Layout.fillWidth: true
|
||||
enabled: backend.virtScreenCreated ? false : true
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
RowLayout {
|
||||
Label { text: "Width"; Layout.fillWidth: true }
|
||||
SpinBox {
|
||||
value: settings.virt.width
|
||||
from: 640
|
||||
to: 1920
|
||||
stepSize: 1
|
||||
editable: true
|
||||
onValueModified: {
|
||||
settings.virt.width = value;
|
||||
}
|
||||
textFromValue: function(value, locale) { return value; }
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Label { text: "Height"; Layout.fillWidth: true }
|
||||
SpinBox {
|
||||
value: settings.virt.height
|
||||
from: 360
|
||||
to: 1080
|
||||
stepSize : 1
|
||||
editable: true
|
||||
onValueModified: {
|
||||
settings.virt.height = value;
|
||||
}
|
||||
textFromValue: function(value, locale) { return value; }
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Label { text: "Portrait Mode"; Layout.fillWidth: true }
|
||||
Switch {
|
||||
checked: settings.virt.portrait
|
||||
onCheckedChanged: {
|
||||
settings.virt.portrait = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Label { text: "HiDPI (2x resolution)"; Layout.fillWidth: true }
|
||||
Switch {
|
||||
checked: settings.virt.hidpi
|
||||
onCheckedChanged: {
|
||||
settings.virt.hidpi = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
Label { id: deviceLabel; text: "Device"; }
|
||||
ComboBox {
|
||||
id: deviceComboBox
|
||||
anchors.left: deviceLabel.right
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 100
|
||||
textRole: "name"
|
||||
model: backend.screens
|
||||
currentIndex: backend.virtScreenIndex
|
||||
onActivated: function(index) {
|
||||
backend.virtScreenIndex = index
|
||||
}
|
||||
delegate: ItemDelegate {
|
||||
width: deviceComboBox.width
|
||||
text: modelData.name
|
||||
font.weight: deviceComboBox.currentIndex === index ? Font.DemiBold : Font.Normal
|
||||
highlighted: ListView.isCurrentItem
|
||||
enabled: modelData.connected ? false : true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.margins: margin / 2
|
||||
Button {
|
||||
id: virtScreenButton
|
||||
Layout.fillWidth: true
|
||||
text: virtScreenAction.text
|
||||
highlighted: true
|
||||
enabled: virtScreenAction.enabled
|
||||
onClicked: {
|
||||
busyDialog.open();
|
||||
virtScreenAction.onTriggered();
|
||||
connectOnce(backend.onVirtScreenCreatedChanged, function(created) {
|
||||
busyDialog.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: displaySettingButton
|
||||
Layout.fillWidth: true
|
||||
text: "Open Display Setting"
|
||||
enabled: backend.virtScreenCreated ? true : false
|
||||
onClicked: {
|
||||
busyDialog.open();
|
||||
window.autoClose = false;
|
||||
if (backend.vncState != Backend.OFF) {
|
||||
console.log("vnc is running");
|
||||
var restoreVNC = true;
|
||||
if (autostart) {
|
||||
autostart = false;
|
||||
var restoreAutoStart = true;
|
||||
}
|
||||
}
|
||||
connectOnce(backend.onDisplaySettingClosed, function() {
|
||||
window.autoClose = true;
|
||||
busyDialog.close();
|
||||
if (restoreAutoStart) {
|
||||
autostart = true;
|
||||
}
|
||||
if (restoreVNC) {
|
||||
backend.startVNC(settings.vnc.port);
|
||||
}
|
||||
});
|
||||
backend.stopVNC();
|
||||
backend.openDisplaySetting();
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
// Empty layout
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
105
virtscreen/qml/VncPage.qml
Normal file
105
virtscreen/qml/VncPage.qml
Normal file
|
@ -0,0 +1,105 @@
|
|||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import VirtScreen.Backend 1.0
|
||||
|
||||
ColumnLayout {
|
||||
GroupBox {
|
||||
title: "VNC Server"
|
||||
Layout.fillWidth: true
|
||||
enabled: backend.vncState == Backend.OFF ? true : false
|
||||
ColumnLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
RowLayout {
|
||||
Label { text: "Port"; Layout.fillWidth: true }
|
||||
SpinBox {
|
||||
value: settings.vnc.port
|
||||
from: 1
|
||||
to: 65535
|
||||
stepSize: 1
|
||||
editable: true
|
||||
onValueModified: {
|
||||
settings.vnc.port = value;
|
||||
}
|
||||
textFromValue: function(value, locale) { return value; }
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
Label { text: "Password"; Layout.fillWidth: true }
|
||||
Button {
|
||||
text: "Delete"
|
||||
font.capitalization: Font.MixedCase
|
||||
highlighted: false
|
||||
enabled: backend.vncUsePassword
|
||||
onClicked: backend.deleteVNCPassword()
|
||||
}
|
||||
Button {
|
||||
text: "New"
|
||||
font.capitalization: Font.MixedCase
|
||||
highlighted: true
|
||||
enabled: !backend.vncUsePassword
|
||||
onClicked: passwordDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: margin / 2
|
||||
Button {
|
||||
id: vncButton
|
||||
Layout.fillWidth: true
|
||||
text: vncAction.text
|
||||
highlighted: true
|
||||
enabled: vncAction.enabled
|
||||
onClicked: vncAction.onTriggered()
|
||||
}
|
||||
CheckBox {
|
||||
checked: autostart
|
||||
onToggled: {
|
||||
autostart = checked;
|
||||
if ((checked == true) && (backend.vncState == Backend.OFF) &&
|
||||
backend.virtScreenCreated) {
|
||||
backend.startVNC(settings.vnc.port);
|
||||
}
|
||||
}
|
||||
}
|
||||
Label { text: "Auto"; }
|
||||
}
|
||||
GroupBox {
|
||||
title: "Available IP addresses"
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 150
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
ListView {
|
||||
id: ipListView
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
parent: ipListView.parent
|
||||
anchors.top: ipListView.top
|
||||
anchors.right: ipListView.right
|
||||
anchors.bottom: ipListView.bottom
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
model: backend.ipAddresses
|
||||
delegate: TextEdit {
|
||||
text: modelData
|
||||
readOnly: true
|
||||
selectByMouse: true
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pointSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
RowLayout {
|
||||
// Empty layout
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
214
virtscreen/qml/main.qml
Normal file
214
virtscreen/qml/main.qml
Normal file
|
@ -0,0 +1,214 @@
|
|||
import QtQuick 2.10
|
||||
|
||||
import Qt.labs.platform 1.0
|
||||
|
||||
import VirtScreen.DisplayProperty 1.0
|
||||
import VirtScreen.Backend 1.0
|
||||
|
||||
Item {
|
||||
property alias window: mainLoader.item
|
||||
property var settings: JSON.parse(backend.settings)
|
||||
property bool autostart: settings.vnc.autostart
|
||||
|
||||
function switchVNC () {
|
||||
if ((backend.vncState == Backend.OFF) && backend.virtScreenCreated) {
|
||||
backend.startVNC(settings.vnc.port);
|
||||
}
|
||||
}
|
||||
|
||||
onAutostartChanged: {
|
||||
if (autostart) {
|
||||
backend.onVirtScreenCreatedChanged.connect(switchVNC);
|
||||
backend.onVncStateChanged.connect(switchVNC);
|
||||
} else {
|
||||
backend.onVirtScreenCreatedChanged.disconnect(switchVNC);
|
||||
backend.onVncStateChanged.disconnect(switchVNC);
|
||||
}
|
||||
}
|
||||
|
||||
// virtscreen.py backend.
|
||||
Backend {
|
||||
id: backend
|
||||
onVncStateChanged: {
|
||||
if (backend.vncState == Backend.ERROR) {
|
||||
autostart = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer object and function
|
||||
Timer {
|
||||
id: timer
|
||||
function setTimeout(cb, delayTime) {
|
||||
timer.interval = delayTime;
|
||||
timer.repeat = false;
|
||||
timer.triggered.connect(cb);
|
||||
timer.triggered.connect(function() {
|
||||
timer.triggered.disconnect(cb);
|
||||
});
|
||||
timer.start();
|
||||
}
|
||||
}
|
||||
|
||||
// One-shot signal connect
|
||||
function connectOnce (signal, slot) {
|
||||
var f = function() {
|
||||
slot.apply(this, arguments);
|
||||
signal.disconnect(f);
|
||||
}
|
||||
signal.connect(f);
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: mainLoader
|
||||
active: false
|
||||
source: "AppWindow.qml"
|
||||
|
||||
onStatusChanged: {
|
||||
console.log("Loader Status Changed.", status);
|
||||
if (status == Loader.Null) {
|
||||
gc();
|
||||
// This cause memory leak at this moment.
|
||||
// backend.clearCache();
|
||||
}
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
window.onVisibleChanged.connect(function(visible) {
|
||||
if (!visible) {
|
||||
console.log("Unloading ApplicationWindow...");
|
||||
mainLoader.active = false;
|
||||
}
|
||||
});
|
||||
// Move window to the corner of the primary display
|
||||
var primary = backend.primary;
|
||||
var width = primary.width;
|
||||
var height = primary.height;
|
||||
var cursor_x = backend.cursor_x - primary.x_offset;
|
||||
var cursor_y = backend.cursor_y - primary.y_offset;
|
||||
var x_mid = width / 2;
|
||||
var y_mid = height / 2;
|
||||
var x = width - window.width; //(cursor_x > x_mid)? width - window.width : 0;
|
||||
var y = (cursor_y > y_mid)? height - window.height : 0;
|
||||
x += primary.x_offset;
|
||||
y += primary.y_offset;
|
||||
window.x = x;
|
||||
window.y = y;
|
||||
window.show();
|
||||
window.raise();
|
||||
window.requestActivate();
|
||||
timer.setTimeout (function() {
|
||||
sysTrayIcon.clicked = false;
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// Sytray Icon
|
||||
SystemTrayIcon {
|
||||
id: sysTrayIcon
|
||||
iconSource: backend.vncState == Backend.CONNECTED ? "../icon/icon_tablet_on.png" :
|
||||
backend.virtScreenCreated ? "../icon/icon_tablet_off.png" :
|
||||
"../icon/icon.png"
|
||||
visible: true
|
||||
property bool clicked: false
|
||||
onMessageClicked: console.log("Message clicked")
|
||||
Component.onCompleted: {
|
||||
// without delay, the message appears in a wierd place
|
||||
timer.setTimeout (function() {
|
||||
showMessage("VirtScreen is running",
|
||||
"The program will keep running in the system tray.\n" +
|
||||
"To terminate the program, choose \"Quit\" in the \n" +
|
||||
"context menu of the system tray entry.");
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
onActivated: function(reason) {
|
||||
if (reason == SystemTrayIcon.Context) {
|
||||
return;
|
||||
}
|
||||
sysTrayIcon.clicked = true;
|
||||
mainLoader.active = true;
|
||||
}
|
||||
|
||||
menu: Menu {
|
||||
MenuItem {
|
||||
id: vncStateText
|
||||
text: !backend.virtScreenCreated ? "Enable Virtual Screen first" :
|
||||
backend.vncState == Backend.OFF ? "Turn on VNC Server in the VNC tab" :
|
||||
backend.vncState == Backend.ERROR ? "Error occurred" :
|
||||
backend.vncState == Backend.WAITING ? "VNC Server is waiting for a client..." :
|
||||
backend.vncState == Backend.CONNECTED ? "Connected" :
|
||||
"Server state error!"
|
||||
}
|
||||
MenuItem {
|
||||
id: errorText
|
||||
visible: (text)
|
||||
text: ""
|
||||
Component.onCompleted : {
|
||||
backend.onError.connect(function(errMsg) {
|
||||
errorText.text = ""; // To trigger onTextChanged signal
|
||||
errorText.text = errMsg;
|
||||
});
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
separator: true
|
||||
}
|
||||
MenuItem {
|
||||
id: virtScreenAction
|
||||
text: backend.virtScreenCreated ? "Disable Virtual Screen" : "Enable Virtual Screen"
|
||||
enabled: autostart ? true :
|
||||
backend.vncState == Backend.OFF ? true : false
|
||||
onTriggered: {
|
||||
// Give a very short delay to show busyDialog.
|
||||
timer.setTimeout (function() {
|
||||
if (!backend.virtScreenCreated) {
|
||||
backend.createVirtScreen(settings.virt.width, settings.virt.height,
|
||||
settings.virt.portrait, settings.virt.hidpi);
|
||||
} else {
|
||||
// If auto start enabled, stop VNC first then
|
||||
if (autostart && (backend.vncState != Backend.OFF)) {
|
||||
autostart = false;
|
||||
connectOnce(backend.onVncStateChanged, function() {
|
||||
console.log("autoOff called here", backend.vncState);
|
||||
if (backend.vncState == Backend.OFF) {
|
||||
console.log("Yes. Delete it");
|
||||
backend.deleteVirtScreen();
|
||||
autostart = true;
|
||||
}
|
||||
});
|
||||
backend.stopVNC();
|
||||
} else {
|
||||
backend.deleteVirtScreen();
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
MenuItem {
|
||||
id: vncAction
|
||||
text: autostart ? "Auto start enabled" :
|
||||
backend.vncState == Backend.OFF ? "Start VNC Server" : "Stop VNC Server"
|
||||
enabled: autostart ? false :
|
||||
backend.virtScreenCreated ? true : false
|
||||
onTriggered: backend.vncState == Backend.OFF ? backend.startVNC(settings.vnc.port) : backend.stopVNC()
|
||||
}
|
||||
MenuItem {
|
||||
separator: true
|
||||
}
|
||||
MenuItem {
|
||||
text: "Open VirtScreen"
|
||||
onTriggered: sysTrayIcon.onActivated(SystemTrayIcon.Trigger)
|
||||
}
|
||||
MenuItem {
|
||||
id: quitAction
|
||||
text: qsTr("&Quit")
|
||||
onTriggered: {
|
||||
settings.vnc.autostart = autostart;
|
||||
backend.settings = JSON.stringify(settings, null, 4);
|
||||
backend.quitProgram();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
58
virtscreen/qml/preferenceDialog.qml
Normal file
58
virtscreen/qml/preferenceDialog.qml
Normal file
|
@ -0,0 +1,58 @@
|
|||
import QtQuick 2.10
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Controls.Material 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
Dialog {
|
||||
id: preferenceDialog
|
||||
title: "Preference"
|
||||
focus: true
|
||||
modal: true
|
||||
visible: true
|
||||
standardButtons: Dialog.Ok
|
||||
x: (window.width - width) / 2
|
||||
y: (window.width - height) / 2
|
||||
width: popupWidth
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
RowLayout {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
Label { id: themeColorLabel; text: "Theme Color"; }
|
||||
ComboBox {
|
||||
id: themeColorComboBox
|
||||
anchors.left: themeColorLabel.right
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: 50
|
||||
Material.background: currentIndex
|
||||
Material.foreground: "white"
|
||||
textRole: "name"
|
||||
model: [{"value": Material.Red, "name": "Red"}, {"value": Material.Pink, "name": "Pink"},
|
||||
{"value": Material.Purple, "name": "Purple"},{"value": Material.DeepPurple, "name": "DeepPurple"},
|
||||
{"value": Material.Indigo, "name": "Indigo"}, {"value": Material.Blue, "name": "Blue"},
|
||||
{"value": Material.LightBlue, "name": "LightBlue"}, {"value": Material.Cyan, "name": "Cyan"},
|
||||
{"value": Material.Teal, "name": "Teal"}, {"value": Material.Green, "name": "Green"},
|
||||
{"value": Material.LightGreen, "name": "LightGreen"}, {"value": Material.Lime, "name": "Lime"},
|
||||
{"value": Material.Yellow, "name": "Yellow"}, {"value": Material.Amber, "name": "Amber"},
|
||||
{"value": Material.Orange, "name": "Orange"}, {"value": Material.DeepOrange, "name": "DeepOrange"},
|
||||
{"value": Material.Brown, "name": "Brown"}, {"value": Material.Grey, "name": "Grey"},
|
||||
{"value": Material.BlueGrey, "name": "BlueGrey"}]
|
||||
currentIndex: settings.theme_color
|
||||
onActivated: function(index) {
|
||||
window.theme_color = index;
|
||||
settings.theme_color = index;
|
||||
}
|
||||
delegate: ItemDelegate {
|
||||
width: parent.width
|
||||
text: modelData.name + (themeColorComboBox.currentIndex === index ? " (Current)" : "")
|
||||
Material.foreground: "white"
|
||||
background: Rectangle {
|
||||
color: Material.color(modelData.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onAccepted: {}
|
||||
onRejected: {}
|
||||
}
|
678
virtscreen/virtscreen.py
Executable file
678
virtscreen/virtscreen.py
Executable file
|
@ -0,0 +1,678 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys, os, subprocess, signal, re, atexit, time, json, shutil
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
from typing import List, Dict
|
||||
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
||||
from PyQt5.QtGui import QIcon, QCursor
|
||||
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty
|
||||
|
||||
from twisted.internet import protocol, error
|
||||
from netifaces import interfaces, ifaddresses, AF_INET
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# file path definitions
|
||||
# -------------------------------------------------------------------------------
|
||||
# Sanitize environment variables
|
||||
# https://wiki.sei.cmu.edu/confluence/display/c/ENV03-C.+Sanitize+the+environment+when+invoking+external+programs
|
||||
del os.environ['HOME'] # Delete $HOME env for security reason. This will make
|
||||
# Path.home() to look up in the password directory (pwd module)
|
||||
os.environ['PATH'] = os.confstr("CS_PATH") # Sanitize $PATH
|
||||
|
||||
# Setting home path and base path
|
||||
HOME_PATH = str(Path.home())
|
||||
if HOME_PATH is not None:
|
||||
HOME_PATH = HOME_PATH + "/.virtscreen"
|
||||
BASE_PATH = os.path.dirname(__file__)
|
||||
# Path in ~/.virtscreen
|
||||
X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt"
|
||||
X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd"
|
||||
CONFIG_PATH = HOME_PATH + "/config.json"
|
||||
# Path in the program path
|
||||
DEFAULT_CONFIG_PATH = BASE_PATH + "/data/config.default.json"
|
||||
ICON_PATH = BASE_PATH + "/icon/icon.png"
|
||||
QML_PATH = BASE_PATH + "/qml"
|
||||
MAIN_QML_PATH = QML_PATH + "/main.qml"
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# Subprocess wrapper
|
||||
# -------------------------------------------------------------------------------
|
||||
class SubprocessWrapper:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def check_output(self, arg) -> None:
|
||||
return subprocess.check_output(arg.split(), stderr=subprocess.STDOUT).decode('utf-8')
|
||||
|
||||
def run(self, arg: str, input: str = None, check=False) -> str:
|
||||
if input:
|
||||
input = input.encode('utf-8')
|
||||
return subprocess.run(arg.split(), input=input, stdout=subprocess.PIPE,
|
||||
check=check, stderr=subprocess.STDOUT).stdout.decode('utf-8')
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# Twisted class
|
||||
# -------------------------------------------------------------------------------
|
||||
class ProcessProtocol(protocol.ProcessProtocol):
|
||||
def __init__(self, onConnected, onOutReceived, onErrRecevied, onProcessEnded, logfile=None):
|
||||
self.onConnected = onConnected
|
||||
self.onOutReceived = onOutReceived
|
||||
self.onErrRecevied = onErrRecevied
|
||||
self.onProcessEnded = onProcessEnded
|
||||
self.logfile = logfile
|
||||
|
||||
def run(self, arg: str):
|
||||
"""Spawn a process
|
||||
|
||||
Arguments:
|
||||
arg {str} -- arguments in string
|
||||
"""
|
||||
|
||||
args = arg.split()
|
||||
reactor.spawnProcess(self, args[0], args=args, env=os.environ)
|
||||
|
||||
def kill(self):
|
||||
"""Kill a spawned process
|
||||
"""
|
||||
self.transport.signalProcess('INT')
|
||||
|
||||
def connectionMade(self):
|
||||
print("connectionMade!")
|
||||
self.onConnected()
|
||||
self.transport.closeStdin() # No more input
|
||||
|
||||
def outReceived(self, data):
|
||||
# print("outReceived! with %d bytes!" % len(data))
|
||||
self.onOutReceived(data)
|
||||
if self.logfile is not None:
|
||||
self.logfile.write(data)
|
||||
|
||||
def errReceived(self, data):
|
||||
# print("errReceived! with %d bytes!" % len(data))
|
||||
self.onErrRecevied(data)
|
||||
if self.logfile is not None:
|
||||
self.logfile.write(data)
|
||||
|
||||
def inConnectionLost(self):
|
||||
print("inConnectionLost! stdin is closed! (we probably did it)")
|
||||
pass
|
||||
|
||||
def outConnectionLost(self):
|
||||
print("outConnectionLost! The child closed their stdout!")
|
||||
pass
|
||||
|
||||
def errConnectionLost(self):
|
||||
print("errConnectionLost! The child closed their stderr.")
|
||||
pass
|
||||
|
||||
def processExited(self, reason):
|
||||
exitCode = reason.value.exitCode
|
||||
if exitCode is None:
|
||||
print("Unknown exit")
|
||||
return
|
||||
print("processEnded, status", exitCode)
|
||||
|
||||
def processEnded(self, reason):
|
||||
if self.logfile is not None:
|
||||
self.logfile.close()
|
||||
exitCode = reason.value.exitCode
|
||||
if exitCode is None:
|
||||
print("Unknown exit")
|
||||
self.onProcessEnded(1)
|
||||
return
|
||||
print("processEnded, status", exitCode)
|
||||
print("quitting")
|
||||
self.onProcessEnded(exitCode)
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# Display properties
|
||||
# -------------------------------------------------------------------------------
|
||||
class Display(object):
|
||||
__slots__ = ['name', 'primary', 'connected', 'active', 'width', 'height', 'x_offset', 'y_offset']
|
||||
|
||||
def __init__(self):
|
||||
self.name: str = None
|
||||
self.primary: bool = False
|
||||
self.connected: bool = False
|
||||
self.active: bool = False
|
||||
self.width: int = 0
|
||||
self.height: int = 0
|
||||
self.x_offset: int = 0
|
||||
self.y_offset: int = 0
|
||||
|
||||
def __str__(self):
|
||||
ret = f"{self.name}"
|
||||
if self.connected:
|
||||
ret += " connected"
|
||||
else:
|
||||
ret += " disconnected"
|
||||
if self.primary:
|
||||
ret += " primary"
|
||||
if self.active:
|
||||
ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}"
|
||||
else:
|
||||
ret += f" not active {self.width}x{self.height}"
|
||||
return ret
|
||||
|
||||
|
||||
class DisplayProperty(QObject):
|
||||
def __init__(self, display: Display, parent=None):
|
||||
super(DisplayProperty, self).__init__(parent)
|
||||
self._display = display
|
||||
|
||||
@property
|
||||
def display(self):
|
||||
return self._display
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def name(self):
|
||||
return self._display.name
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
self._display.name = name
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def primary(self):
|
||||
return self._display.primary
|
||||
|
||||
@primary.setter
|
||||
def primary(self, primary):
|
||||
self._display.primary = primary
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def connected(self):
|
||||
return self._display.connected
|
||||
|
||||
@connected.setter
|
||||
def connected(self, connected):
|
||||
self._display.connected = connected
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def active(self):
|
||||
return self._display.active
|
||||
|
||||
@active.setter
|
||||
def active(self, active):
|
||||
self._display.active = active
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def width(self):
|
||||
return self._display.width
|
||||
|
||||
@width.setter
|
||||
def width(self, width):
|
||||
self._display.width = width
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def height(self):
|
||||
return self._display.height
|
||||
|
||||
@height.setter
|
||||
def height(self, height):
|
||||
self._display.height = height
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def x_offset(self):
|
||||
return self._display.x_offset
|
||||
|
||||
@x_offset.setter
|
||||
def x_offset(self, x_offset):
|
||||
self._display.x_offset = x_offset
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def y_offset(self):
|
||||
return self._display.y_offset
|
||||
|
||||
@y_offset.setter
|
||||
def y_offset(self, y_offset):
|
||||
self._display.y_offset = y_offset
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# Screen adjustment class
|
||||
# -------------------------------------------------------------------------------
|
||||
class XRandR(SubprocessWrapper):
|
||||
DEFAULT_VIRT_SCREEN = "VIRTUAL1"
|
||||
VIRT_SCREEN_SUFFIX = "_virt"
|
||||
|
||||
def __init__(self):
|
||||
super(XRandR, self).__init__()
|
||||
self.mode_name: str
|
||||
self.screens: List[Display] = []
|
||||
self.virt: Display() = None
|
||||
self.primary: Display() = None
|
||||
self.virt_idx: int = None
|
||||
self.primary_idx: int = None
|
||||
# Primary display
|
||||
self._update_screens()
|
||||
|
||||
def _update_screens(self) -> None:
|
||||
output = self.run("xrandr")
|
||||
self.primary = None
|
||||
self.virt = None
|
||||
self.screens = []
|
||||
self.primary_idx = None
|
||||
pattern = re.compile(r"^(\S*)\s+(connected|disconnected)\s+((primary)\s+)?"
|
||||
r"((\d+)x(\d+)\+(\d+)\+(\d+)\s+)?.*$", re.M)
|
||||
for idx, match in enumerate(pattern.finditer(output)):
|
||||
screen = Display()
|
||||
screen.name = match.group(1)
|
||||
if (self.virt_idx is None) and (screen.name == self.DEFAULT_VIRT_SCREEN):
|
||||
self.virt_idx = idx
|
||||
screen.primary = True if match.group(4) else False
|
||||
if screen.primary:
|
||||
self.primary_idx = idx
|
||||
screen.connected = True if match.group(2) == "connected" else False
|
||||
screen.active = True if match.group(5) else False
|
||||
self.screens.append(screen)
|
||||
if not screen.active:
|
||||
continue
|
||||
screen.width = int(match.group(6))
|
||||
screen.height = int(match.group(7))
|
||||
screen.x_offset = int(match.group(8))
|
||||
screen.y_offset = int(match.group(9))
|
||||
print("Display information:")
|
||||
for s in self.screens:
|
||||
print("\t", s)
|
||||
if self.virt_idx == self.primary_idx:
|
||||
raise RuntimeError("Virtual screen must be selected other than the primary screen")
|
||||
if self.virt_idx is None:
|
||||
for idx, screen in enumerate(self.screens):
|
||||
if not screen.connected and not screen.active:
|
||||
self.virt_idx = idx
|
||||
break
|
||||
if self.virt_idx is None:
|
||||
raise RuntimeError("There is no available devices for virtual screen")
|
||||
self.virt = self.screens[self.virt_idx]
|
||||
self.primary = self.screens[self.primary_idx]
|
||||
|
||||
def _add_screen_mode(self, width, height, portrait, hidpi) -> None:
|
||||
# Set virtual screen property first
|
||||
self.virt.width = width
|
||||
self.virt.height = height
|
||||
if portrait:
|
||||
self.virt.width = height
|
||||
self.virt.height = width
|
||||
if hidpi:
|
||||
self.virt.width *= 2
|
||||
self.virt.height *= 2
|
||||
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + self.VIRT_SCREEN_SUFFIX
|
||||
# Then create using xrandr command
|
||||
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
|
||||
try:
|
||||
self.check_output(args_addmode)
|
||||
except subprocess.CalledProcessError:
|
||||
# When failed create mode and then add again
|
||||
output = self.run(f"cvt {self.virt.width} {self.virt.height}")
|
||||
mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1)
|
||||
# Create new screen mode
|
||||
self.check_output(f"xrandr --newmode {self.mode_name} {mode}")
|
||||
# Add mode again
|
||||
self.check_output(args_addmode)
|
||||
# After adding mode the program should delete the mode automatically on exit
|
||||
atexit.register(self.delete_virtual_screen)
|
||||
for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
|
||||
signal.signal(sig, self._signal_handler)
|
||||
|
||||
def _signal_handler(self, signum=None, frame=None) -> None:
|
||||
self.delete_virtual_screen()
|
||||
os._exit(0)
|
||||
|
||||
def get_primary_screen(self) -> Display:
|
||||
self._update_screens()
|
||||
return self.primary
|
||||
|
||||
def get_virtual_screen(self) -> Display:
|
||||
self._update_screens()
|
||||
return self.virt
|
||||
|
||||
def create_virtual_screen(self, width, height, portrait=False, hidpi=False) -> None:
|
||||
print("creating: ", self.virt)
|
||||
self._add_screen_mode(width, height, portrait, hidpi)
|
||||
self.check_output(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
||||
self.check_output("sleep 5")
|
||||
self.check_output(f"xrandr --output {self.virt.name} --preferred")
|
||||
self._update_screens()
|
||||
|
||||
def delete_virtual_screen(self) -> None:
|
||||
try:
|
||||
self.virt.name
|
||||
self.mode_name
|
||||
except AttributeError:
|
||||
return
|
||||
self.run(f"xrandr --output {self.virt.name} --off")
|
||||
self.run(f"xrandr --delmode {self.virt.name} {self.mode_name}")
|
||||
atexit.unregister(self.delete_virtual_screen)
|
||||
self._update_screens()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# QML Backend class
|
||||
# -------------------------------------------------------------------------------
|
||||
class Backend(QObject):
|
||||
""" Backend class for QML frontend """
|
||||
|
||||
class VNCState:
|
||||
""" Enum to indicate a state of the VNC server """
|
||||
OFF = 0
|
||||
ERROR = 1
|
||||
WAITING = 2
|
||||
CONNECTED = 3
|
||||
|
||||
Q_ENUMS(VNCState)
|
||||
|
||||
# Signals
|
||||
onVirtScreenCreatedChanged = pyqtSignal(bool)
|
||||
onVirtScreenIndexChanged = pyqtSignal(int)
|
||||
onVncUsePasswordChanged = pyqtSignal(bool)
|
||||
onVncStateChanged = pyqtSignal(VNCState)
|
||||
onIPAddressesChanged = pyqtSignal()
|
||||
onDisplaySettingClosed = pyqtSignal()
|
||||
onError = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(Backend, self).__init__(parent)
|
||||
# Virtual screen properties
|
||||
self.xrandr: XRandR = XRandR()
|
||||
self._virtScreenCreated: bool = False
|
||||
self._virtScreenIndex: int = self.xrandr.virt_idx
|
||||
# VNC server properties
|
||||
self._vncUsePassword: bool = False
|
||||
self._vncState: self.VNCState = self.VNCState.OFF
|
||||
# Primary screen and mouse posistion
|
||||
self._primaryProp: DisplayProperty
|
||||
self.vncServer: ProcessProtocol
|
||||
|
||||
# Qt properties
|
||||
@pyqtProperty(str, constant=True)
|
||||
def settings(self):
|
||||
try:
|
||||
with open(CONFIG_PATH, "r") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
with open(DEFAULT_CONFIG_PATH, "r") as f:
|
||||
return f.read()
|
||||
|
||||
@settings.setter
|
||||
def settings(self, json_str):
|
||||
with open(CONFIG_PATH, "w") as f:
|
||||
f.write(json_str)
|
||||
|
||||
@pyqtProperty(bool, notify=onVirtScreenCreatedChanged)
|
||||
def virtScreenCreated(self):
|
||||
return self._virtScreenCreated
|
||||
|
||||
@virtScreenCreated.setter
|
||||
def virtScreenCreated(self, value):
|
||||
self._virtScreenCreated = value
|
||||
self.onVirtScreenCreatedChanged.emit(value)
|
||||
|
||||
@pyqtProperty(QQmlListProperty, constant=True)
|
||||
def screens(self):
|
||||
return QQmlListProperty(DisplayProperty, self, [DisplayProperty(x) for x in self.xrandr.screens])
|
||||
|
||||
@pyqtProperty(int, notify=onVirtScreenIndexChanged)
|
||||
def virtScreenIndex(self):
|
||||
return self._virtScreenIndex
|
||||
|
||||
@virtScreenIndex.setter
|
||||
def virtScreenIndex(self, virtScreenIndex):
|
||||
print("Changing virt to ", virtScreenIndex)
|
||||
self.xrandr.virt_idx = virtScreenIndex
|
||||
self.xrandr.virt = self.xrandr.screens[self.xrandr.virt_idx]
|
||||
self._virtScreenIndex = virtScreenIndex
|
||||
|
||||
@pyqtProperty(bool, notify=onVncUsePasswordChanged)
|
||||
def vncUsePassword(self):
|
||||
if os.path.isfile(X11VNC_PASSWORD_PATH):
|
||||
self._vncUsePassword = True
|
||||
else:
|
||||
if self._vncUsePassword:
|
||||
self.vncUsePassword = False
|
||||
return self._vncUsePassword
|
||||
|
||||
@vncUsePassword.setter
|
||||
def vncUsePassword(self, use):
|
||||
self._vncUsePassword = use
|
||||
self.onVncUsePasswordChanged.emit(use)
|
||||
|
||||
@pyqtProperty(VNCState, notify=onVncStateChanged)
|
||||
def vncState(self):
|
||||
return self._vncState
|
||||
|
||||
@vncState.setter
|
||||
def vncState(self, state):
|
||||
self._vncState = state
|
||||
self.onVncStateChanged.emit(self._vncState)
|
||||
|
||||
@pyqtProperty('QStringList', notify=onIPAddressesChanged)
|
||||
def ipAddresses(self):
|
||||
for interface in interfaces():
|
||||
if interface == 'lo':
|
||||
continue
|
||||
addresses = ifaddresses(interface).get(AF_INET, None)
|
||||
if addresses is None:
|
||||
continue
|
||||
for link in addresses:
|
||||
if link is not None:
|
||||
yield link['addr']
|
||||
|
||||
@pyqtProperty(DisplayProperty)
|
||||
def primary(self):
|
||||
self._primaryProp = DisplayProperty(self.xrandr.get_primary_screen())
|
||||
return self._primaryProp
|
||||
|
||||
@pyqtProperty(int)
|
||||
def cursor_x(self):
|
||||
cursor = QCursor().pos()
|
||||
return cursor.x()
|
||||
|
||||
@pyqtProperty(int)
|
||||
def cursor_y(self):
|
||||
cursor = QCursor().pos()
|
||||
return cursor.y()
|
||||
|
||||
# Qt Slots
|
||||
@pyqtSlot(int, int, bool, bool)
|
||||
def createVirtScreen(self, width, height, portrait, hidpi):
|
||||
print("Creating a Virtual Screen...")
|
||||
try:
|
||||
self.xrandr.create_virtual_screen(width, height, portrait, hidpi)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||
return
|
||||
self.virtScreenCreated = True
|
||||
|
||||
@pyqtSlot()
|
||||
def deleteVirtScreen(self):
|
||||
print("Deleting the Virtual Screen...")
|
||||
if self.vncState is not self.VNCState.OFF:
|
||||
self.onError.emit("Turn off the VNC server first")
|
||||
self.virtScreenCreated = True
|
||||
return
|
||||
self.xrandr.delete_virtual_screen()
|
||||
self.virtScreenCreated = False
|
||||
|
||||
@pyqtSlot(str)
|
||||
def createVNCPassword(self, password):
|
||||
if password:
|
||||
password += '\n' + password + '\n\n' # verify + confirm
|
||||
p = SubprocessWrapper()
|
||||
try:
|
||||
p.run(f"x11vnc -storepasswd {X11VNC_PASSWORD_PATH}", input=password, check=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self.onError.emit(str(e.cmd) + '\n' + e.stdout.decode('utf-8'))
|
||||
return
|
||||
self.vncUsePassword = True
|
||||
else:
|
||||
self.onError.emit("Empty password")
|
||||
|
||||
@pyqtSlot()
|
||||
def deleteVNCPassword(self):
|
||||
if os.path.isfile(X11VNC_PASSWORD_PATH):
|
||||
os.remove(X11VNC_PASSWORD_PATH)
|
||||
self.vncUsePassword = False
|
||||
else:
|
||||
self.onError.emit("Failed deleting the password file")
|
||||
|
||||
@pyqtSlot(int)
|
||||
def startVNC(self, port):
|
||||
# Check if a virtual screen created
|
||||
if not self.virtScreenCreated:
|
||||
self.onError.emit("Virtual Screen not crated.")
|
||||
return
|
||||
if self.vncState is not self.VNCState.OFF:
|
||||
self.onError.emit("VNC Server is already running.")
|
||||
return
|
||||
# regex used in callbacks
|
||||
patter_connected = re.compile(r"^.*Got connection from client.*$", re.M)
|
||||
patter_disconnected = re.compile(r"^.*client_count: 0*$", re.M)
|
||||
|
||||
# define callbacks
|
||||
def _onConnected():
|
||||
print("VNC started.")
|
||||
self.vncState = self.VNCState.WAITING
|
||||
|
||||
def _onReceived(data):
|
||||
data = data.decode("utf-8")
|
||||
if (self._vncState is not self.VNCState.CONNECTED) and patter_connected.search(data):
|
||||
print("VNC connected.")
|
||||
self.vncState = self.VNCState.CONNECTED
|
||||
if (self._vncState is self.VNCState.CONNECTED) and patter_disconnected.search(data):
|
||||
print("VNC disconnected.")
|
||||
self.vncState = self.VNCState.WAITING
|
||||
|
||||
def _onEnded(exitCode):
|
||||
if exitCode is not 0:
|
||||
self.vncState = self.VNCState.ERROR
|
||||
self.onError.emit('X11VNC: Error occurred.\nDouble check if the port is already used.')
|
||||
self.vncState = self.VNCState.OFF # TODO: better handling error state
|
||||
else:
|
||||
self.vncState = self.VNCState.OFF
|
||||
print("VNC Exited.")
|
||||
atexit.unregister(self.stopVNC)
|
||||
|
||||
logfile = open(X11VNC_LOG_PATH, "wb")
|
||||
self.vncServer = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, logfile)
|
||||
virt = self.xrandr.get_virtual_screen()
|
||||
clip = f"{virt.width}x{virt.height}+{virt.x_offset}+{virt.y_offset}"
|
||||
arg = f"x11vnc -multiptr -repeat -rfbport {port} -clip {clip}"
|
||||
if self.vncUsePassword:
|
||||
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
|
||||
self.vncServer.run(arg)
|
||||
# auto stop on exit
|
||||
atexit.register(self.stopVNC, force=True)
|
||||
|
||||
@pyqtSlot()
|
||||
def openDisplaySetting(self):
|
||||
# define callbacks
|
||||
def _onConnected():
|
||||
print("External Display Setting opened.")
|
||||
|
||||
def _onReceived(data):
|
||||
pass
|
||||
|
||||
def _onEnded(exitCode):
|
||||
print("External Display Setting closed.")
|
||||
self.onDisplaySettingClosed.emit()
|
||||
if exitCode is not 0:
|
||||
self.onError.emit(f'Error opening "{running_program}".')
|
||||
|
||||
program_list = ["gnome-control-center display", "arandr"]
|
||||
program = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, None)
|
||||
running_program = ''
|
||||
for arg in program_list:
|
||||
if not shutil.which(arg.split()[0]):
|
||||
continue
|
||||
running_program = arg
|
||||
program.run(arg)
|
||||
return
|
||||
self.onError.emit('Failed to find a display settings program.\n'
|
||||
'Please install ARandR package.\n'
|
||||
'(e.g. sudo apt-get install arandr)\n'
|
||||
'Please issue a feature request\n'
|
||||
'if you wish to add a display settings\n'
|
||||
'program for your Desktop Environment.')
|
||||
|
||||
@pyqtSlot()
|
||||
def stopVNC(self, force=False):
|
||||
if force:
|
||||
# Usually called from atexit().
|
||||
self.vncServer.kill()
|
||||
time.sleep(2) # Make sure X11VNC shutdown before execute next atexit().
|
||||
if self._vncState in (self.VNCState.WAITING, self.VNCState.CONNECTED):
|
||||
self.vncServer.kill()
|
||||
else:
|
||||
self.onError.emit("stopVNC called while it is not running")
|
||||
|
||||
@pyqtSlot()
|
||||
def clearCache(self):
|
||||
engine.clearComponentCache()
|
||||
|
||||
@pyqtSlot()
|
||||
def quitProgram(self):
|
||||
self.blockSignals(True) # This will prevent invoking auto-restart or etc.
|
||||
QApplication.instance().quit()
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------
|
||||
# Main Code
|
||||
# -------------------------------------------------------------------------------
|
||||
def main():
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
from PyQt5.QtWidgets import QSystemTrayIcon, QMessageBox
|
||||
|
||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||
QMessageBox.critical(None, "VirtScreen",
|
||||
"Cannot detect system tray on this system.")
|
||||
sys.exit(1)
|
||||
|
||||
if os.environ['XDG_SESSION_TYPE'] == 'wayland':
|
||||
QMessageBox.critical(None, "VirtScreen",
|
||||
"Currently Wayland is not supported")
|
||||
sys.exit(1)
|
||||
if not HOME_PATH:
|
||||
QMessageBox.critical(None, "VirtScreen",
|
||||
"Cannot detect home directory.")
|
||||
sys.exit(1)
|
||||
if not os.path.exists(HOME_PATH):
|
||||
try:
|
||||
os.makedirs(HOME_PATH)
|
||||
except:
|
||||
QMessageBox.critical(None, "VirtScreen",
|
||||
"Cannot create ~/.virtscreen")
|
||||
sys.exit(1)
|
||||
|
||||
import qt5reactor # pylint: disable=E0401
|
||||
|
||||
qt5reactor.install()
|
||||
from twisted.internet import utils, reactor # pylint: disable=E0401
|
||||
|
||||
app.setWindowIcon(QIcon(ICON_PATH))
|
||||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
||||
# os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion"
|
||||
|
||||
# Register the Python type. Its URI is 'People', it's v1.0 and the type
|
||||
# will be called 'Person' in QML.
|
||||
qmlRegisterType(DisplayProperty, 'VirtScreen.DisplayProperty', 1, 0, 'DisplayProperty')
|
||||
qmlRegisterType(Backend, 'VirtScreen.Backend', 1, 0, 'Backend')
|
||||
|
||||
# Create a component factory and load the QML script.
|
||||
engine = QQmlApplicationEngine()
|
||||
engine.load(QUrl(MAIN_QML_PATH))
|
||||
if not engine.rootObjects():
|
||||
QMessageBox.critical(None, "VirtScreen", "Failed to load QML")
|
||||
sys.exit(1)
|
||||
sys.exit(app.exec_())
|
||||
reactor.run()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue