diff --git a/data/ui/tracks_editor_parts_popover.blp b/data/ui/tracks_editor_parts_popover.blp new file mode 100644 index 0000000..74a0815 --- /dev/null +++ b/data/ui/tracks_editor_parts_popover.blp @@ -0,0 +1,35 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusTracksEditorPartsPopover: Gtk.Popover { + styles [ + "selector" + ] + + Adw.ToolbarView { + [top] + Gtk.SearchEntry search_entry { + placeholder-text: _("Search parts…"); + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 6; + search-changed => $search_changed() swapped; + activate => $activate() swapped; + stop-search => $stop_search() swapped; + } + + Gtk.ScrolledWindow scrolled_window { + height-request: 200; + + Gtk.ListBox list_box { + styles [ + "selector-list" + ] + + selection-mode: none; + activate-on-single-click: true; + } + } + } +} diff --git a/data/ui/tracks_editor_track_row.blp b/data/ui/tracks_editor_track_row.blp index 676458a..6426eca 100644 --- a/data/ui/tracks_editor_track_row.blp +++ b/data/ui/tracks_editor_track_row.blp @@ -13,8 +13,21 @@ template $MusicusTracksEditorTrackRow: Adw.ActionRow { } } + Gtk.Button reset_button { + icon-name: "edit-clear-symbolic"; + tooltip-text: _("Clear selected work parts"); + visible: false; + valign: center; + clicked => $reset() swapped; + + styles [ + "flat" + ] + } + Gtk.Button { icon-name: "user-trash-symbolic"; + tooltip-text: _("Remove this track"); valign: center; clicked => $remove() swapped; diff --git a/src/db/models.rs b/src/db/models.rs index 060e58d..6bbabfc 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -149,14 +149,20 @@ impl Work { }) } - pub fn composers_string(&self) -> String { + pub fn composers_string(&self) -> Option { // TODO: Include roles except default composer. - // TODO: Think about works without composers. - self.persons + let composers_string = self + .persons .iter() .map(|p| p.person.name.get().to_string()) .collect::>() - .join(", ") + .join(", "); + + if composers_string.is_empty() { + None + } else { + Some(composers_string) + } } } @@ -169,8 +175,11 @@ impl PartialEq for Work { impl Display for Work { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // TODO: Handle works without composers. - write!(f, "{}: {}", self.composers_string(), self.name) + if let Some(composers) = self.composers_string() { + write!(f, "{}: {}", composers, self.name) + } else { + write!(f, "{}", self.name) + } } } diff --git a/src/editor/ensemble_editor.rs b/src/editor/ensemble_editor.rs index db28df4..62e570c 100644 --- a/src/editor/ensemble_editor.rs +++ b/src/editor/ensemble_editor.rs @@ -79,7 +79,10 @@ impl MusicusEnsembleEditor { if let Some(ensemble) = ensemble { obj.imp().save_row.set_title(&gettext("Save changes")); - obj.imp().ensemble_id.set(ensemble.ensemble_id.clone()).unwrap(); + obj.imp() + .ensemble_id + .set(ensemble.ensemble_id.clone()) + .unwrap(); obj.imp().name_editor.set_translation(&ensemble.name); } diff --git a/src/editor/instrument_editor.rs b/src/editor/instrument_editor.rs index 1a144d7..5af5c7f 100644 --- a/src/editor/instrument_editor.rs +++ b/src/editor/instrument_editor.rs @@ -79,14 +79,20 @@ impl MusicusInstrumentEditor { if let Some(instrument) = instrument { obj.imp().save_row.set_title(&gettext("Save changes")); - obj.imp().instrument_id.set(instrument.instrument_id.clone()).unwrap(); + obj.imp() + .instrument_id + .set(instrument.instrument_id.clone()) + .unwrap(); obj.imp().name_editor.set_translation(&instrument.name); } obj } - pub fn connect_created(&self, f: F) -> glib::SignalHandlerId { + pub fn connect_created( + &self, + f: F, + ) -> glib::SignalHandlerId { self.connect_local("created", true, move |values| { let obj = values[0].get::().unwrap(); let instrument = values[1].get::().unwrap(); diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 563abf1..2533e05 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -13,6 +13,7 @@ pub mod recording_selector_popover; pub mod role_editor; pub mod role_selector_popover; pub mod tracks_editor; +pub mod tracks_editor_parts_popover; pub mod tracks_editor_track_row; pub mod translation_editor; pub mod translation_entry; diff --git a/src/editor/recording_editor.rs b/src/editor/recording_editor.rs index 34e556c..451787c 100644 --- a/src/editor/recording_editor.rs +++ b/src/editor/recording_editor.rs @@ -248,7 +248,11 @@ impl MusicusRecordingEditor { fn set_work(&self, work: Work) { self.imp().work_row.set_title(&work.name.get()); - self.imp().work_row.set_subtitle(&work.composers_string()); + self.imp().work_row.set_subtitle( + &work + .composers_string() + .unwrap_or_else(|| gettext("No composers")), + ); self.imp().work.replace(Some(work)); } diff --git a/src/editor/tracks_editor.rs b/src/editor/tracks_editor.rs index 969272c..ae7d47a 100644 --- a/src/editor/tracks_editor.rs +++ b/src/editor/tracks_editor.rs @@ -181,12 +181,9 @@ impl TracksEditor { } fn set_recording(&self, recording: Recording) { - self.imp().recording_row.set_title(&format!( - "{}: {}", - recording.work.composers_string(), - recording.work.name.get(), - )); - + self.imp() + .recording_row + .set_title(&recording.work.to_string()); self.imp() .recording_row .set_subtitle(&recording.performers_string()); diff --git a/src/editor/tracks_editor_parts_popover.rs b/src/editor/tracks_editor_parts_popover.rs new file mode 100644 index 0000000..2f6fddd --- /dev/null +++ b/src/editor/tracks_editor_parts_popover.rs @@ -0,0 +1,171 @@ +use super::activatable_row::MusicusActivatableRow; +use crate::db::models::Work; + +use gtk::{ + glib::{self, subclass::Signal}, + prelude::*, + subclass::prelude::*, +}; +use once_cell::sync::Lazy; + +use std::cell::{OnceCell, RefCell}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/tracks_editor_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: &super::TracksEditorPartsPopover| { + 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 = MusicusActivatableRow::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 |_: &MusicusActivatableRow| { + 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(); + } +} diff --git a/src/editor/tracks_editor_track_row.rs b/src/editor/tracks_editor_track_row.rs index 26d0f1a..2a24d2b 100644 --- a/src/editor/tracks_editor_track_row.rs +++ b/src/editor/tracks_editor_track_row.rs @@ -1,5 +1,6 @@ use crate::{ db::models::{Recording, Work}, + editor::tracks_editor_parts_popover::TracksEditorPartsPopover, library::MusicusLibrary, }; @@ -29,8 +30,12 @@ mod imp { pub recording: OnceCell, pub track_data: RefCell, + pub parts_popover: OnceCell, + #[template_child] pub select_parts_box: TemplateChild, + #[template_child] + pub reset_button: TemplateChild, } #[glib::object_subclass] @@ -101,9 +106,23 @@ impl TracksEditorTrackRow { } }); + let parts_popover = TracksEditorPartsPopover::new(recording.work.parts.clone()); + + parts_popover.connect_part_selected(clone!( + #[weak] + obj, + move |_, part| { + obj.imp().track_data.borrow_mut().parts.push(part); + obj.parts_updated(); + } + )); + + obj.imp().select_parts_box.append(&parts_popover); + obj.imp().parts_popover.set(parts_popover).unwrap(); + obj.imp().recording.set(recording).unwrap(); obj.imp().track_data.replace(track_data); - obj.update_title(); + obj.parts_updated(); obj } @@ -122,7 +141,13 @@ impl TracksEditorTrackRow { #[template_callback] fn select_parts(&self) { - // self.imp().parts_popover.get().unwrap().popup(); + self.imp().parts_popover.get().unwrap().popup(); + } + + #[template_callback] + fn reset(&self) { + self.imp().track_data.borrow_mut().parts.clear(); + self.parts_updated(); } #[template_callback] @@ -130,9 +155,11 @@ impl TracksEditorTrackRow { self.emit_by_name::<()>("remove", &[]); } - fn update_title(&self) { + fn parts_updated(&self) { let parts = &self.imp().track_data.borrow().parts; + self.imp().reset_button.set_visible(!parts.is_empty()); + self.set_title(&if parts.is_empty() { if self.imp().recording.get().unwrap().work.parts.is_empty() { gettext("Whole work") diff --git a/src/home_page.rs b/src/home_page.rs index 6d7538e..b34162e 100644 --- a/src/home_page.rs +++ b/src/home_page.rs @@ -301,8 +301,12 @@ impl MusicusHomePage { } Tag::Work(work) => { imp.title_label.set_text(&work.name.get()); - imp.subtitle_label.set_text(&work.composers_string()); - imp.subtitle_label.set_visible(true); + if let Some(composers) = work.composers_string() { + imp.subtitle_label.set_text(&composers); + imp.subtitle_label.set_visible(true); + } else { + imp.subtitle_label.set_visible(false); + } } } diff --git a/src/player.rs b/src/player.rs index ad57d70..ad28f86 100644 --- a/src/player.rs +++ b/src/player.rs @@ -222,7 +222,7 @@ impl MusicusPlayer { if tracks.len() == 1 { items.push(PlaylistItem::new( true, - Some(&recording.work.composers_string()), + recording.work.composers_string(), &recording.work.name.get(), Some(&performances), None, @@ -250,7 +250,7 @@ impl MusicusPlayer { items.push(PlaylistItem::new( true, - Some(&recording.work.composers_string()), + recording.work.composers_string(), &recording.work.name.get(), Some(&performances), Some(&track_title(&first_track, 1)), @@ -261,7 +261,7 @@ impl MusicusPlayer { for (index, track) in tracks.enumerate() { items.push(PlaylistItem::new( false, - Some(&recording.work.composers_string()), + recording.work.composers_string(), &recording.work.name.get(), Some(&performances), // track number = track index + 1 (first track) + 1 (zero based) diff --git a/src/playlist_item.rs b/src/playlist_item.rs index db0e160..9cd43e7 100644 --- a/src/playlist_item.rs +++ b/src/playlist_item.rs @@ -52,7 +52,7 @@ glib::wrapper! { impl PlaylistItem { pub fn new( is_title: bool, - composers: Option<&str>, + composers: Option, work: &str, performers: Option<&str>, part_title: Option<&str>, diff --git a/src/recording_tile.rs b/src/recording_tile.rs index 32dd290..64f2aaa 100644 --- a/src/recording_tile.rs +++ b/src/recording_tile.rs @@ -1,11 +1,12 @@ -use gtk::{gio, glib, prelude::*, subclass::prelude::*}; -use std::cell::OnceCell; - use crate::{ db::models::Recording, editor::recording_editor::MusicusRecordingEditor, library::MusicusLibrary, }; +use gettextrs::gettext; +use gtk::{gio, glib, prelude::*, subclass::prelude::*}; +use std::cell::OnceCell; + mod imp { use super::*; @@ -83,8 +84,12 @@ impl MusicusRecordingTile { let imp = obj.imp(); imp.work_label.set_label(&recording.work.name.get()); - imp.composer_label - .set_label(&recording.work.composers_string()); + imp.composer_label.set_label( + &recording + .work + .composers_string() + .unwrap_or_else(|| gettext("No composers")), + ); imp.performances_label .set_label(&recording.performers_string()); diff --git a/src/tag_tile.rs b/src/tag_tile.rs index 5cd3f45..68a4caa 100644 --- a/src/tag_tile.rs +++ b/src/tag_tile.rs @@ -55,8 +55,12 @@ impl MusicusTagTile { } Tag::Work(work) => { imp.title_label.set_label(work.name.get()); - imp.subtitle_label.set_label(&work.composers_string()); - imp.subtitle_label.set_visible(true); + if let Some(composers) = work.composers_string() { + imp.subtitle_label.set_label(&composers); + imp.subtitle_label.set_visible(true); + } else { + imp.subtitle_label.set_visible(false); + } } }