From 0e7a2f1f3dc247f442d12a725fc9f5dd84a7dd6d Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 2 Dec 2020 15:41:19 +0100 Subject: [PATCH] Convert dialogs to navigator screens --- musicus/res/musicus.gresource.xml | 5 +- musicus/res/ui/ensemble_editor.ui | 216 ++++---- musicus/res/ui/instrument_editor.ui | 216 ++++---- musicus/res/ui/performance_editor.ui | 88 +-- musicus/res/ui/person_editor.ui | 216 ++++---- musicus/res/ui/recording_selector.ui | 2 +- musicus/res/ui/selector.ui | 264 +++++++++ musicus/res/ui/track_editor.ui | 142 +++-- musicus/res/ui/tracks_editor.ui | 85 +-- musicus/res/ui/work_editor.ui | 504 ++++++++++-------- .../{part_editor.ui => work_part_editor.ui} | 87 +-- ...ction_editor.ui => work_section_editor.ui} | 86 +-- musicus/res/ui/work_selector.ui | 18 + musicus/src/dialogs/ensemble_selector.rs | 162 ------ musicus/src/dialogs/instrument_selector.rs | 162 ------ musicus/src/dialogs/mod.rs | 30 -- musicus/src/dialogs/person_selector.rs | 163 ------ musicus/src/dialogs/recording/mod.rs | 11 - .../src/dialogs/recording/recording_dialog.rs | 86 --- .../recording/recording_editor_dialog.rs | 64 --- .../dialogs/recording/recording_selector.rs | 174 ------ .../recording_selector_person_screen.rs | 161 ------ .../recording_selector_work_screen.rs | 155 ------ musicus/src/dialogs/work/mod.rs | 11 - musicus/src/dialogs/work/work_dialog.rs | 86 --- .../src/dialogs/work/work_editor_dialog.rs | 64 --- musicus/src/dialogs/work/work_selector.rs | 174 ------ .../work/work_selector_person_screen.rs | 145 ----- .../ensemble.rs} | 62 ++- .../instrument.rs} | 62 ++- musicus/src/editors/mod.rs | 22 + .../performance.rs} | 110 ++-- .../person_editor.rs => editors/person.rs} | 57 +- .../recording.rs} | 134 +++-- .../track_editor.rs => editors/track.rs} | 71 ++- .../tracks_editor.rs => editors/tracks.rs} | 171 +++--- .../work/work_editor.rs => editors/work.rs} | 214 ++++---- .../part_editor.rs => editors/work_part.rs} | 77 +-- .../work_section.rs} | 48 +- musicus/src/main.rs | 11 +- musicus/src/meson.build | 43 +- musicus/src/screens/ensemble_screen.rs | 8 +- musicus/src/screens/person_screen.rs | 8 +- musicus/src/screens/recording_screen.rs | 12 +- musicus/src/screens/work_screen.rs | 8 +- musicus/src/selectors/ensemble.rs | 111 ++++ musicus/src/selectors/instrument.rs | 110 ++++ musicus/src/selectors/mod.rs | 16 + musicus/src/selectors/person.rs | 110 ++++ musicus/src/selectors/recording.rs | 114 ++++ musicus/src/selectors/selector.rs | 201 +++++++ musicus/src/selectors/work.rs | 113 ++++ musicus/src/widgets/mod.rs | 3 + musicus/src/widgets/navigator.rs | 15 +- musicus/src/widgets/navigator_window.rs | 42 ++ musicus/src/window.rs | 8 +- 56 files changed, 2650 insertions(+), 2888 deletions(-) create mode 100644 musicus/res/ui/selector.ui rename musicus/res/ui/{part_editor.ui => work_part_editor.ui} (73%) rename musicus/res/ui/{section_editor.ui => work_section_editor.ui} (53%) delete mode 100644 musicus/src/dialogs/ensemble_selector.rs delete mode 100644 musicus/src/dialogs/instrument_selector.rs delete mode 100644 musicus/src/dialogs/person_selector.rs delete mode 100644 musicus/src/dialogs/recording/mod.rs delete mode 100644 musicus/src/dialogs/recording/recording_dialog.rs delete mode 100644 musicus/src/dialogs/recording/recording_editor_dialog.rs delete mode 100644 musicus/src/dialogs/recording/recording_selector.rs delete mode 100644 musicus/src/dialogs/recording/recording_selector_person_screen.rs delete mode 100644 musicus/src/dialogs/recording/recording_selector_work_screen.rs delete mode 100644 musicus/src/dialogs/work/mod.rs delete mode 100644 musicus/src/dialogs/work/work_dialog.rs delete mode 100644 musicus/src/dialogs/work/work_editor_dialog.rs delete mode 100644 musicus/src/dialogs/work/work_selector.rs delete mode 100644 musicus/src/dialogs/work/work_selector_person_screen.rs rename musicus/src/{dialogs/ensemble_editor.rs => editors/ensemble.rs} (65%) rename musicus/src/{dialogs/instrument_editor.rs => editors/instrument.rs} (65%) create mode 100644 musicus/src/editors/mod.rs rename musicus/src/{dialogs/recording/performance_editor.rs => editors/performance.rs} (60%) rename musicus/src/{dialogs/person_editor.rs => editors/person.rs} (70%) rename musicus/src/{dialogs/recording/recording_editor.rs => editors/recording.rs} (63%) rename musicus/src/{dialogs/track_editor.rs => editors/track.rs} (62%) rename musicus/src/{dialogs/tracks_editor.rs => editors/tracks.rs} (61%) rename musicus/src/{dialogs/work/work_editor.rs => editors/work.rs} (62%) rename musicus/src/{dialogs/work/part_editor.rs => editors/work_part.rs} (61%) rename musicus/src/{dialogs/work/section_editor.rs => editors/work_section.rs} (57%) create mode 100644 musicus/src/selectors/ensemble.rs create mode 100644 musicus/src/selectors/instrument.rs create mode 100644 musicus/src/selectors/mod.rs create mode 100644 musicus/src/selectors/person.rs create mode 100644 musicus/src/selectors/recording.rs create mode 100644 musicus/src/selectors/selector.rs create mode 100644 musicus/src/selectors/work.rs create mode 100644 musicus/src/widgets/navigator_window.rs diff --git a/musicus/res/musicus.gresource.xml b/musicus/res/musicus.gresource.xml index 2e487fc..2a32a46 100644 --- a/musicus/res/musicus.gresource.xml +++ b/musicus/res/musicus.gresource.xml @@ -7,7 +7,6 @@ ui/instrument_editor.ui ui/instrument_selector.ui ui/login_dialog.ui - ui/part_editor.ui ui/performance_editor.ui ui/person_editor.ui ui/person_list.ui @@ -21,13 +20,15 @@ ui/recording_screen.ui ui/recording_selector.ui ui/recording_selector_screen.ui - ui/section_editor.ui + ui/selector.ui ui/server_dialog.ui ui/tracks_editor.ui ui/track_editor.ui ui/window.ui ui/work_editor.ui + ui/work_part_editor.ui ui/work_screen.ui + ui/work_section_editor.ui ui/work_selector.ui ui/work_selector_screen.ui diff --git a/musicus/res/ui/ensemble_editor.ui b/musicus/res/ui/ensemble_editor.ui index 4e3ec68..cb2f81d 100644 --- a/musicus/res/ui/ensemble_editor.ui +++ b/musicus/res/ui/ensemble_editor.ui @@ -2,57 +2,84 @@ - - + + + True False - True - True - dialog + crossfade - + True False - crossfade + vertical - + True False - vertical + Ensemble - + True - False - Ensemble + True + True - - Cancel + True - True - True + False + go-previous-symbolic - - - Save - True - True - True - - - - end - 1 - - + + + + + True + True + True + + + True + False + object-select-symbolic + + + - False - True - 0 + end + 1 + + + False + True + 0 + + + + + True + False + False + + + + + + False + True + 1 + + + + + True + False + 500 + 300 @@ -109,107 +136,54 @@ - - False - True - 1 - - - - - True - False - False - - - False - 6 - end - - - - - - False - False - 0 - - - - - False - 16 - - - True - False - Failed to save ensemble! - - - False - True - 0 - - - - - False - False - 0 - - - - - - - - False - True - 1 - - content + False + True + 2 + + + + + content + + + + + True + False + vertical + + + True + False + Ensemble + + + False + True + 0 - + True False - vertical - - - True - False - Ensemble - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 1 - - + True + True - loading + False + True 1 + + loading + 1 + diff --git a/musicus/res/ui/instrument_editor.ui b/musicus/res/ui/instrument_editor.ui index 25ad2b4..11e80c2 100644 --- a/musicus/res/ui/instrument_editor.ui +++ b/musicus/res/ui/instrument_editor.ui @@ -2,57 +2,84 @@ - - + + + True False - True - True - dialog + crossfade - + True False - crossfade + vertical - + True False - vertical + Instrument - + True - False - Instrument + True + True - - Cancel + True - True - True + False + go-previous-symbolic - - - Save - True - True - True - - - - end - 1 - - + + + + + True + True + True + + + True + False + object-select-symbolic + + + - False - True - 0 + end + 1 + + + False + True + 0 + + + + + True + False + False + + + + + + False + True + 1 + + + + + True + False + 500 + 300 @@ -109,107 +136,54 @@ - - False - True - 1 - - - - - True - False - False - - - False - 6 - end - - - - - - False - False - 0 - - - - - False - 16 - - - True - False - Failed to save instrument! - - - False - True - 0 - - - - - False - False - 0 - - - - - - - - False - True - 1 - - content + False + True + 2 + + + + + content + + + + + True + False + vertical + + + True + False + Instrument + + + False + True + 0 - + True False - vertical - - - True - False - Instrument - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 1 - - + True + True - loading + False + True 1 + + loading + 1 + diff --git a/musicus/res/ui/performance_editor.ui b/musicus/res/ui/performance_editor.ui index 04d8650..07e67b9 100644 --- a/musicus/res/ui/performance_editor.ui +++ b/musicus/res/ui/performance_editor.ui @@ -2,53 +2,65 @@ - - + + + True False - True - True - dialog + vertical - + True False - vertical + Performance - + True - False - Performance + True + True - - Cancel + True - True - True + False + go-previous-symbolic - - - Save - True - False - True - True - - - - end - 1 - - + + + + + True + False + True + True + + + True + False + object-select-symbolic + + + - False - True - 0 + end + 1 + + + False + True + 0 + + + + + True + False + 500 + 300 @@ -249,13 +261,13 @@ - - False - True - 1 - + + False + True + 1 + diff --git a/musicus/res/ui/person_editor.ui b/musicus/res/ui/person_editor.ui index 4e2490e..a75d52c 100644 --- a/musicus/res/ui/person_editor.ui +++ b/musicus/res/ui/person_editor.ui @@ -2,57 +2,84 @@ - - + + + True False - True - True - dialog + crossfade - + True False - crossfade + vertical - + True False - vertical + Person - + True - False - Person + True + True - - Cancel + True - True - True + False + go-previous-symbolic - - - Save - True - True - True - - - - end - 1 - - + + + + + True + True + True + + + True + False + object-select-symbolic + + + - False - True - 0 + end + 1 + + + False + True + 0 + + + + + True + False + False + + + + + + False + True + 1 + + + + + True + False + 500 + 300 @@ -132,107 +159,54 @@ - - False - True - 1 - - - - - True - False - False - - - False - 6 - end - - - - - - False - False - 0 - - - - - False - 16 - - - True - False - Failed to save person! - - - False - True - 0 - - - - - False - False - 0 - - - - - - - - False - True - 1 - - content + False + True + 2 + + + + + content + + + + + True + False + vertical + + + True + False + Person + + + False + True + 0 - + True False - vertical - - - True - False - Person - - - False - True - 0 - - - - - True - False - True - True - - - False - True - 1 - - + True + True - loading + False + True 1 + + loading + 1 + diff --git a/musicus/res/ui/recording_selector.ui b/musicus/res/ui/recording_selector.ui index c1100af..6c4279d 100644 --- a/musicus/res/ui/recording_selector.ui +++ b/musicus/res/ui/recording_selector.ui @@ -99,7 +99,7 @@ - Show works from the server + Show recordings from the server True True False diff --git a/musicus/res/ui/selector.ui b/musicus/res/ui/selector.ui new file mode 100644 index 0000000..0cba2a8 --- /dev/null +++ b/musicus/res/ui/selector.ui @@ -0,0 +1,264 @@ + + + + + + + 250 + True + False + False + vertical + + + True + False + + + True + True + True + + + True + False + go-previous-symbolic + + + + + + + True + True + True + + + True + False + list-add-symbolic + + + + + end + 1 + + + + + False + True + 0 + + + + + True + False + True + + + True + False + 500 + 300 + + + True + False + vertical + 6 + + + True + True + edit-find-symbolic + False + False + Search … + + + False + True + 0 + + + + + Use the Musicus server + True + True + False + start + True + True + + + False + True + 1 + + + + + + + + + False + True + 1 + + + + + True + False + False + False + crossfade + True + + + True + False + 12 + True + + + loading + + + + + 200 + True + True + + + True + False + none + + + True + False + 500 + 300 + + + True + False + start + 6 + 6 + 12 + 6 + 0 + in + + + + + + + + + + + + + + + content + 1 + + + + + True + False + center + center + 18 + vertical + 18 + + + True + False + 0.5 + 80 + network-error-symbolic + + + False + True + 0 + + + + + True + False + 0.5 + An error occured! + + + + + + False + True + 1 + + + + + True + False + 0.5 + The server was not reachable or responded with an error. Please check your internet connection. + center + True + 40 + + + False + True + 2 + + + + + Try again + True + True + True + center + + + + False + True + 3 + + + + + error + 2 + + + + + True + True + 2 + + + + diff --git a/musicus/res/ui/track_editor.ui b/musicus/res/ui/track_editor.ui index eb46f4f..592b1af 100644 --- a/musicus/res/ui/track_editor.ui +++ b/musicus/res/ui/track_editor.ui @@ -3,85 +3,117 @@ - + + True False - True - 350 - 200 - dialog + vertical - + True False - vertical + Track - - True - False - Track - - - Cancel - True - True - True - - - - - Save - True - True - True - - - - end - 1 - - - - - False - True - 0 - - - - + True True + True - + True False - none + go-previous-symbolic + + + + + + + True + True + True + + + True + False + object-select-symbolic + + + + + + end + 1 + + + + + False + True + 0 + + + + + True + True + + + True + False + none + + + True + False + 500 + 300 - + True False - none - - + start + 6 + 6 + 12 + 6 + 0 + in + + True False - Select a recording of a work with multiple parts. + none + + + True + False + 6 + 6 + 6 + 6 + Select a recording of a work with multiple parts. + end + + + + + - - True - True - 1 - + + True + True + 1 + diff --git a/musicus/res/ui/tracks_editor.ui b/musicus/res/ui/tracks_editor.ui index f2ff3c0..4a23e84 100644 --- a/musicus/res/ui/tracks_editor.ui +++ b/musicus/res/ui/tracks_editor.ui @@ -3,60 +3,73 @@ - + + True False - True - 400 - 300 - dialog + vertical - + True False - vertical + Tracks - + True - False - Tracks + False + True + True - - Save + True - False - True - True - + False + object-select-symbolic - - end - + + + + end + + + + + True + True + True - - Cancel + True - True - True + False + go-previous-symbolic - - 1 - - False - True - 0 + 1 + + + False + True + 0 + + + + + True + False + True + 800 + 300 True False + True 18 12 6 @@ -286,13 +299,13 @@ - - True - True - 1 - + + False + True + 1 + diff --git a/musicus/res/ui/work_editor.ui b/musicus/res/ui/work_editor.ui index 82557d6..9c78695 100644 --- a/musicus/res/ui/work_editor.ui +++ b/musicus/res/ui/work_editor.ui @@ -2,7 +2,7 @@ - + True False @@ -17,20 +17,32 @@ False Work - - Cancel + True True True + + + True + False + go-previous-symbolic + + - Save True False True True + + + True + False + object-select-symbolic + + @@ -88,91 +100,99 @@ True False - - + True False - 18 - 12 - 6 + 500 + 300 - + + True - True - True - True + False + 18 + 12 + 6 - + + True + True + True + True + + + True + False + start + Select … + end + + + + + 1 + 1 + + + + True False - start - Select … - end + end + Composer + + 0 + 1 + + + + + True + True + + + 1 + 0 + + + + + True + False + end + Title + + + 0 + 0 + + + + + True + False + end + Publish + + + 0 + 2 + + + + + True + True + start + True + + + 1 + 2 + - - 1 - 1 - - - - - True - False - end - Composer - - - 0 - 1 - - - - - True - True - - - 1 - 0 - - - - - True - False - end - Title - - - 0 - 0 - - - - - True - False - end - Publish - - - 0 - 2 - - - - - True - True - start - True - - - 1 - 2 - @@ -187,63 +207,76 @@ - + True False - 18 - 6 - - - True - True - in - - - - - - True - True - 0 - - + 800 + 300 True False - 0 - vertical + 18 6 - + True True - True + in - - True - False - list-add-symbolic - + - False + True True 0 - + True - True - True + False + 0 + vertical + 6 - + True - False - list-remove-symbolic + True + True + + + True + False + list-add-symbolic + + + + False + True + 0 + + + + + True + True + True + + + True + False + list-remove-symbolic + + + + + False + True + 1 + @@ -253,11 +286,6 @@ - - False - True - 1 - @@ -276,62 +304,153 @@ - + True False - 18 - 6 - - - True - True - in - - - - - - True - True - 0 - - + 800 + 300 True False - vertical + 18 6 - + True True - True + in - - True - False - list-add-symbolic - + - False + True True 0 - + True - True - True + False + vertical + 6 - + True - False - folder-new-symbolic + True + True + + + True + False + list-add-symbolic + + + + False + True + 0 + + + + + True + True + True + + + True + False + folder-new-symbolic + + + + + False + True + 1 + + + + + True + True + True + + + True + False + edit-symbolic + + + + + False + True + 2 + + + + + True + True + True + + + True + False + list-remove-symbolic + + + + + False + True + 3 + + + + + True + True + True + + + True + False + go-down-symbolic + + + + + False + True + end + 4 + + + + + True + True + True + + + True + False + go-up-symbolic + + + + + False + True + end + 5 + @@ -340,90 +459,7 @@ 1 - - - True - True - True - - - True - False - edit-symbolic - - - - - False - True - 2 - - - - - True - True - True - - - True - False - list-remove-symbolic - - - - - False - True - 3 - - - - - True - True - True - - - True - False - go-down-symbolic - - - - - False - True - end - 4 - - - - - True - True - True - - - True - False - go-up-symbolic - - - - - False - True - end - 5 - - - - False - True - 1 - diff --git a/musicus/res/ui/part_editor.ui b/musicus/res/ui/work_part_editor.ui similarity index 73% rename from musicus/res/ui/part_editor.ui rename to musicus/res/ui/work_part_editor.ui index d74cb2f..c64335b 100644 --- a/musicus/res/ui/part_editor.ui +++ b/musicus/res/ui/work_part_editor.ui @@ -2,53 +2,64 @@ - - + + + True False - True - 350 - True - dialog + vertical - + True False - vertical + Work part - + True - False - Work part + True + True - - Cancel + True - True - True + False + go-previous-symbolic - - - Save - True - True - True - - - - end - 1 - - + + + + + True + True + True + + + True + False + object-select-symbolic + + + - False - True - 0 + end + 1 + + + False + True + 0 + + + + + True + False + 500 + 300 @@ -146,13 +157,13 @@ - - False - True - 1 - + + False + True + 1 + diff --git a/musicus/res/ui/section_editor.ui b/musicus/res/ui/work_section_editor.ui similarity index 53% rename from musicus/res/ui/section_editor.ui rename to musicus/res/ui/work_section_editor.ui index e0a3c57..450481f 100644 --- a/musicus/res/ui/section_editor.ui +++ b/musicus/res/ui/work_section_editor.ui @@ -2,52 +2,64 @@ - - + + + True False - True - True - dialog + vertical - + True False - vertical + Work section - + True - False - Work section + True + True - - Cancel + True - True - True + False + go-previous-symbolic - - - Save - True - True - True - - - - end - 1 - - + + + + + True + True + True + + + True + False + object-select-symbolic + + + - False - True - 0 + end + 1 + + + False + True + 0 + + + + + True + False + 500 + 300 @@ -80,13 +92,13 @@ - - False - True - 1 - + + False + True + 1 + diff --git a/musicus/res/ui/work_selector.ui b/musicus/res/ui/work_selector.ui index 7e6a000..7f079b9 100644 --- a/musicus/res/ui/work_selector.ui +++ b/musicus/res/ui/work_selector.ui @@ -49,6 +49,20 @@ True False Select a work + + + True + True + True + + + True + False + go-previous-symbolic + + + + True @@ -62,6 +76,10 @@ + + end + 1 + diff --git a/musicus/src/dialogs/ensemble_selector.rs b/musicus/src/dialogs/ensemble_selector.rs deleted file mode 100644 index fcfe81e..0000000 --- a/musicus/src/dialogs/ensemble_selector.rs +++ /dev/null @@ -1,162 +0,0 @@ -use super::EnsembleEditor; -use crate::backend::Backend; -use crate::database::Ensemble; -use crate::widgets::List; -use gettextrs::gettext; -use gio::prelude::*; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for selecting a ensemble. -pub struct EnsembleSelector { - backend: Rc, - window: libhandy::Window, - server_check_button: gtk::CheckButton, - stack: gtk::Stack, - list: Rc>, - selected_cb: RefCell ()>>>, -} - -impl EnsembleSelector { - pub fn new

(backend: Rc, parent: &P) -> Rc - where - P: IsA, - { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_selector.ui"); - - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::CheckButton, server_check_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - - window.set_transient_for(Some(parent)); - - let list = List::::new(&gettext("No ensembles found.")); - scroll.add(&list.widget); - - let this = Rc::new(Self { - backend, - window, - server_check_button, - stack, - list, - selected_cb: RefCell::new(None), - }); - - // Connect signals and callbacks - - add_button.connect_clicked(clone!(@strong this => move |_| { - let editor = EnsembleEditor::new( - this.backend.clone(), - &this.window, - None, - ); - - editor.set_saved_cb(clone!(@strong this => move |ensemble| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(ensemble); - } - - this.window.close(); - })); - - editor.show(); - })); - - search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.list.invalidate_filter(); - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_ensembles().await { - Ok(ensembles) => { - clone.list.show_items(ensembles); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let ensembles = clone.backend.db().get_ensembles().await.unwrap(); - clone.list.show_items(ensembles); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.server_check_button.connect_toggled( - clone!(@strong this, @strong load_local, @strong load_online => move |_| { - if this.server_check_button.get_active() { - load_online(); - } else { - load_local(); - } - }), - ); - - this.list.set_make_widget(|ensemble: &Ensemble| { - let label = gtk::Label::new(Some(&ensemble.name)); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - this.list - .set_filter(clone!(@strong search_entry => move |ensemble: &Ensemble| { - let search = search_entry.get_text().to_string().to_lowercase(); - search.is_empty() || ensemble.name.contains(&search) - })); - - this.list.set_selected(clone!(@strong this => move |work| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(work.clone()); - } - - this.window.close(); - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - // Initialize - load_online(); - - this - } - - /// Set the closure to be called when the user has selected a ensemble. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Show the ensemble selector. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/musicus/src/dialogs/instrument_selector.rs b/musicus/src/dialogs/instrument_selector.rs deleted file mode 100644 index f2172ee..0000000 --- a/musicus/src/dialogs/instrument_selector.rs +++ /dev/null @@ -1,162 +0,0 @@ -use super::InstrumentEditor; -use crate::backend::Backend; -use crate::database::Instrument; -use crate::widgets::List; -use gettextrs::gettext; -use gio::prelude::*; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for selecting a instrument. -pub struct InstrumentSelector { - backend: Rc, - window: libhandy::Window, - server_check_button: gtk::CheckButton, - stack: gtk::Stack, - list: Rc>, - selected_cb: RefCell ()>>>, -} - -impl InstrumentSelector { - pub fn new

(backend: Rc, parent: &P) -> Rc - where - P: IsA, - { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_selector.ui"); - - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::CheckButton, server_check_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - - window.set_transient_for(Some(parent)); - - let list = List::::new(&gettext("No instruments found.")); - scroll.add(&list.widget); - - let this = Rc::new(Self { - backend, - window, - server_check_button, - stack, - list, - selected_cb: RefCell::new(None), - }); - - // Connect signals and callbacks - - add_button.connect_clicked(clone!(@strong this => move |_| { - let editor = InstrumentEditor::new( - this.backend.clone(), - &this.window, - None, - ); - - editor.set_saved_cb(clone!(@strong this => move |instrument| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(instrument); - } - - this.window.close(); - })); - - editor.show(); - })); - - search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.list.invalidate_filter(); - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_instruments().await { - Ok(instruments) => { - clone.list.show_items(instruments); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let instruments = clone.backend.db().get_instruments().await.unwrap(); - clone.list.show_items(instruments); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.server_check_button.connect_toggled( - clone!(@strong this, @strong load_local, @strong load_online => move |_| { - if this.server_check_button.get_active() { - load_online(); - } else { - load_local(); - } - }), - ); - - this.list.set_make_widget(|instrument: &Instrument| { - let label = gtk::Label::new(Some(&instrument.name)); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - this.list - .set_filter(clone!(@strong search_entry => move |instrument: &Instrument| { - let search = search_entry.get_text().to_string().to_lowercase(); - search.is_empty() || instrument.name.contains(&search) - })); - - this.list.set_selected(clone!(@strong this => move |work| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(work.clone()); - } - - this.window.close(); - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - // Initialize - load_online(); - - this - } - - /// Set the closure to be called when the user has selected a instrument. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Show the instrument selector. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/musicus/src/dialogs/mod.rs b/musicus/src/dialogs/mod.rs index afa2dc2..b244bda 100644 --- a/musicus/src/dialogs/mod.rs +++ b/musicus/src/dialogs/mod.rs @@ -1,41 +1,11 @@ pub mod about; pub use about::*; -pub mod ensemble_editor; -pub use ensemble_editor::*; - -pub mod ensemble_selector; -pub use ensemble_selector::*; - -pub mod instrument_editor; -pub use instrument_editor::*; - -pub mod instrument_selector; -pub use instrument_selector::*; - pub mod login_dialog; pub use login_dialog::*; -pub mod person_editor; -pub use person_editor::*; - -pub mod person_selector; -pub use person_selector::*; - pub mod preferences; pub use preferences::*; pub mod server_dialog; pub use server_dialog::*; - -pub mod recording; -pub use recording::*; - -pub mod track_editor; -pub use track_editor::*; - -pub mod tracks_editor; -pub use tracks_editor::*; - -pub mod work; -pub use work::*; diff --git a/musicus/src/dialogs/person_selector.rs b/musicus/src/dialogs/person_selector.rs deleted file mode 100644 index be31301..0000000 --- a/musicus/src/dialogs/person_selector.rs +++ /dev/null @@ -1,163 +0,0 @@ -use super::PersonEditor; -use crate::backend::Backend; -use crate::database::Person; -use crate::widgets::List; -use gettextrs::gettext; -use gio::prelude::*; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for selecting a person. -pub struct PersonSelector { - backend: Rc, - window: libhandy::Window, - server_check_button: gtk::CheckButton, - stack: gtk::Stack, - list: Rc>, - selected_cb: RefCell ()>>>, -} - -impl PersonSelector { - pub fn new

(backend: Rc, parent: &P) -> Rc - where - P: IsA, - { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_selector.ui"); - - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::CheckButton, server_check_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - - window.set_transient_for(Some(parent)); - - let list = List::::new(&gettext("No persons found.")); - scroll.add(&list.widget); - - let this = Rc::new(Self { - backend, - window, - server_check_button, - stack, - list, - selected_cb: RefCell::new(None), - }); - - // Connect signals and callbacks - - add_button.connect_clicked(clone!(@strong this => move |_| { - let editor = PersonEditor::new( - this.backend.clone(), - &this.window, - None, - ); - - editor.set_saved_cb(clone!(@strong this => move |person| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(person); - } - - this.window.close(); - })); - - editor.show(); - })); - - search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.list.invalidate_filter(); - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_persons().await { - Ok(persons) => { - clone.list.show_items(persons); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let persons = clone.backend.db().get_persons().await.unwrap(); - clone.list.show_items(persons); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.server_check_button.connect_toggled( - clone!(@strong this, @strong load_local, @strong load_online => move |_| { - if this.server_check_button.get_active() { - load_online(); - } else { - load_local(); - } - }), - ); - - this.list.set_make_widget(|person: &Person| { - let label = gtk::Label::new(Some(&person.name_lf())); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - this.list - .set_filter(clone!(@strong search_entry => move |person: &Person| { - let search = search_entry.get_text().to_string().to_lowercase(); - let name = person.name_fl().to_lowercase(); - search.is_empty() || name.contains(&search) - })); - - this.list.set_selected(clone!(@strong this => move |person| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(person.clone()); - } - - this.window.close(); - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - // Initialize - load_online(); - - this - } - - /// Set the closure to be called when the user has selected a person. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Show the person selector. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/musicus/src/dialogs/recording/mod.rs b/musicus/src/dialogs/recording/mod.rs deleted file mode 100644 index 6578b75..0000000 --- a/musicus/src/dialogs/recording/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod recording_dialog; -pub use recording_dialog::*; - -pub mod recording_editor_dialog; -pub use recording_editor_dialog::*; - -mod performance_editor; -mod recording_editor; -mod recording_selector; -mod recording_selector_person_screen; -mod recording_selector_work_screen; diff --git a/musicus/src/dialogs/recording/recording_dialog.rs b/musicus/src/dialogs/recording/recording_dialog.rs deleted file mode 100644 index 0b4d184..0000000 --- a/musicus/src/dialogs/recording/recording_dialog.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::recording_editor::*; -use super::recording_selector::*; -use crate::backend::*; -use crate::database::*; -use glib::clone; -use gtk::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for selecting and creating a recording. -pub struct RecordingDialog { - pub window: libhandy::Window, - stack: gtk::Stack, - selector: Rc, - editor: Rc, - selected_cb: RefCell ()>>>, -} - -impl RecordingDialog { - /// Create a new recording dialog. - pub fn new>(backend: Rc, parent: &W) -> Rc { - // Create UI - - let window = libhandy::Window::new(); - window.set_type_hint(gdk::WindowTypeHint::Dialog); - window.set_modal(true); - window.set_transient_for(Some(parent)); - window.set_default_size(600, 424); - - let selector = RecordingSelector::new(backend.clone()); - let editor = RecordingEditor::new(backend.clone(), &window, None); - - let stack = gtk::Stack::new(); - stack.set_transition_type(gtk::StackTransitionType::Crossfade); - stack.add(&selector.widget); - stack.add(&editor.widget); - window.add(&stack); - window.show_all(); - - let this = Rc::new(Self { - window, - stack, - selector, - editor, - selected_cb: RefCell::new(None), - }); - - // Connect signals and callbacks - - this.selector.set_add_cb(clone!(@strong this => move || { - this.stack.set_visible_child(&this.editor.widget); - })); - - this.selector - .set_selected_cb(clone!(@strong this => move |recording| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(recording); - this.window.close(); - } - })); - - this.editor.set_back_cb(clone!(@strong this => move || { - this.stack.set_visible_child(&this.selector.widget); - })); - - this.editor - .set_selected_cb(clone!(@strong this => move |recording| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(recording); - this.window.close(); - } - })); - - this - } - - /// Set the closure to be called when the user has selected or created a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Show the recording dialog. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/musicus/src/dialogs/recording/recording_editor_dialog.rs b/musicus/src/dialogs/recording/recording_editor_dialog.rs deleted file mode 100644 index 2c0b8cd..0000000 --- a/musicus/src/dialogs/recording/recording_editor_dialog.rs +++ /dev/null @@ -1,64 +0,0 @@ -use super::recording_editor::*; -use crate::backend::*; -use crate::database::*; -use glib::clone; -use gtk::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for creating or editing a recording. -pub struct RecordingEditorDialog { - pub window: libhandy::Window, - selected_cb: RefCell ()>>>, -} - -impl RecordingEditorDialog { - /// Create a new recording editor dialog and optionally initialize it. - pub fn new>( - backend: Rc, - parent: &W, - recording: Option, - ) -> Rc { - // Create UI - - let window = libhandy::Window::new(); - window.set_type_hint(gdk::WindowTypeHint::Dialog); - window.set_modal(true); - window.set_transient_for(Some(parent)); - - let editor = RecordingEditor::new(backend.clone(), &window, recording); - window.add(&editor.widget); - window.show_all(); - - let this = Rc::new(Self { - window, - selected_cb: RefCell::new(None), - }); - - // Connect signals and callbacks - - editor.set_back_cb(clone!(@strong this => move || { - this.window.close(); - })); - - editor.set_selected_cb(clone!(@strong this => move |recording| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(recording); - } - - this.window.close(); - })); - - this - } - - /// Set the closure to be called when the user edited or created a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Show the recording editor dialog. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/musicus/src/dialogs/recording/recording_selector.rs b/musicus/src/dialogs/recording/recording_selector.rs deleted file mode 100644 index a8ce9d4..0000000 --- a/musicus/src/dialogs/recording/recording_selector.rs +++ /dev/null @@ -1,174 +0,0 @@ -use super::recording_selector_person_screen::*; -use crate::backend::Backend; -use crate::database::*; -use crate::widgets::*; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libhandy::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; - -/// A widget for selecting a recording from a list of existing ones. -pub struct RecordingSelector { - pub widget: libhandy::Leaflet, - backend: Rc, - sidebar_box: gtk::Box, - server_check_button: gtk::CheckButton, - stack: gtk::Stack, - list: Rc>, - selected_cb: RefCell ()>>>, - add_cb: RefCell ()>>>, - navigator: Rc, -} - -impl RecordingSelector { - /// Create a new recording selector. - pub fn new(backend: Rc) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_selector.ui"); - - get_widget!(builder, libhandy::Leaflet, widget); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::Box, sidebar_box); - get_widget!(builder, gtk::CheckButton, server_check_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - get_widget!(builder, gtk::Box, empty_screen); - - let list = List::::new(&gettext("No persons found.")); - scroll.add(&list.widget); - - let navigator = Navigator::new(&empty_screen); - widget.add(&navigator.widget); - - let this = Rc::new(Self { - widget, - backend, - sidebar_box, - server_check_button, - stack, - list, - selected_cb: RefCell::new(None), - add_cb: RefCell::new(None), - navigator, - }); - - // Connect signals and callbacks - - add_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.add_cb.borrow() { - cb(); - } - })); - - search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.list.invalidate_filter(); - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_persons().await { - Ok(persons) => { - clone.list.show_items(persons); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let persons = clone.backend.db().get_persons().await.unwrap(); - clone.list.show_items(persons); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.server_check_button.connect_toggled( - clone!(@strong this, @strong load_local, @strong load_online => move |_| { - if this.server_check_button.get_active() { - load_online(); - } else { - load_local(); - } - }), - ); - - this.list.set_make_widget(|person: &Person| { - let label = gtk::Label::new(Some(&person.name_lf())); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - this.list - .set_filter(clone!(@strong search_entry => move |person: &Person| { - let search = search_entry.get_text().to_string().to_lowercase(); - let name = person.name_fl().to_lowercase(); - search.is_empty() || name.contains(&search) - })); - - this.list - .set_selected(clone!(@strong this => move |person| { - let online = this.server_check_button.get_active(); - - let person_screen = RecordingSelectorPersonScreen::new( - this.backend.clone(), - person.clone(), - online, - ); - - person_screen.set_selected_cb(clone!(@strong this => move |work| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(work); - } - })); - - this.navigator.clone().replace(person_screen); - this.widget.set_visible_child(&this.navigator.widget); - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - this.navigator.set_back_cb(clone!(@strong this => move || { - this.widget.set_visible_child(&this.sidebar_box); - })); - - // Initialize - load_online(); - - this - } - - /// Set the closure to be called if the user wants to add a new recording. - pub fn set_add_cb () + 'static>(&self, cb: F) { - self.add_cb.replace(Some(Box::new(cb))); - } - - /// Set the closure to be called when the user has selected a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} diff --git a/musicus/src/dialogs/recording/recording_selector_person_screen.rs b/musicus/src/dialogs/recording/recording_selector_person_screen.rs deleted file mode 100644 index ba64d2a..0000000 --- a/musicus/src/dialogs/recording/recording_selector_person_screen.rs +++ /dev/null @@ -1,161 +0,0 @@ -use super::recording_selector_work_screen::*; -use crate::backend::*; -use crate::database::*; -use crate::widgets::*; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libhandy::HeaderBarExt; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen within the recording selector that presents a list of works and switches to a work -/// screen on selection. -pub struct RecordingSelectorPersonScreen { - backend: Rc, - person: Person, - online: bool, - widget: gtk::Box, - stack: gtk::Stack, - work_list: Rc>, - selected_cb: RefCell ()>>>, - navigator: RefCell>>, -} - -impl RecordingSelectorPersonScreen { - /// Create a new recording selector person screen. - pub fn new(backend: Rc, person: Person, online: bool) -> Rc { - // Create UI - - let builder = - gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_selector_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, libhandy::HeaderBar, header); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - - header.set_title(Some(&person.name_fl())); - - let work_list = List::new(&gettext("No works found.")); - scroll.add(&work_list.widget); - - let this = Rc::new(Self { - backend, - person, - online, - widget, - stack, - work_list, - selected_cb: RefCell::new(None), - navigator: RefCell::new(None), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_works(&clone.person.id).await { - Ok(works) => { - clone.work_list.show_items(works); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.work_list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let works = clone.backend.db().get_works(&clone.person.id).await.unwrap(); - clone.work_list.show_items(works); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.work_list.set_make_widget(|work: &Work| { - let label = gtk::Label::new(Some(&work.title)); - label.set_ellipsize(pango::EllipsizeMode::End); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - this.work_list - .set_selected(clone!(@strong this => move |work| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let work_screen = RecordingSelectorWorkScreen::new( - this.backend.clone(), - work.clone(), - this.online, - ); - - work_screen.set_selected_cb(clone!(@strong this => move |recording| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(recording); - } - })); - - navigator.push(work_screen); - } - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - // Initialize - - if this.online { - load_online(); - } else { - load_local(); - } - - this - } - - /// Sets a closure to be called when the user has selected a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for RecordingSelectorPersonScreen { - 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/musicus/src/dialogs/recording/recording_selector_work_screen.rs b/musicus/src/dialogs/recording/recording_selector_work_screen.rs deleted file mode 100644 index 3e23660..0000000 --- a/musicus/src/dialogs/recording/recording_selector_work_screen.rs +++ /dev/null @@ -1,155 +0,0 @@ -use crate::backend::*; -use crate::database::*; -use crate::widgets::*; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libhandy::HeaderBarExt; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen within the recording selector presenting a list of recordings for a work. -pub struct RecordingSelectorWorkScreen { - backend: Rc, - work: Work, - online: bool, - widget: gtk::Box, - stack: gtk::Stack, - recording_list: Rc>, - selected_cb: RefCell ()>>>, - navigator: RefCell>>, -} - -impl RecordingSelectorWorkScreen { - /// Create a new recording selector work screen. - pub fn new(backend: Rc, work: Work, online: bool) -> Rc { - // Create UI - - let builder = - gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_selector_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, libhandy::HeaderBar, header); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - - header.set_title(Some(&work.title)); - header.set_subtitle(Some(&work.composer.name_fl())); - - let recording_list = List::new(&gettext("No recordings found.")); - scroll.add(&recording_list.widget); - - let this = Rc::new(Self { - backend, - work, - online, - widget, - stack, - recording_list, - selected_cb: RefCell::new(None), - navigator: RefCell::new(None), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_recordings_for_work(&clone.work.id).await { - Ok(recordings) => { - clone.recording_list.show_items(recordings); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.recording_list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let recordings = clone.backend.db().get_recordings_for_work(&clone.work.id).await.unwrap(); - clone.recording_list.show_items(recordings); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.recording_list - .set_make_widget(|recording: &Recording| { - let work_label = gtk::Label::new(Some(&recording.work.get_title())); - work_label.set_ellipsize(pango::EllipsizeMode::End); - work_label.set_halign(gtk::Align::Start); - - let performers_label = gtk::Label::new(Some(&recording.get_performers())); - performers_label.set_ellipsize(pango::EllipsizeMode::End); - performers_label.set_opacity(0.5); - performers_label.set_halign(gtk::Align::Start); - - let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); - vbox.set_border_width(6); - vbox.add(&work_label); - vbox.add(&performers_label); - - vbox.upcast() - }); - - this.recording_list - .set_selected(clone!(@strong this => move |recording| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(recording.clone()); - } - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - // Initialize - - if this.online { - load_online(); - } else { - load_local(); - } - - this - } - - /// Sets a closure to be called when the user has selected a recording. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for RecordingSelectorWorkScreen { - 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/musicus/src/dialogs/work/mod.rs b/musicus/src/dialogs/work/mod.rs deleted file mode 100644 index 3c8e55d..0000000 --- a/musicus/src/dialogs/work/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod work_dialog; -pub use work_dialog::*; - -pub mod work_editor_dialog; -pub use work_editor_dialog::*; - -mod part_editor; -mod section_editor; -mod work_editor; -mod work_selector; -mod work_selector_person_screen; diff --git a/musicus/src/dialogs/work/work_dialog.rs b/musicus/src/dialogs/work/work_dialog.rs deleted file mode 100644 index ca57e22..0000000 --- a/musicus/src/dialogs/work/work_dialog.rs +++ /dev/null @@ -1,86 +0,0 @@ -use super::work_editor::*; -use super::work_selector::*; -use crate::backend::*; -use crate::database::*; -use glib::clone; -use gtk::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for selecting and creating a work. -pub struct WorkDialog { - pub window: libhandy::Window, - stack: gtk::Stack, - selector: Rc, - editor: Rc, - selected_cb: RefCell ()>>>, -} - -impl WorkDialog { - /// Create a new work dialog. - pub fn new>(backend: Rc, parent: &W) -> Rc { - // Create UI - - let window = libhandy::Window::new(); - window.set_type_hint(gdk::WindowTypeHint::Dialog); - window.set_modal(true); - window.set_transient_for(Some(parent)); - window.set_default_size(600, 424); - - let selector = WorkSelector::new(backend.clone()); - let editor = WorkEditor::new(backend.clone(), &window, None); - - let stack = gtk::Stack::new(); - stack.set_transition_type(gtk::StackTransitionType::Crossfade); - stack.add(&selector.widget); - stack.add(&editor.widget); - window.add(&stack); - window.show_all(); - - let this = Rc::new(Self { - window, - stack, - selector, - editor, - selected_cb: RefCell::new(None), - }); - - // Connect signals and callbacks - - this.selector.set_add_cb(clone!(@strong this => move || { - this.stack.set_visible_child(&this.editor.widget); - })); - - this.selector - .set_selected_cb(clone!(@strong this => move |work| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(work); - this.window.close(); - } - })); - - this.editor.set_cancel_cb(clone!(@strong this => move || { - this.stack.set_visible_child(&this.selector.widget); - })); - - this.editor - .set_saved_cb(clone!(@strong this => move |work| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(work); - this.window.close(); - } - })); - - this - } - - /// Set the closure to be called when the user has selected or created a work. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } - - /// Show the work dialog. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/musicus/src/dialogs/work/work_editor_dialog.rs b/musicus/src/dialogs/work/work_editor_dialog.rs deleted file mode 100644 index 9f455b0..0000000 --- a/musicus/src/dialogs/work/work_editor_dialog.rs +++ /dev/null @@ -1,64 +0,0 @@ -use super::work_editor::*; -use crate::backend::*; -use crate::database::*; -use glib::clone; -use gtk::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; - -/// A dialog for creating or editing a work. -pub struct WorkEditorDialog { - pub window: libhandy::Window, - saved_cb: RefCell ()>>>, -} - -impl WorkEditorDialog { - /// Create a new work editor dialog and optionally initialize it. - pub fn new>( - backend: Rc, - parent: &W, - work: Option, - ) -> Rc { - // Create UI - - let window = libhandy::Window::new(); - window.set_type_hint(gdk::WindowTypeHint::Dialog); - window.set_modal(true); - window.set_transient_for(Some(parent)); - - let editor = WorkEditor::new(backend.clone(), &window, work); - window.add(&editor.widget); - window.show_all(); - - let this = Rc::new(Self { - window, - saved_cb: RefCell::new(None), - }); - - // Connect signals and callbacks - - editor.set_cancel_cb(clone!(@strong this => move || { - this.window.close(); - })); - - editor.set_saved_cb(clone!(@strong this => move |work| { - if let Some(cb) = &*this.saved_cb.borrow() { - cb(work); - } - - this.window.close(); - })); - - this - } - - /// Set the closure to be called when the user edited or created a work. - pub fn set_saved_cb () + 'static>(&self, cb: F) { - self.saved_cb.replace(Some(Box::new(cb))); - } - - /// Show the work editor dialog. - pub fn show(&self) { - self.window.show(); - } -} diff --git a/musicus/src/dialogs/work/work_selector.rs b/musicus/src/dialogs/work/work_selector.rs deleted file mode 100644 index 080f0d9..0000000 --- a/musicus/src/dialogs/work/work_selector.rs +++ /dev/null @@ -1,174 +0,0 @@ -use super::work_selector_person_screen::*; -use crate::backend::Backend; -use crate::database::*; -use crate::widgets::*; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libhandy::prelude::*; -use std::cell::RefCell; -use std::rc::Rc; - -/// A widget for selecting a work from a list of existing ones. -pub struct WorkSelector { - pub widget: libhandy::Leaflet, - backend: Rc, - sidebar_box: gtk::Box, - server_check_button: gtk::CheckButton, - stack: gtk::Stack, - list: Rc>, - selected_cb: RefCell ()>>>, - add_cb: RefCell ()>>>, - navigator: Rc, -} - -impl WorkSelector { - /// Create a new work selector. - pub fn new(backend: Rc) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_selector.ui"); - - get_widget!(builder, libhandy::Leaflet, widget); - get_widget!(builder, gtk::Button, add_button); - get_widget!(builder, gtk::Box, sidebar_box); - get_widget!(builder, gtk::CheckButton, server_check_button); - get_widget!(builder, gtk::SearchEntry, search_entry); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - get_widget!(builder, gtk::Box, empty_screen); - - let list = List::::new(&gettext("No persons found.")); - scroll.add(&list.widget); - - let navigator = Navigator::new(&empty_screen); - widget.add(&navigator.widget); - - let this = Rc::new(Self { - widget, - backend, - sidebar_box, - server_check_button, - stack, - list, - selected_cb: RefCell::new(None), - add_cb: RefCell::new(None), - navigator, - }); - - // Connect signals and callbacks - - add_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.add_cb.borrow() { - cb(); - } - })); - - search_entry.connect_search_changed(clone!(@strong this => move |_| { - this.list.invalidate_filter(); - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_persons().await { - Ok(persons) => { - clone.list.show_items(persons); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let persons = clone.backend.db().get_persons().await.unwrap(); - clone.list.show_items(persons); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.server_check_button.connect_toggled( - clone!(@strong this, @strong load_local, @strong load_online => move |_| { - if this.server_check_button.get_active() { - load_online(); - } else { - load_local(); - } - }), - ); - - this.list.set_make_widget(|person: &Person| { - let label = gtk::Label::new(Some(&person.name_lf())); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - this.list - .set_filter(clone!(@strong search_entry => move |person: &Person| { - let search = search_entry.get_text().to_string().to_lowercase(); - let name = person.name_fl().to_lowercase(); - search.is_empty() || name.contains(&search) - })); - - this.list - .set_selected(clone!(@strong this => move |person| { - let online = this.server_check_button.get_active(); - - let person_screen = WorkSelectorPersonScreen::new( - this.backend.clone(), - person.clone(), - online, - ); - - person_screen.set_selected_cb(clone!(@strong this => move |work| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(work); - } - })); - - this.navigator.clone().replace(person_screen); - this.widget.set_visible_child(&this.navigator.widget); - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - this.navigator.set_back_cb(clone!(@strong this => move || { - this.widget.set_visible_child(&this.sidebar_box); - })); - - // Initialize - load_online(); - - this - } - - /// Set the closure to be called if the user wants to add a new work. - pub fn set_add_cb () + 'static>(&self, cb: F) { - self.add_cb.replace(Some(Box::new(cb))); - } - - /// Set the closure to be called when the user has selected a work. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} diff --git a/musicus/src/dialogs/work/work_selector_person_screen.rs b/musicus/src/dialogs/work/work_selector_person_screen.rs deleted file mode 100644 index 2b119c8..0000000 --- a/musicus/src/dialogs/work/work_selector_person_screen.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::backend::*; -use crate::database::*; -use crate::widgets::*; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libhandy::HeaderBarExt; -use std::cell::RefCell; -use std::rc::Rc; - -/// A screen within the work selector that presents a list of works by a person. -pub struct WorkSelectorPersonScreen { - backend: Rc, - person: Person, - online: bool, - widget: gtk::Box, - stack: gtk::Stack, - work_list: Rc>, - selected_cb: RefCell ()>>>, - navigator: RefCell>>, -} - -impl WorkSelectorPersonScreen { - /// Create a new work selector person screen. - pub fn new(backend: Rc, person: Person, online: bool) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_selector_screen.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, libhandy::HeaderBar, header); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Stack, stack); - get_widget!(builder, gtk::ScrolledWindow, scroll); - get_widget!(builder, gtk::Button, try_again_button); - - header.set_title(Some(&person.name_fl())); - - let work_list = List::new(&gettext("No works found.")); - scroll.add(&work_list.widget); - - let this = Rc::new(Self { - backend, - person, - online, - widget, - stack, - work_list, - selected_cb: RefCell::new(None), - navigator: RefCell::new(None), - }); - - // Connect signals and callbacks - - back_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - })); - - let load_online = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - match clone.backend.get_works(&clone.person.id).await { - Ok(works) => { - clone.work_list.show_items(works); - clone.stack.set_visible_child_name("content"); - } - Err(_) => { - clone.work_list.show_items(Vec::new()); - clone.stack.set_visible_child_name("error"); - } - } - }); - })); - - let load_local = Rc::new(clone!(@strong this => move || { - this.stack.set_visible_child_name("loading"); - - let context = glib::MainContext::default(); - let clone = this.clone(); - context.spawn_local(async move { - let works = clone.backend.db().get_works(&clone.person.id).await.unwrap(); - clone.work_list.show_items(works); - clone.stack.set_visible_child_name("content"); - }); - })); - - this.work_list.set_make_widget(|work: &Work| { - let label = gtk::Label::new(Some(&work.title)); - label.set_ellipsize(pango::EllipsizeMode::End); - label.set_halign(gtk::Align::Start); - label.set_margin_start(6); - label.set_margin_end(6); - label.set_margin_top(6); - label.set_margin_bottom(6); - label.upcast() - }); - - this.work_list - .set_selected(clone!(@strong this => move |work| { - if let Some(cb) = &*this.selected_cb.borrow() { - cb(work.clone()); - } - })); - - try_again_button.connect_clicked(clone!(@strong load_online => move |_| { - load_online(); - })); - - // Initialize - - if this.online { - load_online(); - } else { - load_local(); - } - - this - } - - /// Sets a closure to be called when the user has selected a work. - pub fn set_selected_cb () + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for WorkSelectorPersonScreen { - 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/musicus/src/dialogs/ensemble_editor.rs b/musicus/src/editors/ensemble.rs similarity index 65% rename from musicus/src/dialogs/ensemble_editor.rs rename to musicus/src/editors/ensemble.rs index c5b6110..8508f29 100644 --- a/musicus/src/dialogs/ensemble_editor.rs +++ b/musicus/src/editors/ensemble.rs @@ -1,5 +1,6 @@ use crate::backend::Backend; use crate::database::*; +use crate::widgets::{Navigator, NavigatorScreen}; use anyhow::Result; use glib::clone; use gtk::prelude::*; @@ -11,29 +12,24 @@ use std::rc::Rc; pub struct EnsembleEditor { backend: Rc, id: String, - window: libhandy::Window, - stack: gtk::Stack, + widget: gtk::Stack, info_bar: gtk::InfoBar, name_entry: gtk::Entry, upload_switch: gtk::Switch, saved_cb: RefCell ()>>>, + navigator: RefCell>>, } impl EnsembleEditor { /// Create a new ensemble editor and optionally initialize it. - pub fn new>( - backend: Rc, - parent: &P, - ensemble: Option, - ) -> Rc { + pub fn new(backend: Rc, ensemble: Option) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/ensemble_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Stack, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::InfoBar, info_bar); get_widget!(builder, gtk::Entry, name_entry); get_widget!(builder, gtk::Switch, upload_switch); @@ -50,40 +46,44 @@ impl EnsembleEditor { let this = Rc::new(Self { backend, id, - window, - stack, + widget, info_bar, name_entry, upload_switch, saved_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks - cancel_button.connect_clicked(clone!(@strong this => move |_| { - this.window.close(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); save_button.connect_clicked(clone!(@strong this => move |_| { let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - clone.stack.set_visible_child_name("loading"); + clone.widget.set_visible_child_name("loading"); match clone.clone().save().await { Ok(_) => { - clone.window.close(); + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } } Err(_) => { clone.info_bar.set_revealed(true); - clone.stack.set_visible_child_name("content"); + clone.widget.set_visible_child_name("content"); } } }); })); - this.window.set_transient_for(Some(parent)); - this } @@ -92,11 +92,6 @@ impl EnsembleEditor { self.saved_cb.replace(Some(Box::new(cb))); } - /// Show the ensemble editor. - pub fn show(&self) { - self.window.show(); - } - /// Save the ensemble and possibly upload it to the server. async fn save(self: Rc) -> Result<()> { let name = self.name_entry.get_text().to_string(); @@ -111,7 +106,10 @@ impl EnsembleEditor { self.backend.post_ensemble(&ensemble).await?; } - self.backend.db().update_ensemble(ensemble.clone()).await?; + self.backend + .db() + .update_ensemble(ensemble.clone()) + .await?; self.backend.library_changed(); if let Some(cb) = &*self.saved_cb.borrow() { @@ -121,3 +119,17 @@ impl EnsembleEditor { Ok(()) } } + +impl NavigatorScreen for EnsembleEditor { + 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/musicus/src/dialogs/instrument_editor.rs b/musicus/src/editors/instrument.rs similarity index 65% rename from musicus/src/dialogs/instrument_editor.rs rename to musicus/src/editors/instrument.rs index 996d988..34b0960 100644 --- a/musicus/src/dialogs/instrument_editor.rs +++ b/musicus/src/editors/instrument.rs @@ -1,5 +1,6 @@ use crate::backend::Backend; use crate::database::*; +use crate::widgets::{Navigator, NavigatorScreen}; use anyhow::Result; use glib::clone; use gtk::prelude::*; @@ -11,29 +12,24 @@ use std::rc::Rc; pub struct InstrumentEditor { backend: Rc, id: String, - window: libhandy::Window, - stack: gtk::Stack, + widget: gtk::Stack, info_bar: gtk::InfoBar, name_entry: gtk::Entry, upload_switch: gtk::Switch, saved_cb: RefCell ()>>>, + navigator: RefCell>>, } impl InstrumentEditor { /// Create a new instrument editor and optionally initialize it. - pub fn new>( - backend: Rc, - parent: &P, - instrument: Option, - ) -> Rc { + pub fn new(backend: Rc, instrument: Option) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/instrument_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Stack, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::InfoBar, info_bar); get_widget!(builder, gtk::Entry, name_entry); get_widget!(builder, gtk::Switch, upload_switch); @@ -50,40 +46,44 @@ impl InstrumentEditor { let this = Rc::new(Self { backend, id, - window, - stack, + widget, info_bar, name_entry, upload_switch, saved_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks - cancel_button.connect_clicked(clone!(@strong this => move |_| { - this.window.close(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); save_button.connect_clicked(clone!(@strong this => move |_| { let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - clone.stack.set_visible_child_name("loading"); + clone.widget.set_visible_child_name("loading"); match clone.clone().save().await { Ok(_) => { - clone.window.close(); + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } } Err(_) => { clone.info_bar.set_revealed(true); - clone.stack.set_visible_child_name("content"); + clone.widget.set_visible_child_name("content"); } } }); })); - this.window.set_transient_for(Some(parent)); - this } @@ -92,11 +92,6 @@ impl InstrumentEditor { self.saved_cb.replace(Some(Box::new(cb))); } - /// Show the instrument editor. - pub fn show(&self) { - self.window.show(); - } - /// Save the instrument and possibly upload it to the server. async fn save(self: Rc) -> Result<()> { let name = self.name_entry.get_text().to_string(); @@ -111,7 +106,10 @@ impl InstrumentEditor { self.backend.post_instrument(&instrument).await?; } - self.backend.db().update_instrument(instrument.clone()).await?; + self.backend + .db() + .update_instrument(instrument.clone()) + .await?; self.backend.library_changed(); if let Some(cb) = &*self.saved_cb.borrow() { @@ -121,3 +119,17 @@ impl InstrumentEditor { Ok(()) } } + +impl NavigatorScreen for InstrumentEditor { + 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/musicus/src/editors/mod.rs b/musicus/src/editors/mod.rs new file mode 100644 index 0000000..e171277 --- /dev/null +++ b/musicus/src/editors/mod.rs @@ -0,0 +1,22 @@ +pub mod ensemble; +pub use ensemble::*; + +pub mod instrument; +pub use instrument::*; + +pub mod person; +pub use person::*; + +pub mod recording; +pub use recording::*; + +pub mod tracks; +pub use tracks::*; + +pub mod work; +pub use work::*; + +mod performance; +mod track; +mod work_part; +mod work_section; \ No newline at end of file diff --git a/musicus/src/dialogs/recording/performance_editor.rs b/musicus/src/editors/performance.rs similarity index 60% rename from musicus/src/dialogs/recording/performance_editor.rs rename to musicus/src/editors/performance.rs index abfd0b2..a3f520f 100644 --- a/musicus/src/dialogs/recording/performance_editor.rs +++ b/musicus/src/editors/performance.rs @@ -1,6 +1,7 @@ -use crate::backend::*; +use crate::backend::Backend; use crate::database::*; -use crate::dialogs::*; +use crate::selectors::{EnsembleSelector, InstrumentSelector, PersonSelector}; +use crate::widgets::{Navigator, NavigatorScreen}; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -11,7 +12,7 @@ use std::rc::Rc; /// A dialog for editing a performance within a recording. pub struct PerformanceEditor { backend: Rc, - window: libhandy::Window, + widget: gtk::Box, save_button: gtk::Button, person_label: gtk::Label, ensemble_label: gtk::Label, @@ -21,21 +22,18 @@ pub struct PerformanceEditor { ensemble: RefCell>, role: RefCell>, selected_cb: RefCell ()>>>, + navigator: RefCell>>, } impl PerformanceEditor { /// Create a new performance editor. - pub fn new>( - backend: Rc, - parent: &P, - performance: Option, - ) -> Rc { + pub fn new(backend: Rc, performance: Option) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/performance_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); get_widget!(builder, gtk::Button, person_button); get_widget!(builder, gtk::Button, ensemble_button); @@ -45,11 +43,9 @@ impl PerformanceEditor { get_widget!(builder, gtk::Label, ensemble_label); get_widget!(builder, gtk::Label, role_label); - window.set_transient_for(Some(parent)); - let this = Rc::new(PerformanceEditor { backend, - window, + widget, save_button, person_label, ensemble_label, @@ -59,12 +55,16 @@ impl PerformanceEditor { ensemble: RefCell::new(None), role: RefCell::new(None), selected_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks - cancel_button.connect_clicked(clone!(@strong this => move |_| { - this.window.close(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); this.save_button @@ -75,46 +75,61 @@ impl PerformanceEditor { ensemble: this.ensemble.borrow().clone(), role: this.role.borrow().clone(), }); + } - this.window.close(); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); } })); person_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = PersonSelector::new(this.backend.clone(), &this.window); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = PersonSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |person| { - this.show_person(Some(&person)); - this.person.replace(Some(person)); - this.show_ensemble(None); - this.ensemble.replace(None); - })); + selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + this.show_person(Some(&person)); + this.person.replace(Some(person.clone())); + this.show_ensemble(None); + this.ensemble.replace(None); + navigator.clone().pop(); + })); - dialog.show(); + navigator.push(selector); + } })); ensemble_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = EnsembleSelector::new(this.backend.clone(), &this.window); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = EnsembleSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |ensemble| { - this.show_person(None); - this.person.replace(None); - this.show_ensemble(Some(&ensemble)); - this.ensemble.replace(Some(ensemble)); - })); + selector.set_selected_cb(clone!(@strong this, @strong navigator => move |ensemble| { + this.show_person(None); + this.person.replace(None); + this.show_ensemble(Some(&ensemble)); + this.ensemble.replace(Some(ensemble.clone())); + navigator.clone().pop(); + })); - dialog.show(); + navigator.push(selector); + } })); role_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = InstrumentSelector::new(this.backend.clone(), &this.window); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = InstrumentSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |role| { - this.show_role(Some(&role)); - this.role.replace(Some(role)); - })); + selector.set_selected_cb(clone!(@strong this, @strong navigator => move |role| { + this.show_role(Some(&role)); + this.role.replace(Some(role.clone())); + navigator.clone().pop(); + })); - dialog.show(); + navigator.push(selector); + } })); this.reset_role_button @@ -148,11 +163,6 @@ impl PerformanceEditor { self.selected_cb.replace(Some(Box::new(cb))); } - /// Show the performance editor. - pub fn show(&self) { - self.window.show(); - } - /// Update the UI according to person. fn show_person(&self, person: Option<&Person>) { if let Some(person) = person { @@ -184,3 +194,17 @@ impl PerformanceEditor { } } } + +impl NavigatorScreen for PerformanceEditor { + 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/musicus/src/dialogs/person_editor.rs b/musicus/src/editors/person.rs similarity index 70% rename from musicus/src/dialogs/person_editor.rs rename to musicus/src/editors/person.rs index 452b0a3..fdf0e8e 100644 --- a/musicus/src/dialogs/person_editor.rs +++ b/musicus/src/editors/person.rs @@ -1,5 +1,6 @@ use crate::backend::Backend; use crate::database::*; +use crate::widgets::{Navigator, NavigatorScreen}; use anyhow::Result; use glib::clone; use gtk::prelude::*; @@ -11,30 +12,25 @@ use std::rc::Rc; pub struct PersonEditor { backend: Rc, id: String, - window: libhandy::Window, - stack: gtk::Stack, + widget: gtk::Stack, info_bar: gtk::InfoBar, first_name_entry: gtk::Entry, last_name_entry: gtk::Entry, upload_switch: gtk::Switch, saved_cb: RefCell ()>>>, + navigator: RefCell>>, } impl PersonEditor { /// Create a new person editor and optionally initialize it. - pub fn new>( - backend: Rc, - parent: &P, - person: Option, - ) -> Rc { + pub fn new(backend: Rc, person: Option) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/person_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Stack, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, gtk::Stack, stack); get_widget!(builder, gtk::InfoBar, info_bar); get_widget!(builder, gtk::Entry, first_name_entry); get_widget!(builder, gtk::Entry, last_name_entry); @@ -53,41 +49,45 @@ impl PersonEditor { let this = Rc::new(Self { backend, id, - window, - stack, + widget, info_bar, first_name_entry, last_name_entry, upload_switch, saved_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks - cancel_button.connect_clicked(clone!(@strong this => move |_| { - this.window.close(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); save_button.connect_clicked(clone!(@strong this => move |_| { let context = glib::MainContext::default(); let clone = this.clone(); context.spawn_local(async move { - clone.stack.set_visible_child_name("loading"); + clone.widget.set_visible_child_name("loading"); match clone.clone().save().await { Ok(_) => { - clone.window.close(); + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } } Err(_) => { clone.info_bar.set_revealed(true); - clone.stack.set_visible_child_name("content"); + clone.widget.set_visible_child_name("content"); } } }); })); - this.window.set_transient_for(Some(parent)); - this } @@ -96,11 +96,6 @@ impl PersonEditor { self.saved_cb.replace(Some(Box::new(cb))); } - /// Show the person editor. - pub fn show(&self) { - self.window.show(); - } - /// Save the person and possibly upload it to the server. async fn save(self: Rc) -> Result<()> { let first_name = self.first_name_entry.get_text().to_string(); @@ -127,3 +122,17 @@ impl PersonEditor { Ok(()) } } + +impl NavigatorScreen for PersonEditor { + 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/musicus/src/dialogs/recording/recording_editor.rs b/musicus/src/editors/recording.rs similarity index 63% rename from musicus/src/dialogs/recording/recording_editor.rs rename to musicus/src/editors/recording.rs index 50a77c8..7e7c912 100644 --- a/musicus/src/dialogs/recording/recording_editor.rs +++ b/musicus/src/editors/recording.rs @@ -1,8 +1,8 @@ -use super::performance_editor::*; -use crate::backend::*; +use super::performance::PerformanceEditor; +use crate::backend::Backend; use crate::database::*; -use crate::dialogs::*; -use crate::widgets::*; +use crate::selectors::{PersonSelector, WorkSelector}; +use crate::widgets::{List, Navigator, NavigatorScreen}; use anyhow::Result; use gettextrs::gettext; use glib::clone; @@ -16,7 +16,6 @@ use std::rc::Rc; pub struct RecordingEditor { pub widget: gtk::Stack, backend: Rc, - parent: gtk::Window, save_button: gtk::Button, info_bar: gtk::InfoBar, work_label: gtk::Label, @@ -28,16 +27,12 @@ pub struct RecordingEditor { performances: RefCell>, selected_cb: RefCell ()>>>, back_cb: RefCell ()>>>, + navigator: RefCell>>, } impl RecordingEditor { - /// Create a new recording editor widget and optionally initialize it. The parent window is - /// used as the parent for newly created dialogs. - pub fn new>( - backend: Rc, - parent: &W, - recording: Option, - ) -> Rc { + /// Create a new recording editor widget and optionally initialize it. + pub fn new(backend: Rc, recording: Option) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui"); @@ -69,7 +64,6 @@ impl RecordingEditor { let this = Rc::new(RecordingEditor { widget, backend, - parent: parent.clone().upcast(), save_button, info_bar, work_label, @@ -81,6 +75,7 @@ impl RecordingEditor { performances: RefCell::new(performances), selected_cb: RefCell::new(None), back_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks @@ -89,6 +84,11 @@ impl RecordingEditor { if let Some(cb) = &*this.back_cb.borrow() { cb(); } + + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.clone().pop(); + } })); this.save_button @@ -99,7 +99,10 @@ impl RecordingEditor { clone.widget.set_visible_child_name("loading"); match clone.clone().save().await { Ok(_) => { - // We already called the callback. + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.clone().pop(); + } } Err(_) => { clone.info_bar.set_revealed(true); @@ -111,14 +114,26 @@ impl RecordingEditor { })); work_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = WorkDialog::new(this.backend.clone(), &this.parent); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let person_selector = PersonSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |work| { - this.work_selected(&work); - this.work.replace(Some(work)); - })); + person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); + + work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { + this.work_selected(&work); + this.work.replace(Some(work.clone())); - dialog.show(); + navigator.clone().pop(); + navigator.clone().pop(); + })); + + navigator.clone().push(work_selector); + })); + + navigator.push(person_selector); + } })); this.performance_list.set_make_widget(|performance| { @@ -133,42 +148,50 @@ impl RecordingEditor { }); add_performer_button.connect_clicked(clone!(@strong this => move |_| { - let editor = PerformanceEditor::new(this.backend.clone(), &this.parent, None); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = PerformanceEditor::new(this.backend.clone(), None); - editor.set_selected_cb(clone!(@strong this => move |performance| { - let mut performances = this.performances.borrow_mut(); + editor.set_selected_cb(clone!(@strong this, @strong navigator => move |performance| { + let mut performances = this.performances.borrow_mut(); - let index = match this.performance_list.get_selected_index() { - Some(index) => index + 1, - None => performances.len(), - }; + let index = match this.performance_list.get_selected_index() { + Some(index) => index + 1, + None => performances.len(), + }; - performances.insert(index, performance); - this.performance_list.show_items(performances.clone()); - this.performance_list.select_index(index); - })); + performances.insert(index, performance); + this.performance_list.show_items(performances.clone()); + this.performance_list.select_index(index); - editor.show(); + navigator.clone().pop(); + })); + + navigator.push(editor); + } })); edit_performer_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(index) = this.performance_list.get_selected_index() { - let performance = &this.performances.borrow()[index]; + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + if let Some(index) = this.performance_list.get_selected_index() { + let performance = &this.performances.borrow()[index]; - let editor = PerformanceEditor::new( - this.backend.clone(), - &this.parent, - Some(performance.clone()), - ); + let editor = PerformanceEditor::new( + this.backend.clone(), + Some(performance.clone()), + ); - editor.set_selected_cb(clone!(@strong this => move |performance| { - let mut performances = this.performances.borrow_mut(); - performances[index] = performance; - this.performance_list.show_items(performances.clone()); - this.performance_list.select_index(index); - })); + editor.set_selected_cb(clone!(@strong this, @strong navigator => move |performance| { + let mut performances = this.performances.borrow_mut(); + performances[index] = performance; + this.performance_list.show_items(performances.clone()); + this.performance_list.select_index(index); + navigator.clone().pop(); + })); - editor.show(); + navigator.push(editor); + } } })); @@ -240,6 +263,25 @@ impl RecordingEditor { cb(recording.clone()); } + let navigator = self.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.clone().pop(); + } + Ok(()) } } + +impl NavigatorScreen for RecordingEditor { + 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/musicus/src/dialogs/track_editor.rs b/musicus/src/editors/track.rs similarity index 62% rename from musicus/src/dialogs/track_editor.rs rename to musicus/src/editors/track.rs index a0da7b3..5fdff61 100644 --- a/musicus/src/dialogs/track_editor.rs +++ b/musicus/src/editors/track.rs @@ -1,4 +1,5 @@ use crate::database::*; +use crate::widgets::{Navigator, NavigatorScreen}; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; @@ -6,43 +7,58 @@ use std::cell::RefCell; use std::convert::TryInto; use std::rc::Rc; +/// A screen for editing a single track. +// TODO: Refactor. pub struct TrackEditor { - window: libhandy::Window, + widget: gtk::Box, + ready_cb: RefCell ()>>>, + navigator: RefCell>>, } impl TrackEditor { - pub fn new(parent: &W, track: Track, work: Work, callback: F) -> Self - where - W: IsA, - F: Fn(Track) -> () + 'static, - { + /// Create a new track editor. + pub fn new(track: Track, work: Work) -> Rc { + // Create UI + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); get_widget!(builder, gtk::ListBox, list); - window.set_transient_for(Some(parent)); + let this = Rc::new(Self { + widget, + ready_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); - cancel_button.connect_clicked(clone!(@strong window => move |_| { - window.close(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); let work = Rc::new(work); let work_parts = Rc::new(RefCell::new(track.work_parts)); let file_name = track.file_name; - save_button.connect_clicked(clone!(@strong work_parts, @strong window => move |_| { + save_button.connect_clicked(clone!(@strong this, @strong work_parts => move |_| { let mut work_parts = work_parts.borrow_mut(); work_parts.sort(); - callback(Track { - work_parts: work_parts.clone(), - file_name: file_name.clone(), - }); + if let Some(cb) = &*this.ready_cb.borrow() { + cb(Track { + work_parts: work_parts.clone(), + file_name: file_name.clone(), + }); + } - window.close(); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); for (index, part) in work.parts.iter().enumerate() { @@ -108,10 +124,25 @@ impl TrackEditor { section_count += 1; } - Self { window } + this } - pub fn show(&self) { - self.window.show(); + /// Set the closure to be called when the track was edited. + pub fn set_ready_cb () + 'static>(&self, cb: F) { + self.ready_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackEditor { + 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/musicus/src/dialogs/tracks_editor.rs b/musicus/src/editors/tracks.rs similarity index 61% rename from musicus/src/dialogs/tracks_editor.rs rename to musicus/src/editors/tracks.rs index 263ce23..b373b40 100644 --- a/musicus/src/dialogs/tracks_editor.rs +++ b/musicus/src/editors/tracks.rs @@ -1,7 +1,8 @@ -use super::*; -use crate::backend::*; +use super::track::TrackEditor; +use crate::backend::Backend; use crate::database::*; -use crate::widgets::*; +use crate::widgets::{List, Navigator, NavigatorScreen}; +use crate::selectors::{PersonSelector, WorkSelector, RecordingSelector}; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -13,7 +14,7 @@ use std::rc::Rc; // TODO: Disable buttons if no track is selected. pub struct TracksEditor { backend: Rc, - window: libhandy::Window, + widget: gtk::Box, save_button: gtk::Button, recording_stack: gtk::Stack, work_label: gtk::Label, @@ -22,14 +23,14 @@ pub struct TracksEditor { recording: RefCell>, tracks: RefCell>, callback: RefCell ()>>>, + navigator: RefCell>>, } impl TracksEditor { /// Create a new track editor an optionally initialize it with a recording and a list of /// tracks. - pub fn new>( + pub fn new( backend: Rc, - parent: &P, recording: Option, tracks: Vec, ) -> Rc { @@ -37,8 +38,8 @@ impl TracksEditor { let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/tracks_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); get_widget!(builder, gtk::Button, recording_button); get_widget!(builder, gtk::Stack, recording_stack); @@ -51,18 +52,12 @@ impl TracksEditor { get_widget!(builder, gtk::Button, move_track_up_button); get_widget!(builder, gtk::Button, move_track_down_button); - window.set_transient_for(Some(parent)); - - cancel_button.connect_clicked(clone!(@strong window => move |_| { - window.close(); - })); - let track_list = List::new(&gettext("Add some tracks.")); scroll.add(&track_list.widget); let this = Rc::new(Self { backend, - window, + widget, save_button, recording_stack, work_label, @@ -71,10 +66,18 @@ impl TracksEditor { recording: RefCell::new(recording), tracks: RefCell::new(tracks), callback: RefCell::new(None), + navigator: RefCell::new(None), }); // Signals and callbacks + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + this.save_button .connect_clicked(clone!(@strong this => move |_| { let context = glib::MainContext::default(); @@ -99,22 +102,43 @@ impl TracksEditor { callback(); } - this.window.close(); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } }); })); recording_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = RecordingDialog::new(this.backend.clone(), &this.window); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let person_selector = PersonSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |recording| { - this.recording_selected(&recording); - this.recording.replace(Some(recording)); - })); + person_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + let work_selector = WorkSelector::new(this.backend.clone(), person.clone()); + + work_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |work| { + let recording_selector = RecordingSelector::new(this.backend.clone(), work.clone()); + + recording_selector.set_selected_cb(clone!(@strong this, @strong navigator => move |recording| { + this.recording_selected(recording); + this.recording.replace(Some(recording.clone())); - dialog.show(); + navigator.clone().pop(); + navigator.clone().pop(); + navigator.clone().pop(); + })); + + navigator.clone().push(recording_selector); + })); + + navigator.clone().push(work_selector); + })); + + navigator.clone().push(person_selector); } - )); + })); this.track_list .set_make_widget(clone!(@strong this => move |track| { @@ -122,40 +146,43 @@ impl TracksEditor { })); add_track_button.connect_clicked(clone!(@strong this => move |_| { - let music_library_path = this.backend.get_music_library_path().unwrap(); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let music_library_path = this.backend.get_music_library_path().unwrap(); - let dialog = gtk::FileChooserNative::new( - Some(&gettext("Select audio files")), - Some(&this.window), - gtk::FileChooserAction::Open, - None, - None, - ); + let dialog = gtk::FileChooserNative::new( + Some(&gettext("Select audio files")), + Some(&navigator.window), + gtk::FileChooserAction::Open, + None, + None, + ); - dialog.set_select_multiple(true); - dialog.set_current_folder(&music_library_path); + dialog.set_select_multiple(true); + dialog.set_current_folder(&music_library_path); - if let gtk::ResponseType::Accept = dialog.run() { - let mut index = match this.track_list.get_selected_index() { - Some(index) => index + 1, - None => this.tracks.borrow().len(), - }; + if let gtk::ResponseType::Accept = dialog.run() { + let mut index = match this.track_list.get_selected_index() { + Some(index) => index + 1, + None => this.tracks.borrow().len(), + }; - { - let mut tracks = this.tracks.borrow_mut(); - for file_name in dialog.get_filenames() { - let file_name = file_name.strip_prefix(&music_library_path).unwrap(); - tracks.insert(index, Track { - work_parts: Vec::new(), - file_name: String::from(file_name.to_str().unwrap()), - }); - index += 1; + { + let mut tracks = this.tracks.borrow_mut(); + for file_name in dialog.get_filenames() { + let file_name = file_name.strip_prefix(&music_library_path).unwrap(); + tracks.insert(index, Track { + work_parts: Vec::new(), + file_name: String::from(file_name.to_str().unwrap()), + }); + index += 1; + } } - } - this.track_list.show_items(this.tracks.borrow().clone()); - this.autofill_parts(); - this.track_list.select_index(index); + this.track_list.show_items(this.tracks.borrow().clone()); + this.autofill_parts(); + this.track_list.select_index(index); + } } })); @@ -200,14 +227,21 @@ impl TracksEditor { })); edit_track_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(index) = this.track_list.get_selected_index() { - if let Some(recording) = &*this.recording.borrow() { - TrackEditor::new(&this.window, this.tracks.borrow()[index].clone(), recording.work.clone(), clone!(@strong this => move |track| { - let mut tracks = this.tracks.borrow_mut(); - tracks[index] = track; - this.track_list.show_items(tracks.clone()); - this.track_list.select_index(index); - })).show(); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + if let Some(index) = this.track_list.get_selected_index() { + if let Some(recording) = &*this.recording.borrow() { + let editor = TrackEditor::new(this.tracks.borrow()[index].clone(), recording.work.clone()); + + editor.set_ready_cb(clone!(@strong this => move |track| { + let mut tracks = this.tracks.borrow_mut(); + tracks[index] = track; + this.track_list.show_items(tracks.clone()); + this.track_list.select_index(index); + })); + + navigator.push(editor); + } } } })); @@ -228,11 +262,6 @@ impl TracksEditor { self.callback.replace(Some(Box::new(cb))); } - /// Open the track editor. - pub fn show(&self) { - self.window.show(); - } - /// Create a widget representing a track. fn build_track_row(&self, track: &Track) -> gtk::Widget { let mut title_parts = Vec::::new(); @@ -292,3 +321,17 @@ impl TracksEditor { } } } + +impl NavigatorScreen for TracksEditor { + 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/musicus/src/dialogs/work/work_editor.rs b/musicus/src/editors/work.rs similarity index 62% rename from musicus/src/dialogs/work/work_editor.rs rename to musicus/src/editors/work.rs index 76c40cb..4a1c3c4 100644 --- a/musicus/src/dialogs/work/work_editor.rs +++ b/musicus/src/editors/work.rs @@ -1,9 +1,9 @@ -use super::part_editor::*; -use super::section_editor::*; -use crate::backend::*; +use super::work_part::WorkPartEditor; +use super::work_section::WorkSectionEditor; +use crate::backend::Backend; use crate::database::*; -use crate::dialogs::*; -use crate::widgets::*; +use crate::selectors::{InstrumentSelector, PersonSelector}; +use crate::widgets::{List, Navigator, NavigatorScreen}; use anyhow::Result; use gettextrs::gettext; use glib::clone; @@ -22,9 +22,8 @@ enum PartOrSection { /// A widget for editing and creating works. pub struct WorkEditor { - pub widget: gtk::Stack, + widget: gtk::Stack, backend: Rc, - parent: gtk::Window, save_button: gtk::Button, title_entry: gtk::Entry, info_bar: gtk::InfoBar, @@ -36,24 +35,19 @@ pub struct WorkEditor { composer: RefCell>, instruments: RefCell>, structure: RefCell>, - cancel_cb: RefCell ()>>>, saved_cb: RefCell ()>>>, + navigator: RefCell>>, } impl WorkEditor { - /// Create a new work editor widget and optionally initialize it. The parent window is used - /// as the parent for newly created dialogs. - pub fn new>( - backend: Rc, - parent: &P, - work: Option, - ) -> Rc { + /// Create a new work editor widget and optionally initialize it. + pub fn new(backend: Rc, work: Option) -> Rc { // Create UI let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_editor.ui"); get_widget!(builder, gtk::Stack, widget); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); get_widget!(builder, gtk::InfoBar, info_bar); get_widget!(builder, gtk::Entry, title_entry); @@ -102,7 +96,6 @@ impl WorkEditor { let this = Rc::new(Self { widget, backend, - parent: parent.clone().upcast(), save_button, id, info_bar, @@ -114,15 +107,16 @@ impl WorkEditor { composer: RefCell::new(composer), instruments: RefCell::new(instruments), structure: RefCell::new(structure), - cancel_cb: RefCell::new(None), saved_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks - cancel_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.cancel_cb.borrow() { - cb(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); } })); @@ -134,7 +128,10 @@ impl WorkEditor { clone.widget.set_visible_child_name("loading"); match clone.clone().save().await { Ok(_) => { - // We already called the callback. + let navigator = clone.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } } Err(_) => { clone.info_bar.set_revealed(true); @@ -146,14 +143,18 @@ impl WorkEditor { })); composer_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = PersonSelector::new(this.backend.clone(), &this.parent); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = PersonSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |person| { - this.show_composer(&person); - this.composer.replace(Some(person)); - })); + selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + this.show_composer(person); + this.composer.replace(Some(person.clone())); + navigator.clone().pop(); + })); - dialog.show(); + navigator.push(selector); + } })); this.instrument_list.set_make_widget(|instrument| { @@ -168,22 +169,27 @@ impl WorkEditor { }); add_instrument_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = InstrumentSelector::new(this.backend.clone(), &this.parent); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = InstrumentSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |instrument| { - let mut instruments = this.instruments.borrow_mut(); + selector.set_selected_cb(clone!(@strong this, @strong navigator => move |instrument| { + let mut instruments = this.instruments.borrow_mut(); - let index = match this.instrument_list.get_selected_index() { - Some(index) => index + 1, - None => instruments.len(), - }; + let index = match this.instrument_list.get_selected_index() { + Some(index) => index + 1, + None => instruments.len(), + }; - instruments.insert(index, instrument); - this.instrument_list.show_items(instruments.clone()); - this.instrument_list.select_index(index); - })); + instruments.insert(index, instrument.clone()); + this.instrument_list.show_items(instruments.clone()); + this.instrument_list.select_index(index); - dialog.show(); + navigator.clone().pop(); + })); + + navigator.push(selector); + } })); remove_instrument_button.connect_clicked(clone!(@strong this => move |_| { @@ -221,73 +227,84 @@ impl WorkEditor { }); add_part_button.connect_clicked(clone!(@strong this => move |_| { - let editor = PartEditor::new(this.backend.clone(), &this.parent, None); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = WorkPartEditor::new(this.backend.clone(), None); - editor.set_ready_cb(clone!(@strong this => move |part| { - let mut structure = this.structure.borrow_mut(); + editor.set_ready_cb(clone!(@strong this, @strong navigator => move |part| { + let mut structure = this.structure.borrow_mut(); - let index = match this.part_list.get_selected_index() { - Some(index) => index + 1, - None => structure.len(), - }; + let index = match this.part_list.get_selected_index() { + Some(index) => index + 1, + None => structure.len(), + }; - structure.insert(index, PartOrSection::Part(part)); - this.part_list.show_items(structure.clone()); - this.part_list.select_index(index); - })); + structure.insert(index, PartOrSection::Part(part)); + this.part_list.show_items(structure.clone()); + this.part_list.select_index(index); - editor.show(); + navigator.clone().pop(); + })); + + navigator.push(editor); + } })); add_section_button.connect_clicked(clone!(@strong this => move |_| { - let editor = SectionEditor::new(&this.parent, None); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = WorkSectionEditor::new(None); - editor.set_ready_cb(clone!(@strong this => move |section| { - let mut structure = this.structure.borrow_mut(); + editor.set_ready_cb(clone!(@strong this, @strong navigator => move |section| { + let mut structure = this.structure.borrow_mut(); - let index = match this.part_list.get_selected_index() { - Some(index) => index + 1, - None => structure.len(), - }; + let index = match this.part_list.get_selected_index() { + Some(index) => index + 1, + None => structure.len(), + }; - structure.insert(index, PartOrSection::Section(section)); - this.part_list.show_items(structure.clone()); - this.part_list.select_index(index); - })); + structure.insert(index, PartOrSection::Section(section)); + this.part_list.show_items(structure.clone()); + this.part_list.select_index(index); - editor.show(); + navigator.clone().pop(); + })); + + navigator.push(editor); + } })); edit_part_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(index) = this.part_list.get_selected_index() { - match this.structure.borrow()[index].clone() { - PartOrSection::Part(part) => { - let editor = PartEditor::new( - this.backend.clone(), - &this.parent, - Some(part), - ); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + if let Some(index) = this.part_list.get_selected_index() { + match this.structure.borrow()[index].clone() { + PartOrSection::Part(part) => { + let editor = WorkPartEditor::new(this.backend.clone(), Some(part)); - editor.set_ready_cb(clone!(@strong this => move |part| { - let mut structure = this.structure.borrow_mut(); - structure[index] = PartOrSection::Part(part); - this.part_list.show_items(structure.clone()); - this.part_list.select_index(index); - })); + editor.set_ready_cb(clone!(@strong this, @strong navigator => move |part| { + let mut structure = this.structure.borrow_mut(); + structure[index] = PartOrSection::Part(part); + this.part_list.show_items(structure.clone()); + this.part_list.select_index(index); + navigator.clone().pop(); + })); - editor.show(); - } - PartOrSection::Section(section) => { - let editor = SectionEditor::new(&this.parent, Some(section)); + navigator.push(editor); + } + PartOrSection::Section(section) => { + let editor = WorkSectionEditor::new(Some(section)); - editor.set_ready_cb(clone!(@strong this => move |section| { - let mut structure = this.structure.borrow_mut(); - structure[index] = PartOrSection::Section(section); - this.part_list.show_items(structure.clone()); - this.part_list.select_index(index); - })); + editor.set_ready_cb(clone!(@strong this, @strong navigator => move |section| { + let mut structure = this.structure.borrow_mut(); + structure[index] = PartOrSection::Section(section); + this.part_list.show_items(structure.clone()); + this.part_list.select_index(index); + navigator.clone().pop(); + })); - editor.show(); + navigator.push(editor); + } } } } @@ -337,11 +354,6 @@ impl WorkEditor { this } - /// The closure to call when the editor is canceled. - pub fn set_cancel_cb () + 'static>(&self, cb: F) { - self.cancel_cb.replace(Some(Box::new(cb))); - } - /// The closure to call when a work was created. pub fn set_saved_cb () + 'static>(&self, cb: F) { self.saved_cb.replace(Some(Box::new(cb))); @@ -404,3 +416,17 @@ impl WorkEditor { Ok(()) } } + +impl NavigatorScreen for WorkEditor { + 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/musicus/src/dialogs/work/part_editor.rs b/musicus/src/editors/work_part.rs similarity index 61% rename from musicus/src/dialogs/work/part_editor.rs rename to musicus/src/editors/work_part.rs index cda7944..3ef3a3a 100644 --- a/musicus/src/dialogs/work/part_editor.rs +++ b/musicus/src/editors/work_part.rs @@ -1,6 +1,7 @@ -use crate::backend::*; +use crate::backend::Backend; use crate::database::*; -use crate::dialogs::*; +use crate::selectors::PersonSelector; +use crate::widgets::{Navigator, NavigatorScreen}; use gettextrs::gettext; use glib::clone; use gtk::prelude::*; @@ -9,37 +10,32 @@ use std::cell::RefCell; use std::rc::Rc; /// A dialog for creating or editing a work part. -pub struct PartEditor { +pub struct WorkPartEditor { backend: Rc, - window: libhandy::Window, + widget: gtk::Box, title_entry: gtk::Entry, composer_label: gtk::Label, reset_composer_button: gtk::Button, composer: RefCell>, ready_cb: RefCell ()>>>, + navigator: RefCell>>, } -impl PartEditor { +impl WorkPartEditor { /// Create a new part editor and optionally initialize it. - pub fn new>( - backend: Rc, - parent: &P, - part: Option, - ) -> Rc { + pub fn new(backend: Rc, part: Option) -> Rc { // Create UI - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/part_editor.ui"); + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_part_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); get_widget!(builder, gtk::Entry, title_entry); get_widget!(builder, gtk::Button, composer_button); get_widget!(builder, gtk::Label, composer_label); get_widget!(builder, gtk::Button, reset_composer_button); - window.set_transient_for(Some(parent)); - let composer = match part { Some(part) => { title_entry.set_text(&part.title); @@ -50,18 +46,22 @@ impl PartEditor { let this = Rc::new(Self { backend, - window, + widget, title_entry, composer_label, reset_composer_button, composer: RefCell::new(composer), ready_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks - cancel_button.connect_clicked(clone!(@strong this => move |_| { - this.window.close(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); save_button.connect_clicked(clone!(@strong this => move |_| { @@ -72,18 +72,26 @@ impl PartEditor { }); } - this.window.close(); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); composer_button.connect_clicked(clone!(@strong this => move |_| { - let dialog = PersonSelector::new(this.backend.clone(), &this.window); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = PersonSelector::new(this.backend.clone()); - dialog.set_selected_cb(clone!(@strong this => move |person| { - this.show_composer(Some(&person)); - this.composer.replace(Some(person)); - })); + selector.set_selected_cb(clone!(@strong this, @strong navigator => move |person| { + this.show_composer(Some(person)); + this.composer.replace(Some(person.clone())); + navigator.clone().pop(); + })); + + navigator.push(selector); + } - dialog.show(); })); this.reset_composer_button @@ -106,11 +114,6 @@ impl PartEditor { self.ready_cb.replace(Some(Box::new(cb))); } - /// Show the part editor. - pub fn show(&self) { - self.window.show(); - } - /// Update the UI according to person. fn show_composer(&self, person: Option<&Person>) { if let Some(person) = person { @@ -122,3 +125,17 @@ impl PartEditor { } } } + +impl NavigatorScreen for WorkPartEditor { + 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/musicus/src/dialogs/work/section_editor.rs b/musicus/src/editors/work_section.rs similarity index 57% rename from musicus/src/dialogs/work/section_editor.rs rename to musicus/src/editors/work_section.rs index e3b7002..d162b79 100644 --- a/musicus/src/dialogs/work/section_editor.rs +++ b/musicus/src/editors/work_section.rs @@ -1,4 +1,5 @@ use crate::database::*; +use crate::widgets::{Navigator, NavigatorScreen}; use glib::clone; use gtk::prelude::*; use gtk_macros::get_widget; @@ -6,40 +7,43 @@ use std::cell::RefCell; use std::rc::Rc; /// A dialog for creating or editing a work section. -pub struct SectionEditor { - window: libhandy::Window, +pub struct WorkSectionEditor { + widget: gtk::Box, title_entry: gtk::Entry, ready_cb: RefCell ()>>>, + navigator: RefCell>>, } -impl SectionEditor { +impl WorkSectionEditor { /// Create a new section editor and optionally initialize it. - pub fn new>(parent: &P, section: Option) -> Rc { + pub fn new(section: Option) -> Rc { // Create UI - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/section_editor.ui"); + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/work_section_editor.ui"); - get_widget!(builder, libhandy::Window, window); - get_widget!(builder, gtk::Button, cancel_button); + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); get_widget!(builder, gtk::Button, save_button); get_widget!(builder, gtk::Entry, title_entry); - window.set_transient_for(Some(parent)); - if let Some(section) = section { title_entry.set_text(§ion.title); } let this = Rc::new(Self { - window, + widget, title_entry, ready_cb: RefCell::new(None), + navigator: RefCell::new(None), }); // Connect signals and callbacks - cancel_button.connect_clicked(clone!(@strong this => move |_| { - this.window.close(); + back_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); save_button.connect_clicked(clone!(@strong this => move |_| { @@ -50,7 +54,10 @@ impl SectionEditor { }); } - this.window.close(); + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } })); this @@ -62,9 +69,18 @@ impl SectionEditor { pub fn set_ready_cb () + 'static>(&self, cb: F) { self.ready_cb.replace(Some(Box::new(cb))); } +} - /// Show the section editor. - pub fn show(&self) { - self.window.show(); +impl NavigatorScreen for WorkSectionEditor { + 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/musicus/src/main.rs b/musicus/src/main.rs index e3555c8..da2d64f 100644 --- a/musicus/src/main.rs +++ b/musicus/src/main.rs @@ -11,12 +11,14 @@ use glib::clone; use std::cell::RefCell; use std::rc::Rc; -mod config; mod backend; +mod config; mod database; mod dialogs; +mod editors; mod player; mod screens; +mod selectors; mod widgets; mod window; @@ -33,11 +35,8 @@ fn main() { libhandy::init(); resources::init().expect("Failed to initialize resources!"); - let app = gtk::Application::new( - Some("de.johrpan.musicus"), - gio::ApplicationFlags::empty(), - ) - .expect("Failed to initialize GTK application!"); + let app = gtk::Application::new(Some("de.johrpan.musicus"), gio::ApplicationFlags::empty()) + .expect("Failed to initialize GTK application!"); let window: RefCell>> = RefCell::new(None); diff --git a/musicus/src/meson.build b/musicus/src/meson.build index 4f0b78e..72ff3cd 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -52,43 +52,38 @@ sources = files( 'database/tracks.rs', 'database/works.rs', 'dialogs/about.rs', - 'dialogs/ensemble_editor.rs', - 'dialogs/ensemble_selector.rs', - 'dialogs/instrument_editor.rs', - 'dialogs/instrument_selector.rs', 'dialogs/login_dialog.rs', 'dialogs/mod.rs', - 'dialogs/person_editor.rs', - 'dialogs/person_selector.rs', 'dialogs/preferences.rs', 'dialogs/server_dialog.rs', - 'dialogs/recording/mod.rs', - 'dialogs/recording/performance_editor.rs', - 'dialogs/recording/recording_dialog.rs', - 'dialogs/recording/recording_editor_dialog.rs', - 'dialogs/recording/recording_editor.rs', - 'dialogs/recording/recording_selector_person_screen.rs', - 'dialogs/recording/recording_selector.rs', - 'dialogs/recording/recording_selector_work_screen.rs', - 'dialogs/track_editor.rs', - 'dialogs/tracks_editor.rs', - 'dialogs/work/mod.rs', - 'dialogs/work/part_editor.rs', - 'dialogs/work/section_editor.rs', - 'dialogs/work/work_dialog.rs', - 'dialogs/work/work_editor_dialog.rs', - 'dialogs/work/work_editor.rs', - 'dialogs/work/work_selector_person_screen.rs', - 'dialogs/work/work_selector.rs', + 'editors/ensemble.rs', + 'editors/instrument.rs', + 'editors/mod.rs', + 'editors/performance.rs', + 'editors/person.rs', + 'editors/recording.rs', + 'editors/track.rs', + 'editors/tracks.rs', + 'editors/work.rs', + 'editors/work_part.rs', + 'editors/work_section.rs', 'screens/ensemble_screen.rs', 'screens/mod.rs', 'screens/person_screen.rs', 'screens/player_screen.rs', 'screens/recording_screen.rs', 'screens/work_screen.rs', + 'selectors/ensemble.rs', + 'selectors/instrument.rs', + 'selectors/mod.rs', + 'selectors/person.rs', + 'selectors/recording.rs', + 'selectors/selector.rs', + 'selectors/work.rs', 'widgets/list.rs', 'widgets/mod.rs', 'widgets/navigator.rs', + 'widgets/navigator_window.rs', 'widgets/player_bar.rs', 'widgets/poe_list.rs', 'widgets/selector_row.rs', diff --git a/musicus/src/screens/ensemble_screen.rs b/musicus/src/screens/ensemble_screen.rs index c4d5ce7..9809e88 100644 --- a/musicus/src/screens/ensemble_screen.rs +++ b/musicus/src/screens/ensemble_screen.rs @@ -1,8 +1,8 @@ use super::*; use crate::backend::*; use crate::database::*; -use crate::dialogs::EnsembleEditor; -use crate::widgets::*; +use crate::editors::EnsembleEditor; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use gettextrs::gettext; use gio::prelude::*; use glib::clone; @@ -109,7 +109,9 @@ impl EnsembleScreen { })); edit_action.connect_activate(clone!(@strong result => move |_, _| { - EnsembleEditor::new(result.backend.clone(), &result.window, Some(result.ensemble.clone())).show(); + let editor = EnsembleEditor::new(result.backend.clone(), Some(result.ensemble.clone())); + let window = NavigatorWindow::new(editor); + window.show(); })); delete_action.connect_activate(clone!(@strong result => move |_, _| { diff --git a/musicus/src/screens/person_screen.rs b/musicus/src/screens/person_screen.rs index 06b90a6..65dcf9b 100644 --- a/musicus/src/screens/person_screen.rs +++ b/musicus/src/screens/person_screen.rs @@ -1,8 +1,8 @@ use super::*; use crate::backend::*; use crate::database::*; -use crate::dialogs::PersonEditor; -use crate::widgets::*; +use crate::editors::PersonEditor; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use gettextrs::gettext; use gio::prelude::*; use glib::clone; @@ -145,7 +145,9 @@ impl PersonScreen { })); edit_action.connect_activate(clone!(@strong result => move |_, _| { - PersonEditor::new(result.backend.clone(), &result.window, Some(result.person.clone())).show(); + let editor = PersonEditor::new(result.backend.clone(), Some(result.person.clone())); + let window = NavigatorWindow::new(editor); + window.show(); })); delete_action.connect_activate(clone!(@strong result => move |_, _| { diff --git a/musicus/src/screens/recording_screen.rs b/musicus/src/screens/recording_screen.rs index a09e822..9ab007d 100644 --- a/musicus/src/screens/recording_screen.rs +++ b/musicus/src/screens/recording_screen.rs @@ -1,8 +1,8 @@ use crate::backend::*; use crate::database::*; -use crate::dialogs::{RecordingEditorDialog, TracksEditor}; +use crate::editors::{RecordingEditor, TracksEditor}; use crate::player::*; -use crate::widgets::*; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use gettextrs::gettext; use gio::prelude::*; use glib::clone; @@ -111,7 +111,9 @@ impl RecordingScreen { })); edit_action.connect_activate(clone!(@strong result => move |_, _| { - RecordingEditorDialog::new(result.backend.clone(), &result.window, Some(result.recording.clone())).show(); + let editor = RecordingEditor::new(result.backend.clone(), Some(result.recording.clone())); + let window = NavigatorWindow::new(editor); + window.show(); })); delete_action.connect_activate(clone!(@strong result => move |_, _| { @@ -124,7 +126,9 @@ impl RecordingScreen { })); edit_tracks_action.connect_activate(clone!(@strong result => move |_, _| { - TracksEditor::new(result.backend.clone(), &result.window, Some(result.recording.clone()), result.tracks.borrow().clone()).show(); + let editor = TracksEditor::new(result.backend.clone(), Some(result.recording.clone()), result.tracks.borrow().clone()); + let window = NavigatorWindow::new(editor); + window.show(); })); delete_tracks_action.connect_activate(clone!(@strong result => move |_, _| { diff --git a/musicus/src/screens/work_screen.rs b/musicus/src/screens/work_screen.rs index 9af8450..6f3633b 100644 --- a/musicus/src/screens/work_screen.rs +++ b/musicus/src/screens/work_screen.rs @@ -1,8 +1,8 @@ use super::*; use crate::backend::*; use crate::database::*; -use crate::dialogs::WorkEditorDialog; -use crate::widgets::*; +use crate::editors::WorkEditor; +use crate::widgets::{List, Navigator, NavigatorScreen, NavigatorWindow}; use gettextrs::gettext; use gio::prelude::*; use glib::clone; @@ -108,7 +108,9 @@ impl WorkScreen { })); edit_action.connect_activate(clone!(@strong result => move |_, _| { - WorkEditorDialog::new(result.backend.clone(), &result.window, Some(result.work.clone())).show(); + let editor = WorkEditor::new(result.backend.clone(), Some(result.work.clone())); + let window = NavigatorWindow::new(editor); + window.show(); })); delete_action.connect_activate(clone!(@strong result => move |_, _| { diff --git a/musicus/src/selectors/ensemble.rs b/musicus/src/selectors/ensemble.rs new file mode 100644 index 0000000..1e61650 --- /dev/null +++ b/musicus/src/selectors/ensemble.rs @@ -0,0 +1,111 @@ +use super::selector::Selector; +use crate::backend::Backend; +use crate::database::Ensemble; +use crate::editors::EnsembleEditor; +use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for selecting a ensemble. +pub struct EnsembleSelector { + backend: Rc, + selector: Rc>, + selected_cb: RefCell ()>>>, + navigator: RefCell>>, +} + +impl EnsembleSelector { + /// Create a new ensemble selector. + pub fn new(backend: Rc) -> Rc { + // Create UI + + let selector = Selector::::new(); + selector.set_title(&gettext("Select ensemble")); + + let this = Rc::new(Self { + backend, + selector, + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + this.selector.set_back_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + this.selector.set_add_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = EnsembleEditor::new(this.backend.clone(), None); + editor + .set_saved_cb(clone!(@strong this => move |ensemble| this.select(&ensemble))); + navigator.push(editor); + } + })); + + this.selector + .set_load_online(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.get_ensembles().await } + })); + + this.selector + .set_load_local(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.db().get_ensembles().await.unwrap() } + })); + + this.selector.set_make_widget(|ensemble| { + let label = gtk::Label::new(Some(&ensemble.name)); + label.set_halign(gtk::Align::Start); + label.set_margin_start(6); + label.set_margin_end(6); + label.set_margin_top(6); + label.set_margin_bottom(6); + label.upcast() + }); + + this.selector + .set_filter(|search, ensemble| ensemble.name.to_lowercase().contains(search)); + + this.selector + .set_selected_cb(clone!(@strong this => move |ensemble| this.select(ensemble))); + + this + } + + /// Set the closure to be called when an item is selected. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Select a ensemble. + fn select(&self, ensemble: &Ensemble) { + if let Some(cb) = &*self.selected_cb.borrow() { + cb(&ensemble); + } + + } +} + +impl NavigatorScreen for EnsembleSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/selectors/instrument.rs b/musicus/src/selectors/instrument.rs new file mode 100644 index 0000000..a5745f3 --- /dev/null +++ b/musicus/src/selectors/instrument.rs @@ -0,0 +1,110 @@ +use super::selector::Selector; +use crate::backend::Backend; +use crate::database::Instrument; +use crate::editors::InstrumentEditor; +use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for selecting a instrument. +pub struct InstrumentSelector { + backend: Rc, + selector: Rc>, + selected_cb: RefCell ()>>>, + navigator: RefCell>>, +} + +impl InstrumentSelector { + /// Create a new instrument selector. + pub fn new(backend: Rc) -> Rc { + // Create UI + + let selector = Selector::::new(); + selector.set_title(&gettext("Select instrument")); + + let this = Rc::new(Self { + backend, + selector, + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + this.selector.set_back_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + this.selector.set_add_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = InstrumentEditor::new(this.backend.clone(), None); + editor + .set_saved_cb(clone!(@strong this => move |instrument| this.select(&instrument))); + navigator.push(editor); + } + })); + + this.selector + .set_load_online(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.get_instruments().await } + })); + + this.selector + .set_load_local(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.db().get_instruments().await.unwrap() } + })); + + this.selector.set_make_widget(|instrument| { + let label = gtk::Label::new(Some(&instrument.name)); + label.set_halign(gtk::Align::Start); + label.set_margin_start(6); + label.set_margin_end(6); + label.set_margin_top(6); + label.set_margin_bottom(6); + label.upcast() + }); + + this.selector + .set_filter(|search, instrument| instrument.name.to_lowercase().contains(search)); + + this.selector + .set_selected_cb(clone!(@strong this => move |instrument| this.select(instrument))); + + this + } + + /// Set the closure to be called when an item is selected. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Select a instrument. + fn select(&self, instrument: &Instrument) { + if let Some(cb) = &*self.selected_cb.borrow() { + cb(&instrument); + } + } +} + +impl NavigatorScreen for InstrumentSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/selectors/mod.rs b/musicus/src/selectors/mod.rs new file mode 100644 index 0000000..0083998 --- /dev/null +++ b/musicus/src/selectors/mod.rs @@ -0,0 +1,16 @@ +pub mod ensemble; +pub use ensemble::*; + +pub mod instrument; +pub use instrument::*; + +pub mod person; +pub use person::*; + +pub mod recording; +pub use recording::*; + +pub mod work; +pub use work::*; + +mod selector; \ No newline at end of file diff --git a/musicus/src/selectors/person.rs b/musicus/src/selectors/person.rs new file mode 100644 index 0000000..4d4298c --- /dev/null +++ b/musicus/src/selectors/person.rs @@ -0,0 +1,110 @@ +use super::selector::Selector; +use crate::backend::Backend; +use crate::database::Person; +use crate::editors::PersonEditor; +use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for selecting a person. +pub struct PersonSelector { + backend: Rc, + selector: Rc>, + selected_cb: RefCell ()>>>, + navigator: RefCell>>, +} + +impl PersonSelector { + /// Create a new person selector. + pub fn new(backend: Rc) -> Rc { + // Create UI + + let selector = Selector::::new(); + selector.set_title(&gettext("Select person")); + + let this = Rc::new(Self { + backend, + selector, + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + this.selector.set_back_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + this.selector.set_add_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = PersonEditor::new(this.backend.clone(), None); + editor + .set_saved_cb(clone!(@strong this => move |person| this.select(&person))); + navigator.push(editor); + } + })); + + this.selector + .set_load_online(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.get_persons().await } + })); + + this.selector + .set_load_local(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.db().get_persons().await.unwrap() } + })); + + this.selector.set_make_widget(|person| { + let label = gtk::Label::new(Some(&person.name_lf())); + label.set_halign(gtk::Align::Start); + label.set_margin_start(6); + label.set_margin_end(6); + label.set_margin_top(6); + label.set_margin_bottom(6); + label.upcast() + }); + + this.selector + .set_filter(|search, person| person.name_fl().to_lowercase().contains(search)); + + this.selector + .set_selected_cb(clone!(@strong this => move |person| this.select(person))); + + this + } + + /// Set the closure to be called when an item is selected. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Select a person. + fn select(&self, person: &Person) { + if let Some(cb) = &*self.selected_cb.borrow() { + cb(&person); + } + } +} + +impl NavigatorScreen for PersonSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/selectors/recording.rs b/musicus/src/selectors/recording.rs new file mode 100644 index 0000000..a4ca845 --- /dev/null +++ b/musicus/src/selectors/recording.rs @@ -0,0 +1,114 @@ +use super::selector::Selector; +use crate::backend::Backend; +use crate::database::{Recording, Work}; +use crate::editors::RecordingEditor; +use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for selecting a recording. +pub struct RecordingSelector { + backend: Rc, + work: Work, + selector: Rc>, + selected_cb: RefCell ()>>>, + navigator: RefCell>>, +} + +impl RecordingSelector { + /// Create a new recording selector for recordings of a specific work. + pub fn new(backend: Rc, work: Work) -> Rc { + // Create UI + + let selector = Selector::::new(); + selector.set_title(&gettext("Select recording")); + selector.set_subtitle(&work.get_title()); + + let this = Rc::new(Self { + backend, + work, + selector, + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + this.selector.set_back_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + this.selector.set_add_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = RecordingEditor::new(this.backend.clone(), None); + editor + .set_selected_cb(clone!(@strong this => move |recording| this.select(&recording))); + navigator.push(editor); + } + })); + + this.selector + .set_load_online(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.get_recordings_for_work(&clone.work.id).await } + })); + + this.selector + .set_load_local(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.db().get_recordings_for_work(&clone.work.id).await.unwrap() } + })); + + this.selector.set_make_widget(|recording| { + let label = gtk::Label::new(Some(&recording.get_performers())); + label.set_halign(gtk::Align::Start); + label.set_margin_start(6); + label.set_margin_end(6); + label.set_margin_top(6); + label.set_margin_bottom(6); + label.upcast() + }); + + this.selector.set_filter(|search, recording| { + recording.get_performers().to_lowercase().contains(search) + }); + + this.selector + .set_selected_cb(clone!(@strong this => move |recording| this.select(recording))); + + this + } + + /// Set the closure to be called when an item is selected. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Select a recording. + fn select(&self, recording: &Recording) { + if let Some(cb) = &*self.selected_cb.borrow() { + cb(&recording); + } + } +} + +impl NavigatorScreen for RecordingSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/selectors/selector.rs b/musicus/src/selectors/selector.rs new file mode 100644 index 0000000..f7b0de4 --- /dev/null +++ b/musicus/src/selectors/selector.rs @@ -0,0 +1,201 @@ +use crate::widgets::List; +use anyhow::Result; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::HeaderBarExt; +use std::cell::RefCell; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; + +/// A screen that presents a list of items. It allows to switch between the server and the local +/// database and to search within the list. +pub struct Selector { + pub widget: gtk::Box, + header: libhandy::HeaderBar, + server_check_button: gtk::CheckButton, + stack: gtk::Stack, + list: Rc>, + back_cb: RefCell ()>>>, + add_cb: RefCell ()>>>, + load_online: RefCell Box>>>>>>, + load_local: RefCell Box>>>>>, + filter: RefCell bool>>>, +} + +impl Selector { + /// Create a new selector. + pub fn new() -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/selector.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, libhandy::HeaderBar, header); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, add_button); + get_widget!(builder, gtk::SearchEntry, search_entry); + get_widget!(builder, gtk::CheckButton, server_check_button); + get_widget!(builder, gtk::Stack, stack); + get_widget!(builder, gtk::Frame, frame); + get_widget!(builder, gtk::Button, try_again_button); + + let list = List::::new(&gettext("Nothing found.")); + frame.add(&list.widget); + + let this = Rc::new(Self { + widget, + header, + server_check_button, + stack, + list, + back_cb: RefCell::new(None), + add_cb: RefCell::new(None), + load_online: RefCell::new(None), + load_local: RefCell::new(None), + filter: RefCell::new(None), + }); + + // Connect signals and callbacks + + back_button.connect_clicked(clone!(@strong this => move |_| { + if let Some(cb) = &*this.back_cb.borrow() { + cb(); + } + })); + + add_button.connect_clicked(clone!(@strong this => move |_| { + if let Some(cb) = &*this.add_cb.borrow() { + cb(); + } + })); + + search_entry.connect_search_changed(clone!(@strong this => move |_| { + this.list.invalidate_filter(); + })); + + this.server_check_button + .connect_toggled(clone!(@strong this => move |_| { + if this.server_check_button.get_active() { + this.clone().load_online(); + } else { + this.clone().load_local(); + } + })); + + this.list.set_filter( + clone!(@strong this, @strong search_entry => move |item: &T| { + match &*this.filter.borrow() { + Some(filter) => { + let search = search_entry.get_text().to_string().to_lowercase(); + search.is_empty() || filter(&search, item) + } + None => true, + } + }), + ); + + try_again_button.connect_clicked(clone!(@strong this => move |_| { + this.clone().load_online(); + })); + + // Initialize + this.clone().load_online(); + + this + } + + /// Set the title to be shown in the header. + pub fn set_title(&self, title: &str) { + self.header.set_title(Some(title)); + } + + /// Set the subtitle to be shown in the header. + pub fn set_subtitle(&self, subtitle: &str) { + self.header.set_subtitle(Some(subtitle)); + } + + /// Set the closure to be called when the user wants to go back. + pub fn set_back_cb () + 'static>(&self, cb: F) { + self.back_cb.replace(Some(Box::new(cb))); + } + + /// Set the closure to be called when the user wants to add an item. + pub fn set_add_cb () + 'static>(&self, cb: F) { + self.add_cb.replace(Some(Box::new(cb))); + } + + /// Set the async closure to be called to fetch items from the server. If that results in an + /// error, an error screen is shown allowing to try again. + pub fn set_load_online(&self, cb: F) + where + F: (Fn() -> R) + 'static, + R: Future>> + 'static, + { + self.load_online + .replace(Some(Box::new(move || Box::new(cb())))); + } + + /// Set the async closure to be called to get local items. + pub fn set_load_local(&self, cb: F) + where + F: (Fn() -> R) + 'static, + R: Future> + 'static, + { + self.load_local + .replace(Some(Box::new(move || Box::new(cb())))); + } + + /// Set the closure to be called for creating a new list row. + pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { + self.list.set_make_widget(make_widget); + } + + /// Set a closure to call when deciding whether to show an item based on a search string. The + /// search string will be converted to lowercase. + pub fn set_filter bool + 'static>(&self, filter: F) { + self.filter.replace(Some(Box::new(filter))); + } + + /// Set the closure to be called when an item is selected. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.list.set_selected(cb); + } + + fn load_online(self: Rc) { + let context = glib::MainContext::default(); + let clone = self.clone(); + context.spawn_local(async move { + if let Some(cb) = &*self.load_online.borrow() { + self.stack.set_visible_child_name("loading"); + + match Pin::from(cb()).await { + Ok(items) => { + clone.list.show_items(items); + clone.stack.set_visible_child_name("content"); + } + Err(_) => { + clone.list.show_items(Vec::new()); + clone.stack.set_visible_child_name("error"); + } + } + } + }); + } + + fn load_local(self: Rc) { + let context = glib::MainContext::default(); + let clone = self.clone(); + context.spawn_local(async move { + if let Some(cb) = &*self.load_local.borrow() { + self.stack.set_visible_child_name("loading"); + + let items = Pin::from(cb()).await; + clone.list.show_items(items); + clone.stack.set_visible_child_name("content"); + } + }); + } +} diff --git a/musicus/src/selectors/work.rs b/musicus/src/selectors/work.rs new file mode 100644 index 0000000..b116276 --- /dev/null +++ b/musicus/src/selectors/work.rs @@ -0,0 +1,113 @@ +use super::selector::Selector; +use crate::backend::Backend; +use crate::database::{Person, Work}; +use crate::editors::WorkEditor; +use crate::widgets::{Navigator, NavigatorScreen}; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for selecting a work. +pub struct WorkSelector { + backend: Rc, + person: Person, + selector: Rc>, + selected_cb: RefCell ()>>>, + navigator: RefCell>>, +} + +impl WorkSelector { + /// Create a new work selector for works by a specific composer. + pub fn new(backend: Rc, person: Person) -> Rc { + // Create UI + + let selector = Selector::::new(); + selector.set_title(&gettext("Select work")); + selector.set_subtitle(&person.name_fl()); + + let this = Rc::new(Self { + backend, + person, + selector, + selected_cb: RefCell::new(None), + navigator: RefCell::new(None), + }); + + // Connect signals and callbacks + + this.selector.set_back_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + })); + + this.selector.set_add_cb(clone!(@strong this => move || { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let editor = WorkEditor::new(this.backend.clone(), None); + editor + .set_saved_cb(clone!(@strong this => move |work| this.select(&work))); + navigator.push(editor); + } + })); + + this.selector + .set_load_online(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.get_works(&clone.person.id).await } + })); + + this.selector + .set_load_local(clone!(@strong this => move || { + let clone = this.clone(); + async move { clone.backend.db().get_works(&clone.person.id).await.unwrap() } + })); + + this.selector.set_make_widget(|work| { + let label = gtk::Label::new(Some(&work.title)); + label.set_halign(gtk::Align::Start); + label.set_margin_start(6); + label.set_margin_end(6); + label.set_margin_top(6); + label.set_margin_bottom(6); + label.upcast() + }); + + this.selector + .set_filter(|search, work| work.title.to_lowercase().contains(search)); + + this.selector + .set_selected_cb(clone!(@strong this => move |work| this.select(work))); + + this + } + + /// Set the closure to be called when an item is selected. + pub fn set_selected_cb () + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } + + /// Select a work. + fn select(&self, work: &Work) { + if let Some(cb) = &*self.selected_cb.borrow() { + cb(&work); + } + } +} + +impl NavigatorScreen for WorkSelector { + fn attach_navigator(&self, navigator: Rc) { + self.navigator.replace(Some(navigator)); + } + + fn get_widget(&self) -> gtk::Widget { + self.selector.widget.clone().upcast() + } + + fn detach_navigator(&self) { + self.navigator.replace(None); + } +} diff --git a/musicus/src/widgets/mod.rs b/musicus/src/widgets/mod.rs index 511c4b0..9e59253 100644 --- a/musicus/src/widgets/mod.rs +++ b/musicus/src/widgets/mod.rs @@ -4,6 +4,9 @@ pub use list::*; pub mod navigator; pub use navigator::*; +pub mod navigator_window; +pub use navigator_window::*; + pub mod player_bar; pub use player_bar::*; diff --git a/musicus/src/widgets/navigator.rs b/musicus/src/widgets/navigator.rs index 1a94ff7..2ae343b 100644 --- a/musicus/src/widgets/navigator.rs +++ b/musicus/src/widgets/navigator.rs @@ -10,6 +10,7 @@ pub trait NavigatorScreen { } pub struct Navigator { + pub window: gtk::Window, pub widget: gtk::Stack, screens: RefCell>>, old_screens: RefCell>>, @@ -17,17 +18,22 @@ pub struct Navigator { } impl Navigator { - pub fn new(empty_screen: &W) -> Rc + pub fn new(window: &W, empty_screen: &S) -> Rc where - W: IsA, + W: IsA, + S: IsA, { let widget = gtk::Stack::new(); + widget.set_hhomogeneous(false); + widget.set_vhomogeneous(false); + widget.set_interpolate_size(true); widget.set_transition_type(gtk::StackTransitionType::Crossfade); widget.set_hexpand(true); widget.add_named(empty_screen, "empty_screen"); widget.show(); let result = Rc::new(Self { + window: window.clone().upcast(), widget, screens: RefCell::new(Vec::new()), old_screens: RefCell::new(Vec::new()), @@ -48,7 +54,10 @@ impl Navigator { result } - pub fn set_back_cb(&self, cb: F) where F: Fn() -> () + 'static { + pub fn set_back_cb(&self, cb: F) + where + F: Fn() -> () + 'static, + { self.back_cb.replace(Some(Box::new(cb))); } diff --git a/musicus/src/widgets/navigator_window.rs b/musicus/src/widgets/navigator_window.rs new file mode 100644 index 0000000..66fccb7 --- /dev/null +++ b/musicus/src/widgets/navigator_window.rs @@ -0,0 +1,42 @@ +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use std::rc::Rc; + +/// A window hosting a navigator. +pub struct NavigatorWindow { + window: libhandy::Window, + navigator: Rc, +} + +impl NavigatorWindow { + /// Create a new navigator window showing an initial screen. + pub fn new(initial_screen: Rc) -> Rc { + // Create UI + + let window = libhandy::Window::new(); + window.set_default_size(600, 424); + let placeholder = gtk::Label::new(None); + let navigator = Navigator::new(&window, &placeholder); + window.add(&navigator.widget); + + let this = Rc::new(Self { window, navigator }); + + // Connect signals and callbacks + + this.navigator.set_back_cb(clone!(@strong this => move || { + this.window.close(); + })); + + // Initialize + + this.navigator.clone().replace(initial_screen); + + this + } + + /// Show the navigator window. + pub fn show(&self) { + self.window.show(); + } +} diff --git a/musicus/src/window.rs b/musicus/src/window.rs index 9d16f0b..d932bc5 100644 --- a/musicus/src/window.rs +++ b/musicus/src/window.rs @@ -1,5 +1,6 @@ use crate::backend::*; use crate::dialogs::*; +use crate::editors::TracksEditor; use crate::screens::*; use crate::widgets::*; use futures::prelude::*; @@ -42,7 +43,7 @@ impl Window { stack.add_named(&player_screen.widget, "player_screen"); let poe_list = PoeList::new(backend.clone()); - let navigator = Navigator::new(&empty_screen); + let navigator = Navigator::new(&window, &empty_screen); navigator.set_back_cb(clone!(@strong leaflet, @strong sidebar_box => move || { leaflet.set_visible_child(&sidebar_box); })); @@ -84,13 +85,14 @@ impl Window { })); add_button.connect_clicked(clone!(@strong result => move |_| { - let editor = TracksEditor::new(result.backend.clone(), &result.window, None, Vec::new()); + let editor = TracksEditor::new(result.backend.clone(), None, Vec::new()); editor.set_callback(clone!(@strong result => move || { result.reload(); })); - editor.show(); + let window = NavigatorWindow::new(editor); + window.show(); })); result