use crate::{ library::LibraryQuery, search_tag::{MusicusSearchTag, Tag}, }; use adw::{gdk, gio, glib, glib::clone, glib::subclass::Signal, prelude::*, subclass::prelude::*}; use once_cell::sync::Lazy; use std::{cell::RefCell, time::Duration}; mod imp { use super::*; #[derive(Debug, Default, gtk::CompositeTemplate)] #[template(file = "data/ui/search_entry.blp")] pub struct MusicusSearchEntry { #[template_child] pub tags_box: TemplateChild, #[template_child] pub text: TemplateChild, #[template_child] pub clear_icon: TemplateChild, pub tags: RefCell>, pub query_changed: RefCell>, } #[glib::object_subclass] impl ObjectSubclass for MusicusSearchEntry { const NAME: &'static str = "MusicusSearchEntry"; type Type = super::MusicusSearchEntry; type ParentType = gtk::Box; fn class_init(klass: &mut Self::Class) { klass.bind_template(); klass.bind_template_instance_callbacks(); klass.set_css_name("entry"); klass.add_shortcut( >k::Shortcut::builder() .trigger(>k::KeyvalTrigger::new( gdk::Key::Escape, gdk::ModifierType::empty(), )) .action(>k::CallbackAction::new(|widget, _| match widget .downcast_ref::( ) { Some(obj) => { obj.reset(); true } None => false, })) .build(), ); } fn instance_init(obj: &glib::subclass::InitializingObject) { obj.init_template(); } } impl ObjectImpl for MusicusSearchEntry { fn constructed(&self) { let controller = gtk::GestureClick::new(); controller.connect_pressed(|gesture, _, _, _| { gesture.set_state(gtk::EventSequenceState::Claimed); }); controller.connect_released(clone!(@weak self as _self => move |_, _, _, _| { _self.obj().reset(); })); self.clear_icon.add_controller(controller); } fn signals() -> &'static [Signal] { static SIGNALS: Lazy> = Lazy::new(|| { vec![ Signal::builder("activate").build(), Signal::builder("query-changed").build(), ] }); SIGNALS.as_ref() } } impl WidgetImpl for MusicusSearchEntry { fn grab_focus(&self) -> bool { self.text.grab_focus_without_selecting() } } impl BoxImpl for MusicusSearchEntry {} } glib::wrapper! { pub struct MusicusSearchEntry(ObjectSubclass) @extends gtk::Widget; } #[gtk::template_callbacks] impl MusicusSearchEntry { pub fn new() -> Self { glib::Object::new() } pub fn connect_query_changed(&self, f: F) -> glib::SignalHandlerId { self.connect_local("query-changed", true, move |values| { let obj = values[0].get::().unwrap(); f(&obj); None }) } pub fn set_key_capture_widget(&self, widget: &impl IsA) { let controller = gtk::EventControllerKey::new(); controller.connect_key_pressed(clone!(@weak self as _self => @default-return glib::Propagation::Proceed, move |controller, _, _, _| { match controller.forward(&_self.imp().text.get()) { true => { _self.grab_focus(); glib::Propagation::Stop }, false => glib::Propagation::Proceed, } })); controller.connect_key_released(clone!(@weak self as _self => move |controller, _, _, _| { controller.forward(&_self.imp().text.get()); })); widget.add_controller(controller); } pub fn reset(&self) { let mut tags = self.imp().tags.borrow_mut(); while let Some(tag) = tags.pop() { self.imp().tags_box.remove(&tag); } self.imp().text.set_text(""); } pub fn add_tag(&self, tag: Tag) { self.imp().text.set_text(""); let tag = MusicusSearchTag::new(tag); self.imp().tags_box.append(&tag); self.imp().tags.borrow_mut().push(tag); } pub fn query(&self) -> LibraryQuery { let mut query = LibraryQuery { search: self.imp().text.text().to_string(), ..Default::default() }; for tag in &*self.imp().tags.borrow() { match tag.tag().clone() { Tag::Person(person) => query.person = Some(person), Tag::Ensemble(ensemble) => query.ensemble = Some(ensemble), Tag::Work(work) => query.work = Some(work), } } query } #[template_callback] fn activate(&self, _: >k::Text) { self.emit_by_name::<()>("activate", &[]); } #[template_callback] fn backspace(&self, text: >k::Text) { if text.cursor_position() == 0 { let changed = if let Some(tag) = self.imp().tags.borrow_mut().pop() { self.imp().tags_box.remove(&tag); true } else { false }; if changed { self.emit_by_name::<()>("query-changed", &[]); } } } #[template_callback] async fn text_changed(&self, text: >k::Text) { self.imp().clear_icon.set_visible(!text.text().is_empty()); if let Some(cancellable) = self.imp().query_changed.borrow_mut().take() { cancellable.cancel(); } let cancellable = gio::Cancellable::new(); self.imp().query_changed.replace(Some(cancellable.clone())); let _ = gio::CancellableFuture::new( async { glib::timeout_future(Duration::from_millis(150)).await; self.emit_by_name::<()>("query-changed", &[]); }, cancellable, ) .await; } }