From 6abd4504527b297915586dddab147ab1260c8140 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sun, 31 Jan 2021 20:03:20 +0100 Subject: [PATCH] Share UI between screens The recording screen was reverted to a dummy in the process. --- res/musicus.gresource.xml | 6 +- res/ui/ensemble_screen.ui | 126 ----------- res/ui/person_screen.ui | 155 ------------- res/ui/recording_screen.ui | 127 ----------- res/ui/screen.ui | 86 +++++++ res/ui/section.ui | 28 +++ res/ui/work_screen.ui | 139 ------------ src/meson.build | 10 +- .../{ensemble_screen.rs => ensemble.rs} | 102 ++++----- src/screens/mod.rs | 16 +- src/screens/{person_screen.rs => person.rs} | 144 +++++------- src/screens/recording.rs | 106 +++++++++ src/screens/recording_screen.rs | 213 ------------------ src/screens/{work_screen.rs => work.rs} | 107 ++++----- src/widgets/mod.rs | 6 + src/widgets/screen.rs | 113 ++++++++++ src/widgets/section.rs | 48 ++++ 17 files changed, 555 insertions(+), 977 deletions(-) delete mode 100644 res/ui/ensemble_screen.ui delete mode 100644 res/ui/person_screen.ui delete mode 100644 res/ui/recording_screen.ui create mode 100644 res/ui/screen.ui create mode 100644 res/ui/section.ui delete mode 100644 res/ui/work_screen.ui rename src/screens/{ensemble_screen.rs => ensemble.rs} (65%) rename src/screens/{person_screen.rs => person.rs} (60%) create mode 100644 src/screens/recording.rs delete mode 100644 src/screens/recording_screen.rs rename src/screens/{work_screen.rs => work.rs} (63%) create mode 100644 src/widgets/screen.rs create mode 100644 src/widgets/section.rs diff --git a/res/musicus.gresource.xml b/res/musicus.gresource.xml index 9784187..f1dbbbd 100644 --- a/res/musicus.gresource.xml +++ b/res/musicus.gresource.xml @@ -2,20 +2,19 @@ ui/ensemble_editor.ui - ui/ensemble_screen.ui ui/instrument_editor.ui ui/login_dialog.ui ui/medium_editor.ui ui/performance_editor.ui ui/person_editor.ui - ui/person_screen.ui ui/player_bar.ui ui/player_screen.ui ui/poe_list.ui ui/preferences.ui ui/recording_editor.ui - ui/recording_screen.ui ui/register_dialog.ui + ui/screen.ui + ui/section.ui ui/selector.ui ui/server_dialog.ui ui/source_selector.ui @@ -25,7 +24,6 @@ ui/window.ui ui/work_editor.ui ui/work_part_editor.ui - ui/work_screen.ui ui/work_section_editor.ui diff --git a/res/ui/ensemble_screen.ui b/res/ui/ensemble_screen.ui deleted file mode 100644 index 02cde11..0000000 --- a/res/ui/ensemble_screen.ui +++ /dev/null @@ -1,126 +0,0 @@ - - - - - - vertical - - - - - Ensemble - - - - - - go-previous-symbolic - - - - - view-more-symbolic - menu - - - - - edit-find-symbolic - - - - - - - False - - - 400 - true - - - Search recordings … - - - - - - - - - - - loading - - - True - - - - - - - content - - - true - - - 12 - 12 - 18 - 12 - 800 - - - vertical - 12 - - - start - Recordings - - - - - - - - - - - - - - - - - - - - nothing - - - No recordings found. - - - - - - - - -
- - Edit ensemble - widget.edit - - - Delete ensemble - widget.delete - -
-
-
diff --git a/res/ui/person_screen.ui b/res/ui/person_screen.ui deleted file mode 100644 index 4a1aeda..0000000 --- a/res/ui/person_screen.ui +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - vertical - - - - - Person - - - - - - go-previous-symbolic - - - - - menu - view-more-symbolic - - - - - edit-find-symbolic - - - - - - - False - - - 400 - true - - - Search works and recordings … - - - - - - - - - - - loading - - - true - true - center - center - true - - - - - - - content - - - true - - - 12 - 12 - 18 - 12 - 800 - - - vertical - 18 - - - vertical - 12 - - - start - Works - - - - - - - - - - - - - - vertical - 12 - - - start - Recordings - - - - - - - - - - - - - - - - - - - - - - nothing - - - No works or recordings found. - - - - - - - - -
- - Edit person - widget.edit - - - Delete person - widget.delete - -
-
-
diff --git a/res/ui/recording_screen.ui b/res/ui/recording_screen.ui deleted file mode 100644 index 6a0a640..0000000 --- a/res/ui/recording_screen.ui +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - vertical - - - - - vertical - center - - - Recording - - - - - - - - - - - - - go-previous-symbolic - - - - - view-more-symbolic - menu - - - - - - - - - loading - - - True - - - - - - - content - - - true - - - 12 - 12 - 18 - 12 - 800 - - - vertical - 12 - - - start - Tracks - - - - - - - - - - - - Add to playlist - end - - - - - - - - - - - - - - - -
- - Edit recording - widget.edit - - - Delete recording - widget.delete - -
-
- - Edit tracks - widget.edit-tracks - - - Delete tracks - widget.delete-tracks - -
-
-
diff --git a/res/ui/screen.ui b/res/ui/screen.ui new file mode 100644 index 0000000..2027a6c --- /dev/null +++ b/res/ui/screen.ui @@ -0,0 +1,86 @@ + + + + + + vertical + + + + + + + + go-previous-symbolic + + + + + menu + view-more-symbolic + + + + + edit-find-symbolic + + + + + + + False + + + true + + + + + + + + + + + + loading + + + true + true + center + center + 32 + 32 + true + + + + + + + content + + + + + + + vertical + 12 + 12 + 36 + + + + + + + + + + + + + diff --git a/res/ui/section.ui b/res/ui/section.ui new file mode 100644 index 0000000..2d354d1 --- /dev/null +++ b/res/ui/section.ui @@ -0,0 +1,28 @@ + + + + + vertical + 6 + + + 12 + + + end + 0.0 + end + true + 18 + + + + + + + + + + + + diff --git a/res/ui/work_screen.ui b/res/ui/work_screen.ui deleted file mode 100644 index 44f4ca0..0000000 --- a/res/ui/work_screen.ui +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - vertical - - - - - vertical - center - - - Work - - - - - - - - - - - - - go-previous-symbolic - - - - - view-more-symbolic - menu - - - - - edit-find-symbolic - - - - - - - False - - - 400 - true - - - Search recordings … - - - - - - - - - - - loading - - - True - - - - - - - content - - - true - - - 12 - 12 - 18 - 12 - 800 - - - vertical - 12 - - - start - Recordings - - - - - - - - - - - - - - - - - - - - nothing - - - No recordings found. - - - - - - - - -
- - Edit work - widget.edit - - - Delete work - widget.delete - -
-
-
diff --git a/src/meson.build b/src/meson.build index 85c77f1..6c97a64 100644 --- a/src/meson.build +++ b/src/meson.build @@ -77,12 +77,12 @@ sources = files( 'import/track_editor.rs', 'import/track_selector.rs', 'import/track_set_editor.rs', - 'screens/ensemble_screen.rs', + 'screens/ensemble.rs', 'screens/mod.rs', - 'screens/person_screen.rs', + 'screens/person.rs', 'screens/player_screen.rs', - 'screens/recording_screen.rs', - 'screens/work_screen.rs', + 'screens/recording.rs', + 'screens/work.rs', 'selectors/ensemble.rs', 'selectors/instrument.rs', 'selectors/mod.rs', @@ -97,6 +97,8 @@ sources = files( 'widgets/navigator_window.rs', 'widgets/player_bar.rs', 'widgets/poe_list.rs', + 'widgets/screen.rs', + 'widgets/section.rs', 'config.rs', 'config.rs.in', 'main.rs', diff --git a/src/screens/ensemble_screen.rs b/src/screens/ensemble.rs similarity index 65% rename from src/screens/ensemble_screen.rs rename to src/screens/ensemble.rs index f63aea1..dcd5d91 100644 --- a/src/screens/ensemble_screen.rs +++ b/src/screens/ensemble.rs @@ -1,74 +1,72 @@ -use super::*; -use crate::backend::*; -use crate::database::*; +use super::RecordingScreen; + +use crate::backend::Backend; +use crate::database::{Ensemble, Recording}; use crate::editors::EnsembleEditor; -use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; -use gio::prelude::*; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section}; + +use gettextrs::gettext; use glib::clone; use gtk::prelude::*; -use gtk_macros::get_widget; use libadwaita::prelude::*; use std::cell::RefCell; use std::rc::Rc; +/// A screen for showing recordings with a ensemble. pub struct EnsembleScreen { backend: Rc, ensemble: Ensemble, - widget: gtk::Box, - search_entry: gtk::SearchEntry, - stack: gtk::Stack, + widget: Screen, recording_list: Rc, recordings: RefCell>, navigator: RefCell>>, } impl EnsembleScreen { + /// Create a new ensemble screen for the specified ensemble and load the + /// contents asynchronously. pub fn new(backend: Rc, ensemble: Ensemble) -> Rc { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::Frame, recording_frame); - - title_label.set_label(&ensemble.name); - - let edit_action = gio::SimpleAction::new("edit", None); - let delete_action = gio::SimpleAction::new("delete", None); - - let actions = gio::SimpleActionGroup::new(); - actions.add_action(&edit_action); - actions.add_action(&delete_action); - - widget.insert_action_group("widget", Some(&actions)); + let widget = Screen::new(); + widget.set_title(&ensemble.name); let recording_list = List::new(); - recording_frame.set_child(Some(&recording_list.widget)); let this = Rc::new(Self { backend, ensemble, widget, - search_entry, - stack, recording_list, recordings: RefCell::new(Vec::new()), navigator: RefCell::new(None), }); - this.search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.recording_list.invalidate_filter(); - })); - - back_button.connect_clicked(clone!(@strong this => move |_| { + this.widget.set_back_cb(clone!(@strong this => move || { let navigator = this.navigator.borrow().clone(); if let Some(navigator) = navigator { navigator.pop(); } })); + + this.widget.add_action(&gettext("Edit ensemble"), clone!(@strong this => move || { + let editor = EnsembleEditor::new(this.backend.clone(), Some(this.ensemble.clone())); + let window = NavigatorWindow::new(editor); + window.show(); + })); + + this.widget.add_action(&gettext("Delete ensemble"), clone!(@strong this => move || { + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + clone.backend.db().delete_ensemble(&clone.ensemble.id).await.unwrap(); + clone.backend.library_changed(); + }); + })); + + this.widget.set_search_cb(clone!(@strong this => move || { + this.recording_list.invalidate_filter(); + })); + this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| { let recording = &this.recordings.borrow()[index]; @@ -90,28 +88,16 @@ impl EnsembleScreen { this.recording_list.set_filter_cb(clone!(@strong this => move |index| { let recording = &this.recordings.borrow()[index]; - let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); + let search = this.widget.get_search(); let text = recording.work.get_title() + &recording.get_performers(); search.is_empty() || text.to_lowercase().contains(&search) })); - edit_action.connect_activate(clone!(@strong this => move |_, _| { - let editor = EnsembleEditor::new(this.backend.clone(), Some(this.ensemble.clone())); - let window = NavigatorWindow::new(editor); - window.show(); - })); - - delete_action.connect_activate(clone!(@strong this => move |_, _| { - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - clone.backend.db().delete_ensemble(&clone.ensemble.id).await.unwrap(); - clone.backend.library_changed(); - }); - })); + // Load the content asynchronously. let context = glib::MainContext::default(); - let clone = this.clone(); + let clone = Rc::clone(&this); + context.spawn_local(async move { let recordings = clone .backend @@ -120,14 +106,16 @@ impl EnsembleScreen { .await .unwrap(); - if recordings.is_empty() { - clone.stack.set_visible_child_name("nothing"); - } else { + if !recordings.is_empty() { let length = recordings.len(); clone.recordings.replace(recordings); clone.recording_list.update(length); - clone.stack.set_visible_child_name("content"); + + let section = Section::new("Recordings", &clone.recording_list.widget); + clone.widget.add_content(§ion.widget); } + + clone.widget.ready(); }); this @@ -140,7 +128,7 @@ impl NavigatorScreen for EnsembleScreen { } fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() + self.widget.widget.clone().upcast() } fn detach_navigator(&self) { diff --git a/src/screens/mod.rs b/src/screens/mod.rs index 5543659..395629d 100644 --- a/src/screens/mod.rs +++ b/src/screens/mod.rs @@ -1,14 +1,14 @@ -pub mod ensemble_screen; -pub use ensemble_screen::*; +pub mod ensemble; +pub use ensemble::*; -pub mod person_screen; -pub use person_screen::*; +pub mod person; +pub use person::*; pub mod player_screen; pub use player_screen::*; -pub mod work_screen; -pub use work_screen::*; +pub mod work; +pub use work::*; -pub mod recording_screen; -pub use recording_screen::*; +pub mod recording; +pub use recording::*; diff --git a/src/screens/person_screen.rs b/src/screens/person.rs similarity index 60% rename from src/screens/person_screen.rs rename to src/screens/person.rs index 9937acc..6f0c2e1 100644 --- a/src/screens/person_screen.rs +++ b/src/screens/person.rs @@ -1,25 +1,23 @@ -use super::*; -use crate::backend::*; -use crate::database::*; +use super::{WorkScreen, RecordingScreen}; + +use crate::backend::Backend; +use crate::database::{Person, Recording, Work}; use crate::editors::PersonEditor; -use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; -use gio::prelude::*; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section}; + +use gettextrs::gettext; use glib::clone; use gtk::prelude::*; -use gtk_macros::get_widget; use libadwaita::prelude::*; use std::cell::RefCell; use std::rc::Rc; +/// A screen for showing works by and recordings with a person. pub struct PersonScreen { backend: Rc, person: Person, - widget: gtk::Box, - stack: gtk::Stack, - search_entry: gtk::SearchEntry, - work_box: gtk::Box, + widget: Screen, work_list: Rc, - recording_box: gtk::Box, recording_list: Rc, works: RefCell>, recordings: RefCell>, @@ -27,62 +25,54 @@ pub struct PersonScreen { } impl PersonScreen { + /// Create a new person screen for the specified person and load the + /// contents asynchronously. pub fn new(backend: Rc, person: Person) -> Rc { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::Box, work_box); - get_widget!(builder, gtk::Frame, work_frame); - get_widget!(builder, gtk::Box, recording_box); - get_widget!(builder, gtk::Frame, recording_frame); - - title_label.set_label(&person.name_fl()); - - let edit_action = gio::SimpleAction::new("edit", None); - let delete_action = gio::SimpleAction::new("delete", None); - - let actions = gio::SimpleActionGroup::new(); - actions.add_action(&edit_action); - actions.add_action(&delete_action); - - widget.insert_action_group("widget", Some(&actions)); + let widget = Screen::new(); + widget.set_title(&person.name_fl()); let work_list = List::new(); let recording_list = List::new(); - work_frame.set_child(Some(&work_list.widget)); - recording_frame.set_child(Some(&recording_list.widget)); let this = Rc::new(Self { backend, person, widget, - stack, - search_entry, - work_box, work_list, - recording_box, recording_list, works: RefCell::new(Vec::new()), recordings: RefCell::new(Vec::new()), navigator: RefCell::new(None), }); - this.search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.work_list.invalidate_filter(); - this.recording_list.invalidate_filter(); - })); - - back_button.connect_clicked(clone!(@strong this => move |_| { + this.widget.set_back_cb(clone!(@strong this => move || { let navigator = this.navigator.borrow().clone(); if let Some(navigator) = navigator { - navigator.clone().pop(); + navigator.pop(); } })); + + this.widget.add_action(&gettext("Edit person"), clone!(@strong this => move || { + let editor = PersonEditor::new(this.backend.clone(), Some(this.person.clone())); + let window = NavigatorWindow::new(editor); + window.show(); + })); + + this.widget.add_action(&gettext("Delete person"), clone!(@strong this => move || { + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + clone.backend.db().delete_person(&clone.person.id).await.unwrap(); + clone.backend.library_changed(); + }); + })); + + this.widget.set_search_cb(clone!(@strong this => move || { + this.work_list.invalidate_filter(); + this.recording_list.invalidate_filter(); + })); + this.work_list.set_make_widget_cb(clone!(@strong this => move |index| { let work = &this.works.borrow()[index]; @@ -103,7 +93,7 @@ impl PersonScreen { this.work_list.set_filter_cb(clone!(@strong this => move |index| { let work = &this.works.borrow()[index]; - let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); + let search = this.widget.get_search(); let title = work.title.to_lowercase(); search.is_empty() || title.contains(&search) })); @@ -129,28 +119,16 @@ impl PersonScreen { this.recording_list.set_filter_cb(clone!(@strong this => move |index| { let recording = &this.recordings.borrow()[index]; - let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); + let search = this.widget.get_search(); let text = recording.work.get_title() + &recording.get_performers(); - search.is_empty() || text.contains(&search) + search.is_empty() || text.to_lowercase().contains(&search) })); - edit_action.connect_activate(clone!(@strong this => move |_, _| { - let editor = PersonEditor::new(this.backend.clone(), Some(this.person.clone())); - let window = NavigatorWindow::new(editor); - window.show(); - })); - - delete_action.connect_activate(clone!(@strong this => move |_, _| { - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - clone.backend.db().delete_person(&clone.person.id).await.unwrap(); - clone.backend.library_changed(); - }); - })); + // Load the content asynchronously. let context = glib::MainContext::default(); - let clone = this.clone(); + let clone = Rc::clone(&this); + context.spawn_local(async move { let works = clone .backend @@ -166,27 +144,25 @@ impl PersonScreen { .await .unwrap(); - if works.is_empty() && recordings.is_empty() { - clone.stack.set_visible_child_name("nothing"); - } else { - if works.is_empty() { - clone.work_box.hide(); - } else { - let length = works.len(); - clone.works.replace(works); - clone.work_list.update(length); - } + if !works.is_empty() { + let length = works.len(); + clone.works.replace(works); + clone.work_list.update(length); - if recordings.is_empty() { - clone.recording_box.hide(); - } else { - let length = recordings.len(); - clone.recordings.replace(recordings); - clone.recording_list.update(length); - } - - clone.stack.set_visible_child_name("content"); + let section = Section::new("Works", &clone.work_list.widget); + clone.widget.add_content(§ion.widget); } + + if !recordings.is_empty() { + let length = recordings.len(); + clone.recordings.replace(recordings); + clone.recording_list.update(length); + + let section = Section::new("Recordings", &clone.recording_list.widget); + clone.widget.add_content(§ion.widget); + } + + clone.widget.ready(); }); this @@ -199,7 +175,7 @@ impl NavigatorScreen for PersonScreen { } fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() + self.widget.widget.clone().upcast() } fn detach_navigator(&self) { diff --git a/src/screens/recording.rs b/src/screens/recording.rs new file mode 100644 index 0000000..e363356 --- /dev/null +++ b/src/screens/recording.rs @@ -0,0 +1,106 @@ +use crate::backend::Backend; +use crate::database::Recording; +use crate::editors::RecordingEditor; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section}; + +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use libadwaita::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for showing a recording. +pub struct RecordingScreen { + backend: Rc, + recording: Recording, + widget: Screen, + track_list: Rc, + recordings: RefCell>, + navigator: RefCell>>, +} + +impl RecordingScreen { + /// Create a new recording screen for the specified recording and load the + /// contents asynchronously. + pub fn new(backend: Rc, recording: Recording) -> Rc { + let widget = Screen::new(); + widget.set_title(&recording.work.get_title()); + widget.set_subtitle(&recording.get_performers()); + + let track_list = List::new(); + + let this = Rc::new(Self { + backend, + recording, + widget, + track_list, + recordings: RefCell::new(Vec::new()), + navigator: RefCell::new(None), + }); + + this.widget.set_back_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + + this.widget.add_action(&gettext("Edit recording"), clone!(@strong this => move || { + let editor = RecordingEditor::new(this.backend.clone(), Some(this.recording.clone())); + let window = NavigatorWindow::new(editor); + window.show(); + })); + + this.widget.add_action(&gettext("Delete recording"), clone!(@strong this => move || { + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + clone.backend.db().delete_recording(&clone.recording.id).await.unwrap(); + clone.backend.library_changed(); + }); + })); + + this.widget.set_search_cb(clone!(@strong this => move || { + this.track_list.invalidate_filter(); + })); + + // TODO: Implement. + // this.track_list.set_make_widget_cb(clone!(@strong this => move |index| { + // })); + + this.track_list.set_filter_cb(clone!(@strong this => move |index| { + // TODO: Implement. + // search.is_empty() || text.to_lowercase().contains(&search) + true + })); + + // Load the content asynchronously. + + let context = glib::MainContext::default(); + let clone = Rc::clone(&this); + + context.spawn_local(async move { + // TODO: Implement. + + clone.widget.ready(); + }); + + this + } +} + +impl NavigatorScreen for RecordingScreen { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.widget.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/src/screens/recording_screen.rs b/src/screens/recording_screen.rs deleted file mode 100644 index 85b9303..0000000 --- a/src/screens/recording_screen.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::backend::*; -use crate::database::*; -use crate::editors::RecordingEditor; -use crate::player::*; -use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; -use gettextrs::gettext; -use gio::prelude::*; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libadwaita::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; - -/// Representation of one entry within the track list. -enum ListItem { - /// A track row. This hold an index to the track set and an index to the - /// track within the track set. - Track(usize, usize), - - /// A separator intended for use between track sets. - Separator, -} - -pub struct RecordingScreen { - backend: Rc, - recording: Recording, - widget: gtk::Box, - stack: gtk::Stack, - list: Rc, - track_sets: RefCell>, - items: RefCell>, - navigator: RefCell>>, -} - -impl RecordingScreen { - pub fn new(backend: Rc, recording: Recording) -> Rc { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Label, subtitle_label); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::Frame, frame); - get_widget!(builder, gtk::Button, add_to_playlist_button); - - title_label.set_label(&recording.work.get_title()); - subtitle_label.set_label(&recording.get_performers()); - - let edit_action = gio::SimpleAction::new("edit", None); - let delete_action = gio::SimpleAction::new("delete", None); - let edit_tracks_action = gio::SimpleAction::new("edit-tracks", None); - let delete_tracks_action = gio::SimpleAction::new("delete-tracks", None); - - let actions = gio::SimpleActionGroup::new(); - actions.add_action(&edit_action); - actions.add_action(&delete_action); - actions.add_action(&edit_tracks_action); - actions.add_action(&delete_tracks_action); - - widget.insert_action_group("widget", Some(&actions)); - - let list = List::new(); - frame.set_child(Some(&list.widget)); - - let this = Rc::new(Self { - backend, - recording, - widget, - stack, - list, - track_sets: RefCell::new(Vec::new()), - items: RefCell::new(Vec::new()), - navigator: RefCell::new(None), - }); - - this.list.set_make_widget_cb(clone!(@strong this => move |index| { - match this.items.borrow()[index] { - ListItem::Track(track_set_index, track_index) => { - let track_set = &this.track_sets.borrow()[track_set_index]; - let track = &track_set.tracks[track_index]; - - let mut title_parts = Vec::::new(); - for part in &track.work_parts { - title_parts.push(this.recording.work.parts[*part].title.clone()); - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let row = libadwaita::ActionRow::new(); - row.set_title(Some(&title)); - - row.upcast() - } - ListItem::Separator => { - let separator = gtk::Separator::new(gtk::Orientation::Horizontal); - separator.upcast() - } - } - })); - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.clone().pop(); - } - })); - - // TODO: Decide whether to handle multiple track sets. - add_to_playlist_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(player) = this.backend.get_player() { - if let Some(track_set) = this.track_sets.borrow().get(0).cloned() { - let indices = (0..track_set.tracks.len()).collect(); - - let playlist_item = PlaylistItem { - track_set, - indices, - }; - - player.add_item(playlist_item).unwrap(); - } - } - })); - - edit_action.connect_activate(clone!(@strong this => move |_, _| { - let editor = RecordingEditor::new(this.backend.clone(), Some(this.recording.clone())); - let window = NavigatorWindow::new(editor); - window.show(); - })); - - delete_action.connect_activate(clone!(@strong this => move |_, _| { - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - clone.backend.db().delete_recording(&clone.recording.id).await.unwrap(); - clone.backend.library_changed(); - }); - })); - - edit_tracks_action.connect_activate(clone!(@strong this => move |_, _| { - // let editor = TracksEditor::new(this.backend.clone(), Some(this.recording.clone()), this.tracks.borrow().clone()); - // let window = NavigatorWindow::new(editor); - // window.show(); - })); - - delete_tracks_action.connect_activate(clone!(@strong this => move |_, _| { - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - // clone.backend.db().delete_tracks(&clone.recording.id).await.unwrap(); - // clone.backend.library_changed(); - }); - })); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let track_sets = clone - .backend - .db() - .get_track_sets(&clone.recording.id) - .await - .unwrap(); - - clone.show_track_sets(track_sets); - clone.stack.set_visible_child_name("content"); - }); - - this - } - - /// Update the track sets variable as well as the user interface. - fn show_track_sets(&self, track_sets: Vec) { - let mut first = true; - let mut items = Vec::new(); - - for (track_set_index, track_set) in track_sets.iter().enumerate() { - if !first { - items.push(ListItem::Separator); - } else { - first = false; - } - - for (track_index, _) in track_set.tracks.iter().enumerate() { - items.push(ListItem::Track(track_set_index, track_index)); - } - } - - let length = items.len(); - self.items.replace(items); - self.track_sets.replace(track_sets); - self.list.update(length); - } -} - -impl NavigatorScreen for RecordingScreen { - fn attach_navigator(&self, navigator: Rc) { - self.navigator.replace(Some(navigator)); - } - - fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() - } - - fn detach_navigator(&self) { - self.navigator.replace(None); - } -} diff --git a/src/screens/work_screen.rs b/src/screens/work.rs similarity index 63% rename from src/screens/work_screen.rs rename to src/screens/work.rs index 2f8ce60..b969426 100644 --- a/src/screens/work_screen.rs +++ b/src/screens/work.rs @@ -1,76 +1,73 @@ -use super::*; -use crate::backend::*; -use crate::database::*; +use super::RecordingScreen; + +use crate::backend::Backend; +use crate::database::{Work, Recording}; use crate::editors::WorkEditor; -use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; -use gio::prelude::*; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow, Screen, Section}; + +use gettextrs::gettext; use glib::clone; use gtk::prelude::*; -use gtk_macros::get_widget; use libadwaita::prelude::*; use std::cell::RefCell; use std::rc::Rc; +/// A screen for showing recordings of a work. pub struct WorkScreen { backend: Rc, work: Work, - widget: gtk::Box, - stack: gtk::Stack, - search_entry: gtk::SearchEntry, + widget: Screen, recording_list: Rc, recordings: RefCell>, navigator: RefCell>>, } impl WorkScreen { + /// Create a new work screen for the specified work and load the + /// contents asynchronously. pub fn new(backend: Rc, work: Work) -> Rc { - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Label, title_label); - get_widget!(builder, gtk::Label, subtitle_label); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::Frame, recording_frame); - - title_label.set_label(&work.composer.name_fl()); - subtitle_label.set_label(&work.title); - - let edit_action = gio::SimpleAction::new("edit", None); - let delete_action = gio::SimpleAction::new("delete", None); - - let actions = gio::SimpleActionGroup::new(); - actions.add_action(&edit_action); - actions.add_action(&delete_action); - - widget.insert_action_group("widget", Some(&actions)); + let widget = Screen::new(); + widget.set_title(&work.title); + widget.set_subtitle(&work.composer.name_fl()); let recording_list = List::new(); - recording_frame.set_child(Some(&recording_list.widget)); let this = Rc::new(Self { backend, work, widget, - stack, - search_entry, recording_list, recordings: RefCell::new(Vec::new()), navigator: RefCell::new(None), }); - this.search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.recording_list.invalidate_filter(); - })); - - back_button.connect_clicked(clone!(@strong this => move |_| { + this.widget.set_back_cb(clone!(@strong this => move || { let navigator = this.navigator.borrow().clone(); if let Some(navigator) = navigator { - navigator.clone().pop(); + navigator.pop(); } })); + + this.widget.add_action(&gettext("Edit work"), clone!(@strong this => move || { + let editor = WorkEditor::new(this.backend.clone(), Some(this.work.clone())); + let window = NavigatorWindow::new(editor); + window.show(); + })); + + this.widget.add_action(&gettext("Delete work"), clone!(@strong this => move || { + let context = glib::MainContext::default(); + let clone = this.clone(); + context.spawn_local(async move { + clone.backend.db().delete_work(&clone.work.id).await.unwrap(); + clone.backend.library_changed(); + }); + })); + + this.widget.set_search_cb(clone!(@strong this => move || { + this.recording_list.invalidate_filter(); + })); + this.recording_list.set_make_widget_cb(clone!(@strong this => move |index| { let recording = &this.recordings.borrow()[index]; @@ -92,28 +89,16 @@ impl WorkScreen { this.recording_list.set_filter_cb(clone!(@strong this => move |index| { let recording = &this.recordings.borrow()[index]; - let search = this.search_entry.get_text().unwrap().to_string().to_lowercase(); + let search = this.widget.get_search(); let text = recording.work.get_title() + &recording.get_performers(); search.is_empty() || text.to_lowercase().contains(&search) })); - edit_action.connect_activate(clone!(@strong this => move |_, _| { - let editor = WorkEditor::new(this.backend.clone(), Some(this.work.clone())); - let window = NavigatorWindow::new(editor); - window.show(); - })); - - delete_action.connect_activate(clone!(@strong this => move |_, _| { - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - clone.backend.db().delete_work(&clone.work.id).await.unwrap(); - clone.backend.library_changed(); - }); - })); + // Load the content asynchronously. let context = glib::MainContext::default(); - let clone = this.clone(); + let clone = Rc::clone(&this); + context.spawn_local(async move { let recordings = clone .backend @@ -122,14 +107,16 @@ impl WorkScreen { .await .unwrap(); - if recordings.is_empty() { - clone.stack.set_visible_child_name("nothing"); - } else { + if !recordings.is_empty() { let length = recordings.len(); clone.recordings.replace(recordings); clone.recording_list.update(length); - clone.stack.set_visible_child_name("content"); + + let section = Section::new("Recordings", &clone.recording_list.widget); + clone.widget.add_content(§ion.widget); } + + clone.widget.ready(); }); this @@ -142,7 +129,7 @@ impl NavigatorScreen for WorkScreen { } fn get_widget(&self) -> gtk::Widget { - self.widget.clone().upcast() + self.widget.widget.clone().upcast() } fn detach_navigator(&self) { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 923e3a2..7406ccc 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -13,4 +13,10 @@ pub use player_bar::*; pub mod poe_list; pub use poe_list::*; +pub mod screen; +pub use screen::*; + +pub mod section; +pub use section::*; + mod indexed_list_model; diff --git a/src/widgets/screen.rs b/src/widgets/screen.rs new file mode 100644 index 0000000..ff50514 --- /dev/null +++ b/src/widgets/screen.rs @@ -0,0 +1,113 @@ +use gio::prelude::*; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; + +/// A general framework for screens. Screens have a header bar with at least +/// a button to go back and a scrollable content area that clamps its content. +pub struct Screen { + /// The actual GTK widget. + pub widget: gtk::Box, + + /// The button to switch to the previous screen. + back_button: gtk::Button, + + /// The title widget within the header bar. + window_title: libadwaita::WindowTitle, + + /// The action menu. + menu: gio::Menu, + + /// The entry for searching. + search_entry: gtk::SearchEntry, + + /// The stack to switch to the loading page. + stack: gtk::Stack, + + /// The box containing the content. + content_box: gtk::Box, + + /// The actions for the menu. + actions: gio::SimpleActionGroup, +} + +impl Screen { + /// Create a new screen. + pub fn new() -> Self { + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/screen.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, libadwaita::WindowTitle, window_title); + get_widget!(builder, gio::Menu, menu); + get_widget!(builder, gtk::ToggleButton, search_button); + get_widget!(builder, gtk::SearchEntry, search_entry); + get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::Box, content_box); + + let actions = gio::SimpleActionGroup::new(); + widget.insert_action_group("widget", Some(&actions)); + + search_button.connect_toggled(clone!(@strong search_entry => move |search_button| { + if search_button.get_active() { + search_entry.grab_focus(); + } + })); + + Self { + widget, + back_button, + window_title, + menu, + search_entry, + stack, + content_box, + actions, + } + } + + /// Set a closure to be called when the back button is pressed. + pub fn set_back_cb(&self, cb: F) { + self.back_button.connect_clicked(move |_| cb()); + } + + /// Show a title in the header bar. + pub fn set_title(&self, title: &str) { + self.window_title.set_title(Some(title)); + } + + /// Show a subtitle in the header bar. + pub fn set_subtitle(&self, subtitle: &str) { + self.window_title.set_subtitle(Some(subtitle)); + } + + /// Add a new item to the action menu and register a callback for it. + pub fn add_action(&self, label: &str, cb: F) { + let name = rand::random::().to_string(); + let action = gio::SimpleAction::new(&name, None); + action.connect_activate(move |_, _| cb()); + + self.actions.add_action(&action); + self.menu.append(Some(label), Some(&format!("widget.{}", name))); + } + + /// Set the closure to be called when the search string has changed. + pub fn set_search_cb(&self, cb: F) { + self.search_entry.connect_search_changed(move |_| cb()); + } + + /// Get the current search string. + pub fn get_search(&self) -> String { + self.search_entry.get_text().unwrap().to_string().to_lowercase() + } + + /// Hide the loading page and switch to the content. + pub fn ready(&self) { + self.stack.set_visible_child_name("content"); + } + + /// Add content to the bottom of the content area. + pub fn add_content>(&self, content: &W) { + self.content_box.append(content); + } +} diff --git a/src/widgets/section.rs b/src/widgets/section.rs new file mode 100644 index 0000000..6b4f37a --- /dev/null +++ b/src/widgets/section.rs @@ -0,0 +1,48 @@ +use gtk::prelude::*; +use gtk_macros::get_widget; + +/// A widget displaying a title, a framed child widget and, if needed, some +/// actions. +pub struct Section { + /// The actual GTK widget. + pub widget: gtk::Box, + + /// The box containing the title and action buttons. + title_box: gtk::Box, +} + +impl Section { + /// Create a new section. + pub fn new>(title: &str, content: &W) -> Self { + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Box, title_box); + get_widget!(builder, gtk::Label, title_label); + get_widget!(builder, gtk::Frame, frame); + + title_label.set_label(title); + frame.set_child(Some(content)); + + Self { + widget, + title_box, + } + } + + /// Add an action button. This should by definition be something that is + /// doing something with the child widget that is applicable in all + /// situations where the widget is visible. The new button will be packed + /// to the end of the title box. + pub fn add_action(&self, icon_name: &str, cb: F) { + let button = gtk::ButtonBuilder::new() + .has_frame(false) + .valign(gtk::Align::Center) + .icon_name(icon_name) + .build(); + + button.connect_clicked(move |_| cb()); + + self.title_box.append(&button); + } +}