mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
290 lines
11 KiB
QML
290 lines
11 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Services.SystemTray
|
|
import Quickshell.Widgets
|
|
import qs.Commons
|
|
import qs.Modules.MainScreen
|
|
import qs.Services.UI
|
|
import qs.Widgets
|
|
|
|
// A compact grid panel listing all tray items, opened from the Tray widget
|
|
SmartPanel {
|
|
id: root
|
|
|
|
// Do not give exclusive focus to the TrayDrawer or it will prevent the dropdown menu to request it.
|
|
exclusiveKeyboard: false
|
|
|
|
// Widget info for menu functionality (set by Tray widget when opening)
|
|
property string widgetSection: ""
|
|
property int widgetIndex: -1
|
|
|
|
// Sizing properties must stay at root for preferredWidth/Height
|
|
readonly property int maxColumns: 8
|
|
readonly property real cellSize: Math.round(Style.capsuleHeight * 0.65)
|
|
readonly property real outerPadding: Style.marginM
|
|
readonly property real innerSpacing: Style.marginM
|
|
|
|
// All tray items from SystemTray
|
|
readonly property var trayValuesAll: (SystemTray.items && SystemTray.items.values) ? SystemTray.items.values : []
|
|
|
|
// Filtered items - computed in panelContent where isPinned is available
|
|
property var trayValues: []
|
|
|
|
readonly property int itemCount: trayValues.length
|
|
readonly property int columns: Math.max(1, Math.min(maxColumns, itemCount))
|
|
readonly property int rows: Math.max(1, Math.ceil(itemCount / Math.max(1, columns)))
|
|
|
|
preferredWidth: (columns * cellSize) + ((columns - 1) * innerSpacing) + (2 * outerPadding)
|
|
preferredHeight: (rows * cellSize) + ((rows - 1) * innerSpacing) + (2 * outerPadding)
|
|
|
|
// Auto-close drawer when all items are pinned (drawer becomes empty)
|
|
onTrayValuesChanged: {
|
|
if (visible && trayValues.length === 0) {
|
|
close();
|
|
}
|
|
}
|
|
|
|
// Force refresh panelContent settings when drawer opens
|
|
onOpened: {
|
|
if (panelContent && panelContent.settingsVersion !== undefined) {
|
|
panelContent.settingsVersion++;
|
|
}
|
|
}
|
|
|
|
panelContent: Item {
|
|
id: panelContent
|
|
|
|
// Settings state (lazy-loaded with panelContent)
|
|
property int settingsVersion: 0
|
|
|
|
readonly property var widgetSettings: {
|
|
// Reference settingsVersion to force recalculation when it changes
|
|
void (settingsVersion);
|
|
if (root.widgetSection === "" || root.widgetIndex < 0)
|
|
return {};
|
|
var widgets = Settings.getBarWidgetsForScreen(root.screen?.name)[root.widgetSection];
|
|
if (!widgets || root.widgetIndex >= widgets.length)
|
|
return {};
|
|
var settings = widgets[root.widgetIndex];
|
|
if (!settings || settings.id !== "Tray")
|
|
return {};
|
|
return settings;
|
|
}
|
|
|
|
readonly property var pinnedList: widgetSettings.pinned || []
|
|
readonly property bool hidePassive: widgetSettings.hidePassive !== undefined ? widgetSettings.hidePassive : true
|
|
|
|
// Filter tray items - this runs in panelContent context where isPinned is available
|
|
function updateFilteredItems() {
|
|
var filtered = [];
|
|
for (var i = 0; i < root.trayValuesAll.length; i++) {
|
|
var item = root.trayValuesAll[i];
|
|
if (!item)
|
|
continue;
|
|
|
|
// Filter out passive items if hidePassive is enabled
|
|
if (hidePassive && item.status !== undefined && (item.status === SystemTray.Passive || item.status === 0)) {
|
|
continue;
|
|
}
|
|
|
|
// Filter out pinned items
|
|
if (isPinned(item)) {
|
|
continue;
|
|
}
|
|
|
|
filtered.push(item);
|
|
}
|
|
root.trayValues = filtered;
|
|
}
|
|
|
|
// Update filtered items when dependencies change
|
|
Component.onCompleted: updateFilteredItems()
|
|
onPinnedListChanged: updateFilteredItems()
|
|
onHidePassiveChanged: updateFilteredItems()
|
|
|
|
Connections {
|
|
target: root
|
|
function onTrayValuesAllChanged() {
|
|
panelContent.updateFilteredItems();
|
|
}
|
|
}
|
|
|
|
// Helper functions (lazy-loaded with panelContent)
|
|
function wildCardMatch(str, rule) {
|
|
if (!str || !rule)
|
|
return false;
|
|
const placeholder = '\uE000';
|
|
let processedRule = rule.replace(/\*/g, placeholder);
|
|
let escaped = processedRule.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
let pattern = '^' + escaped.replace(new RegExp(placeholder, 'g'), '.*') + '$';
|
|
try {
|
|
return new RegExp(pattern, 'i').test(str);
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function isPinned(item) {
|
|
if (!pinnedList || pinnedList.length === 0)
|
|
return false;
|
|
const title = item?.tooltipTitle || item?.name || item?.id || "";
|
|
for (var i = 0; i < pinnedList.length; i++) {
|
|
if (wildCardMatch(title, pinnedList[i]))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Popup menu state (lazy-loaded with panelContent)
|
|
property int popupMenuUpdateTrigger: 0
|
|
|
|
readonly property var popupMenuWindow: {
|
|
void (popupMenuUpdateTrigger);
|
|
return PanelService.getPopupMenuWindow(screen);
|
|
}
|
|
|
|
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
|
|
|
|
// Dynamic content sizing
|
|
property real contentPreferredWidth: (root.columns * root.cellSize) + ((root.columns - 1) * root.innerSpacing) + (2 * root.outerPadding)
|
|
property real contentPreferredHeight: (root.rows * root.cellSize) + ((root.rows - 1) * root.innerSpacing) + (2 * root.outerPadding)
|
|
|
|
// Connections (lazy-loaded with panelContent)
|
|
Connections {
|
|
target: Settings
|
|
function onSettingsSaved() {
|
|
panelContent.settingsVersion++;
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: PanelService
|
|
function onPopupMenuWindowRegistered(registeredScreen) {
|
|
if (registeredScreen === screen) {
|
|
panelContent.popupMenuUpdateTrigger++;
|
|
}
|
|
}
|
|
}
|
|
|
|
Grid {
|
|
id: grid
|
|
anchors.fill: parent
|
|
anchors.margins: root.outerPadding
|
|
spacing: root.innerSpacing
|
|
columns: root.columns
|
|
rowSpacing: root.innerSpacing
|
|
columnSpacing: root.innerSpacing
|
|
|
|
Repeater {
|
|
id: repeater
|
|
model: root.trayValues
|
|
|
|
delegate: Item {
|
|
width: root.cellSize
|
|
height: root.cellSize
|
|
|
|
IconImage {
|
|
id: trayIcon
|
|
anchors.fill: parent
|
|
asynchronous: true
|
|
backer.fillMode: Image.PreserveAspectFit
|
|
source: {
|
|
let icon = modelData?.icon || "";
|
|
if (!icon)
|
|
return "";
|
|
if (icon.includes("?path=")) {
|
|
const chunks = icon.split("?path=");
|
|
const name = chunks[0];
|
|
const path = chunks[1];
|
|
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
return `file://${path}/${fileName}`;
|
|
}
|
|
return icon;
|
|
}
|
|
|
|
layer.enabled: panelContent.widgetSettings.colorizeIcons !== false
|
|
layer.effect: ShaderEffect {
|
|
property color targetColor: Settings.data.colorSchemes.darkMode ? Color.mOnSurface : Color.mSurfaceVariant
|
|
property real colorizeMode: 1.0
|
|
fragmentShader: Qt.resolvedUrl(Quickshell.shellDir + "/Shaders/qsb/appicon_colorize.frag.qsb")
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
hoverEnabled: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
|
|
|
onClicked: mouse => {
|
|
if (!modelData)
|
|
return;
|
|
if (mouse.button === Qt.LeftButton) {
|
|
if (!modelData.onlyMenu) {
|
|
modelData.activate();
|
|
}
|
|
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
|
|
PanelService.openedPanel.close();
|
|
}
|
|
} else if (mouse.button === Qt.MiddleButton) {
|
|
modelData.secondaryActivate && modelData.secondaryActivate();
|
|
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
|
|
PanelService.openedPanel.close();
|
|
}
|
|
} else if (mouse.button === Qt.RightButton) {
|
|
TooltipService.hideImmediately();
|
|
|
|
if (panelContent.popupMenuWindow && panelContent.popupMenuWindow.visible) {
|
|
panelContent.popupMenuWindow.close();
|
|
return;
|
|
}
|
|
|
|
if (modelData.hasMenu && modelData.menu && panelContent.trayMenu && panelContent.trayMenu.item) {
|
|
const barPosition = Settings.getBarPositionForScreen(root.screen?.name);
|
|
// Increased spacing for better alignment with other context menus
|
|
let menuX, menuY;
|
|
|
|
if (barPosition === "left") {
|
|
menuX = trayIcon.width + Style.marginL;
|
|
menuY = 0;
|
|
} else if (barPosition === "right") {
|
|
menuX = -panelContent.trayMenu.item.width - Style.marginL;
|
|
menuY = 0;
|
|
} else if (barPosition === "bottom") {
|
|
// For bottom bar: let TrayMenu handle positioning by passing anchorY >= 0
|
|
// TrayMenu will position above the anchor item
|
|
menuX = (trayIcon.width / 2) - (panelContent.trayMenu.item.width / 2);
|
|
menuY = trayIcon.height + Style.marginL;
|
|
} else {
|
|
// For top bar: position menu below the icon with more spacing
|
|
menuX = (trayIcon.width / 2) - (panelContent.trayMenu.item.width / 2);
|
|
menuY = trayIcon.height + Style.marginL;
|
|
}
|
|
|
|
PanelService.showTrayMenu(root.screen, modelData, panelContent.trayMenu.item, trayIcon, menuX, menuY, root.widgetSection, root.widgetIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
onWheel: wheel => {
|
|
if (wheel.angleDelta.y > 0)
|
|
modelData?.scrollUp();
|
|
else if (wheel.angleDelta.y < 0)
|
|
modelData?.scrollDown();
|
|
}
|
|
|
|
onEntered: {
|
|
if (panelContent.popupMenuWindow) {
|
|
panelContent.popupMenuWindow.close();
|
|
}
|
|
TooltipService.show(trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
|
|
}
|
|
onExited: TooltipService.hide()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|