From 68e1a276192872c57931e9e43911a479d4619c4a Mon Sep 17 00:00:00 2001 From: littleblack111 Date: Tue, 19 Aug 2025 20:46:39 +0800 Subject: chore(launcher -> popups): reorganize to allow futher expansion on popups refactor(launcher): ILauncher is now a derivative of IPopup --- config/Launcher.qml | 2 +- launcher/ILauncher.qml | 32 -- launcher/Launcher.qml | 388 ------------------------- launcher/components/App.qml | 181 ------------ launcher/components/Calc.qml | 81 ------ launcher/components/Clip.qml | 199 ------------- launcher/components/Emoji.qml | 176 ----------- launcher/components/Finder.qml | 0 launcher/components/Flight.qml | 0 launcher/components/IComponent.qml | 93 ------ launcher/components/IInnerComponent.qml | 34 --- launcher/components/SelectionState.qml | 49 ---- launcher/components/Spell.qml | 235 --------------- launcher/components/Time.qml | 0 launcher/components/Translate.qml | 0 launcher/components/Weather.qml | 0 launcher/components/Web.qml | 0 launcher/todo.md | 37 --- popups/IPopup.qml | 18 ++ popups/launcher/ILauncher.qml | 15 + popups/launcher/Launcher.qml | 388 +++++++++++++++++++++++++ popups/launcher/components/App.qml | 181 ++++++++++++ popups/launcher/components/Calc.qml | 81 ++++++ popups/launcher/components/Clip.qml | 199 +++++++++++++ popups/launcher/components/Emoji.qml | 176 +++++++++++ popups/launcher/components/Finder.qml | 0 popups/launcher/components/Flight.qml | 0 popups/launcher/components/IComponent.qml | 93 ++++++ popups/launcher/components/IInnerComponent.qml | 34 +++ popups/launcher/components/SelectionState.qml | 49 ++++ popups/launcher/components/Spell.qml | 235 +++++++++++++++ popups/launcher/components/Time.qml | 0 popups/launcher/components/Translate.qml | 0 popups/launcher/components/Weather.qml | 0 popups/launcher/components/Web.qml | 0 popups/launcher/todo.md | 37 +++ shell.qml | 8 +- 37 files changed, 1511 insertions(+), 1510 deletions(-) delete mode 100644 launcher/ILauncher.qml delete mode 100644 launcher/Launcher.qml delete mode 100644 launcher/components/App.qml delete mode 100644 launcher/components/Calc.qml delete mode 100644 launcher/components/Clip.qml delete mode 100644 launcher/components/Emoji.qml delete mode 100644 launcher/components/Finder.qml delete mode 100644 launcher/components/Flight.qml delete mode 100644 launcher/components/IComponent.qml delete mode 100644 launcher/components/IInnerComponent.qml delete mode 100644 launcher/components/SelectionState.qml delete mode 100644 launcher/components/Spell.qml delete mode 100644 launcher/components/Time.qml delete mode 100644 launcher/components/Translate.qml delete mode 100644 launcher/components/Weather.qml delete mode 100644 launcher/components/Web.qml delete mode 100644 launcher/todo.md create mode 100644 popups/IPopup.qml create mode 100644 popups/launcher/ILauncher.qml create mode 100644 popups/launcher/Launcher.qml create mode 100644 popups/launcher/components/App.qml create mode 100644 popups/launcher/components/Calc.qml create mode 100644 popups/launcher/components/Clip.qml create mode 100644 popups/launcher/components/Emoji.qml create mode 100644 popups/launcher/components/Finder.qml create mode 100644 popups/launcher/components/Flight.qml create mode 100644 popups/launcher/components/IComponent.qml create mode 100644 popups/launcher/components/IInnerComponent.qml create mode 100644 popups/launcher/components/SelectionState.qml create mode 100644 popups/launcher/components/Spell.qml create mode 100644 popups/launcher/components/Time.qml create mode 100644 popups/launcher/components/Translate.qml create mode 100644 popups/launcher/components/Weather.qml create mode 100644 popups/launcher/components/Web.qml create mode 100644 popups/launcher/todo.md diff --git a/config/Launcher.qml b/config/Launcher.qml index f2141e9..96da659 100644 --- a/config/Launcher.qml +++ b/config/Launcher.qml @@ -2,7 +2,7 @@ pragma Singleton import Quickshell import QtQuick -import qs.launcher.components +import qs.popups.launcher.components Singleton { id: root diff --git a/launcher/ILauncher.qml b/launcher/ILauncher.qml deleted file mode 100644 index afb9250..0000000 --- a/launcher/ILauncher.qml +++ /dev/null @@ -1,32 +0,0 @@ -import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland -import QtQuick.Controls -import QtQuick - -import qs.components -import qs.config - -IWindow { - id: root - required property var parentLoader - // TODO: optional only display on focused screen - focusable: true - - layer: WlrLayer.Overlay - - anchors { - top: false - bottom: false - left: false - right: false - } - - HyprlandFocusGrab { - active: true - windows: [root] - onCleared: { - parentLoader.active = false; // or active = true again - } - } -} diff --git a/launcher/Launcher.qml b/launcher/Launcher.qml deleted file mode 100644 index 25b2926..0000000 --- a/launcher/Launcher.qml +++ /dev/null @@ -1,388 +0,0 @@ -import Quickshell -import QtQuick -import QtQuick.Layouts - -import "components" -import qs.components -import qs.config - -// TODO: currently, we're relying on the compositor to provide the animation which ig is fine, but we should use our own in the future, however it might look weird combined with the compositors's -ILauncher { - id: launcher - - property list widgetItems: Launcher.widgets - property var state: SelectionState - - name: "quickshell::launcher::launcher" - - anchors.top: true - margins.top: Launcher.topMargin - aboveWindows: true - exclusionMode: ExclusionMode.Ignore - - IRect { - id: container - property var selection: launcher.state?.selected || null - property var mappedSelection: selection?.mapToItem(null, 0, 0) - onMappedSelectionChanged: { - // launcher.state?.selected may be slow.. smh qt - // my guess is mapToItem is called when its not finished doing whatever it needs to do, - // so it doesnt get the right vars, but it doesn't update after since it already triggered it - selectionSync.running = true; - } - Timer { - id: selectionSync - running: false - repeat: false - // sync w/ IComponent y animation duration - interval: General.animationDuration / 4 - onTriggered: { - parent.mappedSelection = launcher.state?.selected?.mapToItem(null, 0, 0); - } - } - - IRect { - id: activeRect - visible: parent.mappedSelection || false - y: parent.mappedSelection?.y || 0 - x: parent.mappedSelection?.x || 0 - implicitWidth: launcher.state?.selected?.width || 0 - implicitHeight: launcher.state?.selected?.height || 0 - radius: Launcher.borderRadius - color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.bgTransparency) - Behavior on x { - ISpringAnimation {} - } - Behavior on y { - ISpringAnimation {} - } - Behavior on implicitWidth { - ISpringAnimation {} - } - Behavior on height { - ISpringAnimation {} - } - } - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.bgTransparency) - radius: Launcher.borderRadius - - width: Math.max(root.width, Launcher.defaultWidth) + Launcher.innerMargin - height: root.height - - IRect { - id: root - property alias input: textInput.text - - height: searchBar.height + widgets.height + textInput.height / 2 // * 1.5 - width: Math.max(widgets.implicitWidth, Launcher.defaultWidth) - - anchors.centerIn: parent - - color: "transparent" - - RowLayout { - id: searchBar - anchors { - top: parent.top - left: parent.left - right: parent.right - leftMargin: Launcher.innerMargin - rightMargin: Launcher.innerMargin - // try to center it when no widgets are present - topMargin: widgets.height === Launcher.widgets.length * 5 - 5 ? parent.height / 2 - textInput.height / 2 : Launcher.innerMargin // textInput or Icon is the height of this - - Behavior on topMargin { - ISpringAnimation {} - // NumberAnimation { - // duration: General.animationDuration / 2 - // } - } - } - - spacing: Launcher.innerMargin * 2 - - Icon { - text: "" - } - - RowLayout { - spacing: 0 - TextInput { - id: textInput - property bool pendingUpdate: false - - Layout.fillHeight: true - Layout.fillWidth: true - Layout.maximumWidth: implicitWidth - - color: Colors.foreground1 - selectionColor: Colors.background3 - - text: launcher.state?.input || "" - clip: true - focus: true - - antialiasing: true - smooth: true - font { - pixelSize: Style.font.size.large - family: Style.font.family.sans - wordSpacing: 5 - } - // move to different file if we have more textinput - cursorDelegate: IRect { - id: cursor - color: Colors.cursor - // width: 1.5 - width: 2 - height: 1 - // blinking animation - SequentialAnimation { - running: true - loops: Animation.Infinite - PauseAnimation { - duration: 500 - } - ParallelAnimation { - PropertyAnimation { - target: cursor - property: "opacity" - to: 0 - duration: General.animationDuration / 5 - } - PropertyAnimation { - target: cursor - property: "scale" - to: 0 - duration: General.animationDuration / 5 - } - } - PauseAnimation { - duration: 500 - } - ParallelAnimation { - PropertyAnimation { - target: cursor - property: "opacity" - to: 1 - duration: General.animationDuration / 5 - } - PropertyAnimation { - target: cursor - property: "scale" - to: 1 - duration: General.animationDuration / 5 - } - } - } - } - onTextChanged: { - if (launcher.state.input !== textInput.text) - pendingUpdate = true; - Qt.callLater(() => { - if (pendingUpdate) { - pendingUpdate = false; - launcher.state.input = textInput.text; // TODO: find ways to optimize this, like how i was using an alias - } - }); - } - onCursorPositionChanged: { - launcher.state.cursorPosition = textInput.cursorPosition; - } - // onActiveFocusChanged: { - // parentLoader.active = activeFocus; - // } - onAccepted: { - launcher.state?.priorities[launcher.state.selectedPriority]?._exec(); // TODO: kde like waiting animation for app to launch - parentLoader.active = false; - textInput.text = ""; - } - Component.onCompleted: { - selectAll(); - } - } - IRect { - opacity: launcher.state?.priorities[0]?.predictiveCompletion ? 1 : 0 - Layout.fillHeight: true - Layout.preferredWidth: text.width - radius: Launcher.predictiveCompletionRadius - color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) - IText { - id: text - // animate: true // too jumpy - color: Colors.foreground3 - renderType: Text.CurveRendering - antialiasing: true - smooth: true - font { - pixelSize: Style.font.size.large - family: Style.font.family.sans - wordSpacing: 5 - } - text: launcher.state?.priorities[0]?.predictiveCompletion || "" - } - Behavior on Layout.preferredWidth { - ISpringAnimation {} - // NumberAnimation { - // duration: General.animationDuration / 5 - // } - } - Behavior on opacity { - NumberAnimation { - duration: General.animationDuration / 2 - easing.type: Easing.InOutQuad - } - } - } - Item { - Layout.fillWidth: true - } - Loader { - active: launcher.state?.priorities[0]?.priority || false // .priority should always be true if it's in priorities - sourceComponent: launcher.state?.priorities[0]?.preview - } - } - } - - ColumnLayout { - id: widgets - anchors { - top: searchBar.bottom - left: parent.left - right: parent.right - topMargin: Launcher.innerMargin * 1.5 // gap in between, maybe seperator FIXME - } - - Repeater { - model: launcher.widgetItems - delegate: Loader { - active: true - sourceComponent: modelData - asynchronous: true - onLoaded: { - launcher.state.widgets[index] = item; - // FIXME: prev predictiveCompletion still exists here - // workaround atm: just clear it or preserve the text manually - } - Connections { - target: item - ignoreUnknownSignals: true - function onClose() { - parentLoader.active = false; - } - } - } - } - } - } - - // not Keys.onPressed because textinput will steal it - Shortcut { - sequence: "Escape" - context: Qt.ApplicationShortcut - onActivated: parentLoader.active = false - } - - Shortcut { - sequence: "Ctrl+H" - context: Qt.ApplicationShortcut - onActivated: changePos(1) - } - - Shortcut { - sequence: "Ctrl+J" - context: Qt.ApplicationShortcut - onActivated: changePos(-2) - } - - Shortcut { - sequence: "Ctrl+K" - context: Qt.ApplicationShortcut - onActivated: changePos(2) - } - - Shortcut { - sequence: "Ctrl+L" - context: Qt.ApplicationShortcut - onActivated: changePos(-1) - } - - Shortcut { - sequence: "Down" - context: Qt.ApplicationShortcut - onActivated: changePos(-2) - } - - Shortcut { - sequence: "Up" - context: Qt.ApplicationShortcut - onActivated: changePos(2) - } - - Shortcut { - sequence: "PageDown" - context: Qt.ApplicationShortcut - onActivated: changePos(-20) - } - - Shortcut { - sequence: "PageUp" - context: Qt.ApplicationShortcut - onActivated: changePos(20) - } - } - // 1 = left - // -1 = right - // 2 = up - // -2 = down - function changePos(direction: int) { - const item = launcher.state?.priorities[launcher.state.selectedPriority] || null; - if (!item) - return; - - let set = 0; - - switch (direction) { - case 1: - if (item.prev()) - set = -1; - break; - case -1: - if (item.next()) - set = 1; - break; - case 2: - if (item.up()) - set = -1; - break; - case -2: - if (item.down()) - set = 1; - break; - case 20: - if (item.pgup()) - set = -1; - break; - case -20: - if (item.pgdn()) - set = 1; - break; - } - - switch (set) { - case 1: - if (launcher.state.selectedPriority < launcher.state.priorities.length - 1) - launcher.state.selectedPriority++; - break; - case -1: - if (launcher.state.selectedPriority > 0) - launcher.state.selectedPriority--; - break; - } - - selectionSync.running = true; - } - - implicitWidth: container.width - implicitHeight: container.height -} diff --git a/launcher/components/App.qml b/launcher/components/App.qml deleted file mode 100644 index 0bb5fd9..0000000 --- a/launcher/components/App.qml +++ /dev/null @@ -1,181 +0,0 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts - -import qs.components -import qs.services -import qs.config - -IComponent { - id: root - - property list entries: active ? AppSearch.query(inputCleaned) : [] - property int selectedIndex: -1 - property bool mouseTriggered: false - - name: "Applications" - - implicitHeight: valid ? layout.height : 0 - - preview: Component { - IconImage { - source: Quickshell.iconPath(answer, "image-missing") - implicitWidth: General.appIconSize - implicitHeight: General.appIconSize - } - } - - property string predictiveCompletion: valid ? entries[selectedIndex].name.slice(input.length) : "" - - process: function () { - const isValid = entries.length > 0; - const selected = isValid ? entries[selectedIndex] : ""; - return { - valid: isValid, - priority: isValid, - answer: selected?.icon || "" - }; - } - - exec: function () { - SelectionState.selected.modelData.execute(); - } - - up: function () { - if (selectedIndex <= 0) - return true; - mouseTriggered = false; - selectedIndex--; - } - down: function () { - if (selectedIndex + 1 > listView.count - 1) - return true; - mouseTriggered = false; - selectedIndex++; - } - - home: function () { - if (listView.count === 0) - return true; - mouseTriggered = false; - selectedIndex = -1; - selectedIndex = 0; - } - end: function () { - if (listView.count === 0) - return true; - mouseTriggered = false; - selectedIndex = listView.count; - selectedIndex = listView.count - 1; - } - - pgup: function () { - if (listView.count === 0) - return true; - mouseTriggered = false; - const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); - if (selectedIndex - pageSize < 0) { - selectedIndex = 0; - } else { - selectedIndex -= pageSize; - } - } - - pgdn: function () { - if (listView.count === 0) - return true; - mouseTriggered = false; - const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); - if (selectedIndex + pageSize >= listView.count) { - selectedIndex = listView.count - 1; - } else { - selectedIndex += pageSize; - } - } - - onEntriesChanged: { - if (selectedIndex !== -1) { - syncSelectionState(); - return; - } - selectedIndex = 0; - listView.contentY = 0; - syncSelectionState(); - } - - onSelectedIndexChanged: { - syncSelectionState(); - if (selectedIndex < 0 || selectedIndex >= listView.count || mouseTriggered) - return; - Qt.callLater(() => { - listView.positionViewAtIndex(selectedIndex, ListView.Contain); - }); - } - - syncSelectionState: function () { - Qt.callLater(() => state.selected = listView.itemAtIndex(selectedIndex)); - } - - IInnerComponent { - id: layout - fromParent: false - width: parent.width - height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) - - ListView { - id: listView - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: entries - spacing: 0 - - delegate: Item { - required property DesktopEntry modelData - required property int index - width: item.implicitWidth + Launcher.innerMargin * 4 - height: item.height + Launcher.innerMargin * 4 - - RowLayout { - id: item - scale: index === selectedIndex ? 1.01 : 0.99 - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: Launcher.innerMargin * 2 - spacing: Launcher.innerMargin * 2 - - IconImage { - source: Quickshell.iconPath(modelData.icon, "image-missing") - implicitWidth: General.appIconSize - implicitHeight: General.appIconSize - } - IText { - text: modelData.name - renderType: Text.QtRendering - color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 - } - - Behavior on scale { - ISpringAnimation {} - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onPositionChanged: { - root.mouseTriggered = true; - root.selectedIndex = index; - } - onExited: { - root.mouseTriggered = true; - } - onPressed: { - root._exec(); - } - } - } - } - } -} diff --git a/launcher/components/Calc.qml b/launcher/components/Calc.qml deleted file mode 100644 index c6cdbc1..0000000 --- a/launcher/components/Calc.qml +++ /dev/null @@ -1,81 +0,0 @@ -import Quickshell -import QtQuick -import QtQuick.Layouts - -import qs.components -import qs.services -import qs.config - -IComponent { - property int cursorPosition: SelectionState.cursorPosition - - name: "Calculator" - preview: Component { - Icon { - text: "" - } - } - - process: function () { - const isValid = /^(?=.*\d)(?=.*[+\-*\/\^])[0-9+\-*\/\^().\s]+$/.test(input); - const answer = isValid ? String(eval(input.replace(/\^/g, '**').replace(/([\d)])\(/g, '$1*(').replace(/\)([\d])/g, ')*$1'))) : null; - return { - valid: isValid, - priority: isValid, - answer: answer, - predictiveCompletion: isValid ? ' = ' + answer : '' - }; - } - exec: function () { - Clip.copy(answer); - } - - IInnerComponent { - RowLayout { - spacing: 0 - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: parent.height - - Item { - Layout.fillWidth: true - IText { - anchors.centerIn: parent - elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight - width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) - clip: true - renderType: Text.CurveRendering - visible: valid - text: input.replace(/ /g, "").replace(/\+/g, " + ").replace(/-/g, " - ").replace(/\*/g, " × ").replace(/\//g, " ÷ ").replace(/%/g, " % ").replace(/\(/g, " ( ").replace(/\)/g, " ) ") - font { - pixelSize: Launcher.widgetFontSize - bold: true - } - } - } - - IText { - visible: valid - text: "→" - font.pixelSize: Launcher.widgetFontSize - font.bold: true - } - - Item { - Layout.fillWidth: true - IText { - animate: true - anchors.centerIn: parent - width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) - renderType: Text.CurveRendering - visible: valid - text: valid ? answer : '' - font { - pixelSize: Launcher.widgetFontSize - bold: true - } - } - } - } - } -} diff --git a/launcher/components/Clip.qml b/launcher/components/Clip.qml deleted file mode 100644 index bc8d65f..0000000 --- a/launcher/components/Clip.qml +++ /dev/null @@ -1,199 +0,0 @@ -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts - -import qs.components -import qs.services -import qs.config - -IComponent { - id: root - - property var clipHist: active ? Clip.query(inputCleaned) : [] - property int selectedIndex: -1 - property bool mouseTriggered: false - - name: "Clipboard" - - prefix: "clip" - - implicitHeight: valid ? layout.height : 0 - - process: function () { - const isValid = clipHist.length > 0; - return { - valid: isValid, - priority: isValid - }; - } - - exec: function () { - Clip.decodeAndCopy(clipHist[selectedIndex].raw); - } - - up: function () { - if (selectedIndex <= 0) - return true; - mouseTriggered = false; - selectedIndex--; - } - down: function () { - if (selectedIndex + 1 > listView.count - 1) - return true; - mouseTriggered = false; - selectedIndex++; - } - - onClipHistChanged: { - if (selectedIndex !== -1) { - syncSelectionState(); - return; - } - mouseTriggered = false; - selectedIndex = 0; - listView.positionViewAtBeginning(); - syncSelectionState(); - } - - onSelectedIndexChanged: { - if (!mouseTriggered && selectedIndex >= 0 && selectedIndex < listView.count) { - Qt.callLater(() => { - listView.positionViewAtIndex(selectedIndex, ListView.Contain); - }); - } - syncSelectionState(); - } - - syncSelectionState: function () { - Qt.callLater(() => state.selected = listView.itemAtIndex(selectedIndex)); - } - - IInnerComponent { - id: layout - fromParent: false - width: parent.width - height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - ListView { - id: listView - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: clipHist - spacing: 0 - - delegate: Item { - required property var modelData - required property int index - width: Math.min(item.implicitWidth + Launcher.innerMargin * 4, listView.width) - height: item.height + Launcher.innerMargin * 4 - - Row { - id: item - anchors.left: parent.left - anchors.top: parent.top - anchors.margins: Launcher.innerMargin * 2 - spacing: Launcher.innerMargin * 2 - clip: true - - IconImage { - scale: index === root.selectedIndex ? 1.01 : 0.9 - source: modelData?.appIcon - implicitSize: parent.height - - Behavior on scale { - ISpringAnimation {} - } - } - - Column { - Loader { - sourceComponent: modelData?.type === "image" ? img : text - readonly property Component text: IText { - text: modelData?.data || "" - color: index === root.selectedIndex ? Colors.foreground1 : Colors.foreground2 - font.pixelSize: Launcher.widgetFontSize - } - readonly property Component img: Image { - source: modelData?.data || "" - } - } - Row { - spacing: Launcher.innerMargin - property string sinceWhen: TimeDate.sinceWhen(modelData?.timestamp) || "" - IText { - text: modelData.type - color: index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 - font.pixelSize: Launcher.widgetFontSize / 1.35 - } - IText { - visible: parent.sinceWhen - text: '·' - color: index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 - font.pixelSize: Launcher.widgetFontSize / 1.35 - } - IText { - text: parent.sinceWhen || "" - color: index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 - font.pixelSize: Launcher.widgetFontSize / 1.35 - } - } - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onPositionChanged: { - root.mouseTriggered = true; - root.selectedIndex = index; - } - onExited: { - root.mouseTriggered = true; - } - onPressed: { - root._exec(); - } - } - } - } - - Flickable { - id: preview - clip: true - Layout.fillWidth: true - Layout.fillHeight: true - - contentWidth: loader.item.width - contentHeight: loader.item.height - flickableDirection: Flickable.VerticalFlick - boundsBehavior: Flickable.StopAtBounds - - Loader { - id: loader - sourceComponent: clipHist[selectedIndex]?.type === "image" ? img : text - property Component text: TextEdit { - id: textEdit - readOnly: true - width: preview.width - color: Colors.foreground1 - selectionColor: Colors.background3 - text: clipHist[selectedIndex]?.decoded || "" - } - property Component img: Image { - id: image - width: preview.width - height: preview.height - source: clipHist[selectedIndex]?.image || "" - fillMode: Image.PreserveAspectFit - } - } - } - } - } -} diff --git a/launcher/components/Emoji.qml b/launcher/components/Emoji.qml deleted file mode 100644 index d1343e6..0000000 --- a/launcher/components/Emoji.qml +++ /dev/null @@ -1,176 +0,0 @@ -import Quickshell -import Quickshell.Io -import QtQuick -import QtQuick.Layouts - -import qs.components -import qs.services -import qs.config - -IComponent { - id: root - - property var emojis: active ? Emoji.query(input) : [] - property int selectedIndex: -1 - - name: "Emoji" - prefix: name.toLowerCase() + " " - - onEmojisChanged: { - Qt.callLater(() => { - if (selectedIndex !== -1) { - syncSelectionState(); - return; - } - selectedIndex = emojis.length > 0 ? 0 : -1; - syncSelectionState(); - if (loader.item && loader.item.flickable) - loader.item.flickable.contentY = 0; - }); - } - - onSelectedIndexChanged: { - if (!loader.view || !loader.item || !loader.item.flickable) - return; - if (selectedIndex < 0 || selectedIndex >= loader.view.count) - return; - Qt.callLater(() => { - syncSelectionState(); - loader.view.currentIndex = selectedIndex; - loader.view.positionViewAtIndex(selectedIndex, GridView.Contain); - }); - } - - process: function () { - const isValid = selectedIndex >= 0 && emojis.length > 0; - const select = isValid ? emojis[selectedIndex] : null; - return { - valid: isValid, - priority: isValid, - answer: isValid ? select.emoji : "" - }; - } - - home: function () { - if (!loader.view.count) - return true; - selectedIndex = 0; - } - end: function () { - if (!loader.view.count) - return true; - selectedIndex = loader.view.count - 1; - } - prev: function () { - if (selectedIndex <= 0) - return true; - selectedIndex--; - } - next: function () { - if (selectedIndex + 1 >= emojis.length) - return true; - selectedIndex++; - } - up: function () { - const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; - if (selectedIndex - cols < 0) - return true; - selectedIndex -= cols; - } - down: function () { - const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; - if (selectedIndex + cols >= emojis.length) - return true; - selectedIndex += cols; - } - - exec: function () { - if (selectedIndex >= 0 && emojis.length > 0) - Emoji.copy(emojis[selectedIndex].emoji); - } - - syncSelectionState: function () { - Qt.callLater(() => state.selected = loader.view.itemAtIndex(selectedIndex)); - } - - IInnerComponent { - id: innerLoader - - Loader { - id: loader - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: parent.height - sourceComponent: show - property var view: null - - readonly property Component show: Component { - Item { - property alias flickable: grid - - GridView { - id: grid - anchors.fill: parent - clip: true - boundsBehavior: Flickable.StopAtBounds - cacheBuffer: cellHeight * 2 - model: emojis - interactive: true - flow: GridView.FlowLeftToRight - snapMode: GridView.NoSnap - - cellWidth: Math.min(width / Math.max(1, Math.floor(width / (Launcher.widgetFontSize * 5))), width / 3) - cellHeight: Launcher.widgetFontSize * 2.2 + Launcher.innerMargin * 2 - - delegate: Item { - required property var modelData - required property int index - width: grid.cellWidth - height: grid.cellHeight - scale: index === selectedIndex ? 1.03 : 0.97 - - IRect { - anchors.fill: parent - color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) - radius: Launcher.widgetRadius - - Column { - id: chipContent - anchors.centerIn: parent - spacing: 2 - - Icon { - anchors.horizontalCenter: parent.horizontalCenter - text: modelData.emoji - font.pixelSize: Launcher.widgetFontSize * 1.2 - } - IText { - anchors.horizontalCenter: parent.horizontalCenter - text: modelData.name - font.pixelSize: Launcher.widgetFontSize * 0.7 - color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 - elide: Text.ElideRight - width: parent.parent.width - Launcher.innerMargin * 2 - } - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onPositionChanged: root.selectedIndex = index - onPressed: root._exec() - } - } - - Behavior on scale { - ISpringAnimation {} - } - } - - Component.onCompleted: loader.view = grid - } - } - } - } - } -} diff --git a/launcher/components/Finder.qml b/launcher/components/Finder.qml deleted file mode 100644 index e69de29..0000000 diff --git a/launcher/components/Flight.qml b/launcher/components/Flight.qml deleted file mode 100644 index e69de29..0000000 diff --git a/launcher/components/IComponent.qml b/launcher/components/IComponent.qml deleted file mode 100644 index 4f47b88..0000000 --- a/launcher/components/IComponent.qml +++ /dev/null @@ -1,93 +0,0 @@ -import Quickshell -import QtQuick - -import qs.components -import qs.config - -IRect { - id: root - // virtual properties - property bool standalone: false - property var state: SelectionState - property string name // Component/File name - readonly property string _input: state.input - readonly property string input: !standalone ? _input.slice(prefix.length) : _input - readonly property string inputCleaned: input.toLowerCase().trim() - property string prefix - property bool active: !standalone ? input && _input.startsWith(prefix) : true - property bool valid: !standalone ? processed?.valid || false : true - property bool priority: !standalone ? valid && processed?.priority || false : true // please set priority to false if it's invalid - property string answer: processed?.answer || "" - property Component preview: Component { - IText { - animate: true - text: answer - } - } - property string predictiveCompletion: processed?.predictiveCompletion || "" // would technically work with just from answer, but stuff like calc's answer wouldnt have anything to do with the input - property var processed: active ? process() : {} // cached process, thought qml would do that automatically :/ - property var process: () => ({ - valid: valid, - priority: priority, - answer: answer, - preview: preview, - predictiveCompletion: predictiveCompletion - })// use the current IRect if valid is set - property var syncSelectionState: () => {} - // actions - property var up: () => ({ - top: false - }) - property var down: () => ({ - bottom: false - }) - property var prev: () => {} - property var next: () => {} - property var exec: () => {} - property var home: () => {} - property var end: () => {} - property var pgup: () => {} - property var pgdn: () => {} - - signal close - - function _exec() { - exec(); - close(); - } - - opacity: valid ? 1 : 0 - y: valid ? 0 : -Launcher.widgetHeight - - implicitWidth: valid ? Launcher.widgetWidth : 0 - implicitHeight: valid ? Launcher.widgetHeight : 0 - - // implicitWidth: Launcher.widgetWidth - // implicitHeight: Launcher.widgetHeight - - radius: Launcher.widgetRadius - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetBgTransparency) // TODO when prioritized, highlight - - onPriorityChanged: { - // sync with state.priorities - Qt.callLater(() => { - if (!standalone) - state.priorities = [...[...state.priorities, root].reduce((s, x) => (s[(s.has(x) && 'delete') || 'add'](x), s), new Set())]; - else - state.priorities = [root]; - }); - } - - Behavior on y { - NumberAnimation { - duration: General.animationDuration / 4 - easing.type: Easing.InOutQuad - } - } - - Component.onCompleted: { - Qt.callLater(() => { - syncSelectionState(); - }); - } -} diff --git a/launcher/components/IInnerComponent.qml b/launcher/components/IInnerComponent.qml deleted file mode 100644 index 7bbc9f0..0000000 --- a/launcher/components/IInnerComponent.qml +++ /dev/null @@ -1,34 +0,0 @@ -import QtQuick -import QtQuick.Layouts - -import qs.components -import qs.config - -ColumnLayout { - property bool fromParent: true - property alias titleBar: titleBar - anchors.fill: fromParent ? parent : undefined - spacing: 0 - IRect { - id: titleBar - Layout.fillWidth: true - Layout.fillHeight: false - visible: Launcher.showWidgetTitle && valid - height: childrenRect.height - width: parent.width - topLeftRadius: Launcher.widgetRadius - topRightRadius: Launcher.widgetRadius - color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetTitleBgTransparency) - - IText { - anchors { - top: parent.top - left: parent.left - leftMargin: Launcher.innerMargin - } - visible: valid - font.pixelSize: Launcher.widgetFontSize - text: name - } - } -} diff --git a/launcher/components/SelectionState.qml b/launcher/components/SelectionState.qml deleted file mode 100644 index 1ab577a..0000000 --- a/launcher/components/SelectionState.qml +++ /dev/null @@ -1,49 +0,0 @@ -pragma Singleton -import Quickshell -import QtQuick - -Singleton { - id: root - - PersistentProperties { - id: persist - reloadableId: "launcherSelectionState" - - property string input: "" - property int cursorPosition: 0 - property int selectedPriority: 0 - property int previousSelectedPriority: 0 - } - - property Item selected: null - property alias input: persist.input - property alias cursorPosition: persist.cursorPosition - property var priorities: [] - property alias selectedPriority: persist.selectedPriority - property alias previousSelectedPriority: persist.previousSelectedPriority - property var widgets: [] - - onSelectedPriorityChanged: { - if (selectedPriority > previousSelectedPriority) { - priorities[selectedPriority].home(); - priorities[selectedPriority].syncSelectionState(); - } else { - priorities[selectedPriority].end(); - priorities[selectedPriority].syncSelectionState(); - } - - previousSelectedPriority = selectedPriority; - } - // order them based on widgets(which came from order from config) - onPrioritiesChanged: { - for (let i = priorities.length - 1; i >= 0; i--) - if (!priorities[i] || priorities[i].priority !== true) - priorities.splice(i, 1); - - priorities.sort((a, b) => (widgets.indexOf(a) - widgets.indexOf(b)) || 0); - - priorities.forEach((item, index) => {}); - - priorities[selectedPriority]?.syncSelectionState(); - } -} diff --git a/launcher/components/Spell.qml b/launcher/components/Spell.qml deleted file mode 100644 index fe01c3f..0000000 --- a/launcher/components/Spell.qml +++ /dev/null @@ -1,235 +0,0 @@ -import Quickshell -import Quickshell.Io -import QtQuick -import QtQuick.Layouts - -import qs.components -import qs.services -import qs.config - -IComponent { - id: root - - property list aspell - property int selectedIndex: -1 - - name: "Spell" - prefix: name.toLowerCase() + ' ' - - onValidChanged: { - if (valid) - return; - - aspell = []; - proc.running = false; - selectedIndex = -1; - return; - } - - onAspellChanged: { - if (selectedIndex !== -1) { - syncSelectionState(); - return; - } - selectedIndex = aspell.length > 0 ? 0 : -1; - // selectedIndexChanged dont get called somehow - syncSelectionState(); - if (loader.item && loader.item.flickable) { - loader.item.flickable.contentY = 0; - } - } - - onSelectedIndexChanged: { - if (!loader.view || !loader.item || !loader.item.flickable) - return; - if (selectedIndex < 0 || selectedIndex >= loader.view.count) - return; - Qt.callLater(() => { - syncSelectionState(); - loader.view.currentIndex = selectedIndex; - loader.view.positionViewAtIndex(selectedIndex, GridView.Contain); - }); - } - - process: function () { - Qt.callLater(() => { - proc.running = true; - }); - - const isValid = selectedIndex >= 0 && aspell.length > 0; - const selected = isValid ? aspell[selectedIndex] : ""; - const answer = selected === "*" ? input : selected; - - return { - valid: isValid, - priority: isValid, - answer: answer || "", - predictiveCompletion: answer?.slice(input.length) || "" - }; - } - - home: function () { - if (loader.view.count === 0) - return true; - selectedIndex = -1; - selectedIndex = 0; - } - end: function () { - if (loader.view.count === 0) - return true; - selectedIndex = loader.view.count; - selectedIndex = loader.view.count - 1; - } - - prev: function () { - if (selectedIndex <= 0) - return true; - selectedIndex--; - } - - next: function () { - if (selectedIndex + 1 >= aspell.length) - return true; - selectedIndex++; - } - - up: function () { - const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; - if (selectedIndex - cols < 0) - return true; - selectedIndex -= cols; - } - - down: function () { - const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; - if (selectedIndex + cols >= aspell.length) - return true; - selectedIndex += cols; - } - - exec: function () { - if (selectedIndex >= 0) - if (aspell[selectedIndex] !== "*") - Clip.copy(aspell[selectedIndex]); - else - Clip.copy(input); - } - - syncSelectionState: function () { - Qt.callLater(() => state.selected = loader.view.itemAtIndex(selectedIndex)); - } - - IInnerComponent { - id: innerLoader - Loader { - id: loader - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: parent.height - sourceComponent: aspell[0] === "*" ? correct : show - property var view: null - - readonly property Component show: Component { - Item { - property alias flickable: grid - - GridView { - id: grid - anchors.fill: parent - clip: true - boundsBehavior: Flickable.StopAtBounds - cacheBuffer: 0 - model: aspell - interactive: true - flow: GridView.FlowLeftToRight - snapMode: GridView.NoSnap - - cellWidth: Math.min(width / Math.max(1, Math.floor(width / (Launcher.widgetFontSize * 4))), width / 2) - cellHeight: Launcher.widgetFontSize * 1.4 + Launcher.innerMargin * 2 - - delegate: Item { - required property var modelData - required property int index - - width: grid.cellWidth - height: grid.cellHeight - scale: index === selectedIndex ? 1.03 : 0.97 - - IRect { - anchors.fill: parent - color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) - radius: Launcher.widgetRadius - - IText { - id: chipContent - anchors.centerIn: parent - text: modelData - font.pixelSize: Launcher.widgetFontSize - elide: Text.ElideRight - color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 - } - - MouseArea { - anchors.fill: parent - hoverEnabled: true - onPositionChanged: { - root.selectedIndex = index; - } - onPressed: { - root._exec(); - } - } - } - - Behavior on scale { - ISpringAnimation {} - } - } - - Component.onCompleted: { - loader.view = grid; - } - } - } - } - - readonly property Component correct: Component { - IRect { - Layout.fillWidth: true - color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.widgetBgTransparency / 4) - radius: Launcher.widgetRadius - IText { - anchors.centerIn: parent - text: "Correct" - font.pixelSize: Launcher.widgetFontSize - anchors.horizontalCenter: parent.horizontalCenter - } - } - } - } - } - - Process { - id: proc - command: ["sh", "-c", "echo " + input + " | aspell -a"] - property bool found: false - stdout: SplitParser { - onRead: data => { - if (data.startsWith("&")) { - proc.found = true; - aspell = data.split(/\W+/).filter(input => /^[A-Za-z]+$/.test(input)).slice(1); - } else if (data.startsWith("*")) { - proc.found = true; - aspell = ["*"]; - } - } - } - onStarted: { - found = false; - } - onExited: { - if (!found) - aspell = []; - } - } -} diff --git a/launcher/components/Time.qml b/launcher/components/Time.qml deleted file mode 100644 index e69de29..0000000 diff --git a/launcher/components/Translate.qml b/launcher/components/Translate.qml deleted file mode 100644 index e69de29..0000000 diff --git a/launcher/components/Weather.qml b/launcher/components/Weather.qml deleted file mode 100644 index e69de29..0000000 diff --git a/launcher/components/Web.qml b/launcher/components/Web.qml deleted file mode 100644 index e69de29..0000000 diff --git a/launcher/todo.md b/launcher/todo.md deleted file mode 100644 index 3ed0252..0000000 --- a/launcher/todo.md +++ /dev/null @@ -1,37 +0,0 @@ -when finished: -- [x] reloads dynamically while typing in -- [x] prefix: ... for certain ambiguous queries such as "pi" or "e" for math -- [ ] bang (like ddg's) -- [ ] web search catch all -- [x] height dynamically shrink/grow according to content - -- [ ] web: - - [ ] show multiple options: - - [ ] first - search w/ search engine and open in browser - - [ ] second - fallback/second option - - [ ] custom quick search/fetch - - [ ] e.g. wikipedia: (also stuff like spotify, youtube) - - [ ] match title - - [ ] match first paragraph/description - -- [ ] files: - - [ ] latest modified date - - [ ] file size - - [ ] where it belongs - - [ ] icon/preview/quick look - -- [ ] math: - - [x] expression - - [ ] natural language (i.e. "5 of 3") - - [ ] unit conversion - - [ ] temperature - - [ ] length - - [ ] weight - - [ ] currency - - [ ] color - - [ ] time - -emoji picker -desktopentry action - -stop querying if prev hav no results diff --git a/popups/IPopup.qml b/popups/IPopup.qml new file mode 100644 index 0000000..59d569d --- /dev/null +++ b/popups/IPopup.qml @@ -0,0 +1,18 @@ +import Quickshell.Wayland + +import qs.components + +IWindow { + required property var parentLoader + // TODO: optional only display on focused screen + focusable: true + + layer: WlrLayer.Overlay + + anchors { + top: false + bottom: false + left: false + right: false + } +} diff --git a/popups/launcher/ILauncher.qml b/popups/launcher/ILauncher.qml new file mode 100644 index 0000000..325ab2c --- /dev/null +++ b/popups/launcher/ILauncher.qml @@ -0,0 +1,15 @@ +import Quickshell.Hyprland + +import ".." + +IPopup { + id: root + + HyprlandFocusGrab { + active: true + windows: [root] + onCleared: { + parentLoader.active = false; // or active = true again + } + } +} diff --git a/popups/launcher/Launcher.qml b/popups/launcher/Launcher.qml new file mode 100644 index 0000000..25b2926 --- /dev/null +++ b/popups/launcher/Launcher.qml @@ -0,0 +1,388 @@ +import Quickshell +import QtQuick +import QtQuick.Layouts + +import "components" +import qs.components +import qs.config + +// TODO: currently, we're relying on the compositor to provide the animation which ig is fine, but we should use our own in the future, however it might look weird combined with the compositors's +ILauncher { + id: launcher + + property list widgetItems: Launcher.widgets + property var state: SelectionState + + name: "quickshell::launcher::launcher" + + anchors.top: true + margins.top: Launcher.topMargin + aboveWindows: true + exclusionMode: ExclusionMode.Ignore + + IRect { + id: container + property var selection: launcher.state?.selected || null + property var mappedSelection: selection?.mapToItem(null, 0, 0) + onMappedSelectionChanged: { + // launcher.state?.selected may be slow.. smh qt + // my guess is mapToItem is called when its not finished doing whatever it needs to do, + // so it doesnt get the right vars, but it doesn't update after since it already triggered it + selectionSync.running = true; + } + Timer { + id: selectionSync + running: false + repeat: false + // sync w/ IComponent y animation duration + interval: General.animationDuration / 4 + onTriggered: { + parent.mappedSelection = launcher.state?.selected?.mapToItem(null, 0, 0); + } + } + + IRect { + id: activeRect + visible: parent.mappedSelection || false + y: parent.mappedSelection?.y || 0 + x: parent.mappedSelection?.x || 0 + implicitWidth: launcher.state?.selected?.width || 0 + implicitHeight: launcher.state?.selected?.height || 0 + radius: Launcher.borderRadius + color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.bgTransparency) + Behavior on x { + ISpringAnimation {} + } + Behavior on y { + ISpringAnimation {} + } + Behavior on implicitWidth { + ISpringAnimation {} + } + Behavior on height { + ISpringAnimation {} + } + } + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.bgTransparency) + radius: Launcher.borderRadius + + width: Math.max(root.width, Launcher.defaultWidth) + Launcher.innerMargin + height: root.height + + IRect { + id: root + property alias input: textInput.text + + height: searchBar.height + widgets.height + textInput.height / 2 // * 1.5 + width: Math.max(widgets.implicitWidth, Launcher.defaultWidth) + + anchors.centerIn: parent + + color: "transparent" + + RowLayout { + id: searchBar + anchors { + top: parent.top + left: parent.left + right: parent.right + leftMargin: Launcher.innerMargin + rightMargin: Launcher.innerMargin + // try to center it when no widgets are present + topMargin: widgets.height === Launcher.widgets.length * 5 - 5 ? parent.height / 2 - textInput.height / 2 : Launcher.innerMargin // textInput or Icon is the height of this + + Behavior on topMargin { + ISpringAnimation {} + // NumberAnimation { + // duration: General.animationDuration / 2 + // } + } + } + + spacing: Launcher.innerMargin * 2 + + Icon { + text: "" + } + + RowLayout { + spacing: 0 + TextInput { + id: textInput + property bool pendingUpdate: false + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.maximumWidth: implicitWidth + + color: Colors.foreground1 + selectionColor: Colors.background3 + + text: launcher.state?.input || "" + clip: true + focus: true + + antialiasing: true + smooth: true + font { + pixelSize: Style.font.size.large + family: Style.font.family.sans + wordSpacing: 5 + } + // move to different file if we have more textinput + cursorDelegate: IRect { + id: cursor + color: Colors.cursor + // width: 1.5 + width: 2 + height: 1 + // blinking animation + SequentialAnimation { + running: true + loops: Animation.Infinite + PauseAnimation { + duration: 500 + } + ParallelAnimation { + PropertyAnimation { + target: cursor + property: "opacity" + to: 0 + duration: General.animationDuration / 5 + } + PropertyAnimation { + target: cursor + property: "scale" + to: 0 + duration: General.animationDuration / 5 + } + } + PauseAnimation { + duration: 500 + } + ParallelAnimation { + PropertyAnimation { + target: cursor + property: "opacity" + to: 1 + duration: General.animationDuration / 5 + } + PropertyAnimation { + target: cursor + property: "scale" + to: 1 + duration: General.animationDuration / 5 + } + } + } + } + onTextChanged: { + if (launcher.state.input !== textInput.text) + pendingUpdate = true; + Qt.callLater(() => { + if (pendingUpdate) { + pendingUpdate = false; + launcher.state.input = textInput.text; // TODO: find ways to optimize this, like how i was using an alias + } + }); + } + onCursorPositionChanged: { + launcher.state.cursorPosition = textInput.cursorPosition; + } + // onActiveFocusChanged: { + // parentLoader.active = activeFocus; + // } + onAccepted: { + launcher.state?.priorities[launcher.state.selectedPriority]?._exec(); // TODO: kde like waiting animation for app to launch + parentLoader.active = false; + textInput.text = ""; + } + Component.onCompleted: { + selectAll(); + } + } + IRect { + opacity: launcher.state?.priorities[0]?.predictiveCompletion ? 1 : 0 + Layout.fillHeight: true + Layout.preferredWidth: text.width + radius: Launcher.predictiveCompletionRadius + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + IText { + id: text + // animate: true // too jumpy + color: Colors.foreground3 + renderType: Text.CurveRendering + antialiasing: true + smooth: true + font { + pixelSize: Style.font.size.large + family: Style.font.family.sans + wordSpacing: 5 + } + text: launcher.state?.priorities[0]?.predictiveCompletion || "" + } + Behavior on Layout.preferredWidth { + ISpringAnimation {} + // NumberAnimation { + // duration: General.animationDuration / 5 + // } + } + Behavior on opacity { + NumberAnimation { + duration: General.animationDuration / 2 + easing.type: Easing.InOutQuad + } + } + } + Item { + Layout.fillWidth: true + } + Loader { + active: launcher.state?.priorities[0]?.priority || false // .priority should always be true if it's in priorities + sourceComponent: launcher.state?.priorities[0]?.preview + } + } + } + + ColumnLayout { + id: widgets + anchors { + top: searchBar.bottom + left: parent.left + right: parent.right + topMargin: Launcher.innerMargin * 1.5 // gap in between, maybe seperator FIXME + } + + Repeater { + model: launcher.widgetItems + delegate: Loader { + active: true + sourceComponent: modelData + asynchronous: true + onLoaded: { + launcher.state.widgets[index] = item; + // FIXME: prev predictiveCompletion still exists here + // workaround atm: just clear it or preserve the text manually + } + Connections { + target: item + ignoreUnknownSignals: true + function onClose() { + parentLoader.active = false; + } + } + } + } + } + } + + // not Keys.onPressed because textinput will steal it + Shortcut { + sequence: "Escape" + context: Qt.ApplicationShortcut + onActivated: parentLoader.active = false + } + + Shortcut { + sequence: "Ctrl+H" + context: Qt.ApplicationShortcut + onActivated: changePos(1) + } + + Shortcut { + sequence: "Ctrl+J" + context: Qt.ApplicationShortcut + onActivated: changePos(-2) + } + + Shortcut { + sequence: "Ctrl+K" + context: Qt.ApplicationShortcut + onActivated: changePos(2) + } + + Shortcut { + sequence: "Ctrl+L" + context: Qt.ApplicationShortcut + onActivated: changePos(-1) + } + + Shortcut { + sequence: "Down" + context: Qt.ApplicationShortcut + onActivated: changePos(-2) + } + + Shortcut { + sequence: "Up" + context: Qt.ApplicationShortcut + onActivated: changePos(2) + } + + Shortcut { + sequence: "PageDown" + context: Qt.ApplicationShortcut + onActivated: changePos(-20) + } + + Shortcut { + sequence: "PageUp" + context: Qt.ApplicationShortcut + onActivated: changePos(20) + } + } + // 1 = left + // -1 = right + // 2 = up + // -2 = down + function changePos(direction: int) { + const item = launcher.state?.priorities[launcher.state.selectedPriority] || null; + if (!item) + return; + + let set = 0; + + switch (direction) { + case 1: + if (item.prev()) + set = -1; + break; + case -1: + if (item.next()) + set = 1; + break; + case 2: + if (item.up()) + set = -1; + break; + case -2: + if (item.down()) + set = 1; + break; + case 20: + if (item.pgup()) + set = -1; + break; + case -20: + if (item.pgdn()) + set = 1; + break; + } + + switch (set) { + case 1: + if (launcher.state.selectedPriority < launcher.state.priorities.length - 1) + launcher.state.selectedPriority++; + break; + case -1: + if (launcher.state.selectedPriority > 0) + launcher.state.selectedPriority--; + break; + } + + selectionSync.running = true; + } + + implicitWidth: container.width + implicitHeight: container.height +} diff --git a/popups/launcher/components/App.qml b/popups/launcher/components/App.qml new file mode 100644 index 0000000..0bb5fd9 --- /dev/null +++ b/popups/launcher/components/App.qml @@ -0,0 +1,181 @@ +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + id: root + + property list entries: active ? AppSearch.query(inputCleaned) : [] + property int selectedIndex: -1 + property bool mouseTriggered: false + + name: "Applications" + + implicitHeight: valid ? layout.height : 0 + + preview: Component { + IconImage { + source: Quickshell.iconPath(answer, "image-missing") + implicitWidth: General.appIconSize + implicitHeight: General.appIconSize + } + } + + property string predictiveCompletion: valid ? entries[selectedIndex].name.slice(input.length) : "" + + process: function () { + const isValid = entries.length > 0; + const selected = isValid ? entries[selectedIndex] : ""; + return { + valid: isValid, + priority: isValid, + answer: selected?.icon || "" + }; + } + + exec: function () { + SelectionState.selected.modelData.execute(); + } + + up: function () { + if (selectedIndex <= 0) + return true; + mouseTriggered = false; + selectedIndex--; + } + down: function () { + if (selectedIndex + 1 > listView.count - 1) + return true; + mouseTriggered = false; + selectedIndex++; + } + + home: function () { + if (listView.count === 0) + return true; + mouseTriggered = false; + selectedIndex = -1; + selectedIndex = 0; + } + end: function () { + if (listView.count === 0) + return true; + mouseTriggered = false; + selectedIndex = listView.count; + selectedIndex = listView.count - 1; + } + + pgup: function () { + if (listView.count === 0) + return true; + mouseTriggered = false; + const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); + if (selectedIndex - pageSize < 0) { + selectedIndex = 0; + } else { + selectedIndex -= pageSize; + } + } + + pgdn: function () { + if (listView.count === 0) + return true; + mouseTriggered = false; + const pageSize = Math.floor(listView.height / (General.appIconSize + Launcher.innerMargin * 2)); + if (selectedIndex + pageSize >= listView.count) { + selectedIndex = listView.count - 1; + } else { + selectedIndex += pageSize; + } + } + + onEntriesChanged: { + if (selectedIndex !== -1) { + syncSelectionState(); + return; + } + selectedIndex = 0; + listView.contentY = 0; + syncSelectionState(); + } + + onSelectedIndexChanged: { + syncSelectionState(); + if (selectedIndex < 0 || selectedIndex >= listView.count || mouseTriggered) + return; + Qt.callLater(() => { + listView.positionViewAtIndex(selectedIndex, ListView.Contain); + }); + } + + syncSelectionState: function () { + Qt.callLater(() => state.selected = listView.itemAtIndex(selectedIndex)); + } + + IInnerComponent { + id: layout + fromParent: false + width: parent.width + height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) + + ListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: entries + spacing: 0 + + delegate: Item { + required property DesktopEntry modelData + required property int index + width: item.implicitWidth + Launcher.innerMargin * 4 + height: item.height + Launcher.innerMargin * 4 + + RowLayout { + id: item + scale: index === selectedIndex ? 1.01 : 0.99 + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Launcher.innerMargin * 2 + spacing: Launcher.innerMargin * 2 + + IconImage { + source: Quickshell.iconPath(modelData.icon, "image-missing") + implicitWidth: General.appIconSize + implicitHeight: General.appIconSize + } + IText { + text: modelData.name + renderType: Text.QtRendering + color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 + } + + Behavior on scale { + ISpringAnimation {} + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + root.mouseTriggered = true; + root.selectedIndex = index; + } + onExited: { + root.mouseTriggered = true; + } + onPressed: { + root._exec(); + } + } + } + } + } +} diff --git a/popups/launcher/components/Calc.qml b/popups/launcher/components/Calc.qml new file mode 100644 index 0000000..c6cdbc1 --- /dev/null +++ b/popups/launcher/components/Calc.qml @@ -0,0 +1,81 @@ +import Quickshell +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + property int cursorPosition: SelectionState.cursorPosition + + name: "Calculator" + preview: Component { + Icon { + text: "" + } + } + + process: function () { + const isValid = /^(?=.*\d)(?=.*[+\-*\/\^])[0-9+\-*\/\^().\s]+$/.test(input); + const answer = isValid ? String(eval(input.replace(/\^/g, '**').replace(/([\d)])\(/g, '$1*(').replace(/\)([\d])/g, ')*$1'))) : null; + return { + valid: isValid, + priority: isValid, + answer: answer, + predictiveCompletion: isValid ? ' = ' + answer : '' + }; + } + exec: function () { + Clip.copy(answer); + } + + IInnerComponent { + RowLayout { + spacing: 0 + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + + Item { + Layout.fillWidth: true + IText { + anchors.centerIn: parent + elide: cursorPosition > input.length / 2 ? Text.ElideLeft : Text.ElideRight + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + clip: true + renderType: Text.CurveRendering + visible: valid + text: input.replace(/ /g, "").replace(/\+/g, " + ").replace(/-/g, " - ").replace(/\*/g, " × ").replace(/\//g, " ÷ ").replace(/%/g, " % ").replace(/\(/g, " ( ").replace(/\)/g, " ) ") + font { + pixelSize: Launcher.widgetFontSize + bold: true + } + } + } + + IText { + visible: valid + text: "→" + font.pixelSize: Launcher.widgetFontSize + font.bold: true + } + + Item { + Layout.fillWidth: true + IText { + animate: true + anchors.centerIn: parent + width: Math.min(implicitWidth, parent.width - Launcher.innerMargin * 2) + renderType: Text.CurveRendering + visible: valid + text: valid ? answer : '' + font { + pixelSize: Launcher.widgetFontSize + bold: true + } + } + } + } + } +} diff --git a/popups/launcher/components/Clip.qml b/popups/launcher/components/Clip.qml new file mode 100644 index 0000000..bc8d65f --- /dev/null +++ b/popups/launcher/components/Clip.qml @@ -0,0 +1,199 @@ +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + id: root + + property var clipHist: active ? Clip.query(inputCleaned) : [] + property int selectedIndex: -1 + property bool mouseTriggered: false + + name: "Clipboard" + + prefix: "clip" + + implicitHeight: valid ? layout.height : 0 + + process: function () { + const isValid = clipHist.length > 0; + return { + valid: isValid, + priority: isValid + }; + } + + exec: function () { + Clip.decodeAndCopy(clipHist[selectedIndex].raw); + } + + up: function () { + if (selectedIndex <= 0) + return true; + mouseTriggered = false; + selectedIndex--; + } + down: function () { + if (selectedIndex + 1 > listView.count - 1) + return true; + mouseTriggered = false; + selectedIndex++; + } + + onClipHistChanged: { + if (selectedIndex !== -1) { + syncSelectionState(); + return; + } + mouseTriggered = false; + selectedIndex = 0; + listView.positionViewAtBeginning(); + syncSelectionState(); + } + + onSelectedIndexChanged: { + if (!mouseTriggered && selectedIndex >= 0 && selectedIndex < listView.count) { + Qt.callLater(() => { + listView.positionViewAtIndex(selectedIndex, ListView.Contain); + }); + } + syncSelectionState(); + } + + syncSelectionState: function () { + Qt.callLater(() => state.selected = listView.itemAtIndex(selectedIndex)); + } + + IInnerComponent { + id: layout + fromParent: false + width: parent.width + height: Math.min(listView.contentHeight + titleBar.height, Launcher.widgetHeight * 1.5) + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + ListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: clipHist + spacing: 0 + + delegate: Item { + required property var modelData + required property int index + width: Math.min(item.implicitWidth + Launcher.innerMargin * 4, listView.width) + height: item.height + Launcher.innerMargin * 4 + + Row { + id: item + anchors.left: parent.left + anchors.top: parent.top + anchors.margins: Launcher.innerMargin * 2 + spacing: Launcher.innerMargin * 2 + clip: true + + IconImage { + scale: index === root.selectedIndex ? 1.01 : 0.9 + source: modelData?.appIcon + implicitSize: parent.height + + Behavior on scale { + ISpringAnimation {} + } + } + + Column { + Loader { + sourceComponent: modelData?.type === "image" ? img : text + readonly property Component text: IText { + text: modelData?.data || "" + color: index === root.selectedIndex ? Colors.foreground1 : Colors.foreground2 + font.pixelSize: Launcher.widgetFontSize + } + readonly property Component img: Image { + source: modelData?.data || "" + } + } + Row { + spacing: Launcher.innerMargin + property string sinceWhen: TimeDate.sinceWhen(modelData?.timestamp) || "" + IText { + text: modelData.type + color: index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 + font.pixelSize: Launcher.widgetFontSize / 1.35 + } + IText { + visible: parent.sinceWhen + text: '·' + color: index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 + font.pixelSize: Launcher.widgetFontSize / 1.35 + } + IText { + text: parent.sinceWhen || "" + color: index === root.selectedIndex ? Colors.foreground2 : Colors.foreground3 + font.pixelSize: Launcher.widgetFontSize / 1.35 + } + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + root.mouseTriggered = true; + root.selectedIndex = index; + } + onExited: { + root.mouseTriggered = true; + } + onPressed: { + root._exec(); + } + } + } + } + + Flickable { + id: preview + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + + contentWidth: loader.item.width + contentHeight: loader.item.height + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.StopAtBounds + + Loader { + id: loader + sourceComponent: clipHist[selectedIndex]?.type === "image" ? img : text + property Component text: TextEdit { + id: textEdit + readOnly: true + width: preview.width + color: Colors.foreground1 + selectionColor: Colors.background3 + text: clipHist[selectedIndex]?.decoded || "" + } + property Component img: Image { + id: image + width: preview.width + height: preview.height + source: clipHist[selectedIndex]?.image || "" + fillMode: Image.PreserveAspectFit + } + } + } + } + } +} diff --git a/popups/launcher/components/Emoji.qml b/popups/launcher/components/Emoji.qml new file mode 100644 index 0000000..d1343e6 --- /dev/null +++ b/popups/launcher/components/Emoji.qml @@ -0,0 +1,176 @@ +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + id: root + + property var emojis: active ? Emoji.query(input) : [] + property int selectedIndex: -1 + + name: "Emoji" + prefix: name.toLowerCase() + " " + + onEmojisChanged: { + Qt.callLater(() => { + if (selectedIndex !== -1) { + syncSelectionState(); + return; + } + selectedIndex = emojis.length > 0 ? 0 : -1; + syncSelectionState(); + if (loader.item && loader.item.flickable) + loader.item.flickable.contentY = 0; + }); + } + + onSelectedIndexChanged: { + if (!loader.view || !loader.item || !loader.item.flickable) + return; + if (selectedIndex < 0 || selectedIndex >= loader.view.count) + return; + Qt.callLater(() => { + syncSelectionState(); + loader.view.currentIndex = selectedIndex; + loader.view.positionViewAtIndex(selectedIndex, GridView.Contain); + }); + } + + process: function () { + const isValid = selectedIndex >= 0 && emojis.length > 0; + const select = isValid ? emojis[selectedIndex] : null; + return { + valid: isValid, + priority: isValid, + answer: isValid ? select.emoji : "" + }; + } + + home: function () { + if (!loader.view.count) + return true; + selectedIndex = 0; + } + end: function () { + if (!loader.view.count) + return true; + selectedIndex = loader.view.count - 1; + } + prev: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + next: function () { + if (selectedIndex + 1 >= emojis.length) + return true; + selectedIndex++; + } + up: function () { + const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; + if (selectedIndex - cols < 0) + return true; + selectedIndex -= cols; + } + down: function () { + const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; + if (selectedIndex + cols >= emojis.length) + return true; + selectedIndex += cols; + } + + exec: function () { + if (selectedIndex >= 0 && emojis.length > 0) + Emoji.copy(emojis[selectedIndex].emoji); + } + + syncSelectionState: function () { + Qt.callLater(() => state.selected = loader.view.itemAtIndex(selectedIndex)); + } + + IInnerComponent { + id: innerLoader + + Loader { + id: loader + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + sourceComponent: show + property var view: null + + readonly property Component show: Component { + Item { + property alias flickable: grid + + GridView { + id: grid + anchors.fill: parent + clip: true + boundsBehavior: Flickable.StopAtBounds + cacheBuffer: cellHeight * 2 + model: emojis + interactive: true + flow: GridView.FlowLeftToRight + snapMode: GridView.NoSnap + + cellWidth: Math.min(width / Math.max(1, Math.floor(width / (Launcher.widgetFontSize * 5))), width / 3) + cellHeight: Launcher.widgetFontSize * 2.2 + Launcher.innerMargin * 2 + + delegate: Item { + required property var modelData + required property int index + width: grid.cellWidth + height: grid.cellHeight + scale: index === selectedIndex ? 1.03 : 0.97 + + IRect { + anchors.fill: parent + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + radius: Launcher.widgetRadius + + Column { + id: chipContent + anchors.centerIn: parent + spacing: 2 + + Icon { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.emoji + font.pixelSize: Launcher.widgetFontSize * 1.2 + } + IText { + anchors.horizontalCenter: parent.horizontalCenter + text: modelData.name + font.pixelSize: Launcher.widgetFontSize * 0.7 + color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 + elide: Text.ElideRight + width: parent.parent.width - Launcher.innerMargin * 2 + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: root.selectedIndex = index + onPressed: root._exec() + } + } + + Behavior on scale { + ISpringAnimation {} + } + } + + Component.onCompleted: loader.view = grid + } + } + } + } + } +} diff --git a/popups/launcher/components/Finder.qml b/popups/launcher/components/Finder.qml new file mode 100644 index 0000000..e69de29 diff --git a/popups/launcher/components/Flight.qml b/popups/launcher/components/Flight.qml new file mode 100644 index 0000000..e69de29 diff --git a/popups/launcher/components/IComponent.qml b/popups/launcher/components/IComponent.qml new file mode 100644 index 0000000..4f47b88 --- /dev/null +++ b/popups/launcher/components/IComponent.qml @@ -0,0 +1,93 @@ +import Quickshell +import QtQuick + +import qs.components +import qs.config + +IRect { + id: root + // virtual properties + property bool standalone: false + property var state: SelectionState + property string name // Component/File name + readonly property string _input: state.input + readonly property string input: !standalone ? _input.slice(prefix.length) : _input + readonly property string inputCleaned: input.toLowerCase().trim() + property string prefix + property bool active: !standalone ? input && _input.startsWith(prefix) : true + property bool valid: !standalone ? processed?.valid || false : true + property bool priority: !standalone ? valid && processed?.priority || false : true // please set priority to false if it's invalid + property string answer: processed?.answer || "" + property Component preview: Component { + IText { + animate: true + text: answer + } + } + property string predictiveCompletion: processed?.predictiveCompletion || "" // would technically work with just from answer, but stuff like calc's answer wouldnt have anything to do with the input + property var processed: active ? process() : {} // cached process, thought qml would do that automatically :/ + property var process: () => ({ + valid: valid, + priority: priority, + answer: answer, + preview: preview, + predictiveCompletion: predictiveCompletion + })// use the current IRect if valid is set + property var syncSelectionState: () => {} + // actions + property var up: () => ({ + top: false + }) + property var down: () => ({ + bottom: false + }) + property var prev: () => {} + property var next: () => {} + property var exec: () => {} + property var home: () => {} + property var end: () => {} + property var pgup: () => {} + property var pgdn: () => {} + + signal close + + function _exec() { + exec(); + close(); + } + + opacity: valid ? 1 : 0 + y: valid ? 0 : -Launcher.widgetHeight + + implicitWidth: valid ? Launcher.widgetWidth : 0 + implicitHeight: valid ? Launcher.widgetHeight : 0 + + // implicitWidth: Launcher.widgetWidth + // implicitHeight: Launcher.widgetHeight + + radius: Launcher.widgetRadius + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetBgTransparency) // TODO when prioritized, highlight + + onPriorityChanged: { + // sync with state.priorities + Qt.callLater(() => { + if (!standalone) + state.priorities = [...[...state.priorities, root].reduce((s, x) => (s[(s.has(x) && 'delete') || 'add'](x), s), new Set())]; + else + state.priorities = [root]; + }); + } + + Behavior on y { + NumberAnimation { + duration: General.animationDuration / 4 + easing.type: Easing.InOutQuad + } + } + + Component.onCompleted: { + Qt.callLater(() => { + syncSelectionState(); + }); + } +} diff --git a/popups/launcher/components/IInnerComponent.qml b/popups/launcher/components/IInnerComponent.qml new file mode 100644 index 0000000..7bbc9f0 --- /dev/null +++ b/popups/launcher/components/IInnerComponent.qml @@ -0,0 +1,34 @@ +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.config + +ColumnLayout { + property bool fromParent: true + property alias titleBar: titleBar + anchors.fill: fromParent ? parent : undefined + spacing: 0 + IRect { + id: titleBar + Layout.fillWidth: true + Layout.fillHeight: false + visible: Launcher.showWidgetTitle && valid + height: childrenRect.height + width: parent.width + topLeftRadius: Launcher.widgetRadius + topRightRadius: Launcher.widgetRadius + color: Qt.rgba(Colors.background3.r, Colors.background3.g, Colors.background3.b, Launcher.widgetTitleBgTransparency) + + IText { + anchors { + top: parent.top + left: parent.left + leftMargin: Launcher.innerMargin + } + visible: valid + font.pixelSize: Launcher.widgetFontSize + text: name + } + } +} diff --git a/popups/launcher/components/SelectionState.qml b/popups/launcher/components/SelectionState.qml new file mode 100644 index 0000000..1ab577a --- /dev/null +++ b/popups/launcher/components/SelectionState.qml @@ -0,0 +1,49 @@ +pragma Singleton +import Quickshell +import QtQuick + +Singleton { + id: root + + PersistentProperties { + id: persist + reloadableId: "launcherSelectionState" + + property string input: "" + property int cursorPosition: 0 + property int selectedPriority: 0 + property int previousSelectedPriority: 0 + } + + property Item selected: null + property alias input: persist.input + property alias cursorPosition: persist.cursorPosition + property var priorities: [] + property alias selectedPriority: persist.selectedPriority + property alias previousSelectedPriority: persist.previousSelectedPriority + property var widgets: [] + + onSelectedPriorityChanged: { + if (selectedPriority > previousSelectedPriority) { + priorities[selectedPriority].home(); + priorities[selectedPriority].syncSelectionState(); + } else { + priorities[selectedPriority].end(); + priorities[selectedPriority].syncSelectionState(); + } + + previousSelectedPriority = selectedPriority; + } + // order them based on widgets(which came from order from config) + onPrioritiesChanged: { + for (let i = priorities.length - 1; i >= 0; i--) + if (!priorities[i] || priorities[i].priority !== true) + priorities.splice(i, 1); + + priorities.sort((a, b) => (widgets.indexOf(a) - widgets.indexOf(b)) || 0); + + priorities.forEach((item, index) => {}); + + priorities[selectedPriority]?.syncSelectionState(); + } +} diff --git a/popups/launcher/components/Spell.qml b/popups/launcher/components/Spell.qml new file mode 100644 index 0000000..fe01c3f --- /dev/null +++ b/popups/launcher/components/Spell.qml @@ -0,0 +1,235 @@ +import Quickshell +import Quickshell.Io +import QtQuick +import QtQuick.Layouts + +import qs.components +import qs.services +import qs.config + +IComponent { + id: root + + property list aspell + property int selectedIndex: -1 + + name: "Spell" + prefix: name.toLowerCase() + ' ' + + onValidChanged: { + if (valid) + return; + + aspell = []; + proc.running = false; + selectedIndex = -1; + return; + } + + onAspellChanged: { + if (selectedIndex !== -1) { + syncSelectionState(); + return; + } + selectedIndex = aspell.length > 0 ? 0 : -1; + // selectedIndexChanged dont get called somehow + syncSelectionState(); + if (loader.item && loader.item.flickable) { + loader.item.flickable.contentY = 0; + } + } + + onSelectedIndexChanged: { + if (!loader.view || !loader.item || !loader.item.flickable) + return; + if (selectedIndex < 0 || selectedIndex >= loader.view.count) + return; + Qt.callLater(() => { + syncSelectionState(); + loader.view.currentIndex = selectedIndex; + loader.view.positionViewAtIndex(selectedIndex, GridView.Contain); + }); + } + + process: function () { + Qt.callLater(() => { + proc.running = true; + }); + + const isValid = selectedIndex >= 0 && aspell.length > 0; + const selected = isValid ? aspell[selectedIndex] : ""; + const answer = selected === "*" ? input : selected; + + return { + valid: isValid, + priority: isValid, + answer: answer || "", + predictiveCompletion: answer?.slice(input.length) || "" + }; + } + + home: function () { + if (loader.view.count === 0) + return true; + selectedIndex = -1; + selectedIndex = 0; + } + end: function () { + if (loader.view.count === 0) + return true; + selectedIndex = loader.view.count; + selectedIndex = loader.view.count - 1; + } + + prev: function () { + if (selectedIndex <= 0) + return true; + selectedIndex--; + } + + next: function () { + if (selectedIndex + 1 >= aspell.length) + return true; + selectedIndex++; + } + + up: function () { + const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; + if (selectedIndex - cols < 0) + return true; + selectedIndex -= cols; + } + + down: function () { + const cols = loader.view ? Math.max(1, Math.floor(loader.view.width / loader.view.cellWidth)) : 1; + if (selectedIndex + cols >= aspell.length) + return true; + selectedIndex += cols; + } + + exec: function () { + if (selectedIndex >= 0) + if (aspell[selectedIndex] !== "*") + Clip.copy(aspell[selectedIndex]); + else + Clip.copy(input); + } + + syncSelectionState: function () { + Qt.callLater(() => state.selected = loader.view.itemAtIndex(selectedIndex)); + } + + IInnerComponent { + id: innerLoader + Loader { + id: loader + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: parent.height + sourceComponent: aspell[0] === "*" ? correct : show + property var view: null + + readonly property Component show: Component { + Item { + property alias flickable: grid + + GridView { + id: grid + anchors.fill: parent + clip: true + boundsBehavior: Flickable.StopAtBounds + cacheBuffer: 0 + model: aspell + interactive: true + flow: GridView.FlowLeftToRight + snapMode: GridView.NoSnap + + cellWidth: Math.min(width / Math.max(1, Math.floor(width / (Launcher.widgetFontSize * 4))), width / 2) + cellHeight: Launcher.widgetFontSize * 1.4 + Launcher.innerMargin * 2 + + delegate: Item { + required property var modelData + required property int index + + width: grid.cellWidth + height: grid.cellHeight + scale: index === selectedIndex ? 1.03 : 0.97 + + IRect { + anchors.fill: parent + color: Qt.rgba(Colors.background2.r, Colors.background2.g, Colors.background2.b, Launcher.widgetBgTransparency) + radius: Launcher.widgetRadius + + IText { + id: chipContent + anchors.centerIn: parent + text: modelData + font.pixelSize: Launcher.widgetFontSize + elide: Text.ElideRight + color: index === selectedIndex ? Colors.foreground1 : Colors.foreground2 + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + root.selectedIndex = index; + } + onPressed: { + root._exec(); + } + } + } + + Behavior on scale { + ISpringAnimation {} + } + } + + Component.onCompleted: { + loader.view = grid; + } + } + } + } + + readonly property Component correct: Component { + IRect { + Layout.fillWidth: true + color: Qt.rgba(Colors.background1.r, Colors.background1.g, Colors.background1.b, Launcher.widgetBgTransparency / 4) + radius: Launcher.widgetRadius + IText { + anchors.centerIn: parent + text: "Correct" + font.pixelSize: Launcher.widgetFontSize + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } + } + + Process { + id: proc + command: ["sh", "-c", "echo " + input + " | aspell -a"] + property bool found: false + stdout: SplitParser { + onRead: data => { + if (data.startsWith("&")) { + proc.found = true; + aspell = data.split(/\W+/).filter(input => /^[A-Za-z]+$/.test(input)).slice(1); + } else if (data.startsWith("*")) { + proc.found = true; + aspell = ["*"]; + } + } + } + onStarted: { + found = false; + } + onExited: { + if (!found) + aspell = []; + } + } +} diff --git a/popups/launcher/components/Time.qml b/popups/launcher/components/Time.qml new file mode 100644 index 0000000..e69de29 diff --git a/popups/launcher/components/Translate.qml b/popups/launcher/components/Translate.qml new file mode 100644 index 0000000..e69de29 diff --git a/popups/launcher/components/Weather.qml b/popups/launcher/components/Weather.qml new file mode 100644 index 0000000..e69de29 diff --git a/popups/launcher/components/Web.qml b/popups/launcher/components/Web.qml new file mode 100644 index 0000000..e69de29 diff --git a/popups/launcher/todo.md b/popups/launcher/todo.md new file mode 100644 index 0000000..3ed0252 --- /dev/null +++ b/popups/launcher/todo.md @@ -0,0 +1,37 @@ +when finished: +- [x] reloads dynamically while typing in +- [x] prefix: ... for certain ambiguous queries such as "pi" or "e" for math +- [ ] bang (like ddg's) +- [ ] web search catch all +- [x] height dynamically shrink/grow according to content + +- [ ] web: + - [ ] show multiple options: + - [ ] first - search w/ search engine and open in browser + - [ ] second - fallback/second option + - [ ] custom quick search/fetch + - [ ] e.g. wikipedia: (also stuff like spotify, youtube) + - [ ] match title + - [ ] match first paragraph/description + +- [ ] files: + - [ ] latest modified date + - [ ] file size + - [ ] where it belongs + - [ ] icon/preview/quick look + +- [ ] math: + - [x] expression + - [ ] natural language (i.e. "5 of 3") + - [ ] unit conversion + - [ ] temperature + - [ ] length + - [ ] weight + - [ ] currency + - [ ] color + - [ ] time + +emoji picker +desktopentry action + +stop querying if prev hav no results diff --git a/shell.qml b/shell.qml index 7ef4362..6a64025 100644 --- a/shell.qml +++ b/shell.qml @@ -1,7 +1,7 @@ import Quickshell import Quickshell.Io import "bar" -import "launcher" +import "popups/launcher" import QtQuick @@ -50,8 +50,8 @@ Scope { root.standaloneObj.obj = Qt.createQmlObject(` import Quickshell import QtQuick - import "launcher" - import "launcher/components" + import "popups/launcher" + import "popups/launcher/components" LazyLoader { property QtObject selectionState: QtObject { @@ -66,7 +66,7 @@ Scope { readonly property list widgets: [ Component { Loader { - sourceComponent: Qt.createComponent("launcher/components/" + "${component}.qml") + sourceComponent: Qt.createComponent("popups/launcher/components/" + "${component}.qml") onLoaded: { item.standalone = true item.state = selectionState; -- cgit v1.2.3