fedora
This commit is contained in:
@@ -0,0 +1,580 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property ShellScreen screen
|
||||
|
||||
// Trigger re-evaluation when window is registered
|
||||
property int popupMenuUpdateTrigger: 0
|
||||
|
||||
// Get shared popup menu window from PanelService (reactive to trigger changes)
|
||||
readonly property var popupMenuWindow: {
|
||||
// Reference trigger to force re-evaluation
|
||||
var popupMenuUpdateTriggerRef = popupMenuUpdateTrigger;
|
||||
return PanelService.getPopupMenuWindow(screen);
|
||||
}
|
||||
|
||||
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
|
||||
|
||||
Connections {
|
||||
target: PanelService
|
||||
function onPopupMenuWindowRegistered(registeredScreen) {
|
||||
if (registeredScreen === screen) {
|
||||
root.popupMenuUpdateTrigger++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Widget properties passed from Bar.qml for per-instance settings
|
||||
property string widgetId: ""
|
||||
property string section: ""
|
||||
property int sectionWidgetIndex: -1
|
||||
property int sectionWidgetsCount: 0
|
||||
|
||||
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] ?? {}
|
||||
// Explicit screenName property ensures reactive binding when screen changes
|
||||
readonly property string screenName: screen ? screen.name : ""
|
||||
property var widgetSettings: {
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var widgets = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (widgets && sectionWidgetIndex < widgets.length) {
|
||||
return widgets[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
readonly property string barPosition: Settings.getBarPositionForScreen(screenName)
|
||||
readonly property bool isVertical: barPosition === "left" || barPosition === "right"
|
||||
readonly property real barHeight: Style.getBarHeightForScreen(screenName)
|
||||
readonly property real capsuleHeight: Style.getCapsuleHeightForScreen(screenName)
|
||||
readonly property bool density: Settings.data.bar.density
|
||||
readonly property int iconSize: Style.toOdd(capsuleHeight * 0.65)
|
||||
|
||||
property var blacklist: widgetSettings.blacklist || widgetMetadata.blacklist || [] // Read from settings
|
||||
property var pinned: widgetSettings.pinned || widgetMetadata.pinned || [] // Pinned items (shown inline)
|
||||
property bool drawerEnabled: widgetSettings.drawerEnabled !== undefined ? widgetSettings.drawerEnabled : (widgetMetadata.drawerEnabled !== undefined ? widgetMetadata.drawerEnabled : true) // Enable drawer panel
|
||||
property bool hidePassive: widgetSettings.hidePassive !== undefined ? widgetSettings.hidePassive : true // Hide passive status items
|
||||
readonly property string chevronColorKey: widgetSettings.chevronColor !== undefined ? widgetSettings.chevronColor : widgetMetadata.chevronColor
|
||||
readonly property color chevronColor: Color.resolveColorKey(chevronColorKey)
|
||||
property var filteredItems: [] // Items to show inline (pinned)
|
||||
property var dropdownItems: [] // Items to show in drawer (unpinned)
|
||||
property int hoveredItemIndex: -1 // Track hovered item for dot indicator
|
||||
|
||||
Timer {
|
||||
id: updateDebounceTimer
|
||||
interval: 100 // milliseconds
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: _performFilteredItemsUpdate()
|
||||
}
|
||||
|
||||
readonly property var statusSignature: {
|
||||
if (!SystemTray.items || !SystemTray.items.values) {
|
||||
return "";
|
||||
}
|
||||
var sig = "";
|
||||
var items = SystemTray.items.values;
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
var item = items[i];
|
||||
if (item) {
|
||||
// Direct property access creates reactive binding
|
||||
var s = item.status;
|
||||
sig += (item.id || i) + ":" + (s !== undefined ? s : -1);
|
||||
}
|
||||
}
|
||||
// Trigger update when signature changes (status changed)
|
||||
if (root.hidePassive) {
|
||||
Qt.callLater(root.updateFilteredItems);
|
||||
}
|
||||
return sig;
|
||||
}
|
||||
Repeater {
|
||||
id: statusConnectionsRepeater
|
||||
model: SystemTray.items && SystemTray.items.values ? SystemTray.items.values : []
|
||||
|
||||
delegate: Item {
|
||||
Connections {
|
||||
target: modelData
|
||||
enabled: modelData !== null && modelData !== undefined
|
||||
function onStatusChanged() {
|
||||
if (root.hidePassive) {
|
||||
root.updateFilteredItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _performFilteredItemsUpdate() {
|
||||
// Force a fresh read of settings to ensure we have the latest blacklist
|
||||
var currentSettings = {};
|
||||
if (section && sectionWidgetIndex >= 0 && screenName) {
|
||||
var w = Settings.getBarWidgetsForScreen(screenName)[section];
|
||||
if (w && sectionWidgetIndex < w.length) {
|
||||
currentSettings = w[sectionWidgetIndex];
|
||||
}
|
||||
}
|
||||
|
||||
// Update local properties with fresh data
|
||||
if (currentSettings.blacklist !== undefined)
|
||||
root.blacklist = currentSettings.blacklist;
|
||||
if (currentSettings.pinned !== undefined)
|
||||
root.pinned = currentSettings.pinned;
|
||||
|
||||
let newItems = [];
|
||||
if (SystemTray.items && SystemTray.items.values) {
|
||||
const trayItems = SystemTray.items.values;
|
||||
for (var i = 0; i < trayItems.length; i++) {
|
||||
const item = trayItems[i];
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = item.tooltipTitle || item.name || item.id || "";
|
||||
|
||||
// Skip passive items if hidePassive is enabled
|
||||
if (root.hidePassive && item.status !== undefined && (item.status === SystemTray.Passive || item.status === 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if blacklisted
|
||||
let isBlacklisted = false;
|
||||
if (root.blacklist && root.blacklist.length > 0) {
|
||||
for (var j = 0; j < root.blacklist.length; j++) {
|
||||
const rule = root.blacklist[j];
|
||||
if (wildCardMatch(title, rule)) {
|
||||
isBlacklisted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isBlacklisted) {
|
||||
newItems.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If drawer is disabled, show all items inline
|
||||
if (!root.drawerEnabled) {
|
||||
filteredItems = newItems;
|
||||
dropdownItems = [];
|
||||
} else {
|
||||
// Build inline (pinned) and drawer (unpinned) lists
|
||||
// If pinned list is empty, all items go to drawer (none inline)
|
||||
// If pinned list has items, pinned items are inline, rest go to drawer
|
||||
if (pinned && pinned.length > 0) {
|
||||
let pinnedItems = [];
|
||||
for (var k = 0; k < newItems.length; k++) {
|
||||
const item2 = newItems[k];
|
||||
const title2 = item2.tooltipTitle || item2.name || item2.id || "";
|
||||
for (var m = 0; m < pinned.length; m++) {
|
||||
const rule2 = pinned[m];
|
||||
if (wildCardMatch(title2, rule2)) {
|
||||
pinnedItems.push(item2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
filteredItems = pinnedItems;
|
||||
|
||||
// Unpinned items go to drawer
|
||||
let unpinnedItems = [];
|
||||
for (var v = 0; v < newItems.length; v++) {
|
||||
const cand = newItems[v];
|
||||
let isPinned = false;
|
||||
for (var f = 0; f < filteredItems.length; f++) {
|
||||
if (filteredItems[f] === cand) {
|
||||
isPinned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isPinned)
|
||||
unpinnedItems.push(cand);
|
||||
}
|
||||
dropdownItems = unpinnedItems;
|
||||
} else {
|
||||
// No pinned items: all items go to drawer (none inline)
|
||||
filteredItems = [];
|
||||
dropdownItems = newItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFilteredItems() {
|
||||
updateDebounceTimer.restart();
|
||||
}
|
||||
|
||||
function wildCardMatch(str, rule) {
|
||||
if (!str || !rule) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// First, convert '*' to a placeholder to preserve it, then escape other special regex characters
|
||||
// Use a unique placeholder that won't appear in normal strings
|
||||
const placeholder = '\uE000'; // Private use character
|
||||
let processedRule = rule.replace(/\*/g, placeholder);
|
||||
// Escape all special regex characters (but placeholder won't match this)
|
||||
let escapedRule = processedRule.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
||||
// Convert placeholder back to '.*' for wildcard matching
|
||||
let pattern = escapedRule.replace(new RegExp(placeholder, 'g'), '.*');
|
||||
// Add ^ and $ to match the entire string
|
||||
pattern = '^' + pattern + '$';
|
||||
|
||||
try {
|
||||
const regex = new RegExp(pattern, 'i');
|
||||
// 'i' for case-insensitive
|
||||
return regex.test(str);
|
||||
} catch (e) {
|
||||
Logger.w("Tray", "Invalid regex pattern for wildcard match:", rule, e.message);
|
||||
return false; // If regex is invalid, it won't match
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDrawer(button) {
|
||||
TooltipService.hideImmediately();
|
||||
|
||||
// Close the popup menu if it's open
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
|
||||
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
|
||||
if (panel) {
|
||||
panel.widgetSection = root.section;
|
||||
panel.widgetIndex = root.sectionWidgetIndex;
|
||||
panel.toggle(this);
|
||||
}
|
||||
}
|
||||
|
||||
function onLoaded() {
|
||||
// When the widget is fully initialized with its props set the screen for the trayMenu
|
||||
if (trayMenu && trayMenu.item) {
|
||||
trayMenu.item.screen = screen;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SystemTray.items
|
||||
function onValuesChanged() {
|
||||
root.updateFilteredItems();
|
||||
// Repeater will automatically update when items change
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings
|
||||
function onSettingsSaved() {
|
||||
root.updateFilteredItems();
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for hidePassive changes to update filtering immediately
|
||||
onHidePassiveChanged: {
|
||||
root.updateFilteredItems();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.updateFilteredItems(); // Initial update
|
||||
}
|
||||
|
||||
// Content dimensions for implicit sizing
|
||||
readonly property int visibleItemCount: (root.drawerEnabled && dropdownItems.length > 0 ? 1 : 0) + filteredItems.length
|
||||
readonly property real capsulePadding: 0
|
||||
readonly property real capsuleWidth: isVertical ? capsuleHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2)
|
||||
readonly property real capsuleContentHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : capsuleHeight
|
||||
|
||||
implicitWidth: isVertical ? barHeight : Math.round(trayFlow.implicitWidth + capsulePadding * 2)
|
||||
implicitHeight: isVertical ? Math.round(trayFlow.implicitHeight + capsulePadding * 2) : barHeight
|
||||
visible: filteredItems.length > 0 || dropdownItems.length > 0
|
||||
opacity: (filteredItems.length > 0 || dropdownItems.length > 0) ? 1.0 : 0.0
|
||||
|
||||
// Visual capsule centered in parent
|
||||
Rectangle {
|
||||
id: visualCapsule
|
||||
width: capsuleWidth
|
||||
height: capsuleContentHeight
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
radius: Style.radiusM
|
||||
color: Style.capsuleColor
|
||||
border.color: Style.capsuleBorderColor
|
||||
border.width: Style.capsuleBorderWidth
|
||||
}
|
||||
|
||||
NPopupContextMenu {
|
||||
id: chevronContextMenu
|
||||
|
||||
model: [
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
]
|
||||
|
||||
onTriggered: action => {
|
||||
chevronContextMenu.close();
|
||||
PanelService.closeContextMenu(screen);
|
||||
|
||||
if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Flow {
|
||||
id: trayFlow
|
||||
spacing: 0
|
||||
flow: isVertical ? Flow.TopToBottom : Flow.LeftToRight
|
||||
|
||||
// Position centered in capsule
|
||||
anchors.centerIn: visualCapsule
|
||||
|
||||
// Drawer opener (before items if opposite direction)
|
||||
NIconButton {
|
||||
id: chevronIconBefore
|
||||
visible: root.drawerEnabled && dropdownItems.length > 0 && BarService.getPillDirection(root)
|
||||
width: isVertical ? barHeight : capsuleHeight
|
||||
height: isVertical ? capsuleHeight : barHeight
|
||||
tooltipText: {
|
||||
if (PanelService.getPanel("trayDrawerPanel", root.screen)?.isPanelOpen) {
|
||||
return "";
|
||||
} else {
|
||||
return I18n.tr("tooltips.open-tray-dropdown");
|
||||
}
|
||||
}
|
||||
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
|
||||
baseSize: capsuleHeight
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: "transparent"
|
||||
colorFg: root.chevronColor
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
icon: {
|
||||
switch (barPosition) {
|
||||
case "bottom":
|
||||
return "caret-up";
|
||||
case "left":
|
||||
return "caret-right";
|
||||
case "right":
|
||||
return "caret-left";
|
||||
case "top":
|
||||
default:
|
||||
return "caret-down";
|
||||
}
|
||||
}
|
||||
onClicked: toggleDrawer(this)
|
||||
onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen)
|
||||
}
|
||||
|
||||
// Pinned items
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: root.filteredItems
|
||||
|
||||
delegate: Item {
|
||||
id: trayDelegate
|
||||
required property var modelData
|
||||
required property int index
|
||||
width: isVertical ? barHeight : capsuleHeight
|
||||
height: isVertical ? capsuleHeight : barHeight
|
||||
visible: modelData
|
||||
readonly property bool isHovered: root.hoveredItemIndex === index
|
||||
|
||||
// Tooltip anchor representing the visual area (for proper tooltip positioning)
|
||||
Item {
|
||||
id: tooltipAnchor
|
||||
width: capsuleHeight
|
||||
height: capsuleHeight
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: trayIcon
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
x: Style.pixelAlignCenter(parent.width, width)
|
||||
y: Style.pixelAlignCenter(parent.height, height)
|
||||
asynchronous: true
|
||||
backer.fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: {
|
||||
let icon = modelData?.icon || "";
|
||||
if (!icon) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Process icon path
|
||||
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;
|
||||
}
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
|
||||
layer.enabled: 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")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: hoverIndicator
|
||||
anchors.bottom: trayIcon.bottom
|
||||
anchors.bottomMargin: -2
|
||||
anchors.horizontalCenter: trayIcon.horizontalCenter
|
||||
width: Style.toOdd(iconSize * 0.25)
|
||||
height: 4
|
||||
color: trayDelegate.isHovered ? Color.mHover : "transparent"
|
||||
radius: Math.min(Style.radiusXXS, width / 2)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Style.animationFast
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: itemMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
root.hoveredItemIndex = trayDelegate.index;
|
||||
TooltipService.show(tooltipAnchor, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection(root.screen?.name));
|
||||
} else if (root.hoveredItemIndex === trayDelegate.index) {
|
||||
root.hoveredItemIndex = -1;
|
||||
TooltipService.hide(tooltipAnchor);
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (!modelData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Close any open menu first
|
||||
if (popupMenuWindow) {
|
||||
popupMenuWindow.close();
|
||||
}
|
||||
|
||||
if (!modelData.onlyMenu) {
|
||||
modelData.activate();
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
// Close the menu if it was visible
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
return;
|
||||
}
|
||||
modelData.secondaryActivate && modelData.secondaryActivate();
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
TooltipService.hideImmediately();
|
||||
|
||||
// Close the menu if it was visible
|
||||
if (popupMenuWindow && popupMenuWindow.visible) {
|
||||
popupMenuWindow.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// Close any opened panel
|
||||
if ((PanelService.openedPanel !== null) && !PanelService.openedPanel.isClosing) {
|
||||
PanelService.openedPanel.close();
|
||||
}
|
||||
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu && trayMenu.item) {
|
||||
// Calculate menu position after ensuring menu is loaded
|
||||
const calculateAndShow = () => {
|
||||
// Position menu based on bar position, using tooltipAnchor for proper positioning
|
||||
// Increased spacing for better alignment with other context menus
|
||||
let menuX, menuY;
|
||||
if (barPosition === "left") {
|
||||
// For left bar: position menu to the right of the visual area
|
||||
menuX = tooltipAnchor.width + Style.marginL;
|
||||
menuY = 0;
|
||||
} else if (barPosition === "right") {
|
||||
// For right bar: position menu to the left of the visual area
|
||||
menuX = -trayMenu.item.implicitWidth - Style.marginL;
|
||||
menuY = 0;
|
||||
} else {
|
||||
// For horizontal bars: center horizontally and position below visual area
|
||||
menuX = (tooltipAnchor.width / 2) - (trayMenu.item.implicitWidth / 2);
|
||||
menuY = tooltipAnchor.height + Style.marginS;
|
||||
}
|
||||
|
||||
PanelService.showTrayMenu(root.screen, modelData, trayMenu.item, tooltipAnchor, menuX, menuY, root.section, root.sectionWidgetIndex);
|
||||
};
|
||||
|
||||
// Use Qt.callLater to ensure menu dimensions are calculated
|
||||
Qt.callLater(calculateAndShow);
|
||||
} else {
|
||||
Logger.d("Tray", "No menu available for", modelData.id, "or trayMenu not set");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drawer opener (after items if normal direction)
|
||||
NIconButton {
|
||||
id: chevronIconAfter
|
||||
visible: root.drawerEnabled && dropdownItems.length > 0 && !BarService.getPillDirection(root)
|
||||
width: isVertical ? barHeight : capsuleHeight
|
||||
height: isVertical ? capsuleHeight : barHeight
|
||||
tooltipText: I18n.tr("tooltips.open-tray-dropdown")
|
||||
tooltipDirection: BarService.getTooltipDirection(root.screen?.name)
|
||||
baseSize: capsuleHeight
|
||||
applyUiScale: false
|
||||
customRadius: Style.radiusL
|
||||
colorBg: "transparent"
|
||||
colorFg: root.chevronColor
|
||||
colorBorder: "transparent"
|
||||
colorBorderHover: "transparent"
|
||||
icon: {
|
||||
switch (barPosition) {
|
||||
case "bottom":
|
||||
return "caret-up";
|
||||
case "left":
|
||||
return "caret-right";
|
||||
case "right":
|
||||
return "caret-left";
|
||||
case "top":
|
||||
default:
|
||||
return "caret-down";
|
||||
}
|
||||
}
|
||||
onClicked: toggleDrawer(this)
|
||||
onRightClicked: PanelService.showContextMenu(chevronContextMenu, this, screen)
|
||||
}
|
||||
} // closes Flow
|
||||
}
|
||||
Reference in New Issue
Block a user