You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							421 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
	
	
							421 lines
						
					
					
						
							14 KiB
						
					
					
				#!/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()
 | 
						|
 |