mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-29 14:37:47 -04:00
dbar: animate hiding overlay bars on fullscreen in niri (#4000)
port 2.0
(cherry picked from commit 47f7f3d798)
This commit is contained in:
1 parent
2ba4f1355c
commit
9b762b8ec0
12 files changed
+192
-9
No files matched your search
@@ -1592,6 +1592,7 @@ var LOCAL_SPEC = {
|
||||
popupGapsManual: 4,
|
||||
maximizeDetection: true,
|
||||
useOverlayLayer: false,
|
||||
hideOnFullscreen: false,
|
||||
scrollEnabled: true,
|
||||
scrollXBehavior: "focusWindow",
|
||||
scrollYBehavior: "workspace",
|
||||
|
||||
@@ -914,7 +914,7 @@ Item {
|
||||
|
||||
function toggleReveal(selector: string, value: string): string {
|
||||
return withBarConfig(selector, value, false, bar => {
|
||||
if (!bar.autoHide)
|
||||
if (!bar.autoHide && !BarWidgetService.dankBarItems[bar.id]?.fullscreenAutoHide)
|
||||
return "BAR_AUTO_HIDE_DISABLED";
|
||||
if (!(bar.visible ?? true)) {
|
||||
SettingsData.updateBarConfig(bar.id, {
|
||||
|
||||
@@ -25,6 +25,7 @@ Item {
|
||||
Component.onDestruction: BarWidgetService.unregisterDankBarItem(_registeredBarId, root)
|
||||
|
||||
property alias barVariants: barVariants
|
||||
readonly property bool fullscreenAutoHide: barVariants.instances.some(bar => bar.fullscreenAutoHide)
|
||||
property var hyprlandOverviewLoader: null
|
||||
property bool systemTrayMenuOpen: false
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Modules.DIsland
|
||||
import qs.Services
|
||||
@@ -22,6 +23,13 @@ Item {
|
||||
|
||||
readonly property bool barRevealed: inputMask.showing
|
||||
readonly property bool autoHideActive: topBarCore.autoHide
|
||||
readonly property bool fullscreenAutoHide: !barWindow.isIsland
|
||||
&& !usesFrameBarChrome
|
||||
&& (barConfig?.hideOnFullscreen ?? false)
|
||||
&& hostWindow?.dBarLayer === WlrLayer.Overlay
|
||||
&& CompositorService.isNiri
|
||||
&& !CompositorService.overviewActiveOnScreen(screenName)
|
||||
&& CompositorService.visibleNiriFullscreenToplevelOnScreen(screen)
|
||||
|
||||
readonly property bool isIsland: barConfig?.island === true
|
||||
// Resolver-owned so the body, the frame and the dismiss window agree during the frame latch.
|
||||
@@ -822,7 +830,7 @@ Item {
|
||||
|
||||
readonly property bool reserveExclusiveWhenAutoHidden: FrameTransitionState.effectiveFrameEnabled && usesFrameBarChrome && !!barWindow.screen && SettingsData.isScreenInPreferences(barWindow.screen, SettingsData.frameScreenPreferences)
|
||||
|
||||
readonly property real surfaceExclusiveZone: isIsland ? ((islandFree || (islandHost?.floating ?? false)) ? 0 : SettingsData.islandReserveThickness(barConfig)) : (!(barConfig?.visible ?? true) || (topBarCore.autoHide && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (usesFrameBarChrome ? 0 : (barConfig?.bottomGap ?? 0)))
|
||||
readonly property real surfaceExclusiveZone: isIsland ? ((islandFree || (islandHost?.floating ?? false)) ? 0 : SettingsData.islandReserveThickness(barConfig)) : (!(barConfig?.visible ?? true) || ((barConfig?.autoHide ?? false) && !barWindow.reserveExclusiveWhenAutoHidden)) ? -1 : (barWindow.effectiveBarThickness + effectiveSpacing + (usesFrameBarChrome ? 0 : (barConfig?.bottomGap ?? 0)))
|
||||
|
||||
readonly property alias inputMaskItem: inputMask
|
||||
|
||||
@@ -934,7 +942,12 @@ Item {
|
||||
anchors.fill: parent
|
||||
layer.enabled: false
|
||||
|
||||
property bool autoHide: (!barWindow.isIsland || barWindow.islandOverlay) && (barConfig?.autoHide ?? false)
|
||||
readonly property bool autoHide: ((!barWindow.isIsland || barWindow.islandOverlay) && (barConfig?.autoHide ?? false)) || barWindow.fullscreenAutoHide
|
||||
onAutoHideChanged: {
|
||||
if (!autoHide)
|
||||
SettingsData.setBarIpcReveal(barConfig?.id ?? "", false);
|
||||
evaluateReveal();
|
||||
}
|
||||
property bool revealSticky: false
|
||||
// In click-through mode the hidden bar's input mask covers the full
|
||||
// band while the revealed bar's mask covers only the widget sections,
|
||||
@@ -1032,6 +1045,9 @@ Item {
|
||||
if (inOverviewWithShow)
|
||||
return true;
|
||||
|
||||
if (barWindow.fullscreenAutoHide)
|
||||
return hoverReveal || popoutPinsReveal || revealSticky || ipcReveal;
|
||||
|
||||
const showOnWindowsSetting = barConfig?.showOnWindowsOpen ?? false;
|
||||
if (showOnWindowsSetting && autoHide && CompositorService.windowOverlapSupported) {
|
||||
if (barWindow.shouldHideForWindows)
|
||||
@@ -1048,7 +1064,6 @@ Item {
|
||||
readonly property var rootWindowBarConfig: rootWindow.barConfig
|
||||
|
||||
onRootWindowBarConfigChanged: {
|
||||
autoHide = (!barWindow.isIsland || barWindow.islandOverlay) && (barConfig?.autoHide ?? false);
|
||||
evaluateReveal();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ PanelWindow {
|
||||
readonly property bool isVertical: body.isVertical
|
||||
readonly property int barPos: body.barPos
|
||||
readonly property bool barRevealed: body.barRevealed
|
||||
readonly property bool fullscreenAutoHide: body.fullscreenAutoHide
|
||||
readonly property bool isIsland: body.isIsland
|
||||
readonly property var islandHost: body.islandHost
|
||||
readonly property var leadingSectionRect: body.leadingSectionRect
|
||||
|
||||
@@ -634,6 +634,20 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "barHideOnFullscreen"
|
||||
resetStore: bar
|
||||
resetKeys: ["hideOnFullscreen"]
|
||||
tags: ["bar", "fullscreen", "hide", "visibility"]
|
||||
visible: CompositorService.isNiri && !bar.islandOwnsSelectedBarTop && (bar.selectedBarConfig?.useOverlayLayer ?? false)
|
||||
text: I18n.tr("Hide bar on fullscreen", "bar visibility toggle")
|
||||
description: I18n.tr("Slide the bar away over fullscreen applications. Move the pointer to the screen edge to reveal it.", "bar fullscreen auto-hide description")
|
||||
checked: bar.selectedBarConfig?.hideOnFullscreen ?? false
|
||||
onToggled: toggled => SettingsData.updateBarConfig(bar.selectedBarId, {
|
||||
hideOnFullscreen: toggled
|
||||
})
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "islandUseOverlayLayer"
|
||||
tags: ["island", "fullscreen", "overlay", "layer"]
|
||||
|
||||
@@ -11,6 +11,7 @@ import qs.DCommon.Common as DCommon
|
||||
import qs.Services
|
||||
import "../Common/WorkspaceModel.js" as WorkspaceModel
|
||||
import "../Common/WindowModel.js" as WindowModel
|
||||
import "NiriFullscreen.js" as NiriFullscreen
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -873,6 +874,12 @@ Singleton {
|
||||
return false;
|
||||
}
|
||||
|
||||
function visibleNiriFullscreenToplevelOnScreen(screenOrName) {
|
||||
if (!isNiri || !NiriService.hasInitialConnection)
|
||||
return false;
|
||||
return NiriFullscreen.isFullscreenOnScreen(_screenName(screenOrName), NiriService.allWorkspaces, NiriService.windows, ToplevelManager.toplevels?.values);
|
||||
}
|
||||
|
||||
function filterCurrentDisplay(toplevels, screenName) {
|
||||
if (!toplevels || toplevels.length === 0 || !screenName)
|
||||
return toplevels;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
.pragma library
|
||||
|
||||
function isFullscreenOnScreen(screenName, workspaces, windows, toplevels) {
|
||||
if (!screenName || !workspaces || !windows || !toplevels)
|
||||
return false;
|
||||
|
||||
const workspace = workspaces.find(ws => ws.output === screenName && ws.is_active);
|
||||
if (!workspace || workspace.active_window_id === null || workspace.active_window_id === undefined)
|
||||
return false;
|
||||
|
||||
const window = windows.find(w => w.id === workspace.active_window_id && w.workspace_id === workspace.id);
|
||||
if (!window || !window.app_id)
|
||||
return false;
|
||||
|
||||
const matches = toplevels.filter(t => t && t.appId === window.app_id
|
||||
&& (t.title || "") === (window.title || "")
|
||||
&& t.screens?.some(screen => screen.name === screenName));
|
||||
|
||||
if (window.is_focused) {
|
||||
const focused = matches.filter(t => t.activated);
|
||||
if (focused.length === 1)
|
||||
return !!focused[0].fullscreen;
|
||||
}
|
||||
|
||||
// Niri IPC and wlr-foreign-toplevel have no shared window identifier.
|
||||
// Do not guess when equal titles on the same output make the match ambiguous.
|
||||
const workspaceIds = new Set(workspaces.filter(ws => ws.output === screenName).map(ws => ws.id));
|
||||
const sameWindows = windows.filter(w => workspaceIds.has(w.workspace_id)
|
||||
&& w.app_id === window.app_id && (w.title || "") === (window.title || ""));
|
||||
if (sameWindows.length !== 1 || matches.length !== 1)
|
||||
return false;
|
||||
|
||||
return !!matches[0].fullscreen;
|
||||
}
|
||||
@@ -477,7 +477,7 @@ Singleton {
|
||||
for (const ws of data.workspaces) {
|
||||
const oldWs = root.workspaces[ws.id];
|
||||
newWorkspaces[ws.id] = ws;
|
||||
if (oldWs && oldWs.active_window_id !== undefined) {
|
||||
if (ws.active_window_id === undefined && oldWs && oldWs.active_window_id !== undefined) {
|
||||
newWorkspaces[ws.id].active_window_id = oldWs.active_window_id;
|
||||
}
|
||||
}
|
||||
@@ -582,14 +582,18 @@ Singleton {
|
||||
}
|
||||
|
||||
function handleWorkspaceActiveWindowChanged(data) {
|
||||
if (root.workspaces[data.workspace_id]) {
|
||||
if (data.active_window_id !== null && data.active_window_id !== undefined)
|
||||
const ws = root.workspaces[data.workspace_id];
|
||||
if (ws) {
|
||||
if (ws.is_focused && data.active_window_id !== null && data.active_window_id !== undefined)
|
||||
lastFocusedWindowId = data.active_window_id;
|
||||
updateWorkspace(data.workspace_id, {
|
||||
"active_window_id": data.active_window_id
|
||||
});
|
||||
}
|
||||
|
||||
if (!ws?.is_focused)
|
||||
return;
|
||||
|
||||
let changed = false;
|
||||
const updatedWindows = new Array(windows.length);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const barBlock = source.slice(source.indexOf(" function getBarConfig("), sour
|
||||
const functions = [...barBlock.matchAll(/^ {4,8}function (\w+)\(([^)]*)\)(?:: \w+)? \{\n[\s\S]*?^\s{4,8}\}$/gm)];
|
||||
const untyped = functions.map(match => match[0].replace(/\((.*?)\)(?:: \w+)? \{/, (_, params) => "(" + params.replace(/: \w+/g, "") + ") {"));
|
||||
|
||||
function shell(barConfigs, toggleResults = []) {
|
||||
function shell(barConfigs, toggleResults = [], barItems = {}) {
|
||||
const updates = [];
|
||||
const reveals = [];
|
||||
const SettingsData = {
|
||||
@@ -22,7 +22,8 @@ function shell(barConfigs, toggleResults = []) {
|
||||
return toggleResults.shift();
|
||||
}
|
||||
};
|
||||
const context = vm.createContext({ SettingsData });
|
||||
const BarWidgetService = { dankBarItems: barItems };
|
||||
const context = vm.createContext({ SettingsData, BarWidgetService });
|
||||
for (const body of untyped)
|
||||
vm.runInContext(body.replace(/^ {4}/gm, ""), context);
|
||||
return { context, updates, reveals };
|
||||
@@ -75,6 +76,10 @@ test("toggleReveal only works on auto hide bars and unhides first", () => {
|
||||
assert.equal(context.toggleReveal("id", "hidden"), "BAR_TUCK_SUCCESS");
|
||||
assert.deepEqual(reveals.slice(1), [["toggle", "hidden"], ["toggle", "hidden"]]);
|
||||
bars[2].visible = false;
|
||||
|
||||
const fullscreen = shell(bars, [true], { main: { fullscreenAutoHide: true } });
|
||||
assert.equal(fullscreen.context.toggleReveal("id", "main"), "BAR_REVEAL_SUCCESS");
|
||||
assert.deepEqual(fullscreen.reveals, [["toggle", "main"]]);
|
||||
});
|
||||
|
||||
const ipcBarBody = source.slice(source.indexOf(" function getPreferredBar("), source.indexOf(" readonly property var defaultAppMimeTypes"));
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
import test from 'node:test';
|
||||
|
||||
const source = readFileSync(new URL('../Services/NiriService.qml', import.meta.url), 'utf8');
|
||||
|
||||
function service() {
|
||||
const context = vm.createContext({
|
||||
workspaces: {
|
||||
1: { id: 1, idx: 1, output: 'A', is_active: true, is_focused: true, active_window_id: 10 },
|
||||
2: { id: 2, idx: 1, output: 'B', is_active: true, is_focused: false, active_window_id: 20 }
|
||||
},
|
||||
windows: [
|
||||
{ id: 10, workspace_id: 1, is_focused: true },
|
||||
{ id: 11, workspace_id: 1, is_focused: false },
|
||||
{ id: 20, workspace_id: 2, is_focused: false },
|
||||
{ id: 21, workspace_id: 2, is_focused: false }
|
||||
],
|
||||
allWorkspaces: [],
|
||||
lastFocusedWindowId: 10,
|
||||
updateCurrentOutputWorkspaces() {}
|
||||
});
|
||||
context.root = context;
|
||||
// Execute the actual JS event handlers with their QML property storage mocked.
|
||||
for (const name of ['setWorkspaces', 'updateWorkspace', 'handleWorkspacesChanged', 'handleWindowFocusChanged', 'handleWorkspaceActiveWindowChanged']) {
|
||||
const match = source.match(new RegExp(` function ${name}\\([^]*?(?=\\n function )`));
|
||||
assert.ok(match, `event handler ${name} exists`);
|
||||
vm.runInContext(match[0], context);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
test('numeric focus IDs update string-keyed workspace storage', () => {
|
||||
const s = service();
|
||||
s.handleWindowFocusChanged({ id: 11 });
|
||||
assert.equal(s.workspaces[1].active_window_id, 11);
|
||||
assert.equal(s.allWorkspaces.find(ws => ws.id === 1).active_window_id, 11);
|
||||
assert.equal(s.windows.find(w => w.id === 11).is_focused, true);
|
||||
assert.equal(s.workspaces[2].active_window_id, 20);
|
||||
});
|
||||
|
||||
test('active-window events update the correct workspace without stealing focus', () => {
|
||||
const s = service();
|
||||
s.handleWorkspaceActiveWindowChanged({ workspace_id: 2, active_window_id: 21 });
|
||||
assert.equal(s.workspaces[2].active_window_id, 21);
|
||||
assert.equal(s.windows.find(w => w.id === 10).is_focused, true);
|
||||
assert.equal(s.windows.find(w => w.id === 21).is_focused, false);
|
||||
assert.equal(s.lastFocusedWindowId, 10);
|
||||
});
|
||||
|
||||
test('focused workspace active-window events preserve normal focus updates', () => {
|
||||
const s = service();
|
||||
s.handleWorkspaceActiveWindowChanged({ workspace_id: 1, active_window_id: 11 });
|
||||
assert.equal(s.workspaces[1].active_window_id, 11);
|
||||
assert.equal(s.windows.find(w => w.id === 11).is_focused, true);
|
||||
assert.equal(s.lastFocusedWindowId, 11);
|
||||
});
|
||||
|
||||
test('fresh workspace snapshots replace stale active-window IDs, including null', () => {
|
||||
const s = service();
|
||||
s.handleWorkspacesChanged({ workspaces: [
|
||||
{ ...s.workspaces[1], active_window_id: 11 },
|
||||
{ ...s.workspaces[2], active_window_id: null }
|
||||
] });
|
||||
assert.equal(s.workspaces[1].active_window_id, 11);
|
||||
assert.equal(s.workspaces[2].active_window_id, null);
|
||||
});
|
||||
@@ -3794,6 +3794,39 @@
|
||||
"icon": "border_outer",
|
||||
"conditionKey": "frameEnabled"
|
||||
},
|
||||
{
|
||||
"section": "barHideOnFullscreen",
|
||||
"label": "Hide bar on fullscreen",
|
||||
"tabIndex": 3,
|
||||
"category": "General",
|
||||
"keywords": [
|
||||
"app",
|
||||
"applications",
|
||||
"apps",
|
||||
"away",
|
||||
"bar",
|
||||
"edge",
|
||||
"fullscreen",
|
||||
"general",
|
||||
"hide",
|
||||
"move",
|
||||
"over",
|
||||
"panel",
|
||||
"pointer",
|
||||
"program",
|
||||
"programs",
|
||||
"reveal",
|
||||
"screen",
|
||||
"slide",
|
||||
"statusbar",
|
||||
"taskbar",
|
||||
"topbar",
|
||||
"visibility"
|
||||
],
|
||||
"parentLabel": "Bar",
|
||||
"description": "Slide the bar away over fullscreen applications. Move the pointer to the screen edge to reveal it.",
|
||||
"conditionKey": "isNiri"
|
||||
},
|
||||
{
|
||||
"section": "islandHoverCloseDelay",
|
||||
"label": "Hide delay",
|
||||
|
||||
Reference in new issue
Block a user