1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-27 05:25:19 -04:00

feat(popouts): hover & settings cleanup

This commit is contained in:
purian23
2026-06-27 01:59:45 -04:00
parent 7979fb2b0e
commit 601d4104a3
11 changed files with 200 additions and 73 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/golangci/golangci-lint - repo: https://github.com/golangci/golangci-lint
rev: v2.10.1 rev: v2.12.2
hooks: hooks:
- id: golangci-lint-fmt - id: golangci-lint-fmt
require_serial: true require_serial: true
+1 -1
View File
@@ -191,7 +191,7 @@ Singleton {
const p = getActivePopout(screen); const p = getActivePopout(screen);
if (!p || !_isPopoutPresented(p)) if (!p || !_isPopoutPresented(p))
return false; return false;
return p.hoverDismissEnabled === false; return p.hoverDismissEnabled === false || p.hoverDismissSuspended === true;
} }
function isCurrentPopout(popout, screenName) { function isCurrentPopout(popout, screenName) {
@@ -98,6 +98,7 @@ DankPopout {
property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen
backgroundInteractive: !anyModalOpen backgroundInteractive: !anyModalOpen
hoverDismissSuspended: editMode || anyModalOpen
onCredentialsPromptOpenChanged: { onCredentialsPromptOpenChanged: {
if (credentialsPromptOpen && shouldBeVisible) if (credentialsPromptOpen && shouldBeVisible)
+39 -4
View File
@@ -95,6 +95,14 @@ Item {
enableFrameInsetAnimation.schedule(); enableFrameInsetAnimation.schedule();
} }
Connections {
target: topBarContent._hasBarWindow ? topBarContent.barWindow.axis : null
function onEdgeChanged() {
topBarContent.resetHoverForBarGeometryChange();
}
}
Behavior on anchors.leftMargin { Behavior on anchors.leftMargin {
enabled: _animateFrameInsets && _usesFrameBarChrome enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation { NumberAnimation {
@@ -401,6 +409,19 @@ Item {
property var _pendingHoverHit: null property var _pendingHoverHit: null
property string _pendingHoverTrigger: "" property string _pendingHoverTrigger: ""
function resetHoverForBarGeometryChange() {
_cancelPendingHover();
_hoverCloseTimer.stop();
_pendingPopoutOpenSpec = null;
const activePopout = PopoutManager.getActivePopout(barWindow?.screen);
const hasTransientSurface = activeHoverTrigger !== "" || activePopout?.hoverDismissEnabled === true;
if (hasTransientSurface && !PopoutManager.isActivePopoutPinned(barWindow?.screen))
closeHoverSurfaces();
else
activeHoverTrigger = "";
}
Timer { Timer {
id: _hoverIntentTimer id: _hoverIntentTimer
interval: topBarContent.hoverPopoutDelay interval: topBarContent.hoverPopoutDelay
@@ -525,6 +546,9 @@ Item {
} }
} }
if (typeof popout.prepareForTrigger === "function")
popout.prepareForTrigger(spec.triggerSource, mode);
if (spec.prepare) if (spec.prepare)
spec.prepare(popout); spec.prepare(popout);
@@ -859,6 +883,10 @@ Item {
const inst = _notepadWidgetForScreen()?.notepadInstance; const inst = _notepadWidgetForScreen()?.notepadInstance;
return inst?.isVisible ?? false; return inst?.isVisible ?? false;
} }
if (activeHoverTrigger.startsWith("tray-")) {
const screenName = barWindow.screen?.name;
return !!(screenName && TrayMenuManager.activeTrayMenus[screenName]);
}
const popout = PopoutManager.getActivePopout(barWindow?.screen); const popout = PopoutManager.getActivePopout(barWindow?.screen);
if (!popout) if (!popout)
return false; return false;
@@ -1116,12 +1144,19 @@ Item {
if (!PopoutManager.cursorOverBar(_lastHoverGlobalX, _lastHoverGlobalY)) if (!PopoutManager.cursorOverBar(_lastHoverGlobalX, _lastHoverGlobalY))
return; return;
const activePopout = PopoutManager.getActivePopout(barWindow?.screen);
const targetLoader = _loaderForWidgetId(hit.widgetId);
const targetPopout = _resolvePopoutFromLoader(targetLoader);
const managerOwnsTransition = !!(activePopout && targetPopout);
// A different trigger backed by the same already-open popout swaps tab/position // 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. // in place. PopoutManager also owns handoff between loaded popouts, so only
// pre-close special/unmanaged surfaces here.
if (triggerKey !== activeHoverTrigger && activeHoverTrigger !== "" && !_hitTargetsActivePopout(hit)) { if (triggerKey !== activeHoverTrigger && activeHoverTrigger !== "" && !_hitTargetsActivePopout(hit)) {
// Mark popout as superseded to fade in-place before closing. if (!managerOwnsTransition) {
_beginSupersededCloseForActive(); _beginSupersededCloseForActive();
closeHoverSurfaces(); closeHoverSurfaces();
}
} }
if (!openHoverPopoutForHit(hit)) { if (!openHoverPopoutForHit(hit)) {
@@ -26,6 +26,19 @@ DankPopout {
open(); open();
} }
function prepareForTrigger(triggerSource) {
switch (triggerSource) {
case "memory":
DgopService.setSortBy("memory");
break;
case "cpu":
case "cpu_temp":
case "gpu_temp":
DgopService.setSortBy("cpu");
break;
}
}
popupWidth: Math.round(Theme.fontSizeMedium * 46) popupWidth: Math.round(Theme.fontSizeMedium * 46)
popupHeight: Math.round(Theme.fontSizeMedium * 39) popupHeight: Math.round(Theme.fontSizeMedium * 39)
triggerWidth: 55 triggerWidth: 55
+44 -44
View File
@@ -1256,6 +1256,50 @@ Item {
} }
} }
SettingsToggleCard {
iconName: "touch_app"
title: I18n.tr("Hover Popouts")
description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.")
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
enabled: !(selectedBarConfig?.clickThrough ?? false)
opacity: (selectedBarConfig?.clickThrough ?? false) ? 0.5 : 1.0
checked: selectedBarConfig?.hoverPopouts ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
hoverPopouts: checked
})
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 {
iconName: "fit_screen" iconName: "fit_screen"
title: I18n.tr("Maximize Detection") title: I18n.tr("Maximize Detection")
@@ -1800,50 +1844,6 @@ Item {
} }
} }
SettingsToggleCard {
iconName: "touch_app"
title: I18n.tr("Hover Popouts")
description: I18n.tr("Open widget popouts by hovering over the bar. Moving to another widget switches the popout.")
visible: !dankBarTab.appearanceOnly && selectedBarConfig?.enabled
enabled: !(selectedBarConfig?.clickThrough ?? false)
opacity: (selectedBarConfig?.clickThrough ?? false) ? 0.5 : 1.0
checked: selectedBarConfig?.hoverPopouts ?? false
onToggled: checked => SettingsData.updateBarConfig(selectedBarId, {
hoverPopouts: checked
})
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 {
iconName: "mouse" iconName: "mouse"
title: I18n.tr("Scroll Wheel") title: I18n.tr("Scroll Wheel")
+1 -1
View File
@@ -359,7 +359,7 @@ Item {
tags: ["background", "color", "fill", "fit", "custom"] tags: ["background", "color", "fill", "fit", "custom"]
settingKey: "wallpaperBackgroundColorMode" settingKey: "wallpaperBackgroundColorMode"
text: I18n.tr("Background Color") text: I18n.tr("Background Color")
description: I18n.tr("Color shown for areas not covered by wallpaper (e.g. Fit or Pad modes)") description: I18n.tr("Color shown for areas not covered by wallpaper")
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#") visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
dropdownWidth: 220 dropdownWidth: 220
options: [ options: [
+4
View File
@@ -25,6 +25,7 @@ Item {
property bool suspendShadowWhileResizing: false property bool suspendShadowWhileResizing: false
property bool shouldBeVisible: false property bool shouldBeVisible: false
property bool hoverDismissEnabled: false property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
property var customKeyboardFocus: null property var customKeyboardFocus: null
property bool backgroundInteractive: true property bool backgroundInteractive: true
property bool contentHandlesKeys: false property bool contentHandlesKeys: false
@@ -187,6 +188,8 @@ Item {
} }
function closeFromHoverDismiss() { function closeFromHoverDismiss() {
if (hoverDismissSuspended)
return;
hoverDismissEnabled = false; hoverDismissEnabled = false;
// Enable animations using standard Theme-bound popout motion to preserve bindings. // Enable animations using standard Theme-bound popout motion to preserve bindings.
if (impl.item) if (impl.item)
@@ -307,6 +310,7 @@ Item {
it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition); it.effectiveBarPosition = Qt.binding(() => root.effectiveBarPosition);
it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap); it.effectiveBarBottomGap = Qt.binding(() => root.effectiveBarBottomGap);
it.hoverDismissEnabled = Qt.binding(() => root.hoverDismissEnabled); it.hoverDismissEnabled = Qt.binding(() => root.hoverDismissEnabled);
it.hoverDismissSuspended = Qt.binding(() => root.hoverDismissSuspended);
it.shouldBeVisible = root.shouldBeVisible; it.shouldBeVisible = root.shouldBeVisible;
if (root._primeContent && typeof it.primeContent === "function") if (root._primeContent && typeof it.primeContent === "function")
+51 -17
View File
@@ -409,13 +409,15 @@ Item {
property bool animationsEnabled: true property bool animationsEnabled: true
property bool hoverDismissEnabled: false property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() { function cancelHoverDismiss() {
hoverDismissTracker.cancelPending(); hoverDismissTracker.cancelPending();
_hoverDismissGrace.stop();
} }
function closeFromHoverDismiss() { function closeFromHoverDismiss() {
if (isClosing || !shouldBeVisible) if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return; return;
if (popoutHandle?.closeFromHoverDismiss) if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss(); popoutHandle.closeFromHoverDismiss();
@@ -479,7 +481,10 @@ Item {
} }
function close() { function close() {
_endMorphTravel(); if (_supersededClose && morphTravelEnabled)
_freezeMorphTravel();
else
_endMorphTravel();
isClosing = true; isClosing = true;
shouldBeVisible = false; shouldBeVisible = false;
_primeContent = false; _primeContent = false;
@@ -518,6 +523,7 @@ Item {
onTriggered: { onTriggered: {
if (!shouldBeVisible) { if (!shouldBeVisible) {
contentWindow.visible = false; contentWindow.visible = false;
root._endMorphTravel();
isClosing = false; isClosing = false;
PopoutManager.hidePopout(popoutHandle); PopoutManager.hidePopout(popoutHandle);
popoutClosed(); popoutClosed();
@@ -682,8 +688,9 @@ Item {
NumberAnimation { NumberAnimation {
duration: root._morphTravelDuration duration: root._morphTravelDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
// Emphasized curve for fluid morph travel. // M3 Expressive spatial motion starts with momentum and settles gently,
easing.bezierCurve: Theme.expressiveCurves.emphasized // which keeps rapid hover retargets from pausing between surfaces.
easing.bezierCurve: Theme.variantEnterCurve
} }
} }
@@ -692,10 +699,9 @@ Item {
readonly property real pubBodyW: morphSeedW + (alignedWidth - morphSeedW) * morphProgress readonly property real pubBodyW: morphSeedW + (alignedWidth - morphSeedW) * morphProgress
readonly property real pubBodyH: morphSeedH + (renderedAlignedHeight - morphSeedH) * morphProgress readonly property real pubBodyH: morphSeedH + (renderedAlignedHeight - morphSeedH) * morphProgress
onPubBodyXChanged: _queueBodySync() // One animation drives all four coordinates, so queue one coalesced state update
onPubBodyYChanged: _queueBodySync() // per progress tick instead of reacting independently to each derived property.
onPubBodyWChanged: _queueBodySync() onMorphProgressChanged: _queueBodySync()
onPubBodyHChanged: _queueBodySync()
function _beginMorphTravel() { function _beginMorphTravel() {
morphTravelEnabled = false; morphTravelEnabled = false;
@@ -716,12 +722,13 @@ Item {
morphSeedY = ConnectedModeState.popoutBodyY; morphSeedY = ConnectedModeState.popoutBodyY;
morphSeedW = w; morphSeedW = w;
morphSeedH = h; morphSeedH = h;
// Scale travel time with distance within ~[0.8x, 1.4x] of the popout duration: // Scale spatial motion with both travel and shape change. Never shorten the
// enough room for the emphasized curve to breathe (fluid, not abrupt), capped so // configured enter duration; cap long sweeps so hover switching stays responsive.
// long sweeps don't drag, and collapsing to 0 when popout animations are off. const base = Math.max(0, Theme.variantDuration(root.animationDuration, true));
const base = Math.max(0, root.animationDuration); const travel = Math.hypot(root.alignedX - morphSeedX, root.renderedAlignedY - morphSeedY);
const dist = Math.hypot(root.alignedX - morphSeedX, root.renderedAlignedY - morphSeedY); const resize = Math.hypot(root.alignedWidth - morphSeedW, root.renderedAlignedHeight - morphSeedH);
_morphTravelDuration = Math.round(Math.min(base * 1.4, base * 0.8 + dist * 0.16)); const spatialDistance = travel + resize * 0.35;
_morphTravelDuration = Math.round(Math.min(base * 1.6, base + spatialDistance * 0.15));
morphProgress = 0; morphProgress = 0;
morphTravelEnabled = true; morphTravelEnabled = true;
Qt.callLater(() => { Qt.callLater(() => {
@@ -730,6 +737,25 @@ Item {
}); });
} }
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() { function _endMorphTravel() {
morphTravelEnabled = false; morphTravelEnabled = false;
morphProgress = 1; morphProgress = 1;
@@ -856,16 +882,24 @@ Item {
_hoverOverBody = over; _hoverOverBody = over;
if (over) if (over)
_hoverDismissGrace.stop(); _hoverDismissGrace.stop();
else if (root.hoverDismissEnabled && root.shouldBeVisible) else if (root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible)
_hoverDismissGrace.restart(); _hoverDismissGrace.restart();
} }
onHoverDismissSuspendedChanged: {
if (hoverDismissSuspended) {
_hoverDismissGrace.stop();
} else if (hoverDismissEnabled && shouldBeVisible && !_hoverOverBody) {
_hoverDismissGrace.restart();
}
}
Timer { Timer {
id: _hoverDismissGrace id: _hoverDismissGrace
interval: 150 interval: 150
repeat: false repeat: false
onTriggered: { onTriggered: {
if (!root.hoverDismissEnabled || !root.shouldBeVisible) if (!root.hoverDismissEnabled || root.hoverDismissSuspended || !root.shouldBeVisible)
return; return;
if (root._hoverOverBody) if (root._hoverOverBody)
return; return;
@@ -907,7 +941,7 @@ Item {
HoverDismissTracker { HoverDismissTracker {
id: hoverDismissTracker id: hoverDismissTracker
anchors.fill: parent anchors.fill: parent
enabled: root.hoverDismissEnabled && root.shouldBeVisible enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible
shouldDismiss: function () { shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY); return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
} }
+14 -4
View File
@@ -37,13 +37,15 @@ Item {
property bool isClosing: false property bool isClosing: false
property bool animationsEnabled: true property bool animationsEnabled: true
property bool hoverDismissEnabled: false property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() { function cancelHoverDismiss() {
hoverDismissTracker.cancelPending(); hoverDismissTracker.cancelPending();
_hoverDismissGrace.stop();
} }
function closeFromHoverDismiss() { function closeFromHoverDismiss() {
if (isClosing || !shouldBeVisible) if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return; return;
if (popoutHandle?.closeFromHoverDismiss) if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss(); popoutHandle.closeFromHoverDismiss();
@@ -58,16 +60,24 @@ Item {
_hoverOverBody = over; _hoverOverBody = over;
if (over) if (over)
_hoverDismissGrace.stop(); _hoverDismissGrace.stop();
else if (root.hoverDismissEnabled && root.shouldBeVisible) else if (root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible)
_hoverDismissGrace.restart(); _hoverDismissGrace.restart();
} }
onHoverDismissSuspendedChanged: {
if (hoverDismissSuspended) {
_hoverDismissGrace.stop();
} else if (hoverDismissEnabled && shouldBeVisible && !_hoverOverBody) {
_hoverDismissGrace.restart();
}
}
Timer { Timer {
id: _hoverDismissGrace id: _hoverDismissGrace
interval: 150 interval: 150
repeat: false repeat: false
onTriggered: { onTriggered: {
if (!root.hoverDismissEnabled || !root.shouldBeVisible) if (!root.hoverDismissEnabled || root.hoverDismissSuspended || !root.shouldBeVisible)
return; return;
if (root._hoverOverBody) if (root._hoverOverBody)
return; return;
@@ -641,7 +651,7 @@ Item {
HoverDismissTracker { HoverDismissTracker {
id: hoverDismissTracker id: hoverDismissTracker
anchors.fill: parent anchors.fill: parent
enabled: root.hoverDismissEnabled && root.shouldBeVisible enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible
shouldDismiss: function () { shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY); return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
} }
@@ -362,7 +362,7 @@
"wallpaper" "wallpaper"
], ],
"icon": "wallpaper", "icon": "wallpaper",
"description": "Color shown for areas not covered by wallpaper (e.g. Fit or Pad modes)" "description": "Color shown for areas not covered by wallpaper"
}, },
{ {
"section": "selectedMonitor", "section": "selectedMonitor",
@@ -8139,6 +8139,36 @@
], ],
"icon": "monitor" "icon": "monitor"
}, },
{
"section": "frameLauncherEdgeHover",
"label": "Edge Hover Reveal",
"tabIndex": 33,
"category": "Frame",
"keywords": [
"app drawer",
"app menu",
"applications",
"border",
"connected",
"decoration",
"edge",
"emerge",
"frame",
"free",
"hover",
"hovering",
"launcher",
"open",
"panel",
"reveal",
"start menu",
"statusbar",
"taskbar",
"topbar",
"window"
],
"description": "Open the launcher by hovering the emerge edge (when free of bar and dock)"
},
{ {
"section": "frameEnable", "section": "frameEnable",
"label": "Enable Frame", "label": "Enable Frame",