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

feat(popouts): enhance hover functionality and introduce hover dismiss features

This commit is contained in:
purian23
2026-06-27 22:21:03 -04:00
parent 601d4104a3
commit a8c15fcde9
15 changed files with 1232 additions and 955 deletions
+41 -100
View File
@@ -25,12 +25,12 @@ Singleton {
hoverCursorGlobalY = gy;
}
function cursorOverBar(gx, gy, padding) {
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)
if (!w?.visible || w === excludedWindow)
continue;
if (typeof w.containsGlobalPoint === "function") {
if (w.containsGlobalPoint(gx, gy, pad))
@@ -199,104 +199,22 @@ Singleton {
return !!name && currentPopoutsByScreen[name] === popout;
}
function requestPopout(popout, tabIndex, triggerSource) {
function _requestPopout(popout, tabIndex, triggerSource, hoverRequest) {
if (!popout || !popout.screen)
return;
// Clicking a hover popout pins it open rather than toggling it closed
// Clicking a transient popout pins it instead of toggling it closed.
const wasTransient = popout.hoverDismissEnabled === true;
if (popout.hoverDismissEnabled !== undefined)
if (!hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
screenshotActive = false;
const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName];
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
const alreadyPresented = currentPopout === popout && (hoverRequest ? _isPopoutPresented(popout) : popout.shouldBeVisible);
const willOpen = !(currentPopout === popout && popout.shouldBeVisible && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
if (willOpen) {
popoutOpening();
}
let movedFromOtherScreen = false;
for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName)
continue;
const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout)
continue;
if (_isStale(otherPopout)) {
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
if (otherPopout === popout) {
movedFromOtherScreen = true;
currentPopoutsByScreen[otherScreenName] = null;
currentPopoutTriggers[otherScreenName] = null;
continue;
}
_closePopout(otherPopout);
}
if (currentPopout && currentPopout !== popout) {
if (_isStale(currentPopout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
} else {
_closePopout(currentPopout);
}
}
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
const sameTrigger = triggerId === undefined || currentPopoutTriggers[screenName] === triggerId;
if (sameTrigger) {
if (!wasTransient) {
_closePopout(popout);
return;
}
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
if (triggerId !== undefined)
currentPopoutTriggers[screenName] = triggerId;
return;
}
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex;
}
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
currentPopoutTriggers[screenName] = triggerId;
return;
}
currentPopoutTriggers[screenName] = triggerId;
currentPopoutsByScreen[screenName] = popout;
popoutChanged();
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex;
}
if (currentPopout !== popout) {
ModalManager.closeAllModalsExcept(null);
}
_openPopout(popout);
}
function requestHoverPopout(popout, tabIndex, triggerSource) {
if (!popout || !popout.screen)
return;
screenshotActive = false;
const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName];
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
const willOpen = !(currentPopout === popout && _isPopoutPresented(popout) && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
const willOpen = !(alreadyPresented && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
if (willOpen)
popoutOpening();
@@ -329,23 +247,36 @@ Singleton {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
} else {
// Signal the active popout to fade in-place when morphed
if (typeof currentPopout.beginSupersededClose === "function")
if (hoverRequest && typeof currentPopout.beginSupersededClose === "function")
currentPopout.beginSupersededClose();
_closePopout(currentPopout);
}
}
if (currentPopout === popout && _isPopoutPresented(popout) && !movedFromOtherScreen) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId)
if (alreadyPresented && !movedFromOtherScreen) {
const sameDefinedTrigger = triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId;
if (hoverRequest && sameDefinedTrigger)
return;
if (tabIndex !== undefined && popout.currentTabIndex !== undefined)
if (!hoverRequest && (triggerId === undefined || sameDefinedTrigger)) {
if (!wasTransient) {
_closePopout(popout);
return;
}
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
if (triggerId !== undefined)
currentPopoutTriggers[screenName] = triggerId;
return;
}
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex;
}
if (popout.updateSurfacePosition)
popout.updateSurfacePosition();
currentPopoutTriggers[screenName] = triggerId;
if (popout.hoverDismissEnabled !== undefined)
if (hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = true;
return;
}
@@ -354,15 +285,25 @@ Singleton {
currentPopoutsByScreen[screenName] = popout;
popoutChanged();
if (tabIndex !== undefined && popout.currentTabIndex !== undefined)
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex;
}
if (currentPopout !== popout)
if (currentPopout !== popout) {
ModalManager.closeAllModalsExcept(null);
}
if (popout.hoverDismissEnabled !== undefined)
if (hoverRequest && popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = true;
_openPopout(popout);
}
function requestPopout(popout, tabIndex, triggerSource) {
_requestPopout(popout, tabIndex, triggerSource, false);
}
function requestHoverPopout(popout, tabIndex, triggerSource) {
_requestPopout(popout, tabIndex, triggerSource, true);
}
}
+3 -1
View File
@@ -569,7 +569,9 @@ var SPEC = {
shadowOpacity: 60,
shadowColorMode: "default",
shadowCustomColor: "#000000",
clickThrough: false
clickThrough: false,
hoverPopouts: false,
hoverPopoutDelay: 150
}], onChange: "updateBarConfigs"
},
+40 -674
View File
@@ -388,53 +388,51 @@ Item {
return "left";
}
property string activeHoverTrigger: ""
property real _lastHoverGlobalX: 0
property real _lastHoverGlobalY: 0
readonly property bool hoverPopoutsEnabled: barConfig?.hoverPopouts ?? false
readonly property int hoverPopoutDelay: Math.max(0, barConfig?.hoverPopoutDelay ?? 150)
// Clean up hover state and close transient popouts when the hover feature is disabled.
onHoverPopoutsEnabledChanged: {
if (hoverPopoutsEnabled)
return;
_cancelPendingHover();
_hoverCloseTimer.stop();
if (hasOpenHoverSurface() && !PopoutManager.isActivePopoutPinned(barWindow?.screen))
closeHoverSurfaces();
activeHoverTrigger = "";
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
}
property var _pendingHoverHit: null
property string _pendingHoverTrigger: ""
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() {
_cancelPendingHover();
_hoverCloseTimer.stop();
_pendingPopoutOpenSpec = null;
const activePopout = PopoutManager.getActivePopout(barWindow?.screen);
const hasTransientSurface = activeHoverTrigger !== "" || activePopout?.hoverDismissEnabled === true;
if (hasTransientSurface && !PopoutManager.isActivePopoutPinned(barWindow?.screen))
closeHoverSurfaces();
else
activeHoverTrigger = "";
hoverController.resetForBarGeometryChange();
}
Timer {
id: _hoverIntentTimer
interval: topBarContent.hoverPopoutDelay
repeat: false
onTriggered: topBarContent._commitPendingHover()
}
// Grace timer to prevent flicker when crossing gaps.
Timer {
id: _hoverCloseTimer
interval: 120
repeat: false
onTriggered: topBarContent._commitHoverClose()
function _dashTriggerSource(section, tabIndex) {
return hoverController.dashTriggerSource(section, tabIndex);
}
function getBarPosition() {
@@ -512,7 +510,7 @@ Item {
topBarContent._pendingPopoutOpenSpec = null;
topBarContent._finishWidgetPopoutOpen(pending, loader.item);
if (pending.mode === "hover")
topBarContent.checkHoverPopout(topBarContent._lastHoverGlobalX, topBarContent._lastHoverGlobalY);
hoverController.recheckLatestPoint();
};
if (loader.item) {
onLoaded();
@@ -557,638 +555,6 @@ Item {
return true;
}
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"
}
];
}
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) {
const layout = section.layoutLoader?.item;
if (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 === section.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;
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 (!_widgetSupportsHoverPopout(widgetId, widgetItem))
return;
if (!getWidgetVisible(widgetId))
return;
seen.add(widgetItem);
candidates.push({
widgetId,
widgetItem,
section: widgetItem.section || sectionHint || "right",
wrapper: 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(c => c.widgetItem === entry.host.item);
if (existing) {
existing.wrapper = entry.wrapper;
if (!existing.section)
existing.section = entry.section;
continue;
}
candidates.push({
widgetId: entry.host.widgetId,
widgetItem: entry.host.item,
section: entry.host.item.section || entry.section,
wrapper: entry.wrapper
});
}
return candidates;
}
function _globalItemBounds(item) {
const topLeft = item.mapToItem(null, 0, 0);
return {
x: topLeft.x,
y: topLeft.y,
width: item.width,
height: item.height
};
}
function _hitBoundsForWidget(widgetItem, wrapper) {
if (!widgetItem?.visible)
return null;
if (widgetItem.visualContent !== undefined) {
const visual = widgetItem.visualContent;
if (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);
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 findWidgetAtGlobalPoint(gx, gy) {
const candidates = _collectHoverCandidates();
let best = null;
let bestArea = Infinity;
for (let i = 0; i < candidates.length; i++) {
const entry = candidates[i];
const bounds = _hitBoundsForWidget(entry.widgetItem, entry.wrapper);
if (!bounds || bounds.width <= 0 || bounds.height <= 0)
continue;
if (!_pointInBounds(gx, gy, bounds))
continue;
const area = bounds.width * bounds.height;
if (area < bestArea) {
bestArea = area;
best = {
widgetId: entry.widgetId,
widgetItem: entry.widgetItem,
section: entry.section
};
}
}
return best;
}
function _dashTriggerSource(section, tabIndex) {
return (barConfig?.id ?? "default") + "-" + section + "-" + tabIndex;
}
function _notepadWidgetForScreen() {
const screenName = barWindow?.screen?.name;
const fromRegistry = screenName ? BarWidgetService.getWidget("notepadButton", screenName) : null;
if (fromRegistry)
return fromRegistry;
const candidates = _collectHoverCandidates();
for (let i = 0; i < candidates.length; i++) {
if (candidates[i].widgetId === "notepadButton")
return candidates[i].widgetItem;
}
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 cursorOverHoverChain(gx, gy) {
if (PopoutManager.cursorOverBar(gx, gy))
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();
}
// Fade out the active popout in-place during morph switch transitions.
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 inst = _notepadWidgetForScreen()?.notepadInstance;
if (!inst?.isVisible)
activeHoverTrigger = "";
return;
}
if (activeHoverTrigger === "")
return;
if (!hasOpenHoverSurface())
activeHoverTrigger = "";
}
function hasOpenHoverSurface() {
if (activeHoverTrigger === "")
return false;
if (activeHoverTrigger === "notepadButton") {
const inst = _notepadWidgetForScreen()?.notepadInstance;
return inst?.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 openHoverPopoutForHit(hit) {
if (!hit?.widgetItem)
return false;
const widgetId = hit.widgetId;
const widgetItem = hit.widgetItem;
const section = hit.section;
const mode = "hover";
const base = {
widgetItem,
section,
mode
};
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;
}
switch (widgetId) {
case "launcherButton":
return openWidgetPopout(Object.assign({}, base, {
loader: appDrawerLoader,
triggerSource: "appDrawer",
visualItem: widgetItem
}));
case "clipboard":
return openWidgetPopout(Object.assign({}, base, {
loader: clipboardHistoryPopoutLoader,
triggerSource: "clipboard",
prepare: popout => {
popout.activeTab = "recents";
}
}));
case "clock":
return openWidgetPopout(Object.assign({}, base, {
loader: dankDashPopoutLoader,
tabIndex: 0,
triggerSource: _dashTriggerSource(section, 0),
useCenterSection: true,
setTriggerScreen: true
}));
case "music":
return openWidgetPopout(Object.assign({}, base, {
loader: dankDashPopoutLoader,
tabIndex: 1,
triggerSource: _dashTriggerSource(section, 1),
useCenterSection: true,
setTriggerScreen: true
}));
case "weather":
return openWidgetPopout(Object.assign({}, base, {
loader: dankDashPopoutLoader,
tabIndex: 3,
triggerSource: _dashTriggerSource(section, 3),
useCenterSection: true,
setTriggerScreen: true
}));
case "cpuUsage":
return openWidgetPopout(Object.assign({}, base, {
loader: processListPopoutLoader,
triggerSource: "cpu"
}));
case "memUsage":
return openWidgetPopout(Object.assign({}, base, {
loader: processListPopoutLoader,
triggerSource: "memory"
}));
case "cpuTemp":
return openWidgetPopout(Object.assign({}, base, {
loader: processListPopoutLoader,
triggerSource: "cpu_temp"
}));
case "gpuTemp":
return openWidgetPopout(Object.assign({}, base, {
loader: processListPopoutLoader,
triggerSource: "gpu_temp"
}));
case "notificationButton":
return openWidgetPopout(Object.assign({}, base, {
loader: notificationCenterLoader,
triggerSource: "notifications",
setTriggerScreen: true
}));
case "battery":
return openWidgetPopout(Object.assign({}, base, {
loader: batteryPopoutLoader,
triggerSource: "battery"
}));
case "layout":
return openWidgetPopout(Object.assign({}, base, {
loader: layoutPopoutLoader,
triggerSource: "layout"
}));
case "vpn":
return openWidgetPopout(Object.assign({}, base, {
loader: vpnPopoutLoader,
triggerSource: "vpn"
}));
case "controlCenterButton":
if (openWidgetPopout(Object.assign({}, base, {
loader: controlCenterLoader,
triggerSource: "controlCenter",
setTriggerScreen: true
}))) {
if (controlCenterLoader.item?.shouldBeVisible && NetworkService.wifiEnabled)
NetworkService.scanWifi();
return true;
}
return false;
case "systemUpdate":
return openWidgetPopout(Object.assign({}, base, {
loader: systemUpdateLoader,
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();
// Ignore hover events when a popout is pinned open.
if (PopoutManager.isActivePopoutPinned(barWindow?.screen))
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 = "";
}
// Maps widgets to their loaders to support in-place switching between triggers sharing a popout.
function _loaderForWidgetId(widgetId) {
switch (widgetId) {
case "launcherButton":
return appDrawerLoader;
case "clipboard":
return clipboardHistoryPopoutLoader;
case "clock":
case "music":
case "weather":
return dankDashPopoutLoader;
case "cpuUsage":
case "memUsage":
case "cpuTemp":
case "gpuTemp":
return processListPopoutLoader;
case "notificationButton":
return notificationCenterLoader;
case "battery":
return batteryPopoutLoader;
case "layout":
return layoutPopoutLoader;
case "vpn":
return vpnPopoutLoader;
case "controlCenterButton":
return controlCenterLoader;
case "systemUpdate":
return systemUpdateLoader;
default:
return null;
}
}
function _hitTargetsActivePopout(hit) {
const active = PopoutManager.getActivePopout(barWindow?.screen);
if (!active || !hit)
return false;
const loader = _loaderForWidgetId(hit.widgetId);
if (!loader)
return false;
return _resolvePopoutFromLoader(loader) === active;
}
function _commitPendingHover() {
const hit = _pendingHoverHit;
const triggerKey = _pendingHoverTrigger;
_pendingHoverHit = null;
_pendingHoverTrigger = "";
if (!hit || !hoverPopoutsEnabled)
return;
if (PopoutManager.isActivePopoutPinned(barWindow?.screen))
return;
// Cursor may have left the bar before the timer fired.
if (!PopoutManager.cursorOverBar(_lastHoverGlobalX, _lastHoverGlobalY))
return;
const activePopout = PopoutManager.getActivePopout(barWindow?.screen);
const targetLoader = _loaderForWidgetId(hit.widgetId);
const targetPopout = _resolvePopoutFromLoader(targetLoader);
const managerOwnsTransition = !!(activePopout && targetPopout);
// A different trigger backed by the same already-open popout swaps tab/position
// in place. PopoutManager also owns handoff between loaded popouts, so only
// pre-close special/unmanaged surfaces here.
if (triggerKey !== activeHoverTrigger && activeHoverTrigger !== "" && !_hitTargetsActivePopout(hit)) {
if (!managerOwnsTransition) {
_beginSupersededCloseForActive();
closeHoverSurfaces();
}
}
if (!openHoverPopoutForHit(hit)) {
if (activeHoverTrigger !== "")
closeHoverSurfaces();
return;
}
activeHoverTrigger = triggerKey;
}
function scheduleHoverClose(gx, gy) {
_cancelPendingHover();
if (!hoverPopoutsEnabled)
return;
if (PopoutManager.isActivePopoutPinned(barWindow?.screen))
return;
if (cursorOverHoverChain(gx, gy))
return;
_hoverCloseTimer.restart();
}
function _commitHoverClose() {
const gx = PopoutManager.hoverCursorGlobalX;
const gy = PopoutManager.hoverCursorGlobalY;
if (PopoutManager.isActivePopoutPinned(barWindow?.screen))
return;
if (cursorOverHoverChain(gx, gy))
return;
closeHoverSurfaces();
}
readonly property var widgetVisibility: ({
"cpuUsage": DgopService.dgopAvailable,
"memUsage": DgopService.dgopAvailable,
@@ -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();
}
}
+2 -4
View File
@@ -1123,13 +1123,11 @@ PanelWindow {
const gp = barUnitInset.mapToItem(null, point.position.x, point.position.y);
lastGlobalX = gp.x;
lastGlobalY = gp.y;
topBarContent.checkHoverPopout(gp.x, gp.y);
topBarContent.queueHoverPopout(gp.x, gp.y);
}
onHoveredChanged: {
if (hovered)
return;
topBarContent.scheduleHoverClose(lastGlobalX, lastGlobalY);
topBarContent.updateHoverBarHovered(hovered);
}
}
}
@@ -19,6 +19,7 @@ Item {
property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
property alias widgetLayoutLoader: layoutLoader
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
@@ -19,6 +19,7 @@ Item {
property bool forceVerticalLayout: false
readonly property bool isVertical: overrideAxisLayout ? forceVerticalLayout : (axis?.isVertical ?? false)
property alias widgetLayoutLoader: layoutLoader
implicitHeight: layoutLoader.item ? layoutLoader.item.implicitHeight : 0
implicitWidth: layoutLoader.item ? layoutLoader.item.implicitWidth : 0
+9
View File
@@ -23,6 +23,7 @@ Item {
property bool showSettingsMenu: false
property string pendingSaveContent: ""
readonly property bool conflictBannerVisible: currentTab !== null && NotepadStorageService.conflictTabId === currentTab.id
readonly property bool anyModalOpen: fileDialogOpen || confirmationDialogOpen
property var slideout: null
property bool inPopout: false
property bool surfaceVisible: slideout ? slideout.isVisible : true
@@ -50,6 +51,14 @@ Item {
slideout.suppressOverlayLayer = fileDialogOpen;
}
Binding {
target: root.slideout
property: "hoverDismissSuspended"
value: root.anyModalOpen
when: root.slideout !== null
restoreMode: Binding.RestoreBindingOrValue
}
Connections {
target: slideout
enabled: slideout !== null
@@ -172,6 +172,7 @@ Item {
scrollXBehavior: defaultBar.scrollXBehavior ?? "column",
scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace",
hoverPopouts: defaultBar.hoverPopouts ?? false,
hoverPopoutDelay: defaultBar.hoverPopoutDelay ?? 150,
shadowIntensity: defaultBar.shadowIntensity ?? 0,
shadowOpacity: defaultBar.shadowOpacity ?? 60,
shadowDirectionMode: defaultBar.shadowDirectionMode ?? "inherit",
+63 -77
View File
@@ -42,6 +42,11 @@ Item {
property real _chromeAnimTravelX: 1
property real _chromeAnimTravelY: 1
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 storedBarSpacing: 4
@@ -131,7 +136,11 @@ Item {
updateBodyState: function(_name, 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
@@ -234,11 +243,15 @@ Item {
const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible;
const presented = contentWindow.visible || root.shouldBeVisible;
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 = {
"x": root.pubBodyX,
"y": root.pubBodyY,
"width": root.pubBodyW,
"height": root.pubBodyH
"x": bodyX,
"y": bodyY,
"width": bodyW,
"height": bodyH
};
const animationOffset = {
"x": _connectedChromeAnimX(),
@@ -255,10 +268,10 @@ Item {
"animationOffset": animationOffset,
"scale": 1,
"opacity": Theme.connectedSurfaceColor.a,
"bodyX": root.pubBodyX,
"bodyY": root.pubBodyY,
"bodyW": root.pubBodyW,
"bodyH": root.pubBodyH,
"bodyX": bodyX,
"bodyY": bodyY,
"bodyW": bodyW,
"bodyH": bodyH,
"animX": animationOffset.x,
"animY": animationOffset.y,
"screen": root.screen ? root.screen.name : "",
@@ -270,10 +283,15 @@ Item {
function _publishConnectedChromeState(forceClaim, visibleOverride) {
if (!root.frameOwnsConnectedChrome || !root.screen)
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() {
_resetPublishedBody();
chromeLease.release();
}
@@ -312,7 +330,26 @@ Item {
return;
if (!contentWindow.visible && !shouldBeVisible)
return;
chromeLease.updateBody(root.pubBodyX, root.pubBodyY, root.pubBodyW, root.pubBodyH);
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
@@ -357,7 +394,10 @@ Item {
onContentAnimYChanged: _queueAnimSync()
onRenderedAlignedYChanged: _queueBodySync()
onRenderedAlignedHeightChanged: _queueBodySync()
onScreenChanged: _queueFullSync()
onScreenChanged: {
_resetPublishedBody();
_queueFullSync();
}
onEffectiveBarPositionChanged: _queueFullSync()
Connections {
@@ -412,8 +452,7 @@ Item {
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissTracker.cancelPending();
_hoverDismissGrace.stop();
hoverDismissController.cancelPending();
}
function closeFromHoverDismiss() {
@@ -428,6 +467,7 @@ Item {
function open() {
if (!screen)
return;
_resetPublishedBody();
closeTimer.stop();
isClosing = false;
animationsEnabled = false;
@@ -485,6 +525,7 @@ Item {
_freezeMorphTravel();
else
_endMorphTravel();
_resetPublishedBody();
isClosing = true;
shouldBeVisible = false;
_primeContent = false;
@@ -875,40 +916,6 @@ Item {
readonly property real maskWidth: _dismissZone.width
readonly property real maskHeight: _dismissZone.height
// Track body hover to initiate grace timer for transient dismissal.
property bool _hoverOverBody: false
function _onBodyHoverChanged(over) {
_hoverOverBody = over;
if (over)
_hoverDismissGrace.stop();
else if (root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible)
_hoverDismissGrace.restart();
}
onHoverDismissSuspendedChanged: {
if (hoverDismissSuspended) {
_hoverDismissGrace.stop();
} else if (hoverDismissEnabled && shouldBeVisible && !_hoverOverBody) {
_hoverDismissGrace.restart();
}
}
Timer {
id: _hoverDismissGrace
interval: 150
repeat: false
onTriggered: {
if (!root.hoverDismissEnabled || root.hoverDismissSuspended || !root.shouldBeVisible)
return;
if (root._hoverOverBody)
return;
if (PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY))
return;
root.closeFromHoverDismiss();
}
}
DismissZone {
id: _dismissZone
barPosition: root.effectiveBarPosition
@@ -927,26 +934,13 @@ Item {
visible: false
color: "transparent"
MouseArea {
PopoutHoverDismiss {
id: hoverDismissController
anchors.fill: parent
z: -1
acceptedButtons: Qt.NoButton
hoverEnabled: true
onPositionChanged: mouse => {
const gp = mapToItem(null, mouse.x, mouse.y);
PopoutManager.updateHoverCursor(gp.x, gp.y);
}
}
HoverDismissTracker {
id: hoverDismissTracker
anchors.fill: parent
enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
dismissEnabled: root.hoverDismissEnabled
dismissSuspended: root.hoverDismissSuspended
surfaceVisible: root.shouldBeVisible
onDismissRequested: root.closeFromHoverDismiss()
onHoverMoved: (gx, gy) => PopoutManager.updateHoverCursor(gx, gy)
}
WindowBlur {
@@ -1103,17 +1097,9 @@ Item {
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
// Ancestor HoverHandler to capture body hover reliably.
HoverHandler {
id: bodyHoverHandler
enabled: root.hoverDismissEnabled && root.shouldBeVisible
onHoveredChanged: root._onBodyHoverChanged(hovered)
onPointChanged: {
if (!bodyHoverHandler.hovered)
return;
const gp = contentContainer.mapToItem(null, bodyHoverHandler.point.position.x, bodyHoverHandler.point.position.y);
PopoutManager.updateHoverCursor(gp.x, gp.y);
}
PopoutHoverBodyTracker {
controller: hoverDismissController
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
}
QtObject {
+11 -65
View File
@@ -40,8 +40,7 @@ Item {
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissTracker.cancelPending();
_hoverDismissGrace.stop();
hoverDismissController.cancelPending();
}
function closeFromHoverDismiss() {
@@ -53,40 +52,6 @@ Item {
close();
}
// Track body hover to initiate grace timer for transient dismissal.
property bool _hoverOverBody: false
function _onBodyHoverChanged(over) {
_hoverOverBody = over;
if (over)
_hoverDismissGrace.stop();
else if (root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible)
_hoverDismissGrace.restart();
}
onHoverDismissSuspendedChanged: {
if (hoverDismissSuspended) {
_hoverDismissGrace.stop();
} else if (hoverDismissEnabled && shouldBeVisible && !_hoverOverBody) {
_hoverDismissGrace.restart();
}
}
Timer {
id: _hoverDismissGrace
interval: 150
repeat: false
onTriggered: {
if (!root.hoverDismissEnabled || root.hoverDismissSuspended || !root.shouldBeVisible)
return;
if (root._hoverOverBody)
return;
if (PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY))
return;
root.closeFromHoverDismiss();
}
}
property var customKeyboardFocus: null
property bool backgroundInteractive: true
property bool contentHandlesKeys: false
@@ -637,26 +602,15 @@ Item {
color: "transparent"
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
MouseArea {
PopoutHoverDismiss {
id: hoverDismissController
anchors.fill: parent
z: -1
acceptedButtons: Qt.NoButton
hoverEnabled: true
onPositionChanged: mouse => {
const gp = mapToItem(null, mouse.x, mouse.y);
PopoutManager.updateHoverCursor(gp.x, gp.y);
}
}
HoverDismissTracker {
id: hoverDismissTracker
anchors.fill: parent
enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
dismissEnabled: root.hoverDismissEnabled
dismissSuspended: root.hoverDismissSuspended
surfaceVisible: root.shouldBeVisible
globalOffsetX: root._surfaceMarginLeft
globalOffsetY: root._fullHeight ? 0 : root._surfaceMarginTop
onDismissRequested: root.closeFromHoverDismiss()
onHoverMoved: (gx, gy) => PopoutManager.updateHoverCursor(gx, gy)
}
WindowBlur {
@@ -776,17 +730,9 @@ Item {
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
// Ancestor HoverHandler to capture body hover reliably.
HoverHandler {
id: bodyHoverHandler
enabled: root.hoverDismissEnabled && root.shouldBeVisible
onHoveredChanged: root._onBodyHoverChanged(hovered)
onPointChanged: {
if (!bodyHoverHandler.hovered)
return;
const gp = contentContainer.mapToItem(null, bodyHoverHandler.point.position.x, bodyHoverHandler.point.position.y);
PopoutManager.updateHoverCursor(gp.x, gp.y);
}
PopoutHoverBodyTracker {
controller: hoverDismissController
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
}
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
+9 -14
View File
@@ -14,6 +14,7 @@ PanelWindow {
property bool isVisible: false
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
property var targetScreen: null
property var modelData: null
property bool triggerUsesOverlayLayer: false
@@ -26,6 +27,7 @@ PanelWindow {
property real edgeGap: 0
property string slideEdge: "right"
readonly property bool slideFromLeft: slideEdge === "left"
readonly property real surfaceOriginX: slideFromLeft ? 0 : Math.max(0, (modelData?.width ?? width) - width)
property Component content: null
property string title: ""
property alias container: contentContainer
@@ -48,6 +50,8 @@ PanelWindow {
}
function hideFromHoverDismiss() {
if (hoverDismissSuspended)
return;
hoverDismissEnabled = false;
slideAnimation.duration = Math.round(Theme.expressiveDurations.expressiveDefaultSpatial);
hide();
@@ -62,7 +66,8 @@ PanelWindow {
return false;
const padding = 24;
const topLeft = slideContainer.mapToItem(null, 0, 0);
return gx >= topLeft.x - padding && gx < topLeft.x + slideContainer.width + padding && gy >= topLeft.y - padding && gy < topLeft.y + slideContainer.height + padding;
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() {
@@ -86,25 +91,15 @@ PanelWindow {
color: "transparent"
MouseArea {
anchors.fill: parent
z: -1
acceptedButtons: Qt.NoButton
hoverEnabled: true
onPositionChanged: mouse => {
const gp = mapToItem(null, mouse.x, mouse.y);
PopoutManager.updateHoverCursor(gp.x, gp.y);
}
}
HoverDismissTracker {
id: hoverDismissTracker
anchors.fill: parent
enabled: root.hoverDismissEnabled && root.isVisible
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
+13 -20
View File
@@ -2,34 +2,27 @@ pragma ComponentBehavior: Bound
import QtQuick
Item {
HoverHandler {
id: root
property bool enabled: false
property var shouldDismiss: null
signal dismissRequested
// Emitted on every hover move; passive to avoid blocking overlapping MouseAreas
signal hoverMoved(real gx, real gy)
anchors.fill: parent
HoverHandler {
id: hoverHandler
enabled: root.enabled
onPointChanged: {
if (!root.enabled || !hoverHandler.hovered)
return;
const gp = root.mapToItem(null, hoverHandler.point.position.x, hoverHandler.point.position.y);
root.hoverMoved(gp.x, gp.y);
}
onHoveredChanged: {
if (hoverHandler.hovered || !root.enabled)
return;
if (typeof root.shouldDismiss === "function" && !root.shouldDismiss())
return;
root.dismissRequested();
}
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)
}
}