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:
@@ -385,6 +385,36 @@ Item {
|
||||
property real _lastHoverGlobalY: 0
|
||||
|
||||
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() {
|
||||
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)
|
||||
return loader.item;
|
||||
|
||||
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]
|
||||
];
|
||||
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]];
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
if (loader === pairs[i][0] && pairs[i][1])
|
||||
return pairs[i][1];
|
||||
@@ -803,6 +822,13 @@ Item {
|
||||
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) {
|
||||
const instance = widgetItem.prepareNotepadInstance?.(widgetItem.notepadInstance) ?? widgetItem.notepadInstance;
|
||||
if (!instance || typeof instance.show !== "function")
|
||||
@@ -981,10 +1007,14 @@ Item {
|
||||
PopoutManager.updateHoverCursor(gx, gy);
|
||||
_syncHoverTriggerState();
|
||||
|
||||
// Ignore hover events when a popout is pinned open.
|
||||
if (PopoutManager.isActivePopoutPinned(barWindow?.screen))
|
||||
return;
|
||||
|
||||
const hit = findWidgetAtGlobalPoint(gx, gy);
|
||||
if (!hit) {
|
||||
if (!cursorOverHoverChain(gx, gy))
|
||||
closeHoverSurfaces();
|
||||
_cancelPendingHover();
|
||||
scheduleHoverClose(gx, gy);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1002,16 +1032,97 @@ Item {
|
||||
triggerKey = _dashTriggerSource(hit.section, 3);
|
||||
|
||||
if (!triggerKey) {
|
||||
if (!cursorOverHoverChain(gx, gy))
|
||||
closeHoverSurfaces();
|
||||
_cancelPendingHover();
|
||||
scheduleHoverClose(gx, gy);
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if (!openHoverPopoutForHit(hit)) {
|
||||
if (activeHoverTrigger !== "")
|
||||
@@ -1022,6 +1133,27 @@ Item {
|
||||
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: ({
|
||||
"cpuUsage": DgopService.dgopAvailable,
|
||||
"memUsage": DgopService.dgopAvailable,
|
||||
|
||||
@@ -1111,33 +1111,25 @@ PanelWindow {
|
||||
rightWidgetsModel: barWindow.rightWidgetsModel
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: hoverPopoutArea
|
||||
anchors.fill: parent
|
||||
z: 1
|
||||
hoverEnabled: barConfig?.hoverPopouts ?? false
|
||||
enabled: hoverPopoutArea.hoverEnabled && !barWindow.clickThroughEnabled
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
// Passive HoverHandler to track cursor without intercepting clicks or scroll events.
|
||||
HoverHandler {
|
||||
id: hoverPopoutHandler
|
||||
enabled: (barConfig?.hoverPopouts ?? false) && !barWindow.clickThroughEnabled
|
||||
|
||||
property real lastGlobalX: 0
|
||||
property real lastGlobalY: 0
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
const gp = mapToItem(null, mouse.x, mouse.y);
|
||||
onPointChanged: {
|
||||
const gp = barUnitInset.mapToItem(null, point.position.x, point.position.y);
|
||||
lastGlobalX = gp.x;
|
||||
lastGlobalY = gp.y;
|
||||
topBarContent.checkHoverPopout(gp.x, gp.y);
|
||||
}
|
||||
|
||||
onWheel: wheel => scrollArea.processWheel(wheel)
|
||||
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse)
|
||||
onHoveredChanged: {
|
||||
if (hovered)
|
||||
return;
|
||||
if (topBarContent.cursorOverHoverChain(lastGlobalX, lastGlobalY))
|
||||
return;
|
||||
topBarContent.closeHoverSurfaces();
|
||||
topBarContent.scheduleHoverClose(lastGlobalX, lastGlobalY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
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 {
|
||||
|
||||
@@ -357,6 +357,15 @@ Item {
|
||||
checked: SettingsData.frameLauncherArcExtender
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user