feat: app launcher
This commit is contained in:
287
Shell/Windows/AppLauncher/AppLauncherApp.qml
Normal file
287
Shell/Windows/AppLauncher/AppLauncherApp.qml
Normal file
@@ -0,0 +1,287 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user