mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-28 14:05:21 -04:00
feat(popouts): complete initial hover implementation
This commit is contained in:
@@ -186,6 +186,14 @@ Singleton {
|
|||||||
return currentPopoutsByScreen[screen.name] || null;
|
return currentPopoutsByScreen[screen.name] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if the active popout is pinned for auto-dismissal
|
||||||
|
function isActivePopoutPinned(screen) {
|
||||||
|
const p = getActivePopout(screen);
|
||||||
|
if (!p || !_isPopoutPresented(p))
|
||||||
|
return false;
|
||||||
|
return p.hoverDismissEnabled === false;
|
||||||
|
}
|
||||||
|
|
||||||
function isCurrentPopout(popout, screenName) {
|
function isCurrentPopout(popout, screenName) {
|
||||||
const name = screenName || popout?.screen?.name || "";
|
const name = screenName || popout?.screen?.name || "";
|
||||||
return !!name && currentPopoutsByScreen[name] === popout;
|
return !!name && currentPopoutsByScreen[name] === popout;
|
||||||
@@ -194,6 +202,8 @@ Singleton {
|
|||||||
function requestPopout(popout, tabIndex, triggerSource) {
|
function requestPopout(popout, tabIndex, triggerSource) {
|
||||||
if (!popout || !popout.screen)
|
if (!popout || !popout.screen)
|
||||||
return;
|
return;
|
||||||
|
// Clicking a hover popout pins it open rather than toggling it closed
|
||||||
|
const wasTransient = popout.hoverDismissEnabled === true;
|
||||||
if (popout.hoverDismissEnabled !== undefined)
|
if (popout.hoverDismissEnabled !== undefined)
|
||||||
popout.hoverDismissEnabled = false;
|
popout.hoverDismissEnabled = false;
|
||||||
screenshotActive = false;
|
screenshotActive = false;
|
||||||
@@ -240,13 +250,17 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
if (currentPopout === popout && popout.shouldBeVisible && !movedFromOtherScreen) {
|
||||||
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
|
const sameTrigger = triggerId === undefined || currentPopoutTriggers[screenName] === triggerId;
|
||||||
_closePopout(popout);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (triggerId === undefined) {
|
if (sameTrigger) {
|
||||||
_closePopout(popout);
|
if (!wasTransient) {
|
||||||
|
_closePopout(popout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (popout.updateSurfacePosition)
|
||||||
|
popout.updateSurfacePosition();
|
||||||
|
if (triggerId !== undefined)
|
||||||
|
currentPopoutTriggers[screenName] = triggerId;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,6 +329,9 @@ Singleton {
|
|||||||
currentPopoutsByScreen[screenName] = null;
|
currentPopoutsByScreen[screenName] = null;
|
||||||
currentPopoutTriggers[screenName] = null;
|
currentPopoutTriggers[screenName] = null;
|
||||||
} else {
|
} else {
|
||||||
|
// Signal the active popout to fade in-place when morphed
|
||||||
|
if (typeof currentPopout.beginSupersededClose === "function")
|
||||||
|
currentPopout.beginSupersededClose();
|
||||||
_closePopout(currentPopout);
|
_closePopout(currentPopout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,8 @@ Singleton {
|
|||||||
onFrameLauncherEmergeSideChanged: saveSettings()
|
onFrameLauncherEmergeSideChanged: saveSettings()
|
||||||
property bool frameLauncherArcExtender: false
|
property bool frameLauncherArcExtender: false
|
||||||
onFrameLauncherArcExtenderChanged: saveSettings()
|
onFrameLauncherArcExtenderChanged: saveSettings()
|
||||||
|
property bool frameLauncherEdgeHover: false
|
||||||
|
onFrameLauncherEdgeHoverChanged: saveSettings()
|
||||||
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
||||||
property string frameMode: "connected"
|
property string frameMode: "connected"
|
||||||
onFrameModeChanged: saveSettings()
|
onFrameModeChanged: saveSettings()
|
||||||
@@ -603,9 +605,9 @@ Singleton {
|
|||||||
if (!on && id !== "settings" && current.filter(t => t.enabled && t.id !== "settings").length <= 1)
|
if (!on && id !== "settings" && current.filter(t => t.enabled && t.id !== "settings").length <= 1)
|
||||||
return;
|
return;
|
||||||
dashTabs = current.map(t => t.id === id ? {
|
dashTabs = current.map(t => t.id === id ? {
|
||||||
"id": t.id,
|
"id": t.id,
|
||||||
"enabled": on
|
"enabled": on
|
||||||
} : t);
|
} : t);
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetDashTabs() {
|
function resetDashTabs() {
|
||||||
@@ -1000,7 +1002,8 @@ Singleton {
|
|||||||
"shadowColorMode": "default",
|
"shadowColorMode": "default",
|
||||||
"shadowCustomColor": "#000000",
|
"shadowCustomColor": "#000000",
|
||||||
"clickThrough": false,
|
"clickThrough": false,
|
||||||
"hoverPopouts": false
|
"hoverPopouts": false,
|
||||||
|
"hoverPopoutDelay": 150
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2437,6 +2440,46 @@ Singleton {
|
|||||||
return barConfigs.filter(cfg => cfg.enabled);
|
return barConfigs.filter(cfg => cfg.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _sideToPosition(side) {
|
||||||
|
switch (side) {
|
||||||
|
case "top":
|
||||||
|
return SettingsData.Position.Top;
|
||||||
|
case "bottom":
|
||||||
|
return SettingsData.Position.Bottom;
|
||||||
|
case "left":
|
||||||
|
return SettingsData.Position.Left;
|
||||||
|
case "right":
|
||||||
|
return SettingsData.Position.Right;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a bar occupies the specified screen edge
|
||||||
|
function barOccupiesSide(screen, side) {
|
||||||
|
if (!screen)
|
||||||
|
return false;
|
||||||
|
const sidePos = _sideToPosition(side);
|
||||||
|
if (sidePos < 0)
|
||||||
|
return false;
|
||||||
|
const bars = getEnabledBarConfigs();
|
||||||
|
for (var i = 0; i < bars.length; i++) {
|
||||||
|
const bc = bars[i];
|
||||||
|
if (bc.position !== sidePos)
|
||||||
|
continue;
|
||||||
|
const prefs = bc.screenPreferences || ["all"];
|
||||||
|
if (prefs.includes("all") || isScreenInPreferences(screen, prefs))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the dock occupies the specified screen edge.
|
||||||
|
function dockOccupiesSide(side) {
|
||||||
|
if (!showDock)
|
||||||
|
return false;
|
||||||
|
return dockPosition === _sideToPosition(side);
|
||||||
|
}
|
||||||
|
|
||||||
function getScreensSortedByPosition() {
|
function getScreensSortedByPosition() {
|
||||||
const screens = [];
|
const screens = [];
|
||||||
for (var i = 0; i < Quickshell.screens.length; i++) {
|
for (var i = 0; i < Quickshell.screens.length; i++) {
|
||||||
|
|||||||
@@ -642,6 +642,7 @@ var SPEC = {
|
|||||||
frameCloseGaps: { def: true },
|
frameCloseGaps: { def: true },
|
||||||
frameLauncherEmergeSide: { def: "bottom" },
|
frameLauncherEmergeSide: { def: "bottom" },
|
||||||
frameLauncherArcExtender: { def: false },
|
frameLauncherArcExtender: { def: false },
|
||||||
|
frameLauncherEdgeHover: { def: false },
|
||||||
frameMode: { def: "connected" },
|
frameMode: { def: "connected" },
|
||||||
barInsetPaddingShared: { def: -1 },
|
barInsetPaddingShared: { def: -1 },
|
||||||
barInsetPaddingSyncAll: { def: false },
|
barInsetPaddingSyncAll: { def: false },
|
||||||
|
|||||||
@@ -233,6 +233,12 @@ Item {
|
|||||||
sourceComponent: Frame {}
|
sourceComponent: Frame {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
active: SettingsData.frameEnabled && SettingsData.frameLauncherEdgeHover
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: FrameLauncherHoverZone {}
|
||||||
|
}
|
||||||
|
|
||||||
DeferredAction {
|
DeferredAction {
|
||||||
id: frameSurfaceReloadAction
|
id: frameSurfaceReloadAction
|
||||||
onTriggered: root.frameSurfacesLoaded = true
|
onTriggered: root.frameSurfacesLoaded = true
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ Item {
|
|||||||
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
|
readonly property string resolvedConnectedBarSide: impl.item ? (impl.item.resolvedConnectedBarSide ?? "") : ""
|
||||||
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
|
readonly property bool launcherArcExtenderActive: impl.item ? (impl.item.launcherArcExtenderActive ?? false) : false
|
||||||
property bool triggerUsesOverlayLayer: false
|
property bool triggerUsesOverlayLayer: false
|
||||||
|
property bool edgeHoverManaged: false
|
||||||
|
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,8 @@ Item {
|
|||||||
closeCleanupTimer.stop();
|
closeCleanupTimer.stop();
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
|
_edgeArmed = false;
|
||||||
|
_edgeBodyHover = false;
|
||||||
|
|
||||||
animationsEnabled = false;
|
animationsEnabled = false;
|
||||||
|
|
||||||
@@ -447,6 +449,9 @@ Item {
|
|||||||
|
|
||||||
keyboardActive = false;
|
keyboardActive = false;
|
||||||
spotlightOpen = false;
|
spotlightOpen = false;
|
||||||
|
_edgeRetractGrace.stop();
|
||||||
|
_edgeArmed = false;
|
||||||
|
_edgeBodyHover = false;
|
||||||
ModalManager.closeModal(modalHandle);
|
ModalManager.closeModal(modalHandle);
|
||||||
closeCleanupTimer.start();
|
closeCleanupTimer.start();
|
||||||
}
|
}
|
||||||
@@ -489,6 +494,31 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles hover dismissal grace periods for edge-hover sessions w/cursor
|
||||||
|
readonly property bool _edgeRetractEnabled: (modalHandle && modalHandle.edgeHoverManaged === true) && spotlightOpen && !isClosing
|
||||||
|
property bool _edgeBodyHover: false
|
||||||
|
property bool _edgeArmed: false
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _edgeRetractGrace
|
||||||
|
interval: 150
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (root._edgeRetractEnabled && root._edgeArmed && !root._edgeBodyHover)
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onEdgeBodyHoverChanged(over) {
|
||||||
|
root._edgeBodyHover = over;
|
||||||
|
if (over) {
|
||||||
|
root._edgeArmed = true;
|
||||||
|
_edgeRetractGrace.stop();
|
||||||
|
} else if (root._edgeRetractEnabled) {
|
||||||
|
_edgeRetractGrace.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: spotlightContent?.controller ?? null
|
target: spotlightContent?.controller ?? null
|
||||||
function onModeChanged(mode, userInitiated) {
|
function onModeChanged(mode, userInitiated) {
|
||||||
@@ -628,6 +658,13 @@ Item {
|
|||||||
width: root.alignedWidth
|
width: root.alignedWidth
|
||||||
height: root.contentSurfaceHeight
|
height: root.contentSurfaceHeight
|
||||||
|
|
||||||
|
// Passive tracker for edge-hover dismissal that preserves input events.
|
||||||
|
HoverHandler {
|
||||||
|
id: edgeBodyHoverHandler
|
||||||
|
enabled: root._edgeRetractEnabled
|
||||||
|
onHoveredChanged: root._onEdgeBodyHoverChanged(hovered)
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: root.spotlightOpen
|
enabled: root.spotlightOpen
|
||||||
|
|||||||
@@ -385,6 +385,36 @@ Item {
|
|||||||
property real _lastHoverGlobalY: 0
|
property real _lastHoverGlobalY: 0
|
||||||
|
|
||||||
readonly property bool hoverPopoutsEnabled: barConfig?.hoverPopouts ?? false
|
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 = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
property var _pendingHoverHit: null
|
||||||
|
property string _pendingHoverTrigger: ""
|
||||||
|
|
||||||
|
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 getBarPosition() {
|
function getBarPosition() {
|
||||||
return barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
return barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||||
@@ -435,18 +465,7 @@ Item {
|
|||||||
if (loader.item)
|
if (loader.item)
|
||||||
return loader.item;
|
return loader.item;
|
||||||
|
|
||||||
const pairs = [
|
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]];
|
||||||
[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++) {
|
for (let i = 0; i < pairs.length; i++) {
|
||||||
if (loader === pairs[i][0] && pairs[i][1])
|
if (loader === pairs[i][0] && pairs[i][1])
|
||||||
return pairs[i][1];
|
return pairs[i][1];
|
||||||
@@ -803,6 +822,13 @@ Item {
|
|||||||
TrayMenuManager.closeAllMenus();
|
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) {
|
function openNotepadHover(widgetItem) {
|
||||||
const instance = widgetItem.prepareNotepadInstance?.(widgetItem.notepadInstance) ?? widgetItem.notepadInstance;
|
const instance = widgetItem.prepareNotepadInstance?.(widgetItem.notepadInstance) ?? widgetItem.notepadInstance;
|
||||||
if (!instance || typeof instance.show !== "function")
|
if (!instance || typeof instance.show !== "function")
|
||||||
@@ -981,10 +1007,14 @@ Item {
|
|||||||
PopoutManager.updateHoverCursor(gx, gy);
|
PopoutManager.updateHoverCursor(gx, gy);
|
||||||
_syncHoverTriggerState();
|
_syncHoverTriggerState();
|
||||||
|
|
||||||
|
// Ignore hover events when a popout is pinned open.
|
||||||
|
if (PopoutManager.isActivePopoutPinned(barWindow?.screen))
|
||||||
|
return;
|
||||||
|
|
||||||
const hit = findWidgetAtGlobalPoint(gx, gy);
|
const hit = findWidgetAtGlobalPoint(gx, gy);
|
||||||
if (!hit) {
|
if (!hit) {
|
||||||
if (!cursorOverHoverChain(gx, gy))
|
_cancelPendingHover();
|
||||||
closeHoverSurfaces();
|
scheduleHoverClose(gx, gy);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1002,16 +1032,97 @@ Item {
|
|||||||
triggerKey = _dashTriggerSource(hit.section, 3);
|
triggerKey = _dashTriggerSource(hit.section, 3);
|
||||||
|
|
||||||
if (!triggerKey) {
|
if (!triggerKey) {
|
||||||
if (!cursorOverHoverChain(gx, gy))
|
_cancelPendingHover();
|
||||||
closeHoverSurfaces();
|
scheduleHoverClose(gx, gy);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggerKey === activeHoverTrigger && hasOpenHoverSurface())
|
_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;
|
return;
|
||||||
|
|
||||||
if (triggerKey !== activeHoverTrigger && activeHoverTrigger !== "")
|
// A different trigger backed by the same already-open popout swaps tab/position
|
||||||
|
// in place (requestHoverPopout handles it) — don't close+reopen the same surface.
|
||||||
|
if (triggerKey !== activeHoverTrigger && activeHoverTrigger !== "" && !_hitTargetsActivePopout(hit)) {
|
||||||
|
// Mark popout as superseded to fade in-place before closing.
|
||||||
|
_beginSupersededCloseForActive();
|
||||||
closeHoverSurfaces();
|
closeHoverSurfaces();
|
||||||
|
}
|
||||||
|
|
||||||
if (!openHoverPopoutForHit(hit)) {
|
if (!openHoverPopoutForHit(hit)) {
|
||||||
if (activeHoverTrigger !== "")
|
if (activeHoverTrigger !== "")
|
||||||
@@ -1022,6 +1133,27 @@ Item {
|
|||||||
activeHoverTrigger = triggerKey;
|
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: ({
|
readonly property var widgetVisibility: ({
|
||||||
"cpuUsage": DgopService.dgopAvailable,
|
"cpuUsage": DgopService.dgopAvailable,
|
||||||
"memUsage": DgopService.dgopAvailable,
|
"memUsage": DgopService.dgopAvailable,
|
||||||
|
|||||||
@@ -1111,33 +1111,25 @@ PanelWindow {
|
|||||||
rightWidgetsModel: barWindow.rightWidgetsModel
|
rightWidgetsModel: barWindow.rightWidgetsModel
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
// Passive HoverHandler to track cursor without intercepting clicks or scroll events.
|
||||||
id: hoverPopoutArea
|
HoverHandler {
|
||||||
anchors.fill: parent
|
id: hoverPopoutHandler
|
||||||
z: 1
|
enabled: (barConfig?.hoverPopouts ?? false) && !barWindow.clickThroughEnabled
|
||||||
hoverEnabled: barConfig?.hoverPopouts ?? false
|
|
||||||
enabled: hoverPopoutArea.hoverEnabled && !barWindow.clickThroughEnabled
|
|
||||||
acceptedButtons: Qt.NoButton
|
|
||||||
propagateComposedEvents: true
|
|
||||||
|
|
||||||
property real lastGlobalX: 0
|
property real lastGlobalX: 0
|
||||||
property real lastGlobalY: 0
|
property real lastGlobalY: 0
|
||||||
|
|
||||||
onPositionChanged: mouse => {
|
onPointChanged: {
|
||||||
const gp = mapToItem(null, mouse.x, mouse.y);
|
const gp = barUnitInset.mapToItem(null, point.position.x, point.position.y);
|
||||||
lastGlobalX = gp.x;
|
lastGlobalX = gp.x;
|
||||||
lastGlobalY = gp.y;
|
lastGlobalY = gp.y;
|
||||||
topBarContent.checkHoverPopout(gp.x, gp.y);
|
topBarContent.checkHoverPopout(gp.x, gp.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
onWheel: wheel => scrollArea.processWheel(wheel)
|
onHoveredChanged: {
|
||||||
|
if (hovered)
|
||||||
onContainsMouseChanged: {
|
|
||||||
if (containsMouse)
|
|
||||||
return;
|
return;
|
||||||
if (topBarContent.cursorOverHoverChain(lastGlobalX, lastGlobalY))
|
topBarContent.scheduleHoverClose(lastGlobalX, lastGlobalY);
|
||||||
return;
|
|
||||||
topBarContent.closeHoverSurfaces();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
// Edge strip to trigger launcher hover-reveal when free of panel bars and dock.
|
||||||
|
Variants {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
model: Quickshell.screens
|
||||||
|
|
||||||
|
delegate: Loader {
|
||||||
|
id: zoneLoader
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property string emergeSide: SettingsData.frameLauncherEmergeSide || "bottom"
|
||||||
|
readonly property bool eligible: SettingsData.frameEnabled && SettingsData.frameLauncherEdgeHover && Theme.isConnectedEffect && SettingsData.isScreenInPreferences(zoneLoader.modelData, SettingsData.frameScreenPreferences) && CompositorService.usesConnectedFrameChromeForScreen(zoneLoader.modelData) && !SettingsData.barOccupiesSide(zoneLoader.modelData, zoneLoader.emergeSide) && !SettingsData.dockOccupiesSide(zoneLoader.emergeSide)
|
||||||
|
|
||||||
|
active: eligible
|
||||||
|
asynchronous: false
|
||||||
|
|
||||||
|
sourceComponent: PanelWindow {
|
||||||
|
id: zoneWindow
|
||||||
|
|
||||||
|
readonly property bool vertical: zoneLoader.emergeSide === "left" || zoneLoader.emergeSide === "right"
|
||||||
|
readonly property real triggerThickness: Math.max(6, SettingsData.frameThickness)
|
||||||
|
readonly property bool launcherOpen: PopoutService.dankLauncherV2Modal?.spotlightOpen ?? false
|
||||||
|
property bool _openedForCurrentHover: false
|
||||||
|
|
||||||
|
// Hot zone dimensions centered on the emerge edge to cover the launcher footprint.
|
||||||
|
readonly property real _launcherBaseW: SettingsData.dankLauncherV2Size === "micro" ? 500 : (SettingsData.dankLauncherV2Size === "medium" ? 720 : (SettingsData.dankLauncherV2Size === "large" ? 860 : 620))
|
||||||
|
readonly property real _launcherBaseH: SettingsData.dankLauncherV2Size === "micro" ? 480 : (SettingsData.dankLauncherV2Size === "medium" ? 720 : (SettingsData.dankLauncherV2Size === "large" ? 860 : 600))
|
||||||
|
readonly property real screenW: zoneLoader.modelData?.width ?? 0
|
||||||
|
readonly property real screenH: zoneLoader.modelData?.height ?? 0
|
||||||
|
readonly property real spanW: Math.round(Math.min(_launcherBaseW, screenW - 100) * 1.1)
|
||||||
|
readonly property real spanH: Math.round(Math.min(_launcherBaseH, screenH - 100) * 1.1)
|
||||||
|
|
||||||
|
function requestLauncherOpen() {
|
||||||
|
if (launcherOpen || _openedForCurrentHover)
|
||||||
|
return;
|
||||||
|
_openedForCurrentHover = true;
|
||||||
|
PopoutService.openDankLauncherV2(CompositorService.framePeerSurfacesUseOverlayForScreen(zoneLoader.modelData), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
screen: zoneLoader.modelData
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "dms:frame-launcher-hover"
|
||||||
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
|
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
// Anchor and center the hover zone alignment with the launcher.
|
||||||
|
anchors {
|
||||||
|
top: zoneLoader.emergeSide === "top" || zoneWindow.vertical
|
||||||
|
bottom: zoneLoader.emergeSide === "bottom"
|
||||||
|
left: zoneLoader.emergeSide === "left" || !zoneWindow.vertical
|
||||||
|
right: zoneLoader.emergeSide === "right"
|
||||||
|
}
|
||||||
|
|
||||||
|
margins {
|
||||||
|
left: zoneWindow.vertical ? 0 : Math.max(0, (zoneWindow.screenW - zoneWindow.spanW) / 2)
|
||||||
|
top: zoneWindow.vertical ? Math.max(0, (zoneWindow.screenH - zoneWindow.spanH) / 2) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: zoneWindow.vertical ? zoneWindow.triggerThickness : zoneWindow.spanW
|
||||||
|
implicitHeight: zoneWindow.vertical ? zoneWindow.spanH : zoneWindow.triggerThickness
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: edgeHoverArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
|
||||||
|
onContainsMouseChanged: {
|
||||||
|
if (containsMouse)
|
||||||
|
zoneWindow.requestLauncherOpen();
|
||||||
|
else
|
||||||
|
zoneWindow._openedForCurrentHover = false;
|
||||||
|
}
|
||||||
|
onPositionChanged: {
|
||||||
|
if (containsMouse)
|
||||||
|
zoneWindow.requestLauncherOpen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1811,6 +1811,37 @@ Item {
|
|||||||
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
|
||||||
hoverPopouts: checked
|
hoverPopouts: checked
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
visible: selectedBarConfig?.hoverPopouts ?? false
|
||||||
|
leftPadding: Theme.spacingM
|
||||||
|
|
||||||
|
SettingsSliderRow {
|
||||||
|
id: hoverDelaySlider
|
||||||
|
width: parent.width - parent.leftPadding
|
||||||
|
text: I18n.tr("Open Delay")
|
||||||
|
description: I18n.tr("Time to rest on a widget before its popout opens")
|
||||||
|
value: selectedBarConfig?.hoverPopoutDelay ?? 150
|
||||||
|
minimum: 0
|
||||||
|
maximum: 1000
|
||||||
|
unit: "ms"
|
||||||
|
defaultValue: 150
|
||||||
|
onSliderValueChanged: newValue => {
|
||||||
|
SettingsData.updateBarConfig(selectedBarId, {
|
||||||
|
hoverPopoutDelay: newValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
target: hoverDelaySlider
|
||||||
|
property: "value"
|
||||||
|
value: selectedBarConfig?.hoverPopoutDelay ?? 150
|
||||||
|
restoreMode: Binding.RestoreBinding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsToggleCard {
|
SettingsToggleCard {
|
||||||
|
|||||||
@@ -357,6 +357,15 @@ Item {
|
|||||||
checked: SettingsData.frameLauncherArcExtender
|
checked: SettingsData.frameLauncherArcExtender
|
||||||
onToggled: checked => SettingsData.set("frameLauncherArcExtender", checked)
|
onToggled: checked => SettingsData.set("frameLauncherArcExtender", checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "frameLauncherEdgeHover"
|
||||||
|
tags: ["frame", "connected", "launcher", "hover", "edge", "reveal"]
|
||||||
|
text: I18n.tr("Edge Hover Reveal")
|
||||||
|
description: I18n.tr("Open the launcher by hovering the emerge edge (when free of bar and dock)")
|
||||||
|
checked: SettingsData.frameLauncherEdgeHover
|
||||||
|
onToggled: checked => SettingsData.set("frameLauncherEdgeHover", checked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
|
|||||||
@@ -502,15 +502,26 @@ Singleton {
|
|||||||
property string _dankLauncherV2PendingQuery: ""
|
property string _dankLauncherV2PendingQuery: ""
|
||||||
property string _dankLauncherV2PendingMode: ""
|
property string _dankLauncherV2PendingMode: ""
|
||||||
property bool _dankLauncherV2TriggerUsesOverlayLayer: false
|
property bool _dankLauncherV2TriggerUsesOverlayLayer: false
|
||||||
|
property bool _dankLauncherV2EdgeHoverManaged: false
|
||||||
|
|
||||||
function _setDankLauncherV2TriggerUsesOverlayLayer(value) {
|
function _setDankLauncherV2TriggerUsesOverlayLayer(value) {
|
||||||
_dankLauncherV2TriggerUsesOverlayLayer = value === true;
|
_dankLauncherV2TriggerUsesOverlayLayer = value === true;
|
||||||
|
// Disable edge-hover by default on every open/toggle path unless explicitly enabled.
|
||||||
|
_setDankLauncherV2EdgeHoverManaged(false);
|
||||||
if (dankLauncherV2Modal)
|
if (dankLauncherV2Modal)
|
||||||
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
|
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openDankLauncherV2(triggerUsesOverlayLayer) {
|
// Set edgeHoverManaged to enable hover retraction for edge-hover triggered launcher sessions.
|
||||||
|
function _setDankLauncherV2EdgeHoverManaged(value) {
|
||||||
|
_dankLauncherV2EdgeHoverManaged = value === true;
|
||||||
|
if (dankLauncherV2Modal)
|
||||||
|
dankLauncherV2Modal.edgeHoverManaged = _dankLauncherV2EdgeHoverManaged;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDankLauncherV2(triggerUsesOverlayLayer, edgeHoverManaged) {
|
||||||
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
|
_setDankLauncherV2TriggerUsesOverlayLayer(triggerUsesOverlayLayer);
|
||||||
|
_setDankLauncherV2EdgeHoverManaged(edgeHoverManaged);
|
||||||
if (dankLauncherV2Modal) {
|
if (dankLauncherV2Modal) {
|
||||||
dankLauncherV2Modal.show();
|
dankLauncherV2Modal.show();
|
||||||
} else if (dankLauncherV2ModalLoader) {
|
} else if (dankLauncherV2ModalLoader) {
|
||||||
@@ -591,8 +602,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _onDankLauncherV2ModalLoaded() {
|
function _onDankLauncherV2ModalLoaded() {
|
||||||
if (dankLauncherV2Modal)
|
if (dankLauncherV2Modal) {
|
||||||
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
|
dankLauncherV2Modal.triggerUsesOverlayLayer = _dankLauncherV2TriggerUsesOverlayLayer;
|
||||||
|
dankLauncherV2Modal.edgeHoverManaged = _dankLauncherV2EdgeHoverManaged;
|
||||||
|
}
|
||||||
if (_dankLauncherV2WantsOpen) {
|
if (_dankLauncherV2WantsOpen) {
|
||||||
_dankLauncherV2WantsOpen = false;
|
_dankLauncherV2WantsOpen = false;
|
||||||
if (_dankLauncherV2PendingQuery) {
|
if (_dankLauncherV2PendingQuery) {
|
||||||
|
|||||||
@@ -180,20 +180,22 @@ Item {
|
|||||||
impl.item.cancelHoverDismiss();
|
impl.item.cancelHoverDismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fade out in place during morph switch transitions.
|
||||||
|
function beginSupersededClose() {
|
||||||
|
if (impl.item?.beginSupersededClose)
|
||||||
|
impl.item.beginSupersededClose();
|
||||||
|
}
|
||||||
|
|
||||||
function closeFromHoverDismiss() {
|
function closeFromHoverDismiss() {
|
||||||
hoverDismissEnabled = false;
|
hoverDismissEnabled = false;
|
||||||
if (impl.item) {
|
// Enable animations using standard Theme-bound popout motion to preserve bindings.
|
||||||
|
if (impl.item)
|
||||||
impl.item.animationsEnabled = true;
|
impl.item.animationsEnabled = true;
|
||||||
impl.item.animationDuration = Math.round(Theme.expressiveDurations.expressiveDefaultSpatial);
|
for (const prop of ["dashVisible", "notificationHistoryVisible"]) {
|
||||||
impl.item.animationExitCurve = Theme.expressiveCurves.expressiveDefaultSpatial;
|
if (root[prop] !== undefined) {
|
||||||
}
|
root[prop] = false;
|
||||||
if (dashVisible !== undefined) {
|
return;
|
||||||
dashVisible = false;
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (notificationHistoryVisible !== undefined) {
|
|
||||||
notificationHistoryVisible = false;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (impl.item)
|
if (impl.item)
|
||||||
impl.item.close();
|
impl.item.close();
|
||||||
|
|||||||
@@ -235,10 +235,10 @@ Item {
|
|||||||
const presented = contentWindow.visible || root.shouldBeVisible;
|
const presented = contentWindow.visible || root.shouldBeVisible;
|
||||||
const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open"));
|
const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open"));
|
||||||
const bodyRect = {
|
const bodyRect = {
|
||||||
"x": root.alignedX,
|
"x": root.pubBodyX,
|
||||||
"y": root.renderedAlignedY,
|
"y": root.pubBodyY,
|
||||||
"width": root.alignedWidth,
|
"width": root.pubBodyW,
|
||||||
"height": root.renderedAlignedHeight
|
"height": root.pubBodyH
|
||||||
};
|
};
|
||||||
const animationOffset = {
|
const animationOffset = {
|
||||||
"x": _connectedChromeAnimX(),
|
"x": _connectedChromeAnimX(),
|
||||||
@@ -255,10 +255,10 @@ Item {
|
|||||||
"animationOffset": animationOffset,
|
"animationOffset": animationOffset,
|
||||||
"scale": 1,
|
"scale": 1,
|
||||||
"opacity": Theme.connectedSurfaceColor.a,
|
"opacity": Theme.connectedSurfaceColor.a,
|
||||||
"bodyX": root.alignedX,
|
"bodyX": root.pubBodyX,
|
||||||
"bodyY": root.renderedAlignedY,
|
"bodyY": root.pubBodyY,
|
||||||
"bodyW": root.alignedWidth,
|
"bodyW": root.pubBodyW,
|
||||||
"bodyH": root.renderedAlignedHeight,
|
"bodyH": root.pubBodyH,
|
||||||
"animX": animationOffset.x,
|
"animX": animationOffset.x,
|
||||||
"animY": animationOffset.y,
|
"animY": animationOffset.y,
|
||||||
"screen": root.screen ? root.screen.name : "",
|
"screen": root.screen ? root.screen.name : "",
|
||||||
@@ -312,7 +312,7 @@ Item {
|
|||||||
return;
|
return;
|
||||||
if (!contentWindow.visible && !shouldBeVisible)
|
if (!contentWindow.visible && !shouldBeVisible)
|
||||||
return;
|
return;
|
||||||
chromeLease.updateBody(root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight);
|
chromeLease.updateBody(root.pubBodyX, root.pubBodyY, root.pubBodyW, root.pubBodyH);
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool _animSyncQueued: false
|
property bool _animSyncQueued: false
|
||||||
@@ -430,6 +430,7 @@ Item {
|
|||||||
isClosing = false;
|
isClosing = false;
|
||||||
animationsEnabled = false;
|
animationsEnabled = false;
|
||||||
_primeContent = true;
|
_primeContent = true;
|
||||||
|
_supersededClose = false;
|
||||||
|
|
||||||
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen;
|
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen;
|
||||||
if (screenChanged) {
|
if (screenChanged) {
|
||||||
@@ -444,6 +445,13 @@ Item {
|
|||||||
_captureChromeAnimTravel();
|
_captureChromeAnimTravel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Seed travel coordinates from the outgoing popout to morph continuously.
|
||||||
|
_beginMorphTravel();
|
||||||
|
|
||||||
|
// Skip emerge animation on morph switch.
|
||||||
|
if (morphTravelEnabled)
|
||||||
|
morph.openProgress = 1;
|
||||||
|
|
||||||
if (root.frameOwnsConnectedChrome) {
|
if (root.frameOwnsConnectedChrome) {
|
||||||
chromeLease.beginClaim();
|
chromeLease.beginClaim();
|
||||||
_publishConnectedChromeState(true, true);
|
_publishConnectedChromeState(true, true);
|
||||||
@@ -471,6 +479,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
|
_endMorphTravel();
|
||||||
isClosing = true;
|
isClosing = true;
|
||||||
shouldBeVisible = false;
|
shouldBeVisible = false;
|
||||||
_primeContent = false;
|
_primeContent = false;
|
||||||
@@ -657,6 +666,88 @@ Item {
|
|||||||
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.renderedGeometryGrowing ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Morph transition coordinates to animate travel between popouts during switch.
|
||||||
|
property bool morphTravelEnabled: false
|
||||||
|
property real morphSeedX: 0
|
||||||
|
property real morphSeedY: 0
|
||||||
|
property real morphSeedW: 0
|
||||||
|
property real morphSeedH: 0
|
||||||
|
property real morphProgress: 1
|
||||||
|
// Distance-scaled duration for morph travel.
|
||||||
|
property int _morphTravelDuration: animationDuration
|
||||||
|
|
||||||
|
Behavior on morphProgress {
|
||||||
|
enabled: root.morphTravelEnabled && root.animationsEnabled
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root._morphTravelDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
// Emphasized curve for fluid morph travel.
|
||||||
|
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property real pubBodyX: morphSeedX + (alignedX - morphSeedX) * morphProgress
|
||||||
|
readonly property real pubBodyY: morphSeedY + (renderedAlignedY - morphSeedY) * morphProgress
|
||||||
|
readonly property real pubBodyW: morphSeedW + (alignedWidth - morphSeedW) * morphProgress
|
||||||
|
readonly property real pubBodyH: morphSeedH + (renderedAlignedHeight - morphSeedH) * morphProgress
|
||||||
|
|
||||||
|
onPubBodyXChanged: _queueBodySync()
|
||||||
|
onPubBodyYChanged: _queueBodySync()
|
||||||
|
onPubBodyWChanged: _queueBodySync()
|
||||||
|
onPubBodyHChanged: _queueBodySync()
|
||||||
|
|
||||||
|
function _beginMorphTravel() {
|
||||||
|
morphTravelEnabled = false;
|
||||||
|
morphProgress = 1;
|
||||||
|
if (!root.frameOwnsConnectedChrome || !root.screen)
|
||||||
|
return;
|
||||||
|
if (!root.hoverDismissEnabled)
|
||||||
|
return;
|
||||||
|
if (ConnectedModeState.popoutScreen !== root.screen.name)
|
||||||
|
return;
|
||||||
|
if (!ConnectedModeState.popoutOwnerId || ConnectedModeState.popoutOwnerId === chromeLease.claimId)
|
||||||
|
return;
|
||||||
|
const w = ConnectedModeState.popoutBodyW;
|
||||||
|
const h = ConnectedModeState.popoutBodyH;
|
||||||
|
if (!(w > 0 && h > 0))
|
||||||
|
return;
|
||||||
|
morphSeedX = ConnectedModeState.popoutBodyX;
|
||||||
|
morphSeedY = ConnectedModeState.popoutBodyY;
|
||||||
|
morphSeedW = w;
|
||||||
|
morphSeedH = h;
|
||||||
|
// Scale travel time with distance within ~[0.8x, 1.4x] of the popout duration:
|
||||||
|
// enough room for the emphasized curve to breathe (fluid, not abrupt), capped so
|
||||||
|
// long sweeps don't drag, and collapsing to 0 when popout animations are off.
|
||||||
|
const base = Math.max(0, root.animationDuration);
|
||||||
|
const dist = Math.hypot(root.alignedX - morphSeedX, root.renderedAlignedY - morphSeedY);
|
||||||
|
_morphTravelDuration = Math.round(Math.min(base * 1.4, base * 0.8 + dist * 0.16));
|
||||||
|
morphProgress = 0;
|
||||||
|
morphTravelEnabled = true;
|
||||||
|
Qt.callLater(() => {
|
||||||
|
if (root.shouldBeVisible)
|
||||||
|
root.morphProgress = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function _endMorphTravel() {
|
||||||
|
morphTravelEnabled = false;
|
||||||
|
morphProgress = 1;
|
||||||
|
morphSeedX = 0;
|
||||||
|
morphSeedY = 0;
|
||||||
|
morphSeedW = 0;
|
||||||
|
morphSeedH = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flag to trigger in-place fade-out during a morph switch.
|
||||||
|
property bool _supersededClose: false
|
||||||
|
|
||||||
|
function beginSupersededClose() {
|
||||||
|
// Only set superseded flag for transient hover switches.
|
||||||
|
if (frameOwnsConnectedChrome && hoverDismissEnabled)
|
||||||
|
_supersededClose = true;
|
||||||
|
}
|
||||||
|
|
||||||
readonly property real connectedAnchorX: {
|
readonly property real connectedAnchorX: {
|
||||||
if (!root.usesConnectedSurfaceChrome)
|
if (!root.usesConnectedSurfaceChrome)
|
||||||
return triggerX;
|
return triggerX;
|
||||||
@@ -758,6 +849,32 @@ Item {
|
|||||||
readonly property real maskWidth: _dismissZone.width
|
readonly property real maskWidth: _dismissZone.width
|
||||||
readonly property real maskHeight: _dismissZone.height
|
readonly property real maskHeight: _dismissZone.height
|
||||||
|
|
||||||
|
// Track body hover to initiate grace timer for transient dismissal.
|
||||||
|
property bool _hoverOverBody: false
|
||||||
|
|
||||||
|
function _onBodyHoverChanged(over) {
|
||||||
|
_hoverOverBody = over;
|
||||||
|
if (over)
|
||||||
|
_hoverDismissGrace.stop();
|
||||||
|
else if (root.hoverDismissEnabled && root.shouldBeVisible)
|
||||||
|
_hoverDismissGrace.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _hoverDismissGrace
|
||||||
|
interval: 150
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (!root.hoverDismissEnabled || !root.shouldBeVisible)
|
||||||
|
return;
|
||||||
|
if (root._hoverOverBody)
|
||||||
|
return;
|
||||||
|
if (PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY))
|
||||||
|
return;
|
||||||
|
root.closeFromHoverDismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DismissZone {
|
DismissZone {
|
||||||
id: _dismissZone
|
id: _dismissZone
|
||||||
barPosition: root.effectiveBarPosition
|
barPosition: root.effectiveBarPosition
|
||||||
@@ -795,6 +912,7 @@ Item {
|
|||||||
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
|
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
|
||||||
}
|
}
|
||||||
onDismissRequested: root.closeFromHoverDismiss()
|
onDismissRequested: root.closeFromHoverDismiss()
|
||||||
|
onHoverMoved: (gx, gy) => PopoutManager.updateHoverCursor(gx, gy)
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
@@ -878,10 +996,11 @@ Item {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: contentContainer
|
id: contentContainer
|
||||||
x: root.alignedX
|
// Follow the morphing body bounds during transition.
|
||||||
y: root.renderedAlignedY
|
x: root.morphTravelEnabled ? root.pubBodyX : root.alignedX
|
||||||
width: root.alignedWidth
|
y: root.morphTravelEnabled ? root.pubBodyY : root.renderedAlignedY
|
||||||
height: root.renderedAlignedHeight
|
width: root.morphTravelEnabled ? root.pubBodyW : root.alignedWidth
|
||||||
|
height: root.morphTravelEnabled ? root.pubBodyH : root.renderedAlignedHeight
|
||||||
|
|
||||||
readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
|
readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
|
||||||
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
|
||||||
@@ -950,6 +1069,19 @@ Item {
|
|||||||
|
|
||||||
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
|
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
|
||||||
|
|
||||||
|
// Ancestor HoverHandler to capture body hover reliably.
|
||||||
|
HoverHandler {
|
||||||
|
id: bodyHoverHandler
|
||||||
|
enabled: root.hoverDismissEnabled && root.shouldBeVisible
|
||||||
|
onHoveredChanged: root._onBodyHoverChanged(hovered)
|
||||||
|
onPointChanged: {
|
||||||
|
if (!bodyHoverHandler.hovered)
|
||||||
|
return;
|
||||||
|
const gp = contentContainer.mapToItem(null, bodyHoverHandler.point.position.x, bodyHoverHandler.point.position.y);
|
||||||
|
PopoutManager.updateHoverCursor(gp.x, gp.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
QtObject {
|
QtObject {
|
||||||
id: morph
|
id: morph
|
||||||
property real openProgress: 0
|
property real openProgress: 0
|
||||||
@@ -977,7 +1109,8 @@ Item {
|
|||||||
target: root
|
target: root
|
||||||
function onShouldBeVisibleChanged() {
|
function onShouldBeVisibleChanged() {
|
||||||
root._captureChromeAnimTravel();
|
root._captureChromeAnimTravel();
|
||||||
morph.openProgress = root.shouldBeVisible ? 1 : 0;
|
// Skip reverse emerge animation during a superseded close.
|
||||||
|
morph.openProgress = (root.shouldBeVisible || root._supersededClose) ? 1 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1103,23 +1236,27 @@ Item {
|
|||||||
|
|
||||||
property bool _renderActive: Theme.isDirectionalEffect || shouldBeVisible
|
property bool _renderActive: Theme.isDirectionalEffect || shouldBeVisible
|
||||||
property bool _animating: false
|
property bool _animating: false
|
||||||
property real publishedOpacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
readonly property bool _fadeWithOpacity: !Theme.isDirectionalEffect || root._supersededClose
|
||||||
|
// Fast fade duration for superseded close.
|
||||||
|
readonly property bool _supersededFade: root._supersededClose && !root.shouldBeVisible
|
||||||
|
readonly property real _targetOpacity: root._supersededClose ? (root.shouldBeVisible ? 1 : 0) : (Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0))
|
||||||
|
property real publishedOpacity: _targetOpacity
|
||||||
|
|
||||||
opacity: Theme.isDirectionalEffect ? 1 : (shouldBeVisible ? 1 : 0)
|
opacity: _targetOpacity
|
||||||
visible: _renderActive
|
visible: _renderActive
|
||||||
|
|
||||||
scale: contentContainer.scaleValue
|
scale: contentContainer.scaleValue
|
||||||
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
|
x: Theme.snap(contentContainer.animX + (rollOutAdjuster.baseWidth - width) * (1 - scale) * 0.5, root.dpr)
|
||||||
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
|
y: Theme.snap(contentContainer.animY + (rollOutAdjuster.baseHeight - height) * (1 - scale) * 0.5, root.dpr)
|
||||||
|
|
||||||
layer.enabled: _animating || (!Theme.isDirectionalEffect && publishedOpacity < 1)
|
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
|
||||||
layer.smooth: false
|
layer.smooth: false
|
||||||
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: !Theme.isDirectionalEffect
|
enabled: contentWrapper._fadeWithOpacity
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
onRunningChanged: {
|
onRunningChanged: {
|
||||||
@@ -1131,9 +1268,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on publishedOpacity {
|
Behavior on publishedOpacity {
|
||||||
enabled: !Theme.isDirectionalEffect
|
enabled: contentWrapper._fadeWithOpacity
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
duration: contentWrapper._supersededFade ? Theme.shorterDuration : Math.round(Theme.variantDuration(animationDuration, shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,32 @@ Item {
|
|||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track body hover to initiate grace timer for transient dismissal.
|
||||||
|
property bool _hoverOverBody: false
|
||||||
|
|
||||||
|
function _onBodyHoverChanged(over) {
|
||||||
|
_hoverOverBody = over;
|
||||||
|
if (over)
|
||||||
|
_hoverDismissGrace.stop();
|
||||||
|
else if (root.hoverDismissEnabled && root.shouldBeVisible)
|
||||||
|
_hoverDismissGrace.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: _hoverDismissGrace
|
||||||
|
interval: 150
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
if (!root.hoverDismissEnabled || !root.shouldBeVisible)
|
||||||
|
return;
|
||||||
|
if (root._hoverOverBody)
|
||||||
|
return;
|
||||||
|
if (PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY))
|
||||||
|
return;
|
||||||
|
root.closeFromHoverDismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
property var customKeyboardFocus: null
|
property var customKeyboardFocus: null
|
||||||
property bool backgroundInteractive: true
|
property bool backgroundInteractive: true
|
||||||
property bool contentHandlesKeys: false
|
property bool contentHandlesKeys: false
|
||||||
@@ -620,6 +646,7 @@ Item {
|
|||||||
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
|
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
|
||||||
}
|
}
|
||||||
onDismissRequested: root.closeFromHoverDismiss()
|
onDismissRequested: root.closeFromHoverDismiss()
|
||||||
|
onHoverMoved: (gx, gy) => PopoutManager.updateHoverCursor(gx, gy)
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowBlur {
|
WindowBlur {
|
||||||
@@ -739,6 +766,19 @@ Item {
|
|||||||
|
|
||||||
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
|
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
|
||||||
|
|
||||||
|
// Ancestor HoverHandler to capture body hover reliably.
|
||||||
|
HoverHandler {
|
||||||
|
id: bodyHoverHandler
|
||||||
|
enabled: root.hoverDismissEnabled && root.shouldBeVisible
|
||||||
|
onHoveredChanged: root._onBodyHoverChanged(hovered)
|
||||||
|
onPointChanged: {
|
||||||
|
if (!bodyHoverHandler.hovered)
|
||||||
|
return;
|
||||||
|
const gp = contentContainer.mapToItem(null, bodyHoverHandler.point.position.x, bodyHoverHandler.point.position.y);
|
||||||
|
PopoutManager.updateHoverCursor(gp.x, gp.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
|
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
|
||||||
QtObject {
|
QtObject {
|
||||||
id: morph
|
id: morph
|
||||||
|
|||||||
@@ -9,12 +9,20 @@ Item {
|
|||||||
property var shouldDismiss: null
|
property var shouldDismiss: null
|
||||||
|
|
||||||
signal dismissRequested
|
signal dismissRequested
|
||||||
|
// Emitted on every hover move; passive to avoid blocking overlapping MouseAreas
|
||||||
|
signal hoverMoved(real gx, real gy)
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
HoverHandler {
|
HoverHandler {
|
||||||
id: hoverHandler
|
id: hoverHandler
|
||||||
enabled: root.enabled
|
enabled: root.enabled
|
||||||
|
onPointChanged: {
|
||||||
|
if (!root.enabled || !hoverHandler.hovered)
|
||||||
|
return;
|
||||||
|
const gp = root.mapToItem(null, hoverHandler.point.position.x, hoverHandler.point.position.y);
|
||||||
|
root.hoverMoved(gp.x, gp.y);
|
||||||
|
}
|
||||||
onHoveredChanged: {
|
onHoveredChanged: {
|
||||||
if (hoverHandler.hovered || !root.enabled)
|
if (hoverHandler.hovered || !root.enabled)
|
||||||
return;
|
return;
|
||||||
@@ -24,5 +32,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPending() {}
|
function cancelPending() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user