wbar: add right click menus

This commit is contained in:
end-4
2025-11-15 17:30:51 +01:00
parent 839718cc2b
commit 6ee7212bdc
11 changed files with 334 additions and 104 deletions
@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3.28 2.22a.75.75 0 0 0-1.06 1.06l5.905 5.905L4.81 10.33a1.25 1.25 0 0 0-.476 2.065L7.439 15.5 3 19.94V21h1.06l4.44-4.44 3.105 3.105a1.25 1.25 0 0 0 2.065-.476l1.145-3.313 5.905 5.904a.75.75 0 0 0 1.06-1.06L3.28 2.22Zm10.355 12.476-1.252 3.626-6.705-6.705 3.626-1.252 4.331 4.331Zm6.048-3.876-3.787 1.894 1.118 1.118 3.34-1.67a2.75 2.75 0 0 0 .714-4.404l-4.825-4.826a2.75 2.75 0 0 0-4.405.715l-1.67 3.34 1.118 1.117 1.894-3.787a1.25 1.25 0 0 1 2.002-.325l4.826 4.826a1.25 1.25 0 0 1-.325 2.002Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 622 B

@@ -0,0 +1 @@
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m16.242 2.932 4.826 4.826a2.75 2.75 0 0 1-.715 4.404l-4.87 2.435a.75.75 0 0 0-.374.426l-1.44 4.166a1.25 1.25 0 0 1-2.065.476L8.5 16.561 4.06 21H3v-1.06l4.44-4.44-3.105-3.104a1.25 1.25 0 0 1 .476-2.066l4.166-1.44a.75.75 0 0 0 .426-.373l2.435-4.87a2.75 2.75 0 0 1 4.405-.715Zm3.766 5.886-4.826-4.826a1.25 1.25 0 0 0-2.002.325l-2.435 4.871a2.25 2.25 0 0 1-1.278 1.12l-3.789 1.31 6.705 6.704 1.308-3.789a2.25 2.25 0 0 1 1.12-1.277l4.872-2.436a1.25 1.25 0 0 0 .325-2.002Z" fill="#212121"/></svg>

After

Width:  |  Height:  |  Size: 594 B

@@ -563,6 +563,10 @@ Singleton {
} }
property JsonObject waffles: JsonObject { 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 JsonObject bar: JsonObject {
property bool bottom: true property bool bottom: true
property bool leftAlignApps: false property bool leftAlignApps: false
@@ -285,4 +285,14 @@ Singleton {
} }
return str; 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();
}
);
}
} }
@@ -1,111 +1,40 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.modules.common import qs.modules.common
import qs.modules.common.functions import qs.modules.common.functions
import qs.modules.waffle.looks import qs.modules.waffle.looks
Loader { BarPopup {
id: root id: root
default property var menuData
property var model: [
{iconName: "start-here", text: "Start", action: () => {print("hello")}}
]
padding: 2
property Item anchorItem: parent contentItem: ColumnLayout {
property real visualMargin: 12 anchors.centerIn: parent
readonly property bool barAtBottom: Config.options.waffles.bar.bottom spacing: 0
property real ambientShadowWidth: 1
active: false Repeater {
visible: active model: root.model
sourceComponent: PopupWindow { delegate: WButton {
id: popupWindow id: btn
visible: true Layout.fillWidth: true
Component.onCompleted: {
openAnim.start();
}
anchor { required property var modelData
adjustment: PopupAdjustment.Slide icon.name: modelData.iconName ? modelData.iconName : ""
item: root.anchorItem monochromeIcon: modelData.monochromeIcon ?? true
gravity: root.barAtBottom ? Edges.Top : Edges.Bottom text: modelData.text ? modelData.text : ""
edges: root.barAtBottom ? Edges.Top : Edges.Bottom
}
HyprlandFocusGrab { onClicked: {
id: focusGrab if (modelData.action) modelData.action();
active: true root.close();
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;
} }
} }
} }
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
}
}
} }
} }
@@ -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]
}
}
}
@@ -1,7 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import org.kde.kirigami as Kirigami import Quickshell
import qs import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
@@ -24,10 +24,43 @@ AppButton {
} }
altAction: () => { altAction: () => {
contextMenu.active = !contextMenu.active; contextMenu.active = true;
} }
BarMenu { BarMenu {
id: contextMenu 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"]);
}
},
]
} }
} }
@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.functions
import qs.modules.waffle.looks import qs.modules.waffle.looks
import qs.modules.waffle.bar import qs.modules.waffle.bar
import Quickshell import Quickshell
@@ -16,6 +17,7 @@ AppButton {
property bool hasWindows: appEntry.toplevels.length > 0 property bool hasWindows: appEntry.toplevels.length > 0
signal hoverPreviewRequested() signal hoverPreviewRequested()
signal hoverPreviewDismissed()
multiple: appEntry.toplevels.length > 1 multiple: appEntry.toplevels.length > 1
checked: active checked: active
@@ -43,6 +45,12 @@ AppButton {
} }
} }
altAction: () => {
root.hoverPreviewDismissed()
root.hoverTimer.stop()
contextMenu.active = true;
}
// Active indicator // Active indicator
Rectangle { Rectangle {
id: activeIndicator id: activeIndicator
@@ -74,4 +82,37 @@ AppButton {
extraVisibleCondition: root.shouldShowTooltip && !root.hasWindows extraVisibleCondition: root.shouldShowTooltip && !root.hasWindows
text: desktopEntry ? desktopEntry.name : appEntry.appId 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();
}
}
}] : []),
]
}
} }
@@ -17,10 +17,6 @@ MouseArea {
previewPopup.show(appEntry, button); previewPopup.show(appEntry, button);
} }
function showContextMenu(appEntry, button) {
// TODO
}
// Apps row // Apps row
RowLayout { RowLayout {
id: row id: row
@@ -40,9 +36,8 @@ MouseArea {
onHoverPreviewRequested: { onHoverPreviewRequested: {
root.showPreviewPopup(appEntry, this) root.showPreviewPopup(appEntry, this)
} }
onHoverPreviewDismissed: {
altAction: () => { previewPopup.close()
root.showContextMenu(appEntry, this)
} }
} }
} }
@@ -55,5 +50,4 @@ MouseArea {
anchor.window: root.QsWindow.window anchor.window: root.QsWindow.window
} }
} }
@@ -61,7 +61,7 @@ Singleton {
} }
property QtObject pixelSize: QtObject { property QtObject pixelSize: QtObject {
property real normal: 11 property real normal: 11
property real large: 15 property real large: 14
} }
} }
@@ -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
}
}
}
}
}