1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-08 04:09:15 -04:00

feat: (Launcher/Spotlight): Updated w/New Settings & QOL features

- New Spotlight toggle to show/hide chips, off by default
- Updated blur effects on all launcher inputs and footers
- Fixed previous queries resurfacing
- Upated Spotlight keyboard navigation
- Added functionality to show and shortcut to keybinds from the Launcher tab
This commit is contained in:
purian23
2026-05-21 01:05:56 -04:00
parent 078c9b4890
commit fb9ec8e721
21 changed files with 867 additions and 328 deletions
+3 -3
View File
@@ -8,9 +8,9 @@ const ACTION_TYPES = [
];
const DMS_ACTIONS = [
{ id: "spawn dms ipc call spotlight toggle", label: "App Launcher: Toggle" },
{ id: "spawn dms ipc call spotlight open", label: "App Launcher: Open" },
{ id: "spawn dms ipc call spotlight close", label: "App Launcher: Close" },
{ id: "spawn dms ipc call spotlight toggle", label: "Default Launcher: Toggle" },
{ id: "spawn dms ipc call spotlight open", label: "Default Launcher: Open" },
{ id: "spawn dms ipc call spotlight close", label: "Default Launcher: Close" },
{ id: "spawn dms ipc call spotlight-bar toggle", label: "Spotlight Bar: Toggle" },
{ id: "spawn dms ipc call spotlight-bar open", label: "Spotlight Bar: Open" },
{ id: "spawn dms ipc call spotlight-bar close", label: "Spotlight Bar: Close" },
+1
View File
@@ -450,6 +450,7 @@ Singleton {
property bool dankLauncherV2IncludeFoldersInAll: false
property bool launcherUseOverlayLayer: false
property string launcherStyle: "full"
property bool spotlightBarShowModeChips: false
property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
@@ -216,6 +216,7 @@ var SPEC = {
dankLauncherV2IncludeFoldersInAll: { def: false },
launcherUseOverlayLayer: { def: false },
launcherStyle: { def: "full" },
spotlightBarShowModeChips: { def: false },
useAutoLocation: { def: false },
weatherEnabled: { def: true },
@@ -10,10 +10,11 @@ Rectangle {
property var entry: null
property string cachedImageData: ""
property string cachedMimeType: ""
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 : ""
readonly property string sourceUrl: resolvedSourceUrl(cachedImageData, cachedMimeType || (entry?.mimeType ?? ""))
radius: Math.max(6, Theme.cornerRadius - 2)
clip: true
@@ -24,8 +25,24 @@ Rectangle {
onEntryChanged: reloadPreview()
Component.onCompleted: reloadPreview()
function isImageMimeType(mimeType) {
return (mimeType || "").toString().toLowerCase().startsWith("image/");
}
function resolvedSourceUrl(data, mimeType) {
const rawData = (data || "").toString();
if (rawData.length === 0)
return "";
if (rawData.startsWith("data:"))
return rawData.startsWith("data:image/") ? rawData : "";
if (!isImageMimeType(mimeType))
return "";
return "data:" + mimeType + ";base64," + rawData;
}
function reloadPreview() {
cachedImageData = "";
cachedMimeType = "";
if (!canLoadImage || !entry?.id) {
_requestedEntryId = null;
return;
@@ -40,8 +57,12 @@ Rectangle {
return;
if (response.error)
return;
const data = response.result?.data ?? "";
if (data.length > 0)
const result = response.result ?? {};
const mimeType = (result.mimeType ?? entry?.mimeType ?? "").toString();
const data = (result.data ?? "").toString();
if (data.length === 0 || !resolvedSourceUrl(data, mimeType))
return;
cachedMimeType = mimeType;
cachedImageData = data;
});
}
@@ -35,6 +35,7 @@ Item {
property int gridColumns: SettingsData.appLauncherGridColumns
property int viewModeVersion: 0
property string viewModeContext: "spotlight"
property bool forceLinearNavigation: false
signal itemExecuted
signal searchCompleted
@@ -43,6 +44,10 @@ Item {
signal viewModeChanged(string sectionId, string mode)
signal searchQueryRequested(string query)
Ref {
service: AppSearchService
}
onActiveChanged: {
if (!active) {
SessionData.addLauncherHistory(searchQuery);
@@ -87,15 +92,23 @@ Item {
Connections {
target: ClipboardService
function onLauncherSearchReady(query) {
if (!active || !clipboardSearchEnabledInAll())
if (!active)
return;
if (searchMode !== "all")
const clipboardBuiltInActive = activePluginId === "dms_clipboard_search";
if (!clipboardBuiltInActive && !clipboardSearchEnabledInAll())
return;
if (!clipboardBuiltInActive && searchMode !== "all")
return;
const trimmed = (searchQuery || "").trim();
if (trimmed.length < 2 && query.length > 0)
return;
if (query !== trimmed)
const triggerMatch = detectTrigger(trimmed);
const effectiveQuery = clipboardBuiltInActive && triggerMatch.pluginId === "dms_clipboard_search" ? triggerMatch.query : trimmed;
if (query !== effectiveQuery)
return;
searchDebounce.restart();
}
}
@@ -1711,7 +1724,9 @@ Item {
function selectNext() {
keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
var newIndex = forceLinearNavigation ? Nav.findNextNonHeaderIndex(flatModel, selectedFlatIndex + 1) : Nav.calculateNextIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex === -1)
newIndex = selectedFlatIndex;
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -1721,7 +1736,9 @@ Item {
function selectPrevious() {
keyboardNavigationActive = true;
_cancelPendingSelectionReset();
var newIndex = Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
var newIndex = forceLinearNavigation ? Nav.findPrevNonHeaderIndex(flatModel, selectedFlatIndex - 1) : Nav.calculatePrevIndex(flatModel, selectedFlatIndex, null, null, gridColumns, getSectionViewMode);
if (newIndex === -1)
newIndex = selectedFlatIndex;
if (newIndex !== selectedFlatIndex) {
selectedFlatIndex = newIndex;
updateSelectedItem();
@@ -388,6 +388,7 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.closeTransientUi?.();
// NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
@@ -480,6 +481,7 @@ Item {
function hide() {
if (!spotlightOpen)
return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false;
isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
@@ -47,9 +47,9 @@ Item {
}
}
readonly property int _openDuration: 80
readonly property int _closeDuration: 70
readonly property int _motionDuration: 90
readonly property int _openDuration: 50
readonly property int _closeDuration: 40
readonly property int _motionDuration: 60
// Connected frame mode clamps the centered surface inside frame insets.
readonly property bool frameConnected: CompositorService.usesConnectedFrameChromeForScreen(effectiveScreen)
@@ -142,6 +142,7 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.closeTransientUi?.();
const targetQuery = query || (SettingsData.rememberLastQuery ? (SessionData.launcherLastQuery || "") : "");
const targetMode = mode || SessionData.launcherLastMode || "all";
@@ -202,6 +203,7 @@ Item {
function hide() {
if (!spotlightOpen)
return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false;
isClosing = true;
contentVisible = false;
@@ -133,6 +133,7 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
spotlightContent.closeTransientUi?.();
spotlightContent.searchField.forceActiveFocus();
var targetQuery = "";
@@ -211,6 +212,7 @@ Item {
function hide() {
if (!spotlightOpen)
return;
spotlightContent?.closeTransientUi?.();
openedFromOverview = false;
isClosing = true;
contentVisible = false;
@@ -368,7 +370,7 @@ Item {
WindowBlur {
targetWindow: launcherWindow
readonly property real s: Math.min(1, modalContainer.publishedScale)
readonly property real op: Math.max(0, Math.min(1, (modalContainer.opacity - 0.06) * 2))
readonly property real op: Math.max(0, Math.min(1, (modalContainer.publishedOpacity - 0.06) * 2))
blurX: modalContainer.x + modalContainer.width * (1 - s * op) * 0.5
blurY: modalContainer.y + modalContainer.height * (1 - s * op) * 0.5
blurWidth: contentVisible ? modalContainer.width * s * op : 0
@@ -447,6 +449,7 @@ Item {
property bool _renderActive: contentVisible
property real publishedScale: contentVisible ? 1 : 0.96
property real publishedOpacity: contentVisible ? 1 : 0
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
@@ -478,6 +481,14 @@ Item {
}
}
Behavior on publishedOpacity {
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Connections {
target: root
function onContentVisibleChanged() {
@@ -21,6 +21,17 @@ FocusScope {
property bool editMode: false
property var editingApp: null
property string editAppId: ""
readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real _launcherFieldAlpha: {
if (Theme.transparentBlurLayers)
return 0.28;
if (Theme.blurForegroundLayers)
return Math.max(Theme.popupTransparency, 0.62);
return Theme.popupTransparency;
}
readonly property color _launcherSearchFieldColor: Theme.withAlpha(Theme.surfaceContainerHigh, _launcherFieldAlpha)
readonly property color _launcherSearchBorderColor: Theme.withAlpha(Theme.outline, _blurActive ? 0.16 : Theme.layerOutlineOpacity)
readonly property color _launcherSearchFocusedBorderColor: Theme.withAlpha(Theme.primary, _blurActive ? 0.72 : 1.0)
function resetScroll() {
resultsList.resetScroll();
@@ -30,6 +41,12 @@ FocusScope {
searchField.forceActiveFocus();
}
function closeTransientUi() {
contextMenu.hide();
actionPanel.hide();
root.enabled = true;
}
function openEditMode(app) {
if (!app)
return;
@@ -111,6 +128,21 @@ FocusScope {
}
}
Connections {
target: root.parentModal
ignoreUnknownSignals: true
function onSpotlightOpenChanged() {
if (!root.parentModal?.spotlightOpen)
root.closeTransientUi();
}
function onContentVisibleChanged() {
if (!root.parentModal?.contentVisible)
root.closeTransientUi();
}
}
Keys.onPressed: event => {
if (editMode) {
if (event.key === Qt.Key_Escape) {
@@ -284,7 +316,7 @@ FocusScope {
anchors.bottom: parent.bottom
anchors.leftMargin: root.parentModal?.borderWidth ?? 1
anchors.rightMargin: root.parentModal?.borderWidth ?? 1
anchors.bottomMargin: _connectedBottomEmerge ? Theme.spacingS : (root.parentModal?.borderWidth ?? 1)
anchors.bottomMargin: _connectedBottomEmerge ? 0 : (root.parentModal?.borderWidth ?? 1)
height: showFooter ? (_connectedArcAtFooter ? 76 : 36) : 0
visible: showFooter
clip: true
@@ -293,7 +325,7 @@ FocusScope {
anchors.fill: parent
anchors.topMargin: -Theme.cornerRadius
// In connected mode the launcher provides the surface so update the toolbar for arcs
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false)
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) && !root._blurActive
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
}
@@ -458,9 +490,11 @@ FocusScope {
id: searchField
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
backgroundColor: root._launcherSearchFieldColor
normalBorderColor: root._launcherSearchBorderColor
focusedBorderColor: root._launcherSearchFocusedBorderColor
borderWidth: 1
focusedBorderWidth: 2
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
@@ -1,35 +1,72 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
Popup {
Item {
id: root
visible: false
width: 0
height: 0
property var item: null
property var controller: null
property var searchField: null
property var parentHandler: null
property bool allowEditActions: true
property real menuMargin: 8
property var targetScreen: null
property real anchorX: 0
property real anchorY: 0
property bool openState: false
property bool renderActive: false
readonly property bool blurActive: renderActive && openState && BlurService.enabled && Theme.connectedSurfaceBlurEnabled
readonly property real minMenuWidth: 180
readonly property real maxMenuWidth: Math.max(0, (targetScreen?.width ?? 500) - menuMargin * 2)
readonly property real maxMenuHeight: Math.max(0, (targetScreen?.height ?? 600) - menuMargin * 2)
readonly property string longestMenuText: {
let longest = "";
for (let i = 0; i < menuItems.length; i++) {
const text = menuItems[i].text || "";
if (text.length > longest.length)
longest = text;
}
return longest;
}
readonly property real naturalMenuWidth: Math.max(minMenuWidth, menuTextMetrics.width + Theme.iconSize + Theme.spacingS * 5)
readonly property real effectiveMenuWidth: Math.max(0, Math.min(maxMenuWidth, naturalMenuWidth))
readonly property real naturalMenuHeight: menuItemsHeight() + Theme.spacingS * 2
readonly property real effectiveMenuHeight: Math.min(maxMenuHeight, naturalMenuHeight)
readonly property bool menuScrolls: naturalMenuHeight > effectiveMenuHeight + 0.5
signal hideRequested
signal editAppRequested(var app)
TextMetrics {
id: menuTextMetrics
text: root.longestMenuText
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Normal
}
function hasContextMenuActions(spotlightItem) {
if (!spotlightItem)
return false;
if (spotlightItem.type === "app")
return true;
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
const instance = PluginService.pluginInstances[spotlightItem.pluginId];
if (!instance)
return false;
if (typeof instance.getContextMenuActions !== "function")
return false;
var actions = instance.getContextMenuActions(spotlightItem.data);
const actions = instance.getContextMenuActions(spotlightItem.data);
return Array.isArray(actions) && actions.length > 0;
}
if (spotlightItem.actions && spotlightItem.actions.length > 0)
@@ -54,13 +91,13 @@ Popup {
if (!isPluginItem || !item?.pluginId)
return [];
var instance = PluginService.pluginInstances[item.pluginId];
const instance = PluginService.pluginInstances[item.pluginId];
if (!instance)
return [];
if (typeof instance.getContextMenuActions !== "function")
return [];
var actions = instance.getContextMenuActions(item.data);
const actions = instance.getContextMenuActions(item.data);
if (!Array.isArray(actions))
return [];
@@ -68,8 +105,8 @@ Popup {
}
function executePluginAction(actionOrObj) {
var actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
var closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
const actionFunc = typeof actionOrObj === "function" ? actionOrObj : actionOrObj?.action;
const closeLauncher = typeof actionOrObj === "object" && actionOrObj?.closeLauncher;
if (typeof actionFunc === "function")
actionFunc();
@@ -90,12 +127,12 @@ Popup {
}
readonly property var menuItems: {
var items = [];
const items = [];
if (isPluginItem) {
var pluginActions = getPluginContextMenuActions();
for (var i = 0; i < pluginActions.length; i++) {
var act = pluginActions[i];
const pluginActions = getPluginContextMenuActions();
for (let i = 0; i < pluginActions.length; i++) {
const act = pluginActions[i];
items.push({
type: "item",
icon: act.icon || "play_arrow",
@@ -107,8 +144,8 @@ Popup {
}
if (item?.type !== "app" && item?.actions && item.actions.length > 0) {
for (var i = 0; i < item.actions.length; i++) {
var genericAct = item.actions[i];
for (let i = 0; i < item.actions.length; i++) {
const genericAct = item.actions[i];
items.push({
type: "item",
icon: genericAct.icon || "play_arrow",
@@ -149,8 +186,8 @@ Popup {
items.push({
type: "separator"
});
for (var i = 0; i < item.actions.length; i++) {
var act = item.actions[i];
for (let i = 0; i < item.actions.length; i++) {
const act = item.actions[i];
items.push({
type: "item",
icon: act.icon || "play_arrow",
@@ -183,43 +220,52 @@ Popup {
return items;
}
function menuItemsHeight() {
let h = 0;
for (let i = 0; i < menuItems.length; i++) {
h += menuItems[i].type === "separator" ? 5 : 32;
}
if (menuItems.length > 1)
h += menuItems.length - 1;
return h;
}
function show(x, y, spotlightItem, fromKeyboard) {
if (!spotlightItem?.data)
return;
item = spotlightItem;
selectedMenuIndex = fromKeyboard ? 0 : -1;
keyboardNavigation = fromKeyboard;
const modal = parentHandler?.parentModal ?? null;
const screenRef = modal?.effectiveScreen ?? parentHandler?.Window?.window?.screen ?? searchField?.Window?.window?.screen ?? null;
const screenX = screenRef?.x || 0;
const screenY = screenRef?.y || 0;
const screenRelativeX = modal ? ((modal.alignedX ?? 0) + x) : ((parentHandler ? parentHandler.mapToGlobal(x, y).x : x) - screenX);
const screenRelativeY = modal ? ((modal.alignedY ?? 0) + y) : ((parentHandler ? parentHandler.mapToGlobal(x, y).y : y) - screenY);
targetScreen = screenRef;
anchorX = screenRelativeX + 4;
anchorY = screenRelativeY + 4;
renderActive = true;
openState = true;
if (parentHandler)
parentHandler.enabled = false;
Qt.callLater(() => {
var parentW = parent?.width ?? 500;
var parentH = parent?.height ?? 600;
var menuW = width > 0 ? width : 200;
var menuH = height > 0 ? height : 200;
var margin = 8;
var posX = x + 4;
var posY = y + 4;
if (posX + menuW > parentW - margin) {
posX = Math.max(margin, parentW - menuW - margin);
}
if (posY + menuH > parentH - margin) {
posY = Math.max(margin, parentH - menuH - margin);
}
root.x = posX;
root.y = posY;
open();
menuFlickable.contentY = 0;
keyboardHandler.forceActiveFocus();
ensureSelectedVisible();
});
}
function hide() {
if (parentHandler)
parentHandler.enabled = true;
close();
if (!renderActive)
return;
openState = false;
hideRequested();
}
function togglePin() {
@@ -286,8 +332,8 @@ Popup {
property bool keyboardNavigation: false
readonly property int visibleItemCount: {
var count = 0;
for (var i = 0; i < menuItems.length; i++) {
let count = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item")
count++;
}
@@ -295,22 +341,62 @@ Popup {
}
function selectNext() {
if (visibleItemCount > 0)
if (visibleItemCount > 0) {
keyboardNavigation = true;
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
ensureSelectedVisible();
}
}
function selectPrevious() {
if (visibleItemCount > 0)
if (visibleItemCount > 0) {
keyboardNavigation = true;
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
ensureSelectedVisible();
}
}
function selectedDelegateIndex() {
let itemIndex = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type !== "item")
continue;
if (itemIndex === selectedMenuIndex)
return i;
itemIndex++;
}
return -1;
}
function ensureSelectedVisible() {
Qt.callLater(() => {
if (!menuFlickable || !menuRepeater)
return;
const delegateIndex = selectedDelegateIndex();
if (delegateIndex < 0)
return;
const delegate = menuRepeater.itemAt(delegateIndex);
if (!delegate)
return;
const top = delegate.y;
const bottom = top + delegate.height;
const viewTop = menuFlickable.contentY;
const viewBottom = viewTop + menuFlickable.height;
if (top < viewTop) {
menuFlickable.contentY = Math.max(0, top);
} else if (bottom > viewBottom) {
menuFlickable.contentY = Math.min(Math.max(0, menuFlickable.contentHeight - menuFlickable.height), bottom - menuFlickable.height);
}
});
}
function activateSelected() {
var itemIndex = 0;
for (var i = 0; i < menuItems.length; i++) {
let itemIndex = 0;
for (let i = 0; i < menuItems.length; i++) {
if (menuItems[i].type !== "item")
continue;
if (itemIndex === selectedMenuIndex) {
var menuItem = menuItems[i];
const menuItem = menuItems[i];
if (menuItem.action)
menuItem.action();
else if (menuItem.pluginAction)
@@ -325,51 +411,45 @@ Popup {
}
}
width: menuContainer.implicitWidth
height: menuContainer.implicitHeight
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true
dim: false
background: Item {}
PanelWindow {
id: menuWindow
onOpened: {
Qt.callLater(() => keyboardHandler.forceActiveFocus());
screen: root.targetScreen
visible: root.renderActive
color: "transparent"
WlrLayershell.namespace: "dms:launcher-context-menu"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: root.renderActive ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
}
onClosed: {
if (parentHandler)
parentHandler.enabled = true;
if (searchField?.visible) {
Qt.callLater(() => searchField.forceActiveFocus());
}
WindowBlur {
targetWindow: menuWindow
blurX: root.blurActive ? menuContainer.x : 0
blurY: root.blurActive ? menuContainer.y : 0
blurWidth: root.blurActive ? menuContainer.width : 0
blurHeight: root.blurActive ? menuContainer.height : 0
blurRadius: Theme.cornerRadius
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
MouseArea {
anchors.fill: parent
z: -1
enabled: root.renderActive
onClicked: root.hide()
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
contentItem: Item {
Item {
id: keyboardHandler
focus: true
implicitWidth: menuContainer.implicitWidth
implicitHeight: menuContainer.implicitHeight
anchors.fill: parent
focus: root.openState
Keys.onPressed: event => {
switch (event.key) {
@@ -396,13 +476,31 @@ Popup {
Rectangle {
id: menuContainer
anchors.fill: parent
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.floatingSurface
x: Math.max(root.menuMargin, Math.min(menuWindow.width - width - root.menuMargin, root.anchorX))
y: Math.max(root.menuMargin, Math.min(menuWindow.height - height - root.menuMargin, root.anchorY))
width: root.effectiveMenuWidth
height: root.effectiveMenuHeight
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: BlurService.enabled ? BlurService.borderColor : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: BlurService.enabled ? BlurService.borderWidth : 1
opacity: root.openState ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
onRunningChanged: {
if (!running && !root.openState) {
root.renderActive = false;
if (root.parentHandler)
root.parentHandler.enabled = true;
if (root.searchField?.visible)
Qt.callLater(() => root.searchField.forceActiveFocus());
}
}
}
}
Rectangle {
anchors.fill: parent
@@ -415,13 +513,23 @@ Popup {
z: -1
}
Column {
id: menuColumn
Flickable {
id: menuFlickable
anchors.fill: parent
anchors.margins: Theme.spacingS
clip: true
contentWidth: width
contentHeight: menuColumn.implicitHeight
boundsBehavior: Flickable.StopAtBounds
interactive: root.menuScrolls
Column {
id: menuColumn
width: menuFlickable.width
spacing: 1
Repeater {
id: menuRepeater
model: root.menuItems
Item {
@@ -433,8 +541,8 @@ Popup {
height: modelData.type === "separator" ? 5 : 32
readonly property int itemIndex: {
var count = 0;
for (var i = 0; i < index; i++) {
let count = 0;
for (let i = 0; i < index; i++) {
if (root.menuItems[i].type === "item")
count++;
}
@@ -519,7 +627,7 @@ Popup {
}
onPressed: mouse => menuItemRipple.trigger(mouse.x, mouse.y)
onClicked: {
var menuItem = menuItemDelegate.modelData;
const menuItem = menuItemDelegate.modelData;
if (menuItem.action)
menuItem.action();
else if (menuItem.pluginAction)
@@ -536,4 +644,6 @@ Popup {
}
}
}
}
}
}
@@ -24,13 +24,34 @@ FocusScope {
readonly property real _resultsH: _hasQuery ? Math.min(_resultsContentH, _maxResultsH) : 0
readonly property int _fastDuration: 90
readonly property int _resizeDuration: 110
readonly property bool _blurActive: Theme.blurForegroundLayers || Theme.transparentBlurLayers
readonly property real _searchSurfaceAlpha: {
if (Theme.transparentBlurLayers)
return _hasQuery ? 0.34 : 0.28;
if (Theme.blurForegroundLayers)
return Math.max(Theme.popupTransparency, _hasQuery ? 0.68 : 0.74);
return _hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9);
}
readonly property color _searchSurfaceColor: Theme.withAlpha(_hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, _searchSurfaceAlpha)
readonly property color _searchWellColor: {
if (searchInput.activeFocus)
return Theme.withAlpha(Theme.primaryContainer, Theme.transparentBlurLayers ? 0.42 : 1.0);
if (Theme.transparentBlurLayers)
return Theme.ccPillInactiveBg;
return Theme.surfaceContainer;
}
implicitHeight: _searchAreaH + (_resultsH > 0 ? 1 + _resultsH : 0)
implicitHeight: _searchAreaH + _resultsH
function resetScroll() {
resultsList.resetScroll();
}
function closeTransientUi() {
contextMenu.hide();
root.enabled = true;
}
function _buildRows() {
const flat = searchController.flatModel || [];
const sections = searchController.sections || [];
@@ -121,12 +142,10 @@ FocusScope {
}
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;
@@ -192,6 +211,7 @@ FocusScope {
id: searchController
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
viewModeContext: "spotlight"
forceLinearNavigation: true
onItemExecuted: {
root.parentModal?.hide();
@@ -209,6 +229,21 @@ FocusScope {
allowEditActions: false
}
Connections {
target: root.parentModal
ignoreUnknownSignals: true
function onSpotlightOpenChanged() {
if (!root.parentModal?.spotlightOpen)
root.closeTransientUi();
}
function onContentVisibleChanged() {
if (!root.parentModal?.contentVisible)
root.closeTransientUi();
}
}
Connections {
target: searchController
function onModeChanged(mode) {
@@ -233,7 +268,7 @@ FocusScope {
id: searchBarSurface
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.withAlpha(root._hasQuery ? Theme.surfaceContainerHigh : Theme.surfaceContainer, root._hasQuery ? Theme.popupTransparency : Math.max(0.68, Theme.popupTransparency * 0.9))
color: root._searchSurfaceColor
Behavior on color {
ColorAnimation {
@@ -250,7 +285,7 @@ FocusScope {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
color: searchInput.activeFocus ? Theme.primaryContainer : Theme.surfaceContainer
color: root._searchWellColor
DankIcon {
anchors.centerIn: parent
@@ -269,8 +304,8 @@ FocusScope {
Row {
id: categoryRow
visible: SettingsData.spotlightBarShowModeChips || root._hasQuery
spacing: Theme.spacingXS
visible: root._hasQuery
anchors.verticalCenter: parent.verticalCenter
Repeater {
@@ -376,26 +411,9 @@ FocusScope {
}
}
Rectangle {
anchors.top: searchBarItem.bottom
anchors.left: parent.left
anchors.right: parent.right
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
@@ -12,6 +12,7 @@ Item {
property var controller: null
property bool hasQuery: false
property var rows: []
readonly property real bottomInset: Theme.spacingS
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
@@ -53,7 +54,11 @@ Item {
DankListView {
id: mainListView
anchors.fill: parent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: root.bottomInset
clip: true
visible: root.rows.length > 0
@@ -64,11 +69,6 @@ Item {
objectProp: "_rowId"
}
add: null
remove: null
displaced: null
move: null
delegate: Item {
id: delegateRoot
required property var modelData
@@ -103,7 +103,11 @@ Item {
}
Item {
anchors.fill: parent
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.bottomMargin: root.bottomInset
visible: root.hasQuery && root.rows.length === 0
Row {
@@ -64,6 +64,7 @@ FocusScope {
sourceComponent: KeybindsTab {
parentModal: root.parentModal
requestedSearchQuery: root.parentModal?.keybindSearchQuery ?? ""
}
onActiveChanged: {
@@ -37,6 +37,7 @@ FloatingWindow {
property bool isCompactMode: width < 700
property bool menuVisible: !isCompactMode
property bool enableAnimations: true
property string keybindSearchQuery: ""
signal closingModal
@@ -73,6 +74,11 @@ FloatingWindow {
return sidebar.resolveTabIndex(tabName);
}
function showKeybindsSearch(query: string) {
keybindSearchQuery = query || "";
showWithTabName("keybinds");
}
function toggleMenu() {
enableAnimations = true;
menuVisible = !menuVisible;
+24 -2
View File
@@ -16,6 +16,7 @@ Item {
property var parentModal: null
property string selectedCategory: ""
property string searchQuery: ""
property string requestedSearchQuery: ""
property string expandedKey: ""
property bool showingNewBind: false
@@ -206,13 +207,34 @@ Item {
}
}
Component.onCompleted: _ensureCurrentProvider()
function _applyRequestedSearch() {
if (!requestedSearchQuery)
return;
const query = requestedSearchQuery;
selectedCategory = "";
searchField.text = query;
searchQuery = query;
_updateFiltered();
if (parentModal?.keybindSearchQuery === query)
parentModal.keybindSearchQuery = "";
Qt.callLater(scrollToTop);
}
Component.onCompleted: {
_ensureCurrentProvider();
Qt.callLater(_applyRequestedSearch);
}
onRequestedSearchQueryChanged: Qt.callLater(_applyRequestedSearch)
onVisibleChanged: {
if (!visible)
return;
Qt.callLater(scrollToTop);
_ensureCurrentProvider();
Qt.callLater(() => {
_applyRequestedSearch();
scrollToTop();
});
}
DankFlickable {
+201 -6
View File
@@ -9,6 +9,37 @@ 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"
function openKeybindsSearch(query) {
if (!root.parentModal)
return;
if (typeof root.parentModal.showKeybindsSearch === "function") {
root.parentModal.showKeybindsSearch(query);
} else {
root.parentModal.showWithTabName("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(", ");
}
Component.onCompleted: {
if (KeybindsService.available)
KeybindsService.loadBinds(false);
}
FileBrowserModal {
id: logoFileBrowser
@@ -35,20 +66,20 @@ Item {
SettingsCard {
width: parent.width
iconName: "search"
title: I18n.tr("Launcher Style")
title: I18n.tr("Default Launcher")
settingKey: "launcherStyle"
SettingsControlledByFrame {
visible: SettingsData.connectedFrameModeActive
parentModal: root.parentModal
settingLabel: I18n.tr("Launcher Style")
reason: I18n.tr("Managed by Frame Mode")
settingLabel: I18n.tr("Default Launcher")
reason: I18n.tr("Connected Frame Mode uses the connected launcher for default launcher shortcuts.")
}
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.")
text: SettingsData.launcherStyle === "spotlight" ? I18n.tr("Default launcher shortcuts open the minimal Spotlight Bar. The dedicated Spotlight Bar shortcut below stays independent.") : I18n.tr("Default launcher shortcuts open the full launcher with mode tabs, grid view, and action panel.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
@@ -57,8 +88,8 @@ Item {
SettingsButtonGroupRow {
visible: !SettingsData.connectedFrameModeActive
settingKey: "launcherStyleSelector"
tags: ["launcher", "style", "spotlight", "full", "minimal"]
text: I18n.tr("Style")
tags: ["launcher", "style", "default", "spotlight", "full", "minimal"]
text: I18n.tr("Default Opens")
model: [I18n.tr("Full"), I18n.tr("Spotlight")]
currentIndex: SettingsData.launcherStyle === "spotlight" ? 1 : 0
onSelectionChanged: (index, selected) => {
@@ -68,6 +99,76 @@ Item {
}
}
StyledRect {
id: defaultShortcutCard
width: parent.width
height: defaultShortcutRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: defaultShortcutMouse.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.48) : Theme.withAlpha(Theme.surfaceContainer, 0.35)
border.color: Theme.outlineMedium
border.width: 1
Row {
id: defaultShortcutRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: Math.max(0, parent.width - Theme.iconSize - defaultShortcutValue.width - Theme.spacingM * 2)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("Default Launcher Shortcut")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: !root.keybindsAvailable ? I18n.tr("Bind the spotlight IPC action in your compositor config.") : SettingsData.connectedFrameModeActive ? I18n.tr("Opens the connected launcher in Connected Frame Mode.") : I18n.tr("Follows the default launcher choice selected above.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
StyledText {
id: defaultShortcutValue
text: root.keysLabel(root.defaultLauncherAction)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
width: Math.min(170, implicitWidth)
elide: Text.ElideRight
}
}
MouseArea {
id: defaultShortcutMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openKeybindsSearch(root.defaultLauncherKeybindSearch)
}
}
SettingsToggleRow {
settingKey: "launcherUseOverlayLayer"
tags: ["launcher", "fullscreen", "overlay", "layer"]
@@ -78,6 +179,100 @@ Item {
}
}
SettingsCard {
width: parent.width
iconName: "search"
title: I18n.tr("Spotlight Bar")
settingKey: "spotlightBarLauncher"
StyledText {
width: parent.width
text: I18n.tr("A separate minimal launcher action that works in Standalone, Separate Frame Mode, and Connected Frame Mode.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
}
StyledRect {
id: spotlightShortcutCard
width: parent.width
height: spotlightShortcutRow.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: spotlightShortcutMouse.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.48) : Theme.withAlpha(Theme.surfaceContainer, 0.35)
border.color: Theme.outlineMedium
border.width: 1
Row {
id: spotlightShortcutRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
DankIcon {
name: "keyboard"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
Column {
width: Math.max(0, parent.width - Theme.iconSize - spotlightShortcutValue.width - Theme.spacingM * 2)
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("Spotlight Bar Shortcut")
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
elide: Text.ElideRight
}
StyledText {
text: !root.keybindsAvailable ? I18n.tr("Bind the spotlight-bar IPC action in your compositor config.") : I18n.tr("Uses the spotlight-bar IPC action and always opens the minimal bar.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.WordWrap
}
}
StyledText {
id: spotlightShortcutValue
text: root.keysLabel(root.spotlightBarAction)
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignRight
width: Math.min(170, implicitWidth)
elide: Text.ElideRight
}
}
MouseArea {
id: spotlightShortcutMouse
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openKeybindsSearch(root.spotlightBarKeybindSearch)
}
}
SettingsToggleRow {
settingKey: "spotlightBarShowModeChips"
tags: ["launcher", "spotlight", "bar", "chips", "tabs", "modes"]
text: I18n.tr("Show Mode Chips")
description: I18n.tr("Show All, Apps, Files, and Plugins chips beside the Spotlight Bar input.")
checked: SettingsData.spotlightBarShowModeChips
onToggled: checked => SettingsData.set("spotlightBarShowModeChips", checked)
}
}
SettingsCard {
width: parent.width
iconName: "apps"
+2 -1
View File
@@ -9,6 +9,7 @@ import qs.Services
Singleton {
id: root
readonly property var log: Log.scoped("AppSearchService")
property int refCount: 0
property var applications: []
property var _cachedCategories: null
@@ -297,7 +298,7 @@ Singleton {
function getBuiltInLauncherItems(pluginId, query) {
if (pluginId === "dms_clipboard_search") {
const trimmed = (query || "").toString().trim();
const entries = ClipboardService.getCachedLauncherSearchEntries(trimmed, 20);
const entries = ClipboardService.internalEntries.length > 0 ? ClipboardService.getLauncherEntries(trimmed, 20, 0) : ClipboardService.getCachedLauncherSearchEntries(trimmed, 20);
return entries.map(entry => ({
type: "clipboard",
data: entry
+18
View File
@@ -463,6 +463,24 @@ Singleton {
return _flatCache;
}
function keysForAction(actionId) {
if (!actionId)
return [];
for (let i = 0; i < _flatCache.length; i++) {
const group = _flatCache[i];
if (!group || group.action !== actionId || !Array.isArray(group.keys))
continue;
const keys = [];
for (let k = 0; k < group.keys.length; k++) {
const key = group.keys[k]?.key || "";
if (key)
keys.push(key);
}
return keys;
}
return [];
}
function saveBind(originalKey, bindData) {
if (!bindData.key || !Actions.isValidAction(bindData.action))
return;
+4
View File
@@ -205,6 +205,8 @@ Singleton {
}
function launchDesktopEntry(desktopEntry, useNvidia) {
if (!desktopEntry || !desktopEntry.command)
return;
let cmd = desktopEntry.command;
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
@@ -261,6 +263,8 @@ Singleton {
}
function launchDesktopAction(desktopEntry, action, useNvidia) {
if (!desktopEntry || !action || !action.command)
return;
let cmd = action.command;
const appId = desktopEntry.id || desktopEntry.execString || desktopEntry.exec || "";
+2 -2
View File
@@ -40,8 +40,8 @@ StyledRect {
property color focusedBorderColor: Theme.primary
property color normalBorderColor: Theme.outlineMedium
property color placeholderColor: Theme.outlineButton
property int borderWidth: 1
property int focusedBorderWidth: 2
property real borderWidth: 1
property real focusedBorderWidth: 2
property real cornerRadius: Theme.cornerRadius
readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0)
readonly property real rightPadding: {
@@ -939,7 +939,7 @@
"topbar",
"wayland"
],
"description": "Place this bar on the Wayland overlay layer"
"description": "Place the bar on the Wayland overlay layer"
},
{
"section": "barVisibility",
@@ -967,7 +967,7 @@
"wayland"
],
"icon": "visibility_off",
"description": "Place this bar on the Wayland overlay layer"
"description": "Place the bar on the Wayland overlay layer"
},
{
"section": "workspaceDragReorder",
@@ -2052,6 +2052,50 @@
],
"icon": "extension"
},
{
"section": "launcherStyle",
"label": "Default Launcher",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"app drawer",
"app menu",
"applications",
"default",
"drawer",
"full",
"launcher",
"layer",
"menu",
"minimal",
"opening",
"overlay",
"spotlight",
"start",
"start menu",
"style"
],
"icon": "search",
"description": "Use the overlay layer when opening the launcher"
},
{
"section": "launcherStyleSelector",
"label": "Default Opens",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"default",
"drawer",
"full",
"launcher",
"menu",
"minimal",
"opens",
"spotlight",
"start",
"style"
]
},
{
"section": "niriOverviewOverlayEnabled",
"label": "Enable Overview Overlay",
@@ -2228,31 +2272,6 @@
],
"icon": "apps"
},
{
"section": "launcherStyle",
"label": "Launcher Style",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"app drawer",
"app menu",
"applications",
"drawer",
"full",
"launcher",
"layer",
"menu",
"minimal",
"opening",
"overlay",
"spotlight",
"start",
"start menu",
"style"
],
"icon": "search",
"description": "Use the overlay layer when opening the launcher"
},
{
"section": "spotlightCloseNiriOverview",
"label": "Niri Integration",
@@ -2394,6 +2413,40 @@
],
"description": "Show mode tabs and keyboard hints at the bottom."
},
{
"section": "spotlightBarShowModeChips",
"label": "Show Mode Chips",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"addons",
"apps",
"bar",
"beside",
"chips",
"day",
"drawer",
"extensions",
"files",
"input",
"launcher",
"light mode",
"menu",
"mode",
"modes",
"panel",
"plugins",
"show",
"spotlight",
"start",
"statusbar",
"tabs",
"taskbar",
"topbar",
"widgets"
],
"description": "Show All, Apps, Files, and Plugins chips beside the Spotlight Bar input."
},
{
"section": "launcherLogoSizeOffset",
"label": "Size Offset",
@@ -2458,20 +2511,38 @@
"description": "When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency."
},
{
"section": "launcherStyleSelector",
"label": "Style",
"section": "spotlightBarLauncher",
"label": "Spotlight Bar",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"addons",
"apps",
"bar",
"beside",
"chips",
"day",
"drawer",
"full",
"extensions",
"files",
"input",
"launcher",
"light mode",
"menu",
"minimal",
"modes",
"panel",
"plugins",
"show",
"spotlight",
"start",
"style"
]
"statusbar",
"tabs",
"taskbar",
"topbar",
"widgets"
],
"icon": "search",
"description": "Show All, Apps, Files, and Plugins chips beside the Spotlight Bar input."
},
{
"section": "dankLauncherV2BorderThickness",