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