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:
@@ -215,8 +215,8 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
// Skip if file already exists to preserve user modifications
|
// Skip if file already exists and is not empty to preserve user modifications
|
||||||
if _, err := os.Stat(path); err == nil {
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -567,7 +567,8 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
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))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,7 +236,13 @@ Singleton {
|
|||||||
property bool sortAppsAlphabetically: false
|
property bool sortAppsAlphabetically: false
|
||||||
property int appLauncherGridColumns: 4
|
property int appLauncherGridColumns: 4
|
||||||
property bool spotlightCloseNiriOverview: true
|
property bool spotlightCloseNiriOverview: true
|
||||||
|
property var spotlightSectionViewModes: ({})
|
||||||
property bool niriOverviewOverlayEnabled: true
|
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 _legacyWeatherLocation: "New York, NY"
|
||||||
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
property string _legacyWeatherCoordinates: "40.7128,-74.0060"
|
||||||
|
|||||||
@@ -134,7 +134,13 @@ var SPEC = {
|
|||||||
sortAppsAlphabetically: { def: false },
|
sortAppsAlphabetically: { def: false },
|
||||||
appLauncherGridColumns: { def: 4 },
|
appLauncherGridColumns: { def: 4 },
|
||||||
spotlightCloseNiriOverview: { def: true },
|
spotlightCloseNiriOverview: { def: true },
|
||||||
|
spotlightSectionViewModes: { def: {} },
|
||||||
niriOverviewOverlayEnabled: { def: true },
|
niriOverviewOverlayEnabled: { def: true },
|
||||||
|
dankLauncherV2Size: { def: "compact" },
|
||||||
|
dankLauncherV2BorderEnabled: { def: false },
|
||||||
|
dankLauncherV2BorderThickness: { def: 2 },
|
||||||
|
dankLauncherV2BorderColor: { def: "primary" },
|
||||||
|
dankLauncherV2ShowFooter: { def: true },
|
||||||
|
|
||||||
useAutoLocation: { def: false },
|
useAutoLocation: { def: false },
|
||||||
weatherEnabled: { def: true },
|
weatherEnabled: { def: true },
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import qs.Modals.Clipboard
|
|||||||
import qs.Modals.Greeter
|
import qs.Modals.Greeter
|
||||||
import qs.Modals.Settings
|
import qs.Modals.Settings
|
||||||
import qs.Modals.Spotlight
|
import qs.Modals.Spotlight
|
||||||
|
import qs.Modals.DankLauncherV2
|
||||||
import qs.Modules
|
import qs.Modules
|
||||||
import qs.Modules.AppDrawer
|
import qs.Modules.AppDrawer
|
||||||
import qs.Modules.DankDash
|
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 {
|
ClipboardHistoryModal {
|
||||||
id: clipboardHistoryModalPopup
|
id: clipboardHistoryModalPopup
|
||||||
|
|
||||||
|
|||||||
@@ -1025,6 +1025,35 @@ Item {
|
|||||||
target: "clipboard"
|
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 {
|
IpcHandler {
|
||||||
function open(): string {
|
function open(): string {
|
||||||
FirstLaunchService.showWelcome();
|
FirstLaunchService.showWelcome();
|
||||||
|
|||||||
231
quickshell/Modals/DankLauncherV2/ActionPanel.qml
Normal file
231
quickshell/Modals/DankLauncherV2/ActionPanel.qml
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1570
quickshell/Modals/DankLauncherV2/Controller.qml
Normal file
1570
quickshell/Modals/DankLauncherV2/Controller.qml
Normal file
File diff suppressed because it is too large
Load Diff
283
quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
Normal file
283
quickshell/Modals/DankLauncherV2/DankLauncherV2Modal.qml
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
139
quickshell/Modals/DankLauncherV2/GridItem.qml
Normal file
139
quickshell/Modals/DankLauncherV2/GridItem.qml
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
809
quickshell/Modals/DankLauncherV2/LauncherContent.qml
Normal file
809
quickshell/Modals/DankLauncherV2/LauncherContent.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
484
quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
Normal file
484
quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
189
quickshell/Modals/DankLauncherV2/ResultItem.qml
Normal file
189
quickshell/Modals/DankLauncherV2/ResultItem.qml
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
489
quickshell/Modals/DankLauncherV2/ResultsList.qml
Normal file
489
quickshell/Modals/DankLauncherV2/ResultsList.qml
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
245
quickshell/Modals/DankLauncherV2/Scorer.js
Normal file
245
quickshell/Modals/DankLauncherV2/Scorer.js
Normal 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
|
||||||
|
}
|
||||||
114
quickshell/Modals/DankLauncherV2/Section.qml
Normal file
114
quickshell/Modals/DankLauncherV2/Section.qml
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
169
quickshell/Modals/DankLauncherV2/SectionHeader.qml
Normal file
169
quickshell/Modals/DankLauncherV2/SectionHeader.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
147
quickshell/Modals/DankLauncherV2/TileItem.qml
Normal file
147
quickshell/Modals/DankLauncherV2/TileItem.qml
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,6 +73,9 @@ DankPopout {
|
|||||||
root.close();
|
root.close();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (item && "parentPopout" in item) {
|
||||||
|
item.parentPopout = root;
|
||||||
|
}
|
||||||
if (item) {
|
if (item) {
|
||||||
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
|
root.contentHeight = Qt.binding(() => item.implicitHeight + Theme.spacingS * 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Column {
|
|||||||
property string detailsText: ""
|
property string detailsText: ""
|
||||||
property bool showCloseButton: false
|
property bool showCloseButton: false
|
||||||
property var closePopout: null
|
property var closePopout: null
|
||||||
|
property var parentPopout: null
|
||||||
property alias headerActions: headerActionsLoader.sourceComponent
|
property alias headerActions: headerActionsLoader.sourceComponent
|
||||||
|
|
||||||
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
|
readonly property int headerHeight: popoutHeader.visible ? popoutHeader.height : 0
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ Popup {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: modelData.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
enabled: modelData.enabled
|
enabled: modelData.enabled ?? false
|
||||||
onEntered: {
|
onEntered: {
|
||||||
keyboardNavigation = false;
|
keyboardNavigation = false;
|
||||||
selectedIndex = index;
|
selectedIndex = index;
|
||||||
|
|||||||
@@ -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 {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "open_in_new"
|
iconName: "open_in_new"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import QtQuick.Controls
|
|||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
import qs.Common
|
import qs.Common
|
||||||
import qs.Modals.Spotlight
|
import qs.Modals.DankLauncherV2
|
||||||
import qs.Services
|
import qs.Services
|
||||||
|
|
||||||
Scope {
|
Scope {
|
||||||
@@ -67,229 +67,215 @@ Scope {
|
|||||||
hideSpotlight();
|
hideSpotlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
Variants {
|
||||||
id: niriOverlayLoader
|
id: overlayVariants
|
||||||
active: overlayActive || isClosing
|
model: Quickshell.screens
|
||||||
asynchronous: false
|
|
||||||
|
|
||||||
sourceComponent: Variants {
|
PanelWindow {
|
||||||
id: overlayVariants
|
id: overlayWindow
|
||||||
model: Quickshell.screens
|
required property var modelData
|
||||||
|
|
||||||
PanelWindow {
|
readonly property real dpr: CompositorService.getScreenScale(screen)
|
||||||
id: overlayWindow
|
readonly property bool isActiveScreen: screen.name === NiriService.currentOutput
|
||||||
required property var modelData
|
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]
|
||||||
|
|
||||||
readonly property real dpr: CompositorService.getScreenScale(screen)
|
Connections {
|
||||||
readonly property bool isActiveScreen: screen.name === NiriService.currentOutput
|
target: PopoutManager
|
||||||
readonly property bool shouldShowSpotlight: niriOverviewScope.searchActive && screen.name === niriOverviewScope.searchActiveScreen && !niriOverviewScope.isClosing
|
function onPopoutChanged() {
|
||||||
readonly property bool isSpotlightScreen: screen.name === niriOverviewScope.searchActiveScreen
|
overlayWindow.hasActivePopout = !!PopoutManager.currentPopoutsByScreen[overlayWindow.screen.name];
|
||||||
property bool hasActivePopout: !!PopoutManager.currentPopoutsByScreen[screen.name]
|
|
||||||
property bool hasActiveModal: !!ModalManager.currentModalsByScreen[screen.name]
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: PopoutManager
|
|
||||||
function onPopoutChanged() {
|
|
||||||
overlayWindow.hasActivePopout = !!PopoutManager.currentPopoutsByScreen[overlayWindow.screen.name];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: ModalManager
|
target: ModalManager
|
||||||
function onModalChanged() {
|
function onModalChanged() {
|
||||||
overlayWindow.hasActiveModal = !!ModalManager.currentModalsByScreen[overlayWindow.screen.name];
|
overlayWindow.hasActiveModal = !!ModalManager.currentModalsByScreen[overlayWindow.screen.name];
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
screen: modelData
|
screen: modelData
|
||||||
visible: NiriService.inOverview || niriOverviewScope.isClosing
|
visible: true
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:niri-overview-spotlight"
|
WlrLayershell.namespace: "dms:niri-overview-spotlight"
|
||||||
WlrLayershell.layer: WlrLayer.Overlay
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
WlrLayershell.exclusiveZone: -1
|
WlrLayershell.exclusiveZone: -1
|
||||||
WlrLayershell.keyboardFocus: {
|
WlrLayershell.keyboardFocus: {
|
||||||
if (!NiriService.inOverview)
|
if (!NiriService.inOverview)
|
||||||
return WlrKeyboardFocus.None;
|
return WlrKeyboardFocus.None;
|
||||||
if (!isActiveScreen)
|
if (!isActiveScreen)
|
||||||
return WlrKeyboardFocus.None;
|
return WlrKeyboardFocus.None;
|
||||||
if (niriOverviewScope.releaseKeyboard)
|
if (niriOverviewScope.releaseKeyboard)
|
||||||
return WlrKeyboardFocus.None;
|
return WlrKeyboardFocus.None;
|
||||||
if (hasActivePopout || hasActiveModal)
|
if (hasActivePopout || hasActiveModal)
|
||||||
return WlrKeyboardFocus.None;
|
return WlrKeyboardFocus.None;
|
||||||
return WlrKeyboardFocus.Exclusive;
|
return WlrKeyboardFocus.Exclusive;
|
||||||
|
}
|
||||||
|
|
||||||
|
mask: Region {
|
||||||
|
item: overlayVisible && spotlightContainer.visible ? spotlightContainer : null
|
||||||
|
}
|
||||||
|
|
||||||
|
onShouldShowSpotlightChanged: {
|
||||||
|
if (shouldShowSpotlight) {
|
||||||
|
if (launcherContent?.controller)
|
||||||
|
launcherContent.controller.performSearch();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (!isActiveScreen)
|
||||||
|
return;
|
||||||
|
Qt.callLater(() => keyboardFocusScope.forceActiveFocus());
|
||||||
|
}
|
||||||
|
|
||||||
mask: Region {
|
anchors {
|
||||||
item: spotlightContainer.visible ? spotlightContainer : null
|
top: true
|
||||||
}
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
onShouldShowSpotlightChanged: {
|
FocusScope {
|
||||||
if (shouldShowSpotlight) {
|
id: keyboardFocusScope
|
||||||
if (spotlightContent?.appLauncher)
|
anchors.fill: parent
|
||||||
spotlightContent.appLauncher.ensureInitialized();
|
focus: true
|
||||||
|
|
||||||
|
Keys.onPressed: event => {
|
||||||
|
if (overlayWindow.shouldShowSpotlight || niriOverviewScope.isClosing)
|
||||||
return;
|
return;
|
||||||
}
|
if ([Qt.Key_Escape, Qt.Key_Return].includes(event.key)) {
|
||||||
if (!isActiveScreen)
|
NiriService.toggleOverview();
|
||||||
return;
|
|
||||||
Qt.callLater(() => keyboardFocusScope.forceActiveFocus());
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
FocusScope {
|
|
||||||
id: keyboardFocusScope
|
|
||||||
anchors.fill: parent
|
|
||||||
focus: true
|
|
||||||
|
|
||||||
Keys.onPressed: event => {
|
|
||||||
if (overlayWindow.shouldShowSpotlight || niriOverviewScope.isClosing)
|
|
||||||
return;
|
|
||||||
if ([Qt.Key_Escape, Qt.Key_Return].includes(event.key)) {
|
|
||||||
NiriService.toggleOverview();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Left) {
|
|
||||||
NiriService.moveColumnLeft();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Right) {
|
|
||||||
NiriService.moveColumnRight();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Up) {
|
|
||||||
NiriService.moveWorkspaceUp();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === Qt.Key_Down) {
|
|
||||||
NiriService.moveWorkspaceDown();
|
|
||||||
event.accepted = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.modifiers & (Qt.ControlModifier | Qt.MetaModifier) || [Qt.Key_Delete, Qt.Key_Backspace].includes(event.key)) {
|
|
||||||
event.accepted = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.isAutoRepeat || !event.text)
|
|
||||||
return;
|
|
||||||
if (!spotlightContent?.searchField)
|
|
||||||
return;
|
|
||||||
const trimmedText = event.text.trim();
|
|
||||||
spotlightContent.searchField.text = trimmedText;
|
|
||||||
if (spotlightContent.appLauncher) {
|
|
||||||
spotlightContent.appLauncher.searchQuery = trimmedText;
|
|
||||||
}
|
|
||||||
niriOverviewScope.showSpotlight(overlayWindow.screen.name);
|
|
||||||
Qt.callLater(() => spotlightContent.searchField.forceActiveFocus());
|
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Left) {
|
||||||
|
NiriService.moveColumnLeft();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Right) {
|
||||||
|
NiriService.moveColumnRight();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Up) {
|
||||||
|
NiriService.moveWorkspaceUp();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === Qt.Key_Down) {
|
||||||
|
NiriService.moveWorkspaceDown();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.modifiers & (Qt.ControlModifier | Qt.MetaModifier) || [Qt.Key_Delete, Qt.Key_Backspace].includes(event.key)) {
|
||||||
|
event.accepted = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.isAutoRepeat || !event.text)
|
||||||
|
return;
|
||||||
|
if (!launcherContent?.searchField)
|
||||||
|
return;
|
||||||
|
const trimmedText = event.text.trim();
|
||||||
|
launcherContent.searchField.text = trimmedText;
|
||||||
|
launcherContent.controller.setSearchQuery(trimmedText);
|
||||||
|
niriOverviewScope.showSpotlight(overlayWindow.screen.name);
|
||||||
|
Qt.callLater(() => launcherContent.searchField.forceActiveFocus());
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: spotlightContainer
|
||||||
|
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
||||||
|
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
||||||
|
width: Theme.px(500, overlayWindow.dpr)
|
||||||
|
height: Theme.px(600, overlayWindow.dpr)
|
||||||
|
|
||||||
|
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
||||||
|
|
||||||
|
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
||||||
|
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
||||||
|
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
||||||
|
enabled: overlayWindow.shouldShowSpotlight
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.smooth: false
|
||||||
|
layer.textureSize: Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr))
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
id: scaleAnimation
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.expressiveDurations.fast
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||||
|
onRunningChanged: {
|
||||||
|
if (running || !spotlightContainer.animatingOut)
|
||||||
|
return;
|
||||||
|
niriOverviewScope.resetState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Behavior on opacity {
|
||||||
id: spotlightContainer
|
NumberAnimation {
|
||||||
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
duration: Theme.expressiveDurations.fast
|
||||||
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
easing.type: Easing.BezierSpline
|
||||||
width: Theme.px(500, overlayWindow.dpr)
|
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
||||||
height: Theme.px(600, overlayWindow.dpr)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.color: Theme.outlineMedium
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
LauncherContent {
|
||||||
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
id: launcherContent
|
||||||
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
anchors.fill: parent
|
||||||
enabled: overlayWindow.shouldShowSpotlight
|
anchors.margins: 0
|
||||||
|
|
||||||
layer.enabled: true
|
property var fakeParentModal: QtObject {
|
||||||
layer.smooth: false
|
property bool spotlightOpen: spotlightContainer.visible
|
||||||
layer.textureSize: Qt.size(Math.round(width * overlayWindow.dpr), Math.round(height * overlayWindow.dpr))
|
property bool isClosing: niriOverviewScope.isClosing
|
||||||
|
function hide() {
|
||||||
Behavior on scale {
|
if (niriOverviewScope.searchActive) {
|
||||||
id: scaleAnimation
|
niriOverviewScope.hideAndReleaseKeyboard();
|
||||||
NumberAnimation {
|
return;
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
|
||||||
onRunningChanged: {
|
|
||||||
if (running || !spotlightContainer.animatingOut)
|
|
||||||
return;
|
|
||||||
niriOverviewScope.resetState();
|
|
||||||
}
|
}
|
||||||
|
NiriService.toggleOverview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Connections {
|
||||||
NumberAnimation {
|
target: launcherContent.searchField
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
function onTextChanged() {
|
||||||
easing.type: Easing.BezierSpline
|
if (launcherContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
return;
|
||||||
|
niriOverviewScope.hideSpotlight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Component.onCompleted: {
|
||||||
anchors.fill: parent
|
parentModal = fakeParentModal;
|
||||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
border.color: Theme.outlineMedium
|
|
||||||
border.width: 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SpotlightContent {
|
Connections {
|
||||||
id: spotlightContent
|
target: launcherContent.controller
|
||||||
anchors.fill: parent
|
function onItemExecuted() {
|
||||||
anchors.margins: 0
|
niriOverviewScope.releaseKeyboard = true;
|
||||||
usePopupContextMenu: true
|
|
||||||
|
|
||||||
property var fakeParentModal: QtObject {
|
|
||||||
property bool spotlightOpen: spotlightContainer.visible
|
|
||||||
function hide() {
|
|
||||||
if (niriOverviewScope.searchActive) {
|
|
||||||
niriOverviewScope.hideAndReleaseKeyboard();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
NiriService.toggleOverview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: spotlightContent.searchField
|
|
||||||
function onTextChanged() {
|
|
||||||
if (spotlightContent.searchField.text.length > 0 || !niriOverviewScope.searchActive)
|
|
||||||
return;
|
|
||||||
niriOverviewScope.hideSpotlight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
parentModal = fakeParentModal;
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: spotlightContent.appLauncher
|
|
||||||
function onAppLaunched() {
|
|
||||||
niriOverviewScope.releaseKeyboard = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: spotlightContent.fileSearchController
|
|
||||||
function onFileOpened() {
|
|
||||||
niriOverviewScope.releaseKeyboard = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
113
quickshell/PLUGINS/LauncherImageExample/LauncherImageExample.qml
Normal file
113
quickshell/PLUGINS/LauncherImageExample/LauncherImageExample.qml
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
43
quickshell/PLUGINS/LauncherImageExample/README.md
Normal file
43
quickshell/PLUGINS/LauncherImageExample/README.md
Normal 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.
|
||||||
14
quickshell/PLUGINS/LauncherImageExample/plugin.json
Normal file
14
quickshell/PLUGINS/LauncherImageExample/plugin.json
Normal 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": []
|
||||||
|
}
|
||||||
@@ -13,6 +13,12 @@ Singleton {
|
|||||||
property var _cachedVisibleApps: null
|
property var _cachedVisibleApps: null
|
||||||
property var _hiddenAppsSet: new Set()
|
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 maxResults: 10
|
||||||
readonly property int frecencySampleSize: 10
|
readonly property int frecencySampleSize: 10
|
||||||
|
|
||||||
@@ -43,6 +49,50 @@ Singleton {
|
|||||||
applications = DesktopEntries.applications.values;
|
applications = DesktopEntries.applications.values;
|
||||||
_cachedCategories = null;
|
_cachedCategories = null;
|
||||||
_cachedVisibleApps = 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() {
|
function _rebuildHiddenSet() {
|
||||||
@@ -68,9 +118,18 @@ Singleton {
|
|||||||
target: SessionData
|
target: SessionData
|
||||||
function onHiddenAppsChanged() {
|
function onHiddenAppsChanged() {
|
||||||
root._rebuildHiddenSet();
|
root._rebuildHiddenSet();
|
||||||
|
root.invalidateLauncherCache();
|
||||||
}
|
}
|
||||||
function onAppOverridesChanged() {
|
function onAppOverridesChanged() {
|
||||||
root._cachedVisibleApps = null;
|
root._cachedVisibleApps = null;
|
||||||
|
root.invalidateLauncherCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: AppUsageHistoryData
|
||||||
|
function onAppUsageRankingChanged() {
|
||||||
|
root.invalidateLauncherCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -293,7 +293,6 @@ Singleton {
|
|||||||
pluginDaemonComponents = newDaemons;
|
pluginDaemonComponents = newDaemons;
|
||||||
} else if (isLauncher) {
|
} else if (isLauncher) {
|
||||||
const instance = comp.createObject(root, {
|
const instance = comp.createObject(root, {
|
||||||
"pluginId": pluginId,
|
|
||||||
"pluginService": root
|
"pluginService": root
|
||||||
});
|
});
|
||||||
if (!instance) {
|
if (!instance) {
|
||||||
@@ -702,6 +701,17 @@ Singleton {
|
|||||||
return plugins;
|
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) {
|
function getGlobalVar(pluginId, varName, defaultValue) {
|
||||||
if (globalVars[pluginId] && varName in globalVars[pluginId]) {
|
if (globalVars[pluginId] && varName in globalVars[pluginId]) {
|
||||||
return globalVars[pluginId][varName];
|
return globalVars[pluginId][varName];
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ Singleton {
|
|||||||
property var settingsModalLoader: null
|
property var settingsModalLoader: null
|
||||||
property var clipboardHistoryModal: null
|
property var clipboardHistoryModal: null
|
||||||
property var spotlightModal: null
|
property var spotlightModal: null
|
||||||
|
property var spotlightV2Modal: null
|
||||||
|
property var spotlightV2ModalLoader: null
|
||||||
property var powerMenuModal: null
|
property var powerMenuModal: null
|
||||||
property var processListModal: null
|
property var processListModal: null
|
||||||
property var processListModalLoader: null
|
property var processListModalLoader: null
|
||||||
@@ -361,6 +363,62 @@ Singleton {
|
|||||||
spotlightModal?.close();
|
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() {
|
function openPowerMenu() {
|
||||||
powerMenuModal?.openCentered();
|
powerMenuModal?.openCentered();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ Image {
|
|||||||
property string imagePath: ""
|
property string imagePath: ""
|
||||||
property int maxCacheSize: 512
|
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) {
|
function djb2Hash(str) {
|
||||||
if (!str)
|
if (!str)
|
||||||
return "";
|
return "";
|
||||||
@@ -18,9 +29,15 @@ Image {
|
|||||||
return hash.toString(16).padStart(8, '0');
|
return hash.toString(16).padStart(8, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string imageHash: imagePath ? djb2Hash(imagePath) : ""
|
readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
|
||||||
readonly property string cachePath: imageHash ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
|
readonly property string cachePath: imageHash && !isRemoteUrl ? `${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 encodedImagePath: {
|
||||||
|
if (!normalizedPath)
|
||||||
|
return "";
|
||||||
|
if (isRemoteUrl)
|
||||||
|
return normalizedPath;
|
||||||
|
return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
@@ -33,10 +50,14 @@ Image {
|
|||||||
source = "";
|
source = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isRemoteUrl) {
|
||||||
|
source = imagePath;
|
||||||
|
return;
|
||||||
|
}
|
||||||
Paths.mkdir(Paths.imagecache);
|
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 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;
|
source = cPath || encoded;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +66,7 @@ Image {
|
|||||||
source = encodedImagePath;
|
source = encodedImagePath;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (source != encodedImagePath || status !== Image.Ready || !cachePath)
|
if (isRemoteUrl || source != encodedImagePath || status !== Image.Ready || !cachePath)
|
||||||
return;
|
return;
|
||||||
Paths.mkdir(Paths.imagecache);
|
Paths.mkdir(Paths.imagecache);
|
||||||
const grabPath = cachePath;
|
const grabPath = cachePath;
|
||||||
|
|||||||
@@ -8,17 +8,9 @@ StyledRect {
|
|||||||
LayoutMirroring.enabled: I18n.isRtl
|
LayoutMirroring.enabled: I18n.isRtl
|
||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
activeFocusOnTab: true
|
|
||||||
|
|
||||||
KeyNavigation.tab: keyNavigationTab
|
KeyNavigation.tab: keyNavigationTab
|
||||||
KeyNavigation.backtab: keyNavigationBacktab
|
KeyNavigation.backtab: keyNavigationBacktab
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
|
||||||
if (activeFocus) {
|
|
||||||
textInput.forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property alias text: textInput.text
|
property alias text: textInput.text
|
||||||
property string placeholderText: ""
|
property string placeholderText: ""
|
||||||
property alias font: textInput.font
|
property alias font: textInput.font
|
||||||
|
|||||||
@@ -1109,6 +1109,7 @@
|
|||||||
"tabIndex": 5,
|
"tabIndex": 5,
|
||||||
"category": "Dock",
|
"category": "Dock",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
"always",
|
||||||
"area",
|
"area",
|
||||||
"auto",
|
"auto",
|
||||||
"autohide",
|
"autohide",
|
||||||
@@ -1122,7 +1123,7 @@
|
|||||||
"reveal",
|
"reveal",
|
||||||
"taskbar"
|
"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",
|
"section": "dockBehavior",
|
||||||
@@ -1276,6 +1277,29 @@
|
|||||||
"taskbar"
|
"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",
|
"section": "dockIsolateDisplays",
|
||||||
"label": "Isolate Displays",
|
"label": "Isolate Displays",
|
||||||
@@ -1428,6 +1452,58 @@
|
|||||||
"icon": "computer",
|
"icon": "computer",
|
||||||
"conditionKey": "cupsAvailable"
|
"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",
|
"section": "launcherLogoBrightness",
|
||||||
"label": "Brightness",
|
"label": "Brightness",
|
||||||
@@ -1520,6 +1596,21 @@
|
|||||||
],
|
],
|
||||||
"description": "Adjust the number of columns in grid view mode."
|
"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",
|
"section": "launcherLogoColorInvertOnMode",
|
||||||
"label": "Invert on mode change",
|
"label": "Invert on mode change",
|
||||||
@@ -1629,6 +1720,68 @@
|
|||||||
],
|
],
|
||||||
"icon": "history"
|
"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",
|
"section": "launcherLogoSizeOffset",
|
||||||
"label": "Size Offset",
|
"label": "Size Offset",
|
||||||
@@ -1692,6 +1845,20 @@
|
|||||||
"icon": "sort_by_alpha",
|
"icon": "sort_by_alpha",
|
||||||
"description": "When enabled, apps are sorted alphabetically. When disabled, apps are sorted by usage frequency."
|
"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",
|
"section": "matugenTemplateAlacritty",
|
||||||
"label": "Alacritty",
|
"label": "Alacritty",
|
||||||
@@ -3369,6 +3536,40 @@
|
|||||||
],
|
],
|
||||||
"icon": "security"
|
"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",
|
"section": "lockScreenShowPasswordField",
|
||||||
"label": "Show Password Field",
|
"label": "Show Password Field",
|
||||||
@@ -5047,6 +5248,26 @@
|
|||||||
],
|
],
|
||||||
"description": "Maximum size per clipboard entry"
|
"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",
|
"section": "_tab_24",
|
||||||
"label": "Displays",
|
"label": "Displays",
|
||||||
|
|||||||
Reference in New Issue
Block a user