commands.rs - icy_draw - icy_draw is the successor to mystic draw. fork / mirror
(HTM) git clone https://git.drkhsh.at/icy_draw.git
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
commands.rs (19704B)
---
1 use std::collections::HashMap;
2
3 use eframe::egui::{self, Modifiers};
4 use egui_bind::{BindTarget, KeyOrPointer};
5 use i18n_embed_fl::fl;
6 use icy_engine::PaletteMode;
7
8 use crate::{button_with_shortcut, DocumentTab, Message, MRU_FILES, SETTINGS};
9
10 pub trait CommandState {
11 fn is_enabled(&self, _open_tab_opt: Option<&DocumentTab>) -> bool {
12 true
13 }
14 fn is_checked(&self, _open_tab_opt: Option<&DocumentTab>) -> Option<bool> {
15 None
16 }
17 }
18
19 #[derive(Default)]
20 pub struct AlwaysEnabledState {}
21 impl CommandState for AlwaysEnabledState {}
22
23 #[derive(Default)]
24 pub struct BufferOpenState {}
25
26 impl CommandState for BufferOpenState {
27 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
28 if let Some(pane) = open_tab_opt {
29 return pane.doc.lock().get_ansi_editor().is_some();
30 }
31 false
32 }
33 }
34
35 #[derive(Default)]
36 pub struct CanSwitchPaletteState {}
37
38 impl CommandState for CanSwitchPaletteState {
39 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
40 if let Some(pane) = open_tab_opt {
41 if let Some(editor) = pane.doc.lock().get_ansi_editor() {
42 return !matches!(editor.buffer_view.lock().get_buffer().palette_mode, PaletteMode::Fixed16);
43 }
44 }
45 false
46 }
47 }
48
49 #[derive(Default)]
50 pub struct LayerBordersState {}
51
52 impl CommandState for LayerBordersState {
53 fn is_enabled(&self, _open_tab_opt: Option<&DocumentTab>) -> bool {
54 true
55 }
56
57 fn is_checked(&self, _open_tab_opt: Option<&DocumentTab>) -> Option<bool> {
58 unsafe { Some(SETTINGS.show_layer_borders) }
59 }
60 }
61
62 #[derive(Default)]
63 pub struct LineNumberState {}
64
65 impl CommandState for LineNumberState {
66 fn is_enabled(&self, _open_tab_opt: Option<&DocumentTab>) -> bool {
67 true
68 }
69
70 fn is_checked(&self, _open_tab_opt: Option<&DocumentTab>) -> Option<bool> {
71 unsafe { Some(SETTINGS.show_line_numbers) }
72 }
73 }
74
75 #[derive(Default)]
76 pub struct FileOpenState {}
77
78 impl CommandState for FileOpenState {
79 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
80 open_tab_opt.is_some()
81 }
82 }
83
84 #[derive(Default)]
85 pub struct FileIsDirtyState {}
86
87 impl CommandState for FileIsDirtyState {
88 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
89 if let Some(pane) = open_tab_opt {
90 pane.is_dirty()
91 } else {
92 false
93 }
94 }
95 }
96
97 #[derive(Default)]
98 pub struct HasRecentFilesState {}
99
100 impl CommandState for HasRecentFilesState {
101 fn is_enabled(&self, _open_tab_opt: Option<&DocumentTab>) -> bool {
102 unsafe { !MRU_FILES.get_recent_files().is_empty() }
103 }
104 }
105
106 #[derive(Default)]
107 pub struct CanUndoState {}
108
109 impl CommandState for CanUndoState {
110 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
111 if let Some(pane) = open_tab_opt {
112 return pane.doc.lock().can_undo();
113 }
114 false
115 }
116 }
117 #[derive(Default)]
118 pub struct CanRedoState {}
119
120 impl CommandState for CanRedoState {
121 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
122 if let Some(pane) = open_tab_opt {
123 return pane.doc.lock().can_redo();
124 }
125 false
126 }
127 }
128
129 #[derive(Default)]
130 pub struct CanCutState {}
131
132 impl CommandState for CanCutState {
133 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
134 if let Some(pane) = open_tab_opt {
135 return pane.doc.lock().can_cut();
136 }
137 false
138 }
139 }
140
141 #[derive(Default)]
142 pub struct CanCopyState {}
143
144 impl CommandState for CanCopyState {
145 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
146 if let Some(pane) = open_tab_opt {
147 return pane.doc.lock().can_copy();
148 }
149 false
150 }
151 }
152
153 #[derive(Default)]
154 pub struct CanPasteState {}
155
156 impl CommandState for CanPasteState {
157 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
158 if let Some(pane) = open_tab_opt {
159 return pane.doc.lock().can_paste();
160 }
161 false
162 }
163 }
164
165 #[derive(Default)]
166 pub struct LGAFontState {}
167
168 impl CommandState for LGAFontState {
169 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
170 if let Some(pane) = open_tab_opt {
171 return pane.doc.lock().get_ansi_editor().is_some();
172 }
173 false
174 }
175 fn is_checked(&self, open_tab_opt: Option<&DocumentTab>) -> Option<bool> {
176 if let Some(pane) = open_tab_opt {
177 if let Some(editor) = pane.doc.lock().get_ansi_editor() {
178 return Some(editor.buffer_view.lock().get_buffer().use_letter_spacing());
179 }
180 }
181 Some(false)
182 }
183 }
184 #[derive(Default)]
185 pub struct AspectRatioState {}
186
187 impl CommandState for AspectRatioState {
188 fn is_enabled(&self, open_tab_opt: Option<&DocumentTab>) -> bool {
189 if let Some(pane) = open_tab_opt {
190 return pane.doc.lock().get_ansi_editor().is_some();
191 }
192 false
193 }
194 fn is_checked(&self, open_tab_opt: Option<&DocumentTab>) -> Option<bool> {
195 if let Some(pane) = open_tab_opt {
196 if let Some(editor) = pane.doc.lock().get_ansi_editor() {
197 return Some(editor.buffer_view.lock().get_buffer().use_aspect_ratio());
198 }
199 }
200 Some(false)
201 }
202 }
203
204 pub struct CommandWrapper {
205 key: Option<(KeyOrPointer, Modifiers)>,
206 message: Message,
207 label: String,
208 pub is_enabled: bool,
209 pub is_checked: Option<bool>,
210 state_key: u32,
211 }
212
213 mod modifier_keys {
214 use eframe::egui::Modifiers;
215
216 pub const NONE: Modifiers = Modifiers {
217 alt: false,
218 ctrl: false,
219 shift: false,
220 mac_cmd: false,
221 command: false,
222 };
223
224 pub const CTRL: Modifiers = Modifiers {
225 alt: false,
226 ctrl: true,
227 shift: false,
228 mac_cmd: false,
229 command: false,
230 };
231
232 pub const ALT: Modifiers = Modifiers {
233 alt: true,
234 ctrl: false,
235 shift: false,
236 mac_cmd: false,
237 command: false,
238 };
239
240 pub const ALT_CTRL: Modifiers = Modifiers {
241 alt: true,
242 ctrl: true,
243 shift: false,
244 mac_cmd: false,
245 command: false,
246 };
247
248 pub const CTRL_SHIFT: Modifiers = Modifiers {
249 alt: false,
250 ctrl: true,
251 shift: true,
252 mac_cmd: false,
253 command: false,
254 };
255 }
256
257 macro_rules! key {
258 () => {
259 None
260 };
261 ($key:ident, $modifier: ident) => {
262 Some((egui::Key::$key, modifier_keys::$modifier))
263 };
264 }
265
266 macro_rules! keys {
267 ($( ($l:ident, $translation: expr, $message:ident, $cmd_state: ident$(, $key:ident, $modifier: ident)? ) ),* $(,)? ) => {
268
269 pub struct Commands {
270 state_map: HashMap<u32, Box<dyn CommandState>>,
271 $(
272 pub $l: CommandWrapper,
273 )*
274 }
275
276 impl Default for Commands {
277 fn default() -> Self {
278 let mut state_map = HashMap::<u32, Box<dyn CommandState>>::new();
279 $(
280 state_map.insert(hash(stringify!($cmd_state)), Box::<$cmd_state>::default());
281 )*
282
283 Self {
284 state_map,
285 $(
286 $l: CommandWrapper::new(key!($($key, $modifier)?), Message::$message, fl!(crate::LANGUAGE_LOADER, $translation), hash(stringify!($cmd_state))),
287 )*
288 }
289 }
290 }
291
292 impl Commands {
293 pub fn default_keybindings() -> Vec<(String, egui::Key, Modifiers)> {
294 let mut result = Vec::new();
295 $(
296 let key = key!($($key, $modifier)?);
297 if let Some((key, modifier)) = key {
298 result.push((stringify!($l).to_string(), key, modifier));
299 }
300 )*
301 result
302 }
303 pub fn check(&self, ctx: &egui::Context, message: &mut Option<Message>) {
304 $(
305 if self.$l.is_pressed(ctx) {
306 *message = Some(self.$l.message.clone());
307 return;
308 }
309 )*
310 }
311
312 pub fn update_states(&mut self, open_tab_opt: Option<&DocumentTab>) {
313 let mut result_map = HashMap::new();
314 for (k, v) in &self.state_map {
315 let is_enabled = v.is_enabled(open_tab_opt);
316 let is_checked = v.is_checked(open_tab_opt);
317 result_map.insert(k, (is_enabled, is_checked));
318 }
319
320 $(
321 self.$l.update_state(&result_map);
322 )*
323 }
324
325 pub fn apply_key_bindings(&mut self, key_bindings: &Vec<(String, egui::Key, Modifiers)> ) {
326 for (binding, key, modifier) in key_bindings {
327 match binding.as_str() {
328 $(
329 stringify!($l) => {
330 self.$l.key = Some((KeyOrPointer::Key(*key), *modifier));
331 }
332 )*
333
334 _ => {}
335 }
336 }
337 }
338
339 pub(crate) fn show_keybinds_settings(ui: &mut egui::Ui, filter: &mut String, keys: &mut HashMap<String, (egui::Key, Modifiers)>) -> bool {
340 let mut changed_bindings = false;
341
342 ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
343 let response = ui.button("🗙");
344 if response.clicked() {
345 filter.clear();
346 }
347
348 ui.add(
349 egui::TextEdit::singleline(filter).hint_text(fl!(
350 crate::LANGUAGE_LOADER,
351 "settings-key_filter_preview_text"
352 )),
353 );
354 });
355 egui::ScrollArea::vertical()
356 .max_height(240.0)
357 .show(ui, |ui| {
358
359 $(
360 let label = fl!(crate::LANGUAGE_LOADER, $translation);
361 if filter.is_empty() || label.to_lowercase().contains(filter.to_lowercase().as_str()) {
362 ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| {
363 let mut bind = if let Some(x) = keys.get(stringify!($l)) { Some(x.clone ()) } else {None };
364 if ui.add(egui_bind::Bind::new(
365 stringify!($l).to_string(),
366 &mut bind,
367 )).changed() {
368 if let Some(bind) = bind {
369 keys.insert(stringify!($l).into(), bind);
370 } else {
371 keys.remove(stringify!($l));
372 }
373 changed_bindings = true;
374 }
375 ui.label(label);
376 });
377 }
378 )*
379 });
380 changed_bindings
381 }
382
383 }
384 };
385 }
386
387 fn hash(str: impl Into<String>) -> u32 {
388 use std::collections::hash_map::DefaultHasher;
389 use std::hash::{Hash, Hasher};
390
391 let mut hasher = DefaultHasher::new();
392 str.into().hash(&mut hasher);
393 hasher.finish() as u32
394 }
395
396 impl CommandWrapper {
397 pub fn new(key: Option<(egui::Key, Modifiers)>, message: Message, description: String, state_key: u32) -> Self {
398 let key = key.map(|(k, m)| (KeyOrPointer::Key(k), m));
399 Self {
400 key,
401 message,
402 label: description,
403 state_key,
404 is_enabled: true,
405 is_checked: None,
406 }
407 }
408
409 pub fn update_state(&mut self, result_map: &HashMap<&u32, (bool, Option<bool>)>) {
410 if let Some((is_enabled, is_checked)) = result_map.get(&self.state_key) {
411 self.is_enabled = *is_enabled;
412 self.is_checked = *is_checked;
413 }
414 }
415
416 pub fn is_pressed(&self, ctx: &egui::Context) -> bool {
417 self.key.pressed(ctx)
418 }
419
420 pub fn ui(&self, ui: &mut egui::Ui, message: &mut Option<Message>) {
421 if let Some(mut checked) = self.is_checked {
422 if ui.add(egui::Checkbox::new(&mut checked, &self.label)).clicked() {
423 *message = Some(self.message.clone());
424 ui.close_menu();
425 }
426 return;
427 }
428
429 let response = ui.with_layout(ui.layout().with_cross_justify(true), |ui| {
430 ui.set_enabled(self.is_enabled);
431 if let Some((KeyOrPointer::Key(k), modifier)) = self.key {
432 let mut shortcut = k.name().to_string();
433
434 if modifier.ctrl {
435 shortcut.insert_str(0, "Ctrl+");
436 }
437
438 if modifier.alt {
439 shortcut.insert_str(0, "Alt+");
440 }
441
442 if modifier.shift {
443 shortcut.insert_str(0, "Shift+");
444 }
445
446 button_with_shortcut(ui, true, &self.label, shortcut)
447 } else {
448 ui.add(egui::Button::new(&self.label).wrap(false))
449 }
450 });
451
452 if response.inner.clicked() {
453 *message = Some(self.message.clone());
454 ui.close_menu();
455 }
456 }
457 }
458
459 keys![
460 (new_file, "menu-new", NewFileDialog, AlwaysEnabledState, N, CTRL),
461 (save, "menu-save", SaveFile, FileIsDirtyState, S, CTRL),
462 (save_as, "menu-save-as", SaveFileAs, FileOpenState, S, CTRL_SHIFT),
463 (open_file, "menu-open", OpenFileDialog, AlwaysEnabledState, O, CTRL),
464 (export, "menu-export", ExportFile, BufferOpenState),
465 (edit_font_outline, "menu-edit-font-outline", ShowOutlineDialog, AlwaysEnabledState),
466 (close_window, "menu-close", CloseWindow, AlwaysEnabledState, Q, CTRL),
467 (undo, "menu-undo", Undo, CanUndoState, Z, CTRL),
468 (redo, "menu-redo", Redo, CanRedoState, Z, CTRL_SHIFT),
469 (cut, "menu-cut", Cut, CanCutState, X, CTRL),
470 (copy, "menu-copy", Copy, CanCopyState, C, CTRL),
471 (paste, "menu-paste", Paste, CanPasteState, V, CTRL),
472 (show_settings, "menu-show_settings", ShowSettings, AlwaysEnabledState),
473 (select_all, "menu-select-all", SelectAll, BufferOpenState, A, CTRL),
474 (deselect, "menu-select_nothing", SelectNothing, BufferOpenState),
475 (erase_selection, "menu-erase", DeleteSelection, BufferOpenState, Delete, NONE),
476 (flip_x, "menu-flipx", FlipX, BufferOpenState),
477 (flip_y, "menu-flipy", FlipY, BufferOpenState),
478 (justifycenter, "menu-justifycenter", Center, BufferOpenState),
479 (justifyleft, "menu-justifyleft", JustifyLeft, BufferOpenState),
480 (justifyright, "menu-justifyright", JustifyRight, BufferOpenState),
481 (crop, "menu-crop", Crop, BufferOpenState),
482 (about, "menu-about", ShowAboutDialog, AlwaysEnabledState),
483 (justify_line_center, "menu-justify_line_center", CenterLine, BufferOpenState, C, ALT),
484 (justify_line_left, "menu-justify_line_left", JustifyLineLeft, BufferOpenState, L, ALT),
485 (justify_line_right, "menu-justify_line_right", JustifyLineRight, BufferOpenState, R, ALT),
486 (insert_row, "menu-insert_row", InsertRow, BufferOpenState, ArrowUp, ALT),
487 (delete_row, "menu-delete_row", DeleteRow, BufferOpenState, ArrowDown, ALT),
488 (insert_column, "menu-insert_colum", InsertColumn, BufferOpenState, ArrowRight, ALT),
489 (delete_column, "menu-delete_colum", DeleteColumn, BufferOpenState, ArrowLeft, ALT),
490 (erase_row, "menu-erase_row", EraseRow, BufferOpenState, E, ALT),
491 (erase_row_to_start, "menu-erase_row_to_start", EraseRowToStart, BufferOpenState, Home, ALT),
492 (erase_row_to_end, "menu-erase_row_to_end", EraseRowToEnd, BufferOpenState, End, ALT),
493 (erase_column, "menu-erase_column", EraseColumn, BufferOpenState, E, ALT),
494 (
495 erase_column_to_start,
496 "menu-erase_column_to_start",
497 EraseColumnToStart,
498 BufferOpenState,
499 Home,
500 ALT
501 ),
502 (erase_column_to_end, "menu-erase_column_to_end", EraseColumnToEnd, BufferOpenState, End, ALT),
503 (scroll_area_up, "menu-scroll_area_up", ScrollAreaUp, BufferOpenState, ArrowUp, ALT_CTRL),
504 (scroll_area_down, "menu-scroll_area_down", ScrollAreaDown, BufferOpenState, ArrowDown, ALT_CTRL),
505 (scroll_area_left, "menu-scroll_area_left", ScrollAreaLeft, BufferOpenState, ArrowLeft, ALT_CTRL),
506 (
507 scroll_area_right,
508 "menu-scroll_area_right",
509 ScrollAreaRight,
510 BufferOpenState,
511 ArrowRight,
512 ALT_CTRL
513 ),
514 (set_reference_image, "menu-reference-image", SetReferenceImage, BufferOpenState, O, CTRL_SHIFT),
515 (
516 toggle_reference_image,
517 "menu-toggle-reference-image",
518 ToggleReferenceImage,
519 BufferOpenState,
520 Tab,
521 CTRL
522 ),
523 (clear_reference_image, "menu-clear-reference-image", ClearReferenceImage, BufferOpenState),
524 (
525 pick_attribute_under_caret,
526 "menu-pick_attribute_under_caret",
527 PickAttributeUnderCaret,
528 BufferOpenState,
529 U,
530 ALT
531 ),
532 (switch_to_default_color, "menu-default_color", SwitchToDefaultColor, BufferOpenState, D, CTRL),
533 (toggle_color, "menu-toggle_color", ToggleColor, BufferOpenState, X, ALT),
534 (fullscreen, "menu-toggle_fullscreen", ToggleFullScreen, AlwaysEnabledState, Enter, ALT),
535 (zoom_reset, "menu-zoom_reset", ZoomReset, BufferOpenState, Backspace, CTRL),
536 (zoom_in, "menu-zoom_in", ZoomIn, BufferOpenState, Plus, CTRL),
537 (zoom_out, "menu-zoom_out", ZoomOut, BufferOpenState, Minus, CTRL),
538 (open_tdf_directory, "menu-open_tdf_directoy", OpenTdfDirectory, AlwaysEnabledState),
539 (open_font_selector, "menu-open_font_selector", OpenFontSelector, BufferOpenState),
540 (add_fonts, "menu-add_fonts", OpenAddFonts, BufferOpenState),
541 (open_font_manager, "menu-open_font_manager", OpenFontManager, BufferOpenState),
542 (open_font_directory, "menu-open_font_directoy", OpenFontDirectory, AlwaysEnabledState),
543 (
544 open_palettes_directory,
545 "menu-open_palettes_directoy",
546 OpenPalettesDirectory,
547 AlwaysEnabledState
548 ),
549 (mirror_mode, "menu-mirror_mode", ToggleMirrorMode, BufferOpenState),
550 (clear_recent_open, "menu-open_recent_clear", ClearRecentOpenFiles, HasRecentFilesState),
551 (inverse_selection, "menu-inverse_selection", InverseSelection, BufferOpenState),
552 (clear_selection, "menu-delete_row", ClearSelection, BufferOpenState, Escape, NONE),
553 (select_palette, "menu-select_palette", SelectPalette, CanSwitchPaletteState),
554 (show_layer_borders, "menu-show_layer_borders", ToggleLayerBorders, LayerBordersState),
555 (show_line_numbers, "menu-show_line_numbers", ToggleLineNumbers, LineNumberState),
556 (open_plugin_directory, "menu-open_plugin_directory", OpenPluginDirectory, AlwaysEnabledState),
557 (next_fg_color, "menu-next_fg_color", NextFgColor, BufferOpenState, ArrowDown, CTRL),
558 (prev_fg_color, "menu-prev_fg_color", PreviousFgColor, BufferOpenState, ArrowUp, CTRL),
559 (next_bg_color, "menu-next_bg_color", NextBgColor, BufferOpenState, ArrowRight, CTRL),
560 (prev_bg_color, "menu-prev_bg_color", PreviousBgColor, BufferOpenState, ArrowLeft, CTRL),
561 (lga_font, "menu-9px-font", ToggleLGAFont, LGAFontState),
562 (aspect_ratio, "menu-aspect-ratio", ToggleAspectRatio, AspectRatioState),
563 (toggle_grid_guides, "menu-toggle_grid", ToggleGrid, BufferOpenState),
564 ];