support wrapping
if first element is selected and up is pressed again the last element will be selected and vice versa
This commit is contained in:
parent
adc3ae053a
commit
024e24d4de
1 changed files with 226 additions and 117 deletions
|
@ -16,6 +16,7 @@ use gdk4::{
|
||||||
glib::{self, MainContext, Propagation},
|
glib::{self, MainContext, Propagation},
|
||||||
prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt},
|
prelude::{Cast, DisplayExt, MonitorExt, SurfaceExt},
|
||||||
};
|
};
|
||||||
|
use gtk4::prelude::{AdjustmentExt, EventControllerExt};
|
||||||
use gtk4::{
|
use gtk4::{
|
||||||
Align, Application, ApplicationWindow, CssProvider, EventControllerKey, Expander, FlowBox,
|
Align, Application, ApplicationWindow, CssProvider, EventControllerKey, Expander, FlowBox,
|
||||||
FlowBoxChild, GestureClick, Image, Label, ListBox, ListBoxRow, NaturalWrapMode, Ordering,
|
FlowBoxChild, GestureClick, Image, Label, ListBox, ListBoxRow, NaturalWrapMode, Ordering,
|
||||||
|
@ -609,11 +610,15 @@ fn build_ui<T>(
|
||||||
|
|
||||||
let background = create_background(&config.read().unwrap());
|
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 {
|
let ui_elements = Rc::new(UiElements {
|
||||||
app,
|
app,
|
||||||
window,
|
window: main_window,
|
||||||
background,
|
background,
|
||||||
search: SearchEntry::new(),
|
search: search_entry,
|
||||||
main_box: FlowBox::new(),
|
main_box: FlowBox::new(),
|
||||||
menu_rows: Arc::new(RwLock::new(HashMap::new())),
|
menu_rows: Arc::new(RwLock::new(HashMap::new())),
|
||||||
search_text: Arc::new(Mutex::new(String::new())),
|
search_text: Arc::new(Mutex::new(String::new())),
|
||||||
|
@ -659,6 +664,8 @@ fn build_ui<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
ui_elements.window.set_child(Some(&ui_elements.outer_box));
|
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_widget_name("scroll");
|
||||||
ui_elements.scroll.set_hexpand(true);
|
ui_elements.scroll.set_hexpand(true);
|
||||||
|
@ -758,7 +765,12 @@ fn build_main_box<T: Clone + 'static>(config: &Config, ui_elements: &Rc<UiElemen
|
||||||
fb.invalidate_sort();
|
fb.invalidate_sort();
|
||||||
|
|
||||||
let lock = ui_clone.menu_rows.read().unwrap();
|
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,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -946,7 +958,12 @@ fn build_ui_from_menu_items<T: Clone + 'static + Send>(
|
||||||
if done {
|
if done {
|
||||||
let lock = ui_clone.menu_rows.read().unwrap();
|
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!(
|
log::debug!(
|
||||||
"Created {} menu items in {:?}",
|
"Created {} menu items in {:?}",
|
||||||
|
@ -963,16 +980,18 @@ fn build_ui_from_menu_items<T: Clone + 'static + Send>(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_key_event_handler<T: Clone + 'static + Send>(
|
fn setup_key_event_handler<T: Clone + 'static + Send>(
|
||||||
ui: &Rc<UiElements<T>>,
|
ui_elements: &Rc<UiElements<T>>,
|
||||||
meta: &Rc<MetaData<T>>,
|
meta: &Rc<MetaData<T>>,
|
||||||
custom_keys: Option<&CustomKeys>,
|
custom_keys: Option<&CustomKeys>,
|
||||||
) {
|
) {
|
||||||
let key_controller = EventControllerKey::new();
|
// handle keys as soon as possible
|
||||||
|
// Remove old handler, use only one for both window and search
|
||||||
let ui_clone = Rc::clone(ui);
|
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 meta_clone = Rc::clone(meta);
|
||||||
let keys_clone = custom_keys.cloned();
|
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(
|
handle_key_press(
|
||||||
&ui_clone,
|
&ui_clone,
|
||||||
&meta_clone,
|
&meta_clone,
|
||||||
|
@ -983,7 +1002,24 @@ fn setup_key_event_handler<T: Clone + 'static + Send>(
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
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(
|
fn is_key_match(
|
||||||
|
@ -1014,11 +1050,152 @@ fn handle_key_press<T: Clone + 'static + Send>(
|
||||||
) -> Propagation {
|
) -> Propagation {
|
||||||
log::debug!("received key. code: {key_code}, key: {keyboard_key:?}");
|
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);
|
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<T: Clone + Send + 'static>(ui: &Rc<UiElements<T>>, 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<T: Clone + 'static + Send>(
|
||||||
|
ui: &Rc<UiElements<T>>,
|
||||||
|
meta: &Rc<MetaData<T>>,
|
||||||
|
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
|
// hide search
|
||||||
let propagate = if is_key_match(
|
if is_key_match(
|
||||||
meta.config.read().unwrap().key_hide_search(),
|
meta.config.read().unwrap().key_hide_search(),
|
||||||
&detection_type,
|
&detection_type,
|
||||||
key_code,
|
key_code,
|
||||||
|
@ -1060,104 +1237,7 @@ fn handle_key_press<T: Clone + 'static + Send>(
|
||||||
handle_key_expand(ui, meta)
|
handle_key_expand(ui, meta)
|
||||||
} else {
|
} else {
|
||||||
Propagation::Proceed
|
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<T: Clone + 'static + Send>(
|
|
||||||
ui: &Rc<UiElements<T>>,
|
|
||||||
meta: &Rc<MetaData<T>>,
|
|
||||||
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<T>(ui: &Rc<UiElements<T>>, meta: &Rc<MetaData<T>>, query: &str)
|
fn update_view_from_provider<T>(ui: &Rc<UiElements<T>>, meta: &Rc<MetaData<T>>, query: &str)
|
||||||
|
@ -1183,7 +1263,7 @@ where
|
||||||
meta.search_ignored_words.as_ref(),
|
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() {
|
if meta.config.read().unwrap().auto_select_on_search() {
|
||||||
let visible_items = menu_rows
|
let visible_items = menu_rows
|
||||||
|
@ -1856,22 +1936,51 @@ pub fn filtered_query(search_ignored_words: Option<&Vec<Regex>>, query: &str) ->
|
||||||
}
|
}
|
||||||
query
|
query
|
||||||
}
|
}
|
||||||
|
enum ChildPosition {
|
||||||
|
Front,
|
||||||
|
Back,
|
||||||
|
}
|
||||||
|
|
||||||
fn select_first_visible_child<T: Clone>(
|
fn find_visible_child<T: Clone>(
|
||||||
items: &HashMap<FlowBoxChild, MenuItem<T>>,
|
items: &HashMap<FlowBoxChild, MenuItem<T>>,
|
||||||
flow_box: &FlowBox,
|
flow_box: &FlowBox,
|
||||||
) {
|
direction: &ChildPosition,
|
||||||
for i in 0..items.len() {
|
) -> Option<FlowBoxChild> {
|
||||||
|
let range: Box<dyn Iterator<Item = usize>> = 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);
|
let i_32 = i.try_into().unwrap_or(i32::MAX);
|
||||||
if let Some(child) = flow_box.child_at_index(i_32) {
|
if let Some(child) = flow_box.child_at_index(i_32) {
|
||||||
if child.is_visible() {
|
if child.is_visible() {
|
||||||
flow_box.select_child(&child);
|
return Some(child);
|
||||||
child.grab_focus();
|
|
||||||
child.activate();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_visible_child<T: Clone>(
|
||||||
|
items: &HashMap<FlowBoxChild, MenuItem<T>>,
|
||||||
|
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
|
// allowed because truncating is fine, we do no need the precision
|
||||||
|
|
Loading…
Add table
Reference in a new issue