1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-24 13:32:50 -05:00
Files
DankMaterialShell/quickshell/Modals/DankLauncherV2/LauncherContextMenu.qml
2026-01-22 17:45:38 -05:00

487 lines
16 KiB
QML

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")
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 bool isCoreApp: item?.type === "app" && item?.isCore
readonly property var coreAppData: isCoreApp ? item?.data ?? null : null
readonly property var desktopEntry: !isCoreApp ? (item?.data ?? null) : null
readonly property string appId: {
if (isCoreApp) {
return item?.id || coreAppData?.builtInPluginId || "";
}
return desktopEntry?.id || desktopEntry?.execString || "";
}
readonly property bool isPinned: appId ? SessionData.isPinnedApp(appId) : false
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 (item?.type === "app") {
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
});
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 (isCoreApp) {
if (!coreAppData)
return;
AppSearchService.executeCoreApp(coreAppData);
controller?.itemExecuted();
hide();
return;
}
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);
}
}
}
}
}
}
}
}
}