1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-28 22:12:10 -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
@@ -191,7 +191,7 @@ Singleton {
const p = getActivePopout(screen);
if (!p || !_isPopoutPresented(p))
return false;
return p.hoverDismissEnabled === false;
return p.hoverDismissEnabled === false || p.hoverDismissSuspended === true;
}
function isCurrentPopout(popout, screenName) {
@@ -98,6 +98,7 @@ DankPopout {
property bool anyModalOpen: credentialsPromptOpen || wifiPasswordModalOpen || polkitModalOpen || powerMenuOpen
backgroundInteractive: !anyModalOpen
hoverDismissSuspended: editMode || anyModalOpen
onCredentialsPromptOpenChanged: {
if (credentialsPromptOpen && shouldBeVisible)
+39 -4
View File
@@ -95,6 +95,14 @@ Item {
enableFrameInsetAnimation.schedule();
}
Connections {
target: topBarContent._hasBarWindow ? topBarContent.barWindow.axis : null
function onEdgeChanged() {
topBarContent.resetHoverForBarGeometryChange();
}
}
Behavior on anchors.leftMargin {
enabled: _animateFrameInsets && _usesFrameBarChrome
NumberAnimation {
@@ -401,6 +409,19 @@ Item {
property var _pendingHoverHit: null
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 {
id: _hoverIntentTimer
interval: topBarContent.hoverPopoutDelay
@@ -525,6 +546,9 @@ Item {
}
}
if (typeof popout.prepareForTrigger === "function")
popout.prepareForTrigger(spec.triggerSource, mode);
if (spec.prepare)
spec.prepare(popout);
@@ -859,6 +883,10 @@ Item {
const inst = _notepadWidgetForScreen()?.notepadInstance;
return inst?.isVisible ?? false;
}
if (activeHoverTrigger.startsWith("tray-")) {
const screenName = barWindow.screen?.name;
return !!(screenName && TrayMenuManager.activeTrayMenus[screenName]);
}
const popout = PopoutManager.getActivePopout(barWindow?.screen);
if (!popout)
return false;
@@ -1116,12 +1144,19 @@ Item {
if (!PopoutManager.cursorOverBar(_lastHoverGlobalX, _lastHoverGlobalY))
return;
const activePopout = PopoutManager.getActivePopout(barWindow?.screen);
const targetLoader = _loaderForWidgetId(hit.widgetId);
const targetPopout = _resolvePopoutFromLoader(targetLoader);
const managerOwnsTransition = !!(activePopout && targetPopout);
// A different trigger backed by the same already-open popout swaps tab/position
// in place (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)) {
// Mark popout as superseded to fade in-place before closing.
_beginSupersededCloseForActive();
closeHoverSurfaces();
if (!managerOwnsTransition) {
_beginSupersededCloseForActive();
closeHoverSurfaces();
}
}
if (!openHoverPopoutForHit(hit)) {
@@ -26,6 +26,19 @@ DankPopout {
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)
popupHeight: Math.round(Theme.fontSizeMedium * 39)
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 {
iconName: "fit_screen"
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 {
iconName: "mouse"
title: I18n.tr("Scroll Wheel")
+1 -1
View File
@@ -359,7 +359,7 @@ Item {
tags: ["background", "color", "fill", "fit", "custom"]
settingKey: "wallpaperBackgroundColorMode"
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("#")
dropdownWidth: 220
options: [
+4
View File
@@ -25,6 +25,7 @@ Item {
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
@@ -187,6 +188,8 @@ Item {
}
function closeFromHoverDismiss() {
if (hoverDismissSuspended)
return;
hoverDismissEnabled = false;
// Enable animations using standard Theme-bound popout motion to preserve bindings.
if (impl.item)
@@ -307,6 +310,7 @@ Item {
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")
+51 -17
View File
@@ -409,13 +409,15 @@ Item {
property bool animationsEnabled: true
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissTracker.cancelPending();
_hoverDismissGrace.stop();
}
function closeFromHoverDismiss() {
if (isClosing || !shouldBeVisible)
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return;
if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss();
@@ -479,7 +481,10 @@ Item {
}
function close() {
_endMorphTravel();
if (_supersededClose && morphTravelEnabled)
_freezeMorphTravel();
else
_endMorphTravel();
isClosing = true;
shouldBeVisible = false;
_primeContent = false;
@@ -518,6 +523,7 @@ Item {
onTriggered: {
if (!shouldBeVisible) {
contentWindow.visible = false;
root._endMorphTravel();
isClosing = false;
PopoutManager.hidePopout(popoutHandle);
popoutClosed();
@@ -682,8 +688,9 @@ Item {
NumberAnimation {
duration: root._morphTravelDuration
easing.type: Easing.BezierSpline
// Emphasized curve for fluid morph travel.
easing.bezierCurve: Theme.expressiveCurves.emphasized
// M3 Expressive spatial motion starts with momentum and settles gently,
// 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 pubBodyH: morphSeedH + (renderedAlignedHeight - morphSeedH) * morphProgress
onPubBodyXChanged: _queueBodySync()
onPubBodyYChanged: _queueBodySync()
onPubBodyWChanged: _queueBodySync()
onPubBodyHChanged: _queueBodySync()
// 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;
@@ -716,12 +722,13 @@ Item {
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));
// 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(() => {
@@ -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() {
morphTravelEnabled = false;
morphProgress = 1;
@@ -856,16 +882,24 @@ Item {
_hoverOverBody = over;
if (over)
_hoverDismissGrace.stop();
else if (root.hoverDismissEnabled && root.shouldBeVisible)
else if (root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible)
_hoverDismissGrace.restart();
}
onHoverDismissSuspendedChanged: {
if (hoverDismissSuspended) {
_hoverDismissGrace.stop();
} else if (hoverDismissEnabled && shouldBeVisible && !_hoverOverBody) {
_hoverDismissGrace.restart();
}
}
Timer {
id: _hoverDismissGrace
interval: 150
repeat: false
onTriggered: {
if (!root.hoverDismissEnabled || !root.shouldBeVisible)
if (!root.hoverDismissEnabled || root.hoverDismissSuspended || !root.shouldBeVisible)
return;
if (root._hoverOverBody)
return;
@@ -907,7 +941,7 @@ Item {
HoverDismissTracker {
id: hoverDismissTracker
anchors.fill: parent
enabled: root.hoverDismissEnabled && root.shouldBeVisible
enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
+14 -4
View File
@@ -37,13 +37,15 @@ Item {
property bool isClosing: false
property bool animationsEnabled: true
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
function cancelHoverDismiss() {
hoverDismissTracker.cancelPending();
_hoverDismissGrace.stop();
}
function closeFromHoverDismiss() {
if (isClosing || !shouldBeVisible)
if (hoverDismissSuspended || isClosing || !shouldBeVisible)
return;
if (popoutHandle?.closeFromHoverDismiss)
popoutHandle.closeFromHoverDismiss();
@@ -58,16 +60,24 @@ Item {
_hoverOverBody = over;
if (over)
_hoverDismissGrace.stop();
else if (root.hoverDismissEnabled && root.shouldBeVisible)
else if (root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible)
_hoverDismissGrace.restart();
}
onHoverDismissSuspendedChanged: {
if (hoverDismissSuspended) {
_hoverDismissGrace.stop();
} else if (hoverDismissEnabled && shouldBeVisible && !_hoverOverBody) {
_hoverDismissGrace.restart();
}
}
Timer {
id: _hoverDismissGrace
interval: 150
repeat: false
onTriggered: {
if (!root.hoverDismissEnabled || !root.shouldBeVisible)
if (!root.hoverDismissEnabled || root.hoverDismissSuspended || !root.shouldBeVisible)
return;
if (root._hoverOverBody)
return;
@@ -641,7 +651,7 @@ Item {
HoverDismissTracker {
id: hoverDismissTracker
anchors.fill: parent
enabled: root.hoverDismissEnabled && root.shouldBeVisible
enabled: root.hoverDismissEnabled && !root.hoverDismissSuspended && root.shouldBeVisible
shouldDismiss: function () {
return !PopoutManager.cursorOverBar(PopoutManager.hoverCursorGlobalX, PopoutManager.hoverCursorGlobalY);
}
@@ -362,7 +362,7 @@
"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",
@@ -8139,6 +8139,36 @@
],
"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",
"label": "Enable Frame",