From 55b344605b0528d388a925d40d78f3638a45beaf Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Fri, 31 May 2024 13:39:27 +0200 Subject: [PATCH] editor: First changes for work editor --- data/res/style.css | 15 ++ data/ui/instrument_selector_popover.blp | 35 ++++ data/ui/person_selector_popover.blp | 35 ++++ data/ui/role_selector_popover.blp | 35 ++++ data/ui/translation_entry.blp | 25 ++- data/ui/work_editor.blp | 119 ++++++++++++++ data/ui/work_editor_composer_row.blp | 33 ++++ src/db/mod.rs | 8 +- src/db/models.rs | 76 ++++++++- src/db/tables.rs | 10 +- src/editor/instrument_selector_popover.rs | 188 ++++++++++++++++++++++ src/editor/mod.rs | 7 +- src/editor/person_selector_popover.rs | 188 ++++++++++++++++++++++ src/editor/role_selector_popover.rs | 188 ++++++++++++++++++++++ src/editor/work_editor.rs | 162 +++++++++++++++++++ src/editor/work_editor_composer_row.rs | 129 +++++++++++++++ src/library.rs | 51 ++++++ src/library_manager.rs | 4 +- src/util.rs | 2 +- 19 files changed, 1291 insertions(+), 19 deletions(-) create mode 100644 data/ui/instrument_selector_popover.blp create mode 100644 data/ui/person_selector_popover.blp create mode 100644 data/ui/role_selector_popover.blp create mode 100644 data/ui/work_editor.blp create mode 100644 data/ui/work_editor_composer_row.blp create mode 100644 src/editor/instrument_selector_popover.rs create mode 100644 src/editor/person_selector_popover.rs create mode 100644 src/editor/role_selector_popover.rs create mode 100644 src/editor/work_editor.rs create mode 100644 src/editor/work_editor_composer_row.rs diff --git a/data/res/style.css b/data/res/style.css index 7081478..b9c4662 100644 --- a/data/res/style.css +++ b/data/res/style.css @@ -62,4 +62,19 @@ .playlisttile .parttitle { font-size: smaller; +} + +.selector > contents { + padding: 0; +} + +.selector-list { + padding-left: 8px; + padding-right: 8px; + padding-bottom: 8px; +} + +.selector-list > row { + padding: 6px; + border-radius: 6px; } \ No newline at end of file diff --git a/data/ui/instrument_selector_popover.blp b/data/ui/instrument_selector_popover.blp new file mode 100644 index 0000000..0026ccc --- /dev/null +++ b/data/ui/instrument_selector_popover.blp @@ -0,0 +1,35 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusInstrumentSelectorPopover: Gtk.Popover { + styles [ + "selector" + ] + + Adw.ToolbarView { + [top] + Gtk.SearchEntry search_entry { + placeholder-text: _("Search instruments…"); + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 6; + search-changed => $search_changed() swapped; + activate => $activate() swapped; + stop-search => $stop_search() swapped; + } + + Gtk.ScrolledWindow scrolled_window { + height-request: 200; + + Gtk.ListBox list_box { + styles [ + "selector-list" + ] + + selection-mode: none; + activate-on-single-click: true; + } + } + } +} diff --git a/data/ui/person_selector_popover.blp b/data/ui/person_selector_popover.blp new file mode 100644 index 0000000..f79bc63 --- /dev/null +++ b/data/ui/person_selector_popover.blp @@ -0,0 +1,35 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusPersonSelectorPopover: Gtk.Popover { + styles [ + "selector" + ] + + Adw.ToolbarView { + [top] + Gtk.SearchEntry search_entry { + placeholder-text: _("Search persons…"); + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 6; + search-changed => $search_changed() swapped; + activate => $activate() swapped; + stop-search => $stop_search() swapped; + } + + Gtk.ScrolledWindow scrolled_window { + height-request: 200; + + Gtk.ListBox list_box { + styles [ + "selector-list" + ] + + selection-mode: none; + activate-on-single-click: true; + } + } + } +} diff --git a/data/ui/role_selector_popover.blp b/data/ui/role_selector_popover.blp new file mode 100644 index 0000000..8e3c773 --- /dev/null +++ b/data/ui/role_selector_popover.blp @@ -0,0 +1,35 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusRoleSelectorPopover: Gtk.Popover { + styles [ + "selector" + ] + + Adw.ToolbarView { + [top] + Gtk.SearchEntry search_entry { + placeholder-text: _("Search roles…"); + margin-start: 8; + margin-end: 8; + margin-top: 8; + margin-bottom: 6; + search-changed => $search_changed() swapped; + activate => $activate() swapped; + stop-search => $stop_search() swapped; + } + + Gtk.ScrolledWindow scrolled_window { + height-request: 200; + + Gtk.ListBox list_box { + styles [ + "selector-list" + ] + + selection-mode: none; + activate-on-single-click: true; + } + } + } +} diff --git a/data/ui/translation_entry.blp b/data/ui/translation_entry.blp index 3161f8e..9e2475f 100644 --- a/data/ui/translation_entry.blp +++ b/data/ui/translation_entry.blp @@ -1,20 +1,27 @@ using Gtk 4.0; using Adw 1; -template $MusicusTranslationEntry : Adw.EntryRow { +template $MusicusTranslationEntry: Adw.EntryRow { title: _("Translated name"); Gtk.Button { - icon-name: "edit-delete-symbolic"; + icon-name: "user-trash-symbolic"; valign: center; clicked => $remove() swapped; - styles ["flat"] + + styles [ + "flat" + ] } Gtk.Button { valign: center; clicked => $open_lang_popover() swapped; + styles [ + "flat" + ] + Gtk.Box { spacing: 6; @@ -38,7 +45,10 @@ template $MusicusTranslationEntry : Adw.EntryRow { Gtk.Label { label: _("Language code"); halign: start; - styles ["heading"] + + styles [ + "heading" + ] } Gtk.Label { @@ -48,7 +58,10 @@ template $MusicusTranslationEntry : Adw.EntryRow { wrap: true; max-width-chars: 40; halign: start; - styles ["dim-label"] + + styles [ + "dim-label" + ] } Gtk.Entry lang_entry {} @@ -56,4 +69,4 @@ template $MusicusTranslationEntry : Adw.EntryRow { } } } -} \ No newline at end of file +} diff --git a/data/ui/work_editor.blp b/data/ui/work_editor.blp new file mode 100644 index 0000000..c37ae00 --- /dev/null +++ b/data/ui/work_editor.blp @@ -0,0 +1,119 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusWorkEditor: Adw.NavigationPage { + title: _("Work"); + + Adw.ToolbarView { + [top] + Adw.HeaderBar header_bar {} + + Gtk.ScrolledWindow { + Adw.Clamp { + Gtk.Box { + orientation: vertical; + margin-bottom: 24; + margin-start: 12; + margin-end: 12; + + $MusicusTranslationSection name_section {} + + Gtk.Label { + label: _("Composers"); + xalign: 0; + margin-top: 24; + + styles [ + "heading" + ] + } + + Gtk.ListBox composer_list { + selection-mode: none; + margin-top: 12; + + styles [ + "boxed-list" + ] + + Adw.ActionRow { + title: _("Add composer"); + activatable: true; + activated => $add_person() swapped; + + [prefix] + Gtk.Box select_person_box { + Gtk.Image { + icon-name: "list-add-symbolic"; + } + } + } + } + + Gtk.Label { + label: _("Structure"); + xalign: 0; + margin-top: 24; + + styles [ + "heading" + ] + } + + Gtk.ListBox part_list { + selection-mode: none; + margin-top: 12; + + styles [ + "boxed-list" + ] + + Adw.ActionRow { + title: _("Add part"); + activatable: true; + activated => $add_part() swapped; + + [prefix] + Gtk.Image { + icon-name: "list-add-symbolic"; + } + } + } + + Gtk.Label { + label: _("Instruments"); + xalign: 0; + margin-top: 24; + + styles [ + "heading" + ] + } + + Gtk.ListBox instrument_list { + selection-mode: none; + margin-top: 12; + margin-bottom: 24; + + styles [ + "boxed-list" + ] + + Adw.ActionRow { + title: _("Add instrument"); + activatable: true; + activated => $add_instrument() swapped; + + [prefix] + Gtk.Box select_instrument_box { + Gtk.Image { + icon-name: "list-add-symbolic"; + } + } + } + } + } + } + } + } +} diff --git a/data/ui/work_editor_composer_row.blp b/data/ui/work_editor_composer_row.blp new file mode 100644 index 0000000..c7b414f --- /dev/null +++ b/data/ui/work_editor_composer_row.blp @@ -0,0 +1,33 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusWorkEditorComposerRow: Adw.ActionRow { + Gtk.Button { + icon-name: "user-trash-symbolic"; + valign: center; + clicked => $remove() swapped; + + styles [ + "flat" + ] + } + + Gtk.Button { + valign: center; + clicked => $open_role_popover() swapped; + + styles [ + "flat" + ] + + Gtk.Box role_box { + spacing: 6; + + Gtk.Label role_label {} + + Gtk.Image { + icon-name: "pan-down-symbolic"; + } + } + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs index b2fd98e..97ddf6f 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,7 +2,7 @@ pub mod models; pub mod schema; pub mod tables; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; use anyhow::{anyhow, Result}; use diesel::{ @@ -63,6 +63,12 @@ impl TranslatedString { } } +impl Display for TranslatedString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.get()) + } +} + impl FromSql for TranslatedString where String: FromSql, diff --git a/src/db/models.rs b/src/db/models.rs index ba50a60..d2d8c91 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -16,7 +16,7 @@ pub struct Work { pub work_id: String, pub name: TranslatedString, pub parts: Vec, - pub persons: Vec, + pub persons: Vec, pub instruments: Vec, } @@ -27,6 +27,14 @@ pub struct WorkPart { pub name: TranslatedString, } +#[derive(Queryable, Selectable, Clone, Debug)] +pub struct Composer { + #[diesel(embed)] + pub person: Person, + #[diesel(embed)] + pub role: Role, +} + #[derive(Clone, Debug)] pub struct Ensemble { pub ensemble_id: String, @@ -65,6 +73,45 @@ impl PartialEq for Person { } } +impl Display for Person { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Display for Instrument { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Eq for Instrument {} +impl PartialEq for Instrument { + fn eq(&self, other: &Self) -> bool { + self.instrument_id == other.instrument_id + } +} + +impl Display for Role { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + +impl Eq for Role {} +impl PartialEq for Role { + fn eq(&self, other: &Self) -> bool { + self.role_id == other.role_id + } +} + +impl Eq for Composer {} +impl PartialEq for Composer { + fn eq(&self, other: &Self) -> bool { + self.person == other.person && self.role == other.role + } +} + impl Work { pub fn from_table(data: tables::Work, connection: &mut SqliteConnection) -> Result { fn visit_children( @@ -95,11 +142,11 @@ impl Work { let parts = visit_children(&data.work_id, 0, connection)?; - let persons: Vec = persons::table - .inner_join(work_persons::table) + let persons: Vec = persons::table + .inner_join(work_persons::table.inner_join(roles::table)) .order(work_persons::sequence_number) .filter(work_persons::work_id.eq(&data.work_id)) - .select(tables::Person::as_select()) + .select(Composer::as_select()) .load(connection)?; let instruments: Vec = instruments::table @@ -119,9 +166,10 @@ impl Work { } pub fn composers_string(&self) -> String { + // TODO: Include roles except default composer. self.persons .iter() - .map(|p| p.name.get().to_string()) + .map(|p| p.person.name.get().to_string()) .collect::>() .join(", ") } @@ -134,6 +182,12 @@ impl PartialEq for Work { } } +impl Display for Work { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.composers_string(), self.name) + } +} + impl Ensemble { pub fn from_table(data: tables::Ensemble, connection: &mut SqliteConnection) -> Result { let persons: Vec<(Person, Instrument)> = persons::table @@ -158,6 +212,12 @@ impl PartialEq for Ensemble { } } +impl Display for Ensemble { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} + impl Recording { pub fn from_table( data: tables::Recording, @@ -227,6 +287,12 @@ impl Recording { } } +impl Display for Recording { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}; {}", self.work, self.performers_string()) + } +} + impl Performer { pub fn from_table( data: tables::RecordingPerson, diff --git a/src/db/tables.rs b/src/db/tables.rs index 052f0da..98c4d50 100644 --- a/src/db/tables.rs +++ b/src/db/tables.rs @@ -4,10 +4,12 @@ use chrono::NaiveDateTime; use diesel::prelude::*; use diesel::sqlite::Sqlite; +use gtk::glib::{self, Boxed}; use super::{schema::*, TranslatedString}; -#[derive(Insertable, Queryable, Selectable, Clone, Debug)] +#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)] +#[boxed_type(name = "MusicusPerson")] #[diesel(check_for_backend(Sqlite))] pub struct Person { pub person_id: String, @@ -18,7 +20,8 @@ pub struct Person { pub last_played_at: Option, } -#[derive(Insertable, Queryable, Selectable, Clone, Debug)] +#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)] +#[boxed_type(name = "MusicusRole")] #[diesel(check_for_backend(Sqlite))] pub struct Role { pub role_id: String, @@ -28,7 +31,8 @@ pub struct Role { pub last_used_at: NaiveDateTime, } -#[derive(Insertable, Queryable, Selectable, Clone, Debug)] +#[derive(Boxed, Insertable, Queryable, Selectable, Clone, Debug)] +#[boxed_type(name = "MusicusInstrument")] #[diesel(check_for_backend(Sqlite))] pub struct Instrument { pub instrument_id: String, diff --git a/src/editor/instrument_selector_popover.rs b/src/editor/instrument_selector_popover.rs new file mode 100644 index 0000000..29b11cc --- /dev/null +++ b/src/editor/instrument_selector_popover.rs @@ -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, + + pub instruments: RefCell>, + + #[template_child] + pub search_entry: TemplateChild, + #[template_child] + pub scrolled_window: TemplateChild, + #[template_child] + pub list_box: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for 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) { + 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> = 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) + @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( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_local("instrument-selected", true, move |values| { + let obj = values[0].get::().unwrap(); + let instrument = values[1].get::().unwrap(); + f(&obj, instrument); + None + }) + } + + #[template_callback] + fn search_changed(&self, entry: >k::SearchEntry) { + self.search(&entry.text()); + } + + #[template_callback] + fn activate(&self, _: >k::SearchEntry) { + if let Some(instrument) = self.imp().instruments.borrow().first() { + self.select(instrument.clone()); + } else { + self.create(); + } + } + + #[template_callback] + fn stop_search(&self, _: >k::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( + >k::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(>k::Image::builder().icon_name("list-add-symbolic").build()); + create_box.append( + >k::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(); + } +} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 0f41d6b..5085a6b 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -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; \ No newline at end of file +pub mod translation_section; +pub mod work_editor; +pub mod work_editor_composer_row; \ No newline at end of file diff --git a/src/editor/person_selector_popover.rs b/src/editor/person_selector_popover.rs new file mode 100644 index 0000000..debe598 --- /dev/null +++ b/src/editor/person_selector_popover.rs @@ -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, + + pub persons: RefCell>, + + #[template_child] + pub search_entry: TemplateChild, + #[template_child] + pub scrolled_window: TemplateChild, + #[template_child] + pub list_box: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for 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) { + 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> = 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) + @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( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_local("person-selected", true, move |values| { + let obj = values[0].get::().unwrap(); + let person = values[1].get::().unwrap(); + f(&obj, person); + None + }) + } + + #[template_callback] + fn search_changed(&self, entry: >k::SearchEntry) { + self.search(&entry.text()); + } + + #[template_callback] + fn activate(&self, _: >k::SearchEntry) { + if let Some(person) = self.imp().persons.borrow().first() { + self.select(person.clone()); + } else { + self.create(); + } + } + + #[template_callback] + fn stop_search(&self, _: >k::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( + >k::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(>k::Image::builder().icon_name("list-add-symbolic").build()); + create_box.append( + >k::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(); + } +} diff --git a/src/editor/role_selector_popover.rs b/src/editor/role_selector_popover.rs new file mode 100644 index 0000000..852eadd --- /dev/null +++ b/src/editor/role_selector_popover.rs @@ -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, + + pub roles: RefCell>, + + #[template_child] + pub search_entry: TemplateChild, + #[template_child] + pub scrolled_window: TemplateChild, + #[template_child] + pub list_box: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for 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) { + 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> = 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) + @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( + &self, + f: F, + ) -> glib::SignalHandlerId { + self.connect_local("role-selected", true, move |values| { + let obj = values[0].get::().unwrap(); + let role = values[1].get::().unwrap(); + f(&obj, role); + None + }) + } + + #[template_callback] + fn search_changed(&self, entry: >k::SearchEntry) { + self.search(&entry.text()); + } + + #[template_callback] + fn activate(&self, _: >k::SearchEntry) { + if let Some(role) = self.imp().roles.borrow().first() { + self.select(role.clone()); + } else { + self.create(); + } + } + + #[template_callback] + fn stop_search(&self, _: >k::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( + >k::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(>k::Image::builder().icon_name("list-add-symbolic").build()); + create_box.append( + >k::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(); + } +} diff --git a/src/editor/work_editor.rs b/src/editor/work_editor.rs new file mode 100644 index 0000000..eb9ea47 --- /dev/null +++ b/src/editor/work_editor.rs @@ -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, + + // 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>, + pub instruments: RefCell>, + + pub persons_popover: OnceCell, + pub instruments_popover: OnceCell, + + #[template_child] + pub composer_list: TemplateChild, + #[template_child] + pub select_person_box: TemplateChild, + #[template_child] + pub instrument_list: TemplateChild, + #[template_child] + pub select_instrument_box: TemplateChild, + } + + #[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) { + 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) + @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(); + } +} diff --git a/src/editor/work_editor_composer_row.rs b/src/editor/work_editor_composer_row.rs new file mode 100644 index 0000000..1e1b5c4 --- /dev/null +++ b/src/editor/work_editor_composer_row.rs @@ -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, + + pub composer: RefCell>, + pub role_popover: OnceCell, + + #[template_child] + pub role_label: TemplateChild, + #[template_child] + pub role_box: TemplateChild, + } + + #[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) { + obj.init_template(); + } + } + + #[glib::derived_properties] + impl ObjectImpl for MusicusWorkEditorComposerRow { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + 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) + @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(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("remove", true, move |values| { + let obj = values[0].get::().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, _: >k::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, _: >k::Button) { + self.emit_by_name::<()>("remove", &[]); + } +} diff --git a/src/library.rs b/src/library.rs index f4b347b..41fdea3 100644 --- a/src/library.rs +++ b/src/library.rs @@ -357,6 +357,57 @@ impl MusicusLibrary { ), } } + + pub fn search_persons(&self, search: &str) -> Result> { + let search = format!("%{}%", search); + let mut binding = self.imp().connection.borrow_mut(); + let connection = &mut *binding.as_mut().unwrap(); + + let persons = persons::table + .order(persons::last_used_at.desc()) + .filter(persons::name.like(&search)) + .limit(20) + .load(connection)?; + + Ok(persons) + } + + pub fn search_roles(&self, search: &str) -> Result> { + let search = format!("%{}%", search); + let mut binding = self.imp().connection.borrow_mut(); + let connection = &mut *binding.as_mut().unwrap(); + + let roles = roles::table + .order(roles::last_used_at.desc()) + .filter(roles::name.like(&search)) + .limit(20) + .load(connection)?; + + Ok(roles) + } + + pub fn search_instruments(&self, search: &str) -> Result> { + let search = format!("%{}%", search); + let mut binding = self.imp().connection.borrow_mut(); + let connection = &mut *binding.as_mut().unwrap(); + + let instruments = instruments::table + .order(instruments::last_used_at.desc()) + .filter(instruments::name.like(&search)) + .limit(20) + .load(connection)?; + + Ok(instruments) + } + + pub fn composer_default_role(&self) -> Result { + let mut binding = self.imp().connection.borrow_mut(); + let connection = &mut *binding.as_mut().unwrap(); + + Ok(roles::table + .filter(roles::role_id.eq("380d7e09eb2f49c1a90db2ba4acb6ffd")) + .first::(connection)?) + } } #[derive(Default, Debug)] diff --git a/src/library_manager.rs b/src/library_manager.rs index 9ff95e4..481a7a6 100644 --- a/src/library_manager.rs +++ b/src/library_manager.rs @@ -5,7 +5,7 @@ use adw::{ use gtk::glib::{self, Properties}; use std::cell::OnceCell; -use crate::editor::person_editor::MusicusPersonEditor; +use crate::editor::work_editor::MusicusWorkEditor; use crate::library::MusicusLibrary; mod imp { @@ -39,7 +39,7 @@ mod imp { impl ObjectImpl for LibraryManager { fn constructed(&self) { self.parent_constructed(); - self.obj().set_child(Some(&MusicusPersonEditor::new())); + self.obj().set_child(Some(&MusicusWorkEditor::new(self.library.get().unwrap()))); } } diff --git a/src/util.rs b/src/util.rs index cda86b5..b33fb3c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -11,7 +11,7 @@ lazy_static! { }, None => "generic".to_string(), }; - + log::info!("Intialized user language to '{lang}'."); lang };