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