Version 0.1
This commit is contained in:
@@ -0,0 +1,385 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user