Add work and recording editor

This commit is contained in:
Elias Projahn 2025-01-15 11:23:04 +01:00
parent 36b2f1097e
commit 364557d959
30 changed files with 3308 additions and 418 deletions

View file

@ -0,0 +1,112 @@
use std::cell::OnceCell;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::glib::{self, subclass::Signal};
use once_cell::sync::Lazy;
use crate::{
db::models::Ensemble, editor::translation_editor::MusicusTranslationEditor,
library::MusicusLibrary,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/ensemble_editor.blp")]
pub struct MusicusEnsembleEditor {
pub navigation: OnceCell<adw::NavigationView>,
pub library: OnceCell<MusicusLibrary>,
pub ensemble_id: OnceCell<String>,
#[template_child]
pub name_editor: TemplateChild<MusicusTranslationEditor>,
#[template_child]
pub save_button: TemplateChild<gtk::Button>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusEnsembleEditor {
const NAME: &'static str = "MusicusEnsembleEditor";
type Type = super::MusicusEnsembleEditor;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
MusicusTranslationEditor::static_type();
klass.bind_template();
klass.bind_template_instance_callbacks();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MusicusEnsembleEditor {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("created")
.param_types([Ensemble::static_type()])
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusEnsembleEditor {}
impl NavigationPageImpl for MusicusEnsembleEditor {}
}
glib::wrapper! {
pub struct MusicusEnsembleEditor(ObjectSubclass<imp::MusicusEnsembleEditor>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl MusicusEnsembleEditor {
pub fn new(
navigation: &adw::NavigationView,
library: &MusicusLibrary,
ensemble: Option<&Ensemble>,
) -> Self {
let obj: Self = glib::Object::new();
obj.imp().navigation.set(navigation.to_owned()).unwrap();
obj.imp().library.set(library.to_owned()).unwrap();
if let Some(ensemble) = ensemble {
obj.imp().save_button.set_label(&gettext("Save changes"));
obj.imp().ensemble_id.set(ensemble.ensemble_id.clone()).unwrap();
obj.imp().name_editor.set_translation(&ensemble.name);
}
obj
}
pub fn connect_created<F: Fn(&Self, Ensemble) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("created", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let ensemble = values[1].get::<Ensemble>().unwrap();
f(&obj, ensemble);
None
})
}
#[template_callback]
fn save(&self, _: &gtk::Button) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
if let Some(ensemble_id) = self.imp().ensemble_id.get() {
library.update_ensemble(ensemble_id, name).unwrap();
} else {
let ensemble = library.create_ensemble(name).unwrap();
self.emit_by_name::<()>("created", &[&ensemble]);
}
self.imp().navigation.get().unwrap().pop();
}
}

View file

@ -0,0 +1,204 @@
use crate::{db::models::Ensemble, library::MusicusLibrary};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use std::cell::{OnceCell, RefCell};
use super::activatable_row::MusicusActivatableRow;
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::MusicusEnsembleSelectorPopover)]
#[template(file = "data/ui/ensemble_selector_popover.blp")]
pub struct MusicusEnsembleSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub ensembles: RefCell<Vec<Ensemble>>,
#[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 MusicusEnsembleSelectorPopover {
const NAME: &'static str = "MusicusEnsembleSelectorPopover";
type Type = super::MusicusEnsembleSelectorPopover;
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();
}
}
#[glib::derived_properties]
impl ObjectImpl for MusicusEnsembleSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj()
.connect_visible_notify(|obj: &super::MusicusEnsembleSelectorPopover| {
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);
}
});
self.obj().search("");
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("ensemble-selected")
.param_types([Ensemble::static_type()])
.build(),
Signal::builder("create").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusEnsembleSelectorPopover {
// 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 MusicusEnsembleSelectorPopover {}
}
glib::wrapper! {
pub struct MusicusEnsembleSelectorPopover(ObjectSubclass<imp::MusicusEnsembleSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl MusicusEnsembleSelectorPopover {
pub fn new(library: &MusicusLibrary) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_ensemble_selected<F: Fn(&Self, Ensemble) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("ensemble-selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let ensemble = values[1].get::<Ensemble>().unwrap();
f(&obj, ensemble);
None
})
}
pub fn connect_create<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("create", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
#[template_callback]
fn search_changed(&self, entry: &gtk::SearchEntry) {
self.search(&entry.text());
}
#[template_callback]
fn activate(&self, _: &gtk::SearchEntry) {
if let Some(ensemble) = self.imp().ensembles.borrow().first() {
self.select(ensemble.clone());
} else {
self.create();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::SearchEntry) {
self.popdown();
}
fn search(&self, search: &str) {
let imp = self.imp();
let ensembles = imp
.library
.get()
.unwrap()
.search_ensembles(search)
.unwrap();
imp.list_box.remove_all();
for ensemble in &ensembles {
let row = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(ensemble.to_string())
.halign(gtk::Align::Start)
.build(),
);
let ensemble = ensemble.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
obj.select(ensemble.clone());
});
imp.list_box.append(&row);
}
let create_box = gtk::Box::builder().spacing(12).build();
create_box.append(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::Label::builder()
.label(gettext("Create new ensemble"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
obj.create();
});
imp.list_box.append(&create_row);
imp.ensembles.replace(ensembles);
}
fn select(&self, ensemble: Ensemble) {
self.emit_by_name::<()>("ensemble-selected", &[&ensemble]);
self.popdown();
}
fn create(&self) {
self.emit_by_name::<()>("create", &[]);
self.popdown();
}
}

View file

@ -0,0 +1,112 @@
use std::cell::OnceCell;
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::glib::{self, subclass::Signal};
use once_cell::sync::Lazy;
use crate::{
db::models::Instrument, editor::translation_editor::MusicusTranslationEditor,
library::MusicusLibrary,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/instrument_editor.blp")]
pub struct MusicusInstrumentEditor {
pub navigation: OnceCell<adw::NavigationView>,
pub library: OnceCell<MusicusLibrary>,
pub instrument_id: OnceCell<String>,
#[template_child]
pub name_editor: TemplateChild<MusicusTranslationEditor>,
#[template_child]
pub save_button: TemplateChild<gtk::Button>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusInstrumentEditor {
const NAME: &'static str = "MusicusInstrumentEditor";
type Type = super::MusicusInstrumentEditor;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
MusicusTranslationEditor::static_type();
klass.bind_template();
klass.bind_template_instance_callbacks();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for MusicusInstrumentEditor {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("created")
.param_types([Instrument::static_type()])
.build()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusInstrumentEditor {}
impl NavigationPageImpl for MusicusInstrumentEditor {}
}
glib::wrapper! {
pub struct MusicusInstrumentEditor(ObjectSubclass<imp::MusicusInstrumentEditor>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl MusicusInstrumentEditor {
pub fn new(
navigation: &adw::NavigationView,
library: &MusicusLibrary,
instrument: Option<&Instrument>,
) -> Self {
let obj: Self = glib::Object::new();
obj.imp().navigation.set(navigation.to_owned()).unwrap();
obj.imp().library.set(library.to_owned()).unwrap();
if let Some(instrument) = instrument {
obj.imp().save_button.set_label(&gettext("Save changes"));
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 {
self.connect_local("created", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let instrument = values[1].get::<Instrument>().unwrap();
f(&obj, instrument);
None
})
}
#[template_callback]
fn save(&self, _: &gtk::Button) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
if let Some(instrument_id) = self.imp().instrument_id.get() {
library.update_instrument(instrument_id, name).unwrap();
} else {
let instrument = library.create_instrument(name).unwrap();
self.emit_by_name::<()>("created", &[&instrument]);
}
self.imp().navigation.get().unwrap().pop();
}
}

View file

@ -1,10 +1,18 @@
pub mod activatable_row;
pub mod ensemble_editor;
pub mod ensemble_selector_popover;
pub mod instrument_editor;
pub mod instrument_selector_popover;
pub mod performer_role_selector_popover;
pub mod person_editor;
pub mod person_selector_popover;
pub mod recording_editor;
pub mod recording_editor_ensemble_row;
pub mod recording_editor_performer_row;
pub mod role_editor;
pub mod role_selector_popover;
pub mod translation_editor;
pub mod translation_entry;
pub mod work_editor;
pub mod work_editor_composer_row;
pub mod work_editor_composer_row;
pub mod work_selector_popover;

View file

@ -0,0 +1,328 @@
use crate::{
db::models::{Instrument, Role},
library::MusicusLibrary,
};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
pango,
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use std::cell::{OnceCell, RefCell};
use super::activatable_row::MusicusActivatableRow;
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::MusicusPerformerRoleSelectorPopover)]
#[template(file = "data/ui/performer_role_selector_popover.blp")]
pub struct MusicusPerformerRoleSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub roles: RefCell<Vec<Role>>,
pub instruments: RefCell<Vec<Instrument>>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub role_view: TemplateChild<adw::ToolbarView>,
#[template_child]
pub role_search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub role_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub role_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub instrument_view: TemplateChild<adw::ToolbarView>,
#[template_child]
pub instrument_search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub instrument_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub instrument_list: TemplateChild<gtk::ListBox>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusPerformerRoleSelectorPopover {
const NAME: &'static str = "MusicusPerformerRoleSelectorPopover";
type Type = super::MusicusPerformerRoleSelectorPopover;
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();
}
}
#[glib::derived_properties]
impl ObjectImpl for MusicusPerformerRoleSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj().connect_visible_notify(
|obj: &super::MusicusPerformerRoleSelectorPopover| {
if obj.is_visible() {
obj.imp().stack.set_visible_child(&*obj.imp().role_view);
obj.imp().role_search_entry.set_text("");
obj.imp().role_search_entry.grab_focus();
obj.imp().role_scrolled_window.vadjustment().set_value(0.0);
}
},
);
self.obj().search_roles("");
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("selected")
.param_types([Role::static_type(), Instrument::static_type()])
.build(),
Signal::builder("create-role").build(),
Signal::builder("create-instrument").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusPerformerRoleSelectorPopover {
// TODO: Fix focus.
fn focus(&self, direction_type: gtk::DirectionType) -> bool {
if direction_type == gtk::DirectionType::Down {
if self.stack.visible_child() == Some(self.role_list.get().upcast()) {
self.role_list.child_focus(direction_type)
} else {
self.instrument_list.child_focus(direction_type)
}
} else {
self.parent_focus(direction_type)
}
}
}
impl PopoverImpl for MusicusPerformerRoleSelectorPopover {}
}
glib::wrapper! {
pub struct MusicusPerformerRoleSelectorPopover(ObjectSubclass<imp::MusicusPerformerRoleSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl MusicusPerformerRoleSelectorPopover {
pub fn new(library: &MusicusLibrary) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_selected<F: Fn(&Self, Role, Option<Instrument>) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let role = values[1].get::<Role>().unwrap();
let instrument = values[2].get::<Option<Instrument>>().unwrap();
f(&obj, role, instrument);
None
})
}
pub fn connect_create_role<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("create-role", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
pub fn connect_create_instrument<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("create-instrument", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
#[template_callback]
fn role_search_changed(&self, entry: &gtk::SearchEntry) {
self.search_roles(&entry.text());
}
#[template_callback]
fn role_activate(&self, _: &gtk::SearchEntry) {
if let Some(role) = self.imp().roles.borrow().first() {
self.select_role(role.to_owned());
} else {
self.create_role();
}
}
#[template_callback]
fn back_button_clicked(&self, _: &gtk::Button) {
self.imp().stack.set_visible_child(&*self.imp().role_view);
self.imp().role_search_entry.grab_focus();
}
#[template_callback]
fn instrument_search_changed(&self, entry: &gtk::SearchEntry) {
self.search_instruments(&entry.text());
}
#[template_callback]
fn instrument_activate(&self, _: &gtk::SearchEntry) {
if let Some(instrument) = self.imp().instruments.borrow().first() {
self.select_instrument(instrument.clone());
} else {
self.create_instrument();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::SearchEntry) {
self.popdown();
}
fn search_roles(&self, search: &str) {
let imp = self.imp();
let roles = imp.library.get().unwrap().search_roles(search).unwrap();
imp.role_list.remove_all();
for role in &roles {
let row = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(role.to_string())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
let role = role.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
obj.select_role(role.clone());
});
imp.role_list.append(&row);
}
let create_box = gtk::Box::builder().spacing(12).build();
create_box.append(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::Label::builder()
.label(gettext("Create new role"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
obj.create_role();
});
imp.role_list.append(&create_row);
imp.roles.replace(roles);
}
fn search_instruments(&self, search: &str) {
let imp = self.imp();
let instruments = imp
.library
.get()
.unwrap()
.search_instruments(search)
.unwrap();
imp.instrument_list.remove_all();
for instrument in &instruments {
let row = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(instrument.name.get())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
let instrument = instrument.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
obj.select_instrument(instrument.clone());
});
imp.instrument_list.append(&row);
}
let create_box = gtk::Box::builder().spacing(12).build();
create_box.append(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::Label::builder()
.label(gettext("Create new instrument"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
obj.create_instrument();
});
imp.instrument_list.append(&create_row);
imp.instruments.replace(instruments);
}
fn select_role(&self, role: Role) {
if role == self.library().performer_default_role().unwrap() {
self.imp().instrument_search_entry.set_text("");
self.imp().instrument_search_entry.grab_focus();
self.imp()
.instrument_scrolled_window
.vadjustment()
.set_value(0.0);
self.imp()
.stack
.set_visible_child(&*self.imp().instrument_view);
self.search_instruments("");
} else {
self.emit_by_name::<()>("selected", &[&role, &None::<Instrument>]);
self.popdown();
}
}
fn select_instrument(&self, instrument: Instrument) {
let role = self.library().performer_default_role().unwrap();
self.emit_by_name::<()>("selected", &[&role, &instrument]);
self.popdown();
}
fn create_role(&self) {
self.emit_by_name::<()>("create-role", &[]);
self.popdown();
}
fn create_instrument(&self) {
self.emit_by_name::<()>("create-instrument", &[]);
self.popdown();
}
}

View file

@ -0,0 +1,317 @@
use std::cell::{OnceCell, RefCell};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::glib::{self, clone, subclass::Signal, Properties};
use once_cell::sync::Lazy;
use crate::{
db::models::{Ensemble, EnsemblePerformer, Performer, Person, Recording, Work},
editor::{
ensemble_editor::MusicusEnsembleEditor,
ensemble_selector_popover::MusicusEnsembleSelectorPopover,
person_editor::MusicusPersonEditor, person_selector_popover::MusicusPersonSelectorPopover,
recording_editor_ensemble_row::MusicusRecordingEditorEnsembleRow,
recording_editor_performer_row::MusicusRecordingEditorPerformerRow,
work_selector_popover::MusicusWorkSelectorPopover,
},
library::MusicusLibrary,
};
mod imp {
use crate::editor::work_editor::MusicusWorkEditor;
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::MusicusRecordingEditor)]
#[template(file = "data/ui/recording_editor.blp")]
pub struct MusicusRecordingEditor {
#[property(get, construct_only)]
pub navigation: OnceCell<adw::NavigationView>,
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub recording_id: OnceCell<String>,
pub work: RefCell<Option<Work>>,
pub performer_rows: RefCell<Vec<MusicusRecordingEditorPerformerRow>>,
pub ensemble_rows: RefCell<Vec<MusicusRecordingEditorEnsembleRow>>,
pub work_selector_popover: OnceCell<MusicusWorkSelectorPopover>,
pub persons_popover: OnceCell<MusicusPersonSelectorPopover>,
pub ensembles_popover: OnceCell<MusicusEnsembleSelectorPopover>,
#[template_child]
pub work_row: TemplateChild<adw::ActionRow>,
#[template_child]
pub select_work_box: TemplateChild<gtk::Box>,
#[template_child]
pub year_row: TemplateChild<adw::SpinRow>,
#[template_child]
pub performer_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub select_person_box: TemplateChild<gtk::Box>,
#[template_child]
pub ensemble_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub select_ensemble_box: TemplateChild<gtk::Box>,
#[template_child]
pub save_button: TemplateChild<gtk::Button>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusRecordingEditor {
const NAME: &'static str = "MusicusRecordingEditor";
type Type = super::MusicusRecordingEditor;
type ParentType = adw::NavigationPage;
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();
}
}
#[glib::derived_properties]
impl ObjectImpl for MusicusRecordingEditor {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("created")
.param_types([Recording::static_type()])
.build()]
});
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
let work_selector_popover =
MusicusWorkSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().clone();
work_selector_popover.connect_selected(move |_, work| {
obj.set_work(work);
});
let obj = self.obj().clone();
work_selector_popover.connect_create(move |_| {
let editor = MusicusWorkEditor::new(&obj.navigation(), &obj.library(), None);
editor.connect_created(clone!(
#[weak]
obj,
move |_, work| {
obj.set_work(work);
}
));
obj.navigation().push(&editor);
});
self.select_work_box.append(&work_selector_popover);
self.work_selector_popover
.set(work_selector_popover)
.unwrap();
let persons_popover = MusicusPersonSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().clone();
persons_popover.connect_person_selected(move |_, person| {
obj.add_performer(person);
});
let obj = self.obj().clone();
persons_popover.connect_create(move |_| {
let editor = MusicusPersonEditor::new(&obj.navigation(), &obj.library(), None);
editor.connect_created(clone!(
#[weak]
obj,
move |_, person| {
obj.add_performer(person);
}
));
obj.navigation().push(&editor);
});
self.select_person_box.append(&persons_popover);
self.persons_popover.set(persons_popover).unwrap();
let ensembles_popover =
MusicusEnsembleSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().clone();
ensembles_popover.connect_ensemble_selected(move |_, ensemble| {
obj.add_ensemble(ensemble);
});
let obj = self.obj().clone();
ensembles_popover.connect_create(move |_| {
let editor = MusicusEnsembleEditor::new(&obj.navigation(), &obj.library(), None);
editor.connect_created(clone!(
#[weak]
obj,
move |_, ensemble| {
obj.add_ensemble(ensemble);
}
));
obj.navigation().push(&editor);
});
self.select_ensemble_box.append(&ensembles_popover);
self.ensembles_popover.set(ensembles_popover).unwrap();
}
}
impl WidgetImpl for MusicusRecordingEditor {}
impl NavigationPageImpl for MusicusRecordingEditor {}
}
glib::wrapper! {
pub struct MusicusRecordingEditor(ObjectSubclass<imp::MusicusRecordingEditor>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl MusicusRecordingEditor {
pub fn new(
navigation: &adw::NavigationView,
library: &MusicusLibrary,
recording: Option<&Recording>,
) -> Self {
let obj: Self = glib::Object::builder()
.property("navigation", navigation)
.property("library", library)
.build();
if let Some(recording) = recording {
obj.imp().save_button.set_label(&gettext("Save changes"));
obj.imp()
.recording_id
.set(recording.recording_id.clone())
.unwrap();
// TODO: Initialize data.
}
obj
}
#[template_callback]
fn select_work(&self, _: &adw::ActionRow) {
self.imp().work_selector_popover.get().unwrap().popup();
}
#[template_callback]
fn select_person(&self, _: &adw::ActionRow) {
self.imp().persons_popover.get().unwrap().popup();
}
#[template_callback]
fn select_ensemble(&self, _: &adw::ActionRow) {
self.imp().ensembles_popover.get().unwrap().popup();
}
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.replace(Some(work));
}
fn add_performer(&self, person: Person) {
let role = self.library().performer_default_role().unwrap();
let performer = Performer {
person,
role,
instrument: None,
};
let row =
MusicusRecordingEditorPerformerRow::new(&self.navigation(), &self.library(), performer);
row.connect_remove(clone!(
#[weak(rename_to = this)]
self,
move |row| {
this.imp().performer_list.remove(row);
this.imp().performer_rows.borrow_mut().retain(|c| c != row);
}
));
self.imp()
.performer_list
.insert(&row, self.imp().performer_rows.borrow().len() as i32);
self.imp().performer_rows.borrow_mut().push(row);
}
fn add_ensemble(&self, ensemble: Ensemble) {
let role = self.library().performer_default_role().unwrap();
let performer = EnsemblePerformer { ensemble, role };
let row =
MusicusRecordingEditorEnsembleRow::new(&self.navigation(), &self.library(), performer);
row.connect_remove(clone!(
#[weak(rename_to = this)]
self,
move |row| {
this.imp().ensemble_list.remove(row);
this.imp().ensemble_rows.borrow_mut().retain(|c| c != row);
}
));
self.imp()
.ensemble_list
.insert(&row, self.imp().ensemble_rows.borrow().len() as i32);
self.imp().ensemble_rows.borrow_mut().push(row);
}
#[template_callback]
fn save(&self, _: &gtk::Button) {
let library = self.imp().library.get().unwrap();
// TODO: No work selected?
let work = self.imp().work.borrow().as_ref().unwrap().clone();
let year = self.imp().year_row.value() as i32;
let performers = self
.imp()
.performer_rows
.borrow()
.iter()
.map(|p| p.performer())
.collect::<Vec<Performer>>();
let ensembles = self
.imp()
.ensemble_rows
.borrow()
.iter()
.map(|e| e.ensemble())
.collect::<Vec<EnsemblePerformer>>();
if let Some(recording_id) = self.imp().recording_id.get() {
library
.update_recording(recording_id, work, Some(year), performers, ensembles)
.unwrap();
} else {
let recording = library
.create_recording(work, Some(year), performers, ensembles)
.unwrap();
self.emit_by_name::<()>("created", &[&recording]);
}
self.imp().navigation.get().unwrap().pop();
}
}

View file

@ -0,0 +1,149 @@
use std::cell::{OnceCell, RefCell};
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib::{self, clone, subclass::Signal, Properties};
use once_cell::sync::Lazy;
use crate::{
db::models::EnsemblePerformer,
editor::{role_editor::MusicusRoleEditor, role_selector_popover::MusicusRoleSelectorPopover},
library::MusicusLibrary,
};
mod imp {
use super::*;
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
#[properties(wrapper_type = super::MusicusRecordingEditorEnsembleRow)]
#[template(file = "data/ui/recording_editor_ensemble_row.blp")]
pub struct MusicusRecordingEditorEnsembleRow {
#[property(get, construct_only)]
pub navigation: OnceCell<adw::NavigationView>,
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub ensemble: RefCell<Option<EnsemblePerformer>>,
pub role_popover: OnceCell<MusicusRoleSelectorPopover>,
#[template_child]
pub role_label: TemplateChild<gtk::Label>,
#[template_child]
pub role_box: TemplateChild<gtk::Box>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusRecordingEditorEnsembleRow {
const NAME: &'static str = "MusicusRecordingEditorEnsembleRow";
type Type = super::MusicusRecordingEditorEnsembleRow;
type ParentType = adw::ActionRow;
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();
}
}
#[glib::derived_properties]
impl ObjectImpl for MusicusRecordingEditorEnsembleRow {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
let role_popover = MusicusRoleSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().to_owned();
role_popover.connect_role_selected(move |_, role| {
if let Some(ensemble) = &mut *obj.imp().ensemble.borrow_mut() {
obj.imp().role_label.set_label(&role.to_string());
ensemble.role = role;
}
});
let obj = self.obj().to_owned();
role_popover.connect_create(move |_| {
let editor = MusicusRoleEditor::new(&obj.navigation(), &obj.library(), None);
editor.connect_created(clone!(
#[weak]
obj,
move |_, role| {
if let Some(ensemble) = &mut *obj.imp().ensemble.borrow_mut() {
obj.imp().role_label.set_label(&role.to_string());
ensemble.role = role;
};
}
));
obj.navigation().push(&editor);
});
self.role_box.append(&role_popover);
self.role_popover.set(role_popover).unwrap();
}
}
impl WidgetImpl for MusicusRecordingEditorEnsembleRow {}
impl ListBoxRowImpl for MusicusRecordingEditorEnsembleRow {}
impl PreferencesRowImpl for MusicusRecordingEditorEnsembleRow {}
impl ActionRowImpl for MusicusRecordingEditorEnsembleRow {}
}
glib::wrapper! {
pub struct MusicusRecordingEditorEnsembleRow(ObjectSubclass<imp::MusicusRecordingEditorEnsembleRow>)
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow;
}
#[gtk::template_callbacks]
impl MusicusRecordingEditorEnsembleRow {
pub fn new(
navigation: &adw::NavigationView,
library: &MusicusLibrary,
ensemble: EnsemblePerformer,
) -> Self {
let obj: Self = glib::Object::builder()
.property("navigation", navigation)
.property("library", library)
.build();
obj.set_ensemble(ensemble);
obj
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
pub fn ensemble(&self) -> EnsemblePerformer {
self.imp().ensemble.borrow().to_owned().unwrap()
}
fn set_ensemble(&self, ensemble: EnsemblePerformer) {
self.set_title(&ensemble.ensemble.to_string());
self.imp().role_label.set_label(&ensemble.role.to_string());
self.imp().ensemble.replace(Some(ensemble));
}
#[template_callback]
fn open_role_popover(&self, _: &gtk::Button) {
self.imp().role_popover.get().unwrap().popup();
}
#[template_callback]
fn remove(&self, _: &gtk::Button) {
self.emit_by_name::<()>("remove", &[]);
}
}

View file

@ -0,0 +1,188 @@
use std::cell::{OnceCell, RefCell};
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib::{self, clone, subclass::Signal, Properties};
use once_cell::sync::Lazy;
use crate::{
db::models::Performer,
editor::{
performer_role_selector_popover::MusicusPerformerRoleSelectorPopover,
role_editor::MusicusRoleEditor,
},
library::MusicusLibrary,
};
mod imp {
use crate::editor::instrument_editor::MusicusInstrumentEditor;
use super::*;
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
#[properties(wrapper_type = super::MusicusRecordingEditorPerformerRow)]
#[template(file = "data/ui/recording_editor_performer_row.blp")]
pub struct MusicusRecordingEditorPerformerRow {
#[property(get, construct_only)]
pub navigation: OnceCell<adw::NavigationView>,
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub performer: RefCell<Option<Performer>>,
pub role_popover: OnceCell<MusicusPerformerRoleSelectorPopover>,
#[template_child]
pub role_label: TemplateChild<gtk::Label>,
#[template_child]
pub role_box: TemplateChild<gtk::Box>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusRecordingEditorPerformerRow {
const NAME: &'static str = "MusicusRecordingEditorPerformerRow";
type Type = super::MusicusRecordingEditorPerformerRow;
type ParentType = adw::ActionRow;
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();
}
}
#[glib::derived_properties]
impl ObjectImpl for MusicusRecordingEditorPerformerRow {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
let role_popover =
MusicusPerformerRoleSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().to_owned();
role_popover.connect_selected(move |_, role, instrument| {
if let Some(performer) = &mut *obj.imp().performer.borrow_mut() {
let label = match &instrument {
Some(instrument) => instrument.to_string(),
None => role.to_string(),
};
obj.imp().role_label.set_label(&label);
performer.role = role;
performer.instrument = instrument;
}
});
let obj = self.obj().to_owned();
role_popover.connect_create_role(move |_| {
let editor = MusicusRoleEditor::new(&obj.navigation(), &obj.library(), None);
editor.connect_created(clone!(
#[weak]
obj,
move |_, role| {
if let Some(performer) = &mut *obj.imp().performer.borrow_mut() {
obj.imp().role_label.set_label(&role.to_string());
performer.role = role;
performer.instrument = None;
};
}
));
obj.navigation().push(&editor);
});
let obj = self.obj().to_owned();
role_popover.connect_create_instrument(move |_| {
let editor = MusicusInstrumentEditor::new(&obj.navigation(), &obj.library(), None);
editor.connect_created(clone!(
#[weak]
obj,
move |_, instrument| {
if let Some(performer) = &mut *obj.imp().performer.borrow_mut() {
obj.imp().role_label.set_label(&instrument.to_string());
performer.role = obj.library().performer_default_role().unwrap();
performer.instrument = Some(instrument);
};
}
));
obj.navigation().push(&editor);
});
self.role_box.append(&role_popover);
self.role_popover.set(role_popover).unwrap();
}
}
impl WidgetImpl for MusicusRecordingEditorPerformerRow {}
impl ListBoxRowImpl for MusicusRecordingEditorPerformerRow {}
impl PreferencesRowImpl for MusicusRecordingEditorPerformerRow {}
impl ActionRowImpl for MusicusRecordingEditorPerformerRow {}
}
glib::wrapper! {
pub struct MusicusRecordingEditorPerformerRow(ObjectSubclass<imp::MusicusRecordingEditorPerformerRow>)
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow;
}
#[gtk::template_callbacks]
impl MusicusRecordingEditorPerformerRow {
pub fn new(
navigation: &adw::NavigationView,
library: &MusicusLibrary,
performer: Performer,
) -> Self {
let obj: Self = glib::Object::builder()
.property("navigation", navigation)
.property("library", library)
.build();
obj.set_performer(performer);
obj
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
pub fn performer(&self) -> Performer {
self.imp().performer.borrow().to_owned().unwrap()
}
fn set_performer(&self, performer: Performer) {
self.set_title(&performer.person.to_string());
let label = match &performer.instrument {
Some(instrument) => instrument.to_string(),
None => performer.role.to_string(),
};
self.imp().role_label.set_label(&label.to_string());
self.imp().performer.replace(Some(performer));
}
#[template_callback]
fn open_role_popover(&self, _: &gtk::Button) {
self.imp().role_popover.get().unwrap().popup();
}
#[template_callback]
fn remove(&self, _: &gtk::Button) {
self.emit_by_name::<()>("remove", &[]);
}
}

View file

@ -1,9 +1,20 @@
use std::cell::{OnceCell, RefCell};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::glib::{
clone, Properties,
{self, subclass::Signal},
};
use once_cell::sync::Lazy;
use crate::{
db::{
self,
models::{Composer, Instrument, Person, Work, WorkPart},
},
editor::{
instrument_editor::MusicusInstrumentEditor,
instrument_selector_popover::MusicusInstrumentSelectorPopover,
person_editor::MusicusPersonEditor, person_selector_popover::MusicusPersonSelectorPopover,
translation_editor::MusicusTranslationEditor,
@ -12,12 +23,6 @@ use crate::{
library::MusicusLibrary,
};
use adw::{prelude::*, subclass::prelude::*};
use gettextrs::gettext;
use gtk::glib::{self, clone, Properties};
use std::cell::{OnceCell, RefCell};
mod imp {
use super::*;
@ -31,10 +36,13 @@ mod imp {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub work_id: OnceCell<String>,
// Holding a reference to each composer row is the simplest way to enumerate all
// results when finishing the process of editing the work. The composer rows
// handle all state related to the composer.
pub composer_rows: RefCell<Vec<MusicusWorkEditorComposerRow>>,
// TODO: These need to be PartRows!
pub parts: RefCell<Vec<WorkPart>>,
pub instruments: RefCell<Vec<Instrument>>,
@ -53,6 +61,8 @@ mod imp {
pub instrument_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub select_instrument_box: TemplateChild<gtk::Box>,
#[template_child]
pub save_button: TemplateChild<gtk::Button>,
}
#[glib::object_subclass]
@ -74,6 +84,16 @@ mod imp {
#[glib::derived_properties]
impl ObjectImpl for MusicusWorkEditor {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("created")
.param_types([Work::static_type()])
.build()]
});
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
@ -106,43 +126,24 @@ mod imp {
MusicusInstrumentSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().clone();
instruments_popover.connect_instrument_selected(
move |_: &MusicusInstrumentSelectorPopover, instrument: Instrument| {
let row = adw::ActionRow::builder()
.title(instrument.to_string())
.build();
instruments_popover.connect_instrument_selected(move |_, instrument| {
obj.add_instrument_row(instrument);
});
let remove_button = gtk::Button::builder()
.icon_name("user-trash-symbolic")
.valign(gtk::Align::Center)
.css_classes(["flat"])
.build();
let obj = self.obj().clone();
instruments_popover.connect_create(move |_| {
let editor = MusicusInstrumentEditor::new(&obj.navigation(), &obj.library(), None);
remove_button.connect_clicked(clone!(
#[weak]
obj,
#[weak]
row,
#[strong]
instrument,
move |_| {
obj.imp().instrument_list.remove(&row);
obj.imp()
.instruments
.borrow_mut()
.retain(|i| *i != instrument);
}
));
editor.connect_created(clone!(
#[weak]
obj,
move |_, instrument| {
obj.add_instrument_row(instrument);
}
));
row.add_suffix(&remove_button);
obj.imp()
.instrument_list
.insert(&row, obj.imp().instruments.borrow().len() as i32);
obj.imp().instruments.borrow_mut().push(instrument);
},
);
obj.navigation().push(&editor);
});
self.select_instrument_box.append(&instruments_popover);
self.instruments_popover.set(instruments_popover).unwrap();
@ -170,13 +171,24 @@ impl MusicusWorkEditor {
.property("library", library)
.build();
if let Some(_work) = work {
if let Some(work) = work {
obj.imp().save_button.set_label(&gettext("Save changes"));
obj.imp().work_id.set(work.work_id.clone()).unwrap();
// TODO: Initialize work data.
}
obj
}
pub fn connect_created<F: Fn(&Self, Work) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("created", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let work = values[1].get::<Work>().unwrap();
f(&obj, work);
None
})
}
#[template_callback]
fn add_person(&self, _: &adw::ActionRow) {
self.imp().persons_popover.get().unwrap().popup();
@ -246,4 +258,69 @@ impl MusicusWorkEditor {
self.imp().composer_rows.borrow_mut().push(row);
}
fn add_instrument_row(&self, instrument: Instrument) {
let row = adw::ActionRow::builder()
.title(instrument.to_string())
.build();
let remove_button = gtk::Button::builder()
.icon_name("user-trash-symbolic")
.valign(gtk::Align::Center)
.css_classes(["flat"])
.build();
remove_button.connect_clicked(clone!(
#[weak(rename_to = this)]
self,
#[weak]
row,
#[strong]
instrument,
move |_| {
this.imp().instrument_list.remove(&row);
this.imp()
.instruments
.borrow_mut()
.retain(|i| *i != instrument);
}
));
row.add_suffix(&remove_button);
self.imp()
.instrument_list
.insert(&row, self.imp().instruments.borrow().len() as i32);
self.imp().instruments.borrow_mut().push(instrument);
}
#[template_callback]
fn save(&self, _: &gtk::Button) {
let library = self.imp().library.get().unwrap();
let name = self.imp().name_editor.translation();
let parts = self.imp().parts.borrow().clone();
let composers = self
.imp()
.composer_rows
.borrow()
.iter()
.map(|c| c.composer())
.collect::<Vec<Composer>>();
let instruments = self.imp().instruments.borrow().clone();
if let Some(work_id) = self.imp().work_id.get() {
library
.update_work(work_id, name, parts, composers, instruments)
.unwrap();
} else {
let work = library
.create_work(name, parts, composers, instruments)
.unwrap();
self.emit_by_name::<()>("created", &[&work]);
}
self.imp().navigation.get().unwrap().pop();
}
}

View file

@ -5,7 +5,7 @@ use gtk::glib::{self, clone, subclass::Signal, Properties};
use once_cell::sync::Lazy;
use crate::{
db::models::{Composer, Role},
db::models::Composer,
editor::{role_editor::MusicusRoleEditor, role_selector_popover::MusicusRoleSelectorPopover},
library::MusicusLibrary,
};
@ -142,14 +142,6 @@ impl MusicusWorkEditorComposerRow {
self.imp().role_popover.get().unwrap().popup();
}
#[template_callback]
fn role_selected(&self, role: Role) {
if let Some(composer) = &mut *self.imp().composer.borrow_mut() {
self.imp().role_label.set_label(&role.to_string());
composer.role = role;
}
}
#[template_callback]
fn remove(&self, _: &gtk::Button) {
self.emit_by_name::<()>("remove", &[]);

View file

@ -0,0 +1,308 @@
use crate::{
db::models::{Person, Work},
library::MusicusLibrary,
};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
pango,
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use std::cell::{OnceCell, RefCell};
use super::activatable_row::MusicusActivatableRow;
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::MusicusWorkSelectorPopover)]
#[template(file = "data/ui/work_selector_popover.blp")]
pub struct MusicusWorkSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub composers: RefCell<Vec<Person>>,
pub composer: RefCell<Option<Person>>,
pub works: RefCell<Vec<Work>>,
#[template_child]
pub stack: TemplateChild<gtk::Stack>,
#[template_child]
pub composer_view: TemplateChild<adw::ToolbarView>,
#[template_child]
pub composer_search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub composer_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub composer_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub work_view: TemplateChild<adw::ToolbarView>,
#[template_child]
pub composer_label: TemplateChild<gtk::Label>,
#[template_child]
pub work_search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub work_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub work_list: TemplateChild<gtk::ListBox>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusWorkSelectorPopover {
const NAME: &'static str = "MusicusWorkSelectorPopover";
type Type = super::MusicusWorkSelectorPopover;
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();
}
}
#[glib::derived_properties]
impl ObjectImpl for MusicusWorkSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj()
.connect_visible_notify(|obj: &super::MusicusWorkSelectorPopover| {
if obj.is_visible() {
obj.imp().stack.set_visible_child(&*obj.imp().composer_view);
obj.imp().composer_search_entry.set_text("");
obj.imp().composer_search_entry.grab_focus();
obj.imp()
.composer_scrolled_window
.vadjustment()
.set_value(0.0);
}
});
self.obj().search_composers("");
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("selected")
.param_types([Work::static_type()])
.build(),
Signal::builder("create").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusWorkSelectorPopover {
// TODO: Fix focus.
fn focus(&self, direction_type: gtk::DirectionType) -> bool {
if direction_type == gtk::DirectionType::Down {
if self.stack.visible_child() == Some(self.composer_list.get().upcast()) {
self.composer_list.child_focus(direction_type)
} else {
self.work_list.child_focus(direction_type)
}
} else {
self.parent_focus(direction_type)
}
}
}
impl PopoverImpl for MusicusWorkSelectorPopover {}
}
glib::wrapper! {
pub struct MusicusWorkSelectorPopover(ObjectSubclass<imp::MusicusWorkSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl MusicusWorkSelectorPopover {
pub fn new(library: &MusicusLibrary) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_selected<F: Fn(&Self, Work) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let work = values[1].get::<Work>().unwrap();
f(&obj, work);
None
})
}
pub fn connect_create<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("create", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
f(&obj);
None
})
}
#[template_callback]
fn composer_search_changed(&self, entry: &gtk::SearchEntry) {
self.search_composers(&entry.text());
}
#[template_callback]
fn composer_activate(&self, _: &gtk::SearchEntry) {
if let Some(composer) = self.imp().composers.borrow().first() {
self.select_composer(composer.to_owned());
} else {
self.create();
}
}
#[template_callback]
fn back_button_clicked(&self, _: &gtk::Button) {
self.imp()
.stack
.set_visible_child(&*self.imp().composer_view);
self.imp().composer_search_entry.grab_focus();
}
#[template_callback]
fn work_search_changed(&self, entry: &gtk::SearchEntry) {
self.search_works(&entry.text());
}
#[template_callback]
fn work_activate(&self, _: &gtk::SearchEntry) {
if let Some(work) = self.imp().works.borrow().first() {
self.select(work.clone());
} else {
self.create();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::SearchEntry) {
self.popdown();
}
fn search_composers(&self, search: &str) {
let imp = self.imp();
let persons = imp.library.get().unwrap().search_persons(search).unwrap();
imp.composer_list.remove_all();
for person in &persons {
let row = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(person.to_string())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
let person = person.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
obj.select_composer(person.clone());
});
imp.composer_list.append(&row);
}
let create_box = gtk::Box::builder().spacing(12).build();
create_box.append(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::Label::builder()
.label(gettext("Create new work"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
obj.create();
});
imp.composer_list.append(&create_row);
imp.composers.replace(persons);
}
fn search_works(&self, search: &str) {
let imp = self.imp();
let works = imp
.library
.get()
.unwrap()
.search_works(imp.composer.borrow().as_ref().unwrap(), search)
.unwrap();
imp.work_list.remove_all();
for work in &works {
let row = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(work.name.get())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
let work = work.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
obj.select(work.clone());
});
imp.work_list.append(&row);
}
let create_box = gtk::Box::builder().spacing(12).build();
create_box.append(&gtk::Image::builder().icon_name("list-add-symbolic").build());
create_box.append(
&gtk::Label::builder()
.label(gettext("Create new work"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
obj.create();
});
imp.work_list.append(&create_row);
imp.works.replace(works);
}
fn select_composer(&self, person: Person) {
self.imp().composer_label.set_text(person.name.get());
self.imp().work_search_entry.set_text("");
self.imp().work_search_entry.grab_focus();
self.imp().work_scrolled_window.vadjustment().set_value(0.0);
self.imp().stack.set_visible_child(&*self.imp().work_view);
self.imp().composer.replace(Some(person.clone()));
self.search_works("");
}
fn select(&self, work: Work) {
self.emit_by_name::<()>("selected", &[&work]);
self.popdown();
}
fn create(&self) {
self.emit_by_name::<()>("create", &[]);
self.popdown();
}
}