Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/axidev_osk/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from .runtime.application import ApplicationRuntime
from .runtime.registries import ServiceRegistry
from .services.keyboard import KeyboardService
from .services.kwin_lock import KWinLockService
from .services.secure_input_panel import SecureInputPanelService
from .services.single_instance import ExistingInstanceActivated
from .windows.overlay import OverlayBackend, prepare_always_on_top_window_environment

Expand Down Expand Up @@ -63,7 +63,7 @@ def _input_panel_services(
services = ServiceRegistry()
services.register("keyboard", KeyboardService(), autostart=not lock_lifecycle)
if lock_lifecycle:
services.register("kwin_lock", KWinLockService(parent=app))
services.register("secure_input_panel", SecureInputPanelService(parent=app))
return services


Expand Down
298 changes: 256 additions & 42 deletions src/axidev_osk/cli/linux_greeter.py

Large diffs are not rendered by default.

57 changes: 29 additions & 28 deletions src/axidev_osk/runtime/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
from ..config.models import AppConfig, ChromeConfig, PromptConfig, SurfaceConfig, WindowConfig
from ..services import register_services
from ..services.keyboard import KeyboardService
from ..services.kwin_lock import KWinLockService
from ..styles.theme import apply_theme
from ..windows.surface import register_surfaces
from .context import Context
Expand All @@ -26,7 +25,7 @@
route_component_pressed,
route_hot_corner_triggered,
)
from .events import ScreenLockStateChanged, WindowCloseRequested
from .events import WindowCloseRequested
from .prompt import PromptResolutionWaiter
from .registries import (
ComponentRegistry,
Expand Down Expand Up @@ -72,7 +71,7 @@ def __init__(

self._app = app
self._show_startup_windows = show_startup_windows
self._screen_locked: bool | None = None
self._secure_input_panel_prepared = False
self._config = config or build_default_app_config()
self._dispatcher = Dispatcher()
self._services = services or ServiceRegistry()
Expand Down Expand Up @@ -157,38 +156,40 @@ def _handle_window_close_requested(self, event: object) -> None:
if isinstance(event, WindowCloseRequested):
self._quit_controller.request_quit()

def _handle_screen_lock_state_changed(self, event: object) -> None:
"""Create or destroy secure runtime resources as KDE locks and unlocks."""
def _prepare_secure_input_panel(self) -> None:
"""Create the keyboard window and backend requested by the lock-screen button."""

if not isinstance(event, ScreenLockStateChanged):
return
if event.locked == self._screen_locked:
if event.locked:
self._services.get("kwin_lock", KWinLockService).activate()
if self._secure_input_panel_prepared:
return
window_id = self._config.keyboard_window_id
if event.locked:
try:
self._keyboard.start(self.context)
window = self._window_manager.show(window_id)
window.set_close_enabled(False)
except Exception:
try:
self._keyboard.start(self.context)
window = self._window_manager.show(window_id)
window.set_close_enabled(False)
self._services.get("kwin_lock", KWinLockService).activate()
self._window_manager.destroy(window_id)
except Exception:
try:
self._window_manager.destroy(window_id)
except Exception:
_logger.exception("Failed to destroy a partially started lock window")
try:
self._keyboard.shutdown()
except Exception:
_logger.exception("Failed to shut down keyboard output after lock startup failed")
raise
else:
_logger.exception("Failed to destroy a partially prepared secure input panel")
try:
self._keyboard.shutdown()
except Exception:
_logger.exception("Failed to shut down keyboard output after panel preparation failed")
raise
self._secure_input_panel_prepared = True

def _release_secure_input_panel(self) -> None:
"""Destroy runtime resources released by the lock-screen QML."""

if not self._secure_input_panel_prepared:
return
try:
self._window_manager.destroy(self._config.keyboard_window_id)
finally:
try:
self._window_manager.destroy(window_id)
finally:
self._keyboard.shutdown()
self._screen_locked = event.locked
finally:
self._secure_input_panel_prepared = False

def _handle_hot_corner_triggered(self, event: object) -> None:
"""Map hot-corner events to managed window visibility commands."""
Expand Down
12 changes: 11 additions & 1 deletion src/axidev_osk/runtime/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ class StateSet:
value: object


@dataclass(frozen=True, slots=True)
class SecureInputPanelPrepare:
"""Command requesting creation of secure input-panel runtime resources."""


@dataclass(frozen=True, slots=True)
class SecureInputPanelRelease:
"""Command requesting cleanup of secure input-panel runtime resources."""


@dataclass(frozen=True, slots=True)
class WindowShow:
"""Command requesting a managed window to be shown.
Expand Down Expand Up @@ -134,4 +144,4 @@ class AppQuit:
exit_code: int = 0


RuntimeCommand = KeyboardRegisterKeySpec | KeyboardKeyDown | KeyboardKeyUp | KeyboardSyncLatchedKey | StateSet | WindowShow | WindowHide | WindowToggleOpacity | WindowClose | AppQuit
RuntimeCommand = KeyboardRegisterKeySpec | KeyboardKeyDown | KeyboardKeyUp | KeyboardSyncLatchedKey | StateSet | SecureInputPanelPrepare | SecureInputPanelRelease | WindowShow | WindowHide | WindowToggleOpacity | WindowClose | AppQuit
11 changes: 10 additions & 1 deletion src/axidev_osk/runtime/event_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
KeyboardRegisterKeySpec,
KeyboardKeyUp,
KeyboardSyncLatchedKey,
SecureInputPanelPrepare,
SecureInputPanelRelease,
StateSet,
WindowClose,
WindowHide,
Expand Down Expand Up @@ -69,6 +71,14 @@ def register_context_command_handlers(registry: EventHandlerRegistry) -> None:
def register_event_handlers(registry: EventHandlerRegistry) -> None:
"""Register application-level runtime handlers in deterministic order."""

registry.register_command_handler(
SecureInputPanelPrepare,
lambda runtime: lambda command: runtime._prepare_secure_input_panel(),
)
registry.register_command_handler(
SecureInputPanelRelease,
lambda runtime: lambda command: runtime._release_secure_input_panel(),
)
registry.register_command_handler(
WindowShow,
lambda runtime: lambda command: runtime._window_manager.show(command.window_id),
Expand All @@ -94,7 +104,6 @@ def register_event_handlers(registry: EventHandlerRegistry) -> None:
lambda runtime: lambda command: runtime._app.exit(command.exit_code),
)
registry.register_event_handler(lambda runtime: runtime._handle_window_close_requested)
registry.register_event_handler(lambda runtime: runtime._handle_screen_lock_state_changed)
registry.register_event_handler(lambda runtime: runtime._handle_hot_corner_triggered)
registry.register_event_handler(lambda runtime: runtime._handle_component_pressed)

Expand Down
8 changes: 0 additions & 8 deletions src/axidev_osk/runtime/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,6 @@ class HotCornerTriggered:
corner: str


@dataclass(frozen=True, slots=True)
class ScreenLockStateChanged:
"""The desktop session entered or left its locked state."""

locked: bool


@dataclass(frozen=True, slots=True)
class WindowCloseRequested:
"""A managed window requested application shutdown confirmation.
Expand Down Expand Up @@ -143,7 +136,6 @@ class PromptResolved:
| BackendKeyStateChanged
| KeyLatchChanged
| HotCornerTriggered
| ScreenLockStateChanged
| WindowCloseRequested
| PromptResolved
)
5 changes: 0 additions & 5 deletions src/axidev_osk/runtime/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ def _handle_window_close_requested(self, event: object) -> None:
if isinstance(event, WindowCloseRequested):
self._dispatcher.dispatch_command(AppQuit())

def _handle_screen_lock_state_changed(self, event: object) -> None:
"""Ignore platform lock-state events in the generic test runtime."""

del event

def _handle_hot_corner_triggered(self, event: object) -> None:
"""Route hot-corner visibility commands through production helper."""

Expand Down
110 changes: 0 additions & 110 deletions src/axidev_osk/services/kwin_lock.py

This file was deleted.

62 changes: 62 additions & 0 deletions src/axidev_osk/services/secure_input_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Session D-Bus control surface for the Plasma lock-screen button."""

from __future__ import annotations

from typing import TYPE_CHECKING

from PySide6.QtCore import QObject, Slot
from PySide6.QtDBus import QDBusConnection

from ..runtime.commands import SecureInputPanelPrepare, SecureInputPanelRelease

if TYPE_CHECKING:
from ..runtime.context import Context

SERVICE_NAME = "org.axidev.OSK.LockScreen"
OBJECT_PATH = "/org/axidev/OSK/LockScreen"


class SecureInputPanelService(QObject):
"""Translate lock-screen D-Bus requests into runtime commands."""

def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent)
self._connection = QDBusConnection.sessionBus()
self._context: Context | None = None

def start(self, context: Context) -> None:
"""Publish the lock-screen control object on the session bus."""

if not self._connection.isConnected():
raise RuntimeError("KDE session bus is unavailable")
if not self._connection.registerService(SERVICE_NAME):
raise RuntimeError(f"Cannot register D-Bus service {SERVICE_NAME}")
if not self._connection.registerObject(
OBJECT_PATH,
self,
QDBusConnection.RegisterOption.ExportAllSlots,
):
self._connection.unregisterService(SERVICE_NAME)
raise RuntimeError(f"Cannot register D-Bus object {OBJECT_PATH}")
self._context = context

def stop(self) -> None:
"""Remove the lock-screen control object from the session bus."""

self._context = None
self._connection.unregisterObject(OBJECT_PATH)
self._connection.unregisterService(SERVICE_NAME)

@Slot()
def prepare(self) -> None:
"""Request secure input-panel creation through the runtime queue."""

if self._context is not None:
self._context.dispatcher.dispatch_command(SecureInputPanelPrepare())

@Slot()
def release(self) -> None:
"""Request secure input-panel cleanup through the runtime queue."""

if self._context is not None:
self._context.dispatcher.dispatch_command(SecureInputPanelRelease())
Loading