use std::cell::{OnceCell, RefCell}; use gtk::{ glib::{self, subclass::Signal}, prelude::*, subclass::prelude::*, }; use once_cell::sync::Lazy; use crate::{db::models::Work, util::activatable_row::ActivatableRow}; mod imp { use super::*; #[derive(Debug, Default, gtk::CompositeTemplate)] #[template(file = "data/ui/editor/tracks/parts_popover.blp")] pub struct TracksEditorPartsPopover { pub parts: OnceCell>, pub parts_filtered: RefCell>, #[template_child] pub search_entry: TemplateChild, #[template_child] pub scrolled_window: TemplateChild, #[template_child] pub list_box: TemplateChild, } #[glib::object_subclass] impl ObjectSubclass for TracksEditorPartsPopover { const NAME: &'static str = "MusicusTracksEditorPartsPopover"; type Type = super::TracksEditorPartsPopover; type ParentType = gtk::Popover; fn class_init(klass: &mut Self::Class) { klass.bind_template(); klass.bind_template_instance_callbacks(); } fn instance_init(obj: &glib::subclass::InitializingObject) { obj.init_template(); } } impl ObjectImpl for TracksEditorPartsPopover { fn constructed(&self) { self.parent_constructed(); self.obj().connect_visible_notify(|obj| { if obj.is_visible() { obj.imp().search_entry.set_text(""); obj.imp().search_entry.grab_focus(); obj.imp().scrolled_window.vadjustment().set_value(0.0); } }); } fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![ Signal::builder("part-selected") .param_types([Work::static_type()]) .build(), Signal::builder("create").build(), ] }); SIGNALS.as_ref() } } impl WidgetImpl for TracksEditorPartsPopover { // TODO: Fix focus. fn focus(&self, direction_type: gtk::DirectionType) -> bool { if direction_type == gtk::DirectionType::Down { self.list_box.child_focus(direction_type) } else { self.parent_focus(direction_type) } } } impl PopoverImpl for TracksEditorPartsPopover {} } glib::wrapper! { pub struct TracksEditorPartsPopover(ObjectSubclass) @extends gtk::Widget, gtk::Popover; } #[gtk::template_callbacks] impl TracksEditorPartsPopover { pub fn new(parts: Vec) -> Self { let obj: Self = glib::Object::new(); obj.imp().parts.set(parts).unwrap(); obj.search(""); obj } pub fn connect_part_selected( &self, f: F, ) -> glib::SignalHandlerId { self.connect_local("part-selected", true, move |values| { let obj = values[0].get::().unwrap(); let role = values[1].get::().unwrap(); f(&obj, role); None }) } #[template_callback] fn search_changed(&self, entry: >k::SearchEntry) { self.search(&entry.text()); } #[template_callback] fn activate(&self, _: >k::SearchEntry) { if let Some(work) = self.imp().parts_filtered.borrow().first() { self.select(work.clone()); } } #[template_callback] fn stop_search(&self, _: >k::SearchEntry) { self.popdown(); } fn search(&self, search: &str) { let imp = self.imp(); let parts_filtered = imp .parts .get() .unwrap() .iter() .filter(|p| p.name.get().to_lowercase().contains(&search.to_lowercase())) .cloned() .collect::>(); imp.list_box.remove_all(); for part in &parts_filtered { let row = ActivatableRow::new( >k::Label::builder() .label(part.to_string()) .halign(gtk::Align::Start) .build(), ); row.set_tooltip_text(Some(&part.to_string())); let part = part.clone(); let obj = self.clone(); row.connect_activated(move |_: &ActivatableRow| { obj.select(part.clone()); }); imp.list_box.append(&row); } imp.parts_filtered.replace(parts_filtered); } fn select(&self, part: Work) { self.emit_by_name::<()>("part-selected", &[&part]); self.popdown(); } }