1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-28 22:12:10 -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
+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,