From 6ee7212bdce32131d523f53c3c84d12a348c6a4f Mon Sep 17 00:00:00 2001 From: end-4 <97237370+end-4@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:30:51 +0100 Subject: [PATCH] wbar: add right click menus --- .../ii/assets/icons/fluent/pin-off.svg | 1 + .../quickshell/ii/assets/icons/fluent/pin.svg | 1 + .../quickshell/ii/modules/common/Config.qml | 4 + .../modules/common/functions/StringUtils.qml | 10 ++ .../ii/modules/waffle/bar/BarMenu.qml | 115 ++++------------ .../ii/modules/waffle/bar/BarPopup.qml | 123 ++++++++++++++++++ .../ii/modules/waffle/bar/StartButton.qml | 37 +++++- .../waffle/bar/tasks/TaskAppButton.qml | 41 ++++++ .../ii/modules/waffle/bar/tasks/Tasks.qml | 10 +- .../ii/modules/waffle/looks/Looks.qml | 2 +- .../ii/modules/waffle/looks/WButton.qml | 94 +++++++++++++ 11 files changed, 334 insertions(+), 104 deletions(-) create mode 100644 dots/.config/quickshell/ii/assets/icons/fluent/pin-off.svg create mode 100644 dots/.config/quickshell/ii/assets/icons/fluent/pin.svg create mode 100644 dots/.config/quickshell/ii/modules/waffle/bar/BarPopup.qml create mode 100644 dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/pin-off.svg b/dots/.config/quickshell/ii/assets/icons/fluent/pin-off.svg new file mode 100644 index 000000000..046a27cea --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/pin-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/assets/icons/fluent/pin.svg b/dots/.config/quickshell/ii/assets/icons/fluent/pin.svg new file mode 100644 index 000000000..b31790338 --- /dev/null +++ b/dots/.config/quickshell/ii/assets/icons/fluent/pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index eb995df55..d5169e701 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -563,6 +563,10 @@ Singleton { } property JsonObject waffles: JsonObject { + // Animations on Windoes are kinda janky. Set the following to + // false will make (some) stuff also be like that for accuracy. + // Example: the right-click menu of the Start button + property bool smootherAnimations: true property JsonObject bar: JsonObject { property bool bottom: true property bool leftAlignApps: false diff --git a/dots/.config/quickshell/ii/modules/common/functions/StringUtils.qml b/dots/.config/quickshell/ii/modules/common/functions/StringUtils.qml index 88ab95b2d..0839b5386 100644 --- a/dots/.config/quickshell/ii/modules/common/functions/StringUtils.qml +++ b/dots/.config/quickshell/ii/modules/common/functions/StringUtils.qml @@ -285,4 +285,14 @@ Singleton { } return str; } + + function toTitleCase(str) { + // Replace "-" and "_" with space, then capitalize each word + return str.replace(/[-_]/g, " ").replace( + /\w\S*/g, + function(txt) { + return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); + } + ); + } } diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/BarMenu.qml b/dots/.config/quickshell/ii/modules/waffle/bar/BarMenu.qml index 2dbda095d..14a9bc0ec 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/BarMenu.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/BarMenu.qml @@ -1,111 +1,40 @@ import QtQuick import QtQuick.Controls +import QtQuick.Layouts import Quickshell import Quickshell.Hyprland import qs.modules.common import qs.modules.common.functions import qs.modules.waffle.looks -Loader { +BarPopup { id: root + default property var menuData + property var model: [ + {iconName: "start-here", text: "Start", action: () => {print("hello")}} + ] + padding: 2 - property Item anchorItem: parent - property real visualMargin: 12 - readonly property bool barAtBottom: Config.options.waffles.bar.bottom - property real ambientShadowWidth: 1 + contentItem: ColumnLayout { + anchors.centerIn: parent + spacing: 0 - active: false - visible: active - sourceComponent: PopupWindow { - id: popupWindow - visible: true - Component.onCompleted: { - openAnim.start(); - } + Repeater { + model: root.model + delegate: WButton { + id: btn + Layout.fillWidth: true - anchor { - adjustment: PopupAdjustment.Slide - item: root.anchorItem - gravity: root.barAtBottom ? Edges.Top : Edges.Bottom - edges: root.barAtBottom ? Edges.Top : Edges.Bottom - } + required property var modelData + icon.name: modelData.iconName ? modelData.iconName : "" + monochromeIcon: modelData.monochromeIcon ?? true + text: modelData.text ? modelData.text : "" - HyprlandFocusGrab { - id: focusGrab - active: true - windows: [popupWindow] - onCleared: { - closeAnim.start(); - } - } - - implicitWidth: realContent.implicitWidth + (ambientShadow.border.width * 2) + (root.visualMargin * 2) - implicitHeight: realContent.implicitHeight + (ambientShadow.border.width * 2) + (root.visualMargin * 2) - - property real sourceEdgeMargin: -implicitHeight - PropertyAnimation { - id: openAnim - target: popupWindow - property: "sourceEdgeMargin" - to: (root.ambientShadowWidth + root.visualMargin) - duration: 200 - easing.type: Easing.BezierSpline - easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn - } - SequentialAnimation { - id: closeAnim - PropertyAnimation { - target: popupWindow - property: "sourceEdgeMargin" - to: -implicitHeight - duration: 150 - easing.type: Easing.BezierSpline - easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut - } - ScriptAction { - script: { - root.active = false; + onClicked: { + if (modelData.action) modelData.action(); + root.close(); } } } - - color: "transparent" - Rectangle { - id: ambientShadow - z: 0 - anchors { - fill: realContent - margins: -border.width - } - border.color: ColorUtils.transparentize(Looks.colors.bg0Border, Looks.shadowTransparency) - border.width: root.ambientShadowWidth - color: "transparent" - radius: realContent.radius + border.width - } - - Rectangle { - id: realContent - z: 1 - anchors { - left: parent.left - right: parent.right - top: root.barAtBottom ? undefined : parent.top - bottom: root.barAtBottom ? parent.bottom : undefined - margins: root.ambientShadowWidth + root.visualMargin - // Opening anim - bottomMargin: root.barAtBottom ? popupWindow.sourceEdgeMargin : (root.ambientShadowWidth + root.visualMargin) - topMargin: root.barAtBottom ? (root.ambientShadowWidth + root.visualMargin) : popupWindow.sourceEdgeMargin - } - color: Looks.colors.bg1 - radius: Looks.radius.large - - // test - implicitWidth: 300 - implicitHeight: 400 - - Menu { - id: menu - } - } } } diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/BarPopup.qml b/dots/.config/quickshell/ii/modules/waffle/bar/BarPopup.qml new file mode 100644 index 000000000..30f695367 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/bar/BarPopup.qml @@ -0,0 +1,123 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Hyprland +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +Loader { + id: root + + required property var contentItem + property real padding: Looks.radius.large - Looks.radius.medium + property bool noSmoothClosing: !Config.options.waffles.smootherAnimations + + property Item anchorItem: parent + property real visualMargin: 12 + readonly property bool barAtBottom: Config.options.waffles.bar.bottom + property real ambientShadowWidth: 1 + + function close() { + item.close(); + } + + active: false + visible: active + sourceComponent: PopupWindow { + id: popupWindow + visible: true + Component.onCompleted: { + openAnim.start(); + } + + anchor { + adjustment: PopupAdjustment.ResizeY | PopupAdjustment.SlideX + item: root.anchorItem + gravity: root.barAtBottom ? Edges.Top : Edges.Bottom + edges: root.barAtBottom ? Edges.Top : Edges.Bottom + } + + HyprlandFocusGrab { + id: focusGrab + active: true + windows: [popupWindow] + onCleared: { + root.close() + } + } + + function close() { + if (root.noSmoothClosing) root.active = false; + else closeAnim.start(); + } + + implicitWidth: realContent.implicitWidth + (ambientShadow.border.width * 2) + (root.visualMargin * 2) + implicitHeight: realContent.implicitHeight + (ambientShadow.border.width * 2) + (root.visualMargin * 2) + + property real sourceEdgeMargin: -implicitHeight + PropertyAnimation { + id: openAnim + target: popupWindow + property: "sourceEdgeMargin" + to: (root.ambientShadowWidth + root.visualMargin) + duration: 200 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeIn + } + SequentialAnimation { + id: closeAnim + PropertyAnimation { + target: popupWindow + property: "sourceEdgeMargin" + to: -implicitHeight + duration: 150 + easing.type: Easing.BezierSpline + easing.bezierCurve: Looks.transition.easing.bezierCurve.easeOut + } + ScriptAction { + script: { + root.active = false; + } + } + } + + color: "transparent" + Rectangle { + id: ambientShadow + z: 0 + anchors { + fill: realContent + margins: -border.width + } + border.color: ColorUtils.transparentize(Looks.colors.bg0Border, Looks.shadowTransparency) + border.width: root.ambientShadowWidth + color: "transparent" + radius: realContent.radius + border.width + } + + Rectangle { + id: realContent + z: 1 + anchors { + left: parent.left + right: parent.right + top: root.barAtBottom ? undefined : parent.top + bottom: root.barAtBottom ? parent.bottom : undefined + margins: root.ambientShadowWidth + root.visualMargin + // Opening anim + bottomMargin: root.barAtBottom ? popupWindow.sourceEdgeMargin : (root.ambientShadowWidth + root.visualMargin) + topMargin: root.barAtBottom ? (root.ambientShadowWidth + root.visualMargin) : popupWindow.sourceEdgeMargin + } + color: Looks.colors.bg1 + radius: Looks.radius.large + + // test + implicitWidth: root.contentItem.implicitWidth + (root.padding * 2) + implicitHeight: root.contentItem.implicitHeight + (root.padding * 2) + + children: [root.contentItem] + } + } +} diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml index a7911d181..1f2ed8342 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/StartButton.qml @@ -1,7 +1,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import org.kde.kirigami as Kirigami +import Quickshell import qs import qs.services import qs.modules.common @@ -24,10 +24,43 @@ AppButton { } altAction: () => { - contextMenu.active = !contextMenu.active; + contextMenu.active = true; } BarMenu { id: contextMenu + + model: [ + { + text: Translation.tr("Terminal"), + action: () => { + Quickshell.execDetached(["bash", "-c", Config.options.apps.terminal]); + } + }, + { + text: Translation.tr("Task Manager"), + action: () => { + Quickshell.execDetached(["bash", "-c", Config.options.apps.taskManager]); + } + }, + { + text: Translation.tr("Settings"), + action: () => { + Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("settings.qml")]); + } + }, + { + text: Translation.tr("File Explorer"), + action: () => { + Qt.openUrlExternally(Directories.home); + } + }, + { + text: Translation.tr("Search"), + action: () => { + Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "overview", "toggle"]); + } + }, + ] } } diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/TaskAppButton.qml b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/TaskAppButton.qml index ce7349a97..58f5a9959 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/TaskAppButton.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/TaskAppButton.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.services import qs.modules.common +import qs.modules.common.functions import qs.modules.waffle.looks import qs.modules.waffle.bar import Quickshell @@ -16,6 +17,7 @@ AppButton { property bool hasWindows: appEntry.toplevels.length > 0 signal hoverPreviewRequested() + signal hoverPreviewDismissed() multiple: appEntry.toplevels.length > 1 checked: active @@ -43,6 +45,12 @@ AppButton { } } + altAction: () => { + root.hoverPreviewDismissed() + root.hoverTimer.stop() + contextMenu.active = true; + } + // Active indicator Rectangle { id: activeIndicator @@ -74,4 +82,37 @@ AppButton { extraVisibleCondition: root.shouldShowTooltip && !root.hasWindows text: desktopEntry ? desktopEntry.name : appEntry.appId } + + BarMenu { + id: contextMenu + + model: [ + { + iconName: root.iconName, + text: root.desktopEntry ? root.desktopEntry.name : StringUtils.toTitleCase(appEntry.appId), + monochromeIcon: false, + action: () => { + if (root.desktopEntry) { + root.desktopEntry.execute() + } + } + }, + { + iconName: root.appEntry.pinned ? "pin-off" : "pin", + text: root.appEntry.pinned ? qsTr("Unpin from taskbar") : qsTr("Pin to taskbar"), + action: () => { + TaskbarApps.togglePin(root.appEntry.appId); + } + }, + ...(root.appEntry.toplevels.length > 0 ? [{ + iconName: "dismiss", + text: root.multiple ? qsTr("Close all windows") : qsTr("Close window"), + action: () => { + for (let toplevel of root.appEntry.toplevels) { + toplevel.close(); + } + } + }] : []), + ] + } } diff --git a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml index 260c1c37f..ad42a93fa 100644 --- a/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml +++ b/dots/.config/quickshell/ii/modules/waffle/bar/tasks/Tasks.qml @@ -17,10 +17,6 @@ MouseArea { previewPopup.show(appEntry, button); } - function showContextMenu(appEntry, button) { - // TODO - } - // Apps row RowLayout { id: row @@ -40,9 +36,8 @@ MouseArea { onHoverPreviewRequested: { root.showPreviewPopup(appEntry, this) } - - altAction: () => { - root.showContextMenu(appEntry, this) + onHoverPreviewDismissed: { + previewPopup.close() } } } @@ -55,5 +50,4 @@ MouseArea { anchor.window: root.QsWindow.window } - } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml index 09176830f..8ece22ea5 100644 --- a/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml +++ b/dots/.config/quickshell/ii/modules/waffle/looks/Looks.qml @@ -61,7 +61,7 @@ Singleton { } property QtObject pixelSize: QtObject { property real normal: 11 - property real large: 15 + property real large: 14 } } diff --git a/dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml b/dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml new file mode 100644 index 000000000..5583fd48f --- /dev/null +++ b/dots/.config/quickshell/ii/modules/waffle/looks/WButton.qml @@ -0,0 +1,94 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import qs.modules.common +import qs.modules.common.functions +import qs.modules.waffle.looks + +// Generic button with background +Button { + id: root + + property color colBackgroundHover: Looks.colors.bg2Hover + property color colBackgroundActive: Looks.colors.bg2Active + property color colBackground: ColorUtils.transparentize(Looks.colors.bg1) + + property alias monochromeIcon: buttonIcon.monochrome + + property var altAction: () => {} + property var middleClickAction: () => {} + + property real inset: 2 + topInset: inset + bottomInset: inset + leftInset: inset + rightInset: inset + horizontalPadding: 10 + verticalPadding: 6 + implicitHeight: contentItem.implicitHeight + verticalPadding * 2 + implicitWidth: contentItem.implicitWidth + horizontalPadding * 2 + + background: Rectangle { + radius: Looks.radius.medium + color: { + if (root.down) { + return root.colBackgroundActive; + } else if ((root.hovered && !root.down) || root.checked) { + return root.colBackgroundHover; + } else { + return root.colBackground; + } + } + Behavior on color { + animation: Looks.transition.color.createObject(this) + } + } + + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton | Qt.MiddleButton + onClicked: (event) => { + if (event.button === Qt.LeftButton) root.clicked(); + if (event.button === Qt.RightButton) root.altAction(); + if (event.button === Qt.MiddleButton) root.middleClickAction(); + } + } + + contentItem: Item { + anchors { + fill: parent + margins: root.inset + } + implicitWidth: contentLayout.implicitWidth + implicitHeight: contentLayout.implicitHeight + RowLayout { + id: contentLayout + anchors { + fill: parent + leftMargin: root.horizontalPadding + rightMargin: root.horizontalPadding + } + spacing: 12 + FluentIcon { + id: buttonIcon + monochrome: true + implicitSize: 16 + Layout.leftMargin: 6 + Layout.fillWidth: false + Layout.alignment: Qt.AlignVCenter + visible: root.icon.name !== "" + icon: root.icon.name + } + WText { + Layout.rightMargin: 12 + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + text: root.text + horizontalAlignment: Text.AlignLeft + font { + pixelSize: Looks.font.pixelSize.large + } + } + } + } +}