diff --git a/modules/style/quickshell/shell/AudioPopup.qml b/modules/style/quickshell/shell/AudioPopup.qml new file mode 100644 index 0000000..b8e27ee --- /dev/null +++ b/modules/style/quickshell/shell/AudioPopup.qml @@ -0,0 +1,103 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import QtQuick.Layouts +import Quickshell.Wayland +import Quickshell.Services.Pipewire + +Singleton { + id: audioPopup + property bool popupOpen: true + + property var volume: sink.ready ? audioPopup.sink.audio.volume : 0 + + property var visible: volume + + property PwNode sink: Pipewire.defaultAudioSink + + // bind the node so we can read its properties + PwObjectTracker { + objects: [audioPopup.sink] + } + + Timer { + id: timer + interval: 3000 + running: false + repeat: false + + onTriggered: audioPopup.visible = false + } + + LazyLoader { + id: loader + activeAsync: audioPopup.popupOpen + + PanelWindow { + id: popup + width: 400 + height: 30 + visible: true + + // Give the window an empty click mask so all clicks pass through it. + mask: Region {} + + // Use the wlroots specific layer property to ensure it displays over + // fullscreen windows. + WlrLayershell.layer: WlrLayer.Overlay + + color: "transparent" + + anchors { + bottom: true + } + + margins { + bottom: 250 + } + + Rectangle { + id: rect + Layout.fillWidth: true + anchors.verticalCenter: parent.verticalCenter + color: "white" + height: parent.height + width: parent.width + radius: 5 + opacity: 0 + + anchors { + left: parent.left + } + + Behavior on width { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + + Rectangle { + color: "black" + height: 20 + radius: height / 2 + + anchors { + left: parent.left + } + + topLeftRadius: 0 + bottomLeftRadius: 0 + + anchors.verticalCenter: parent.verticalCenter + width: parent.width * audioPopup.sink.audio.volume + } + } + } + } + + function init() { + } +} diff --git a/modules/style/quickshell/shell/ClockWidget.qml b/modules/style/quickshell/shell/ClockWidget.qml new file mode 100644 index 0000000..6cf0ad7 --- /dev/null +++ b/modules/style/quickshell/shell/ClockWidget.qml @@ -0,0 +1,46 @@ +import QtQuick +import Quickshell + +Rectangle { + + + width: text.width + implicitHeight: text.height + // border.color: "black" + border.width: 2 + radius: 0 + color: "transparent" + + Behavior on implicitHeight { + NumberAnimation { + duration: 100 + easing.type: Easing.OutCubic + } + } + + Item { + width: parent.width + height: text.height + + anchors.centerIn: parent + + SystemClock { + id: clock + precision: SystemClock.Seconds + } + + Text { + id: text + anchors.centerIn: parent + property var date: Date() + + text: Qt.formatDateTime(clock.date, "hh mm") + + font.family: "ComicShannsMono Nerd Font Mono" + font.weight: Font.ExtraBold + font.pointSize: 12 + + color: "black" + } + } +} diff --git a/modules/style/quickshell/shell/Launcher.qml b/modules/style/quickshell/shell/Launcher.qml new file mode 100644 index 0000000..183175e --- /dev/null +++ b/modules/style/quickshell/shell/Launcher.qml @@ -0,0 +1,310 @@ +pragma Singleton +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets + +import "./config" + +Singleton { + id: launcher + property bool launcherOpen: false + + IpcHandler { + target: "launcher" + + function open(): void { + launcher.launcherOpen = true; + } + + function close(): void { + launcher.launcherOpen = false; + } + + function toggle(): void { + launcher.launcherOpen = !launcher.launcherOpen; + } + } + + LazyLoader { + id: loader + activeAsync: launcher.launcherOpen + + PanelWindow { + implicitWidth: 450 + implicitHeight: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10 + color: "transparent" + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + + Rectangle { + + height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10) + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + width: 450 + color: Config.catppuccin.base + radius: 5 + border.color: Config.catppuccin.mantle + border.width: 2 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 7 + anchors.bottomMargin: 0 + spacing: 0 + + Rectangle { + id: searchContainer + Layout.fillWidth: true + implicitHeight: searchbox.implicitHeight + 10 + color: Config.catppuccin.base + radius: 3 + border.color: Config.catppuccin.mantle + + RowLayout { + id: searchbox + anchors.fill: parent + anchors.margins: 5 + + TextInput { + id: search + Layout.fillWidth: true + color: Config.catppuccin.text + font.pointSize: 13 + + focus: true + Keys.forwardTo: [list] + Keys.onEscapePressed: launcher.launcherOpen = false + + Keys.onPressed: event => { + if (event.modifiers & Qt.ControlModifier) { + if (event.key == Qt.Key_J) { + list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1; + event.accepted = true; + } else if (event.key == Qt.Key_K) { + list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1; + event.accepted = true; + } + } + } + + onAccepted: { + if (list.currentItem) { + list.currentItem.clicked(null); + } + } + + onTextChanged: { + list.currentIndex = 0; + } + } + } + } + + ListView { + id: list + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + cacheBuffer: 0 // works around QTBUG-131106 + //reuseItems: true + model: ScriptModel { + values: DesktopEntries.applications.values.map(object => { + const stxt = search.text.toLowerCase(); + const ntxt = object.name.toLowerCase(); + let si = 0; + let ni = 0; + + let matches = []; + let startMatch = -1; + + for (let si = 0; si != stxt.length; ++si) { + const sc = stxt[si]; + + while (true) { + // Drop any entries with letters that don't exist in order + if (ni == ntxt.length) + return null; + + const nc = ntxt[ni++]; + + if (nc == sc) { + if (startMatch == -1) + startMatch = ni; + break; + } else { + if (startMatch != -1) { + matches.push({ + index: startMatch, + length: ni - startMatch + }); + + startMatch = -1; + } + } + } + } + + if (startMatch != -1) { + matches.push({ + index: startMatch, + length: ni - startMatch + 1 + }); + } + + return { + object: object, + matches: matches + }; + }).filter(entry => entry !== null).sort((a, b) => { + let ai = 0; + let bi = 0; + let s = 0; + + while (ai != a.matches.length && bi != b.matches.length) { + const am = a.matches[ai]; + const bm = b.matches[bi]; + + s = bm.length - am.length; + if (s != 0) + return s; + + s = am.index - bm.index; + if (s != 0) + return s; + + ++ai; + ++bi; + } + + s = a.matches.length - b.matches.length; + if (s != 0) + return s; + + s = a.object.name.length - b.object.name.length; + if (s != 0) + return s; + + return a.object.name.localeCompare(b.object.name); + }).map(entry => entry.object) + + onValuesChanged: list.currentIndex = 0 + } + + topMargin: 7 + bottomMargin: list.count == 0 ? 0 : 7 + + add: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 100 + } + } + + displaced: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 1 + duration: 100 + } + } + + move: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 1 + duration: 100 + } + } + + remove: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 0 + duration: 100 + } + } + + highlight: Rectangle { + radius: 5 + color: "transparent" + border.color: Config.catppuccin.lavender + border.width: 2 + } + keyNavigationEnabled: true + keyNavigationWraps: true + highlightMoveVelocity: -1 + highlightMoveDuration: 50 + preferredHighlightBegin: list.topMargin + preferredHighlightEnd: list.height - list.bottomMargin + highlightRangeMode: ListView.ApplyRange + snapMode: ListView.SnapToItem + + readonly property real delegateHeight: 44 + + delegate: MouseArea { + required property DesktopEntry modelData + + implicitHeight: list.delegateHeight + implicitWidth: ListView.view.width + + onClicked: { + modelData.execute(); + launcher.launcherOpen = false; + } + + RowLayout { + id: delegateLayout + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 5 + } + + IconImage { + Layout.alignment: Qt.AlignVCenter + asynchronous: true + implicitSize: 30 + source: Quickshell.iconPath(modelData.icon) + } + Text { + text: modelData.name + color: Config.catppuccin.text + font.family: "JetBrainsMono Nerd Font Mono" + font.pointSize: 13 + Layout.alignment: Qt.AlignVCenter + } + } + } + } + } + } + } + } + function init() { + } +} diff --git a/modules/style/quickshell/shell/ReloadPopup.qml b/modules/style/quickshell/shell/ReloadPopup.qml new file mode 100644 index 0000000..9c6e2fc --- /dev/null +++ b/modules/style/quickshell/shell/ReloadPopup.qml @@ -0,0 +1,127 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell + +Scope { + id: root + property bool failed + property string errorString + + // Connect to the Quickshell global to listen for the reload signals. + Connections { + target: Quickshell + + function onReloadCompleted() { + root.failed = false; + popupLoader.loading = true; + } + + function onReloadFailed(error: string) { + // Close any existing popup before making a new one. + popupLoader.active = false; + + root.failed = true; + root.errorString = error; + popupLoader.loading = true; + } + } + + // Keep the popup in a loader because it isn't needed most of the timeand will take up + // memory that could be used for something else. + LazyLoader { + id: popupLoader + + PanelWindow { + id: popup + + anchors { + top: true + right: true + } + + margins { + top: 25 + left: 25 + } + + width: rect.width + height: rect.height + + // color blending is a bit odd as detailed in the type reference. + color: "transparent" + + Rectangle { + id: rect + color: root.failed ? "#40802020" : "#40009020" + + implicitHeight: layout.implicitHeight + 50 + implicitWidth: layout.implicitWidth + 30 + + // Fills the whole area of the rectangle, making any clicks go to it, + // which dismiss the popup. + MouseArea { + id: mouseArea + anchors.fill: parent + onClicked: popupLoader.active = false + + // makes the mouse area track mouse hovering, so the hide animation + // can be paused when hovering. + hoverEnabled: true + } + + ColumnLayout { + id: layout + anchors { + top: parent.top + topMargin: 20 + horizontalCenter: parent.horizontalCenter + } + + Text { + text: root.failed ? "Reloaing failed." : "Reloading completed!" + color: "white" + } + + Text { + text: root.errorString + color: "white" + // When visible is false, it also takes up no space. + visible: root.errorString != "" + } + } + + // A progress bar on the bottom of the screen, showing how long until the + // popup is removed. + Rectangle { + id: bar + color: "#20ffffff" + anchors.bottom: parent.bottom + anchors.left: parent.left + height: 20 + + PropertyAnimation { + id: anim + target: bar + property: "width" + from: rect.width + to: 0 + duration: failed ? 10000 : 800 + onFinished: popupLoader.active = false + + // Pause the animation when the mouse is hovering over the popup, + // so it stays onscreen while reading. This updates reactively + // when the mouse moves on and off the popup. + paused: mouseArea.containsMouse + } + } + + // We could set `running: true` inside the animation, but the width of the + // rectangle might not be calculated yet, due to the layout. + // In the `Component.onCompleted` event handler, all of the component's + // properties and children have been initialized. + Component.onCompleted: anim.start() + } + } + } +} diff --git a/modules/style/quickshell/shell/SysTray.qml b/modules/style/quickshell/shell/SysTray.qml new file mode 100644 index 0000000..7445bcd --- /dev/null +++ b/modules/style/quickshell/shell/SysTray.qml @@ -0,0 +1,57 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import QtQuick.Effects +import QtQuick.Controls +import Quickshell +import Quickshell.Services.SystemTray + +Rectangle { + id: root + required property var bar + + width: parent.width + height: column.height + 10 + color: "#30c0ffff" + radius: 5 + border.color: "black" + border.width: 2 + + RowLayout { + id: column + spacing: 10 + + anchors.centerIn: parent + + Repeater { + model: SystemTray.items + + Item { + id: item + + required property SystemTrayItem modelData + + height: 35 + width: 35 + + Image { + source: item.modelData.icon + anchors.fill: parent + + MouseArea { + anchors.fill: parent + onClicked: function (mouse) { + if (mouse.button === Qt.LeftButton) { + item.modelData.activate(); + } + if (mouse.button === Qt.RightButton) { + if (item.modelData.hasMenu) {} + } + } + } + } + } + } + } +} diff --git a/modules/style/quickshell/shell/config/Config.qml b/modules/style/quickshell/shell/config/Config.qml new file mode 100644 index 0000000..875a4df --- /dev/null +++ b/modules/style/quickshell/shell/config/Config.qml @@ -0,0 +1,46 @@ +pragma Singleton + +import Quickshell +import QtQuick + +Singleton { + id: root + + readonly property QtObject bar: QtObject { + readonly property int width: 50 + readonly property var colors: QtObject { + readonly property color bar: "#1e1e2e" + readonly property color barOutline: "#50ffffff" + readonly property color widget: "#25ceffff" + readonly property color widgetActive: "#80ceffff" + readonly property color widgetOutline: "#40ffffff" + readonly property color widgetOutlineSeparate: "#20ffffff" + readonly property color separator: "#60ffffff" + } + } + + readonly property QtObject border: QtObject { + readonly property int thickness: 8 + readonly property color color: "#1e1e2e" + readonly property int rounding: 25 + } + + readonly property QtObject catppuccin: QtObject { + readonly property color base: "#1e1e2e" + readonly property color mantle: "#181825" + readonly property color surface0: "#313244" + readonly property color surface1: "#45475a" + readonly property color surface2: "#585b70" + readonly property color text: "#cdd6f4" + readonly property color rosewater: "#f5e0dc" + readonly property color lavender: "#b4befe" + readonly property color red: "#f38ba8" + readonly property color peach: "#fab387" + readonly property color yellow: "#f9e2af" + readonly property color green: "#a6e3a1" + readonly property color teal: "#a6e3a1" + readonly property color blue: "#89b4fa" + readonly property color mauve: "#cba6f7" + readonly property color flamingo: "#f2cdcd" + } +} diff --git a/modules/style/quickshell/shell/modules/bar/Bar.qml b/modules/style/quickshell/shell/modules/bar/Bar.qml new file mode 100644 index 0000000..b0fc519 --- /dev/null +++ b/modules/style/quickshell/shell/modules/bar/Bar.qml @@ -0,0 +1,50 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell + +import "../../config" +import "components" + +Item { + id: root + + required property ShellScreen screen + + anchors { + top: parent.top + bottom: parent.bottom + left: parent.left + } + + implicitWidth: Config.bar.width + + Item { + id: child + + anchors { + top: parent.top + bottom: parent.bottom + horizontalCenter: parent.horizontalCenter + } + + implicitWidth: Math.max(clock.implicitWidth, workspaces.implicitWidth) + + ColumnLayout { + spacing: 2 + anchors { + top: parent.top + left: parent.left + right: parent.right + margins: 0 + } + Clock { + id: clock + } + Workspaces { + id: workspaces + screen: root.screen + } + } + } +} diff --git a/modules/style/quickshell/shell/modules/bar/components/Clock.qml b/modules/style/quickshell/shell/modules/bar/components/Clock.qml new file mode 100644 index 0000000..4826851 --- /dev/null +++ b/modules/style/quickshell/shell/modules/bar/components/Clock.qml @@ -0,0 +1,34 @@ +import QtQuick +import Quickshell + +import "../../../config" + +Rectangle { + id: root + + width: text.width + 5 + height: text.height + 5 + implicitWidth: width + border.color: Config.catppuccin.rosewater + border.width: 0 + radius: 5 + color: "transparent" + + Text { + id: text + anchors.centerIn: parent + property var date: Date() + + text: Qt.formatDateTime(clock.date, "hh mm") + + font.family: "JetBrainsMono NF Mono" + font.pointSize: 15 + + color: Config.catppuccin.text + } + + SystemClock { + id: clock + precision: SystemClock.Seconds + } +} diff --git a/modules/style/quickshell/shell/modules/bar/components/Workspaces.qml b/modules/style/quickshell/shell/modules/bar/components/Workspaces.qml new file mode 100644 index 0000000..91bc9d2 --- /dev/null +++ b/modules/style/quickshell/shell/modules/bar/components/Workspaces.qml @@ -0,0 +1,109 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import QtQuick.Layouts + +import "../../../services/niri" +import "../../../config" + +Rectangle { + id: root + + required property ShellScreen screen + property var workspaces: Niri.workspaces + property var wsCount: Niri.workspaces.length + property var activeWorkspace: Niri.activeWorkspace + property var activeWorkspaceIndex: Niri.activeWorkspaceIndex + + property int wsItemHeight: 15 + + signal workspaceAdded(workspace: var) + function onWorkspaceAdded(workspace: var) { + root.workspaces.push(workspace); + } + + // property bool _: log() + // function log() { + // console.log(workspaces.values); + // return true; + // } + + // Works + height: 300 + + // Gives warning + // height: workspaces.length * root.wsItemHeight + implicitWidth: list.implicitWidth + color: "transparent" + border.color: Config.catppuccin.rosewater + border.width: 0 + radius: 7 + + Layout.fillWidth: true + + ListView { + id: list + model: root.workspaces + implicitHeight: contentHeight + implicitWidth: contentItem.childrenRect.width + anchors.horizontalCenter: parent.horizontalCenter + + // anchors.fill: parent + + delegate: Item { + id: wsItem + // Name of the workspace + property string name: "VOID" + // ID of the workspace + required property string id + + required property string output + + property bool isActive: (id - 1 == root.activeWorkspaceIndex) + + property real animActive: isActive ? 1 : 0.65 + Behavior on animActive { + NumberAnimation { + duration: 150 + } + } + + // property bool isCorrectScreen: log() + // function log() { + // console.log("Screen name: " + root.screen.name); + // console.log(wsItem.output); + // console.log(wsItem.id); + + // let isCorrect = root.screen.name == wsItem.output; + // console.log("isCorrect: ", isCorrect); + // return root.screen.name == wsItem.output; + // } + + implicitHeight: root.wsItemHeight + implicitWidth: 50 + + anchors { + right: parent.right + left: parent.left + } + + Rectangle { + anchors.centerIn: parent + height: wsItem.height - 5 + width: parent.width * wsItem.animActive + radius: height / 2 + border.color: Config.catppuccin.mantle + border.width: 0 + color: Config.catppuccin.blue + } + } + } + + Component.onCompleted: { + Niri.workspaces.forEach(workspace => { + root.workspaceAdded(workspace); + }); + } +} diff --git a/modules/style/quickshell/shell/modules/drawers/Backgrounds.qml b/modules/style/quickshell/shell/modules/drawers/Backgrounds.qml new file mode 100644 index 0000000..388efc4 --- /dev/null +++ b/modules/style/quickshell/shell/modules/drawers/Backgrounds.qml @@ -0,0 +1,9 @@ +import Quickshell +import QtQuick +import QtQuick.Shapes + +import "../notifications" as Notifications + +Rectangle { + required property Item bar +} diff --git a/modules/style/quickshell/shell/modules/drawers/Border.qml b/modules/style/quickshell/shell/modules/drawers/Border.qml new file mode 100644 index 0000000..9117fee --- /dev/null +++ b/modules/style/quickshell/shell/modules/drawers/Border.qml @@ -0,0 +1,54 @@ +import Quickshell +import QtQuick +import QtQuick.Effects + +import "../../config" + +Item { + id: root + + required property Item bar + + anchors.fill: parent + + Rectangle { + id: rect + + anchors.fill: parent + color: Config.border.color + visible: false + + + Behavior on color { + ColorAnimation { + duration: 150 + easing.type: Easing.BezierSpline + } + } + } + + Item { + id: mask + + anchors.fill: parent + layer.enabled: true + visible: false + + Rectangle { + anchors.fill: parent + anchors.margins: Config.border.thickness + anchors.leftMargin: root.bar.implicitWidth + radius: Config.border.rounding + } + } + + MultiEffect { + anchors.fill: parent + maskEnabled: true + maskInverted: true + maskSource: mask + source: rect + maskThresholdMin: 0.5 + maskSpreadAtMin: 1 + } +} diff --git a/modules/style/quickshell/shell/modules/drawers/Drawers.qml b/modules/style/quickshell/shell/modules/drawers/Drawers.qml new file mode 100644 index 0000000..eb85dea --- /dev/null +++ b/modules/style/quickshell/shell/modules/drawers/Drawers.qml @@ -0,0 +1,105 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Effects + +import "../bar" +import "../volume" +import "../notifications" + +import "../../services" +import "../../config" + +Variants { + model: Quickshell.screens + + Scope { + id: scope + required property ShellScreen modelData + + Exclusions { + screen: scope.modelData + bar: bar + } + + PanelWindow { + id: win + + screen: scope.modelData + color: "transparent" + + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + + // Clickthrough mask. + // Clickable areas of the window are determined by the provided region. + mask: Region { + // Start at the bottom left; right of the bar and on top of the border + x: bar.implicitWidth + y: Config.border.thickness + + // Width is the window width - the bar's width - the border thickness + width: win.width - bar.implicitWidth - Config.border.thickness + + // Height is window width - the border thickness x2 —top border and bottom border. + height: win.height - Config.border.thickness * 2 + + // Setting the intersection mode to Xor will invert the mask and make everything in the mask region not clickable and pass through clicks inside it through the window. + intersection: Intersection.Xor + Region { + item: volume + intersection: Intersection.Subtract + } + } + + anchors { + top: true + bottom: true + left: true + right: true + } + + Item { + id: background + + anchors.fill: parent + visible: false + + Border { + bar: bar + } + + Backgrounds { + bar: bar + } + } + + MultiEffect { + anchors.fill: source + source: background + shadowEnabled: true + blurMax: 15 + } + + Bar { + id: bar + screen: scope.modelData + } + + VolumeSlider { + id: volume + isInRightPanel: hover.isInRightPanel + screen: scope.modelData + } + + Hover { + id: hover + screen: scope.modelData + bar: bar + } + } + } +} diff --git a/modules/style/quickshell/shell/modules/drawers/Exclusions.qml b/modules/style/quickshell/shell/modules/drawers/Exclusions.qml new file mode 100644 index 0000000..d99092d --- /dev/null +++ b/modules/style/quickshell/shell/modules/drawers/Exclusions.qml @@ -0,0 +1,38 @@ + +pragma ComponentBehavior: Bound +import Quickshell +import QtQuick + +import "../../config" + +Scope { + id: root + required property ShellScreen screen + required property Item bar + + ExclusionZone { + anchors.left: true + exclusiveZone: root.bar.implicitWidth + } + + ExclusionZone { + anchors.top: true + } + + ExclusionZone { + anchors.right: true + } + + ExclusionZone { + anchors.bottom: true + } + + component ExclusionZone: PanelWindow { + screen: root.screen + color: "transparent" + exclusiveZone: Config.border.thickness + implicitHeight: Config.border.thickness + implicitWidth: Config.border.thickness + mask: Region {} + } +} diff --git a/modules/style/quickshell/shell/modules/launcher/Launcher.qml b/modules/style/quickshell/shell/modules/launcher/Launcher.qml new file mode 100644 index 0000000..8cf4f37 --- /dev/null +++ b/modules/style/quickshell/shell/modules/launcher/Launcher.qml @@ -0,0 +1,319 @@ +pragma Singleton +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Widgets + +Singleton { + id: launcher + property bool launcherOpen: false + + IpcHandler { + target: "launcher" + + function open(): void { + launcher.launcherOpen = true; + } + + function close(): void { + launcher.launcherOpen = false; + } + + function toggle(): void { + launcher.launcherOpen = !launcher.launcherOpen; + } + } + + LazyLoader { + id: loader + activeAsync: launcher.launcherOpen + + PanelWindow { + width: 450 + height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10 + color: "transparent" + + + anchors { + bottom: parent.bottom + } + + + WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive + WlrLayershell.namespace: "shell:launcher" + + Rectangle { + + height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10) + Behavior on height { + NumberAnimation { + duration: 200 + easing.type: Easing.OutCubic + } + } + width: 450 + color: "#30c0afaf" + radius: 5 + border.color: "black" + border.width: 2 + + ColumnLayout { + anchors.fill: parent + anchors.margins: 7 + anchors.bottomMargin: 0 + spacing: 0 + + Rectangle { + id: searchContainer + Layout.fillWidth: true + implicitHeight: searchbox.implicitHeight + 10 + color: "#30c0ffff" + radius: 3 + border.color: "#50ffffff" + + RowLayout { + id: searchbox + anchors.fill: parent + anchors.margins: 5 + + IconImage { + implicitSize: parent.height + source: "root:icons/magnifying-glass.svg" + } + + TextInput { + id: search + Layout.fillWidth: true + color: "black" + + focus: true + Keys.forwardTo: [list] + Keys.onEscapePressed: launcher.launcherOpen = false + + Keys.onPressed: event => { + if (event.modifiers & Qt.ControlModifier) { + if (event.key == Qt.Key_J) { + list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1; + event.accepted = true; + } else if (event.key == Qt.Key_K) { + list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1; + event.accepted = true; + } + } + } + + onAccepted: { + if (list.currentItem) { + list.currentItem.clicked(null); + } + } + + onTextChanged: { + list.currentIndex = 0; + } + } + } + } + + ListView { + id: list + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + cacheBuffer: 0 // works around QTBUG-131106 + //reuseItems: true + model: ScriptModel { + values: DesktopEntries.applications.values.map(object => { + const stxt = search.text.toLowerCase(); + const ntxt = object.name.toLowerCase(); + let si = 0; + let ni = 0; + + let matches = []; + let startMatch = -1; + + for (let si = 0; si != stxt.length; ++si) { + const sc = stxt[si]; + + while (true) { + // Drop any entries with letters that don't exist in order + if (ni == ntxt.length) + return null; + + const nc = ntxt[ni++]; + + if (nc == sc) { + if (startMatch == -1) + startMatch = ni; + break; + } else { + if (startMatch != -1) { + matches.push({ + index: startMatch, + length: ni - startMatch + }); + + startMatch = -1; + } + } + } + } + + if (startMatch != -1) { + matches.push({ + index: startMatch, + length: ni - startMatch + 1 + }); + } + + return { + object: object, + matches: matches + }; + }).filter(entry => entry !== null).sort((a, b) => { + let ai = 0; + let bi = 0; + let s = 0; + + while (ai != a.matches.length && bi != b.matches.length) { + const am = a.matches[ai]; + const bm = b.matches[bi]; + + s = bm.length - am.length; + if (s != 0) + return s; + + s = am.index - bm.index; + if (s != 0) + return s; + + ++ai; + ++bi; + } + + s = a.matches.length - b.matches.length; + if (s != 0) + return s; + + s = a.object.name.length - b.object.name.length; + if (s != 0) + return s; + + return a.object.name.localeCompare(b.object.name); + }).map(entry => entry.object) + + onValuesChanged: list.currentIndex = 0 + } + + topMargin: 7 + bottomMargin: list.count == 0 ? 0 : 7 + + add: Transition { + NumberAnimation { + property: "opacity" + from: 0 + to: 1 + duration: 100 + } + } + + displaced: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 1 + duration: 100 + } + } + + move: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 1 + duration: 100 + } + } + + remove: Transition { + NumberAnimation { + property: "y" + duration: 200 + easing.type: Easing.OutCubic + } + NumberAnimation { + property: "opacity" + to: 0 + duration: 100 + } + } + + highlight: Rectangle { + radius: 5 + color: "#20e0ffff" + border.color: "#30ffffff" + border.width: 1 + } + keyNavigationEnabled: true + keyNavigationWraps: true + highlightMoveVelocity: -1 + highlightMoveDuration: 50 + preferredHighlightBegin: list.topMargin + preferredHighlightEnd: list.height - list.bottomMargin + highlightRangeMode: ListView.ApplyRange + snapMode: ListView.SnapToItem + + readonly property real delegateHeight: 44 + + delegate: MouseArea { + required property DesktopEntry modelData + + implicitHeight: list.delegateHeight + implicitWidth: ListView.view.width + + onClicked: { + modelData.execute(); + launcher.launcherOpen = false; + } + + RowLayout { + id: delegateLayout + anchors { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: 5 + } + + IconImage { + Layout.alignment: Qt.AlignVCenter + asynchronous: true + implicitSize: 30 + source: Quickshell.iconPath(modelData.icon) + } + Text { + text: modelData.name + color: "black" + font.family: "ComicShannsMono Nerd Font Mono" + Layout.alignment: Qt.AlignVCenter + } + } + } + } + } + } + } + } + function init() { + } +} diff --git a/modules/style/quickshell/shell/modules/notifications/Notification.qml b/modules/style/quickshell/shell/modules/notifications/Notification.qml new file mode 100644 index 0000000..2569210 --- /dev/null +++ b/modules/style/quickshell/shell/modules/notifications/Notification.qml @@ -0,0 +1,18 @@ +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Layouts + +import "../../config" +import "../../services" + +Rectangle { + id: root + color: "transparent" + required property Notification.Notif modelData + + Text { + text: root.modelData.summary + } +} diff --git a/modules/style/quickshell/shell/modules/volume/VolumeSlider.qml b/modules/style/quickshell/shell/modules/volume/VolumeSlider.qml new file mode 100644 index 0000000..af00a16 --- /dev/null +++ b/modules/style/quickshell/shell/modules/volume/VolumeSlider.qml @@ -0,0 +1,41 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Wayland +import Quickshell.Services.Notifications +import QtQuick +import QtQuick.Effects + +Rectangle { + id: root + required property bool isInRightPanel + required property ShellScreen screen + property bool isVisible + color: "transparent" + + property bool _: log() + function log() { + console.log(hover.hovered); + return true + } + + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + } + + implicitWidth: 60 + implicitHeight: screen.height / 3 + + HoverHandler { + id: hover + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + } + + Rectangle { + anchors.right: parent.right + color: "green" + implicitWidth: hover.hovered | root.isInRightPanel ? 60 : 10 + implicitHeight: root.screen.height / 3 + } +} diff --git a/modules/style/quickshell/shell/services/Hover.qml b/modules/style/quickshell/shell/services/Hover.qml new file mode 100644 index 0000000..cd29894 --- /dev/null +++ b/modules/style/quickshell/shell/services/Hover.qml @@ -0,0 +1,49 @@ +import Quickshell +import QtQuick +import "../config" + +MouseArea { + id: root + + anchors.fill: parent + hoverEnabled: true + + required property ShellScreen screen + // required property Panels panels + required property Item bar + + property bool showVolumeMenu: false + property bool isInRightPanel: false + + // function withinPanelHeight(panel: Item, x: real, y: real): bool { + // const panelY = Config.border.thickness + panel.y; + // return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; + // } + + // function inLeftBorder(x: real, y: real): bool { + // return x <= Config.border.thickness; + // } + + function inRightPanel(x: real, y: real): bool { + // Cursor is in middle veritcal third of screen + // Cursor is in the right border + return y >= root.screen.height / 3 && y <= (root.screen.height / 3) * 2 && x >= root.screen.width - Config.border.thickness; + } + + // Update on mouse cursor movement + onPositionChanged: event => { + const x = event.x; + const y = event.y; + + root.isInRightPanel = inRightPanel(x, y); + + console.log("In right panel: " + root.isInRightPanel); + + console.log("x:" + x + " y: " + y); + } + onContainsMouseChanged: { + if (!containsMouse) { + root.isInRightPanel = false; + } + } +} diff --git a/modules/style/quickshell/shell/services/Notification.qml b/modules/style/quickshell/shell/services/Notification.qml new file mode 100644 index 0000000..bdf52de --- /dev/null +++ b/modules/style/quickshell/shell/services/Notification.qml @@ -0,0 +1,20 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Services.Notifications + +/** + * Provides extra features not in Quickshell.Services.Notifications: + * - Persistent storage + * - Popup notifications, with timeout + * - Notification groups by app + */ +Singleton { + id: root + NotificationServer { + + } +} diff --git a/modules/style/quickshell/shell/services/niri/Niri.qml b/modules/style/quickshell/shell/services/niri/Niri.qml new file mode 100644 index 0000000..de16157 --- /dev/null +++ b/modules/style/quickshell/shell/services/niri/Niri.qml @@ -0,0 +1,94 @@ +// Kind thanks to https://github.com/MapoMagpie/nixos-flakes/blob/main/home/ui/quickshell/config/Data/Niri.qml +// This file was taken from there and further modified. + +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + // property var data + property var workspaces: [] + property var activeWorkspace: "VOID" + property var activeWorkspaceIndex: 0 + property var windows: [] + // property var activedWindowId: 0 + + Process { + id: proc + command: ["niri", "msg", "-j", "event-stream"] + + running: true + stdout: SplitParser { + onRead: data => { + var event = JSON.parse(data); + let workspaces = []; + if (event.WorkspacesChanged) { + root.workspaces = event.WorkspacesChanged.workspaces; + root.workspaces = root.workspaces.sort((a, b) => a.id - b.id); + root.activeWorkspaceIndex = root.workspaces.findIndex(w => w.is_focused); + if (root.activeWorkspaceIndex < 0) { + root.activeWorkspaceIndex = 0; + } + root.activeWorkspace = root.workspaces[root.activeWorkspaceIndex].name; + } + if (event.WindowsChanged) { + root.windows = [...event.WindowsChanged.windows].sort((a, b) => a.id - b.id); + } + if (event.WindowOpenedOrChanged) { + const window = event.WindowOpenedOrChanged.window; + const index = root.windows.findIndex(w => w.id === window.id); + // console.log("window opened or changed: ", index, ", win id: ", window.id); + if (index >= 0) { + // console.log("replace window, old: ", root.windows[index].id, ", new: ", window.id); + root.windows[index] = window; + } else { + // console.log("push window, new: ", window.id); + root.windows.push(window); + } + root.windows = [...root.windows.sort((a, b) => a.id - b.id)]; + } + if (event.WindowClosed) { + const index = root.windows.findIndex(w => w.id === event.WindowClosed.id); + // console.log("window closed: ", index, ", win id: ", event.WindowClosed.id); + if (index >= 0) { + root.windows.splice(index, 1); + } + root.windows = [...root.windows.sort((a, b) => a.id - b.id)]; + } + if (event.WorkspaceActivated) { + root.activeWorkspaceIndex = root.workspaces.findIndex(w => w.id === event.WorkspaceActivated.id); + if (root.activeWorkspaceIndex < 0) { + root.activeWorkspaceIndex = 0; + } + root.activeWorkspace = root.workspaces[root.activeWorkspaceIndex].name; + } + } + } + } + // component Workspace: QtObject { + // required property int id + // property int idx + // property string name: "VOID" + // required property string output + // property bool is_active + // property bool is_focused + // property int active_window_id + // } +} + +// { +// "workspaces": [ +// { +// "id": 5, +// "idx": 4, +// "name": "GAME", +// "output": "DP-3", +// "is_active": false, +// "is_focused": false, +// "active_window_id": null +// }, +// ] +// } diff --git a/modules/style/quickshell/shell/shell.qml b/modules/style/quickshell/shell/shell.qml index e69de29..05fea55 100644 --- a/modules/style/quickshell/shell/shell.qml +++ b/modules/style/quickshell/shell/shell.qml @@ -0,0 +1,21 @@ +//@ pragma Env QS_NO_RELOAD_POPUP=1 + +import Quickshell +import QtQuick + +import "modules" +import "modules/drawers" +import "services" + +// import "modules/background" + +ShellRoot { + id: shellroot + + Component.onCompleted: [Launcher.init()] + + Drawers {} + // Background {}Popup + // + +}