Skip to content

Commit 4eb37bb

Browse files
committed
session: support for custom keyboard shortcuts
1 parent 8b64b35 commit 4eb37bb

12 files changed

Lines changed: 115 additions & 12 deletions

File tree

assets/translations/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,7 @@
597597
"keybind-invalid-title": "Keybind not allowed",
598598
"keybind-invalid-super": "The Super/Windows key cannot be used in shell keybinds.",
599599
"keybind-invalid-printable": "Printable keys (letters, numbers, symbols) require a modifier such as Ctrl or Alt.",
600+
"keybind-invalid-modifier": "This shortcut accepts a single key without modifiers.",
600601
"power-profiles": "Power profiles",
601602
"power-profile-title": "Power mode",
602603
"power-profile-body": "Switched to: {profile}"
@@ -688,6 +689,7 @@
688689
"label-placeholder": "Optional button text",
689690
"glyph-field": "Glyph",
690691
"glyph-placeholder": "Optional icon id",
692+
"shortcut-label": "Shortcut",
691693
"variant-label": "Button style",
692694
"move-up": "Up",
693695
"move-down": "Down",

src/config/config_export.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ namespace config_export {
127127
item.insert_or_assign("label", action.label.value_or(""));
128128
item.insert_or_assign("glyph", action.glyph.value_or(""));
129129
item.insert_or_assign("variant", std::string(enumToKey(kSessionActionButtonVariants, action.variant)));
130+
item.insert_or_assign("shortcut",
131+
action.shortcut.has_value() ? keyChordToString(*action.shortcut) : std::string{});
130132
array.push_back(std::move(item));
131133
}
132134
return array;

src/config/config_overrides.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "config/config_service.h"
2+
#include "core/key_chord.h"
23
#include "core/log.h"
34
#include "shell/settings/widget_settings_registry.h"
45
#include "theme/scheme.h"
@@ -501,6 +502,9 @@ namespace {
501502
row.insert_or_assign("glyph", *item.glyph);
502503
}
503504
row.insert_or_assign("variant", std::string(enumToKey(kSessionActionButtonVariants, item.variant)));
505+
if (item.shortcut.has_value()) {
506+
row.insert_or_assign("shortcut", keyChordToString(*item.shortcut));
507+
}
504508
array.push_back(std::move(row));
505509
}
506510
table.insert_or_assign(key, std::move(array));

src/config/config_service.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,6 +1509,12 @@ void ConfigService::parseTableInto(const toml::table& tbl, Config& config, bool
15091509
kLog.warn("unknown shell.session.actions variant \"{}\"", key);
15101510
}
15111511
}
1512+
if (auto v = (*entryTbl)["shortcut"].value<std::string>()) {
1513+
const std::string s = StringUtils::trim(*v);
1514+
if (!s.empty()) {
1515+
row.shortcut = parseKeyChordSpec(s);
1516+
}
1517+
}
15121518
shell.session.actions.push_back(std::move(row));
15131519
}
15141520
}

src/config/config_types.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,15 @@ std::vector<ShortcutConfig> defaultControlCenterShortcuts() {
5555
std::vector<SessionPanelActionConfig> defaultSessionPanelActions() {
5656
return {
5757
SessionPanelActionConfig{"lock", true, std::nullopt, std::nullopt, std::nullopt,
58-
SessionActionButtonVariant::Default},
58+
SessionActionButtonVariant::Default, KeyChord{XKB_KEY_1, 0}},
5959
SessionPanelActionConfig{"logout", true, std::nullopt, std::nullopt, std::nullopt,
60-
SessionActionButtonVariant::Default},
60+
SessionActionButtonVariant::Default, KeyChord{XKB_KEY_2, 0}},
6161
SessionPanelActionConfig{"suspend", true, std::nullopt, std::nullopt, std::nullopt,
62-
SessionActionButtonVariant::Default},
62+
SessionActionButtonVariant::Default, KeyChord{XKB_KEY_3, 0}},
6363
SessionPanelActionConfig{"reboot", true, std::nullopt, std::nullopt, std::nullopt,
64-
SessionActionButtonVariant::Default},
64+
SessionActionButtonVariant::Default, KeyChord{XKB_KEY_4, 0}},
6565
SessionPanelActionConfig{"shutdown", true, std::nullopt, std::nullopt, std::nullopt,
66-
SessionActionButtonVariant::Destructive},
66+
SessionActionButtonVariant::Destructive, KeyChord{XKB_KEY_5, 0}},
6767
};
6868
}
6969

src/config/config_types.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ struct SessionPanelActionConfig {
128128
std::optional<std::string> label;
129129
std::optional<std::string> glyph;
130130
SessionActionButtonVariant variant = SessionActionButtonVariant::Default;
131+
std::optional<KeyChord> shortcut;
131132

132133
bool operator==(const SessionPanelActionConfig&) const = default;
133134
};

src/core/key_chord.cpp

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,6 @@ std::optional<KeyChord> parseKeyChordSpec(std::string_view rawSpec) {
102102
return std::nullopt;
103103
}
104104

105-
if (modifiers == 0 && isPrintableKey(static_cast<std::uint32_t>(sym))) {
106-
throw std::runtime_error("printable keys require a modifier (e.g. \"Ctrl+a\")");
107-
}
108-
109105
return KeyChord{.sym = static_cast<std::uint32_t>(sym), .modifiers = modifiers};
110106
}
111107

src/core/key_chord.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ struct KeyChord {
1313
};
1414

1515
// Throws std::runtime_error if spec contains a Super-family modifier.
16+
// Bare printable keys (e.g. "1", "a") are accepted — UI-level policy is
17+
// enforced by KeybindRecorder's ModifierPolicy, not here.
1618
[[nodiscard]] std::optional<KeyChord> parseKeyChordSpec(std::string_view spec);
1719
[[nodiscard]] std::string keyChordToString(const KeyChord& chord);
1820
[[nodiscard]] std::string keyChordDisplayLabel(const KeyChord& chord);

src/shell/session/session_panel.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,9 @@ Button* SessionPanel::createActionButton(const SessionPanelActionConfig& cfg, fl
648648
const std::string labelText =
649649
cfg.label.has_value() && !cfg.label->empty() ? *cfg.label : i18n::tr(labelKeyForAction(cfg.action));
650650
button->setText(labelText);
651+
if (cfg.shortcut.has_value() && cfg.shortcut->sym != 0) {
652+
button->setBadge(keyChordDisplayLabel(*cfg.shortcut));
653+
}
651654
button->setGlyph(cfg.glyph.has_value() && !cfg.glyph->empty() ? *cfg.glyph : defaultGlyphForAction(cfg.action));
652655
applyActionButtonPalette(*button, cfg, panelCardOpacity());
653656
button->setDirection(FlexDirection::Vertical);
@@ -768,6 +771,16 @@ bool SessionPanel::handleKeyEvent(std::uint32_t sym, std::uint32_t modifiers) {
768771
if (m_visibleButtons.empty()) {
769772
return false;
770773
}
774+
775+
for (std::size_t i = 0; i < m_visibleEntries.size(); ++i) {
776+
const auto& cfg = m_visibleEntries[i];
777+
if (cfg.shortcut.has_value() && keyChordMatches(*cfg.shortcut, sym, modifiers)) {
778+
PanelManager::instance().close();
779+
invokeEntry(cfg);
780+
return true;
781+
}
782+
}
783+
771784
const std::size_t lastIndex = m_visibleButtons.size() - 1;
772785

773786
if (KeybindMatcher::matches(KeybindAction::Left, sym, modifiers)) {

src/shell/settings/settings_content.cpp

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,47 @@ namespace settings {
623623
variantBlock->addChild(std::move(variantSelect));
624624
fields->addChild(std::move(variantBlock));
625625

626+
auto shortcutBlock = std::make_unique<Flex>();
627+
shortcutBlock->setDirection(FlexDirection::Horizontal);
628+
shortcutBlock->setAlign(FlexAlign::Center);
629+
shortcutBlock->setGap(Style::spaceXs * scale);
630+
shortcutBlock->setFlexGrow(1.0f);
631+
shortcutBlock->addChild(makeLabel(i18n::tr("settings.session-actions.shortcut-label"),
632+
Style::fontSizeCaption * scale, colorSpecFromRole(ColorRole::OnSurfaceVariant),
633+
false));
634+
635+
auto shortcutRecorder = std::make_unique<KeybindRecorder>();
636+
shortcutRecorder->setScale(scale);
637+
shortcutRecorder->setModifierPolicy(ModifierPolicy::Optional);
638+
shortcutRecorder->setChord(row.shortcut);
639+
shortcutRecorder->setUnsetPlaceholder(i18n::tr("settings.controls.keybind.unset-placeholder"));
640+
shortcutRecorder->setRecordingPlaceholder(i18n::tr("settings.controls.keybind.recording-placeholder"));
641+
shortcutRecorder->setOnCommit([&row, persist](KeyChord chord) {
642+
row.shortcut = chord;
643+
persist();
644+
});
645+
auto* shortcutRecorderPtr = shortcutRecorder.get();
646+
shortcutBlock->addChild(std::move(shortcutRecorder));
647+
648+
if (row.shortcut.has_value()) {
649+
auto clearBtn = std::make_unique<Button>();
650+
clearBtn->setGlyph("close");
651+
clearBtn->setVariant(ButtonVariant::Ghost);
652+
clearBtn->setGlyphSize(Style::fontSizeCaption * scale);
653+
clearBtn->setMinWidth(Style::controlHeightSm * scale);
654+
clearBtn->setMinHeight(Style::controlHeightSm * scale);
655+
clearBtn->setPadding(Style::spaceXs * scale);
656+
clearBtn->setRadius(Style::scaledRadiusSm(scale));
657+
clearBtn->setOnClick([&row, persist, shortcutRecorderPtr]() {
658+
row.shortcut = std::nullopt;
659+
shortcutRecorderPtr->setChord(std::nullopt);
660+
persist();
661+
});
662+
shortcutBlock->addChild(std::move(clearBtn));
663+
}
664+
665+
fields->addChild(std::move(shortcutBlock));
666+
626667
body->addChild(std::move(fields));
627668
section.addChild(std::move(body));
628669
}
@@ -1991,7 +2032,8 @@ namespace settings {
19912032
addBtn->setRadius(Style::scaledRadiusMd(scale));
19922033
addBtn->setOnClick([state, commit]() {
19932034
state->push_back(SessionPanelActionConfig{"command", true, "notify-send 'Noctalia' 'Custom session entry'",
1994-
std::nullopt, std::nullopt, SessionActionButtonVariant::Default});
2035+
std::nullopt, std::nullopt, SessionActionButtonVariant::Default,
2036+
std::nullopt});
19952037
commit();
19962038
});
19972039
block->addChild(std::move(addBtn));

0 commit comments

Comments
 (0)