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 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; } // for some reason selection changing doesnt call onMappedSelectionChanged, so we manually trigger it onSelectionChanged: { selectionSync.running = true; } Timer { id: selectionSync // TODO: evaluate weather i just leave it running repeat, might be costy though 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 { id: springAnim speed: 1.5 } } Behavior on y { ISpringAnimation { speed: 1.5 } } Behavior on implicitWidth { NumberAnimation { duration: springAnim.duration easing.type: Easing.BezierSpline // fast slow easing.bezierCurve: [0.33, 0.8, 0, 1] } } Behavior on implicitHeight { NumberAnimation { duration: springAnim.duration easing.type: Easing.BezierSpline // fast slow easing.bezierCurve: [0.33, 0.8, 0, 1] } } } 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 {} } } spacing: Launcher.innerMargin * 2 Icon { text: "" } RowLayout { spacing: 0 TextInput { id: textInput property bool pendingUpdate: false Keys.onPressed: event => { if (event.key === Qt.Key_End || event.key === Qt.Key_Home) { if (event.key === Qt.Key_End) { endShortcut.activated(); } else if (event.key === Qt.Key_Home) { homeShortcut.activated(); } event.accepted = true; } } 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 } } } } HoverHandler { cursorShape: Qt.IBeamCursor } 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[launcher.state.selectedPriority]?.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[launcher.state.selectedPriority]?.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[launcher.state.selectedPriority]?.priority || false // .priority should always be true if it's in priorities sourceComponent: launcher.state?.priorities[launcher.state.selectedPriority]?.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 visible: item?.valid ?? true // not item?.standalone cuz when its standalone it doesnt appear in launcher.widgetItems 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: "PgDown" context: Qt.ApplicationShortcut onActivated: changePos(-20) } Shortcut { sequence: "PgUp" context: Qt.ApplicationShortcut onActivated: changePos(20) } Shortcut { id: homeShortcut sequence: "Home" context: Qt.ApplicationShortcut onActivated: changePos(200) } Shortcut { id: endShortcut sequence: StandardKey.MoveToEndOfDocument context: Qt.ApplicationShortcut onActivated: changePos(-200) } } // 1 = left // -1 = right // 2 = up // -2 = down function changePos(direction: int) { const item = launcher.state?.priorities[launcher.state.selectedPriority]; 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; case 200: if (item.home()) set = -1; break; case -200: if (item.end()) set = 1; break; } if (set !== 0) { switch (set) { case 1: if (launcher.state.selectedPriority < launcher.state.priorities.length - 1) { launcher.state.selectedPriority++; launcher.state.priorities[launcher.state.selectedPriority].home(); } break; case -1: if (launcher.state.selectedPriority > 0) { launcher.state.selectedPriority--; launcher.state.priorities[launcher.state.selectedPriority].end(); } break; } } } implicitWidth: container.width implicitHeight: container.height Connections { target: launcher?.state // launcher.state may be null ignoreUnknownSignals: true function onSyncSelectionState() { selectionSync.running = true; } } }