BarWidgets: First pass on contextual widget menu accessible from right-click.

Testing on volume widget for now.
This commit is contained in:
ItsLemmy
2025-11-17 20:35:45 -05:00
parent 1b114a0c5f
commit 3283aacf9b
11 changed files with 452 additions and 124 deletions
+2 -9
View File
@@ -98,13 +98,6 @@ PopupWindow {
}
}
// Full-sized, transparent MouseArea to track the mouse.
MouseArea {
id: rootMouseArea
anchors.fill: parent
hoverEnabled: true
}
Item {
anchors.fill: parent
Keys.onEscapePressed: root.hideMenu()
@@ -186,7 +179,7 @@ PopupWindow {
Rectangle {
id: innerRect
anchors.fill: parent
color: mouseArea.containsMouse ? Color.mTertiary : Color.transparent
color: mouseArea.containsMouse ? Color.mHover : Color.transparent
radius: Style.radiusS
visible: !(modelData?.isSeparator ?? false)
@@ -199,7 +192,7 @@ PopupWindow {
NText {
id: text
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnTertiary : Color.mOnSurface) : Color.mOnSurfaceVariant
color: (modelData?.enabled ?? true) ? (mouseArea.containsMouse ? Color.mOnHover : Color.mOnSurface) : Color.mOnSurfaceVariant
text: modelData?.text !== "" ? modelData?.text.replace(/[\n\r]+/g, ' ') : "..."
pointSize: Style.fontSizeS
verticalAlignment: Text.AlignVCenter
+21 -21
View File
@@ -16,22 +16,22 @@ Rectangle {
property ShellScreen screen
// Trigger re-evaluation when window is registered
property int trayMenuUpdateTrigger: 0
property int popupMenuUpdateTrigger: 0
// Get shared tray menu window from PanelService (reactive to trigger changes)
readonly property var trayMenuWindow: {
// Get shared popup menu window from PanelService (reactive to trigger changes)
readonly property var popupMenuWindow: {
// Reference trigger to force re-evaluation
var _ = trayMenuUpdateTrigger;
return PanelService.getTrayMenuWindow(screen);
var _ = popupMenuUpdateTrigger;
return PanelService.getPopupMenuWindow(screen);
}
readonly property var trayMenu: trayMenuWindow ? trayMenuWindow.trayMenuLoader : null
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
Connections {
target: PanelService
function onTrayMenuWindowRegistered(registeredScreen) {
function onPopupMenuWindowRegistered(registeredScreen) {
if (registeredScreen === screen) {
root.trayMenuUpdateTrigger++;
root.popupMenuUpdateTrigger++;
}
}
}
@@ -183,9 +183,9 @@ Rectangle {
function toggleDrawer(button) {
TooltipService.hideImmediately();
// Close the tray menu if it's open
if (trayMenuWindow && trayMenuWindow.visible) {
trayMenuWindow.close();
// Close the popup menu if it's open
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
}
const panel = PanelService.getPanel("trayDrawerPanel", root.screen);
@@ -320,8 +320,8 @@ Rectangle {
if (mouse.button === Qt.LeftButton) {
// Close any open menu first
if (trayMenuWindow) {
trayMenuWindow.close();
if (popupMenuWindow) {
popupMenuWindow.close();
}
if (!modelData.onlyMenu) {
@@ -329,8 +329,8 @@ Rectangle {
}
} else if (mouse.button === Qt.MiddleButton) {
// Close the menu if it was visible
if (trayMenuWindow && trayMenuWindow.visible) {
trayMenuWindow.close();
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
modelData.secondaryActivate && modelData.secondaryActivate();
@@ -338,8 +338,8 @@ Rectangle {
TooltipService.hideImmediately();
// Close the menu if it was visible
if (trayMenuWindow && trayMenuWindow.visible) {
trayMenuWindow.close();
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
@@ -348,8 +348,8 @@ Rectangle {
PanelService.openedPanel.close();
}
if (modelData.hasMenu && modelData.menu && trayMenuWindow && trayMenu && trayMenu.item) {
trayMenuWindow.open();
if (modelData.hasMenu && modelData.menu && popupMenuWindow && trayMenu && trayMenu.item) {
popupMenuWindow.open();
// Position menu based on bar position
let menuX, menuY;
@@ -376,8 +376,8 @@ Rectangle {
}
}
onEntered: {
if (trayMenuWindow) {
trayMenuWindow.close();
if (popupMenuWindow) {
popupMenuWindow.close();
}
TooltipService.show(screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection());
}
+54 -4
View File
@@ -58,6 +58,10 @@ Item {
}
}
function openExternalMixer() {
Quickshell.execDetached(["sh", "-c", "pwvucontrol || pavucontrol"]);
}
Timer {
id: externalHideTimer
running: false
@@ -67,6 +71,45 @@ Item {
}
}
NPopupContextMenu {
id: contextMenu
model: [
{
"label": I18n.tr("context-menu.toggle-mute"),
"action": "toggle_mute",
"icon": AudioService.muted ? "volume-off" : "volume"
},
{
"label": I18n.tr("context-menu.open-mixer"),
"action": "open_mixer",
"icon": "adjustments"
}
// ,
// {
// "label": I18n.tr("context-menu.widget-settings"),
// "action": "widget_settings",
// "icon": "settings"
// },
]
onTriggered: action => {
if (action === "toggle_mute") {
AudioService.setOutputMuted(!AudioService.muted);
} else if (action === "open_mixer") {
root.openExternalMixer();
} else if (action === "widget_settings")
// TODO: Open widget settings
{}
// Close the popup menu window after handling the action
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
popupMenuWindow.close();
}
}
}
BarPill {
id: pill
@@ -100,10 +143,17 @@ Item {
PanelService.getPanel("audioPanel", screen)?.toggle(this);
}
onRightClicked: {
AudioService.setOutputMuted(!AudioService.muted);
}
onMiddleClicked: {
Quickshell.execDetached(["sh", "-c", "pwvucontrol || pavucontrol"]);
// Get the shared popup menu window for this screen
var popupMenuWindow = PanelService.getPopupMenuWindow(screen);
if (popupMenuWindow) {
// Calculate position using centralized helper (with center-based positioning)
const pos = BarService.getContextMenuPosition(pill, contextMenu.implicitWidth, contextMenu.implicitHeight);
// Show the context menu inside the popup window
contextMenu.openAtItem(pill, pos.x, pos.y);
popupMenuWindow.showContextMenu(contextMenu);
}
}
onMiddleClicked: root.openExternalMixer()
}
}
+3 -3
View File
@@ -95,7 +95,7 @@ Variants {
}
}
// TrayMenuWindow - separate window for tray context menus
// PopupMenuWindow - reusable popup window for both tray menus and context menus
// Disabled when bar is hidden or not configured for this screen
Loader {
active: {
@@ -108,12 +108,12 @@ Variants {
}
asynchronous: false
sourceComponent: TrayMenuWindow {
sourceComponent: PopupMenuWindow {
screen: modelData
}
onLoaded: {
Logger.d("AllScreens", "TrayMenuWindow created for", modelData?.name);
Logger.d("AllScreens", "PopupMenuWindow created for", modelData?.name);
}
}
}
+92
View File
@@ -0,0 +1,92 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Services.UI
// Generic full-screen popup window for menus and context menus
// This is a top-level PanelWindow (sibling to MainScreen, not nested inside it)
// Provides click-outside-to-close functionality for any popup content
// Loads TrayMenu by default but can show context menus via showContextMenu()
PanelWindow {
id: root
required property ShellScreen screen
property string windowType: "popupmenu" // Used for namespace and registration
// Content item to display (set by the popup that uses this window)
property var contentItem: null
// Expose the trayMenu Loader directly (for backward compatibility)
readonly property alias trayMenuLoader: trayMenuLoader
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
visible: false
color: Color.transparent
// Use Top layer (same as MainScreen) for proper event handling
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "noctalia-" + windowType + "-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// Register with PanelService so widgets can find this window
Component.onCompleted: {
objectName = "popupMenuWindow-" + (screen?.name || "unknown");
PanelService.registerPopupMenuWindow(screen, root);
}
// Load TrayMenu as the default content
Loader {
id: trayMenuLoader
source: Quickshell.shellDir + "/Modules/Bar/Extras/TrayMenu.qml"
onLoaded: {
if (item) {
item.screen = root.screen;
// Set the loaded item as default content
root.contentItem = item;
}
}
}
function open() {
visible = true;
}
// Show a context menu (temporarily replaces TrayMenu as content)
function showContextMenu(menu) {
if (menu) {
contentItem = menu;
open();
}
}
function close() {
visible = false;
// Call close/hide method on current content
if (contentItem) {
if (typeof contentItem.hideMenu === "function") {
contentItem.hideMenu();
} else if (typeof contentItem.close === "function") {
contentItem.close();
}
}
// Restore TrayMenu as default content
if (trayMenuLoader.item) {
contentItem = trayMenuLoader.item;
}
}
// Full-screen click catcher - click anywhere outside content closes the window
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.close()
}
// Content will be parented here by the popup
// (e.g., TrayMenu, NPopupContextMenu)
}
-63
View File
@@ -1,63 +0,0 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import qs.Commons
import qs.Modules.Bar.Extras
import qs.Services.UI
// Separate window for TrayMenu context menus
// This is a top-level PanelWindow (sibling to MainScreen, not nested inside it)
PanelWindow {
id: root
required property ShellScreen screen
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
visible: false
color: Color.transparent
// Use Top layer (same as MainScreen) for proper event handling
WlrLayershell.layer: WlrLayer.Top
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "noctalia-traymenu-" + (screen?.name || "unknown")
WlrLayershell.exclusionMode: ExclusionMode.Ignore
// Expose the trayMenu Loader directly
readonly property alias trayMenuLoader: trayMenu
// Register with PanelService so panels can find this window
Component.onCompleted: {
objectName = "trayMenuWindow-" + (screen?.name || "unknown");
PanelService.registerTrayMenuWindow(screen, root);
}
function open() {
visible = true;
}
function close() {
visible = false;
if (trayMenu.item) {
trayMenu.item.hideMenu();
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: root.close()
}
Loader {
id: trayMenu
source: Quickshell.shellDir + "/Modules/Bar/Extras/TrayMenu.qml"
onLoaded: {
if (item) {
item.screen = root.screen;
}
}
}
}
+13 -13
View File
@@ -100,22 +100,22 @@ SmartPanel {
}
// Trigger re-evaluation when window is registered
property int trayMenuUpdateTrigger: 0
property int popupMenuUpdateTrigger: 0
// Get the trayMenu window and loader from PanelService (reactive to trigger changes)
readonly property var trayMenuWindow: {
readonly property var popupMenuWindow: {
// Reference trigger to force re-evaluation
var _ = trayMenuUpdateTrigger;
return PanelService.getTrayMenuWindow(screen);
var _ = popupMenuUpdateTrigger;
return PanelService.getPopupMenuWindow(screen);
}
readonly property var trayMenu: trayMenuWindow ? trayMenuWindow.trayMenuLoader : null
readonly property var trayMenu: popupMenuWindow ? popupMenuWindow.trayMenuLoader : null
Connections {
target: PanelService
function onTrayMenuWindowRegistered(registeredScreen) {
function onPopupMenuWindowRegistered(registeredScreen) {
if (registeredScreen === screen) {
root.trayMenuUpdateTrigger++;
root.popupMenuUpdateTrigger++;
}
}
}
@@ -192,13 +192,13 @@ SmartPanel {
TooltipService.hideImmediately();
// Close menu if already visible
if (trayMenuWindow && trayMenuWindow.visible) {
trayMenuWindow.close();
if (popupMenuWindow && popupMenuWindow.visible) {
popupMenuWindow.close();
return;
}
if (modelData.hasMenu && modelData.menu && trayMenuWindow && trayMenu && trayMenu.item) {
trayMenuWindow.open();
if (modelData.hasMenu && modelData.menu && popupMenuWindow && trayMenu && trayMenu.item) {
popupMenuWindow.open();
// Position menu at the tray icon
const barPosition = Settings.data.bar.position;
@@ -232,8 +232,8 @@ SmartPanel {
}
onEntered: {
if (trayMenuWindow) {
trayMenuWindow.close();
if (popupMenuWindow) {
popupMenuWindow.close();
}
TooltipService.show(screen, trayIcon, modelData.tooltipTitle || modelData.name || modelData.id || "Tray Item", BarService.getTooltipDirection());
}