Files
DankMaterialShell/quickshell/Modules/Settings/LauncherTab.qml
T
Marlon Kranzandpurian23 6d331b8fde fix(settings): show what frame mode controls (#3654)
* fix(settings): show what frame mode controls

With frame mode on, many settings did nothing or were hidden while
still applying, and nothing said so. The frame takes its color and
opacity from the first non-island bar, but that bar's Surface card is
hidden while the frame styles it, so neither had a visible control.

Add Background and Override/Opacity rows to the Appearance Frame card,
bound to the same frame bar the renderer reads. Settings the frame
provides in connected mode get a "Managed by Frame in Connected Mode"
note (Interface style border, corner style, background blur and dock
opacity, shadow targets, launcher border); settings with no effect get
"Disabled by Frame Mode" (maximize detection, bar shadow, the bar's
overlay layer in separate mode). Bar size, the bar's overlay layer in
connected mode, dock Over fullscreen and the controls of overlay-layer
docks and launchers are shown again, since they still apply.
Notification Over fullscreen shows as forced on when the frame covers
every display, and launcher rows the Spotlight style ignores are
disabled.

Also relabel the frame Width row as Thickness, raise the inset padding
maximum to its automatic range, and drop a FrameWindow handler for the
removed frameOpacity signal. No new strings.

* settings: show what frame mode controls, fix review findings

---------

Co-authored-by: purian23 <[email protected]>
2026-10-10 00:37:54 -04:00

869 lines
38 KiB
QML

import QtQuick
import qs.Common
import qs.Services
import qs.DCommon.Widgets
import qs.Modules.Settings.Widgets
Item {
id: root
property var parentModal: null
readonly property string defaultLauncherAction: "spawn dms ipc call spotlight toggle"
readonly property string spotlightBarAction: "spawn dms ipc call spotlight-bar toggle"
readonly property int keybindDataVersion: KeybindsService._dataVersion
readonly property bool keybindsAvailable: KeybindsService.available
readonly property string defaultLauncherKeybindSearch: "spotlight toggle"
readonly property string spotlightBarKeybindSearch: "spotlight-bar"
readonly property bool spotlightIgnoresRows: SettingsData.launcherStyle === "spotlight" && !SettingsData.connectedFrameModeActive
// The niri overview launcher still uses size and footer when it is set to the full style.
readonly property bool spotlightIgnoresSize: spotlightIgnoresRows && !(CompositorService.isNiri && SettingsData.niriOverviewOverlayEnabled && SettingsData.niriOverviewLauncherStyle !== "spotlight")
readonly property var builtInPluginIds: ["dms_settings", "dms_notepad", "dms_sysmon", "dms_colorpicker", "dms_settings_search", "dms_clipboard_search", "dms_power", "dms_vpn", "dms_qr_generator"]
function openKeybindsSearch(query) {
if (!root.parentModal)
return;
if (typeof root.parentModal.showKeybindsSearch === "function") {
root.parentModal.showKeybindsSearch(query);
} else {
root.parentModal.navigateTo("keybinds");
}
}
function keysLabel(actionId) {
void (keybindDataVersion);
if (!keybindsAvailable)
return I18n.tr("Manual config");
const keys = KeybindsService.keysForAction(actionId);
if (!keys || keys.length === 0)
return I18n.tr("Not bound");
return keys.join(", ");
}
function lastLaunchedText(lastUsed) {
if (!lastUsed)
return I18n.tr("Never used");
const date = new Date(lastUsed);
const diffMs = Date.now() - date.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMins < 1)
return I18n.tr("Last launched just now");
if (diffMins < 60)
return diffMins === 1 ? I18n.tr("Last launched %1 minute ago", "singular, app usage list in launcher settings, %1 is 1").arg(diffMins) : I18n.tr("Last launched %1 minutes ago", "plural, app usage list in launcher settings, %1 is a count").arg(diffMins);
if (diffHours < 24)
return diffHours === 1 ? I18n.tr("Last launched %1 hour ago", "singular, app usage list in launcher settings, %1 is 1").arg(diffHours) : I18n.tr("Last launched %1 hours ago", "plural, app usage list in launcher settings, %1 is a count").arg(diffHours);
if (diffDays < 7)
return diffDays === 1 ? I18n.tr("Last launched %1 day ago", "singular, app usage list in launcher settings, %1 is 1").arg(diffDays) : I18n.tr("Last launched %1 days ago", "plural, app usage list in launcher settings, %1 is a count").arg(diffDays);
return I18n.tr("Last launched %1", "app usage list in launcher settings, %1 is a localized date").arg(date.toLocaleDateString());
}
Component.onCompleted: {
KeybindsService.addRef();
if (KeybindsService.available)
KeybindsService.loadBinds(false);
}
Component.onDestruction: KeybindsService.removeRef()
SettingsPage {
id: mainColumn
SettingsCard {
width: parent.width
iconName: "search"
title: I18n.tr("Default action")
settingKey: "launcherStyle"
SettingsButtonGroupRow {
readonly property bool islandOffered: SettingsData.dankIslandEnabled || SettingsData.launcherStyle === "island"
readonly property bool spotlightOffered: !SettingsData.connectedFrameModeActive
settingKey: "launcherStyleSelector"
tags: ["launcher", "style", "default", "spotlight", "full", "minimal", "island", "dankisland"]
resetKeys: ["launcherStyle"]
text: I18n.tr("Opens", "verb, row label, which launcher style the shortcut opens")
model: [I18n.tr("Full", "adjective, full size launcher style option")].concat(spotlightOffered ? [I18n.tr("Spotlight", "launcher style option, small centered search bar")] : []).concat(islandOffered ? [I18n.tr("Island")] : [])
values: ["full"].concat(spotlightOffered ? ["spotlight"] : []).concat(islandOffered ? ["island"] : [])
value: SettingsData.launcherStyle
fallbackValue: "full"
onValueSelected: value => SettingsData.set("launcherStyle", value)
}
SettingsRow {
iconName: "keyboard"
title: I18n.tr("Shortcut", "noun, keyboard shortcut row in launcher settings")
subtitle: root.keybindsAvailable ? "" : I18n.tr("Bind the %1 IPC action in your compositor config.", "launcher shortcut hint, %1 is the ipc target name").arg("spotlight")
trailingBadge: root.keysLabel(root.defaultLauncherAction)
trailingBadgeColor: Theme.primary
showChevron: root.keybindsAvailable
clickable: root.keybindsAvailable
onClicked: root.openKeybindsSearch(root.defaultLauncherKeybindSearch)
}
}
SettingsCard {
width: parent.width
iconName: "overview_key"
title: I18n.tr("Overview")
settingKey: "launcherNiriOverview"
tags: ["launcher", "niri", "overview", "overlay"]
visible: CompositorService.isNiri
SettingsToggleRow {
settingKey: "niriOverviewOverlayEnabled"
tags: ["launcher", "niri", "overview", "overlay", "enable"]
text: I18n.tr("Overlay")
description: I18n.tr("Typing in the overview opens the launcher")
checked: SettingsData.niriOverviewOverlayEnabled
onToggled: checked => SettingsData.set("niriOverviewOverlayEnabled", checked)
}
SettingsButtonGroupRow {
visible: SettingsData.niriOverviewOverlayEnabled
settingKey: "niriOverviewLauncherStyle"
tags: ["launcher", "niri", "overview", "overlay", "style", "spotlight", "full"]
text: I18n.tr("Opens")
model: [I18n.tr("Full"), I18n.tr("Spotlight")]
currentIndex: SettingsData.niriOverviewLauncherStyle === "spotlight" ? 1 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("niriOverviewLauncherStyle", index === 1 ? "spotlight" : "full");
}
}
}
SettingsCard {
width: parent.width
iconName: "tune"
title: I18n.tr("Appearance")
settingKey: "dankLauncherV2Appearance"
tags: ["launcher", "size", "grid", "columns", "footer", "badges", "darken"]
SettingsButtonGroupRow {
readonly property var sizes: ["micro", "compact", "medium", "large"]
enabled: !root.spotlightIgnoresSize
settingKey: "dankLauncherV2Size"
tags: ["launcher", "size", "micro", "compact", "medium", "large"]
text: I18n.tr("Size")
model: ["1", "2", "3", "4"]
currentIndex: {
const index = sizes.indexOf(SettingsData.dankLauncherV2Size);
return index >= 0 ? index : 2;
}
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("dankLauncherV2Size", sizes[index]);
}
}
SettingsSliderRow {
settingKey: "appLauncherGridColumns"
tags: ["launcher", "grid", "columns", "layout"]
text: I18n.tr("Grid columns")
unit: ""
minimum: 2
maximum: 8
value: SettingsData.appLauncherGridColumns
onSliderValueChanged: newValue => SettingsData.set("appLauncherGridColumns", newValue)
}
SettingsToggleRow {
settingKey: "dankLauncherV2ShowFooter"
tags: ["launcher", "footer", "hints", "shortcuts", "modes", "filters"]
text: I18n.tr("Show footer")
checked: SettingsData.dankLauncherV2ShowFooter
enabled: SettingsData.dankLauncherV2Size !== "micro" && !root.spotlightIgnoresSize
onToggled: checked => SettingsData.set("dankLauncherV2ShowFooter", checked)
}
SettingsToggleRow {
settingKey: "dankLauncherV2ShowSourceBadges"
tags: ["launcher", "appearance", "badge", "source", "flatpak", "snap", "appimage", "nix"]
text: I18n.tr("Show package source badges")
checked: SettingsData.dankLauncherV2ShowSourceBadges
onToggled: checked => SettingsData.set("dankLauncherV2ShowSourceBadges", checked)
}
SettingsControlledBy {
visible: SettingsData.frameEnabled
parentModal: root.parentModal
section: "frameBorder"
settingLabel: I18n.tr("Darken modal background")
reason: I18n.tr("Disabled by Frame Mode")
}
SettingsToggleRow {
enabled: !SettingsData.frameEnabled
settingKey: "modalDarkenBackground"
tags: ["modal", "darken", "background", "overlay", "launcher"]
text: I18n.tr("Darken modal background")
checked: SettingsData.modalDarkenBackground
onToggled: checked => SettingsData.set("modalDarkenBackground", checked)
}
}
SettingsControlledBy {
visible: SettingsData.connectedFrameModeActive && !SettingsData.launcherUseOverlayLayer
parentModal: root.parentModal
section: "frameBorder"
settingLabel: I18n.tr("Border")
reason: I18n.tr("Managed by Frame in Connected Mode")
}
SettingsToggleCard {
width: parent.width
visible: !SettingsData.connectedFrameModeActive || SettingsData.launcherUseOverlayLayer
enabled: !root.spotlightIgnoresRows
iconName: "border_style"
title: I18n.tr("Border")
settingKey: "dankLauncherV2BorderEnabled"
tags: ["launcher", "border", "outline"]
checked: SettingsData.dankLauncherV2BorderEnabled
onToggled: checked => SettingsData.set("dankLauncherV2BorderEnabled", checked)
SettingsSliderRow {
settingKey: "dankLauncherV2BorderThickness"
tags: ["launcher", "border", "thickness"]
text: I18n.tr("Thickness")
minimum: 1
maximum: 6
value: SettingsData.dankLauncherV2BorderThickness
unit: "px"
onSliderValueChanged: newValue => SettingsData.set("dankLauncherV2BorderThickness", newValue)
}
SettingsButtonGroupRow {
readonly property var colors: ["primary", "secondary", "outline", "surfaceText"]
settingKey: "dankLauncherV2BorderColor"
tags: ["launcher", "border", "color"]
text: I18n.tr("Color")
model: [I18n.tr("Primary", "primary color"), I18n.tr("Secondary", "secondary color"), I18n.tr("Outline", "outline color"), I18n.tr("Text", "text color")]
currentIndex: Math.max(0, colors.indexOf(SettingsData.dankLauncherV2BorderColor))
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("dankLauncherV2BorderColor", colors[index]);
}
}
}
SettingsCard {
width: parent.width
iconName: "search"
title: I18n.tr("Search", "launcher settings section title, noun")
settingKey: "searchOptions"
SettingsToggleRow {
settingKey: "searchAppActions"
tags: ["launcher", "search", "actions", "shortcuts", "desktop"]
text: I18n.tr("App actions")
description: I18n.tr("Search also matches actions apps provide, like New Window", "launcher app actions search toggle description")
checked: SessionData.searchAppActions
onToggled: checked => SessionData.setSearchAppActions(checked)
}
SettingsToggleRow {
settingKey: "rememberLastMode"
tags: ["launcher", "remember", "last", "mode", "tab"]
text: I18n.tr("Remember last mode")
checked: SettingsData.rememberLastMode
onToggled: checked => SettingsData.set("rememberLastMode", checked)
}
SettingsToggleRow {
settingKey: "launcherHistoryEnabled"
tags: ["launcher", "history", "privacy", "usage", "recent", "persist", "save"]
text: I18n.tr("History")
description: I18n.tr("Record app usage and past queries. Turning this off clears what is stored.", "launcher history toggle description")
checked: SettingsData.launcherHistoryEnabled
onToggled: checked => SettingsData.set("launcherHistoryEnabled", checked)
}
SettingsToggleRow {
settingKey: "rememberLastQuery"
tags: ["launcher", "remember", "last", "search", "query"]
text: I18n.tr("Remember last query")
checked: SettingsData.rememberLastQuery
onToggled: checked => SettingsData.set("rememberLastQuery", checked)
}
SettingsToggleRow {
settingKey: "sortAppsAlphabetically"
tags: ["launcher", "sort", "alphabetically", "apps", "order", "usage", "frequency"]
text: I18n.tr("Sort alphabetically")
checked: SettingsData.sortAppsAlphabetically
onToggled: checked => SettingsData.set("sortAppsAlphabetically", checked)
}
SettingsToggleRow {
settingKey: "dankLauncherV2IncludeFilesInAll"
tags: ["launcher", "files", "dsearch", "all", "results", "indexed"]
text: I18n.tr("Include files in All tab")
checked: SettingsData.dankLauncherV2IncludeFilesInAll
onToggled: checked => SettingsData.set("dankLauncherV2IncludeFilesInAll", checked)
}
SettingsToggleRow {
settingKey: "dankLauncherV2IncludeFoldersInAll"
tags: ["launcher", "folders", "dirs", "dsearch", "all", "results", "indexed"]
text: I18n.tr("Include folders in All tab")
checked: SettingsData.dankLauncherV2IncludeFoldersInAll
onToggled: checked => SettingsData.set("dankLauncherV2IncludeFoldersInAll", checked)
}
}
SettingsCard {
id: hiddenAppsCard
width: parent.width
iconName: "visibility_off"
title: I18n.tr("Hidden apps")
settingKey: "hiddenApps"
property var hiddenAppsModel: {
SessionData.hiddenApps;
const apps = [];
const allApps = AppSearchService.applications || [];
for (const hiddenId of SessionData.hiddenApps) {
const app = allApps.find(a => (a.id || a.execString || a.exec) === hiddenId);
if (app) {
apps.push({
id: hiddenId,
name: app.name || hiddenId,
icon: app.icon || "",
comment: app.comment || ""
});
} else {
apps.push({
id: hiddenId,
name: hiddenId,
icon: "",
comment: ""
});
}
}
return apps.sort((a, b) => a.name.localeCompare(b.name));
}
SettingsRow {
body: StyledText {
width: parent.width
text: I18n.tr("Right-click an app in the launcher and choose 'Hide App'")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
Repeater {
model: hiddenAppsCard.hiddenAppsModel
SettingsRow {
id: hiddenAppRow
required property var modelData
title: modelData.name
subtitle: modelData.comment || modelData.id
leading: AppIconRenderer {
width: Theme.iconSize
height: Theme.iconSize
iconValue: hiddenAppRow.modelData.icon || "application-x-executable"
iconSize: Theme.iconSize
fallbackText: (hiddenAppRow.modelData.name || "?").charAt(0).toUpperCase()
}
DActionButton {
iconName: "visibility"
Accessible.name: I18n.tr("Show")
iconColor: Theme.primary
onClicked: SessionData.showApp(hiddenAppRow.modelData.id)
}
}
}
SettingsRow {
visible: hiddenAppsCard.hiddenAppsModel.length === 0
body: StyledText {
width: parent.width
text: I18n.tr("No hidden apps.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
}
}
}
SettingsCard {
id: appOverridesCard
width: parent.width
iconName: "edit"
title: I18n.tr("App customizations")
settingKey: "appOverrides"
property var overridesModel: {
SessionData.appOverrides;
const items = [];
const allApps = AppSearchService.applications || [];
for (const appId in SessionData.appOverrides) {
const override = SessionData.appOverrides[appId];
const app = allApps.find(a => (a.id || a.execString || a.exec) === appId);
items.push({
id: appId,
name: override.name || app?.name || appId,
originalName: app?.name || appId,
icon: override.icon || app?.icon || "",
hasOverride: true
});
}
return items.sort((a, b) => a.name.localeCompare(b.name));
}
SettingsRow {
body: StyledText {
width: parent.width
text: I18n.tr("Right-click an app in the launcher and choose 'Edit App'")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
Repeater {
model: appOverridesCard.overridesModel
SettingsRow {
id: overrideRow
required property var modelData
title: modelData.name
subtitle: modelData.originalName !== modelData.name ? modelData.originalName : modelData.id
leading: AppIconRenderer {
width: Theme.iconSize
height: Theme.iconSize
iconValue: overrideRow.modelData.icon || "application-x-executable"
iconSize: Theme.iconSize
fallbackText: (overrideRow.modelData.name || "?").charAt(0).toUpperCase()
}
DActionButton {
iconName: "delete"
tooltipText: I18n.tr("Reset to default")
iconColor: Theme.error
onClicked: SessionData.clearAppOverride(overrideRow.modelData.id)
}
}
}
SettingsRow {
visible: appOverridesCard.overridesModel.length === 0
body: StyledText {
width: parent.width
text: I18n.tr("No app customizations.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
}
}
}
SettingsCard {
width: parent.width
iconName: "apps"
title: I18n.tr("Built-in apps", "launcher settings card, DMS tools listed with the installed apps")
settingKey: "launcherBuiltInApps"
tags: ["launcher", "apps", "builtin", "settings", "notepad", "system monitor", "color picker"]
Repeater {
model: root.builtInPluginIds.filter(id => AppSearchService.builtInPlugins[id]?.isLauncher !== true)
SettingsToggleRow {
required property string modelData
readonly property var plugin: AppSearchService.builtInPlugins[modelData]
iconName: plugin?.cornerIcon ?? "extension"
text: plugin?.name ?? modelData
checked: SettingsData.getBuiltInPluginSetting(modelData, "enabled", true)
onToggled: checked => SettingsData.setBuiltInPluginSetting(modelData, "enabled", checked)
}
}
}
SettingsCard {
id: pluginVisibilityCard
width: parent.width
iconName: "filter_list"
title: I18n.tr("Plugin visibility")
settingKey: "pluginVisibility"
property var allLauncherPlugins: {
SettingsData.launcherPluginOrder;
SettingsData.dankLauncherV2IncludeFilesInAll;
SettingsData.dankLauncherV2IncludeFoldersInAll;
var plugins = [];
var builtIn = AppSearchService.getBuiltInLauncherPlugins() || {};
for (var pluginId in builtIn) {
var plugin = builtIn[pluginId];
plugins.push({
id: pluginId,
name: plugin.name || pluginId,
icon: plugin.cornerIcon || "extension",
iconType: "material",
isBuiltIn: true,
isVirtual: false,
trigger: AppSearchService.getBuiltInPluginTrigger(pluginId) || ""
});
}
var thirdParty = PluginService.getLauncherPlugins() || {};
for (var pluginId in thirdParty) {
var plugin = thirdParty[pluginId];
var rawIcon = plugin.icon || "extension";
plugins.push({
id: pluginId,
name: plugin.name || pluginId,
icon: rawIcon.startsWith("material:") ? rawIcon.substring(9) : rawIcon.startsWith("unicode:") ? rawIcon.substring(8) : rawIcon,
iconType: rawIcon.startsWith("unicode:") ? "unicode" : "material",
isBuiltIn: false,
isVirtual: false,
trigger: PluginService.getPluginTrigger(pluginId) || ""
});
}
if (SettingsData.dankLauncherV2IncludeFilesInAll) {
plugins.push({
id: "__files",
name: I18n.tr("Files"),
icon: "insert_drive_file",
iconType: "material",
isBuiltIn: false,
isVirtual: true,
trigger: "/"
});
}
if (SettingsData.dankLauncherV2IncludeFoldersInAll) {
plugins.push({
id: "__folders",
name: I18n.tr("Folders"),
icon: "folder",
iconType: "material",
isBuiltIn: false,
isVirtual: true,
trigger: "/"
});
}
return SettingsData.getOrderedLauncherPlugins(plugins);
}
SettingsRow {
body: StyledText {
width: parent.width
text: I18n.tr("Plugins shown in All mode without a trigger. Drag to reorder")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
}
SettingsReorderList {
id: pluginList
model: pluginVisibilityCard.allLauncherPlugins
onReordered: indices => SettingsData.setLauncherPluginOrder(indices.map(i => pluginVisibilityCard.allLauncherPlugins[i].id))
delegate: SettingsReorderRow {
id: pluginRow
required property var modelData
reorderList: pluginList
title: modelData.name
subtitle: modelData.trigger ? I18n.tr("Trigger: %1", "launcher plugin subtitle, %1 is the trigger prefix text").arg(modelData.trigger) : I18n.tr("No trigger")
iconName: modelData.iconType !== "unicode" ? modelData.icon : ""
textIcon: modelData.iconType === "unicode" ? modelData.icon : ""
DBadge {
anchors.verticalCenter: parent.verticalCenter
visible: pluginRow.modelData.isBuiltIn
text: I18n.tr("Built-in", "badge on launcher plugins that ship with DMS")
color: Theme.primaryHover
textColor: Theme.primary
}
DToggle {
anchors.verticalCenter: parent.verticalCenter
hideText: true
checked: {
switch (pluginRow.modelData.id) {
case "__files":
return SettingsData.dankLauncherV2IncludeFilesInAll;
case "__folders":
return SettingsData.dankLauncherV2IncludeFoldersInAll;
default:
return SettingsData.getPluginAllowWithoutTrigger(pluginRow.modelData.id);
}
}
onToggled: isChecked => {
switch (pluginRow.modelData.id) {
case "__files":
SettingsData.set("dankLauncherV2IncludeFilesInAll", isChecked);
return;
case "__folders":
SettingsData.set("dankLauncherV2IncludeFoldersInAll", isChecked);
return;
default:
SettingsData.setPluginAllowWithoutTrigger(pluginRow.modelData.id, isChecked);
}
}
}
}
}
SettingsRow {
visible: pluginVisibilityCard.allLauncherPlugins.length === 0
body: StyledText {
width: parent.width
text: I18n.tr("No launcher plugins installed.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
}
}
}
SettingsCard {
width: parent.width
iconName: "extension"
title: I18n.tr("Built-in plugins")
settingKey: "launcherBuiltInPlugins"
tags: ["launcher", "plugins", "trigger", "builtin", "power", "vpn", "qr", "clipboard", "search"]
Repeater {
model: root.builtInPluginIds.filter(id => AppSearchService.builtInPlugins[id]?.isLauncher === true)
SettingsRow {
id: builtInRow
required property string modelData
readonly property var plugin: AppSearchService.builtInPlugins[modelData]
iconName: plugin?.cornerIcon ?? "extension"
title: plugin?.name ?? modelData
DTextField {
outlined: true
leftIconName: "keyboard"
labelText: I18n.tr("Trigger", "noun, launcher plugin trigger prefix text field label")
width: Theme.fontSizeMedium * 6 + Theme.iconButtonSize
anchors.verticalCenter: parent.verticalCenter
onTextEdited: SettingsData.setBuiltInPluginSetting(builtInRow.modelData, "trigger", text)
Component.onCompleted: text = SettingsData.getBuiltInPluginSetting(builtInRow.modelData, "trigger", builtInRow.plugin?.defaultTrigger ?? "")
}
DToggle {
hideText: true
anchors.verticalCenter: parent.verticalCenter
checked: SettingsData.getBuiltInPluginSetting(builtInRow.modelData, "enabled", true)
onToggled: checked => SettingsData.setBuiltInPluginSetting(builtInRow.modelData, "enabled", checked)
}
}
}
}
SettingsCard {
width: parent.width
iconName: "horizontal_split"
title: I18n.tr("Spotlight bar")
settingKey: "spotlightBar"
tags: ["launcher", "spotlight", "bar", "shortcut", "chips"]
SettingsRow {
iconName: "keyboard"
title: I18n.tr("Shortcut")
subtitle: root.keybindsAvailable ? "" : I18n.tr("Bind the %1 IPC action in your compositor config.").arg("spotlight-bar")
trailingBadge: root.keysLabel(root.spotlightBarAction)
trailingBadgeColor: Theme.primary
showChevron: root.keybindsAvailable
clickable: root.keybindsAvailable
onClicked: root.openKeybindsSearch(root.spotlightBarKeybindSearch)
}
SettingsToggleRow {
settingKey: "spotlightBarShowModeChips"
tags: ["launcher", "spotlight", "bar", "chips", "tabs", "modes", "filters"]
text: I18n.tr("Show mode chips")
checked: SettingsData.spotlightBarShowModeChips
onToggled: checked => SettingsData.set("spotlightBarShowModeChips", checked)
}
}
SettingsCard {
width: parent.width
iconName: "rocket_launch"
title: I18n.tr("Launching", "launcher settings card about how apps are launched")
settingKey: "launcherLaunching"
tags: ["launcher", "prefix", "terminal", "uwsm"]
SettingsTextFieldRow {
settingKey: "launchPrefix"
tags: ["launcher", "prefix", "uwsm", "command", "launch"]
text: I18n.tr("Launch prefix")
description: I18n.tr("Added in front of every app launch command", "launcher launch prefix field description")
leftIconName: "terminal"
placeholderText: I18n.tr("Enter launch prefix (e.g., 'uwsm-app')")
value: SettingsData.launchPrefix
onValueEdited: value => SettingsData.set("launchPrefix", value)
}
TerminalPickerRow {}
}
SettingsCard {
id: recentAppsCard
width: parent.width
iconName: "history"
title: I18n.tr("Recently used apps")
settingKey: "recentApps"
visible: SettingsData.launcherHistoryEnabled
collapsible: true
expanded: false
property var rankedAppsModel: {
var ranking = AppUsageHistoryData.appUsageRanking;
if (!ranking)
return [];
var apps = [];
for (var appId in ranking) {
var appData = ranking[appId];
apps.push({
"id": appId,
"name": appData.name,
"exec": appData.exec,
"icon": appData.icon,
"comment": appData.comment,
"usageCount": appData.usageCount,
"lastUsed": appData.lastUsed
});
}
apps.sort(function (a, b) {
if (a.usageCount !== b.usageCount)
return b.usageCount - a.usageCount;
return a.name.localeCompare(b.name);
});
return apps.slice(0, 20);
}
SettingsRow {
body: Row {
width: parent.width
spacing: Theme.spacingM
StyledText {
width: parent.width - clearAllButton.width - Theme.spacingM
text: I18n.tr("Apps are ordered by usage frequency, then last used, then alphabetically.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
anchors.verticalCenter: parent.verticalCenter
}
DActionButton {
id: clearAllButton
iconName: "delete_sweep"
tooltipText: I18n.tr("Clear All")
iconSize: Theme.iconSizeMedium
iconColor: Theme.error
anchors.verticalCenter: parent.verticalCenter
onClicked: AppUsageHistoryData.clear()
}
}
}
Repeater {
model: recentAppsCard.rankedAppsModel
SettingsRow {
id: rankedAppRow
required property var modelData
required property int index
title: modelData.name || I18n.tr("Unknown App")
subtitle: root.lastLaunchedText(modelData.lastUsed)
leading: [
StyledText {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize
text: (rankedAppRow.index + 1).toString()
font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium
color: Theme.primary
},
AppIconRenderer {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize
height: Theme.iconSize
iconValue: rankedAppRow.modelData.icon || "application-x-executable"
iconSize: Theme.iconSize
fallbackText: (rankedAppRow.modelData.name || "?").charAt(0).toUpperCase()
}
]
DActionButton {
iconName: "close"
Accessible.name: I18n.tr("Remove")
iconColor: Theme.error
onClicked: {
const currentRanking = Object.assign({}, AppUsageHistoryData.appUsageRanking || {});
delete currentRanking[rankedAppRow.modelData.id];
AppUsageHistoryData.appUsageRanking = currentRanking;
AppUsageHistoryData.saveSettings();
}
}
}
}
SettingsRow {
visible: recentAppsCard.rankedAppsModel.length === 0
body: StyledText {
width: parent.width
text: I18n.tr("No apps have been launched yet.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
}
}
}
SettingsCard {
width: parent.width
title: I18n.tr("Advanced")
settingKey: "launcherAdvanced"
tags: ["launcher", "advanced", "overlay", "layer", "unload", "memory"]
collapsible: true
expanded: false
SettingsToggleRow {
settingKey: "launcherUseOverlayLayer"
tags: ["launcher", "fullscreen", "overlay", "layer"]
text: I18n.tr("Use overlay layer")
checked: SettingsData.launcherUseOverlayLayer
onToggled: checked => SettingsData.set("launcherUseOverlayLayer", checked)
}
SettingsToggleRow {
enabled: !root.spotlightIgnoresRows
settingKey: "dankLauncherV2UnloadOnClose"
tags: ["launcher", "unload", "close", "memory", "vram"]
text: I18n.tr("Unload on close")
description: I18n.tr("Frees the launcher when closed and rebuilds it on open", "launcher unload on close toggle description")
checked: SettingsData.dankLauncherV2UnloadOnClose
onToggled: checked => SettingsData.set("dankLauncherV2UnloadOnClose", checked)
}
}
}
}