Skip to content

Instantly share code, notes, and snippets.

@ssokolow
Created December 5, 2024 22:24
Show Gist options
  • Save ssokolow/1cbe7d9341d6b4401b17c0f7eebbd2b8 to your computer and use it in GitHub Desktop.
Save ssokolow/1cbe7d9341d6b4401b17c0f7eebbd2b8 to your computer and use it in GitHub Desktop.
Python+QtDBUS reimplementation of (most of) org.kde.StatusNotifierItem
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""A demonstration of implementing (most of) org.kde.StatusNotifierItem using
QtDBus to serve as as an example of how more advanced QtDBus features map to
PyQt5, absent robust documentation for how the schema annotations translate.
"""
import os
import signal
import sys
from PyQt5.QtCore import QObject, qCritical
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Q_CLASSINFO
from PyQt5.QtCore import pyqtProperty # type: ignore[attr-defined]
from PyQt5.QtDBus import QDBus, QDBusConnection, QDBusInterface
from PyQt5.QtWidgets import QApplication, QMainWindow
def die(msg):
"""Die without trying to core dump like qFatal would"""
qCritical("ERROR: ", msg)
sys.exit(1)
class StatusNotifierItem(QObject):
attention_icon = "go-previous"
icon = "go-up"
status = "Active"
title = "StatusNotifierItem Demo"
Q_CLASSINFO("D-Bus Interface", 'org.kde.StatusNotifierItem')
def __init__(self, window, *args, **kwargs):
self.window = window
super().__init__(*args, **kwargs)
# -- Properties --
@pyqtProperty(str)
def Category(self):
return "ApplicationStatus"
@pyqtProperty(str)
def Id(self):
return "KSNI Example"
@pyqtProperty(str)
def Title(self):
return self.title
@pyqtProperty(str)
def Status(self):
return self.status
@pyqtProperty(int)
def WindowId(self):
return int(self.window.winId())
@pyqtProperty(str)
def IconName(self):
return self.icon
# IconPixmap
# OverlayIconName (May not be implemented on Kubuntu 22.04 LTS)
# OverlayIconPixmap (May not be implemented on Kubuntu 22.04 LTS)
@pyqtProperty(str)
def AttentionIconName(self):
return self.attention_icon
# AttentionIconPixmap
# AttentionMovieName
# ToolTip
@pyqtProperty(bool)
def ItemIsMenu(self):
return False # Don't prefer left-clicking being ContextMenu too
# Menu
# -- Methods --
@pyqtSlot(int, int)
def Activate(self, x, y):
print(f"Activated: {x}, {y}")
if self.window.isVisible():
self.window.hide()
else:
self.window.showNormal()
@pyqtSlot(int, int)
def ContextMenu(self, x, y):
print(f"Context Menu: {x}, {y}")
@pyqtSlot(int, int)
def SecondaryActivate(self, x, y):
print(f"Secondary Activated: {x}, {y}")
if self.status == "Active":
self.status = "NeedsAttention"
else:
self.status = "Active"
self.NewStatus.emit(self.status)
self.title = f"SNI Status is {self.status}."
self.NewTitle.emit()
@pyqtSlot(int, str)
def Scroll(self, delta, orientation):
print(f"Scrolled: {delta}, {orientation}")
if delta < 0:
if self.attention_icon == "go-previous":
self.attention_icon = "go-next"
else:
self.attention_icon = "go-previous"
self.NewAttentionIcon.emit()
else:
if self.icon == "go-up":
self.icon = "go-down"
else:
self.icon = "go-up"
self.NewIcon.emit()
# -- Signals --
# NewOverlayIcon
# NewToolTip
NewIcon = pyqtSignal()
NewAttentionIcon = pyqtSignal()
NewStatus = pyqtSignal(str)
NewTitle = pyqtSignal()
def main():
app = QApplication(sys.argv)
window = QMainWindow()
session_bus = QDBusConnection.sessionBus()
if not session_bus.isConnected():
die("Failed to connect to D-Bus session bus")
svc_name = "org.freedesktop.StatusNotifierItem-{}-0".format(os.getpid())
if not session_bus.registerService(svc_name):
die("Could not register service for status notifier item")
notifier_item = StatusNotifierItem(window)
session_bus.registerObject('/StatusNotifierItem', notifier_item,
QDBusConnection.ExportAllContents)
watcher = QDBusInterface(
"org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher",
"org.kde.StatusNotifierWatcher", session_bus)
if not watcher.isValid():
die("Couldn't find org.kde.StatusNotifierWatcher")
response = watcher.call(QDBus.AutoDetect, # type: ignore[attr-defined]
"RegisterStatusNotifierItem", svc_name)
if response.errorName():
die(response.errorMessage())
signal.signal(signal.SIGINT, signal.SIG_DFL) # Allow Ctrl+C in all.exec_()
print("Press Ctrl+C to quit or close main window")
sys.exit(app.exec_()) # Start main loop
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment