1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-04 04:42:05 -04:00
Files
DankMaterialShell/quickshell/Modals/DankLauncherV2/ActionPanel.qml
Michael Erdely 190fd662ad Implement more intuitive keybinds for Launcher (#2002)
With programs like rofi, pressing the tab key advances to the next item
in the list. This change makes the Launcher behave in the same way,
moving the action cycling to Ctrl+Tab (and Ctrl+Shift+Tab for reverse.
2026-03-16 11:07:25 -04:00

257 lines
8.4 KiB
QML

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);
}
switch (selectedItem?.type) {
case "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
});
}
break;
case "plugin_browse":
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
break;
case "app":
if (selectedItem?.isCore)
break;
if (SessionService.nvidiaCommand) {
result.push({
name: I18n.tr("Launch on dGPU"),
icon: "memory",
action: "launch_dgpu"
});
}
if (selectedItem?.actions) {
for (var i = 0; i < selectedItem.actions.length; i++) {
result.push(selectedItem.actions[i]);
}
}
break;
}
return result;
}
readonly property bool hasActions: {
switch (selectedItem?.type) {
case "app":
return !selectedItem?.isCore;
case "plugin":
return getPluginContextMenuActions().length > 0;
case "plugin_browse":
return selectedItem?.actions?.length > 0;
default:
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(reverse = false) {
if (actions.length > 0) {
if (! reverse)
selectedActionIndex = (selectedActionIndex + 1) % actions.length;
else
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);
}
}
}