1
0
Fork 0
mirror of https://github.com/kbumsik/VirtScreen.git synced 2025-03-09 15:40:18 +00:00

Merge pull request #2 from kbumsik/qml

Switch to QML
This commit is contained in:
Bumsik Kim 2018-05-10 15:59:42 -04:00 committed by GitHub
commit abba868446
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 662 additions and 487 deletions

330
main.qml
View file

@ -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()
} }
} }
} }

View file

@ -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()