editor: First changes for work editor

This commit is contained in:
Elias Projahn 2024-05-31 13:39:27 +02:00
parent 15ba043050
commit 55b344605b
19 changed files with 1291 additions and 19 deletions

View file

@ -0,0 +1,188 @@
use crate::{db::models::Instrument, 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::MusicusInstrumentSelectorPopover)]
#[template(file = "data/ui/instrument_selector_popover.blp")]
pub struct MusicusInstrumentSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
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 MusicusInstrumentSelectorPopover {
const NAME: &'static str = "MusicusInstrumentSelectorPopover";
type Type = super::MusicusInstrumentSelectorPopover;
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 MusicusInstrumentSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj()
.connect_visible_notify(|obj: &super::MusicusInstrumentSelectorPopover| {
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()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusInstrumentSelectorPopover {
// 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 MusicusInstrumentSelectorPopover {}
}
glib::wrapper! {
pub struct MusicusInstrumentSelectorPopover(ObjectSubclass<imp::MusicusInstrumentSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl MusicusInstrumentSelectorPopover {
pub fn new(library: &MusicusLibrary) -> 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
})
}
#[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 = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(instrument.to_string())
.halign(gtk::Align::Start)
.build(),
);
let instrument = instrument.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
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 = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
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) {
log::info!("Create instrument!");
self.popdown();
}
}

View file

@ -1,4 +1,9 @@
pub mod activatable_row;
pub mod instrument_selector_popover;
pub mod person_editor;
pub mod person_selector_popover;
pub mod role_selector_popover;
pub mod translation_entry;
pub mod translation_section;
pub mod translation_section;
pub mod work_editor;
pub mod work_editor_composer_row;

View file

@ -0,0 +1,188 @@
use crate::{db::models::Person, 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::MusicusPersonSelectorPopover)]
#[template(file = "data/ui/person_selector_popover.blp")]
pub struct MusicusPersonSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
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 MusicusPersonSelectorPopover {
const NAME: &'static str = "MusicusPersonSelectorPopover";
type Type = super::MusicusPersonSelectorPopover;
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 MusicusPersonSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj()
.connect_visible_notify(|obj: &super::MusicusPersonSelectorPopover| {
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()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusPersonSelectorPopover {
// 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 MusicusPersonSelectorPopover {}
}
glib::wrapper! {
pub struct MusicusPersonSelectorPopover(ObjectSubclass<imp::MusicusPersonSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl MusicusPersonSelectorPopover {
pub fn new(library: &MusicusLibrary) -> 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
})
}
#[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 = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(person.to_string())
.halign(gtk::Align::Start)
.build(),
);
let person = person.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
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 = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
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) {
log::info!("Create person!");
self.popdown();
}
}

View file

@ -0,0 +1,188 @@
use crate::{db::models::Role, 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::MusicusRoleSelectorPopover)]
#[template(file = "data/ui/role_selector_popover.blp")]
pub struct MusicusRoleSelectorPopover {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
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 MusicusRoleSelectorPopover {
const NAME: &'static str = "MusicusRoleSelectorPopover";
type Type = super::MusicusRoleSelectorPopover;
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 MusicusRoleSelectorPopover {
fn constructed(&self) {
self.parent_constructed();
self.obj()
.connect_visible_notify(|obj: &super::MusicusRoleSelectorPopover| {
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()]
});
SIGNALS.as_ref()
}
}
impl WidgetImpl for MusicusRoleSelectorPopover {
// 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 MusicusRoleSelectorPopover {}
}
glib::wrapper! {
pub struct MusicusRoleSelectorPopover(ObjectSubclass<imp::MusicusRoleSelectorPopover>)
@extends gtk::Widget, gtk::Popover;
}
#[gtk::template_callbacks]
impl MusicusRoleSelectorPopover {
pub fn new(library: &MusicusLibrary) -> 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
})
}
#[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 = MusicusActivatableRow::new(
&gtk::Label::builder()
.label(role.to_string())
.halign(gtk::Align::Start)
.build(),
);
let role = role.clone();
let obj = self.clone();
row.connect_activated(move |_: &MusicusActivatableRow| {
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 = MusicusActivatableRow::new(&create_box);
let obj = self.clone();
create_row.connect_activated(move |_: &MusicusActivatableRow| {
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) {
log::info!("Create role!");
self.popdown();
}
}

162
src/editor/work_editor.rs Normal file
View file

@ -0,0 +1,162 @@
use crate::{
db::models::{Composer, Instrument, Person},
editor::{
instrument_selector_popover::MusicusInstrumentSelectorPopover,
person_selector_popover::MusicusPersonSelectorPopover,
translation_section::MusicusTranslationSection,
work_editor_composer_row::MusicusWorkEditorComposerRow,
},
library::MusicusLibrary,
};
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib::{self, clone, Properties};
use std::cell::{OnceCell, RefCell};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate, Properties)]
#[properties(wrapper_type = super::MusicusWorkEditor)]
#[template(file = "data/ui/work_editor.blp")]
pub struct MusicusWorkEditor {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
// 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>>,
pub instruments: RefCell<Vec<Instrument>>,
pub persons_popover: OnceCell<MusicusPersonSelectorPopover>,
pub instruments_popover: OnceCell<MusicusInstrumentSelectorPopover>,
#[template_child]
pub composer_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub select_person_box: TemplateChild<gtk::Box>,
#[template_child]
pub instrument_list: TemplateChild<gtk::ListBox>,
#[template_child]
pub select_instrument_box: TemplateChild<gtk::Box>,
}
#[glib::object_subclass]
impl ObjectSubclass for MusicusWorkEditor {
const NAME: &'static str = "MusicusWorkEditor";
type Type = super::MusicusWorkEditor;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
MusicusTranslationSection::static_type();
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 MusicusWorkEditor {
fn constructed(&self) {
self.parent_constructed();
let persons_popover = MusicusPersonSelectorPopover::new(self.library.get().unwrap());
let obj = self.obj().clone();
persons_popover.connect_person_selected(
move |_: &MusicusPersonSelectorPopover, person: Person| {
let role = obj.library().composer_default_role().unwrap();
let composer = Composer { person, role };
let row = MusicusWorkEditorComposerRow::new(&obj.library(), composer);
row.connect_remove(clone!(@weak obj => move |row| {
obj.imp().composer_list.remove(row);
obj.imp().composer_rows.borrow_mut().retain(|c| c != row);
}));
obj.imp()
.composer_list
.insert(&row, obj.imp().composer_rows.borrow().len() as i32);
obj.imp().composer_rows.borrow_mut().push(row);
},
);
self.select_person_box.append(&persons_popover);
self.persons_popover.set(persons_popover).unwrap();
let instruments_popover =
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();
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 obj, @weak row, @strong instrument => move |_| {
obj.imp().instrument_list.remove(&row);
let mut instruments = obj.imp().instruments.borrow_mut();
let index = instruments.iter().position(|i| *i == instrument).unwrap();
instruments.remove(index);
}),
);
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);
},
);
self.select_instrument_box.append(&instruments_popover);
self.instruments_popover.set(instruments_popover).unwrap();
}
}
impl WidgetImpl for MusicusWorkEditor {}
impl NavigationPageImpl for MusicusWorkEditor {}
}
glib::wrapper! {
pub struct MusicusWorkEditor(ObjectSubclass<imp::MusicusWorkEditor>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl MusicusWorkEditor {
pub fn new(library: &MusicusLibrary) -> Self {
glib::Object::builder().property("library", library).build()
}
#[template_callback]
fn add_person(&self, _: &adw::ActionRow) {
self.imp().persons_popover.get().unwrap().popup();
}
#[template_callback]
fn add_part(&self, _: &adw::ActionRow) {
todo!();
}
#[template_callback]
fn add_instrument(&self, _: &adw::ActionRow) {
self.imp().instruments_popover.get().unwrap().popup();
}
}

View file

@ -0,0 +1,129 @@
use crate::{
db::models::{Composer, Role},
editor::role_selector_popover::MusicusRoleSelectorPopover,
library::MusicusLibrary,
};
use adw::{prelude::*, subclass::prelude::*};
use gtk::glib::{self, subclass::Signal, Properties};
use once_cell::sync::Lazy;
use std::cell::{OnceCell, RefCell};
mod imp {
use super::*;
#[derive(Properties, Debug, Default, gtk::CompositeTemplate)]
#[properties(wrapper_type = super::MusicusWorkEditorComposerRow)]
#[template(file = "data/ui/work_editor_composer_row.blp")]
pub struct MusicusWorkEditorComposerRow {
#[property(get, construct_only)]
pub library: OnceCell<MusicusLibrary>,
pub composer: RefCell<Option<Composer>>,
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 MusicusWorkEditorComposerRow {
const NAME: &'static str = "MusicusWorkEditorComposerRow";
type Type = super::MusicusWorkEditorComposerRow;
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 MusicusWorkEditorComposerRow {
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(composer) = &mut *obj.imp().composer.borrow_mut() {
obj.imp().role_label.set_label(&role.to_string());
composer.role = role;
}
});
self.role_box.append(&role_popover);
self.role_popover.set(role_popover).unwrap();
}
}
impl WidgetImpl for MusicusWorkEditorComposerRow {}
impl ListBoxRowImpl for MusicusWorkEditorComposerRow {}
impl PreferencesRowImpl for MusicusWorkEditorComposerRow {}
impl ActionRowImpl for MusicusWorkEditorComposerRow {}
}
glib::wrapper! {
pub struct MusicusWorkEditorComposerRow(ObjectSubclass<imp::MusicusWorkEditorComposerRow>)
@extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow;
}
#[gtk::template_callbacks]
impl MusicusWorkEditorComposerRow {
pub fn new(library: &MusicusLibrary, composer: Composer) -> Self {
let obj: Self = glib::Object::builder().property("library", library).build();
obj.set_composer(composer);
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 composer(&self) -> Composer {
self.imp().composer.borrow().to_owned().unwrap()
}
fn set_composer(&self, composer: Composer) {
self.set_title(&composer.person.to_string());
self.imp().role_label.set_label(&composer.role.to_string());
self.imp().composer.replace(Some(composer));
}
#[template_callback]
fn open_role_popover(&self, _: &gtk::Button) {
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", &[]);
}
}