init
This commit is contained in:
121 files changed
+17641
No files matched your search
@@ -0,0 +1,146 @@
|
||||
"""Application wiring: Qt, QML, the tray, and the D-Bus service.
|
||||
|
||||
Qt Quick Controls' **Material** style gives real Material 3 widgets rendered by
|
||||
Qt itself — no web view, no bundled browser; the window is a native Wayland
|
||||
surface with the session's own fonts, scaling and colour scheme.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
from pathlib import Path
|
||||
|
||||
# The style has to be chosen before the first Qt object exists. PyQt6 on Arch
|
||||
# ships no QtQuickControls2 binding, so the environment variable — which Qt
|
||||
# itself reads — is the portable way to ask for Material.
|
||||
os.environ.setdefault("QT_QUICK_CONTROLS_STYLE", "Material")
|
||||
os.environ.setdefault("QT_QUICK_CONTROLS_MATERIAL_VARIANT", "Normal")
|
||||
|
||||
from PyQt6.QtCore import QObject, QTimer, QUrl
|
||||
from PyQt6.QtGui import QIcon
|
||||
from PyQt6.QtQml import QQmlApplicationEngine
|
||||
from PyQt6.QtQuick import QQuickWindow
|
||||
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon
|
||||
|
||||
from kadence_desktop import APP_ID, __version__
|
||||
from kadence_desktop.config import DesktopConfig
|
||||
from kadence_desktop.controller import Controller
|
||||
from kadence_desktop.integrations import dbus_service
|
||||
from kadence_desktop.tray import Tray, render_icon
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
QML_DIR = Path(__file__).resolve().parent / "qml"
|
||||
|
||||
|
||||
class KadenceApp(QObject):
|
||||
def __init__(self, argv: list[str], *, minimized: bool = False) -> None:
|
||||
super().__init__()
|
||||
# QApplication (not QGuiApplication): the tray icon and its menu are
|
||||
# widgets, and they are what makes the app usable with no window open.
|
||||
self._app = QApplication(argv)
|
||||
self._app.setApplicationName("Kadence")
|
||||
self._app.setApplicationDisplayName("Kadence")
|
||||
self._app.setOrganizationName("Kadence")
|
||||
self._app.setApplicationVersion(__version__)
|
||||
# Wayland takes the window's app id from here: without it the window
|
||||
# has no icon and the task manager cannot group it.
|
||||
self._app.setDesktopFileName("kadence")
|
||||
self._app.setWindowIcon(_load_icon())
|
||||
self._app.setQuitOnLastWindowClosed(False)
|
||||
|
||||
self._config = DesktopConfig.load()
|
||||
self._controller = Controller(self._config)
|
||||
|
||||
self._engine = QQmlApplicationEngine()
|
||||
self._engine.rootContext().setContextProperty("controller", self._controller)
|
||||
self._engine.rootContext().setContextProperty("appVersion", __version__)
|
||||
self._engine.addImportPath(str(QML_DIR))
|
||||
self._engine.load(QUrl.fromLocalFile(str(QML_DIR / "Main.qml")))
|
||||
if not self._engine.rootObjects():
|
||||
raise SystemExit("Could not load the QML interface.")
|
||||
self._window: QQuickWindow = self._engine.rootObjects()[0]
|
||||
|
||||
self._tray: Tray | None = None
|
||||
if self._config.tray_icon and QSystemTrayIcon.isSystemTrayAvailable():
|
||||
self._tray = Tray(self._controller, self)
|
||||
self._tray.showRequested.connect(self.toggle_window)
|
||||
self._tray.quitRequested.connect(self.quit)
|
||||
self._tray.show()
|
||||
|
||||
self._controller.showWindowRequested.connect(self.show_window)
|
||||
self._app.aboutToQuit.connect(self._controller.shutdown)
|
||||
|
||||
if self._config.dbus_service:
|
||||
self._register_dbus()
|
||||
|
||||
if minimized or self._config.start_minimized:
|
||||
self._window.hide()
|
||||
else:
|
||||
self.show_window()
|
||||
|
||||
# Let Ctrl+C in a terminal actually kill the app (Qt swallows SIGINT
|
||||
# unless the event loop gets a chance to run Python code now and then).
|
||||
signal.signal(signal.SIGINT, lambda *_args: self.quit())
|
||||
self._wakeup = QTimer(self)
|
||||
self._wakeup.start(500)
|
||||
self._wakeup.timeout.connect(lambda: None)
|
||||
|
||||
# ---- window ------------------------------------------------------------
|
||||
|
||||
def show_window(self) -> None:
|
||||
self._window.show()
|
||||
self._window.raise_()
|
||||
self._window.requestActivate()
|
||||
|
||||
def toggle_window(self) -> None:
|
||||
if self._window.isVisible() and self._window.isActive():
|
||||
self._window.hide()
|
||||
else:
|
||||
self.show_window()
|
||||
|
||||
# ---- D-Bus -------------------------------------------------------------
|
||||
|
||||
def _register_dbus(self) -> None:
|
||||
controller = self._controller
|
||||
actions = {
|
||||
"start_work": lambda: (controller.startWork(), "started")[1],
|
||||
"start_break": lambda: (controller.startBreak(), "break started")[1],
|
||||
"toggle": lambda: (controller.toggle(), "toggled")[1],
|
||||
"pause": lambda: (controller.pause(), "paused")[1],
|
||||
"resume": lambda: (controller.resume(), "resumed")[1],
|
||||
"complete": lambda: (controller.complete(), "completed")[1],
|
||||
"stop": lambda: (controller.abandon("stopped over D-Bus"), "stopped")[1],
|
||||
"extend": lambda: (controller.extend(5), "extended")[1],
|
||||
"interrupt": lambda: (controller.interrupt(), "noted")[1],
|
||||
"status": controller.statusLine,
|
||||
"send_report": lambda: (controller.sendReport(), "sending")[1],
|
||||
"raise_window": lambda: (self.show_window(), "shown")[1],
|
||||
}
|
||||
self._dbus = dbus_service.TimerDBusService(actions, self)
|
||||
if not dbus_service.register(self._dbus):
|
||||
log.warning("another instance owns %s — this one will not be scriptable", APP_ID)
|
||||
|
||||
# ---- lifecycle ---------------------------------------------------------
|
||||
|
||||
def quit(self) -> None:
|
||||
self._controller.shutdown()
|
||||
if self._tray:
|
||||
self._tray.hide()
|
||||
self._app.quit()
|
||||
|
||||
def run(self) -> int:
|
||||
return self._app.exec()
|
||||
|
||||
|
||||
def _load_icon() -> QIcon:
|
||||
"""The packaged icon if it is installed, else the one the tray paints."""
|
||||
themed = QIcon.fromTheme("kadence")
|
||||
if not themed.isNull():
|
||||
return themed
|
||||
packaged = Path(__file__).resolve().parent.parent / "packaging" / "kadence.svg"
|
||||
if packaged.is_file():
|
||||
return QIcon(str(packaged))
|
||||
return render_icon(0.0, "work", False, True, False)
|
||||
Reference in new issue
Block a user