feat: app launcher
This commit is contained in:
@@ -4,6 +4,7 @@ import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "Windows/QuickSettings"
|
||||
import "Windows/AppLauncher"
|
||||
|
||||
PanelWindow {
|
||||
id: overlay
|
||||
@@ -70,4 +71,9 @@ PanelWindow {
|
||||
id: quickSettingsWindow
|
||||
manager: ShellStateManager
|
||||
}
|
||||
|
||||
AppLauncher {
|
||||
id: appLauncherWindow
|
||||
manager: ShellStateManager
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,11 @@ QtObject {
|
||||
if (!ShellStateManager.shellOpen)
|
||||
return false;
|
||||
|
||||
var context = ShellStateManager.quickSettingsOpen ? "quickSettings" : "topbar";
|
||||
var context = "topbar";
|
||||
if (ShellStateManager.appLauncherOpen)
|
||||
context = "appLauncher";
|
||||
else if (ShellStateManager.quickSettingsOpen)
|
||||
context = "quickSettings";
|
||||
var handler = handlers[context];
|
||||
if (handler) {
|
||||
var handled = handler(key);
|
||||
@@ -39,7 +43,9 @@ QtObject {
|
||||
}
|
||||
|
||||
if (key === Qt.Key_Escape || key === Qt.Key_Shift || key === Qt.Key_X) {
|
||||
if (context === "quickSettings") {
|
||||
if (context === "appLauncher") {
|
||||
ShellStateManager.closeAppLauncher();
|
||||
} else if (context === "quickSettings") {
|
||||
ShellStateManager.closeQuickSettings();
|
||||
} else if (context === "topbar") {
|
||||
ShellStateManager.closeShell();
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Io
|
||||
|
||||
QtObject {
|
||||
id: manager
|
||||
|
||||
property bool shellOpen: false
|
||||
property bool quickSettingsOpen: false
|
||||
property bool appLauncherOpen: false
|
||||
property var appUsageCounts: ({})
|
||||
property string appUsagePath: {
|
||||
var homeDir = Quickshell.env("HOME");
|
||||
return homeDir ? homeDir + "/.local/share/deltarunequickshell/apps.json" : "";
|
||||
}
|
||||
property bool appUsageLoaded: false
|
||||
property var windowRequests: ({})
|
||||
property var quickSettingsPayload: ({})
|
||||
property var globals: ({})
|
||||
@@ -15,6 +24,8 @@ QtObject {
|
||||
signal shellClosed
|
||||
signal quickSettingsOpened
|
||||
signal quickSettingsClosed
|
||||
signal appLauncherOpened
|
||||
signal appLauncherClosed
|
||||
signal windowRequested(string name, var payload)
|
||||
|
||||
function openShell() {
|
||||
@@ -33,6 +44,9 @@ QtObject {
|
||||
if (quickSettingsOpen) {
|
||||
closeQuickSettings();
|
||||
}
|
||||
if (appLauncherOpen) {
|
||||
closeAppLauncher();
|
||||
}
|
||||
shellOpen = false;
|
||||
shellClosed();
|
||||
}
|
||||
@@ -55,6 +69,9 @@ QtObject {
|
||||
var normalizedPayload = payload || ({});
|
||||
quickSettingsPayload = normalizedPayload;
|
||||
console.log("ShellStateManager: openQuickSettings", normalizedPayload);
|
||||
if (appLauncherOpen) {
|
||||
closeAppLauncher();
|
||||
}
|
||||
if (!quickSettingsOpen) {
|
||||
quickSettingsOpen = true;
|
||||
quickSettingsOpened();
|
||||
@@ -74,6 +91,72 @@ QtObject {
|
||||
quickSettingsOpen ? closeQuickSettings() : openQuickSettings(payload);
|
||||
}
|
||||
|
||||
function openAppLauncher(payload) {
|
||||
var normalizedPayload = payload || ({});
|
||||
console.log("ShellStateManager: openAppLauncher", normalizedPayload);
|
||||
if (quickSettingsOpen) {
|
||||
closeQuickSettings();
|
||||
}
|
||||
if (!appLauncherOpen) {
|
||||
appLauncherOpen = true;
|
||||
appLauncherOpened();
|
||||
}
|
||||
requestWindow("appLauncher", normalizedPayload);
|
||||
}
|
||||
|
||||
function closeAppLauncher() {
|
||||
if (appLauncherOpen) {
|
||||
console.log("ShellStateManager: closeAppLauncher");
|
||||
appLauncherOpen = false;
|
||||
appLauncherClosed();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAppLauncher(payload) {
|
||||
appLauncherOpen ? closeAppLauncher() : openAppLauncher(payload);
|
||||
}
|
||||
|
||||
function bumpAppUsage(id) {
|
||||
if (!id)
|
||||
return;
|
||||
var updated = Object.assign({}, appUsageCounts);
|
||||
updated[id] = (updated[id] || 0) + 1;
|
||||
appUsageCounts = updated;
|
||||
}
|
||||
|
||||
function persistAppUsage() {
|
||||
if (!appUsageLoaded)
|
||||
return;
|
||||
if (!appUsageFile || !appUsageAdapter)
|
||||
return;
|
||||
appUsageAdapter.counts = appUsageCounts || ({});
|
||||
appUsageFile.writeAdapter();
|
||||
}
|
||||
|
||||
onAppUsageCountsChanged: persistAppUsage()
|
||||
|
||||
property FileView appUsageFile: FileView {
|
||||
id: appUsageFile
|
||||
path: manager.appUsagePath
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: {
|
||||
if (appUsageAdapter) {
|
||||
manager.appUsageCounts = appUsageAdapter.counts || ({});
|
||||
}
|
||||
manager.appUsageLoaded = true;
|
||||
}
|
||||
onLoadFailed: {
|
||||
manager.appUsageLoaded = true;
|
||||
manager.persistAppUsage();
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: appUsageAdapter
|
||||
property var counts: ({})
|
||||
}
|
||||
}
|
||||
|
||||
function setGlobal(key, value) {
|
||||
globals[key] = value;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,12 @@ Item {
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (iconSources[selectedIndex] === "item.png") {
|
||||
ShellStateManager.openAppLauncher({
|
||||
source: "Topbar"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -84,7 +90,7 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
selected: topbar.selectedIndex == repeatitem.index
|
||||
iconSource: topbar.iconSources[repeatitem.index]
|
||||
showSoul: !topbar.manager.quickSettingsOpen
|
||||
showSoul: !topbar.manager.quickSettingsOpen && !topbar.manager.appLauncherOpen
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -96,6 +102,11 @@ Item {
|
||||
source: "Topbar"
|
||||
});
|
||||
}
|
||||
if (topbar.iconSources[repeatitem.index] === "item.png") {
|
||||
ShellStateManager.openAppLauncher({
|
||||
source: "Topbar"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
44
Shell/Windows/AppLauncher/AppLauncher.qml
Normal file
44
Shell/Windows/AppLauncher/AppLauncher.qml
Normal file
@@ -0,0 +1,44 @@
|
||||
import QtQuick
|
||||
import "../.."
|
||||
import "../../Window" as ShellWindow
|
||||
|
||||
ShellWindow.Window {
|
||||
id: appLauncherWindow
|
||||
property var manager: ShellStateManager
|
||||
|
||||
property var appLauncherApp: null
|
||||
|
||||
width: 1217 + 52
|
||||
height: 767 + 52
|
||||
visible: manager ? manager.appLauncherOpen : false
|
||||
anchors.centerIn: parent
|
||||
|
||||
Loader {
|
||||
id: appLauncherLoader
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
source: "./AppLauncherApp.qml"
|
||||
onLoaded: {
|
||||
appLauncherApp = item;
|
||||
if (appLauncherApp)
|
||||
appLauncherApp.manager = manager;
|
||||
}
|
||||
}
|
||||
|
||||
onManagerChanged: {
|
||||
if (appLauncherApp)
|
||||
appLauncherApp.manager = manager;
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: appLauncherKeyHandler
|
||||
function handle(event) {
|
||||
if (appLauncherApp && appLauncherApp.handleKey)
|
||||
return appLauncherApp.handleKey(event.key);
|
||||
return false;
|
||||
}
|
||||
|
||||
Component.onCompleted: ShellInputManager.registerHandler("appLauncher", handle)
|
||||
Component.onDestruction: ShellInputManager.unregisterHandler("appLauncher")
|
||||
}
|
||||
}
|
||||
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