mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-21 18:54:01 -04:00
* feat(island): add system tray slot and overflow menu to compact home
- Add system tray slot and overflow expander menu in Dynamic Island
- Share tray icon path resolution and DBus context menu fallback in TrayMenuManager
- Support edge-anchored context menu positioning across top, bottom, left, and right
- Lock bottom and edge anchoring in DContextMenu to prevent animation jitter
* fix(island): only instantiate tray menus when tray slot is enabled
* feat(island): expand tray item and caret click area to slot height
* fix(island): use Theme.surfaceTextHover and restore DContextMenu height behavior
* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island
* Revert "refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid, support tray order and IPC in island"
This reverts commit 29888f89cd.
* refactor(tray): use shared TrayMenuManager helpers and TrayItemIcon, support tray order and IPC in island
* refactor(tray): extract shared TrayMenuContent and TrayOverflowGrid
* feat(island): support tray item drag and drop reordering
* fix(island): prevent re-entrant tray drag reordering on model destruction
* fix(tray): remove native submenu anchor hydration to prevent popup stutter
* fix(tray): decouple submenu hydrator from anchor window to avoid stutter
* fix(tray): restore anchor window on submenu hydrator and disable on island
* tray: fix bar overflow surface & reuse max visible items on the island
209 lines
7.5 KiB
QML
209 lines
7.5 KiB
QML
pragma Singleton
|
|
|
|
import Quickshell
|
|
import Quickshell.Services.SystemTray
|
|
import QtQuick
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
property var activeTrayMenus: ({})
|
|
property var _pendingMenuRequest: null
|
|
property var _trayKeyByItem: new Map()
|
|
|
|
readonly property var envHiddenTrayIds: {
|
|
const envValue = Quickshell.env("DMS_HIDE_TRAYIDS") || "";
|
|
return envValue ? envValue.split(",").map(id => id.trim().toLowerCase()) : [];
|
|
}
|
|
|
|
function isEnvHidden(itemId) {
|
|
if (!itemId || !envHiddenTrayIds.length)
|
|
return false;
|
|
return envHiddenTrayIds.includes(itemId.toLowerCase());
|
|
}
|
|
|
|
function filterEnvHidden(items) {
|
|
if (!items)
|
|
return [];
|
|
if (!envHiddenTrayIds.length)
|
|
return items;
|
|
return items.filter(item => !isEnvHidden(item?.id));
|
|
}
|
|
|
|
Connections {
|
|
target: SystemTray
|
|
function onItemUnregistered(item) {
|
|
root._trayKeyByItem.delete(item);
|
|
}
|
|
}
|
|
|
|
function getTrayItemKey(item, allItems) {
|
|
const id = item?.id || "";
|
|
if (!id)
|
|
return "";
|
|
if (_trayKeyByItem.has(item))
|
|
return _trayKeyByItem.get(item);
|
|
const pool = allItems || SystemTray.items.values;
|
|
const taken = new Set();
|
|
for (const live of pool) {
|
|
if (live !== item && (live?.id || "") === id && _trayKeyByItem.has(live))
|
|
taken.add(_trayKeyByItem.get(live));
|
|
}
|
|
let key = id;
|
|
for (let n = 1; taken.has(key); n++)
|
|
key = `${id}::${n}`;
|
|
_trayKeyByItem.set(item, key);
|
|
return key;
|
|
}
|
|
|
|
function resolveOrderIndex(key, orderMap) {
|
|
if (orderMap.has(key))
|
|
return orderMap.get(key);
|
|
return -1;
|
|
}
|
|
|
|
function sortByPreferredOrder(items, savedOrder) {
|
|
if (!items)
|
|
return [];
|
|
const order = savedOrder || [];
|
|
const orderMap = new Map();
|
|
order.forEach((key, idx) => orderMap.set(key, idx));
|
|
|
|
return [...items].sort((a, b) => {
|
|
const keyA = getTrayItemKey(a, items);
|
|
const keyB = getTrayItemKey(b, items);
|
|
const orderA = resolveOrderIndex(keyA, orderMap);
|
|
const orderB = resolveOrderIndex(keyB, orderMap);
|
|
const posA = orderA >= 0 ? orderA : 10000 + items.indexOf(a);
|
|
const posB = orderB >= 0 ? orderB : 10000 + items.indexOf(b);
|
|
return posA - posB;
|
|
});
|
|
}
|
|
|
|
function dragShiftOffset(index, draggedIndex, dropTargetIndex, shiftAmount) {
|
|
if (draggedIndex < 0 || index === draggedIndex || dropTargetIndex < 0)
|
|
return 0;
|
|
if (draggedIndex < dropTargetIndex && index > draggedIndex && index <= dropTargetIndex)
|
|
return -shiftAmount;
|
|
if (draggedIndex > dropTargetIndex && index >= dropTargetIndex && index < draggedIndex)
|
|
return shiftAmount;
|
|
return 0;
|
|
}
|
|
|
|
function reorderItemKeys(allKeys, fromKey, toKey) {
|
|
if (!allKeys || !fromKey || !toKey)
|
|
return null;
|
|
const fullOrder = [...allKeys];
|
|
const fromIdx = fullOrder.indexOf(fromKey);
|
|
const toIdx = fullOrder.indexOf(toKey);
|
|
if (fromIdx < 0 || toIdx < 0 || fromIdx === toIdx)
|
|
return null;
|
|
const moved = fullOrder.splice(fromIdx, 1)[0];
|
|
fullOrder.splice(toIdx, 0, moved);
|
|
return fullOrder;
|
|
}
|
|
|
|
signal openTrayMenuRequested
|
|
|
|
function requestOpenMenu(itemId, screenName) {
|
|
_pendingMenuRequest = {
|
|
"itemId": itemId,
|
|
"screenName": screenName
|
|
};
|
|
openTrayMenuRequested();
|
|
}
|
|
|
|
// Every SystemTrayBar instance receives the signal; the claim ensures
|
|
// exactly one opens the menu, preferring the requested screen
|
|
function claimMenuRequest(instanceScreenName) {
|
|
if (!_pendingMenuRequest)
|
|
return null;
|
|
if (_pendingMenuRequest.screenName && _pendingMenuRequest.screenName !== instanceScreenName)
|
|
return null;
|
|
const request = _pendingMenuRequest;
|
|
_pendingMenuRequest = null;
|
|
return request;
|
|
}
|
|
|
|
function findTrayItem(itemId: string): var {
|
|
if (!itemId)
|
|
return null;
|
|
|
|
return SystemTray.items.values.find(item => {
|
|
const id = item?.id || "";
|
|
const title = item?.tooltipTitle || "";
|
|
const fullKey = title ? `${id}::${title}` : id;
|
|
return fullKey === itemId || id === itemId;
|
|
});
|
|
}
|
|
|
|
function registerMenu(screenName, menu) {
|
|
if (!screenName || !menu)
|
|
return;
|
|
const newMenus = Object.assign({}, activeTrayMenus);
|
|
newMenus[screenName] = menu;
|
|
activeTrayMenus = newMenus;
|
|
}
|
|
|
|
function unregisterMenu(screenName) {
|
|
if (!screenName)
|
|
return;
|
|
const newMenus = Object.assign({}, activeTrayMenus);
|
|
delete newMenus[screenName];
|
|
activeTrayMenus = newMenus;
|
|
}
|
|
|
|
function closeHoverMenus() {
|
|
for (const screenName in activeTrayMenus) {
|
|
const menu = activeTrayMenus[screenName]
|
|
if (!menu || menu.openedByHover !== true) continue
|
|
if (typeof menu.close === "function") {
|
|
menu.close()
|
|
} else if (menu.showMenu !== undefined) {
|
|
menu.showMenu = false
|
|
}
|
|
}
|
|
}
|
|
|
|
function closeAllMenus() {
|
|
for (const screenName in activeTrayMenus) {
|
|
const menu = activeTrayMenus[screenName];
|
|
if (!menu)
|
|
continue;
|
|
if (typeof menu.close === "function") {
|
|
menu.close();
|
|
} else if (menu.showMenu !== undefined) {
|
|
menu.showMenu = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function iconSourceFor(trayItem) {
|
|
let icon = trayItem && trayItem.icon;
|
|
if (typeof icon === "string" || icon instanceof String) {
|
|
if (icon === "")
|
|
return "";
|
|
if (icon.includes("?path=")) {
|
|
const split = icon.split("?path=");
|
|
if (split.length !== 2)
|
|
return icon;
|
|
const name = split[0];
|
|
const path = split[1];
|
|
let fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
if (fileName.startsWith("dropboxstatus"))
|
|
fileName = `hicolor/16x16/status/${fileName}`;
|
|
return `file://${path}/${fileName}`;
|
|
}
|
|
if (icon.startsWith("/") && !icon.startsWith("file://"))
|
|
return `file://${icon}`;
|
|
return icon;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function callContextMenuFallback(trayItemId, globalX, globalY) {
|
|
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
|
|
Quickshell.execDetached(["bash", "-c", script, "_", trayItemId, String(Math.round(globalX)), String(Math.round(globalY))]);
|
|
}
|
|
}
|