fix(ipc): open bar widget popouts at the right bar, hidden bars included

- References #3786
This commit is contained in:
purian23 committed 2026-10-15 01:40:05 -04:00
1 parent ad964512a9
commit 87ce92703c
5 files changed
+140 -30

No files matched your search

+30 -20
View File
@@ -1,4 +1,5 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Services.SystemTray
@@ -33,29 +34,38 @@ Item {
for (const screenName in BarWidgetService.frameHostedBars)
bars.push(...BarWidgetService.frameBarsForScreen(screenName));
let currentBar = null;
let islandBar = null;
for (const bar of bars) {
if (!bar)
continue;
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
const hasRef = !refPropertyName || !!bar[refPropertyName];
if (!hasRef)
continue;
if (bar.isIsland) {
if (!islandBar || onFocusedScreen)
islandBar = bar;
continue;
const configOrder = SettingsData.barConfigs.map(config => config.id);
const screenOrder = Quickshell.screens.map(screen => screen.name);
const lastBarId = focusedScreenName ? SettingsData.lastUsedBarByScreen[focusedScreenName] : "";
// Same order as BarWidgetService.resolveIpcWidget, so every IPC path picks the same bar.
const rank = bar => {
const screenName = bar.modelData?.name;
const onFocused = !!focusedScreenName && screenName === focusedScreenName;
const registration = refPropertyName ? BarWidgetService.registrationForItem(bar[refPropertyName]) : null;
const tier = !refPropertyName || BarWidgetService.registrationShown(registration) ? 0 : BarWidgetService.registrationLive(registration) ? 1 : 2;
const position = bar.barConfig?.position;
return [bar.isIsland ? 1 : 0, onFocused ? 0 : 1, tier, onFocused && bar.barConfig?.id === lastBarId ? 0 : 1, screenOrder.indexOf(screenName), position === SettingsData.Position.Left || position === SettingsData.Position.Right ? 1 : 0, configOrder.indexOf(bar.barConfig?.id)];
};
const compare = (a, b) => {
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i])
return a[i] - b[i];
}
return 0;
};
currentBar = bar;
if (onFocusedScreen)
break;
let best = null;
let bestRank = null;
for (const bar of bars) {
if (!bar || (refPropertyName && !bar[refPropertyName]))
continue;
const barRank = rank(bar);
if (!best || compare(barRank, bestRank) < 0) {
best = bar;
bestRank = barRank;
}
}
return currentBar ?? islandBar;
return best;
}
readonly property var defaultAppMimeTypes: ({
+9
View File
@@ -140,9 +140,17 @@ Item {
if (!loader.item)
return;
loader.item.screen = screen;
positionUnresolvedPopout(loader.item, systemUpdateButtonRef);
PopoutManager.requestPopout(loader.item, undefined, "systemUpdate");
}
function positionUnresolvedPopout(popout, item) {
const section = item?.section || "right";
const anchor = item ?? dashSectionItem(section);
if (anchor)
topBarContent.surfaceContext.positionPopout(popout, anchor, section);
}
function triggerControlCenter() {
if (BarWidgetService.triggerWidgetPopout("controlCenterButton", {
screenName: screen?.name,
@@ -157,6 +165,7 @@ Item {
if (!loader.item)
return;
loader.item.triggerScreen = screen;
positionUnresolvedPopout(loader.item, controlCenterButtonRef);
loader.item.toggle();
if (loader.item.shouldBeVisible && NetworkService.wifiEnabled)
NetworkService.scanWifi();
@@ -84,7 +84,7 @@ Item {
}
function _finishWidgetPopoutOpen(spec, popout) {
if (spec.registration && !BarWidgetService.registrationActive(spec.registration))
if (spec.registration && !BarWidgetService.registrationActive(spec.registration, true))
return false;
const effectiveBarConfig = barConfig;
const barPosition = getBarPosition();
+38 -9
View File
@@ -121,15 +121,15 @@ Singleton {
return null;
}
function registrationActive(entry) {
return registrationLive(entry) && entry.item.effectiveVisible !== false;
function registrationActive(entry, includeHidden = false) {
return registrationLive(entry, includeHidden) && entry.item.effectiveVisible !== false;
}
function registrationShown(entry) {
return registrationActive(entry) && entry.context?.owner?.widgetEnabled !== false;
}
function registrationLive(entry) {
function registrationLive(entry, includeHidden = false) {
if (!entry?.item || widgetRegistry[entry.key] !== entry)
return false;
const context = entry.context;
@@ -137,18 +137,47 @@ Singleton {
return true;
if (!context.owner?.active || context.surface.config.enabled === false)
return false;
if (context.surface.config.visible === false && !(context.surface.host?.barRevealed ?? context.surface.revealed))
if (!includeHidden && context.surface.config.visible === false && !(context.surface.host?.barRevealed ?? context.surface.revealed))
return false;
if (context.kind === "dock")
return SettingsData.dockConfigsForScreen(context.surface.screen).some(config => config.id === context.barId);
return ShellLayout.forScreen(entry.screenName)?.instances.some(instance => instance.barId === context.barId) === true;
}
function resolveWidget(widgetId, target) {
// IPC passes includeHidden: a hidden bar's widget still opens, anchored where the bar sits.
function resolveWidget(widgetId, target, includeHidden = false) {
const configs = SettingsData.barConfigs.concat(SettingsData.dockConfigs ?? []).map(config => config.id);
const screens = Quickshell.screens.map(screen => screen.name);
const select = eligible => Registry.select(widgetRegistry, widgetId, target, configs, screens, eligible);
return select(registrationShown) ?? select(registrationLive);
return select(registrationShown) ?? select(registrationLive) ?? (includeHidden ? select(entry => registrationLive(entry, true)) : null);
}
// Matches getPreferredBar in DMSShellIPC: focused screen, then visible before hidden, then the last used bar.
function resolveIpcWidget(widgetId) {
const configs = SettingsData.barConfigs.concat(SettingsData.dockConfigs ?? []).map(config => config.id);
const screens = Quickshell.screens.map(screen => screen.name);
const screenName = getFocusedScreenName();
const lastBarId = screenName ? SettingsData.lastUsedBarByScreen[screenName] : "";
const tiers = [registrationShown, registrationLive, entry => registrationLive(entry, true)];
const focusedTargets = screenName ? [
{
screenName,
barId: lastBarId
},
{
screenName
}
] : [];
for (const targets of [focusedTargets, [{}]]) {
for (const eligible of tiers) {
for (const target of targets) {
const hit = Registry.select(widgetRegistry, widgetId, target, configs, screens, eligible);
if (hit)
return hit;
}
}
}
return null;
}
function getWidget(widgetId, screenName, instanceId) {
@@ -198,7 +227,7 @@ Singleton {
}
function getWidgetOnFocusedScreen(widgetId) {
return getWidget(widgetId, getFocusedScreenName()) || getWidget(widgetId);
return resolveIpcWidget(widgetId)?.item ?? null;
}
readonly property bool focusedScreenDetectionSupported: (CompositorService.isAqueous && AqueousService.available) || CompositorService.isHyprland || CompositorService.isNiri || CompositorService.isMango || CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle || CompositorService.isUmbriel
@@ -214,7 +243,7 @@ Singleton {
}
function hasWidget(widgetId) {
return getWidget(widgetId) !== null;
return resolveIpcWidget(widgetId) !== null;
}
function ensureWidget(widgetId) {
@@ -224,7 +253,7 @@ Singleton {
}
function triggerWidgetPopout(widgetId, target) {
const widget = target ? resolveWidget(widgetId, target)?.item : getWidgetOnFocusedScreen(widgetId);
const widget = target ? resolveWidget(widgetId, target, true)?.item : getWidgetOnFocusedScreen(widgetId);
if (!widget)
return false;
+62
View File
@@ -76,3 +76,65 @@ test("toggleReveal only works on auto hide bars and unhides first", () => {
assert.deepEqual(reveals.slice(1), [["toggle", "hidden"], ["toggle", "hidden"]]);
bars[2].visible = false;
});
const ipcBarBody = source.slice(source.indexOf(" function getPreferredBar("), source.indexOf(" readonly property var defaultAppMimeTypes"));
const serviceSource = readFileSync(new URL("../Services/BarWidgetService.qml", import.meta.url), "utf8");
const serviceFunctions = ["registrationActive", "registrationShown", "registrationLive", "resolveWidget", "resolveIpcWidget"].map(name => serviceSource.match(new RegExp(`^ {4}function ${name}\\([\\s\\S]*?^ {4}\\}$`, "m"))[0]);
const registrySource = readFileSync(new URL("../Common/InstanceRegistry.js", import.meta.url), "utf8").replace(/^\.pragma.*$/m, "");
// Both IPC paths, getPreferredBar for control center and updates, resolveIpcWidget for `widget`, over one registry.
function ipcBarChoice(bars, { focused = "", lastUsed = {}, screens = ["A", "B"] } = {}) {
const registry = vm.createContext({});
vm.runInContext(registrySource, registry);
const widgetRegistry = {};
const windows = bars.map(spec => {
const instanceId = `bar:${spec.id}:right:cc`;
const config = { id: spec.id, enabled: true, visible: !spec.hidden, position: spec.position ?? 0 };
const entry = {
key: registry.key(spec.screen, instanceId),
widgetId: "controlCenterButton",
screenName: spec.screen,
instanceId,
item: { effectiveVisible: true },
context: { barId: spec.id, kind: "bar", section: "right", occurrenceOrder: 0, owner: { active: true, widgetEnabled: !spec.disabled }, surface: { screen: { name: spec.screen }, config, host: { barRevealed: false } } }
};
widgetRegistry[entry.key] = entry;
return { id: spec.id, modelData: { name: spec.screen }, barConfig: config, isIsland: false, controlCenterButtonRef: entry.item };
});
const context = vm.createContext({
Registry: registry,
widgetRegistry,
Quickshell: { screens: screens.map(name => ({ name })) },
SettingsData: { barConfigs: windows.map(bar => bar.barConfig), dockConfigs: [], lastUsedBarByScreen: lastUsed, Position: { Top: 0, Bottom: 1, Left: 2, Right: 3 } },
ShellLayout: { forScreen: name => ({ instances: windows.filter(bar => bar.modelData.name === name).map(bar => ({ barId: bar.id })) }) },
root: { dankBarRepeater: { count: windows.length, itemAt: i => ({ item: { barVariants: { instances: [windows[i]] } } }) } },
getFocusedScreenName: () => focused
});
for (const fn of serviceFunctions)
vm.runInContext(fn.replace(/^ {4}/gm, ""), context);
context.BarWidgetService = {
getFocusedScreenName: () => focused,
frameHostedBars: {},
frameBarsForScreen: () => [],
registrationForItem: item => Object.values(widgetRegistry).find(entry => entry.item === item) ?? null,
registrationShown: context.registrationShown,
registrationLive: context.registrationLive
};
vm.runInContext(ipcBarBody.replace(/^ {4}/gm, ""), context);
return [context.getPreferredBar("controlCenterButtonRef")?.id ?? null, context.resolveIpcWidget("controlCenterButton")?.context.barId ?? null];
}
test("every IPC path picks the same bar, visible first", () => {
const cases = [
["visible bar beats a hidden one on the same screen", [{ id: "main", screen: "A", hidden: true }, { id: "second", screen: "A" }], { focused: "A" }, "second"],
["last used bar wins among visible bars", [{ id: "main", screen: "A" }, { id: "second", screen: "A" }], { focused: "A", lastUsed: { A: "second" } }, "second"],
["a visible bar beats a hidden last used one", [{ id: "main", screen: "A", hidden: true }, { id: "second", screen: "A" }], { focused: "A", lastUsed: { A: "main" } }, "second"],
["a shown bar beats a disabled last used one", [{ id: "main", screen: "A", disabled: true }, { id: "second", screen: "A" }], { focused: "A", lastUsed: { A: "main" } }, "second"],
["a hidden bar still answers when it is the only one", [{ id: "main", screen: "A", hidden: true }], { focused: "A" }, "main"],
["focused screen beats visibility elsewhere", [{ id: "main", screen: "A" }, { id: "other", screen: "B", hidden: true }], { focused: "B" }, "other"],
["unknown focus falls back to screen order", [{ id: "main", screen: "A" }, { id: "other", screen: "B" }], { screens: ["B", "A"] }, "other"],
["horizontal bar beats vertical in a tie", [{ id: "side", screen: "A", position: 2 }, { id: "top", screen: "A" }], { focused: "A" }, "top"]
];
for (const [name, bars, opts, expected] of cases)
assert.deepEqual(ipcBarChoice(bars, opts), [expected, expected], name);
});