mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-29 14:32:08 -04:00
Compare commits
2 Commits
05c7a77c8b
...
c923c43322
| Author | SHA1 | Date | |
|---|---|---|---|
| c923c43322 | |||
| 9f2ae6241e |
@@ -258,6 +258,8 @@ Singleton {
|
|||||||
onFrameLauncherEmergeSideChanged: saveSettings()
|
onFrameLauncherEmergeSideChanged: saveSettings()
|
||||||
property bool frameLauncherArcExtender: false
|
property bool frameLauncherArcExtender: false
|
||||||
onFrameLauncherArcExtenderChanged: saveSettings()
|
onFrameLauncherArcExtenderChanged: saveSettings()
|
||||||
|
property bool frameUseSpotlightLauncher: false
|
||||||
|
onFrameUseSpotlightLauncherChanged: saveSettings()
|
||||||
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
readonly property string frameModalEmergeSide: frameLauncherEmergeSide === "top" ? "bottom" : "top"
|
||||||
property string frameMode: "connected"
|
property string frameMode: "connected"
|
||||||
onFrameModeChanged: saveSettings()
|
onFrameModeChanged: saveSettings()
|
||||||
@@ -447,6 +449,7 @@ Singleton {
|
|||||||
property bool dankLauncherV2UnloadOnClose: false
|
property bool dankLauncherV2UnloadOnClose: false
|
||||||
property bool dankLauncherV2IncludeFilesInAll: false
|
property bool dankLauncherV2IncludeFilesInAll: false
|
||||||
property bool dankLauncherV2IncludeFoldersInAll: false
|
property bool dankLauncherV2IncludeFoldersInAll: false
|
||||||
|
property string launcherStyle: "full"
|
||||||
|
|
||||||
property string _legacyWeatherLocation: "New York, NY"
|
property string _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ var SPEC = {
|
|||||||
dankLauncherV2UnloadOnClose: { def: false },
|
dankLauncherV2UnloadOnClose: { def: false },
|
||||||
dankLauncherV2IncludeFilesInAll: { def: false },
|
dankLauncherV2IncludeFilesInAll: { def: false },
|
||||||
dankLauncherV2IncludeFoldersInAll: { def: false },
|
dankLauncherV2IncludeFoldersInAll: { def: false },
|
||||||
|
launcherStyle: { def: "full" },
|
||||||
|
|
||||||
useAutoLocation: { def: false },
|
useAutoLocation: { def: false },
|
||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
@@ -572,6 +573,7 @@ var SPEC = {
|
|||||||
frameCloseGaps: { def: true },
|
frameCloseGaps: { def: true },
|
||||||
frameLauncherEmergeSide: { def: "bottom" },
|
frameLauncherEmergeSide: { def: "bottom" },
|
||||||
frameLauncherArcExtender: { def: false },
|
frameLauncherArcExtender: { def: false },
|
||||||
|
frameUseSpotlightLauncher: { def: false },
|
||||||
frameMode: { def: "connected" }
|
frameMode: { def: "connected" }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "clipboard":
|
||||||
|
if (selectedItem?.actions) {
|
||||||
|
for (var i = 0; i < selectedItem.actions.length; i++) {
|
||||||
|
result.push(selectedItem.actions[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var entry: null
|
||||||
|
property string cachedImageData: ""
|
||||||
|
property var _requestedEntryId: null
|
||||||
|
|
||||||
|
readonly property bool canLoadImage: !!entry?.isImage && (entry?.mimeType ?? "").startsWith("image/")
|
||||||
|
readonly property string sourceUrl: cachedImageData.length > 0 ? "data:" + (entry?.mimeType ?? "image/png") + ";base64," + cachedImageData : ""
|
||||||
|
|
||||||
|
radius: Math.max(6, Theme.cornerRadius - 2)
|
||||||
|
clip: true
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Theme.withAlpha(Theme.outline, 0.16)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
onEntryChanged: reloadPreview()
|
||||||
|
Component.onCompleted: reloadPreview()
|
||||||
|
|
||||||
|
function reloadPreview() {
|
||||||
|
cachedImageData = "";
|
||||||
|
if (!canLoadImage || !entry?.id) {
|
||||||
|
_requestedEntryId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entryId = entry.id;
|
||||||
|
_requestedEntryId = entryId;
|
||||||
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
|
"id": entryId
|
||||||
|
}, function (response) {
|
||||||
|
if (_requestedEntryId !== entryId)
|
||||||
|
return;
|
||||||
|
if (response.error)
|
||||||
|
return;
|
||||||
|
const data = response.result?.data ?? "";
|
||||||
|
if (data.length > 0)
|
||||||
|
cachedImageData = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: previewImage
|
||||||
|
anchors.fill: parent
|
||||||
|
source: root.sourceUrl
|
||||||
|
asynchronous: true
|
||||||
|
cache: false
|
||||||
|
smooth: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
visible: status === Image.Ready
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "image"
|
||||||
|
size: Math.min(22, Math.max(16, root.height * 0.46))
|
||||||
|
color: Theme.primary
|
||||||
|
visible: previewImage.status !== Image.Ready
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,10 @@ Item {
|
|||||||
signal searchQueryRequested(string query)
|
signal searchQueryRequested(string query)
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (!active) {
|
if (active) {
|
||||||
|
if (clipboardSearchEnabledInAll())
|
||||||
|
ClipboardService.ensureLauncherHistory();
|
||||||
|
} else {
|
||||||
SessionData.addLauncherHistory(searchQuery);
|
SessionData.addLauncherHistory(searchQuery);
|
||||||
|
|
||||||
sections = [];
|
sections = [];
|
||||||
@@ -69,6 +72,28 @@ Item {
|
|||||||
AppSearchService.invalidateLauncherCache();
|
AppSearchService.invalidateLauncherCache();
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
}
|
}
|
||||||
|
function onLauncherPluginVisibilityChanged() {
|
||||||
|
AppSearchService.invalidateLauncherCache();
|
||||||
|
_clearModeCache();
|
||||||
|
if (active)
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
function onBuiltInPluginSettingsChanged() {
|
||||||
|
AppSearchService.invalidateLauncherCache();
|
||||||
|
_clearModeCache();
|
||||||
|
if (active)
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ClipboardService
|
||||||
|
function onInternalEntriesChanged() {
|
||||||
|
if (!active || !clipboardSearchEnabledInAll())
|
||||||
|
return;
|
||||||
|
if (searchMode === "all" && searchQuery.length >= 2)
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -124,6 +149,18 @@ Item {
|
|||||||
function pasteSelected() {
|
function pasteSelected() {
|
||||||
if (!selectedItem)
|
if (!selectedItem)
|
||||||
return;
|
return;
|
||||||
|
if (selectedItem.type === "clipboard") {
|
||||||
|
if (SettingsData.clipboardEnterToPaste) {
|
||||||
|
ClipboardService.copyEntry(selectedItem.data, function () {
|
||||||
|
root.itemExecuted();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ClipboardService.pasteEntry(selectedItem.data, function () {
|
||||||
|
root.itemExecuted();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!SessionService.wtypeAvailable) {
|
if (!SessionService.wtypeAvailable) {
|
||||||
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
|
ToastService.showError(I18n.tr("wtype not available - install wtype for paste support"));
|
||||||
return;
|
return;
|
||||||
@@ -155,6 +192,20 @@ Item {
|
|||||||
priority: 2,
|
priority: 2,
|
||||||
defaultViewMode: "list"
|
defaultViewMode: "list"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "settings",
|
||||||
|
title: I18n.tr("Settings", "settings window title"),
|
||||||
|
icon: "settings",
|
||||||
|
priority: 2.35,
|
||||||
|
defaultViewMode: "list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "clipboard",
|
||||||
|
title: I18n.tr("Clipboard"),
|
||||||
|
icon: "content_paste",
|
||||||
|
priority: 2.45,
|
||||||
|
defaultViewMode: "list"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "browse_plugins",
|
id: "browse_plugins",
|
||||||
title: I18n.tr("Browse"),
|
title: I18n.tr("Browse"),
|
||||||
@@ -352,6 +403,9 @@ Item {
|
|||||||
searchQuery = query;
|
searchQuery = query;
|
||||||
searchDebounce.restart();
|
searchDebounce.restart();
|
||||||
|
|
||||||
|
if (searchMode === "all" && clipboardSearchEnabledInAll() && query.length >= 2)
|
||||||
|
ClipboardService.ensureLauncherHistory();
|
||||||
|
|
||||||
var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
|
var filesInAll = searchMode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll);
|
||||||
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
|
if (searchMode !== "plugins" && (searchMode === "files" || query.startsWith("/") || filesInAll) && query.length > 0) {
|
||||||
fileSearchDebounce.restart();
|
fileSearchDebounce.restart();
|
||||||
@@ -370,6 +424,8 @@ Item {
|
|||||||
searchMode = mode;
|
searchMode = mode;
|
||||||
modeChanged(mode);
|
modeChanged(mode);
|
||||||
performSearch();
|
performSearch();
|
||||||
|
if (mode === "all" && clipboardSearchEnabledInAll() && searchQuery.length >= 2)
|
||||||
|
ClipboardService.ensureLauncherHistory();
|
||||||
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
|
var filesInAll = mode === "all" && (SettingsData.dankLauncherV2IncludeFilesInAll || SettingsData.dankLauncherV2IncludeFoldersInAll) && searchQuery.length > 0;
|
||||||
if (mode === "files" || filesInAll) {
|
if (mode === "files" || filesInAll) {
|
||||||
fileSearchDebounce.restart();
|
fileSearchDebounce.restart();
|
||||||
@@ -612,7 +668,7 @@ Item {
|
|||||||
if (triggerMatch.isBuiltIn) {
|
if (triggerMatch.isBuiltIn) {
|
||||||
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
|
var builtInItems = AppSearchService.getBuiltInLauncherItems(triggerMatch.pluginId, triggerMatch.query);
|
||||||
for (var j = 0; j < builtInItems.length; j++) {
|
for (var j = 0; j < builtInItems.length; j++) {
|
||||||
allItems.push(transformBuiltInLauncherItem(builtInItems[j], triggerMatch.pluginId));
|
allItems.push(transformBuiltInSearchItem(builtInItems[j], triggerMatch.pluginId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,7 +804,7 @@ Item {
|
|||||||
|
|
||||||
var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery);
|
var builtInItems = AppSearchService.getBuiltInLauncherItems(pluginFilter, searchQuery);
|
||||||
for (var j = 0; j < builtInItems.length; j++) {
|
for (var j = 0; j < builtInItems.length; j++) {
|
||||||
allItems.push(transformBuiltInLauncherItem(builtInItems[j], pluginFilter));
|
allItems.push(transformBuiltInSearchItem(builtInItems[j], pluginFilter));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var emptyTriggerPlugins = getEmptyTriggerPlugins();
|
var emptyTriggerPlugins = getEmptyTriggerPlugins();
|
||||||
@@ -764,7 +820,7 @@ Item {
|
|||||||
var pluginId = builtInLauncherPlugins[i];
|
var pluginId = builtInLauncherPlugins[i];
|
||||||
var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
|
var blItems = AppSearchService.getBuiltInLauncherItems(pluginId, searchQuery);
|
||||||
for (var j = 0; j < blItems.length; j++) {
|
for (var j = 0; j < blItems.length; j++) {
|
||||||
allItems.push(transformBuiltInLauncherItem(blItems[j], pluginId));
|
allItems.push(transformBuiltInSearchItem(blItems[j], pluginId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -799,6 +855,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchMode === "all") {
|
if (searchMode === "all") {
|
||||||
|
appendSharedAllResults(allItems, searchQuery);
|
||||||
if (searchQuery && searchQuery.length >= 2) {
|
if (searchQuery && searchQuery.length >= 2) {
|
||||||
_pluginPhasePending = true;
|
_pluginPhasePending = true;
|
||||||
_phase1Items = allItems.slice();
|
_phase1Items = allItems.slice();
|
||||||
@@ -814,7 +871,7 @@ Item {
|
|||||||
if (plugin.isBuiltIn) {
|
if (plugin.isBuiltIn) {
|
||||||
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
||||||
for (var j = 0; j < blItems.length; j++)
|
for (var j = 0; j < blItems.length; j++)
|
||||||
allItems.push(transformBuiltInLauncherItem(blItems[j], plugin.id));
|
allItems.push(transformBuiltInSearchItem(blItems[j], plugin.id));
|
||||||
} else {
|
} else {
|
||||||
var pItems = getPluginItems(plugin.id, searchQuery);
|
var pItems = getPluginItems(plugin.id, searchQuery);
|
||||||
for (var j = 0; j < pItems.length; j++)
|
for (var j = 0; j < pItems.length; j++)
|
||||||
@@ -883,11 +940,13 @@ Item {
|
|||||||
if (currentVersion !== _searchVersion)
|
if (currentVersion !== _searchVersion)
|
||||||
return;
|
return;
|
||||||
var plugin = allPluginsOrdered[i];
|
var plugin = allPluginsOrdered[i];
|
||||||
|
if (plugin.isBuiltIn && (plugin.id === "dms_settings_search" || plugin.id === "dms_clipboard_search"))
|
||||||
|
continue;
|
||||||
if (plugin.isBuiltIn) {
|
if (plugin.isBuiltIn) {
|
||||||
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
var blItems = AppSearchService.getBuiltInLauncherItems(plugin.id, searchQuery);
|
||||||
var blLimit = Math.min(blItems.length, maxPerPlugin);
|
var blLimit = Math.min(blItems.length, maxPerPlugin);
|
||||||
for (var j = 0; j < blLimit; j++) {
|
for (var j = 0; j < blLimit; j++) {
|
||||||
var item = transformBuiltInLauncherItem(blItems[j], plugin.id);
|
var item = transformBuiltInSearchItem(blItems[j], plugin.id);
|
||||||
item._preScored = 900 - j;
|
item._preScored = 900 - j;
|
||||||
allItems.push(item);
|
allItems.push(item);
|
||||||
}
|
}
|
||||||
@@ -1110,10 +1169,56 @@ Item {
|
|||||||
return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open"));
|
return Transform.transformBuiltInLauncherItem(item, pluginId, I18n.tr("Open"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transformBuiltInSearchItem(item, pluginId) {
|
||||||
|
if (pluginId === "dms_clipboard_search" || item.type === "clipboard")
|
||||||
|
return transformClipboardEntry(item.data || item);
|
||||||
|
return transformBuiltInLauncherItem(item, pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
function transformFileResult(file) {
|
function transformFileResult(file) {
|
||||||
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal"));
|
return Transform.transformFileResult(file, I18n.tr("Open"), I18n.tr("Open folder"), I18n.tr("Copy path"), I18n.tr("Open in terminal"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transformClipboardEntry(entry) {
|
||||||
|
var copyLabel = I18n.tr("Copy");
|
||||||
|
var pasteLabel = I18n.tr("Paste");
|
||||||
|
var primaryLabel = SettingsData.clipboardEnterToPaste ? pasteLabel : copyLabel;
|
||||||
|
var pasteHintLabel = SettingsData.clipboardEnterToPaste ? I18n.tr("Shift+Enter to copy") : I18n.tr("Shift+Enter to paste");
|
||||||
|
return Transform.transformClipboardItem(entry, copyLabel, pasteLabel, primaryLabel, I18n.tr("Image"), I18n.tr("Text"), I18n.tr("Pinned"), pasteHintLabel, "", I18n.tr("Clipboard"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function builtInLauncherVisibleInAll(pluginId) {
|
||||||
|
return SettingsData.getBuiltInPluginSetting(pluginId, "enabled", true) && SettingsData.getPluginAllowWithoutTrigger(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipboardSearchEnabledInAll() {
|
||||||
|
return builtInLauncherVisibleInAll("dms_clipboard_search") && ClipboardService.clipboardAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSharedAllResults(allItems, query) {
|
||||||
|
if (!query || query.length < 2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (builtInLauncherVisibleInAll("dms_settings_search")) {
|
||||||
|
var settingsItems = AppSearchService.getBuiltInLauncherItems("dms_settings_search", query);
|
||||||
|
var settingsLimit = Math.min(settingsItems.length, 8);
|
||||||
|
for (var i = 0; i < settingsLimit; i++) {
|
||||||
|
settingsItems[i]._preScored = 890 - i;
|
||||||
|
allItems.push(transformBuiltInSearchItem(settingsItems[i], "dms_settings_search"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clipboardSearchEnabledInAll()) {
|
||||||
|
ClipboardService.ensureLauncherHistory();
|
||||||
|
var clipboardItems = AppSearchService.getBuiltInLauncherItems("dms_clipboard_search", query);
|
||||||
|
var clipboardLimit = Math.min(clipboardItems.length, 8);
|
||||||
|
for (var j = 0; j < clipboardLimit; j++) {
|
||||||
|
clipboardItems[j]._preScored = 840 - j;
|
||||||
|
allItems.push(transformBuiltInSearchItem(clipboardItems[j], "dms_clipboard_search"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function detectTrigger(query) {
|
function detectTrigger(query) {
|
||||||
if (!query || query.length === 0)
|
if (!query || query.length === 0)
|
||||||
return {
|
return {
|
||||||
@@ -1308,7 +1413,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildDynamicSectionDefs(items) {
|
function buildDynamicSectionDefs(items) {
|
||||||
var baseDefs = sectionDefinitions.slice();
|
var baseDefs = sectionDefinitions.map(function (def) {
|
||||||
|
return Object.assign({}, def);
|
||||||
|
});
|
||||||
var pluginSections = {};
|
var pluginSections = {};
|
||||||
var order = SettingsData.launcherPluginOrder || [];
|
var order = SettingsData.launcherPluginOrder || [];
|
||||||
var orderMap = {};
|
var orderMap = {};
|
||||||
@@ -1316,6 +1423,12 @@ Item {
|
|||||||
orderMap[order[k]] = k;
|
orderMap[order[k]] = k;
|
||||||
var unorderedPriority = 2.6 + order.length * 0.01;
|
var unorderedPriority = 2.6 + order.length * 0.01;
|
||||||
|
|
||||||
|
for (var d = 0; d < baseDefs.length; d++) {
|
||||||
|
var virtualId = baseDefs[d].id === "settings" ? "dms_settings_search" : baseDefs[d].id === "clipboard" ? "dms_clipboard_search" : "";
|
||||||
|
if (virtualId && orderMap[virtualId] !== undefined)
|
||||||
|
baseDefs[d].priority = 2.6 + orderMap[virtualId] * 0.01;
|
||||||
|
}
|
||||||
|
|
||||||
for (var i = 0; i < items.length; i++) {
|
for (var i = 0; i < items.length; i++) {
|
||||||
var section = items[i].section;
|
var section = items[i].section;
|
||||||
if (!section || !section.startsWith("plugin_"))
|
if (!section || !section.startsWith("plugin_"))
|
||||||
@@ -1768,6 +1881,20 @@ Item {
|
|||||||
AppSearchService.executePluginItem(item.data, item.pluginId);
|
AppSearchService.executePluginItem(item.data, item.pluginId);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "setting":
|
||||||
|
AppSearchService.executeBuiltInLauncherItem(item.data);
|
||||||
|
break;
|
||||||
|
case "clipboard":
|
||||||
|
if (SettingsData.clipboardEnterToPaste) {
|
||||||
|
ClipboardService.pasteEntry(item.data, function () {
|
||||||
|
root.itemExecuted();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ClipboardService.copyEntry(item.data, function () {
|
||||||
|
root.itemExecuted();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
case "file":
|
case "file":
|
||||||
openFile(item.data?.path);
|
openFile(item.data?.path);
|
||||||
break;
|
break;
|
||||||
@@ -1803,6 +1930,16 @@ Item {
|
|||||||
case "execute":
|
case "execute":
|
||||||
executeItem(item);
|
executeItem(item);
|
||||||
break;
|
break;
|
||||||
|
case "clipboard_copy":
|
||||||
|
ClipboardService.copyEntry(item.data, function () {
|
||||||
|
root.itemExecuted();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
case "clipboard_paste":
|
||||||
|
ClipboardService.pasteEntry(item.data, function () {
|
||||||
|
root.itemExecuted();
|
||||||
|
});
|
||||||
|
return;
|
||||||
case "launch_dgpu":
|
case "launch_dgpu":
|
||||||
if (item.type === "app" && item.data) {
|
if (item.type === "app" && item.data) {
|
||||||
launchAppWithNvidia(item.data);
|
launchAppWithNvidia(item.data);
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ Item {
|
|||||||
impl.item.toggleWithMode(mode);
|
impl.item.toggleWithMode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var _desiredBackend: SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp
|
readonly property bool useSpotlightBackend: SettingsData.connectedFrameModeActive ? SettingsData.frameUseSpotlightLauncher : SettingsData.launcherStyle === "spotlight"
|
||||||
|
readonly property var _desiredBackend: useSpotlightBackend ? spotlightComp : (SettingsData.connectedFrameModeActive ? connectedComp : standaloneComp)
|
||||||
property var _resolvedBackend: null
|
property var _resolvedBackend: null
|
||||||
|
|
||||||
Component.onCompleted: _resolvedBackend = _desiredBackend
|
Component.onCompleted: _resolvedBackend = _desiredBackend
|
||||||
@@ -71,6 +72,12 @@ Item {
|
|||||||
function onConnectedFrameModeActiveChanged() {
|
function onConnectedFrameModeActiveChanged() {
|
||||||
root._maybeResolveBackend();
|
root._maybeResolveBackend();
|
||||||
}
|
}
|
||||||
|
function onFrameUseSpotlightLauncherChanged() {
|
||||||
|
root._maybeResolveBackend();
|
||||||
|
}
|
||||||
|
function onLauncherStyleChanged() {
|
||||||
|
root._maybeResolveBackend();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defer Loader source-component swap until impl is fully closed; avoids
|
// Defer Loader source-component swap until impl is fully closed; avoids
|
||||||
@@ -100,6 +107,11 @@ Item {
|
|||||||
DankLauncherV2ModalConnected {}
|
DankLauncherV2ModalConnected {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: spotlightComp
|
||||||
|
DankLauncherV2ModalSpotlight {}
|
||||||
|
}
|
||||||
|
|
||||||
function _wireBackend(it) {
|
function _wireBackend(it) {
|
||||||
if (!it)
|
if (!it)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
readonly property var log: Log.scoped("DankLauncherV2ModalSpotlight")
|
||||||
|
|
||||||
|
property var modalHandle: root
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
property bool spotlightOpen: false
|
||||||
|
property bool keyboardActive: false
|
||||||
|
property bool contentVisible: false
|
||||||
|
property var spotlightContent: contentLoader.item
|
||||||
|
property bool openedFromOverview: false
|
||||||
|
property bool isClosing: false
|
||||||
|
property bool _pendingInitialize: false
|
||||||
|
property string _pendingQuery: ""
|
||||||
|
property string _pendingMode: ""
|
||||||
|
|
||||||
|
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||||
|
readonly property var effectiveScreen: launcherWindow.screen
|
||||||
|
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||||
|
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||||
|
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||||
|
|
||||||
|
readonly property int _openDuration: 80
|
||||||
|
readonly property int _closeDuration: 70
|
||||||
|
readonly property int _motionDuration: 90
|
||||||
|
|
||||||
|
// Connected frame mode clamps the centered surface inside frame insets.
|
||||||
|
readonly property bool frameConnected: SettingsData.connectedFrameModeActive && !!effectiveScreen && SettingsData.isScreenInPreferences(effectiveScreen, SettingsData.frameScreenPreferences)
|
||||||
|
|
||||||
|
function _frameEdgeInset(side) {
|
||||||
|
if (!effectiveScreen || !frameConnected)
|
||||||
|
return 0;
|
||||||
|
return SettingsData.frameEdgeInsetForSide(effectiveScreen, side);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed 680px width, centered horizontally (respecting frame insets)
|
||||||
|
readonly property int modalWidth: Math.min(680, screenWidth - 80)
|
||||||
|
readonly property real modalX: {
|
||||||
|
const insetL = _frameEdgeInset("left");
|
||||||
|
const insetR = _frameEdgeInset("right");
|
||||||
|
const usable = Math.max(0, screenWidth - insetL - insetR);
|
||||||
|
return insetL + Math.max(0, (usable - modalWidth) / 2);
|
||||||
|
}
|
||||||
|
// Keep the search bar centered; results expand downward unless the screen edge clamps it.
|
||||||
|
readonly property real modalY: {
|
||||||
|
const insetT = _frameEdgeInset("top");
|
||||||
|
const insetB = _frameEdgeInset("bottom");
|
||||||
|
const searchBarH = 56;
|
||||||
|
const usableH = Math.max(searchBarH, screenHeight - insetT - insetB);
|
||||||
|
const preferred = insetT + Math.max(0, usableH * 0.33 - searchBarH / 2);
|
||||||
|
const maxY = Math.max(insetT, screenHeight - insetB - _contentImplicitH);
|
||||||
|
return Math.max(insetT, Math.min(preferred, maxY));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic height from content
|
||||||
|
readonly property real _contentImplicitH: contentLoader.item?.implicitHeight ?? 56
|
||||||
|
readonly property int modalHeight: _contentImplicitH
|
||||||
|
|
||||||
|
readonly property var shadowLevel: Theme.elevationLevel3
|
||||||
|
readonly property real shadowFallbackOffset: 6
|
||||||
|
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||||
|
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
|
||||||
|
|
||||||
|
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||||
|
readonly property real alignedX: Theme.snap(modalX, dpr)
|
||||||
|
readonly property real alignedY: Theme.snap(modalY, dpr)
|
||||||
|
|
||||||
|
// Extra headroom above the window for the slide-in animation
|
||||||
|
readonly property real _animHeadroom: 16
|
||||||
|
readonly property real windowX: Math.max(0, Theme.snap(alignedX - shadowPad, dpr))
|
||||||
|
readonly property real windowY: Math.max(0, Theme.snap(alignedY - shadowPad - _animHeadroom, dpr))
|
||||||
|
readonly property real contentX: Theme.snap(alignedX - windowX, dpr)
|
||||||
|
readonly property real contentY: Theme.snap(alignedY - windowY, dpr)
|
||||||
|
readonly property real windowWidth: alignedWidth + contentX + shadowPad
|
||||||
|
readonly property real _animatedContentH: Theme.snap(_contentImplicitH, dpr)
|
||||||
|
readonly property real windowHeight: _animatedContentH + contentY + shadowPad + _animHeadroom
|
||||||
|
|
||||||
|
readonly property color backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
readonly property real cornerRadius: Theme.cornerRadius
|
||||||
|
|
||||||
|
readonly property color borderColor: {
|
||||||
|
if (!SettingsData.dankLauncherV2BorderEnabled)
|
||||||
|
return Theme.outlineMedium;
|
||||||
|
switch (SettingsData.dankLauncherV2BorderColor) {
|
||||||
|
case "primary":
|
||||||
|
return Theme.primary;
|
||||||
|
case "secondary":
|
||||||
|
return Theme.secondary;
|
||||||
|
case "outline":
|
||||||
|
return Theme.outline;
|
||||||
|
case "surfaceText":
|
||||||
|
return Theme.surfaceText;
|
||||||
|
default:
|
||||||
|
return Theme.primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||||
|
|
||||||
|
signal dialogClosed
|
||||||
|
|
||||||
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
|
_pendingQuery = query || "";
|
||||||
|
_pendingMode = mode || "";
|
||||||
|
_pendingInitialize = true;
|
||||||
|
contentVisible = true;
|
||||||
|
contentLoader.active = true;
|
||||||
|
|
||||||
|
if (spotlightContent) {
|
||||||
|
_initializeContent(_pendingQuery, _pendingMode);
|
||||||
|
_pendingInitialize = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initializeContent(query, mode) {
|
||||||
|
if (!spotlightContent)
|
||||||
|
return;
|
||||||
|
contentVisible = true;
|
||||||
|
|
||||||
|
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
|
||||||
|
const targetMode = mode || SessionData.launcherLastMode || "all";
|
||||||
|
|
||||||
|
if (spotlightContent.searchField) {
|
||||||
|
spotlightContent.searchField.text = targetQuery;
|
||||||
|
}
|
||||||
|
if (spotlightContent.controller) {
|
||||||
|
spotlightContent.controller.reset();
|
||||||
|
spotlightContent.controller.searchMode = targetMode;
|
||||||
|
spotlightContent.controller.historyIndex = -1;
|
||||||
|
if (targetQuery.length > 0)
|
||||||
|
spotlightContent.controller.setSearchQuery(targetQuery);
|
||||||
|
}
|
||||||
|
if (spotlightContent.resetScroll) {
|
||||||
|
spotlightContent.resetScroll();
|
||||||
|
}
|
||||||
|
if (spotlightContent.searchField) {
|
||||||
|
spotlightContent.searchField.forceActiveFocus();
|
||||||
|
spotlightContent.searchField.cursorPosition = spotlightContent.searchField.text.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _finishShow(query, mode) {
|
||||||
|
spotlightOpen = true;
|
||||||
|
isClosing = false;
|
||||||
|
openedFromOverview = false;
|
||||||
|
keyboardActive = true;
|
||||||
|
ModalManager.openModal(modalHandle);
|
||||||
|
if (useHyprlandFocusGrab)
|
||||||
|
focusGrab.active = true;
|
||||||
|
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function _openCommon(query, mode) {
|
||||||
|
closeCleanupTimer.stop();
|
||||||
|
const focusedScreen = CompositorService.getFocusedScreen();
|
||||||
|
if (focusedScreen && launcherWindow.screen !== focusedScreen) {
|
||||||
|
spotlightOpen = false;
|
||||||
|
isClosing = false;
|
||||||
|
launcherWindow.screen = focusedScreen;
|
||||||
|
Qt.callLater(() => root._finishShow(query, mode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_finishShow(query, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show() {
|
||||||
|
_openCommon("", "");
|
||||||
|
}
|
||||||
|
function showWithQuery(query) {
|
||||||
|
_openCommon(query, "");
|
||||||
|
}
|
||||||
|
function showWithMode(mode) {
|
||||||
|
_openCommon("", mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
if (!spotlightOpen)
|
||||||
|
return;
|
||||||
|
openedFromOverview = false;
|
||||||
|
isClosing = true;
|
||||||
|
contentVisible = false;
|
||||||
|
keyboardActive = false;
|
||||||
|
spotlightOpen = false;
|
||||||
|
focusGrab.active = false;
|
||||||
|
ModalManager.closeModal(modalHandle);
|
||||||
|
closeCleanupTimer.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
spotlightOpen ? hide() : show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWithMode(mode) {
|
||||||
|
spotlightOpen ? hide() : showWithMode(mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleWithQuery(query) {
|
||||||
|
spotlightOpen ? hide() : showWithQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: closeCleanupTimer
|
||||||
|
interval: root._motionDuration + 30
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
isClosing = false;
|
||||||
|
dialogClosed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
id: focusGrab
|
||||||
|
windows: [launcherWindow]
|
||||||
|
active: false
|
||||||
|
onCleared: {
|
||||||
|
if (spotlightOpen)
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ModalManager
|
||||||
|
function onCloseAllModalsExcept(excludedModal) {
|
||||||
|
if (excludedModal !== modalHandle && spotlightOpen)
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: Quickshell
|
||||||
|
function onScreensChanged() {
|
||||||
|
if (Quickshell.screens.length === 0)
|
||||||
|
return;
|
||||||
|
const screenName = launcherWindow.screen?.name;
|
||||||
|
if (screenName) {
|
||||||
|
for (let i = 0; i < Quickshell.screens.length; i++) {
|
||||||
|
if (Quickshell.screens[i].name === screenName)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (spotlightOpen)
|
||||||
|
hide();
|
||||||
|
const newScreen = CompositorService.getFocusedScreen() ?? Quickshell.screens[0];
|
||||||
|
if (newScreen)
|
||||||
|
launcherWindow.screen = newScreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background click catcher
|
||||||
|
PanelWindow {
|
||||||
|
id: clickCatcher
|
||||||
|
screen: launcherWindow.screen
|
||||||
|
visible: spotlightOpen || isClosing
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "dms:spotlight:clickcatcher"
|
||||||
|
WlrLayershell.layer: WlrLayershell.Top
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
bottom: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: bgMask
|
||||||
|
|
||||||
|
Region {
|
||||||
|
item: bgHole
|
||||||
|
intersection: Intersection.Subtract
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: bgMask
|
||||||
|
visible: false
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bgHole
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
x: root.windowX
|
||||||
|
y: root.windowY
|
||||||
|
width: root.windowWidth
|
||||||
|
height: root.windowHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: spotlightOpen
|
||||||
|
onClicked: root.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launcher window
|
||||||
|
PanelWindow {
|
||||||
|
id: launcherWindow
|
||||||
|
visible: spotlightOpen || isClosing
|
||||||
|
color: "transparent"
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|
||||||
|
WindowBlur {
|
||||||
|
targetWindow: launcherWindow
|
||||||
|
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
|
||||||
|
blurX: modalContainer.x
|
||||||
|
blurY: modalContainer.y + modalContainer.slideOffset
|
||||||
|
blurWidth: contentVisible ? root.alignedWidth * op : 0
|
||||||
|
blurHeight: contentVisible ? root._contentImplicitH * op : 0
|
||||||
|
blurRadius: root.cornerRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "dms:spotlight"
|
||||||
|
WlrLayershell.layer: {
|
||||||
|
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||||
|
case "overlay":
|
||||||
|
return WlrLayershell.Overlay;
|
||||||
|
default:
|
||||||
|
return WlrLayershell.Top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.margins {
|
||||||
|
left: root.windowX
|
||||||
|
top: root.windowY
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitWidth: root.windowWidth
|
||||||
|
implicitHeight: root.windowHeight
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: inputMask
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: inputMask
|
||||||
|
visible: false
|
||||||
|
color: "transparent"
|
||||||
|
x: modalContainer.x
|
||||||
|
y: modalContainer.y + modalContainer.slideOffset
|
||||||
|
width: root.alignedWidth
|
||||||
|
height: root._contentImplicitH
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: modalContainer
|
||||||
|
x: root.contentX
|
||||||
|
y: root.contentY
|
||||||
|
width: root.alignedWidth
|
||||||
|
height: root._animatedContentH
|
||||||
|
visible: _renderActive
|
||||||
|
|
||||||
|
property bool _renderActive: contentVisible
|
||||||
|
property real slideOffset: contentVisible ? 0 : -root._animHeadroom
|
||||||
|
|
||||||
|
opacity: contentVisible ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: contentVisible ? root._openDuration : root._closeDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: contentVisible ? [0.0, 0.0, 0.2, 1.0, 1.0, 1.0] : [0.4, 0.0, 1.0, 1.0, 1.0, 1.0]
|
||||||
|
onRunningChanged: {
|
||||||
|
if (!running && !root.contentVisible)
|
||||||
|
modalContainer._renderActive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on slideOffset {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root._motionDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: contentVisible ? [0.2, 0.0, 0.0, 1.0, 1.0, 1.0] : [0.4, 0.0, 1.0, 1.0, 1.0, 1.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root
|
||||||
|
function onContentVisibleChanged() {
|
||||||
|
if (root.contentVisible)
|
||||||
|
modalContainer._renderActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevationShadow {
|
||||||
|
anchors.fill: contentWrapper
|
||||||
|
level: root.shadowLevel
|
||||||
|
fallbackOffset: root.shadowFallbackOffset
|
||||||
|
targetColor: root.backgroundColor
|
||||||
|
borderColor: root.borderColor
|
||||||
|
borderWidth: root.borderWidth
|
||||||
|
targetRadius: root.cornerRadius
|
||||||
|
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: contentWrapper
|
||||||
|
x: 0
|
||||||
|
y: modalContainer.slideOffset
|
||||||
|
width: parent.width
|
||||||
|
height: root._animatedContentH
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
onPressed: mouse => mouse.accepted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: keyboardActive
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
active: root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||||
|
asynchronous: false
|
||||||
|
sourceComponent: SpotlightLauncherContent {
|
||||||
|
focus: true
|
||||||
|
parentModal: root
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoaded: {
|
||||||
|
if (root._pendingInitialize) {
|
||||||
|
root._initializeContent(root._pendingQuery, root._pendingMode);
|
||||||
|
root._pendingInitialize = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: event => {
|
||||||
|
root.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,17 +94,19 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.action || "",
|
id: item.action || "",
|
||||||
type: "plugin",
|
type: item.type || "plugin",
|
||||||
name: item.name || "",
|
name: item.name || "",
|
||||||
subtitle: item.comment || "",
|
subtitle: item.comment || "",
|
||||||
icon: icon,
|
icon: icon,
|
||||||
iconType: iconType,
|
iconType: iconType,
|
||||||
section: "plugin_" + pluginId,
|
section: item.section || ("plugin_" + pluginId),
|
||||||
data: item,
|
data: item.data || item,
|
||||||
pluginId: pluginId,
|
pluginId: pluginId,
|
||||||
isBuiltInLauncher: true,
|
isBuiltInLauncher: true,
|
||||||
keywords: item.keywords || [],
|
keywords: item.keywords || [],
|
||||||
actions: [],
|
actions: [],
|
||||||
|
source: item.source || "",
|
||||||
|
badgeLabel: item.badgeLabel || "",
|
||||||
primaryAction: {
|
primaryAction: {
|
||||||
name: openLabel,
|
name: openLabel,
|
||||||
icon: "open_in_new",
|
icon: "open_in_new",
|
||||||
@@ -117,6 +119,58 @@ function transformBuiltInLauncherItem(item, pluginId, openLabel) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function transformClipboardItem(entry, copyLabel, pasteLabel, primaryLabel, imageLabel, textLabel, pinnedLabel, pasteHintLabel, copyHintLabel, clipboardLabel) {
|
||||||
|
var preview = (entry.preview || "").toString().replace(/\s+/g, " ").trim();
|
||||||
|
var isImage = entry.isImage || false;
|
||||||
|
var title = preview.length > 0 ? preview : imageLabel;
|
||||||
|
if (title.length > 140)
|
||||||
|
title = title.substring(0, 137) + "...";
|
||||||
|
|
||||||
|
var typeLabel = isImage ? imageLabel : textLabel;
|
||||||
|
var subtitle = typeLabel;
|
||||||
|
if (entry.pinned)
|
||||||
|
subtitle = pinnedLabel + " - " + subtitle;
|
||||||
|
if (pasteHintLabel)
|
||||||
|
subtitle += " - " + pasteHintLabel;
|
||||||
|
if (copyHintLabel)
|
||||||
|
subtitle += " - " + copyHintLabel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "clipboard_" + (entry.id || entry.hash || title),
|
||||||
|
type: "clipboard",
|
||||||
|
name: title,
|
||||||
|
subtitle: subtitle,
|
||||||
|
icon: isImage ? "image" : "content_paste",
|
||||||
|
iconType: "material",
|
||||||
|
section: "clipboard",
|
||||||
|
data: entry,
|
||||||
|
keywords: [preview, isImage ? "image" : "text", "clipboard"],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
name: copyLabel,
|
||||||
|
icon: "content_copy",
|
||||||
|
action: "clipboard_copy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: pasteLabel,
|
||||||
|
icon: "content_paste",
|
||||||
|
action: "clipboard_paste"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
source: clipboardLabel,
|
||||||
|
badgeLabel: clipboardLabel,
|
||||||
|
primaryAction: {
|
||||||
|
name: primaryLabel,
|
||||||
|
icon: primaryLabel === pasteLabel ? "content_paste" : "content_copy",
|
||||||
|
action: primaryLabel === pasteLabel ? "clipboard_paste" : "clipboard_copy"
|
||||||
|
},
|
||||||
|
_hName: "",
|
||||||
|
_hSub: "",
|
||||||
|
_hRich: false,
|
||||||
|
_preScored: entry._preScored
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) {
|
function transformFileResult(file, openLabel, openFolderLabel, copyPathLabel, openTerminalLabel) {
|
||||||
var filename = file.path ? file.path.split("/").pop() : "";
|
var filename = file.path ? file.path.split("/").pop() : "";
|
||||||
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
|
var dirname = file.path ? file.path.substring(0, file.path.lastIndexOf("/")) : "";
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Popup {
|
|||||||
property var controller: null
|
property var controller: null
|
||||||
property var searchField: null
|
property var searchField: null
|
||||||
property var parentHandler: null
|
property var parentHandler: null
|
||||||
|
property bool allowEditActions: true
|
||||||
|
|
||||||
signal hideRequested
|
signal hideRequested
|
||||||
signal editAppRequested(var app)
|
signal editAppRequested(var app)
|
||||||
@@ -31,6 +32,8 @@ Popup {
|
|||||||
var actions = instance.getContextMenuActions(spotlightItem.data);
|
var actions = instance.getContextMenuActions(spotlightItem.data);
|
||||||
return Array.isArray(actions) && actions.length > 0;
|
return Array.isArray(actions) && actions.length > 0;
|
||||||
}
|
}
|
||||||
|
if (spotlightItem.actions && spotlightItem.actions.length > 0)
|
||||||
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +82,13 @@ Popup {
|
|||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function executeLauncherAction(actionData) {
|
||||||
|
if (!controller || !item || !actionData)
|
||||||
|
return;
|
||||||
|
controller.executeAction(item, actionData);
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
|
||||||
readonly property var menuItems: {
|
readonly property var menuItems: {
|
||||||
var items = [];
|
var items = [];
|
||||||
|
|
||||||
@@ -96,6 +106,19 @@ Popup {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item?.type !== "app" && item?.actions && item.actions.length > 0) {
|
||||||
|
for (var i = 0; i < item.actions.length; i++) {
|
||||||
|
var genericAct = item.actions[i];
|
||||||
|
items.push({
|
||||||
|
type: "item",
|
||||||
|
icon: genericAct.icon || "play_arrow",
|
||||||
|
text: genericAct.name || "",
|
||||||
|
launcherActionData: genericAct
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
if (item?.type === "app") {
|
if (item?.type === "app") {
|
||||||
items.push({
|
items.push({
|
||||||
type: "item",
|
type: "item",
|
||||||
@@ -112,12 +135,14 @@ Popup {
|
|||||||
text: I18n.tr("Hide App"),
|
text: I18n.tr("Hide App"),
|
||||||
action: hideCurrentApp
|
action: hideCurrentApp
|
||||||
});
|
});
|
||||||
items.push({
|
if (allowEditActions) {
|
||||||
type: "item",
|
items.push({
|
||||||
icon: "edit",
|
type: "item",
|
||||||
text: I18n.tr("Edit App"),
|
icon: "edit",
|
||||||
action: editCurrentApp
|
text: I18n.tr("Edit App"),
|
||||||
});
|
action: editCurrentApp
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item?.actions && item.actions.length > 0) {
|
if (item?.actions && item.actions.length > 0) {
|
||||||
@@ -290,6 +315,8 @@ Popup {
|
|||||||
menuItem.action();
|
menuItem.action();
|
||||||
else if (menuItem.pluginAction)
|
else if (menuItem.pluginAction)
|
||||||
executePluginAction(menuItem.pluginAction);
|
executePluginAction(menuItem.pluginAction);
|
||||||
|
else if (menuItem.launcherActionData)
|
||||||
|
executeLauncherAction(menuItem.launcherActionData);
|
||||||
else if (menuItem.actionData)
|
else if (menuItem.actionData)
|
||||||
executeDesktopAction(menuItem.actionData);
|
executeDesktopAction(menuItem.actionData);
|
||||||
return;
|
return;
|
||||||
@@ -497,6 +524,8 @@ Popup {
|
|||||||
menuItem.action();
|
menuItem.action();
|
||||||
else if (menuItem.pluginAction)
|
else if (menuItem.pluginAction)
|
||||||
root.executePluginAction(menuItem.pluginAction);
|
root.executePluginAction(menuItem.pluginAction);
|
||||||
|
else if (menuItem.launcherActionData)
|
||||||
|
root.executeLauncherAction(menuItem.launcherActionData);
|
||||||
else if (menuItem.actionData)
|
else if (menuItem.actionData)
|
||||||
root.executeDesktopAction(menuItem.actionData);
|
root.executeDesktopAction(menuItem.actionData);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Rectangle {
|
|||||||
return item.icon || "";
|
return item.icon || "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
||||||
|
|
||||||
width: parent?.width ?? 200
|
width: parent?.width ?? 200
|
||||||
height: 52
|
height: 52
|
||||||
@@ -154,6 +155,14 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
ClipboardLauncherPreview {
|
||||||
|
width: root.hasClipboardPreview ? 56 : 0
|
||||||
|
height: 36
|
||||||
|
visible: root.hasClipboardPreview
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
entry: root.item?.data ?? null
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: allModeToggle
|
id: allModeToggle
|
||||||
visible: root.item?.type === "plugin_browse"
|
visible: root.item?.type === "plugin_browse"
|
||||||
@@ -208,9 +217,15 @@ Rectangle {
|
|||||||
text: {
|
text: {
|
||||||
if (!root.item)
|
if (!root.item)
|
||||||
return "";
|
return "";
|
||||||
|
if ((root.item.badgeLabel ?? "").length > 0)
|
||||||
|
return root.item.badgeLabel;
|
||||||
switch (root.item.type) {
|
switch (root.item.type) {
|
||||||
case "plugin":
|
case "plugin":
|
||||||
return I18n.tr("Plugin");
|
return I18n.tr("Plugin");
|
||||||
|
case "setting":
|
||||||
|
return I18n.tr("Setting");
|
||||||
|
case "clipboard":
|
||||||
|
return I18n.tr("Clipboard");
|
||||||
case "file":
|
case "file":
|
||||||
return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
|
return root.item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const Weights = {
|
|||||||
typeBonus: {
|
typeBonus: {
|
||||||
app: 1000,
|
app: 1000,
|
||||||
plugin: 900,
|
plugin: 900,
|
||||||
|
setting: 850,
|
||||||
|
clipboard: 825,
|
||||||
file: 800,
|
file: 800,
|
||||||
action: 600
|
action: 600
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,475 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
FocusScope {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var parentModal: null
|
||||||
|
property alias searchField: searchInput
|
||||||
|
property alias controller: searchController
|
||||||
|
|
||||||
|
readonly property bool _hasQuery: searchInput.text.length > 0
|
||||||
|
readonly property real _searchBarH: 56
|
||||||
|
readonly property real _surfaceInset: BlurService.enabled ? (_hasQuery ? Theme.spacingS : Theme.spacingXS) : 0
|
||||||
|
readonly property real _searchAreaH: _searchBarH + _surfaceInset * 2
|
||||||
|
readonly property real _statusH: 92
|
||||||
|
readonly property real _rowH: 64
|
||||||
|
readonly property real _maxResultsH: Math.min(430, (parentModal?.screenHeight ?? 900) * 0.55)
|
||||||
|
readonly property var _resultRows: _buildRows()
|
||||||
|
readonly property real _resultsContentH: _resultRows.length > 0 ? _resultRows.length * _rowH : _statusH
|
||||||
|
readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0
|
||||||
|
readonly property int _fastDuration: 90
|
||||||
|
readonly property int _resizeDuration: 110
|
||||||
|
|
||||||
|
implicitHeight: _searchAreaH + (_resultsH > 0 ? 1 + _resultsH : 0)
|
||||||
|
|
||||||
|
function resetScroll() {
|
||||||
|
resultsList.resetScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _buildRows() {
|
||||||
|
const flat = searchController.flatModel || [];
|
||||||
|
const sections = searchController.sections || [];
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 0; i < flat.length; i++) {
|
||||||
|
const entry = flat[i];
|
||||||
|
if (!entry || entry.isHeader || !entry.item)
|
||||||
|
continue;
|
||||||
|
const section = sections[entry.sectionIndex] || null;
|
||||||
|
rows.push({
|
||||||
|
"_rowId": entry.item.id || (entry.sectionId + ":" + entry.indexInSection + ":" + i),
|
||||||
|
"item": entry.item,
|
||||||
|
"flatIndex": i,
|
||||||
|
"sectionTitle": section?.title || "",
|
||||||
|
"sectionIcon": section?.icon || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _focusSearch() {
|
||||||
|
searchInput.forceActiveFocus();
|
||||||
|
searchInput.cursorPosition = searchInput.text.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showContextMenu(item, sceneX, sceneY, fromKeyboard) {
|
||||||
|
if (!item || !contextMenu.hasContextMenuActions(item))
|
||||||
|
return;
|
||||||
|
const localPos = root.mapFromItem(null, sceneX, sceneY);
|
||||||
|
contextMenu.show(localPos.x, localPos.y, item, fromKeyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _handleKey(event) {
|
||||||
|
const hasCtrl = event.modifiers & Qt.ControlModifier;
|
||||||
|
const hasAlt = event.modifiers & Qt.AltModifier;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case Qt.Key_Escape:
|
||||||
|
if (searchController.clearPluginFilter()) {
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.parentModal?.hide();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Backspace:
|
||||||
|
if (searchInput.text.length === 0) {
|
||||||
|
if (searchController.clearPluginFilter()) {
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (searchController.autoSwitchedToFiles) {
|
||||||
|
searchController.restorePreviousMode();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Down:
|
||||||
|
searchController.selectNext();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Up:
|
||||||
|
searchController.selectPrevious();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_PageDown:
|
||||||
|
searchController.selectPageDown(7);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_PageUp:
|
||||||
|
searchController.selectPageUp(7);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_J:
|
||||||
|
if (hasCtrl) {
|
||||||
|
searchController.selectNext();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_K:
|
||||||
|
if (hasCtrl) {
|
||||||
|
searchController.selectPrevious();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_Tab:
|
||||||
|
if (_hasQuery)
|
||||||
|
_cycleCategory(false);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Backtab:
|
||||||
|
if (_hasQuery)
|
||||||
|
_cycleCategory(true);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Return:
|
||||||
|
case Qt.Key_Enter:
|
||||||
|
if (event.modifiers & Qt.ShiftModifier) {
|
||||||
|
searchController.pasteSelected();
|
||||||
|
} else {
|
||||||
|
searchController.executeSelected();
|
||||||
|
}
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
case Qt.Key_Menu:
|
||||||
|
case Qt.Key_F10:
|
||||||
|
if (contextMenu.hasContextMenuActions(searchController.selectedItem)) {
|
||||||
|
const scenePos = resultsList.getSelectedItemPosition();
|
||||||
|
_showContextMenu(searchController.selectedItem, scenePos.x, scenePos.y, true);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_1:
|
||||||
|
if (hasCtrl || hasAlt) {
|
||||||
|
searchController.setMode("all");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_2:
|
||||||
|
if (hasCtrl || hasAlt) {
|
||||||
|
searchController.setMode("apps");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_3:
|
||||||
|
if (hasCtrl || hasAlt) {
|
||||||
|
searchController.setMode("files");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_4:
|
||||||
|
if (hasCtrl || hasAlt) {
|
||||||
|
searchController.setMode("plugins");
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Qt.Key_Slash:
|
||||||
|
if (event.modifiers === Qt.NoModifier && searchInput.text.length === 0) {
|
||||||
|
searchController.setMode("files", true);
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.accepted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Controller {
|
||||||
|
id: searchController
|
||||||
|
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||||
|
viewModeContext: "spotlight"
|
||||||
|
|
||||||
|
onItemExecuted: {
|
||||||
|
root.parentModal?.hide();
|
||||||
|
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview)
|
||||||
|
NiriService.toggleOverview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LauncherContextMenu {
|
||||||
|
id: contextMenu
|
||||||
|
parent: root
|
||||||
|
controller: searchController
|
||||||
|
searchField: searchInput
|
||||||
|
parentHandler: root
|
||||||
|
allowEditActions: false
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: searchController
|
||||||
|
function onModeChanged(mode) {
|
||||||
|
if (searchController.autoSwitchedToFiles)
|
||||||
|
return;
|
||||||
|
SessionData.setLauncherLastMode(mode);
|
||||||
|
}
|
||||||
|
function onSearchQueryRequested(query) {
|
||||||
|
searchInput.text = query;
|
||||||
|
root._focusSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: searchBarItem
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: root._searchAreaH
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: searchBarSurface
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: root._surfaceInset
|
||||||
|
radius: height / 2
|
||||||
|
color: Theme.withAlpha(root._hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, root._hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9))
|
||||||
|
border.color: BlurService.enabled && !root._hasQuery ? Theme.withAlpha(Theme.outline, 0.08) : "transparent"
|
||||||
|
border.width: BlurService.enabled && !root._hasQuery ? 1 : 0
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: root._fastDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: leadingWell
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: height / 2
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: searchInput.activeFocus ? Theme.primaryContainer : Theme.surfaceContainer
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: searchController.activePluginId ? "extension" : searchController.searchMode === "files" ? "folder" : "search"
|
||||||
|
size: 20
|
||||||
|
color: searchInput.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: rightControls
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: categoryRow
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: root._hasQuery
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root._categoryModel
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: categoryChip
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
readonly property bool isSelected: root._isCategorySelected(modelData)
|
||||||
|
|
||||||
|
width: chipLabel.implicitWidth + Theme.spacingM * 2
|
||||||
|
height: 26
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: height / 2
|
||||||
|
color: categoryChip.isSelected ? Theme.primary : chipArea.containsMouse ? Theme.surfaceHover : Theme.surfaceVariantAlpha
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: root._fastDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: chipLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: categoryChip.modelData.label
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: categoryChip.isSelected ? Font.Medium : Font.Normal
|
||||||
|
color: categoryChip.isSelected ? Theme.primaryText : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: chipArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root._selectCategory(categoryChip.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: clearButton
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "close"
|
||||||
|
iconSize: 16
|
||||||
|
visible: searchInput.text.length > 0
|
||||||
|
onClicked: {
|
||||||
|
searchInput.text = "";
|
||||||
|
searchController.reset();
|
||||||
|
root._focusSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.left: leadingWell.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.right: rightControls.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: I18n.tr("Spotlight Search")
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.outlineButton
|
||||||
|
visible: searchInput.text.length === 0
|
||||||
|
clip: true
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: searchInput
|
||||||
|
anchors.left: leadingWell.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.right: rightControls.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
font.pixelSize: 18
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
selectionColor: Theme.primary
|
||||||
|
selectedTextColor: Theme.primaryText
|
||||||
|
clip: true
|
||||||
|
focus: true
|
||||||
|
|
||||||
|
onTextChanged: {
|
||||||
|
if (text.length > 0) {
|
||||||
|
searchController.setSearchQuery(text);
|
||||||
|
} else {
|
||||||
|
searchController.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: event => root._handleKey(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.top: searchBarItem.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: root._surfaceInset
|
||||||
|
anchors.rightMargin: root._surfaceInset
|
||||||
|
height: 1
|
||||||
|
color: Theme.outlineMedium
|
||||||
|
opacity: root._resultsH > 0 ? 0.55 : 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root._fastDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: resultsContainer
|
||||||
|
anchors.top: searchBarItem.bottom
|
||||||
|
anchors.topMargin: 1
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
clip: true
|
||||||
|
height: root._resultsH
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: root._resizeDuration
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: [0.2, 0.0, 0.0, 1.0, 1.0, 1.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SpotlightResultsList {
|
||||||
|
id: resultsList
|
||||||
|
anchors.fill: parent
|
||||||
|
controller: searchController
|
||||||
|
hasQuery: root._hasQuery
|
||||||
|
rows: root._resultRows
|
||||||
|
|
||||||
|
onItemRightClicked: (index, item, sceneX, sceneY) => {
|
||||||
|
root._showContextMenu(item, sceneX, sceneY, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var _categoryModel: [
|
||||||
|
{
|
||||||
|
"label": I18n.tr("All"),
|
||||||
|
"mode": "all"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": I18n.tr("Apps"),
|
||||||
|
"mode": "apps"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": I18n.tr("Files"),
|
||||||
|
"mode": "files"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": I18n.tr("Plugins"),
|
||||||
|
"mode": "plugins"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
function _isCategorySelected(cat) {
|
||||||
|
return searchController.searchMode === cat.mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _cycleCategory(reverse) {
|
||||||
|
let idx = 0;
|
||||||
|
for (let i = 0; i < _categoryModel.length; i++) {
|
||||||
|
if (_isCategorySelected(_categoryModel[i])) {
|
||||||
|
idx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
idx = reverse ? (idx - 1 + _categoryModel.length) % _categoryModel.length : (idx + 1) % _categoryModel.length;
|
||||||
|
_selectCategory(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _selectCategory(index) {
|
||||||
|
const cat = _categoryModel[index];
|
||||||
|
if (!cat)
|
||||||
|
return;
|
||||||
|
searchController.setMode(cat.mode, false);
|
||||||
|
if (root._hasQuery)
|
||||||
|
searchController.setSearchQuery(searchInput.text);
|
||||||
|
root._focusSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import "../../Common/htmlElide.js" as HtmlElide
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var item: null
|
||||||
|
property string sectionTitle: ""
|
||||||
|
property string sectionIcon: ""
|
||||||
|
property bool isSelected: false
|
||||||
|
property var controller: null
|
||||||
|
property int flatIndex: -1
|
||||||
|
property bool isHovered: itemArea.containsMouse || quickToggleArea.containsMouse
|
||||||
|
|
||||||
|
signal clicked
|
||||||
|
signal rightClicked(real mouseX, real mouseY)
|
||||||
|
|
||||||
|
readonly property string iconValue: {
|
||||||
|
if (!item)
|
||||||
|
return "";
|
||||||
|
switch (item.iconType) {
|
||||||
|
case "material":
|
||||||
|
case "nerd":
|
||||||
|
return "material:" + (item.icon || "apps");
|
||||||
|
case "unicode":
|
||||||
|
return "unicode:" + (item.icon || "");
|
||||||
|
case "composite":
|
||||||
|
return item.iconFull || "";
|
||||||
|
case "image":
|
||||||
|
default:
|
||||||
|
return item.icon || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property string previewSource: {
|
||||||
|
const data = item?.data;
|
||||||
|
const raw = data?.imageUrl || data?.imagePath || (data?.path && isImageFile(data.path) ? data.path : "");
|
||||||
|
if (!raw)
|
||||||
|
return "";
|
||||||
|
if (raw.startsWith("http://") || raw.startsWith("https://") || raw.startsWith("file://"))
|
||||||
|
return raw;
|
||||||
|
if (raw.startsWith("/"))
|
||||||
|
return "file://" + raw;
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
readonly property bool hasClipboardPreview: item?.type === "clipboard" && item?.data?.isImage === true && (item?.data?.mimeType ?? "").startsWith("image/")
|
||||||
|
readonly property bool hasMediaPreview: previewSource.length > 0 || hasClipboardPreview
|
||||||
|
readonly property bool previewAnimated: previewSource.toLowerCase().indexOf(".gif") >= 0
|
||||||
|
|
||||||
|
readonly property string typeLabel: {
|
||||||
|
if (!item)
|
||||||
|
return "";
|
||||||
|
if ((item.badgeLabel ?? "").length > 0)
|
||||||
|
return item.badgeLabel;
|
||||||
|
switch (item.type) {
|
||||||
|
case "plugin_browse":
|
||||||
|
return I18n.tr("Browse");
|
||||||
|
case "plugin":
|
||||||
|
return I18n.tr("Plugin");
|
||||||
|
case "setting":
|
||||||
|
return I18n.tr("Setting");
|
||||||
|
case "clipboard":
|
||||||
|
return I18n.tr("Clipboard");
|
||||||
|
case "file":
|
||||||
|
return item.data?.is_dir ? I18n.tr("Folder") : I18n.tr("File");
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width: parent?.width ?? 200
|
||||||
|
height: 64
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: root.isSelected ? Theme.primaryPressed : root.isHovered ? Theme.primaryHoverLight : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: 90
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankRipple {
|
||||||
|
id: rippleLayer
|
||||||
|
rippleColor: Theme.surfaceText
|
||||||
|
cornerRadius: root.radius
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: itemArea
|
||||||
|
z: 2
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: root.item?.type === "plugin_browse" ? 38 : 0
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onPositionChanged: {
|
||||||
|
if (root.controller)
|
||||||
|
root.controller.keyboardNavigationActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressed: mouse => {
|
||||||
|
if (mouse.button === Qt.LeftButton)
|
||||||
|
rippleLayer.trigger(mouse.x, mouse.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: mouse => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
const scenePos = mapToItem(null, mouse.x, mouse.y);
|
||||||
|
root.rightClicked(scenePos.x, scenePos.y);
|
||||||
|
} else {
|
||||||
|
root.clicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: iconWell
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: root.isSelected ? Theme.primaryContainer : Theme.surfaceContainerHigh
|
||||||
|
border.color: Theme.withAlpha(root.isSelected ? Theme.primary : Theme.outline, root.isSelected ? 0.28 : 0.12)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
AppIconRenderer {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 30
|
||||||
|
height: 30
|
||||||
|
iconValue: root.iconValue
|
||||||
|
iconSize: 30
|
||||||
|
fallbackText: (root.item?.name?.length > 0) ? root.item.name.charAt(0).toUpperCase() : "?"
|
||||||
|
materialIconSizeAdjustment: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: textColumn
|
||||||
|
anchors.left: iconWell.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.right: previewFrame.visible ? previewFrame.left : metaRow.left
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: nameText
|
||||||
|
width: parent.width
|
||||||
|
text: root.item?._hName ?? root.item?.name ?? ""
|
||||||
|
textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
maximumLineCount: 1
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
TextMetrics {
|
||||||
|
id: subProbe
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
elide: Qt.ElideRight
|
||||||
|
elideWidth: textColumn.width
|
||||||
|
text: root.item?._hRich ? HtmlElide.stripHtmlTags(root.item?._hSub ?? "") : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property int _richBudget: {
|
||||||
|
if (!subProbe.text)
|
||||||
|
return 0;
|
||||||
|
const elided = subProbe.elidedText;
|
||||||
|
return elided.endsWith("\u2026") ? elided.length - 1 : elided.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.item?._hRich ? HtmlElide.elideRichText(root.item._hSub ?? "", textColumn._richBudget) : (root.item?.subtitle ?? root.sectionTitle)
|
||||||
|
textFormat: root.item?._hRich ? Text.RichText : Text.PlainText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
maximumLineCount: 1
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: text.length > 0
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: metaRow
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: childrenRect.width > 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.typeLabel.length > 0
|
||||||
|
width: typeText.implicitWidth + Theme.spacingS * 2
|
||||||
|
height: 22
|
||||||
|
radius: height / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: Theme.surfaceVariantAlpha
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: typeText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.typeLabel
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 1
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
visible: root.item?.type === "plugin_browse"
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: height / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: quickToggleArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
readonly property bool isAllowed: {
|
||||||
|
if (root.item?.type !== "plugin_browse")
|
||||||
|
return false;
|
||||||
|
const pluginId = root.item?.data?.pluginId;
|
||||||
|
if (!pluginId)
|
||||||
|
return false;
|
||||||
|
SettingsData.launcherPluginVisibility;
|
||||||
|
return SettingsData.getPluginAllowWithoutTrigger(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: parent.isAllowed ? "visibility" : "visibility_off"
|
||||||
|
size: 17
|
||||||
|
color: parent.isAllowed ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: quickToggleArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
const pluginId = root.item?.data?.pluginId;
|
||||||
|
if (!pluginId)
|
||||||
|
return;
|
||||||
|
SettingsData.setPluginAllowWithoutTrigger(pluginId, !parent.isAllowed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceBadge {
|
||||||
|
visible: root.item?.type === "app"
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
source: root.item?.type === "app" ? (root.item.source || "") : ""
|
||||||
|
glyphSize: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: previewFrame
|
||||||
|
visible: root.hasMediaPreview
|
||||||
|
width: 64
|
||||||
|
height: 44
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
anchors.right: metaRow.left
|
||||||
|
anchors.rightMargin: metaRow.visible ? Theme.spacingS : 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
clip: true
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
border.color: Theme.withAlpha(Theme.outline, 0.16)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: root.previewSource
|
||||||
|
asynchronous: true
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
visible: !root.hasClipboardPreview && !root.previewAnimated
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedImage {
|
||||||
|
anchors.fill: parent
|
||||||
|
source: root.previewSource
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
playing: visible
|
||||||
|
visible: !root.hasClipboardPreview && root.previewAnimated
|
||||||
|
}
|
||||||
|
|
||||||
|
ClipboardLauncherPreview {
|
||||||
|
anchors.fill: parent
|
||||||
|
entry: root.item?.data ?? null
|
||||||
|
visible: root.hasClipboardPreview
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isImageFile(path) {
|
||||||
|
if (!path)
|
||||||
|
return false;
|
||||||
|
const ext = path.split(".").pop().toLowerCase();
|
||||||
|
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "jxl", "avif", "heif", "exr"].indexOf(ext) >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var controller: null
|
||||||
|
property bool hasQuery: false
|
||||||
|
property var rows: []
|
||||||
|
|
||||||
|
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
|
||||||
|
|
||||||
|
function resetScroll() {
|
||||||
|
mainListView.contentY = mainListView.originY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureVisible(flatIndex) {
|
||||||
|
if (!controller || flatIndex < 0)
|
||||||
|
return;
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if ((rows[i]?.flatIndex ?? -1) === flatIndex) {
|
||||||
|
mainListView.positionViewAtIndex(i, ListView.Contain);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedItemPosition() {
|
||||||
|
const fallback = mapToItem(null, width / 2, Math.min(height / 2, 56));
|
||||||
|
if (!controller || controller.selectedFlatIndex < 0)
|
||||||
|
return fallback;
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
if ((rows[i]?.flatIndex ?? -1) === controller.selectedFlatIndex) {
|
||||||
|
const rowY = i * mainListView.rowHeight - mainListView.contentY + mainListView.originY;
|
||||||
|
return mapToItem(null, width / 2, Math.max(28, Math.min(height - 28, rowY + mainListView.rowHeight / 2)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: root.controller
|
||||||
|
function onSelectedFlatIndexChanged() {
|
||||||
|
if (root.controller?.keyboardNavigationActive)
|
||||||
|
Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankListView {
|
||||||
|
id: mainListView
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
visible: root.rows.length > 0
|
||||||
|
|
||||||
|
readonly property int rowHeight: 64
|
||||||
|
|
||||||
|
model: ScriptModel {
|
||||||
|
values: root.rows
|
||||||
|
objectProp: "_rowId"
|
||||||
|
}
|
||||||
|
|
||||||
|
add: null
|
||||||
|
remove: null
|
||||||
|
displaced: null
|
||||||
|
move: null
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
id: delegateRoot
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: mainListView.width
|
||||||
|
height: mainListView.rowHeight
|
||||||
|
|
||||||
|
SpotlightResultRow {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.topMargin: 3
|
||||||
|
anchors.bottomMargin: 3
|
||||||
|
item: delegateRoot.modelData?.item ?? null
|
||||||
|
sectionTitle: delegateRoot.modelData?.sectionTitle ?? ""
|
||||||
|
sectionIcon: delegateRoot.modelData?.sectionIcon ?? ""
|
||||||
|
flatIndex: delegateRoot.modelData?.flatIndex ?? -1
|
||||||
|
controller: root.controller
|
||||||
|
isSelected: (delegateRoot.modelData?.flatIndex ?? -1) === root.controller?.selectedFlatIndex
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
if (root.controller && delegateRoot.modelData?.item)
|
||||||
|
root.controller.executeItem(delegateRoot.modelData.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
onRightClicked: (mouseX, mouseY) => {
|
||||||
|
root.itemRightClicked(delegateRoot.modelData?.flatIndex ?? -1, delegateRoot.modelData?.item ?? null, mouseX, mouseY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.hasQuery && root.rows.length === 0
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: Theme.surfaceContainerHigh
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: root.controller?.isSearching || root.controller?.isFileSearching ? "search" : statusIcon()
|
||||||
|
size: 22
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
|
||||||
|
function statusIcon() {
|
||||||
|
const mode = root.controller?.searchMode ?? "all";
|
||||||
|
if (mode === "files")
|
||||||
|
return "folder_open";
|
||||||
|
if (mode === "plugins")
|
||||||
|
return "extension";
|
||||||
|
if (mode === "apps")
|
||||||
|
return "apps";
|
||||||
|
return "search_off";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: Math.min(420, root.width - 88)
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: statusTitle()
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
|
||||||
|
function statusTitle() {
|
||||||
|
if (root.controller?.isSearching || root.controller?.isFileSearching)
|
||||||
|
return I18n.tr("Searching");
|
||||||
|
if ((root.controller?.searchMode ?? "") === "files" && !DSearchService.dsearchAvailable)
|
||||||
|
return I18n.tr("File search unavailable");
|
||||||
|
if ((root.controller?.searchMode ?? "") === "files" && (root.controller?.searchQuery?.length ?? 0) < 2)
|
||||||
|
return I18n.tr("Keep typing");
|
||||||
|
return I18n.tr("No results");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: statusSubtitle()
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
maximumLineCount: 2
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
elide: Text.ElideRight
|
||||||
|
|
||||||
|
function statusSubtitle() {
|
||||||
|
if ((root.controller?.searchMode ?? "") === "files" && !DSearchService.dsearchAvailable)
|
||||||
|
return I18n.tr("Install dsearch to search files.");
|
||||||
|
if ((root.controller?.searchMode ?? "") === "files" && (root.controller?.searchQuery?.length ?? 0) < 2)
|
||||||
|
return I18n.tr("Type at least 2 characters to search files.");
|
||||||
|
return I18n.tr("Try a different search or switch filters.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -205,7 +205,9 @@ FocusScope {
|
|||||||
visible: active
|
visible: active
|
||||||
focus: active
|
focus: active
|
||||||
|
|
||||||
sourceComponent: LauncherTab {}
|
sourceComponent: LauncherTab {
|
||||||
|
parentModal: root.parentModal
|
||||||
|
}
|
||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active && item)
|
if (active && item)
|
||||||
|
|||||||
@@ -308,6 +308,15 @@ Item {
|
|||||||
onToggled: checked => SettingsData.set("frameCloseGaps", !checked)
|
onToggled: checked => SettingsData.set("frameCloseGaps", !checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "frameUseSpotlightLauncher"
|
||||||
|
tags: ["frame", "connected", "launcher", "spotlight", "search", "minimal"]
|
||||||
|
text: I18n.tr("Use Spotlight Launcher")
|
||||||
|
description: I18n.tr("Use the centered minimal launcher instead of the connected V2 launcher")
|
||||||
|
checked: SettingsData.frameUseSpotlightLauncher
|
||||||
|
onToggled: checked => SettingsData.set("frameUseSpotlightLauncher", checked)
|
||||||
|
}
|
||||||
|
|
||||||
SettingsButtonGroupRow {
|
SettingsButtonGroupRow {
|
||||||
settingKey: "frameLauncherEmergeSide"
|
settingKey: "frameLauncherEmergeSide"
|
||||||
tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"]
|
tags: ["frame", "connected", "launcher", "modal", "emerge", "direction", "bottom", "top"]
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import qs.Modules.Settings.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
property var parentModal: null
|
||||||
|
|
||||||
FileBrowserModal {
|
FileBrowserModal {
|
||||||
id: logoFileBrowser
|
id: logoFileBrowser
|
||||||
browserTitle: I18n.tr("Select Launcher Logo")
|
browserTitle: I18n.tr("Select Launcher Logo")
|
||||||
@@ -30,6 +32,43 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: Theme.spacingXL
|
spacing: Theme.spacingXL
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "search"
|
||||||
|
title: I18n.tr("Launcher Style")
|
||||||
|
settingKey: "launcherStyle"
|
||||||
|
|
||||||
|
SettingsControlledByFrame {
|
||||||
|
visible: SettingsData.connectedFrameModeActive
|
||||||
|
parentModal: root.parentModal
|
||||||
|
settingLabel: I18n.tr("Launcher Style")
|
||||||
|
reason: I18n.tr("Managed by Frame Mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
visible: !SettingsData.connectedFrameModeActive
|
||||||
|
text: SettingsData.launcherStyle === "spotlight" ? I18n.tr("Minimal Spotlight-style bar: appears instantly at the top of the screen and expands as you type.") : I18n.tr("Full-featured launcher with mode tabs, grid view, and action panel.")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsButtonGroupRow {
|
||||||
|
visible: !SettingsData.connectedFrameModeActive
|
||||||
|
settingKey: "launcherStyleSelector"
|
||||||
|
tags: ["launcher", "style", "spotlight", "full", "minimal"]
|
||||||
|
text: I18n.tr("Style")
|
||||||
|
model: [I18n.tr("Full"), I18n.tr("Spotlight")]
|
||||||
|
currentIndex: SettingsData.launcherStyle === "spotlight" ? 1 : 0
|
||||||
|
onSelectionChanged: (index, selected) => {
|
||||||
|
if (!selected)
|
||||||
|
return;
|
||||||
|
SettingsData.set("launcherStyle", index === 1 ? "spotlight" : "full");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "apps"
|
iconName: "apps"
|
||||||
@@ -526,7 +565,7 @@ Item {
|
|||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Repeater {
|
Repeater {
|
||||||
model: ["dms_settings", "dms_notepad", "dms_sysmon", "dms_settings_search"]
|
model: ["dms_settings", "dms_notepad", "dms_sysmon", "dms_settings_search", "dms_clipboard_search"]
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: Rectangle {
|
||||||
id: pluginDelegate
|
id: pluginDelegate
|
||||||
|
|||||||
@@ -211,11 +211,21 @@ Singleton {
|
|||||||
},
|
},
|
||||||
"dms_settings_search": {
|
"dms_settings_search": {
|
||||||
id: "dms_settings_search",
|
id: "dms_settings_search",
|
||||||
name: I18n.tr("Settings", "settings window title"),
|
name: I18n.tr("Settings Search"),
|
||||||
cornerIcon: "search",
|
cornerIcon: "search",
|
||||||
comment: "DMS",
|
comment: I18n.tr("DMS Settings"),
|
||||||
defaultTrigger: "?",
|
defaultTrigger: "?",
|
||||||
isLauncher: true
|
isLauncher: true
|
||||||
|
},
|
||||||
|
"dms_clipboard_search": {
|
||||||
|
id: "dms_clipboard_search",
|
||||||
|
name: I18n.tr("Clipboard"),
|
||||||
|
cornerIcon: "content_paste",
|
||||||
|
comment: "DMS",
|
||||||
|
defaultTrigger: "cb",
|
||||||
|
isLauncher: true,
|
||||||
|
viewMode: "list",
|
||||||
|
viewModeEnforced: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -285,6 +295,16 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getBuiltInLauncherItems(pluginId, query) {
|
function getBuiltInLauncherItems(pluginId, query) {
|
||||||
|
if (pluginId === "dms_clipboard_search") {
|
||||||
|
ClipboardService.ensureLauncherHistory();
|
||||||
|
const trimmed = (query || "").toString().trim();
|
||||||
|
const entries = trimmed.length === 0 ? ClipboardService.getRecentLauncherEntries(20) : ClipboardService.getLauncherEntries(trimmed, 20, 1);
|
||||||
|
return entries.map(entry => ({
|
||||||
|
type: "clipboard",
|
||||||
|
data: entry
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (pluginId !== "dms_settings_search")
|
if (pluginId !== "dms_settings_search")
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
@@ -295,10 +315,15 @@ Singleton {
|
|||||||
const r = results[i];
|
const r = results[i];
|
||||||
items.push({
|
items.push({
|
||||||
name: r.label,
|
name: r.label,
|
||||||
|
type: "setting",
|
||||||
|
section: "settings",
|
||||||
icon: "material:" + r.icon,
|
icon: "material:" + r.icon,
|
||||||
comment: r.category,
|
comment: r.description || r.category,
|
||||||
action: "settings_nav:" + r.tabIndex + ":" + r.section,
|
action: "settings_nav:" + r.tabIndex + ":" + r.section,
|
||||||
categories: ["Settings"],
|
categories: ["Settings"],
|
||||||
|
keywords: r.keywords || [],
|
||||||
|
source: I18n.tr("Settings", "settings window title"),
|
||||||
|
badgeLabel: I18n.tr("Setting"),
|
||||||
isCore: true,
|
isCore: true,
|
||||||
isBuiltInLauncher: true,
|
isBuiltInLauncher: true,
|
||||||
builtInPluginId: pluginId
|
builtInPluginId: pluginId
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ Singleton {
|
|||||||
property int selectedIndex: 0
|
property int selectedIndex: 0
|
||||||
property bool keyboardNavigationActive: false
|
property bool keyboardNavigationActive: false
|
||||||
property int refCount: 0
|
property int refCount: 0
|
||||||
|
property real _launcherLastRefresh: 0
|
||||||
|
|
||||||
signal historyCopied
|
signal historyCopied
|
||||||
signal historyCleared
|
signal historyCleared
|
||||||
@@ -90,6 +91,68 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureLauncherHistory() {
|
||||||
|
if (!clipboardAvailable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (internalEntries.length === 0 || now - _launcherLastRefresh > 5000) {
|
||||||
|
_launcherLastRefresh = now;
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLauncherEntries(query, limit, minLength) {
|
||||||
|
if (!clipboardAvailable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = (query || "").toString().trim();
|
||||||
|
const requiredLength = minLength !== undefined ? minLength : 2;
|
||||||
|
if (trimmed.length < requiredLength) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQuery = trimmed.toLowerCase();
|
||||||
|
const maxItems = limit > 0 ? limit : 8;
|
||||||
|
const matches = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < internalEntries.length; i++) {
|
||||||
|
const entry = internalEntries[i];
|
||||||
|
const preview = getEntryPreview(entry).toString();
|
||||||
|
const typeText = entry.isImage ? "image picture screenshot clipboard" : "text clipboard";
|
||||||
|
const haystack = (preview + " " + typeText).toLowerCase();
|
||||||
|
if (haystack.indexOf(lowerQuery) === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
matches.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
matches.sort((a, b) => {
|
||||||
|
if (a.pinned !== b.pinned)
|
||||||
|
return b.pinned ? 1 : -1;
|
||||||
|
return (b.id || 0) - (a.id || 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
return matches.slice(0, maxItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecentLauncherEntries(limit) {
|
||||||
|
if (!clipboardAvailable) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxItems = limit > 0 ? limit : 20;
|
||||||
|
const entries = internalEntries.slice();
|
||||||
|
entries.sort((a, b) => {
|
||||||
|
if (a.pinned !== b.pinned)
|
||||||
|
return b.pinned ? 1 : -1;
|
||||||
|
return (b.id || 0) - (a.id || 0);
|
||||||
|
});
|
||||||
|
return entries.slice(0, maxItems);
|
||||||
|
}
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
searchText = "";
|
searchText = "";
|
||||||
selectedIndex = 0;
|
selectedIndex = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user