yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
shortcut_manager.cc
Go to the documentation of this file.
1#include "shortcut_manager.h"
2
3#include <algorithm>
4#include <cstddef>
5#include <functional>
6#include <string>
7#include <utility>
8#include <vector>
9
10#include "absl/strings/match.h"
11#include "absl/strings/str_split.h"
12#include "absl/strings/string_view.h"
13#include "app/gui/core/input.h"
15#include "imgui/imgui.h"
16
17namespace yaze {
18namespace editor {
19
20namespace {
22 int required_mods = 0; // ImGuiMod_* mask
23 std::vector<ImGuiKey> main_keys; // Non-modifier keys
24};
25
26ParsedChord DecomposeChord(const std::vector<ImGuiKey>& keys) {
27 ParsedChord out;
28 out.main_keys.reserve(keys.size());
29
30 for (ImGuiKey key : keys) {
31 const int key_value = static_cast<int>(key);
32 if (key_value & ImGuiMod_Mask_) {
33 out.required_mods |= key_value & ImGuiMod_Mask_;
34 continue;
35 }
36 out.main_keys.push_back(key);
37 }
38
39 return out;
40}
41
42int CountMods(int mods) {
43 int count = 0;
44 if (mods & ImGuiMod_Ctrl)
45 ++count;
46 if (mods & ImGuiMod_Shift)
47 ++count;
48 if (mods & ImGuiMod_Alt)
49 ++count;
50 if (mods & ImGuiMod_Super)
51 ++count;
52 return count;
53}
54
56 // Higher wins.
57 switch (scope) {
59 return 3;
61 return 2;
63 return 1;
64 }
65 return 0;
66}
67
68bool ModsSatisfied(int pressed_mods, int required_mods) {
69 if (required_mods == 0) {
70 // A plain-key command must not shadow a modified chord that is handled by
71 // the active panel (for example D vs Ctrl/Cmd+D in the dungeon editor).
72 constexpr int kRelevantMods =
73 ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiMod_Alt | ImGuiMod_Super;
74 return (pressed_mods & kRelevantMods) == 0;
75 }
76
77 auto has = [&](int mod) -> bool {
78 return (pressed_mods & mod) != 0;
79 };
80
81 // macOS: ImGui may swap Cmd(Super) and Ctrl at the io.AddKeyEvent() layer
82 // (ConfigMacOSXBehaviors). Treat Ctrl and Super as equivalent requirements so
83 // shortcuts work regardless of that swap and for users who press either key.
84 const bool mac = gui::IsMacPlatform();
85
86 if (required_mods & ImGuiMod_Shift) {
87 if (!has(ImGuiMod_Shift))
88 return false;
89 }
90 if (required_mods & ImGuiMod_Alt) {
91 if (!has(ImGuiMod_Alt))
92 return false;
93 }
94
95 if (required_mods & ImGuiMod_Ctrl) {
96 if (mac) {
97 if (!(has(ImGuiMod_Ctrl) || has(ImGuiMod_Super)))
98 return false;
99 } else {
100 if (!has(ImGuiMod_Ctrl))
101 return false;
102 }
103 }
104 if (required_mods & ImGuiMod_Super) {
105 if (mac) {
106 if (!(has(ImGuiMod_Super) || has(ImGuiMod_Ctrl)))
107 return false;
108 } else {
109 if (!has(ImGuiMod_Super))
110 return false;
111 }
112 }
113
114 return true;
115}
116} // namespace
117
118std::string PrintShortcut(const std::vector<ImGuiKey>& keys) {
119 // Use the platform-aware FormatShortcut from platform_keys.h
120 // This handles Ctrl→Cmd and Alt→Opt conversions for macOS/WASM
121 return gui::FormatShortcut(keys);
122}
123
124std::string InferShortcutGroup(absl::string_view name) {
125 using absl::StartsWith;
126 using absl::StrContains;
127
128 if (StartsWith(name, "File")) {
129 return "File";
130 }
131 if (StartsWith(name, "Edit")) {
132 return "Edit";
133 }
134 if (StartsWith(name, "Help")) {
135 return "Help";
136 }
137 if (StartsWith(name, "Tools") || name == "Command Palette" ||
138 name == "Global Search" || name == "Load Last ROM" ||
139 name == "Show About") {
140 return "Tools";
141 }
142 if (StartsWith(name, "Layout") || StartsWith(name, "Apply Layout") ||
143 StartsWith(name, "Apply Profile") || StartsWith(name, "Apply:") ||
144 StartsWith(name, "layout:")) {
145 return "Layout";
146 }
147 if (StartsWith(name, "drawer:") ||
148 (StartsWith(name, "View: Toggle") &&
149 (StrContains(name, "Drawer") || StrContains(name, "Panel") ||
150 StrContains(name, "Notifications") || StrContains(name, "Agent") ||
151 StrContains(name, "Proposals") || StrContains(name, "Settings") ||
152 StrContains(name, "Help") || StrContains(name, "Project") ||
153 StrContains(name, "Properties"))) ||
154 StartsWith(name, "View: Next Right") ||
155 StartsWith(name, "View: Previous Right")) {
156 return "Drawers";
157 }
158 if (StartsWith(name, "window:") || StartsWith(name, "Window") ||
159 StartsWith(name, "Panel Browser") || StartsWith(name, "Window Browser") ||
160 StartsWith(name, "Window Finder") || StartsWith(name, "View: Show")) {
161 return "Windows";
162 }
163 if (StartsWith(name, "View") || StartsWith(name, "Sidebar")) {
164 return "View";
165 }
166 if (StrContains(name, ".")) {
167 return "Editor";
168 }
169 return "Other";
170}
171
172std::vector<ImGuiKey> ParseShortcut(const std::string& shortcut) {
173 std::vector<ImGuiKey> keys;
174 if (shortcut.empty()) {
175 return keys;
176 }
177
178 // Split on '+' and trim whitespace
179 std::vector<std::string> parts = absl::StrSplit(shortcut, '+');
180 for (auto& part : parts) {
181 // Trim leading/trailing spaces
182 while (!part.empty() && (part.front() == ' ' || part.front() == '\t')) {
183 part.erase(part.begin());
184 }
185 while (!part.empty() && (part.back() == ' ' || part.back() == '\t')) {
186 part.pop_back();
187 }
188 if (part.empty())
189 continue;
190
191 std::string lower;
192 lower.reserve(part.size());
193 for (char c : part)
194 lower.push_back(static_cast<char>(std::tolower(c)));
195
196 // Modifiers (support platform aliases)
197 if (lower == "ctrl" || lower == "control") {
198 keys.push_back(ImGuiMod_Ctrl);
199 continue;
200 }
201 if (lower == "cmd" || lower == "command") {
202 // ImGui's macOS behaviors swap Cmd/Super into ImGuiMod_Ctrl at the
203 // time of io.AddKeyEvent(), so using ImGuiMod_Ctrl here makes "Cmd"
204 // bindings work as expected and round-trip with PrintShortcut().
205 keys.push_back(gui::IsMacPlatform() ? ImGuiMod_Ctrl : ImGuiMod_Super);
206 continue;
207 }
208 if (lower == "win" || lower == "super") {
209 keys.push_back(ImGuiMod_Super);
210 continue;
211 }
212 if (lower == "alt" || lower == "opt" || lower == "option") {
213 keys.push_back(ImGuiMod_Alt);
214 continue;
215 }
216 if (lower == "shift") {
217 keys.push_back(ImGuiMod_Shift);
218 continue;
219 }
220
221 // Function keys
222 if (lower.size() >= 2 && lower[0] == 'f') {
223 int fnum = 0;
224 try {
225 fnum = std::stoi(lower.substr(1));
226 } catch (...) {
227 fnum = 0;
228 }
229 if (fnum >= 1 && fnum <= 24) {
230 keys.push_back(static_cast<ImGuiKey>(ImGuiKey_F1 + (fnum - 1)));
231 continue;
232 }
233 }
234
235 // Single character keys
236 if (part.size() == 1) {
237 ImGuiKey mapped = gui::MapKeyToImGuiKey(part[0]);
238 if (mapped != ImGuiKey_COUNT) {
239 keys.push_back(mapped);
240 continue;
241 }
242 }
243 }
244
245 return keys;
246}
247
248void ExecuteShortcuts(const ShortcutManager& shortcut_manager) {
249 // Check for keyboard shortcuts using the shortcut manager.
250 //
251 // Note: we intentionally do NOT gate on io.WantCaptureKeyboard here. In an
252 // ImGui-first app it is frequently true (focused windows, menus, etc) and
253 // would incorrectly disable shortcuts globally.
254 const ImGuiIO& io = ImGui::GetIO();
255
256 struct Candidate {
257 const Shortcut* shortcut = nullptr;
258 int scope_priority = 0;
259 int key_count = 0;
260 int mod_count = 0;
261 std::string name;
262 };
263
264 auto better = [](const Candidate& a, const Candidate& b) -> bool {
265 if (a.scope_priority != b.scope_priority)
266 return a.scope_priority > b.scope_priority;
267 if (a.key_count != b.key_count)
268 return a.key_count > b.key_count;
269 if (a.mod_count != b.mod_count)
270 return a.mod_count > b.mod_count;
271 return a.name < b.name;
272 };
273
274 Candidate best;
275 bool have_best = false;
276
277 for (const auto& [name, shortcut] : shortcut_manager.GetShortcuts()) {
278 if (!shortcut.callback) {
279 continue;
280 }
281 if (shortcut.keys.empty()) {
282 continue; // command palette only
283 }
284
285 const ParsedChord chord = DecomposeChord(shortcut.keys);
286 if (chord.main_keys.empty()) {
287 continue;
288 }
289
290 // When typing in an InputText, don't steal plain keys (Space, letters, etc).
291 if (io.WantTextInput && chord.required_mods == 0) {
292 continue;
293 }
294
295 // Modifier satisfaction (macOS Cmd/Ctrl handling is normalized by
296 // ModsSatisfied()).
297 if (!ModsSatisfied(io.KeyMods, chord.required_mods)) {
298 continue;
299 }
300
301 // Require all non-mod keys, with the last key triggering on press.
302 bool chord_pressed = true;
303 for (size_t i = 0; i + 1 < chord.main_keys.size(); ++i) {
304 if (!ImGui::IsKeyDown(chord.main_keys[i])) {
305 chord_pressed = false;
306 break;
307 }
308 }
309 if (!chord_pressed) {
310 continue;
311 }
312 if (!ImGui::IsKeyPressed(chord.main_keys.back(), false /* repeat */)) {
313 continue;
314 }
315
316 Candidate cand;
317 cand.shortcut = &shortcut;
318 cand.scope_priority = ScopePriority(shortcut.scope);
319 cand.key_count = static_cast<int>(chord.main_keys.size());
320 cand.mod_count = CountMods(chord.required_mods);
321 cand.name = name;
322
323 if (!have_best || better(cand, best)) {
324 best = std::move(cand);
325 have_best = true;
326 }
327 }
328
329 if (have_best && best.shortcut && best.shortcut->callback) {
330 best.shortcut->callback();
331 }
332}
333
334bool ShortcutManager::UpdateShortcutKeys(const std::string& name,
335 const std::vector<ImGuiKey>& keys) {
336 auto it = shortcuts_.find(name);
337 if (it == shortcuts_.end()) {
338 return false;
339 }
340 it->second.keys = keys;
341 return true;
342}
343
344} // namespace editor
345} // namespace yaze
346
347// Implementation in header file (inline methods)
348namespace yaze {
349namespace editor {
350
352 std::function<void()> save_callback, std::function<void()> open_callback,
353 std::function<void()> close_callback, std::function<void()> find_callback,
354 std::function<void()> settings_callback) {
355 // Ctrl+S - Save
356 if (save_callback) {
357 RegisterShortcut("save", {ImGuiMod_Ctrl, ImGuiKey_S}, save_callback);
358 }
359
360 // Ctrl+O - Open
361 if (open_callback) {
362 RegisterShortcut("open", {ImGuiMod_Ctrl, ImGuiKey_O}, open_callback);
363 }
364
365 // Ctrl+W - Close
366 if (close_callback) {
367 RegisterShortcut("close", {ImGuiMod_Ctrl, ImGuiKey_W}, close_callback);
368 }
369
370 // Ctrl+F - Find
371 if (find_callback) {
372 RegisterShortcut("find", {ImGuiMod_Ctrl, ImGuiKey_F}, find_callback);
373 }
374
375 // Ctrl+, - Settings
376 if (settings_callback) {
377 RegisterShortcut("settings", {ImGuiMod_Ctrl, ImGuiKey_Comma},
378 settings_callback);
379 }
380
381 // Ctrl+Tab - Next tab (placeholder for now)
382 // Ctrl+Shift+Tab - Previous tab (placeholder for now)
383}
384
386 std::function<void()> focus_left, std::function<void()> focus_right,
387 std::function<void()> focus_up, std::function<void()> focus_down,
388 std::function<void()> close_window, std::function<void()> split_horizontal,
389 std::function<void()> split_vertical) {
390 // Ctrl+Arrow keys for window navigation
391 if (focus_left) {
392 RegisterShortcut("focus_left", {ImGuiMod_Ctrl, ImGuiKey_LeftArrow},
393 focus_left);
394 }
395
396 if (focus_right) {
397 RegisterShortcut("focus_right", {ImGuiMod_Ctrl, ImGuiKey_RightArrow},
398 focus_right);
399 }
400
401 if (focus_up) {
402 RegisterShortcut("focus_up", {ImGuiMod_Ctrl, ImGuiKey_UpArrow}, focus_up);
403 }
404
405 if (focus_down) {
406 RegisterShortcut("focus_down", {ImGuiMod_Ctrl, ImGuiKey_DownArrow},
407 focus_down);
408 }
409
410 // Ctrl+W, C - Close current window
411 if (close_window) {
412 RegisterShortcut("close_window", {ImGuiMod_Ctrl, ImGuiKey_W, ImGuiKey_C},
413 close_window);
414 }
415
416 // Ctrl+W, S - Split horizontal
417 if (split_horizontal) {
418 RegisterShortcut("split_horizontal",
419 {ImGuiMod_Ctrl, ImGuiKey_W, ImGuiKey_S}, split_horizontal);
420 }
421
422 // Ctrl+W, V - Split vertical
423 if (split_vertical) {
424 RegisterShortcut("split_vertical", {ImGuiMod_Ctrl, ImGuiKey_W, ImGuiKey_V},
425 split_vertical);
426 }
427}
428
429} // namespace editor
430} // namespace yaze
void RegisterShortcut(const std::string &name, const std::vector< ImGuiKey > &keys, Shortcut::Scope scope=Shortcut::Scope::kGlobal)
void RegisterStandardShortcuts(std::function< void()> save_callback, std::function< void()> open_callback, std::function< void()> close_callback, std::function< void()> find_callback, std::function< void()> settings_callback)
void RegisterWindowNavigationShortcuts(std::function< void()> focus_left, std::function< void()> focus_right, std::function< void()> focus_up, std::function< void()> focus_down, std::function< void()> close_window, std::function< void()> split_horizontal, std::function< void()> split_vertical)
std::unordered_map< std::string, Shortcut > shortcuts_
const std::unordered_map< std::string, Shortcut > & GetShortcuts() const
bool UpdateShortcutKeys(const std::string &name, const std::vector< ImGuiKey > &keys)
ParsedChord DecomposeChord(const std::vector< ImGuiKey > &keys)
bool ModsSatisfied(int pressed_mods, int required_mods)
std::string InferShortcutGroup(absl::string_view name)
Menu-IA group for a shortcut/command name (File, View, Drawers, …).
std::vector< ImGuiKey > ParseShortcut(const std::string &shortcut)
std::string PrintShortcut(const std::vector< ImGuiKey > &keys)
void ExecuteShortcuts(const ShortcutManager &shortcut_manager)
std::string FormatShortcut(const std::vector< ImGuiKey > &keys)
Format a list of ImGui keys into a human-readable shortcut string.
bool IsMacPlatform()
Check if running on macOS (native or web)
ImGuiKey MapKeyToImGuiKey(char key)
Definition input.cc:708