diff --git a/src/main_window.rs b/src/main_window.rs index 4b1b6e5..9b099a0 100644 --- a/src/main_window.rs +++ b/src/main_window.rs @@ -2,8 +2,7 @@ use crate::cache::Cache; use crate::config::ConfigService; use crate::format; use crate::model::record::{Record, RecordType, RECORD_TYPE_GENERIC}; -use crate::model::tree::RecordNode; -use crate::model::tree::RecordTree; +use crate::model::tree::{RecordNode, RecordTree}; use crate::search::item::SearchMatch; use crate::ui; use crate::ui::dashboard::PSDashboard; @@ -14,7 +13,7 @@ use crate::ui::dialogs::say::say; use crate::ui::edit_record::dialog::edit_record; use crate::ui::forms::entry::form_password_entry; use crate::ui::open_file::OpenFile; -use crate::ui::search_bar::SearchEvent; +use crate::ui::search_bar::{SearchEvent, SearchEventType}; use crate::utils::typed_list_store::TypedListStore; use crate::utils::ui::*; use gtk::{gio, glib, prelude::*, subclass::prelude::*}; @@ -38,8 +37,6 @@ pub struct OpenedFile { mod imp { use super::*; use crate::ui::file_pane::FilePane; - use crate::ui::search_bar::PSSearchBar; - use crate::ui::search_pane::SearchPane; use crate::ui::toast::Toast; use std::cell::OnceCell; @@ -48,7 +45,6 @@ mod imp { #[default] Initial, FileOpened, - SearchResults, } #[derive(Default)] @@ -61,9 +57,7 @@ mod imp { pub toast: Toast, pub dashboard: PSDashboard, pub open_file: OpenFile, - pub search_bar: PSSearchBar, pub file_pane: FilePane, - pub search_pane: SearchPane, pub delete_handler: RefCell>, @@ -138,11 +132,9 @@ mod imp { .add_named(&self.dashboard.get_widget(), Some("dashboard")); self.stack.add_named(&self.open_file, Some("open_file")); self.stack.add_named(&self.file_pane, Some("file")); - self.stack.add_named(&self.search_pane, Some("search")); let main_pane = gtk::Grid::builder().build(); - main_pane.attach(&self.search_bar, 0, 0, 1, 1); - main_pane.attach(&self.stack, 0, 1, 1, 1); + main_pane.attach(&self.stack, 0, 0, 1, 1); let overlay = overlayed(&main_pane, &self.toast.as_widget()); win.set_child(Some(&overlay)); @@ -159,33 +151,6 @@ mod imp { }); *self.delete_handler.borrow_mut() = Some(delete_handler); - self.search_bar.connect_search(glib::clone!( - #[weak] - win, - move |event| { - glib::MainContext::default().spawn_local(async move { - win.search(&event).await; - }); - } - )); - self.search_bar.connect_configure(glib::clone!( - #[weak(rename_to = imp)] - self, - move |search_config| { - imp.config_service.get().unwrap().update(|config| { - config.search_in_secrets = search_config.search_in_secrets; - }); - } - )); - self.search_bar.connect_search_closed(glib::clone!( - #[weak(rename_to = imp)] - self, - move || { - imp.search_reset(); - imp.set_mode(imp::AppMode::FileOpened); - } - )); - self.file_pane.connect_edit_record(glib::clone!( #[weak] win, @@ -209,23 +174,6 @@ mod imp { imp.toast.notify(message); } )); - - self.search_pane.connect_edit_record(glib::clone!( - #[weak] - win, - move |_, position, record_node| { - glib::MainContext::default().spawn_local(async move { - win.action_edit(position, record_node).await; - }); - } - )); - self.search_pane.connect_user_notification(glib::clone!( - #[weak(rename_to = imp)] - self, - move |_, message| { - imp.toast.notify(message); - } - )); } } @@ -251,20 +199,12 @@ mod imp { self.stack.set_visible_child_name("file"); self.search_reset(); } - AppMode::SearchResults => { - self.file_actions.set_enabled(false); - - self.stack.set_visible_child_name("search"); - self.search_bar.set_search_mode(true); - } } self.mode.set(mode); } pub fn search_reset(&self) { - self.search_bar.set_search_mode(false); - self.search_bar.reset(); - self.search_pane.reset(); + self.file_pane.view().search_reset(); } } } @@ -306,75 +246,6 @@ impl PSMainWindow { } } - async fn search(&self, event: &SearchEvent) { - if event.query.is_empty() { - return; - } - - fn traverse( - records: &TypedListStore, - path: TypedListStore, - query: &str, - search_in_secrets: bool, - result: &mut TypedListStore, - ) { - for record in records { - if record.record().has_text(query, search_in_secrets) { - result.append(&SearchMatch::new(&record, &path.clone_list())); - } - if let Some(children) = record.children() { - let path_to_record = path.appended(record.clone()); - traverse(children, path_to_record, query, search_in_secrets, result); - } - } - } - - let mut matching_records = Default::default(); - traverse( - &self.imp().file_pane.file().records, // current records ???? - Default::default(), - &event.query, - event.search_in_secrets, - &mut matching_records, - ); - - // self.imp().view.select_position_async(0).await; // TODO: throttle - - // let private = self.private(); - // let model = private.data.borrow().as_model(); - // let iters = flatten_tree(&model); - - // let mut search_iter: Box> = match event.event_type { - // SearchEventType::Change | SearchEventType::Next => Box::new(iters.iter()), - // SearchEventType::Prev => Box::new(iters.iter().rev()), - // }; - // if let Some((_selection_record, selection_iter)) = private.view.get_selected_record() { - // search_iter = Box::new( - // search_iter.skip_while(move |iter| model.path(iter) != model.path(&selection_iter)), - // ); - // } - // match event.event_type { - // SearchEventType::Change => {} - // SearchEventType::Next | SearchEventType::Prev => { - // search_iter = Box::new(search_iter.skip(1)) - // } - // }; - - // let next_match = search_iter - // .map(|iter| (iter, private.data.borrow().get(iter))) - // .find(|(_iter, record)| record.has_text(&event.query, event.search_in_secrets)); - - // if let Some(next_match) = next_match { - // private.view.select_iter(next_match.0); - // self.listview_cursor_changed(&[next_match.1]); - // } else { - // self.error_bell(); - // } - - self.imp().set_mode(imp::AppMode::SearchResults); - self.imp().search_pane.set_model(matching_records).await; - } - fn get_usernames(&self) -> Vec { get_usernames(&self.imp().file_pane.file()) } @@ -400,7 +271,7 @@ impl PSMainWindow { .header_bar .set_title_widget(Some(&crate::utils::ui::title(WINDOW_TITLE))); } - imp::AppMode::FileOpened | imp::AppMode::SearchResults => { + imp::AppMode::FileOpened => { let mut display_filename = self .file() .filename @@ -527,15 +398,15 @@ impl PSMainWindow { win.imp().cache.set(cache.clone()).ok().unwrap(); win.imp().dashboard.update(cache); - let config = config_service.get(); - win.imp().search_bar.configure(config.search_in_secrets); - config_service.on_change.subscribe(glib::clone!( - #[weak] - win, - move |new_config| { - win.imp().search_bar.configure(new_config.search_in_secrets); - } - )); + // let config = config_service.get(); + // win.imp().search_bar.configure(config.search_in_secrets); + // config_service.on_change.subscribe(glib::clone!( + // #[weak] + // win, + // move |new_config| { + // win.imp().search_bar.configure(new_config.search_in_secrets); + // } + // )); win.present(); win.imp().set_mode(imp::AppMode::Initial); @@ -606,7 +477,7 @@ impl PSMainWindow { #[action(name = "find")] fn action_find(&self) { - self.imp().search_bar.set_search_mode(true); + self.imp().file_pane.view().search_start(); } #[action(name = "add")] diff --git a/src/model/tree.rs b/src/model/tree.rs index 8ade9dc..3604de1 100644 --- a/src/model/tree.rs +++ b/src/model/tree.rs @@ -65,3 +65,43 @@ impl RecordNode { pub struct RecordTree { pub records: TypedListStore, } + +impl RecordTree { + pub fn remove(&self, record_node: &RecordNode) { + fn traverse(list: &TypedListStore, record_node: &RecordNode) { + if let Some(position) = list.find(record_node) { + list.remove(position); + } + for item in list { + if let Some(children) = item.children() { + traverse(children, record_node); + } + } + } + traverse(&self.records, record_node); + } + + pub fn depth_first_iter(&self, forward: bool) -> impl Iterator { + RecordTreeIter { + records: self.records.clone(), + stack: Vec::new(), + position: 0, + forward, + } + } +} + +pub struct RecordTreeIter { + records: TypedListStore, + stack: Vec>, + position: u32, + forward: bool, +} + +impl Iterator for RecordTreeIter { + type Item = RecordNode; + + fn next(&mut self) -> Option { + None + } +} diff --git a/src/ui/file_pane.rs b/src/ui/file_pane.rs index 1d2e8be..6848d5a 100644 --- a/src/ui/file_pane.rs +++ b/src/ui/file_pane.rs @@ -27,7 +27,6 @@ mod imp { use crate::ui::record_type_popover::RecordTypePopoverBuilder; use crate::ui::record_view::has_record::{PSRecordViewOptions, RECORD_NODE_HAS_RECORD}; use crate::ui::record_view::item::DropOption; - use crate::utils::typed_list_store::TypedListStore; use crate::utils::ui::{action_button, action_popover_button, orphan_all_children}; use std::cell::RefCell; use std::sync::OnceLock; @@ -36,8 +35,6 @@ mod imp { pub nav_bar: PSNavBar, pub view: PSRecordView, pub file: RefCell, - pub current_path: TypedListStore, - pub current_records: RefCell>, } #[glib::object_subclass] @@ -75,8 +72,6 @@ mod imp { drag_and_drop: true, }), file: Default::default(), - current_path: Default::default(), - current_records: Default::default(), } } } @@ -135,49 +130,13 @@ mod imp { self.view.connect_record_activated(glib::clone!( #[weak(rename_to = imp)] self, - move |position| { + move |position, record_node| { glib::MainContext::default().spawn_local(async move { - imp.row_activated(position).await; + imp.row_activated(position, record_node).await; }); } )); - self.nav_bar.connect_go_home(glib::clone!( - #[weak(rename_to = imp)] - self, - move || { - glib::MainContext::default().spawn_local(async move { imp.go_home().await }); - } - )); - self.nav_bar.connect_go_path(glib::clone!( - #[weak(rename_to = imp)] - self, - move |position| { - glib::MainContext::default() - .spawn_local(async move { imp.go_path(position).await }); - } - )); - self.nav_bar.connect_go_up(glib::clone!( - #[weak(rename_to = imp)] - self, - move || { - glib::MainContext::default().spawn_local(async move { imp.go_up().await }); - } - )); - self.view.connect_go_home(glib::clone!( - #[weak(rename_to = imp)] - self, - move || { - glib::MainContext::default().spawn_local(async move { imp.go_home().await }); - } - )); - self.view.connect_go_up(glib::clone!( - #[weak(rename_to = imp)] - self, - move || { - glib::MainContext::default().spawn_local(async move { imp.go_up().await }); - } - )); self.view.connect_move_record(glib::clone!( #[weak(rename_to = imp)] self, @@ -190,10 +149,7 @@ mod imp { } )); - self.set_view_model(&self.file.borrow().records); - - self.nav_bar - .set_model(self.current_path.untyped().upcast_ref()); + self.update_view_model(); let shortcuts = gtk::ShortcutController::new(); shortcuts.add_shortcut( @@ -236,81 +192,37 @@ mod imp { impl WidgetImpl for FilePane {} impl FilePane { - pub fn set_view_model(&self, model: &TypedListStore) { - *self.current_records.borrow_mut() = model.clone(); - self.view.set_model(model.untyped().upcast_ref()); - } - - async fn go_home(&self) { - self.current_path.remove_all(); - self.set_view_model(&self.file.borrow().records); - self.view.select_position_async(0).await; - } - - async fn go_path(&self, position: u32) { - if position + 1 >= self.current_path.len() { - return; - } - let prev = self.current_path.get(position + 1); - self.current_path.truncate(position + 1); - let records = match self.current_path.last() { - Some(parent) => parent.children().unwrap().clone(), - None => self.file.borrow().records.clone(), - }; - self.set_view_model(&records); - if let Some(prev) = prev { - self.view.select_object(prev.upcast_ref()).await; - } - } - - async fn go_up(&self) { - let prev = self.current_path.pop_back(); - let records = match self.current_path.last() { - Some(parent) => parent.children().unwrap().clone(), - None => self.file.borrow().records.clone(), - }; - self.set_view_model(&records); - if let Some(prev) = prev { - self.view.select_object(prev.upcast_ref()).await; - } + pub fn update_view_model(&self) { + self.view.set_model(&*self.file.borrow()); } - async fn row_activated(&self, position: u32) { - let Some(record) = self.current_records.borrow().get(position) else { - return; - }; - if let Some(children) = record.children() { - self.current_path.append(&record); - self.set_view_model(children); - self.view.select_position_async(0).await; - } else { - self.obj().emit_edit_record(position, &record); - } + async fn row_activated(&self, position: u32, record: RecordNode) { + self.obj().emit_edit_record(position, &record); } fn move_record(&self, src: &RecordNode, dst: &RecordNode, option: DropOption) { - let Some(src_pos) = self.current_records.borrow().iter().position(|r| r == *src) else { - return; - }; - self.current_records.borrow().remove(src_pos as u32); - - let dst_pos = self.current_records.borrow().iter().position(|r| r == *dst); - match option { - DropOption::Above => { - self.current_records - .borrow() - .insert(dst_pos.unwrap_or(0), src); - } - DropOption::Below => { - self.current_records - .borrow() - .insert(dst_pos.map(|p| p + 1), src); - } - DropOption::Into => { - self.obj().append_records_to(Some(dst), &[src.clone()]); - } - } - self.obj().emit_file_changed(); + // let Some(src_pos) = self.current_records.borrow().iter().position(|r| r == *src) else { + // return; + // }; + // self.current_records.borrow().remove(src_pos as u32); + + // let dst_pos = self.current_records.borrow().iter().position(|r| r == *dst); + // match option { + // DropOption::Above => { + // self.current_records + // .borrow() + // .insert(dst_pos.unwrap_or(0), src); + // } + // DropOption::Below => { + // self.current_records + // .borrow() + // .insert(dst_pos.map(|p| p + 1), src); + // } + // DropOption::Into => { + // self.obj().append_records_to(Some(dst), &[src.clone()]); + // } + // } + // self.obj().emit_file_changed(); } } } @@ -337,48 +249,53 @@ impl FilePane { pub async fn set_file(&self, file: RecordTree) { *self.imp().file.borrow_mut() = file; - self.imp().current_path.remove_all(); - self.imp().set_view_model(&self.imp().file.borrow().records); + self.imp().update_view_model(); self.imp().view.select_position_async(0).await; self.selection_changed(gtk::Bitset::new_empty()); self.grab_focus_to_view(); } - // pub async fn reset(&self) { - // self.set_model(Default::default()).await; - // } + pub async fn reset(&self) { + self.set_file(Default::default()).await; + } - fn get_record(&self, position: u32) -> Option { - self.imp().current_records.borrow().get(position) + fn record_at(&self, position: u32) -> Option { + self.imp().view.record_at(position) } pub fn append_record(&self, record_node: &RecordNode) { - self.imp().current_records.borrow().append(record_node); - let position = self.imp().current_records.borrow().len() - 1; - self.imp().view.select_position(position); - self.selection_changed(gtk::Bitset::new_range(position, 1)); + // self.imp().current_records.borrow().append(record_node); + // let position = self.imp().current_records.borrow().len() - 1; + // self.imp().view.select_position(position); + // self.selection_changed(gtk::Bitset::new_range(position, 1)); } pub fn replace_record(&self, position: u32, record_node: RecordNode) { - self.imp() - .current_records - .borrow() - .set(position, record_node); - self.imp().view.select_position(position); - self.selection_changed(gtk::Bitset::new_range(position, 1)); + // self.imp() + // .current_records + // .borrow() + // .set(position, record_node); + // self.imp().view.select_position(position); + // self.selection_changed(gtk::Bitset::new_range(position, 1)); } fn remove_record(&self, position: u32) { - self.imp().current_records.borrow().remove(position); - self.selection_changed(gtk::Bitset::new_empty()); + if let Some(record) = self.record_at(position) { + self.imp().file.borrow().remove(&record); + self.selection_changed(gtk::Bitset::new_range(position, 1)); + } } - fn remove_records(&self, mut positions: Vec) { - positions.sort(); - for position in positions.into_iter().rev() { - self.imp().current_records.borrow().remove(position); + fn remove_records(&self, positions: Vec) { + let record_nodes = positions + .into_iter() + .flat_map(|p| self.record_at(p)) + .collect::>(); + for record_node in record_nodes { + self.imp().file.borrow().remove(&record_node); } + self.selection_changed(gtk::Bitset::new_empty()); } fn append_records_to(&self, parent: Option<&RecordNode>, records: &[RecordNode]) { @@ -402,7 +319,7 @@ impl FilePane { fn selection_changed(&self, selected: gtk::Bitset) { let selected_record = if selected.size() == 1 { - self.get_record(selected.nth(0)) + self.imp().view.record_at(selected.nth(0)) } else { None }; @@ -421,11 +338,14 @@ impl FilePane { self.action_set_enabled(ACTION_MERGE, selected.size() > 1); } + pub fn selected_record(&self) -> Option<(u32, RecordNode)> { + let position = self.imp().view.get_selected_position()?; + let record_node = self.record_at(position)?; + Some((position, record_node)) + } + fn action_copy_name(&self) { - let Some(position) = self.imp().view.get_selected_position() else { - return; - }; - let Some(record_node) = self.get_record(position) else { + let Some((_position, record_node)) = self.selected_record() else { return; }; let Some(username) = record_node.record().username() else { @@ -436,10 +356,7 @@ impl FilePane { } fn action_copy_password(&self) { - let Some(position) = self.imp().view.get_selected_position() else { - return; - }; - let Some(record_node) = self.get_record(position) else { + let Some((_position, record_node)) = self.selected_record() else { return; }; let Some(password) = record_node.record().password() else { @@ -465,7 +382,7 @@ impl FilePane { let (positions, records): (Vec, Vec) = positions .iter_asc() .filter_map(|position| { - let record = self.get_record(position)?; + let record = self.record_at(position)?; if dest.iter().any(|p| p == record) { None } else { @@ -480,13 +397,9 @@ impl FilePane { } async fn action_edit(&self) { - let Some(position) = self.imp().view.get_selected_position() else { - return; - }; - let Some(record_node) = self.get_record(position) else { - return; - }; - self.emit_edit_record(position, &record_node); + if let Some((position, record_node)) = self.selected_record() { + self.emit_edit_record(position, &record_node); + } } async fn action_delele(&self) { @@ -518,7 +431,7 @@ impl FilePane { let (positions, records): (Vec, Vec) = positions .iter_asc() .filter_map(|position| { - let record = self.get_record(position)?; + let record = self.record_at(position)?; if record.is_group() { None } else { @@ -572,6 +485,10 @@ impl FilePane { self.emit_file_changed(); } + pub fn view(&self) -> &PSRecordView { + &self.imp().view + } + fn emit_user_notification(&self, message: &str) { self.emit_by_name::<()>(SIGNAL_USER_NOTIFICATION, &[&message]); } diff --git a/src/ui/record_view/view.rs b/src/ui/record_view/view.rs index 6890a55..95de0fc 100644 --- a/src/ui/record_view/view.rs +++ b/src/ui/record_view/view.rs @@ -1,25 +1,33 @@ use super::has_record::PSRecordViewOptions; use super::item::DropOption; -use crate::utils::ui::pending; +use crate::{ + model::tree::{RecordNode, RecordTree}, + utils::ui::pending, +}; use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*}; mod imp { use super::*; + use crate::search::item::SearchMatch; use crate::ui::record_view::item::PSRecordViewItem; + use crate::ui::search_bar::{PSSearchBar, SearchEvent, SearchEventType}; + use crate::utils::grid_layout::PSGridLayoutExt; + use crate::utils::typed_list_store::TypedListStore; use crate::utils::ui::{orphan_all_children, scrolled}; use crate::weak_map::WeakMap; use std::cell::{OnceCell, RefCell}; use std::rc::Rc; use std::sync::OnceLock; - pub const SIGNAL_GO_HOME: &str = "ps-go-home"; - pub const SIGNAL_GO_UP: &str = "ps-go-up"; pub const SIGNAL_SELECTION_CHANGED: &str = "ps-selection-changed"; pub const SIGNAL_RECORD_ACTIVATED: &str = "ps-record-activated"; pub const SIGNAL_MOVE_RECORD: &str = "ps-record-move"; pub struct PSRecordView { + pub model: RefCell, + pub search_result: TypedListStore, pub options: OnceCell, + pub search_bar: PSSearchBar, pub list_view: gtk::ListView, pub selection: gtk::MultiSelection, pub popup_model: Rc>>, @@ -34,7 +42,10 @@ mod imp { fn new() -> Self { Self { + model: Default::default(), + search_result: Default::default(), options: Default::default(), + search_bar: Default::default(), list_view: Default::default(), selection: gtk::MultiSelection::new(None::), popup_model: Default::default(), @@ -49,7 +60,7 @@ mod imp { let obj = self.obj(); obj.set_focusable(true); - obj.set_layout_manager(Some(gtk::BinLayout::new())); + obj.set_layout_manager(Some(gtk::GridLayout::new())); let factory = gtk::SignalListItemFactory::new(); factory.connect_setup(glib::clone!( @@ -78,8 +89,22 @@ mod imp { self.list_view.connect_activate(glib::clone!( #[weak] obj, - move |_list_view, position| { - obj.emit_record_activated(position); + move |list_view, position| { + if let Some(item) = list_view + .model() + .and_then(|m| m.item(position)) + .and_downcast::() + { + if item.is_expandable() { + item.set_expanded(!item.is_expanded()); + } else if let Some(record_node) = item.item().and_downcast::() { + obj.emit_record_activated(position, &record_node); + } else { + // TODO: warn unreachable + } + } else { + // TODO: warn unreachable + } } )); @@ -99,17 +124,40 @@ mod imp { move |selection, _pos, _n| obj.emit_selection_changed(&selection.selection()) )); - scrolled(&self.list_view).set_parent(&*obj); + obj.grid_attach(&self.search_bar).set_row(0); + obj.grid_attach(&scrolled(&self.list_view)).set_row(1); + + self.search_bar.connect_search(glib::clone!( + #[weak(rename_to = imp)] + self, + move |event| { + glib::MainContext::default().spawn_local(async move { + imp.search(&event).await; + }); + } + )); + // self.search_bar.connect_configure(glib::clone!( + // #[weak(rename_to = imp)] + // self, + // move |search_config| { + // imp.config_service.get().unwrap().update(|config| { + // config.search_in_secrets = search_config.search_in_secrets; + // }); + // } + // )); + self.search_bar.connect_search_closed(glib::clone!( + #[weak] + obj, + move || obj.search_reset() + )); } fn signals() -> &'static [glib::subclass::Signal] { static SIGNALS: OnceLock> = OnceLock::new(); SIGNALS.get_or_init(|| { vec![ - glib::subclass::Signal::builder(SIGNAL_GO_HOME).build(), - glib::subclass::Signal::builder(SIGNAL_GO_UP).build(), glib::subclass::Signal::builder(SIGNAL_RECORD_ACTIVATED) - .param_types([u32::static_type()]) + .param_types([u32::static_type(), RecordNode::static_type()]) .build(), glib::subclass::Signal::builder(SIGNAL_SELECTION_CHANGED) .param_types([gtk::Bitset::static_type()]) @@ -138,6 +186,69 @@ mod imp { let position = model.iter().position(|r| r.as_ref() == Ok(object))?; position.try_into().ok() } + + fn find_selection_in_search_result(&self) -> Option { + self.obj() + .get_selected_position() + .and_then(|position| self.obj().record_at(position)) + .and_then(|record_node| { + self.search_result + .iter() + .position(|sm| *sm.record() == record_node) + }) + } + + async fn search(&self, event: &SearchEvent) { + if event.query.is_empty() { + return; + } + + fn traverse( + records: &TypedListStore, + path: TypedListStore, + query: &str, + search_in_secrets: bool, + result: &TypedListStore, + ) { + for record in records { + if record.record().has_text(query, search_in_secrets) { + result.append(&SearchMatch::new(&record, &path.clone_list())); + } + if let Some(children) = record.children() { + let path_to_record = path.appended(record.clone()); + traverse(children, path_to_record, query, search_in_secrets, result); + } + } + } + + let search_match_index = match event.event_type { + SearchEventType::Change => { + self.search_result.remove_all(); + traverse( + &self.model.borrow().records, + Default::default(), + &event.query, + event.search_in_secrets, + &self.search_result, + ); + Some(0) + } + SearchEventType::Next => self + .find_selection_in_search_result() + .map(|index| (index as u32 + 1).min(self.search_result.len())), + SearchEventType::Prev => self + .find_selection_in_search_result() + .map(|index| ((index as u32).saturating_sub(1))), + }; + let Some(search_match) = + search_match_index.and_then(|index| self.search_result.get(index)) + else { + return; + }; + + self.file_pane.expand_path(search_match.path()); + self.file_pane.select_record(search_match.record()); + } } impl PSRecordView { @@ -145,7 +256,10 @@ mod imp { let Some(list_item) = item.downcast_ref::() else { return; }; + let child = PSRecordViewItem::new(*self.options.get().unwrap()); + let expander = gtk::TreeExpander::builder().child(&child).build(); + let popup_model = self.popup_model.clone(); child.connect_context_menu(move |_record| popup_model.borrow().clone()); let obj = self.obj(); @@ -154,17 +268,24 @@ mod imp { obj, move |_, src, dst, opt| obj.emit_move_record(src, dst, opt) )); - list_item.set_child(Some(&child)); + list_item.set_child(Some(&expander)); } fn bind_item(&self, item: &glib::Object) { let Some(list_item) = item.downcast_ref::() else { return; }; - let Some(child) = list_item.child().and_downcast::() else { + let Some(expander) = list_item.child().and_downcast::() else { + return; + }; + let Some(child) = expander.child().and_downcast::() else { + return; + }; + let Some(tree_list_row) = list_item.item().and_downcast::() else { return; }; - let Some(record_node) = list_item.item() else { + expander.set_list_row(Some(&tree_list_row)); + let Some(record_node) = tree_list_row.item() else { return; }; let position = list_item.position(); @@ -177,7 +298,11 @@ mod imp { let Some(list_item) = item.downcast_ref::() else { return; }; - let Some(child) = list_item.child().and_downcast::() else { + let Some(expander) = list_item.child().and_downcast::() else { + return; + }; + expander.set_list_row(None); + let Some(child) = expander.child().and_downcast::() else { return; }; self.mapping.remove_key(list_item.position()); @@ -189,7 +314,10 @@ mod imp { let Some(list_item) = item.downcast_ref::() else { return; }; - if let Some(child) = list_item.child().and_downcast::() { + let Some(expander) = list_item.child().and_downcast::() else { + return; + }; + if let Some(child) = expander.child().and_downcast::() { child.set_record_node(None); self.mapping.remove_value(&child); } @@ -210,8 +338,17 @@ impl PSRecordView { obj } - pub fn set_model(&self, model: &gio::ListModel) { - self.imp().selection.set_model(Some(model)); + pub fn set_model(&self, model: &RecordTree) { + *self.imp().model.borrow_mut() = model.clone(); + self.imp().search_result.remove_all(); + + let tree_model = + gtk::TreeListModel::new(model.records.untyped().clone(), false, false, |node| { + let r = node.downcast_ref::()?; + let children = r.children()?; + Some(children.untyped().clone().upcast()) + }); + self.imp().selection.set_model(Some(&tree_model)); } pub fn get_selected_positions(&self) -> gtk::Bitset { @@ -227,6 +364,13 @@ impl PSRecordView { Some(pos) } + pub fn record_at(&self, position: u32) -> Option { + let model = self.imp().selection.model()?; + let tree_list_row = model.item(position).and_downcast::()?; + let record = tree_list_row.item().and_downcast::()?; + Some(record) + } + pub fn set_popup(&self, popup_model: &gio::MenuModel) { *self.imp().popup_model.borrow_mut() = Some(popup_model.clone()); } @@ -269,58 +413,29 @@ impl PSRecordView { fn on_key_press(&self, key: gdk::Key, modifier: gdk::ModifierType) -> glib::Propagation { match (modifier, key) { - (gdk::ModifierType::ALT_MASK, gdk::Key::Home) => { - self.emit_go_home(); - glib::Propagation::Stop - } - (gdk::ModifierType::ALT_MASK, gdk::Key::Up) => { - self.emit_go_up(); - glib::Propagation::Stop - } - (gdk::ModifierType::ALT_MASK, gdk::Key::Down) => { - if let Some(position) = self.get_selected_position() { - self.emit_record_activated(position); - glib::Propagation::Stop - } else { - glib::Propagation::Proceed - } - } + (gdk::ModifierType::ALT_MASK, gdk::Key::Down) => self + .get_selected_position() + .and_then(|position| { + let record = self.record_at(position)?; + self.emit_record_activated(position, &record); + Some(glib::Propagation::Stop) + }) + .unwrap_or(glib::Propagation::Proceed), _ => glib::Propagation::Proceed, } } -} - -impl PSRecordView { - fn emit_go_home(&self) { - self.emit_by_name::<()>(imp::SIGNAL_GO_HOME, &[]); - } - pub fn connect_go_home(&self, f: F) -> glib::signal::SignalHandlerId - where - F: Fn() + 'static, - { - self.connect_closure( - imp::SIGNAL_GO_HOME, - false, - glib::closure_local!(move |_self: &Self| (f)()), - ) - } - - fn emit_go_up(&self) { - self.emit_by_name::<()>(imp::SIGNAL_GO_UP, &[]); + pub fn search_start(&self) { + self.imp().search_bar.set_search_mode(true); } - pub fn connect_go_up(&self, f: F) -> glib::signal::SignalHandlerId - where - F: Fn() + 'static, - { - self.connect_closure( - imp::SIGNAL_GO_UP, - false, - glib::closure_local!(move |_self: &Self| (f)()), - ) + pub fn search_reset(&self) { + self.imp().search_bar.set_search_mode(false); + self.imp().search_bar.reset(); } +} +impl PSRecordView { fn emit_selection_changed(&self, selection: >k::Bitset) { self.emit_by_name::<()>(imp::SIGNAL_SELECTION_CHANGED, &[selection]); } @@ -336,18 +451,21 @@ impl PSRecordView { ) } - fn emit_record_activated(&self, position: u32) { - self.emit_by_name::<()>(imp::SIGNAL_RECORD_ACTIVATED, &[&position]); + fn emit_record_activated(&self, position: u32, record_node: &RecordNode) { + self.emit_by_name::<()>(imp::SIGNAL_RECORD_ACTIVATED, &[&position, record_node]); } pub fn connect_record_activated(&self, f: F) -> glib::signal::SignalHandlerId where - F: Fn(u32) + 'static, + F: Fn(u32, RecordNode) + 'static, { self.connect_closure( imp::SIGNAL_RECORD_ACTIVATED, false, - glib::closure_local!(move |_self: &Self, position| (f)(position)), + glib::closure_local!(move |_self: &Self, position, record_node| (f)( + position, + record_node + )), ) } diff --git a/src/ui/search_pane.rs b/src/ui/search_pane.rs index 1d72643..2df3a27 100644 --- a/src/ui/search_pane.rs +++ b/src/ui/search_pane.rs @@ -96,19 +96,13 @@ mod imp { self.view.connect_record_activated(glib::clone!( #[weak(rename_to = imp)] self, - move |position| { + move |position, _| { glib::MainContext::default().spawn_local(async move { imp.row_activated(position).await; }); } )); - self.view.connect_go_home(glib::clone!( - #[weak] - obj, - move || obj.emit_go_home() - )); - let shortcuts = gtk::ShortcutController::new(); shortcuts.add_shortcut( gtk::Shortcut::builder() @@ -155,7 +149,7 @@ mod imp { impl SearchPane { pub fn set_view_model(&self, model: &TypedListStore) { *self.current_records.borrow_mut() = model.clone(); - self.view.set_model(model.untyped().upcast_ref()); + // self.view.set_model(model.untyped().upcast_ref()); } async fn row_activated(&self, position: u32) {