yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
native_menu_bridge.cc
Go to the documentation of this file.
2
3#include <string>
4#include <utility>
5
6#include "absl/strings/str_format.h"
7#include "app/editor/editor.h"
9
10namespace yaze {
11namespace platform {
12namespace native_menu {
13
14namespace {
15
16constexpr KeyChord Cmd(char key) {
17 KeyChord c;
18 c.key = key;
19 c.command = true;
20 return c;
21}
22
23constexpr KeyChord CmdShift(char key) {
24 KeyChord c = Cmd(key);
25 c.shift = true;
26 return c;
27}
28
29constexpr KeyChord CmdOpt(char key) {
30 KeyChord c = Cmd(key);
31 c.option = true;
32 return c;
33}
34
35constexpr KeyChord CmdCtrl(char key) {
36 KeyChord c = Cmd(key);
37 c.control = true;
38 return c;
39}
40
41constexpr KeyChord kNoChord{};
42
43char ChordKeyFromImGuiKey(ImGuiKey key) {
44 if (key >= ImGuiKey_A && key <= ImGuiKey_Z) {
45 return static_cast<char>('a' + (key - ImGuiKey_A));
46 }
47 if (key >= ImGuiKey_0 && key <= ImGuiKey_9) {
48 return static_cast<char>('0' + (key - ImGuiKey_0));
49 }
50 switch (key) {
51 case ImGuiKey_Comma:
52 return ',';
53 case ImGuiKey_Period:
54 return '.';
55 case ImGuiKey_Slash:
56 return '/';
57 case ImGuiKey_Semicolon:
58 return ';';
59 case ImGuiKey_Apostrophe:
60 return '\'';
61 case ImGuiKey_Minus:
62 return '-';
63 case ImGuiKey_Equal:
64 return '=';
65 case ImGuiKey_LeftBracket:
66 return '[';
67 case ImGuiKey_RightBracket:
68 return ']';
69 case ImGuiKey_Backslash:
70 return '\\';
71 case ImGuiKey_GraveAccent:
72 return '`';
73 default:
74 return 0;
75 }
76}
77
78bool HasCallback(const editor::Shortcut* shortcut) {
79 return shortcut != nullptr && static_cast<bool>(shortcut->callback);
80}
81
82// A ShortcutManager binding with a different action already uses this exact
83// chord; the native item must then not claim it.
85 const KeyChord& chord) {
86 if (shortcuts == nullptr || !chord.valid()) {
87 return false;
88 }
89 for (const auto& [name, shortcut] : shortcuts->GetShortcuts()) {
90 if (!shortcut.callback || shortcut.keys.empty()) {
91 continue;
92 }
93 if (ChordFromImGuiKeys(shortcut.keys) == chord) {
94 return true;
95 }
96 }
97 return false;
98}
99
100} // namespace
101
102const std::vector<ActionSpec>& ActionSpecs() {
103 // The Settings editor switch, registered by shortcut_configurator.cc as
104 // "switch.<EditorType>" (default Ctrl/Cmd+,). Declared before kSpecs so the
105 // name outlives every spec that points at it.
106 static const std::string kSettingsShortcut = absl::StrFormat(
107 "switch.%d", static_cast<int>(editor::EditorType::kSettings));
108 // clang-format off
109 static const std::vector<ActionSpec> kSpecs = {
110 // App menu
111 {MenuAction::kAbout, "About yaze", "Show About", kNoChord,
113 // Settings follows the live "switch.<kSettings>" binding: rebound, the
114 // item shows the new chord (ImGui-owned); unbound, it shows none. Cmd+,
115 // is only native-owned when that shortcut is not registered at all and
116 // nothing else binds Cmd+,. A click always runs the host action.
117 {MenuAction::kSettings, "Settings\xE2\x80\xA6", kSettingsShortcut.c_str(),
118 Cmd(','), Dispatch::kHost, Dispatch::kHost, false},
119 {MenuAction::kHide, "Hide yaze", nullptr, Cmd('h'),
121 {MenuAction::kHideOthers, "Hide Others", nullptr, CmdOpt('h'),
123 {MenuAction::kShowAll, "Show All", nullptr, kNoChord,
125 {MenuAction::kQuit, "Quit yaze", "Quit", Cmd('q'),
127 // File
128 {MenuAction::kOpenRom, "Open ROM\xE2\x80\xA6", "Open", Cmd('o'),
130 {MenuAction::kOpenProject, "Open Project\xE2\x80\xA6", nullptr, kNoChord,
132 {MenuAction::kSave, "Save", "Save", Cmd('s'),
134 {MenuAction::kSaveAs, "Save As\xE2\x80\xA6", "Save As", CmdShift('s'),
136 {MenuAction::kCloseRom, "Close ROM", "Close ROM", Cmd('w'),
138 // Edit
139 {MenuAction::kUndo, "Undo", "Undo", Cmd('z'),
141 {MenuAction::kRedo, "Redo", "Redo", CmdShift('z'),
143 // ImGui text fields handle Cmd+X/C/V/A themselves, so these chords always
144 // belong to ImGui even without a ShortcutManager entry.
145 {MenuAction::kCut, "Cut", "Cut", Cmd('x'),
147 {MenuAction::kCopy, "Copy", "Copy", Cmd('c'),
149 {MenuAction::kPaste, "Paste", "Paste", Cmd('v'),
151 {MenuAction::kSelectAll, "Select All", nullptr, Cmd('a'),
153 // View
154 {MenuAction::kToggleSidebar, "Toggle Sidebar", "view.toggle_activity_bar",
155 kNoChord, Dispatch::kShortcut, Dispatch::kHost, false},
156 {MenuAction::kToggleFullScreen, "Enter Full Screen", nullptr, CmdCtrl('f'),
158 // Window
159 {MenuAction::kMinimize, "Minimize", nullptr, Cmd('m'),
161 {MenuAction::kZoom, "Zoom", nullptr, kNoChord,
163 {MenuAction::kBringAllToFront, "Bring All to Front", nullptr, kNoChord,
165 // Help
166 {MenuAction::kKeyboardShortcuts, "Keyboard Shortcuts", "Keyboard Shortcuts",
167 kNoChord, Dispatch::kShortcut, Dispatch::kHost, false},
168 };
169 // clang-format on
170 return kSpecs;
171}
172
174 for (const auto& spec : ActionSpecs()) {
175 if (spec.action == action) {
176 return &spec;
177 }
178 }
179 return nullptr;
180}
181
182KeyChord ChordFromImGuiKeys(const std::vector<ImGuiKey>& keys) {
183 KeyChord chord;
184 char main_key = 0;
185 int main_count = 0;
186 for (ImGuiKey key : keys) {
187 const int value = static_cast<int>(key);
188 if (value == ImGuiMod_Ctrl) {
189 // ShortcutManager's Ctrl is Command on macOS (ImGui swaps Cmd/Ctrl).
190 chord.command = true;
191 } else if (value == ImGuiMod_Super) {
192 chord.control = true;
193 } else if (value == ImGuiMod_Shift) {
194 chord.shift = true;
195 } else if (value == ImGuiMod_Alt) {
196 chord.option = true;
197 } else {
198 ++main_count;
199 main_key = ChordKeyFromImGuiKey(key);
200 }
201 }
202 if (main_count != 1 || main_key == 0) {
203 return KeyChord{};
204 }
205 // A menu key equivalent needs Command or Control; plain keys stay in ImGui.
206 if (!chord.command && !chord.control) {
207 return KeyChord{};
208 }
209 chord.key = main_key;
210 return chord;
211}
212
213ImGuiKey ImGuiKeyFromChordKey(char key) {
214 if (key >= 'a' && key <= 'z') {
215 return static_cast<ImGuiKey>(ImGuiKey_A + (key - 'a'));
216 }
217 return ImGuiKey_None;
218}
219
221 MenuAction action) {
222 ResolvedItem result;
223 const ActionSpec* spec = FindSpec(action);
224 if (spec == nullptr) {
225 return result;
226 }
227
228 if (spec->shortcut_name != nullptr && shortcuts != nullptr) {
229 const editor::Shortcut* shortcut =
230 shortcuts->FindShortcut(spec->shortcut_name);
231 if (HasCallback(shortcut)) {
232 result.dispatch = spec->dispatch;
233 result.chord = ChordFromImGuiKeys(shortcut->keys);
234 result.owner =
236 return result;
237 }
238 }
239
240 result.dispatch =
241 spec->shortcut_name != nullptr ? spec->fallback : spec->dispatch;
242 result.chord = spec->default_chord;
243 if (!result.chord.valid()) {
244 result.owner = ChordOwner::kNone;
245 } else if (spec->imgui_owns_default_chord) {
246 result.owner = ChordOwner::kImGui;
247 } else if (ExactShortcutConflict(shortcuts, result.chord)) {
248 // Another action owns this chord. e.g. Cmd+H is the agent sidebar: Hide
249 // yaze keeps its item, loses ⌘H. An action whose own shortcut exists
250 // resolved above from its live binding, so any binding here is foreign.
251 result.chord = KeyChord{};
252 result.owner = ChordOwner::kNone;
253 } else {
254 result.owner = ChordOwner::kNative;
255 }
256 return result;
257}
258
260 const KeyChord& chord) {
261 if (!chord.valid()) {
262 return false;
263 }
264 for (const auto& spec : ActionSpecs()) {
265 if (spec.default_chord != chord) {
266 // Fast path: native ownership only ever comes from a default chord.
267 continue;
268 }
269 const ResolvedItem item = ResolveItem(shortcuts, spec.action);
270 if (item.owner == ChordOwner::kNative && item.chord == chord) {
271 return true;
272 }
273 }
274 return false;
275}
276
278 const KeyChord& chord) {
279 if (!chord.valid()) {
280 // Function keys, arrows, etc. never map to a ShortcutManager chord here.
281 return true;
282 }
283 if (IsNativeOwnedChord(shortcuts, chord)) {
284 return true;
285 }
286 if (ExactShortcutConflict(shortcuts, chord)) {
287 return false;
288 }
289 for (const auto& spec : ActionSpecs()) {
290 const ResolvedItem item = ResolveItem(shortcuts, spec.action);
291 if (item.owner == ChordOwner::kImGui && item.chord == chord) {
292 return false;
293 }
294 }
295 return true;
296}
297
299 switch (action) {
303 return host.HasLoadedRom();
305 return host.HasCurrentEditor() && host.CanUndo();
307 return host.HasCurrentEditor() && host.CanRedo();
308 case MenuAction::kCut:
311 return host.WantsTextInput() || host.HasCurrentEditor();
313 return host.WantsTextInput();
314 default:
315 return true;
316 }
317}
318
320 const ActionSpec* spec = FindSpec(action);
321 if (spec == nullptr) {
322 return false;
323 }
324 editor::ShortcutManager* shortcuts = host.shortcut_manager();
325 const ResolvedItem item = ResolveItem(shortcuts, action);
326
327 auto run_shortcut = [&host, shortcuts](const char* name) {
328 if (shortcuts == nullptr || name == nullptr) {
329 return false;
330 }
331 if (!HasCallback(shortcuts->FindShortcut(name))) {
332 return false;
333 }
334 std::string shortcut_name = name;
335 host.Defer([shortcuts, shortcut_name]() {
336 const editor::Shortcut* shortcut = shortcuts->FindShortcut(shortcut_name);
337 if (shortcut != nullptr && shortcut->callback) {
338 shortcut->callback();
339 }
340 });
341 return true;
342 };
343
344 switch (item.dispatch) {
345 case Dispatch::kCocoa:
346 return false;
348 if (!run_shortcut(spec->shortcut_name)) {
349 host.RunHostAction(action);
350 }
351 return true;
353 if (host.WantsTextInput()) {
354 const ImGuiKey key = ImGuiKeyFromChordKey(spec->default_chord.key);
355 if (key != ImGuiKey_None) {
356 host.InjectCommandChord(key);
357 }
358 return true;
359 }
360 run_shortcut(spec->shortcut_name);
361 return true;
362 case Dispatch::kHost:
363 host.RunHostAction(action);
364 return true;
365 }
366 return false;
367}
368
369std::string RecentFileLabel(const std::string& path) {
370 const size_t slash = path.find_last_of("/\\");
371 if (slash == std::string::npos || slash + 1 >= path.size()) {
372 return path;
373 }
374 return path.substr(slash + 1);
375}
376
377} // namespace native_menu
378} // namespace platform
379} // namespace yaze
const Shortcut * FindShortcut(const std::string &name) const
const std::unordered_map< std::string, Shortcut > & GetShortcuts() const
virtual editor::ShortcutManager * shortcut_manager()=0
virtual void InjectCommandChord(ImGuiKey key)=0
virtual void RunHostAction(MenuAction action)=0
virtual void Defer(std::function< void()> action)=0
bool ExactShortcutConflict(const editor::ShortcutManager *shortcuts, const KeyChord &chord)
const ActionSpec * FindSpec(MenuAction action)
bool IsActionEnabled(NativeMenuHost &host, MenuAction action)
bool IsNativeOwnedChord(const editor::ShortcutManager *shortcuts, const KeyChord &chord)
ResolvedItem ResolveItem(const editor::ShortcutManager *shortcuts, MenuAction action)
const std::vector< ActionSpec > & ActionSpecs()
bool ShouldMenuPerformKeyEquivalent(const editor::ShortcutManager *shortcuts, const KeyChord &chord)
bool PerformAction(NativeMenuHost &host, MenuAction action)
KeyChord ChordFromImGuiKeys(const std::vector< ImGuiKey > &keys)
std::string RecentFileLabel(const std::string &path)
ImGuiKey ImGuiKeyFromChordKey(char key)
std::function< void()> callback
std::vector< ImGuiKey > keys