mirror of
https://github.com/kbumsik/VirtScreen.git
synced 2025-03-09 15:40:18 +00:00
commit
abba868446
2 changed files with 662 additions and 487 deletions
330
main.qml
330
main.qml
|
@ -6,12 +6,14 @@ import QtQuick.Window 2.2
|
||||||
|
|
||||||
import Qt.labs.platform 1.0 as Labs
|
import Qt.labs.platform 1.0 as Labs
|
||||||
|
|
||||||
|
import VirtScreen.DisplayProperty 1.0
|
||||||
import VirtScreen.Backend 1.0
|
import VirtScreen.Backend 1.0
|
||||||
|
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: window
|
id: window
|
||||||
visible: true
|
visible: false
|
||||||
|
flags: Qt.FramelessWindowHint
|
||||||
title: "Basic layouts"
|
title: "Basic layouts"
|
||||||
|
|
||||||
Material.theme: Material.Light
|
Material.theme: Material.Light
|
||||||
|
@ -19,22 +21,52 @@ ApplicationWindow {
|
||||||
|
|
||||||
property int margin: 11
|
property int margin: 11
|
||||||
width: 380
|
width: 380
|
||||||
height: 600
|
height: 500
|
||||||
|
|
||||||
|
// hide screen when loosing focus
|
||||||
|
onActiveFocusItemChanged: {
|
||||||
|
if ((!activeFocusItem) && (!sysTrayIcon.clicked)) {
|
||||||
|
this.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// virtscreen.py backend.
|
||||||
|
Backend {
|
||||||
|
id: backend
|
||||||
|
}
|
||||||
|
property bool vncAutoStart: false
|
||||||
|
|
||||||
|
function switchVNC(value) {
|
||||||
|
if (value) {
|
||||||
|
backend.startVNC();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onVncAutoStartChanged: {
|
||||||
|
if (vncAutoStart) {
|
||||||
|
backend.onVirtScreenCreatedChanged.connect(switchVNC);
|
||||||
|
} else {
|
||||||
|
backend.onVirtScreenCreatedChanged.disconnect(switchVNC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Timer object and function
|
// Timer object and function
|
||||||
Timer {
|
Timer {
|
||||||
id: timer
|
id: timer
|
||||||
}
|
|
||||||
|
|
||||||
function setTimeout(cb, delayTime) {
|
function setTimeout(cb, delayTime) {
|
||||||
timer.interval = delayTime;
|
timer.interval = delayTime;
|
||||||
timer.repeat = false;
|
timer.repeat = false;
|
||||||
timer.triggered.connect(cb);
|
timer.triggered.connect(cb);
|
||||||
|
timer.triggered.connect(function() {
|
||||||
|
timer.triggered.disconnect(cb);
|
||||||
|
});
|
||||||
timer.start();
|
timer.start();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
header: TabBar {
|
header: TabBar {
|
||||||
id: tabBar
|
id: tabBar
|
||||||
|
position: TabBar.Header
|
||||||
width: parent.width
|
width: parent.width
|
||||||
currentIndex: 0
|
currentIndex: 0
|
||||||
|
|
||||||
|
@ -52,128 +84,284 @@ ApplicationWindow {
|
||||||
currentIndex: tabBar.currentIndex
|
currentIndex: tabBar.currentIndex
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
// enabled: enabler.checked
|
anchors.top: parent.top
|
||||||
// anchors.top: parent.top
|
anchors.left: parent.left
|
||||||
// anchors.left: parent.left
|
anchors.right: parent.right
|
||||||
// anchors.right: parent.right
|
anchors.margins: margin
|
||||||
// anchors.margins: margin
|
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
title: "Virtual Display"
|
title: "Virtual Display"
|
||||||
// font.bold: true
|
// font.bold: true
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
enabled: backend.virtScreenCreated ? false : true
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Width"; Layout.fillWidth: true }
|
Label { text: "Width"; Layout.fillWidth: true }
|
||||||
SpinBox { value: 1368
|
SpinBox {
|
||||||
|
value: backend.virt.width
|
||||||
from: 640
|
from: 640
|
||||||
to: 1920
|
to: 1920
|
||||||
stepSize: 1
|
stepSize: 1
|
||||||
editable: true
|
editable: true
|
||||||
textFromValue: function(value, locale) {
|
onValueModified: {
|
||||||
return Number(value).toLocaleString(locale, 'f', 0) + " px";
|
backend.virt.width = value;
|
||||||
}
|
}
|
||||||
|
textFromValue: function(value, locale) { return value; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Height"; Layout.fillWidth: true }
|
Label { text: "Height"; Layout.fillWidth: true }
|
||||||
SpinBox { value: 1024
|
SpinBox {
|
||||||
|
value: backend.virt.height
|
||||||
from: 360
|
from: 360
|
||||||
to: 1080
|
to: 1080
|
||||||
stepSize : 1
|
stepSize : 1
|
||||||
editable: true
|
editable: true
|
||||||
textFromValue: function(value, locale) {
|
onValueModified: {
|
||||||
return Number(value).toLocaleString(locale, 'f', 0) + " px";
|
backend.virt.height = value;
|
||||||
}
|
}
|
||||||
|
textFromValue: function(value, locale) { return value; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Portrait Mode"; Layout.fillWidth: true }
|
Label { text: "Portrait Mode"; Layout.fillWidth: true }
|
||||||
Switch { checked: false }
|
Switch {
|
||||||
|
checked: backend.portrait
|
||||||
|
onCheckedChanged: {
|
||||||
|
backend.portrait = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "HiDPI (2x resolution)"; Layout.fillWidth: true }
|
Label { text: "HiDPI (2x resolution)"; Layout.fillWidth: true }
|
||||||
Switch { checked: false }
|
Switch {
|
||||||
|
checked: backend.hidpi
|
||||||
|
onCheckedChanged: {
|
||||||
|
backend.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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: "Create a Virtual Display"
|
id: virtScreenButton
|
||||||
Layout.fillWidth: true
|
text: backend.virtScreenCreated ? "Disable Virtual Screen" : "Enable Virtual Screen"
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
// Material.background: Material.Teal
|
// Material.background: Material.Teal
|
||||||
// Material.foreground: Material.Grey
|
// Material.foreground: Material.Grey
|
||||||
|
enabled: window.vncAutoStart ? true :
|
||||||
|
backend.vncState == Backend.OFF ? true : false
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: busyDialog
|
||||||
|
modal: true
|
||||||
|
closePolicy: Popup.NoAutoClose
|
||||||
|
|
||||||
|
x: (parent.width - width) / 2
|
||||||
|
y: (parent.height - height) / 2
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
x: (parent.width - width) / 2
|
||||||
|
y: (parent.height - height) / 2
|
||||||
|
running: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
busyDialog.open();
|
||||||
|
// Give a very short delay to show busyDialog.
|
||||||
|
timer.setTimeout (function() {
|
||||||
|
if (!backend.virtScreenCreated) {
|
||||||
|
backend.createVirtScreen();
|
||||||
|
} else {
|
||||||
|
function autoOff() {
|
||||||
|
console.log("autoOff called here", backend.vncState);
|
||||||
|
if (backend.vncState == Backend.OFF) {
|
||||||
|
console.log("Yes. Delete it");
|
||||||
|
backend.deleteVirtScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.vncAutoStart && (backend.vncState != Backend.OFF)) {
|
||||||
|
backend.onVncStateChanged.connect(autoOff);
|
||||||
|
backend.onVncStateChanged.connect(function() {
|
||||||
|
backend.onVncStateChanged.disconnect(autoOff);
|
||||||
|
});
|
||||||
|
backend.stopVNC();
|
||||||
|
} else {
|
||||||
|
backend.deleteVirtScreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
backend.onVirtScreenCreatedChanged.connect(function(created) {
|
||||||
|
busyDialog.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
// enabled: enabler.checked
|
anchors.top: parent.top
|
||||||
// anchors.top: parent.top
|
anchors.left: parent.left
|
||||||
// anchors.left: parent.left
|
anchors.right: parent.right
|
||||||
// anchors.right: parent.right
|
anchors.margins: margin
|
||||||
// anchors.margins: margin
|
|
||||||
|
|
||||||
GroupBox {
|
GroupBox {
|
||||||
title: "VNC Server"
|
title: "VNC Server"
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
// Layout.fillWidth: true
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
enabled: backend.vncState == Backend.OFF ? true : false
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
|
||||||
Label { text: "Port"; Layout.fillWidth: true }
|
Label { text: "Port"; Layout.fillWidth: true }
|
||||||
SpinBox {
|
SpinBox {
|
||||||
value: 5900
|
value: backend.vncPort
|
||||||
from: 1
|
from: 1
|
||||||
to: 65535
|
to: 65535
|
||||||
stepSize: 1
|
stepSize: 1
|
||||||
editable: true
|
editable: true
|
||||||
|
onValueModified: {
|
||||||
|
backend.vncPort = value;
|
||||||
|
}
|
||||||
|
textFromValue: function(value, locale) { return value; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
Label { text: "Password" }
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
Label { id: passwordLabel; text: "Password" }
|
||||||
TextField {
|
TextField {
|
||||||
Layout.fillWidth: true
|
anchors.left: passwordLabel.right
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: margin
|
||||||
|
|
||||||
placeholderText: "Password";
|
placeholderText: "Password";
|
||||||
|
text: backend.vncPassword;
|
||||||
echoMode: TextInput.Password;
|
echoMode: TextInput.Password;
|
||||||
|
onTextEdited: {
|
||||||
|
backend.vncPassword = text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
text: "Start VNC Server"
|
id: vncButton
|
||||||
Layout.fillWidth: true
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottomMargin: 0
|
||||||
|
|
||||||
|
text: window.vncAutoStart ? "Auto start enabled" :
|
||||||
|
backend.vncState == Backend.OFF ? "Start VNC Server" : "Stop VNC Server"
|
||||||
|
enabled: window.vncAutoStart ? false :
|
||||||
|
backend.virtScreenCreated ? true : false
|
||||||
// Material.background: Material.Teal
|
// Material.background: Material.Teal
|
||||||
// Material.foreground: Material.Grey
|
// Material.foreground: Material.Grey
|
||||||
|
onClicked: backend.vncState == Backend.OFF ? backend.startVNC() : backend.stopVNC()
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.top: vncButton.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: vncButton.height - 10
|
||||||
|
|
||||||
|
Label { text: "Auto start"; }
|
||||||
|
Switch {
|
||||||
|
checked: window.vncAutoStart
|
||||||
|
onCheckedChanged: {
|
||||||
|
if ((checked == true) && (backend.vncState == Backend.OFF) &&
|
||||||
|
backend.virtScreenCreated) {
|
||||||
|
backend.startVNC();
|
||||||
|
}
|
||||||
|
window.vncAutoStart = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
// width: 180;
|
||||||
|
height: 200
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
|
||||||
|
model: backend.ipAddresses
|
||||||
|
delegate: Text {
|
||||||
|
text: modelData
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer: ToolBar {
|
footer: ToolBar {
|
||||||
|
font.weight: Font.Medium
|
||||||
|
font.pointSize: 11 //parent.font.pointSize + 1
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.margins: spacing
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: margin + 10
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: "VNC Server Waiting."
|
id: vncStateLabel
|
||||||
}
|
text: !backend.virtScreenCreated ? "Enable Virtual Screen first." :
|
||||||
Item { Layout.fillWidth: true }
|
backend.vncState == Backend.OFF ? "Turn on VNC Server in the VNC tab." :
|
||||||
CheckBox {
|
backend.vncState == Backend.WAITING ? "VNC Server is waiting for a client..." :
|
||||||
id: enabler
|
backend.vncState == Backend.CONNECTED ? "Connected." :
|
||||||
text: "Server Enabled"
|
"Server state error!"
|
||||||
checked: true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,25 +371,55 @@ ApplicationWindow {
|
||||||
id: sysTrayIcon
|
id: sysTrayIcon
|
||||||
iconSource: "icon/icon.png"
|
iconSource: "icon/icon.png"
|
||||||
visible: true
|
visible: true
|
||||||
|
property bool clicked: false
|
||||||
|
|
||||||
onMessageClicked: console.log("Message clicked")
|
onMessageClicked: console.log("Message clicked")
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
// without delay, the message appears in a wierd place
|
// without delay, the message appears in a wierd place
|
||||||
setTimeout (function() {
|
timer.setTimeout (function() {
|
||||||
showMessage("Message title", "Something important came up. Click this to know more.");
|
showMessage("VirtScreen is running",
|
||||||
}, 1000);
|
"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: {
|
onActivated: function(reason) {
|
||||||
window.show()
|
console.log(reason);
|
||||||
window.raise()
|
if (reason == Labs.SystemTrayIcon.Context) {
|
||||||
window.requestActivate()
|
return;
|
||||||
|
}
|
||||||
|
if (window.visible) {
|
||||||
|
window.hide();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sysTrayIcon.clicked = true;
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu: Labs.Menu {
|
menu: Labs.Menu {
|
||||||
Labs.MenuItem {
|
Labs.MenuItem {
|
||||||
text: qsTr("&Quit")
|
text: qsTr("&Quit")
|
||||||
onTriggered: Qt.quit()
|
onTriggered: backend.quitProgram()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
781
virtscreen.py
781
virtscreen.py
|
@ -1,20 +1,16 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
import os, re, time
|
import sys, os, subprocess, signal, re, atexit, time
|
||||||
from PyQt5.QtGui import QIcon, QCursor, QFocusEvent
|
from enum import Enum
|
||||||
from PyQt5.QtCore import pyqtSlot, Qt, QEvent
|
from typing import List
|
||||||
from PyQt5.QtWidgets import (QAction, QApplication, QCheckBox, QComboBox,
|
|
||||||
QDialog, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
|
from PyQt5.QtWidgets import QApplication
|
||||||
QMessageBox, QMenu, QPushButton, QSpinBox, QStyle, QSystemTrayIcon,
|
from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS
|
||||||
QTextEdit, QVBoxLayout, QListWidget)
|
from PyQt5.QtGui import QIcon, QCursor
|
||||||
|
from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty
|
||||||
|
|
||||||
from twisted.internet import protocol, error
|
from twisted.internet import protocol, error
|
||||||
from netifaces import interfaces, ifaddresses, AF_INET
|
from netifaces import interfaces, ifaddresses, AF_INET
|
||||||
import subprocess
|
|
||||||
import atexit, signal
|
|
||||||
|
|
||||||
# Redirect stdout to /dev/null. Uncomment it while debugging.
|
|
||||||
# import sys
|
|
||||||
# sys.stdout = open(os.devnull, "a")
|
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# file path definitions
|
# file path definitions
|
||||||
|
@ -53,30 +49,150 @@ class SubprocessWrapper:
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Display properties
|
# Display properties
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
class DisplayProperty:
|
class DisplayProperty(QObject):
|
||||||
def __init__(self):
|
def __init__(self, parent=None):
|
||||||
self.name: str
|
super(DisplayProperty, self).__init__(parent)
|
||||||
self.width: int
|
self._name: str
|
||||||
self.height: int
|
self._primary: bool
|
||||||
self.x_offset: int
|
self._connected: bool
|
||||||
self.y_offset: int
|
self._active: bool
|
||||||
|
self._width: int
|
||||||
|
self._height: int
|
||||||
|
self._x_offset: int
|
||||||
|
self._y_offset: int
|
||||||
|
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 += " not active"
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
@name.setter
|
||||||
|
def name(self, name):
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def primary(self):
|
||||||
|
return self._primary
|
||||||
|
@primary.setter
|
||||||
|
def primary(self, primary):
|
||||||
|
self._primary = primary
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def connected(self):
|
||||||
|
return self._connected
|
||||||
|
@connected.setter
|
||||||
|
def connected(self, connected):
|
||||||
|
self._connected = connected
|
||||||
|
|
||||||
|
@pyqtProperty(bool, constant=True)
|
||||||
|
def active(self):
|
||||||
|
return self._active
|
||||||
|
@active.setter
|
||||||
|
def active(self, active):
|
||||||
|
self._active = active
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def width(self):
|
||||||
|
return self._width
|
||||||
|
@width.setter
|
||||||
|
def width(self, width):
|
||||||
|
self._width = width
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def height(self):
|
||||||
|
return self._height
|
||||||
|
@height.setter
|
||||||
|
def height(self, height):
|
||||||
|
self._height = height
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def x_offset(self):
|
||||||
|
return self._x_offset
|
||||||
|
@x_offset.setter
|
||||||
|
def x_offset(self, x_offset):
|
||||||
|
self._x_offset = x_offset
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant=True)
|
||||||
|
def y_offset(self):
|
||||||
|
return self._y_offset
|
||||||
|
@y_offset.setter
|
||||||
|
def y_offset(self, y_offset):
|
||||||
|
self._y_offset = y_offset
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Screen adjustment class
|
# Screen adjustment class
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
class XRandR(SubprocessWrapper):
|
class XRandR(SubprocessWrapper):
|
||||||
|
DEFAULT_VIRT_SCREEN = "VIRTUAL1"
|
||||||
|
VIRT_SCREEN_SUFFIX = "_virt"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(XRandR, self).__init__()
|
super(XRandR, self).__init__()
|
||||||
self.mode_name: str
|
self.mode_name: str
|
||||||
self.scrren_suffix = "_virt"
|
self.screens: List[DisplayProperty] = []
|
||||||
# Thoese will be created in set_virtual_screen()
|
self.virt: DisplayProperty() = None
|
||||||
self.virt = DisplayProperty()
|
self.primary: DisplayProperty() = None
|
||||||
self.virt.name = "VIRTUAL1"
|
self.virt_idx: int = None
|
||||||
|
self.primary_idx: int = None
|
||||||
# Primary display
|
# Primary display
|
||||||
self.primary = DisplayProperty()
|
self._update_screens()
|
||||||
self._update_primary_screen()
|
|
||||||
|
|
||||||
def _add_screen_mode(self) -> None:
|
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 = DisplayProperty()
|
||||||
|
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")
|
||||||
|
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.width
|
||||||
|
self.virt.height = 2 * self.virt.height
|
||||||
|
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}"
|
args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}"
|
||||||
try:
|
try:
|
||||||
self.check_call(args_addmode)
|
self.check_call(args_addmode)
|
||||||
|
@ -93,49 +209,25 @@ class XRandR(SubprocessWrapper):
|
||||||
for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
|
for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]:
|
||||||
signal.signal(sig, self._signal_handler)
|
signal.signal(sig, self._signal_handler)
|
||||||
|
|
||||||
def _update_primary_screen(self) -> None:
|
|
||||||
output = self.run("xrandr")
|
|
||||||
match = re.search(r"^(\w*)\s+.*primary\s*(\d+)x(\d+)\+(\d+)\+(\d+).*$", output, re.M)
|
|
||||||
self.primary.name = match.group(1)
|
|
||||||
self.primary.width = int(match.group(2))
|
|
||||||
self.primary.height = int(match.group(3))
|
|
||||||
self.primary.x_offset = int(match.group(4))
|
|
||||||
self.primary.y_offset = int(match.group(5))
|
|
||||||
|
|
||||||
def _update_virtual_screen(self) -> None:
|
|
||||||
output = self.run("xrandr")
|
|
||||||
match = re.search(r"^" + self.virt.name + r"\s+.*\s+(\d+)x(\d+)\+(\d+)\+(\d+).*$", output, re.M)
|
|
||||||
self.virt.width = int(match.group(1))
|
|
||||||
self.virt.height = int(match.group(2))
|
|
||||||
self.virt.x_offset = int(match.group(3))
|
|
||||||
self.virt.y_offset = int(match.group(4))
|
|
||||||
|
|
||||||
def _signal_handler(self, signum=None, frame=None) -> None:
|
def _signal_handler(self, signum=None, frame=None) -> None:
|
||||||
self.delete_virtual_screen()
|
self.delete_virtual_screen()
|
||||||
os._exit(0)
|
os._exit(0)
|
||||||
|
|
||||||
|
def get_primary_screen(self) -> DisplayProperty:
|
||||||
|
self._update_screens()
|
||||||
|
return self.primary
|
||||||
|
|
||||||
def get_virtual_screen(self) -> DisplayProperty:
|
def get_virtual_screen(self) -> DisplayProperty:
|
||||||
self._update_virtual_screen()
|
self._update_screens()
|
||||||
return self.virt
|
return self.virt
|
||||||
|
|
||||||
def set_virtual_screen(self, width, height, portrait=False, hidpi=False):
|
def create_virtual_screen(self, width, height, portrait=False, hidpi=False) -> None:
|
||||||
self.virt.width = width
|
print("creating: ", self.virt)
|
||||||
self.virt.height = height
|
self._add_screen_mode(width, height, portrait, hidpi)
|
||||||
if portrait:
|
|
||||||
self.virt.width = height
|
|
||||||
self.virt.height = width
|
|
||||||
if hidpi:
|
|
||||||
self.virt.width = 2 * self.virt.width
|
|
||||||
self.virt.height = 2 * self.virt.height
|
|
||||||
self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + self.scrren_suffix
|
|
||||||
|
|
||||||
def create_virtual_screen(self) -> None:
|
|
||||||
self._add_screen_mode()
|
|
||||||
self.check_call(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
self.check_call(f"xrandr --output {self.virt.name} --mode {self.mode_name}")
|
||||||
self.check_call("sleep 5")
|
self.check_call("sleep 5")
|
||||||
self.check_call(f"xrandr --output {self.virt.name} --auto")
|
self.check_call(f"xrandr --output {self.virt.name} --preferred")
|
||||||
self._update_primary_screen()
|
self._update_screens()
|
||||||
self._update_virtual_screen()
|
|
||||||
|
|
||||||
def delete_virtual_screen(self) -> None:
|
def delete_virtual_screen(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
@ -145,12 +237,15 @@ class XRandR(SubprocessWrapper):
|
||||||
return
|
return
|
||||||
self.call(f"xrandr --output {self.virt.name} --off")
|
self.call(f"xrandr --output {self.virt.name} --off")
|
||||||
self.call(f"xrandr --delmode {self.virt.name} {self.mode_name}")
|
self.call(f"xrandr --delmode {self.virt.name} {self.mode_name}")
|
||||||
|
atexit.unregister(self.delete_virtual_screen)
|
||||||
|
self._update_screens()
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Twisted class
|
# Twisted class
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
class ProcessProtocol(protocol.ProcessProtocol):
|
class ProcessProtocol(protocol.ProcessProtocol):
|
||||||
def __init__(self, onOutReceived, onErrRecevied, onProcessEnded, logfile=None):
|
def __init__(self, onConnected, onOutReceived, onErrRecevied, onProcessEnded, logfile=None):
|
||||||
|
self.onConnected = onConnected
|
||||||
self.onOutReceived = onOutReceived
|
self.onOutReceived = onOutReceived
|
||||||
self.onErrRecevied = onErrRecevied
|
self.onErrRecevied = onErrRecevied
|
||||||
self.onProcessEnded = onProcessEnded
|
self.onProcessEnded = onProcessEnded
|
||||||
|
@ -173,16 +268,17 @@ class ProcessProtocol(protocol.ProcessProtocol):
|
||||||
|
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
print("connectionMade!")
|
print("connectionMade!")
|
||||||
|
self.onConnected()
|
||||||
self.transport.closeStdin() # No more input
|
self.transport.closeStdin() # No more input
|
||||||
|
|
||||||
def outReceived(self, data):
|
def outReceived(self, data):
|
||||||
print("outReceived! with %d bytes!" % len(data))
|
# print("outReceived! with %d bytes!" % len(data))
|
||||||
self.onOutReceived(data)
|
self.onOutReceived(data)
|
||||||
if self.logfile is not None:
|
if self.logfile is not None:
|
||||||
self.logfile.write(data)
|
self.logfile.write(data)
|
||||||
|
|
||||||
def errReceived(self, data):
|
def errReceived(self, data):
|
||||||
print("outReceived! with %d bytes!" % len(data))
|
# print("errReceived! with %d bytes!" % len(data))
|
||||||
self.onErrRecevied(data)
|
self.onErrRecevied(data)
|
||||||
if self.logfile is not None:
|
if self.logfile is not None:
|
||||||
self.logfile.write(data)
|
self.logfile.write(data)
|
||||||
|
@ -219,313 +315,212 @@ class ProcessProtocol(protocol.ProcessProtocol):
|
||||||
self.onProcessEnded(exitCode)
|
self.onProcessEnded(exitCode)
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Qt Window class
|
# QML Backend class
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
class Window(QDialog):
|
class Backend(QObject):
|
||||||
def __init__(self):
|
""" Backend class for QML frontend """
|
||||||
super(Window, self).__init__()
|
class VNCState:
|
||||||
# Create objects
|
""" Enum to indicate a state of the VNC server """
|
||||||
self.createDisplayGroupBox()
|
OFF = 0
|
||||||
self.createVNCGroupBox()
|
WAITING = 1
|
||||||
self.createBottomLayout()
|
CONNECTED = 2
|
||||||
self.createActions()
|
|
||||||
self.createTrayIcon()
|
Q_ENUMS(VNCState)
|
||||||
|
|
||||||
|
# Signals
|
||||||
|
onVirtScreenCreatedChanged = pyqtSignal(bool)
|
||||||
|
onVirtScreenIndexChanged = pyqtSignal(int)
|
||||||
|
onVncStateChanged = pyqtSignal(VNCState)
|
||||||
|
onIPAddressesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super(Backend, self).__init__(parent)
|
||||||
|
# objects
|
||||||
self.xrandr = XRandR()
|
self.xrandr = XRandR()
|
||||||
# Additional attributes
|
# Virtual screen properties
|
||||||
self.isDisplayCreated = False
|
self._virt = DisplayProperty()
|
||||||
self.isVNCRunning = False
|
self.virt.width = 1368
|
||||||
self.isQuitProgramPending = False
|
self.virt.height = 1024
|
||||||
# Update UI
|
self._portrait = False
|
||||||
self.update_ip_address()
|
self._hidpi = False
|
||||||
# Put togather
|
self._virtScreenCreated = False
|
||||||
mainLayout = QVBoxLayout()
|
self._screens: List[DisplayProperty] = self.xrandr.screens
|
||||||
mainLayout.addWidget(self.displayGroupBox)
|
self._virtScreenIndex = self.xrandr.virt_idx
|
||||||
mainLayout.addWidget(self.VNCGroupBox)
|
# VNC server properties
|
||||||
mainLayout.addLayout(self.bottomLayout)
|
self._vncPort = 5900
|
||||||
self.setLayout(mainLayout)
|
self._vncPassword = ""
|
||||||
# Events
|
self._vncState = Backend.VNCState.OFF
|
||||||
self.trayIcon.activated.connect(self.iconActivated)
|
self._ipAddresses: List[str] = []
|
||||||
self.createDisplayButton.pressed.connect(self.createDisplayPressed)
|
self.updateIPAddresses()
|
||||||
self.startVNCButton.pressed.connect(self.startVNCPressed)
|
# Primary screen and mouse posistion
|
||||||
QApplication.desktop().resized.connect(self.screenChanged)
|
self._primary: DisplayProperty() = self.xrandr.get_primary_screen()
|
||||||
# QApplication.desktop().resized.connect(self.startVNCPressed)
|
self._cursor_x: int
|
||||||
# QApplication.desktop().screenCountChanged.connect(self.startVNCPressed)
|
self._cursor_y: int
|
||||||
self.bottomQuitButton.pressed.connect(self.quitProgram)
|
|
||||||
# Show
|
|
||||||
self.setWindowIcon(self.icon)
|
|
||||||
self.trayIcon.show()
|
|
||||||
self.trayIcon.setToolTip("VirtScreen")
|
|
||||||
self.setWindowTitle("VirtScreen")
|
|
||||||
self.resize(400, 300)
|
|
||||||
|
|
||||||
def setVisible(self, visible):
|
# Qt properties
|
||||||
"""Override of setVisible(bool)
|
@pyqtProperty(DisplayProperty)
|
||||||
|
def virt(self):
|
||||||
|
return self._virt
|
||||||
|
@virt.setter
|
||||||
|
def virt(self, virt):
|
||||||
|
self._virt = virt
|
||||||
|
|
||||||
Arguments:
|
@pyqtProperty(bool)
|
||||||
visible {bool} -- true to show, false to hide
|
def portrait(self):
|
||||||
"""
|
return self._portrait
|
||||||
self.openAction.setEnabled(self.isMaximized() or not visible)
|
@portrait.setter
|
||||||
super(Window, self).setVisible(visible)
|
def portrait(self, portrait):
|
||||||
|
self._portrait = portrait
|
||||||
|
|
||||||
def changeEvent(self, event):
|
@pyqtProperty(bool)
|
||||||
"""Override of QWidget::changeEvent()
|
def hidpi(self):
|
||||||
|
return self._hidpi
|
||||||
|
@hidpi.setter
|
||||||
|
def hidpi(self, hidpi):
|
||||||
|
self._hidpi = hidpi
|
||||||
|
|
||||||
Arguments:
|
@pyqtProperty(bool, notify=onVirtScreenCreatedChanged)
|
||||||
event {QEvent} -- QEvent
|
def virtScreenCreated(self):
|
||||||
"""
|
return self._virtScreenCreated
|
||||||
if event.type() == QEvent.ActivationChange and not self.isActiveWindow():
|
@virtScreenCreated.setter
|
||||||
self.hide()
|
def virtScreenCreated(self, value):
|
||||||
|
self._virtScreenCreated = value
|
||||||
|
self.onVirtScreenCreatedChanged.emit(value)
|
||||||
|
|
||||||
def closeEvent(self, event):
|
@pyqtProperty(QQmlListProperty)
|
||||||
"""Override of closeEvent()
|
def screens(self):
|
||||||
|
return QQmlListProperty(DisplayProperty, self, self._screens)
|
||||||
|
|
||||||
Arguments:
|
@pyqtProperty(int, notify=onVirtScreenIndexChanged)
|
||||||
event {QCloseEvent} -- QCloseEvent
|
def virtScreenIndex(self):
|
||||||
"""
|
return self._virtScreenIndex
|
||||||
if self.trayIcon.isVisible():
|
@virtScreenIndex.setter
|
||||||
self.hide()
|
def virtScreenIndex(self, virtScreenIndex):
|
||||||
self.showMessage()
|
print("Changing virt to ", virtScreenIndex)
|
||||||
event.ignore()
|
self.xrandr.virt_idx = virtScreenIndex
|
||||||
else:
|
self.xrandr.virt = self.xrandr.screens[self.xrandr.virt_idx]
|
||||||
QApplication.instance().quit()
|
self._virtScreenIndex = virtScreenIndex
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtProperty(int)
|
||||||
def createDisplayPressed(self):
|
def vncPort(self):
|
||||||
if not self.isDisplayCreated:
|
return self._vncPort
|
||||||
# Create virtual screen
|
@vncPort.setter
|
||||||
self.createDisplayButton.setEnabled(False)
|
def vncPort(self, port):
|
||||||
width = self.displayWidthSpinBox.value()
|
self._vncPort = port
|
||||||
height = self.displayHeightSpinBox.value()
|
|
||||||
portrait = self.displayPortraitCheckBox.isChecked()
|
|
||||||
hidpi = self.displayHIDPICheckBox.isChecked()
|
|
||||||
self.xrandr.set_virtual_screen(width, height, portrait, hidpi)
|
|
||||||
self.xrandr.create_virtual_screen()
|
|
||||||
self.createDisplayButton.setText("Disable the virtual display")
|
|
||||||
self.isDisplayCreated = True
|
|
||||||
self.createDisplayButton.setEnabled(True)
|
|
||||||
self.startVNCButton.setEnabled(True)
|
|
||||||
self.trayIcon.setIcon(self.icon_tablet_off)
|
|
||||||
else:
|
|
||||||
# Delete the screen
|
|
||||||
self.createDisplayButton.setEnabled(False)
|
|
||||||
self.xrandr.delete_virtual_screen()
|
|
||||||
self.isDisplayCreated = False
|
|
||||||
self.createDisplayButton.setText("Create a Virtual Display")
|
|
||||||
self.createDisplayButton.setEnabled(True)
|
|
||||||
self.startVNCButton.setEnabled(False)
|
|
||||||
self.trayIcon.setIcon(self.icon)
|
|
||||||
self.createDisplayAction.setEnabled(not self.isDisplayCreated)
|
|
||||||
self.deleteDisplayAction.setEnabled(self.isDisplayCreated)
|
|
||||||
self.startVNCAction.setEnabled(self.isDisplayCreated)
|
|
||||||
self.stopVNCAction.setEnabled(False)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtProperty(str)
|
||||||
def startVNCPressed(self):
|
def vncPassword(self):
|
||||||
if not self.isVNCRunning:
|
return self._vncPassword
|
||||||
self.startVNC()
|
@vncPassword.setter
|
||||||
else:
|
def vncPassword(self, vncPassword):
|
||||||
self.VNCServer.kill()
|
self._vncPassword = vncPassword
|
||||||
|
|
||||||
@pyqtSlot('QSystemTrayIcon::ActivationReason')
|
@pyqtProperty(VNCState, notify=onVncStateChanged)
|
||||||
def iconActivated(self, reason):
|
def vncState(self):
|
||||||
if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.DoubleClick):
|
return self._vncState
|
||||||
if self.isVisible():
|
@vncState.setter
|
||||||
self.hide()
|
def vncState(self, state):
|
||||||
else:
|
self._vncState = state
|
||||||
# move the widget to one of 4 coners of the primary display,
|
self.onVncStateChanged.emit(self._vncState)
|
||||||
# depending on the current mouse cursor.
|
|
||||||
screen = QApplication.desktop().screenGeometry()
|
@pyqtProperty('QStringList', notify=onIPAddressesChanged)
|
||||||
x_mid = screen.width() / 2
|
def ipAddresses(self):
|
||||||
y_mid = screen.height() / 2
|
return self._ipAddresses
|
||||||
|
|
||||||
|
@pyqtProperty(DisplayProperty)
|
||||||
|
def primary(self):
|
||||||
|
self._primary = self.xrandr.get_primary_screen()
|
||||||
|
return self._primary
|
||||||
|
|
||||||
|
@pyqtProperty(int)
|
||||||
|
def cursor_x(self):
|
||||||
cursor = QCursor().pos()
|
cursor = QCursor().pos()
|
||||||
x = (screen.width() - self.width()) if (cursor.x() > x_mid) else 0
|
self._cursor_x = cursor.x()
|
||||||
y = (screen.height() - self.height()) if (cursor.y() > y_mid) else 0
|
return self._cursor_x
|
||||||
self.move(x, y)
|
|
||||||
self.showNormal()
|
|
||||||
elif reason == QSystemTrayIcon.MiddleClick:
|
|
||||||
self.showMessage()
|
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtProperty(int)
|
||||||
def screenChanged(self, count):
|
def cursor_y(self):
|
||||||
for i in range(QApplication.desktop().screenCount()):
|
cursor = QCursor().pos()
|
||||||
print(QApplication.desktop().availableGeometry(i))
|
self._cursor_y = cursor.y()
|
||||||
|
return self._cursor_y
|
||||||
|
|
||||||
|
# Qt Slots
|
||||||
|
@pyqtSlot()
|
||||||
|
def createVirtScreen(self):
|
||||||
|
print("Creating a Virtual Screen...")
|
||||||
|
self.xrandr.create_virtual_screen(self.virt.width, self.virt.height, self.portrait, self.hidpi)
|
||||||
|
self.virtScreenCreated = True
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def showMessage(self):
|
def deleteVirtScreen(self):
|
||||||
self.trayIcon.showMessage("VirtScreen is running",
|
print("Deleting the Virtual Screen...")
|
||||||
"The program will keep running in the system tray. To \n"
|
if self.vncState is not Backend.VNCState.OFF:
|
||||||
"terminate the program, choose \"Quit\" in the \n"
|
print("Turn off the VNC server first")
|
||||||
"context menu of the system tray entry.",
|
self.virtScreenCreated = True
|
||||||
QSystemTrayIcon.MessageIcon(QSystemTrayIcon.Information),
|
return
|
||||||
7 * 1000)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def quitProgram(self):
|
|
||||||
self.isQuitProgramPending = True
|
|
||||||
try:
|
|
||||||
# Rest of quit sequence will be handled in the callback.
|
|
||||||
self.VNCServer.kill()
|
|
||||||
except (AttributeError, error.ProcessExitedAlready):
|
|
||||||
self.xrandr.delete_virtual_screen()
|
self.xrandr.delete_virtual_screen()
|
||||||
QApplication.instance().quit()
|
self.virtScreenCreated = False
|
||||||
|
|
||||||
def createDisplayGroupBox(self):
|
@pyqtSlot()
|
||||||
self.displayGroupBox = QGroupBox("Virtual Display Settings")
|
def startVNC(self):
|
||||||
|
# Check if a virtual screen created
|
||||||
|
if not self.virtScreenCreated:
|
||||||
|
print("Virtual Screen not crated.")
|
||||||
|
return
|
||||||
|
if self.vncState is not Backend.VNCState.OFF:
|
||||||
|
print("VNC Server is already running.")
|
||||||
|
return
|
||||||
|
# regex used in callbacks
|
||||||
|
re_connection = re.compile(r"^.*Got connection from client.*$", re.M)
|
||||||
|
# define callbacks
|
||||||
|
def _onConnected():
|
||||||
|
print("VNC started.")
|
||||||
|
self.vncState = Backend.VNCState.WAITING
|
||||||
|
def _onReceived(data):
|
||||||
|
data = data.decode("utf-8")
|
||||||
|
if (self._vncState is not Backend.VNCState.CONNECTED) and re_connection.search(data):
|
||||||
|
print("VNC connected.")
|
||||||
|
self.vncState = Backend.VNCState.CONNECTED
|
||||||
|
def _onEnded(exitCode):
|
||||||
|
print("VNC Exited.")
|
||||||
|
self.vncState = Backend.VNCState.OFF
|
||||||
|
atexit.unregister(self.stopVNC)
|
||||||
|
# Set password
|
||||||
|
password = False
|
||||||
|
if self.vncPassword:
|
||||||
|
print("There is password. Creating.")
|
||||||
|
password = True
|
||||||
|
p = SubprocessWrapper()
|
||||||
|
try:
|
||||||
|
p.run(f"x11vnc -storepasswd {self.vncPassword} {X11VNC_PASSWORD_PATH}")
|
||||||
|
except:
|
||||||
|
password = False
|
||||||
|
logfile = open(X11VNC_LOG_PATH, "wb")
|
||||||
|
self.vncServer = ProcessProtocol(_onConnected, _onReceived, _onReceived, _onEnded, logfile)
|
||||||
|
port = self.vncPort
|
||||||
|
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 password:
|
||||||
|
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
|
||||||
|
self.vncServer.run(arg)
|
||||||
|
# auto stop on exit
|
||||||
|
atexit.register(self.stopVNC, force=True)
|
||||||
|
|
||||||
# Resolution Row
|
@pyqtSlot()
|
||||||
resolutionLabel = QLabel("Resolution:")
|
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 (Backend.VNCState.WAITING, Backend.VNCState.CONNECTED):
|
||||||
|
self.vncServer.kill()
|
||||||
|
else:
|
||||||
|
print("stopVNC called while it is not running")
|
||||||
|
|
||||||
self.displayWidthSpinBox = QSpinBox()
|
@pyqtSlot()
|
||||||
self.displayWidthSpinBox.setRange(640, 1920)
|
def updateIPAddresses(self):
|
||||||
self.displayWidthSpinBox.setSuffix("px")
|
self._ipAddresses.clear()
|
||||||
self.displayWidthSpinBox.setValue(1368)
|
|
||||||
|
|
||||||
xLabel = QLabel("x")
|
|
||||||
|
|
||||||
self.displayHeightSpinBox = QSpinBox()
|
|
||||||
self.displayHeightSpinBox.setRange(360, 1080)
|
|
||||||
self.displayHeightSpinBox.setSuffix("px")
|
|
||||||
self.displayHeightSpinBox.setValue(1024)
|
|
||||||
|
|
||||||
# Portrait and HiDPI
|
|
||||||
self.displayPortraitCheckBox = QCheckBox("Portrait Mode")
|
|
||||||
self.displayPortraitCheckBox.setChecked(False)
|
|
||||||
|
|
||||||
self.displayHIDPICheckBox = QCheckBox("HiDPI (2x resolution)")
|
|
||||||
self.displayHIDPICheckBox.setChecked(False)
|
|
||||||
|
|
||||||
# Start button
|
|
||||||
self.createDisplayButton = QPushButton("Create a Virtual Display")
|
|
||||||
self.createDisplayButton.setDefault(True)
|
|
||||||
|
|
||||||
# Notice Label
|
|
||||||
self.displayNoticeLabel = QLabel("After creating, you can adjust the display's " +
|
|
||||||
"position in the Desktop Environment's settings " +
|
|
||||||
"or ARandR.")
|
|
||||||
self.displayNoticeLabel.setWordWrap(True)
|
|
||||||
font = self.displayNoticeLabel.font()
|
|
||||||
font.setPointSize(9)
|
|
||||||
self.displayNoticeLabel.setFont(font)
|
|
||||||
|
|
||||||
# Putting them together
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
|
||||||
# Grid layout for screen settings
|
|
||||||
gridLayout = QGridLayout()
|
|
||||||
# Resolution row
|
|
||||||
rowLayout = QHBoxLayout()
|
|
||||||
rowLayout.addWidget(resolutionLabel)
|
|
||||||
rowLayout.addWidget(self.displayWidthSpinBox)
|
|
||||||
rowLayout.addWidget(xLabel)
|
|
||||||
rowLayout.addWidget(self.displayHeightSpinBox)
|
|
||||||
rowLayout.addStretch()
|
|
||||||
layout.addLayout(rowLayout)
|
|
||||||
# Portrait & HiDPI
|
|
||||||
rowLayout = QHBoxLayout()
|
|
||||||
rowLayout.addWidget(self.displayPortraitCheckBox)
|
|
||||||
rowLayout.addWidget(self.displayHIDPICheckBox)
|
|
||||||
rowLayout.addStretch()
|
|
||||||
layout.addLayout(rowLayout)
|
|
||||||
# Display create button and Notice label
|
|
||||||
layout.addWidget(self.createDisplayButton)
|
|
||||||
layout.addWidget(self.displayNoticeLabel)
|
|
||||||
|
|
||||||
self.displayGroupBox.setLayout(layout)
|
|
||||||
|
|
||||||
def createVNCGroupBox(self):
|
|
||||||
self.VNCGroupBox = QGroupBox("VNC Server")
|
|
||||||
|
|
||||||
portLabel = QLabel("Port:")
|
|
||||||
self.VNCPortSpinBox = QSpinBox()
|
|
||||||
self.VNCPortSpinBox.setRange(1, 65535)
|
|
||||||
self.VNCPortSpinBox.setValue(5900)
|
|
||||||
|
|
||||||
passwordLabel = QLabel("Password:")
|
|
||||||
self.VNCPasswordLineEdit = QLineEdit()
|
|
||||||
self.VNCPasswordLineEdit.setEchoMode(QLineEdit.Password)
|
|
||||||
self.VNCPasswordLineEdit.setText("")
|
|
||||||
|
|
||||||
IPLabel = QLabel("Connect a VNC client to one of:")
|
|
||||||
self.VNCIPListWidget = QListWidget()
|
|
||||||
|
|
||||||
self.startVNCButton = QPushButton("Start VNC Server")
|
|
||||||
self.startVNCButton.setDefault(False)
|
|
||||||
self.startVNCButton.setEnabled(False)
|
|
||||||
|
|
||||||
# Set Overall layout
|
|
||||||
layout = QVBoxLayout()
|
|
||||||
rowLayout = QHBoxLayout()
|
|
||||||
rowLayout.addWidget(portLabel)
|
|
||||||
rowLayout.addWidget(self.VNCPortSpinBox)
|
|
||||||
rowLayout.addWidget(passwordLabel)
|
|
||||||
rowLayout.addWidget(self.VNCPasswordLineEdit)
|
|
||||||
layout.addLayout(rowLayout)
|
|
||||||
layout.addWidget(self.startVNCButton)
|
|
||||||
layout.addWidget(IPLabel)
|
|
||||||
layout.addWidget(self.VNCIPListWidget)
|
|
||||||
self.VNCGroupBox.setLayout(layout)
|
|
||||||
|
|
||||||
def createBottomLayout(self):
|
|
||||||
self.bottomLayout = QVBoxLayout()
|
|
||||||
|
|
||||||
# Create button
|
|
||||||
self.bottomQuitButton = QPushButton("Quit")
|
|
||||||
self.bottomQuitButton.setDefault(False)
|
|
||||||
self.bottomQuitButton.setEnabled(True)
|
|
||||||
|
|
||||||
# Set Overall layout
|
|
||||||
hLayout = QHBoxLayout()
|
|
||||||
hLayout.addStretch()
|
|
||||||
hLayout.addWidget(self.bottomQuitButton)
|
|
||||||
self.bottomLayout.addLayout(hLayout)
|
|
||||||
|
|
||||||
def createActions(self):
|
|
||||||
self.createDisplayAction = QAction("Create display", self)
|
|
||||||
self.createDisplayAction.triggered.connect(self.createDisplayPressed)
|
|
||||||
self.createDisplayAction.setEnabled(True)
|
|
||||||
|
|
||||||
self.deleteDisplayAction = QAction("Disable display", self)
|
|
||||||
self.deleteDisplayAction.triggered.connect(self.createDisplayPressed)
|
|
||||||
self.deleteDisplayAction.setEnabled(False)
|
|
||||||
|
|
||||||
self.startVNCAction = QAction("&Start sharing", self)
|
|
||||||
self.startVNCAction.triggered.connect(self.startVNCPressed)
|
|
||||||
self.startVNCAction.setEnabled(False)
|
|
||||||
|
|
||||||
self.stopVNCAction = QAction("S&top sharing", self)
|
|
||||||
self.stopVNCAction.triggered.connect(self.startVNCPressed)
|
|
||||||
self.stopVNCAction.setEnabled(False)
|
|
||||||
|
|
||||||
self.openAction = QAction("&Open VirtScreen", self)
|
|
||||||
self.openAction.triggered.connect(self.showNormal)
|
|
||||||
|
|
||||||
self.quitAction = QAction("&Quit", self)
|
|
||||||
self.quitAction.triggered.connect(self.quitProgram)
|
|
||||||
|
|
||||||
def createTrayIcon(self):
|
|
||||||
# Menu
|
|
||||||
self.trayIconMenu = QMenu(self)
|
|
||||||
self.trayIconMenu.addAction(self.createDisplayAction)
|
|
||||||
self.trayIconMenu.addAction(self.deleteDisplayAction)
|
|
||||||
self.trayIconMenu.addSeparator()
|
|
||||||
self.trayIconMenu.addAction(self.startVNCAction)
|
|
||||||
self.trayIconMenu.addAction(self.stopVNCAction)
|
|
||||||
self.trayIconMenu.addSeparator()
|
|
||||||
self.trayIconMenu.addAction(self.openAction)
|
|
||||||
self.trayIconMenu.addSeparator()
|
|
||||||
self.trayIconMenu.addAction(self.quitAction)
|
|
||||||
|
|
||||||
# Icons
|
|
||||||
self.icon = QIcon(ICON_PATH)
|
|
||||||
self.icon_tablet_off = QIcon(ICON_TABLET_OFF_PATH)
|
|
||||||
self.icon_tablet_on = QIcon(ICON_TABLET_ON_PATH)
|
|
||||||
|
|
||||||
self.trayIcon = QSystemTrayIcon(self)
|
|
||||||
self.trayIcon.setContextMenu(self.trayIconMenu)
|
|
||||||
self.trayIcon.setIcon(self.icon)
|
|
||||||
|
|
||||||
def update_ip_address(self):
|
|
||||||
self.VNCIPListWidget.clear()
|
|
||||||
for interface in interfaces():
|
for interface in interfaces():
|
||||||
if interface == 'lo':
|
if interface == 'lo':
|
||||||
continue
|
continue
|
||||||
|
@ -534,70 +529,22 @@ class Window(QDialog):
|
||||||
continue
|
continue
|
||||||
for link in addresses:
|
for link in addresses:
|
||||||
if link is not None:
|
if link is not None:
|
||||||
self.VNCIPListWidget.addItem(link['addr'])
|
self._ipAddresses.append(link['addr'])
|
||||||
|
self.onIPAddressesChanged.emit()
|
||||||
|
|
||||||
def startVNC(self):
|
@pyqtSlot()
|
||||||
def _onReceived(data):
|
def quitProgram(self):
|
||||||
data = data.decode("utf-8")
|
|
||||||
for line in data.splitlines():
|
|
||||||
# TODO: Update state of the server
|
|
||||||
pass
|
|
||||||
def _onEnded(exitCode):
|
|
||||||
self.startVNCButton.setEnabled(False)
|
|
||||||
self.isVNCRunning = False
|
|
||||||
if self.isQuitProgramPending:
|
|
||||||
self.xrandr.delete_virtual_screen()
|
|
||||||
QApplication.instance().quit()
|
QApplication.instance().quit()
|
||||||
self.startVNCButton.setText("Start VNC Server")
|
|
||||||
self.startVNCButton.setEnabled(True)
|
|
||||||
self.createDisplayButton.setEnabled(True)
|
|
||||||
self.deleteDisplayAction.setEnabled(True)
|
|
||||||
self.startVNCAction.setEnabled(True)
|
|
||||||
self.stopVNCAction.setEnabled(False)
|
|
||||||
self.trayIcon.setIcon(self.icon_tablet_off)
|
|
||||||
# Setting UI before starting
|
|
||||||
self.createDisplayButton.setEnabled(False)
|
|
||||||
self.createDisplayAction.setEnabled(False)
|
|
||||||
self.deleteDisplayAction.setEnabled(False)
|
|
||||||
self.startVNCButton.setEnabled(False)
|
|
||||||
self.startVNCButton.setText("Running...")
|
|
||||||
self.startVNCAction.setEnabled(False)
|
|
||||||
# Set password
|
|
||||||
isPassword = False
|
|
||||||
if self.VNCPasswordLineEdit.text():
|
|
||||||
isPassword = True
|
|
||||||
p = SubprocessWrapper()
|
|
||||||
try:
|
|
||||||
p.run(f"x11vnc -storepasswd {self.VNCPasswordLineEdit.text()} {X11VNC_PASSWORD_PATH}")
|
|
||||||
except:
|
|
||||||
isPassword = False
|
|
||||||
# Run VNC server
|
|
||||||
self.isVNCRunning = True
|
|
||||||
logfile = open(X11VNC_LOG_PATH, "wb")
|
|
||||||
self.VNCServer = ProcessProtocol(_onReceived, _onReceived, _onEnded, logfile)
|
|
||||||
port = self.VNCPortSpinBox.value()
|
|
||||||
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 isPassword:
|
|
||||||
arg += f" -rfbauth {X11VNC_PASSWORD_PATH}"
|
|
||||||
self.VNCServer.run(arg)
|
|
||||||
self.update_ip_address()
|
|
||||||
# Change UI
|
|
||||||
self.startVNCButton.setEnabled(True)
|
|
||||||
self.startVNCButton.setText("Stop Sharing")
|
|
||||||
self.stopVNCAction.setEnabled(True)
|
|
||||||
self.trayIcon.setIcon(self.icon_tablet_on)
|
|
||||||
|
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
# Main Code
|
# Main Code
|
||||||
#-------------------------------------------------------------------------------
|
#-------------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||||
import sys
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import QSystemTrayIcon, QMessageBox
|
||||||
|
|
||||||
if not QSystemTrayIcon.isSystemTrayAvailable():
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
QMessageBox.critical(None, "VirtScreen",
|
QMessageBox.critical(None, "VirtScreen",
|
||||||
"I couldn't detect any system tray on this system.")
|
"I couldn't detect any system tray on this system.")
|
||||||
|
@ -623,10 +570,20 @@ if __name__ == '__main__':
|
||||||
qt5reactor.install()
|
qt5reactor.install()
|
||||||
from twisted.internet import utils, reactor # pylint: disable=E0401
|
from twisted.internet import utils, reactor # pylint: disable=E0401
|
||||||
|
|
||||||
QApplication.setQuitOnLastWindowClosed(False)
|
app.setWindowIcon(QIcon(ICON_PATH))
|
||||||
window = Window()
|
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material"
|
||||||
window.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
|
# os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion"
|
||||||
time.sleep(2) # Otherwise the trayicon message will be shown in weird position
|
|
||||||
window.showMessage()
|
# 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'))
|
||||||
|
if not engine.rootObjects():
|
||||||
|
QMessageBox.critical(None, "VirtScreen", "Failed to load qml")
|
||||||
|
sys.exit(1)
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec_())
|
||||||
reactor.run()
|
reactor.run()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue