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
+ }
+ }
+ }
+ }
+}