|
|
|
#!/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()
|