diff --git a/Modules/CustomTrayMenu.qml b/Modules/CustomTrayMenu.qml index bd0503c..3684a2b 100644 --- a/Modules/CustomTrayMenu.qml +++ b/Modules/CustomTrayMenu.qml @@ -3,28 +3,13 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import QtQuick.Window // for Window, flags +import qs.Modules PopupWindow { id: popup + color: "#FF202020" - property QsMenuHandle menuHandle - property alias entries: menuModel - - QsMenuOpener { - id: menu - menu: popup.menuHandle - } - - ListModel { id: menuModel } - - implicitWidth: contentColumn.implicitWidth + 16 - implicitHeight: contentColumn.implicitHeight + 16 - - Rectangle { - color: "#202020CC" - radius: 4 - anchors.fill: parent - } + required property QsMenuOpener trayMenu Column { id: contentColumn @@ -32,11 +17,10 @@ PopupWindow { spacing: 4 Repeater { id: repeater - model: menuModel + model: popup.trayMenu.children Row { id: entryRow - height: 30 - width: parent.implicitWidth + anchors.fill: parent property var entry: modelData MouseArea { anchors.fill: parent @@ -54,33 +38,10 @@ PopupWindow { } Text { text: entryRow.entry.text - color: "white" + color: "black" anchors.verticalCenter: parent.verticalCenter } } } } - - function rebuild() { - menuModel.clear() - console.log(menu.children.count) - if (!menu) return - for (let i = 0; i < menu.children.count; ++i) { - let e = menu.children.get(i) - menuModel.append({ - text: e.text, - icon: e.icon, - triggered: e.triggered, - entryObject: e - }) - } - } - - onMenuHandleChanged: rebuild - Connections { - target: menu - function onCountChanged() { - popup.rebuild - } - } } diff --git a/Modules/TrayItem.qml b/Modules/TrayItem.qml index afb1f02..4af6330 100644 --- a/Modules/TrayItem.qml +++ b/Modules/TrayItem.qml @@ -2,12 +2,10 @@ import QtQuick import Quickshell import Quickshell.Widgets import Quickshell.Services.SystemTray -import qs.Modules IconImage { id: root required property SystemTrayItem item - property var customMenu source: root.item.icon implicitSize: 15 @@ -19,34 +17,19 @@ IconImage { case Qt.LeftButton: root.item.activate(); break; case Qt.RightButton: if (root.item.hasMenu) { - - root.customMenu = menuComponent.createObject(root); - root.customMenu.menuHandle = root.item.menu; - const window = QsWindow.window; - const widgetRect = window.contentItem.mapFromItem(root, 0, root.height + 4, root.width, root.height); - root.customMenu.anchor.rect = widgetRect - root.customMenu.anchor.window = window - root.customMenu.anchor.adjustment = PopupAdjustment.Flip - root.customMenu.visible = true; - root.customMenu.rebuild(); - // menuAnchor.anchor.rect = widgetRect; - // menuAnchor.open(); + const widgetRect = window.contentItem.mapFromItem(root, 0, root.height + 10 , root.width, root.height); + menuAnchor.anchor.rect = widgetRect; + menuAnchor.open(); } break; } } } - - Component { - id: menuComponent - CustomTrayMenu {} + QsMenuAnchor { + id: menuAnchor + menu: root.item.menu + anchor.window: root.QsWindow.window?? null + anchor.adjustment: PopupAdjustment.Flip } - - // QsMenuAnchor { - // id: menuAnchor - // menu: root.item.menu - // anchor.window: root.QsWindow.window?? null - // anchor.adjustment: PopupAdjustment.Flip - // } } diff --git a/tray/Data/Colors.qml b/tray/Data/Colors.qml new file mode 100644 index 0000000..fd1994e --- /dev/null +++ b/tray/Data/Colors.qml @@ -0,0 +1,151 @@ +pragma Singleton +import Quickshell +import Quickshell.Io +import QtQuick + +import qs.Data as Dat + +Singleton { + property var current: (true) ? dark : light + property alias dark: dark + property alias light: light + + function withAlpha(color: color, alpha: real): color { + return Qt.rgba(color.r, color.g, color.b, alpha); + } + + FileView { + path: { + const colors_location = (Quickshell.env("KURU_COLORS")); + if (colors_location) { + colors_location; + } else { + Dat.Paths.config + "/colors.json"; + } + } + watchChanges: true + + onAdapterUpdated: writeAdapter() + onFileChanged: reload() + + // writes the defualt values if file not found + onLoadFailed: err => { + if (err == FileViewError.FileNotFound) { + writeAdapter(); + } + } + + JsonAdapter { + id: adapter + + property JsonObject colors: JsonObject { + property JsonObject dark: JsonObject { + id: dark + + property string background: "#121318" + property string error: "#ffb4ab" + property string error_container: "#93000a" + property string inverse_on_surface: "#2f3036" + property string inverse_primary: "#4d5c92" + property string inverse_surface: "#e3e1e9" + property string on_background: "#e3e1e9" + property string on_error: "#690005" + property string on_error_container: "#ffdad6" + property string on_primary: "#1d2d61" + property string on_primary_container: "#dce1ff" + property string on_primary_fixed: "#04174b" + property string on_primary_fixed_variant: "#354479" + property string on_secondary: "#2b3042" + property string on_secondary_container: "#dee1f9" + property string on_secondary_fixed: "#161b2c" + property string on_secondary_fixed_variant: "#424659" + property string on_surface: "#e3e1e9" + property string on_surface_variant: "#c6c5d0" + property string on_tertiary: "#432740" + property string on_tertiary_container: "#ffd7f5" + property string on_tertiary_fixed: "#2c122a" + property string on_tertiary_fixed_variant: "#5b3d57" + property string outline: "#90909a" + property string outline_variant: "#45464f" + property string primary: "#b6c4ff" + property string primary_container: "#354479" + property string primary_fixed: "#dce1ff" + property string primary_fixed_dim: "#b6c4ff" + property string scrim: "#000000" + property string secondary: "#c2c5dd" + property string secondary_container: "#424659" + property string secondary_fixed: "#dee1f9" + property string secondary_fixed_dim: "#c2c5dd" + property string shadow: "#000000" + property string surface: "#121318" + property string surface_bright: "#38393f" + property string surface_container: "#1e1f25" + property string surface_container_high: "#292a2f" + property string surface_container_highest: "#34343a" + property string surface_container_low: "#1a1b21" + property string surface_container_lowest: "#0d0e13" + property string surface_dim: "#121318" + property string surface_tint: "#b6c4ff" + property string tertiary: "#e3bada" + property string tertiary_container: "#5b3d57" + property string tertiary_fixed: "#ffd7f5" + property string tertiary_fixed_dim: "#e3bada" + } + property JsonObject light: JsonObject { + id: light + + property string background: "#f4fafb" + property string error: "#ba1a1a" + property string error_container: "#ffdad6" + property string inverse_on_surface: "#ecf2f2" + property string inverse_primary: "#80d4da" + property string inverse_surface: "#2b3232" + property string on_background: "#161d1d" + property string on_error: "#ffffff" + property string on_error_container: "#410002" + property string on_primary: "#ffffff" + property string on_primary_container: "#002022" + property string on_primary_fixed: "#002022" + property string on_primary_fixed_variant: "#004f53" + property string on_secondary: "#ffffff" + property string on_secondary_container: "#041f21" + property string on_secondary_fixed: "#041f21" + property string on_secondary_fixed_variant: "#324b4d" + property string on_surface: "#161d1d" + property string on_surface_variant: "#3f4949" + property string on_tertiary: "#ffffff" + property string on_tertiary_container: "#091b36" + property string on_tertiary_fixed: "#091b36" + property string on_tertiary_fixed_variant: "#374764" + property string outline: "#6f7979" + property string outline_variant: "#bec8c9" + property string primary: "#00696e" + property string primary_container: "#9cf0f6" + property string primary_fixed: "#9cf0f6" + property string primary_fixed_dim: "#80d4da" + property string scrim: "#000000" + property string secondary: "#4a6365" + property string secondary_container: "#cce8e9" + property string secondary_fixed: "#cce8e9" + property string secondary_fixed_dim: "#b1cccd" + property string shadow: "#000000" + property string source_color: "#478185" + property string surface: "#f4fafb" + property string surface_bright: "#f4fafb" + property string surface_container: "#e9efef" + property string surface_container_high: "#e3e9e9" + property string surface_container_highest: "#dde4e4" + property string surface_container_low: "#eff5f5" + property string surface_container_lowest: "#ffffff" + property string surface_dim: "#d5dbdb" + property string surface_tint: "#00696e" + property string surface_variant: "#dae4e5" + property string tertiary: "#4e5f7d" + property string tertiary_container: "#d6e3ff" + property string tertiary_fixed: "#d6e3ff" + property string tertiary_fixed_dim: "#b6c7e9" + } + } + } + } +} diff --git a/tray/Data/MaterialEasing.qml b/tray/Data/MaterialEasing.qml new file mode 100644 index 0000000..dde4344 --- /dev/null +++ b/tray/Data/MaterialEasing.qml @@ -0,0 +1,27 @@ +pragma Singleton +import Quickshell + +Singleton { + id: root + + // thanks to Soramane :> + // expressive curves => thanks end cutie ;) + readonly property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + readonly property int emphasizedAccelTime: 200 + readonly property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + readonly property int emphasizedDecelTime: 400 + readonly property int emphasizedTime: 500 + readonly property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] + readonly property int expressiveDefaultSpatialTime: 500 + readonly property list expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] + readonly property int expressiveEffectsTime: 200 + readonly property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] + readonly property int expressiveFastSpatialTime: 350 + readonly property list standard: [0.2, 0, 0, 1, 1, 1] + readonly property list standardAccel: [0.3, 0, 1, 1, 1, 1] + readonly property int standardAccelTime: 200 + readonly property list standardDecel: [0, 0, 0, 1, 1, 1] + readonly property int standardDecelTime: 250 + readonly property int standardTime: 300 +} diff --git a/tray/Data/Paths.qml b/tray/Data/Paths.qml new file mode 100644 index 0000000..17d5c4e --- /dev/null +++ b/tray/Data/Paths.qml @@ -0,0 +1,53 @@ +pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Io +import Qt.labs.platform + +Singleton { + id: root + + // refer https://doc.qt.io/qt-6/qstandardpaths.html#StandardLocation-enum + // god fucking knows how soramane found this + readonly property url cache: `${StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]}/kurukurubar` + readonly property url config: `${StandardPaths.standardLocations(StandardPaths.GenericConfigLocation)[0]}/kurukurubar` + + function getPath(caller, url: string): string { + let filename = url.split('/').pop(); + let filepath = root.cache + "/" + filename; + let script = root.urlToPath(Qt.resolvedUrl("../scripts/cacheImg.sh")); + + let process = cacheImg.incubateObject(root, { + "command": ["bash", script, url, root.urlToPath(root.cache)], + "running": true + }); + + process.onStatusChanged = function (status) { + if (status != Component.Ready) { + return; + } + + process.object.exited.connect((eCode, eStat) => { + if (eCode == 0) { + caller.source = filepath; + } else { + console.log("[ERROR] cacheImg exited with error code: " + eCode); + } + process.object.destroy(); + }); + }; + + return ""; + } + + function urlToPath(url: url): string { + return url.toString().replace("file://", ""); + } + + Component { + id: cacheImg + + Process { + } + } +} diff --git a/tray/Generics/MatIcon.qml b/tray/Generics/MatIcon.qml new file mode 100644 index 0000000..7f96535 --- /dev/null +++ b/tray/Generics/MatIcon.qml @@ -0,0 +1,31 @@ +// https://m3.material.io/styles/typography/editorial-treatments#a8196c1e-387e-4303-b0bf-b9bac44e4e72 +// a thin wrapper for placing using Material Symbols +// credit to end for leading me down this route +import QtQuick +import qs.Data as Dat + +Text { + id: root + + property real fill: 0 + property int grad: 0 + required property string icon + + font.family: "Material Symbols Rounded" + font.hintingPreference: Font.PreferFullHinting + font.variableAxes: { + "FILL": root.fill, + "opsz": root.fontInfo.pixelSize, + "GRAD": root.grad, + "wght": root.fontInfo.weight + } + renderType: Text.NativeRendering + text: root.icon + + Behavior on fill { + NumberAnimation { + duration: Dat.MaterialEasing.standardTime + easing.bezierCurve: Dat.MaterialEasing.standard + } + } +} diff --git a/tray/Generics/ToggleButton.qml b/tray/Generics/ToggleButton.qml new file mode 100644 index 0000000..b1a3e46 --- /dev/null +++ b/tray/Generics/ToggleButton.qml @@ -0,0 +1,119 @@ +import QtQuick + +import qs.Data as Dat +import qs.Generics as Gen + +Rectangle { + id: root + + required property bool active + property color activeColor: Dat.Colors.current.primary + property color activeIconColor: Dat.Colors.current.on_primary + property alias icon: matIcon + property alias mArea: mouseArea + property color passiveColor: Dat.Colors.current.surface_container + property color passiveIconColor: Dat.Colors.current.on_surface + + color: "transparent" + state: (root.active) ? "ACTIVE" : "PASSIVE" + + states: [ + State { + name: "ACTIVE" + + PropertyChanges { + bgToggle.color: root.activeColor + bgToggle.opacity: 1 + bgToggle.visible: true + bgToggle.width: bgToggle.parent.width + matIcon.color: root.activeIconColor + matIcon.fill: 1 + } + }, + State { + name: "PASSIVE" + + PropertyChanges { + bgToggle.color: root.passiveColor + bgToggle.opacity: 0 + bgToggle.visible: false + bgToggle.width: 0 + matIcon.color: root.passiveIconColor + matIcon.fill: 0 + } + } + ] + transitions: [ + Transition { + from: "PASSIVE" + to: "ACTIVE" + + SequentialAnimation { + PropertyAction { + property: "visible" + target: bgToggle + } + + ParallelAnimation { + NumberAnimation { + duration: Dat.MaterialEasing.standardTime + easing.bezierCurve: Dat.MaterialEasing.standard + properties: "width, opacity" + target: bgToggle + } + + ColorAnimation { + duration: Dat.MaterialEasing.standardTime + targets: [bgToggle, matIcon] + } + } + } + }, + Transition { + from: "ACTIVE" + to: "PASSIVE" + + SequentialAnimation { + ParallelAnimation { + NumberAnimation { + duration: Dat.MaterialEasing.standardTime + easing.bezierCurve: Dat.MaterialEasing.standard + properties: "width, opacity" + target: bgToggle + } + + ColorAnimation { + duration: Dat.MaterialEasing.standardTime + targets: [bgToggle, matIcon] + } + } + + PropertyAction { + property: "visible" + target: bgToggle + } + } + } + ] + + Rectangle { + id: bgToggle + + anchors.centerIn: parent + height: this.width + radius: this.width + } + + Gen.MatIcon { + id: matIcon + + anchors.centerIn: parent + icon: "" + } + + Gen.MouseArea { + id: mouseArea + + layerColor: matIcon.color + } +} diff --git a/tray/TrayItem.qml b/tray/TrayItem.qml new file mode 100644 index 0000000..24f9ddb --- /dev/null +++ b/tray/TrayItem.qml @@ -0,0 +1,73 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Services.SystemTray + +Item { + id: root + + required property int index + property var menu: TrayItemMenu { + height: trayMenu.children.values.length * 30 + trayMenu: trayMenu + width: 500 + } + required property SystemTrayItem modelData + + implicitHeight: trayItemIcon.width + implicitWidth: this.implicitHeight + + Image { + id: trayItemIcon + + anchors.centerIn: parent + antialiasing: true + height: this.width + mipmap: true + smooth: true + source: { + // adapted from soramanew + const icon = root.modelData?.icon; + if (icon.includes("?path=")) { + const [name, path] = icon.split("?path="); + return `file://${path}/${name.slice(name.lastIndexOf("/") + 1)}`; + } + return root.modelData.icon; + } + width: 18 + + // too blurry for now + // layer.enabled: true + // layer.effect: MultiEffect { + // colorizationColor: Dat.Colors.current.secondary + // colorization: 1.0 + // antialiasing: true + // smooth: true + // } + + MouseArea { + acceptedButtons: Qt.LeftButton | Qt.RightButton + anchors.fill: parent + + onClicked: mevent => { + if (mevent.button == Qt.LeftButton) { + root.modelData.activate(); + return; + } + + if (!root.modelData.hasMenu) { + return; + } else { + root.menu.visible = true; + } + } + } + } + + QsMenuOpener { + id: trayMenu + + menu: root.modelData?.menu + } +} diff --git a/tray/TrayItemMenu.qml b/tray/TrayItemMenu.qml new file mode 100644 index 0000000..c038462 --- /dev/null +++ b/tray/TrayItemMenu.qml @@ -0,0 +1,157 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell + +import qs.Data as Dat +import qs.Generics as Gen + +PopupWindow { + id: root + + required property QsMenuOpener trayMenu + + color: Dat.Colors.current.surface_container + anchor.window: QsWindow.window + + Behavior on trayMenu { + SequentialAnimation { + NumberAnimation { + duration: Dat.MaterialEasing.standardTime + easing.bezierCurve: Dat.MaterialEasing.standard + from: 1 + property: "opacity" + target: root + to: 0 + } + + PropertyAction { + property: "trayMenu" + target: root + } + + NumberAnimation { + duration: Dat.MaterialEasing.standardDecelTime + easing.bezierCurve: Dat.MaterialEasing.standardDecel + from: 0 + property: "opacity" + target: root + to: 1 + } + } + } + + ListView { + id: view + + anchors.fill: parent + spacing: 3 + + delegate: Rectangle { + id: entry + + property var child: QsMenuOpener { + menu: entry.modelData + } + required property QsMenuEntry modelData + + color: "transparent" + height: (modelData?.isSeparator) ? 2 : 28 + radius: 20 + width: root.width + + MouseArea { + visible: (entry.modelData?.enabled && !entry.modelData?.isSeparator) ?? true + + onClicked: { + if (entry.modelData.hasChildren) { + root.trayMenu = entry.child; + view.positionViewAtBeginning(); + } else { + entry.modelData.triggered(); + } + } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: (entry.modelData?.buttonType == QsMenuButtonType.None) ? 10 : 2 + anchors.rightMargin: 10 + + Item { + Layout.fillHeight: true + implicitWidth: this.height + visible: entry.modelData?.buttonType == QsMenuButtonType.CheckBox + + Gen.MatIcon { + anchors.centerIn: parent + color: Dat.Colors.current.primary + fill: entry.modelData?.checkState == Qt.Checked + font.pixelSize: parent.width * 0.8 + icon: (entry.modelData?.checkState != Qt.Checked) ? "check_box_outline_blank" : "check_box" + } + } + + // untested cause nothing I use have radio buttons + // if you use this and find somethings wrong / "yes rexi everything is fine" lemme know by opening an issue + Item { + Layout.fillHeight: true + implicitWidth: this.height + visible: entry.modelData?.buttonType == QsMenuButtonType.RadioButton + + Gen.MatIcon { + anchors.centerIn: parent + color: Dat.Colors.current.primary + fill: entry.modelData?.checkState == Qt.Checked + font.pixelSize: parent.width * 0.8 + icon: (entry.modelData?.checkState != Qt.Checked) ? "radio_button_unchecked" : "radio_button_checked" + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + Text { + id: text + + anchors.fill: parent + color: (entry.modelData?.enabled) ? Dat.Colors.current.on_surface : Dat.Colors.current.primary + font.pointSize: 11 + text: entry.modelData?.text ?? "" + verticalAlignment: Text.AlignVCenter + } + } + + Item { + Layout.fillHeight: true + implicitWidth: this.height + visible: entry.modelData?.icon ?? false + + Image { + anchors.fill: parent + anchors.margins: 3 + fillMode: Image.PreserveAspectFit + source: entry.modelData?.icon ?? "" + } + } + + Item { + Layout.fillHeight: true + implicitWidth: this.height + visible: entry.modelData?.hasChildren ?? false + + Text { + anchors.centerIn: parent + color: Dat.Colors.current.on_surface + font.pointSize: 11 + text: "" + } + } + } + } + model: ScriptModel { + values: [...root.trayMenu?.children.values] + } + } +} diff --git a/tray/shell.qml b/tray/shell.qml new file mode 100644 index 0000000..9dab3ad --- /dev/null +++ b/tray/shell.qml @@ -0,0 +1,102 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import QtQuick +import Quickshell.Hyprland +import Quickshell.Services.SystemTray +import QtQuick.Layouts + +import qs.Data as Dat +import qs.Generics as Gen + +PanelWindow { + id: root + anchors { + top: true + right: true + left: true + } + + implicitHeight: 35 + + RowLayout { + id: trayLayout + anchors.fill: parent + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + ListView { + id: trayItemRow + + anchors.fill: parent + orientation: ListView.Horizontal + snapMode: ListView.SnapToItem + spacing: 10 + + add: Transition { + SequentialAnimation { + NumberAnimation { + duration: 0 + property: "opacity" + to: 0 + } + + PauseAnimation { + duration: addDisAni.duration / 2 + } + + NumberAnimation { + duration: Dat.MaterialEasing.emphasizedTime + easing.bezierCurve: Dat.MaterialEasing.emphasized + from: 0 + property: "opacity" + to: 1 + } + } + } + addDisplaced: Transition { + SequentialAnimation { + NumberAnimation { + id: addDisAni + + duration: Dat.MaterialEasing.emphasizedDecelTime + easing.bezierCurve: Dat.MaterialEasing.emphasizedDecel + properties: "x" + } + } + } + delegate: TrayItem { + } + model: ScriptModel { + values: [...SystemTray.items.values] + } + remove: Transition { + NumberAnimation { + id: removeAni + + duration: Dat.MaterialEasing.emphasizedTime + easing.bezierCurve: Dat.MaterialEasing.emphasized + from: 1 + property: "opacity" + to: 0 + } + } + removeDisplaced: Transition { + SequentialAnimation { + PauseAnimation { + duration: removeAni.duration / 2 + } + + NumberAnimation { + duration: Dat.MaterialEasing.emphasizedDecelTime + easing.bezierCurve: Dat.MaterialEasing.emphasizedDecel + properties: "x" + } + } + } + } + } + } +}