#!/usr/bin/env python3 import os import subprocess from PyQt5 import QtCore, QtGui, QtDBus, QtWidgets from PyQt5.QtCore import pyqtSignal as Signal, pyqtSlot as Slot, pyqtProperty as Property, Q_CLASSINFO if "WAYLAND_DISPLAY" in os.environ: IS_WAYLAND = True KSCREEN_OUTPUT = "unknown DSI-1-unknown" else: IS_WAYLAND = False KSCREEN_OUTPUT = "DSI-1" XINPUT_TOUCH = "silead_ts" IIO_TO_KSCREEN = { "normal": "none", "right-up": "right", "left-up": "left", "bottom-up": "inverted" } KEYBOARD_ORIENTATION = IIO_TO_KSCREEN["right-up"] IIO_BUSNAME = "net.hadess.SensorProxy" IIO_OBJPATH = "/net/hadess/SensorProxy" KWIN_BUSNAME = "org.kde.KWin" KWIN_OBJPATH = "/org/kde/KWin" # DBus class DIKWinTabletModeManager(QtDBus.QDBusAbstractInterface): tabletModeAvailableChanged = Signal(bool) @Property(bool, notify=tabletModeAvailableChanged) def tabletModeAvailable(self): return self.property("tabletModeAvailable") tabletModeChanged = Signal(bool) @Property(bool, notify=tabletModeChanged) def tabletMode(self): return self.property("tabletMode") @Slot(QtDBus.QDBusMessage) def _on_properties_changed(self, msg): intf, updated, invald = msg.arguments() updated = dict(updated) if "tabletModeAvailable" in updated: self.tabletModeAvailableChanged.emit(updated["tabletModeAvailable"]) if "tabletMode" in updated: self.tabletModeChanged.emit(updated["tabletMode"]) def __init__(self, service, path, connection, parent=None): super().__init__(service, path, "org.kde.KWin.TabletModeManager", connection, parent) if not connection.connect(service, path, "org.freedesktop.DBus.Properties", "PropertiesChanged", ["org.kde.KWin.TabletModeManager"], "sa{sv}as", self._on_properties_changed): raise RuntimeError("Could not connect to PropertiesChanged") class DIKWinVirtualKeyboard(QtDBus.QDBusAbstractInterface): enabledChanged = Signal(bool) activeChanged = Signal(bool) @Property(bool, notify=enabledChanged) def enabled(self): return self.property("enabled") @enabled.write def enabled(self, value): self.setProperty("enabled", value) @Property(bool, notify=activeChanged) def active(self): return self.property("active") @active.write def active(self, value): self.setProperty("active", value) @Slot(QtDBus.QDBusMessage) def _on_properties_changed(self, msg): intf, updated, invalid = msg.arguments() updated = dict(updated) if "enabled" in updated: self.enabledChanged.emit(updated["enabled"]) if "active" in updated: self.activeChanged.emit(updated["active"]) def __init__(self, service, path, connection, parent=None): super().__init__(service, path, "org.kde.kwin.VirtualKeyboard", connection, parent) if not connection.connect(service, path, "org.freedesktop.DBus.Properties", "PropertiesChanged", ["ork.kde.kwin.VirtualKeyboard"], "sa{sv}as", self._on_properties_changed): raise RuntimeError("Could not connect to PropertiesChanged") class DISensorProxy(QtDBus.QDBusAbstractInterface): HasAccelerometerChanged = Signal(bool) @Property(bool, notify=HasAccelerometerChanged) def HasAccelerometer(self): return self.property("HasAccelerometer") AccelerometerOrientationChanged = Signal(str) @Property(str, notify=AccelerometerOrientationChanged) def AccelerometerOrientation(self) -> str: return self.property("AccelerometerOrientation") def ClaimAccelerometer(self): self.call("ClaimAccelerometer") def ReleaseAccelerometer(self): self.call("ReleaseAccelerometer") @Slot(QtDBus.QDBusMessage) def _on_properties_changed(self, msg): intf, updated, invald = msg.arguments() updated = dict(updated) if "HasAccelerometer" in updated: self.HasAccelerometerChanged.emit(updated["HasAccelerometerChanged"]) if "AccelerometerOrientation" in updated: self.AccelerometerOrientationChanged.emit(updated["AccelerometerOrientation"]) def __init__(self, service, path, connection, parent=None): super().__init__(service, path, "net.hadess.SensorProxy", connection, parent) if not connection.connect(service, path, "org.freedesktop.DBus.Properties", "PropertiesChanged", ["net.hadess.SensorProxy"], "sa{sv}as", self._on_properties_changed): raise RuntimeError("Could not connect to PropertiesChanged") class DARotated(QtDBus.QDBusAbstractAdaptor): Q_CLASSINFO("D-Bus Interface", "me.sodimm.oro.Rotated") def __init__(self, app): super().__init__(app) self.app = app app.orientationChanged.connect(self.ScreenTurned) ScreenTurned = Signal(str) @Property(str) def Orientation(self): return self.app.orientation @Property(bool) def AutoTurn(self): return self.app.turn_enabled @AutoTurn.write def AutoTurn(self, val): self.app.set_turn_enabled(val) @Property(bool) def TabletModeManager(self): return self.app.tmm_enabled @TabletModeManager.write def TabletModeManager(self, val): self.app.set_tmm_enabled(val) @Property(bool) def TabletModeManagerAvailable(self): return self.app.tmm is not None @Slot(str, result=bool) def Turn(self, orientation): if orientation in ("none", "right", "left", "inverted"): self.app.turn_screen(orientation) return True return False @Slot() def Quit(self): self.app.qapp.quit() class DAOSKHelper(QtDBus.QDBusAbstractAdaptor): Q_CLASSINFO("D-Bus Interface", "me.sodimm.oro.OSKHelper") def __init__(self, oh): super().__init__(oh) self.oh = oh @Property(bool) def enabled(self): return self.oh.enabled @enabled.write def enabled(self, value): self.oh.enabled = value @Slot() def Show(self): self.app.show_osk() # App Logic class OSKHelper(QtCore.QObject): def __init__(self, app): super().__init__(app) self.app = app self.kwin_vkb = app.kwin_vkb self.icon = QtGui.QIcon.fromTheme("input-keyboard-virtual") self.systray = QtWidgets.QSystemTrayIcon(self.icon, self) self.systray.activated.connect(self.toggle_osk) self.adaptor = DAOSKHelper(self) @property def enabled(self): return self.systray.isVisible() @enabled.setter def enabled(self, value): if value: self.systray.show() else: self.systray.hide() @Slot() def show_osk(self): if not self.kwin_vkb.enabled: self.kwin_vkb.enabled = True self.kwin_vkb.active = True @Slot() def toggle_osk(self): if not self.kwin_vkb.enabled: self.kwin_vkb.enabled = True self.kwin_vkb.active = not self.kwin_vkb.active class Main(QtCore.QObject): def __init__(self, argv): super().__init__() #self.qapp = QtCore.QCoreApplication(argv) self.qapp = QtWidgets.QApplication(argv) self.qapp.setApplicationName("iio-rotated") self.qapp.setApplicationDisplayName("Screen Rotation") # TODO: Figure out initial orientation self.orientation = None self.turn_enabled = True self.tmm_enabled = IS_WAYLAND self.tablet_mode = None # Turning orientationChanged = Signal(str) def turn_screen(self, orientation): """ Turn the Display into a different orientation @param orientation The KScreen orientation (none|right|left|inverted) """ try: subprocess.check_call(["kscreen-doctor", "output.%s.rotation.%s" % (KSCREEN_OUTPUT, orientation)]) if not IS_WAYLAND: subprocess.check_call(["xinput", "--map-to-output", XINPUT_TOUCH, KSCREEN_OUTPUT]) except: import traceback traceback.print_exc() else: self.orientation = orientation self.orientationChanged.emit(orientation) # Auto-Turning def on_device_turned(self, direction): print("Device Turned", direction) if self.turn_enabled: self.turn_screen(IIO_TO_KSCREEN[direction]) def set_turn_enabled(self, v): self.turn_enabled = v self.turn_enabled_changed.emit(v) def set_tmm_enabled(self, v): self.tmm_enabled = v self.tmm_enabled_changed.emit(v) turn_enabled_changed = Signal(bool) tmm_enabled_changed = Signal(bool) def on_systray_clicked(self, reason): print("Activated", reason) if reason == QtWidgets.QSystemTrayIcon.Trigger: if not self.tmm_enabled: self.set_turn_enabled(not self.turn_enabled) def on_tabletmode(self, v): if self.tmm_enabled: self.set_turn_enabled(v) if v: print("Entered Tablet Mode") #self.turn_screen(IIO_TO_KSCREEN[self.iio.AccelerometerOrientation]) else: print("Left Tablet Mode") self.turn_screen(KEYBOARD_ORIENTATION) # Main def main(self): # Set up Session Bus self.session_bus = QtDBus.QDBusConnection.sessionBus() if not self.session_bus.isConnected(): raise RuntimeError("Not connected to Session Bus") if not self.session_bus.registerService("me.sodimm.oro.Rotated"): raise RuntimeError("Could not register D-Bus Service. Maybe another instance is already running") self.adaptor = DARotated(self) self.session_bus.registerObject("/Rotated", self) try: self.tmm = DIKWinTabletModeManager(KWIN_BUSNAME, KWIN_OBJPATH, self.session_bus) except: print("Could not connect to KWin TabletModeManager") import traceback traceback.print_exc() self.tmm_enabled = False self.tmm = None try: self.kwin_vkb = DIKWinVirtualKeyboard(KWIN_BUSNAME, "/VirtualKeyboard", self.session_bus) except: self.kwin_vkb = None self.osk_helper = None else: self.osk_helper = OSKHelper(self) self.session_bus.registerObject("/OSKHelper", self.osk_helper) self.osk_helper.enabled = False and IS_WAYLAND # Connect to System Bus self.system_bus = QtDBus.QDBusConnection.systemBus() if not self.system_bus.isConnected(): raise RuntimeError("Not connected to System Bus") # Look for iio-sensor-proxy self.iio = iio = DISensorProxy(IIO_BUSNAME, IIO_OBJPATH, self.system_bus) if not iio.HasAccelerometer: raise RuntimeError("No accelerometer reported") # Set up System Tray Icon self.icon_lock = QtGui.QIcon.fromTheme("emblem-locked") self.icon_turn = QtGui.QIcon.fromTheme("emblem-unlocked") # TODO: Directly work with StatusNotifierItem API? self.systray = QtWidgets.QSystemTrayIcon(self.icon_turn, self) #self.systray.setToolTip("Screen Orientation") self.turn_enabled_changed.connect(lambda v: self.systray.setIcon(self.icon_turn if v else self.icon_lock)) self.menu = QtWidgets.QMenu() # Tablet Mode detection enable action = self.menu.addAction("Detect Tablet Mode") action.setEnabled(self.tmm is not None) action.setCheckable(True) action.setChecked(self.tmm_enabled) action.triggered.connect(lambda c: self.set_tmm_enabled(c)) self.tmm_enabled_changed.connect(lambda c, action=action: action.setChecked(c)) # Auto turn enable action = self.menu.addAction("Auto-Turn") action.setCheckable(True) action.setChecked(self.turn_enabled) action.setEnabled(not self.tmm_enabled) action.triggered.connect(lambda c: self.set_turn_enabled(c)) self.tmm_enabled_changed.connect(lambda v, action=action: action.setEnabled(not v)) self.turn_enabled_changed.connect(lambda v, action=action: action.setChecked(v)) self.menu.addSeparator() # Manual turning for label, direction in (("Normal", "none"), ("Anti-CW", "right"), ("Clockwise", "left"), ("Upside-Down", "inverted")): action = self.menu.addAction("Turn %s" % label) action.triggered.connect(lambda *a, d=direction: self.turn_screen(d)) action.setEnabled(not self.turn_enabled) self.turn_enabled_changed.connect(lambda v, action=action: action.setEnabled(not v)) self.menu.addSeparator() # Quit action = self.menu.addAction("Quit") action.triggered.connect(self.qapp.quit) self.systray.setContextMenu(self.menu) self.systray.activated.connect(self.on_systray_clicked) self.systray.show() if self.tmm is not None: #self.tmm.tabletModeAvailableChanged.connect(self.updateTabletModeAvailable) #self.updateTabletModeAvailable(self.tmm.tabletModeAvailable) self.tmm.tabletModeChanged.connect(self.on_tabletmode) self.on_tabletmode(self.tmm.tabletMode) # Run iio.ClaimAccelerometer() iio.AccelerometerOrientationChanged.connect(self.on_device_turned) try: self.qapp.exec() finally: iio.ReleaseAccelerometer() if __name__ == "__main__": import sys Main(sys.argv).main()