Refactor module layout

This commit is contained in:
Elias Projahn 2025-03-01 09:57:01 +01:00
parent e59052a362
commit 5956b7ff15
70 changed files with 757 additions and 841 deletions

198
src/selector/ensemble.rs Normal file
View file

@ -0,0 +1,198 @@
use std::cell::{OnceCell, RefCell};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use crate::{activatable_row::ActivatableRow, db::models::Ensemble, library::Library};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::EnsembleSelectorPopover)]
#[template(file = "data/ui/selector/ensemble.blp")]
pub struct EnsembleSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<Library>,
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 EnsembleSelectorPopover {
const NAME: &'static str = "MusicusEnsembleSelectorPopover";
type Type = super::EnsembleSelectorPopover;
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 EnsembleSelectorPopover {
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);
}
});
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 EnsembleSelectorPopover {
// 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 EnsembleSelectorPopover {}
}
glib::wrapper! {
pub struct EnsembleSelectorPopover(ObjectSubclass<imp::EnsembleSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl EnsembleSelectorPopover {
pub fn new(library: &Library) -> 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 = ActivatableRow::new(
&gtk::Label::builder()
.label(ensemble.to_string())
.halign(gtk::Align::Start)
.build(),
);
row.set_tooltip_text(Some(&ensemble.to_string()));
let ensemble = ensemble.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
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();
}
}

203
src/selector/instrument.rs Normal file
View file

@ -0,0 +1,203 @@
use std::cell::{OnceCell, RefCell};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use crate::{activatable_row::ActivatableRow, db::models::Instrument, library::Library};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::InstrumentSelectorPopover)]
#[template(file = "data/ui/selector/instrument.blp")]
pub struct InstrumentSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<Library>,
pub instruments: RefCell<Vec<Instrument>>,
#[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 InstrumentSelectorPopover {
const NAME: &'static str = "MusicusInstrumentSelectorPopover";
type Type = super::InstrumentSelectorPopover;
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 InstrumentSelectorPopover {
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);
}
});
self.obj().search("");
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("instrument-selected")
.param_types([Instrument::static_type()])
.build(),
Signal::builder("create").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for InstrumentSelectorPopover {
// 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 InstrumentSelectorPopover {}
}
glib::wrapper! {
pub struct InstrumentSelectorPopover(ObjectSubclass<imp::InstrumentSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl InstrumentSelectorPopover {
pub fn new(library: &Library) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_instrument_selected<F: Fn(&Self, Instrument) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("instrument-selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let instrument = values[1].get::<Instrument>().unwrap();
f(&obj, instrument);
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(instrument) = self.imp().instruments.borrow().first() {
self.select(instrument.clone());
} else {
self.create();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::SearchEntry) {
self.popdown();
}
fn search(&self, search: &str) {
let imp = self.imp();
let instruments = imp
.library
.get()
.unwrap()
.search_instruments(search)
.unwrap();
imp.list_box.remove_all();
for instrument in &instruments {
let row = ActivatableRow::new(
&gtk::Label::builder()
.label(instrument.to_string())
.halign(gtk::Align::Start)
.build(),
);
row.set_tooltip_text(Some(&instrument.to_string()));
let instrument = instrument.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
obj.select(instrument.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 instrument"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
obj.create();
});
imp.list_box.append(&create_row);
imp.instruments.replace(instruments);
}
fn select(&self, instrument: Instrument) {
self.emit_by_name::<()>("instrument-selected", &[&instrument]);
self.popdown();
}
fn create(&self) {
self.emit_by_name::<()>("create", &[]);
self.popdown();
}
}

7
src/selector/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod ensemble;
pub mod instrument;
pub mod performer_role;
pub mod person;
pub mod recording;
pub mod role;
pub mod work;

View file

@ -0,0 +1,329 @@
use std::cell::{OnceCell, RefCell};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
pango,
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use crate::{
activatable_row::ActivatableRow,
db::models::{Instrument, Role},
library::Library,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::PerformerRoleSelectorPopover)]
#[template(file = "data/ui/selector/performer_role.blp")]
pub struct PerformerRoleSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<Library>,
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 PerformerRoleSelectorPopover {
const NAME: &'static str = "MusicusPerformerRoleSelectorPopover";
type Type = super::PerformerRoleSelectorPopover;
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 PerformerRoleSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj().connect_visible_notify(|obj| {
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 PerformerRoleSelectorPopover {
// 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 PerformerRoleSelectorPopover {}
}
glib::wrapper! {
pub struct PerformerRoleSelectorPopover(ObjectSubclass<imp::PerformerRoleSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl PerformerRoleSelectorPopover {
pub fn new(library: &Library) -> 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) {
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 = ActivatableRow::new(
&gtk::Label::builder()
.label(role.to_string())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&role.to_string()));
let role = role.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(
&gtk::Label::builder()
.label(instrument.to_string())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&instrument.to_string()));
let instrument = instrument.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
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();
}
}

198
src/selector/person.rs Normal file
View file

@ -0,0 +1,198 @@
use std::cell::{OnceCell, RefCell};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use crate::{activatable_row::ActivatableRow, db::models::Person, library::Library};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::PersonSelectorPopover)]
#[template(file = "data/ui/selector/person.blp")]
pub struct PersonSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<Library>,
pub persons: RefCell<Vec<Person>>,
#[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 PersonSelectorPopover {
const NAME: &'static str = "MusicusPersonSelectorPopover";
type Type = super::PersonSelectorPopover;
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 PersonSelectorPopover {
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);
}
});
self.obj().search("");
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("person-selected")
.param_types([Person::static_type()])
.build(),
Signal::builder("create").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for PersonSelectorPopover {
// 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 PersonSelectorPopover {}
}
glib::wrapper! {
pub struct PersonSelectorPopover(ObjectSubclass<imp::PersonSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl PersonSelectorPopover {
pub fn new(library: &Library) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_person_selected<F: Fn(&Self, Person) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("person-selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let person = values[1].get::<Person>().unwrap();
f(&obj, person);
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(person) = self.imp().persons.borrow().first() {
self.select(person.clone());
} else {
self.create();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::SearchEntry) {
self.popdown();
}
fn search(&self, search: &str) {
let imp = self.imp();
let persons = imp.library.get().unwrap().search_persons(search).unwrap();
imp.list_box.remove_all();
for person in &persons {
let row = ActivatableRow::new(
&gtk::Label::builder()
.label(person.to_string())
.halign(gtk::Align::Start)
.build(),
);
row.set_tooltip_text(Some(&person.to_string()));
let person = person.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
obj.select(person.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 person"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
obj.create();
});
imp.list_box.append(&create_row);
imp.persons.replace(persons);
}
fn select(&self, person: Person) {
self.emit_by_name::<()>("person-selected", &[&person]);
self.popdown();
}
fn create(&self) {
self.emit_by_name::<()>("create", &[]);
self.popdown();
}
}

422
src/selector/recording.rs Normal file
View file

@ -0,0 +1,422 @@
use std::cell::{OnceCell, RefCell};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
pango,
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use crate::{
activatable_row::ActivatableRow,
db::models::{Person, Recording, Work},
library::Library,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::RecordingSelectorPopover)]
#[template(file = "data/ui/selector/recording.blp")]
pub struct RecordingSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<Library>,
pub composers: RefCell<Vec<Person>>,
pub works: RefCell<Vec<Work>>,
pub recordings: RefCell<Vec<Recording>>,
pub composer: RefCell<Option<Person>>,
pub work: RefCell<Option<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>,
#[template_child]
pub recording_view: TemplateChild<adw::ToolbarView>,
#[template_child]
pub work_label: TemplateChild<gtk::Label>,
#[template_child]
pub recording_search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
pub recording_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
pub recording_list: TemplateChild<gtk::ListBox>,
}
#[glib::object_subclass]
impl ObjectSubclass for RecordingSelectorPopover {
const NAME: &'static str = "MusicusRecordingSelectorPopover";
type Type = super::RecordingSelectorPopover;
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 RecordingSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj().connect_visible_notify(|obj| {
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([Recording::static_type()])
.build(),
Signal::builder("create").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for RecordingSelectorPopover {
// 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 if self.stack.visible_child() == Some(self.work_list.get().upcast()) {
self.work_list.child_focus(direction_type)
} else {
self.recording_list.child_focus(direction_type)
}
} else {
self.parent_focus(direction_type)
}
}
}
impl PopoverImpl for RecordingSelectorPopover {}
}
glib::wrapper! {
pub struct RecordingSelectorPopover(ObjectSubclass<imp::RecordingSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl RecordingSelectorPopover {
pub fn new(library: &Library) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_selected<F: Fn(&Self, Recording) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let recording = values[1].get::<Recording>().unwrap();
f(&obj, recording);
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_to_composer(&self) {
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(work.to_owned());
} else {
self.create();
}
}
#[template_callback]
fn back_to_work(&self) {
self.imp().stack.set_visible_child(&*self.imp().work_view);
self.imp().work_search_entry.grab_focus();
}
#[template_callback]
fn recording_search_changed(&self, entry: &gtk::SearchEntry) {
self.search_recordings(&entry.text());
}
#[template_callback]
fn recording_activate(&self, _: &gtk::SearchEntry) {
if let Some(recording) = self.imp().recordings.borrow().first() {
self.select(recording.to_owned());
} 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 = ActivatableRow::new(
&gtk::Label::builder()
.label(person.to_string())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&person.to_string()));
let person = person.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
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 recording"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(
&gtk::Label::builder()
.label(work.name.get())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&work.name.get()));
let work = work.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
obj.select_work(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 recording"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
obj.create();
});
imp.work_list.append(&create_row);
imp.works.replace(works);
}
fn search_recordings(&self, search: &str) {
let imp = self.imp();
let recordings = imp
.library
.get()
.unwrap()
.search_recordings(imp.work.borrow().as_ref().unwrap(), search)
.unwrap();
imp.recording_list.remove_all();
for recording in &recordings {
let mut label = recording.performers_string();
if let Some(year) = recording.year {
label.push_str(&format!(" ({year})"));
}
let row = ActivatableRow::new(
&gtk::Label::builder()
.label(&label)
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&label));
let recording = recording.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
obj.select(recording.clone());
});
imp.recording_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 recording"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
obj.create();
});
imp.recording_list.append(&create_row);
imp.recordings.replace(recordings);
}
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_work(&self, work: Work) {
self.imp().work_label.set_text(work.name.get());
self.imp().recording_search_entry.set_text("");
self.imp().recording_search_entry.grab_focus();
self.imp()
.recording_scrolled_window
.vadjustment()
.set_value(0.0);
self.imp()
.stack
.set_visible_child(&*self.imp().recording_view);
self.imp().work.replace(Some(work.clone()));
self.search_recordings("");
}
fn select(&self, recording: Recording) {
self.emit_by_name::<()>("selected", &[&recording]);
self.popdown();
}
fn create(&self) {
self.emit_by_name::<()>("create", &[]);
self.popdown();
}
}

198
src/selector/role.rs Normal file
View file

@ -0,0 +1,198 @@
use std::cell::{OnceCell, RefCell};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use crate::{activatable_row::ActivatableRow, db::models::Role, library::Library};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::RoleSelectorPopover)]
#[template(file = "data/ui/selector/role.blp")]
pub struct RoleSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<Library>,
pub roles: RefCell<Vec<Role>>,
#[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 RoleSelectorPopover {
const NAME: &'static str = "MusicusRoleSelectorPopover";
type Type = super::RoleSelectorPopover;
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 RoleSelectorPopover {
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);
}
});
self.obj().search("");
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![
Signal::builder("role-selected")
.param_types([Role::static_type()])
.build(),
Signal::builder("create").build(),
]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for RoleSelectorPopover {
// 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 RoleSelectorPopover {}
}
glib::wrapper! {
pub struct RoleSelectorPopover(ObjectSubclass<imp::RoleSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl RoleSelectorPopover {
pub fn new(library: &Library) -> Self {
glib::Object::builder().property("library", library).build()
}
pub fn connect_role_selected<F: Fn(&Self, Role) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.connect_local("role-selected", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let role = values[1].get::<Role>().unwrap();
f(&obj, role);
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(role) = self.imp().roles.borrow().first() {
self.select(role.clone());
} else {
self.create();
}
}
#[template_callback]
fn stop_search(&self, _: &gtk::SearchEntry) {
self.popdown();
}
fn search(&self, search: &str) {
let imp = self.imp();
let roles = imp.library.get().unwrap().search_roles(search).unwrap();
imp.list_box.remove_all();
for role in &roles {
let row = ActivatableRow::new(
&gtk::Label::builder()
.label(role.to_string())
.halign(gtk::Align::Start)
.build(),
);
row.set_tooltip_text(Some(&role.to_string()));
let role = role.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
obj.select(role.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 role"))
.halign(gtk::Align::Start)
.build(),
);
let create_row = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
obj.create();
});
imp.list_box.append(&create_row);
imp.roles.replace(roles);
}
fn select(&self, role: Role) {
self.emit_by_name::<()>("role-selected", &[&role]);
self.popdown();
}
fn create(&self) {
self.emit_by_name::<()>("create", &[]);
self.popdown();
}
}

310
src/selector/work.rs Normal file
View file

@ -0,0 +1,310 @@
use std::cell::{OnceCell, RefCell};
use gettextrs::gettext;
use gtk::{
glib::{self, subclass::Signal, Properties},
pango,
prelude::*,
subclass::prelude::*,
};
use once_cell::sync::Lazy;
use crate::{
activatable_row::ActivatableRow,
db::models::{Person, Work},
library::Library,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::WorkSelectorPopover)]
#[template(file = "data/ui/selector/work.blp")]
pub struct WorkSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<Library>,
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 WorkSelectorPopover {
const NAME: &'static str = "MusicusWorkSelectorPopover";
type Type = super::WorkSelectorPopover;
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 WorkSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj().connect_visible_notify(|obj| {
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 WorkSelectorPopover {
// 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 WorkSelectorPopover {}
}
glib::wrapper! {
pub struct WorkSelectorPopover(ObjectSubclass<imp::WorkSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl WorkSelectorPopover {
pub fn new(library: &Library) -> 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) {
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 = ActivatableRow::new(
&gtk::Label::builder()
.label(person.to_string())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&person.to_string()));
let person = person.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(
&gtk::Label::builder()
.label(work.name.get())
.halign(gtk::Align::Start)
.ellipsize(pango::EllipsizeMode::Middle)
.build(),
);
row.set_tooltip_text(Some(&work.name.get()));
let work = work.clone();
let obj = self.clone();
row.connect_activated(move |_: &ActivatableRow| {
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 = ActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &ActivatableRow| {
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();
}
}