From 024e24d4def6a079594fa575b0c5f87728f76833 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 3 Jul 2025 20:50:59 +0200 Subject: [PATCH] support wrapping if first element is selected and up is pressed again the last element will be selected and vice versa --- worf/src/lib/gui.rs | 343 +++++++++++++++++++++++++++++--------------- 1 file changed, 226 insertions(+), 117 deletions(-) diff --git a/worf/src/lib/gui.rs b/worf/src/lib/gui.rs index 89f9f85..ad621c4 100644 --- a/worf/src/lib/gui.rs +++ b/worf/src/lib/gui.rs @@ -16,6 +16,7 @@ use gdk4::{ glib::{self, MainContext, Propagation}, prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt}, }; +use gtk4::prelude::{AdjustmentExt, EventControllerExt}; use gtk4::{ Align, Application, ApplicationWindow, CssProvider, EventControllerKey, Expander, FlowBox, FlowBoxChild, GestureClick, Image, Label, ListBox, ListBoxRow, NaturalWrapMode, Ordering, @@ -609,11 +610,15 @@ fn build_ui( let background = create_background(&config.read().unwrap()); + let search_entry = SearchEntry::new(); + search_entry.set_can_focus(true); + let main_window = window.clone(); + main_window.set_can_focus(true); let ui_elements = Rc::new(UiElements { app, - window, + window: main_window, background, - search: SearchEntry::new(), + search: search_entry, main_box: FlowBox::new(), menu_rows: Arc::new(RwLock::new(HashMap::new())), search_text: Arc::new(Mutex::new(String::new())), @@ -659,6 +664,8 @@ fn build_ui( } ui_elements.window.set_child(Some(&ui_elements.outer_box)); + // Set initial focus to the search entry + ui_elements.search.grab_focus(); ui_elements.scroll.set_widget_name("scroll"); ui_elements.scroll.set_hexpand(true); @@ -758,7 +765,12 @@ fn build_main_box(config: &Config, ui_elements: &Rc( if done { let lock = ui_clone.menu_rows.read().unwrap(); - select_first_visible_child(&lock, &ui_clone.main_box); + select_visible_child( + &*lock, + &ui_clone.main_box, + &ui_clone.scroll, + &ChildPosition::Front, + ); log::debug!( "Created {} menu items in {:?}", @@ -963,16 +980,18 @@ fn build_ui_from_menu_items( } fn setup_key_event_handler( - ui: &Rc>, + ui_elements: &Rc>, meta: &Rc>, custom_keys: Option<&CustomKeys>, ) { - let key_controller = EventControllerKey::new(); - - let ui_clone = Rc::clone(ui); + // handle keys as soon as possible + // Remove old handler, use only one for both window and search + let key_controller_window = EventControllerKey::new(); + key_controller_window.set_propagation_phase(gtk4::PropagationPhase::Capture); + let ui_clone = Rc::clone(ui_elements); let meta_clone = Rc::clone(meta); let keys_clone = custom_keys.cloned(); - key_controller.connect_key_pressed(move |_, key_value, key_code, modifier| { + key_controller_window.connect_key_pressed(move |_, key_value, key_code, modifier| { handle_key_press( &ui_clone, &meta_clone, @@ -983,7 +1002,24 @@ fn setup_key_event_handler( ) }); - ui.window.add_controller(key_controller); + ui_elements.window.add_controller(key_controller_window); + + let key_controller_search = EventControllerKey::new(); + key_controller_search.set_propagation_phase(gtk4::PropagationPhase::Capture); + let ui_clone2 = Rc::clone(ui_elements); + let meta_clone2 = Rc::clone(meta); + let keys_clone2 = custom_keys.cloned(); + key_controller_search.connect_key_pressed(move |_, key_value, key_code, modifier| { + handle_key_press( + &ui_clone2, + &meta_clone2, + key_value, + key_code, + modifier, + keys_clone2.as_ref(), + ) + }); + ui_elements.search.add_controller(key_controller_search); } fn is_key_match( @@ -1014,11 +1050,152 @@ fn handle_key_press( ) -> Propagation { log::debug!("received key. code: {key_code}, key: {keyboard_key:?}"); - let detection_type = + let propagate = handle_custom_keys(ui, meta, keyboard_key, key_code, modifier_type, custom_keys); + if propagate == Propagation::Stop { + return propagate; + } + + match keyboard_key { + gdk4::Key::BackSpace | gdk4::Key::Delete => { + let mut query = { + let search_text = ui.search_text.lock().unwrap(); + search_text.clone() + }; + if !query.is_empty() { + let pos = ui.search.position(); + let del_pos = if keyboard_key == gdk4::Key::BackSpace { + pos - 1 + } else { + pos + }; + if let Some((start, ch)) = query.char_indices().nth(del_pos as usize) { + let end = start + ch.len_utf8(); + query.replace_range(start..end, ""); + } + set_search_text(ui, meta, &query); + ui.search.set_position(pos - 1); + update_view_from_provider(ui, meta, &query); + } + } + gdk4::Key::Home => { + ui.search.set_position(0); + } + gdk4::Key::Left => { + ui.search.set_position(ui.search.position() - 1); + } + gdk4::Key::Right => { + ui.search.set_position(ui.search.position() + 1); + } + gdk4::Key::End => { + if let Ok(i) = i32::try_from(ui.search_text.lock().unwrap().len() + 1) { + ui.search.set_position(i); + } + } + gdk4::Key::Up => { + return move_selection(ui, true); + } + gdk4::Key::Down => { + return move_selection(ui, false); + } + _ => { + if let Some(c) = keyboard_key.to_unicode() { + let mut query = { + let search_text = ui.search_text.lock().unwrap(); + search_text.clone() + }; + let pos = ui.search.position(); + let byte_idx = query + .char_indices() + .nth(pos as usize) + .map_or_else(|| query.len(), |(i, _)| i); + query.insert(byte_idx, c); + set_search_text(ui, meta, &query); + ui.search.set_position(pos + 1); + update_view_from_provider(ui, meta, &query); + } + } + } + Propagation::Proceed +} + +fn move_selection(ui: &Rc>, up: bool) -> Propagation { + let selected_children = ui.main_box.selected_children(); + let Some(selected) = selected_children.first() else { + return Propagation::Proceed; + }; + + let Some(first_child) = find_visible_child( + &ui.menu_rows.read().unwrap(), + &ui.main_box, + &ChildPosition::Front, + ) else { + return Propagation::Proceed; + }; + + let Some(last_child) = find_visible_child( + &ui.menu_rows.read().unwrap(), + &ui.main_box, + &ChildPosition::Back, + ) else { + return Propagation::Proceed; + }; + + if up && first_child == *selected { + select_visible_child( + &ui.menu_rows.read().unwrap(), + &ui.main_box, + &ui.scroll, + &ChildPosition::Back, + ); + Propagation::Stop + } else if !up && last_child == *selected { + select_visible_child( + &ui.menu_rows.read().unwrap(), + &ui.main_box, + &ui.scroll, + &ChildPosition::Front, + ); + Propagation::Stop + } else { + Propagation::Proceed + } +} + +fn handle_custom_keys( + ui: &Rc>, + meta: &Rc>, + keyboard_key: gdk4::Key, + key_code: u32, + modifier_type: gdk4::ModifierType, + custom_keys: Option<&CustomKeys>, +) -> Propagation { + let detection_type = meta.config.read().unwrap().key_detection_type(); + if let Some(custom_keys) = custom_keys { + let mods = modifiers_from_mask(modifier_type); + for custom_key in &custom_keys.bindings { + let custom_key_match = if detection_type == KeyDetectionType::Code { + custom_key.key == key_code.into() + } else { + custom_key.key == keyboard_key.to_upper().into() + } && mods.is_subset(&custom_key.modifiers); + + log::debug!("custom key {custom_key:?}, match {custom_key_match}"); + + if custom_key_match { + let search_lock = ui.search_text.lock().unwrap(); + if let Err(e) = + handle_selected_item(ui, meta, Some(&search_lock), None, Some(custom_key)) + { + log::error!("{e}"); + } + } + } + } + // hide search - let propagate = if is_key_match( + if is_key_match( meta.config.read().unwrap().key_hide_search(), &detection_type, key_code, @@ -1060,104 +1237,7 @@ fn handle_key_press( handle_key_expand(ui, meta) } else { Propagation::Proceed - }; - - if propagate == Propagation::Stop { - return propagate; } - - match keyboard_key { - gdk4::Key::BackSpace | gdk4::Key::Delete => { - let mut query = { - let search_text = ui.search_text.lock().unwrap(); - search_text.clone() - }; - - if !query.is_empty() { - let pos = ui.search.position(); - let del_pos = if keyboard_key == gdk4::Key::BackSpace { - pos - 1 - } else { - pos - }; - if let Some((start, ch)) = query.char_indices().nth(del_pos as usize) { - let end = start + ch.len_utf8(); - query.replace_range(start..end, ""); - } - - set_search_text(ui, meta, &query); - ui.search.set_position(pos - 1); - update_view_from_provider(ui, meta, &query); - } - } - gdk4::Key::Home => { - ui.search.set_position(0); - } - gdk4::Key::Left => { - ui.search.set_position(ui.search.position() - 1); - } - gdk4::Key::Right => { - ui.search.set_position(ui.search.position() + 1); - } - gdk4::Key::End => { - if let Ok(i) = i32::try_from(ui.search_text.lock().unwrap().len() + 1) { - ui.search.set_position(i); - } - } - _ => { - if let Some(c) = keyboard_key.to_unicode() { - let mut query = { - let search_text = ui.search_text.lock().unwrap(); - search_text.clone() - }; - - let pos = ui.search.position(); - let byte_idx = query - .char_indices() - .nth(pos as usize) - .map_or_else(|| query.len(), |(i, _)| i); - - query.insert(byte_idx, c); - set_search_text(ui, meta, &query); - ui.search.set_position(pos + 1); - update_view_from_provider(ui, meta, &query); - } - } - } - Propagation::Proceed -} - -fn handle_custom_keys( - ui: &Rc>, - meta: &Rc>, - keyboard_key: gdk4::Key, - key_code: u32, - modifier_type: gdk4::ModifierType, - custom_keys: Option<&CustomKeys>, -) -> KeyDetectionType { - let detection_type = meta.config.read().unwrap().key_detection_type(); - if let Some(custom_keys) = custom_keys { - let mods = modifiers_from_mask(modifier_type); - for custom_key in &custom_keys.bindings { - let custom_key_match = if detection_type == KeyDetectionType::Code { - custom_key.key == key_code.into() - } else { - custom_key.key == keyboard_key.to_upper().into() - } && mods.is_subset(&custom_key.modifiers); - - log::debug!("custom key {custom_key:?}, match {custom_key_match}"); - - if custom_key_match { - let search_lock = ui.search_text.lock().unwrap(); - if let Err(e) = - handle_selected_item(ui, meta, Some(&search_lock), None, Some(custom_key)) - { - log::error!("{e}"); - } - } - } - } - detection_type } fn update_view_from_provider(ui: &Rc>, meta: &Rc>, query: &str) @@ -1183,7 +1263,7 @@ where meta.search_ignored_words.as_ref(), ); - select_first_visible_child(&*menu_rows, &ui.main_box); + select_visible_child(&*menu_rows, &ui.main_box, &ui.scroll, &ChildPosition::Front); if meta.config.read().unwrap().auto_select_on_search() { let visible_items = menu_rows @@ -1856,22 +1936,51 @@ pub fn filtered_query(search_ignored_words: Option<&Vec>, query: &str) -> } query } +enum ChildPosition { + Front, + Back, +} -fn select_first_visible_child( +fn find_visible_child( items: &HashMap>, flow_box: &FlowBox, -) { - for i in 0..items.len() { + direction: &ChildPosition, +) -> Option { + let range: Box> = match direction { + ChildPosition::Front => Box::new(0..items.len()), + ChildPosition::Back => Box::new((0..items.len()).rev()), + }; + + for i in range { let i_32 = i.try_into().unwrap_or(i32::MAX); if let Some(child) = flow_box.child_at_index(i_32) { if child.is_visible() { - flow_box.select_child(&child); - child.grab_focus(); - child.activate(); - return; + return Some(child); } } } + + None +} + +fn select_visible_child( + items: &HashMap>, + flow_box: &FlowBox, + scroll: &ScrolledWindow, + direction: &ChildPosition, +) { + if let Some(child) = find_visible_child(items, flow_box, direction) { + flow_box.select_child(&child); + child.grab_focus(); + child.activate(); + + let vadj = scroll.vadjustment(); + let new_scroll = match direction { + ChildPosition::Front => 0.0, + ChildPosition::Back => vadj.upper() - vadj.page_size(), + }; + vadj.set_value(new_scroll); + } } // allowed because truncating is fine, we do no need the precision