PluginSystem: refactored CurrentScreenDetector so it can be used by core IPC calls AND plugins IPC calls.

This commit is contained in:
Lemmy
2025-12-14 15:52:36 -05:00
parent e71d336085
commit fa989dd962
4 changed files with 202 additions and 146 deletions
+105
View File
@@ -0,0 +1,105 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
/**
* Detects which screen the cursor is currently on by creating a temporary
* invisible PanelWindow. Use withTargetScreen() to get the screen asynchronously.
*
* Usage:
* CurrentScreenDetector {
* id: screenDetector
* }
*
* function doSomething() {
* screenDetector.withTargetScreen(function(screen) {
* // screen is the ShellScreen where cursor is
* })
* }
*/
Item {
id: root
// Pending callback to execute once screen is detected
property var pendingCallback: null
// Detected screen
property var detectedScreen: null
// Signal emitted when screen is detected from the PanelWindow
signal screenDetected(var detectedScreen)
onScreenDetected: function (detectedScreen) {
root.detectedScreen = detectedScreen;
screenDetectorDebounce.restart();
}
/**
* Execute callback with the screen where the cursor currently is.
* On single-monitor setups, executes immediately.
* On multi-monitor setups, briefly opens an invisible window to detect the screen.
*/
function withTargetScreen(callback: var): void {
if (root.pendingCallback) {
Logger.w("CurrentScreenDetector", "Another detection is pending, ignoring new call");
return;
}
// Single monitor setup can execute immediately
if (Quickshell.screens.length === 1) {
callback(Quickshell.screens[0]);
} else {
// Multi-monitor setup needs async detection
root.detectedScreen = null;
root.pendingCallback = callback;
screenDetectorLoader.active = true;
}
}
Timer {
id: screenDetectorDebounce
running: false
interval: 20
onTriggered: {
Logger.d("CurrentScreenDetector", "Screen debounced to:", root.detectedScreen?.name || "null");
// Execute pending callback if any
if (root.pendingCallback) {
if (!Settings.data.general.allowPanelsOnScreenWithoutBar) {
// If we explicitly disabled panels on screen without bar, check if bar is configured
// for this screen, and fallback to primary screen if necessary
var monitors = Settings.data.bar.monitors || [];
const hasBar = monitors.length === 0 || monitors.includes(root.detectedScreen?.name);
if (!hasBar) {
root.detectedScreen = Quickshell.screens[0];
}
}
Logger.d("CurrentScreenDetector", "Executing callback on screen:", root.detectedScreen.name);
root.pendingCallback(root.detectedScreen);
root.pendingCallback = null;
}
// Clean up
screenDetectorLoader.active = false;
}
}
// Invisible dummy PanelWindow to detect which screen should receive the action
Loader {
id: screenDetectorLoader
active: false
sourceComponent: PanelWindow {
implicitWidth: 0
implicitHeight: 0
color: Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-screen-detector"
mask: Region {}
onScreenChanged: root.screenDetected(screen)
}
}
}
+71 -145
View File
@@ -18,6 +18,9 @@ import qs.Services.UI
Item {
id: root
// Screen detector passed from shell.qml
required property CurrentScreenDetector screenDetector
IpcHandler {
target: "bar"
function toggle() {
@@ -40,10 +43,10 @@ Item {
if (Settings.data.ui.settingsPanelMode === "window") {
SettingsPanelService.toggleWindow();
} else {
root.withTargetScreen(screen => {
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel?.toggle();
});
root.screenDetector.withTargetScreen(screen => {
var settingsPanel = PanelService.getPanel("settingsPanel", screen);
settingsPanel?.toggle();
});
}
}
}
@@ -51,10 +54,10 @@ Item {
IpcHandler {
target: "calendar"
function toggle() {
root.withTargetScreen(screen => {
var clockPanel = PanelService.getPanel("clockPanel", screen);
clockPanel?.toggle(null, "Clock");
});
root.screenDetector.withTargetScreen(screen => {
var clockPanel = PanelService.getPanel("clockPanel", screen);
clockPanel?.toggle(null, "Clock");
});
}
}
@@ -62,10 +65,10 @@ Item {
target: "notifications"
function toggleHistory() {
// Will attempt to open the panel next to the bar button if any.
root.withTargetScreen(screen => {
var notificationHistoryPanel = PanelService.getPanel("notificationHistoryPanel", screen);
notificationHistoryPanel.toggle(null, "NotificationHistory");
});
root.screenDetector.withTargetScreen(screen => {
var notificationHistoryPanel = PanelService.getPanel("notificationHistoryPanel", screen);
notificationHistoryPanel.toggle(null, "NotificationHistory");
});
}
function toggleDND() {
NotificationService.doNotDisturb = !NotificationService.doNotDisturb;
@@ -103,39 +106,39 @@ Item {
IpcHandler {
target: "launcher"
function toggle() {
root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen || (launcherPanel?.isPanelOpen && !launcherPanel?.activePlugin))
launcherPanel?.toggle();
launcherPanel?.setSearchText("");
});
root.screenDetector.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen || (launcherPanel?.isPanelOpen && !launcherPanel?.activePlugin))
launcherPanel?.toggle();
launcherPanel?.setSearchText("");
});
}
function clipboard() {
root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen) {
launcherPanel?.toggle();
}
launcherPanel?.setSearchText(">clip ");
});
root.screenDetector.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen) {
launcherPanel?.toggle();
}
launcherPanel?.setSearchText(">clip ");
});
}
function calculator() {
root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen) {
launcherPanel?.toggle();
}
launcherPanel?.setSearchText(">calc ");
});
root.screenDetector.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen) {
launcherPanel?.toggle();
}
launcherPanel?.setSearchText(">calc ");
});
}
function emoji() {
root.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen) {
launcherPanel?.toggle();
}
launcherPanel?.setSearchText(">emoji ");
});
root.screenDetector.withTargetScreen(screen => {
var launcherPanel = PanelService.getPanel("launcherPanel", screen);
if (!launcherPanel?.isPanelOpen) {
launcherPanel?.toggle();
}
launcherPanel?.setSearchText(">emoji ");
});
}
}
@@ -206,10 +209,10 @@ Item {
IpcHandler {
target: "sessionMenu"
function toggle() {
root.withTargetScreen(screen => {
var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen);
sessionMenuPanel?.toggle();
});
root.screenDetector.withTargetScreen(screen => {
var sessionMenuPanel = PanelService.getPanel("sessionMenuPanel", screen);
sessionMenuPanel?.toggle();
});
}
function lockAndSuspend() {
@@ -220,15 +223,15 @@ Item {
IpcHandler {
target: "controlCenter"
function toggle() {
root.withTargetScreen(screen => {
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
if (Settings.data.controlCenter.position === "close_to_bar_button") {
// Will attempt to open the panel next to the bar button if any.
controlCenterPanel?.toggle(null, "ControlCenter");
} else {
controlCenterPanel?.toggle();
}
});
root.screenDetector.withTargetScreen(screen => {
var controlCenterPanel = PanelService.getPanel("controlCenterPanel", screen);
if (Settings.data.controlCenter.position === "close_to_bar_button") {
// Will attempt to open the panel next to the bar button if any.
controlCenterPanel?.toggle(null, "ControlCenter");
} else {
controlCenterPanel?.toggle();
}
});
}
}
@@ -244,10 +247,10 @@ Item {
target: "wallpaper"
function toggle() {
if (Settings.data.wallpaper.enabled) {
root.withTargetScreen(screen => {
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen);
wallpaperPanel?.toggle();
});
root.screenDetector.withTargetScreen(screen => {
var wallpaperPanel = PanelService.getPanel("wallpaperPanel", screen);
wallpaperPanel?.toggle();
});
}
}
@@ -309,10 +312,10 @@ Item {
NetworkService.setWifiEnabled(false);
}
function togglePanel() {
root.withTargetScreen(screen => {
var wifiPanel = PanelService.getPanel("wifiPanel", screen);
wifiPanel?.toggle(null, "WiFi");
});
root.screenDetector.withTargetScreen(screen => {
var wifiPanel = PanelService.getPanel("wifiPanel", screen);
wifiPanel?.toggle(null, "WiFi");
});
}
}
@@ -328,20 +331,20 @@ Item {
BluetoothService.setBluetoothEnabled(false);
}
function togglePanel() {
root.withTargetScreen(screen => {
var bluetoothPanel = PanelService.getPanel("bluetoothPanel", screen);
bluetoothPanel?.toggle(null, "Bluetooth");
});
root.screenDetector.withTargetScreen(screen => {
var bluetoothPanel = PanelService.getPanel("bluetoothPanel", screen);
bluetoothPanel?.toggle(null, "Bluetooth");
});
}
}
IpcHandler {
target: "battery"
function togglePanel() {
root.withTargetScreen(screen => {
var batteryPanel = PanelService.getPanel("batteryPanel", screen);
batteryPanel?.toggle(null, "Battery");
});
root.screenDetector.withTargetScreen(screen => {
var batteryPanel = PanelService.getPanel("batteryPanel", screen);
batteryPanel?.toggle(null, "Battery");
});
}
}
@@ -442,81 +445,4 @@ Item {
}
}
}
// -------------------------------------------------------------------
// Queue an IPC panel operation - will execute when screen is detected
// -------------------------------------------------------------------
function withTargetScreen(callback) {
if (pendingCallback) {
Logger.w("IPC", "Another IPC call is pending, ignoring new call");
return;
}
// Single monitor setup can execute immediately
if (Quickshell.screens.length === 1) {
callback(Quickshell.screens[0]);
} else {
// Multi-monitors setup needs to start async detection
detectedScreen = null;
pendingCallback = callback;
screenDetectorLoader.active = true;
}
}
/**
* For IPC calls on multi-monitors setup that will open panels on screen,
* we need to open a QS PanelWindow and wait for it's "screen" property to stabilize.
*/
property ShellScreen detectedScreen: null
property var pendingCallback: null
Timer {
id: screenDetectorDebounce
running: false
interval: 20
onTriggered: {
Logger.d("IPC", "Screen debounced to:", detectedScreen?.name || "null");
// Execute pending callback if any
if (pendingCallback) {
if (!Settings.data.general.allowPanelsOnScreenWithoutBar) {
// If we explicitely disabled panels on screen without bar, check if bar is configured
// for this screen, and fallback to primary screen if necessary
var monitors = Settings.data.bar.monitors || [];
const hasBar = monitors.length === 0 || monitors.includes(detectedScreen?.name);
if (!hasBar) {
detectedScreen = Quickshell.screens[0];
}
}
Logger.d("IPC", "Executing pending IPC callback on screen:", detectedScreen.name);
pendingCallback(detectedScreen);
pendingCallback = null;
}
// Clean up
screenDetectorLoader.active = false;
}
}
// Invisible dummy PanelWindow to detect which screen should receive IPC calls
Loader {
id: screenDetectorLoader
active: false
sourceComponent: PanelWindow {
implicitWidth: 0
implicitHeight: 0
color: Color.transparent
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "noctalia-ipc-screen-detector"
mask: Region {}
onScreenChanged: {
detectedScreen = screen;
screenDetectorDebounce.restart();
}
}
}
// -------------------------------------------------------------------
// -------------------------------------------------------------------
}
+15
View File
@@ -47,6 +47,9 @@ Singleton {
// Plugin container from shell.qml (for placing Main instances in graphics scene)
property var pluginContainer: null
// Screen detector from shell.qml (for withTargetScreen in plugin API)
property var screenDetector: null
// Track if we need to initialize once container is ready
property bool needsInit: false
@@ -579,6 +582,7 @@ Singleton {
property var saveSettings: null
property var openPanel: null
property var closePanel: null
property var withTargetScreen: null
property var tr: null
property var trp: null
property var hasTranslation: null
@@ -650,6 +654,17 @@ Singleton {
return false;
};
// ----------------------------------------
api.withTargetScreen = function (callback) {
// Detect which screen the cursor is on and call callback with that screen
if (!root.screenDetector) {
Logger.w("PluginAPI", "Screen detector not available, using primary screen");
callback(Quickshell.screens[0]);
return;
}
root.screenDetector.withTargetScreen(callback);
};
// ----------------------------------------
// Translation function
api.tr = function (key, interpolations) {
+11 -1
View File
@@ -121,15 +121,25 @@ ShellRoot {
// Settings window mode (single window across all monitors)
SettingsPanelWindow {}
// Shared screen detector for IPC and plugins
CurrentScreenDetector {
id: screenDetector
}
// IPCService is treated as a service but it must be in graphics scene.
IPCService {}
IPCService {
id: ipcService
screenDetector: screenDetector
}
// Container for plugins Main.qml instances (must be in graphics scene)
Item {
id: pluginContainer
visible: false
Component.onCompleted: {
PluginService.pluginContainer = pluginContainer;
PluginService.screenDetector = screenDetector;
}
}