From 5e81abe0f40ea1134421608a9a2be364973c15ae Mon Sep 17 00:00:00 2001 From: sandroid Date: Thu, 14 Nov 2024 00:59:34 +0100 Subject: [PATCH 1/4] Feature: Navigate to item starting with character --- src/app.rs | 7 +++++++ src/tab.rs | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 7388ec17..299488af 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1945,6 +1945,13 @@ impl Application for App { return self.update(action.message(Some(entity))); } } + if let Key::Character(char) = key { + return self.update(Message::TabMessage( + Some(entity), + // TODO need to store keys and send them all + tab::Message::SelectNextPrefix(char), + )); + } } Message::MaybeExit => { if self.window_id_opt.is_none() && self.pending_operations.is_empty() { diff --git a/src/tab.rs b/src/tab.rs index cedb092a..bbc5d182 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -27,7 +27,7 @@ use cosmic::{ Size, Subscription, }, - iced_core::{mouse::ScrollDelta, widget::tree}, + iced_core::{mouse::ScrollDelta, widget::tree, SmolStr}, theme, widget::{ self, @@ -1071,6 +1071,7 @@ pub enum Message { SearchContext(Location, SearchContextWrapper), SearchReady(bool), SelectAll, + SelectNextPrefix(SmolStr), SetSort(HeadingOptions, bool), Thumbnail(PathBuf, ItemThumbnail), ToggleShowHidden, @@ -1857,6 +1858,41 @@ impl Tab { } } + pub fn select_next_prefix(&mut self, prefix: &str) -> bool { + *self.cached_selected.borrow_mut() = None; + let mut found = false; + if let Some(ref mut items) = self.items_opt { + let focus = self.select_focus; + let start = focus.map(|i| i + 1).unwrap_or(0); + let (until, after) = items.split_at_mut(start); + for (i, item) in after + .iter_mut() + .enumerate() + .map(|x| (x.0 + start, x.1)) + .chain(until.iter_mut().enumerate()) + { + if !found + && (!item.hidden || self.config.show_hidden) + && item.name.to_lowercase().starts_with(prefix) + { + item.selected = true; + self.select_focus = Some(i); + found = true; + } else { + item.selected = false; + } + } + + // Reselect the original selection in case no new selection was found + if !found { + if let Some(f) = focus { + items[f].selected = true; + } + } + } + found + } + pub fn select_path(&mut self, path: PathBuf) { let location = Location::Path(path); *self.cached_selected.borrow_mut() = None; @@ -2807,6 +2843,17 @@ impl Tab { )); } } + Message::SelectNextPrefix(s) => { + self.select_next_prefix(&s); + if let Some(offset) = self.select_focus_scroll() { + commands.push(Command::Iced( + scrollable::scroll_to(self.scrollable_id.clone(), offset).into(), + )); + } + if let Some(id) = self.select_focus_id() { + commands.push(Command::Iced(widget::button::focus(id).into())); + } + } Message::SetSort(heading_option, dir) => { if !matches!(self.location, Location::Search(..)) { self.sort_name = heading_option; From 1b87c4c696954455b3a3bd1cadf7d8a179be2c09 Mon Sep 17 00:00:00 2001 From: sandroid Date: Fri, 15 Nov 2024 22:38:40 +0100 Subject: [PATCH 2/4] Implement prefix search with more than one character --- src/app.rs | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- src/tab.rs | 12 +++++++++++- 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 299488af..13eaa6c0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -471,6 +471,49 @@ pub struct FavoriteIndex(usize); pub struct MounterData(MounterKey, MounterItem); +struct PrefixSearch { + search: String, + reset_delay: u64, + next_reset: Option, +} + +impl PrefixSearch { + pub fn new(reset_delay: u64) -> PrefixSearch { + PrefixSearch { + search: String::new(), + reset_delay, + next_reset: None, + } + } + + pub fn reset(&mut self) { + self.search.clear(); + self.next_reset = None; + } + + pub fn update_search(&mut self, string: &str) { + // Clear the word when the last typed character is older then the reset delay + if let Some(next_reset) = self.next_reset { + if next_reset <= Instant::now() { + self.reset(); + } + } + // Add the typed character + self.search.push_str(string); + // Restart the reset timeout + let delay = time::Duration::from_millis(self.reset_delay); + self.next_reset = Instant::now().checked_add(delay); + // Reset the search term when calculating the next reset failed + if self.next_reset.is_none() { + self.search.clear(); + } + } + + pub fn word(&self) -> &str { + &self.search + } +} + #[derive(Clone, Debug)] pub enum WindowKind { Desktop(Entity), @@ -538,6 +581,7 @@ pub struct App { tab_dnd_hover: Option<(Entity, Instant)>, nav_drag_id: DragId, tab_drag_id: DragId, + prefix_search: PrefixSearch, } impl App { @@ -1466,6 +1510,7 @@ impl Application for App { tab_dnd_hover: None, nav_drag_id: DragId::new(), tab_drag_id: DragId::new(), + prefix_search: PrefixSearch::new(500), // TODO do not hardcode delay? }; let mut commands = vec![app.update_config()]; @@ -1942,14 +1987,15 @@ impl Application for App { let entity = self.tab_model.active(); for (key_bind, action) in self.key_binds.iter() { if key_bind.matches(modifiers, &key) { + self.prefix_search.reset(); return self.update(action.message(Some(entity))); } } if let Key::Character(char) = key { + self.prefix_search.update_search(&char); return self.update(Message::TabMessage( Some(entity), - // TODO need to store keys and send them all - tab::Message::SelectNextPrefix(char), + tab::Message::SelectNextPrefix(self.prefix_search.word().into()), )); } } diff --git a/src/tab.rs b/src/tab.rs index bbc5d182..3f5ab79b 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1859,6 +1859,16 @@ impl Tab { } pub fn select_next_prefix(&mut self, prefix: &str) -> bool { + // Special case: when all entered characters are the same, only search for said character. This allows quickly cycling through items starting with the same character. + let term = match prefix.chars().next() { + Some(first) => if prefix.chars().all(|c| c == first) { + &prefix[..1] + } else { + prefix + }, + None => return false, + }; + *self.cached_selected.borrow_mut() = None; let mut found = false; if let Some(ref mut items) = self.items_opt { @@ -1873,7 +1883,7 @@ impl Tab { { if !found && (!item.hidden || self.config.show_hidden) - && item.name.to_lowercase().starts_with(prefix) + && item.name.to_lowercase().starts_with(term) { item.selected = true; self.select_focus = Some(i); From 3f7c735cecac66a8667d5073ac0555da9aa50cb7 Mon Sep 17 00:00:00 2001 From: sandroid Date: Fri, 15 Nov 2024 22:52:02 +0100 Subject: [PATCH 3/4] Fix prefix search for special characters --- src/app.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 13eaa6c0..a88af4e1 100644 --- a/src/app.rs +++ b/src/app.rs @@ -25,6 +25,7 @@ use cosmic::{ window::{self, Event as WindowEvent, Id as WindowId}, Alignment, Event, Length, Size, Subscription, }, + iced_core::SmolStr, iced_runtime::clipboard, style, theme, widget::{ @@ -278,7 +279,7 @@ pub enum Message { DialogUpdate(DialogPage), DialogUpdateComplete(DialogPage), ExtractHere(Option), - Key(Modifiers, Key), + Key(Modifiers, Key, Option), LaunchUrl(String), MaybeExit, Modifiers(Modifiers), @@ -1983,7 +1984,7 @@ impl Application for App { } } } - Message::Key(modifiers, key) => { + Message::Key(modifiers, key, text) => { let entity = self.tab_model.active(); for (key_bind, action) in self.key_binds.iter() { if key_bind.matches(modifiers, &key) { @@ -1991,8 +1992,8 @@ impl Application for App { return self.update(action.message(Some(entity))); } } - if let Key::Character(char) = key { - self.prefix_search.update_search(&char); + if let Some(text) = text { + self.prefix_search.update_search(&text); return self.update(Message::TabMessage( Some(entity), tab::Message::SelectNextPrefix(self.prefix_search.word().into()), @@ -4214,8 +4215,8 @@ impl Application for App { let mut subscriptions = vec![ event::listen_with(|event, status, _window_id| match event { - Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => match status { - event::Status::Ignored => Some(Message::Key(modifiers, key)), + Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, text, ..}) => match status { + event::Status::Ignored => Some(Message::Key(modifiers, key, text)), event::Status::Captured => None, }, Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { From 89890cc1ce10bf3d1b82b860002e469d0ec583cb Mon Sep 17 00:00:00 2001 From: sandroid Date: Sun, 17 Nov 2024 14:47:09 +0100 Subject: [PATCH 4/4] Fix: step through items in the correct order when sort direction is reversed --- src/tab.rs | 85 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/src/tab.rs b/src/tab.rs index 3f5ab79b..3b6f98a6 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -1858,49 +1858,66 @@ impl Tab { } } - pub fn select_next_prefix(&mut self, prefix: &str) -> bool { - // Special case: when all entered characters are the same, only search for said character. This allows quickly cycling through items starting with the same character. - let term = match prefix.chars().next() { - Some(first) => if prefix.chars().all(|c| c == first) { - &prefix[..1] - } else { - prefix - }, - None => return false, - }; - + pub fn select_next_prefix(&mut self, prefix: &str) { *self.cached_selected.borrow_mut() = None; - let mut found = false; if let Some(ref mut items) = self.items_opt { - let focus = self.select_focus; - let start = focus.map(|i| i + 1).unwrap_or(0); + // Special case: when all entered characters are the same, only search for said character. + // This allows quickly cycling through items starting with the same character. + let term = match prefix.chars().next() { + Some(first) if prefix.chars().all(|c| c == first) => &prefix[..1], + Some(_) => prefix, + None => return (), + }; + + // Have to add 1 to the start index when not reversing because the + // currently selected item should be included in until so it gets + // considered last instead of first. + // When ordered reverse, we don't want to do so because moving it + // last would put it as first item when iterating in reverse. + let start = self + .select_focus + .map_or(0, |i| if self.sort_direction { i + 1 } else { i }); let (until, after) = items.split_at_mut(start); - for (i, item) in after + + // First iterate over all items after the current selection, then wrap around + let iter = after .iter_mut() .enumerate() .map(|x| (x.0 + start, x.1)) - .chain(until.iter_mut().enumerate()) - { - if !found - && (!item.hidden || self.config.show_hidden) - && item.name.to_lowercase().starts_with(term) - { - item.selected = true; - self.select_focus = Some(i); - found = true; - } else { - item.selected = false; - } - } + .chain(until.iter_mut().enumerate()); - // Reselect the original selection in case no new selection was found - if !found { - if let Some(f) = focus { - items[f].selected = true; - } + let found = if self.sort_direction { + Self::select_first_prefix(term, iter, self.config.show_hidden) + } else { + Self::select_first_prefix(term, iter.rev(), self.config.show_hidden) + }; + + if found.is_some() { + self.select_focus = found; + } else if let Some(focus) = self.select_focus { + items[focus].selected = true; + }; + } + } + + fn select_first_prefix<'a>( + prefix: &str, + iterator: impl Iterator, + consider_hidden: bool, + ) -> Option { + let mut selected = None; + for (i, item) in iterator { + if selected.is_none() + && (!item.hidden || consider_hidden) + && item.display_name.to_lowercase().starts_with(prefix) + { + selected = Some(i); + item.selected = true; + } else { + item.selected = false; } } - found + selected } pub fn select_path(&mut self, path: PathBuf) {