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

feat(HoverMode): implement hover popout & launcher functionality in all modes

- New Hover toggle found in DankBar Settings
- New Hover to Reveal Launcher in FrameTab Settings
This commit is contained in:
purian23
2026-06-27 22:47:38 -04:00
parent 48f6a0c632
commit 6bee1b2c86
32 changed files with 2266 additions and 364 deletions
+50
View File
@@ -24,6 +24,8 @@ Item {
property list<real> animationExitCurve: Theme.variantPopoutExitCurve
property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
property var customKeyboardFocus: null
property bool backgroundInteractive: true
property bool contentHandlesKeys: false
@@ -82,6 +84,8 @@ Item {
readonly property real alignedY: impl.item ? impl.item.alignedY : 0
readonly property real alignedWidth: impl.item ? impl.item.alignedWidth : 0
readonly property real alignedHeight: impl.item ? impl.item.alignedHeight : 0
readonly property real renderedAlignedY: impl.item ? (impl.item.renderedAlignedY ?? impl.item.alignedY) : 0
readonly property real renderedAlignedHeight: impl.item ? (impl.item.renderedAlignedHeight ?? impl.item.alignedHeight) : 0
readonly property real maskX: impl.item ? impl.item.maskX : 0
readonly property real maskY: impl.item ? impl.item.maskY : 0
readonly property real maskWidth: impl.item ? impl.item.maskWidth : 0
@@ -172,6 +176,36 @@ Item {
impl.item.close();
}
function cancelHoverDismiss() {
if (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() {
if (hoverDismissSuspended)
return;
hoverDismissEnabled = false;
// Enable animations using standard Theme-bound popout motion to preserve bindings.
if (impl.item)
impl.item.animationsEnabled = true;
for (const prop of ["dashVisible", "notificationHistoryVisible"]) {
if (root[prop] !== undefined) {
root[prop] = false;
return;
}
}
if (impl.item)
impl.item.close();
else
close();
}
function toggle() {
(shouldBeVisible || _pendingOpen) ? close() : open();
}
@@ -210,6 +244,20 @@ Item {
impl.item.updateSurfacePosition();
}
function containsGlobalPoint(gx, gy) {
if (!screen)
return false;
const presented = shouldBeVisible || (impl.item?.isClosing ?? false);
if (!presented)
return false;
const padding = 24;
const x = alignedX - padding;
const y = renderedAlignedY - padding;
const w = alignedWidth + padding * 2;
const h = renderedAlignedHeight + padding * 2;
return gx >= x && gx <= x + w && gy >= y && gy <= y + h;
}
Loader {
id: impl
active: root.screen !== null
@@ -261,6 +309,8 @@ Item {
it.screen = Qt.binding(() => root.screen);
it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition);
it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap);
it.hoverDismissEnabled = Qt.binding(() => root.hoverDismissEnabled);
it.hoverDismissSuspended = Qt.binding(() => root.hoverDismissSuspended);
it.shouldBeVisible = root.shouldBeVisible;
if (root._primeContent && typeof it.primeContent === "function")
+217 -24
View File
@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -41,6 +42,11 @@ Item {
property real _chromeAnimTravelX: 1
property real _chromeAnimTravelY: 1
property bool _fullSyncQueued: false
property bool _publishedBodyValid: false
property real _publishedBodyX: 0
property real _publishedBodyY: 0
property real _publishedBodyW: 0
property real _publishedBodyH: 0
property real storedBarThickness: Theme.barHeight - 4
property real storedBarSpacing: 4
@@ -130,7 +136,11 @@ Item {
updateBodyState: function(_name, ownerId, bodyX, bodyY, bodyW, bodyH) {
return ConnectedModeState.setPopoutBody(ownerId, bodyX, bodyY, bodyW, bodyH);
}
onRecoveryRequested: root._queueFullSync()
onClaimIdChanged: root._resetPublishedBody()
onRecoveryRequested: {
root._resetPublishedBody();
root._queueFullSync();
}
}
property var _lastOpenedScreen: null
@@ -233,11 +243,15 @@ Item {
const visible = visibleOverride !== undefined ? !!visibleOverride : contentWindow.visible;
const presented = contentWindow.visible || root.shouldBeVisible;
const phase = root.isClosing ? "closing" : (!presented ? "hidden" : (!contentWindow.visible && root.shouldBeVisible ? "opening" : "open"));
const bodyX = Theme.snap(root.pubBodyX, root.dpr);
const bodyY = Theme.snap(root.pubBodyY, root.dpr);
const bodyW = Theme.snap(root.pubBodyW, root.dpr);
const bodyH = Theme.snap(root.pubBodyH, root.dpr);
const bodyRect = {
"x": root.alignedX,
"y": root.renderedAlignedY,
"width": root.alignedWidth,
"height": root.renderedAlignedHeight
"x": bodyX,
"y": bodyY,
"width": bodyW,
"height": bodyH
};
const animationOffset = {
"x": _connectedChromeAnimX(),
@@ -254,10 +268,10 @@ Item {
"animationOffset": animationOffset,
"scale": 1,
"opacity": Theme.connectedSurfaceColor.a,
"bodyX": root.alignedX,
"bodyY": root.renderedAlignedY,
"bodyW": root.alignedWidth,
"bodyH": root.renderedAlignedHeight,
"bodyX": bodyX,
"bodyY": bodyY,
"bodyW": bodyW,
"bodyH": bodyH,
"animX": animationOffset.x,
"animY": animationOffset.y,
"screen": root.screen ? root.screen.name : "",
@@ -269,10 +283,15 @@ Item {
function _publishConnectedChromeState(forceClaim, visibleOverride) {
if (!root.frameOwnsConnectedChrome || !root.screen)
return false;
return chromeLease.publish(_connectedChromeState(visibleOverride), !!forceClaim);
const state = _connectedChromeState(visibleOverride);
const published = chromeLease.publish(state, !!forceClaim);
if (published)
_rememberPublishedBody(state.bodyX, state.bodyY, state.bodyW, state.bodyH);
return published;
}
function _releaseConnectedChromeState() {
_resetPublishedBody();
chromeLease.release();
}
@@ -311,7 +330,26 @@ Item {
return;
if (!contentWindow.visible && !shouldBeVisible)
return;
chromeLease.updateBody(root.alignedX, root.renderedAlignedY, root.alignedWidth, root.renderedAlignedHeight);
const bodyX = Theme.snap(root.pubBodyX, root.dpr);
const bodyY = Theme.snap(root.pubBodyY, root.dpr);
const bodyW = Theme.snap(root.pubBodyW, root.dpr);
const bodyH = Theme.snap(root.pubBodyH, root.dpr);
if (_publishedBodyValid && _publishedBodyX === bodyX && _publishedBodyY === bodyY && _publishedBodyW === bodyW && _publishedBodyH === bodyH)
return;
if (chromeLease.updateBody(bodyX, bodyY, bodyW, bodyH))
_rememberPublishedBody(bodyX, bodyY, bodyW, bodyH);
}
function _rememberPublishedBody(bodyX, bodyY, bodyW, bodyH) {
_publishedBodyX = bodyX;
_publishedBodyY = bodyY;
_publishedBodyW = bodyW;
_publishedBodyH = bodyH;
_publishedBodyValid = true;
}
function _resetPublishedBody() {
_publishedBodyValid = false;
}
property bool _animSyncQueued: false
@@ -356,7 +394,10 @@ Item {
onContentAnimYChanged: _queueAnimSync()
onRenderedAlignedYChanged: _queueBodySync()
onRenderedAlignedHeightChanged: _queueBodySync()
onScreenChanged: _queueFullSync()
onScreenChanged: {
_resetPublishedBody();
_queueFullSync();
}
onEffectiveBarPositionChanged: _queueFullSync()
Connections {
@@ -407,14 +448,31 @@ Item {
onFrameOwnsConnectedChromeChanged: _syncPopoutChromeState()
property bool animationsEnabled: true
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissController.cancelPending();
}
function closeFromHoverDismiss() {
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return;
if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss();
else
close();
}
function open() {
if (!screen)
return;
_resetPublishedBody();
closeTimer.stop();
isClosing = false;
animationsEnabled = false;
_primeContent = true;
_supersededClose = false;
const screenChanged = _lastOpenedScreen !== null && _lastOpenedScreen !== screen;
if (screenChanged) {
@@ -429,6 +487,13 @@ Item {
_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) {
chromeLease.beginClaim();
_publishConnectedChromeState(true, true);
@@ -456,6 +521,11 @@ Item {
}
function close() {
if (_supersededClose && morphTravelEnabled)
_freezeMorphTravel();
else
_endMorphTravel();
_resetPublishedBody();
isClosing = true;
shouldBeVisible = false;
_primeContent = false;
@@ -494,6 +564,7 @@ Item {
onTriggered: {
if (!shouldBeVisible) {
contentWindow.visible = false;
root._endMorphTravel();
isClosing = false;
PopoutManager.hidePopout(popoutHandle);
popoutClosed();
@@ -642,6 +713,108 @@ Item {
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
// M3 Expressive spatial motion starts with momentum and settles gently,
// which keeps rapid hover retargets from pausing between surfaces.
easing.bezierCurve: Theme.variantEnterCurve
}
}
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
// One animation drives all four coordinates, so queue one coalesced state update
// per progress tick instead of reacting independently to each derived property.
onMorphProgressChanged: _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 spatial motion with both travel and shape change. Never shorten the
// configured enter duration; cap long sweeps so hover switching stays responsive.
const base = Math.max(0, Theme.variantDuration(root.animationDuration, true));
const travel = Math.hypot(root.alignedX - morphSeedX, root.renderedAlignedY - morphSeedY);
const resize = Math.hypot(root.alignedWidth - morphSeedW, root.renderedAlignedHeight - morphSeedH);
const spatialDistance = travel + resize * 0.35;
_morphTravelDuration = Math.round(Math.min(base * 1.6, base + spatialDistance * 0.15));
morphProgress = 0;
morphTravelEnabled = true;
Qt.callLater(() => {
if (root.shouldBeVisible)
root.morphProgress = 1;
});
}
function _freezeMorphTravel() {
const x = pubBodyX;
const y = pubBodyY;
const w = pubBodyW;
const h = pubBodyH;
// A third hover can supersede a morph before it settles. Freeze the outgoing
// content at the live rectangle so it fades in place while the next surface
// inherits exactly the same geometry.
morphTravelEnabled = false;
morphSeedX = x;
morphSeedY = y;
morphSeedW = w;
morphSeedH = h;
morphProgress = 0;
morphTravelEnabled = true;
_syncPopoutBody();
}
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: {
if (!root.usesConnectedSurfaceChrome)
return triggerX;
@@ -761,6 +934,15 @@ Item {
visible: false
color: "transparent"
PopoutHoverDismiss {
id: hoverDismissController
anchors.fill: parent
dismissEnabled: root.hoverDismissEnabled
dismissSuspended: root.hoverDismissSuspended
surfaceVisible: root.shouldBeVisible
onDismissRequested: root.closeFromHoverDismiss()
}
WindowBlur {
id: popoutBlur
targetWindow: contentWindow
@@ -842,10 +1024,11 @@ Item {
Item {
id: contentContainer
x: root.alignedX
y: root.renderedAlignedY
width: root.alignedWidth
height: root.renderedAlignedHeight
// Follow the morphing body bounds during transition.
x: root.morphTravelEnabled ? root.pubBodyX : root.alignedX
y: root.morphTravelEnabled ? root.pubBodyY : root.renderedAlignedY
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 barBottom: effectiveBarPosition === SettingsData.Position.Bottom
@@ -914,6 +1097,11 @@ Item {
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
PopoutHoverBodyTracker {
controller: hoverDismissController
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
}
QtObject {
id: morph
property real openProgress: 0
@@ -941,7 +1129,8 @@ Item {
target: root
function onShouldBeVisibleChanged() {
root._captureChromeAnimTravel();
morph.openProgress = root.shouldBeVisible ? 1 : 0;
// Skip reverse emerge animation during a superseded close.
morph.openProgress = (root.shouldBeVisible || root._supersededClose) ? 1 : 0;
}
}
@@ -1067,23 +1256,27 @@ Item {
property bool _renderActive: Theme.isDirectionalEffect || shouldBeVisible
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
scale: contentContainer.scaleValue
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)
layer.enabled: _animating || (!Theme.isDirectionalEffect && publishedOpacity < 1)
layer.enabled: _animating || (_fadeWithOpacity && publishedOpacity < 1)
layer.smooth: false
layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
enabled: contentWrapper._fadeWithOpacity
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.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
onRunningChanged: {
@@ -1095,9 +1288,9 @@ Item {
}
Behavior on publishedOpacity {
enabled: !Theme.isDirectionalEffect
enabled: contentWrapper._fadeWithOpacity
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.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
@@ -5,6 +5,7 @@ import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -35,6 +36,22 @@ Item {
property bool shouldBeVisible: false
property bool isClosing: false
property bool animationsEnabled: true
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissController.cancelPending();
}
function closeFromHoverDismiss() {
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return;
if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss();
else
close();
}
property var customKeyboardFocus: null
property bool backgroundInteractive: true
property bool contentHandlesKeys: false
@@ -585,6 +602,17 @@ Item {
color: "transparent"
readonly property bool closeVisualActive: root.shouldBeVisible || root.isClosing
PopoutHoverDismiss {
id: hoverDismissController
anchors.fill: parent
dismissEnabled: root.hoverDismissEnabled
dismissSuspended: root.hoverDismissSuspended
surfaceVisible: root.shouldBeVisible
globalOffsetX: root._surfaceMarginLeft
globalOffsetY: root._fullHeight ? 0 : root._surfaceMarginTop
onDismissRequested: root.closeFromHoverDismiss()
}
WindowBlur {
id: popoutBlur
targetWindow: contentWindow
@@ -702,6 +730,11 @@ Item {
readonly property real computedScaleCollapsed: root.animationScaleCollapsed
PopoutHoverBodyTracker {
controller: hoverDismissController
trackingEnabled: root.hoverDismissEnabled && root.shouldBeVisible
}
// openProgress: 0 = closed (at offset, scaleCollapsed), 1 = open (at 0, scale 1).
QtObject {
id: morph
+39 -2
View File
@@ -13,6 +13,8 @@ PanelWindow {
WlrLayershell.namespace: layerNamespace
property bool isVisible: false
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
property var targetScreen: null
property var modelData: null
property bool triggerUsesOverlayLayer: false
@@ -25,6 +27,7 @@ PanelWindow {
property real edgeGap: 0
property string slideEdge: "right"
readonly property bool slideFromLeft: slideEdge === "left"
readonly property real surfaceOriginX: slideFromLeft ? 0 : Math.max(0, (modelData?.width ?? width) - width)
property Component content: null
property string title: ""
property alias container: contentContainer
@@ -46,6 +49,27 @@ PanelWindow {
isVisible = false;
}
function hideFromHoverDismiss() {
if (hoverDismissSuspended)
return;
hoverDismissEnabled = false;
slideAnimation.duration = Math.round(Theme.expressiveDurations.expressiveDefaultSpatial);
hide();
}
function cancelHoverDismiss() {
hoverDismissTracker.cancelPending();
}
function containsGlobalPoint(gx, gy) {
if (!isVisible || !modelData)
return false;
const padding = 24;
const topLeft = slideContainer.mapToItem(null, 0, 0);
const globalX = surfaceOriginX + topLeft.x;
return gx >= globalX - padding && gx < globalX + slideContainer.width + padding && gy >= topLeft.y - padding && gy < topLeft.y + slideContainer.height + padding;
}
function toggle() {
if (isVisible) {
hide();
@@ -67,6 +91,17 @@ PanelWindow {
color: "transparent"
HoverDismissTracker {
id: hoverDismissTracker
parent: root.contentItem
enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.isVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
onDismissRequested: root.hideFromHoverDismiss()
onHoverMoved: (sceneX, sceneY) => PopoutManager.updateHoverCursor(root.surfaceOriginX + sceneX, sceneY)
}
readonly property bool slideoutBlurActive: root.visible && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
WlrLayershell.layer: (!suppressOverlayLayer && (triggerUsesOverlayLayer || CompositorService.framePeerSurfacesUseOverlayForScreen(modelData))) ? WlrLayershell.Overlay : WlrLayershell.Top
@@ -117,8 +152,10 @@ PanelWindow {
easing.type: Easing.OutCubic
onRunningChanged: {
if (!running && !root.isVisible) {
root.mappedVisible = false;
if (!running) {
if (!root.isVisible)
root.mappedVisible = false;
slideAnimation.duration = 450;
}
}
}
@@ -0,0 +1,30 @@
pragma ComponentBehavior: Bound
import QtQuick
HoverHandler {
id: root
property var shouldDismiss: null
signal dismissRequested
// Emitted on every hover move; passive to avoid blocking overlapping MouseAreas
signal hoverMoved(real gx, real gy)
onPointChanged: {
if (!enabled || !hovered)
return;
const gp = parent.mapToItem(null, point.position.x, point.position.y);
hoverMoved(gp.x, gp.y);
}
onHoveredChanged: {
if (hovered || !enabled)
return;
if (typeof shouldDismiss === "function" && !shouldDismiss())
return;
dismissRequested();
}
function cancelPending() {
}
}
@@ -0,0 +1,25 @@
pragma ComponentBehavior: Bound
import QtQuick
HoverHandler {
id: root
required property var controller
property bool trackingEnabled: false
enabled: trackingEnabled
onTrackingEnabledChanged: {
if (!trackingEnabled)
controller.updateBodyHover(false);
}
onHoveredChanged: controller.updateBodyHover(hovered)
onPointChanged: {
if (!hovered)
return;
const gp = parent.mapToItem(null, point.position.x, point.position.y);
controller.updateCursor(gp.x, gp.y);
}
}
+75
View File
@@ -0,0 +1,75 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
Item {
id: root
required property bool dismissEnabled
required property bool dismissSuspended
required property bool surfaceVisible
property int graceInterval: 150
property bool bodyHovered: false
property real globalOffsetX: 0
property real globalOffsetY: 0
signal dismissRequested
function cancelPending() {
graceTimer.stop();
hoverTracker.cancelPending();
}
function updateBodyHover(over) {
bodyHovered = over;
if (over) {
graceTimer.stop();
} else if (dismissEnabled && !dismissSuspended && surfaceVisible) {
graceTimer.restart();
}
}
function updateCursor(sceneX, sceneY) {
PopoutManager.updateHoverCursor(sceneX + globalOffsetX, sceneY + globalOffsetY);
}
onDismissEnabledChanged: {
if (!dismissEnabled)
cancelPending();
}
onDismissSuspendedChanged: {
if (dismissSuspended)
graceTimer.stop();
else if (dismissEnabled && surfaceVisible && !bodyHovered)
graceTimer.restart();
}
onSurfaceVisibleChanged: {
if (!surfaceVisible)
cancelPending();
}
Timer {
id: graceTimer
interval: root.graceInterval
repeat: false
onTriggered: {
if (!root.dismissEnabled || root.dismissSuspended || !root.surfaceVisible || root.bodyHovered)
return;
if (PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY))
return;
root.dismissRequested();
}
}
HoverDismissTracker {
id: hoverTracker
enabled: root.dismissEnabled && !root.dismissSuspended && root.surfaceVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
onDismissRequested: root.dismissRequested()
onHoverMoved: (gx, gy) => root.updateCursor(gx, gy)
}
}