From 73c8f5fb7c0edaaa8435ed632c2bdef3906ff4fb Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Sun, 6 May 2018 19:24:06 -0400 Subject: [PATCH 01/23] QML: First Basic backend (Doesn't do anything for now) --- main.qml | 62 +++-- virtscreen.py | 680 ++++++-------------------------------------------- 2 files changed, 126 insertions(+), 616 deletions(-) diff --git a/main.qml b/main.qml index b260bff..4a93645 100644 --- a/main.qml +++ b/main.qml @@ -1,8 +1,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import QtQuick.Controls.Material 2.3 +// import QtQuick.Controls.Material 2.3 import QtQuick.Layouts 1.3 -import QtQuick.Window 2.2 import Qt.labs.platform 1.0 as Labs @@ -14,27 +13,31 @@ ApplicationWindow { visible: true title: "Basic layouts" - Material.theme: Material.Light - Material.accent: Material.Teal + // Material.theme: Material.Light + // Material.accent: Material.Teal property int margin: 11 width: 380 height: 600 + Backend { + id: backend + } + // Timer object and function Timer { id: timer - } - - function setTimeout(cb, delayTime) { - timer.interval = delayTime; - timer.repeat = false; - timer.triggered.connect(cb); - timer.start(); + function setTimeout(cb, delayTime) { + timer.interval = delayTime; + timer.repeat = false; + timer.triggered.connect(cb); + timer.start(); + } } header: TabBar { id: tabBar + position: TabBar.Header width: parent.width currentIndex: 0 @@ -69,7 +72,8 @@ ApplicationWindow { RowLayout { Layout.fillWidth: true Label { text: "Width"; Layout.fillWidth: true } - SpinBox { value: 1368 + SpinBox { + value: backend.width from: 640 to: 1920 stepSize: 1 @@ -77,13 +81,17 @@ ApplicationWindow { textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 0) + " px"; } + onValueModified: { + backend.width = value; + } } } RowLayout { Layout.fillWidth: true Label { text: "Height"; Layout.fillWidth: true } - SpinBox { value: 1024 + SpinBox { + value: backend.height from: 360 to: 1080 stepSize : 1 @@ -91,19 +99,32 @@ ApplicationWindow { textFromValue: function(value, locale) { return Number(value).toLocaleString(locale, 'f', 0) + " px"; } + onValueModified: { + backend.height = value; + } } } RowLayout { Layout.fillWidth: true Label { text: "Portrait Mode"; Layout.fillWidth: true } - Switch { checked: false } + Switch { + checked: backend.portrait + onCheckedChanged: { + backend.portrait = checked; + } + } } RowLayout { Layout.fillWidth: true Label { text: "HiDPI (2x resolution)"; Layout.fillWidth: true } - Switch { checked: false } + Switch { + checked: backend.hidpi + onCheckedChanged: { + backend.hidpi = checked; + } + } } } } @@ -134,11 +155,14 @@ ApplicationWindow { Layout.fillWidth: true Label { text: "Port"; Layout.fillWidth: true } SpinBox { - value: 5900 + value: backend.vncPort from: 1 to: 65535 stepSize: 1 editable: true + onValueModified: { + backend.vncPort = value; + } } } @@ -148,7 +172,11 @@ ApplicationWindow { TextField { Layout.fillWidth: true placeholderText: "Password"; + text: backend.vncPassword; echoMode: TextInput.Password; + onTextEdited: { + backend.vncPassword = text; + } } } } @@ -187,7 +215,7 @@ ApplicationWindow { onMessageClicked: console.log("Message clicked") Component.onCompleted: { // 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."); }, 1000); } diff --git a/virtscreen.py b/virtscreen.py index baba39a..f9a6abd 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -1,632 +1,114 @@ #!/usr/bin/env python -import os, re, time -from PyQt5.QtGui import QIcon, QCursor, QFocusEvent -from PyQt5.QtCore import pyqtSlot, Qt, QEvent -from PyQt5.QtWidgets import (QAction, QApplication, QCheckBox, QComboBox, - QDialog, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, - QMessageBox, QMenu, QPushButton, QSpinBox, QStyle, QSystemTrayIcon, - QTextEdit, QVBoxLayout, QListWidget) -from twisted.internet import protocol, error -from netifaces import interfaces, ifaddresses, AF_INET -import subprocess -import atexit, signal +import sys, os -# Redirect stdout to /dev/null. Uncomment it while debugging. -# import sys -# sys.stdout = open(os.devnull, "a") +from PyQt5.QtWidgets import QApplication +from PyQt5.QtCore import QObject, QUrl, Qt +from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal +from PyQt5.QtGui import QIcon +from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine #------------------------------------------------------------------------------- # file path definitions #------------------------------------------------------------------------------- -HOME_PATH = os.getenv('HOME', None) -if HOME_PATH is not None: - HOME_PATH = HOME_PATH + "/.virtscreen" -X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt" -X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd" -CONFIG_PATH = HOME_PATH + "/config" - PROGRAM_PATH = "." ICON_PATH = PROGRAM_PATH + "/icon/icon.png" ICON_TABLET_OFF_PATH = PROGRAM_PATH + "/icon/icon_tablet_off.png" ICON_TABLET_ON_PATH = PROGRAM_PATH + "/icon/icon_tablet_on.png" #------------------------------------------------------------------------------- -# Subprocess wrapper +# QML Backend class #------------------------------------------------------------------------------- -class SubprocessWrapper: - def __init__(self, stdout:str=os.devnull, stderr:str=os.devnull): - self.stdout: str = stdout - self.stderr: str = stderr +class Backend(QObject): + width_changed = pyqtSignal(int) + virtScreenChanged = pyqtSignal(bool) + vncChanged = pyqtSignal(bool) + + def __init__(self, parent=None): + super(Backend, self).__init__(parent) + # Virtual screen properties + self._width = 1368 + self._height = 1024 + self._portrait = True + self._hidpi = False + self._virtScreenCreated = False + # VNC server properties + self._vncPort = 5900 + self._vncPassword = "" + self._vncState = False - def call(self, arg) -> None: - with open(os.devnull, "w") as f: - subprocess.call(arg.split(), stdout=f, stderr=f) - - def check_call(self, arg) -> None: - with open(os.devnull, "w") as f: - subprocess.check_call(arg.split(), stdout=f, stderr=f) + @pyqtProperty(int, notify=width_changed) + def width(self): + return self._width + @width.setter + def width(self, width): + self._width = width + self.width_changed.emit(self._width) - def run(self, arg: str) -> str: - return subprocess.run(arg.split(), stdout=subprocess.PIPE).stdout.decode('utf-8') + @pyqtProperty(int) + def height(self): + return self._height + @height.setter + def height(self, height): + self._height = height -#------------------------------------------------------------------------------- -# Display properties -#------------------------------------------------------------------------------- -class DisplayProperty: - def __init__(self): - self.name: str - self.width: int - self.height: int - self.x_offset: int - self.y_offset: int + @pyqtProperty(bool) + def portrait(self): + return self._portrait + @portrait.setter + def portrait(self, portrait): + self._portrait = portrait -#------------------------------------------------------------------------------- -# Screen adjustment class -#------------------------------------------------------------------------------- -class XRandR(SubprocessWrapper): - def __init__(self): - super(XRandR, self).__init__() - self.mode_name: str - self.scrren_suffix = "_virt" - # Thoese will be created in set_virtual_screen() - self.virt = DisplayProperty() - self.virt.name = "VIRTUAL1" - # Primary display - self.primary = DisplayProperty() - self._update_primary_screen() + @pyqtProperty(bool) + def hidpi(self): + return self._hidpi + @hidpi.setter + def hidpi(self, hidpi): + self._hidpi = hidpi - def _add_screen_mode(self) -> None: - args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}" - try: - self.check_call(args_addmode) - except subprocess.CalledProcessError: - # When failed create mode and then add again - output = self.run(f"cvt {self.virt.width} {self.virt.height}") - mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1) - # Create new screen mode - self.check_call(f"xrandr --newmode {self.mode_name} {mode}") - # Add mode again - self.check_call(args_addmode) - # After adding mode the program should delete the mode automatically on exit - atexit.register(self.delete_virtual_screen) - for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]: - signal.signal(sig, self._signal_handler) - - def _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: - self.delete_virtual_screen() - os._exit(0) - - def get_virtual_screen(self) -> DisplayProperty: - self._update_virtual_screen() - return self.virt - - def set_virtual_screen(self, width, height, portrait=False, hidpi=False): - 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.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("sleep 5") - self.check_call(f"xrandr --output {self.virt.name} --auto") - self._update_primary_screen() - self._update_virtual_screen() - - def delete_virtual_screen(self) -> None: - try: - self.virt.name - self.mode_name - except AttributeError: - return - self.call(f"xrandr --output {self.virt.name} --off") - self.call(f"xrandr --delmode {self.virt.name} {self.mode_name}") - -#------------------------------------------------------------------------------- -# Twisted class -#------------------------------------------------------------------------------- -class ProcessProtocol(protocol.ProcessProtocol): - def __init__(self, onOutReceived, onErrRecevied, onProcessEnded, logfile=None): - self.onOutReceived = onOutReceived - self.onErrRecevied = onErrRecevied - self.onProcessEnded = onProcessEnded - self.logfile = logfile - - def run(self, arg: str): - """Spawn a process + @pyqtProperty(bool) + def virtScreenCreated(self): + return self._virtScreenCreated - Arguments: - arg {str} -- arguments in string - """ + @pyqtProperty(int) + def vncPort(self): + return self._vncPort + @vncPort.setter + def vncPort(self, vncPort): + self._vncPort = vncPort - args = arg.split() - reactor.spawnProcess(self, args[0], args=args, env=os.environ) - - def kill(self): - """Kill a spawned process - """ - self.transport.signalProcess('INT') - - def connectionMade(self): - print("connectionMade!") - self.transport.closeStdin() # No more input - - def outReceived(self, data): - print("outReceived! with %d bytes!" % len(data)) - self.onOutReceived(data) - if self.logfile is not None: - self.logfile.write(data) - - def errReceived(self, data): - print("outReceived! with %d bytes!" % len(data)) - self.onErrRecevied(data) - if self.logfile is not None: - self.logfile.write(data) - - def inConnectionLost(self): - print("inConnectionLost! stdin is closed! (we probably did it)") - pass - - def outConnectionLost(self): - print("outConnectionLost! The child closed their stdout!") - pass - - def errConnectionLost(self): - print("errConnectionLost! The child closed their stderr.") - pass - - def processExited(self, reason): - exitCode = reason.value.exitCode - if exitCode is None: - print("Unknown exit") - return - print("processEnded, status", exitCode) - - def processEnded(self, reason): - if self.logfile is not None: - self.logfile.close() - exitCode = reason.value.exitCode - if exitCode is None: - print("Unknown exit") - self.onProcessEnded(1) - return - print("processEnded, status", exitCode) - print("quitting") - self.onProcessEnded(exitCode) - -#------------------------------------------------------------------------------- -# Qt Window class -#------------------------------------------------------------------------------- -class Window(QDialog): - def __init__(self): - super(Window, self).__init__() - # Create objects - self.createDisplayGroupBox() - self.createVNCGroupBox() - self.createBottomLayout() - self.createActions() - self.createTrayIcon() - self.xrandr = XRandR() - # Additional attributes - self.isDisplayCreated = False - self.isVNCRunning = False - self.isQuitProgramPending = False - # Update UI - self.update_ip_address() - # Put togather - mainLayout = QVBoxLayout() - mainLayout.addWidget(self.displayGroupBox) - mainLayout.addWidget(self.VNCGroupBox) - mainLayout.addLayout(self.bottomLayout) - self.setLayout(mainLayout) - # Events - self.trayIcon.activated.connect(self.iconActivated) - self.createDisplayButton.pressed.connect(self.createDisplayPressed) - self.startVNCButton.pressed.connect(self.startVNCPressed) - QApplication.desktop().resized.connect(self.screenChanged) - # QApplication.desktop().resized.connect(self.startVNCPressed) - # QApplication.desktop().screenCountChanged.connect(self.startVNCPressed) - 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): - """Override of setVisible(bool) - - Arguments: - visible {bool} -- true to show, false to hide - """ - self.openAction.setEnabled(self.isMaximized() or not visible) - super(Window, self).setVisible(visible) - - def changeEvent(self, event): - """Override of QWidget::changeEvent() - - Arguments: - event {QEvent} -- QEvent - """ - if event.type() == QEvent.ActivationChange and not self.isActiveWindow(): - self.hide() - - def closeEvent(self, event): - """Override of closeEvent() - - Arguments: - event {QCloseEvent} -- QCloseEvent - """ - if self.trayIcon.isVisible(): - self.hide() - self.showMessage() - event.ignore() - else: - QApplication.instance().quit() - - @pyqtSlot() - def createDisplayPressed(self): - if not self.isDisplayCreated: - # Create virtual screen - self.createDisplayButton.setEnabled(False) - width = self.displayWidthSpinBox.value() - 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() - def startVNCPressed(self): - if not self.isVNCRunning: - self.startVNC() - else: - self.VNCServer.kill() - - @pyqtSlot('QSystemTrayIcon::ActivationReason') - def iconActivated(self, reason): - if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.DoubleClick): - if self.isVisible(): - self.hide() - else: - # move the widget to one of 4 coners of the primary display, - # depending on the current mouse cursor. - screen = QApplication.desktop().screenGeometry() - x_mid = screen.width() / 2 - y_mid = screen.height() / 2 - cursor = QCursor().pos() - x = (screen.width() - self.width()) if (cursor.x() > x_mid) else 0 - y = (screen.height() - self.height()) if (cursor.y() > y_mid) else 0 - self.move(x, y) - self.showNormal() - elif reason == QSystemTrayIcon.MiddleClick: - self.showMessage() - - @pyqtSlot(int) - def screenChanged(self, count): - for i in range(QApplication.desktop().screenCount()): - print(QApplication.desktop().availableGeometry(i)) - - @pyqtSlot() - def showMessage(self): - self.trayIcon.showMessage("VirtScreen is running", - "The program will keep running in the system tray. To \n" - "terminate the program, choose \"Quit\" in the \n" - "context menu of the system tray entry.", - QSystemTrayIcon.MessageIcon(QSystemTrayIcon.Information), - 7 * 1000) + @pyqtProperty(str) + def vncPassword(self): + return self._vncPassword + @vncPassword.setter + def vncPassword(self, vncPassword): + self._vncPassword = vncPassword + print(self._vncPassword) + # Qt Slots @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() - QApplication.instance().quit() - - def createDisplayGroupBox(self): - self.displayGroupBox = QGroupBox("Virtual Display Settings") - - # Resolution Row - resolutionLabel = QLabel("Resolution:") - - self.displayWidthSpinBox = QSpinBox() - self.displayWidthSpinBox.setRange(640, 1920) - self.displayWidthSpinBox.setSuffix("px") - 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(): - if interface == 'lo': - continue - addresses = ifaddresses(interface).get(AF_INET, None) - if addresses is None: - continue - for link in addresses: - if link is not None: - self.VNCIPListWidget.addItem(link['addr']) - - def startVNC(self): - def _onReceived(data): - 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() - 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) + QApplication.instance().quit() #------------------------------------------------------------------------------- # Main Code #------------------------------------------------------------------------------- if __name__ == '__main__': - import sys - + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) app = QApplication(sys.argv) + app.setWindowIcon(QIcon(ICON_PATH)) + # os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" + os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion" + + # Register the Python type. Its URI is 'People', it's v1.0 and the type + # will be called 'Person' in QML. + qmlRegisterType(Backend, 'VirtScreen.Backend', 1, 0, 'Backend') - if not QSystemTrayIcon.isSystemTrayAvailable(): - QMessageBox.critical(None, "VirtScreen", - "I couldn't detect any system tray on this system.") - sys.exit(1) - - if os.environ['XDG_SESSION_TYPE'] == 'wayland': - QMessageBox.critical(None, "VirtScreen", - "Currently Wayland is not supported") - sys.exit(1) - if HOME_PATH is None: - QMessageBox.critical(None, "VirtScreen", - "VirtScreen cannot detect $HOME") - sys.exit(1) - if not os.path.exists(HOME_PATH): - try: - os.makedirs(HOME_PATH) - except: - QMessageBox.critical(None, "VirtScreen", - "VirtScreen cannot create ~/.virtscreen") - sys.exit(1) - - import qt5reactor # pylint: disable=E0401 - qt5reactor.install() - from twisted.internet import utils, reactor # pylint: disable=E0401 - - QApplication.setQuitOnLastWindowClosed(False) - window = Window() - window.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) - time.sleep(2) # Otherwise the trayicon message will be shown in weird position - window.showMessage() + # Create a component factory and load the QML script. + engine = QQmlApplicationEngine() + engine.load(QUrl('main.qml')) + if not engine.rootObjects(): + print("Failed to load qml") + exit(1) sys.exit(app.exec_()) - reactor.run() From 7307a1111c3f9773102be75658dd0aeab35f5f9c Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Sun, 6 May 2018 20:11:30 -0400 Subject: [PATCH 02/23] Backend: Added VNC State Enum --- main.qml | 2 +- virtscreen.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/main.qml b/main.qml index 4a93645..9d42d53 100644 --- a/main.qml +++ b/main.qml @@ -195,7 +195,7 @@ ApplicationWindow { RowLayout { anchors.margins: spacing Label { - text: "VNC Server Waiting." + text: backend.vncState } Item { Layout.fillWidth: true } CheckBox { diff --git a/virtscreen.py b/virtscreen.py index f9a6abd..8e1544b 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import sys, os +from enum import Enum from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QObject, QUrl, Qt @@ -19,10 +20,18 @@ ICON_TABLET_ON_PATH = PROGRAM_PATH + "/icon/icon_tablet_on.png" #------------------------------------------------------------------------------- # QML Backend class #------------------------------------------------------------------------------- +class VNCState(Enum): + """ Enum to indicate a state of the VNC server """ + OFF = "Off" + WAITING = "Waiting" + CONNECTED = "Connected" + class Backend(QObject): + """ Backend class for QML frontend """ + # Signals width_changed = pyqtSignal(int) virtScreenChanged = pyqtSignal(bool) - vncChanged = pyqtSignal(bool) + vncChanged = pyqtSignal(str) def __init__(self, parent=None): super(Backend, self).__init__(parent) @@ -35,8 +44,9 @@ class Backend(QObject): # VNC server properties self._vncPort = 5900 self._vncPassword = "" - self._vncState = False - + self._vncState = VNCState.OFF + + # Qt properties @pyqtProperty(int, notify=width_changed) def width(self): return self._width @@ -85,6 +95,10 @@ class Backend(QObject): self._vncPassword = vncPassword print(self._vncPassword) + @pyqtProperty(str) + def vncState(self): + return self._vncState.value + # Qt Slots @pyqtSlot() def quitProgram(self): @@ -94,7 +108,6 @@ class Backend(QObject): # Main Code #------------------------------------------------------------------------------- if __name__ == '__main__': - import sys QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) app = QApplication(sys.argv) app.setWindowIcon(QIcon(ICON_PATH)) From 9fe8e36d22ad03629144e9db44c608b0718426db Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 7 May 2018 11:36:12 -0400 Subject: [PATCH 03/23] QML: Backend service is fully merged with the existing code --- main.qml | 16 ++- virtscreen.py | 320 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 323 insertions(+), 13 deletions(-) diff --git a/main.qml b/main.qml index 9d42d53..ed51ae0 100644 --- a/main.qml +++ b/main.qml @@ -134,6 +134,13 @@ ApplicationWindow { Layout.fillWidth: true // Material.background: Material.Teal // Material.foreground: Material.Grey + onClicked: { + if (!backend.virtScreenCreated) { + backend.createVirtScreen(); + } else { + backend.deleteVirtScreen(); + } + } } } @@ -187,6 +194,13 @@ ApplicationWindow { Layout.fillWidth: true // Material.background: Material.Teal // Material.foreground: Material.Grey + onClicked: { + if (backend.vncState == 'Off') { + backend.startVNC() + } else { + backend.stopVNC() + } + } } } } @@ -229,7 +243,7 @@ ApplicationWindow { menu: Labs.Menu { Labs.MenuItem { text: qsTr("&Quit") - onTriggered: Qt.quit() + onTriggered: backend.quitProgram() } } } diff --git a/virtscreen.py b/virtscreen.py index 8e1544b..7385c00 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import sys, os +import sys, os, subprocess, signal, re, atexit from enum import Enum from PyQt5.QtWidgets import QApplication @@ -9,14 +9,213 @@ from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine +from twisted.internet import protocol, error +from netifaces import interfaces, ifaddresses, AF_INET + #------------------------------------------------------------------------------- # file path definitions #------------------------------------------------------------------------------- +HOME_PATH = os.getenv('HOME', None) +if HOME_PATH is not None: + HOME_PATH = HOME_PATH + "/.virtscreen" +X11VNC_LOG_PATH = HOME_PATH + "/x11vnc_log.txt" +X11VNC_PASSWORD_PATH = HOME_PATH + "/x11vnc_passwd" +CONFIG_PATH = HOME_PATH + "/config" + PROGRAM_PATH = "." ICON_PATH = PROGRAM_PATH + "/icon/icon.png" ICON_TABLET_OFF_PATH = PROGRAM_PATH + "/icon/icon_tablet_off.png" ICON_TABLET_ON_PATH = PROGRAM_PATH + "/icon/icon_tablet_on.png" +#------------------------------------------------------------------------------- +# Subprocess wrapper +#------------------------------------------------------------------------------- +class SubprocessWrapper: + def __init__(self, stdout:str=os.devnull, stderr:str=os.devnull): + self.stdout: str = stdout + self.stderr: str = stderr + + def call(self, arg) -> None: + with open(os.devnull, "w") as f: + subprocess.call(arg.split(), stdout=f, stderr=f) + + def check_call(self, arg) -> None: + with open(os.devnull, "w") as f: + subprocess.check_call(arg.split(), stdout=f, stderr=f) + + def run(self, arg: str) -> str: + return subprocess.run(arg.split(), stdout=subprocess.PIPE).stdout.decode('utf-8') + +#------------------------------------------------------------------------------- +# Display properties +#------------------------------------------------------------------------------- +class DisplayProperty: + def __init__(self): + self.name: str + self.width: int + self.height: int + self.x_offset: int + self.y_offset: int + +#------------------------------------------------------------------------------- +# Screen adjustment class +#------------------------------------------------------------------------------- +class XRandR(SubprocessWrapper): + def __init__(self): + super(XRandR, self).__init__() + self.mode_name: str + self.scrren_suffix = "_virt" + # Thoese will be created in set_virtual_screen() + self.virt = DisplayProperty() + self.virt.name = "VIRTUAL1" + # Primary display + self.primary = DisplayProperty() + self._update_primary_screen() + + def _add_screen_mode(self) -> None: + args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}" + try: + self.check_call(args_addmode) + except subprocess.CalledProcessError: + # When failed create mode and then add again + output = self.run(f"cvt {self.virt.width} {self.virt.height}") + mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1) + # Create new screen mode + self.check_call(f"xrandr --newmode {self.mode_name} {mode}") + # Add mode again + self.check_call(args_addmode) + # After adding mode the program should delete the mode automatically on exit + atexit.register(self.delete_virtual_screen) + for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]: + signal.signal(sig, self._signal_handler) + + def _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: + self.delete_virtual_screen() + os._exit(0) + + def get_virtual_screen(self) -> DisplayProperty: + self._update_virtual_screen() + return self.virt + + def set_virtual_screen(self, width, height, portrait=False, hidpi=False): + 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.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("sleep 5") + self.check_call(f"xrandr --output {self.virt.name} --auto") + self._update_primary_screen() + self._update_virtual_screen() + + def delete_virtual_screen(self) -> None: + try: + self.virt.name + self.mode_name + except AttributeError: + return + self.call(f"xrandr --output {self.virt.name} --off") + self.call(f"xrandr --delmode {self.virt.name} {self.mode_name}") + +#------------------------------------------------------------------------------- +# Twisted class +#------------------------------------------------------------------------------- +class ProcessProtocol(protocol.ProcessProtocol): + def __init__(self, onConnected, onOutReceived, onErrRecevied, onProcessEnded, logfile=None): + self.onConnected = onConnected + self.onOutReceived = onOutReceived + self.onErrRecevied = onErrRecevied + self.onProcessEnded = onProcessEnded + self.logfile = logfile + + def run(self, arg: str): + """Spawn a process + + Arguments: + arg {str} -- arguments in string + """ + + args = arg.split() + reactor.spawnProcess(self, args[0], args=args, env=os.environ) + + def kill(self): + """Kill a spawned process + """ + self.transport.signalProcess('INT') + + def connectionMade(self): + print("connectionMade!") + self.onConnected() + self.transport.closeStdin() # No more input + + def outReceived(self, data): + print("outReceived! with %d bytes!" % len(data)) + self.onOutReceived(data) + if self.logfile is not None: + self.logfile.write(data) + + def errReceived(self, data): + print("outReceived! with %d bytes!" % len(data)) + self.onErrRecevied(data) + if self.logfile is not None: + self.logfile.write(data) + + def inConnectionLost(self): + print("inConnectionLost! stdin is closed! (we probably did it)") + pass + + def outConnectionLost(self): + print("outConnectionLost! The child closed their stdout!") + pass + + def errConnectionLost(self): + print("errConnectionLost! The child closed their stderr.") + pass + + def processExited(self, reason): + exitCode = reason.value.exitCode + if exitCode is None: + print("Unknown exit") + return + print("processEnded, status", exitCode) + + def processEnded(self, reason): + if self.logfile is not None: + self.logfile.close() + exitCode = reason.value.exitCode + if exitCode is None: + print("Unknown exit") + self.onProcessEnded(1) + return + print("processEnded, status", exitCode) + print("quitting") + self.onProcessEnded(exitCode) + #------------------------------------------------------------------------------- # QML Backend class #------------------------------------------------------------------------------- @@ -29,31 +228,31 @@ class VNCState(Enum): class Backend(QObject): """ Backend class for QML frontend """ # Signals - width_changed = pyqtSignal(int) virtScreenChanged = pyqtSignal(bool) - vncChanged = pyqtSignal(str) + vncStateChanged = pyqtSignal(str) def __init__(self, parent=None): super(Backend, self).__init__(parent) # Virtual screen properties self._width = 1368 self._height = 1024 - self._portrait = True + self._portrait = False self._hidpi = False self._virtScreenCreated = False # VNC server properties self._vncPort = 5900 self._vncPassword = "" self._vncState = VNCState.OFF + # objects + self.xrandr = XRandR() # Qt properties - @pyqtProperty(int, notify=width_changed) + @pyqtProperty(int) def width(self): return self._width @width.setter def width(self, width): self._width = width - self.width_changed.emit(self._width) @pyqtProperty(int) def height(self): @@ -77,15 +276,19 @@ class Backend(QObject): self._hidpi = hidpi @pyqtProperty(bool) - def virtScreenCreated(self): + def virtScreenCreated(self, notify=virtScreenChanged): return self._virtScreenCreated + @virtScreenCreated.setter + def virtScreenCreated(self, value): + self._virtScreenCreated = value + self.virtScreenChanged.emit(value) @pyqtProperty(int) def vncPort(self): return self._vncPort @vncPort.setter - def vncPort(self, vncPort): - self._vncPort = vncPort + def vncPort(self, port): + self._vncPort = port @pyqtProperty(str) def vncPassword(self): @@ -96,10 +299,74 @@ class Backend(QObject): print(self._vncPassword) @pyqtProperty(str) - def vncState(self): + def vncState(self, notify=vncStateChanged): return self._vncState.value + @vncState.setter + def vncState(self, state): + self._vncState = state + self.vncStateChanged.emit(self._vncState.value) # Qt Slots + @pyqtSlot() + def createVirtScreen(self): + print("Creating a Virtual Screen...") + self.xrandr.set_virtual_screen(self.width, self.height, self.portrait, self.hidpi) + self.xrandr.create_virtual_screen() + self.virtScreenCreated = True + + # Qt Slots + @pyqtSlot() + def deleteVirtScreen(self): + print("Deleting the Virtual Screen...") + self.xrandr.delete_virtual_screen() + self.virtScreenCreated = False + + @pyqtSlot() + def startVNC(self): + # Check if a virtual screen created + if not self.virtScreenCreated: + print("Virtual Screen not crated.") + return + # define callbacks + def _onConnected(): + print("VNC started.") + self.vncState = VNCState.WAITING + def _onReceived(data): + data = data.decode("utf-8") + for line in data.splitlines(): + # TODO: Update state of the server + pass + def _onEnded(exitCode): + print("VNC Exited.") + self.vncState = VNCState.OFF + # 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) + + @pyqtSlot() + def stopVNC(self): + print(self.vncState) + if self.vncState in (VNCState.WAITING.value, VNCState.CONNECTED.value): + self.vncServer.kill() + else: + print("stopVNC called while it is not running") + @pyqtSlot() def quitProgram(self): QApplication.instance().quit() @@ -110,9 +377,37 @@ class Backend(QObject): if __name__ == '__main__': QApplication.setAttribute(Qt.AA_EnableHighDpiScaling) app = QApplication(sys.argv) + + from PyQt5.QtWidgets import QSystemTrayIcon, QMessageBox + + if not QSystemTrayIcon.isSystemTrayAvailable(): + QMessageBox.critical(None, "VirtScreen", + "I couldn't detect any system tray on this system.") + sys.exit(1) + + if os.environ['XDG_SESSION_TYPE'] == 'wayland': + QMessageBox.critical(None, "VirtScreen", + "Currently Wayland is not supported") + sys.exit(1) + if HOME_PATH is None: + QMessageBox.critical(None, "VirtScreen", + "VirtScreen cannot detect $HOME") + sys.exit(1) + if not os.path.exists(HOME_PATH): + try: + os.makedirs(HOME_PATH) + except: + QMessageBox.critical(None, "VirtScreen", + "VirtScreen cannot create ~/.virtscreen") + sys.exit(1) + + import qt5reactor # pylint: disable=E0401 + qt5reactor.install() + from twisted.internet import utils, reactor # pylint: disable=E0401 + app.setWindowIcon(QIcon(ICON_PATH)) - # os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" - os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion" + os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" + # os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion" # Register the Python type. Its URI is 'People', it's v1.0 and the type # will be called 'Person' in QML. @@ -125,3 +420,4 @@ if __name__ == '__main__': print("Failed to load qml") exit(1) sys.exit(app.exec_()) + reactor.run() From 0975e4e8ac71d47912b4cf15ae3f2f606ea2e454 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 7 May 2018 16:15:41 -0400 Subject: [PATCH 04/23] QML: auto positioning window --- main.qml | 23 ++++++++++++++++++++++- virtscreen.py | 37 +++++++++++++++++++++++++++++++++---- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/main.qml b/main.qml index ed51ae0..e4c3485 100644 --- a/main.qml +++ b/main.qml @@ -2,6 +2,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 // import QtQuick.Controls.Material 2.3 import QtQuick.Layouts 1.3 +import QtQuick.Window 2.2 import Qt.labs.platform 1.0 as Labs @@ -10,7 +11,8 @@ import VirtScreen.Backend 1.0 ApplicationWindow { id: window - visible: true + visible: false + flags: Qt.FramelessWindowHint title: "Basic layouts" // Material.theme: Material.Light @@ -20,6 +22,14 @@ ApplicationWindow { width: 380 height: 600 + // hide screen when loosing focus + onActiveFocusItemChanged: { + if (!activeFocusItem) { + this.hide(); + } + } + + // virtscreen.py hackend. Backend { id: backend } @@ -235,6 +245,17 @@ ApplicationWindow { } onActivated: { + if (window.visible) { + window.hide(); + return; + } + // Move window to the corner of the primary display + var width = backend.primaryDisplayWidth; + var height = backend.primaryDisplayHeight; + var x_mid = width / 2; + var y_mid = height / 2; + window.x = (backend.cursor_x > x_mid)? width - window.width : 0; + window.y = (backend.cursor_y > y_mid)? height - window.height : 0; window.show() window.raise() window.requestActivate() diff --git a/virtscreen.py b/virtscreen.py index 7385c00..fde4bbd 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -6,7 +6,7 @@ from enum import Enum from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QObject, QUrl, Qt from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QIcon, QCursor from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine from twisted.internet import protocol, error @@ -243,6 +243,11 @@ class Backend(QObject): self._vncPort = 5900 self._vncPassword = "" self._vncState = VNCState.OFF + # Primary screen and mouse posistion + self._cursor_x: int + self._cursor_y: int + self._primaryDisplayWidth: int + self._primaryDisplayHeight: int # objects self.xrandr = XRandR() @@ -305,7 +310,31 @@ class Backend(QObject): def vncState(self, state): self._vncState = state self.vncStateChanged.emit(self._vncState.value) - + + @pyqtProperty(int) + def cursor_x(self): + cursor = QCursor().pos() + self._cursor_x = cursor.x() + return self._cursor_x + + @pyqtProperty(int) + def cursor_y(self): + cursor = QCursor().pos() + self._cursor_y = cursor.y() + return self._cursor_y + + @pyqtProperty(int) + def primaryDisplayWidth(self): + screen = QApplication.desktop().screenGeometry() + self._primaryDisplayWidth = screen.width() + return self._primaryDisplayWidth + + @pyqtProperty(int) + def primaryDisplayHeight(self): + screen = QApplication.desktop().screenGeometry() + self._primaryDisplayHeight = screen.height() + return self._primaryDisplayHeight + # Qt Slots @pyqtSlot() def createVirtScreen(self): @@ -417,7 +446,7 @@ if __name__ == '__main__': engine = QQmlApplicationEngine() engine.load(QUrl('main.qml')) if not engine.rootObjects(): - print("Failed to load qml") - exit(1) + QMessageBox.critical(None, "VirtScreen", "Failed to load qml") + sys.exit(1) sys.exit(app.exec_()) reactor.run() From b8d9a7dc0798723bfefb3342d24a7c63176e9020 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 7 May 2018 16:53:43 -0400 Subject: [PATCH 05/23] QML: graceful shutdown process --- virtscreen.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/virtscreen.py b/virtscreen.py index fde4bbd..965759b 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -import sys, os, subprocess, signal, re, atexit +import sys, os, subprocess, signal, re, atexit, time from enum import Enum from PyQt5.QtWidgets import QApplication @@ -141,6 +141,7 @@ class XRandR(SubprocessWrapper): return self.call(f"xrandr --output {self.virt.name} --off") self.call(f"xrandr --delmode {self.virt.name} {self.mode_name}") + atexit.unregister(self.delete_virtual_screen) #------------------------------------------------------------------------------- # Twisted class @@ -174,13 +175,13 @@ class ProcessProtocol(protocol.ProcessProtocol): self.transport.closeStdin() # No more input def outReceived(self, data): - print("outReceived! with %d bytes!" % len(data)) + # print("outReceived! with %d bytes!" % len(data)) self.onOutReceived(data) if self.logfile is not None: self.logfile.write(data) def errReceived(self, data): - print("outReceived! with %d bytes!" % len(data)) + # print("errReceived! with %d bytes!" % len(data)) self.onErrRecevied(data) if self.logfile is not None: self.logfile.write(data) @@ -347,6 +348,9 @@ class Backend(QObject): @pyqtSlot() def deleteVirtScreen(self): print("Deleting the Virtual Screen...") + if self.vncState != VNCState.OFF.value: + print("Turn off the VNC server first") + return self.xrandr.delete_virtual_screen() self.virtScreenCreated = False @@ -368,6 +372,7 @@ class Backend(QObject): def _onEnded(exitCode): print("VNC Exited.") self.vncState = VNCState.OFF + atexit.unregister(self.stopVNC) # Set password password = False if self.vncPassword: @@ -387,10 +392,15 @@ class Backend(QObject): if password: arg += f" -rfbauth {X11VNC_PASSWORD_PATH}" self.vncServer.run(arg) + # auto stop on exit + atexit.register(self.stopVNC, force=True) @pyqtSlot() - def stopVNC(self): - print(self.vncState) + 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 (VNCState.WAITING.value, VNCState.CONNECTED.value): self.vncServer.kill() else: From d1775042b16a76313d1eb4a678bf5517254c7132 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 7 May 2018 21:57:57 -0400 Subject: [PATCH 06/23] QML: Added busyindicator popup when creating a screen --- main.qml | 39 ++++++++++++++++++++++++++++++++++++++- virtscreen.py | 1 + 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/main.qml b/main.qml index e4c3485..4243108 100644 --- a/main.qml +++ b/main.qml @@ -41,6 +41,9 @@ ApplicationWindow { timer.interval = delayTime; timer.repeat = false; timer.triggered.connect(cb); + timer.triggered.connect(function() { + timer.triggered.disconnect(cb); + }); timer.start(); } } @@ -140,16 +143,50 @@ ApplicationWindow { } Button { - text: "Create a Virtual Display" + id: virtScreenButton + text: "Enable Virtual Screen" Layout.fillWidth: true // Material.background: Material.Teal // Material.foreground: Material.Grey + + 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: { + virtScreenButton.enabled = false; + busyDialog.open(); + // Give a very short delay to show busyDialog. + timer.setTimeout (function() { if (!backend.virtScreenCreated) { backend.createVirtScreen(); } else { backend.deleteVirtScreen(); } + }, 200); + } + + Component.onCompleted: { + backend.virtScreenChanged.connect(function(created) { + busyDialog.close(); + virtScreenButton.enabled = true; + if (created) { + virtScreenButton.text = "Disable Virtual Screen" + } else { + virtScreenButton.text = "Enable Virtual Screen" + } + }); } } } diff --git a/virtscreen.py b/virtscreen.py index 965759b..4297b22 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -350,6 +350,7 @@ class Backend(QObject): print("Deleting the Virtual Screen...") if self.vncState != VNCState.OFF.value: print("Turn off the VNC server first") + self.virtScreenCreated = True return self.xrandr.delete_virtual_screen() self.virtScreenCreated = False From a07fecd5740a1ea9c4ab08779733bc1dd24e82c6 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 7 May 2018 23:34:11 -0400 Subject: [PATCH 07/23] QML: VNC Server state parsing and updating --- main.qml | 52 ++++++++++++++++++++++++++++++++++++++------------- virtscreen.py | 27 +++++++++++++------------- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/main.qml b/main.qml index 4243108..7787ee4 100644 --- a/main.qml +++ b/main.qml @@ -1,6 +1,6 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -// import QtQuick.Controls.Material 2.3 +import QtQuick.Controls.Material 2.3 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 @@ -15,8 +15,8 @@ ApplicationWindow { flags: Qt.FramelessWindowHint title: "Basic layouts" - // Material.theme: Material.Light - // Material.accent: Material.Teal + Material.theme: Material.Light + Material.accent: Material.Teal property int margin: 11 width: 380 @@ -29,7 +29,7 @@ ApplicationWindow { } } - // virtscreen.py hackend. + // virtscreen.py backend. Backend { id: backend } @@ -169,16 +169,16 @@ ApplicationWindow { busyDialog.open(); // Give a very short delay to show busyDialog. timer.setTimeout (function() { - if (!backend.virtScreenCreated) { - backend.createVirtScreen(); - } else { - backend.deleteVirtScreen(); - } + if (!backend.virtScreenCreated) { + backend.createVirtScreen(); + } else { + backend.deleteVirtScreen(); + } }, 200); } Component.onCompleted: { - backend.virtScreenChanged.connect(function(created) { + backend.onVirtScreenCreatedChanged.connect(function(created) { busyDialog.close(); virtScreenButton.enabled = true; if (created) { @@ -237,7 +237,9 @@ ApplicationWindow { } Button { + id: vncButton text: "Start VNC Server" + enabled: false Layout.fillWidth: true // Material.background: Material.Teal // Material.foreground: Material.Grey @@ -248,6 +250,23 @@ ApplicationWindow { backend.stopVNC() } } + + Component.onCompleted: { + backend.onVncStateChanged.connect(function(state) { + if (state == "Off") { + vncButton.text = "Start VNC Server"; + } else { + vncButton.text = "Stop VNC Server"; + } + }); + backend.onVirtScreenCreatedChanged.connect(function(created) { + if (created) { + vncButton.enabled = true; + } else { + vncButton.enabled = false; + } + }); + } } } } @@ -256,6 +275,7 @@ ApplicationWindow { RowLayout { anchors.margins: spacing Label { + id: vncStateLabel text: backend.vncState } Item { Layout.fillWidth: true } @@ -265,6 +285,12 @@ ApplicationWindow { checked: true } } + + Component.onCompleted: { + backend.onVncStateChanged.connect(function(state) { + vncStateLabel.text = state; + }); + } } // Sytray Icon @@ -293,9 +319,9 @@ ApplicationWindow { var y_mid = height / 2; window.x = (backend.cursor_x > x_mid)? width - window.width : 0; window.y = (backend.cursor_y > y_mid)? height - window.height : 0; - window.show() - window.raise() - window.requestActivate() + window.show(); + window.raise(); + window.requestActivate(); } menu: Labs.Menu { diff --git a/virtscreen.py b/virtscreen.py index 4297b22..ef020f7 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -229,8 +229,8 @@ class VNCState(Enum): class Backend(QObject): """ Backend class for QML frontend """ # Signals - virtScreenChanged = pyqtSignal(bool) - vncStateChanged = pyqtSignal(str) + onVirtScreenCreatedChanged = pyqtSignal(bool) + onVncStateChanged = pyqtSignal(str) def __init__(self, parent=None): super(Backend, self).__init__(parent) @@ -281,13 +281,13 @@ class Backend(QObject): def hidpi(self, hidpi): self._hidpi = hidpi - @pyqtProperty(bool) - def virtScreenCreated(self, notify=virtScreenChanged): + @pyqtProperty(bool, notify=onVirtScreenCreatedChanged) + def virtScreenCreated(self): return self._virtScreenCreated @virtScreenCreated.setter def virtScreenCreated(self, value): self._virtScreenCreated = value - self.virtScreenChanged.emit(value) + self.onVirtScreenCreatedChanged.emit(value) @pyqtProperty(int) def vncPort(self): @@ -302,15 +302,14 @@ class Backend(QObject): @vncPassword.setter def vncPassword(self, vncPassword): self._vncPassword = vncPassword - print(self._vncPassword) - @pyqtProperty(str) - def vncState(self, notify=vncStateChanged): + @pyqtProperty(str, notify=onVncStateChanged) + def vncState(self): return self._vncState.value @vncState.setter def vncState(self, state): self._vncState = state - self.vncStateChanged.emit(self._vncState.value) + self.onVncStateChanged.emit(self._vncState.value) @pyqtProperty(int) def cursor_x(self): @@ -361,15 +360,17 @@ class Backend(QObject): if not self.virtScreenCreated: print("Virtual Screen not crated.") 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 = VNCState.WAITING def _onReceived(data): data = data.decode("utf-8") - for line in data.splitlines(): - # TODO: Update state of the server - pass + if (self._vncState is not VNCState.CONNECTED) and re_connection.search(data): + print("VNC connected.") + self.vncState = VNCState.CONNECTED def _onEnded(exitCode): print("VNC Exited.") self.vncState = VNCState.OFF @@ -402,7 +403,7 @@ class Backend(QObject): # Usually called from atexit(). self.vncServer.kill() time.sleep(2) # Make sure X11VNC shutdown before execute next atexit. - if self.vncState in (VNCState.WAITING.value, VNCState.CONNECTED.value): + if self._vncState in (VNCState.WAITING, VNCState.CONNECTED): self.vncServer.kill() else: print("stopVNC called while it is not running") From c6a3d8f73de2c68f92f0dd85329a1cd874bd9ae0 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Mon, 7 May 2018 23:45:17 -0400 Subject: [PATCH 08/23] QML: Corrected systrayicon message --- main.qml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main.qml b/main.qml index 7787ee4..8899f0a 100644 --- a/main.qml +++ b/main.qml @@ -303,8 +303,11 @@ ApplicationWindow { Component.onCompleted: { // without delay, the message appears in a wierd place timer.setTimeout (function() { - showMessage("Message title", "Something important came up. Click this to know more."); - }, 1000); + showMessage("VirtScreen is running", + "The program will keep running in the system tray.\n" + + "To terminate the program, choose \"Quit\" in the \n" + + "context menu of the system tray entry."); + }, 7000); } onActivated: { From aaf3b28206c1de028ec5088897d57c659391fc0e Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 8 May 2018 11:44:06 -0400 Subject: [PATCH 09/23] XRandR: Fixed issue with parsing primary screen in some hardware --- virtscreen.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/virtscreen.py b/virtscreen.py index ef020f7..a9272a5 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -91,12 +91,12 @@ class XRandR(SubprocessWrapper): 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) + match = re.search(r"^(\S*)\s+(connected|disconnected)\s+primary\s+(\d+)x(\d+)\+(\d+)\+(\d+)\s+.*$", 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)) + self.primary.width = int(match.group(3)) + self.primary.height = int(match.group(4)) + self.primary.x_offset = int(match.group(5)) + self.primary.y_offset = int(match.group(6)) def _update_virtual_screen(self) -> None: output = self.run("xrandr") From b0c07d55be7519757f24cc638fc729ddd46f3695 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Tue, 8 May 2018 18:45:51 -0400 Subject: [PATCH 10/23] XRandR: Full parsing of xrandr display information --- virtscreen.py | 56 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/virtscreen.py b/virtscreen.py index a9272a5..d6d572b 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -2,6 +2,7 @@ import sys, os, subprocess, signal, re, atexit, time from enum import Enum +from typing import List from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QObject, QUrl, Qt @@ -51,25 +52,43 @@ class SubprocessWrapper: #------------------------------------------------------------------------------- class DisplayProperty: def __init__(self): - self.name: str + self.name: str + self.primary: bool + self.connected: 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" + if self.primary: + ret += " primary" + ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}" + else: + ret += " disconnected" + return ret #------------------------------------------------------------------------------- # Screen adjustment class #------------------------------------------------------------------------------- class XRandR(SubprocessWrapper): + DEFAULT_VIRT_SCREEN = "VIRTUAL1" + VIRT_SCREEN_SUFFIX = "_virt" + def __init__(self): super(XRandR, self).__init__() self.mode_name: str - self.scrren_suffix = "_virt" + self.scrren_suffix = self.VIRT_SCREEN_SUFFIX + self.screens: List[DisplayProperty] = [] # Thoese will be created in set_virtual_screen() self.virt = DisplayProperty() - self.virt.name = "VIRTUAL1" + self.virt.name = self.DEFAULT_VIRT_SCREEN # Primary display - self.primary = DisplayProperty() + self.primary: DisplayProperty() + self.primary_idx: int + self.virtual_idx: int self._update_primary_screen() def _add_screen_mode(self) -> None: @@ -91,12 +110,29 @@ class XRandR(SubprocessWrapper): def _update_primary_screen(self) -> None: output = self.run("xrandr") - match = re.search(r"^(\S*)\s+(connected|disconnected)\s+primary\s+(\d+)x(\d+)\+(\d+)\+(\d+)\s+.*$", output, re.M) - self.primary.name = match.group(1) - self.primary.width = int(match.group(3)) - self.primary.height = int(match.group(4)) - self.primary.x_offset = int(match.group(5)) - self.primary.y_offset = int(match.group(6)) + self.screens = [] + 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 screen.name == self.DEFAULT_VIRT_SCREEN: + self.virtual_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 + self.screens.append(screen) + if not screen.connected: + 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) + self.primary = self.screens[self.primary_idx] def _update_virtual_screen(self) -> None: output = self.run("xrandr") From 66fcf5f173d82698f4a1aa2c2767d5271e4f4a39 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 08:27:29 -0400 Subject: [PATCH 11/23] XRandR: parsing active monitor --- virtscreen.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/virtscreen.py b/virtscreen.py index d6d572b..89c3edd 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -55,6 +55,7 @@ class DisplayProperty: self.name: str self.primary: bool self.connected: bool + self.active: bool self.width: int self.height: int self.x_offset: int @@ -65,7 +66,10 @@ class DisplayProperty: ret += " connected" if self.primary: ret += " primary" - ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}" + if self.active: + ret += f" {self.width}x{self.height}+{self.x_offset}+{self.y_offset}" + else: + ret += " not active" else: ret += " disconnected" return ret @@ -82,13 +86,13 @@ class XRandR(SubprocessWrapper): self.mode_name: str self.scrren_suffix = self.VIRT_SCREEN_SUFFIX self.screens: List[DisplayProperty] = [] + self.primary_idx: int = None + self.virtual_idx: int = None # Thoese will be created in set_virtual_screen() self.virt = DisplayProperty() self.virt.name = self.DEFAULT_VIRT_SCREEN # Primary display self.primary: DisplayProperty() - self.primary_idx: int - self.virtual_idx: int self._update_primary_screen() def _add_screen_mode(self) -> None: @@ -111,6 +115,8 @@ class XRandR(SubprocessWrapper): def _update_primary_screen(self) -> None: output = self.run("xrandr") self.screens = [] + self.primary_idx = None + self.virtual_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)): @@ -122,8 +128,9 @@ class XRandR(SubprocessWrapper): 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.connected: + if not screen.connected or not screen.active: continue screen.width = int(match.group(6)) screen.height = int(match.group(7)) From 1576ffee49c4b034f28646d248426e452a50d40f Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 08:32:07 -0400 Subject: [PATCH 12/23] QML: Show window always on the right --- main.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.qml b/main.qml index 8899f0a..3541050 100644 --- a/main.qml +++ b/main.qml @@ -320,7 +320,7 @@ ApplicationWindow { var height = backend.primaryDisplayHeight; var x_mid = width / 2; var y_mid = height / 2; - window.x = (backend.cursor_x > x_mid)? width - window.width : 0; + window.x = width - window.width; //(backend.cursor_x > x_mid)? width - window.width : 0; window.y = (backend.cursor_y > y_mid)? height - window.height : 0; window.show(); window.raise(); From 822891396d67c6648822e21e759028c0aff70210 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 11:37:02 -0400 Subject: [PATCH 13/23] QML: Preven closing the window right after clicking systrayicon --- main.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/main.qml b/main.qml index 3541050..fa4a66d 100644 --- a/main.qml +++ b/main.qml @@ -24,7 +24,7 @@ ApplicationWindow { // hide screen when loosing focus onActiveFocusItemChanged: { - if (!activeFocusItem) { + if ((!activeFocusItem) && (!sysTrayIcon.clicked)) { this.hide(); } } @@ -298,6 +298,7 @@ ApplicationWindow { id: sysTrayIcon iconSource: "icon/icon.png" visible: true + property bool clicked: false onMessageClicked: console.log("Message clicked") Component.onCompleted: { @@ -310,11 +311,13 @@ ApplicationWindow { }, 7000); } - onActivated: { + onActivated: function(reason) { + console.log(reason); if (window.visible) { window.hide(); return; } + sysTrayIcon.clicked = true; // Move window to the corner of the primary display var width = backend.primaryDisplayWidth; var height = backend.primaryDisplayHeight; @@ -325,6 +328,9 @@ ApplicationWindow { window.show(); window.raise(); window.requestActivate(); + timer.setTimeout (function() { + sysTrayIcon.clicked = false; + }, 200); } menu: Labs.Menu { From 599beb255713a34fc3bbf0324163c6cd6d68c1ed Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 12:54:21 -0400 Subject: [PATCH 14/23] XRandR: code cleanup --- virtscreen.py | 96 ++++++++++++++++++++++++--------------------------- 1 file changed, 45 insertions(+), 51 deletions(-) diff --git a/virtscreen.py b/virtscreen.py index 89c3edd..962cc48 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -84,46 +84,28 @@ class XRandR(SubprocessWrapper): def __init__(self): super(XRandR, self).__init__() self.mode_name: str - self.scrren_suffix = self.VIRT_SCREEN_SUFFIX self.screens: List[DisplayProperty] = [] + self.virt: DisplayProperty() = None + self.primary: DisplayProperty() = None + self.virt_idx: int = None self.primary_idx: int = None - self.virtual_idx: int = None - # Thoese will be created in set_virtual_screen() - self.virt = DisplayProperty() - self.virt.name = self.DEFAULT_VIRT_SCREEN # Primary display - self.primary: DisplayProperty() - self._update_primary_screen() + self._update_screens() - def _add_screen_mode(self) -> None: - args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}" - try: - self.check_call(args_addmode) - except subprocess.CalledProcessError: - # When failed create mode and then add again - output = self.run(f"cvt {self.virt.width} {self.virt.height}") - mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1) - # Create new screen mode - self.check_call(f"xrandr --newmode {self.mode_name} {mode}") - # Add mode again - self.check_call(args_addmode) - # After adding mode the program should delete the mode automatically on exit - atexit.register(self.delete_virtual_screen) - for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]: - signal.signal(sig, self._signal_handler) - - def _update_primary_screen(self) -> None: + def _update_screens(self) -> None: output = self.run("xrandr") + self.primary = None + self.virt = None self.screens = [] + self.virt_idx = None self.primary_idx = None - self.virtual_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 screen.name == self.DEFAULT_VIRT_SCREEN: - self.virtual_idx = idx + self.virt_idx = idx screen.primary = True if match.group(4) else False if screen.primary: self.primary_idx = idx @@ -139,25 +121,13 @@ class XRandR(SubprocessWrapper): 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 _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: - self.delete_virtual_screen() - os._exit(0) - - def get_virtual_screen(self) -> DisplayProperty: - self._update_virtual_screen() - return self.virt - - def set_virtual_screen(self, width, height, portrait=False, hidpi=False): + 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: @@ -166,15 +136,39 @@ class XRandR(SubprocessWrapper): 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 + self.mode_name = str(self.virt.width) + "x" + str(self.virt.height) + self.VIRT_SCREEN_SUFFIX + # Then create using xrandr command + args_addmode = f"xrandr --addmode {self.virt.name} {self.mode_name}" + try: + self.check_call(args_addmode) + except subprocess.CalledProcessError: + # When failed create mode and then add again + output = self.run(f"cvt {self.virt.width} {self.virt.height}") + mode = re.search(r"^.*Modeline\s*\".*\"\s*(.*)$", output, re.M).group(1) + # Create new screen mode + self.check_call(f"xrandr --newmode {self.mode_name} {mode}") + # Add mode again + self.check_call(args_addmode) + # After adding mode the program should delete the mode automatically on exit + atexit.register(self.delete_virtual_screen) + for sig in [signal.SIGTERM, signal.SIGHUP, signal.SIGQUIT]: + signal.signal(sig, self._signal_handler) + + def _signal_handler(self, signum=None, frame=None) -> None: + self.delete_virtual_screen() + os._exit(0) + + def get_virtual_screen(self) -> DisplayProperty: + self._update_screens() + return self.virt - def create_virtual_screen(self) -> None: - self._add_screen_mode() + def create_virtual_screen(self, width, height, portrait=False, hidpi=False) -> None: + print("creating: ", self.virt) + self._add_screen_mode(width, height, portrait, hidpi) self.check_call(f"xrandr --output {self.virt.name} --mode {self.mode_name}") self.check_call("sleep 5") self.check_call(f"xrandr --output {self.virt.name} --auto") - self._update_primary_screen() - self._update_virtual_screen() + self._update_screens() def delete_virtual_screen(self) -> None: try: @@ -185,6 +179,7 @@ class XRandR(SubprocessWrapper): self.call(f"xrandr --output {self.virt.name} --off") self.call(f"xrandr --delmode {self.virt.name} {self.mode_name}") atexit.unregister(self.delete_virtual_screen) + self._update_screens() #------------------------------------------------------------------------------- # Twisted class @@ -382,8 +377,7 @@ class Backend(QObject): @pyqtSlot() def createVirtScreen(self): print("Creating a Virtual Screen...") - self.xrandr.set_virtual_screen(self.width, self.height, self.portrait, self.hidpi) - self.xrandr.create_virtual_screen() + self.xrandr.create_virtual_screen(self.width, self.height, self.portrait, self.hidpi) self.virtScreenCreated = True # Qt Slots From 68efb022c75b85f1928599ad11145c9dcb692f81 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 12:55:18 -0400 Subject: [PATCH 15/23] SystrayIcon: Prevent opening window by right click --- main.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/main.qml b/main.qml index fa4a66d..5c9a0df 100644 --- a/main.qml +++ b/main.qml @@ -308,11 +308,14 @@ ApplicationWindow { "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."); - }, 7000); + }, 1500); } onActivated: function(reason) { console.log(reason); + if (reason == Labs.SystemTrayIcon.Context) { + return; + } if (window.visible) { window.hide(); return; From 6bee3d556a8e1be8ec49c2e75389a210d60136d1 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 19:40:07 -0400 Subject: [PATCH 16/23] Backend: make use of DisplayProperty class --- main.qml | 24 ++++----- virtscreen.py | 133 ++++++++++++++++++++++++++++++++++---------------- 2 files changed, 102 insertions(+), 55 deletions(-) diff --git a/main.qml b/main.qml index 5c9a0df..84e71df 100644 --- a/main.qml +++ b/main.qml @@ -6,6 +6,7 @@ import QtQuick.Window 2.2 import Qt.labs.platform 1.0 as Labs +import VirtScreen.DisplayProperty 1.0 import VirtScreen.Backend 1.0 @@ -34,6 +35,10 @@ ApplicationWindow { id: backend } + DisplayProperty { + id: display + } + // Timer object and function Timer { id: timer @@ -86,16 +91,13 @@ ApplicationWindow { Layout.fillWidth: true Label { text: "Width"; Layout.fillWidth: true } SpinBox { - value: backend.width + value: backend.virt.width from: 640 to: 1920 stepSize: 1 editable: true - textFromValue: function(value, locale) { - return Number(value).toLocaleString(locale, 'f', 0) + " px"; - } onValueModified: { - backend.width = value; + backend.virt.width = value; } } } @@ -104,16 +106,13 @@ ApplicationWindow { Layout.fillWidth: true Label { text: "Height"; Layout.fillWidth: true } SpinBox { - value: backend.height + value: backend.virt.height from: 360 to: 1080 stepSize : 1 editable: true - textFromValue: function(value, locale) { - return Number(value).toLocaleString(locale, 'f', 0) + " px"; - } onValueModified: { - backend.height = value; + backend.virt.height = value; } } } @@ -322,8 +321,9 @@ ApplicationWindow { } sysTrayIcon.clicked = true; // Move window to the corner of the primary display - var width = backend.primaryDisplayWidth; - var height = backend.primaryDisplayHeight; + var primary = backend.primary; + var width = primary.width; + var height = primary.height; var x_mid = width / 2; var y_mid = height / 2; window.x = width - window.width; //(backend.cursor_x > x_mid)? width - window.width : 0; diff --git a/virtscreen.py b/virtscreen.py index 962cc48..a3415a2 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -50,16 +50,17 @@ class SubprocessWrapper: #------------------------------------------------------------------------------- # Display properties #------------------------------------------------------------------------------- -class DisplayProperty: - def __init__(self): - self.name: str - self.primary: bool - self.connected: bool - self.active: bool - self.width: int - self.height: int - self.x_offset: int - self.y_offset: int +class DisplayProperty(QObject): + def __init__(self, parent=None): + super(DisplayProperty, self).__init__(parent) + self._name: str + self._primary: bool + self._connected: bool + 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: @@ -73,6 +74,62 @@ class DisplayProperty: else: ret += " disconnected" return ret + + @pyqtProperty(str) + def name(self): + return self._name + @name.setter + def name(self, name): + self._name = name + + @pyqtProperty(bool) + def primary(self): + return self._primary + @primary.setter + def primary(self, primary): + self._primary = primary + + @pyqtProperty(bool) + def connected(self): + return self._connected + @connected.setter + def connected(self, connected): + self._connected = connected + + @pyqtProperty(bool) + def active(self): + return self._active + @active.setter + def active(self, active): + self._active = active + + @pyqtProperty(int) + def width(self): + return self._width + @width.setter + def width(self, width): + self._width = width + + @pyqtProperty(int) + def height(self): + return self._height + @height.setter + def height(self, height): + self._height = height + + @pyqtProperty(int) + def x_offset(self): + return self._x_offset + @x_offset.setter + def x_offset(self, x_offset): + self._x_offset = x_offset + + @pyqtProperty(int) + 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 @@ -158,6 +215,10 @@ class XRandR(SubprocessWrapper): self.delete_virtual_screen() os._exit(0) + def get_primary_screen(self) -> DisplayProperty: + self._update_screens() + return self.primary + def get_virtual_screen(self) -> DisplayProperty: self._update_screens() return self.virt @@ -272,9 +333,12 @@ class Backend(QObject): def __init__(self, parent=None): super(Backend, self).__init__(parent) + # objects + self.xrandr = XRandR() # Virtual screen properties - self._width = 1368 - self._height = 1024 + self._virt = DisplayProperty() + self.virt.width = 1368 + self.virt.height = 1024 self._portrait = False self._hidpi = False self._virtScreenCreated = False @@ -283,27 +347,17 @@ class Backend(QObject): self._vncPassword = "" self._vncState = VNCState.OFF # Primary screen and mouse posistion + self._primary: DisplayProperty() = self.xrandr.get_primary_screen() self._cursor_x: int self._cursor_y: int - self._primaryDisplayWidth: int - self._primaryDisplayHeight: int - # objects - self.xrandr = XRandR() # Qt properties - @pyqtProperty(int) - def width(self): - return self._width - @width.setter - def width(self, width): - self._width = width - - @pyqtProperty(int) - def height(self): - return self._height - @height.setter - def height(self, height): - self._height = height + @pyqtProperty(DisplayProperty) + def virt(self): + return self._virt + @virt.setter + def virt(self, virt): + self._virt = virt @pyqtProperty(bool) def portrait(self): @@ -349,6 +403,11 @@ class Backend(QObject): self._vncState = state self.onVncStateChanged.emit(self._vncState.value) + @pyqtProperty(DisplayProperty) + def primary(self): + self._primary = self.xrandr.get_primary_screen() + return self._primary + @pyqtProperty(int) def cursor_x(self): cursor = QCursor().pos() @@ -360,27 +419,14 @@ class Backend(QObject): cursor = QCursor().pos() self._cursor_y = cursor.y() return self._cursor_y - - @pyqtProperty(int) - def primaryDisplayWidth(self): - screen = QApplication.desktop().screenGeometry() - self._primaryDisplayWidth = screen.width() - return self._primaryDisplayWidth - - @pyqtProperty(int) - def primaryDisplayHeight(self): - screen = QApplication.desktop().screenGeometry() - self._primaryDisplayHeight = screen.height() - return self._primaryDisplayHeight # Qt Slots @pyqtSlot() def createVirtScreen(self): print("Creating a Virtual Screen...") - self.xrandr.create_virtual_screen(self.width, self.height, self.portrait, self.hidpi) + self.xrandr.create_virtual_screen(self.virt.width, self.virt.height, self.portrait, self.hidpi) self.virtScreenCreated = True - # Qt Slots @pyqtSlot() def deleteVirtScreen(self): print("Deleting the Virtual Screen...") @@ -489,6 +535,7 @@ if __name__ == '__main__': # 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. From 1e11c99b5050317868d9413f8b1c749a9a98ac0f Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 21:09:32 -0400 Subject: [PATCH 17/23] QML: Added combobox to select device (no real effect on select yet) --- main.qml | 26 ++++++++++++++++++++++++++ virtscreen.py | 7 ++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/main.qml b/main.qml index 84e71df..2bc1d58 100644 --- a/main.qml +++ b/main.qml @@ -138,6 +138,32 @@ ApplicationWindow { } } } + + RowLayout { + Layout.fillWidth: true + Label { text: "Device"; Layout.fillWidth: true } + ComboBox { + id: deviceComboBox + textRole: "name" + model: [] + + Component.onCompleted: { + var screens = backend.screens; + var list = []; + for (var i = 0; i < screens.length; i++) { + list.push(screens[i]); + } + deviceComboBox.model = list; + } + 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 + } + } + } } } diff --git a/virtscreen.py b/virtscreen.py index a3415a2..81b34f5 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -8,7 +8,7 @@ from PyQt5.QtWidgets import QApplication from PyQt5.QtCore import QObject, QUrl, Qt from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal from PyQt5.QtGui import QIcon, QCursor -from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine +from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty from twisted.internet import protocol, error from netifaces import interfaces, ifaddresses, AF_INET @@ -342,6 +342,7 @@ class Backend(QObject): self._portrait = False self._hidpi = False self._virtScreenCreated = False + self._screens: List[DisplayProperty] = self.xrandr.screens # VNC server properties self._vncPort = 5900 self._vncPassword = "" @@ -381,6 +382,10 @@ class Backend(QObject): self._virtScreenCreated = value self.onVirtScreenCreatedChanged.emit(value) + @pyqtProperty(QQmlListProperty) + def screens(self): + return QQmlListProperty(DisplayProperty, self, self._screens) + @pyqtProperty(int) def vncPort(self): return self._vncPort From 1b9b08f09440ca5b7b1bb3d2329991146881294d Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Wed, 9 May 2018 23:54:45 -0400 Subject: [PATCH 18/23] XRandR: Support for displays other than VIRTUAL1 --- main.qml | 8 +++++++- virtscreen.py | 46 ++++++++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/main.qml b/main.qml index 2bc1d58..0dc57b6 100644 --- a/main.qml +++ b/main.qml @@ -154,13 +154,19 @@ ApplicationWindow { list.push(screens[i]); } deviceComboBox.model = list; + deviceComboBox.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 + enabled: modelData.connected ? false : true } } } diff --git a/virtscreen.py b/virtscreen.py index 81b34f5..3ec07b1 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -65,66 +65,66 @@ class DisplayProperty(QObject): ret = f"{self.name}" if self.connected: ret += " connected" - 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" 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) + @pyqtProperty(str, constant=True) def name(self): return self._name @name.setter def name(self, name): self._name = name - @pyqtProperty(bool) + @pyqtProperty(bool, constant=True) def primary(self): return self._primary @primary.setter def primary(self, primary): self._primary = primary - @pyqtProperty(bool) + @pyqtProperty(bool, constant=True) def connected(self): return self._connected @connected.setter def connected(self, connected): self._connected = connected - @pyqtProperty(bool) + @pyqtProperty(bool, constant=True) def active(self): return self._active @active.setter def active(self, active): self._active = active - @pyqtProperty(int) + @pyqtProperty(int, constant=True) def width(self): return self._width @width.setter def width(self, width): self._width = width - @pyqtProperty(int) + @pyqtProperty(int, constant=True) def height(self): return self._height @height.setter def height(self, height): self._height = height - @pyqtProperty(int) + @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) + @pyqtProperty(int, constant=True) def y_offset(self): return self._y_offset @y_offset.setter @@ -154,14 +154,13 @@ class XRandR(SubprocessWrapper): self.primary = None self.virt = None self.screens = [] - self.virt_idx = None 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 screen.name == self.DEFAULT_VIRT_SCREEN: + 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: @@ -169,7 +168,7 @@ class XRandR(SubprocessWrapper): 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.connected or not screen.active: + if not screen.active: continue screen.width = int(match.group(6)) screen.height = int(match.group(7)) @@ -228,7 +227,7 @@ class XRandR(SubprocessWrapper): self._add_screen_mode(width, height, portrait, hidpi) self.check_call(f"xrandr --output {self.virt.name} --mode {self.mode_name}") 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_screens() def delete_virtual_screen(self) -> None: @@ -343,6 +342,7 @@ class Backend(QObject): self._hidpi = False self._virtScreenCreated = False self._screens: List[DisplayProperty] = self.xrandr.screens + self._virtScreenIndex = self.xrandr.virt_idx # VNC server properties self._vncPort = 5900 self._vncPassword = "" @@ -386,6 +386,16 @@ class Backend(QObject): def screens(self): return QQmlListProperty(DisplayProperty, self, self._screens) + @pyqtProperty(int) + def virtScreenIndex(self): + return self._virtScreenIndex + @virtScreenIndex.setter + def virtScreenIndex(self, virtScreenIndex): + print("Changing virt to ", virtScreenIndex) + self.xrandr.virt_idx = virtScreenIndex + self.xrandr.virt = self.xrandr.screens[self.xrandr.virt_idx] + self._virtScreenIndex = virtScreenIndex + @pyqtProperty(int) def vncPort(self): return self._vncPort From 81b6d8919b302037b176bd0f616f9919798175ea Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 10 May 2018 00:22:29 -0400 Subject: [PATCH 19/23] QML: smarter window moving on show --- main.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/main.qml b/main.qml index 0dc57b6..6427d10 100644 --- a/main.qml +++ b/main.qml @@ -356,10 +356,16 @@ ApplicationWindow { 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; - window.x = width - window.width; //(backend.cursor_x > x_mid)? width - window.width : 0; - window.y = (backend.cursor_y > y_mid)? height - window.height : 0; + 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(); From df362568f64edf8b72a0c0cfc4138794204c966f Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 10 May 2018 02:59:53 -0400 Subject: [PATCH 20/23] Backend: Use Q_ENUM --- main.qml | 10 ++-------- virtscreen.py | 39 ++++++++++++++++++++------------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/main.qml b/main.qml index 6427d10..15c15bb 100644 --- a/main.qml +++ b/main.qml @@ -275,7 +275,7 @@ ApplicationWindow { // Material.background: Material.Teal // Material.foreground: Material.Grey onClicked: { - if (backend.vncState == 'Off') { + if (backend.vncState == Backend.OFF) { backend.startVNC() } else { backend.stopVNC() @@ -284,7 +284,7 @@ ApplicationWindow { Component.onCompleted: { backend.onVncStateChanged.connect(function(state) { - if (state == "Off") { + if (state == Backend.OFF) { vncButton.text = "Start VNC Server"; } else { vncButton.text = "Stop VNC Server"; @@ -316,12 +316,6 @@ ApplicationWindow { checked: true } } - - Component.onCompleted: { - backend.onVncStateChanged.connect(function(state) { - vncStateLabel.text = state; - }); - } } // Sytray Icon diff --git a/virtscreen.py b/virtscreen.py index 3ec07b1..c7ae20d 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -5,8 +5,7 @@ from enum import Enum from typing import List from PyQt5.QtWidgets import QApplication -from PyQt5.QtCore import QObject, QUrl, Qt -from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal +from PyQt5.QtCore import QObject, QUrl, Qt, pyqtProperty, pyqtSlot, pyqtSignal, Q_ENUMS from PyQt5.QtGui import QIcon, QCursor from PyQt5.QtQml import qmlRegisterType, QQmlApplicationEngine, QQmlListProperty @@ -318,17 +317,19 @@ class ProcessProtocol(protocol.ProcessProtocol): #------------------------------------------------------------------------------- # QML Backend class #------------------------------------------------------------------------------- -class VNCState(Enum): - """ Enum to indicate a state of the VNC server """ - OFF = "Off" - WAITING = "Waiting" - CONNECTED = "Connected" - class Backend(QObject): """ Backend class for QML frontend """ + class VNCState: + """ Enum to indicate a state of the VNC server """ + OFF = 0 + WAITING = 1 + CONNECTED = 2 + + Q_ENUMS(VNCState) + # Signals onVirtScreenCreatedChanged = pyqtSignal(bool) - onVncStateChanged = pyqtSignal(str) + onVncStateChanged = pyqtSignal(VNCState) def __init__(self, parent=None): super(Backend, self).__init__(parent) @@ -346,7 +347,7 @@ class Backend(QObject): # VNC server properties self._vncPort = 5900 self._vncPassword = "" - self._vncState = VNCState.OFF + self._vncState = Backend.VNCState.OFF # Primary screen and mouse posistion self._primary: DisplayProperty() = self.xrandr.get_primary_screen() self._cursor_x: int @@ -410,13 +411,13 @@ class Backend(QObject): def vncPassword(self, vncPassword): self._vncPassword = vncPassword - @pyqtProperty(str, notify=onVncStateChanged) + @pyqtProperty(VNCState, notify=onVncStateChanged) def vncState(self): - return self._vncState.value + return self._vncState @vncState.setter def vncState(self, state): self._vncState = state - self.onVncStateChanged.emit(self._vncState.value) + self.onVncStateChanged.emit(self._vncState) @pyqtProperty(DisplayProperty) def primary(self): @@ -445,7 +446,7 @@ class Backend(QObject): @pyqtSlot() def deleteVirtScreen(self): print("Deleting the Virtual Screen...") - if self.vncState != VNCState.OFF.value: + if self.vncState is not Backend.VNCState.OFF: print("Turn off the VNC server first") self.virtScreenCreated = True return @@ -463,15 +464,15 @@ class Backend(QObject): # define callbacks def _onConnected(): print("VNC started.") - self.vncState = VNCState.WAITING + self.vncState = Backend.VNCState.WAITING def _onReceived(data): data = data.decode("utf-8") - if (self._vncState is not VNCState.CONNECTED) and re_connection.search(data): + if (self._vncState is not Backend.VNCState.CONNECTED) and re_connection.search(data): print("VNC connected.") - self.vncState = VNCState.CONNECTED + self.vncState = Backend.VNCState.CONNECTED def _onEnded(exitCode): print("VNC Exited.") - self.vncState = VNCState.OFF + self.vncState = Backend.VNCState.OFF atexit.unregister(self.stopVNC) # Set password password = False @@ -501,7 +502,7 @@ class Backend(QObject): # Usually called from atexit(). self.vncServer.kill() time.sleep(2) # Make sure X11VNC shutdown before execute next atexit. - if self._vncState in (VNCState.WAITING, VNCState.CONNECTED): + if self._vncState in (Backend.VNCState.WAITING, Backend.VNCState.CONNECTED): self.vncServer.kill() else: print("stopVNC called while it is not running") From 85b0670a92d02cfe6558de1d2fb8666a5df89357 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 10 May 2018 04:12:03 -0400 Subject: [PATCH 21/23] QML: Better layout using anchors, auto change with binding --- main.qml | 120 ++++++++++++++++++++++++++----------------------------- 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/main.qml b/main.qml index 15c15bb..38e5633 100644 --- a/main.qml +++ b/main.qml @@ -73,22 +73,24 @@ ApplicationWindow { currentIndex: tabBar.currentIndex ColumnLayout { - // enabled: enabler.checked - // anchors.top: parent.top - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.margins: margin + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: margin GroupBox { title: "Virtual Display" // font.bold: true - Layout.fillWidth: true + anchors.left: parent.left + anchors.right: parent.right + + enabled: backend.virtScreenCreated ? false : true ColumnLayout { - Layout.fillWidth: true + anchors.left: parent.left + anchors.right: parent.right RowLayout { - Layout.fillWidth: true Label { text: "Width"; Layout.fillWidth: true } SpinBox { value: backend.virt.width @@ -99,11 +101,11 @@ ApplicationWindow { onValueModified: { backend.virt.width = value; } + textFromValue: function(value, locale) { return value; } } } RowLayout { - Layout.fillWidth: true Label { text: "Height"; Layout.fillWidth: true } SpinBox { value: backend.virt.height @@ -114,11 +116,11 @@ ApplicationWindow { onValueModified: { backend.virt.height = value; } + textFromValue: function(value, locale) { return value; } } } RowLayout { - Layout.fillWidth: true Label { text: "Portrait Mode"; Layout.fillWidth: true } Switch { checked: backend.portrait @@ -129,7 +131,6 @@ ApplicationWindow { } RowLayout { - Layout.fillWidth: true Label { text: "HiDPI (2x resolution)"; Layout.fillWidth: true } Switch { checked: backend.hidpi @@ -140,10 +141,16 @@ ApplicationWindow { } RowLayout { - Layout.fillWidth: true - Label { text: "Device"; Layout.fillWidth: true } + 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: 120 + textRole: "name" model: [] @@ -175,10 +182,13 @@ ApplicationWindow { Button { id: virtScreenButton - text: "Enable Virtual Screen" - 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.foreground: Material.Grey + enabled: backend.vncState == Backend.OFF ? true : false Popup { id: busyDialog @@ -196,7 +206,6 @@ ApplicationWindow { } onClicked: { - virtScreenButton.enabled = false; busyDialog.open(); // Give a very short delay to show busyDialog. timer.setTimeout (function() { @@ -211,33 +220,27 @@ ApplicationWindow { Component.onCompleted: { backend.onVirtScreenCreatedChanged.connect(function(created) { busyDialog.close(); - virtScreenButton.enabled = true; - if (created) { - virtScreenButton.text = "Disable Virtual Screen" - } else { - virtScreenButton.text = "Enable Virtual Screen" - } }); } } } ColumnLayout { - // enabled: enabler.checked - // anchors.top: parent.top - // anchors.left: parent.left - // anchors.right: parent.right - // anchors.margins: margin + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: margin GroupBox { title: "VNC Server" - Layout.fillWidth: true - // Layout.fillWidth: true + anchors.left: parent.left + anchors.right: parent.right + ColumnLayout { - Layout.fillWidth: true + anchors.left: parent.left + anchors.right: parent.right RowLayout { - Layout.fillWidth: true Label { text: "Port"; Layout.fillWidth: true } SpinBox { value: backend.vncPort @@ -248,14 +251,20 @@ ApplicationWindow { onValueModified: { backend.vncPort = value; } + textFromValue: function(value, locale) { return value; } } } RowLayout { - Layout.fillWidth: true - Label { text: "Password" } + anchors.left: parent.left + anchors.right: parent.right + + Label { id: passwordLabel; text: "Password" } TextField { - Layout.fillWidth: true + anchors.left: passwordLabel.right + anchors.right: parent.right + anchors.margins: margin + placeholderText: "Password"; text: backend.vncPassword; echoMode: TextInput.Password; @@ -269,45 +278,30 @@ ApplicationWindow { Button { id: vncButton - text: "Start VNC Server" - enabled: false - Layout.fillWidth: true + anchors.left: parent.left + anchors.right: parent.right + + text: backend.vncState == Backend.OFF ? "Start VNC Server" : "Stop VNC Server" + enabled: backend.virtScreenCreated ? true : false // Material.background: Material.Teal // Material.foreground: Material.Grey - onClicked: { - if (backend.vncState == Backend.OFF) { - backend.startVNC() - } else { - backend.stopVNC() - } - } - - Component.onCompleted: { - backend.onVncStateChanged.connect(function(state) { - if (state == Backend.OFF) { - vncButton.text = "Start VNC Server"; - } else { - vncButton.text = "Stop VNC Server"; - } - }); - backend.onVirtScreenCreatedChanged.connect(function(created) { - if (created) { - vncButton.enabled = true; - } else { - vncButton.enabled = false; - } - }); - } + onClicked: backend.vncState == Backend.OFF ? backend.startVNC() : backend.stopVNC() } } } footer: ToolBar { RowLayout { - anchors.margins: spacing + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: margin + Label { id: vncStateLabel - text: backend.vncState + text: backend.vncState == Backend.OFF ? "Off" : + backend.vncState == Backend.WAITING ? "Waiting" : + backend.vncState == Backend.CONNECTED ? "Connected" : + "Server state error!" } Item { Layout.fillWidth: true } CheckBox { From 3a76aa7ef722f95c73b76b110e0d3be4ce845834 Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 10 May 2018 12:15:28 -0400 Subject: [PATCH 22/23] Backend: show ip list --- main.qml | 25 ++++++++++++++----------- virtscreen.py | 24 +++++++++++++++++++++++- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/main.qml b/main.qml index 38e5633..619321e 100644 --- a/main.qml +++ b/main.qml @@ -152,17 +152,8 @@ ApplicationWindow { anchors.leftMargin: 120 textRole: "name" - model: [] - - Component.onCompleted: { - var screens = backend.screens; - var list = []; - for (var i = 0; i < screens.length; i++) { - list.push(screens[i]); - } - deviceComboBox.model = list; - deviceComboBox.currentIndex = backend.virtScreenIndex - } + model: backend.screens + currentIndex: backend.virtScreenIndex onActivated: function(index) { backend.virtScreenIndex = index @@ -287,6 +278,18 @@ ApplicationWindow { // Material.foreground: Material.Grey onClicked: backend.vncState == Backend.OFF ? backend.startVNC() : backend.stopVNC() } + + ListView { + // width: 180; + height: 200 + anchors.left: parent.left + anchors.right: parent.right + + model: backend.ipAddresses + delegate: Text { + text: modelData + } + } } } diff --git a/virtscreen.py b/virtscreen.py index c7ae20d..3c2568f 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -329,7 +329,9 @@ class Backend(QObject): # Signals onVirtScreenCreatedChanged = pyqtSignal(bool) + onVirtScreenIndexChanged = pyqtSignal(int) onVncStateChanged = pyqtSignal(VNCState) + onIPAddressesChanged = pyqtSignal() def __init__(self, parent=None): super(Backend, self).__init__(parent) @@ -348,6 +350,8 @@ class Backend(QObject): self._vncPort = 5900 self._vncPassword = "" self._vncState = Backend.VNCState.OFF + self._ipAddresses: List[str] = [] + self.updateIPAddresses() # Primary screen and mouse posistion self._primary: DisplayProperty() = self.xrandr.get_primary_screen() self._cursor_x: int @@ -387,7 +391,7 @@ class Backend(QObject): def screens(self): return QQmlListProperty(DisplayProperty, self, self._screens) - @pyqtProperty(int) + @pyqtProperty(int, notify=onVirtScreenIndexChanged) def virtScreenIndex(self): return self._virtScreenIndex @virtScreenIndex.setter @@ -418,6 +422,10 @@ class Backend(QObject): def vncState(self, state): self._vncState = state self.onVncStateChanged.emit(self._vncState) + + @pyqtProperty('QStringList', notify=onIPAddressesChanged) + def ipAddresses(self): + return self._ipAddresses @pyqtProperty(DisplayProperty) def primary(self): @@ -507,6 +515,20 @@ class Backend(QObject): else: print("stopVNC called while it is not running") + @pyqtSlot() + def updateIPAddresses(self): + self._ipAddresses.clear() + for interface in interfaces(): + if interface == 'lo': + continue + addresses = ifaddresses(interface).get(AF_INET, None) + if addresses is None: + continue + for link in addresses: + if link is not None: + self._ipAddresses.append(link['addr']) + self.onIPAddressesChanged.emit() + @pyqtSlot() def quitProgram(self): QApplication.instance().quit() From b51ad567bb7a0f610243efc9ecea12e6e48a011d Mon Sep 17 00:00:00 2001 From: Bumsik Kim Date: Thu, 10 May 2018 15:57:25 -0400 Subject: [PATCH 23/23] QML: Auto start support. Better footbar text --- main.qml | 87 ++++++++++++++++++++++++++++++++++++++++----------- virtscreen.py | 3 ++ 2 files changed, 72 insertions(+), 18 deletions(-) diff --git a/main.qml b/main.qml index 619321e..93c6307 100644 --- a/main.qml +++ b/main.qml @@ -21,7 +21,7 @@ ApplicationWindow { property int margin: 11 width: 380 - height: 600 + height: 500 // hide screen when loosing focus onActiveFocusItemChanged: { @@ -34,9 +34,20 @@ ApplicationWindow { Backend { id: backend } + property bool vncAutoStart: false - DisplayProperty { - id: display + function switchVNC(value) { + if (value) { + backend.startVNC(); + } + } + + onVncAutoStartChanged: { + if (vncAutoStart) { + backend.onVirtScreenCreatedChanged.connect(switchVNC); + } else { + backend.onVirtScreenCreatedChanged.disconnect(switchVNC); + } } // Timer object and function @@ -149,7 +160,7 @@ ApplicationWindow { id: deviceComboBox anchors.left: deviceLabel.right anchors.right: parent.right - anchors.leftMargin: 120 + anchors.leftMargin: 100 textRole: "name" model: backend.screens @@ -179,7 +190,8 @@ ApplicationWindow { anchors.right: parent.right // Material.background: Material.Teal // Material.foreground: Material.Grey - enabled: backend.vncState == Backend.OFF ? true : false + enabled: window.vncAutoStart ? true : + backend.vncState == Backend.OFF ? true : false Popup { id: busyDialog @@ -203,7 +215,23 @@ ApplicationWindow { if (!backend.virtScreenCreated) { backend.createVirtScreen(); } else { - backend.deleteVirtScreen(); + 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); } @@ -227,6 +255,8 @@ ApplicationWindow { anchors.left: parent.left anchors.right: parent.right + enabled: backend.vncState == Backend.OFF ? true : false + ColumnLayout { anchors.left: parent.left anchors.right: parent.right @@ -271,14 +301,35 @@ ApplicationWindow { id: vncButton anchors.left: parent.left anchors.right: parent.right + anchors.bottomMargin: 0 - text: backend.vncState == Backend.OFF ? "Start VNC Server" : "Stop VNC Server" - enabled: backend.virtScreenCreated ? true : false + 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.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 @@ -294,24 +345,24 @@ ApplicationWindow { } footer: ToolBar { + font.weight: Font.Medium + font.pointSize: 11 //parent.font.pointSize + 1 + RowLayout { + anchors.top: parent.top + anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.margins: margin + anchors.leftMargin: margin + 10 Label { id: vncStateLabel - text: backend.vncState == Backend.OFF ? "Off" : - backend.vncState == Backend.WAITING ? "Waiting" : - backend.vncState == Backend.CONNECTED ? "Connected" : + text: !backend.virtScreenCreated ? "Enable Virtual Screen first." : + backend.vncState == Backend.OFF ? "Turn on VNC Server in the VNC tab." : + backend.vncState == Backend.WAITING ? "VNC Server is waiting for a client..." : + backend.vncState == Backend.CONNECTED ? "Connected." : "Server state error!" } - Item { Layout.fillWidth: true } - CheckBox { - id: enabler - text: "Server Enabled" - checked: true - } } } diff --git a/virtscreen.py b/virtscreen.py index 3c2568f..e0d88cf 100755 --- a/virtscreen.py +++ b/virtscreen.py @@ -467,6 +467,9 @@ class Backend(QObject): 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