Files
DeltaruneQuickshell/Shell/Windows/AppLauncher/AppLauncherApp.qml
2026-02-05 18:43:54 +02:00

288 lines
8.1 KiB
QML

import QtQuick
import Quickshell
import Quickshell.Widgets
import "../.."
Item {
id: root
width: parent ? parent.width : 1280
height: parent ? parent.height : 820
focus: true
/* ------------------------------
PIXEL CONSTANTS (DO NOT TOUCH)
------------------------------ */
property int menuLeft: 64
property int menuTop: 140
property int lineHeight: 38 + 40 + 1
property int nameFontSize: 32
property int stateFontSize: 28
property int stateColumnX: 824
property int soulOffsetX: -36 - 32
property int soulOffsetY: -26
/* ------------------------------ */
property int iconSize: 36
property int textStartX: 239
property int textRightPadding: 96
property int visibleRows: Math.max(1, Math.floor((height - menuTop - 64) / lineHeight))
property int scrollOffset: 0
property ShellStateManager manager: null
property int activeSelection: 0
property bool isSelected: true
ListModel {
id: appEntriesModel
}
function clamp(v, lo, hi) {
return Math.max(lo, Math.min(hi, v));
}
function menuLength() {
return appEntriesModel.count;
}
function menuAt(index) {
if (index < 0 || index >= appEntriesModel.count)
return null;
var row = appEntriesModel.get(index);
return row ? row.entry : null;
}
function wrapIndex(i) {
const length = menuLength();
if (length === 0)
return 0;
return (i + length) % length;
}
function clampSelection() {
const length = menuLength();
if (length === 0) {
activeSelection = 0;
return;
}
activeSelection = clamp(activeSelection, 0, length - 1);
}
function ensureVisible() {
const length = menuLength();
if (length === 0)
return;
if (activeSelection < scrollOffset)
scrollOffset = activeSelection;
else if (activeSelection >= scrollOffset + visibleRows)
scrollOffset = Math.max(0, activeSelection - visibleRows + 1);
}
function appName(entry) {
if (!entry)
return "";
if (entry.name && entry.name.length > 0)
return entry.name;
return entry.id || "";
}
function appUsageCount(entry) {
if (!entry || !manager || !manager.appUsageCounts)
return 0;
var key = entry.id || entry.name;
return manager.appUsageCounts[key] || 0;
}
function applicationsToArray() {
var apps = DesktopEntries.applications;
var list = [];
if (!apps)
return list;
if (apps.values && apps.values.length !== undefined) {
for (var i = 0; i < apps.values.length; i++) {
list.push(apps.values[i]);
}
} else if (apps.length !== undefined) {
for (var j = 0; j < apps.length; j++) {
list.push(apps[j]);
}
} else if (apps.count !== undefined && apps.get) {
for (var k = 0; k < apps.count; k++) {
list.push(apps.get(k));
}
}
return list;
}
function rebuildAppEntries() {
var list = applicationsToArray();
console.log("AppLauncher: rebuilding entries", list.length);
var filtered = [];
for (var i = 0; i < list.length; i++) {
var entry = list[i];
if (!entry)
continue;
if (entry.noDisplay)
continue;
filtered.push(entry);
}
console.log("AppLauncher: filtered entries", filtered.length);
filtered.sort(function (a, b) {
var usageA = appUsageCount(a);
var usageB = appUsageCount(b);
if (usageA !== usageB)
return usageB - usageA;
var nameA = appName(a);
var nameB = appName(b);
return nameA.localeCompare(nameB);
});
appEntriesModel.clear();
for (var i = 0; i < filtered.length; i++) {
appEntriesModel.append({
entry: filtered[i]
});
}
clampSelection();
ensureVisible();
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
rebuildAppEntries();
}
}
Connections {
target: manager
ignoreUnknownSignals: true
function onAppUsageCountsChanged() {
rebuildAppEntries();
}
function onAppLauncherOpened() {
activeSelection = 0;
isSelected = true;
clampSelection();
}
}
Text {
text: "APPS"
font.family: "8bitoperator JVE"
font.pixelSize: 71
renderType: Text.NativeRendering
font.hintingPreference: Font.PreferNoHinting
smooth: false
antialiasing: false
anchors.horizontalCenter: parent.horizontalCenter
color: "#ffffff"
y: 32
}
Repeater {
model: appEntriesModel
delegate: Item {
width: root.width
height: lineHeight
x: 0
y: menuTop + (index - root.scrollOffset) * lineHeight
visible: index >= root.scrollOffset && index < root.scrollOffset + root.visibleRows
Text {
x: root.textStartX
y: 0
text: root.appName(model.entry)
width: root.width - root.textStartX - root.textRightPadding
font.family: "8bitoperator JVE"
font.pixelSize: 71
font.letterSpacing: 1
renderType: Text.NativeRendering
font.hintingPreference: Font.PreferNoHinting
smooth: false
antialiasing: false
wrapMode: Text.NoWrap
elide: Text.ElideRight
color: (root.activeSelection == index && root.isSelected == true) ? "#fefe00" : "#ffffff"
}
IconImage {
id: appIcon
x: 182
y: 8 + 14
implicitSize: root.iconSize
source: Quickshell.iconPath(model.entry && model.entry.icon ? model.entry.icon : "")
asynchronous: true
}
Image {
source: "../QuickSettings/soul.png"
width: 36
height: 36
x: appIcon.x
y: appIcon.y
opacity: 0.7
visible: root.activeSelection == index
}
}
}
/* ------------------------------
INPUT HANDLING
------------------------------ */
function handleKey(key) {
switch (key) {
case Qt.Key_Up:
activeSelection = wrapIndex(activeSelection - 1);
ensureVisible();
return true;
case Qt.Key_Down:
activeSelection = wrapIndex(activeSelection + 1);
ensureVisible();
return true;
case Qt.Key_Z:
case Qt.Key_Return:
case Qt.Key_Enter:
{
var entry = menuAt(activeSelection);
if (entry) {
if (manager && manager.bumpAppUsage) {
manager.bumpAppUsage(entry.id || entry.name);
}
entry.execute();
}
if (manager && manager.closeShell) {
manager.closeShell();
}
return true;
}
case Qt.Key_X:
case Qt.Key_Shift:
case Qt.Key_Escape:
if (manager && manager.closeAppLauncher) {
manager.closeAppLauncher();
}
return true;
}
return false;
}
Component.onCompleted: {
activeSelection = 0;
isSelected = true;
scrollOffset = 0;
rebuildAppEntries();
Qt.callLater(rebuildAppEntries);
ShellInputManager.registerHandler("appLauncher", handleKey);
}
Component.onDestruction: {
ShellInputManager.unregisterHandler("appLauncher");
}
}