editor: implement tracks editor parts list

This commit is contained in:
Elias Projahn 2025-02-16 08:46:40 +01:00
parent 642d9340e5
commit 53680df13d
15 changed files with 311 additions and 32 deletions

View file

@ -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);
}

View file

@ -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<F: Fn(&Self, Instrument) + 'static>(&self, f: F) -> glib::SignalHandlerId {
pub fn connect_created<F: Fn(&Self, Instrument) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("created", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let instrument = values[1].get::<Instrument>().unwrap();

View file

@ -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;

View file

@ -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));
}

View file

@ -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());

View file

@ -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<Vec<Work>>,
pub parts_filtered: RefCell<Vec<Work>>,
#[template_child]
pub search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub list_box: TemplateChild<gtk::ListBox>,
}
#[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<Self>) {
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<Vec<Signal>> = 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<imp::TracksEditorPartsPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl TracksEditorPartsPopover {
pub fn new(parts: Vec<Work>) -> Self {
let obj: Self = glib::Object::new();
obj.imp().parts.set(parts).unwrap();
obj.search("");
obj
}
pub fn connect_part_selected<F: Fn(&Self, Work) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("part-selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let role = values[1].get::<Work>().unwrap();
f(&obj, role);
None
})
}
#[template_callback]
fn search_changed(&self, entry: &gtk::SearchEntry) {
self.search(&entry.text());
}
#[template_callback]
fn activate(&self, _: &gtk::SearchEntry) {
if let Some(work) = self.imp().parts_filtered.borrow().first() {
self.select(work.clone());
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::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::<Vec<Work>>();
imp.list_box.remove_all();
for part in &parts_filtered {
let row = MusicusActivatableRow::new(
&gtk::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();
}
}

View file

@ -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<Recording>,
pub track_data: RefCell<TracksEditorTrackData>,
pub parts_popover: OnceCell<TracksEditorPartsPopover>,
#[template_child]
pub select_parts_box: TemplateChild<gtk::Box>,
#[template_child]
pub reset_button: TemplateChild<gtk::Button>,
}
#[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")