1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-28 14:05:21 -04:00

feat(HoverMode): implement hover popout & launcher functionality in all modes

- New Hover toggle found in DankBar Settings
- New Hover to Reveal Launcher in FrameTab Settings
This commit is contained in:
purian23
2026-06-27 22:47:38 -04:00
parent 48f6a0c632
commit 6bee1b2c86
32 changed files with 2266 additions and 364 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.10.1 rev: v2.12.2
hooks: hooks:
- id: golangci-lint-fmt - id: golangci-lint-fmt
require_serial: true require_serial: true
+1 -1
View File
@@ -22,7 +22,6 @@ require (
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
go.etcd.io/bbolt v1.4.3 go.etcd.io/bbolt v1.4.3
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 go4.org/mem v0.0.0-20240501181205-ae6ca9944745
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f
golang.org/x/image v0.39.0 golang.org/x/image v0.39.0
tailscale.com v1.96.5 tailscale.com v1.96.5
) )
@@ -64,6 +63,7 @@ require (
github.com/yeqown/reedsolomon v1.0.0 // indirect github.com/yeqown/reedsolomon v1.0.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.50.0 // indirect golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
+2 -2
View File
@@ -1,8 +1,8 @@
package utils package utils
import "golang.org/x/exp/constraints" import "cmp"
func Clamp[T constraints.Ordered](val, min, max T) T { func Clamp[T cmp.Ordered](val, min, max T) T {
if val < min { if val < min {
return min return min
} }
+135 -22
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import QtQuick import QtQuick
import qs.Common
Singleton { Singleton {
id: root id: root
@@ -16,8 +17,76 @@ Singleton {
signal popoutOpening signal popoutOpening
signal popoutChanged signal popoutChanged
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 _isPopoutPresented(popout) {
if (!popout)
return false;
try {
if (popout.dashVisible !== undefined)
return !!popout.dashVisible;
if (popout.notificationHistoryVisible !== undefined)
return !!popout.notificationHistoryVisible;
return !!(popout.shouldBeVisible || popout.isClosing);
} catch (e) {
return false;
}
}
function _openPopout(popout) {
if (popout.dashVisible !== undefined) {
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing)
popout.dashVisible = false;
popout.dashVisible = true;
return;
}
if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true;
return;
}
popout.open();
}
function _closePopout(popout) { function _closePopout(popout) {
try { try {
if (popout?.hoverDismissEnabled) {
if (typeof popout.closeFromHoverDismiss === "function") {
popout.closeFromHoverDismiss();
return;
}
}
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
switch (true) { switch (true) {
case popout.dashVisible !== undefined: case popout.dashVisible !== undefined:
popout.dashVisible = false; popout.dashVisible = false;
@@ -89,7 +158,26 @@ Singleton {
continue; continue;
_closePopout(popout); _closePopout(popout);
} }
currentPopoutsByScreen = {}; // Keep map entries until each popout's close animation finishes (hidePopout).
}
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 cancelHoverDismiss(screen) {
const popout = getActivePopout(screen);
if (popout?.cancelHoverDismiss)
popout.cancelHoverDismiss();
} }
function getActivePopout(screen) { function getActivePopout(screen) {
@@ -98,23 +186,37 @@ Singleton {
return currentPopoutsByScreen[screen.name] || 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 || !_isPopoutPresented(p))
return false;
return p.hoverDismissEnabled === false || p.hoverDismissSuspended === true;
}
function isCurrentPopout(popout, screenName) { function isCurrentPopout(popout, screenName) {
const name = screenName || popout?.screen?.name || ""; const name = screenName || popout?.screen?.name || "";
return !!name && currentPopoutsByScreen[name] === popout; return !!name && currentPopoutsByScreen[name] === popout;
} }
function requestPopout(popout, tabIndex, triggerSource) { function _requestPopout(popout, tabIndex, triggerSource, hoverRequest) {
if (!popout || !popout.screen) if (!popout || !popout.screen)
return; return;
// Clicking a transient popout pins it instead of toggling it closed.
const wasTransient = popout.hoverDismissEnabled === true;
if (!hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
screenshotActive = false; screenshotActive = false;
const screenName = popout.screen.name; const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName]; const currentPopout = currentPopoutsByScreen[screenName];
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex; const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
const alreadyPresented = currentPopout === popout && (hoverRequest ? _isPopoutPresented(popout) : popout.shouldBeVisible);
const willOpen = !(currentPopout === popout && popout.shouldBeVisible && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId); const willOpen = !(alreadyPresented && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
if (willOpen) { if (willOpen)
popoutOpening(); popoutOpening();
}
let movedFromOtherScreen = false; let movedFromOtherScreen = false;
for (const otherScreenName in currentPopoutsByScreen) { for (const otherScreenName in currentPopoutsByScreen) {
@@ -145,18 +247,26 @@ Singleton {
currentPopoutsByScreen[screenName] = null; currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null; currentPopoutTriggers[screenName] = null;
} else { } else {
if (hoverRequest && typeof currentPopout.beginSupersededClose === "function")
currentPopout.beginSupersededClose();
_closePopout(currentPopout); _closePopout(currentPopout);
} }
} }
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) { if (alreadyPresented && !movedFromOtherScreen) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) { const sameDefinedTrigger = triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId;
_closePopout(popout); if (hoverRequest && sameDefinedTrigger)
return; return;
}
if (triggerId === undefined) { if (!hoverRequest && (triggerId === undefined || sameDefinedTrigger)) {
_closePopout(popout); if (!wasTransient) {
_closePopout(popout);
return;
}
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
if (triggerId !== undefined)
currentPopoutTriggers[screenName] = triggerId;
return; return;
} }
@@ -166,6 +276,8 @@ Singleton {
if (popout.updateSurfacePosition) if (popout.updateSurfacePosition)
popout.updateSurfacePosition(); popout.updateSurfacePosition();
currentPopoutTriggers[screenName] = triggerId; currentPopoutTriggers[screenName] = triggerId;
if (hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = true;
return; return;
} }
@@ -181,16 +293,17 @@ Singleton {
ModalManager.closeAllModalsExcept(null); ModalManager.closeAllModalsExcept(null);
} }
if (movedFromOtherScreen) { if (hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.open(); popout.hoverDismissEnabled = true;
} else {
if (popout.dashVisible !== undefined) { _openPopout(popout);
popout.dashVisible = true; }
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true; function requestPopout(popout, tabIndex, triggerSource) {
} else { _requestPopout(popout, tabIndex, triggerSource, false);
popout.open(); }
}
} function requestHoverPopout(popout, tabIndex, triggerSource) {
_requestPopout(popout, tabIndex, triggerSource, true);
} }
} }
+48 -4
View File
@@ -291,6 +291,8 @@ Singleton {
onFrameLauncherEmergeSideChanged: saveSettings() onFrameLauncherEmergeSideChanged: saveSettings()
property bool frameLauncherArcExtender: false property bool frameLauncherArcExtender: false
onFrameLauncherArcExtenderChanged: saveSettings() onFrameLauncherArcExtenderChanged: saveSettings()
property bool frameLauncherEdgeHover: false
onFrameLauncherEdgeHoverChanged: saveSettings()
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top" readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
property string frameMode: "connected" property string frameMode: "connected"
onFrameModeChanged: saveSettings() onFrameModeChanged: saveSettings()
@@ -603,9 +605,9 @@ Singleton {
if (!on && id !== "settings" && current.filter(t => t.enabled && t.id !== "settings").length <= 1) if (!on && id !== "settings" && current.filter(t => t.enabled && t.id !== "settings").length <= 1)
return; return;
dashTabs = current.map(t => t.id === id ? { dashTabs = current.map(t => t.id === id ? {
"id": t.id, "id": t.id,
"enabled": on "enabled": on
} : t); } : t);
} }
function resetDashTabs() { function resetDashTabs() {
@@ -999,7 +1001,9 @@ Singleton {
"shadowOpacity": 60, "shadowOpacity": 60,
"shadowColorMode": "default", "shadowColorMode": "default",
"shadowCustomColor": "#000000", "shadowCustomColor": "#000000",
"clickThrough": false "clickThrough": false,
"hoverPopouts": false,
"hoverPopoutDelay": 150
} }
] ]
@@ -2436,6 +2440,46 @@ Singleton {
return barConfigs.filter(cfg => cfg.enabled); return barConfigs.filter(cfg => cfg.enabled);
} }
function _sideToPosition(side) {
switch (side) {
case "top":
return SettingsData.Position.Top;
case "bottom":
return SettingsData.Position.Bottom;
case "left":
return SettingsData.Position.Left;
case "right":
return SettingsData.Position.Right;
}
return -1;
}
// Check if a bar occupies the specified screen edge
function barOccupiesSide(screen, side) {
if (!screen)
return false;
const sidePos = _sideToPosition(side);
if (sidePos < 0)
return false;
const bars = getEnabledBarConfigs();
for (var i = 0; i < bars.length; i++) {
const bc = bars[i];
if (bc.position !== sidePos)
continue;
const prefs = bc.screenPreferences || ["all"];
if (prefs.includes("all") || isScreenInPreferences(screen, prefs))
return true;
}
return false;
}
// Check if the dock occupies the specified screen edge.
function dockOccupiesSide(side) {
if (!showDock)
return false;
return dockPosition === _sideToPosition(side);
}
function getScreensSortedByPosition() { function getScreensSortedByPosition() {
const screens = []; const screens = [];
for (var i = 0; i < Quickshell.screens.length; i++) { for (var i = 0; i < Quickshell.screens.length; i++) {
+4 -1
View File
@@ -569,7 +569,9 @@ var SPEC = {
shadowOpacity: 60, shadowOpacity: 60,
shadowColorMode: "default", shadowColorMode: "default",
shadowCustomColor: "#000000", shadowCustomColor: "#000000",
clickThrough: false clickThrough: false,
hoverPopouts: false,
hoverPopoutDelay: 150
}], onChange: "updateBarConfigs" }], onChange: "updateBarConfigs"
}, },
@@ -642,6 +644,7 @@ var SPEC = {
frameCloseGaps: { def: true }, frameCloseGaps: { def: true },
frameLauncherEmergeSide: { def: "bottom" }, frameLauncherEmergeSide: { def: "bottom" },
frameLauncherArcExtender: { def: false }, frameLauncherArcExtender: { def: false },
frameLauncherEdgeHover: { def: false },
frameMode: { def: "connected" }, frameMode: { def: "connected" },
barInsetPaddingShared: { def: -1 }, barInsetPaddingShared: { def: -1 },
barInsetPaddingSyncAll: { def: false }, barInsetPaddingSyncAll: { def: false },
+6
View File
@@ -233,6 +233,12 @@ Item {
sourceComponent: Frame {} sourceComponent: Frame {}
} }
Loader {
active: SettingsData.frameEnabled && SettingsData.frameLauncherEdgeHover
asynchronous: false
sourceComponent: FrameLauncherHoverZone {}
}
DeferredAction { DeferredAction {
id: frameSurfaceReloadAction id: frameSurfaceReloadAction
onTriggered: root.frameSurfacesLoaded = true onTriggered: root.frameSurfacesLoaded = true
@@ -24,6 +24,7 @@ Item {
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : "" readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
property bool triggerUsesOverlayLayer: false property bool triggerUsesOverlayLayer: false
property bool edgeHoverManaged: false
signal dialogClosed signal dialogClosed
@@ -394,6 +394,8 @@ Item {
closeCleanupTimer.stop(); closeCleanupTimer.stop();
isClosing = false; isClosing = false;
openedFromOverview = false; openedFromOverview = false;
_edgeArmed = false;
_edgeBodyHover = false;
animationsEnabled = false; animationsEnabled = false;
@@ -447,6 +449,9 @@ Item {
keyboardActive = false; keyboardActive = false;
spotlightOpen = false; spotlightOpen = false;
_edgeRetractGrace.stop();
_edgeArmed = false;
_edgeBodyHover = false;
ModalManager.closeModal(modalHandle); ModalManager.closeModal(modalHandle);
closeCleanupTimer.start(); closeCleanupTimer.start();
} }
@@ -489,6 +494,31 @@ Item {
} }
} }
// Handles hover dismissal grace periods for edge-hover sessions w/cursor
readonly property bool _edgeRetractEnabled: (modalHandle && modalHandle.edgeHoverManaged === true) && spotlightOpen && !isClosing
property bool _edgeBodyHover: false
property bool _edgeArmed: false
Timer {
id: _edgeRetractGrace
interval: 150
repeat: false
onTriggered: {
if (root._edgeRetractEnabled && root._edgeArmed && !root._edgeBodyHover)
root.hide();
}
}
function _onEdgeBodyHoverChanged(over) {
root._edgeBodyHover = over;
if (over) {
root._edgeArmed = true;
_edgeRetractGrace.stop();
} else if (root._edgeRetractEnabled) {
_edgeRetractGrace.restart();
}
}
Connections { Connections {
target: spotlightContent?.controller ?? null target: spotlightContent?.controller ?? null
function onModeChanged(mode, userInitiated) { function onModeChanged(mode, userInitiated) {
@@ -628,6 +658,13 @@ Item {
width: root.alignedWidth width: root.alignedWidth
height: root.contentSurfaceHeight height: root.contentSurfaceHeight
// Passive tracker for edge-hover dismissal that preserves input events.
HoverHandler {
id: edgeBodyHoverHandler
enabled: root._edgeRetractEnabled
onHoveredChanged: root._onEdgeBodyHoverChanged(hovered)
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: root.spotlightOpen enabled: root.spotlightOpen
@@ -98,6 +98,7 @@ DankPopout {
property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen
backgroundInteractive: !anyModalOpen backgroundInteractive: !anyModalOpen
hoverDismissSuspended: editMode || anyModalOpen
onCredentialsPromptOpenChanged: { onCredentialsPromptOpenChanged: {
if (credentialsPromptOpen && shouldBeVisible) if (credentialsPromptOpen && shouldBeVisible)
+307 -302
View File
@@ -95,6 +95,14 @@ Item {
enableFrameInsetAnimation.schedule(); enableFrameInsetAnimation.schedule();
} }
Connections {
target: topBarContent._hasBarWindow ? topBarContent.barWindow.axis : null
function onEdgeChanged() {
topBarContent.resetHoverForBarGeometryChange();
}
}
Behavior on anchors.leftMargin { Behavior on anchors.leftMargin {
enabled: _animateFrameInsets && _usesFrameBarChrome enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation { NumberAnimation {
@@ -380,6 +388,173 @@ Item {
return "left"; return "left";
} }
DankBarHoverController {
id: hoverController
barContent: topBarContent
barWindow: topBarContent.barWindow
barConfig: topBarContent.barConfig
hLeftSection: topBarContent.hLeftSection
hCenterSection: topBarContent.hCenterSection
hRightSection: topBarContent.hRightSection
vLeftSection: topBarContent.vLeftSection
vCenterSection: topBarContent.vCenterSection
vRightSection: topBarContent.vRightSection
leftWidgetsModel: topBarContent.leftWidgetsModel
centerWidgetsModel: topBarContent.centerWidgetsModel
rightWidgetsModel: topBarContent.rightWidgetsModel
}
readonly property string activeHoverTrigger: hoverController.activeHoverTrigger
readonly property bool hoverPopoutsEnabled: hoverController.hoverPopoutsEnabled
function queueHoverPopout(gx, gy) {
hoverController.queueHoverPoint(gx, gy);
}
function checkHoverPopout(gx, gy) {
hoverController.checkHoverPopout(gx, gy);
}
function findWidgetAtGlobalPoint(gx, gy) {
return hoverController.findWidgetAtGlobalPoint(gx, gy);
}
function scheduleHoverClose(gx, gy) {
hoverController.scheduleHoverClose(gx, gy);
}
function updateHoverBarHovered(hovered) {
hoverController.updateBarHovered(hovered);
}
function resetHoverForBarGeometryChange() {
hoverController.resetForBarGeometryChange();
}
function _dashTriggerSource(section, tabIndex) {
return hoverController.dashTriggerSource(section, tabIndex);
}
function getBarPosition() {
return barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
}
function resolveWidgetTriggerGeometry(widgetItem, section, opts) {
opts = opts || {};
if (opts.useCenterSection && section === "center") {
const centerSection = barWindow.isVertical ? vCenterSection : hCenterSection;
if (centerSection) {
if (barWindow.isVertical) {
const centerY = centerSection.height / 2;
return {
triggerPos: centerSection.mapToItem(null, 0, centerY),
triggerWidth: centerSection.height
};
}
return {
triggerPos: centerSection.mapToItem(null, 0, 0),
triggerWidth: centerSection.width
};
}
}
const ref = opts.visualItem || widgetItem.visualContent || widgetItem;
const w = opts.triggerWidth !== undefined ? opts.triggerWidth : (widgetItem.visualWidth !== undefined ? widgetItem.visualWidth : widgetItem.width);
return {
triggerPos: ref.mapToItem(null, 0, 0),
triggerWidth: w
};
}
function openWidgetPopout(spec) {
if (!spec?.loader)
return false;
spec.loader.active = true;
let popout = _resolvePopoutFromLoader(spec.loader);
if (!popout) {
_queuePopoutLoaderOpen(spec);
return false;
}
return _finishWidgetPopoutOpen(spec, popout);
}
function _resolvePopoutFromLoader(loader) {
if (!loader)
return null;
if (loader.item)
return loader.item;
const pairs = [[PopoutService.appDrawerLoader, PopoutService.appDrawerPopout], [PopoutService.batteryPopoutLoader, PopoutService.batteryPopout], [PopoutService.clipboardHistoryPopoutLoader, PopoutService.clipboardHistoryPopout], [PopoutService.controlCenterLoader, PopoutService.controlCenterPopout], [PopoutService.dankDashPopoutLoader, PopoutService.dankDashPopout], [PopoutService.layoutPopoutLoader, PopoutService.layoutPopout], [PopoutService.notificationCenterLoader, PopoutService.notificationCenterPopout], [PopoutService.processListPopoutLoader, PopoutService.processListPopout], [PopoutService.systemUpdateLoader, PopoutService.systemUpdatePopout], [PopoutService.vpnPopoutLoader, PopoutService.vpnPopout]];
for (let i = 0; i < pairs.length; i++) {
if (loader === pairs[i][0] && pairs[i][1])
return pairs[i][1];
}
return null;
}
property var _pendingPopoutOpenSpec: null
function _queuePopoutLoaderOpen(spec) {
if (_pendingPopoutOpenSpec && _pendingPopoutOpenSpec.loader === spec.loader)
return;
_pendingPopoutOpenSpec = spec;
const loader = spec.loader;
const onLoaded = function () {
if (!loader.item)
return;
if (loader.loaded)
loader.loaded.disconnect(onLoaded);
const pending = topBarContent._pendingPopoutOpenSpec;
if (!pending || pending.loader !== loader)
return;
topBarContent._pendingPopoutOpenSpec = null;
topBarContent._finishWidgetPopoutOpen(pending, loader.item);
if (pending.mode === "hover")
hoverController.recheckLatestPoint();
};
if (loader.item) {
onLoaded();
return;
}
if (loader.loaded)
loader.loaded.connect(onLoaded);
}
function _finishWidgetPopoutOpen(spec, popout) {
const effectiveBarConfig = barConfig;
const barPosition = getBarPosition();
const widgetSection = spec.section || "right";
const mode = spec.mode || "click";
if (popout.setBarContext)
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
if (spec.setTriggerScreen)
popout.triggerScreen = barWindow.screen;
if (popout.setTriggerPosition && spec.widgetItem) {
const geom = resolveWidgetTriggerGeometry(spec.widgetItem, widgetSection, {
useCenterSection: spec.useCenterSection,
visualItem: spec.visualItem,
triggerWidth: spec.triggerWidth
});
if (geom.triggerPos) {
const pos = SettingsData.getPopupTriggerPosition(geom.triggerPos, barWindow.screen, barWindow.effectiveBarThickness, geom.triggerWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
popout.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
}
if (typeof popout.prepareForTrigger === "function")
popout.prepareForTrigger(spec.triggerSource, mode);
if (spec.prepare)
spec.prepare(popout);
const request = mode === "hover" ? PopoutManager.requestHoverPopout : PopoutManager.requestPopout;
request(popout, spec.tabIndex, spec.triggerSource);
return true;
}
readonly property var widgetVisibility: ({ readonly property var widgetVisibility: ({
"cpuUsage": DgopService.dgopAvailable, "cpuUsage": DgopService.dgopAvailable,
"memUsage": DgopService.dgopAvailable, "memUsage": DgopService.dgopAvailable,
@@ -702,27 +877,18 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
popoutTarget: clipboardHistoryPopoutLoader.item ?? null popoutTarget: clipboardHistoryPopoutLoader.item ?? null
function openClipboardPopout(initialTab) { function openClipboardPopout(initialTab, mode) {
clipboardHistoryPopoutLoader.active = true; openWidgetPopout({
if (!clipboardHistoryPopoutLoader.item) { loader: clipboardHistoryPopoutLoader,
return; widgetItem: clipboardWidget,
} section: topBarContent.getWidgetSection(parent) || "right",
const popout = clipboardHistoryPopoutLoader.item; triggerSource: "clipboard",
const effectiveBarConfig = topBarContent.barConfig; mode: mode || "click",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); prepare: popout => {
if (popout.setBarContext) { if (initialTab)
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); popout.activeTab = initialTab;
} }
if (popout.setTriggerPosition) { });
const globalPos = clipboardWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, clipboardWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
popout.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
if (initialTab) {
popout.activeTab = initialTab;
}
PopoutManager.requestPopout(popout, undefined, "clipboard");
} }
onClipboardClicked: openClipboardPopout("recents") onClipboardClicked: openClipboardPopout("recents")
@@ -821,9 +987,14 @@ Item {
} }
onClicked: { onClicked: {
if (!_preparePopout()) topBarContent.openWidgetPopout({
return; loader: appDrawerLoader,
PopoutManager.requestPopout(appDrawerLoader.item, undefined, "appDrawer"); widgetItem: launcherButton,
section: launcherButton.section,
triggerSource: "appDrawer",
mode: "click",
visualItem: launcherButton
});
} }
} }
} }
@@ -890,6 +1061,7 @@ Item {
id: clockComponent id: clockComponent
Clock { Clock {
id: clockWidget
axis: barWindow.axis axis: barWindow.axis
compactMode: topBarContent.overlapping compactMode: topBarContent.overlapping
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
@@ -909,43 +1081,17 @@ Item {
} }
onClockClicked: { onClockClicked: {
dankDashPopoutLoader.active = true; const section = topBarContent.getWidgetSection(parent) || "center";
if (dankDashPopoutLoader.item) { topBarContent.openWidgetPopout({
const effectiveBarConfig = topBarContent.barConfig; loader: dankDashPopoutLoader,
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); widgetItem: clockWidget,
if (dankDashPopoutLoader.item.setBarContext) { section,
dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); tabIndex: 0,
} triggerSource: topBarContent._dashTriggerSource(section, 0),
if (dankDashPopoutLoader.item.setTriggerPosition) { mode: "click",
let triggerPos, triggerWidth; useCenterSection: true,
if (section === "center") { setTriggerScreen: true
const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? vCenterSection : vCenterSection) : hCenterSection; });
if (centerSection) {
if (barWindow.isVertical) {
const centerY = centerSection.height / 2;
const centerGlobalPos = centerSection.mapToItem(null, 0, centerY);
triggerPos = centerGlobalPos;
triggerWidth = centerSection.height;
} else {
const centerGlobalPos = centerSection.mapToItem(null, 0, 0);
triggerPos = centerGlobalPos;
triggerWidth = centerSection.width;
}
} else {
triggerPos = visualContent.mapToItem(null, 0, 0);
triggerWidth = visualWidth;
}
} else {
triggerPos = visualContent.mapToItem(null, 0, 0);
triggerWidth = visualWidth;
}
const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
} else {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen;
}
PopoutManager.requestPopout(dankDashPopoutLoader.item, 0, (effectiveBarConfig?.id ?? "default") + "-" + section + "-0");
}
} }
} }
} }
@@ -954,6 +1100,7 @@ Item {
id: mediaComponent id: mediaComponent
Media { Media {
id: mediaWidget
axis: barWindow.axis axis: barWindow.axis
compactMode: topBarContent.spacingTight || topBarContent.overlapping compactMode: topBarContent.spacingTight || topBarContent.overlapping
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
@@ -962,43 +1109,17 @@ Item {
popoutTarget: dankDashPopoutLoader.item ?? null popoutTarget: dankDashPopoutLoader.item ?? null
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
dankDashPopoutLoader.active = true; const section = topBarContent.getWidgetSection(parent) || "center";
if (dankDashPopoutLoader.item) { topBarContent.openWidgetPopout({
const effectiveBarConfig = topBarContent.barConfig; loader: dankDashPopoutLoader,
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); widgetItem: mediaWidget,
if (dankDashPopoutLoader.item.setBarContext) { section,
dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); tabIndex: 1,
} triggerSource: topBarContent._dashTriggerSource(section, 1),
if (dankDashPopoutLoader.item.setTriggerPosition) { mode: "click",
let triggerPos, triggerWidth; useCenterSection: true,
if (section === "center") { setTriggerScreen: true
const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? vCenterSection : vCenterSection) : hCenterSection; });
if (centerSection) {
if (barWindow.isVertical) {
const centerY = centerSection.height / 2;
const centerGlobalPos = centerSection.mapToItem(null, 0, centerY);
triggerPos = centerGlobalPos;
triggerWidth = centerSection.height;
} else {
const centerGlobalPos = centerSection.mapToItem(null, 0, 0);
triggerPos = centerGlobalPos;
triggerWidth = centerSection.width;
}
} else {
triggerPos = visualContent.mapToItem(null, 0, 0);
triggerWidth = visualWidth;
}
} else {
triggerPos = visualContent.mapToItem(null, 0, 0);
triggerWidth = visualWidth;
}
const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
} else {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen;
}
PopoutManager.requestPopout(dankDashPopoutLoader.item, 1, (effectiveBarConfig?.id ?? "default") + "-" + section + "-1");
}
} }
} }
} }
@@ -1007,6 +1128,7 @@ Item {
id: weatherComponent id: weatherComponent
Weather { Weather {
id: weatherWidget
axis: barWindow.axis axis: barWindow.axis
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
@@ -1014,47 +1136,17 @@ Item {
popoutTarget: dankDashPopoutLoader.item ?? null popoutTarget: dankDashPopoutLoader.item ?? null
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
dankDashPopoutLoader.active = true; const section = topBarContent.getWidgetSection(parent) || "center";
if (dankDashPopoutLoader.item) { topBarContent.openWidgetPopout({
const effectiveBarConfig = topBarContent.barConfig; loader: dankDashPopoutLoader,
// Calculate barPosition from axis.edge widgetItem: weatherWidget,
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); section,
if (dankDashPopoutLoader.item.setBarContext) { tabIndex: 3,
dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); triggerSource: topBarContent._dashTriggerSource(section, 3),
} mode: "click",
if (dankDashPopoutLoader.item.setTriggerPosition) { useCenterSection: true,
// For center section widgets, use center section bounds for DankDash centering setTriggerScreen: true
let triggerPos, triggerWidth; });
if (section === "center") {
const centerSection = barWindow.isVertical ? (barWindow.axis?.edge === "left" ? vCenterSection : vCenterSection) : hCenterSection;
if (centerSection) {
// For vertical bars, use center Y of section; for horizontal, use left edge
if (barWindow.isVertical) {
const centerY = centerSection.height / 2;
const centerGlobalPos = centerSection.mapToItem(null, 0, centerY);
triggerPos = centerGlobalPos;
triggerWidth = centerSection.height;
} else {
// For horizontal bars, use left edge (DankPopout will center it)
const centerGlobalPos = centerSection.mapToItem(null, 0, 0);
triggerPos = centerGlobalPos;
triggerWidth = centerSection.width;
}
} else {
triggerPos = visualContent.mapToItem(null, 0, 0);
triggerWidth = visualWidth;
}
} else {
triggerPos = visualContent.mapToItem(null, 0, 0);
triggerWidth = visualWidth;
}
const pos = SettingsData.getPopupTriggerPosition(triggerPos, barWindow.screen, barWindow.effectiveBarThickness, triggerWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
dankDashPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
} else {
dankDashPopoutLoader.item.triggerScreen = barWindow.screen;
}
PopoutManager.requestPopout(dankDashPopoutLoader.item, 3, (effectiveBarConfig?.id ?? "default") + "-" + section + "-3");
}
} }
} }
} }
@@ -1100,22 +1192,13 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onCpuClicked: { onCpuClicked: {
processListPopoutLoader.active = true; topBarContent.openWidgetPopout({
if (!processListPopoutLoader.item) { loader: processListPopoutLoader,
return; widgetItem: cpuWidget,
} section: topBarContent.getWidgetSection(parent) || "right",
const effectiveBarConfig = topBarContent.barConfig; triggerSource: "cpu",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); mode: "click"
if (processListPopoutLoader.item.setBarContext) { });
processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (processListPopoutLoader.item.setTriggerPosition) {
const globalPos = cpuWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, cpuWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "cpu");
} }
} }
} }
@@ -1133,22 +1216,13 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onRamClicked: { onRamClicked: {
processListPopoutLoader.active = true; topBarContent.openWidgetPopout({
if (!processListPopoutLoader.item) { loader: processListPopoutLoader,
return; widgetItem: ramWidget,
} section: topBarContent.getWidgetSection(parent) || "right",
const effectiveBarConfig = topBarContent.barConfig; triggerSource: "memory",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); mode: "click"
if (processListPopoutLoader.item.setBarContext) { });
processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (processListPopoutLoader.item.setTriggerPosition) {
const globalPos = ramWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, ramWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "memory");
} }
} }
} }
@@ -1180,22 +1254,13 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onCpuTempClicked: { onCpuTempClicked: {
processListPopoutLoader.active = true; topBarContent.openWidgetPopout({
if (!processListPopoutLoader.item) { loader: processListPopoutLoader,
return; widgetItem: cpuTempWidget,
} section: topBarContent.getWidgetSection(parent) || "right",
const effectiveBarConfig = topBarContent.barConfig; triggerSource: "cpu_temp",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); mode: "click"
if (processListPopoutLoader.item.setBarContext) { });
processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (processListPopoutLoader.item.setTriggerPosition) {
const globalPos = cpuTempWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, cpuTempWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "cpu_temp");
} }
} }
} }
@@ -1213,22 +1278,13 @@ Item {
parentScreen: barWindow.screen parentScreen: barWindow.screen
widgetData: parent.widgetData widgetData: parent.widgetData
onGpuTempClicked: { onGpuTempClicked: {
processListPopoutLoader.active = true; topBarContent.openWidgetPopout({
if (!processListPopoutLoader.item) { loader: processListPopoutLoader,
return; widgetItem: gpuTempWidget,
} section: topBarContent.getWidgetSection(parent) || "right",
const effectiveBarConfig = topBarContent.barConfig; triggerSource: "gpu_temp",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); mode: "click"
if (processListPopoutLoader.item.setBarContext) { });
processListPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (processListPopoutLoader.item.setTriggerPosition) {
const globalPos = gpuTempWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, gpuTempWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
processListPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(processListPopoutLoader.item, undefined, "gpu_temp");
} }
} }
} }
@@ -1253,23 +1309,14 @@ Item {
popoutTarget: notificationCenterLoader.item ?? null popoutTarget: notificationCenterLoader.item ?? null
parentScreen: barWindow.screen parentScreen: barWindow.screen
onClicked: { onClicked: {
notificationCenterLoader.active = true; topBarContent.openWidgetPopout({
if (!notificationCenterLoader.item) { loader: notificationCenterLoader,
return; widgetItem: notificationButton,
} section: topBarContent.getWidgetSection(parent) || "right",
notificationCenterLoader.item.triggerScreen = barWindow.screen; triggerSource: "notifications",
const effectiveBarConfig = topBarContent.barConfig; mode: "click",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); setTriggerScreen: true
if (notificationCenterLoader.item.setBarContext) { });
notificationCenterLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (notificationCenterLoader.item.setTriggerPosition) {
const globalPos = notificationButton.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, notificationButton.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
notificationCenterLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(notificationCenterLoader.item, undefined, "notifications");
} }
} }
} }
@@ -1289,22 +1336,13 @@ Item {
popoutTarget: batteryPopoutLoader.item ?? null popoutTarget: batteryPopoutLoader.item ?? null
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleBatteryPopup: { onToggleBatteryPopup: {
batteryPopoutLoader.active = true; topBarContent.openWidgetPopout({
if (!batteryPopoutLoader.item) { loader: batteryPopoutLoader,
return; widgetItem: batteryWidget,
} section: topBarContent.getWidgetSection(parent) || "right",
const effectiveBarConfig = topBarContent.barConfig; triggerSource: "battery",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); mode: "click"
if (batteryPopoutLoader.item.setBarContext) { });
batteryPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (batteryPopoutLoader.item.setTriggerPosition) {
const globalPos = batteryWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, batteryWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
batteryPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(batteryPopoutLoader.item, undefined, "battery");
} }
} }
} }
@@ -1322,20 +1360,13 @@ Item {
popoutTarget: layoutPopoutLoader.item ?? null popoutTarget: layoutPopoutLoader.item ?? null
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleLayoutPopup: { onToggleLayoutPopup: {
layoutPopoutLoader.active = true; topBarContent.openWidgetPopout({
if (!layoutPopoutLoader.item) loader: layoutPopoutLoader,
return; widgetItem: layoutWidget,
const effectiveBarConfig = topBarContent.barConfig; section: topBarContent.getWidgetSection(parent) || "center",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); triggerSource: "layout",
mode: "click"
if (layoutPopoutLoader.item.setTriggerPosition) { });
const globalPos = layoutWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, layoutWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "center";
layoutPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(layoutPopoutLoader.item, undefined, "layout");
} }
} }
} }
@@ -1355,24 +1386,13 @@ Item {
popoutTarget: vpnPopoutLoader.item ?? null popoutTarget: vpnPopoutLoader.item ?? null
parentScreen: barWindow.screen parentScreen: barWindow.screen
onToggleVpnPopup: { onToggleVpnPopup: {
vpnPopoutLoader.active = true; topBarContent.openWidgetPopout({
if (!vpnPopoutLoader.item) loader: vpnPopoutLoader,
return; widgetItem: vpnWidget,
const effectiveBarConfig = topBarContent.barConfig; section: topBarContent.getWidgetSection(parent) || "right",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); triggerSource: "vpn",
mode: "click"
if (vpnPopoutLoader.item.setBarContext) { });
vpnPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0);
}
if (vpnPopoutLoader.item.setTriggerPosition) {
const globalPos = vpnWidget.mapToItem(null, 0, 0);
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, vpnWidget.width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const widgetSection = topBarContent.getWidgetSection(parent) || "right";
vpnPopoutLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, widgetSection, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(vpnPopoutLoader.item, undefined, "vpn");
} }
} }
} }
@@ -1381,6 +1401,7 @@ Item {
id: controlCenterButtonComponent id: controlCenterButtonComponent
ControlCenterButton { ControlCenterButton {
id: controlCenterButton
isActive: controlCenterLoader.item ? controlCenterLoader.item.shouldBeVisible : false isActive: controlCenterLoader.item ? controlCenterLoader.item.shouldBeVisible : false
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
@@ -1403,25 +1424,16 @@ Item {
} }
onClicked: { onClicked: {
controlCenterLoader.active = true; topBarContent.openWidgetPopout({
if (!controlCenterLoader.item) { loader: controlCenterLoader,
return; widgetItem: controlCenterButton,
} section: topBarContent.getWidgetSection(parent) || "right",
controlCenterLoader.item.triggerScreen = barWindow.screen; triggerSource: "controlCenter",
if (controlCenterLoader.item.setTriggerPosition) { mode: "click",
const globalPos = mapToItem(null, 0, 0); setTriggerScreen: true
// Use topBarContent.barConfig directly });
const effectiveBarConfig = topBarContent.barConfig; if (controlCenterLoader.item?.shouldBeVisible && NetworkService.wifiEnabled)
// Calculate barPosition from axis.edge like Battery widget does
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
const pos = SettingsData.getPopupTriggerPosition(globalPos, barWindow.screen, barWindow.effectiveBarThickness, width, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
const section = topBarContent.getWidgetSection(parent) || "right";
controlCenterLoader.item.setTriggerPosition(pos.x, pos.y, pos.width, section, barWindow.screen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(controlCenterLoader.item, undefined, "controlCenter");
if (controlCenterLoader.item.shouldBeVisible && NetworkService.wifiEnabled) {
NetworkService.scanWifi(); NetworkService.scanWifi();
}
} }
} }
} }
@@ -1531,6 +1543,7 @@ Item {
id: systemUpdateComponent id: systemUpdateComponent
SystemUpdate { SystemUpdate {
id: systemUpdateWidget
isActive: systemUpdateLoader.item ? systemUpdateLoader.item.shouldBeVisible : false isActive: systemUpdateLoader.item ? systemUpdateLoader.item.shouldBeVisible : false
widgetThickness: barWindow.widgetThickness widgetThickness: barWindow.widgetThickness
barThickness: barWindow.effectiveBarThickness barThickness: barWindow.effectiveBarThickness
@@ -1549,22 +1562,14 @@ Item {
} }
onClicked: { onClicked: {
systemUpdateLoader.active = true; topBarContent.openWidgetPopout({
if (!systemUpdateLoader.item) loader: systemUpdateLoader,
return; widgetItem: systemUpdateWidget,
const popout = systemUpdateLoader.item; section: topBarContent.getWidgetSection(parent) || "right",
const effectiveBarConfig = topBarContent.barConfig; triggerSource: "systemUpdate",
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); mode: "click",
if (popout.setBarContext) { visualItem: systemUpdateWidget
popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); });
}
if (popout.setTriggerPosition) {
const globalPos = visualContent.mapToItem(null, 0, 0);
const currentScreen = parentScreen || Screen;
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barWindow.effectiveBarThickness, visualWidth, effectiveBarConfig?.spacing ?? 4, barPosition, effectiveBarConfig);
popout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barWindow.effectiveBarThickness, effectiveBarConfig?.spacing ?? 4, effectiveBarConfig);
}
PopoutManager.requestPopout(popout, undefined, "systemUpdate");
} }
} }
} }
@@ -0,0 +1,938 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
Item {
id: root
required property var barContent
required property var barWindow
required property var barConfig
required property var hLeftSection
required property var hCenterSection
required property var hRightSection
required property var vLeftSection
required property var vCenterSection
required property var vRightSection
property var leftWidgetsModel
property var centerWidgetsModel
property var rightWidgetsModel
property string activeHoverTrigger: ""
readonly property bool hoverPopoutsEnabled: barConfig?.hoverPopouts ?? false
readonly property int hoverPopoutDelay: Math.max(0, barConfig?.hoverPopoutDelay ?? 150)
property real _lastHoverGlobalX: 0
property real _lastHoverGlobalY: 0
property bool _hitTestPending: false
property bool _barHovered: false
property bool _barExitPending: false
property var _pendingHoverHit: null
property string _pendingHoverTrigger: ""
property bool _candidateCacheValid: false
property var _candidateCache: []
property var _candidateWatchers: []
property bool _lastLookupWasMiss: false
width: 0
height: 0
onLeftWidgetsModelChanged: invalidateCandidateCache()
onCenterWidgetsModelChanged: invalidateCandidateCache()
onRightWidgetsModelChanged: invalidateCandidateCache()
onHoverPopoutsEnabledChanged: {
if (hoverPopoutsEnabled)
return;
cancelQueuedHitTest();
_cancelPendingHover();
_hoverCloseTimer.stop();
if (hasOpenHoverSurface() && !isActiveHoverSurfacePinned())
closeHoverSurfaces();
activeHoverTrigger = "";
}
Component.onDestruction: _disconnectCandidateWatchers()
Connections {
target: root.barContent
function onWidthChanged() {
root.invalidateCandidateCache();
}
function onHeightChanged() {
root.invalidateCandidateCache();
}
}
Connections {
target: root.barWindow
function onScreenChanged() {
root.invalidateCandidateCache();
}
}
Connections {
target: BarWidgetService
function onWidgetRegistered(_widgetId, screenName) {
if (screenName === root.barWindow?.screen?.name)
root.invalidateCandidateCache();
}
function onWidgetUnregistered(_widgetId, screenName) {
if (screenName === root.barWindow?.screen?.name)
root.invalidateCandidateCache();
}
}
FrameAnimation {
running: root._hitTestPending
onTriggered: {
root._hitTestPending = false;
root.checkHoverPopout(root._lastHoverGlobalX, root._lastHoverGlobalY);
}
}
Timer {
id: _hoverIntentTimer
interval: root.hoverPopoutDelay
repeat: false
onTriggered: root._commitPendingHover()
}
// Grace timer to prevent flicker when crossing gaps.
Timer {
id: _hoverCloseTimer
interval: 120
repeat: false
onTriggered: root._commitHoverClose()
}
function queueHoverPoint(gx, gy) {
_lastHoverGlobalX = gx;
_lastHoverGlobalY = gy;
_barHovered = true;
_barExitPending = false;
PopoutManager.updateHoverCursor(gx, gy);
if (hoverPopoutsEnabled)
_hitTestPending = true;
}
function updateBarHovered(hovered) {
_barHovered = hovered;
if (hovered) {
_barExitPending = false;
_hoverCloseTimer.stop();
return;
}
cancelQueuedHitTest();
_cancelPendingHover();
if (!hoverPopoutsEnabled || isActiveHoverSurfacePinned())
return;
_barExitPending = true;
_hoverCloseTimer.restart();
}
function cancelQueuedHitTest() {
_hitTestPending = false;
}
function recheckLatestPoint() {
checkHoverPopout(_lastHoverGlobalX, _lastHoverGlobalY);
}
function resetForBarGeometryChange() {
invalidateCandidateCache();
cancelQueuedHitTest();
_cancelPendingHover();
_hoverCloseTimer.stop();
barContent._pendingPopoutOpenSpec = null;
const activePopout = PopoutManager.getActivePopout(barWindow?.screen);
const hasTransientSurface = activeHoverTrigger !== "" || activePopout?.hoverDismissEnabled === true;
if (hasTransientSurface && !isActiveHoverSurfacePinned())
closeHoverSurfaces();
else
activeHoverTrigger = "";
}
function invalidateCandidateCache() {
_candidateCacheValid = false;
_candidateCache = [];
_lastLookupWasMiss = false;
_disconnectCandidateWatchers();
}
function _disconnectCandidateWatchers() {
const watchers = _candidateWatchers;
_candidateWatchers = [];
for (let i = 0; i < watchers.length; i++) {
const watcher = watchers[i];
try {
const signal = watcher.object?.[watcher.signalName];
if (signal && typeof signal.disconnect === "function")
signal.disconnect(watcher.callback);
} catch (e) {}
}
}
function _watchCandidateObject(object) {
if (!object)
return;
for (let i = 0; i < _candidateWatchers.length; i++) {
if (_candidateWatchers[i].object === object)
return;
}
const signalNames = ["xChanged", "yChanged", "widthChanged", "heightChanged", "visibleChanged", "parentChanged", "childrenChanged", "itemChanged", "activeChanged", "destroyed"];
for (let i = 0; i < signalNames.length; i++) {
const signalName = signalNames[i];
try {
const signal = object[signalName];
if (!signal || typeof signal.connect !== "function")
continue;
const callback = function () {
root.invalidateCandidateCache();
};
signal.connect(callback);
_candidateWatchers.push({
object,
signalName,
callback
});
} catch (e) {}
}
}
function _getBarSections() {
if (barWindow.isVertical) {
return [
{
section: vLeftSection,
name: "left"
},
{
section: vCenterSection,
name: "center"
},
{
section: vRightSection,
name: "right"
}
];
}
return [
{
section: hLeftSection,
name: "left"
},
{
section: hCenterSection,
name: "center"
},
{
section: hRightSection,
name: "right"
}
];
}
// The widget registry is keyed by (widgetId, screenName)
function _itemBelongsToThisBar(item) {
const owner = barContent;
if (!owner || !item)
return true;
let node = item;
let guard = 0;
while (node && guard < 100) {
if (node === owner)
return true;
node = node.parent;
guard++;
}
return false;
}
function _findWidgetHostInWrapper(wrapper) {
if (wrapper.widgetId !== undefined)
return wrapper;
const children = wrapper.children || [];
for (let i = 0; i < children.length; i++) {
if (children[i].widgetId !== undefined)
return children[i];
}
return null;
}
function _collectSectionWrappers(section) {
_watchCandidateObject(section);
const layoutLoader = section.widgetLayoutLoader;
_watchCandidateObject(layoutLoader);
const layout = layoutLoader?.item;
if (layout) {
_watchCandidateObject(layout);
return layout.children || [];
}
const children = section.children || [];
const wrappers = [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (!child || child === layoutLoader)
continue;
if (child.itemData !== undefined || child.widgetId !== undefined || _findWidgetHostInWrapper(child))
wrappers.push(child);
}
return wrappers;
}
function _widgetSupportsHoverPopout(widgetId, widgetItem) {
if (!widgetId || !widgetItem)
return false;
if (typeof widgetItem.triggerHoverPopout === "function")
return true;
if (widgetId === "systemTray" && typeof widgetItem.openHoverAtGlobalPoint === "function")
return true;
switch (widgetId) {
case "launcherButton":
case "clipboard":
case "clock":
case "music":
case "weather":
case "cpuUsage":
case "memUsage":
case "cpuTemp":
case "gpuTemp":
case "notificationButton":
case "battery":
case "layout":
case "vpn":
case "controlCenterButton":
case "systemUpdate":
case "notepadButton":
case "systemTray":
return true;
default:
return false;
}
}
function _enumerateWidgetHosts() {
const hosts = [];
const sections = _getBarSections();
for (let s = 0; s < sections.length; s++) {
const sectionEntry = sections[s];
const section = sectionEntry.section;
if (!section)
continue;
const wrappers = _collectSectionWrappers(section);
for (let i = 0; i < wrappers.length; i++) {
const wrapper = wrappers[i];
const host = _findWidgetHostInWrapper(wrapper);
if (!host?.widgetId)
continue;
_watchCandidateObject(wrapper);
_watchCandidateObject(host);
hosts.push({
host,
wrapper,
section: sectionEntry.name
});
}
}
return hosts;
}
function _collectHoverCandidates() {
const screenName = barWindow.screen?.name;
const candidates = [];
const seen = new Set();
function addCandidate(widgetId, widgetItem, sectionHint) {
if (!widgetId || !widgetItem || seen.has(widgetItem))
return;
if (!root._itemBelongsToThisBar(widgetItem))
return;
if (!root._widgetSupportsHoverPopout(widgetId, widgetItem))
return;
if (!root.barContent.getWidgetVisible(widgetId))
return;
seen.add(widgetItem);
candidates.push({
widgetId,
widgetItem,
section: widgetItem.section || sectionHint || "right",
wrapper: null,
host: null
});
}
if (screenName) {
const registry = BarWidgetService.widgetRegistry;
if (registry && typeof registry === "object") {
for (const widgetId in registry) {
const screenMap = registry[widgetId];
if (!screenMap || typeof screenMap !== "object")
continue;
const widgetItem = screenMap[screenName];
if (widgetItem)
addCandidate(widgetId, widgetItem, widgetItem.section);
}
}
}
const hosts = _enumerateWidgetHosts();
for (let i = 0; i < hosts.length; i++) {
const entry = hosts[i];
if (!entry.host?.item)
continue;
const existing = candidates.find(candidate => candidate.widgetItem === entry.host.item);
if (existing) {
existing.wrapper = entry.wrapper;
existing.host = entry.host;
if (!existing.section)
existing.section = entry.section;
continue;
}
if (!_widgetSupportsHoverPopout(entry.host.widgetId, entry.host.item))
continue;
candidates.push({
widgetId: entry.host.widgetId,
widgetItem: entry.host.item,
section: entry.host.item.section || entry.section,
wrapper: entry.wrapper,
host: entry.host
});
}
return candidates;
}
function _globalItemBounds(item) {
try {
const topLeft = item.mapToItem(null, 0, 0);
return {
x: topLeft.x,
y: topLeft.y,
width: item.width,
height: item.height
};
} catch (e) {
return null;
}
}
function _hitBoundsForWidget(widgetItem, wrapper) {
try {
if (!widgetItem?.visible)
return null;
if (widgetItem.visualContent !== undefined) {
const visual = widgetItem.visualContent;
if (visual && visual.width > 0 && visual.height > 0)
return _globalItemBounds(visual);
}
if (widgetItem.width > 0 && widgetItem.height > 0)
return _globalItemBounds(widgetItem);
if (wrapper && wrapper.width > 0 && wrapper.height > 0)
return _globalItemBounds(wrapper);
} catch (e) {}
return null;
}
function _pointInBounds(gx, gy, bounds) {
return gx >= bounds.x && gx < bounds.x + bounds.width && gy >= bounds.y && gy < bounds.y + bounds.height;
}
function _sameBounds(a, b) {
return !!a && !!b && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
}
function _buildCandidateCache() {
_disconnectCandidateWatchers();
const candidates = _collectHoverCandidates();
const cache = [];
for (let i = 0; i < candidates.length; i++) {
const entry = candidates[i];
const bounds = _hitBoundsForWidget(entry.widgetItem, entry.wrapper);
_watchCandidateObject(entry.widgetItem);
_watchCandidateObject(entry.wrapper);
_watchCandidateObject(entry.host);
try {
_watchCandidateObject(entry.widgetItem?.visualContent);
} catch (e) {}
if (!bounds || bounds.width <= 0 || bounds.height <= 0)
continue;
cache.push({
widgetId: entry.widgetId,
widgetItem: entry.widgetItem,
section: entry.section,
wrapper: entry.wrapper,
bounds
});
}
_candidateCache = cache;
_candidateCacheValid = true;
_lastLookupWasMiss = false;
}
function _scanCandidateCache(gx, gy) {
let best = null;
let bestArea = Infinity;
for (let i = 0; i < _candidateCache.length; i++) {
const entry = _candidateCache[i];
const bounds = entry.bounds;
if (!_pointInBounds(gx, gy, bounds))
continue;
const area = bounds.width * bounds.height;
if (area < bestArea) {
bestArea = area;
best = entry;
}
}
return best;
}
function _validatedHit(entry, gx, gy) {
if (!entry)
return null;
const liveBounds = _hitBoundsForWidget(entry.widgetItem, entry.wrapper);
if (!liveBounds || !_pointInBounds(gx, gy, liveBounds))
return null;
if (!_sameBounds(entry.bounds, liveBounds))
return null;
return {
widgetId: entry.widgetId,
widgetItem: entry.widgetItem,
section: entry.section
};
}
function findWidgetAtGlobalPoint(gx, gy) {
if (!_candidateCacheValid)
_buildCandidateCache();
let entry = _scanCandidateCache(gx, gy);
let hit = _validatedHit(entry, gx, gy);
if (entry && !hit) {
invalidateCandidateCache();
_buildCandidateCache();
entry = _scanCandidateCache(gx, gy);
hit = _validatedHit(entry, gx, gy);
} else if (!entry && !_lastLookupWasMiss) {
// One live rebuild on entry into an empty gap covers layout changes whose
// source did not expose a QML geometry signal without rescanning every frame.
invalidateCandidateCache();
_buildCandidateCache();
entry = _scanCandidateCache(gx, gy);
hit = _validatedHit(entry, gx, gy);
}
_lastLookupWasMiss = !hit;
return hit;
}
function dashTriggerSource(section, tabIndex) {
return (barConfig?.id ?? "default") + "-" + section + "-" + tabIndex;
}
function _notepadWidgetForScreen() {
// Prefer this bar's own enumerated candidates; the registry is screen-keyed and a
// sibling bar on the same screen can shadow it.
if (!_candidateCacheValid)
_buildCandidateCache();
for (let i = 0; i < _candidateCache.length; i++) {
if (_candidateCache[i].widgetId === "notepadButton")
return _candidateCache[i].widgetItem;
}
const screenName = barWindow?.screen?.name;
const fromRegistry = screenName ? BarWidgetService.getWidget("notepadButton", screenName) : null;
if (fromRegistry && _itemBelongsToThisBar(fromRegistry))
return fromRegistry;
return null;
}
function notepadContainsGlobalPoint(gx, gy) {
const instance = _notepadWidgetForScreen()?.notepadInstance;
if (!instance?.isVisible || typeof instance.containsGlobalPoint !== "function")
return false;
return instance.containsGlobalPoint(gx, gy);
}
function isActiveHoverSurfacePinned() {
if (activeHoverTrigger === "notepadButton") {
const instance = _notepadWidgetForScreen()?.notepadInstance;
if (instance?.hoverDismissSuspended === true)
return true;
}
return PopoutManager.isActivePopoutPinned(barWindow?.screen);
}
function cursorOverHoverChain(gx, gy, excludedBarWindow) {
if (PopoutManager.cursorOverBar(gx, gy, undefined, excludedBarWindow))
return true;
const popout = PopoutManager.getActivePopout(barWindow?.screen);
if (popout?.containsGlobalPoint?.(gx, gy))
return true;
if (notepadContainsGlobalPoint(gx, gy))
return true;
const screenName = barWindow.screen?.name;
if (screenName && TrayMenuManager.activeTrayMenus[screenName])
return true;
return false;
}
function _closeHoverNotepad() {
if (activeHoverTrigger !== "notepadButton")
return;
const instance = _notepadWidgetForScreen()?.notepadInstance;
if (!instance)
return;
if (instance.hoverDismissEnabled !== undefined)
instance.hoverDismissEnabled = false;
if (typeof instance.hideFromHoverDismiss === "function")
instance.hideFromHoverDismiss();
else if (typeof instance.hide === "function")
instance.hide();
}
function closeHoverSurfaces() {
_closeHoverNotepad();
activeHoverTrigger = "";
PopoutManager.closePopoutForScreen(barWindow?.screen);
TrayMenuManager.closeAllMenus();
}
function _beginSupersededCloseForActive() {
const popout = PopoutManager.getActivePopout(barWindow?.screen);
if (popout && typeof popout.beginSupersededClose === "function")
popout.beginSupersededClose();
}
function openNotepadHover(widgetItem) {
const instance = widgetItem.prepareNotepadInstance?.(widgetItem.notepadInstance) ?? widgetItem.notepadInstance;
if (!instance || typeof instance.show !== "function")
return false;
if (instance.hoverDismissEnabled !== undefined)
instance.hoverDismissEnabled = true;
instance.show();
return true;
}
function _syncHoverTriggerState() {
if (activeHoverTrigger === "notepadButton") {
const instance = _notepadWidgetForScreen()?.notepadInstance;
if (!instance?.isVisible)
activeHoverTrigger = "";
return;
}
if (activeHoverTrigger !== "" && !hasOpenHoverSurface())
activeHoverTrigger = "";
}
function hasOpenHoverSurface() {
if (activeHoverTrigger === "")
return false;
if (activeHoverTrigger === "notepadButton") {
const instance = _notepadWidgetForScreen()?.notepadInstance;
return instance?.isVisible ?? false;
}
if (activeHoverTrigger.startsWith("tray-")) {
const screenName = barWindow.screen?.name;
return !!(screenName && TrayMenuManager.activeTrayMenus[screenName]);
}
const popout = PopoutManager.getActivePopout(barWindow?.screen);
if (!popout)
return false;
if (popout.dashVisible !== undefined)
return !!popout.dashVisible || !!popout.isClosing;
if (popout.notificationHistoryVisible !== undefined)
return !!popout.notificationHistoryVisible || !!popout.isClosing;
return !!(popout.shouldBeVisible || popout.isClosing);
}
function _loaderForWidgetId(widgetId) {
switch (widgetId) {
case "launcherButton":
return PopoutService.appDrawerLoader;
case "clipboard":
return PopoutService.clipboardHistoryPopoutLoader;
case "clock":
case "music":
case "weather":
return PopoutService.dankDashPopoutLoader;
case "cpuUsage":
case "memUsage":
case "cpuTemp":
case "gpuTemp":
return PopoutService.processListPopoutLoader;
case "notificationButton":
return PopoutService.notificationCenterLoader;
case "battery":
return PopoutService.batteryPopoutLoader;
case "layout":
return PopoutService.layoutPopoutLoader;
case "vpn":
return PopoutService.vpnPopoutLoader;
case "controlCenterButton":
return PopoutService.controlCenterLoader;
case "systemUpdate":
return PopoutService.systemUpdateLoader;
default:
return null;
}
}
function openHoverPopoutForHit(hit) {
if (!hit?.widgetItem)
return false;
const widgetId = hit.widgetId;
const widgetItem = hit.widgetItem;
const section = hit.section;
const base = {
widgetItem,
section,
mode: "hover"
};
if (widgetId === "systemTray") {
if (typeof widgetItem.openHoverAtGlobalPoint !== "function")
return false;
return !!widgetItem.openHoverAtGlobalPoint(hit.globalX, hit.globalY);
}
if (typeof widgetItem.triggerHoverPopout === "function") {
widgetItem.triggerHoverPopout(hit.widgetId);
return true;
}
const loader = _loaderForWidgetId(widgetId);
switch (widgetId) {
case "launcherButton":
return barContent.openWidgetPopout(Object.assign({}, base, {
loader,
triggerSource: "appDrawer",
visualItem: widgetItem
}));
case "clipboard":
return barContent.openWidgetPopout(Object.assign({}, base, {
loader,
triggerSource: "clipboard",
prepare: popout => {
popout.activeTab = "recents";
}
}));
case "clock":
case "music":
case "weather":
{
const tabIndex = widgetId === "clock" ? 0 : (widgetId === "music" ? 1 : 3);
return barContent.openWidgetPopout(Object.assign({}, base, {
loader,
tabIndex,
triggerSource: dashTriggerSource(section, tabIndex),
useCenterSection: true,
setTriggerScreen: true
}));
}
case "cpuUsage":
case "memUsage":
case "cpuTemp":
case "gpuTemp":
{
const triggerSources = {
cpuUsage: "cpu",
memUsage: "memory",
cpuTemp: "cpu_temp",
gpuTemp: "gpu_temp"
};
return barContent.openWidgetPopout(Object.assign({}, base, {
loader,
triggerSource: triggerSources[widgetId]
}));
}
case "notificationButton":
return barContent.openWidgetPopout(Object.assign({}, base, {
loader,
triggerSource: "notifications",
setTriggerScreen: true
}));
case "battery":
case "layout":
case "vpn":
{
const triggerSources = {
battery: "battery",
layout: "layout",
vpn: "vpn"
};
return barContent.openWidgetPopout(Object.assign({}, base, {
loader,
triggerSource: triggerSources[widgetId]
}));
}
case "controlCenterButton":
if (barContent.openWidgetPopout(Object.assign({}, base, {
loader,
triggerSource: "controlCenter",
setTriggerScreen: true
}))) {
if (loader.item?.shouldBeVisible && NetworkService.wifiEnabled)
NetworkService.scanWifi();
return true;
}
return false;
case "systemUpdate":
return barContent.openWidgetPopout(Object.assign({}, base, {
loader,
triggerSource: "systemUpdate",
visualItem: widgetItem
}));
case "notepadButton":
return openNotepadHover(widgetItem);
default:
return false;
}
}
function checkHoverPopout(gx, gy) {
if (!hoverPopoutsEnabled)
return;
_lastHoverGlobalX = gx;
_lastHoverGlobalY = gy;
PopoutManager.updateHoverCursor(gx, gy);
_syncHoverTriggerState();
if (isActiveHoverSurfacePinned())
return;
const hit = findWidgetAtGlobalPoint(gx, gy);
if (!hit) {
_cancelPendingHover();
scheduleHoverClose(gx, gy);
return;
}
hit.globalX = gx;
hit.globalY = gy;
let triggerKey = hit.widgetId;
if (hit.widgetId === "systemTray")
triggerKey = hit.widgetItem.hoverTriggerAtGlobalPoint?.(gx, gy) || "";
else if (hit.widgetId === "clock")
triggerKey = dashTriggerSource(hit.section, 0);
else if (hit.widgetId === "music")
triggerKey = dashTriggerSource(hit.section, 1);
else if (hit.widgetId === "weather")
triggerKey = dashTriggerSource(hit.section, 3);
if (!triggerKey) {
_cancelPendingHover();
scheduleHoverClose(gx, gy);
return;
}
_hoverCloseTimer.stop();
if (triggerKey === activeHoverTrigger && hasOpenHoverSurface()) {
_cancelPendingHover();
return;
}
_pendingHoverHit = hit;
if (_pendingHoverTrigger !== triggerKey) {
_pendingHoverTrigger = triggerKey;
if (hoverPopoutDelay <= 0)
_commitPendingHover();
else
_hoverIntentTimer.restart();
}
}
function _cancelPendingHover() {
_hoverIntentTimer.stop();
_pendingHoverHit = null;
_pendingHoverTrigger = "";
}
function _hitTargetsActivePopout(hit) {
const active = PopoutManager.getActivePopout(barWindow?.screen);
if (!active || !hit)
return false;
const loader = _loaderForWidgetId(hit.widgetId);
if (!loader)
return false;
return barContent._resolvePopoutFromLoader(loader) === active;
}
function _commitPendingHover() {
const hit = _pendingHoverHit;
const triggerKey = _pendingHoverTrigger;
_pendingHoverHit = null;
_pendingHoverTrigger = "";
if (!hit || !hoverPopoutsEnabled)
return;
if (isActiveHoverSurfacePinned())
return;
if (!PopoutManager.cursorOverBar(_lastHoverGlobalX, _lastHoverGlobalY))
return;
const activePopout = PopoutManager.getActivePopout(barWindow?.screen);
const targetLoader = _loaderForWidgetId(hit.widgetId);
const targetPopout = barContent._resolvePopoutFromLoader(targetLoader);
const managerOwnsTransition = !!(activePopout && targetPopout);
if (triggerKey !== activeHoverTrigger && activeHoverTrigger !== "" && !_hitTargetsActivePopout(hit)) {
if (!managerOwnsTransition) {
_beginSupersededCloseForActive();
closeHoverSurfaces();
}
}
if (!openHoverPopoutForHit(hit)) {
if (activeHoverTrigger !== "")
closeHoverSurfaces();
return;
}
activeHoverTrigger = triggerKey;
}
function scheduleHoverClose(gx, gy) {
cancelQueuedHitTest();
_cancelPendingHover();
_barExitPending = false;
if (!hoverPopoutsEnabled)
return;
if (isActiveHoverSurfacePinned())
return;
if (cursorOverHoverChain(gx, gy))
return;
_hoverCloseTimer.restart();
}
function _commitHoverClose() {
const gx = PopoutManager.hoverCursorGlobalX;
const gy = PopoutManager.hoverCursorGlobalY;
if (isActiveHoverSurfacePinned())
return;
if (_barHovered)
return;
const excludedBar = _barExitPending ? barWindow : null;
if (cursorOverHoverChain(gx, gy, excludedBar))
return;
_barExitPending = false;
closeHoverSurfaces();
}
}
+31 -1
View File
@@ -719,6 +719,14 @@ PanelWindow {
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null
readonly property real _revealProgress: topBarSlide.x + topBarSlide.y readonly property real _revealProgress: topBarSlide.x + topBarSlide.y
function containsGlobalPoint(gx, gy, padding) {
const pad = padding !== undefined ? padding : 16;
if (!inputMask.showing)
return false;
const topLeft = inputMask.mapToItem(null, 0, 0);
return gx >= topLeft.x - pad && gx < topLeft.x + inputMask.width + pad && gy >= topLeft.y - pad && gy < topLeft.y + inputMask.height + pad;
}
function sectionRect(section, isCenter, _dep) { function sectionRect(section, isCenter, _dep) {
if (!section) if (!section)
return { return {
@@ -1020,7 +1028,7 @@ PanelWindow {
} }
} }
onWheel: wheel => { function processWheel(wheel) {
if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) { if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) {
wheel.accepted = false; wheel.accepted = false;
return; return;
@@ -1089,6 +1097,8 @@ PanelWindow {
wheel.accepted = false; wheel.accepted = false;
} }
onWheel: wheel => processWheel(wheel)
} }
DankBarContent { DankBarContent {
@@ -1100,6 +1110,26 @@ PanelWindow {
centerWidgetsModel: barWindow.centerWidgetsModel centerWidgetsModel: barWindow.centerWidgetsModel
rightWidgetsModel: barWindow.rightWidgetsModel rightWidgetsModel: barWindow.rightWidgetsModel
} }
// Passive HoverHandler to track cursor without intercepting clicks or scroll events.
HoverHandler {
id: hoverPopoutHandler
enabled: (barConfig?.hoverPopouts ?? false) && !barWindow.clickThroughEnabled
property real lastGlobalX: 0
property real lastGlobalY: 0
onPointChanged: {
const gp = barUnitInset.mapToItem(null, point.position.x, point.position.y);
lastGlobalX = gp.x;
lastGlobalY = gp.y;
topBarContent.queueHoverPopout(gp.x, gp.y);
}
onHoveredChanged: {
topBarContent.updateHoverBarHovered(hovered);
}
}
} }
} }
} }
@@ -19,6 +19,7 @@ Item {
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
property alias widgetLayoutLoader: layoutLoader
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
@@ -19,6 +19,7 @@ Item {
property bool forceVerticalLayout: false property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false) readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
property alias widgetLayoutLoader: layoutLoader
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0 implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0 implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
@@ -2103,4 +2103,53 @@ BasePill {
return; return;
currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj); currentTrayMenu.showForTrayItem(item, anchor, screen, atBottom, vertical ?? false, axisObj);
} }
function _trayLayoutRoot() {
const contentChildren = root.visualContent?.children;
if (!contentChildren || contentChildren.length === 0)
return null;
const contentRoot = contentChildren[0];
return contentRoot?.layoutLoader?.item || null;
}
function _trayHitAtGlobalPoint(gx, gy) {
if (!root.visible || root.width <= 0 || root.height <= 0)
return null;
const local = root.mapFromItem(null, gx, gy);
if (local.x < 0 || local.y < 0 || local.x > root.width || local.y > root.height)
return null;
const layout = _trayLayoutRoot();
if (!layout)
return null;
const layoutLocal = layout.mapFromItem(null, gx, gy);
const children = layout.children || [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (!child.visible || child.width <= 0 || child.height <= 0)
continue;
if (layoutLocal.x < child.x || layoutLocal.x >= child.x + child.width)
continue;
if (layoutLocal.y < child.y || layoutLocal.y >= child.y + child.height)
continue;
if (child.trayItem)
return child;
}
return null;
}
function hoverTriggerAtGlobalPoint(gx, gy) {
const hit = _trayHitAtGlobalPoint(gx, gy);
if (!hit?.trayItem?.hasMenu)
return "";
return "tray-" + (hit.trayItem.id || hit.itemKey || "");
}
function openHoverAtGlobalPoint(gx, gy) {
const hit = _trayHitAtGlobalPoint(gx, gy);
if (!hit?.trayItem?.hasMenu)
return false;
const anchor = hit.children?.length > 0 ? hit.children[0] : hit;
showForTrayItem(hit.trayItem, anchor, parentScreen, isAtBottom, isVerticalOrientation, axis);
return true;
}
} }
@@ -0,0 +1,92 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
// Edge strip to trigger launcher hover-reveal when free of panel bars and dock.
Variants {
id: root
model: Quickshell.screens
delegate: Loader {
id: zoneLoader
required property var modelData
readonly property string emergeSide: SettingsData.frameLauncherEmergeSide || "bottom"
readonly property bool eligible: SettingsData.frameEnabled && SettingsData.frameLauncherEdgeHover && Theme.isConnectedEffect && SettingsData.isScreenInPreferences(zoneLoader.modelData, SettingsData.frameScreenPreferences) && CompositorService.usesConnectedFrameChromeForScreen(zoneLoader.modelData) && !SettingsData.barOccupiesSide(zoneLoader.modelData, zoneLoader.emergeSide) && !SettingsData.dockOccupiesSide(zoneLoader.emergeSide)
active: eligible
asynchronous: false
sourceComponent: PanelWindow {
id: zoneWindow
readonly property bool vertical: zoneLoader.emergeSide === "left" || zoneLoader.emergeSide === "right"
readonly property real triggerThickness: Math.max(6, SettingsData.frameThickness)
readonly property bool launcherOpen: PopoutService.dankLauncherV2Modal?.spotlightOpen ?? false
property bool _openedForCurrentHover: false
// Hot zone dimensions centered on the emerge edge to cover the launcher footprint.
readonly property real _launcherBaseW: SettingsData.dankLauncherV2Size === "micro" ? 500 : (SettingsData.dankLauncherV2Size === "medium" ? 720 : (SettingsData.dankLauncherV2Size === "large" ? 860 : 620))
readonly property real _launcherBaseH: SettingsData.dankLauncherV2Size === "micro" ? 480 : (SettingsData.dankLauncherV2Size === "medium" ? 720 : (SettingsData.dankLauncherV2Size === "large" ? 860 : 600))
readonly property real screenW: zoneLoader.modelData?.width ?? 0
readonly property real screenH: zoneLoader.modelData?.height ?? 0
readonly property real spanW: Math.round(Math.min(_launcherBaseW, screenW - 100) * 1.1)
readonly property real spanH: Math.round(Math.min(_launcherBaseH, screenH - 100) * 1.1)
function requestLauncherOpen() {
if (launcherOpen || _openedForCurrentHover)
return;
_openedForCurrentHover = true;
PopoutService.openDankLauncherV2(CompositorService.framePeerSurfacesUseOverlayForScreen(zoneLoader.modelData), true);
}
screen: zoneLoader.modelData
color: "transparent"
WlrLayershell.namespace: "dms:frame-launcher-hover"
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
// Anchor and center the hover zone alignment with the launcher.
anchors {
top: zoneLoader.emergeSide === "top" || zoneWindow.vertical
bottom: zoneLoader.emergeSide === "bottom"
left: zoneLoader.emergeSide === "left" || !zoneWindow.vertical
right: zoneLoader.emergeSide === "right"
}
margins {
left: zoneWindow.vertical ? 0 : Math.max(0, (zoneWindow.screenW - zoneWindow.spanW) / 2)
top: zoneWindow.vertical ? Math.max(0, (zoneWindow.screenH - zoneWindow.spanH) / 2) : 0
}
implicitWidth: zoneWindow.vertical ? zoneWindow.triggerThickness : zoneWindow.spanW
implicitHeight: zoneWindow.vertical ? zoneWindow.spanH : zoneWindow.triggerThickness
MouseArea {
id: edgeHoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
onContainsMouseChanged: {
if (containsMouse)
zoneWindow.requestLauncherOpen();
else
zoneWindow._openedForCurrentHover = false;
}
onPositionChanged: {
if (containsMouse)
zoneWindow.requestLauncherOpen();
}
}
}
}
}
+9
View File
@@ -23,6 +23,7 @@ Item {
property bool showSettingsMenu: false property bool showSettingsMenu: false
property string pendingSaveContent: "" property string pendingSaveContent: ""
readonly property bool conflictBannerVisible: currentTab !== null && NotepadStorageService.conflictTabId === currentTab.id readonly property bool conflictBannerVisible: currentTab !== null && NotepadStorageService.conflictTabId === currentTab.id
readonly property bool anyModalOpen: fileDialogOpen || confirmationDialogOpen
property var slideout: null property var slideout: null
property bool inPopout: false property bool inPopout: false
property bool surfaceVisible: slideout ? slideout.isVisible : true property bool surfaceVisible: slideout ? slideout.isVisible : true
@@ -50,6 +51,14 @@ Item {
slideout.suppressOverlayLayer = fileDialogOpen; slideout.suppressOverlayLayer = fileDialogOpen;
} }
Binding {
target: root.slideout
property: "hoverDismissSuspended"
value: root.anyModalOpen
when: root.slideout !== null
restoreMode: Binding.RestoreBindingOrValue
}
Connections { Connections {
target: slideout target: slideout
enabled: slideout !== null enabled: slideout !== null
@@ -330,6 +330,24 @@ Item {
pluginPopout.toggle(); pluginPopout.toggle();
} }
function triggerHoverPopout(widgetHostId) {
if (pillClickAction) {
triggerPopout();
return;
}
if (!hasPopout)
return;
const pill = isVertical ? verticalPill : horizontalPill;
const globalPos = pill.visualContent.mapToItem(null, 0, 0);
const currentScreen = parentScreen || Screen;
const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1));
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, pill.visualWidth, barSpacing, barPosition, barConfig);
pluginPopout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, barSpacing, barConfig);
PopoutManager.requestHoverPopout(pluginPopout, undefined, widgetHostId || pluginId);
}
PluginPopout { PluginPopout {
id: pluginPopout id: pluginPopout
contentWidth: root.popoutWidth contentWidth: root.popoutWidth
@@ -26,6 +26,19 @@ DankPopout {
open(); open();
} }
function prepareForTrigger(triggerSource) {
switch (triggerSource) {
case "memory":
DgopService.setSortBy("memory");
break;
case "cpu":
case "cpu_temp":
case "gpu_temp":
DgopService.setSortBy("cpu");
break;
}
}
popupWidth: Math.round(Theme.fontSizeMedium * 46) popupWidth: Math.round(Theme.fontSizeMedium * 46)
popupHeight: Math.round(Theme.fontSizeMedium * 39) popupHeight: Math.round(Theme.fontSizeMedium * 39)
triggerWidth: 55 triggerWidth: 55
@@ -171,6 +171,8 @@ Item {
scrollEnabled: defaultBar.scrollEnabled ?? true, scrollEnabled: defaultBar.scrollEnabled ?? true,
scrollXBehavior: defaultBar.scrollXBehavior ?? "column", scrollXBehavior: defaultBar.scrollXBehavior ?? "column",
scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace", scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace",
hoverPopouts: defaultBar.hoverPopouts ?? false,
hoverPopoutDelay: defaultBar.hoverPopoutDelay ?? 150,
shadowIntensity: defaultBar.shadowIntensity ?? 0, shadowIntensity: defaultBar.shadowIntensity ?? 0,
shadowOpacity: defaultBar.shadowOpacity ?? 60, shadowOpacity: defaultBar.shadowOpacity ?? 60,
shadowDirectionMode: defaultBar.shadowDirectionMode ?? "inherit", shadowDirectionMode: defaultBar.shadowDirectionMode ?? "inherit",
@@ -1255,6 +1257,50 @@ Item {
} }
} }
SettingsToggleCard {
iconName: "touch_app"
title: I18n.tr("Hover Popouts")
description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.")
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
enabled: !(selectedBarConfig?.clickThrough ?? false)
opacity: (selectedBarConfig?.clickThrough ?? false) ? 0.5 : 1.0
checked: selectedBarConfig?.hoverPopouts ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
hoverPopouts: checked
})
Column {
width: parent.width
spacing: Theme.spacingS
visible: selectedBarConfig?.hoverPopouts ?? false
leftPadding: Theme.spacingM
SettingsSliderRow {
id: hoverDelaySlider
width: parent.width - parent.leftPadding
text: I18n.tr("Open Delay")
description: I18n.tr("Time to rest on a widget before its popout opens")
value: selectedBarConfig?.hoverPopoutDelay ?? 150
minimum: 0
maximum: 1000
unit: "ms"
defaultValue: 150
onSliderValueChanged: newValue => {
SettingsData.updateBarConfig(selectedBarId, {
hoverPopoutDelay: newValue
});
}
Binding {
target: hoverDelaySlider
property: "value"
value: selectedBarConfig?.hoverPopoutDelay ?? 150
restoreMode: Binding.RestoreBinding
}
}
}
}
SettingsToggleCard { SettingsToggleCard {
iconName: "fit_screen" iconName: "fit_screen"
title: I18n.tr("Maximize Detection") title: I18n.tr("Maximize Detection")
+9
View File
@@ -357,6 +357,15 @@ Item {
checked: SettingsData.frameLauncherArcExtender checked: SettingsData.frameLauncherArcExtender
onToggled: checked => SettingsData.set("frameLauncherArcExtender", checked) onToggled: checked => SettingsData.set("frameLauncherArcExtender", checked)
} }
SettingsToggleRow {
settingKey: "frameLauncherEdgeHover"
tags: ["frame", "connected", "launcher", "hover", "edge", "reveal"]
text: I18n.tr("Edge Hover Reveal")
description: I18n.tr("Open the launcher by hovering the emerge edge (when free of bar and dock)")
checked: SettingsData.frameLauncherEdgeHover
onToggled: checked => SettingsData.set("frameLauncherEdgeHover", checked)
}
} }
SettingsCard { SettingsCard {
+1 -1
View File
@@ -359,7 +359,7 @@ Item {
tags: ["background", "color", "fill", "fit", "custom"] tags: ["background", "color", "fill", "fit", "custom"]
settingKey: "wallpaperBackgroundColorMode" settingKey: "wallpaperBackgroundColorMode"
text: I18n.tr("Background Color") text: I18n.tr("Background Color")
description: I18n.tr("Color shown for areas not covered by wallpaper (e.g. Fit or Pad modes)") description: I18n.tr("Color shown for areas not covered by wallpaper")
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#") visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
dropdownWidth: 220 dropdownWidth: 220
options: [ options: [
+15 -2
View File
@@ -502,15 +502,26 @@ Singleton {
property string _dankLauncherV2PendingQuery: "" property string _dankLauncherV2PendingQuery: ""
property string _dankLauncherV2PendingMode: "" property string _dankLauncherV2PendingMode: ""
property bool _dankLauncherV2TriggerUsesOverlayLayer: false property bool _dankLauncherV2TriggerUsesOverlayLayer: false
property bool _dankLauncherV2EdgeHoverManaged: false
function _setDankLauncherV2TriggerUsesOverlayLayer(value) { function _setDankLauncherV2TriggerUsesOverlayLayer(value) {
_dankLauncherV2TriggerUsesOverlayLayer = value === true; _dankLauncherV2TriggerUsesOverlayLayer = value === true;
// Disable edge-hover by default on every open/toggle path unless explicitly enabled.
_setDankLauncherV2EdgeHoverManaged(false);
if (dankLauncherV2Modal) if (dankLauncherV2Modal)
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer; dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
} }
function openDankLauncherV2(triggerUsesOverlayLayer) { // Set edgeHoverManaged to enable hover retraction for edge-hover triggered launcher sessions.
function _setDankLauncherV2EdgeHoverManaged(value) {
_dankLauncherV2EdgeHoverManaged = value === true;
if (dankLauncherV2Modal)
dankLauncherV2Modal.edgeHoverManaged = _dankLauncherV2EdgeHoverManaged;
}
function openDankLauncherV2(triggerUsesOverlayLayer, edgeHoverManaged) {
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer); _setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
_setDankLauncherV2EdgeHoverManaged(edgeHoverManaged);
if (dankLauncherV2Modal) { if (dankLauncherV2Modal) {
dankLauncherV2Modal.show(); dankLauncherV2Modal.show();
} else if (dankLauncherV2ModalLoader) { } else if (dankLauncherV2ModalLoader) {
@@ -591,8 +602,10 @@ Singleton {
} }
function _onDankLauncherV2ModalLoaded() { function _onDankLauncherV2ModalLoaded() {
if (dankLauncherV2Modal) if (dankLauncherV2Modal) {
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer; dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
dankLauncherV2Modal.edgeHoverManaged = _dankLauncherV2EdgeHoverManaged;
}
if (_dankLauncherV2WantsOpen) { if (_dankLauncherV2WantsOpen) {
_dankLauncherV2WantsOpen = false; _dankLauncherV2WantsOpen = false;
if (_dankLauncherV2PendingQuery) { if (_dankLauncherV2PendingQuery) {
+50
View File
@@ -24,6 +24,8 @@ Item {
property list<real> animationExitCurve: Theme.variantPopoutExitCurve property list<real> animationExitCurve: Theme.variantPopoutExitCurve
property bool suspendShadowWhileResizing: false property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false property bool shouldBeVisible: false
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool backgroundInteractive: true property bool backgroundInteractive: true
property bool contentHandlesKeys: false property bool contentHandlesKeys: false
@@ -82,6 +84,8 @@ Item {
readonly property real alignedY: impl.item ? impl.item.alignedY : 0 readonly property real alignedY: impl.item ? impl.item.alignedY : 0
readonly property real alignedWidth: impl.item ? impl.item.alignedWidth : 0 readonly property real alignedWidth: impl.item ? impl.item.alignedWidth : 0
readonly property real alignedHeight: impl.item ? impl.item.alignedHeight : 0 readonly property real alignedHeight: impl.item ? impl.item.alignedHeight : 0
readonly property real renderedAlignedY: impl.item ? (impl.item.renderedAlignedY ?? impl.item.alignedY) : 0
readonly property real renderedAlignedHeight: impl.item ? (impl.item.renderedAlignedHeight ?? impl.item.alignedHeight) : 0
readonly property real maskX: impl.item ? impl.item.maskX : 0 readonly property real maskX: impl.item ? impl.item.maskX : 0
readonly property real maskY: impl.item ? impl.item.maskY : 0 readonly property real maskY: impl.item ? impl.item.maskY : 0
readonly property real maskWidth: impl.item ? impl.item.maskWidth : 0 readonly property real maskWidth: impl.item ? impl.item.maskWidth : 0
@@ -172,6 +176,36 @@ Item {
impl.item.close(); impl.item.close();
} }
function cancelHoverDismiss() {
if (impl.item?.cancelHoverDismiss)
impl.item.cancelHoverDismiss();
}
// Fade out in place during morph switch transitions.
function beginSupersededClose() {
if (impl.item?.beginSupersededClose)
impl.item.beginSupersededClose();
}
function closeFromHoverDismiss() {
if (hoverDismissSuspended)
return;
hoverDismissEnabled = false;
// Enable animations using standard Theme-bound popout motion to preserve bindings.
if (impl.item)
impl.item.animationsEnabled = true;
for (const prop of ["dashVisible", "notificationHistoryVisible"]) {
if (root[prop] !== undefined) {
root[prop] = false;
return;
}
}
if (impl.item)
impl.item.close();
else
close();
}
function toggle() { function toggle() {
(shouldBeVisible || _pendingOpen) ? close() : open(); (shouldBeVisible || _pendingOpen) ? close() : open();
} }
@@ -210,6 +244,20 @@ Item {
impl.item.updateSurfacePosition(); impl.item.updateSurfacePosition();
} }
function containsGlobalPoint(gx, gy) {
if (!screen)
return false;
const presented = shouldBeVisible || (impl.item?.isClosing ?? false);
if (!presented)
return false;
const padding = 24;
const x = alignedX - padding;
const y = renderedAlignedY - padding;
const w = alignedWidth + padding * 2;
const h = renderedAlignedHeight + padding * 2;
return gx >= x && gx <= x + w && gy >= y && gy <= y + h;
}
Loader { Loader {
id: impl id: impl
active: root.screen !== null active: root.screen !== null
@@ -261,6 +309,8 @@ Item {
it.screen = Qt.binding(() => root.screen); it.screen = Qt.binding(() => root.screen);
it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition); it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition);
it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap); it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap);
it.hoverDismissEnabled = Qt.binding(() => root.hoverDismissEnabled);
it.hoverDismissSuspended = Qt.binding(() => root.hoverDismissSuspended);
it.shouldBeVisible = root.shouldBeVisible; it.shouldBeVisible = root.shouldBeVisible;
if (root._primeContent && typeof it.primeContent === "function") if (root._primeContent && typeof it.primeContent === "function")
+217 -24
View File
@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -41,6 +42,11 @@ Item {
property real _chromeAnimTravelX: 1 property real _chromeAnimTravelX: 1
property real _chromeAnimTravelY: 1 property real _chromeAnimTravelY: 1
property bool _fullSyncQueued: false property bool _fullSyncQueued: false
property bool _publishedBodyValid: false
property real _publishedBodyX: 0
property real _publishedBodyY: 0
property real _publishedBodyW: 0
property real _publishedBodyH: 0
property real storedBarThickness: Theme.barHeight - 4 property real storedBarThickness: Theme.barHeight - 4
property real storedBarSpacing: 4 property real storedBarSpacing: 4
@@ -130,7 +136,11 @@ Item {
updateBodyState: function(_name, ownerId, bodyX, bodyY, bodyW, bodyH) { updateBodyState: function(_name, ownerId, bodyX, bodyY, bodyW, bodyH) {
return ConnectedModeState.setPopoutBody(ownerId, bodyX, bodyY, bodyW, bodyH); return ConnectedModeState.setPopoutBody(ownerId, bodyX, bodyY, bodyW, bodyH);
} }
onRecoveryRequested: root._queueFullSync() onClaimIdChanged: root._resetPublishedBody()
onRecoveryRequested: {
root._resetPublishedBody();
root._queueFullSync();
}
} }
property var _lastOpenedScreen: null property var _lastOpenedScreen: null
@@ -233,11 +243,15 @@ Item {
const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible; const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible;
const presented = contentWindow.visible || root.shouldBeVisible; const presented = contentWindow.visible || root.shouldBeVisible;
const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open")); const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open"));
const bodyX = Theme.snap(root.pubBodyX, root.dpr);
const bodyY = Theme.snap(root.pubBodyY, root.dpr);
const bodyW = Theme.snap(root.pubBodyW, root.dpr);
const bodyH = Theme.snap(root.pubBodyH, root.dpr);
const bodyRect = { const bodyRect = {
"x": root.alignedX, "x": bodyX,
"y": root.renderedAlignedY, "y": bodyY,
"width": root.alignedWidth, "width": bodyW,
"height": root.renderedAlignedHeight "height": bodyH
}; };
const animationOffset = { const animationOffset = {
"x": _connectedChromeAnimX(), "x": _connectedChromeAnimX(),
@@ -254,10 +268,10 @@ Item {
"animationOffset": animationOffset, "animationOffset": animationOffset,
"scale": 1, "scale": 1,
"opacity": Theme.connectedSurfaceColor.a, "opacity": Theme.connectedSurfaceColor.a,
"bodyX": root.alignedX, "bodyX": bodyX,
"bodyY": root.renderedAlignedY, "bodyY": bodyY,
"bodyW": root.alignedWidth, "bodyW": bodyW,
"bodyH": root.renderedAlignedHeight, "bodyH": bodyH,
"animX": animationOffset.x, "animX": animationOffset.x,
"animY": animationOffset.y, "animY": animationOffset.y,
"screen": root.screen ? root.screen.name : "", "screen": root.screen ? root.screen.name : "",
@@ -269,10 +283,15 @@ Item {
function _publishConnectedChromeState(forceClaim, visibleOverride) { function _publishConnectedChromeState(forceClaim, visibleOverride) {
if (!root.frameOwnsConnectedChrome || !root.screen) if (!root.frameOwnsConnectedChrome || !root.screen)
return false; return false;
return chromeLease.publish(_connectedChromeState(visibleOverride), !!forceClaim); const state = _connectedChromeState(visibleOverride);
const published = chromeLease.publish(state, !!forceClaim);
if (published)
_rememberPublishedBody(state.bodyX, state.bodyY, state.bodyW, state.bodyH);
return published;
} }
function _releaseConnectedChromeState() { function _releaseConnectedChromeState() {
_resetPublishedBody();
chromeLease.release(); chromeLease.release();
} }
@@ -311,7 +330,26 @@ Item {
return; return;
if (!contentWindow.visible && !shouldBeVisible) if (!contentWindow.visible && !shouldBeVisible)
return; return;
chromeLease.updateBody(root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight); const bodyX = Theme.snap(root.pubBodyX, root.dpr);
const bodyY = Theme.snap(root.pubBodyY, root.dpr);
const bodyW = Theme.snap(root.pubBodyW, root.dpr);
const bodyH = Theme.snap(root.pubBodyH, root.dpr);
if (_publishedBodyValid && _publishedBodyX === bodyX && _publishedBodyY === bodyY && _publishedBodyW === bodyW && _publishedBodyH === bodyH)
return;
if (chromeLease.updateBody(bodyX, bodyY, bodyW, bodyH))
_rememberPublishedBody(bodyX, bodyY, bodyW, bodyH);
}
function _rememberPublishedBody(bodyX, bodyY, bodyW, bodyH) {
_publishedBodyX = bodyX;
_publishedBodyY = bodyY;
_publishedBodyW = bodyW;
_publishedBodyH = bodyH;
_publishedBodyValid = true;
}
function _resetPublishedBody() {
_publishedBodyValid = false;
} }
property bool _animSyncQueued: false property bool _animSyncQueued: false
@@ -356,7 +394,10 @@ Item {
onContentAnimYChanged: _queueAnimSync() onContentAnimYChanged: _queueAnimSync()
onRenderedAlignedYChanged: _queueBodySync() onRenderedAlignedYChanged: _queueBodySync()
onRenderedAlignedHeightChanged: _queueBodySync() onRenderedAlignedHeightChanged: _queueBodySync()
onScreenChanged: _queueFullSync() onScreenChanged: {
_resetPublishedBody();
_queueFullSync();
}
onEffectiveBarPositionChanged: _queueFullSync() onEffectiveBarPositionChanged: _queueFullSync()
Connections { Connections {
@@ -407,14 +448,31 @@ Item {
onFrameOwnsConnectedChromeChanged: _syncPopoutChromeState() onFrameOwnsConnectedChromeChanged: _syncPopoutChromeState()
property bool animationsEnabled: true property bool animationsEnabled: true
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissController.cancelPending();
}
function closeFromHoverDismiss() {
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return;
if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss();
else
close();
}
function open() { function open() {
if (!screen) if (!screen)
return; return;
_resetPublishedBody();
closeTimer.stop(); closeTimer.stop();
isClosing = false; isClosing = false;
animationsEnabled = false; animationsEnabled = false;
_primeContent = true; _primeContent = true;
_supersededClose = false;
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen; const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen;
if (screenChanged) { if (screenChanged) {
@@ -429,6 +487,13 @@ Item {
_captureChromeAnimTravel(); _captureChromeAnimTravel();
} }
// Seed travel coordinates from the outgoing popout to morph continuously.
_beginMorphTravel();
// Skip emerge animation on morph switch.
if (morphTravelEnabled)
morph.openProgress = 1;
if (root.frameOwnsConnectedChrome) { if (root.frameOwnsConnectedChrome) {
chromeLease.beginClaim(); chromeLease.beginClaim();
_publishConnectedChromeState(true, true); _publishConnectedChromeState(true, true);
@@ -456,6 +521,11 @@ Item {
} }
function close() { function close() {
if (_supersededClose && morphTravelEnabled)
_freezeMorphTravel();
else
_endMorphTravel();
_resetPublishedBody();
isClosing = true; isClosing = true;
shouldBeVisible = false; shouldBeVisible = false;
_primeContent = false; _primeContent = false;
@@ -494,6 +564,7 @@ Item {
onTriggered: { onTriggered: {
if (!shouldBeVisible) { if (!shouldBeVisible) {
contentWindow.visible = false; contentWindow.visible = false;
root._endMorphTravel();
isClosing = false; isClosing = false;
PopoutManager.hidePopout(popoutHandle); PopoutManager.hidePopout(popoutHandle);
popoutClosed(); popoutClosed();
@@ -642,6 +713,108 @@ Item {
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
} }
} }
// Morph transition coordinates to animate travel between popouts during switch.
property bool morphTravelEnabled: false
property real morphSeedX: 0
property real morphSeedY: 0
property real morphSeedW: 0
property real morphSeedH: 0
property real morphProgress: 1
// Distance-scaled duration for morph travel.
property int _morphTravelDuration: animationDuration
Behavior on morphProgress {
enabled: root.morphTravelEnabled && root.animationsEnabled
NumberAnimation {
duration: root._morphTravelDuration
easing.type: Easing.BezierSpline
// M3 Expressive spatial motion starts with momentum and settles gently,
// which keeps rapid hover retargets from pausing between surfaces.
easing.bezierCurve: Theme.variantEnterCurve
}
}
readonly property real pubBodyX: morphSeedX + (alignedX - morphSeedX) * morphProgress
readonly property real pubBodyY: morphSeedY + (renderedAlignedY - morphSeedY) * morphProgress
readonly property real pubBodyW: morphSeedW + (alignedWidth - morphSeedW) * morphProgress
readonly property real pubBodyH: morphSeedH + (renderedAlignedHeight - morphSeedH) * morphProgress
// One animation drives all four coordinates, so queue one coalesced state update
// per progress tick instead of reacting independently to each derived property.
onMorphProgressChanged: _queueBodySync()
function _beginMorphTravel() {
morphTravelEnabled = false;
morphProgress = 1;
if (!root.frameOwnsConnectedChrome || !root.screen)
return;
if (!root.hoverDismissEnabled)
return;
if (ConnectedModeState.popoutScreen !== root.screen.name)
return;
if (!ConnectedModeState.popoutOwnerId || ConnectedModeState.popoutOwnerId === chromeLease.claimId)
return;
const w = ConnectedModeState.popoutBodyW;
const h = ConnectedModeState.popoutBodyH;
if (!(w > 0 && h > 0))
return;
morphSeedX = ConnectedModeState.popoutBodyX;
morphSeedY = ConnectedModeState.popoutBodyY;
morphSeedW = w;
morphSeedH = h;
// Scale spatial motion with both travel and shape change. Never shorten the
// configured enter duration; cap long sweeps so hover switching stays responsive.
const base = Math.max(0, Theme.variantDuration(root.animationDuration, true));
const travel = Math.hypot(root.alignedX - morphSeedX, root.renderedAlignedY - morphSeedY);
const resize = Math.hypot(root.alignedWidth - morphSeedW, root.renderedAlignedHeight - morphSeedH);
const spatialDistance = travel + resize * 0.35;
_morphTravelDuration = Math.round(Math.min(base * 1.6, base + spatialDistance * 0.15));
morphProgress = 0;
morphTravelEnabled = true;
Qt.callLater(() => {
if (root.shouldBeVisible)
root.morphProgress = 1;
});
}
function _freezeMorphTravel() {
const x = pubBodyX;
const y = pubBodyY;
const w = pubBodyW;
const h = pubBodyH;
// A third hover can supersede a morph before it settles. Freeze the outgoing
// content at the live rectangle so it fades in place while the next surface
// inherits exactly the same geometry.
morphTravelEnabled = false;
morphSeedX = x;
morphSeedY = y;
morphSeedW = w;
morphSeedH = h;
morphProgress = 0;
morphTravelEnabled = true;
_syncPopoutBody();
}
function _endMorphTravel() {
morphTravelEnabled = false;
morphProgress = 1;
morphSeedX = 0;
morphSeedY = 0;
morphSeedW = 0;
morphSeedH = 0;
}
// Flag to trigger in-place fade-out during a morph switch.
property bool _supersededClose: false
function beginSupersededClose() {
// Only set superseded flag for transient hover switches.
if (frameOwnsConnectedChrome && hoverDismissEnabled)
_supersededClose = true;
}
readonly property real connectedAnchorX: { readonly property real connectedAnchorX: {
if (!root.usesConnectedSurfaceChrome) if (!root.usesConnectedSurfaceChrome)
return triggerX; return triggerX;
@@ -761,6 +934,15 @@ Item {
visible: false visible: false
color: "transparent" color: "transparent"
PopoutHoverDismiss {
id: hoverDismissController
anchors.fill: parent
dismissEnabled: root.hoverDismissEnabled
dismissSuspended: root.hoverDismissSuspended
surfaceVisible: root.shouldBeVisible
onDismissRequested: root.closeFromHoverDismiss()
}
WindowBlur { WindowBlur {
id: popoutBlur id: popoutBlur
targetWindow: contentWindow targetWindow: contentWindow
@@ -842,10 +1024,11 @@ Item {
Item { Item {
id: contentContainer id: contentContainer
x: root.alignedX // Follow the morphing body bounds during transition.
y: root.renderedAlignedY x: root.morphTravelEnabled ? root.pubBodyX : root.alignedX
width: root.alignedWidth y: root.morphTravelEnabled ? root.pubBodyY : root.renderedAlignedY
height: root.renderedAlignedHeight width: root.morphTravelEnabled ? root.pubBodyW : root.alignedWidth
height: root.morphTravelEnabled ? root.pubBodyH : root.renderedAlignedHeight
readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
@@ -914,6 +1097,11 @@ Item {
readonly property real computedScaleCollapsed: root.animationScaleCollapsed readonly property real computedScaleCollapsed: root.animationScaleCollapsed
PopoutHoverBodyTracker {
controller: hoverDismissController
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
}
QtObject { QtObject {
id: morph id: morph
property real openProgress: 0 property real openProgress: 0
@@ -941,7 +1129,8 @@ Item {
target: root target: root
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
root._captureChromeAnimTravel(); root._captureChromeAnimTravel();
morph.openProgress = root.shouldBeVisible ? 1 : 0; // Skip reverse emerge animation during a superseded close.
morph.openProgress = (root.shouldBeVisible || root._supersededClose) ? 1 : 0;
} }
} }
@@ -1067,23 +1256,27 @@ Item {
property bool _renderActive: Theme.isDirectionalEffect || shouldBeVisible property bool _renderActive: Theme.isDirectionalEffect || shouldBeVisible
property bool _animating: false property bool _animating: false
property real publishedOpacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0) readonly property bool _fadeWithOpacity: !Theme.isDirectionalEffect || root._supersededClose
// Fast fade duration for superseded close.
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
property real publishedOpacity: _targetOpacity
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0) opacity: _targetOpacity
visible: _renderActive visible: _renderActive
scale: contentContainer.scaleValue scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr) x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr) y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
layer.enabled: _animating || (!Theme.isDirectionalEffect && publishedOpacity < 1) layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
layer.smooth: false layer.smooth: false
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0) layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
Behavior on opacity { Behavior on opacity {
enabled: !Theme.isDirectionalEffect enabled: contentWrapper._fadeWithOpacity
NumberAnimation { NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
onRunningChanged: { onRunningChanged: {
@@ -1095,9 +1288,9 @@ Item {
} }
Behavior on publishedOpacity { Behavior on publishedOpacity {
enabled: !Theme.isDirectionalEffect enabled: contentWrapper._fadeWithOpacity
NumberAnimation { NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale) duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets
Item { Item {
id: root id: root
@@ -35,6 +36,22 @@ Item {
property bool shouldBeVisible: false property bool shouldBeVisible: false
property bool isClosing: false property bool isClosing: false
property bool animationsEnabled: true property bool animationsEnabled: true
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissController.cancelPending();
}
function closeFromHoverDismiss() {
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return;
if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss();
else
close();
}
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool backgroundInteractive: true property bool backgroundInteractive: true
property bool contentHandlesKeys: false property bool contentHandlesKeys: false
@@ -585,6 +602,17 @@ Item {
color: "transparent" color: "transparent"
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
PopoutHoverDismiss {
id: hoverDismissController
anchors.fill: parent
dismissEnabled: root.hoverDismissEnabled
dismissSuspended: root.hoverDismissSuspended
surfaceVisible: root.shouldBeVisible
globalOffsetX: root._surfaceMarginLeft
globalOffsetY: root._fullHeight ? 0 : root._surfaceMarginTop
onDismissRequested: root.closeFromHoverDismiss()
}
WindowBlur { WindowBlur {
id: popoutBlur id: popoutBlur
targetWindow: contentWindow targetWindow: contentWindow
@@ -702,6 +730,11 @@ Item {
readonly property real computedScaleCollapsed: root.animationScaleCollapsed readonly property real computedScaleCollapsed: root.animationScaleCollapsed
PopoutHoverBodyTracker {
controller: hoverDismissController
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
}
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1). // openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject { QtObject {
id: morph id: morph
+39 -2
View File
@@ -13,6 +13,8 @@ PanelWindow {
WlrLayershell.namespace: layerNamespace WlrLayershell.namespace: layerNamespace
property bool isVisible: false property bool isVisible: false
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
property var targetScreen: null property var targetScreen: null
property var modelData: null property var modelData: null
property bool triggerUsesOverlayLayer: false property bool triggerUsesOverlayLayer: false
@@ -25,6 +27,7 @@ PanelWindow {
property real edgeGap: 0 property real edgeGap: 0
property string slideEdge: "right" property string slideEdge: "right"
readonly property bool slideFromLeft: slideEdge === "left" readonly property bool slideFromLeft: slideEdge === "left"
readonly property real surfaceOriginX: slideFromLeft ? 0 : Math.max(0, (modelData?.width ?? width) - width)
property Component content: null property Component content: null
property string title: "" property string title: ""
property alias container: contentContainer property alias container: contentContainer
@@ -46,6 +49,27 @@ PanelWindow {
isVisible = false; isVisible = false;
} }
function hideFromHoverDismiss() {
if (hoverDismissSuspended)
return;
hoverDismissEnabled = false;
slideAnimation.duration = Math.round(Theme.expressiveDurations.expressiveDefaultSpatial);
hide();
}
function cancelHoverDismiss() {
hoverDismissTracker.cancelPending();
}
function containsGlobalPoint(gx, gy) {
if (!isVisible || !modelData)
return false;
const padding = 24;
const topLeft = slideContainer.mapToItem(null, 0, 0);
const globalX = surfaceOriginX + topLeft.x;
return gx >= globalX - padding && gx < globalX + slideContainer.width + padding && gy >= topLeft.y - padding && gy < topLeft.y + slideContainer.height + padding;
}
function toggle() { function toggle() {
if (isVisible) { if (isVisible) {
hide(); hide();
@@ -67,6 +91,17 @@ PanelWindow {
color: "transparent" color: "transparent"
HoverDismissTracker {
id: hoverDismissTracker
parent: root.contentItem
enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.isVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
onDismissRequested: root.hideFromHoverDismiss()
onHoverMoved: (sceneX, sceneY) => PopoutManager.updateHoverCursor(root.surfaceOriginX + sceneX, sceneY)
}
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
@@ -117,8 +152,10 @@ PanelWindow {
easing.type: Easing.OutCubic easing.type: Easing.OutCubic
onRunningChanged: { onRunningChanged: {
if (!running && !root.isVisible) { if (!running) {
root.mappedVisible = false; if (!root.isVisible)
root.mappedVisible = false;
slideAnimation.duration = 450;
} }
} }
} }
@@ -0,0 +1,30 @@
pragma ComponentBehavior: Bound
import QtQuick
HoverHandler {
id: root
property var shouldDismiss: null
signal dismissRequested
// Emitted on every hover move; passive to avoid blocking overlapping MouseAreas
signal hoverMoved(real gx, real gy)
onPointChanged: {
if (!enabled || !hovered)
return;
const gp = parent.mapToItem(null, point.position.x, point.position.y);
hoverMoved(gp.x, gp.y);
}
onHoveredChanged: {
if (hovered || !enabled)
return;
if (typeof shouldDismiss === "function" && !shouldDismiss())
return;
dismissRequested();
}
function cancelPending() {
}
}
@@ -0,0 +1,25 @@
pragma ComponentBehavior: Bound
import QtQuick
HoverHandler {
id: root
required property var controller
property bool trackingEnabled: false
enabled: trackingEnabled
onTrackingEnabledChanged: {
if (!trackingEnabled)
controller.updateBodyHover(false);
}
onHoveredChanged: controller.updateBodyHover(hovered)
onPointChanged: {
if (!hovered)
return;
const gp = parent.mapToItem(null, point.position.x, point.position.y);
controller.updateCursor(gp.x, gp.y);
}
}
+75
View File
@@ -0,0 +1,75 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
Item {
id: root
required property bool dismissEnabled
required property bool dismissSuspended
required property bool surfaceVisible
property int graceInterval: 150
property bool bodyHovered: false
property real globalOffsetX: 0
property real globalOffsetY: 0
signal dismissRequested
function cancelPending() {
graceTimer.stop();
hoverTracker.cancelPending();
}
function updateBodyHover(over) {
bodyHovered = over;
if (over) {
graceTimer.stop();
} else if (dismissEnabled && !dismissSuspended && surfaceVisible) {
graceTimer.restart();
}
}
function updateCursor(sceneX, sceneY) {
PopoutManager.updateHoverCursor(sceneX + globalOffsetX, sceneY + globalOffsetY);
}
onDismissEnabledChanged: {
if (!dismissEnabled)
cancelPending();
}
onDismissSuspendedChanged: {
if (dismissSuspended)
graceTimer.stop();
else if (dismissEnabled && surfaceVisible && !bodyHovered)
graceTimer.restart();
}
onSurfaceVisibleChanged: {
if (!surfaceVisible)
cancelPending();
}
Timer {
id: graceTimer
interval: root.graceInterval
repeat: false
onTriggered: {
if (!root.dismissEnabled || root.dismissSuspended || !root.surfaceVisible || root.bodyHovered)
return;
if (PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY))
return;
root.dismissRequested();
}
}
HoverDismissTracker {
id: hoverTracker
enabled: root.dismissEnabled && !root.dismissSuspended && root.surfaceVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
onDismissRequested: root.dismissRequested()
onHoverMoved: (gx, gy) => root.updateCursor(gx, gy)
}
}
@@ -362,7 +362,7 @@
"wallpaper" "wallpaper"
], ],
"icon": "wallpaper", "icon": "wallpaper",
"description": "Color shown for areas not covered by wallpaper (e.g. Fit or Pad modes)" "description": "Color shown for areas not covered by wallpaper"
}, },
{ {
"section": "selectedMonitor", "section": "selectedMonitor",
@@ -8139,6 +8139,36 @@
], ],
"icon": "monitor" "icon": "monitor"
}, },
{
"section": "frameLauncherEdgeHover",
"label": "Edge Hover Reveal",
"tabIndex": 33,
"category": "Frame",
"keywords": [
"app drawer",
"app menu",
"applications",
"border",
"connected",
"decoration",
"edge",
"emerge",
"frame",
"free",
"hover",
"hovering",
"launcher",
"open",
"panel",
"reveal",
"start menu",
"statusbar",
"taskbar",
"topbar",
"window"
],
"description": "Open the launcher by hovering the emerge edge (when free of bar and dock)"
},
{ {
"section": "frameEnable", "section": "frameEnable",
"label": "Enable Frame", "label": "Enable Frame",