diff --git a/quickshell/Common/PopoutManager.qml b/quickshell/Common/PopoutManager.qml index f7dcc49a..90a49444 100644 --- a/quickshell/Common/PopoutManager.qml +++ b/quickshell/Common/PopoutManager.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import Quickshell import QtQuick +import qs.Common Singleton { id: root @@ -16,8 +17,76 @@ Singleton { signal popoutOpening signal popoutChanged + property real hoverCursorGlobalX: 0 + property real hoverCursorGlobalY: 0 + + function updateHoverCursor(gx, gy) { + hoverCursorGlobalX = gx; + hoverCursorGlobalY = gy; + } + + function cursorOverBar(gx, gy, padding) { + 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) + 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) { try { + if (popout?.hoverDismissEnabled) { + if (typeof popout.closeFromHoverDismiss === "function") { + popout.closeFromHoverDismiss(); + return; + } + } + if (popout.hoverDismissEnabled !== undefined) + popout.hoverDismissEnabled = false; switch (true) { case popout.dashVisible !== undefined: popout.dashVisible = false; @@ -89,7 +158,26 @@ Singleton { continue; _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) { @@ -106,6 +194,8 @@ Singleton { function requestPopout(popout, tabIndex, triggerSource) { if (!popout || !popout.screen) return; + if (popout.hoverDismissEnabled !== undefined) + popout.hoverDismissEnabled = false; screenshotActive = false; const screenName = popout.screen.name; const currentPopout = currentPopoutsByScreen[screenName]; @@ -181,16 +271,81 @@ Singleton { ModalManager.closeAllModalsExcept(null); } - if (movedFromOtherScreen) { - popout.open(); - } else { - if (popout.dashVisible !== undefined) { - popout.dashVisible = true; - } else if (popout.notificationHistoryVisible !== undefined) { - popout.notificationHistoryVisible = true; + _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); + 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 { - popout.open(); + _closePopout(currentPopout); } } + + if (currentPopout === popout && _isPopoutPresented(popout) && !movedFromOtherScreen) { + 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) + popout.hoverDismissEnabled = true; + return; + } + + currentPopoutTriggers[screenName] = triggerId; + currentPopoutsByScreen[screenName] = popout; + popoutChanged(); + + if (tabIndex !== undefined && popout.currentTabIndex !== undefined) + popout.currentTabIndex = tabIndex; + + if (currentPopout !== popout) + ModalManager.closeAllModalsExcept(null); + + if (popout.hoverDismissEnabled !== undefined) + popout.hoverDismissEnabled = true; + + _openPopout(popout); } } diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 59b3facf..64d02d0b 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -810,7 +810,8 @@ Singleton { "shadowOpacity": 60, "shadowColorMode": "default", "shadowCustomColor": "#000000", - "clickThrough": false + "clickThrough": false, + "hoverPopouts": false } ] diff --git a/quickshell/Modules/DankBar/DankBarContent.qml b/quickshell/Modules/DankBar/DankBarContent.qml index ef3c914d..e4b16426 100644 --- a/quickshell/Modules/DankBar/DankBarContent.qml +++ b/quickshell/Modules/DankBar/DankBarContent.qml @@ -380,6 +380,648 @@ Item { return "left"; } + property string activeHoverTrigger: "" + property real _lastHoverGlobalX: 0 + property real _lastHoverGlobalY: 0 + + readonly property bool hoverPopoutsEnabled: barConfig?.hoverPopouts ?? false + + 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") + topBarContent.checkHoverPopout(topBarContent._lastHoverGlobalX, topBarContent._lastHoverGlobalY); + }; + 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 (spec.prepare) + spec.prepare(popout); + + const request = mode === "hover" ? PopoutManager.requestHoverPopout : PopoutManager.requestPopout; + request(popout, spec.tabIndex, spec.triggerSource); + 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(); + } + + 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; + } + 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(); + + const hit = findWidgetAtGlobalPoint(gx, gy); + if (!hit) { + if (!cursorOverHoverChain(gx, gy)) + closeHoverSurfaces(); + 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) { + if (!cursorOverHoverChain(gx, gy)) + closeHoverSurfaces(); + return; + } + + if (triggerKey === activeHoverTrigger && hasOpenHoverSurface()) + return; + + if (triggerKey !== activeHoverTrigger && activeHoverTrigger !== "") + closeHoverSurfaces(); + + if (!openHoverPopoutForHit(hit)) { + if (activeHoverTrigger !== "") + closeHoverSurfaces(); + return; + } + + activeHoverTrigger = triggerKey; + } + readonly property var widgetVisibility: ({ "cpuUsage": DgopService.dgopAvailable, "memUsage": DgopService.dgopAvailable, @@ -696,27 +1338,18 @@ Item { parentScreen: barWindow.screen popoutTarget: clipboardHistoryPopoutLoader.item ?? null - function openClipboardPopout(initialTab) { - clipboardHistoryPopoutLoader.active = true; - if (!clipboardHistoryPopoutLoader.item) { - return; - } - const popout = clipboardHistoryPopoutLoader.item; - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - if (popout.setBarContext) { - popout.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); - } - 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"); + function openClipboardPopout(initialTab, mode) { + openWidgetPopout({ + loader: clipboardHistoryPopoutLoader, + widgetItem: clipboardWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "clipboard", + mode: mode || "click", + prepare: popout => { + if (initialTab) + popout.activeTab = initialTab; + } + }); } onClipboardClicked: openClipboardPopout("recents") @@ -815,9 +1448,14 @@ Item { } onClicked: { - if (!_preparePopout()) - return; - PopoutManager.requestPopout(appDrawerLoader.item, undefined, "appDrawer"); + topBarContent.openWidgetPopout({ + loader: appDrawerLoader, + widgetItem: launcherButton, + section: launcherButton.section, + triggerSource: "appDrawer", + mode: "click", + visualItem: launcherButton + }); } } } @@ -884,6 +1522,7 @@ Item { id: clockComponent Clock { + id: clockWidget axis: barWindow.axis compactMode: topBarContent.overlapping barThickness: barWindow.effectiveBarThickness @@ -903,43 +1542,17 @@ Item { } onClockClicked: { - dankDashPopoutLoader.active = true; - if (dankDashPopoutLoader.item) { - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - if (dankDashPopoutLoader.item.setBarContext) { - dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); - } - if (dankDashPopoutLoader.item.setTriggerPosition) { - let triggerPos, triggerWidth; - if (section === "center") { - 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"); - } + const section = topBarContent.getWidgetSection(parent) || "center"; + topBarContent.openWidgetPopout({ + loader: dankDashPopoutLoader, + widgetItem: clockWidget, + section, + tabIndex: 0, + triggerSource: topBarContent._dashTriggerSource(section, 0), + mode: "click", + useCenterSection: true, + setTriggerScreen: true + }); } } } @@ -948,6 +1561,7 @@ Item { id: mediaComponent Media { + id: mediaWidget axis: barWindow.axis compactMode: topBarContent.spacingTight || topBarContent.overlapping barThickness: barWindow.effectiveBarThickness @@ -956,43 +1570,17 @@ Item { popoutTarget: dankDashPopoutLoader.item ?? null parentScreen: barWindow.screen onClicked: { - dankDashPopoutLoader.active = true; - if (dankDashPopoutLoader.item) { - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - if (dankDashPopoutLoader.item.setBarContext) { - dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); - } - if (dankDashPopoutLoader.item.setTriggerPosition) { - let triggerPos, triggerWidth; - if (section === "center") { - 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"); - } + const section = topBarContent.getWidgetSection(parent) || "center"; + topBarContent.openWidgetPopout({ + loader: dankDashPopoutLoader, + widgetItem: mediaWidget, + section, + tabIndex: 1, + triggerSource: topBarContent._dashTriggerSource(section, 1), + mode: "click", + useCenterSection: true, + setTriggerScreen: true + }); } } } @@ -1001,6 +1589,7 @@ Item { id: weatherComponent Weather { + id: weatherWidget axis: barWindow.axis barThickness: barWindow.effectiveBarThickness widgetThickness: barWindow.widgetThickness @@ -1008,47 +1597,17 @@ Item { popoutTarget: dankDashPopoutLoader.item ?? null parentScreen: barWindow.screen onClicked: { - dankDashPopoutLoader.active = true; - if (dankDashPopoutLoader.item) { - const effectiveBarConfig = topBarContent.barConfig; - // Calculate barPosition from axis.edge - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - if (dankDashPopoutLoader.item.setBarContext) { - dankDashPopoutLoader.item.setBarContext(barPosition, effectiveBarConfig?.bottomGap ?? 0); - } - if (dankDashPopoutLoader.item.setTriggerPosition) { - // For center section widgets, use center section bounds for DankDash centering - 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"); - } + const section = topBarContent.getWidgetSection(parent) || "center"; + topBarContent.openWidgetPopout({ + loader: dankDashPopoutLoader, + widgetItem: weatherWidget, + section, + tabIndex: 3, + triggerSource: topBarContent._dashTriggerSource(section, 3), + mode: "click", + useCenterSection: true, + setTriggerScreen: true + }); } } } @@ -1094,22 +1653,13 @@ Item { parentScreen: barWindow.screen widgetData: parent.widgetData onCpuClicked: { - processListPopoutLoader.active = true; - if (!processListPopoutLoader.item) { - return; - } - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - 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"); + topBarContent.openWidgetPopout({ + loader: processListPopoutLoader, + widgetItem: cpuWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "cpu", + mode: "click" + }); } } } @@ -1127,22 +1677,13 @@ Item { parentScreen: barWindow.screen widgetData: parent.widgetData onRamClicked: { - processListPopoutLoader.active = true; - if (!processListPopoutLoader.item) { - return; - } - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - 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"); + topBarContent.openWidgetPopout({ + loader: processListPopoutLoader, + widgetItem: ramWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "memory", + mode: "click" + }); } } } @@ -1174,22 +1715,13 @@ Item { parentScreen: barWindow.screen widgetData: parent.widgetData onCpuTempClicked: { - processListPopoutLoader.active = true; - if (!processListPopoutLoader.item) { - return; - } - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - 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"); + topBarContent.openWidgetPopout({ + loader: processListPopoutLoader, + widgetItem: cpuTempWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "cpu_temp", + mode: "click" + }); } } } @@ -1207,22 +1739,13 @@ Item { parentScreen: barWindow.screen widgetData: parent.widgetData onGpuTempClicked: { - processListPopoutLoader.active = true; - if (!processListPopoutLoader.item) { - return; - } - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - 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"); + topBarContent.openWidgetPopout({ + loader: processListPopoutLoader, + widgetItem: gpuTempWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "gpu_temp", + mode: "click" + }); } } } @@ -1247,23 +1770,14 @@ Item { popoutTarget: notificationCenterLoader.item ?? null parentScreen: barWindow.screen onClicked: { - notificationCenterLoader.active = true; - if (!notificationCenterLoader.item) { - return; - } - notificationCenterLoader.item.triggerScreen = barWindow.screen; - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - 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"); + topBarContent.openWidgetPopout({ + loader: notificationCenterLoader, + widgetItem: notificationButton, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "notifications", + mode: "click", + setTriggerScreen: true + }); } } } @@ -1283,22 +1797,13 @@ Item { popoutTarget: batteryPopoutLoader.item ?? null parentScreen: barWindow.screen onToggleBatteryPopup: { - batteryPopoutLoader.active = true; - if (!batteryPopoutLoader.item) { - return; - } - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - 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"); + topBarContent.openWidgetPopout({ + loader: batteryPopoutLoader, + widgetItem: batteryWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "battery", + mode: "click" + }); } } } @@ -1316,20 +1821,13 @@ Item { popoutTarget: layoutPopoutLoader.item ?? null parentScreen: barWindow.screen onToggleLayoutPopup: { - layoutPopoutLoader.active = true; - if (!layoutPopoutLoader.item) - return; - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - - 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"); + topBarContent.openWidgetPopout({ + loader: layoutPopoutLoader, + widgetItem: layoutWidget, + section: topBarContent.getWidgetSection(parent) || "center", + triggerSource: "layout", + mode: "click" + }); } } } @@ -1349,24 +1847,13 @@ Item { popoutTarget: vpnPopoutLoader.item ?? null parentScreen: barWindow.screen onToggleVpnPopup: { - vpnPopoutLoader.active = true; - if (!vpnPopoutLoader.item) - return; - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - - 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"); + topBarContent.openWidgetPopout({ + loader: vpnPopoutLoader, + widgetItem: vpnWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "vpn", + mode: "click" + }); } } } @@ -1375,6 +1862,7 @@ Item { id: controlCenterButtonComponent ControlCenterButton { + id: controlCenterButton isActive: controlCenterLoader.item ? controlCenterLoader.item.shouldBeVisible : false widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness @@ -1397,25 +1885,16 @@ Item { } onClicked: { - controlCenterLoader.active = true; - if (!controlCenterLoader.item) { - return; - } - controlCenterLoader.item.triggerScreen = barWindow.screen; - if (controlCenterLoader.item.setTriggerPosition) { - const globalPos = mapToItem(null, 0, 0); - // Use topBarContent.barConfig directly - const effectiveBarConfig = topBarContent.barConfig; - // 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) { + topBarContent.openWidgetPopout({ + loader: controlCenterLoader, + widgetItem: controlCenterButton, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "controlCenter", + mode: "click", + setTriggerScreen: true + }); + if (controlCenterLoader.item?.shouldBeVisible && NetworkService.wifiEnabled) NetworkService.scanWifi(); - } } } } @@ -1525,6 +2004,7 @@ Item { id: systemUpdateComponent SystemUpdate { + id: systemUpdateWidget isActive: systemUpdateLoader.item ? systemUpdateLoader.item.shouldBeVisible : false widgetThickness: barWindow.widgetThickness barThickness: barWindow.effectiveBarThickness @@ -1543,22 +2023,14 @@ Item { } onClicked: { - systemUpdateLoader.active = true; - if (!systemUpdateLoader.item) - return; - const popout = systemUpdateLoader.item; - const effectiveBarConfig = topBarContent.barConfig; - const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1)); - if (popout.setBarContext) { - 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"); + topBarContent.openWidgetPopout({ + loader: systemUpdateLoader, + widgetItem: systemUpdateWidget, + section: topBarContent.getWidgetSection(parent) || "right", + triggerSource: "systemUpdate", + mode: "click", + visualItem: systemUpdateWidget + }); } } } diff --git a/quickshell/Modules/DankBar/DankBarWindow.qml b/quickshell/Modules/DankBar/DankBarWindow.qml index 02a23513..8ffa0644 100644 --- a/quickshell/Modules/DankBar/DankBarWindow.qml +++ b/quickshell/Modules/DankBar/DankBarWindow.qml @@ -709,6 +709,14 @@ PanelWindow { readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null 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) { if (!section) return { @@ -1010,7 +1018,7 @@ PanelWindow { } } - onWheel: wheel => { + function processWheel(wheel) { if (!(barConfig?.scrollEnabled ?? true) || actionInProgress) { wheel.accepted = false; return; @@ -1079,6 +1087,8 @@ PanelWindow { wheel.accepted = false; } + + onWheel: wheel => processWheel(wheel) } DankBarContent { @@ -1090,6 +1100,36 @@ PanelWindow { centerWidgetsModel: barWindow.centerWidgetsModel rightWidgetsModel: barWindow.rightWidgetsModel } + + MouseArea { + id: hoverPopoutArea + anchors.fill: parent + z: 1 + hoverEnabled: barConfig?.hoverPopouts ?? false + enabled: hoverPopoutArea.hoverEnabled && !barWindow.clickThroughEnabled + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + + property real lastGlobalX: 0 + property real lastGlobalY: 0 + + onPositionChanged: mouse => { + const gp = mapToItem(null, mouse.x, mouse.y); + lastGlobalX = gp.x; + lastGlobalY = gp.y; + topBarContent.checkHoverPopout(gp.x, gp.y); + } + + onWheel: wheel => scrollArea.processWheel(wheel) + + onContainsMouseChanged: { + if (containsMouse) + return; + if (topBarContent.cursorOverHoverChain(lastGlobalX, lastGlobalY)) + return; + topBarContent.closeHoverSurfaces(); + } + } } } } diff --git a/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml b/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml index 6fd746fb..a7d63cde 100644 --- a/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml +++ b/quickshell/Modules/DankBar/Widgets/SystemTrayBar.qml @@ -1922,4 +1922,53 @@ BasePill { return; 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; + } } diff --git a/quickshell/Modules/Plugins/PluginComponent.qml b/quickshell/Modules/Plugins/PluginComponent.qml index 39d9ff68..59667a2d 100644 --- a/quickshell/Modules/Plugins/PluginComponent.qml +++ b/quickshell/Modules/Plugins/PluginComponent.qml @@ -330,6 +330,24 @@ Item { 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 { id: pluginPopout contentWidth: root.popoutWidth diff --git a/quickshell/Modules/Settings/DankBarTab.qml b/quickshell/Modules/Settings/DankBarTab.qml index 633e2f7f..b027efce 100644 --- a/quickshell/Modules/Settings/DankBarTab.qml +++ b/quickshell/Modules/Settings/DankBarTab.qml @@ -164,6 +164,7 @@ Item { scrollEnabled: defaultBar.scrollEnabled ?? true, scrollXBehavior: defaultBar.scrollXBehavior ?? "column", scrollYBehavior: defaultBar.scrollYBehavior ?? "workspace", + hoverPopouts: defaultBar.hoverPopouts ?? false, shadowIntensity: defaultBar.shadowIntensity ?? 0, shadowOpacity: defaultBar.shadowOpacity ?? 60, shadowDirectionMode: defaultBar.shadowDirectionMode ?? "inherit", @@ -1741,6 +1742,19 @@ 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 + }) + } + SettingsToggleCard { iconName: "mouse" title: I18n.tr("Scroll Wheel") diff --git a/quickshell/Widgets/DankPopout.qml b/quickshell/Widgets/DankPopout.qml index 1fc6c347..9563b700 100644 --- a/quickshell/Widgets/DankPopout.qml +++ b/quickshell/Widgets/DankPopout.qml @@ -24,6 +24,7 @@ Item { property list animationExitCurve: Theme.variantPopoutExitCurve property bool suspendShadowWhileResizing: false property bool shouldBeVisible: false + property bool hoverDismissEnabled: false property var customKeyboardFocus: null property bool backgroundInteractive: true property bool contentHandlesKeys: false @@ -82,6 +83,8 @@ Item { readonly property real alignedY: impl.item ? impl.item.alignedY : 0 readonly property real alignedWidth: impl.item ? impl.item.alignedWidth : 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 maskY: impl.item ? impl.item.maskY : 0 readonly property real maskWidth: impl.item ? impl.item.maskWidth : 0 @@ -172,6 +175,32 @@ Item { impl.item.close(); } + function cancelHoverDismiss() { + if (impl.item?.cancelHoverDismiss) + impl.item.cancelHoverDismiss(); + } + + function closeFromHoverDismiss() { + hoverDismissEnabled = false; + if (impl.item) { + impl.item.animationsEnabled = true; + impl.item.animationDuration = Math.round(Theme.expressiveDurations.expressiveDefaultSpatial); + impl.item.animationExitCurve = Theme.expressiveCurves.expressiveDefaultSpatial; + } + if (dashVisible !== undefined) { + dashVisible = false; + return; + } + if (notificationHistoryVisible !== undefined) { + notificationHistoryVisible = false; + return; + } + if (impl.item) + impl.item.close(); + else + close(); + } + function toggle() { (shouldBeVisible || _pendingOpen) ? close() : open(); } @@ -210,6 +239,20 @@ Item { 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 { id: impl active: root.screen !== null @@ -261,6 +304,7 @@ Item { it.screen = Qt.binding(() => root.screen); it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition); it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap); + it.hoverDismissEnabled = Qt.binding(() => root.hoverDismissEnabled); it.shouldBeVisible = root.shouldBeVisible; if (root._primeContent && typeof it.primeContent === "function") diff --git a/quickshell/Widgets/DankPopoutConnected.qml b/quickshell/Widgets/DankPopoutConnected.qml index 3d88a5ce..77e34680 100644 --- a/quickshell/Widgets/DankPopoutConnected.qml +++ b/quickshell/Widgets/DankPopoutConnected.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Wayland import qs.Common import qs.Services +import qs.Widgets Item { id: root @@ -407,6 +408,20 @@ Item { onFrameOwnsConnectedChromeChanged: _syncPopoutChromeState() property bool animationsEnabled: true + property bool hoverDismissEnabled: false + + function cancelHoverDismiss() { + hoverDismissTracker.cancelPending(); + } + + function closeFromHoverDismiss() { + if (isClosing || !shouldBeVisible) + return; + if (popoutHandle?.closeFromHoverDismiss) + popoutHandle.closeFromHoverDismiss(); + else + close(); + } function open() { if (!screen) @@ -761,6 +776,27 @@ Item { visible: false 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.shouldBeVisible + shouldDismiss: function () { + return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY); + } + onDismissRequested: root.closeFromHoverDismiss() + } + WindowBlur { id: popoutBlur targetWindow: contentWindow diff --git a/quickshell/Widgets/DankPopoutStandalone.qml b/quickshell/Widgets/DankPopoutStandalone.qml index 783388ae..f21b4157 100644 --- a/quickshell/Widgets/DankPopoutStandalone.qml +++ b/quickshell/Widgets/DankPopoutStandalone.qml @@ -5,6 +5,7 @@ import Quickshell import Quickshell.Wayland import qs.Common import qs.Services +import qs.Widgets Item { id: root @@ -35,6 +36,21 @@ Item { property bool shouldBeVisible: false property bool isClosing: false property bool animationsEnabled: true + property bool hoverDismissEnabled: false + + function cancelHoverDismiss() { + hoverDismissTracker.cancelPending(); + } + + function closeFromHoverDismiss() { + if (isClosing || !shouldBeVisible) + return; + if (popoutHandle?.closeFromHoverDismiss) + popoutHandle.closeFromHoverDismiss(); + else + close(); + } + property var customKeyboardFocus: null property bool backgroundInteractive: true property bool contentHandlesKeys: false @@ -585,6 +601,27 @@ Item { color: "transparent" readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing + 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.shouldBeVisible + shouldDismiss: function () { + return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY); + } + onDismissRequested: root.closeFromHoverDismiss() + } + WindowBlur { id: popoutBlur targetWindow: contentWindow diff --git a/quickshell/Widgets/DankSlideout.qml b/quickshell/Widgets/DankSlideout.qml index 6d940f52..eca9367c 100644 --- a/quickshell/Widgets/DankSlideout.qml +++ b/quickshell/Widgets/DankSlideout.qml @@ -13,6 +13,7 @@ PanelWindow { WlrLayershell.namespace: layerNamespace property bool isVisible: false + property bool hoverDismissEnabled: false property var targetScreen: null property var modelData: null property bool triggerUsesOverlayLayer: false @@ -39,6 +40,24 @@ PanelWindow { isVisible = false; } + function hideFromHoverDismiss() { + 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); + return gx >= topLeft.x - padding && gx < topLeft.x + slideContainer.width + padding && gy >= topLeft.y - padding && gy < topLeft.y + slideContainer.height + padding; + } + function toggle() { if (isVisible) { hide(); @@ -60,6 +79,27 @@ 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 + shouldDismiss: function () { + return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY); + } + onDismissRequested: root.hideFromHoverDismiss() + } + readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled WlrLayershell.layer: (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData)) ? WlrLayershell.Overlay : WlrLayershell.Top @@ -104,8 +144,10 @@ PanelWindow { easing.type: Easing.OutCubic onRunningChanged: { - if (!running && !root.isVisible) { - root.mappedVisible = false; + if (!running) { + if (!root.isVisible) + root.mappedVisible = false; + slideAnimation.duration = 450; } } } diff --git a/quickshell/Widgets/HoverDismissTracker.qml b/quickshell/Widgets/HoverDismissTracker.qml new file mode 100644 index 00000000..1f2cc9d9 --- /dev/null +++ b/quickshell/Widgets/HoverDismissTracker.qml @@ -0,0 +1,28 @@ +pragma ComponentBehavior: Bound + +import QtQuick + +Item { + id: root + + property bool enabled: false + property var shouldDismiss: null + + signal dismissRequested + + anchors.fill: parent + + HoverHandler { + id: hoverHandler + enabled: root.enabled + onHoveredChanged: { + if (hoverHandler.hovered || !root.enabled) + return; + if (typeof root.shouldDismiss === "function" && !root.shouldDismiss()) + return; + root.dismissRequested(); + } + } + + function cancelPending() {} +}