add noctalia and fuzzel
This commit is contained in:
+654
@@ -0,0 +1,654 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.System
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var launcher: null
|
||||
property string name: I18n.tr("launcher.providers.applications")
|
||||
property bool handleSearch: true
|
||||
property var entries: []
|
||||
property string supportedLayouts: "both"
|
||||
property bool isDefaultProvider: true // This provider handles empty search
|
||||
property bool ignoreDensity: false // Apps should scale with launcher density
|
||||
property bool trackUsage: true // Track usage frequency for "most used" sorting
|
||||
|
||||
// Category support
|
||||
property string selectedCategory: "all"
|
||||
property bool showsCategories: true // Default to showing categories
|
||||
property var categories: ["all", "Pinned", "AudioVideo", "Chat", "Development", "Education", "Game", "Graphics", "Network", "Office", "System", "Misc", "WebBrowser"]
|
||||
property var availableCategories: ["all"] // Reactive property for available categories
|
||||
|
||||
property var categoryIcons: ({
|
||||
"all": "apps",
|
||||
"Pinned": "pin",
|
||||
"AudioVideo": "music",
|
||||
"Chat": "message-circle",
|
||||
"Development": "code",
|
||||
"Education": "school" // Includes Science
|
||||
,
|
||||
"Game": "device-gamepad",
|
||||
"Graphics": "brush",
|
||||
"Network": "wifi",
|
||||
"Office": "file-text",
|
||||
"System": "device-desktop" // Includes Settings and Utility
|
||||
,
|
||||
"Misc": "dots",
|
||||
"WebBrowser": "world"
|
||||
})
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
"all": I18n.tr("launcher.categories.all"),
|
||||
"Pinned": I18n.tr("launcher.categories.pinned"),
|
||||
"AudioVideo": I18n.tr("launcher.categories.audiovideo"),
|
||||
"Chat": I18n.tr("launcher.categories.chat"),
|
||||
"Development": I18n.tr("launcher.categories.development"),
|
||||
"Education": I18n.tr("launcher.categories.education"),
|
||||
"Game": I18n.tr("launcher.categories.game"),
|
||||
"Graphics": I18n.tr("launcher.categories.graphics"),
|
||||
"Network": I18n.tr("common.network"),
|
||||
"Office": I18n.tr("launcher.categories.office"),
|
||||
"System": I18n.tr("launcher.categories.system"),
|
||||
"Misc": I18n.tr("launcher.categories.misc"),
|
||||
"WebBrowser": I18n.tr("launcher.categories.webbrowser")
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
function init() {
|
||||
loadApplications();
|
||||
migrateLegacyUsageKeys();
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
// Just update available categories in case pinned apps changed
|
||||
updateAvailableCategories();
|
||||
// Default to Pinned if there are pinned apps, otherwise all
|
||||
if (availableCategories.includes("Pinned")) {
|
||||
selectedCategory = "Pinned";
|
||||
} else {
|
||||
selectedCategory = "all";
|
||||
}
|
||||
// Set category mode initially (will be updated when getResults is called)
|
||||
showsCategories = true;
|
||||
}
|
||||
|
||||
// Reload applications when desktop entries change on disk
|
||||
Connections {
|
||||
target: typeof DesktopEntries !== 'undefined' ? DesktopEntries.applications : null
|
||||
function onValuesChanged() {
|
||||
Logger.d("ApplicationsProvider", "Desktop entries changed, reloading applications");
|
||||
loadApplications();
|
||||
}
|
||||
}
|
||||
|
||||
function selectCategory(category) {
|
||||
selectedCategory = category;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
function getAppCategories(app) {
|
||||
if (!app)
|
||||
return [];
|
||||
|
||||
const result = [];
|
||||
|
||||
if (app.categories) {
|
||||
if (Array.isArray(app.categories)) {
|
||||
for (let cat of app.categories) {
|
||||
if (cat && cat.trim && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
}
|
||||
}
|
||||
} else if (typeof app.categories === 'string') {
|
||||
const cats = app.categories.split(';').filter(c => c && c.trim() !== '');
|
||||
for (let cat of cats) {
|
||||
const trimmed = cat.trim();
|
||||
if (trimmed && !result.includes(trimmed)) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
}
|
||||
} else if (app.categories.length !== undefined) {
|
||||
try {
|
||||
for (let i = 0; i < app.categories.length; i++) {
|
||||
const cat = app.categories[i];
|
||||
if (cat && cat.trim && typeof cat.trim === 'function' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
} else if (cat && typeof cat === 'string' && cat.trim() !== '') {
|
||||
result.push(cat.trim());
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (app.Categories) {
|
||||
const cats = app.Categories.split(';').filter(c => c && c.trim() !== '');
|
||||
for (let cat of cats) {
|
||||
const trimmed = cat.trim();
|
||||
if (trimmed && !result.includes(trimmed)) {
|
||||
result.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getAppCategory(app) {
|
||||
const appCategories = getAppCategories(app);
|
||||
if (appCategories.length === 0)
|
||||
return null;
|
||||
|
||||
const priorityCategories = ["AudioVideo", "Chat", "WebBrowser", "Game", "Development", "Graphics", "Office", "Education", "System", "Network", "Misc"];
|
||||
|
||||
for (let cat of appCategories) {
|
||||
if (cat === "AudioVideo" || cat === "Audio" || cat === "Video") {
|
||||
return "AudioVideo";
|
||||
}
|
||||
}
|
||||
|
||||
if (appCategories.includes("Chat") || appCategories.includes("InstantMessaging")) {
|
||||
return "Chat";
|
||||
}
|
||||
|
||||
if (appCategories.includes("WebBrowser")) {
|
||||
return "WebBrowser";
|
||||
}
|
||||
|
||||
// Map Science to Education
|
||||
if (appCategories.includes("Science")) {
|
||||
return "Education";
|
||||
}
|
||||
|
||||
// Map Settings to System
|
||||
if (appCategories.includes("Settings")) {
|
||||
return "System";
|
||||
}
|
||||
|
||||
// Map Utility to System
|
||||
if (appCategories.includes("Utility")) {
|
||||
return "System";
|
||||
}
|
||||
|
||||
for (let priorityCat of priorityCategories) {
|
||||
if (appCategories.includes(priorityCat) && root.categories.includes(priorityCat)) {
|
||||
return priorityCat;
|
||||
}
|
||||
}
|
||||
|
||||
return "Misc";
|
||||
}
|
||||
|
||||
// Helper function to normalize app IDs for case-insensitive matching
|
||||
function normalizeAppId(appId) {
|
||||
if (!appId || typeof appId !== 'string')
|
||||
return "";
|
||||
return appId.toLowerCase().trim();
|
||||
}
|
||||
|
||||
// Helper function to check if an app is pinned
|
||||
function isAppPinned(app) {
|
||||
if (!app)
|
||||
return false;
|
||||
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
|
||||
const appId = getAppKey(app);
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
return pinnedApps.some(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
}
|
||||
|
||||
function appMatchesCategory(app, category) {
|
||||
// Check if app matches the selected category
|
||||
if (category === "all")
|
||||
return true;
|
||||
|
||||
// Handle Pinned category separately
|
||||
if (category === "Pinned") {
|
||||
return isAppPinned(app);
|
||||
}
|
||||
|
||||
// Get the primary category for this app (first matching standard category)
|
||||
const primaryCategory = getAppCategory(app);
|
||||
|
||||
// If app has no matching standard category, don't show it in any category (only in "all")
|
||||
if (!primaryCategory)
|
||||
return false;
|
||||
|
||||
// Map Audio/Video to AudioVideo
|
||||
if (category === "AudioVideo") {
|
||||
const appCategories = getAppCategories(app);
|
||||
// Show if app has AudioVideo, Audio, or Video
|
||||
return appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video");
|
||||
}
|
||||
|
||||
// Map Science to Education
|
||||
if (category === "Education") {
|
||||
const appCategories = getAppCategories(app);
|
||||
return appCategories.includes("Education") || appCategories.includes("Science");
|
||||
}
|
||||
|
||||
// Map Settings and Utility to System
|
||||
if (category === "System") {
|
||||
const appCategories = getAppCategories(app);
|
||||
return appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility");
|
||||
}
|
||||
|
||||
// Only show app in its primary category to avoid overlap
|
||||
// This ensures each app appears in exactly one category tab
|
||||
return category === primaryCategory;
|
||||
}
|
||||
|
||||
function getAvailableCategories() {
|
||||
const categorySet = new Set();
|
||||
let hasAudioVideo = false;
|
||||
let hasEducation = false;
|
||||
let hasSystem = false;
|
||||
let hasPinned = false;
|
||||
|
||||
// Check if there are any pinned apps
|
||||
const pinnedApps = Settings.data.appLauncher.pinnedApps || [];
|
||||
if (pinnedApps.length > 0) {
|
||||
// Verify that at least one pinned app exists in entries
|
||||
for (let app of entries) {
|
||||
if (isAppPinned(app)) {
|
||||
hasPinned = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let app of entries) {
|
||||
const appCategories = getAppCategories(app);
|
||||
const primaryCategory = getAppCategory(app);
|
||||
|
||||
if (appCategories.includes("AudioVideo") || appCategories.includes("Audio") || appCategories.includes("Video")) {
|
||||
hasAudioVideo = true;
|
||||
} else if (appCategories.includes("Education") || appCategories.includes("Science")) {
|
||||
hasEducation = true;
|
||||
} else if (appCategories.includes("System") || appCategories.includes("Settings") || appCategories.includes("Utility")) {
|
||||
hasSystem = true;
|
||||
} else if (primaryCategory && root.categories.includes(primaryCategory)) {
|
||||
categorySet.add(primaryCategory);
|
||||
}
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
// Add Pinned category first if there are pinned apps
|
||||
if (hasPinned) {
|
||||
result.push("Pinned");
|
||||
}
|
||||
|
||||
result.push("all");
|
||||
|
||||
if (hasAudioVideo) {
|
||||
categorySet.add("AudioVideo");
|
||||
}
|
||||
if (hasEducation) {
|
||||
categorySet.add("Education");
|
||||
}
|
||||
if (hasSystem) {
|
||||
categorySet.add("System");
|
||||
}
|
||||
|
||||
for (let cat of root.categories) {
|
||||
if (cat !== "all" && cat !== "Pinned" && cat !== "Misc" && categorySet.has(cat)) {
|
||||
result.push(cat);
|
||||
}
|
||||
}
|
||||
|
||||
if (categorySet.has("Misc")) {
|
||||
result.push("Misc");
|
||||
}
|
||||
|
||||
if (result.length === 1) {
|
||||
const fallback = root.categories.filter(c => c !== "Misc");
|
||||
fallback.push("Misc");
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadApplications() {
|
||||
if (typeof DesktopEntries === 'undefined') {
|
||||
Logger.w("ApplicationsProvider", "DesktopEntries service not available");
|
||||
return;
|
||||
}
|
||||
|
||||
const allApps = DesktopEntries.applications.values || [];
|
||||
const seen = new Map(); // Map of appId -> exec command
|
||||
|
||||
entries = allApps.filter(app => {
|
||||
if (!app || app.noDisplay || app.hidden)
|
||||
return false;
|
||||
|
||||
const appId = app.id || app.name;
|
||||
const execCmd = getExecutableName(app);
|
||||
|
||||
// Check if we've seen this app ID before
|
||||
if (seen.has(appId)) {
|
||||
const previousExec = seen.get(appId);
|
||||
|
||||
// If exec is different, it's a legitimate different entry - keep it
|
||||
if (previousExec !== execCmd) {
|
||||
Logger.d("ApplicationsProvider", `Keeping variant of ${appId}: ${execCmd} (differs from ${previousExec})`);
|
||||
// Add with modified ID to make it unique
|
||||
app.id = `${appId}_${execCmd}`;
|
||||
seen.set(app.id, execCmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Same appId AND same exec = true duplicate, skip it
|
||||
Logger.d("ApplicationsProvider", `Skipping duplicate: ${appId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.set(appId, execCmd);
|
||||
return true;
|
||||
}).map(app => {
|
||||
app.executableName = getExecutableName(app);
|
||||
return app;
|
||||
});
|
||||
|
||||
Logger.d("ApplicationsProvider", `Loaded ${entries.length} applications`);
|
||||
updateAvailableCategories();
|
||||
}
|
||||
|
||||
function updateAvailableCategories() {
|
||||
availableCategories = getAvailableCategories();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Settings.data.appLauncher
|
||||
function onPinnedAppsChanged() {
|
||||
const wasViewingPinned = selectedCategory === "Pinned";
|
||||
updateAvailableCategories();
|
||||
|
||||
// If we were viewing Pinned category and it's no longer available, switch to "all"
|
||||
if (wasViewingPinned && !availableCategories.includes("Pinned")) {
|
||||
selectedCategory = "all";
|
||||
}
|
||||
|
||||
// Update results if we're currently viewing the Pinned category
|
||||
if (selectedCategory === "Pinned" && launcher) {
|
||||
launcher.updateResults();
|
||||
} else if (wasViewingPinned && selectedCategory === "all" && launcher) {
|
||||
// Also update results when switching to "all"
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getExecutableName(app) {
|
||||
if (!app)
|
||||
return "";
|
||||
|
||||
// Try to get executable name from command array
|
||||
if (app.command && Array.isArray(app.command) && app.command.length > 0) {
|
||||
const cmd = app.command[0];
|
||||
// Extract just the executable name from the full path
|
||||
const parts = cmd.split('/');
|
||||
const executable = parts[parts.length - 1];
|
||||
// Remove any arguments or parameters
|
||||
return executable.split(' ')[0];
|
||||
}
|
||||
|
||||
// Try to get from exec property if available
|
||||
if (app.exec) {
|
||||
const parts = app.exec.split('/');
|
||||
const executable = parts[parts.length - 1];
|
||||
return executable.split(' ')[0];
|
||||
}
|
||||
|
||||
// Fallback to app id (desktop file name without .desktop)
|
||||
if (app.id) {
|
||||
return app.id.replace('.desktop', '');
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!entries || entries.length === 0)
|
||||
return [];
|
||||
|
||||
// Set category mode based on whether there's a query
|
||||
const isSearching = !!(query && query.trim() !== "");
|
||||
showsCategories = !isSearching;
|
||||
|
||||
// Filter by category only when NOT searching
|
||||
let filteredEntries = entries;
|
||||
if (!isSearching && selectedCategory && selectedCategory !== "all") {
|
||||
filteredEntries = entries.filter(app => appMatchesCategory(app, selectedCategory));
|
||||
}
|
||||
|
||||
if (!query || query.trim() === "") {
|
||||
// Return filtered apps, optionally sorted by usage
|
||||
let sorted;
|
||||
if (Settings.data.appLauncher.sortByMostUsed) {
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
// Pinned first
|
||||
const aPinned = isAppPinned(a);
|
||||
const bPinned = isAppPinned(b);
|
||||
if (aPinned !== bPinned)
|
||||
return aPinned ? -1 : 1;
|
||||
|
||||
const ua = getUsageCount(a);
|
||||
const ub = getUsageCount(b);
|
||||
if (ub !== ua)
|
||||
return ub - ua;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
} else {
|
||||
sorted = filteredEntries.slice().sort((a, b) => {
|
||||
const aPinned = isAppPinned(a);
|
||||
const bPinned = isAppPinned(b);
|
||||
if (aPinned !== bPinned)
|
||||
return aPinned ? -1 : 1;
|
||||
return (a.name || "").toLowerCase().localeCompare((b.name || "").toLowerCase());
|
||||
});
|
||||
}
|
||||
return sorted.map(app => createResultEntry(app));
|
||||
}
|
||||
|
||||
// Use fuzzy search if available, fallback to simple search
|
||||
if (typeof FuzzySort !== 'undefined') {
|
||||
const fuzzyResults = FuzzySort.go(query, filteredEntries, {
|
||||
"keys": ["name", "comment", "genericName", "executableName"],
|
||||
"limit": 20
|
||||
});
|
||||
|
||||
// Sort pinned first within fuzzy results while preserving fuzzysort order otherwise
|
||||
const pinned = [];
|
||||
const nonPinned = [];
|
||||
for (const r of fuzzyResults) {
|
||||
const app = r.obj;
|
||||
if (isAppPinned(app))
|
||||
pinned.push(r);
|
||||
else
|
||||
nonPinned.push(r);
|
||||
}
|
||||
return pinned.concat(nonPinned).map(result => createResultEntry(result.obj, result.score));
|
||||
} else {
|
||||
// Fallback to simple search
|
||||
const searchTerm = query.toLowerCase();
|
||||
return filteredEntries.filter(app => {
|
||||
const name = (app.name || "").toLowerCase();
|
||||
const comment = (app.comment || "").toLowerCase();
|
||||
const generic = (app.genericName || "").toLowerCase();
|
||||
const executable = getExecutableName(app).toLowerCase();
|
||||
return name.includes(searchTerm) || comment.includes(searchTerm) || generic.includes(searchTerm) || executable.includes(searchTerm);
|
||||
}).sort((a, b) => {
|
||||
// Prioritize name matches, then executable matches
|
||||
const aName = a.name.toLowerCase();
|
||||
const bName = b.name.toLowerCase();
|
||||
const aExecutable = getExecutableName(a).toLowerCase();
|
||||
const bExecutable = getExecutableName(b).toLowerCase();
|
||||
const aStarts = aName.startsWith(searchTerm);
|
||||
const bStarts = bName.startsWith(searchTerm);
|
||||
const aExecStarts = aExecutable.startsWith(searchTerm);
|
||||
const bExecStarts = bExecutable.startsWith(searchTerm);
|
||||
|
||||
// Prioritize name matches first
|
||||
if (aStarts && !bStarts)
|
||||
return -1;
|
||||
if (!aStarts && bStarts)
|
||||
return 1;
|
||||
|
||||
// Then prioritize executable matches
|
||||
if (aExecStarts && !bExecStarts)
|
||||
return -1;
|
||||
if (!aExecStarts && bExecStarts)
|
||||
return 1;
|
||||
|
||||
return aName.localeCompare(bName);
|
||||
}).slice(0, 20).map(app => createResultEntry(app));
|
||||
}
|
||||
}
|
||||
|
||||
function createResultEntry(app, score) {
|
||||
return {
|
||||
"appId": getAppKey(app),
|
||||
"usageKey": getAppKey(app),
|
||||
"name": app.name || "Unknown",
|
||||
"description": app.genericName || app.comment || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"isImage": false,
|
||||
"_score": (score !== undefined ? score : 0),
|
||||
"provider": root,
|
||||
"onActivate": function () {
|
||||
// Close the launcher/SmartPanel immediately without any animations.
|
||||
// Ensures we are not preventing the future focusing of the app
|
||||
launcher.closeImmediately();
|
||||
|
||||
// Defer execution to next event loop iteration to ensure panel is fully closed
|
||||
Qt.callLater(() => {
|
||||
Logger.d("ApplicationsProvider", `Launching: ${app.name} (App ID: ${app.id || "unknown"})`);
|
||||
|
||||
const execString = (app.exec !== undefined && app.exec !== null) ? String(app.exec) : "";
|
||||
const commandArgs = Array.isArray(app.command) ? app.command : (app.command && app.command.length !== undefined) ? Array.from(app.command) : [];
|
||||
let hasQuotedArgs = execString.includes("\"") || execString.includes("'");
|
||||
let hasSpaceArgs = false;
|
||||
if (!hasQuotedArgs) {
|
||||
hasQuotedArgs = commandArgs.some(arg => {
|
||||
const text = String(arg);
|
||||
return text.includes("\"") || text.includes("'");
|
||||
});
|
||||
}
|
||||
if (!hasSpaceArgs) {
|
||||
hasSpaceArgs = commandArgs.some(arg => String(arg).includes(" "));
|
||||
}
|
||||
if (app.execute && (hasQuotedArgs || hasSpaceArgs)) {
|
||||
Logger.d("ApplicationsProvider", `Detected quoted/space arguments in Exec for ${app.name}, using app.execute()`);
|
||||
app.execute();
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.data.appLauncher.customLaunchPrefixEnabled && Settings.data.appLauncher.customLaunchPrefix) {
|
||||
// Use custom launch prefix
|
||||
const prefix = Settings.data.appLauncher.customLaunchPrefix.split(" ");
|
||||
Logger.d("ApplicationsProvider", `Using custom launch prefix: ${Settings.data.appLauncher.customLaunchPrefix}`);
|
||||
|
||||
if (app.runInTerminal) {
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = prefix.concat(terminal.concat(app.command));
|
||||
Logger.d("ApplicationsProvider", `Executing command (with prefix and terminal): ${command.join(" ")}`);
|
||||
Quickshell.execDetached(command);
|
||||
} else {
|
||||
const command = prefix.concat(app.command);
|
||||
Logger.d("ApplicationsProvider", `Executing command (with prefix): ${command.join(" ")}`);
|
||||
Quickshell.execDetached(command);
|
||||
}
|
||||
} else {
|
||||
if (app.runInTerminal) {
|
||||
Logger.d("ApplicationsProvider", "Executing terminal app manually: " + app.name);
|
||||
const terminal = Settings.data.appLauncher.terminalCommand.split(" ");
|
||||
const command = terminal.concat(app.command);
|
||||
Logger.d("ApplicationsProvider", "Executing command (manual terminal): " + command.join(" "));
|
||||
CompositorService.spawn(command);
|
||||
} else if (app.command && app.command.length > 0) {
|
||||
Logger.d("ApplicationsProvider", "Executing command: " + app.command.join(" "));
|
||||
CompositorService.spawn(app.command);
|
||||
} else if (app.execute) {
|
||||
Logger.d("ApplicationsProvider", "Calling app.execute() for: " + app.name);
|
||||
app.execute();
|
||||
} else {
|
||||
Logger.w("ApplicationsProvider", `Could not launch: ${app.name}. No valid launch method.`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Item actions for launcher delegate
|
||||
function getItemActions(item) {
|
||||
if (!item || !item.appId)
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
"icon": isAppPinned({
|
||||
"id": item.appId
|
||||
}) ? "unpin" : "pin",
|
||||
"tooltip": isAppPinned({
|
||||
"id": item.appId
|
||||
}) ? I18n.tr("common.unpin") : I18n.tr("common.pin"),
|
||||
"action": function () {
|
||||
togglePin(item.appId);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function togglePin(appId) {
|
||||
if (!appId)
|
||||
return;
|
||||
const normalizedId = normalizeAppId(appId);
|
||||
let arr = (Settings.data.appLauncher.pinnedApps || []).slice();
|
||||
const idx = arr.findIndex(pinnedId => normalizeAppId(pinnedId) === normalizedId);
|
||||
if (idx >= 0)
|
||||
arr.splice(idx, 1);
|
||||
else
|
||||
arr.push(appId);
|
||||
Settings.data.appLauncher.pinnedApps = arr;
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Usage tracking helpers
|
||||
function getAppKey(app) {
|
||||
if (app && app.id)
|
||||
return String(app.id);
|
||||
if (app && app.command && app.command.join)
|
||||
return app.command.join(" ");
|
||||
return String(app && app.name ? app.name : "unknown");
|
||||
}
|
||||
|
||||
function getUsageCount(app) {
|
||||
return ShellState.getLauncherUsageCount(getAppKey(app));
|
||||
}
|
||||
|
||||
// Migrate legacy command-based usage keys to canonical app-id keys at startup
|
||||
function migrateLegacyUsageKeys() {
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const app = entries[i];
|
||||
if (app && app.id && app.command && app.command.join) {
|
||||
const key = getAppKey(app);
|
||||
const legacyKey = app.command.join(" ");
|
||||
if (legacyKey !== key && ShellState.getLauncherUsageCount(legacyKey) > 0) {
|
||||
ShellState.migrateLauncherUsage(legacyKey, key);
|
||||
Logger.d("ApplicationsProvider", `Migrated usage: "${legacyKey}" → "${key}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "../../../../Helpers/AdvancedMath.js" as AdvancedMath
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("launcher.providers.calculator")
|
||||
property var launcher: null
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
property bool handleSearch: true // Contribute to regular search
|
||||
property string supportedLayouts: "list"
|
||||
|
||||
// Initialize provider
|
||||
function init() {
|
||||
Logger.d("CalculatorProvider", "Initialized");
|
||||
}
|
||||
|
||||
// Get search results - evaluates math expressions inline
|
||||
function getResults(query) {
|
||||
if (!query)
|
||||
return [];
|
||||
|
||||
const trimmed = query.trim();
|
||||
if (!trimmed || !isMathExpression(trimmed))
|
||||
return [];
|
||||
|
||||
try {
|
||||
const result = AdvancedMath.evaluate(trimmed);
|
||||
const formattedResult = AdvancedMath.formatResult(result);
|
||||
return [
|
||||
{
|
||||
"name": formattedResult,
|
||||
"description": I18n.tr("launcher.providers.calculator-press-enter-to-copy"),
|
||||
"icon": iconMode === "tabler" ? "calculator" : "accessories-calculator",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"provider": root,
|
||||
"onActivate": function () {
|
||||
// Copy result to clipboard via xclip
|
||||
Quickshell.execDetached(["sh", "-c", "echo -n '" + formattedResult.replace(/'/g, "'\\''") + "' | wl-copy"]);
|
||||
ToastService.showNotice(I18n.tr("common.copied-to-clipboard"), formattedResult);
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a string is a valid math expression
|
||||
function isMathExpression(expr) {
|
||||
// Allow: digits, operators, parentheses, decimal points, whitespace, letters (for functions), commas
|
||||
if (!/^[\d\s\+\-\*\/\(\)\.\%\^a-zA-Z,]+$/.test(expr))
|
||||
return false;
|
||||
|
||||
// Must contain at least one operator OR a function call (letter followed by parenthesis)
|
||||
if (!/[+\-*/%\^]/.test(expr) && !/[a-zA-Z]\s*\(/.test(expr))
|
||||
return false;
|
||||
|
||||
// Reject if ends with an operator (incomplete expression)
|
||||
if (/[+\-*/%\^]\s*$/.test(expr))
|
||||
return false;
|
||||
|
||||
// Reject if it's just letters (would match app names)
|
||||
if (/^[a-zA-Z\s]+$/.test(expr))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+457
@@ -0,0 +1,457 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
import qs.Services.Noctalia
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("launcher.providers.clipboard")
|
||||
property var launcher: null
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
property string supportedLayouts: "list" // List view for clipboard content
|
||||
property bool wrapNavigation: false // Don't wrap at end of list
|
||||
|
||||
// Provider capabilities
|
||||
property bool handleSearch: false // Don't handle regular search
|
||||
|
||||
// Preview support
|
||||
property bool hasPreview: Settings.data.appLauncher.enableClipPreview
|
||||
property string previewComponentPath: "./ClipboardPreview.qml"
|
||||
|
||||
// Image handling - expose revision for reactive updates in delegates
|
||||
readonly property int imageRevision: ClipboardService.revision
|
||||
|
||||
// Categories
|
||||
property var availableCategories: Settings.data.appLauncher.enableClipboardChips ? ["All", "Images", "Links", "Files", "Code", "Colors"] : []
|
||||
property string selectedCategory: "All"
|
||||
|
||||
function selectCategory(cat) {
|
||||
if (selectedCategory !== cat) {
|
||||
selectedCategory = cat;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var categoryIcons: {
|
||||
"All": iconMode === "tabler" ? "border-all" : "view-grid",
|
||||
"Images": iconMode === "tabler" ? "photo" : "image",
|
||||
"Links": iconMode === "tabler" ? "link" : "insert-link",
|
||||
"Files": iconMode === "tabler" ? "file" : "text-x-generic",
|
||||
"Code": iconMode === "tabler" ? "code" : "text-x-script",
|
||||
"Colors": iconMode === "tabler" ? "palette" : "color-picker"
|
||||
}
|
||||
|
||||
// Internal state
|
||||
property bool isWaitingForData: false
|
||||
property bool gotResults: false
|
||||
property string lastSearchText: ""
|
||||
|
||||
// Listen for clipboard data updates
|
||||
Connections {
|
||||
target: ClipboardService
|
||||
function onListCompleted() {
|
||||
if (gotResults && (lastSearchText === searchText)) {
|
||||
// Do not update results after the first fetch.
|
||||
// This will avoid the list resetting every 2seconds when the service updates.
|
||||
return;
|
||||
}
|
||||
// Refresh results if we're waiting for data or if clipboard plugin is active
|
||||
if (isWaitingForData || (launcher && launcher.searchText.startsWith(">clip"))) {
|
||||
isWaitingForData = false;
|
||||
gotResults = true;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
function onActiveChanged() {
|
||||
// When active state changes (e.g. dependency check completes), refresh results
|
||||
if (ClipboardService.active && launcher && launcher.searchText.startsWith(">clip")) {
|
||||
isWaitingForData = true;
|
||||
gotResults = false;
|
||||
ClipboardService.list(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize provider
|
||||
function init() {
|
||||
Logger.d("ClipboardProvider", "Initialized");
|
||||
// Pre-load clipboard data if service is active
|
||||
if (ClipboardService.active) {
|
||||
ClipboardService.list(100);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when launcher opens
|
||||
function onOpened() {
|
||||
isWaitingForData = true;
|
||||
gotResults = false;
|
||||
lastSearchText = "";
|
||||
|
||||
// Refresh clipboard history when launcher opens
|
||||
if (ClipboardService.active) {
|
||||
ClipboardService.list(100);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">clip");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">clip",
|
||||
"description": I18n.tr("launcher.providers.clipboard-search-description"),
|
||||
"icon": iconMode === "tabler" ? "clipboard" : "diodon",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">clip ");
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": ">clip clear",
|
||||
"description": I18n.tr("launcher.providers.clipboard-clear-description"),
|
||||
"icon": iconMode === "tabler" ? "trash" : "user-trash",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
ClipboardService.wipeAll();
|
||||
launcher.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Get search results
|
||||
function getResults(searchText) {
|
||||
if (!searchText.startsWith(">clip")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
lastSearchText = searchText;
|
||||
const results = [];
|
||||
const query = searchText.slice(5).trim();
|
||||
|
||||
// Check if clipboard service is not active
|
||||
if (!ClipboardService.active) {
|
||||
// If dependency check hasn't completed yet, show loading instead of disabled
|
||||
if (!ClipboardService.dependencyChecked) {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-history-disabled"),
|
||||
"description": I18n.tr("launcher.providers.clipboard-history-disabled-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Special command: clear
|
||||
if (query === "clear") {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-clear-history"),
|
||||
"description": I18n.tr("launcher.providers.clipboard-clear-description-full"),
|
||||
"icon": iconMode === "tabler" ? "trash" : "user-trash",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
ClipboardService.wipeAll();
|
||||
launcher.close();
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Show loading state if data is being loaded
|
||||
if (ClipboardService.loading || isWaitingForData) {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Get clipboard items
|
||||
const items = ClipboardService.items || [];
|
||||
|
||||
// If no items and we haven't tried loading yet, trigger a load
|
||||
if (items.count === 0 && !ClipboardService.loading) {
|
||||
isWaitingForData = true;
|
||||
ClipboardService.list(100);
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.clipboard-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Search clipboard items
|
||||
const searchTerm = query.toLowerCase();
|
||||
|
||||
const now = Date.now() / 1000;
|
||||
|
||||
const catMap = {
|
||||
"Images": "image",
|
||||
"Links": "link",
|
||||
"Files": "file",
|
||||
"Code": "code",
|
||||
"Colors": "color"
|
||||
};
|
||||
|
||||
// Filter and format results
|
||||
items.forEach(function (item) {
|
||||
// Category filter
|
||||
if (Settings.data.appLauncher.enableClipboardChips && root.selectedCategory !== "All") {
|
||||
if (item.contentType !== catMap[root.selectedCategory]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const preview = (item.preview || "").toLowerCase();
|
||||
|
||||
// Skip if search term doesn't match
|
||||
if (searchTerm && preview.indexOf(searchTerm) === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstSeen = ClipboardService.firstSeenById[item.id] || now;
|
||||
|
||||
// Format the result based on type
|
||||
let entry;
|
||||
if (item.isImage) {
|
||||
entry = formatImageEntry(item, firstSeen);
|
||||
} else {
|
||||
entry = formatTextEntry(item, firstSeen);
|
||||
}
|
||||
|
||||
// Add activation handler
|
||||
entry.onActivate = function () {
|
||||
if (Settings.data.appLauncher.autoPasteClipboard) {
|
||||
launcher.closeImmediately();
|
||||
Qt.callLater(() => {
|
||||
ClipboardService.pasteFromClipboard(item.id, item.mime);
|
||||
});
|
||||
} else {
|
||||
ClipboardService.copyToClipboard(item.id);
|
||||
launcher.close();
|
||||
}
|
||||
};
|
||||
|
||||
results.push(entry);
|
||||
});
|
||||
|
||||
// Show empty state if no results
|
||||
if (results.length === 0) {
|
||||
results.push({
|
||||
"name": searchTerm ? "No matching clipboard items" : "Clipboard is empty",
|
||||
"description": searchTerm ? `No items containing "${query}"` : "Copy something to see it here",
|
||||
"icon": iconMode === "tabler" ? "clipboard" : "text-x-generic",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {// Do nothing
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//Logger.i("ClipboardPlugin", `Returning ${results.length} results for query: "${query}"`)
|
||||
return results;
|
||||
}
|
||||
|
||||
function formatImageEntry(item, firstSeen) {
|
||||
const meta = ClipboardService.parseImageMeta(item.preview);
|
||||
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
|
||||
let desc = meta ? `${meta.fmt} • ${meta.size}` : item.mime || "Image data";
|
||||
if (timeStr)
|
||||
desc += ` • ${timeStr}`;
|
||||
|
||||
return {
|
||||
"name": meta ? `Image ${meta.w}×${meta.h}` : "Image",
|
||||
"description": desc,
|
||||
"icon": iconMode === "tabler" ? "photo" : "image",
|
||||
"isTablerIcon": true,
|
||||
"isImage": true,
|
||||
"imageWidth": meta ? meta.w : 0,
|
||||
"imageHeight": meta ? meta.h : 0,
|
||||
"clipboardId": item.id,
|
||||
"mime": item.mime,
|
||||
"preview": item.preview,
|
||||
"provider": root
|
||||
};
|
||||
}
|
||||
|
||||
function formatTextEntry(item, firstSeen) {
|
||||
const preview = (item.preview || "").trim();
|
||||
const lines = preview.split('\n').filter(l => l.trim());
|
||||
|
||||
let title = lines[0] || "Empty text";
|
||||
if (title.length > 60) {
|
||||
title = title.substring(0, 57) + "...";
|
||||
}
|
||||
|
||||
let description = "";
|
||||
if (lines.length > 1) {
|
||||
description = lines[1];
|
||||
if (description.length > 80) {
|
||||
description = description.substring(0, 77) + "...";
|
||||
}
|
||||
} else {
|
||||
// Preview is truncated at ~100 chars, so we can't show exact count
|
||||
if (preview.length >= 100) {
|
||||
description = I18n.tr("toast.clipboard.long-text");
|
||||
} else {
|
||||
const chars = preview.length;
|
||||
const words = preview.split(/\s+/).length;
|
||||
description = `${chars} characters, ${words} word${words !== 1 ? 's' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
const timeStr = Time.formatRelativeTime(new Date(firstSeen * 1000));
|
||||
if (timeStr)
|
||||
description += ` • ${timeStr}`;
|
||||
|
||||
let defaultIcon = iconMode === "tabler" ? "clipboard" : "text-x-generic";
|
||||
let colorHex = "";
|
||||
if (Settings.data.appLauncher.enableClipboardSmartIcons) {
|
||||
if (item.contentType === "link")
|
||||
defaultIcon = iconMode === "tabler" ? "link" : "insert-link";
|
||||
else if (item.contentType === "file")
|
||||
defaultIcon = iconMode === "tabler" ? "file" : "text-x-generic";
|
||||
else if (item.contentType === "code")
|
||||
defaultIcon = iconMode === "tabler" ? "code" : "text-x-script";
|
||||
else if (item.contentType === "color") {
|
||||
defaultIcon = iconMode === "tabler" ? "palette" : "color-picker";
|
||||
colorHex = preview;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
"name": title,
|
||||
"description": description,
|
||||
"icon": defaultIcon,
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"clipboardId": item.id,
|
||||
"preview": preview,
|
||||
"contentType": item.contentType,
|
||||
"colorHex": colorHex,
|
||||
"provider": root
|
||||
};
|
||||
}
|
||||
|
||||
function getImageForItem(clipboardId) {
|
||||
return ClipboardService.getImageData ? ClipboardService.getImageData(clipboardId) : null;
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// Item actions for launcher delegate
|
||||
function getItemActions(item) {
|
||||
if (!item || !item.clipboardId)
|
||||
return [];
|
||||
|
||||
var actions = [];
|
||||
|
||||
// Annotation tool for images
|
||||
if (item.isImage && Settings.data.appLauncher.screenshotAnnotationTool !== "") {
|
||||
actions.push({
|
||||
"icon": "pencil",
|
||||
"tooltip": I18n.tr("tooltips.open-annotation-tool"),
|
||||
"action": function () {
|
||||
var tool = Settings.data.appLauncher.screenshotAnnotationTool;
|
||||
Quickshell.execDetached(["sh", "-c", "cliphist decode " + item.clipboardId + " | " + tool]);
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete action
|
||||
actions.push({
|
||||
"icon": "trash",
|
||||
"tooltip": I18n.tr("launcher.providers.clipboard-delete"),
|
||||
"action": function () {
|
||||
deleteItem(item);
|
||||
}
|
||||
});
|
||||
|
||||
return actions;
|
||||
}
|
||||
|
||||
function canDeleteItem(item) {
|
||||
return item && !!item.clipboardId;
|
||||
}
|
||||
|
||||
function deleteItem(item) {
|
||||
if (!item || !item.clipboardId)
|
||||
return;
|
||||
|
||||
// Set provider state before deletion so refresh works
|
||||
gotResults = false;
|
||||
isWaitingForData = true;
|
||||
lastSearchText = launcher ? launcher.searchText : "";
|
||||
|
||||
// Delete the item
|
||||
ClipboardService.deleteById(String(item.clipboardId));
|
||||
}
|
||||
|
||||
// Prepare item for display (handles image decoding)
|
||||
function prepareItem(item) {
|
||||
if (item && item.isImage && item.clipboardId) {
|
||||
if (!ClipboardService.getImageData(item.clipboardId)) {
|
||||
ClipboardService.decodeToDataUrl(item.clipboardId, item.mime, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get image URL for item (used by delegates)
|
||||
function getImageUrl(item) {
|
||||
if (!item || !item.clipboardId)
|
||||
return "";
|
||||
return ClipboardService.getImageData(item.clipboardId) || "";
|
||||
}
|
||||
|
||||
// Get preview data for the preview panel
|
||||
function getPreviewData(item) {
|
||||
if (!item)
|
||||
return null;
|
||||
return {
|
||||
"clipboardId": item.clipboardId,
|
||||
"isImage": item.isImage,
|
||||
"mime": item.mime,
|
||||
"preview": item.preview
|
||||
};
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
Item {
|
||||
property var launcher: null
|
||||
property string name: I18n.tr("common.command")
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
function handleCommand(query) {
|
||||
return query.startsWith(">cmd");
|
||||
}
|
||||
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">cmd",
|
||||
"description": I18n.tr("launcher.providers.command-description"),
|
||||
"icon": iconMode === "tabler" ? "terminal" : "utilities-terminal",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">cmd ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query.startsWith(">cmd"))
|
||||
return [];
|
||||
|
||||
let expression = query.substring(4).trim();
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("common.command"),
|
||||
"description": I18n.tr("launcher.providers.command-description"),
|
||||
"icon": iconMode === "tabler" ? "terminal" : "utilities-terminal",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.closeImmediately();
|
||||
Qt.callLater(() => {
|
||||
Logger.d("CommandProvider", "Executing shell command: " + expression);
|
||||
Quickshell.execDetached(["sh", "-c", expression]);
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Keyboard
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("launcher.providers.emoji")
|
||||
property var launcher: null
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
property bool handleSearch: false
|
||||
property string supportedLayouts: "grid" // Only grid layout for emoji
|
||||
property int preferredGridColumns: 7 // More columns for compact emoji display
|
||||
property real preferredGridCellRatio: 1.15 // Slightly taller than wide to accommodate label
|
||||
property bool supportsAutoPaste: true // Emoji can be auto-pasted
|
||||
property bool ignoreDensity: false // Emoji should scale with launcher density
|
||||
|
||||
property string selectedCategory: "recent"
|
||||
property bool showsCategories: true // Default to showing categories
|
||||
|
||||
// Empty state message for category view
|
||||
readonly property string emptyBrowsingMessage: selectedCategory === "recent" ? I18n.tr("launcher.providers.emoji-no-recent") : ""
|
||||
|
||||
property var categoryIcons: ({
|
||||
"all": "apps",
|
||||
"recent": "clock",
|
||||
"people": "user",
|
||||
"animals": "paw",
|
||||
"nature": "leaf",
|
||||
"food": "apple",
|
||||
"activity": "run",
|
||||
"travel": "plane",
|
||||
"objects": "home",
|
||||
"symbols": "star",
|
||||
"flags": "flag"
|
||||
})
|
||||
|
||||
property var categories: ["all", "recent", "people", "animals", "nature", "food", "activity", "travel", "objects", "symbols", "flags"]
|
||||
|
||||
function getCategoryName(category) {
|
||||
const names = {
|
||||
"all": I18n.tr("launcher.categories.all"),
|
||||
"recent": I18n.tr("launcher.categories.emoji-recent"),
|
||||
"people": I18n.tr("launcher.categories.emoji-people"),
|
||||
"animals": I18n.tr("launcher.categories.emoji-animals"),
|
||||
"nature": I18n.tr("launcher.categories.emoji-nature"),
|
||||
"food": I18n.tr("launcher.categories.emoji-food"),
|
||||
"activity": I18n.tr("launcher.categories.emoji-activity"),
|
||||
"travel": I18n.tr("launcher.categories.emoji-travel"),
|
||||
"objects": I18n.tr("launcher.categories.emoji-objects"),
|
||||
"symbols": I18n.tr("launcher.categories.emoji-symbols"),
|
||||
"flags": I18n.tr("launcher.categories.emoji-flags")
|
||||
};
|
||||
return names[category] || category;
|
||||
}
|
||||
|
||||
// Force update results when emoji service loads
|
||||
Connections {
|
||||
target: EmojiService
|
||||
function onLoadedChanged() {
|
||||
if (EmojiService.loaded && root.launcher) {
|
||||
root.launcher.updateResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize provider
|
||||
function init() {
|
||||
Logger.d("EmojiProvider", "Initialized");
|
||||
}
|
||||
|
||||
function selectCategory(category) {
|
||||
selectedCategory = category;
|
||||
if (launcher) {
|
||||
launcher.updateResults();
|
||||
}
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
// Always reset to "recent" category when opening
|
||||
selectedCategory = "recent";
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">emoji");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">emoji",
|
||||
"description": I18n.tr("launcher.providers.emoji-search-description"),
|
||||
"icon": iconMode === "tabler" ? "mood-smile" : "face-smile",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">emoji ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Get search results
|
||||
function getResults(searchText) {
|
||||
if (!searchText.startsWith(">emoji")) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!EmojiService.loaded) {
|
||||
return [
|
||||
{
|
||||
"name": I18n.tr("launcher.providers.emoji-loading"),
|
||||
"description": I18n.tr("launcher.providers.emoji-loading-description"),
|
||||
"icon": iconMode === "tabler" ? "refresh" : "view-refresh",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
var query = searchText.slice(6).trim();
|
||||
var emojis = [];
|
||||
|
||||
if (query !== "" || selectedCategory === "all") {
|
||||
emojis = EmojiService.search(query);
|
||||
} else {
|
||||
emojis = EmojiService.getEmojisByCategory(selectedCategory);
|
||||
}
|
||||
return emojis.map(formatEmojiEntry);
|
||||
}
|
||||
|
||||
// Format an emoji entry for the results list
|
||||
function formatEmojiEntry(emoji) {
|
||||
let title = emoji.name;
|
||||
let description = emoji.keywords.join(", ");
|
||||
|
||||
if (emoji.category) {
|
||||
description += " • Category: " + emoji.category;
|
||||
}
|
||||
|
||||
const emojiChar = emoji.emoji;
|
||||
|
||||
return {
|
||||
"name": title,
|
||||
"description": description,
|
||||
"icon": null,
|
||||
"isImage": false,
|
||||
"displayString": emojiChar,
|
||||
"autoPasteText": emojiChar,
|
||||
"provider": root,
|
||||
"onAutoPaste": function () {
|
||||
EmojiService.recordUsage(emojiChar);
|
||||
},
|
||||
"onActivate": function () {
|
||||
EmojiService.copy(emojiChar);
|
||||
launcher.close();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("tooltips.session-menu")
|
||||
property var launcher: null
|
||||
property bool handleSearch: Settings.data.appLauncher.enableSessionSearch
|
||||
property string supportedLayouts: "list"
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
// Session actions with search keywords
|
||||
readonly property var sessionActions: [
|
||||
{
|
||||
"action": "lock",
|
||||
"labelKey": "common.lock",
|
||||
"icon": iconMode === "tabler" ? "lock" : "system-lock-screen",
|
||||
"keywords": ["lock", "screen", "secure"]
|
||||
},
|
||||
{
|
||||
"action": "suspend",
|
||||
"labelKey": "common.suspend",
|
||||
"icon": iconMode === "tabler" ? "suspend" : "system-suspend",
|
||||
"keywords": ["suspend", "sleep", "standby"]
|
||||
},
|
||||
{
|
||||
"action": "hibernate",
|
||||
"labelKey": "common.hibernate",
|
||||
"icon": iconMode === "tabler" ? "hibernate" : "system-suspend-hibernate",
|
||||
"keywords": ["hibernate", "disk"]
|
||||
},
|
||||
{
|
||||
"action": "reboot",
|
||||
"labelKey": "common.reboot",
|
||||
"icon": iconMode === "tabler" ? "reboot" : "system-reboot",
|
||||
"keywords": ["reboot", "restart", "reload"]
|
||||
},
|
||||
{
|
||||
"action": "rebootToUefi",
|
||||
"labelKey": "common.reboot-to-uefi",
|
||||
"icon": iconMode === "tabler" ? "reboot" : "system-reboot",
|
||||
"keywords": ["reboot", "uefi", "firmware", "bios"]
|
||||
},
|
||||
{
|
||||
"action": "userspaceReboot",
|
||||
"labelKey": "common.userspace-reboot",
|
||||
"icon": iconMode === "tabler" ? "rotate" : "system-reboot",
|
||||
"keywords": ["reboot", "restart", "soft", "userspace"]
|
||||
},
|
||||
{
|
||||
"action": "logout",
|
||||
"labelKey": "common.logout",
|
||||
"icon": iconMode === "tabler" ? "logout" : "system-log-out",
|
||||
"keywords": ["logout", "sign out", "exit", "leave"]
|
||||
},
|
||||
{
|
||||
"action": "shutdown",
|
||||
"labelKey": "common.shutdown",
|
||||
"icon": iconMode === "tabler" ? "shutdown" : "system-shutdown",
|
||||
"keywords": ["shutdown", "power off", "turn off", "poweroff"]
|
||||
}
|
||||
]
|
||||
|
||||
function init() {
|
||||
Logger.d("SessionProvider", "Initialized");
|
||||
}
|
||||
|
||||
function getEnabledActions() {
|
||||
var powerOptions = Settings.data.sessionMenu.powerOptions || [];
|
||||
var enabledSet = {};
|
||||
|
||||
for (var i = 0; i < powerOptions.length; i++) {
|
||||
if (powerOptions[i].enabled) {
|
||||
enabledSet[powerOptions[i].action] = powerOptions[i];
|
||||
}
|
||||
}
|
||||
|
||||
var enabled = [];
|
||||
for (var j = 0; j < sessionActions.length; j++) {
|
||||
var action = sessionActions[j];
|
||||
if (enabledSet[action.action]) {
|
||||
enabled.push({
|
||||
"action": action.action,
|
||||
"labelKey": action.labelKey,
|
||||
"icon": action.icon,
|
||||
"keywords": action.keywords,
|
||||
"command": enabledSet[action.action].command || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return enabled;
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query)
|
||||
return [];
|
||||
|
||||
var trimmed = query.trim();
|
||||
if (!trimmed || trimmed.length < 2)
|
||||
return [];
|
||||
|
||||
var enabledActions = getEnabledActions();
|
||||
if (enabledActions.length === 0)
|
||||
return [];
|
||||
|
||||
// Build searchable items with resolved translations
|
||||
var items = [];
|
||||
for (var i = 0; i < enabledActions.length; i++) {
|
||||
var action = enabledActions[i];
|
||||
var label = I18n.tr(action.labelKey);
|
||||
items.push({
|
||||
"action": action.action,
|
||||
"icon": action.icon,
|
||||
"label": label,
|
||||
"command": action.command,
|
||||
"searchText": [label.toLowerCase()].concat(action.keywords).join(" ")
|
||||
});
|
||||
}
|
||||
|
||||
var results = FuzzySort.go(trimmed, items, {
|
||||
"keys": ["label", "searchText"],
|
||||
"limit": 6
|
||||
});
|
||||
|
||||
var launcherItems = [];
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
var entry = results[j].obj;
|
||||
var score = results[j].score;
|
||||
|
||||
launcherItems.push({
|
||||
"name": entry.label,
|
||||
"description": I18n.tr("tooltips.session-menu"),
|
||||
"icon": entry.icon,
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"_score": score - 1,
|
||||
"provider": root,
|
||||
"onActivate": createActivateHandler(entry.action, entry.command)
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function createActivateHandler(action, command) {
|
||||
return function () {
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
|
||||
// Execute via Qt.callLater, but reference only singletons
|
||||
// (root may be destroyed after launcher.close() unloads the panel)
|
||||
Qt.callLater(() => {
|
||||
switch (action) {
|
||||
case "lock":
|
||||
if (PanelService.lockScreen && !PanelService.lockScreen.active) {
|
||||
PanelService.lockScreen.active = true;
|
||||
}
|
||||
break;
|
||||
case "suspend":
|
||||
if (Settings.data.general.lockOnSuspend) {
|
||||
CompositorService.lockAndSuspend();
|
||||
} else {
|
||||
CompositorService.suspend();
|
||||
}
|
||||
break;
|
||||
case "hibernate":
|
||||
CompositorService.hibernate();
|
||||
break;
|
||||
case "reboot":
|
||||
CompositorService.reboot();
|
||||
break;
|
||||
case "rebootToUefi":
|
||||
CompositorService.rebootToUefi();
|
||||
break;
|
||||
case "userspaceReboot":
|
||||
CompositorService.userspaceReboot();
|
||||
break;
|
||||
case "logout":
|
||||
CompositorService.logout();
|
||||
break;
|
||||
case "shutdown":
|
||||
CompositorService.shutdown();
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import QtQuick
|
||||
import qs.Commons
|
||||
import qs.Services.UI
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Provider metadata
|
||||
property string name: I18n.tr("common.settings")
|
||||
property var launcher: null
|
||||
property bool handleSearch: Settings.data.appLauncher.enableSettingsSearch
|
||||
property string supportedLayouts: "list"
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
function init() {
|
||||
Logger.d("SettingsProvider", "Initialized");
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">settings");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">settings",
|
||||
"description": I18n.tr("launcher.providers.settings-search-description"),
|
||||
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">settings ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query || SettingsSearchService.searchIndex.length === 0)
|
||||
return [];
|
||||
|
||||
var trimmed = query.trim();
|
||||
|
||||
// Handle command mode: ">settings" or ">settings <search>"
|
||||
var isCommandMode = trimmed.startsWith(">settings");
|
||||
if (isCommandMode) {
|
||||
// Extract search term after ">settings "
|
||||
var searchTerm = trimmed.substring(9).trim();
|
||||
// In command mode, show all settings if no search term
|
||||
if (searchTerm.length === 0) {
|
||||
return getAllSettings();
|
||||
}
|
||||
trimmed = searchTerm;
|
||||
} else {
|
||||
// Regular search mode - require at least 2 chars
|
||||
if (!trimmed || trimmed.length < 2)
|
||||
return [];
|
||||
}
|
||||
|
||||
// Build searchable items with resolved translations, filtering out invisible entries
|
||||
let items = [];
|
||||
for (let j = 0; j < SettingsSearchService.searchIndex.length; j++) {
|
||||
const entry = SettingsSearchService.searchIndex[j];
|
||||
if (!SettingsSearchService.isEntryVisible(entry))
|
||||
continue;
|
||||
items.push({
|
||||
"labelKey": entry.labelKey,
|
||||
"descriptionKey": entry.descriptionKey,
|
||||
"widget": entry.widget,
|
||||
"tab": entry.tab,
|
||||
"tabLabel": entry.tabLabel,
|
||||
"subTab": entry.subTab,
|
||||
"subTabLabel": entry.subTabLabel || null,
|
||||
"label": I18n.tr(entry.labelKey),
|
||||
"description": entry.descriptionKey ? I18n.tr(entry.descriptionKey) : "",
|
||||
"subTabName": entry.subTabLabel ? I18n.tr(entry.subTabLabel) : ""
|
||||
});
|
||||
}
|
||||
|
||||
const results = FuzzySort.go(trimmed, items, {
|
||||
"keys": ["label", "subTabName", "description"],
|
||||
"limit": 10,
|
||||
"scoreFn": function (r) {
|
||||
const labelScore = r[0].score;
|
||||
const subTabScore = r[1].score * 1.5;
|
||||
const descScore = r[2].score;
|
||||
return Math.max(labelScore, subTabScore, descScore);
|
||||
}
|
||||
});
|
||||
|
||||
let launcherItems = [];
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const entry = results[i].obj;
|
||||
const score = results[i].score;
|
||||
const tabName = I18n.tr(entry.tabLabel);
|
||||
const subTabName = entry.subTabName || "";
|
||||
const breadcrumb = subTabName ? (tabName + " › " + subTabName) : tabName;
|
||||
|
||||
launcherItems.push({
|
||||
"name": entry.label,
|
||||
"description": breadcrumb,
|
||||
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"_score": score - 2,
|
||||
"provider": root,
|
||||
"onActivate": createActivateHandler(entry)
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function getAllSettings() {
|
||||
var launcherItems = [];
|
||||
|
||||
for (var j = 0; j < SettingsSearchService.searchIndex.length; j++) {
|
||||
var entry = SettingsSearchService.searchIndex[j];
|
||||
if (!SettingsSearchService.isEntryVisible(entry))
|
||||
continue;
|
||||
var label = I18n.tr(entry.labelKey);
|
||||
var tabName = I18n.tr(entry.tabLabel);
|
||||
var subTabName = entry.subTabLabel ? I18n.tr(entry.subTabLabel) : "";
|
||||
var breadcrumb = subTabName ? (tabName + " › " + subTabName) : tabName;
|
||||
|
||||
launcherItems.push({
|
||||
"name": label,
|
||||
"description": breadcrumb,
|
||||
"icon": iconMode === "tabler" ? "settings" : "preferences-system",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"_score": 0,
|
||||
"provider": root,
|
||||
"onActivate": createActivateHandler({
|
||||
"labelKey": entry.labelKey,
|
||||
"descriptionKey": entry.descriptionKey,
|
||||
"widget": entry.widget,
|
||||
"tab": entry.tab,
|
||||
"tabLabel": entry.tabLabel,
|
||||
"subTab": entry.subTab,
|
||||
"subTabLabel": entry.subTabLabel || null
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function createActivateHandler(entry) {
|
||||
return function () {
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
|
||||
Qt.callLater(() => {
|
||||
SettingsPanelService.openToEntry(entry, launcher.screen);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Compositor
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string name: I18n.tr("common.windows")
|
||||
property var launcher: null
|
||||
property bool handleSearch: Settings.data.appLauncher.enableWindowsSearch
|
||||
property string supportedLayouts: "list"
|
||||
property string iconMode: Settings.data.appLauncher.iconMode
|
||||
|
||||
function init() {
|
||||
Logger.d("WindowsProvider", "Initialized");
|
||||
}
|
||||
|
||||
// Check if this provider handles the command
|
||||
function handleCommand(searchText) {
|
||||
return searchText.startsWith(">win");
|
||||
}
|
||||
|
||||
// Return available commands when user types ">"
|
||||
function commands() {
|
||||
return [
|
||||
{
|
||||
"name": ">win",
|
||||
"description": I18n.tr("launcher.providers.windows-search-description"),
|
||||
"icon": iconMode === "tabler" ? "app-window" : "preferences-system-windows",
|
||||
"isTablerIcon": true,
|
||||
"isImage": false,
|
||||
"onActivate": function () {
|
||||
launcher.setSearchText(">win ");
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
if (!query)
|
||||
return [];
|
||||
|
||||
var trimmed = query.trim();
|
||||
|
||||
// Handle command mode: ">win" or ">win <search>"
|
||||
var isCommandMode = trimmed.startsWith(">win");
|
||||
if (isCommandMode) {
|
||||
// Extract search term after ">win "
|
||||
var searchTerm = trimmed.substring(4).trim();
|
||||
// In command mode, show all windows if no search term
|
||||
if (searchTerm.length === 0) {
|
||||
return getAllWindows();
|
||||
}
|
||||
trimmed = searchTerm;
|
||||
} else {
|
||||
// Regular search mode - require at least 2 chars
|
||||
if (trimmed.length < 2)
|
||||
return [];
|
||||
}
|
||||
|
||||
var items = [];
|
||||
|
||||
// Collect all windows from CompositorService
|
||||
for (var i = 0; i < CompositorService.windows.count; i++) {
|
||||
var win = CompositorService.windows.get(i);
|
||||
items.push({
|
||||
"id": win.id,
|
||||
"title": win.title || "",
|
||||
"appId": win.appId || "",
|
||||
"workspaceId": win.workspaceId,
|
||||
"isFocused": win.isFocused,
|
||||
"searchText": (win.title + " " + win.appId).toLowerCase()
|
||||
});
|
||||
}
|
||||
|
||||
// Fuzzy search on title and appId
|
||||
var results = FuzzySort.go(trimmed, items, {
|
||||
"keys": ["title", "appId"],
|
||||
"limit": 10
|
||||
});
|
||||
|
||||
// Map to launcher items
|
||||
var launcherItems = [];
|
||||
for (var j = 0; j < results.length; j++) {
|
||||
var entry = results[j].obj;
|
||||
var score = results[j].score;
|
||||
|
||||
// Get icon name from DesktopEntry if available, otherwise use appId
|
||||
var iconName = entry.appId;
|
||||
var appEntry = ThemeIcons.findAppEntry(entry.appId);
|
||||
if (appEntry && appEntry.icon) {
|
||||
iconName = appEntry.icon;
|
||||
}
|
||||
|
||||
launcherItems.push({
|
||||
"name": entry.title || entry.appId,
|
||||
"description": entry.appId,
|
||||
"icon": iconName || "application-x-executable",
|
||||
"isTablerIcon": false,
|
||||
"badgeIcon": "app-window",
|
||||
"_score": score,
|
||||
"provider": root,
|
||||
"windowId": entry.id,
|
||||
"onActivate": createActivateHandler(entry)
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function getAllWindows() {
|
||||
var launcherItems = [];
|
||||
|
||||
for (var i = 0; i < CompositorService.windows.count; i++) {
|
||||
var win = CompositorService.windows.get(i);
|
||||
|
||||
var iconName = win.appId;
|
||||
var appEntry = ThemeIcons.findAppEntry(win.appId);
|
||||
if (appEntry && appEntry.icon) {
|
||||
iconName = appEntry.icon;
|
||||
}
|
||||
|
||||
launcherItems.push({
|
||||
"name": win.title || win.appId,
|
||||
"description": win.appId,
|
||||
"icon": iconName || "application-x-executable",
|
||||
"isTablerIcon": false,
|
||||
"badgeIcon": "app-window",
|
||||
"_score": 0,
|
||||
"provider": root,
|
||||
"windowId": win.id,
|
||||
"onActivate": createActivateHandler({
|
||||
"id": win.id
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
return launcherItems;
|
||||
}
|
||||
|
||||
function createActivateHandler(windowEntry) {
|
||||
return function () {
|
||||
if (launcher)
|
||||
launcher.close();
|
||||
|
||||
Qt.callLater(() => {
|
||||
// Find the actual window object to pass to focusWindow
|
||||
for (var i = 0; i < CompositorService.windows.count; i++) {
|
||||
var win = CompositorService.windows.get(i);
|
||||
if (win.id === windowEntry.id) {
|
||||
CompositorService.focusWindow(win);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user