diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index b79d1c6..2a521e8 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -25,6 +25,7 @@ use time::UtcOffset; use crate::{ app_config::{deserialize_config, AppConfigVersion}, config::{load_project_config, ProjectObjectNode}, + hotkeys, jobs::{ objdiff::{start_build, ObjDiffConfig}, Job, JobQueue, JobResult, JobStatus, @@ -527,6 +528,10 @@ impl App { } fn post_update(&mut self, ctx: &egui::Context, action: Option) { + if action.is_some() { + ctx.request_repaint(); + } + self.appearance.post_update(ctx); let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state; @@ -690,13 +695,13 @@ impl eframe::App for App { *show_side_panel = !*show_side_panel; } ui.separator(); - ui.menu_button("File", |ui| { + ui.menu_button(hotkeys::button_alt_text(ui, "_File"), |ui| { #[cfg(debug_assertions)] - if ui.button("Debug…").clicked() { + if ui.button(hotkeys::button_alt_text(ui, "_Debug…")).clicked() { *show_debug = !*show_debug; ui.close_menu(); } - if ui.button("Project…").clicked() { + if ui.button(hotkeys::button_alt_text(ui, "_Project…")).clicked() { *show_project_config = !*show_project_config; ui.close_menu(); } @@ -705,10 +710,11 @@ impl eframe::App for App { } else { vec![] }; + let recent_projects_text = hotkeys::button_alt_text(ui, "_Recent Projects…"); if recent_projects.is_empty() { - ui.add_enabled(false, egui::Button::new("Recent projects…")); + ui.add_enabled(false, egui::Button::new(recent_projects_text)); } else { - ui.menu_button("Recent Projects…", |ui| { + ui.menu_button(recent_projects_text, |ui| { if ui.button("Clear").clicked() { state.write().unwrap().config.recent_projects.clear(); }; @@ -721,36 +727,39 @@ impl eframe::App for App { } }); } - if ui.button("Appearance…").clicked() { + if ui.button(hotkeys::button_alt_text(ui, "_Appearance…")).clicked() { *show_appearance_config = !*show_appearance_config; ui.close_menu(); } - if ui.button("Graphics…").clicked() { + if ui.button(hotkeys::button_alt_text(ui, "_Graphics…")).clicked() { *show_graphics = !*show_graphics; ui.close_menu(); } - if ui.button("Quit").clicked() { + if ui.button(hotkeys::button_alt_text(ui, "_Quit")).clicked() { ctx.send_viewport_cmd(egui::ViewportCommand::Close); } }); - ui.menu_button("Tools", |ui| { - if ui.button("Demangle…").clicked() { + ui.menu_button(hotkeys::button_alt_text(ui, "_Tools"), |ui| { + if ui.button(hotkeys::button_alt_text(ui, "_Demangle…")).clicked() { *show_demangle = !*show_demangle; ui.close_menu(); } - if ui.button("Rlwinm Decoder…").clicked() { + if ui.button(hotkeys::button_alt_text(ui, "_Rlwinm Decoder…")).clicked() { *show_rlwinm_decode = !*show_rlwinm_decode; ui.close_menu(); } }); - ui.menu_button("Diff Options", |ui| { - if ui.button("Arch Settings…").clicked() { + ui.menu_button(hotkeys::button_alt_text(ui, "_Diff Options"), |ui| { + if ui.button(hotkeys::button_alt_text(ui, "_Arch Settings…")).clicked() { *show_arch_config = !*show_arch_config; ui.close_menu(); } let mut state = state.write().unwrap(); let response = ui - .checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes") + .checkbox( + &mut state.config.rebuild_on_changes, + hotkeys::button_alt_text(ui, "_Rebuild on changes"), + ) .on_hover_text("Automatically re-run the build & diff when files change."); if response.changed() { state.watcher_change = true; @@ -759,18 +768,21 @@ impl eframe::App for App { !diff_state.symbol_state.disable_reverse_fn_order, egui::Checkbox::new( &mut diff_state.symbol_state.reverse_fn_order, - "Reverse function order (-inline deferred)", + hotkeys::button_alt_text( + ui, + "Reverse function order (-inline _deferred)", + ), ), ) .on_disabled_hover_text(CONFIG_DISABLED_TEXT); ui.checkbox( &mut diff_state.symbol_state.show_hidden_symbols, - "Show hidden symbols", + hotkeys::button_alt_text(ui, "Show _hidden symbols"), ); if ui .checkbox( &mut state.config.diff_obj_config.relax_reloc_diffs, - "Relax relocation diffs", + hotkeys::button_alt_text(ui, "Rela_x relocation diffs"), ) .on_hover_text( "Ignores differences in relocation targets. (Address, name, etc)", @@ -782,7 +794,7 @@ impl eframe::App for App { if ui .checkbox( &mut state.config.diff_obj_config.space_between_args, - "Space between args", + hotkeys::button_alt_text(ui, "_Space between args"), ) .changed() { @@ -791,14 +803,17 @@ impl eframe::App for App { if ui .checkbox( &mut state.config.diff_obj_config.combine_data_sections, - "Combine data sections", + hotkeys::button_alt_text(ui, "Combine _data sections"), ) .on_hover_text("Combines data sections with equal names.") .changed() { state.queue_reload = true; } - if ui.button("Clear custom symbol mappings").clicked() { + if ui + .button(hotkeys::button_alt_text(ui, "_Clear custom symbol mappings")) + .clicked() + { state.clear_mappings(); diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff()); state.queue_reload = true; diff --git a/objdiff-gui/src/hotkeys.rs b/objdiff-gui/src/hotkeys.rs index e68571b..92b1f43 100644 --- a/objdiff-gui/src/hotkeys.rs +++ b/objdiff-gui/src/hotkeys.rs @@ -1,5 +1,6 @@ use egui::{ - style::ScrollAnimation, vec2, Context, Key, KeyboardShortcut, Modifiers, PointerButton, + style::ScrollAnimation, text::LayoutJob, vec2, Align, Context, FontSelection, Key, + KeyboardShortcut, Modifiers, PointerButton, RichText, Ui, WidgetText, }; fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) } @@ -106,3 +107,122 @@ const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers:: pub fn consume_change_base_shortcut(ctx: &Context) -> bool { ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT)) } + +fn shortcut_key(text: &str) -> (usize, char, Key) { + let i = text.chars().position(|c| c == '_').expect("No underscore in text"); + let key = text.chars().nth(i + 1).expect("No character after underscore"); + let shortcut_key = match key { + 'a' | 'A' => Key::A, + 'b' | 'B' => Key::B, + 'c' | 'C' => Key::C, + 'd' | 'D' => Key::D, + 'e' | 'E' => Key::E, + 'f' | 'F' => Key::F, + 'g' | 'G' => Key::G, + 'h' | 'H' => Key::H, + 'i' | 'I' => Key::I, + 'j' | 'J' => Key::J, + 'k' | 'K' => Key::K, + 'l' | 'L' => Key::L, + 'm' | 'M' => Key::M, + 'n' | 'N' => Key::N, + 'o' | 'O' => Key::O, + 'p' | 'P' => Key::P, + 'q' | 'Q' => Key::Q, + 'r' | 'R' => Key::R, + 's' | 'S' => Key::S, + 't' | 'T' => Key::T, + 'u' | 'U' => Key::U, + 'v' | 'V' => Key::V, + 'w' | 'W' => Key::W, + 'x' | 'X' => Key::X, + 'y' | 'Y' => Key::Y, + 'z' | 'Z' => Key::Z, + _ => panic!("Invalid key {}", key), + }; + (i, key, shortcut_key) +} + +fn alt_text_ui(ui: &Ui, text: &str, i: usize, key: char, interactive: bool) -> WidgetText { + let color = if interactive { + ui.visuals().widgets.inactive.text_color() + } else { + ui.visuals().widgets.noninteractive.text_color() + }; + let mut job = LayoutJob::default(); + if i > 0 { + let text = &text[..i]; + RichText::new(text).color(color).append_to( + &mut job, + ui.style(), + FontSelection::Default, + Align::Center, + ); + } + let mut rt = RichText::new(key).color(color); + if ui.input(|i| i.modifiers.alt) { + rt = rt.underline(); + } + rt.append_to(&mut job, ui.style(), FontSelection::Default, Align::Center); + if i < text.len() - 1 { + let text = &text[i + 2..]; + RichText::new(text).color(color).append_to( + &mut job, + ui.style(), + FontSelection::Default, + Align::Center, + ); + } + job.into() +} + +pub fn button_alt_text(ui: &Ui, text: &str) -> WidgetText { + let (n, c, key) = shortcut_key(text); + let result = alt_text_ui(ui, text, n, c, true); + if ui.input_mut(|i| check_alt_key(i, c, key)) { + let btn_id = ui.next_auto_id(); + ui.memory_mut(|m| m.request_focus(btn_id)); + ui.input_mut(|i| { + i.events.push(egui::Event::Key { + key: Key::Enter, + physical_key: None, + pressed: true, + repeat: false, + modifiers: Default::default(), + }) + }); + } + result +} + +pub fn alt_text(ui: &Ui, text: &str, enter: bool) -> WidgetText { + let (n, c, key) = shortcut_key(text); + let result = alt_text_ui(ui, text, n, c, false); + if ui.input_mut(|i| check_alt_key(i, c, key)) { + let btn_id = ui.next_auto_id(); + ui.memory_mut(|m| m.request_focus(btn_id)); + if enter { + ui.input_mut(|i| { + i.events.push(egui::Event::Key { + key: Key::Enter, + physical_key: None, + pressed: true, + repeat: false, + modifiers: Default::default(), + }) + }); + } + } + result +} + +fn check_alt_key(i: &mut egui::InputState, c: char, key: Key) -> bool { + if i.consume_key(Modifiers::ALT, key) { + // Remove any text input events that match the key + let cs = c.to_string(); + i.events.retain(|e| !matches!(e, egui::Event::Text(t) if *t == cs)); + true + } else { + false + } +} diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index da382de..e4efd52 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -219,7 +219,7 @@ pub fn config_ui( ui.horizontal(|ui| { ui.heading("Project"); - if ui.button(RichText::new("Settings")).clicked() { + if ui.button("Settings").clicked() { *show_config_window = true; } }); @@ -255,8 +255,9 @@ pub fn config_ui( } } else { let had_search = !config_state.object_search.is_empty(); - let response = - egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui); + let response = egui::TextEdit::singleline(&mut config_state.object_search) + .hint_text(hotkeys::alt_text(ui, "Filter _objects", false)) + .ui(ui); if hotkeys::consume_object_filter_shortcut(ui.ctx()) { response.request_focus(); } diff --git a/objdiff-gui/src/views/jobs.rs b/objdiff-gui/src/views/jobs.rs index f6fc2d5..509fc0f 100644 --- a/objdiff-gui/src/views/jobs.rs +++ b/objdiff-gui/src/views/jobs.rs @@ -3,6 +3,7 @@ use std::cmp::Ordering; use egui::{ProgressBar, RichText, Widget}; use crate::{ + hotkeys, jobs::{JobQueue, JobStatus}, views::appearance::Appearance, }; @@ -94,7 +95,14 @@ impl From<&JobStatus> for JobStatusDisplay { } pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool { - ui.label("Jobs:"); + let mut clicked = false; + if egui::Label::new(hotkeys::alt_text(ui, "_Jobs:", true)) + .sense(egui::Sense::click()) + .ui(ui) + .clicked() + { + clicked = true; + } let mut statuses = Vec::new(); for job in jobs.iter_mut() { let Ok(status) = job.context.status.read() else { @@ -105,7 +113,6 @@ pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appeara let running_jobs = statuses.iter().filter(|s| !s.error).count(); let error_jobs = statuses.iter().filter(|s| s.error).count(); - let mut clicked = false; let spinner = egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color); match running_jobs.cmp(&1) { diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 0d008c3..05cc9ed 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -901,7 +901,9 @@ pub fn symbol_diff_ui( }); let mut search = state.search.clone(); - let response = TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui); + let response = TextEdit::singleline(&mut search) + .hint_text(hotkeys::alt_text(ui, "Filter _symbols", false)) + .ui(ui); if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) { response.request_focus(); }