Files
Radu Macocian 58823cb24a
Tag and publish to AUR / tag (push) Successful in 1m20s
Tag and publish to AUR / publish (push) Has been skipped
Moved to module structure to work in embeded qs processes
2026-07-15 15:25:33 +02:00

386 lines
16 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
PanelWindow {
id: root
visible: LauncherService.visible
color: "transparent"
anchors {
top: true
bottom: true
left: true
right: true
}
exclusiveZone: 0
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.layer: WlrLayer.Overlay
readonly property int panelWidth: Config.panelWidth
readonly property int panelHeight: Config.panelHeight
// Hyprland-only nicety: close on workspace switch + focus-grab dismissal.
// Loaded lazily so the import only resolves on Hyprland.
Loader {
active: Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE").length > 0
source: "HyprlandIntegration.qml"
onLoaded: {
item.window = root;
item.armed = Qt.binding(() => root.visible);
}
}
MouseArea {
anchors.fill: parent
onClicked: LauncherService.close()
}
readonly property bool searching: LauncherService.query.length > 0
readonly property bool gifMode: LauncherService.gifMode
readonly property bool masonryActive: gifMode
&& LauncherService.filtered.length > 0
&& LauncherService.filtered[0]._isGif === true
readonly property int columns: 6
readonly property int cellSize: 100
// Hover-selection is disarmed whenever the query changes — so the list
// rebuilding under a stationary cursor doesn't yank the keyboard selection
// off the top result. Re-armed once the cursor actually moves.
property bool hoverArmed: true
Connections {
target: LauncherService
function onQueryChanged() { root.hoverArmed = false }
}
onVisibleChanged: {
if (visible) {
queryField.text = "";
LauncherService.selectedIndex = 0;
hoverArmed = false;
Qt.callLater(() => queryField.forceActiveFocus());
}
}
HoverHandler {
onPointChanged: root.hoverArmed = true
}
Rectangle {
anchors.centerIn: parent
width: root.panelWidth
height: root.panelHeight
color: Theme.opaque_background
radius: Theme.radius
border.color: Theme.border_subtle
border.width: 1
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onPressed: mouse => mouse.accepted = true
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.padding * 3
spacing: Theme.spacing * 2
Rectangle {
Layout.fillWidth: true
implicitHeight: queryField.implicitHeight + Theme.padding * 2
color: Theme.overlay_weak
radius: Theme.radius / 2
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.padding * 2
anchors.rightMargin: Theme.padding * 2
spacing: Theme.spacing
Text {
text: ""
color: Theme.idle
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
TextField {
id: queryField
Layout.fillWidth: true
placeholderText: "Search applications… = calc . unicode ! gif"
color: Theme.foreground
placeholderTextColor: Theme.idle
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
background: Item {}
selectByMouse: true
onTextChanged: {
LauncherService.query = text;
LauncherService.selectedIndex = 0;
}
Keys.onPressed: event => {
const list = LauncherService.filtered;
const cols = root.searching ? 1 : root.columns;
let idx = LauncherService.selectedIndex;
if (event.key === Qt.Key_Escape) {
LauncherService.close();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
LauncherService.launchSelected();
event.accepted = true;
} else if (event.key === Qt.Key_Down) {
idx = Math.min(list.length - 1, idx + cols);
LauncherService.selectedIndex = idx;
event.accepted = true;
} else if (event.key === Qt.Key_Up) {
idx = Math.max(0, idx - cols);
LauncherService.selectedIndex = idx;
event.accepted = true;
} else if (event.key === Qt.Key_Right && (!root.searching || root.masonryActive)) {
LauncherService.selectedIndex = Math.min(list.length - 1, idx + 1);
event.accepted = true;
} else if (event.key === Qt.Key_Left && (!root.searching || root.masonryActive)) {
LauncherService.selectedIndex = Math.max(0, idx - 1);
event.accepted = true;
}
}
}
}
}
// Grid view (empty query) ----------------------------------------
GridView {
id: gridView
visible: !root.searching
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
cellWidth: width / root.columns
cellHeight: root.cellSize
model: visible ? LauncherService.filtered : null
currentIndex: LauncherService.selectedIndex
onCurrentIndexChanged: positionViewAtIndex(currentIndex, GridView.Contain)
delegate: Item {
id: gridCell
required property int index
required property var modelData
width: GridView.view.cellWidth
height: GridView.view.cellHeight
Rectangle {
anchors.fill: parent
anchors.margins: 4
radius: Theme.radius / 2
color: (LauncherService.selectedIndex === gridCell.index)
? Theme.overlay_strong
: (hover.containsMouse ? Theme.overlay_weak : "transparent")
border.color: (LauncherService.selectedIndex === gridCell.index)
? Theme.border_subtle : "transparent"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.padding
spacing: 2
Item {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 48
Layout.preferredHeight: 48
IconOrFallback {
anchors.fill: parent
entry: gridCell.modelData
}
}
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
text: gridCell.modelData ? gridCell.modelData.name : ""
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize * 0.75
horizontalAlignment: Text.AlignHCenter
elide: Text.ElideRight
maximumLineCount: 2
wrapMode: Text.Wrap
}
}
MouseArea {
id: hover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: LauncherService.launch(gridCell.modelData)
onPositionChanged: if (root.hoverArmed) LauncherService.selectedIndex = gridCell.index
}
}
}
}
// List view (with query) -----------------------------------------
ListView {
id: listView
visible: root.searching && !root.masonryActive
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 2
model: visible ? LauncherService.filtered : null
currentIndex: LauncherService.selectedIndex
onCurrentIndexChanged: positionViewAtIndex(currentIndex, ListView.Contain)
delegate: Rectangle {
required property int index
required property var modelData
width: ListView.view.width
height: 48
radius: Theme.radius / 2
color: (LauncherService.selectedIndex === index)
? Theme.overlay_strong
: (rowHover.containsMouse ? Theme.overlay_weak : "transparent")
RowLayout {
anchors.fill: parent
anchors.leftMargin: Theme.padding * 2
anchors.rightMargin: Theme.padding * 2
spacing: Theme.spacing * 2
Item {
Layout.preferredWidth: modelData && modelData._noIcon ? 0 : 32
Layout.preferredHeight: 32
visible: !(modelData && modelData._noIcon)
IconOrFallback {
anchors.fill: parent
entry: modelData
}
}
Text {
visible: !!(modelData && modelData._glyph)
Layout.preferredWidth: 36
Layout.alignment: Qt.AlignVCenter
text: modelData && modelData._glyph ? modelData._glyph : ""
color: Theme.foreground
font.family: "Noto Color Emoji"
font.pixelSize: Theme.fontSize * 1.4
horizontalAlignment: Text.AlignHCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
text: modelData ? modelData.name : ""
color: Theme.foreground
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
Layout.fillWidth: true
visible: text.length > 0
text: modelData ? (modelData.genericName || modelData.comment || "") : ""
color: Theme.idle
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize * 0.7
elide: Text.ElideRight
maximumLineCount: 1
}
}
}
MouseArea {
id: rowHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: LauncherService.launch(modelData)
onPositionChanged: if (root.hoverArmed) LauncherService.selectedIndex = index
}
}
}
// Masonry view (gif results) -------------------------------------
Flickable {
id: masonry
visible: root.masonryActive
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
contentWidth: width
contentHeight: masonryRow.implicitHeight
boundsBehavior: Flickable.StopAtBounds
RowLayout {
id: masonryRow
width: masonry.width
spacing: Theme.spacing
Repeater {
model: 3
ColumnLayout {
required property int index
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: Theme.spacing
Repeater {
model: LauncherService.gifColumns[index] || []
delegate: Rectangle {
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: width * (modelData._ratio || 1)
radius: Theme.radius / 2
color: "transparent"
border.width: (LauncherService.selectedIndex === modelData._globalIndex) ? 2 : 0
border.color: Theme.accent
AnimatedImage {
anchors.fill: parent
anchors.margins: 2
source: modelData._previewUrl || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
smooth: true
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: LauncherService.launch(modelData)
onPositionChanged: if (root.hoverArmed) LauncherService.selectedIndex = modelData._globalIndex
}
}
}
}
}
}
}
Text {
Layout.alignment: Qt.AlignHCenter
visible: LauncherService.filtered.length === 0
text: root.gifMode ? "No GIFs match" : "No applications match"
color: Theme.idle
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
}
}
}