Skip to content

Commit abe68ef

Browse files
authored
objdiff-gui: Implement keyboard shortcuts (encounter#139)
* Fix missing dependency feature for objdiff-gui * Update .gitignore * Add enter and back hotkeys * Add scroll hotkeys * Add hotkeys to select the next symbol above/below the current one in the listing * Do not clear highlighted symbol when backing out of diff view * Do not clear highlighted symbol when hovering mouse over an unpaired symbol * Auto-scroll the keyboard-selected symbols into view if offscreen * Fix some hotkeys stealing input from focused widgets e.g. The symbol list was stealing the W/S key presses when typing into the symbol filter text edit. If the user actually wants to use these shortcuts while a widget is focused, they can simply press the escape key to unfocus all widgets and then press the shortcut. * Add Ctrl+F/S shortcuts for focusing the object and symbol filter text edits * Add space as alternative to enter hotkey This is for consistency with egui's builtint enter/space hotkey for interacting with the focused widget. * Add hotkeys to change target and base functions * Split function diff view: Enable PageUp/PageDown/Home/End for scrolling * Add escape as an alternative to back hotkey * Fix auto-scrolling to highlighted symbol only working for the left side The flag is cleared after one scroll to avoid doing it continuously, but this breaks when we need to scroll to both the left and the right symbol at the same time. So now each side has its own flag to keep track of this state independently. * Simplify clearing of the autoscroll flag, remove &mut State * Found a better place to clear the autoscroll flag DiffViewState::post_update is where the flag gets set, so clearing it right before that at the start of the function seems to make the most sense, instead of doing it in App::update.
1 parent 304df96 commit abe68ef

File tree

7 files changed

+244
-51
lines changed

7 files changed

+244
-51
lines changed

objdiff-gui/src/hotkeys.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use egui::{
2+
style::ScrollAnimation, vec2, Context, Key, KeyboardShortcut, Modifiers, PointerButton,
3+
};
4+
5+
fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) }
6+
7+
pub fn enter_pressed(ctx: &Context) -> bool {
8+
if any_widget_focused(ctx) {
9+
return false;
10+
}
11+
ctx.input_mut(|i| {
12+
i.key_pressed(Key::Enter)
13+
|| i.key_pressed(Key::Space)
14+
|| i.pointer.button_pressed(PointerButton::Extra2)
15+
})
16+
}
17+
18+
pub fn back_pressed(ctx: &Context) -> bool {
19+
if any_widget_focused(ctx) {
20+
return false;
21+
}
22+
ctx.input_mut(|i| {
23+
i.key_pressed(Key::Backspace)
24+
|| i.key_pressed(Key::Escape)
25+
|| i.pointer.button_pressed(PointerButton::Extra1)
26+
})
27+
}
28+
29+
pub fn up_pressed(ctx: &Context) -> bool {
30+
if any_widget_focused(ctx) {
31+
return false;
32+
}
33+
ctx.input_mut(|i| i.key_pressed(Key::ArrowUp) || i.key_pressed(Key::W))
34+
}
35+
36+
pub fn down_pressed(ctx: &Context) -> bool {
37+
if any_widget_focused(ctx) {
38+
return false;
39+
}
40+
ctx.input_mut(|i| i.key_pressed(Key::ArrowDown) || i.key_pressed(Key::S))
41+
}
42+
43+
pub fn page_up_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageUp)) }
44+
45+
pub fn page_down_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageDown)) }
46+
47+
pub fn home_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::Home)) }
48+
49+
pub fn end_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::End)) }
50+
51+
pub fn check_scroll_hotkeys(ui: &mut egui::Ui, include_small_increments: bool) {
52+
let ui_height = ui.available_rect_before_wrap().height();
53+
if up_pressed(ui.ctx()) && include_small_increments {
54+
ui.scroll_with_delta_animation(vec2(0.0, ui_height / 10.0), ScrollAnimation::none());
55+
} else if down_pressed(ui.ctx()) && include_small_increments {
56+
ui.scroll_with_delta_animation(vec2(0.0, -ui_height / 10.0), ScrollAnimation::none());
57+
} else if page_up_pressed(ui.ctx()) {
58+
ui.scroll_with_delta_animation(vec2(0.0, ui_height), ScrollAnimation::none());
59+
} else if page_down_pressed(ui.ctx()) {
60+
ui.scroll_with_delta_animation(vec2(0.0, -ui_height), ScrollAnimation::none());
61+
} else if home_pressed(ui.ctx()) {
62+
ui.scroll_with_delta_animation(vec2(0.0, f32::INFINITY), ScrollAnimation::none());
63+
} else if end_pressed(ui.ctx()) {
64+
ui.scroll_with_delta_animation(vec2(0.0, -f32::INFINITY), ScrollAnimation::none());
65+
}
66+
}
67+
68+
pub fn consume_up_key(ctx: &Context) -> bool {
69+
if any_widget_focused(ctx) {
70+
return false;
71+
}
72+
ctx.input_mut(|i| {
73+
i.consume_key(Modifiers::NONE, Key::ArrowUp) || i.consume_key(Modifiers::NONE, Key::W)
74+
})
75+
}
76+
77+
pub fn consume_down_key(ctx: &Context) -> bool {
78+
if any_widget_focused(ctx) {
79+
return false;
80+
}
81+
ctx.input_mut(|i| {
82+
i.consume_key(Modifiers::NONE, Key::ArrowDown) || i.consume_key(Modifiers::NONE, Key::S)
83+
})
84+
}
85+
86+
const OBJECT_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::F);
87+
88+
pub fn consume_object_filter_shortcut(ctx: &Context) -> bool {
89+
ctx.input_mut(|i| i.consume_shortcut(&OBJECT_FILTER_SHORTCUT))
90+
}
91+
92+
const SYMBOL_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S);
93+
94+
pub fn consume_symbol_filter_shortcut(ctx: &Context) -> bool {
95+
ctx.input_mut(|i| i.consume_shortcut(&SYMBOL_FILTER_SHORTCUT))
96+
}
97+
98+
const CHANGE_TARGET_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::T);
99+
100+
pub fn consume_change_target_shortcut(ctx: &Context) -> bool {
101+
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_TARGET_SHORTCUT))
102+
}
103+
104+
const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::B);
105+
106+
pub fn consume_change_base_shortcut(ctx: &Context) -> bool {
107+
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT))
108+
}

objdiff-gui/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ mod app;
44
mod app_config;
55
mod config;
66
mod fonts;
7+
mod hotkeys;
78
mod jobs;
89
mod update;
910
mod views;

objdiff-gui/src/views/config.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use strum::{EnumMessage, VariantArray};
2121
use crate::{
2222
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
2323
config::ProjectObjectNode,
24+
hotkeys,
2425
jobs::{
2526
check_update::{start_check_update, CheckUpdateResult},
2627
update::start_update,
@@ -254,7 +255,11 @@ pub fn config_ui(
254255
}
255256
} else {
256257
let had_search = !config_state.object_search.is_empty();
257-
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
258+
let response =
259+
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
260+
if hotkeys::consume_object_filter_shortcut(ui.ctx()) {
261+
response.request_focus();
262+
}
258263

259264
let mut root_open = None;
260265
let mut node_open = NodeOpen::Default;

objdiff-gui/src/views/data_diff.rs

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ use objdiff_core::{
77
};
88
use time::format_description;
99

10-
use crate::views::{
11-
appearance::Appearance,
12-
column_layout::{render_header, render_table},
13-
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
14-
write_text,
10+
use crate::{
11+
hotkeys,
12+
views::{
13+
appearance::Appearance,
14+
column_layout::{render_header, render_table},
15+
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
16+
write_text,
17+
},
1518
};
1619

1720
const BYTES_PER_ROW: usize = 16;
@@ -176,6 +179,8 @@ fn data_table_ui(
176179
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
177180
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff));
178181

182+
hotkeys::check_scroll_hotkeys(ui, true);
183+
179184
render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
180185
let i = row.index();
181186
let address = i * BYTES_PER_ROW;
@@ -224,7 +229,7 @@ pub fn data_diff_ui(
224229
render_header(ui, available_width, 2, |ui, column| {
225230
if column == 0 {
226231
// Left column
227-
if ui.button("⏴ Back").clicked() {
232+
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
228233
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
229234
}
230235

objdiff-gui/src/views/extab_diff.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ use objdiff_core::{
55
};
66
use time::format_description;
77

8-
use crate::views::{
9-
appearance::Appearance,
10-
column_layout::{render_header, render_strips},
11-
function_diff::FunctionDiffContext,
12-
symbol_diff::{
13-
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName,
14-
View,
8+
use crate::{
9+
hotkeys,
10+
views::{
11+
appearance::Appearance,
12+
column_layout::{render_header, render_strips},
13+
function_diff::FunctionDiffContext,
14+
symbol_diff::{
15+
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState,
16+
SymbolRefByName, View,
17+
},
1518
},
1619
};
1720

@@ -136,7 +139,7 @@ pub fn extab_diff_ui(
136139
if column == 0 {
137140
// Left column
138141
ui.horizontal(|ui| {
139-
if ui.button("⏴ Back").clicked() {
142+
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
140143
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
141144
}
142145
ui.separator();
@@ -232,6 +235,8 @@ pub fn extab_diff_ui(
232235
}
233236
});
234237

238+
hotkeys::check_scroll_hotkeys(ui, true);
239+
235240
// Table
236241
render_strips(ui, available_width, 2, |ui, column| {
237242
if column == 0 {

objdiff-gui/src/views/function_diff.rs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ use objdiff_core::{
1414
};
1515
use time::format_description;
1616

17-
use crate::views::{
18-
appearance::Appearance,
19-
column_layout::{render_header, render_strips, render_table},
20-
symbol_diff::{
21-
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState,
22-
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
17+
use crate::{
18+
hotkeys,
19+
views::{
20+
appearance::Appearance,
21+
column_layout::{render_header, render_strips, render_table},
22+
symbol_diff::{
23+
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation,
24+
DiffViewState, SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View,
25+
},
2326
},
2427
};
2528

@@ -436,6 +439,7 @@ fn asm_table_ui(
436439
};
437440
if left_len.is_some() && right_len.is_some() {
438441
// Joint view
442+
hotkeys::check_scroll_hotkeys(ui, true);
439443
render_table(
440444
ui,
441445
available_width,
@@ -471,6 +475,7 @@ fn asm_table_ui(
471475
if column == 0 {
472476
if let Some(ctx) = left_ctx {
473477
if ctx.has_symbol() {
478+
hotkeys::check_scroll_hotkeys(ui, false);
474479
render_table(
475480
ui,
476481
available_width / 2.0,
@@ -516,9 +521,6 @@ fn asm_table_ui(
516521
SymbolRefByName::new(right_symbol, right_section),
517522
));
518523
}
519-
DiffViewAction::SetSymbolHighlight(_, _) => {
520-
// Ignore
521-
}
522524
_ => {
523525
ret = Some(action);
524526
}
@@ -531,6 +533,7 @@ fn asm_table_ui(
531533
} else if column == 1 {
532534
if let Some(ctx) = right_ctx {
533535
if ctx.has_symbol() {
536+
hotkeys::check_scroll_hotkeys(ui, false);
534537
render_table(
535538
ui,
536539
available_width / 2.0,
@@ -576,9 +579,6 @@ fn asm_table_ui(
576579
right_symbol_ref,
577580
));
578581
}
579-
DiffViewAction::SetSymbolHighlight(_, _) => {
580-
// Ignore
581-
}
582582
_ => {
583583
ret = Some(action);
584584
}
@@ -679,7 +679,7 @@ pub fn function_diff_ui(
679679
if column == 0 {
680680
// Left column
681681
ui.horizontal(|ui| {
682-
if ui.button("⏴ Back").clicked() {
682+
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
683683
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
684684
}
685685
ui.separator();
@@ -712,10 +712,11 @@ pub fn function_diff_ui(
712712
.color(appearance.highlight_color),
713713
);
714714
if right_ctx.is_some_and(|m| m.has_symbol())
715-
&& ui
715+
&& (ui
716716
.button("Change target")
717717
.on_hover_text_at_pointer("Choose a different symbol to use as the target")
718718
.clicked()
719+
|| hotkeys::consume_change_target_shortcut(ui.ctx()))
719720
{
720721
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
721722
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
@@ -789,6 +790,7 @@ pub fn function_diff_ui(
789790
"Choose a different symbol to use as the base",
790791
)
791792
.clicked()
793+
|| hotkeys::consume_change_base_shortcut(ui.ctx())
792794
{
793795
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
794796
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));

0 commit comments

Comments
 (0)