mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-29 22:47:47 -04:00
feat(keybinds): redesign cheatsheet with category navigation (#3534)
* feat(keybinds): redesign cheatsheet with category navigation, M3 styling, and click-to-execute - Add category sidebar with Material Symbols icons and badge counts - Move search field to top of sidebar with Ctrl+F shortcut - Implement 2-column masonry layout for keybind items - Deduplicate duplicate actions and stack multi-key combos vertically - Collapse consecutive workspace keybindings (1-9) into range entries - Add media & hardware icons for volume, mic, brightness, and PrtSc keycaps - Preserve Niri window filter properties and distinguish same-app Alt-Tab binds - Enable click-to-execute for keybind actions across supported compositors * fix(keybinds): preserve niri action properties and disable execution on range rows * fix(keybinds): convert niri action properties to CLI flags and disable IPC execution for window switcher binds * fix(keybinds): pass skip-confirmation as boolean switch in niri executeAction * fix(keybinds): handle niri spawn-sh actions in executeAction
This commit is contained in:
1 parent
80060ee925
commit
9d1884fb19
9 files changed
+977
-216
No files matched your search
@@ -24,13 +24,15 @@ type niriActionPart struct {
|
||||
quoted bool
|
||||
}
|
||||
|
||||
var niriActionPropertyOrder = []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"}
|
||||
var niriActionPropertyOrder = []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms", "filter", "scope"}
|
||||
var niriActionProperties = map[string]struct{}{
|
||||
"focus": {},
|
||||
"show-pointer": {},
|
||||
"write-to-disk": {},
|
||||
"skip-confirmation": {},
|
||||
"delay-ms": {},
|
||||
"filter": {},
|
||||
"scope": {},
|
||||
}
|
||||
|
||||
func NewNiriProvider(configDir string) *NiriProvider {
|
||||
@@ -466,7 +468,7 @@ func (n *NiriProvider) buildActionNode(action string) *document.Node {
|
||||
}
|
||||
|
||||
func (n *NiriProvider) isNiriActionPropertyToken(part niriActionPart) bool {
|
||||
if part.quoted || !strings.Contains(part.value, "=") {
|
||||
if !strings.Contains(part.value, "=") {
|
||||
return false
|
||||
}
|
||||
key, _, ok := strings.Cut(part.value, "=")
|
||||
|
||||
@@ -483,7 +483,7 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
|
||||
args = append(args, arg.ValueString())
|
||||
}
|
||||
if actionNode.Properties != nil {
|
||||
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
|
||||
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms", "filter", "scope"} {
|
||||
if val, ok := actionNode.Properties.Get(propName); ok {
|
||||
args = append(args, propName+"="+val.String())
|
||||
}
|
||||
|
||||
@@ -811,6 +811,7 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
||||
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; }
|
||||
Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; }
|
||||
Alt+Tab { next-window scope="output"; }
|
||||
Alt+grave { next-window filter="app-id"; }
|
||||
}
|
||||
`
|
||||
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||
@@ -822,8 +823,8 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
||||
t.Fatalf("ParseNiriKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(result.Section.Keybinds) != 3 {
|
||||
t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds))
|
||||
if len(result.Section.Keybinds) != 4 {
|
||||
t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds))
|
||||
}
|
||||
|
||||
for _, kb := range result.Section.Keybinds {
|
||||
@@ -849,8 +850,16 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
||||
t.Errorf("move-column-to-workspace missing focus=false arg")
|
||||
}
|
||||
case "next-window":
|
||||
if kb.Key != "Tab" {
|
||||
t.Errorf("next-window key = %q, want 'Tab'", kb.Key)
|
||||
if kb.Key == "Tab" {
|
||||
if len(kb.Args) != 1 || kb.Args[0] != "scope=\"output\"" {
|
||||
t.Errorf("next-window Tab args = %v, want [scope=\"output\"]", kb.Args)
|
||||
}
|
||||
} else if kb.Key == "grave" {
|
||||
if len(kb.Args) != 1 || kb.Args[0] != "filter=\"app-id\"" {
|
||||
t.Errorf("next-window grave args = %v, want [filter=\"app-id\"]", kb.Args)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("unexpected next-window key: %q", kb.Key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,3 +711,30 @@ func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
|
||||
t.Error("set-column-width -10% not found after round-trip")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNiriGenerateActionWithFilterAndScope(t *testing.T) {
|
||||
provider := NewNiriProvider("")
|
||||
|
||||
binds := map[string]*overrideBind{
|
||||
"Alt+grave": {
|
||||
Key: "Alt+grave",
|
||||
Action: `next-window filter="app-id"`,
|
||||
Description: "Next Window (Same Application)",
|
||||
},
|
||||
"Alt+Tab": {
|
||||
Key: "Alt+Tab",
|
||||
Action: `next-window scope="output"`,
|
||||
Description: "Next Window",
|
||||
},
|
||||
}
|
||||
|
||||
content := provider.generateBindsContent(binds)
|
||||
expected := `binds {
|
||||
Alt+Tab hotkey-overlay-title="Next Window" { next-window scope="output"; }
|
||||
Alt+grave hotkey-overlay-title="Next Window (Same Application)" { next-window filter="app-id"; }
|
||||
}
|
||||
`
|
||||
if content != expected {
|
||||
t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected)
|
||||
}
|
||||
}
|
||||
@@ -343,3 +343,145 @@ function eventMatchesCombo(event, combo) {
|
||||
return false;
|
||||
return event.key === qtKeyFromName(keyName);
|
||||
}
|
||||
|
||||
const KEY_GLYPH_MAP = {
|
||||
"shift": "⇧",
|
||||
"ctrl": "⌃",
|
||||
"control": "⌃",
|
||||
"alt": "⌥",
|
||||
"mod": "⌘",
|
||||
"super": "⌘",
|
||||
"meta": "⌘",
|
||||
"win": "⌘",
|
||||
"return": "⏎",
|
||||
"enter": "⏎",
|
||||
"tab": "⇥",
|
||||
"backspace": "⌫",
|
||||
"space": "␣",
|
||||
"escape": "Esc",
|
||||
"esc": "Esc",
|
||||
"up": "↑",
|
||||
"down": "↓",
|
||||
"left": "←",
|
||||
"right": "→",
|
||||
"page_up": "⇞",
|
||||
"pageup": "⇞",
|
||||
"page_down": "⇟",
|
||||
"pagedown": "⇟",
|
||||
"home": "Home",
|
||||
"end": "End",
|
||||
"delete": "Del",
|
||||
"del": "Del",
|
||||
"insert": "Ins",
|
||||
"print": "PrtSc",
|
||||
"printscreen": "PrtSc",
|
||||
"prtscr": "PrtSc",
|
||||
"prtsc": "PrtSc",
|
||||
"sysrq": "PrtSc",
|
||||
"plus": "+",
|
||||
"minus": "-",
|
||||
"equal": "=",
|
||||
"slash": "/",
|
||||
"backslash": "\\",
|
||||
"bracketleft": "[",
|
||||
"bracketright": "]",
|
||||
"semicolon": ";",
|
||||
"apostrophe": "'",
|
||||
"comma": ",",
|
||||
"period": ".",
|
||||
"grave": "`",
|
||||
"wheelscrolldown": " ↓",
|
||||
"wheelscrollup": " ↑",
|
||||
"wheelscrollleft": " ←",
|
||||
"wheelscrollright": " →",
|
||||
"mousescrolldown": " ↓",
|
||||
"mousescrollup": " ↑",
|
||||
"mouse_down": " ↓",
|
||||
"mouse_up": " ↑",
|
||||
"mouse_left": " ←",
|
||||
"mouse_right": " →",
|
||||
"scrolldown": " ↓",
|
||||
"scrollup": " ↑",
|
||||
|
||||
// Audio & Volume
|
||||
"xf86audioraisevolume": " +",
|
||||
"audioraisevolume": " +",
|
||||
"xf86audiolowervolume": " -",
|
||||
"audiolowervolume": " -",
|
||||
"xf86audiomute": "",
|
||||
"audiomute": "",
|
||||
"xf86audiomicmute": "",
|
||||
"audiomicmute": "",
|
||||
|
||||
// Media Playback
|
||||
"xf86audioplay": "",
|
||||
"audioplay": "",
|
||||
"xf86audiopause": "",
|
||||
"audiopause": "",
|
||||
"xf86audioplaypause": "",
|
||||
"audioplaypause": "",
|
||||
"xf86audionext": "",
|
||||
"audionext": "",
|
||||
"xf86audioprev": "",
|
||||
"audioprev": "",
|
||||
"xf86audiostop": "",
|
||||
"audiostop": "",
|
||||
"xf86audiorecord": "Rec",
|
||||
"audiorecord": "Rec",
|
||||
"xf86audiomedia": "Media",
|
||||
"audiomedia": "Media",
|
||||
|
||||
// Display & Brightness
|
||||
"xf86monbrightnessup": " +",
|
||||
"monbrightnessup": " +",
|
||||
"xf86monbrightnessdown": " -",
|
||||
"monbrightnessdown": " -",
|
||||
"xf86kbdbrightnessup": " +",
|
||||
"kbdbrightnessup": " +",
|
||||
"xf86kbdbrightnessdown": " -",
|
||||
"kbdbrightnessdown": " -",
|
||||
|
||||
// Hardware & System
|
||||
"xf86calculator": "Calc",
|
||||
"xf86search": "Search",
|
||||
"xf86mail": "Mail",
|
||||
"xf86homepage": "Web",
|
||||
"xf86explorer": "Files",
|
||||
"xf86poweroff": "Power",
|
||||
"xf86sleep": "Sleep",
|
||||
"xf86wakeup": "Wake",
|
||||
"xf86eject": "Eject",
|
||||
"xf86launch0": "Launch 0",
|
||||
"xf86launch1": "PrtSc",
|
||||
"launch1": "PrtSc"
|
||||
};
|
||||
|
||||
function formatKeyTokens(keyString) {
|
||||
if (!keyString)
|
||||
return [];
|
||||
var str = keyString;
|
||||
var plusEnds = false;
|
||||
if (str.endsWith("++")) {
|
||||
plusEnds = true;
|
||||
str = str.slice(0, -2);
|
||||
}
|
||||
var parts = str.split("+");
|
||||
var tokens = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var part = parts[i].trim();
|
||||
if (!part)
|
||||
continue;
|
||||
var lower = part.toLowerCase();
|
||||
if (KEY_GLYPH_MAP[lower] !== undefined) {
|
||||
tokens.push(KEY_GLYPH_MAP[lower]);
|
||||
} else if (part.length === 1) {
|
||||
tokens.push(part.toUpperCase());
|
||||
} else {
|
||||
tokens.push(part);
|
||||
}
|
||||
}
|
||||
if (plusEnds) {
|
||||
tokens.push("+");
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
@@ -227,7 +227,9 @@ const NIRI_ACTIONS = {
|
||||
],
|
||||
"Alt-Tab": [
|
||||
{ id: "next-window", label: "Next Window" },
|
||||
{ id: "previous-window", label: "Previous Window" }
|
||||
{ id: "previous-window", label: "Previous Window" },
|
||||
{ id: "next-window filter=\"app-id\"", label: "Next Window (Same Application)" },
|
||||
{ id: "previous-window filter=\"app-id\"", label: "Previous Window (Same Application)" }
|
||||
]
|
||||
};
|
||||
|
||||
@@ -932,8 +934,16 @@ function getActionLabel(action, compositor) {
|
||||
return compAct.label;
|
||||
var base = action.split(" ")[0];
|
||||
compAct = findCompositorAction(compositor, base);
|
||||
if (compAct)
|
||||
if (compAct) {
|
||||
var arg = action.slice(base.length).trim();
|
||||
if (arg && compAct.label.includes("(by index)")) {
|
||||
return compAct.label.replace("(by index)", arg).trim();
|
||||
}
|
||||
if (arg.includes("filter=") && arg.includes("app-id")) {
|
||||
return compAct.label + " (Same Application)";
|
||||
}
|
||||
return compAct.label;
|
||||
}
|
||||
}
|
||||
|
||||
if (action.startsWith("spawn sh -c "))
|
||||
@@ -948,7 +958,7 @@ function getActionType(action) {
|
||||
return "compositor";
|
||||
if (isDmsAction(action))
|
||||
return "dms";
|
||||
if (/^spawn \w+ -c /.test(action) || action.startsWith("spawn_shell "))
|
||||
if (/^spawn \w+ -c /.test(action) || action.startsWith("spawn_shell ") || action.startsWith("spawn-sh "))
|
||||
return "shell";
|
||||
if (action.startsWith("spawn "))
|
||||
return "spawn";
|
||||
@@ -971,6 +981,8 @@ function isValidAction(action) {
|
||||
case "spawn sh -c ''":
|
||||
case "spawn_shell":
|
||||
case "spawn_shell ":
|
||||
case "spawn-sh":
|
||||
case "spawn-sh ":
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -1027,6 +1039,12 @@ function parseShellCommand(action) {
|
||||
}
|
||||
if (action.startsWith("spawn_shell "))
|
||||
return action.slice(12);
|
||||
if (action.startsWith("spawn-sh ")) {
|
||||
var cmd = action.slice(9).trim();
|
||||
if ((cmd.startsWith('"') && cmd.endsWith('"')) || (cmd.startsWith("'") && cmd.endsWith("'")))
|
||||
cmd = cmd.slice(1, -1);
|
||||
return cmd;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@@ -4,15 +4,129 @@ import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import "../Common/KeyUtils.js" as KeyUtils
|
||||
import "../Common/KeybindActions.js" as Actions
|
||||
|
||||
FocusScope {
|
||||
id: content
|
||||
focus: true
|
||||
|
||||
function getBindLabel(bind) {
|
||||
if (!bind)
|
||||
return "";
|
||||
var label = bind.desc || "";
|
||||
if (!label && bind.action) {
|
||||
label = Actions.getActionLabel(bind.action, KeybindsService.currentProvider) || bind.action;
|
||||
}
|
||||
if (!label)
|
||||
return "";
|
||||
if (label.includes("(by index)") && bind.action) {
|
||||
var parts = bind.action.trim().split(/\s+/);
|
||||
if (parts.length > 1) {
|
||||
var arg = parts[parts.length - 1];
|
||||
label = label.replace("(by index)", arg).trim();
|
||||
}
|
||||
}
|
||||
if ((bind.action === "next-window" || bind.action === "previous-window") && (bind.key || "").toLowerCase().includes("grave")) {
|
||||
if (label === "Next Window")
|
||||
label = "Next Window (Same Application)";
|
||||
else if (label === "Previous Window")
|
||||
label = "Previous Window (Same Application)";
|
||||
}
|
||||
if (label.startsWith("Media: "))
|
||||
label = label.slice(7).trim();
|
||||
return label;
|
||||
}
|
||||
|
||||
Component {
|
||||
id: keybindItemDelegate
|
||||
|
||||
DankListItem {
|
||||
id: keybindRow
|
||||
required property var modelData
|
||||
readonly property bool canExecute: !keybindRow.modelData.isRange && KeybindsService.canExecuteAction(keybindRow.modelData.action)
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
implicitHeight: Math.max(Theme.menuItemHeight, keycapsCol.implicitHeight + Theme.spacingS * 2)
|
||||
firstInGroup: true
|
||||
lastInGroup: true
|
||||
radius: Theme.groupedListOuterRadius
|
||||
Accessible.name: keybindRow.modelData.label || content.getBindLabel(keybindRow.modelData)
|
||||
Accessible.description: (keybindRow.modelData.allKeys ? keybindRow.modelData.allKeys.join(", ") : (keybindRow.modelData.key || "")) + " • " + (keybindRow.modelData.action || "")
|
||||
onClicked: {
|
||||
if (keybindRow.canExecute && KeybindsService.executeAction(keybindRow.modelData.action))
|
||||
content.closeRequested();
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
anchors.bottomMargin: Theme.spacingXS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Description / Action (Leading)
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
text: keybindRow.modelData.label || content.getBindLabel(keybindRow.modelData)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: keybindRow.contentColor
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
// DankKeycap items (Trailing - stacked vertically if multiple combos)
|
||||
Column {
|
||||
id: keycapsCol
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: keybindRow.modelData.keyCombos || (keybindRow.modelData.key ? [KeyUtils.formatKeyTokens(keybindRow.modelData.key)] : [])
|
||||
|
||||
Row {
|
||||
anchors.right: parent ? parent.right : undefined
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
Repeater {
|
||||
model: modelData
|
||||
|
||||
DankKeycap {
|
||||
text: modelData
|
||||
textColor: Theme.primary
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rowHoverArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
cursorShape: keybindRow.canExecute ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
}
|
||||
|
||||
DankTooltipHost {
|
||||
text: keybindRow.canExecute ? ((keybindRow.modelData.action || keybindRow.modelData.desc || "") + " • " + I18n.tr("Click to run", "cheatsheet action tooltip suffix")) : (keybindRow.modelData.label || keybindRow.modelData.action || keybindRow.modelData.desc || "")
|
||||
target: keybindRow
|
||||
hoverArea: rowHoverArea
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property real scrollStep: 60
|
||||
property var activeFlickable: mainFlickable
|
||||
property var activeFlickable: rightFlickable
|
||||
property bool showFloatingToggle: true
|
||||
property bool floating: false
|
||||
property alias searchField: searchField
|
||||
property string selectedCategory: "All"
|
||||
|
||||
signal closeRequested
|
||||
signal floatingToggleRequested
|
||||
@@ -21,7 +135,7 @@ FocusScope {
|
||||
if (!activeFlickable)
|
||||
return;
|
||||
let newY = activeFlickable.contentY + scrollStep;
|
||||
newY = Math.min(newY, activeFlickable.contentHeight - activeFlickable.height);
|
||||
newY = Math.min(newY, Math.max(0, activeFlickable.contentHeight - activeFlickable.height));
|
||||
activeFlickable.contentY = newY;
|
||||
}
|
||||
|
||||
@@ -33,7 +147,24 @@ FocusScope {
|
||||
activeFlickable.contentY = newY;
|
||||
}
|
||||
|
||||
function focusSearch() {
|
||||
if (searchField) {
|
||||
searchField.forceActiveFocus();
|
||||
searchField.selectAll();
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: "Ctrl+F"
|
||||
onActivated: content.focusSearch()
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_F) {
|
||||
focusSearch();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
@@ -58,269 +189,595 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
readonly property var categoryPriority: ({
|
||||
"window": 1,
|
||||
"windows": 1,
|
||||
"window management": 1,
|
||||
"workspace": 2,
|
||||
"workspaces": 2,
|
||||
"applications": 3,
|
||||
"apps": 3,
|
||||
"audio": 4,
|
||||
"sound": 4,
|
||||
"media": 5,
|
||||
"brightness": 6,
|
||||
"dms": 7,
|
||||
"compositor": 8,
|
||||
"custom": 9,
|
||||
"system": 10
|
||||
})
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
function getCategoryIcon(catName) {
|
||||
if (!catName)
|
||||
return "keyboard";
|
||||
const lower = catName.toLowerCase();
|
||||
if (lower === "all")
|
||||
return "apps";
|
||||
if (lower.includes("window"))
|
||||
return "desktop_windows";
|
||||
if (lower.includes("workspace"))
|
||||
return "grid_view";
|
||||
if (lower.includes("alt-tab") || lower.includes("alt_tab") || lower.includes("switcher"))
|
||||
return "swap_horiz";
|
||||
if (lower.includes("screenshot") || lower.includes("capture"))
|
||||
return "screenshot_region";
|
||||
if (lower.includes("overview"))
|
||||
return "overview";
|
||||
if (lower.includes("monitor") || lower.includes("display"))
|
||||
return "monitor";
|
||||
if (lower.includes("exec") || lower.includes("terminal") || lower.includes("spawn") || lower.includes("command"))
|
||||
return "terminal";
|
||||
if (lower.includes("scratchpad"))
|
||||
return "sticky_note_2";
|
||||
if (lower.includes("layout"))
|
||||
return "view_quilt";
|
||||
if (lower.includes("gap"))
|
||||
return "border_inner";
|
||||
if (lower.includes("tag"))
|
||||
return "tag";
|
||||
if (lower.includes("app"))
|
||||
return "open_in_new";
|
||||
if (lower.includes("audio") || lower.includes("sound") || lower.includes("media"))
|
||||
return "volume_up";
|
||||
if (lower.includes("bright"))
|
||||
return "brightness_6";
|
||||
if (lower.includes("dms"))
|
||||
return "widgets";
|
||||
if (lower.includes("system") || lower.includes("power"))
|
||||
return "power_settings_new";
|
||||
if (lower.includes("compositor"))
|
||||
return "tune";
|
||||
if (lower.includes("custom"))
|
||||
return "edit";
|
||||
if (lower.includes("other"))
|
||||
return "more_horiz";
|
||||
return "keyboard";
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
text: KeybindsService.cheatsheet.title || I18n.tr("Keybinds")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.primary
|
||||
}
|
||||
property var rawBinds: KeybindsService.cheatsheet.binds || ({})
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
function collapseSequentialBinds(list) {
|
||||
if (!list || list.length < 3)
|
||||
return list || [];
|
||||
const numberPattern = /^(.*?)(\s+)(\d+)$/;
|
||||
const clusters = {};
|
||||
|
||||
DankActionButton {
|
||||
visible: content.showFloatingToggle
|
||||
iconName: content.floating ? "close_fullscreen" : "open_in_new"
|
||||
tooltipText: content.floating ? I18n.tr("Dock window") : I18n.tr("Open as window")
|
||||
onClicked: content.floatingToggleRequested()
|
||||
}
|
||||
|
||||
DankSearchField {
|
||||
id: searchField
|
||||
Layout.alignment: Qt.AlignRight
|
||||
keyForwardTargets: [content]
|
||||
onTextEdited: searchDebounce.restart()
|
||||
Keys.onEscapePressed: event => {
|
||||
content.closeRequested();
|
||||
event.accepted = true;
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i];
|
||||
const match = (item.label || "").match(numberPattern);
|
||||
const combo = item.keyCombos && item.keyCombos[0];
|
||||
if (match && combo && combo.length > 0) {
|
||||
const lastToken = combo[combo.length - 1];
|
||||
const num = parseInt(match[3], 10);
|
||||
if (!isNaN(num) && lastToken === String(num)) {
|
||||
const baseLabel = match[1];
|
||||
const mods = combo.slice(0, -1);
|
||||
const modSig = mods.join("+");
|
||||
const clusterKey = (baseLabel + "||" + modSig).toLowerCase();
|
||||
if (!clusters[clusterKey])
|
||||
clusters[clusterKey] = [];
|
||||
clusters[clusterKey].push({
|
||||
item: item,
|
||||
num: num,
|
||||
baseLabel: baseLabel,
|
||||
mods: mods,
|
||||
index: i
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchDebounce
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
mainFlickable.categories = mainFlickable.generateCategories(searchField.text);
|
||||
const collapsedMap = {};
|
||||
const skipIndices = {};
|
||||
|
||||
for (const cKey in clusters) {
|
||||
const cluster = clusters[cKey];
|
||||
if (cluster.length >= 3) {
|
||||
cluster.sort((a, b) => a.num - b.num);
|
||||
let isSeq = true;
|
||||
for (let k = 1; k < cluster.length; k++) {
|
||||
if (cluster[k].num !== cluster[k - 1].num + 1) {
|
||||
isSeq = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isSeq) {
|
||||
const first = cluster[0];
|
||||
const last = cluster[cluster.length - 1];
|
||||
const rangeStr = first.num + "–" + last.num;
|
||||
const combinedLabel = first.baseLabel + " " + rangeStr;
|
||||
const allKeys = [];
|
||||
for (let k = 0; k < cluster.length; k++) {
|
||||
if (cluster[k].item.allKeys) {
|
||||
for (let ak = 0; ak < cluster[k].item.allKeys.length; ak++) {
|
||||
allKeys.push(cluster[k].item.allKeys[ak]);
|
||||
}
|
||||
} else if (cluster[k].item.key) {
|
||||
allKeys.push(cluster[k].item.key);
|
||||
}
|
||||
if (k > 0) {
|
||||
skipIndices[cluster[k].index] = true;
|
||||
}
|
||||
}
|
||||
collapsedMap[first.index] = {
|
||||
action: "",
|
||||
desc: combinedLabel,
|
||||
label: combinedLabel,
|
||||
key: first.item.key,
|
||||
allKeys: allKeys,
|
||||
tokenSigs: {},
|
||||
keyCombos: [[...first.mods, rangeStr]],
|
||||
isRange: true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
id: mainFlickable
|
||||
width: parent.width
|
||||
height: parent.height - parent.spacing - 40
|
||||
contentWidth: rowLayout.implicitWidth
|
||||
contentHeight: rowLayout.implicitHeight
|
||||
clip: true
|
||||
const result = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (collapsedMap[i]) {
|
||||
result.push(collapsedMap[i]);
|
||||
} else if (!skipIndices[i]) {
|
||||
result.push(list[i]);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
property var rawBinds: KeybindsService.cheatsheet.binds || {}
|
||||
function generateCategories(query) {
|
||||
const lowerQuery = query ? query.toLowerCase().trim() : "";
|
||||
const lowerQueryWords = lowerQuery ? lowerQuery.split(/\s+/) : [];
|
||||
const processed = {};
|
||||
let totalCount = 0;
|
||||
|
||||
function generateCategories(query) {
|
||||
const lowerQuery = query ? query.toLowerCase().trim() : "";
|
||||
const lowerQueryWords = query.split(/\s+/);
|
||||
const processed = {};
|
||||
for (const cat in rawBinds) {
|
||||
const binds = rawBinds[cat];
|
||||
if (!Array.isArray(binds))
|
||||
continue;
|
||||
const catLower = cat.toLowerCase();
|
||||
const subcatMap = {};
|
||||
let hasBinds = false;
|
||||
|
||||
for (const cat in rawBinds) {
|
||||
const binds = rawBinds[cat];
|
||||
const catLower = cat.toLowerCase();
|
||||
const subcats = {};
|
||||
let hasSubcats = false;
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
const bind = binds[i];
|
||||
const keyLower = (bind.key || "").toLowerCase();
|
||||
const descLower = (bind.desc || "").toLowerCase();
|
||||
const actionLower = (bind.action || "").toLowerCase();
|
||||
for (let i = 0; i < binds.length; i++) {
|
||||
const bind = binds[i];
|
||||
if (bind.hideOnOverlay)
|
||||
continue;
|
||||
|
||||
if (bind.hideOnOverlay)
|
||||
continue;
|
||||
let shouldContinue = false;
|
||||
for (let j = 0; j < lowerQueryWords.length; j++) {
|
||||
const word = lowerQueryWords[j];
|
||||
if (!(word.length === 0 || keyLower.includes(word) || descLower.includes(word) || catLower.includes(word) || actionLower.includes(word))) {
|
||||
shouldContinue = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldContinue)
|
||||
continue;
|
||||
const label = content.getBindLabel(bind);
|
||||
const labelLower = label.toLowerCase();
|
||||
const keyTokens = KeyUtils.formatKeyTokens(bind.key);
|
||||
const tokenSig = keyTokens.join("+");
|
||||
const keyLower = (bind.key || "").toLowerCase();
|
||||
const descLower = (bind.desc || "").toLowerCase();
|
||||
const actionLower = (bind.action || "").toLowerCase();
|
||||
|
||||
if (bind.subcat) {
|
||||
hasSubcats = true;
|
||||
if (!subcats[bind.subcat])
|
||||
subcats[bind.subcat] = [];
|
||||
subcats[bind.subcat].push(bind);
|
||||
} else {
|
||||
if (!subcats["_root"])
|
||||
subcats["_root"] = [];
|
||||
subcats["_root"].push(bind);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(subcats).length === 0)
|
||||
let matched = true;
|
||||
for (let j = 0; j < lowerQueryWords.length; j++) {
|
||||
const word = lowerQueryWords[j];
|
||||
if (!word)
|
||||
continue;
|
||||
if (!keyLower.includes(word) && !labelLower.includes(word) && !descLower.includes(word) && !catLower.includes(word) && !actionLower.includes(word) && !tokenSig.toLowerCase().includes(word)) {
|
||||
matched = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matched)
|
||||
continue;
|
||||
|
||||
const subcatName = bind.subcat || "_root";
|
||||
if (!subcatMap[subcatName])
|
||||
subcatMap[subcatName] = {};
|
||||
|
||||
const isGraveAltTab = (bind.action === "next-window" || bind.action === "previous-window") && (bind.key || "").toLowerCase().includes("grave") && !bind.action.includes("filter=");
|
||||
const bindAction = isGraveAltTab ? (bind.action + ' filter="app-id"') : (bind.action || "");
|
||||
const groupKey = (label + "||" + bindAction).toLowerCase();
|
||||
if (subcatMap[subcatName][groupKey]) {
|
||||
const existing = subcatMap[subcatName][groupKey];
|
||||
if (tokenSig && !existing.tokenSigs[tokenSig] && keyTokens.length > 0) {
|
||||
existing.tokenSigs[tokenSig] = true;
|
||||
existing.keyCombos.push(keyTokens);
|
||||
existing.allKeys.push(bind.key);
|
||||
}
|
||||
} else {
|
||||
const sigs = {};
|
||||
if (tokenSig && keyTokens.length > 0)
|
||||
sigs[tokenSig] = true;
|
||||
subcatMap[subcatName][groupKey] = {
|
||||
action: bindAction,
|
||||
desc: bind.desc,
|
||||
label: label,
|
||||
key: bind.key,
|
||||
allKeys: [bind.key],
|
||||
tokenSigs: sigs,
|
||||
keyCombos: keyTokens.length > 0 ? [keyTokens] : []
|
||||
};
|
||||
hasBinds = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasBinds) {
|
||||
const subcats = {};
|
||||
let catBindCount = 0;
|
||||
for (const subcatName in subcatMap) {
|
||||
const groups = subcatMap[subcatName];
|
||||
const rawList = [];
|
||||
for (const gKey in groups) {
|
||||
rawList.push(groups[gKey]);
|
||||
}
|
||||
const collapsedList = content.collapseSequentialBinds(rawList);
|
||||
subcats[subcatName] = collapsedList;
|
||||
catBindCount += collapsedList.length;
|
||||
}
|
||||
if (catBindCount > 0) {
|
||||
processed[cat] = {
|
||||
hasSubcats: hasSubcats,
|
||||
name: cat,
|
||||
count: catBindCount,
|
||||
subcats: subcats,
|
||||
subcatKeys: Object.keys(subcats)
|
||||
};
|
||||
totalCount += catBindCount;
|
||||
}
|
||||
|
||||
return processed;
|
||||
}
|
||||
}
|
||||
|
||||
property var categories: generateCategories("")
|
||||
const keys = Object.keys(processed).sort((a, b) => {
|
||||
const pA = categoryPriority[a.toLowerCase()] || 99;
|
||||
const pB = categoryPriority[b.toLowerCase()] || 99;
|
||||
if (pA !== pB)
|
||||
return pA - pB;
|
||||
return a.localeCompare(b);
|
||||
});
|
||||
|
||||
function estimateCategoryHeight(catName) {
|
||||
const catData = categories[catName];
|
||||
if (!catData)
|
||||
return 0;
|
||||
let bindCount = 0;
|
||||
for (const key of catData.subcatKeys) {
|
||||
bindCount += catData.subcats[key]?.length || 0;
|
||||
if (key !== "_root")
|
||||
bindCount += 1;
|
||||
}
|
||||
return 40 + bindCount * 28;
|
||||
return {
|
||||
byCategory: processed,
|
||||
sortedKeys: keys,
|
||||
totalCount: totalCount
|
||||
};
|
||||
}
|
||||
|
||||
property string activeSearchQuery: ""
|
||||
property var dataModel: generateCategories("")
|
||||
|
||||
Connections {
|
||||
target: KeybindsService
|
||||
function onCheatsheetLoaded() {
|
||||
content.dataModel = content.generateCategories(content.activeSearchQuery);
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchDebounce
|
||||
interval: 120
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
content.activeSearchQuery = searchField.text;
|
||||
content.dataModel = content.generateCategories(searchField.text);
|
||||
if (searchField.text.trim() !== "" && content.selectedCategory !== "All") {
|
||||
if (!content.dataModel.byCategory[content.selectedCategory])
|
||||
content.selectedCategory = "All";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property var categoryKeys: Object.keys(categories)
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
|
||||
function distributeCategories(cols) {
|
||||
const columns = [];
|
||||
const heights = [];
|
||||
for (let i = 0; i < cols; i++) {
|
||||
columns.push([]);
|
||||
heights.push(0);
|
||||
}
|
||||
const sorted = [...categoryKeys].sort((a, b) => estimateCategoryHeight(b) - estimateCategoryHeight(a));
|
||||
for (const cat of sorted) {
|
||||
let minIdx = 0;
|
||||
for (let i = 1; i < cols; i++) {
|
||||
if (heights[i] < heights[minIdx])
|
||||
minIdx = i;
|
||||
// Sidebar
|
||||
Item {
|
||||
id: sidebar
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: 260
|
||||
|
||||
// Search Bar + Float Window Toggle at top of sidebar
|
||||
RowLayout {
|
||||
id: searchRow
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankSearchField {
|
||||
id: searchField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: I18n.tr("Search keybinds...", "keybinds cheatsheet search placeholder")
|
||||
keyForwardTargets: [content]
|
||||
onTextChanged: {
|
||||
if (text.trim() === "") {
|
||||
searchDebounce.stop();
|
||||
content.activeSearchQuery = "";
|
||||
content.dataModel = content.generateCategories("");
|
||||
} else {
|
||||
searchDebounce.restart();
|
||||
}
|
||||
}
|
||||
Keys.onEscapePressed: event => {
|
||||
content.closeRequested();
|
||||
event.accepted = true;
|
||||
}
|
||||
columns[minIdx].push(cat);
|
||||
heights[minIdx] += estimateCategoryHeight(cat);
|
||||
}
|
||||
return columns;
|
||||
|
||||
DankActionButton {
|
||||
visible: content.showFloatingToggle
|
||||
buttonSize: Theme.iconButtonSize
|
||||
iconName: content.floating ? "close_fullscreen" : "open_in_new"
|
||||
tooltipText: content.floating ? I18n.tr("Dock window") : I18n.tr("Open as window")
|
||||
onClicked: content.floatingToggleRequested()
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rowLayout
|
||||
width: mainFlickable.width
|
||||
spacing: Theme.spacingM
|
||||
DankFlickable {
|
||||
id: sidebarFlickable
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: searchRow.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
contentWidth: width
|
||||
contentHeight: sidebarCol.implicitHeight
|
||||
clip: true
|
||||
|
||||
property int numColumns: Math.max(1, Math.min(3, Math.floor(width / 350)))
|
||||
property var columnCategories: mainFlickable.distributeCategories(numColumns)
|
||||
Column {
|
||||
id: sidebarCol
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.numColumns
|
||||
// "All" Category Tab
|
||||
DankListItem {
|
||||
id: allTab
|
||||
width: sidebarCol.width
|
||||
implicitHeight: Theme.menuItemHeight
|
||||
isSelected: content.selectedCategory === "All"
|
||||
surfaceColor: "transparent"
|
||||
border.width: 0
|
||||
radius: Theme.cornerRadiusFull
|
||||
topLeftRadius: Theme.cornerRadiusFull
|
||||
topRightRadius: Theme.cornerRadiusFull
|
||||
bottomLeftRadius: Theme.cornerRadiusFull
|
||||
bottomRightRadius: Theme.cornerRadiusFull
|
||||
Accessible.name: I18n.tr("All")
|
||||
onClicked: content.selectedCategory = "All"
|
||||
|
||||
Column {
|
||||
id: masonryColumn
|
||||
width: (rowLayout.width - rowLayout.spacing * (rowLayout.numColumns - 1)) / rowLayout.numColumns
|
||||
spacing: Theme.spacingXL
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: rowLayout.columnCategories[index] || []
|
||||
DankIcon {
|
||||
name: "apps"
|
||||
size: Theme.iconSizeSmall
|
||||
color: allTab.contentColor
|
||||
}
|
||||
|
||||
Column {
|
||||
id: categoryColumn
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
StyledText {
|
||||
text: I18n.tr("All")
|
||||
color: allTab.contentColor
|
||||
font.weight: allTab.isSelected ? Theme.fontWeightMedium : Theme.fontWeightNormal
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
property string catName: modelData
|
||||
property var catData: mainFlickable.categories[catName]
|
||||
DankBadge {
|
||||
text: content.dataModel.totalCount.toString()
|
||||
color: allTab.isSelected ? Theme.primary : Theme.surfaceVariant
|
||||
textColor: allTab.isSelected ? Theme.onPrimary : Theme.surfaceVariantText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Divider between All and specific categories
|
||||
Rectangle {
|
||||
width: sidebarCol.width - Theme.spacingS * 2
|
||||
height: Theme.dividerWidth
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Theme.outlineVariant
|
||||
opacity: 0.3
|
||||
}
|
||||
|
||||
// Category Tabs
|
||||
Repeater {
|
||||
model: content.dataModel.sortedKeys
|
||||
|
||||
DankListItem {
|
||||
id: catTab
|
||||
required property var modelData
|
||||
|
||||
readonly property string catName: modelData
|
||||
readonly property var catInfo: content.dataModel.byCategory[catName]
|
||||
|
||||
width: sidebarCol.width
|
||||
implicitHeight: Theme.menuItemHeight
|
||||
isSelected: content.selectedCategory === catTab.catName
|
||||
surfaceColor: "transparent"
|
||||
border.width: 0
|
||||
radius: Theme.cornerRadiusFull
|
||||
topLeftRadius: Theme.cornerRadiusFull
|
||||
topRightRadius: Theme.cornerRadiusFull
|
||||
bottomLeftRadius: Theme.cornerRadiusFull
|
||||
bottomRightRadius: Theme.cornerRadiusFull
|
||||
Accessible.name: I18n.tr(catTab.catName)
|
||||
onClicked: content.selectedCategory = catTab.catName
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: content.getCategoryIcon(catTab.catName)
|
||||
size: Theme.iconSizeSmall
|
||||
color: catTab.contentColor
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: categoryColumn.catName
|
||||
text: I18n.tr(catTab.catName)
|
||||
color: catTab.contentColor
|
||||
font.weight: catTab.isSelected ? Theme.fontWeightMedium : Theme.fontWeightNormal
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankBadge {
|
||||
text: (catTab.catInfo?.count || 0).toString()
|
||||
color: catTab.isSelected ? Theme.primary : Theme.surfaceVariant
|
||||
textColor: catTab.isSelected ? Theme.onPrimary : Theme.surfaceVariantText
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical Divider between Sidebar and Main Content
|
||||
Rectangle {
|
||||
id: vDivider
|
||||
anchors.left: sidebar.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: Theme.dividerWidth
|
||||
color: Theme.outlineVariant
|
||||
opacity: 0.4
|
||||
}
|
||||
|
||||
// Main Content Area
|
||||
Item {
|
||||
id: mainArea
|
||||
anchors.left: vDivider.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
DankFlickable {
|
||||
id: rightFlickable
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: contentCol.implicitHeight
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
id: contentCol
|
||||
width: rightFlickable.width - Theme.spacingS
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Categories Renderer
|
||||
Repeater {
|
||||
model: content.selectedCategory === "All" ? content.dataModel.sortedKeys : (content.dataModel.byCategory[content.selectedCategory] ? [content.selectedCategory] : [])
|
||||
|
||||
Column {
|
||||
id: sectionCol
|
||||
width: contentCol.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
readonly property string sectionCatName: modelData
|
||||
readonly property var sectionCatInfo: content.dataModel.byCategory[sectionCatName]
|
||||
|
||||
// Section Header
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: content.getCategoryIcon(sectionCol.sectionCatName)
|
||||
size: Theme.iconSizeMedium
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr(sectionCol.sectionCatName)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.primary
|
||||
Layout.fillWidth: true
|
||||
height: Theme.dividerWidth
|
||||
color: Theme.outlineVariant
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingXS
|
||||
}
|
||||
// Subcategories Repeater
|
||||
Repeater {
|
||||
model: sectionCol.sectionCatInfo?.subcatKeys || []
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
id: subcatCol
|
||||
width: sectionCol.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: categoryColumn.catData?.subcatKeys || []
|
||||
readonly property string subcatName: modelData
|
||||
readonly property var subcatBinds: sectionCol.sectionCatInfo?.subcats?.[subcatName] || []
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
// Subcategory Title (if not _root)
|
||||
StyledText {
|
||||
visible: subcatCol.subcatName !== "_root"
|
||||
text: I18n.tr(subcatCol.subcatName)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
readonly property bool isTwoColumn: rightFlickable.width > 550
|
||||
|
||||
// 2-Column Responsive Masonry Layout
|
||||
RowLayout {
|
||||
id: keybindsMasonry
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Left Column
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
property string subcatName: modelData
|
||||
property var subcatBinds: categoryColumn.catData?.subcats?.[subcatName] || []
|
||||
|
||||
StyledText {
|
||||
visible: parent.subcatName !== "_root"
|
||||
text: parent.subcatName
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.primary
|
||||
opacity: 0.7
|
||||
Repeater {
|
||||
model: subcatCol.subcatBinds ? subcatCol.subcatBinds.filter((_, idx) => !subcatCol.isTwoColumn || idx % 2 === 0) : []
|
||||
delegate: keybindItemDelegate
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
// Right Column
|
||||
ColumnLayout {
|
||||
visible: subcatCol.isTwoColumn
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: parent.parent.subcatBinds
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 24
|
||||
|
||||
StyledRect {
|
||||
id: keyBadge
|
||||
width: Math.min(keyText.implicitWidth + 12, 160)
|
||||
height: 22
|
||||
radius: Theme.cornerRadiusXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledText {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
color: Theme.secondary
|
||||
text: (modelData.key || "").replace(/\+/g, " + ")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Theme.fontWeightMedium
|
||||
isMonospace: true
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, 148)
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 170
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.desc || modelData.action || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
opacity: 0.9
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
Repeater {
|
||||
model: subcatCol.isTwoColumn && subcatCol.subcatBinds ? subcatCol.subcatBinds.filter((_, idx) => idx % 2 === 1) : []
|
||||
delegate: keybindItemDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -330,6 +787,36 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty State
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
visible: content.dataModel.totalCount === 0
|
||||
|
||||
DankIcon {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
name: content.activeSearchQuery.trim() !== "" ? "search_off" : "keyboard"
|
||||
size: Theme.iconSizeLarge + Theme.spacingL
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: content.activeSearchQuery.trim() !== "" ? I18n.tr("No results found") : I18n.tr("No keybinds found")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: I18n.tr("Try a different search")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: content.activeSearchQuery.trim() !== ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ DankModal {
|
||||
|
||||
content: Component {
|
||||
KeybindsContent {
|
||||
anchors.fill: parent
|
||||
showFloatingToggle: true
|
||||
floating: false
|
||||
onCloseRequested: overlay.close()
|
||||
|
||||
@@ -322,6 +322,81 @@ Singleton {
|
||||
cheatsheetProcess.running = true;
|
||||
}
|
||||
|
||||
function canExecuteAction(action) {
|
||||
if (!action)
|
||||
return false;
|
||||
if (action.startsWith("spawn ") || action.startsWith("spawn_shell ") || action.startsWith("spawn-sh ") || action.startsWith("exec "))
|
||||
return true;
|
||||
const provider = currentProvider || cheatsheetProvider;
|
||||
if (provider === "niri") {
|
||||
const base = action.trim().split(/\s+/)[0];
|
||||
if (base === "next-window" || base === "previous-window")
|
||||
return false;
|
||||
}
|
||||
return provider === "niri" || provider === "hyprland" || provider === "mangowc";
|
||||
}
|
||||
|
||||
function executeAction(action) {
|
||||
if (!action)
|
||||
return false;
|
||||
log.info("Executing keybind action:", action);
|
||||
|
||||
if (action.startsWith("spawn ") || action.startsWith("spawn_shell ") || action.startsWith("spawn-sh ") || action.startsWith("exec ")) {
|
||||
let cmd = action;
|
||||
if (cmd.startsWith("spawn "))
|
||||
cmd = cmd.slice(6).trim();
|
||||
else if (cmd.startsWith("spawn_shell "))
|
||||
cmd = cmd.slice(12).trim();
|
||||
else if (cmd.startsWith("spawn-sh ")) {
|
||||
cmd = cmd.slice(9).trim();
|
||||
if ((cmd.startsWith('"') && cmd.endsWith('"')) || (cmd.startsWith("'") && cmd.endsWith("'")))
|
||||
cmd = cmd.slice(1, -1);
|
||||
} else if (cmd.startsWith("exec "))
|
||||
cmd = cmd.slice(5).trim();
|
||||
|
||||
Quickshell.execDetached(["sh", "-c", cmd]);
|
||||
return true;
|
||||
}
|
||||
|
||||
const provider = currentProvider || cheatsheetProvider;
|
||||
if (provider === "niri") {
|
||||
const parts = action.trim().split(/\s+/);
|
||||
if (parts.length === 0 || parts[0] === "next-window" || parts[0] === "previous-window")
|
||||
return false;
|
||||
|
||||
const cmdParts = [];
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const eqIdx = part.indexOf("=");
|
||||
if (eqIdx !== -1) {
|
||||
const key = part.slice(0, eqIdx);
|
||||
const val = part.slice(eqIdx + 1).replace(/^["']|["']$/g, "");
|
||||
if (key === "skip-confirmation") {
|
||||
if (val === "true")
|
||||
cmdParts.push("--skip-confirmation");
|
||||
} else {
|
||||
cmdParts.push("--" + key + "=" + val);
|
||||
}
|
||||
} else {
|
||||
cmdParts.push(part);
|
||||
}
|
||||
}
|
||||
Quickshell.execDetached(["sh", "-c", "niri msg action " + cmdParts.join(" ")]);
|
||||
return true;
|
||||
}
|
||||
if (provider === "hyprland") {
|
||||
Quickshell.execDetached(["sh", "-c", "hyprctl dispatch " + action]);
|
||||
return true;
|
||||
}
|
||||
if (provider === "mangowc") {
|
||||
const mmsgParams = action.trim().split(/\s+/).join(",");
|
||||
Quickshell.execDetached(["sh", "-c", "mmsg -d " + mmsgParams]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function loadBinds(showLoading) {
|
||||
if (currentProvider === "aqueous") {
|
||||
_loadPending = true;
|
||||
|
||||
Reference in new issue
Block a user