mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 15:24:00 -04:00
946 lines
31 KiB
QML
946 lines
31 KiB
QML
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
|
|
property var widgetOwner: null
|
|
property var sections: []
|
|
|
|
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 var _pendingHoverHit: null
|
|
property string _pendingHoverTrigger: ""
|
|
property string _hoverReopenSuppressedTrigger: ""
|
|
|
|
property bool _candidateCacheValid: false
|
|
property var _candidateCache: []
|
|
property var _candidateWatchers: []
|
|
property bool _lastLookupWasMiss: false
|
|
|
|
width: 0
|
|
height: 0
|
|
|
|
onLeftWidgetsModelChanged: invalidateCandidateCache()
|
|
onCenterWidgetsModelChanged: invalidateCandidateCache()
|
|
onRightWidgetsModelChanged: invalidateCandidateCache()
|
|
onSectionsChanged: invalidateCandidateCache()
|
|
|
|
onHoverPopoutsEnabledChanged: {
|
|
invalidateCandidateCache();
|
|
if (hoverPopoutsEnabled)
|
|
return;
|
|
cancelQueuedHitTest();
|
|
_cancelPendingHover();
|
|
_hoverCloseTimer.stop();
|
|
if (hasOpenHoverSurface() && !isActiveHoverSurfacePinned())
|
|
closeHoverSurfaces();
|
|
activeHoverTrigger = "";
|
|
_hoverReopenSuppressedTrigger = "";
|
|
}
|
|
|
|
Component.onDestruction: _disconnectCandidateWatchers()
|
|
|
|
Connections {
|
|
target: root.widgetOwner || root.barContent
|
|
enabled: root.hoverPopoutsEnabled
|
|
|
|
function onWidthChanged() {
|
|
root.invalidateCandidateCache();
|
|
}
|
|
|
|
function onHeightChanged() {
|
|
root.invalidateCandidateCache();
|
|
}
|
|
}
|
|
|
|
readonly property var barWindowScreen: root.barWindow?.screen ?? null
|
|
|
|
onBarWindowScreenChanged: invalidateCandidateCache()
|
|
|
|
Connections {
|
|
target: PopoutManager
|
|
enabled: root.hoverPopoutsEnabled
|
|
|
|
function onPopoutChanged() {
|
|
root._reconcileClosedHoverSurface();
|
|
}
|
|
|
|
function onPopoutToggledClosed(screenName) {
|
|
root._suppressReopenAfterToggleClose(screenName);
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: BarWidgetService
|
|
enabled: root.hoverPopoutsEnabled
|
|
|
|
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;
|
|
PopoutManager.updateHoverCursor(gx, gy);
|
|
if (hoverPopoutsEnabled)
|
|
_hitTestPending = true;
|
|
}
|
|
|
|
function updateBarHovered(hovered) {
|
|
_barHovered = hovered;
|
|
if (hovered) {
|
|
_hoverCloseTimer.stop();
|
|
return;
|
|
}
|
|
|
|
cancelQueuedHitTest();
|
|
_cancelPendingHover();
|
|
_hoverReopenSuppressedTrigger = "";
|
|
if (!hoverPopoutsEnabled || isActiveHoverSurfacePinned())
|
|
return;
|
|
_hoverCloseTimer.restart();
|
|
}
|
|
|
|
function cancelQueuedHitTest() {
|
|
_hitTestPending = false;
|
|
}
|
|
|
|
function recheckLatestPoint() {
|
|
checkHoverPopout(_lastHoverGlobalX, _lastHoverGlobalY);
|
|
}
|
|
|
|
function resetForBarGeometryChange() {
|
|
invalidateCandidateCache();
|
|
cancelQueuedHitTest();
|
|
_cancelPendingHover();
|
|
_hoverCloseTimer.stop();
|
|
_hoverReopenSuppressedTrigger = "";
|
|
barContent.cancelQueuedWidgetPopout();
|
|
|
|
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() {
|
|
return root.sections || [];
|
|
}
|
|
|
|
function _itemBelongsToThisBar(item) {
|
|
const owner = root.widgetOwner || 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.inOverflow)
|
|
return null;
|
|
if (wrapper.host)
|
|
return wrapper.host;
|
|
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;
|
|
}
|
|
|
|
readonly property var _hoverSpecs: ({
|
|
launcherButton: {
|
|
loader: "appDrawerLoader",
|
|
triggerSource: "appDrawer",
|
|
useVisualItem: true
|
|
},
|
|
clipboard: {
|
|
loader: "clipboardHistoryPopoutLoader",
|
|
triggerSource: "clipboard",
|
|
prepare: popout => popout.activeTab = "recents"
|
|
},
|
|
clock: {
|
|
loader: "dankDashPopoutLoader",
|
|
dashTab: "overview"
|
|
},
|
|
music: {
|
|
loader: "dankDashPopoutLoader",
|
|
dashTab: "media"
|
|
},
|
|
weather: {
|
|
loader: "dankDashPopoutLoader",
|
|
dashTab: "weather"
|
|
},
|
|
cpuUsage: {
|
|
loader: "processListPopoutLoader",
|
|
triggerSource: "cpu"
|
|
},
|
|
memUsage: {
|
|
loader: "processListPopoutLoader",
|
|
triggerSource: "memory"
|
|
},
|
|
cpuTemp: {
|
|
loader: "processListPopoutLoader",
|
|
triggerSource: "cpu_temp"
|
|
},
|
|
gpuTemp: {
|
|
loader: "processListPopoutLoader",
|
|
triggerSource: "gpu_temp"
|
|
},
|
|
notificationButton: {
|
|
loader: "notificationCenterLoader",
|
|
triggerSource: "notifications",
|
|
setTriggerScreen: true
|
|
},
|
|
battery: {
|
|
loader: "batteryPopoutLoader",
|
|
triggerSource: "battery"
|
|
},
|
|
layout: {
|
|
loader: "layoutPopoutLoader",
|
|
triggerSource: "layout"
|
|
},
|
|
vpn: {
|
|
loader: "vpnPopoutLoader",
|
|
triggerSource: "vpn"
|
|
},
|
|
powerMenuButton: {
|
|
loader: "powerMenuPopoutLoader",
|
|
triggerSource: "powerMenu"
|
|
},
|
|
colorPicker: {
|
|
loader: "colorPickerPopoutLoader",
|
|
triggerSource: "colorPicker"
|
|
},
|
|
controlCenterButton: {
|
|
loader: "controlCenterLoader",
|
|
triggerSource: "controlCenter",
|
|
setTriggerScreen: true,
|
|
afterOpen: loader => {
|
|
if (loader.item?.shouldBeVisible && NetworkService.wifiEnabled)
|
|
NetworkService.scanWifi();
|
|
}
|
|
},
|
|
systemUpdate: {
|
|
loader: "systemUpdateLoader",
|
|
triggerSource: "systemUpdate",
|
|
useVisualItem: true
|
|
}
|
|
})
|
|
|
|
function _widgetSupportsHoverPopout(widgetId, widgetItem) {
|
|
if (!widgetId || !widgetItem)
|
|
return false;
|
|
if (typeof widgetItem.triggerHoverPopout === "function")
|
|
return true;
|
|
if (widgetId === "systemTray")
|
|
return typeof widgetItem.openHoverAtGlobalPoint === "function";
|
|
return widgetId === "notepadButton" || _hoverSpecs[widgetId] !== undefined;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
Connections {
|
|
target: root.barWindow?.hostWindow ?? null
|
|
enabled: root.hoverPopoutsEnabled
|
|
|
|
function onMarginsChanged() {
|
|
root.invalidateCandidateCache();
|
|
}
|
|
}
|
|
|
|
function _globalItemBounds(item) {
|
|
try {
|
|
const topLeft = barContent.surfaceContext.screenPoint(item, 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, tabId) {
|
|
return (barConfig?.id ?? "default") + "-" + section + "-" + tabId;
|
|
}
|
|
|
|
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.dismissHoverPopoutForScreen(barWindow?.screen);
|
|
TrayMenuManager.closeHoverMenus();
|
|
}
|
|
|
|
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 _reconcileClosedHoverSurface() {
|
|
const hadStaleTrigger = activeHoverTrigger !== "" && !hasOpenHoverSurface();
|
|
if (hadStaleTrigger)
|
|
activeHoverTrigger = "";
|
|
// A cursor resting on the trigger emits no further points; retest so the popout
|
|
// reopens without needing the cursor to leave and come back.
|
|
if (hadStaleTrigger && _barHovered && hoverPopoutsEnabled)
|
|
recheckLatestPoint();
|
|
}
|
|
|
|
function _suppressReopenAfterToggleClose(screenName) {
|
|
if (screenName !== barWindow?.screen?.name)
|
|
return;
|
|
activeHoverTrigger = "";
|
|
if (!hoverPopoutsEnabled)
|
|
return;
|
|
_cancelPendingHover();
|
|
const gx = PopoutManager.hoverCursorGlobalX;
|
|
const gy = PopoutManager.hoverCursorGlobalY;
|
|
const hit = findWidgetAtGlobalPoint(gx, gy);
|
|
_hoverReopenSuppressedTrigger = hit ? _triggerKeyForHit(hit, gx, gy) : "";
|
|
}
|
|
|
|
function _syncHoverTriggerState() {
|
|
if (activeHoverTrigger === "notepadButton") {
|
|
const instance = _notepadWidgetForScreen()?.notepadInstance;
|
|
if (!instance?.isVisible) {
|
|
_hoverReopenSuppressedTrigger = activeHoverTrigger;
|
|
activeHoverTrigger = "";
|
|
}
|
|
return;
|
|
}
|
|
if (activeHoverTrigger !== "" && !hasOpenHoverSurface()) {
|
|
// Tray menus close outside PopoutManager; popout closures reconcile via
|
|
// onPopoutChanged and must not leave a suppression that eats the next hover.
|
|
_hoverReopenSuppressedTrigger = activeHoverTrigger.startsWith("tray-") ? activeHoverTrigger : "";
|
|
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) {
|
|
const loaderName = _hoverSpecs[widgetId]?.loader;
|
|
return loaderName ? PopoutService[loaderName] : null;
|
|
}
|
|
|
|
function openHoverPopoutForHit(hit) {
|
|
if (!hit?.widgetItem)
|
|
return false;
|
|
|
|
const widgetItem = hit.widgetItem;
|
|
if (hit.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;
|
|
}
|
|
if (hit.widgetId === "notepadButton")
|
|
return openNotepadHover(widgetItem);
|
|
|
|
const spec = _hoverSpecs[hit.widgetId];
|
|
if (!spec)
|
|
return false;
|
|
|
|
const loader = PopoutService[spec.loader];
|
|
const request = {
|
|
widgetItem,
|
|
section: hit.section,
|
|
mode: "hover",
|
|
loader,
|
|
triggerSource: spec.dashTab ? dashTriggerSource(hit.section, spec.dashTab) : spec.triggerSource
|
|
};
|
|
if (spec.dashTab) {
|
|
request.prepare = popout => popout.requestTab(spec.dashTab);
|
|
request.useCenterSection = true;
|
|
request.setTriggerScreen = true;
|
|
}
|
|
if (spec.prepare)
|
|
request.prepare = spec.prepare;
|
|
if (spec.setTriggerScreen)
|
|
request.setTriggerScreen = true;
|
|
if (spec.useVisualItem)
|
|
request.visualItem = widgetItem;
|
|
|
|
if (!barContent.openWidgetPopout(request))
|
|
return false;
|
|
if (spec.afterOpen)
|
|
spec.afterOpen(loader);
|
|
return true;
|
|
}
|
|
|
|
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) {
|
|
_hoverReopenSuppressedTrigger = "";
|
|
_cancelPendingHover();
|
|
scheduleHoverClose(gx, gy);
|
|
return;
|
|
}
|
|
|
|
hit.globalX = gx;
|
|
hit.globalY = gy;
|
|
|
|
const triggerKey = _triggerKeyForHit(hit, gx, gy);
|
|
|
|
if (!triggerKey) {
|
|
_cancelPendingHover();
|
|
scheduleHoverClose(gx, gy);
|
|
return;
|
|
}
|
|
|
|
if (_hoverReopenSuppressedTrigger === triggerKey) {
|
|
_cancelPendingHover();
|
|
_hoverCloseTimer.stop();
|
|
return;
|
|
}
|
|
if (_hoverReopenSuppressedTrigger !== "")
|
|
_hoverReopenSuppressedTrigger = "";
|
|
|
|
_hoverCloseTimer.stop();
|
|
|
|
if (triggerKey === activeHoverTrigger && hasOpenHoverSurface() && PopoutManager.getActivePopout(barWindow?.screen)?.isClosing !== true) {
|
|
_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();
|
|
if (!hoverPopoutsEnabled)
|
|
return;
|
|
if (!hasOpenHoverSurface())
|
|
return;
|
|
if (isActiveHoverSurfacePinned())
|
|
return;
|
|
if (cursorOverHoverChain(gx, gy, barWindow?.hostWindow ?? null))
|
|
return;
|
|
_hoverCloseTimer.restart();
|
|
}
|
|
|
|
function _triggerKeyForHit(hit, gx, gy) {
|
|
if (hit.widgetId === "systemTray")
|
|
return hit.widgetItem.hoverTriggerAtGlobalPoint?.(gx, gy) || "";
|
|
const dashTab = _hoverSpecs[hit.widgetId]?.dashTab;
|
|
return dashTab ? dashTriggerSource(hit.section, dashTab) : hit.widgetId;
|
|
}
|
|
|
|
function _cursorRestsOnActiveTrigger(gx, gy) {
|
|
if (activeHoverTrigger === "")
|
|
return false;
|
|
const hit = findWidgetAtGlobalPoint(gx, gy);
|
|
if (!hit)
|
|
return false;
|
|
return _triggerKeyForHit(hit, gx, gy) === activeHoverTrigger;
|
|
}
|
|
|
|
function _commitHoverClose() {
|
|
const gx = PopoutManager.hoverCursorGlobalX;
|
|
const gy = PopoutManager.hoverCursorGlobalY;
|
|
if (isActiveHoverSurfacePinned())
|
|
return;
|
|
// A popout surface mapping can pull pointer focus off the bar on Hyprland; a leave
|
|
// with the cursor still resting on the trigger widget is not a real exit (#3248).
|
|
if (_cursorRestsOnActiveTrigger(gx, gy))
|
|
return;
|
|
if (cursorOverHoverChain(gx, gy, barWindow?.hostWindow ?? null))
|
|
return;
|
|
closeHoverSurfaces();
|
|
}
|
|
}
|