1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00

launcher: Dank Launcher V2 (beta)

- Aggregate plugins/extensions in new "all" tab
- Quick tab actions
- New tile mode for results
- Plugins can enforce/require view mode, or set preferred default
- Danksearch under "files" category
This commit is contained in:
bbedward
2026-01-20 17:54:30 -05:00
parent 3c39162016
commit 1d5d876e16
31 changed files with 5778 additions and 216 deletions

View File

@@ -215,8 +215,8 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
// Skip if file already exists to preserve user modifications
if _, err := os.Stat(path); err == nil {
// Skip if file already exists and is not empty to preserve user modifications
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
continue
}
@@ -567,7 +567,8 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
for _, cfg := range configs {
path := filepath.Join(dmsDir, cfg.name)
if _, err := os.Stat(path); err == nil {
// Skip if file already exists and is not empty to preserve user modifications
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
continue
}

View File

@@ -236,7 +236,13 @@ Singleton {
property bool sortAppsAlphabetically: false
property int appLauncherGridColumns: 4
property bool spotlightCloseNiriOverview: true
property var spotlightSectionViewModes: ({})
property bool niriOverviewOverlayEnabled: true
property string dankLauncherV2Size: "compact"
property bool dankLauncherV2BorderEnabled: false
property int dankLauncherV2BorderThickness: 2
property string dankLauncherV2BorderColor: "primary"
property bool dankLauncherV2ShowFooter: true
property string _legacyWeatherLocation: "New York, NY"
property string _legacyWeatherCoordinates: "40.7128,-74.0060"

View File

@@ -134,7 +134,13 @@ var SPEC = {
sortAppsAlphabetically: { def: false },
appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true },
spotlightSectionViewModes: { def: {} },
niriOverviewOverlayEnabled: { def: true },
dankLauncherV2Size: { def: "compact" },
dankLauncherV2BorderEnabled: { def: false },
dankLauncherV2BorderThickness: { def: 2 },
dankLauncherV2BorderColor: { def: "primary" },
dankLauncherV2ShowFooter: { def: true },
useAutoLocation: { def: false },
weatherEnabled: { def: true },

View File

@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
import qs.Modals.Greeter
import qs.Modals.Settings
import qs.Modals.Spotlight
import qs.Modals.DankLauncherV2
import qs.Modules
import qs.Modules.AppDrawer
import qs.Modules.DankDash
@@ -514,6 +515,25 @@ Item {
}
}
LazyLoader {
id: spotlightV2ModalLoader
active: false
Component.onCompleted: {
PopoutService.spotlightV2ModalLoader = spotlightV2ModalLoader;
}
DankLauncherV2Modal {
id: spotlightV2Modal
Component.onCompleted: {
PopoutService.spotlightV2Modal = spotlightV2Modal;
PopoutService._onSpotlightV2ModalLoaded();
}
}
}
ClipboardHistoryModal {
id: clipboardHistoryModalPopup

View File

@@ -1025,6 +1025,35 @@ Item {
target: "clipboard"
}
IpcHandler {
function open(): string {
PopoutService.openSpotlightV2();
return "LAUNCHER_OPEN_SUCCESS";
}
function close(): string {
PopoutService.closeSpotlightV2();
return "LAUNCHER_CLOSE_SUCCESS";
}
function toggle(): string {
PopoutService.toggleSpotlightV2();
return "LAUNCHER_TOGGLE_SUCCESS";
}
function openQuery(query: string): string {
PopoutService.openSpotlightV2WithQuery(query);
return "LAUNCHER_OPEN_QUERY_SUCCESS";
}
function toggleQuery(query: string): string {
PopoutService.toggleSpotlightV2();
return "LAUNCHER_TOGGLE_QUERY_SUCCESS";
}
target: "launcher"
}
IpcHandler {
function open(): string {
FirstLaunchService.showWelcome();

View File

@@ -0,0 +1,231 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
id: root
property var selectedItem: null
property var controller: null
property bool expanded: false
property int selectedActionIndex: 0
function getPluginContextMenuActions() {
if (selectedItem?.type !== "plugin" || !selectedItem?.pluginId)
return [];
var instance = PluginService.pluginInstances[selectedItem.pluginId];
if (!instance)
return [];
if (typeof instance.getContextMenuActions !== "function")
return [];
var actions = instance.getContextMenuActions(selectedItem.data);
if (!Array.isArray(actions))
return [];
return actions;
}
readonly property var actions: {
var result = [];
if (selectedItem?.primaryAction) {
result.push(selectedItem.primaryAction);
}
if (selectedItem?.type === "plugin") {
var pluginActions = getPluginContextMenuActions();
for (var i = 0; i < pluginActions.length; i++) {
var act = pluginActions[i];
result.push({
name: act.text || act.name || "",
icon: act.icon || "play_arrow",
action: "plugin_action",
pluginAction: act.action
});
}
} else if (selectedItem?.type === "app" && !selectedItem?.isCore) {
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
}
return result;
}
readonly property bool hasActions: {
if (selectedItem?.type === "app" && !selectedItem?.isCore)
return true;
if (selectedItem?.type === "plugin") {
var pluginActions = getPluginContextMenuActions();
return pluginActions.length > 0;
}
return actions.length > 1;
}
width: parent?.width ?? 200
height: expanded && hasActions ? 52 : 0
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
clip: true
Behavior on height {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
anchors.top: parent.top
width: parent.width
height: 1
color: Theme.outlineMedium
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Flickable {
id: actionsFlickable
anchors.left: parent.left
anchors.right: tabHint.left
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
height: parent.height
contentWidth: actionsRow.width
contentHeight: height
clip: true
boundsBehavior: Flickable.StopAtBounds
flickableDirection: Flickable.HorizontalFlick
Row {
id: actionsRow
height: parent.height
spacing: Theme.spacingS
Repeater {
model: root.actions
Rectangle {
id: actionButton
required property var modelData
required property int index
width: actionContent.implicitWidth + Theme.spacingM * 2
height: actionsRow.height
radius: Theme.cornerRadius
color: index === root.selectedActionIndex ? Theme.primaryHover : actionArea.containsMouse ? Theme.surfaceHover : "transparent"
Row {
id: actionContent
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: actionButton.modelData?.icon ?? "play_arrow"
size: 16
color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: actionButton.modelData?.name ?? ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: actionButton.index === root.selectedActionIndex ? Theme.primary : Theme.surfaceText
}
}
MouseArea {
id: actionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.controller && root.selectedItem) {
root.controller.executeAction(root.selectedItem, actionButton.modelData);
}
}
onEntered: root.selectedActionIndex = actionButton.index
}
}
}
}
}
StyledText {
id: tabHint
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: root.hasActions
text: "Tab"
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.outlineButton
}
}
function toggle() {
expanded = !expanded;
selectedActionIndex = 0;
}
function show() {
expanded = true;
selectedActionIndex = actions.length > 1 ? 1 : 0;
}
function hide() {
expanded = false;
selectedActionIndex = 0;
}
function cycleAction() {
if (actions.length > 0) {
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
ensureSelectedVisible();
}
}
function ensureSelectedVisible() {
if (selectedActionIndex < 0 || !actionsRow.children || selectedActionIndex >= actionsRow.children.length)
return;
var buttonX = 0;
for (var i = 0; i < selectedActionIndex; i++) {
var child = actionsRow.children[i];
if (child)
buttonX += child.width + actionsRow.spacing;
}
var button = actionsRow.children[selectedActionIndex];
if (!button)
return;
var buttonRight = buttonX + button.width;
var viewLeft = actionsFlickable.contentX;
var viewRight = viewLeft + actionsFlickable.width;
if (buttonX < viewLeft) {
actionsFlickable.contentX = Math.max(0, buttonX - Theme.spacingS);
} else if (buttonRight > viewRight) {
actionsFlickable.contentX = Math.min(actionsFlickable.contentWidth - actionsFlickable.width, buttonRight - actionsFlickable.width + Theme.spacingS);
}
}
function executeSelectedAction() {
if (!controller || !selectedItem || selectedActionIndex >= actions.length)
return;
var action = actions[selectedActionIndex];
if (action.action === "plugin_action" && typeof action.pluginAction === "function") {
action.pluginAction();
controller.performSearch();
controller.itemExecuted();
} else {
controller.executeAction(selectedItem, action);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,283 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
property alias spotlightContent: launcherContent
property bool openedFromOverview: false
property bool isClosing: false
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 baseWidth: SettingsData.dankLauncherV2Size === "medium" ? 600 : SettingsData.dankLauncherV2Size === "large" ? 720 : 500
readonly property int baseHeight: SettingsData.dankLauncherV2Size === "medium" ? 680 : SettingsData.dankLauncherV2Size === "large" ? 820 : 560
readonly property int modalWidth: Math.min(baseWidth, screenWidth - 100)
readonly property int modalHeight: Math.min(baseHeight, screenHeight - 100)
readonly property real modalX: (screenWidth - modalWidth) / 2
readonly property real modalY: (screenHeight - modalHeight) / 2
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 : 1
signal dialogClosed
function _initializeAndShow(query) {
contentVisible = true;
spotlightContent.searchField.forceActiveFocus();
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query;
}
if (spotlightContent.controller) {
spotlightContent.controller.searchMode = "all";
spotlightContent.controller.activePluginId = "";
spotlightContent.controller.activePluginName = "";
spotlightContent.controller.pluginFilter = "";
spotlightContent.controller.collapsedSections = {};
if (query) {
spotlightContent.controller.setSearchQuery(query);
} else {
spotlightContent.controller.searchQuery = "";
spotlightContent.controller.performSearch();
}
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll();
}
if (spotlightContent.actionPanel) {
spotlightContent.actionPanel.hide();
}
}
function show() {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_initializeAndShow("");
}
function showWithQuery(query) {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_initializeAndShow(query);
}
function hide() {
if (!spotlightOpen)
return;
openedFromOverview = false;
isClosing = true;
contentVisible = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(root);
closeCleanupTimer.start();
}
function toggle() {
spotlightOpen ? hide() : show();
}
Timer {
id: closeCleanupTimer
interval: Theme.expressiveDurations.expressiveFastSpatial + 50
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 !== root && spotlightOpen) {
hide();
}
}
}
PanelWindow {
id: launcherWindow
visible: true
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:launcher"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
bottom: true
left: true
right: true
}
mask: Region {
item: spotlightOpen ? fullScreenMask : null
}
Item {
id: fullScreenMask
anchors.fill: parent
}
Rectangle {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveFastSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.emphasized
}
}
}
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: mouse => {
var contentX = modalContainer.x;
var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
root.hide();
}
}
}
Item {
id: modalContainer
x: root.modalX
y: root.modalY
width: root.modalWidth
height: root.modalHeight
visible: contentVisible || opacity > 0
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
}
}
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
radius: root.cornerRadius
}
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
LauncherContent {
id: launcherContent
anchors.fill: parent
parentModal: root
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
}
}
}

View File

@@ -0,0 +1,139 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property var item: null
property bool isSelected: false
property bool isHovered: itemArea.containsMouse
property var controller: null
property int flatIndex: -1
signal clicked
signal rightClicked(real mouseX, real mouseY)
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
Column {
anchors.centerIn: parent
anchors.margins: Theme.spacingS
spacing: Theme.spacingS
width: parent.width - Theme.spacingM
Item {
width: iconSize
height: iconSize
anchors.horizontalCenter: parent.horizontalCenter
property int iconSize: Math.min(48, Math.max(32, root.width * 0.45))
Image {
id: appIcon
anchors.fill: parent
visible: root.item?.iconType === "image"
asynchronous: true
source: root.item?.iconType === "image" ? "image://icon/" + (root.item?.icon || "application-x-executable") : ""
sourceSize.width: parent.iconSize
sourceSize.height: parent.iconSize
fillMode: Image.PreserveAspectFit
cache: false
}
DankIcon {
anchors.centerIn: parent
visible: root.item?.iconType === "material" || root.item?.iconType === "nerd"
name: root.item?.icon ?? "apps"
size: parent.iconSize * 0.7
color: root.isSelected ? Theme.primary : Theme.surfaceText
}
Item {
anchors.fill: parent
visible: root.item?.iconType === "composite"
Image {
anchors.fill: parent
asynchronous: true
source: {
if (!root.item || root.item.iconType !== "composite")
return "";
var iconFull = root.item.iconFull || "";
if (iconFull.startsWith("svg+corner:")) {
var parts = iconFull.substring(11).split("|");
return parts[0] || "";
}
return "";
}
sourceSize.width: parent.width
sourceSize.height: parent.height
fillMode: Image.PreserveAspectFit
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 16
height: 16
radius: 8
color: Theme.surfaceContainer
DankIcon {
anchors.centerIn: parent
name: {
if (!root.item || root.item.iconType !== "composite")
return "";
var iconFull = root.item.iconFull || "";
if (iconFull.startsWith("svg+corner:")) {
var parts = iconFull.substring(11).split("|");
return parts[1] || "";
}
return "";
}
size: 12
color: root.isSelected ? Theme.primary : Theme.surfaceText
}
}
}
}
StyledText {
width: parent.width
text: root.item?.name ?? ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: root.isSelected ? Theme.primary : Theme.surfaceText
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
maximumLineCount: 2
wrapMode: Text.Wrap
}
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y);
root.rightClicked(scenePos.x, scenePos.y);
} else {
root.clicked();
}
}
onPositionChanged: {
if (root.controller) {
root.controller.keyboardNavigationActive = false;
}
}
}
}

View File

@@ -0,0 +1,809 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
FocusScope {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
property var parentModal: null
property alias searchField: searchField
property alias controller: controller
property alias resultsList: resultsList
property alias actionPanel: actionPanel
property bool editMode: false
property var editingApp: null
property string editAppId: ""
function resetScroll() {
resultsList.resetScroll();
}
function focusSearchField() {
searchField.forceActiveFocus();
}
function openEditMode(app) {
if (!app)
return;
editingApp = app;
editAppId = app.id || app.execString || app.exec || "";
var existing = SessionData.getAppOverride(editAppId);
editNameField.text = existing?.name || "";
editIconField.text = existing?.icon || "";
editCommentField.text = existing?.comment || "";
editEnvVarsField.text = existing?.envVars || "";
editExtraFlagsField.text = existing?.extraFlags || "";
editMode = true;
Qt.callLater(() => editNameField.forceActiveFocus());
}
function closeEditMode() {
editMode = false;
editingApp = null;
editAppId = "";
Qt.callLater(() => searchField.forceActiveFocus());
}
function saveAppOverride() {
var override = {};
if (editNameField.text.trim())
override.name = editNameField.text.trim();
if (editIconField.text.trim())
override.icon = editIconField.text.trim();
if (editCommentField.text.trim())
override.comment = editCommentField.text.trim();
if (editEnvVarsField.text.trim())
override.envVars = editEnvVarsField.text.trim();
if (editExtraFlagsField.text.trim())
override.extraFlags = editExtraFlagsField.text.trim();
SessionData.setAppOverride(editAppId, override);
closeEditMode();
}
function resetAppOverride() {
SessionData.clearAppOverride(editAppId);
closeEditMode();
}
function showContextMenu(item, x, y, fromKeyboard) {
if (!item)
return;
if (item.isCore)
return;
if (!contextMenu.hasContextMenuActions(item))
return;
contextMenu.show(x, y, item, fromKeyboard);
}
anchors.fill: parent
focus: true
Controller {
id: controller
onItemExecuted: {
if (root.parentModal) {
root.parentModal.hide();
}
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview();
}
}
}
LauncherContextMenu {
id: contextMenu
parent: root
controller: root.controller
searchField: root.searchField
parentHandler: root
onEditAppRequested: app => {
root.openEditMode(app);
}
}
Keys.onPressed: event => {
if (editMode) {
if (event.key === Qt.Key_Escape) {
closeEditMode();
event.accepted = true;
}
return;
}
var hasCtrl = event.modifiers & Qt.ControlModifier;
event.accepted = true;
switch (event.key) {
case Qt.Key_Escape:
if (actionPanel.expanded) {
actionPanel.hide();
return;
}
if (controller.clearPluginFilter())
return;
if (root.parentModal)
root.parentModal.hide();
return;
case Qt.Key_Backspace:
if (searchField.text.length === 0 && controller.clearPluginFilter())
return;
event.accepted = false;
return;
case Qt.Key_Down:
controller.selectNext();
return;
case Qt.Key_Up:
controller.selectPrevious();
return;
case Qt.Key_PageDown:
controller.selectPageDown(8);
return;
case Qt.Key_PageUp:
controller.selectPageUp(8);
return;
case Qt.Key_Right:
controller.selectRight();
return;
case Qt.Key_Left:
controller.selectLeft();
return;
case Qt.Key_J:
if (hasCtrl) {
controller.selectNext();
return;
}
event.accepted = false;
return;
case Qt.Key_K:
if (hasCtrl) {
controller.selectPrevious();
return;
}
event.accepted = false;
return;
case Qt.Key_N:
if (hasCtrl) {
controller.selectNextSection();
return;
}
event.accepted = false;
return;
case Qt.Key_P:
if (hasCtrl) {
controller.selectPreviousSection();
return;
}
event.accepted = false;
return;
case Qt.Key_Tab:
if (actionPanel.hasActions) {
actionPanel.expanded ? actionPanel.cycleAction() : actionPanel.show();
}
return;
case Qt.Key_Backtab:
if (actionPanel.expanded)
actionPanel.hide();
return;
case Qt.Key_Return:
case Qt.Key_Enter:
if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
actionPanel.executeSelectedAction();
} else {
controller.executeSelected();
}
return;
case Qt.Key_Menu:
case Qt.Key_F10:
if (contextMenu.hasContextMenuActions(controller.selectedItem)) {
var scenePos = resultsList.getSelectedItemPosition();
var localPos = root.mapFromItem(null, scenePos.x, scenePos.y);
showContextMenu(controller.selectedItem, localPos.x, localPos.y, true);
}
return;
case Qt.Key_1:
if (hasCtrl) {
controller.setMode("all");
return;
}
event.accepted = false;
return;
case Qt.Key_2:
if (hasCtrl) {
controller.setMode("apps");
return;
}
event.accepted = false;
return;
case Qt.Key_3:
if (hasCtrl) {
controller.setMode("files");
return;
}
event.accepted = false;
return;
case Qt.Key_4:
if (hasCtrl) {
controller.setMode("plugins");
return;
}
event.accepted = false;
return;
case Qt.Key_Slash:
if (event.modifiers === Qt.NoModifier && searchField.text.length === 0) {
controller.setMode("files");
return;
}
event.accepted = false;
return;
default:
event.accepted = false;
}
}
Item {
anchors.fill: parent
visible: !editMode
Rectangle {
id: footerBar
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: SettingsData.dankLauncherV2ShowFooter ? 32 : 0
visible: SettingsData.dankLauncherV2ShowFooter
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
Row {
id: modeButtonsRow
x: I18n.isRtl ? parent.width - width - Theme.spacingS : Theme.spacingS
y: (parent.height - height) / 2
spacing: 2
Repeater {
model: [
{
id: "all",
label: I18n.tr("All"),
icon: "search",
shortcut: "⌃1"
},
{
id: "apps",
label: I18n.tr("Apps"),
icon: "apps",
shortcut: "⌃2"
},
{
id: "files",
label: I18n.tr("Files"),
icon: "folder",
shortcut: "⌃3"
},
{
id: "plugins",
label: I18n.tr("Plugins"),
icon: "extension",
shortcut: "⌃4"
}
]
Rectangle {
required property var modelData
required property int index
width: modeButtonMetrics.width + Theme.spacingM * 2
height: footerBar.height - 4
radius: Theme.cornerRadius - 2
color: controller.searchMode === modelData.id || modeArea.containsMouse ? Theme.primaryContainer : "transparent"
TextMetrics {
id: modeButtonMetrics
font.pixelSize: Theme.fontSizeSmall
text: modelData.label + " " + modelData.shortcut
}
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: modelData.icon
size: 14
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceVariantText
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: controller.searchMode === modelData.id ? Theme.primary : Theme.surfaceText
}
StyledText {
text: modelData.shortcut
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.surfaceVariantText
}
}
MouseArea {
id: modeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: controller.setMode(modelData.id)
}
}
}
}
Row {
id: hintsRow
x: I18n.isRtl ? Theme.spacingS : parent.width - width - Theme.spacingS
y: (parent.height - height) / 2
spacing: Theme.spacingM
StyledText {
text: "↑↓ " + I18n.tr("nav")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
StyledText {
text: "↵ " + I18n.tr("open")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
StyledText {
text: "Tab " + I18n.tr("actions")
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
visible: actionPanel.hasActions
}
}
}
Column {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: footerBar.top
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingXS
spacing: Theme.spacingM
clip: false
Row {
width: parent.width
height: 56
spacing: Theme.spacingS
Rectangle {
id: pluginBadge
visible: controller.activePluginName.length > 0
width: visible ? pluginBadgeContent.implicitWidth + Theme.spacingM : 0
height: 32
anchors.verticalCenter: parent.verticalCenter
radius: 16
color: Theme.primary
Row {
id: pluginBadgeContent
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: "extension"
size: 14
color: Theme.primaryText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: controller.activePluginName
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primaryText
}
}
Behavior on width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankTextField {
id: searchField
width: parent.width - (pluginBadge.visible ? pluginBadge.width + Theme.spacingS : 0)
height: 56
cornerRadius: Theme.cornerRadius
backgroundColor: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
normalBorderColor: Theme.outlineMedium
focusedBorderColor: Theme.primary
leftIconName: controller.activePluginId ? "extension" : controller.searchQuery.startsWith("/") ? "folder" : "search"
leftIconSize: Theme.iconSize
leftIconColor: Theme.surfaceVariantText
leftIconFocusedColor: Theme.primary
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
placeholderText: ""
ignoreUpDownKeys: true
ignoreTabKeys: true
keyForwardTargets: [root]
onTextChanged: {
controller.setSearchQuery(text);
if (actionPanel.expanded) {
actionPanel.hide();
}
}
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
if (root.parentModal) {
root.parentModal.hide();
}
event.accepted = true;
} else if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter)) {
if (actionPanel.expanded && actionPanel.selectedActionIndex > 0) {
actionPanel.executeSelectedAction();
} else {
controller.executeSelected();
}
event.accepted = true;
}
}
}
}
Item {
width: parent.width
height: parent.height - 56 - actionPanel.height - Theme.spacingM * 2
opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList {
id: resultsList
anchors.fill: parent
controller: root.controller
onItemRightClicked: (index, item, sceneX, sceneY) => {
if (item && contextMenu.hasContextMenuActions(item)) {
var localPos = root.mapFromItem(null, sceneX, sceneY);
root.showContextMenu(item, localPos.x, localPos.y, false);
}
}
}
}
ActionPanel {
id: actionPanel
width: parent.width
selectedItem: controller.selectedItem
controller: controller
}
}
}
Connections {
target: controller
function onSelectedItemChanged() {
if (actionPanel.expanded && !actionPanel.hasActions) {
actionPanel.hide();
}
}
function onSearchQueryRequested(query) {
searchField.text = query;
}
}
FocusScope {
id: editView
anchors.fill: parent
anchors.margins: Theme.spacingM
visible: editMode
focus: editMode
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
closeEditMode();
event.accepted = true;
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
} else if (event.key === Qt.Key_S && event.modifiers & Qt.ControlModifier) {
saveAppOverride();
event.accepted = true;
}
}
Column {
anchors.fill: parent
spacing: Theme.spacingM
Row {
width: parent.width
spacing: Theme.spacingM
Rectangle {
width: 40
height: 40
radius: Theme.cornerRadius
color: backButtonArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: 20
color: Theme.surfaceText
}
MouseArea {
id: backButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: closeEditMode()
}
}
Image {
width: 40
height: 40
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
sourceSize.width: 40
sourceSize.height: 40
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
spacing: 2
StyledText {
text: I18n.tr("Edit App")
font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: editingApp?.name || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outlineMedium
}
Flickable {
width: parent.width
height: parent.height - y - buttonsRow.height - Theme.spacingM
contentHeight: editFieldsColumn.height
clip: true
boundsBehavior: Flickable.StopAtBounds
Column {
id: editFieldsColumn
width: parent.width
spacing: Theme.spacingS
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Name")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editNameField
width: parent.width
height: 44
placeholderText: editingApp?.name || ""
keyNavigationTab: editIconField
keyNavigationBacktab: editExtraFlagsField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editIconField
width: parent.width
height: 44
placeholderText: editingApp?.icon || ""
keyNavigationTab: editCommentField
keyNavigationBacktab: editNameField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Description")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editCommentField
width: parent.width
height: 44
placeholderText: editingApp?.comment || ""
keyNavigationTab: editEnvVarsField
keyNavigationBacktab: editIconField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Environment Variables")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
StyledText {
text: "KEY=value KEY2=value2"
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
}
DankTextField {
id: editEnvVarsField
width: parent.width
height: 44
placeholderText: "VAR=value"
keyNavigationTab: editExtraFlagsField
keyNavigationBacktab: editCommentField
}
}
Column {
width: parent.width
spacing: 4
StyledText {
text: I18n.tr("Extra Arguments")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
DankTextField {
id: editExtraFlagsField
width: parent.width
height: 44
placeholderText: "--flag --option=value"
keyNavigationTab: editNameField
keyNavigationBacktab: editEnvVarsField
}
}
}
}
Row {
id: buttonsRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingM
Rectangle {
id: resetButton
width: 90
height: 40
radius: Theme.cornerRadius
color: resetButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
visible: SessionData.getAppOverride(editAppId) !== null
StyledText {
text: I18n.tr("Reset")
font.pixelSize: Theme.fontSizeMedium
color: Theme.error
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: resetButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: resetAppOverride()
}
}
Rectangle {
id: cancelButton
width: 90
height: 40
radius: Theme.cornerRadius
color: cancelButtonArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariantAlpha
StyledText {
text: I18n.tr("Cancel")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: cancelButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: closeEditMode()
}
}
Rectangle {
id: saveButton
width: 90
height: 40
radius: Theme.cornerRadius
color: saveButtonArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.9) : Theme.primary
StyledText {
text: I18n.tr("Save")
font.pixelSize: Theme.fontSizeMedium
color: Theme.primaryText
font.weight: Font.Medium
anchors.centerIn: parent
}
MouseArea {
id: saveButtonArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: saveAppOverride()
}
}
}
}
}
}

View File

@@ -0,0 +1,484 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Popup {
id: root
property var item: null
property var controller: null
property var searchField: null
property var parentHandler: null
signal hideRequested
signal editAppRequested(var app)
function hasContextMenuActions(spotlightItem) {
if (!spotlightItem)
return false;
if (spotlightItem.type === "app" && !spotlightItem.isCore)
return true;
if (spotlightItem.type === "plugin" && spotlightItem.pluginId) {
var instance = PluginService.pluginInstances[spotlightItem.pluginId];
if (!instance)
return false;
if (typeof instance.getContextMenuActions !== "function")
return false;
var actions = instance.getContextMenuActions(spotlightItem.data);
return Array.isArray(actions) && actions.length > 0;
}
return false;
}
readonly property var desktopEntry: item?.data ?? null
readonly property string appId: desktopEntry?.id || desktopEntry?.execString || ""
readonly property bool isPinned: SessionData.isPinnedApp(appId)
readonly property bool isRegularApp: item?.type === "app" && !item.isCore && desktopEntry
readonly property bool isPluginItem: item?.type === "plugin"
function getPluginContextMenuActions() {
if (!isPluginItem || !item?.pluginId)
return [];
var instance = PluginService.pluginInstances[item.pluginId];
if (!instance)
return [];
if (typeof instance.getContextMenuActions !== "function")
return [];
var actions = instance.getContextMenuActions(item.data);
if (!Array.isArray(actions))
return [];
return actions;
}
function executePluginAction(actionFunc) {
if (typeof actionFunc === "function") {
actionFunc();
}
controller?.performSearch();
hide();
}
readonly property var menuItems: {
var items = [];
if (isPluginItem) {
var pluginActions = getPluginContextMenuActions();
for (var i = 0; i < pluginActions.length; i++) {
var act = pluginActions[i];
items.push({
type: "item",
icon: act.icon || "play_arrow",
text: act.text || act.name || "",
pluginAction: act.action
});
}
return items;
}
if (!desktopEntry)
return items;
items.push({
type: "item",
icon: isPinned ? "keep_off" : "push_pin",
text: isPinned ? I18n.tr("Unpin from Dock") : I18n.tr("Pin to Dock"),
action: togglePin
});
if (isRegularApp) {
items.push({
type: "item",
icon: "visibility_off",
text: I18n.tr("Hide App"),
action: hideCurrentApp
});
items.push({
type: "item",
icon: "edit",
text: I18n.tr("Edit App"),
action: editCurrentApp
});
}
if (item?.actions && item.actions.length > 0) {
items.push({
type: "separator"
});
for (var i = 0; i < item.actions.length; i++) {
var act = item.actions[i];
items.push({
type: "item",
icon: act.icon || "play_arrow",
text: act.name || "",
actionData: act
});
}
}
items.push({
type: "separator"
});
items.push({
type: "item",
icon: "launch",
text: I18n.tr("Launch"),
action: launchApp
});
if (SessionService.nvidiaCommand) {
items.push({
type: "separator"
});
items.push({
type: "item",
icon: "memory",
text: I18n.tr("Launch on dGPU"),
action: launchWithNvidia
});
}
return items;
}
function show(x, y, spotlightItem, fromKeyboard) {
if (!spotlightItem?.data)
return;
item = spotlightItem;
selectedMenuIndex = fromKeyboard ? 0 : -1;
keyboardNavigation = fromKeyboard;
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();
});
}
function hide() {
if (parentHandler)
parentHandler.enabled = true;
close();
}
function togglePin() {
if (!appId)
return;
if (isPinned)
SessionData.removePinnedApp(appId);
else
SessionData.addPinnedApp(appId);
hide();
}
function hideCurrentApp() {
if (!appId)
return;
SessionData.hideApp(appId);
controller?.performSearch();
hide();
}
function editCurrentApp() {
if (!desktopEntry)
return;
editAppRequested(desktopEntry);
hide();
}
function launchApp() {
if (!desktopEntry)
return;
SessionService.launchDesktopEntry(desktopEntry);
AppUsageHistoryData.addAppUsage(desktopEntry);
controller?.itemExecuted();
hide();
}
function launchWithNvidia() {
if (!desktopEntry)
return;
SessionService.launchDesktopEntry(desktopEntry, true);
AppUsageHistoryData.addAppUsage(desktopEntry);
controller?.itemExecuted();
hide();
}
function executeDesktopAction(actionData) {
if (!desktopEntry || !actionData)
return;
SessionService.launchDesktopAction(desktopEntry, actionData.actionData || actionData);
AppUsageHistoryData.addAppUsage(desktopEntry);
controller?.itemExecuted();
hide();
}
property int selectedMenuIndex: 0
property bool keyboardNavigation: false
readonly property int visibleItemCount: {
var count = 0;
for (var i = 0; i < menuItems.length; i++) {
if (menuItems[i].type === "item")
count++;
}
return count;
}
function selectNext() {
if (visibleItemCount > 0)
selectedMenuIndex = (selectedMenuIndex + 1) % visibleItemCount;
}
function selectPrevious() {
if (visibleItemCount > 0)
selectedMenuIndex = (selectedMenuIndex - 1 + visibleItemCount) % visibleItemCount;
}
function activateSelected() {
var itemIndex = 0;
for (var i = 0; i < menuItems.length; i++) {
if (menuItems[i].type !== "item")
continue;
if (itemIndex === selectedMenuIndex) {
var menuItem = menuItems[i];
if (menuItem.action)
menuItem.action();
else if (menuItem.pluginAction)
executePluginAction(menuItem.pluginAction);
else if (menuItem.actionData)
executeDesktopAction(menuItem.actionData);
return;
}
itemIndex++;
}
}
width: menuContainer.implicitWidth
height: menuContainer.implicitHeight
padding: 0
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
modal: true
dim: false
background: Item {}
onOpened: {
Qt.callLater(() => keyboardHandler.forceActiveFocus());
}
onClosed: {
if (parentHandler)
parentHandler.enabled = true;
if (searchField?.visible) {
Qt.callLater(() => searchField.forceActiveFocus());
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
contentItem: Item {
id: keyboardHandler
focus: true
implicitWidth: menuContainer.implicitWidth
implicitHeight: menuContainer.implicitHeight
Keys.onPressed: event => {
switch (event.key) {
case Qt.Key_Down:
root.selectNext();
event.accepted = true;
return;
case Qt.Key_Up:
root.selectPrevious();
event.accepted = true;
return;
case Qt.Key_Return:
case Qt.Key_Enter:
root.activateSelected();
event.accepted = true;
return;
case Qt.Key_Escape:
case Qt.Key_Left:
root.hide();
event.accepted = true;
return;
}
}
Rectangle {
id: menuContainer
anchors.fill: parent
implicitWidth: Math.max(180, menuColumn.implicitWidth + Theme.spacingS * 2)
implicitHeight: menuColumn.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
border.width: 1
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: -1
}
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 1
Repeater {
model: root.menuItems
Item {
id: menuItemDelegate
required property var modelData
required property int index
width: menuColumn.width
height: modelData.type === "separator" ? 5 : 32
readonly property int itemIndex: {
var count = 0;
for (var i = 0; i < index; i++) {
if (root.menuItems[i].type === "item")
count++;
}
return count;
}
Rectangle {
visible: menuItemDelegate.modelData.type === "separator"
width: parent.width - Theme.spacingS * 2
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width
height: 1
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
}
}
Rectangle {
visible: menuItemDelegate.modelData.type === "item"
width: parent.width
height: parent.height
radius: Theme.cornerRadius
color: {
if (root.keyboardNavigation && root.selectedMenuIndex === menuItemDelegate.itemIndex) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2);
}
return itemMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent";
}
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Item {
width: Theme.iconSize - 2
height: Theme.iconSize - 2
anchors.verticalCenter: parent.verticalCenter
DankIcon {
visible: (menuItemDelegate.modelData?.icon ?? "").length > 0
name: menuItemDelegate.modelData?.icon ?? ""
size: Theme.iconSize - 2
color: Theme.surfaceText
opacity: 0.7
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: menuItemDelegate.modelData.text || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - (Theme.iconSize - 2) - Theme.spacingS
}
}
MouseArea {
id: itemMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
root.keyboardNavigation = false;
root.selectedMenuIndex = menuItemDelegate.itemIndex;
}
onClicked: {
var menuItem = menuItemDelegate.modelData;
if (menuItem.action)
menuItem.action();
else if (menuItem.pluginAction)
root.executePluginAction(menuItem.pluginAction);
else if (menuItem.actionData)
root.executeDesktopAction(menuItem.actionData);
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,189 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property var item: null
property bool isSelected: false
property bool isHovered: itemArea.containsMouse
property var controller: null
property int flatIndex: -1
signal clicked
signal rightClicked(real mouseX, real mouseY)
width: parent?.width ?? 200
height: 52
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
radius: Theme.cornerRadius
Row {
anchors.fill: parent
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingM
Item {
width: 36
height: 36
anchors.verticalCenter: parent.verticalCenter
Image {
id: appIcon
anchors.fill: parent
visible: root.item?.iconType === "image"
asynchronous: true
source: root.item?.iconType === "image" ? "image://icon/" + (root.item?.icon || "application-x-executable") : ""
sourceSize.width: 36
sourceSize.height: 36
fillMode: Image.PreserveAspectFit
cache: false
}
DankIcon {
anchors.centerIn: parent
visible: root.item?.iconType === "material" || root.item?.iconType === "nerd"
name: root.item?.icon ?? "apps"
size: 24
color: Theme.surfaceText
}
Item {
anchors.fill: parent
visible: root.item?.iconType === "composite"
Image {
anchors.fill: parent
asynchronous: true
source: {
if (!root.item || root.item.iconType !== "composite")
return "";
var iconFull = root.item.iconFull || "";
if (iconFull.startsWith("svg+corner:")) {
var parts = iconFull.substring(11).split("|");
return parts[0] || "";
}
return "";
}
sourceSize.width: 36
sourceSize.height: 36
fillMode: Image.PreserveAspectFit
}
Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
width: 16
height: 16
radius: 8
color: Theme.surfaceContainer
DankIcon {
anchors.centerIn: parent
name: {
if (!root.item || root.item.iconType !== "composite")
return "";
var iconFull = root.item.iconFull || "";
if (iconFull.startsWith("svg+corner:")) {
var parts = iconFull.substring(11).split("|");
return parts[1] || "";
}
return "";
}
size: 12
color: Theme.surfaceText
}
}
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 36 - Theme.spacingM * 3 - rightContent.width
spacing: 2
StyledText {
width: parent.width
text: root.item?.name ?? ""
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
horizontalAlignment: Text.AlignLeft
}
StyledText {
width: parent.width
text: root.item?.subtitle ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
visible: text.length > 0
horizontalAlignment: Text.AlignLeft
}
}
Row {
id: rightContent
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Rectangle {
visible: root.item?.type && root.item.type !== "app"
width: typeBadge.implicitWidth + Theme.spacingS * 2
height: 20
radius: 10
color: Theme.surfaceVariantAlpha
anchors.verticalCenter: parent.verticalCenter
StyledText {
id: typeBadge
anchors.centerIn: parent
text: {
if (!root.item)
return "";
switch (root.item.type) {
case "calculator":
return I18n.tr("Calc");
case "plugin":
return I18n.tr("Plugin");
case "file":
return I18n.tr("File");
default:
return "";
}
}
font.pixelSize: Theme.fontSizeSmall - 2
color: Theme.surfaceVariantText
}
}
}
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y);
root.rightClicked(scenePos.x, scenePos.y);
} else {
root.clicked();
}
}
onPositionChanged: {
if (root.controller) {
root.controller.keyboardNavigationActive = false;
}
}
}
}

View File

@@ -0,0 +1,489 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property var controller: null
property int gridColumns: controller?.gridColumns ?? 4
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
function resetScroll() {
mainFlickable.contentY = 0;
}
function ensureVisible(index) {
if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length)
return;
var entry = controller.flatModel[index];
if (!entry || entry.isHeader)
return;
scrollItemIntoView(index, entry.sectionId);
}
function scrollItemIntoView(flatIndex, sectionId) {
var sections = controller?.sections ?? [];
var sectionIndex = -1;
for (var i = 0; i < sections.length; i++) {
if (sections[i].id === sectionId) {
sectionIndex = i;
break;
}
}
if (sectionIndex < 0)
return;
var itemInSection = 0;
var foundSection = false;
for (var i = 0; i < controller.flatModel.length && i < flatIndex; i++) {
var e = controller.flatModel[i];
if (e.isHeader && e.section?.id === sectionId)
foundSection = true;
else if (foundSection && !e.isHeader && e.sectionId === sectionId)
itemInSection++;
}
var mode = controller.getSectionViewMode(sectionId);
var sectionY = 0;
for (var i = 0; i < sectionIndex; i++) {
sectionY += getSectionHeight(sections[i]);
}
var itemY, itemHeight;
if (mode === "list") {
itemY = itemInSection * 52;
itemHeight = 52;
} else {
var cols = controller.getGridColumns(sectionId);
var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor((mainFlickable.width - (root.gridColumns - 1) * 4) / root.gridColumns);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var row = Math.floor(itemInSection / cols);
itemY = row * cellHeight;
itemHeight = cellHeight;
}
var targetY = sectionY + 32 + itemY;
var targetBottom = targetY + itemHeight;
var stickyHeight = stickyHeader.visible ? 32 : 0;
var shadowPadding = 24;
if (targetY < mainFlickable.contentY + stickyHeight) {
mainFlickable.contentY = Math.max(0, targetY - stickyHeight);
} else if (targetBottom > mainFlickable.contentY + mainFlickable.height - shadowPadding) {
mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, targetBottom - mainFlickable.height + shadowPadding);
}
}
function getSectionHeight(section) {
var mode = controller?.getSectionViewMode(section.id) ?? "list";
if (section.collapsed)
return 32;
if (mode === "list") {
return 32 + (section.items?.length ?? 0) * 52;
} else {
var cols = controller?.getGridColumns(section.id) ?? root.gridColumns;
var rows = Math.ceil((section.items?.length ?? 0) / cols);
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor((root.width - (cols - 1) * 4) / cols);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
return 32 + rows * cellHeight;
}
}
function getSelectedItemPosition() {
var fallback = mapToItem(null, width / 2, height / 2);
if (!controller?.flatModel || controller.selectedFlatIndex < 0)
return fallback;
var entry = controller.flatModel[controller.selectedFlatIndex];
if (!entry || entry.isHeader)
return fallback;
var sections = controller.sections;
var sectionIndex = -1;
for (var i = 0; i < sections.length; i++) {
if (sections[i].id === entry.sectionId) {
sectionIndex = i;
break;
}
}
if (sectionIndex < 0)
return fallback;
var sectionY = 0;
for (var i = 0; i < sectionIndex; i++) {
sectionY += getSectionHeight(sections[i]);
}
var mode = controller.getSectionViewMode(entry.sectionId);
var itemInSection = entry.indexInSection || 0;
var itemY, itemX, itemH;
if (mode === "list") {
itemY = sectionY + 32 + itemInSection * 52;
itemX = width / 2;
itemH = 52;
} else {
var cols = controller.getGridColumns(entry.sectionId);
var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor((width - (cols - 1) * 4) / cols);
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
var row = Math.floor(itemInSection / cols);
var col = itemInSection % cols;
itemY = sectionY + 32 + row * cellHeight;
itemX = col * cellWidth + cellWidth / 2;
itemH = cellHeight;
}
var visualY = itemY - mainFlickable.contentY + itemH / 2;
var clampedY = Math.max(40, Math.min(height - 40, visualY));
return mapToItem(null, itemX, clampedY);
}
Connections {
target: root.controller
function onSelectedFlatIndexChanged() {
if (root.controller?.keyboardNavigationActive) {
Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
}
}
}
DankFlickable {
id: mainFlickable
anchors.fill: parent
contentWidth: width
contentHeight: sectionsColumn.height
clip: true
Column {
id: sectionsColumn
width: parent.width
Repeater {
model: root.controller?.sections ?? []
Column {
id: sectionDelegate
required property var modelData
required property int index
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
readonly property string sectionId: modelData?.id ?? ""
readonly property string currentViewMode: {
void (versionTrigger);
return root.controller?.getSectionViewMode(sectionId) ?? "list";
}
readonly property bool isGridMode: currentViewMode === "grid" || currentViewMode === "tile"
readonly property bool isCollapsed: modelData?.collapsed ?? false
width: sectionsColumn.width
SectionHeader {
width: parent.width
height: 32
section: sectionDelegate.modelData
controller: root.controller
viewMode: sectionDelegate.currentViewMode
canChangeViewMode: root.controller?.canChangeSectionViewMode(sectionDelegate.sectionId) ?? false
canCollapse: root.controller?.canCollapseSection(sectionDelegate.sectionId) ?? false
}
Column {
id: listContent
width: parent.width
visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
Repeater {
model: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? [])
ResultItem {
required property var modelData
required property int index
width: listContent.width
height: 52
item: modelData
isSelected: getFlatIndex() === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: getFlatIndex()
function getFlatIndex() {
if (!sectionDelegate?.sectionId)
return -1;
var flatIdx = 0;
var sections = root.controller?.sections ?? [];
for (var i = 0; i < sections.length; i++) {
flatIdx++;
if (sections[i].id === sectionDelegate.sectionId)
return flatIdx + index;
if (!sections[i].collapsed)
flatIdx += sections[i].items?.length ?? 0;
}
return -1;
}
onClicked: {
if (root.controller) {
root.controller.executeItem(modelData);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(getFlatIndex(), modelData, mouseX, mouseY);
}
}
}
}
Grid {
id: gridContent
width: parent.width
visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns
readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor((width - (root.gridColumns - 1) * 4) / root.gridColumns)
readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24
Repeater {
model: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : []
Item {
id: gridDelegateItem
required property var modelData
required property int index
width: gridContent.cellWidth
height: gridContent.cellHeight
function getFlatIndex() {
if (!sectionDelegate?.sectionId)
return -1;
var flatIdx = 0;
var sections = root.controller?.sections ?? [];
for (var i = 0; i < sections.length; i++) {
flatIdx++;
if (sections[i].id === sectionDelegate.sectionId)
return flatIdx + index;
if (!sections[i].collapsed)
flatIdx += sections[i].items?.length ?? 0;
}
return -1;
}
readonly property int cachedFlatIndex: getFlatIndex()
GridItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: sectionDelegate.currentViewMode === "grid"
item: gridDelegateItem.modelData
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridDelegateItem.cachedFlatIndex
onClicked: {
if (root.controller) {
root.controller.executeItem(gridDelegateItem.modelData);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
}
}
TileItem {
width: parent.width - 4
height: parent.height - 4
anchors.centerIn: parent
visible: sectionDelegate.currentViewMode === "tile"
item: gridDelegateItem.modelData
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: gridDelegateItem.cachedFlatIndex
onClicked: {
if (root.controller) {
root.controller.executeItem(gridDelegateItem.modelData);
}
}
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
}
}
}
}
}
}
}
}
}
Rectangle {
id: bottomShadow
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 24
z: 100
visible: {
if (mainFlickable.contentHeight <= mainFlickable.height)
return false;
var atBottom = mainFlickable.contentY >= mainFlickable.contentHeight - mainFlickable.height - 5;
if (atBottom)
return false;
var flatModel = root.controller?.flatModel;
if (!flatModel || flatModel.length === 0)
return false;
var lastItemIdx = -1;
for (var i = flatModel.length - 1; i >= 0; i--) {
if (!flatModel[i].isHeader) {
lastItemIdx = i;
break;
}
}
if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx)
return false;
return true;
}
gradient: Gradient {
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
}
}
}
Rectangle {
id: stickyHeader
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 32
z: 101
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: stickyHeaderSection !== null
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
readonly property var stickyHeaderSection: {
if (!root.controller?.sections || root.controller.sections.length === 0)
return null;
var sections = root.controller.sections;
if (sections.length === 0)
return null;
var scrollY = mainFlickable.contentY;
if (scrollY <= 32)
return null;
var y = 0;
for (var i = 0; i < sections.length; i++) {
var section = sections[i];
var sectionHeight = root.getSectionHeight(section);
if (scrollY < y + sectionHeight)
return section;
y += sectionHeight;
}
return sections[sections.length - 1];
}
SectionHeader {
width: parent.width
section: stickyHeader.stickyHeaderSection
controller: root.controller
viewMode: {
void (stickyHeader.versionTrigger);
return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list";
}
canChangeViewMode: {
void (stickyHeader.versionTrigger);
return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false;
}
canCollapse: {
void (stickyHeader.versionTrigger);
return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
}
isSticky: true
}
}
Item {
anchors.centerIn: parent
visible: !root.controller?.sections || root.controller.sections.length === 0
width: emptyColumn.implicitWidth
height: emptyColumn.implicitHeight
Column {
id: emptyColumn
spacing: Theme.spacingM
DankIcon {
anchors.horizontalCenter: parent.horizontalCenter
name: getEmptyIcon()
size: 48
color: Theme.outlineButton
function getEmptyIcon() {
var mode = root.controller?.searchMode ?? "all";
if (mode === "files")
return "folder_open";
if (mode === "plugins")
return "extension";
if (mode === "apps")
return "apps";
if (root.controller?.searchQuery?.length > 0)
return "search_off";
return "search";
}
}
StyledText {
anchors.horizontalCenter: parent.horizontalCenter
text: getEmptyText()
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignHCenter
function getEmptyText() {
var mode = root.controller?.searchMode ?? "all";
var hasQuery = root.controller?.searchQuery?.length > 0;
if (mode === "files") {
if (!DSearchService.dsearchAvailable)
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
if (!hasQuery)
return I18n.tr("Type to search files");
if (root.controller.searchQuery.length < 2)
return I18n.tr("Type at least 2 characters");
return I18n.tr("No files found");
}
if (mode === "plugins") {
if (!hasQuery)
return I18n.tr("Browse or search plugins");
return I18n.tr("No plugin results");
}
if (mode === "apps") {
if (!hasQuery)
return I18n.tr("Type to search apps");
return I18n.tr("No apps found");
}
if (hasQuery)
return I18n.tr("No results found");
return I18n.tr("Type to search");
}
}
}
}
}

View File

@@ -0,0 +1,245 @@
.pragma library
const Weights = {
exactMatch: 10000,
prefixMatch: 5000,
wordBoundary: 1000,
substring: 500,
fuzzy: 100,
frecency: 2000,
typeBonus: {
app: 1000,
plugin: 900,
file: 800,
action: 600
}
}
function tokenize(text) {
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function(w) { return w.length > 0 })
}
function hasWordBoundaryMatch(text, query) {
var textWords = tokenize(text)
var queryWords = tokenize(query)
if (queryWords.length === 0) return false
if (queryWords.length > textWords.length) return false
for (var i = 0; i <= textWords.length - queryWords.length; i++) {
var allMatch = true
for (var j = 0; j < queryWords.length; j++) {
if (!textWords[i + j].startsWith(queryWords[j])) {
allMatch = false
break
}
}
if (allMatch) return true
}
return false
}
function levenshteinDistance(s1, s2) {
var len1 = s1.length
var len2 = s2.length
var matrix = []
for (var i = 0; i <= len1; i++) {
matrix[i] = [i]
}
for (var j = 0; j <= len2; j++) {
matrix[0][j] = j
}
for (var i = 1; i <= len1; i++) {
for (var j = 1; j <= len2; j++) {
var cost = s1[i - 1] === s2[j - 1] ? 0 : 1
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j - 1] + cost
)
}
}
return matrix[len1][len2]
}
function fuzzyScore(text, query) {
var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3
var bestScore = 0
if (Math.abs(text.length - query.length) <= maxDistance) {
var distance = levenshteinDistance(text, query)
if (distance <= maxDistance) {
var maxLen = Math.max(text.length, query.length)
bestScore = 1 - (distance / maxLen)
}
}
var words = tokenize(text)
for (var i = 0; i < words.length && bestScore < 0.8; i++) {
if (Math.abs(words[i].length - query.length) > maxDistance) continue
var wordDistance = levenshteinDistance(words[i], query)
if (wordDistance <= maxDistance) {
var wordMaxLen = Math.max(words[i].length, query.length)
var score = 1 - (wordDistance / wordMaxLen)
bestScore = Math.max(bestScore, score)
}
}
return bestScore
}
function getTimeBucketWeight(daysSinceUsed) {
for (var i = 0; i < TimeBuckets.length; i++) {
if (daysSinceUsed <= TimeBuckets[i].maxDays) {
return TimeBuckets[i].weight
}
}
return 10
}
function calculateTextScore(name, query) {
if (name === query) return Weights.exactMatch
if (name.startsWith(query)) return Weights.prefixMatch
if (name.includes(query)) return Weights.substring
if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
if (query.length >= 3) {
var fs = fuzzyScore(name, query)
if (fs > 0) return fs * Weights.fuzzy
}
return 0
}
function score(item, query, frecencyData) {
var typeBonus = Weights.typeBonus[item.type] || 0
if (!query || query.length === 0) {
var usageCount = frecencyData ? frecencyData.usageCount : 0
return typeBonus + (usageCount * 100)
}
var name = (item.name || "").toLowerCase()
var q = query.toLowerCase()
var textScore = calculateTextScore(name, q)
if (textScore === 0 && item.subtitle) {
var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q)
textScore = subtitleScore * 0.5
}
if (textScore === 0 && item.keywords) {
for (var i = 0; i < item.keywords.length; i++) {
var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q)
if (keywordScore > 0) {
textScore = keywordScore * 0.3
break
}
}
}
if (textScore === 0) return 0
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 10, Weights.frecency) : 0
return textScore + usageBonus + typeBonus
}
function scoreItems(items, query, getFrecencyFn) {
var scored = []
for (var i = 0; i < items.length; i++) {
var item = items[i]
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
var itemScore = score(item, query, frecencyData)
if (itemScore > 0 || !query || query.length === 0) {
scored.push({
item: item,
score: itemScore
})
}
}
scored.sort(function(a, b) {
return b.score - a.score
})
return scored
}
function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) {
var sections = {}
var result = []
var limit = maxPerSection || 50
for (var i = 0; i < sectionOrder.length; i++) {
var sectionId = sectionOrder[i].id
sections[sectionId] = {
id: sectionId,
title: sectionOrder[i].title,
icon: sectionOrder[i].icon,
priority: sectionOrder[i].priority,
items: [],
collapsed: false
}
}
for (var i = 0; i < scoredItems.length; i++) {
var scoredItem = scoredItems[i]
var item = scoredItem.item
var sectionId = item.section || "apps"
if (sections[sectionId] && sections[sectionId].items.length < limit) {
sections[sectionId].items.push(item)
} else if (sections["apps"] && sections["apps"].items.length < limit) {
sections["apps"].items.push(item)
}
}
for (var i = 0; i < sectionOrder.length; i++) {
var section = sections[sectionOrder[i].id]
if (section && section.items.length > 0) {
if (sortAlphabetically && section.id === "apps") {
section.items.sort(function(a, b) {
return (a.name || "").localeCompare(b.name || "")
})
}
result.push(section)
}
}
return result
}
function flattenSections(sections) {
var flat = []
for (var i = 0; i < sections.length; i++) {
var section = sections[i]
flat.push({
isHeader: true,
section: section,
sectionId: section.id,
sectionIndex: i
})
if (!section.collapsed) {
for (var j = 0; j < section.items.length; j++) {
flat.push({
isHeader: false,
item: section.items[j],
sectionId: section.id,
sectionIndex: i,
indexInSection: j
})
}
}
}
return flat
}

View File

@@ -0,0 +1,114 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
Item {
id: root
property var section: null
property var controller: null
property string viewMode: "list"
property int gridColumns: 4
property int startIndex: 0
signal itemClicked(int flatIndex)
signal itemRightClicked(int flatIndex, var item, real mouseX, real mouseY)
height: headerItem.height + (section?.collapsed ? 0 : contentLoader.height + Theme.spacingXS)
width: parent?.width ?? 200
SectionHeader {
id: headerItem
width: parent.width
section: root.section
controller: root.controller
viewMode: root.viewMode
canChangeViewMode: root.controller?.canChangeSectionViewMode(root.section?.id) ?? true
onViewModeToggled: {
if (root.controller && root.section) {
var newMode = root.viewMode === "list" ? "grid" : "list";
root.controller.setSectionViewMode(root.section.id, newMode);
}
}
}
Loader {
id: contentLoader
anchors.top: headerItem.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: Theme.spacingXS
active: !root.section?.collapsed
visible: active
sourceComponent: root.viewMode === "grid" ? gridComponent : listComponent
Component {
id: listComponent
Column {
spacing: 2
width: contentLoader.width
Repeater {
model: ScriptModel {
values: root.section?.items ?? []
objectProp: "id"
}
ResultItem {
required property var modelData
required property int index
width: parent?.width ?? 200
item: modelData
isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: root.startIndex + index
onClicked: root.itemClicked(root.startIndex + index)
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
}
}
}
}
}
Component {
id: gridComponent
Flow {
width: contentLoader.width
spacing: 4
Repeater {
model: ScriptModel {
values: root.section?.items ?? []
objectProp: "id"
}
GridItem {
required property var modelData
required property int index
width: Math.floor((contentLoader.width - (root.gridColumns - 1) * 4) / root.gridColumns)
height: width + 24
item: modelData
isSelected: (root.startIndex + index) === root.controller?.selectedFlatIndex
controller: root.controller
flatIndex: root.startIndex + index
onClicked: root.itemClicked(root.startIndex + index)
onRightClicked: (mouseX, mouseY) => {
root.itemRightClicked(root.startIndex + index, modelData, mouseX, mouseY);
}
}
}
}
}
}
}

View File

@@ -0,0 +1,169 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property var section: null
property var controller: null
property string viewMode: "list"
property bool canChangeViewMode: true
property bool canCollapse: true
property bool isSticky: false
signal viewModeToggled
width: parent?.width ?? 200
height: 32
color: isSticky ? "transparent" : (hoverArea.containsMouse ? Theme.surfaceHover : "transparent")
radius: Theme.cornerRadius / 2
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
}
Row {
id: leftContent
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: root.section?.icon ?? "folder"
size: 16
color: Theme.surfaceVariantText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.section?.title ?? ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.section?.items?.length ?? 0
font.pixelSize: Theme.fontSizeSmall
color: Theme.outlineButton
}
}
Row {
id: rightContent
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
Row {
id: viewModeRow
anchors.verticalCenter: parent.verticalCenter
spacing: 2
visible: root.canChangeViewMode && !root.section?.collapsed
Repeater {
model: [
{
mode: "list",
icon: "view_list"
},
{
mode: "grid",
icon: "grid_view"
},
{
mode: "tile",
icon: "view_module"
}
]
Rectangle {
required property var modelData
required property int index
width: 20
height: 20
radius: 4
color: root.viewMode === modelData.mode ? Theme.primaryHover : modeArea.containsMouse ? Theme.surfaceHover : "transparent"
DankIcon {
anchors.centerIn: parent
name: parent.modelData.icon
size: 14
color: root.viewMode === parent.modelData.mode ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: modeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.viewMode !== parent.modelData.mode && root.controller && root.section) {
root.controller.setSectionViewMode(root.section.id, parent.modelData.mode);
}
}
}
}
}
}
Item {
id: collapseButton
width: root.canCollapse ? 24 : 0
height: 24
visible: root.canCollapse
anchors.verticalCenter: parent.verticalCenter
DankIcon {
anchors.centerIn: parent
name: root.section?.collapsed ? "expand_more" : "expand_less"
size: 16
color: collapseArea.containsMouse ? Theme.primary : Theme.surfaceVariantText
}
MouseArea {
id: collapseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.controller && root.section) {
root.controller.toggleSection(root.section.id);
}
}
}
}
}
MouseArea {
anchors.fill: parent
anchors.rightMargin: rightContent.width + Theme.spacingS
cursorShape: root.canCollapse ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.canCollapse
onClicked: {
if (root.canCollapse && root.controller && root.section) {
root.controller.toggleSection(root.section.id);
}
}
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.outlineMedium
visible: root.isSticky
}
}

View File

@@ -0,0 +1,147 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
id: root
property var item: null
property bool isSelected: false
property bool isHovered: itemArea.containsMouse
property var controller: null
property int flatIndex: -1
signal clicked
signal rightClicked(real mouseX, real mouseY)
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryPressed : isHovered ? Theme.primaryPressed : "transparent"
border.width: isSelected ? 2 : 0
border.color: Theme.primary
readonly property string imageSource: {
if (!item?.data)
return "";
var data = item.data;
if (data.imageUrl)
return data.imageUrl;
if (data.imagePath)
return data.imagePath;
if (data.path && isImageFile(data.path))
return data.path;
return "";
}
readonly property bool useImage: imageSource.length > 0
readonly property bool useIconProvider: !useImage && item?.iconType === "image"
function isImageFile(path) {
if (!path)
return false;
var ext = path.split('.').pop().toLowerCase();
return ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp"].indexOf(ext) >= 0;
}
Item {
anchors.fill: parent
anchors.margins: 4
Rectangle {
id: imageContainer
anchors.fill: parent
radius: Theme.cornerRadius - 2
color: Theme.surfaceContainerHigh
clip: true
CachingImage {
anchors.fill: parent
visible: root.useImage
imagePath: root.imageSource
maxCacheSize: 256
}
Image {
id: iconImage
anchors.fill: parent
visible: root.useIconProvider
source: root.useIconProvider ? "image://icon/" + (root.item?.icon || "application-x-executable") : ""
sourceSize.width: parent.width * 2
sourceSize.height: parent.height * 2
fillMode: Image.PreserveAspectCrop
cache: false
asynchronous: true
}
DankIcon {
anchors.centerIn: parent
visible: !root.useImage && !root.useIconProvider
name: root.item?.icon ?? "image"
size: Math.min(parent.width, parent.height) * 0.4
color: Theme.surfaceVariantText
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: labelText.implicitHeight + Theme.spacingS * 2
color: Theme.withAlpha(Theme.surfaceContainer, 0.85)
visible: root.item?.name?.length > 0
StyledText {
id: labelText
anchors.fill: parent
anchors.margins: Theme.spacingXS
text: root.item?.name ?? ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
Rectangle {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingXS
width: 20
height: 20
radius: 10
color: Theme.primary
visible: root.isSelected
DankIcon {
anchors.centerIn: parent
name: "check"
size: 14
color: Theme.primaryText
}
}
}
}
MouseArea {
id: itemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.RightButton) {
var scenePos = mapToItem(null, mouse.x, mouse.y);
root.rightClicked(scenePos.x, scenePos.y);
return;
}
root.clicked();
}
onPositionChanged: {
if (root.controller)
root.controller.keyboardNavigationActive = false;
}
}
}

View File

@@ -73,6 +73,9 @@ DankPopout {
root.close();
};
}
if (item && "parentPopout" in item) {
item.parentPopout = root;
}
if (item) {
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
}

View File

@@ -9,6 +9,7 @@ Column {
property string detailsText: ""
property bool showCloseButton: false
property var closePopout: null
property var parentPopout: null
property alias headerActions: headerActionsLoader.sourceComponent
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0

View File

@@ -318,7 +318,7 @@ Popup {
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: modelData.enabled
enabled: modelData.enabled ?? false
onEntered: {
keyboardNavigation = false;
selectedIndex = index;

View File

@@ -353,6 +353,116 @@ Item {
}
}
SettingsCard {
width: parent.width
iconName: "tune"
title: I18n.tr("Appearance", "launcher appearance settings")
settingKey: "dankLauncherV2Appearance"
Column {
width: parent.width
spacing: Theme.spacingM
StyledText {
text: I18n.tr("Size", "launcher size option")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Item {
width: parent.width
height: sizeGroup.implicitHeight
clip: true
DankButtonGroup {
id: sizeGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 400 ? 60 : 80
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Compact", "compact launcher size"), I18n.tr("Medium", "medium launcher size"), I18n.tr("Large", "large launcher size")]
currentIndex: SettingsData.dankLauncherV2Size === "compact" ? 0 : SettingsData.dankLauncherV2Size === "large" ? 2 : 1
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("dankLauncherV2Size", index === 0 ? "compact" : index === 2 ? "large" : "medium");
}
}
}
}
SettingsToggleRow {
settingKey: "dankLauncherV2ShowFooter"
tags: ["launcher", "footer", "hints", "shortcuts"]
text: I18n.tr("Show Footer", "launcher footer visibility")
description: I18n.tr("Show mode tabs and keyboard hints at the bottom.", "launcher footer description")
checked: SettingsData.dankLauncherV2ShowFooter
onToggled: checked => SettingsData.set("dankLauncherV2ShowFooter", checked)
}
SettingsToggleRow {
settingKey: "dankLauncherV2BorderEnabled"
tags: ["launcher", "border", "outline"]
text: I18n.tr("Border", "launcher border option")
checked: SettingsData.dankLauncherV2BorderEnabled
onToggled: checked => SettingsData.set("dankLauncherV2BorderEnabled", checked)
}
Column {
width: parent.width
spacing: Theme.spacingM
visible: SettingsData.dankLauncherV2BorderEnabled
SettingsSliderRow {
settingKey: "dankLauncherV2BorderThickness"
tags: ["launcher", "border", "thickness"]
text: I18n.tr("Thickness", "border thickness")
minimum: 1
maximum: 6
value: SettingsData.dankLauncherV2BorderThickness
defaultValue: 2
unit: "px"
onSliderValueChanged: newValue => SettingsData.set("dankLauncherV2BorderThickness", newValue)
}
Column {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Color", "border color")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter
}
Item {
width: parent.width
height: borderColorGroup.implicitHeight
clip: true
DankButtonGroup {
id: borderColorGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 400 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 400 ? 50 : 70
textSize: parent.width < 400 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Primary", "primary color"), I18n.tr("Secondary", "secondary color"), I18n.tr("Outline", "outline color"), I18n.tr("Text", "text color")]
currentIndex: SettingsData.dankLauncherV2BorderColor === "secondary" ? 1 : SettingsData.dankLauncherV2BorderColor === "outline" ? 2 : SettingsData.dankLauncherV2BorderColor === "surfaceText" ? 3 : 0
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("dankLauncherV2BorderColor", index === 1 ? "secondary" : index === 2 ? "outline" : index === 3 ? "surfaceText" : "primary");
}
}
}
}
}
}
SettingsCard {
width: parent.width
iconName: "open_in_new"

View File

@@ -3,7 +3,7 @@ import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import qs.Common
import qs.Modals.Spotlight
import qs.Modals.DankLauncherV2
import qs.Services
Scope {
@@ -67,12 +67,7 @@ Scope {
hideSpotlight();
}
Loader {
id: niriOverlayLoader
active: overlayActive || isClosing
asynchronous: false
sourceComponent: Variants {
Variants {
id: overlayVariants
model: Quickshell.screens
@@ -84,6 +79,7 @@ Scope {
readonly property bool isActiveScreen: screen.name === NiriService.currentOutput
readonly property bool shouldShowSpotlight: niriOverviewScope.searchActive && screen.name === niriOverviewScope.searchActiveScreen && !niriOverviewScope.isClosing
readonly property bool isSpotlightScreen: screen.name === niriOverviewScope.searchActiveScreen
readonly property bool overlayVisible: NiriService.inOverview || niriOverviewScope.isClosing
property bool hasActivePopout: !!PopoutManager.currentPopoutsByScreen[screen.name]
property bool hasActiveModal: !!ModalManager.currentModalsByScreen[screen.name]
@@ -102,7 +98,7 @@ Scope {
}
screen: modelData
visible: NiriService.inOverview || niriOverviewScope.isClosing
visible: true
color: "transparent"
WlrLayershell.namespace: "dms:niri-overview-spotlight"
@@ -121,13 +117,13 @@ Scope {
}
mask: Region {
item: spotlightContainer.visible ? spotlightContainer : null
item: overlayVisible && spotlightContainer.visible ? spotlightContainer : null
}
onShouldShowSpotlightChanged: {
if (shouldShowSpotlight) {
if (spotlightContent?.appLauncher)
spotlightContent.appLauncher.ensureInitialized();
if (launcherContent?.controller)
launcherContent.controller.performSearch();
return;
}
if (!isActiveScreen)
@@ -187,15 +183,13 @@ Scope {
if (event.isAutoRepeat || !event.text)
return;
if (!spotlightContent?.searchField)
if (!launcherContent?.searchField)
return;
const trimmedText = event.text.trim();
spotlightContent.searchField.text = trimmedText;
if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = trimmedText;
}
launcherContent.searchField.text = trimmedText;
launcherContent.controller.setSearchQuery(trimmedText);
niriOverviewScope.showSpotlight(overlayWindow.screen.name);
Qt.callLater(() => spotlightContent.searchField.forceActiveFocus());
Qt.callLater(() => launcherContent.searchField.forceActiveFocus());
event.accepted = true;
}
}
@@ -221,9 +215,9 @@ Scope {
Behavior on scale {
id: scaleAnimation
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
onRunningChanged: {
if (running || !spotlightContainer.animatingOut)
return;
@@ -234,9 +228,9 @@ Scope {
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
}
}
@@ -248,14 +242,14 @@ Scope {
border.width: 1
}
SpotlightContent {
id: spotlightContent
LauncherContent {
id: launcherContent
anchors.fill: parent
anchors.margins: 0
usePopupContextMenu: true
property var fakeParentModal: QtObject {
property bool spotlightOpen: spotlightContainer.visible
property bool isClosing: niriOverviewScope.isClosing
function hide() {
if (niriOverviewScope.searchActive) {
niriOverviewScope.hideAndReleaseKeyboard();
@@ -266,9 +260,9 @@ Scope {
}
Connections {
target: spotlightContent.searchField
target: launcherContent.searchField
function onTextChanged() {
if (spotlightContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
return;
niriOverviewScope.hideSpotlight();
}
@@ -279,15 +273,8 @@ Scope {
}
Connections {
target: spotlightContent.appLauncher
function onAppLaunched() {
niriOverviewScope.releaseKeyboard = true;
}
}
Connections {
target: spotlightContent.fileSearchController
function onFileOpened() {
target: launcherContent.controller
function onItemExecuted() {
niriOverviewScope.releaseKeyboard = true;
}
}
@@ -296,4 +283,3 @@ Scope {
}
}
}
}

View File

@@ -0,0 +1,113 @@
import QtQuick
import Quickshell
import qs.Services
QtObject {
id: root
property var pluginService: null
property string trigger: "img"
signal itemsChanged
readonly property var images: [
{
name: "DankDash",
imageUrl: "https://danklinux.com/img/dankdash.png",
comment: "DankMaterialShell Dashboard"
},
{
name: "Control Center",
imageUrl: "https://danklinux.com/img/cc.png",
comment: "System Control Center"
},
{
name: "Desktop",
imageUrl: "https://danklinux.com/img/desktop.png",
comment: "Desktop Environment"
},
{
name: "Search",
imageUrl: "https://danklinux.com/img/dsearch.png",
comment: "Application Search"
},
{
name: "Theme Registry",
imageUrl: "https://danklinux.com/img/blog/v1.2/themeregistry.png",
comment: "Theme Registry Browser"
},
{
name: "Monitor Settings",
imageUrl: "https://danklinux.com/img/blog/v1.2/monitordark.png",
comment: "Display Configuration"
}
]
function getItems(query) {
const lowerQuery = query ? query.toLowerCase().trim() : "";
if (lowerQuery.length === 0) {
return images.map(img => ({
name: img.name,
icon: "material:image",
comment: img.comment,
action: "view:" + img.imageUrl,
categories: ["Image Gallery"],
imageUrl: img.imageUrl
}));
}
return images.filter(img => img.name.toLowerCase().includes(lowerQuery) || img.comment.toLowerCase().includes(lowerQuery)).map(img => ({
name: img.name,
icon: "material:image",
comment: img.comment,
action: "view:" + img.imageUrl,
categories: ["Image Gallery"],
imageUrl: img.imageUrl
}));
}
function executeItem(item) {
if (!item?.action)
return;
const actionParts = item.action.split(":");
const actionType = actionParts[0];
const actionData = actionParts.slice(1).join(":");
if (actionType === "view") {
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Image Gallery", "Viewing: " + item.name);
}
}
}
function getContextMenuActions(item) {
if (!item)
return [];
return [
{
icon: "open_in_new",
text: "Open in Browser",
action: () => {
const url = item.imageUrl || "";
if (url) {
Qt.openUrlExternally(url);
}
}
},
{
icon: "content_copy",
text: "Copy URL",
action: () => {
const url = item.imageUrl || "";
if (url) {
Quickshell.execDetached(["dms", "cl", "copy", url]);
if (typeof ToastService !== "undefined") {
ToastService.showInfo("Copied", url);
}
}
}
}
];
}
}

View File

@@ -0,0 +1,43 @@
# LauncherImageExample
Example launcher plugin demonstrating tile mode with URL-based images.
## Features
- **Tile Mode**: Uses `viewMode: "tile"` in plugin.json to display results as image tiles
- **Enforced View Mode**: Uses `viewModeEnforced: true` to lock the view to tile mode (users cannot change it)
- **URL Images**: Demonstrates using `imageUrl` property for remote images
## Usage
1. Open the launcher (DankLauncherV2)
2. Type `img` to activate the plugin
3. Browse DankMaterialShell screenshots in tile view
## Plugin Configuration
```json
{
"viewMode": "tile",
"viewModeEnforced": true
}
```
- `viewMode`: Sets the default view mode ("list", "grid", or "tile")
- `viewModeEnforced`: When true, users cannot switch view modes for this plugin
## Item Data Structure
To display images in tile mode, set `imageUrl` directly on the item:
```javascript
{
name: "Image Title",
icon: "material:image",
comment: "Image description",
categories: ["Category"],
imageUrl: "https://example.com/image.png"
}
```
The `imageUrl` property supports remote URLs or local files, use `file://` prefix for local files.

View File

@@ -0,0 +1,14 @@
{
"id": "launcherImageExample",
"name": "Image Gallery Example",
"description": "Example launcher plugin demonstrating tile mode with images",
"version": "1.0.0",
"author": "DMS Team",
"icon": "photo_library",
"type": "launcher",
"trigger": "img",
"viewMode": "tile",
"viewModeEnforced": true,
"component": "./LauncherImageExample.qml",
"permissions": []
}

View File

@@ -13,6 +13,12 @@ Singleton {
property var _cachedVisibleApps: null
property var _hiddenAppsSet: new Set()
property var _transformCache: ({})
property var _cachedDefaultSections: []
property var _cachedDefaultFlatModel: []
property bool _defaultCacheValid: false
property int cacheVersion: 0
readonly property int maxResults: 10
readonly property int frecencySampleSize: 10
@@ -43,6 +49,50 @@ Singleton {
applications = DesktopEntries.applications.values;
_cachedCategories = null;
_cachedVisibleApps = null;
invalidateLauncherCache();
}
function invalidateLauncherCache() {
_transformCache = {};
_defaultCacheValid = false;
_cachedDefaultSections = [];
_cachedDefaultFlatModel = [];
cacheVersion++;
}
function getOrTransformApp(app, transformFn) {
const id = app.id || app.execString || app.exec || "";
if (!id)
return transformFn(app);
const cached = _transformCache[id];
if (cached) {
const currentIcon = app.icon || "";
const cachedSourceIcon = cached._sourceIcon || "";
if (currentIcon === cachedSourceIcon)
return cached;
}
const transformed = transformFn(app);
transformed._sourceIcon = app.icon || "";
_transformCache[id] = transformed;
return transformed;
}
function getCachedDefaultSections() {
if (!_defaultCacheValid)
return null;
return _cachedDefaultSections;
}
function setCachedDefaultSections(sections, flatModel) {
_cachedDefaultSections = sections.map(function (s) {
return Object.assign({}, s);
});
_cachedDefaultFlatModel = flatModel.slice();
_defaultCacheValid = true;
}
function isCacheValid() {
return _defaultCacheValid;
}
function _rebuildHiddenSet() {
@@ -68,9 +118,18 @@ Singleton {
target: SessionData
function onHiddenAppsChanged() {
root._rebuildHiddenSet();
root.invalidateLauncherCache();
}
function onAppOverridesChanged() {
root._cachedVisibleApps = null;
root.invalidateLauncherCache();
}
}
Connections {
target: AppUsageHistoryData
function onAppUsageRankingChanged() {
root.invalidateLauncherCache();
}
}

View File

@@ -293,7 +293,6 @@ Singleton {
pluginDaemonComponents = newDaemons;
} else if (isLauncher) {
const instance = comp.createObject(root, {
"pluginId": pluginId,
"pluginService": root
});
if (!instance) {
@@ -702,6 +701,17 @@ Singleton {
return plugins;
}
function getPluginViewPreference(pluginId) {
const plugin = availablePlugins[pluginId];
if (!plugin)
return null;
return {
mode: plugin.viewMode || null,
enforced: plugin.viewModeEnforced === true
};
}
function getGlobalVar(pluginId, varName, defaultValue) {
if (globalVars[pluginId] && varName in globalVars[pluginId]) {
return globalVars[pluginId][varName];

View File

@@ -21,6 +21,8 @@ Singleton {
property var settingsModalLoader: null
property var clipboardHistoryModal: null
property var spotlightModal: null
property var spotlightV2Modal: null
property var spotlightV2ModalLoader: null
property var powerMenuModal: null
property var processListModal: null
property var processListModalLoader: null
@@ -361,6 +363,62 @@ Singleton {
spotlightModal?.close();
}
property bool _spotlightV2WantsOpen: false
property bool _spotlightV2WantsToggle: false
property string _spotlightV2PendingQuery: ""
function openSpotlightV2() {
if (spotlightV2Modal) {
spotlightV2Modal.show();
} else if (spotlightV2ModalLoader) {
_spotlightV2WantsOpen = true;
_spotlightV2WantsToggle = false;
spotlightV2ModalLoader.active = true;
}
}
function openSpotlightV2WithQuery(query: string) {
if (spotlightV2Modal) {
spotlightV2Modal.showWithQuery(query);
} else if (spotlightV2ModalLoader) {
_spotlightV2PendingQuery = query;
_spotlightV2WantsOpen = true;
_spotlightV2WantsToggle = false;
spotlightV2ModalLoader.active = true;
}
}
function closeSpotlightV2() {
spotlightV2Modal?.hide();
}
function toggleSpotlightV2() {
if (spotlightV2Modal) {
spotlightV2Modal.toggle();
} else if (spotlightV2ModalLoader) {
_spotlightV2WantsToggle = true;
_spotlightV2WantsOpen = false;
spotlightV2ModalLoader.active = true;
}
}
function _onSpotlightV2ModalLoaded() {
if (_spotlightV2WantsOpen) {
_spotlightV2WantsOpen = false;
if (_spotlightV2PendingQuery) {
spotlightV2Modal?.showWithQuery(_spotlightV2PendingQuery);
_spotlightV2PendingQuery = "";
} else {
spotlightV2Modal?.show();
}
return;
}
if (_spotlightV2WantsToggle) {
_spotlightV2WantsToggle = false;
spotlightV2Modal?.toggle();
}
}
function openPowerMenu() {
powerMenuModal?.openCentered();
}

View File

@@ -7,6 +7,17 @@ Image {
property string imagePath: ""
property int maxCacheSize: 512
readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
readonly property string normalizedPath: {
if (!imagePath)
return "";
if (isRemoteUrl)
return imagePath;
if (imagePath.startsWith("file://"))
return imagePath.substring(7);
return imagePath;
}
function djb2Hash(str) {
if (!str)
return "";
@@ -18,9 +29,15 @@ Image {
return hash.toString(16).padStart(8, '0');
}
readonly property string imageHash: imagePath ? djb2Hash(imagePath) : ""
readonly property string cachePath: imageHash ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
readonly property string encodedImagePath: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
readonly property string cachePath: imageHash && !isRemoteUrl ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
readonly property string encodedImagePath: {
if (!normalizedPath)
return "";
if (isRemoteUrl)
return normalizedPath;
return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
}
asynchronous: true
fillMode: Image.PreserveAspectCrop
@@ -33,10 +50,14 @@ Image {
source = "";
return;
}
if (isRemoteUrl) {
source = imagePath;
return;
}
Paths.mkdir(Paths.imagecache);
const hash = djb2Hash(imagePath);
const hash = djb2Hash(normalizedPath);
const cPath = hash ? `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png` : "";
const encoded = "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/');
const encoded = "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
source = cPath || encoded;
}
@@ -45,7 +66,7 @@ Image {
source = encodedImagePath;
return;
}
if (source != encodedImagePath || status !== Image.Ready || !cachePath)
if (isRemoteUrl || source != encodedImagePath || status !== Image.Ready || !cachePath)
return;
Paths.mkdir(Paths.imagecache);
const grabPath = cachePath;

View File

@@ -8,17 +8,9 @@ StyledRect {
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
activeFocusOnTab: true
KeyNavigation.tab: keyNavigationTab
KeyNavigation.backtab: keyNavigationBacktab
onActiveFocusChanged: {
if (activeFocus) {
textInput.forceActiveFocus();
}
}
property alias text: textInput.text
property string placeholderText: ""
property alias font: textInput.font

View File

@@ -1109,6 +1109,7 @@
"tabIndex": 5,
"category": "Dock",
"keywords": [
"always",
"area",
"auto",
"autohide",
@@ -1122,7 +1123,7 @@
"reveal",
"taskbar"
],
"description": "Hide the dock when not in use and reveal it when hovering near the dock area"
"description": "Always hide the dock and reveal it when hovering near the dock area"
},
{
"section": "dockBehavior",
@@ -1276,6 +1277,29 @@
"taskbar"
]
},
{
"section": "dockSmartAutoHide",
"label": "Intelligent Auto-hide",
"tabIndex": 5,
"category": "Dock",
"keywords": [
"auto",
"autohide",
"dock",
"floating",
"hide",
"intelligent",
"launcher bar",
"overlap",
"panel",
"show",
"smart",
"taskbar",
"windows"
],
"description": "Show dock when floating windows don",
"conditionKey": "isNiri"
},
{
"section": "dockIsolateDisplays",
"label": "Isolate Displays",
@@ -1428,6 +1452,58 @@
"icon": "computer",
"conditionKey": "cupsAvailable"
},
{
"section": "appOverrides",
"label": "App Customizations",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"app",
"customizations",
"drawer",
"launcher",
"menu",
"start"
],
"icon": "edit"
},
{
"section": "dankLauncherV2Appearance",
"label": "Appearance",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"appearance",
"bottom",
"drawer",
"footer",
"hints",
"keyboard",
"launcher",
"menu",
"mode",
"shortcuts",
"show",
"start",
"tabs"
],
"icon": "tune",
"description": "Show mode tabs and keyboard hints at the bottom."
},
{
"section": "dankLauncherV2BorderEnabled",
"label": "Border",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"border",
"drawer",
"launcher",
"menu",
"outline",
"start"
]
},
{
"section": "launcherLogoBrightness",
"label": "Brightness",
@@ -1520,6 +1596,21 @@
],
"description": "Adjust the number of columns in grid view mode."
},
{
"section": "hiddenApps",
"label": "Hidden Apps",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"apps",
"drawer",
"hidden",
"launcher",
"menu",
"start"
],
"icon": "visibility_off"
},
{
"section": "launcherLogoColorInvertOnMode",
"label": "Invert on mode change",
@@ -1629,6 +1720,68 @@
],
"icon": "history"
},
{
"section": "searchAppActions",
"label": "Search App Actions",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"actions",
"app",
"desktop",
"drawer",
"include",
"launcher",
"menu",
"results",
"search",
"shortcuts",
"start"
],
"description": "Include desktop actions (shortcuts) in search results."
},
{
"section": "searchOptions",
"label": "Search Options",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"actions",
"desktop",
"drawer",
"include",
"launcher",
"menu",
"options",
"results",
"search",
"shortcuts",
"start"
],
"icon": "search",
"description": "Include desktop actions (shortcuts) in search results."
},
{
"section": "dankLauncherV2ShowFooter",
"label": "Show Footer",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"bottom",
"drawer",
"footer",
"hints",
"keyboard",
"launcher",
"menu",
"mode",
"shortcuts",
"show",
"start",
"tabs"
],
"description": "Show mode tabs and keyboard hints at the bottom."
},
{
"section": "launcherLogoSizeOffset",
"label": "Size Offset",
@@ -1692,6 +1845,20 @@
"icon": "sort_by_alpha",
"description": "When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency."
},
{
"section": "dankLauncherV2BorderThickness",
"label": "Thickness",
"tabIndex": 9,
"category": "Launcher",
"keywords": [
"border",
"drawer",
"launcher",
"menu",
"start",
"thickness"
]
},
{
"section": "matugenTemplateAlacritty",
"label": "Alacritty",
@@ -3369,6 +3536,40 @@
],
"icon": "security"
},
{
"section": "lockScreenPowerOffMonitorsOnLock",
"label": "Power off monitors on lock",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"activates",
"display",
"displays",
"dpms",
"hibernate",
"immediately",
"lock",
"lockscreen",
"login",
"monitor",
"monitors",
"off",
"output",
"outputs",
"password",
"power",
"reboot",
"restart",
"screen",
"screens",
"security",
"shutdown",
"sleep",
"suspend",
"turn"
],
"description": "Turn off all displays immediately when the lock screen activates"
},
{
"section": "lockScreenShowPasswordField",
"label": "Show Password Field",
@@ -5047,6 +5248,26 @@
],
"description": "Maximum size per clipboard entry"
},
{
"section": "maxPinned",
"label": "Maximum Pinned Entries",
"tabIndex": 23,
"category": "System",
"keywords": [
"clipboard",
"entries",
"limit",
"linux",
"max",
"maximum",
"number",
"os",
"pinned",
"saved",
"system"
],
"description": "Maximum number of entries that can be saved"
},
{
"section": "_tab_24",
"label": "Displays",