Merge recording editor and selector into one dialog

This commit is contained in:
Elias Projahn 2020-11-08 12:11:54 +01:00
parent 3e34658b25
commit 99bbd9e58f
22 changed files with 1167 additions and 1032 deletions

View file

@ -16,9 +16,6 @@ pub use instrument_selector::*;
pub mod part_editor;
pub use part_editor::*;
pub mod performance_editor;
pub use performance_editor::*;
pub mod person_editor;
pub use person_editor::*;
@ -28,11 +25,8 @@ pub use person_selector::*;
pub mod preferences;
pub use preferences::*;
pub mod recording_editor;
pub use recording_editor::*;
pub mod recording_selector;
pub use recording_selector::*;
pub mod recording;
pub use recording::*;
pub mod section_editor;
pub use section_editor::*;

View file

@ -1,147 +0,0 @@
use super::*;
use crate::backend::Backend;
use crate::database::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
pub struct PerformanceEditor<F>
where
F: Fn(PerformanceDescription) -> () + 'static,
{
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
save_button: gtk::Button,
person_label: gtk::Label,
ensemble_label: gtk::Label,
role_label: gtk::Label,
person: RefCell<Option<Person>>,
ensemble: RefCell<Option<Ensemble>>,
role: RefCell<Option<Instrument>>,
}
impl<F> PerformanceEditor<F>
where
F: Fn(PerformanceDescription) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
performance: Option<PerformanceDescription>,
callback: F,
) -> Rc<Self> {
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::Button, save_button);
get_widget!(builder, gtk::Button, person_button);
get_widget!(builder, gtk::Button, ensemble_button);
get_widget!(builder, gtk::Button, role_button);
get_widget!(builder, gtk::Button, reset_role_button);
get_widget!(builder, gtk::Label, person_label);
get_widget!(builder, gtk::Label, ensemble_label);
get_widget!(builder, gtk::Label, role_label);
let (person, ensemble, role) = match performance {
Some(performance) => {
match performance.person.clone() {
Some(person) => {
person_label.set_text(&person.name_fl());
save_button.set_sensitive(true);
}
None => (),
}
match performance.ensemble.clone() {
Some(ensemble) => {
ensemble_label.set_text(&ensemble.name);
save_button.set_sensitive(true);
}
None => (),
}
match performance.role.clone() {
Some(role) => role_label.set_text(&role.name),
None => (),
}
(performance.person, performance.ensemble, performance.role)
}
None => (None, None, None),
};
let result = Rc::new(PerformanceEditor {
backend: backend,
window: window,
callback: callback,
save_button: save_button,
person_label: person_label,
ensemble_label: ensemble_label,
role_label: role_label,
person: RefCell::new(person),
ensemble: RefCell::new(ensemble),
role: RefCell::new(role),
});
cancel_button.connect_clicked(clone!(@strong result => move |_| {
result.window.close();
}));
result
.save_button
.connect_clicked(clone!(@strong result => move |_| {
(result.callback)(PerformanceDescription {
person: result.person.borrow().clone(),
ensemble: result.ensemble.borrow().clone(),
role: result.role.borrow().clone(),
});
result.window.close();
}));
person_button.connect_clicked(clone!(@strong result => move |_| {
PersonSelector::new(result.backend.clone(), &result.window, clone!(@strong result => move |person| {
result.person.replace(Some(person.clone()));
result.person_label.set_text(&person.name_fl());
result.ensemble.replace(None);
result.ensemble_label.set_text(&gettext("Select …"));
result.save_button.set_sensitive(true);
})).show();
}));
ensemble_button.connect_clicked(clone!(@strong result => move |_| {
EnsembleSelector::new(result.backend.clone(), &result.window, clone!(@strong result => move |ensemble| {
result.ensemble.replace(Some(ensemble.clone()));
result.ensemble_label.set_text(&ensemble.name);
result.person.replace(None);
result.person_label.set_text(&gettext("Select …"));
result.save_button.set_sensitive(true);
})).show();
}));
role_button.connect_clicked(clone!(@strong result => move |_| {
InstrumentSelector::new(result.backend.clone(), &result.window, clone!(@strong result => move |role| {
result.role.replace(Some(role.clone()));
result.role_label.set_text(&role.name);
})).show();
}));
reset_role_button.connect_clicked(clone!(@strong result => move |_| {
result.role.replace(None);
result.role_label.set_text(&gettext("Select …"));
}));
result.window.set_transient_for(Some(parent));
result
}
pub fn show(&self) {
self.window.show();
}
}

View file

@ -0,0 +1,11 @@
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;

View file

@ -0,0 +1,174 @@
use crate::backend::*;
use crate::database::*;
use crate::dialogs::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A dialog for editing a performance within a recording.
pub struct PerformanceEditor {
backend: Rc<Backend>,
window: libhandy::Window,
save_button: gtk::Button,
person_label: gtk::Label,
ensemble_label: gtk::Label,
role_label: gtk::Label,
reset_role_button: gtk::Button,
person: RefCell<Option<Person>>,
ensemble: RefCell<Option<Ensemble>>,
role: RefCell<Option<Instrument>>,
selected_cb: RefCell<Option<Box<dyn Fn(PerformanceDescription) -> ()>>>,
}
impl PerformanceEditor {
/// Create a new performance editor.
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
performance: Option<PerformanceDescription>,
) -> Rc<Self> {
// 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::Button, save_button);
get_widget!(builder, gtk::Button, person_button);
get_widget!(builder, gtk::Button, ensemble_button);
get_widget!(builder, gtk::Button, role_button);
get_widget!(builder, gtk::Button, reset_role_button);
get_widget!(builder, gtk::Label, person_label);
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,
save_button,
person_label,
ensemble_label,
role_label,
reset_role_button,
person: RefCell::new(None),
ensemble: RefCell::new(None),
role: RefCell::new(None),
selected_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
this.window.close();
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(PerformanceDescription {
person: this.person.borrow().clone(),
ensemble: this.ensemble.borrow().clone(),
role: this.role.borrow().clone(),
});
this.window.close();
}
}));
person_button.connect_clicked(clone!(@strong this => move |_| {
PersonSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |person| {
this.show_person(Some(&person));
this.person.replace(Some(person));
this.show_ensemble(None);
this.ensemble.replace(None);
})).show();
}));
ensemble_button.connect_clicked(clone!(@strong this => move |_| {
EnsembleSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |ensemble| {
this.show_person(None);
this.person.replace(None);
this.show_ensemble(Some(&ensemble));
this.ensemble.replace(Some(ensemble));
})).show();
}));
role_button.connect_clicked(clone!(@strong this => move |_| {
InstrumentSelector::new(this.backend.clone(), &this.window, clone!(@strong this => move |role| {
this.show_role(Some(&role));
this.role.replace(Some(role));
})).show();
}));
this.reset_role_button
.connect_clicked(clone!(@strong this => move |_| {
this.show_role(None);
this.role.replace(None);
}));
// Initialize
if let Some(performance) = performance {
if let Some(person) = performance.person {
this.show_person(Some(&person));
this.person.replace(Some(person));
} else if let Some(ensemble) = performance.ensemble {
this.show_ensemble(Some(&ensemble));
this.ensemble.replace(Some(ensemble));
}
if let Some(role) = performance.role {
this.show_role(Some(&role));
this.role.replace(Some(role));
}
}
this
}
/// Set a closure to be called when the user has chosen to save the performance.
pub fn set_selected_cb<F: Fn(PerformanceDescription) -> () + 'static>(&self, cb: F) {
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 {
self.person_label.set_text(&person.name_fl());
self.save_button.set_sensitive(true);
} else {
self.person_label.set_text(&gettext("Select …"));
}
}
/// Update the UI according to ensemble.
fn show_ensemble(&self, ensemble: Option<&Ensemble>) {
if let Some(ensemble) = ensemble {
self.ensemble_label.set_text(&ensemble.name);
self.save_button.set_sensitive(true);
} else {
self.ensemble_label.set_text(&gettext("Select …"));
}
}
/// Update the UI according to role.
fn show_role(&self, role: Option<&Instrument>) {
if let Some(role) = role {
self.role_label.set_text(&role.name);
self.reset_role_button.show();
} else {
self.role_label.set_text(&gettext("Select …"));
self.reset_role_button.hide();
}
}
}

View file

@ -0,0 +1,86 @@
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<RecordingSelector>,
editor: Rc<RecordingEditor>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
}
impl RecordingDialog {
/// Create a new recording dialog.
pub fn new<W: IsA<gtk::Window>>(backend: Rc<Backend>, parent: &W) -> Rc<Self> {
// 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<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the recording dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -0,0 +1,197 @@
use super::performance_editor::*;
use crate::backend::*;
use crate::database::*;
use crate::dialogs::*;
use crate::widgets::*;
use gettextrs::gettext;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
/// A widget for creating or editing a recording.
// TODO: Disable buttons if no performance is selected.
pub struct RecordingEditor {
pub widget: gtk::Box,
backend: Rc<Backend>,
parent: gtk::Window,
save_button: gtk::Button,
work_label: gtk::Label,
comment_entry: gtk::Entry,
performance_list: Rc<List<PerformanceDescription>>,
id: i64,
work: RefCell<Option<WorkDescription>>,
performances: RefCell<Vec<PerformanceDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
back_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
}
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<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
recording: Option<RecordingDescription>,
) -> Rc<Self> {
// Create UI
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui");
get_widget!(builder, gtk::Box, widget);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Button, work_button);
get_widget!(builder, gtk::Label, work_label);
get_widget!(builder, gtk::Entry, comment_entry);
get_widget!(builder, gtk::ScrolledWindow, scroll);
get_widget!(builder, gtk::Button, add_performer_button);
get_widget!(builder, gtk::Button, edit_performer_button);
get_widget!(builder, gtk::Button, remove_performer_button);
let performance_list = List::new(&gettext("No performers added."));
scroll.add(&performance_list.widget);
let (id, work, performances) = match recording {
Some(recording) => (recording.id, Some(recording.work), recording.performances),
None => (rand::random::<u32>().into(), None, Vec::new()),
};
let this = Rc::new(RecordingEditor {
widget,
backend,
parent: parent.clone().upcast(),
save_button,
work_label,
comment_entry,
performance_list,
id,
work: RefCell::new(work),
performances: RefCell::new(performances),
selected_cb: RefCell::new(None),
back_cb: RefCell::new(None),
});
// Connect signals and callbacks
cancel_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(cb) = &*this.back_cb.borrow() {
cb();
}
}));
this.save_button
.connect_clicked(clone!(@strong this => move |_| {
let recording = RecordingDescription {
id: this.id,
work: this.work.borrow().clone().expect("Tried to create recording without work!"),
comment: this.comment_entry.get_text().to_string(),
performances: this.performances.borrow().clone(),
};
let c = glib::MainContext::default();
let clone = this.clone();
c.spawn_local(async move {
clone.backend.update_recording(recording.clone().into()).await.unwrap();
if let Some(cb) = &*clone.selected_cb.borrow() {
cb(recording.clone());
}
});
}));
work_button.connect_clicked(clone!(@strong this => move |_| {
WorkSelector::new(this.backend.clone(), &this.parent, clone!(@strong this => move |work| {
this.work_selected(&work);
this.work.replace(Some(work));
})).show();
}));
this.performance_list.set_make_widget(|performance| {
let label = gtk::Label::new(Some(&performance.get_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()
});
add_performer_button.connect_clicked(clone!(@strong this => move |_| {
let editor = PerformanceEditor::new(this.backend.clone(), &this.parent, None);
editor.set_selected_cb(clone!(@strong this => 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(),
};
performances.push(performance);
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
}));
editor.show();
}));
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 editor = PerformanceEditor::new(
this.backend.clone(),
&this.parent,
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.show();
}
}));
remove_performer_button.connect_clicked(clone!(@strong this => move |_| {
if let Some(index) = this.performance_list.get_selected_index() {
let mut performances = this.performances.borrow_mut();
performances.remove(index);
this.performance_list.show_items(performances.clone());
this.performance_list.select_index(index);
}
}));
// Initialize
if let Some(work) = &*this.work.borrow() {
this.work_selected(work);
}
this.performance_list.show_items(this.performances.borrow().clone());
this
}
/// Set the closure to be called if the editor is canceled.
pub fn set_back_cb<F: Fn() -> () + 'static>(&self, cb: F) {
self.back_cb.replace(Some(Box::new(cb)));
}
/// Set the closure to be called if the recording was created.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Update the UI according to work.
fn work_selected(&self, work: &WorkDescription) {
self.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title));
self.save_button.set_sensitive(true);
}
}

View file

@ -0,0 +1,63 @@
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<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
}
impl RecordingEditorDialog {
/// Create a new recording editor dialog and optionally initialize it.
pub fn new<W: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &W,
recording: Option<RecordingDescription>,
) -> Rc<Self> {
// 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<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
/// Show the recording editor dialog.
pub fn show(&self) {
self.window.show();
}
}

View file

@ -0,0 +1,89 @@
use super::recording_selector_person_screen::*;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
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<Backend>,
sidebar_box: gtk::Box,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
add_cb: RefCell<Option<Box<dyn Fn() -> ()>>>,
navigator: Rc<Navigator>,
}
impl RecordingSelector {
/// Create a new recording selector.
pub fn new(backend: Rc<Backend>) -> Rc<Self> {
// 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::Box, empty_screen);
let person_list = PersonList::new(backend.clone());
sidebar_box.pack_start(&person_list.widget, true, true, 0);
let navigator = Navigator::new(&empty_screen);
widget.add(&navigator.widget);
let this = Rc::new(Self {
widget,
backend,
sidebar_box,
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();
}
}));
person_list.set_selected(clone!(@strong this => move |person| {
let person_screen = RecordingSelectorPersonScreen::new(
this.backend.clone(),
person.clone(),
);
person_screen.set_selected_cb(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording);
}
}));
this.navigator.clone().push(person_screen);
this.widget.set_visible_child(&this.navigator.widget);
}));
this.navigator.set_back_cb(clone!(@strong this => move || {
this.widget.set_visible_child(&this.sidebar_box);
}));
this
}
/// Set the closure to be called if the editor is user wants to add a new recording.
pub fn set_add_cb<F: Fn() -> () + '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<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}

View file

@ -0,0 +1,126 @@
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 persons
/// and switches to a work screen on selection.
pub struct RecordingSelectorPersonScreen {
backend: Rc<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingSelectorPersonScreen {
/// Create a new recording selector person screen.
pub fn new(backend: Rc<Backend>, person: Person) -> Rc<Self> {
// 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);
header.set_title(Some(&person.name_fl()));
let work_list = List::new(&gettext("No works found."));
stack.add_named(&work_list.widget, "content");
let this = Rc::new(Self {
backend,
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();
}
}));
this.work_list.set_make_widget(|work: &WorkDescription| {
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(),
);
work_screen.set_selected_cb(clone!(@strong this => move |recording| {
if let Some(cb) = &*this.selected_cb.borrow() {
cb(recording);
}
}));
navigator.push(work_screen);
}
}));
// Initialize
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.await
.unwrap();
clone.work_list.show_items(works);
clone.stack.set_visible_child_name("content");
});
this
}
/// Sets a closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for RecordingSelectorPersonScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -0,0 +1,120 @@
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<Backend>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
selected_cb: RefCell<Option<Box<dyn Fn(RecordingDescription) -> ()>>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingSelectorWorkScreen {
/// Create a new recording selector work screen.
pub fn new(backend: Rc<Backend>, work: WorkDescription) -> Rc<Self> {
// 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);
header.set_title(Some(&work.title));
header.set_subtitle(Some(&work.composer.name_fl()));
let recording_list = List::new(&gettext("No recordings found."));
stack.add_named(&recording_list.widget, "content");
let this = Rc::new(Self {
backend,
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();
}
}));
this.recording_list.set_make_widget(|recording: &RecordingDescription| {
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());
}
}));
// Initialize
let context = glib::MainContext::default();
let clone = this.clone();
context.spawn_local(async move {
let recordings = clone
.backend
.get_recordings_for_work(work.id)
.await
.unwrap();
clone.recording_list.show_items(recordings);
clone.stack.set_visible_child_name("content");
});
this
}
/// Sets a closure to be called when the user has selected a recording.
pub fn set_selected_cb<F: Fn(RecordingDescription) -> () + 'static>(&self, cb: F) {
self.selected_cb.replace(Some(Box::new(cb)));
}
}
impl NavigatorScreen for RecordingSelectorWorkScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -1,186 +0,0 @@
use super::*;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use std::cell::RefCell;
use std::rc::Rc;
use std::convert::TryInto;
pub struct RecordingEditor<F>
where
F: Fn(RecordingDescription) -> () + 'static,
{
backend: Rc<Backend>,
window: libhandy::Window,
callback: F,
id: i64,
save_button: gtk::Button,
work_label: gtk::Label,
work: RefCell<Option<WorkDescription>>,
comment_entry: gtk::Entry,
performers: RefCell<Vec<PerformanceDescription>>,
performer_list: gtk::ListBox,
}
impl<F> RecordingEditor<F>
where
F: Fn(RecordingDescription) -> () + 'static,
{
pub fn new<P: IsA<gtk::Window>>(
backend: Rc<Backend>,
parent: &P,
recording: Option<RecordingDescription>,
callback: F,
) -> Rc<Self> {
let builder =
gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_editor.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, gtk::Button, cancel_button);
get_widget!(builder, gtk::Button, save_button);
get_widget!(builder, gtk::Button, work_button);
get_widget!(builder, gtk::Label, work_label);
get_widget!(builder, gtk::Entry, comment_entry);
get_widget!(builder, gtk::ListBox, performer_list);
get_widget!(builder, gtk::Button, add_performer_button);
get_widget!(builder, gtk::Button, edit_performer_button);
get_widget!(builder, gtk::Button, remove_performer_button);
let (id, work, performers) = match recording {
Some(recording) => {
save_button.set_sensitive(true);
work_label.set_text(&recording.work.get_title());
comment_entry.set_text(&recording.comment);
(recording.id, Some(recording.work), recording.performances)
}
None => (rand::random::<u32>().into(), None, Vec::new()),
};
let result = Rc::new(RecordingEditor {
backend: backend,
window: window,
callback: callback,
id: id,
save_button: save_button,
work_label: work_label,
work: RefCell::new(work),
comment_entry: comment_entry,
performers: RefCell::new(performers),
performer_list: performer_list,
});
cancel_button.connect_clicked(clone!(@strong result => move |_| {
result.window.close();
}));
result
.save_button
.connect_clicked(clone!(@strong result => move |_| {
let recording = RecordingDescription {
id: result.id,
work: result.work.borrow().clone().expect("Tried to create recording without work!"),
comment: result.comment_entry.get_text().to_string(),
performances: result.performers.borrow().to_vec(),
};
let c = glib::MainContext::default();
let clone = result.clone();
c.spawn_local(async move {
clone.backend.update_recording(recording.clone().into()).await.unwrap();
clone.window.close();
(clone.callback)(recording.clone());
});
}));
work_button.connect_clicked(clone!(@strong result => move |_| {
WorkSelector::new(result.backend.clone(), &result.window, clone!(@strong result => move |work| {
result.work.replace(Some(work.clone()));
result.work_label.set_text(&format!("{}: {}", work.composer.name_fl(), work.title));
result.save_button.set_sensitive(true);
})).show();
}));
add_performer_button.connect_clicked(clone!(@strong result => move |_| {
PerformanceEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |performance| {
{
let mut performers = result.performers.borrow_mut();
performers.push(performance);
}
result.show_performers();
})).show();
}));
edit_performer_button.connect_clicked(clone!(@strong result => move |_| {
let row = result.get_selected_performer_row();
match row {
Some(row) => {
let index = row.get_index();
let index: usize = index.try_into().unwrap();
let performer = result.performers.borrow()[index].clone();
PerformanceEditor::new(result.backend.clone(), &result.window, Some(performer), clone!(@strong result => move |performer| {
result.performers.borrow_mut()[index] = performer;
result.show_performers();
})).show();
}
None => (),
}
}));
remove_performer_button.connect_clicked(clone!(@strong result => move |_| {
let row = result.get_selected_performer_row();
match row {
Some(row) => {
let index = row.get_index();
let index: usize = index.try_into().unwrap();
result.performers.borrow_mut().remove(index);
result.show_performers();
}
None => (),
}
}));
result.window.set_transient_for(Some(parent));
result.show_performers();
result
}
pub fn show(&self) {
self.window.show();
}
fn show_performers(&self) {
for child in self.performer_list.get_children() {
self.performer_list.remove(&child);
}
for (index, performer) in self.performers.borrow().iter().enumerate() {
let label = gtk::Label::new(Some(&performer.get_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);
let row = SelectorRow::new(index.try_into().unwrap(), &label);
row.show_all();
self.performer_list.insert(&row, -1);
}
}
fn get_selected_performer_row(&self) -> Option<SelectorRow> {
match self.performer_list.get_selected_rows().first() {
Some(row) => match row.get_child() {
Some(child) => Some(child.downcast().unwrap()),
None => None,
},
None => None,
}
}
}

View file

@ -1,279 +0,0 @@
use super::*;
use crate::backend::Backend;
use crate::database::*;
use crate::widgets::*;
use gettextrs::gettext;
use gio::prelude::*;
use glib::clone;
use gtk::prelude::*;
use gtk_macros::get_widget;
use libhandy::prelude::*;
use libhandy::HeaderBarExt;
use std::cell::RefCell;
use std::rc::Rc;
pub struct RecordingSelector {
backend: Rc<Backend>,
window: libhandy::Window,
callback: Box<dyn Fn(RecordingDescription) -> () + 'static>,
leaflet: libhandy::Leaflet,
navigator: Rc<Navigator>,
}
impl RecordingSelector {
pub fn new<P, F>(backend: Rc<Backend>, parent: &P, callback: F) -> Rc<Self>
where
P: IsA<gtk::Window>,
F: Fn(RecordingDescription) -> () + 'static,
{
let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/recording_selector.ui");
get_widget!(builder, libhandy::Window, window);
get_widget!(builder, libhandy::Leaflet, leaflet);
get_widget!(builder, gtk::Button, add_button);
get_widget!(builder, gtk::Box, sidebar_box);
get_widget!(builder, gtk::Box, empty_screen);
let person_list = PersonList::new(backend.clone());
sidebar_box.pack_start(&person_list.widget, true, true, 0);
let navigator = Navigator::new(&empty_screen);
leaflet.add(&navigator.widget);
navigator.set_back_cb(clone!(@strong leaflet, @strong sidebar_box => move || {
leaflet.set_visible_child(&sidebar_box);
}));
let result = Rc::new(Self {
backend: backend,
window: window,
callback: Box::new(callback),
leaflet: leaflet,
navigator: navigator,
});
add_button.connect_clicked(clone!(@strong result => move |_| {
let editor = RecordingEditor::new(
result.backend.clone(),
&result.window,
None,
clone!(@strong result => move |recording| {
result.select(recording);
}),
);
editor.show();
}));
person_list.set_selected(clone!(@strong result => move |person| {
result.navigator.clone().replace(RecordingSelectorPersonScreen::new(result.backend.clone(), result.clone(), person.clone()));
result.leaflet.set_visible_child(&result.navigator.widget);
}));
result.window.set_transient_for(Some(parent));
result
}
pub fn show(&self) {
self.window.show();
}
pub fn select(&self, recording: RecordingDescription) {
self.window.close();
(self.callback)(recording);
}
}
struct RecordingSelectorPersonScreen {
backend: Rc<Backend>,
selector: Rc<RecordingSelector>,
widget: gtk::Box,
stack: gtk::Stack,
work_list: Rc<List<WorkDescription>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingSelectorPersonScreen {
pub fn new(backend: Rc<Backend>, selector: Rc<RecordingSelector>, person: Person) -> Rc<Self> {
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);
header.set_title(Some(&person.name_fl()));
let work_list = List::new(&gettext("No works found."));
work_list.set_make_widget(|work: &WorkDescription| {
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()
});
stack.add_named(&work_list.widget, "content");
let result = Rc::new(Self {
backend,
selector,
widget,
stack,
work_list,
navigator: RefCell::new(None),
});
back_button.connect_clicked(clone!(@strong result => move |_| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}));
result
.work_list
.set_selected(clone!(@strong result => move |work| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.push(RecordingSelectorWorkScreen::new(result.backend.clone(), result.selector.clone(), work.clone()));
}
}));
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let works = clone
.backend
.get_work_descriptions(person.id)
.await
.unwrap();
clone.work_list.show_items(works);
clone.stack.set_visible_child_name("content");
});
result
}
}
impl NavigatorScreen for RecordingSelectorPersonScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}
struct RecordingSelectorWorkScreen {
backend: Rc<Backend>,
selector: Rc<RecordingSelector>,
widget: gtk::Box,
stack: gtk::Stack,
recording_list: Rc<List<RecordingDescription>>,
navigator: RefCell<Option<Rc<Navigator>>>,
}
impl RecordingSelectorWorkScreen {
pub fn new(
backend: Rc<Backend>,
selector: Rc<RecordingSelector>,
work: WorkDescription,
) -> Rc<Self> {
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);
header.set_title(Some(&work.title));
header.set_subtitle(Some(&work.composer.name_fl()));
let recording_list = List::new(&gettext("No recordings found."));
recording_list.set_make_widget(|recording: &RecordingDescription| {
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()
});
stack.add_named(&recording_list.widget, "content");
let result = Rc::new(Self {
backend,
selector,
widget,
stack,
recording_list,
navigator: RefCell::new(None),
});
back_button.connect_clicked(clone!(@strong result => move |_| {
let navigator = result.navigator.borrow().clone();
if let Some(navigator) = navigator {
navigator.clone().pop();
}
}));
result
.recording_list
.set_selected(clone!(@strong result => move |recording| {
result.selector.select(recording.clone());
}));
let context = glib::MainContext::default();
let clone = result.clone();
context.spawn_local(async move {
let recordings = clone
.backend
.get_recordings_for_work(work.id)
.await
.unwrap();
clone.recording_list.show_items(recordings);
clone.stack.set_visible_child_name("content");
});
result
}
}
impl NavigatorScreen for RecordingSelectorWorkScreen {
fn attach_navigator(&self, navigator: Rc<Navigator>) {
self.navigator.replace(Some(navigator));
}
fn get_widget(&self) -> gtk::Widget {
self.widget.clone().upcast()
}
fn detach_navigator(&self) {
self.navigator.replace(None);
}
}

View file

@ -95,14 +95,14 @@ impl TracksEditor {
}));
recording_button.connect_clicked(clone!(@strong this => move |_| {
RecordingSelector::new(
this.backend.clone(),
&this.window,
clone!(@strong this => move |recording| {
this.recording_selected(&recording);
this.recording.replace(Some(recording));
}),
).show();
let dialog = RecordingDialog::new(this.backend.clone(), &this.window);
dialog.set_selected_cb(clone!(@strong this => move |recording| {
this.recording_selected(&recording);
this.recording.replace(Some(recording));
}));
dialog.show();
}
));

View file

@ -45,12 +45,17 @@ sources = files(
'dialogs/instrument_selector.rs',
'dialogs/mod.rs',
'dialogs/part_editor.rs',
'dialogs/performance_editor.rs',
'dialogs/person_editor.rs',
'dialogs/person_selector.rs',
'dialogs/preferences.rs',
'dialogs/recording_editor.rs',
'dialogs/recording_selector.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/section_editor.rs',
'dialogs/track_editor.rs',
'dialogs/tracks_editor.rs',

View file

@ -166,9 +166,13 @@ impl Window {
result.window,
"add-recording",
clone!(@strong result => move |_, _| {
RecordingEditor::new(result.backend.clone(), &result.window, None, clone!(@strong result => move |_| {
let dialog = RecordingDialog::new(result.backend.clone(), &result.window);
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
})).show();
}));
dialog.show();
})
);
@ -292,9 +296,13 @@ impl Window {
let c = glib::MainContext::default();
c.spawn_local(async move {
let recording = result.backend.get_recording_description(id).await.unwrap();
RecordingEditor::new(result.backend.clone(), &result.window, Some(recording), clone!(@strong result => move |_| {
let dialog = RecordingEditorDialog::new(result.backend.clone(), &result.window, Some(recording));
dialog.set_selected_cb(clone!(@strong result => move |_| {
result.reload();
})).show();
}));
dialog.show();
});
})
);