1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18:28 -04:00

fix(Hover): refactor & update hover tracking w/context menus

Fixes #2737
This commit is contained in:
purian23
2026-07-04 00:08:58 -04:00
parent 2861cc89c6
commit c554d973ef
30 changed files with 297 additions and 27 deletions
+29 -7
View File
@@ -79,12 +79,6 @@ Singleton {
function _closePopout(popout) {
try {
if (popout?.hoverDismissEnabled) {
if (typeof popout.closeFromHoverDismiss === "function") {
popout.closeFromHoverDismiss();
return;
}
}
if (popout.hoverDismissEnabled !== undefined)
popout.hoverDismissEnabled = false;
switch (true) {
@@ -104,6 +98,20 @@ Singleton {
}
}
function _dismissPopoutFromHover(popout) {
try {
if (!popout || popout.hoverDismissEnabled !== true)
return;
if (typeof popout.closeFromHoverDismiss === "function") {
popout.closeFromHoverDismiss();
return;
}
_closePopout(popout);
} catch (e) {
return;
}
}
function _isStale(popout) {
try {
if (!popout || !("shouldBeVisible" in popout))
@@ -174,6 +182,19 @@ Singleton {
_closePopout(popout);
}
function dismissHoverPopoutForScreen(screen) {
if (!screen)
return;
const screenName = screen.name;
const popout = currentPopoutsByScreen[screenName];
if (!popout || _isStale(popout)) {
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
return;
}
_dismissPopoutFromHover(popout);
}
function cancelHoverDismiss(screen) {
const popout = getActivePopout(screen);
if (popout?.cancelHoverDismiss)
@@ -191,7 +212,8 @@ Singleton {
const p = getActivePopout(screen);
if (!p || !_isPopoutPresented(p))
return false;
return p.hoverDismissEnabled === false || p.hoverDismissSuspended === true;
const dismissSuspended = p.effectiveHoverDismissSuspended ?? p.hoverDismissSuspended;
return p.hoverDismissEnabled === false || dismissSuspended === true;
}
function isCurrentPopout(popout, screenName) {
@@ -8,6 +8,7 @@ Item {
id: clipboardContent
required property var modal
property var transientSurfaceTracker: null
property alias searchField: searchField
property alias clipboardListView: clipboardListView
@@ -66,7 +67,7 @@ Item {
contextMenu.hide();
}
readonly property bool contextMenuActive: contextMenu.openState
readonly property bool contextMenuActive: contextMenu.renderActive
anchors.fill: parent
@@ -74,6 +75,7 @@ Item {
id: contextMenu
modal: clipboardContent.modal
parentHandler: clipboardContent
transientSurfaceTracker: clipboardContent.transientSurfaceTracker
}
Column {
@@ -23,6 +23,8 @@ Item {
property real anchorY: 0
property bool openState: false
property bool renderActive: false
property var transientSurfaceTracker: null
readonly property alias contextWindow: menuWindow
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
readonly property bool hasPinnedDuplicate: !!entry && !entry.pinned && ClipboardService.getPinnedEntryByHash(entry.hash) !== null
@@ -90,6 +92,18 @@ Item {
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
onRenderActiveChanged: transientSurfaceTracker?.setActive(root, renderActive, contextWindow)
Component.onDestruction: transientSurfaceTracker?.unregister(root)
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
root.hide();
}
}
TextMetrics {
id: menuTextMetrics
text: root.longestMenuText
@@ -9,6 +9,7 @@ FocusScope {
property var clearConfirmDialog: null
property var surfaceHost: null
property var transientSurfaceTracker: null
property string activeTab: "recents"
property bool showKeyboardHints: false
@@ -198,6 +199,7 @@ FocusScope {
id: historyContent
anchors.fill: parent
modal: root
transientSurfaceTracker: root.transientSurfaceTracker
}
}
@@ -130,6 +130,7 @@ DankModal {
content: Component {
ClipboardHistoryContent {
surfaceHost: clipboardHistoryModal
transientSurfaceTracker: clipboardHistoryModal.transientSurfaceTracker
clearConfirmDialog: clearConfirmDialog
onCloseRequested: clipboardHistoryModal.hide()
onInstantCloseRequested: clipboardHistoryModal.instantHide()
@@ -141,6 +141,7 @@ DankPopout {
LayoutMirroring.childrenInherit: true
surfaceHost: root
transientSurfaceTracker: root.transientSurfaceTracker
clearConfirmDialog: clearConfirmDialog
onCloseRequested: root.hide()
onInstantCloseRequested: root.hide()
+11 -1
View File
@@ -2,6 +2,7 @@ import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
@@ -38,6 +39,7 @@ Item {
property bool keepContentLoaded: false
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
readonly property alias transientSurfaceTracker: _transientSurfaceTracker
property bool useOverlayLayer: false
signal opened
@@ -47,6 +49,10 @@ Item {
readonly property var contentLoader: impl.item ? impl.item.contentLoader : null
readonly property alias modalFocusScope: _modalFocusScope
TransientSurfaceTracker {
id: _transientSurfaceTracker
}
FocusScope {
id: _modalFocusScope
objectName: "modalFocusScope"
@@ -56,7 +62,7 @@ Item {
// Hyprland OnDemand grab delivers keyboard focus to the modal content surface.
HyprlandFocusGrab {
windows: root.contentWindow ? [root.contentWindow] : []
windows: (root.contentWindow ? [root.contentWindow] : []).concat(root.transientSurfaceTracker?.focusWindows ?? [])
active: KeyboardFocus.wantsGrab(root.shouldHaveFocus, root.customKeyboardFocus)
property var restoreToplevel: null
@@ -79,11 +85,13 @@ Item {
}
function close() {
transientSurfaceTracker?.closeAll?.();
if (impl.item)
impl.item.close();
}
function instantClose() {
transientSurfaceTracker?.closeAll?.();
if (impl.item && typeof impl.item.instantClose === "function")
impl.item.instantClose();
}
@@ -175,6 +183,8 @@ Item {
Connections {
target: root
function onShouldBeVisibleChanged() {
if (!root.shouldBeVisible)
root.transientSurfaceTracker?.closeAll?.();
if (impl.item && impl.item.shouldBeVisible !== root.shouldBeVisible)
impl.item.shouldBeVisible = root.shouldBeVisible;
}
@@ -35,6 +35,10 @@ Item {
property real _frozenMotionX: 0
property real _frozenMotionY: 0
TransientSurfaceTracker {
id: transientSurfaces
}
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: contentWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
@@ -499,10 +503,18 @@ Item {
}
// Handles hover dismissal grace periods for edge-hover sessions w/cursor
readonly property bool _edgeRetractEnabled: (modalHandle && modalHandle.edgeHoverManaged === true) && spotlightOpen && !isClosing
readonly property bool _edgeRetractEnabled: (modalHandle && modalHandle.edgeHoverManaged === true) && spotlightOpen && !isClosing && !transientSurfaces.active
property bool _edgeBodyHover: false
property bool _edgeArmed: false
on_EdgeRetractEnabledChanged: {
if (!_edgeRetractEnabled) {
_edgeRetractGrace.stop();
} else if (_edgeArmed && !_edgeBodyHover) {
_edgeRetractGrace.restart();
}
}
Timer {
id: _edgeRetractGrace
interval: 150
@@ -534,9 +546,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
readonly property var contextMenuWindow: root.spotlightContent?.activeContextMenu?.contextWindow ?? null
readonly property bool contextMenuActive: root.spotlightContent?.activeContextMenu?.renderActive ?? false
windows: contextMenuActive && contextMenuWindow ? [contentWindow, contextMenuWindow] : [contentWindow]
windows: [contentWindow].concat(transientSurfaces.focusWindows)
active: root.useHyprlandFocusGrab && root.spotlightOpen
onCleared: {
@@ -869,6 +879,7 @@ Item {
sourceComponent: LauncherContent {
focus: true
parentModal: root
transientSurfaceTracker: transientSurfaces
}
onLoaded: {
@@ -26,6 +26,10 @@ Item {
property string _pendingMode: ""
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
TransientSurfaceTracker {
id: transientSurfaces
}
readonly property var effectiveScreen: launcherWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
@@ -228,9 +232,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
readonly property var contextMenuWindow: root.spotlightContent?.activeContextMenu?.contextWindow ?? null
readonly property bool contextMenuActive: root.spotlightContent?.activeContextMenu?.renderActive ?? false
windows: contextMenuActive && contextMenuWindow ? [launcherWindow, contextMenuWindow] : [launcherWindow]
windows: [launcherWindow].concat(transientSurfaces.focusWindows)
active: root.useHyprlandFocusGrab && root.keyboardActive
onCleared: {
if (spotlightOpen)
@@ -482,6 +484,7 @@ Item {
sourceComponent: SpotlightLauncherContent {
focus: true
parentModal: root
transientSurfaceTracker: transientSurfaces
}
onLoaded: {
@@ -27,6 +27,10 @@ Item {
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
TransientSurfaceTracker {
id: transientSurfaces
}
readonly property var effectiveScreen: launcherWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
@@ -260,9 +264,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
readonly property var contextMenuWindow: root.spotlightContent?.activeContextMenu?.contextWindow ?? null
readonly property bool contextMenuActive: root.spotlightContent?.activeContextMenu?.renderActive ?? false
windows: contextMenuActive && contextMenuWindow ? [launcherWindow, contextMenuWindow] : [launcherWindow]
windows: [launcherWindow].concat(transientSurfaces.focusWindows)
active: root.useHyprlandFocusGrab && root.keyboardActive
onCleared: {
@@ -530,6 +532,7 @@ Item {
sourceComponent: LauncherContent {
focus: true
parentModal: root
transientSurfaceTracker: transientSurfaces
}
onLoaded: {
@@ -18,6 +18,7 @@ FocusScope {
property alias resultsList: resultsList
property alias actionPanel: actionPanel
readonly property alias activeContextMenu: contextMenu
property var transientSurfaceTracker: null
property bool editMode: false
property var editingApp: null
@@ -43,7 +44,7 @@ FocusScope {
}
function closeTransientUi() {
contextMenu.hide();
transientSurfaceTracker?.closeAll?.();
actionPanel.hide();
root.enabled = true;
}
@@ -123,6 +124,7 @@ FocusScope {
controller: root.controller
searchField: root.searchField
parentHandler: root
transientSurfaceTracker: root.transientSurfaceTracker
onEditAppRequested: app => {
root.openEditMode(app);
@@ -779,6 +781,7 @@ FocusScope {
anchors.fill: parent
controller: root.controller
leadingSectionHeaderAtBottom: contentHolder.inverted
transientSurfaceTracker: root.transientSurfaceTracker
onItemRightClicked: (index, item, sceneX, sceneY) => {
if (item && contextMenu.hasContextMenuActions(item)) {
@@ -25,6 +25,7 @@ Item {
property real anchorY: 0
property bool openState: false
property bool renderActive: false
property var transientSurfaceTracker: null
readonly property alias contextWindow: menuWindow
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
@@ -49,6 +50,18 @@ Item {
signal hideRequested
signal editAppRequested(var app)
onRenderActiveChanged: transientSurfaceTracker?.setActive(root, renderActive, contextWindow)
Component.onDestruction: transientSurfaceTracker?.unregister(root)
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
root.hide();
}
}
TextMetrics {
id: menuTextMetrics
text: root.longestMenuText
@@ -15,6 +15,7 @@ Item {
property var _visualRows: []
property var _flatIndexToRowMap: ({})
property var _cumulativeHeights: []
property var transientSurfaceTracker: null
readonly property bool _bottomSectionHeaderActive: leadingSectionHeaderAtBottom && (controller?.sections?.length ?? 0) > 0
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
@@ -259,6 +260,7 @@ Item {
return root.controller?.canChangeSectionViewMode(delegateRoot.modelData?.sectionId ?? "") ?? false;
}
canCollapse: root.controller?.canCollapseSection(delegateRoot.modelData?.sectionId ?? "") ?? false
transientSurfaceTracker: root.transientSurfaceTracker
}
ResultItem {
@@ -447,6 +449,7 @@ Item {
return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
}
isSticky: true
transientSurfaceTracker: root.transientSurfaceTracker
}
}
@@ -472,6 +475,7 @@ Item {
canCollapse: root.controller?.canCollapseSection(section?.id ?? "") ?? false
isSticky: true
popupAbove: true
transientSurfaceTracker: root.transientSurfaceTracker
}
Item {
@@ -17,9 +17,21 @@ Rectangle {
property bool isSticky: false
property bool popupAbove: false
property Item popupAboveItem: null
property var transientSurfaceTracker: null
signal viewModeToggled
Component.onDestruction: transientSurfaceTracker?.unregister(root)
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
categoryPopup.close();
}
}
width: parent?.width ?? 200
height: 32
color: isSticky ? Theme.withAlpha(Theme.surfaceHover, 0) : (hoverArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceHover, 0))
@@ -142,6 +154,8 @@ Rectangle {
dim: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onVisibleChanged: root.transientSurfaceTracker?.setActive(root, visible, null)
background: Rectangle {
color: "transparent"
}
@@ -12,6 +12,7 @@ FocusScope {
property alias searchField: searchInput
property alias controller: searchController
readonly property alias activeContextMenu: contextMenu
property var transientSurfaceTracker: null
readonly property bool _hasQuery: searchInput.text.length > 0
readonly property real _searchBarH: 56
@@ -48,7 +49,7 @@ FocusScope {
}
function closeTransientUi() {
contextMenu.hide();
transientSurfaceTracker?.closeAll?.();
root.enabled = true;
}
@@ -220,6 +221,7 @@ FocusScope {
searchField: searchInput
parentHandler: root
allowEditActions: false
transientSurfaceTracker: root.transientSurfaceTracker
}
Connections {
+3
View File
@@ -210,12 +210,14 @@ DankModal {
NotificationHeader {
id: notificationHeader
keyboardController: modalKeyboardController
transientSurfaceTracker: notificationModal.transientSurfaceTracker
onCurrentTabChanged: notificationModal.currentTab = currentTab
Component.onCompleted: notificationModal.notificationHeaderRef = notificationHeader
}
NotificationSettings {
id: notificationSettings
transientSurfaceTracker: notificationModal.transientSurfaceTracker
expanded: notificationHeader.showSettings
}
@@ -225,6 +227,7 @@ DankModal {
height: parent.height - y
visible: notificationHeader.currentTab === 0
keyboardController: modalKeyboardController
transientSurfaceTracker: notificationModal.transientSurfaceTracker
Component.onCompleted: {
notificationModal.notificationListRef = notificationList;
if (modalKeyboardController) {
@@ -122,6 +122,7 @@ DankPopout {
anchors.fill: parent
parentModal: modalAdapter
viewModeContext: "appDrawer"
transientSurfaceTracker: appDrawerPopout.transientSurfaceTracker
}
Keys.onEscapePressed: event => {
@@ -608,7 +608,7 @@ Item {
function closeHoverSurfaces() {
_closeHoverNotepad();
activeHoverTrigger = "";
PopoutManager.closePopoutForScreen(barWindow?.screen);
PopoutManager.dismissHoverPopoutForScreen(barWindow?.screen);
TrayMenuManager.closeAllMenus();
}
@@ -17,6 +17,7 @@ DankListView {
property int swipingCardIndex: -1
property real swipingCardOffset: 0
property bool _stableHeightUpdatePending: false
property var transientSurfaceTracker: null
readonly property real shadowBlurPx: Theme.elevationEnabled ? ((Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined) ? Theme.elevationLevel1.blurPx : 4) : 0
readonly property real shadowHorizontalGutter: Theme.snap(Math.max(Theme.spacingS, Math.min(32, shadowBlurPx * 1.5 + 6)), 1)
readonly property real shadowVerticalGutter: Theme.snap(Math.max(Theme.spacingXS, 6), 1)
@@ -173,6 +174,7 @@ DankListView {
notificationGroup: modelData
keyboardNavigationActive: listView.keyboardActive
animateExpansion: listView.cardAnimateExpansion && listView.listInitialized
transientSurfaceTracker: listView.transientSurfaceTracker
opacity: {
const swipeAmount = Math.abs(delegateRoot.swipeOffset);
if (swipeAmount <= delegateRoot.swipeFadeStartOffset)
@@ -30,6 +30,7 @@ Rectangle {
property real swipingNotificationOffset: 0
property real listLevelAdjacentScaleInfluence: 1.0
property bool listLevelScaleAnimationsEnabled: true
property var transientSurfaceTracker: null
readonly property bool compactMode: SettingsData.notificationCompactMode
readonly property real cardPadding: compactMode ? Theme.notificationCardPaddingCompact : Theme.notificationCardPadding
@@ -63,6 +64,20 @@ Rectangle {
});
}
Component.onDestruction: {
transientSurfaceTracker?.unregister(root);
notificationCardContextMenu.close();
}
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
notificationCardContextMenu.close();
}
}
function expansionMotionDuration() {
if (isDescriptionToggleAnimation)
return descriptionExpanded ? Theme.notificationInlineExpandDuration : Theme.notificationInlineCollapseDuration;
@@ -1059,6 +1074,8 @@ Rectangle {
width: 220
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onVisibleChanged: root.transientSurfaceTracker?.setActive(root, visible, null)
background: Rectangle {
color: BlurService.enabled ? Theme.surfaceContainer : Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
@@ -283,11 +283,13 @@ DankPopout {
NotificationHeader {
id: notificationHeader
objectName: "notificationHeader"
transientSurfaceTracker: root.transientSurfaceTracker
onHeightChanged: notificationContent.cachedHeaderHeight = height
}
NotificationSettings {
id: notificationSettings
transientSurfaceTracker: root.transientSurfaceTracker
expanded: notificationHeader.showSettings
maxAllowedHeight: notificationContent.settingsMaxHeight
}
@@ -306,6 +308,7 @@ DankPopout {
anchors.topMargin: -(shadowVerticalGutter + delegateShadowGutter / 2)
anchors.bottomMargin: -(shadowVerticalGutter + delegateShadowGutter / 2)
cardAnimateExpansion: true
transientSurfaceTracker: root.transientSurfaceTracker
}
}
@@ -10,6 +10,19 @@ Item {
property bool showSettings: false
property int currentTab: 0
property bool showDndMenu: false
property var transientSurfaceTracker: null
onShowDndMenuChanged: transientSurfaceTracker?.setActive(root, showDndMenu, null)
Component.onDestruction: transientSurfaceTracker?.unregister(root)
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
root.showDndMenu = false;
}
}
onCurrentTabChanged: {
if (currentTab === 1 && !SettingsData.notificationHistoryEnabled)
@@ -7,6 +7,7 @@ Rectangle {
property bool expanded: false
property real maxAllowedHeight: 0
property var transientSurfaceTracker: null
readonly property real naturalContentHeight: contentColumn.height + Theme.spacingL * 2
width: parent.width
@@ -178,6 +179,8 @@ Rectangle {
}
DankDropdown {
id: lowTimeoutDropdown
transientSurfaceTracker: root.transientSurfaceTracker
text: I18n.tr("Low Priority")
description: I18n.tr("Timeout for low priority notifications")
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
@@ -193,6 +196,8 @@ Rectangle {
}
DankDropdown {
id: normalTimeoutDropdown
transientSurfaceTracker: root.transientSurfaceTracker
text: I18n.tr("Normal Priority")
description: I18n.tr("Timeout for normal priority notifications")
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
@@ -208,6 +213,8 @@ Rectangle {
}
DankDropdown {
id: criticalTimeoutDropdown
transientSurfaceTracker: root.transientSurfaceTracker
text: I18n.tr("Critical Priority")
description: I18n.tr("Timeout for critical priority notifications")
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
@@ -11,12 +11,25 @@ PanelWindow {
property string appName: ""
property string desktopEntry: ""
property point anchorPos: Qt.point(0, 0)
property var transientSurfaceTracker: null
signal muted
signal dismissRequested
readonly property bool isMuted: SettingsData.isAppMuted(appName, desktopEntry)
onVisibleChanged: transientSurfaceTracker?.setActive(root, visible, root)
Component.onDestruction: transientSurfaceTracker?.unregister(root)
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
root.closeMenu();
}
}
function showAt(x, y, targetScreen) {
if (targetScreen)
screen = targetScreen;
@@ -70,6 +70,11 @@ PanelWindow {
property real _storedTopMargin: 0
property real _storedBottomMargin: 0
property bool _inlineGeometryReady: false
readonly property bool contextMenuActive: transientSurfaces.active
TransientSurfaceTracker {
id: transientSurfaces
}
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
@@ -122,6 +127,7 @@ PanelWindow {
if (exiting || _isDestroying) {
return;
}
closeTransientUi();
exiting = true;
exitStarted();
popupChromeGeometryChanged();
@@ -135,6 +141,7 @@ PanelWindow {
if (_isDestroying) {
return;
}
closeTransientUi();
_isDestroying = true;
exiting = true;
visible = false;
@@ -147,6 +154,7 @@ PanelWindow {
return;
}
closeTransientUi();
_finalized = true;
_isDestroying = true;
exitWatchdog.stop();
@@ -155,6 +163,11 @@ PanelWindow {
win.exitFinished();
}
function closeTransientUi() {
transientSurfaces.closeAll();
popupContextMenuLoader.active = false;
}
visible: !_finalized
WlrLayershell.layer: {
const shouldUseOverlay = notificationData && (SettingsData.notificationOverlayEnabled || notificationData.urgency === NotificationUrgency.Critical);
@@ -274,6 +287,7 @@ PanelWindow {
}
onNotificationDataChanged: {
if (!_isDestroying) {
closeTransientUi();
if (SettingsData.notificationPopupPrivacyMode)
descriptionExpanded = false;
wrapperConn.target = win.notificationData || null;
@@ -286,6 +300,7 @@ PanelWindow {
}
}
Component.onDestruction: {
closeTransientUi();
_isDestroying = true;
exitWatchdog.stop();
if (notificationData && notificationData.timer) {
@@ -716,7 +731,7 @@ PanelWindow {
if (cardHoverHandler.hovered) {
if (notificationData.timer)
notificationData.timer.stop();
} else if (notificationData.popup && notificationData.timer) {
} else if (!win.contextMenuActive && notificationData.popup && notificationData.timer) {
notificationData.timer.restart();
}
}
@@ -1357,11 +1372,26 @@ PanelWindow {
interval: 160
repeat: false
onTriggered: {
if (notificationData && notificationData.timer && !exiting && !_isDestroying)
if (notificationData && notificationData.timer && !contextMenuActive && !exiting && !_isDestroying)
notificationData.timer.start();
}
}
Connections {
target: popupContextMenuLoader.item
ignoreUnknownSignals: true
function onVisibleChanged() {
if (!notificationData?.timer || exiting || _isDestroying)
return;
if (win.contextMenuActive) {
notificationData.timer.stop();
} else if (!cardHoverHandler.hovered && notificationData.popup) {
notificationData.timer.restart();
}
}
}
Timer {
id: exitWatchdog
@@ -1387,6 +1417,7 @@ PanelWindow {
active: false
sourceComponent: NotificationContextMenu {
transientSurfaceTracker: transientSurfaces
appName: notificationData?.appName ?? ""
desktopEntry: notificationData?.desktopEntry ?? ""
onMuted: {
@@ -12,10 +12,23 @@ Popup {
property int selectedIndex: -1
property bool keyboardNavigation: false
property var parentFocusItem: null
property var transientSurfaceTracker: null
signal menuClosed
signal processKilled
onVisibleChanged: transientSurfaceTracker?.setActive(root, visible, null)
Component.onDestruction: transientSurfaceTracker?.unregister(root)
Connections {
target: processContextMenu.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
processContextMenu.close();
}
}
readonly property var menuItems: [
{
text: I18n.tr("Copy PID"),
@@ -66,6 +66,7 @@ DankPopout {
ProcessContextMenu {
id: processContextMenu
transientSurfaceTracker: processListPopout.transientSurfaceTracker
}
content: Component {
+13
View File
@@ -54,11 +54,14 @@ Item {
property bool addHorizontalPadding: false
property string emptyText: ""
property bool usePopupTransparency: !checkParentDisablesTransparency()
property var transientSurfaceTracker: null
signal valueChanged(string value)
property bool menuOpen: false
onMenuOpenChanged: transientSurfaceTracker?.setActive(root, menuOpen, null)
function closeDropdownMenu() {
if (!root.menuOpen && !dropdownMenu.opened && !dropdownMenu.visible)
return;
@@ -113,10 +116,20 @@ Item {
implicitHeight: !showTrigger ? 0 : (compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM))
Component.onDestruction: {
transientSurfaceTracker?.unregister(root);
if (root.menuOpen || dropdownMenu.opened || dropdownMenu.visible)
dropdownMenu.close();
}
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
root.closeDropdownMenu();
}
}
Column {
id: labelColumn
+14 -3
View File
@@ -27,6 +27,8 @@ Item {
property bool hoverDismissEnabled: false
property bool hoverDismissSuspended: false
property var customKeyboardFocus: null
readonly property alias transientSurfaceTracker: _transientSurfaceTracker
readonly property bool effectiveHoverDismissSuspended: hoverDismissSuspended || (transientSurfaceTracker?.active ?? false)
property bool backgroundInteractive: true
property bool contentHandlesKeys: false
property bool fullHeightSurface: false
@@ -55,6 +57,10 @@ Item {
readonly property var backgroundWindow: impl.item ? impl.item.backgroundWindow : null
readonly property var contentWindow: impl.item ? impl.item.contentWindow : null
TransientSurfaceTracker {
id: _transientSurfaceTracker
}
// Hyprland OnDemand grab: whitelist popout surfaces and bars so dismiss clicks still land.
HyprlandFocusGrab {
windows: {
@@ -63,7 +69,8 @@ Item {
list.push(root.contentWindow);
if (root.backgroundWindow && root.backgroundWindow !== root.contentWindow)
list.push(root.backgroundWindow);
return list.concat(KeyboardFocus.barWindows);
const transientWindows = root.transientSurfaceTracker?.focusWindows ?? [];
return list.concat(transientWindows).concat(KeyboardFocus.barWindows);
}
active: KeyboardFocus.wantsGrab(root.shouldBeVisible, root.customKeyboardFocus)
@@ -170,6 +177,7 @@ Item {
function close() {
_pendingOpen = false;
_pendingOpenTimer.stop();
transientSurfaceTracker?.closeAll?.();
if (impl.item)
impl.item.close();
}
@@ -186,8 +194,9 @@ Item {
}
function closeFromHoverDismiss() {
if (hoverDismissSuspended)
if (effectiveHoverDismissSuspended)
return;
transientSurfaceTracker?.closeAll?.();
hoverDismissEnabled = false;
// Enable animations using standard Theme-bound popout motion to preserve bindings.
if (impl.item)
@@ -308,7 +317,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.hoverDismissSuspended = Qt.binding(() => root.effectiveHoverDismissSuspended);
it.shouldBeVisible = root.shouldBeVisible;
if (root._primeContent && typeof it.primeContent === "function")
@@ -332,6 +341,8 @@ Item {
Connections {
target: root
function onShouldBeVisibleChanged() {
if (!root.shouldBeVisible)
root.transientSurfaceTracker?.closeAll?.();
if (impl.item && impl.item.shouldBeVisible !== root.shouldBeVisible)
impl.item.shouldBeVisible = root.shouldBeVisible;
}
@@ -0,0 +1,37 @@
pragma ComponentBehavior: Bound
import QtQuick
QtObject {
id: root
property var _entries: []
readonly property bool active: _entries.length > 0
readonly property var focusWindows: _entries.map(entry => entry.focusWindow).filter(window => window)
signal closeRequested
function setActive(owner, active, focusWindow) {
if (!owner)
return;
const next = _entries.filter(entry => entry.owner !== owner);
if (active) {
next.push({
"owner": owner,
"focusWindow": focusWindow ?? null
});
}
_entries = next;
}
function unregister(owner) {
setActive(owner, false, null);
}
function closeAll() {
if (_entries.length === 0)
return;
closeRequested();
_entries = [];
}
}