Files
DankMaterialShell/quickshell/Common/PopoutManager.qml
T

503 lines
18 KiB
QML

pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import qs.Common
Singleton {
id: root
property var currentPopoutsByScreen: ({})
property var currentPopoutTriggers: ({})
// Set by the screenshot IPC handshake (dms screenshot region select); cleared by end() or any popout/modal open.
property bool screenshotActive: false
signal popoutOpening
signal popoutChanged
signal popoutToggledClosed(string screenName)
property real hoverCursorGlobalX: 0
property real hoverCursorGlobalY: 0
function updateHoverCursor(gx, gy) {
hoverCursorGlobalX = gx;
hoverCursorGlobalY = gy;
}
function cursorOverBar(gx, gy, padding, excludedWindow) {
const pad = padding !== undefined ? padding : 16;
const bars = KeyboardFocus.barWindows || [];
for (let i = 0; i < bars.length; i++) {
const w = bars[i];
if (!w?.visible || w === excludedWindow)
continue;
if (typeof w.containsGlobalPoint === "function") {
if (w.containsGlobalPoint(gx, gy, pad))
return true;
continue;
}
const item = w.contentItem;
if (!item || typeof item.mapToItem !== "function")
continue;
const topLeft = item.mapToItem(null, 0, 0);
if (!topLeft)
continue;
if (gx >= topLeft.x - pad && gx < topLeft.x + item.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + item.height + pad)
return true;
}
return false;
}
function _openPopout(popout) {
if (popout.dashVisible !== undefined) {
let flagStayedTrue = popout.dashVisible === true;
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing) {
popout.dashVisible = false;
flagStayedTrue = false;
}
popout.dashVisible = true;
// Flag already true (e.g. retargeting to another monitor) won't re-fire
// the change handler, so drive the surface open explicitly.
if (flagStayedTrue)
_ensureSurfaceOpen(popout);
return;
}
if (popout.notificationHistoryVisible !== undefined) {
const flagStayedTrue = popout.notificationHistoryVisible === true;
popout.notificationHistoryVisible = true;
if (flagStayedTrue)
_ensureSurfaceOpen(popout);
return;
}
_ensureSurfaceOpen(popout);
}
function _ensureSurfaceOpen(popout) {
if (typeof popout.present === "function") {
popout.present();
return;
}
if (typeof popout.open === "function")
popout.open();
}
function _closePopout(popout) {
try {
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
switch (true) {
case popout.dashVisible !== undefined:
popout.dashVisible = false;
return;
case popout.notificationHistoryVisible !== undefined:
popout.notificationHistoryVisible = false;
return;
default:
if (typeof popout.close !== "function")
return;
popout.close();
}
} catch (e) {
return;
}
}
function _dismissPopoutFromHover(popout) {
try {
if (!popout || popout.hoverDismissEnabled !== true)
return;
if (typeof popout.closeFromHoverDismiss === "function") {
popout.closeFromHoverDismiss();
return;
}
_closePopout(popout);
} catch (e) {
return;
}
}
function _isStale(popout) {
try {
if (!popout || !("shouldBeVisible" in popout))
return true;
if (!popout.screen)
return true;
return false;
} catch (e) {
return true;
}
}
function showPopout(popout) {
if (!popout || !popout.screen)
return;
screenshotActive = false;
popoutOpening();
const screenName = popout.screen.name;
for (const otherScreenName in currentPopoutsByScreen) {
const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout || otherPopout === popout)
continue;
if (_isStale(otherPopout)) {
currentPopoutsByScreen[otherScreenName] = null;
continue;
}
_closePopout(otherPopout);
}
currentPopoutsByScreen[screenName] = popout;
popoutChanged();
ModalManager.closeAllModalsExcept(null);
}
function hidePopout(popout) {
if (!popout || !popout.screen)
return;
const screenName = popout.screen.name;
if (currentPopoutsByScreen[screenName] === popout) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
popoutChanged();
}
}
function closeAllPopouts() {
for (const screenName in currentPopoutsByScreen) {
const popout = currentPopoutsByScreen[screenName];
if (!popout || _isStale(popout))
continue;
_closePopout(popout);
}
// Keep map entries until each popout's close animation finishes (hidePopout).
}
function dismissAllForScreen(screenName) {
if (!screenName)
return;
if (currentPopoutsByScreen[screenName])
closeAllPopouts();
if (ModalManager.currentModalsByScreen[screenName])
ModalManager.closeAllModalsExcept(null);
TrayMenuManager.closeAllMenus();
}
function closePopoutForScreen(screen) {
if (!screen)
return;
const screenName = screen.name;
const popout = currentPopoutsByScreen[screenName];
if (!popout || _isStale(popout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
return;
}
_closePopout(popout);
}
function dismissHoverPopoutForScreen(screen) {
if (!screen)
return;
const screenName = screen.name;
const popout = currentPopoutsByScreen[screenName];
if (!popout || _isStale(popout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
return;
}
_dismissPopoutFromHover(popout);
}
function cancelHoverDismiss(screen) {
const popout = getActivePopout(screen);
if (popout?.cancelHoverDismiss)
popout.cancelHoverDismiss();
}
function getActivePopout(screen) {
if (!screen)
return null;
return currentPopoutsByScreen[screen.name] || null;
}
// Checks if the active popout is pinned for auto-dismissal
function isActivePopoutPinned(screen) {
const p = getActivePopout(screen);
if (!p || !p.shouldBeVisible)
return false;
const dismissSuspended = p.effectiveHoverDismissSuspended ?? p.hoverDismissSuspended;
return p.hoverDismissEnabled === false || dismissSuspended === true;
}
function isCurrentPopout(popout, screenName) {
const name = screenName || popout?.screen?.name || "";
return !!name && currentPopoutsByScreen[name] === popout;
}
function _requestPopout(popout, tabIndex, triggerSource, hoverRequest) {
if (!popout || !popout.screen)
return;
// Clicks pin a popout, while clicking its active trigger toggles it closed below.
if (!hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
screenshotActive = false;
const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName];
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
const alreadyPresented = currentPopout === popout && popout.shouldBeVisible;
const willOpen = !(alreadyPresented && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
if (willOpen)
popoutOpening();
let movedFromOtherScreen = false;
for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName)
continue;
const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout)
continue;
if (_isStale(otherPopout)) {
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
if (otherPopout === popout) {
movedFromOtherScreen = true;
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
_closePopout(otherPopout);
}
if (currentPopout && currentPopout !== popout) {
if (_isStale(currentPopout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
} else {
if (hoverRequest && typeof currentPopout.beginSupersededClose === "function")
currentPopout.beginSupersededClose();
_closePopout(currentPopout);
}
}
if (alreadyPresented && !movedFromOtherScreen) {
const sameDefinedTrigger = triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId;
if (hoverRequest && sameDefinedTrigger)
return;
if (!hoverRequest && (triggerId === undefined || sameDefinedTrigger)) {
_closePopout(popout);
popoutToggledClosed(screenName);
return;
}
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex;
}
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
currentPopoutTriggers[screenName] = triggerId;
if (hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = true;
return;
}
currentPopoutTriggers[screenName] = triggerId;
currentPopoutsByScreen[screenName] = popout;
popoutChanged();
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex;
}
if (currentPopout !== popout) {
ModalManager.closeAllModalsExcept(null);
}
if (hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = true;
_openPopout(popout);
}
function requestPopout(popout, tabIndex, triggerSource) {
_requestPopout(popout, tabIndex, triggerSource, false);
}
function requestHoverPopout(popout, tabIndex, triggerSource) {
_requestPopout(popout, tabIndex, triggerSource, true);
}
function _islandBarBand(host, screen) {
const vertical = host.edge === "left" || host.edge === "right";
if (host.embedded) {
const start = vertical ? host.hostOriginX : host.hostOriginY;
return {
"start": start,
"end": start + host.bandThickness
};
}
if (host.freeMode)
return null;
const extent = vertical ? screen.width : screen.height;
const instances = ShellLayout.forScreen(screen)?.instances ?? [];
let start = Infinity;
let end = -Infinity;
for (const instance of instances) {
if (instance.edge !== host.edge || instance.barId === host.barId || (instance.kind !== "bar" && instance.kind !== "frame") || instance.rowThickness <= 0)
continue;
const config = SettingsData.getBarConfig(instance.barId);
if (!config || config.enabled === false || config.visible === false)
continue;
const near = instance.rowOffset ?? 0;
const far = near + instance.rowThickness;
const bandStart = host.edge === "top" || host.edge === "left" ? near : extent - far;
const bandEnd = host.edge === "top" || host.edge === "left" ? far : extent - near;
start = Math.min(start, bandStart);
end = Math.max(end, bandEnd);
}
return end > start ? {
"start": start,
"end": end
} : null;
}
function getActiveSurfaces() {
const results = [];
// The host registry covers embedded, standalone, and free Islands with their live visual bounds.
for (const key of Object.keys(IslandHostRegistry.hosts || {})) {
const host = IslandHostRegistry.hosts[key];
const screen = host?.screen;
const controller = host?.islandController;
const snapVisible = controller?.expanded || (!host?.embedded && host?.motionRunning);
if (!screen?.name || !snapVisible || host.surfaceOpacity <= 0)
continue;
const rect = host.surfaceBodyRect?.();
if (!rect || rect.width <= 0 || rect.height <= 0)
continue;
let x = rect.x;
let y = rect.y;
let width = rect.width;
let height = rect.height;
const barBand = _islandBarBand(host, screen);
if (barBand) {
switch (host.edge) {
case "top":
if (y < barBand.end && y + height > barBand.start) {
const inset = Math.max(0, Math.min(height, barBand.end - y));
y += inset;
height -= inset;
}
break;
case "bottom":
if (y + height > barBand.start && y < barBand.end)
height = Math.max(0, barBand.start - y);
break;
case "left":
if (x < barBand.end && x + width > barBand.start) {
const inset = Math.max(0, Math.min(width, barBand.end - x));
x += inset;
width -= inset;
}
break;
case "right":
if (x + width > barBand.start && x < barBand.end)
width = Math.max(0, barBand.start - x);
break;
}
}
const left = Math.max(0, x);
const top = Math.max(0, y);
const right = Math.min(screen.width, x + width);
const bottom = Math.min(screen.height, y + height);
if (right <= left || bottom <= top)
continue;
results.push({
"name": "dms:dankisland-surface",
"type": "island",
"x": Math.round((screen.x ?? 0) + left),
"y": Math.round((screen.y ?? 0) + top),
"width": Math.round(right - left),
"height": Math.round(bottom - top)
});
}
for (const screenName in currentPopoutsByScreen) {
const popout = currentPopoutsByScreen[screenName];
if (!popout || !popout.shouldBeVisible)
continue;
const host = popout.impl?.item ?? popout;
if (!host || !host.shouldBeVisible || host.isClosing)
continue;
const screen = host.surfaceScreen ?? popout.screen ?? null;
const sx = screen?.x ?? 0;
const sy = screen?.y ?? 0;
const px = host.renderedAlignedX;
const py = host.renderedAlignedY;
const pw = host.renderedAlignedWidth ?? popout.popupWidth ?? 0;
const ph = host.renderedAlignedHeight ?? popout.popupHeight ?? 0;
// Skip if origin is not yet laid out (undefined/null means not positioned).
if (px == null || py == null || pw <= 0 || ph <= 0)
continue;
results.push({
"name": popout.layerNamespace || "popout",
"type": "popout",
"x": Math.round(sx + px),
"y": Math.round(sy + py),
"width": Math.round(pw),
"height": Math.round(ph)
});
}
const modals = ModalManager.currentModalsByScreen || {};
for (const screenName in modals) {
const modal = modals[screenName];
if (!modal || !modal.shouldBeVisible)
continue;
const host = modal.impl?.item ?? modal;
if (!host || !host.shouldBeVisible || host.isClosing)
continue;
const screen = host.effectiveScreen ?? modal.targetScreen ?? null;
const sx = screen?.x ?? 0;
const sy = screen?.y ?? 0;
const mx = host.alignedX;
const my = host.alignedY;
const mw = host.alignedWidth ?? modal.modalWidth ?? 0;
const mh = host.alignedHeight ?? modal.modalHeight ?? 0;
// Skip if origin is not yet laid out.
if (mx == null || my == null || mw <= 0 || mh <= 0)
continue;
results.push({
"name": modal.layerNamespace || "modal",
"type": "modal",
"x": Math.round(sx + mx),
"y": Math.round(sy + my),
"width": Math.round(mw),
"height": Math.round(mh)
});
}
return results;
}
}