From dbae0ad81b777fac41674330070730ab7c8909e3 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Wed, 13 Jan 2021 19:27:22 +0100 Subject: [PATCH] Make track set editor functional --- musicus/src/editors/track_set.rs | 580 ------------------------- musicus/src/editors/track_source.rs | 42 -- musicus/src/import/medium_editor.rs | 10 +- musicus/src/import/mod.rs | 3 + musicus/src/import/track_editor.rs | 110 +++++ musicus/src/import/track_selector.rs | 122 ++++++ musicus/src/import/track_set_editor.rs | 275 ++++++++++++ musicus/src/meson.build | 1 - 8 files changed, 515 insertions(+), 628 deletions(-) delete mode 100644 musicus/src/editors/track_set.rs delete mode 100644 musicus/src/editors/track_source.rs create mode 100644 musicus/src/import/track_editor.rs create mode 100644 musicus/src/import/track_selector.rs create mode 100644 musicus/src/import/track_set_editor.rs diff --git a/musicus/src/editors/track_set.rs b/musicus/src/editors/track_set.rs deleted file mode 100644 index 716fa82..0000000 --- a/musicus/src/editors/track_set.rs +++ /dev/null @@ -1,580 +0,0 @@ -use crate::backend::Backend; -use crate::database::{Recording, Track, TrackSet}; -use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector}; -use crate::widgets::{Navigator, NavigatorScreen}; -use gettextrs::gettext; -use glib::clone; -use gtk::prelude::*; -use gtk_macros::get_widget; -use libhandy::prelude::*; -use std::cell::{Cell, RefCell}; -use std::collections::HashSet; -use std::rc::Rc; - -/// Representation of a track that can be imported into the music library. -#[derive(Debug, Clone)] -struct TrackSource { - /// A short string identifying the track for the user. - pub description: String, - - /// Whether the track is ready to be imported. - pub ready: bool, -} - -/// Representation of a medium that can be imported into the music library. -#[derive(Debug, Clone)] -struct MediumSource { - /// The tracks that can be imported from the medium. - pub tracks: Vec, - - /// Whether all tracks are ready to be imported. - pub ready: bool, -} - -impl MediumSource { - /// Create a dummy medium source for testing purposes. - fn dummy() -> Self { - let mut tracks = Vec::new(); - - for index in 0..20 { - tracks.push(TrackSource { - description: format!("Track {}", index + 1), - ready: Cell::new(true), - }); - } - - Self { - tracks, - ready: Cell::new(true), - } - } -} - -/// A track while being edited. -#[derive(Debug, Clone)] -struct TrackData<'a> { - /// A reference to the selected track source. - pub source: &'a TrackSource, - - /// The actual value for the track. - pub track: Track, -} - -/// A track set while being edited. -#[derive(Debug, Clone)] -struct TrackSetData<'a> { - /// The recording to which the tracks belong. - pub recording: Option, - - /// The tracks that are being edited. - pub tracks: Vec>, -} - -impl TrackSetData { - /// Create a new empty track set. - pub fn new() -> Self { - Self { - recording: None, - tracks: Vec::new(), - } - } -} - -/// A screen for editing a set of tracks for one recording. -pub struct TrackSetEditor { - backend: Rc, - source: Rc>, - widget: gtk::Box, - save_button: gtk::Button, - recording_row: libhandy::ActionRow, - track_list: List, - data: RefCell, - done_cb: RefCell>>, - navigator: RefCell>>, -} - -impl TrackSetEditor { - /// Create a new track set editor. - pub fn new(backend: Rc, source: Rc) -> Rc { - // TODO: Replace with argument. - let source = Rc::new(RefCell::new(MediumSource::dummy())); - - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, save_button); - get_widget!(builder, libhandy::ActionRow, recording_row); - get_widget!(builder, gtk::Button, select_recording_button); - get_widget!(builder, gtk::Button, edit_tracks_button); - get_widget!(builder, gtk::Frame, tracks_frame); - - let track_list = List::new(&gettext!("No tracks added")); - tracks_frame.add(&track_list.widget); - - let this = Rc::new(Self { - backend, - source, - widget, - save_button, - recording_row, - track_list, - data: RefCell::new(TrackSetData::new()), - done_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.save_button.connect_clicked(clone!(@strong this => move |_| { - if let Some(cb) = &*this.done_cb.borrow() {} - })); - - select_recording_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let person_selector = PersonSelector::new(this.backend.clone()); - - 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| { - let mut data = this.data.borrow_mut(); - data.recording = Some(recording); - this.recording_selected(); - - navigator.clone().pop(); - navigator.clone().pop(); - navigator.clone().pop(); - })); - - navigator.clone().push(recording_selector); - })); - - navigator.clone().push(work_selector); - })); - - navigator.clone().push(person_selector); - } - })); - - edit_tracks_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - let selector = TrackSelector::new(Rc::clone(this.source)); - - selector.set_selected_cb(clone!(@strong this => move |selection| { - let mut tracks = Vec::new(); - - for index in selection { - let track = Track { - work_parts: Vec::new(), - }; - - let source = this.source.tracks[index].clone(); - - let data = TrackData { - track, - source, - }; - - tracks.push(data); - } - - let length = tracks.len(); - this.tracks.replace(tracks); - this.track_list.update(length); - this.autofill_parts(); - })); - - navigator.push(selector); - } - })); - - this.track_list.set_make_widget(clone!(@strong this => move |index| { - let data = &this.tracks.borrow()[index]; - - let mut title_parts = Vec::::new(); - - if let Some(recording) = &*this.recording.borrow() { - for part in &data.track.work_parts { - title_parts.push(recording.work.parts[*part].title.clone()); - } - } - - let title = if title_parts.is_empty() { - gettext("Unknown") - } else { - title_parts.join(", ") - }; - - let subtitle = data.source.description.clone(); - - let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); - let edit_button = gtk::Button::new(); - edit_button.set_relief(gtk::ReliefStyle::None); - edit_button.set_valign(gtk::Align::Center); - edit_button.add(&edit_image); - - let row = libhandy::ActionRow::new(); - row.set_activatable(true); - row.set_title(Some(&title)); - row.set_subtitle(Some(&subtitle)); - row.add(&edit_button); - row.set_activatable_widget(Some(&edit_button)); - row.show_all(); - - edit_button.connect_clicked(clone!(@strong this => move |_| { - let recording = this.recording.borrow().clone(); - let navigator = this.navigator.borrow().clone(); - - if let (Some(recording), Some(navigator)) = (recording, navigator) { - let editor = TrackEditor::new(recording, Vec::new()); - - editor.set_selected_cb(clone!(@strong this => move |selection| { - { - let mut tracks = &mut this.data.borrow_mut().tracks; - let mut track = &mut tracks[index]; - track.track.work_parts = selection; - }; - - this.update_tracks(); - })); - - navigator.push(editor); - } - })); - - row.upcast() - })); - - this - } - - /// Set the closure to be called when the user has created the track set. - pub fn set_done_cb(&self, cb: F) { - self.done_cb.replace(Some(Box::new(cb))); - } - - /// Set everything up after selecting a recording. - fn recording_selected(&self) { - if let Some(recording) = self.data.borrow().recording { - self.recording_row.set_title(Some(&recording.work.get_title())); - self.recording_row.set_subtitle(Some(&recording.get_performers())); - self.save_button.set_sensitive(true); - } - - self.autofill_parts(); - } - - /// Automatically try to put work part information from the selected recording into the - /// selected tracks. - fn autofill_parts(&self) { - if let Some(recording) = self.data.borrow().recording { - let mut tracks = self.tracks.borrow_mut(); - - for (index, _) in recording.work.parts.iter().enumerate() { - if let Some(mut data) = tracks.get_mut(index) { - data.track.work_parts = vec![index]; - } else { - break; - } - } - } - - self.update_tracks(); - } - - /// Update the track list. - fn update_tracks(&self) { - let length = self.data.borrow().tracks.len(); - self.track_list.update(length); - } -} - -impl NavigatorScreen for TrackSetEditor { - 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); - } -} - - -/// A screen for selecting tracks from a medium. -struct TrackSelector { - source: Rc>, - widget: gtk::Box, - select_button: gtk::Button, - selection: RefCell>, - selected_cb: RefCell)>>>, - navigator: RefCell>>, -} - -impl TrackSelector { - /// Create a new track selector. - pub fn new(source: Rc>) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, select_button); - get_widget!(builder, gtk::Frame, tracks_frame); - - let track_list = gtk::ListBox::new(); - track_list.set_selection_mode(gtk::SelectionMode::None); - track_list.set_vexpand(false); - track_list.show(); - tracks_frame.add(&track_list); - - let this = Rc::new(Self { - source, - widget, - select_button, - selection: RefCell::new(Vec::new()), - 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.select_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - - if let Some(cb) = &*this.selected_cb.borrow() { - let selection = this.selection.borrow().clone(); - cb(selection); - } - })); - - for (index, track) in this.tracks.iter().enumerate() { - let check = gtk::CheckButton::new(); - - check.connect_toggled(clone!(@strong this => move |check| { - let mut selection = this.selection.borrow_mut(); - if check.get_active() { - selection.push(index); - } else { - if let Some(pos) = selection.iter().position(|part| *part == index) { - selection.remove(pos); - } - } - - if selection.is_empty() { - this.select_button.set_sensitive(false); - } else { - this.select_button.set_sensitive(true); - } - })); - - let row = libhandy::ActionRow::new(); - row.add_prefix(&check); - row.set_activatable_widget(Some(&check)); - row.set_title(Some(&track.description)); - row.show_all(); - - track_list.add(&row); - } - - this - } - - /// Set the closure to be called when the user has selected tracks. The - /// closure will be called with the indices of the selected tracks as its - /// argument. - pub fn set_selected_cb) + 'static>(&self, cb: F) { - self.selected_cb.replace(Some(Box::new(cb))); - } -} - -impl NavigatorScreen for TrackSelector { - 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); - } -} - -/// A screen for editing a single track. -struct TrackEditor { - widget: gtk::Box, - selection: RefCell>, - selected_cb: RefCell)>>>, - navigator: RefCell>>, -} - -impl TrackEditor { - /// Create a new track editor. - pub fn new(recording: Recording, selection: Vec) -> Rc { - // Create UI - - let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); - - get_widget!(builder, gtk::Box, widget); - get_widget!(builder, gtk::Button, back_button); - get_widget!(builder, gtk::Button, select_button); - get_widget!(builder, gtk::Frame, parts_frame); - - let parts_list = gtk::ListBox::new(); - parts_list.set_selection_mode(gtk::SelectionMode::None); - parts_list.set_vexpand(false); - parts_list.show(); - parts_frame.add(&parts_list); - - let this = Rc::new(Self { - widget, - selection: RefCell::new(selection), - 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(); - } - })); - - select_button.connect_clicked(clone!(@strong this => move |_| { - let navigator = this.navigator.borrow().clone(); - if let Some(navigator) = navigator { - navigator.pop(); - } - - if let Some(cb) = &*this.selected_cb.borrow() { - let selection = this.selection.borrow().clone(); - cb(selection); - } - })); - - for (index, part) in recording.work.parts.iter().enumerate() { - let check = gtk::CheckButton::new(); - - check.connect_toggled(clone!(@strong this => move |check| { - let mut selection = this.selection.borrow_mut(); - if check.get_active() { - selection.push(index); - } else { - if let Some(pos) = selection.iter().position(|part| *part == index) { - selection.remove(pos); - } - } - })); - - let row = libhandy::ActionRow::new(); - row.add_prefix(&check); - row.set_activatable_widget(Some(&check)); - row.set_title(Some(&part.title)); - row.show_all(); - - parts_list.add(&row); - } - - this - } - - /// Set the closure to be called when the user has edited the track. - pub fn set_selected_cb) + 'static>(&self, cb: F) { - self.selected_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); - } -} - -/// A simple list of widgets. -struct List { - pub widget: gtk::ListBox, - make_widget: RefCell gtk::Widget>>>, -} - -impl List { - /// Create a new list. The list will be empty. - pub fn new(placeholder_text: &str) -> Self { - let placeholder_label = gtk::Label::new(Some(placeholder_text)); - placeholder_label.set_margin_top(6); - placeholder_label.set_margin_bottom(6); - placeholder_label.set_margin_start(6); - placeholder_label.set_margin_end(6); - placeholder_label.show(); - - let widget = gtk::ListBox::new(); - widget.set_selection_mode(gtk::SelectionMode::None); - widget.set_placeholder(Some(&placeholder_label)); - widget.show(); - - Self { - widget, - make_widget: RefCell::new(None), - } - } - - /// Set the closure to be called to construct widgets for the items. - pub fn set_make_widget gtk::Widget + 'static>(&self, make_widget: F) { - self.make_widget.replace(Some(Box::new(make_widget))); - } - - /// Call the make_widget function for each item. This will automatically - /// show all children by indices 0..length. - pub fn update(&self, length: usize) { - for child in self.widget.get_children() { - self.widget.remove(&child); - } - - if let Some(make_widget) = &*self.make_widget.borrow() { - for index in 0..length { - let row = make_widget(index); - self.widget.insert(&row, -1); - } - } - } -} diff --git a/musicus/src/editors/track_source.rs b/musicus/src/editors/track_source.rs deleted file mode 100644 index 3f09d05..0000000 --- a/musicus/src/editors/track_source.rs +++ /dev/null @@ -1,42 +0,0 @@ -use anyhow::Result; -use std::cell::Cell; -use std::path::Path; - -/// One track within a [`TrackSource`]. -#[derive(Debug, Clone)] -pub struct TrackState { - pub description: String, -} - -/// A live representation of a source of audio tracks. -pub struct TrackSource { - pub tracks: Vec, - pub ready: Cell, -} - -impl TrackSource { - /// Create a new track source for a folder. This will provide the folder's - /// files as selectable tracks and be ready immediately. - pub fn folder(path: &Path) -> Result { - let mut tracks = Vec::::new(); - - let entries = std::fs::read_dir(path)?; - for entry in entries { - let entry = entry?; - if entry.file_type()?.is_file() { - let file_name = entry.file_name(); - let track = TrackState { description: file_name.to_str().unwrap().to_owned() }; - tracks.push(track); - } - } - - tracks.sort_unstable_by(|a, b| { - a.description.cmp(&b.description) - }); - - Ok(Self { - tracks, - ready: Cell::new(true), - }) - } -} diff --git a/musicus/src/import/medium_editor.rs b/musicus/src/import/medium_editor.rs index 85b8a8d..742067c 100644 --- a/musicus/src/import/medium_editor.rs +++ b/musicus/src/import/medium_editor.rs @@ -1,6 +1,6 @@ use super::disc_source::DiscSource; +use super::track_set_editor::TrackSetEditor; use crate::backend::Backend; -// use crate::editors::{TrackSetEditor, TrackSource}; use crate::widgets::{Navigator, NavigatorScreen}; use crate::widgets::new_list::List; use glib::clone; @@ -12,7 +12,7 @@ use std::rc::Rc; /// A dialog for editing metadata while importing music into the music library. pub struct MediumEditor { backend: Rc, - source: DiscSource, + source: Rc, widget: gtk::Box, navigator: RefCell>>, } @@ -34,7 +34,7 @@ impl MediumEditor { let this = Rc::new(Self { backend, - source, + source: Rc::new(source), widget, navigator: RefCell::new(None), }); @@ -51,8 +51,8 @@ impl MediumEditor { add_button.connect_clicked(clone!(@strong this => move |_| { let navigator = this.navigator.borrow().clone(); if let Some(navigator) = navigator { - // let editor = TrackSetEditor::new(this.backend.clone(), this.source.clone()); - // navigator.push(editor); + let editor = TrackSetEditor::new(this.backend.clone(), Rc::clone(&this.source)); + navigator.push(editor); } })); diff --git a/musicus/src/import/mod.rs b/musicus/src/import/mod.rs index 248a7b7..2744611 100644 --- a/musicus/src/import/mod.rs +++ b/musicus/src/import/mod.rs @@ -1,5 +1,8 @@ mod disc_source; mod medium_editor; mod source_selector; +mod track_editor; +mod track_selector; +mod track_set_editor; pub use source_selector::SourceSelector; diff --git a/musicus/src/import/track_editor.rs b/musicus/src/import/track_editor.rs new file mode 100644 index 0000000..ff02c04 --- /dev/null +++ b/musicus/src/import/track_editor.rs @@ -0,0 +1,110 @@ +use crate::database::Recording; +use crate::widgets::{Navigator, NavigatorScreen}; +use crate::widgets::new_list::List; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for editing a single track. +pub struct TrackEditor { + widget: gtk::Box, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackEditor { + /// Create a new track editor. + pub fn new(recording: Recording, selection: Vec) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, parts_frame); + + let parts_list = gtk::ListBox::new(); + parts_list.set_selection_mode(gtk::SelectionMode::None); + parts_list.set_vexpand(false); + parts_list.show(); + parts_frame.add(&parts_list); + + let this = Rc::new(Self { + widget, + selection: RefCell::new(selection), + 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(); + } + })); + + select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, part) in recording.work.parts.iter().enumerate() { + let check = gtk::CheckButton::new(); + check.set_active(this.selection.borrow().contains(&index)); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + })); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&part.title)); + row.show_all(); + + parts_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has edited the track. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_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/import/track_selector.rs b/musicus/src/import/track_selector.rs new file mode 100644 index 0000000..3404593 --- /dev/null +++ b/musicus/src/import/track_selector.rs @@ -0,0 +1,122 @@ +use super::disc_source::DiscSource; +use crate::widgets::{Navigator, NavigatorScreen}; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A screen for selecting tracks from a medium. +pub struct TrackSelector { + source: Rc, + widget: gtk::Box, + select_button: gtk::Button, + selection: RefCell>, + selected_cb: RefCell)>>>, + navigator: RefCell>>, +} + +impl TrackSelector { + /// Create a new track selector. + pub fn new(source: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_selector.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, select_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = gtk::ListBox::new(); + track_list.set_selection_mode(gtk::SelectionMode::None); + track_list.set_vexpand(false); + track_list.show(); + tracks_frame.add(&track_list); + + let this = Rc::new(Self { + source, + widget, + select_button, + selection: RefCell::new(Vec::new()), + 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.select_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + navigator.pop(); + } + + if let Some(cb) = &*this.selected_cb.borrow() { + let selection = this.selection.borrow().clone(); + cb(selection); + } + })); + + for (index, track) in this.source.tracks.iter().enumerate() { + let check = gtk::CheckButton::new(); + + check.connect_toggled(clone!(@strong this => move |check| { + let mut selection = this.selection.borrow_mut(); + if check.get_active() { + selection.push(index); + } else { + if let Some(pos) = selection.iter().position(|part| *part == index) { + selection.remove(pos); + } + } + + if selection.is_empty() { + this.select_button.set_sensitive(false); + } else { + this.select_button.set_sensitive(true); + } + })); + + let title = format!("Track {}", track.number); + + let row = libhandy::ActionRow::new(); + row.add_prefix(&check); + row.set_activatable_widget(Some(&check)); + row.set_title(Some(&title)); + row.show_all(); + + track_list.add(&row); + } + + this + } + + /// Set the closure to be called when the user has selected tracks. The + /// closure will be called with the indices of the selected tracks as its + /// argument. + pub fn set_selected_cb) + 'static>(&self, cb: F) { + self.selected_cb.replace(Some(Box::new(cb))); + } +} + +impl NavigatorScreen for TrackSelector { + 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/import/track_set_editor.rs b/musicus/src/import/track_set_editor.rs new file mode 100644 index 0000000..a418123 --- /dev/null +++ b/musicus/src/import/track_set_editor.rs @@ -0,0 +1,275 @@ +use super::disc_source::DiscSource; +use super::track_editor::TrackEditor; +use super::track_selector::TrackSelector; +use crate::backend::Backend; +use crate::database::{Recording, Track, TrackSet}; +use crate::selectors::{PersonSelector, RecordingSelector, WorkSelector}; +use crate::widgets::{Navigator, NavigatorScreen}; +use crate::widgets::new_list::List; +use gettextrs::gettext; +use glib::clone; +use gtk::prelude::*; +use gtk_macros::get_widget; +use libhandy::prelude::*; +use std::cell::{Cell, RefCell}; +use std::collections::HashSet; +use std::rc::Rc; + +/// A track set before being imported. +#[derive(Clone, Debug)] +pub struct TrackSetData { + pub recording: Recording, + pub tracks: Vec, +} + +/// A track before being imported. +#[derive(Clone, Debug)] +pub struct TrackData { + /// Index of the track source within the medium source's tracks. + pub track_source: usize, + + /// Actual track data. + pub work_parts: Vec, +} + +/// A screen for editing a set of tracks for one recording. +pub struct TrackSetEditor { + backend: Rc, + source: Rc, + widget: gtk::Box, + save_button: gtk::Button, + recording_row: libhandy::ActionRow, + track_list: List, + recording: RefCell>, + tracks: RefCell>, + done_cb: RefCell>>, + navigator: RefCell>>, +} + +impl TrackSetEditor { + /// Create a new track set editor. + pub fn new(backend: Rc, source: Rc) -> Rc { + // Create UI + + let builder = gtk::Builder::from_resource("/de/johrpan/musicus/ui/track_set_editor.ui"); + + get_widget!(builder, gtk::Box, widget); + get_widget!(builder, gtk::Button, back_button); + get_widget!(builder, gtk::Button, save_button); + get_widget!(builder, libhandy::ActionRow, recording_row); + get_widget!(builder, gtk::Button, select_recording_button); + get_widget!(builder, gtk::Button, edit_tracks_button); + get_widget!(builder, gtk::Frame, tracks_frame); + + let track_list = List::new(&gettext!("No tracks added")); + tracks_frame.add(&track_list.widget); + + let this = Rc::new(Self { + backend, + source, + widget, + save_button, + recording_row, + track_list, + recording: RefCell::new(None), + tracks: RefCell::new(Vec::new()), + done_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.save_button.connect_clicked(clone!(@strong this => move |_| { + if let Some(cb) = &*this.done_cb.borrow() {} + })); + + select_recording_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let person_selector = PersonSelector::new(this.backend.clone()); + + 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.replace(Some(recording.clone())); + this.recording_selected(); + + navigator.clone().pop(); + navigator.clone().pop(); + navigator.clone().pop(); + })); + + navigator.clone().push(recording_selector); + })); + + navigator.clone().push(work_selector); + })); + + navigator.clone().push(person_selector); + } + })); + + edit_tracks_button.connect_clicked(clone!(@strong this => move |_| { + let navigator = this.navigator.borrow().clone(); + if let Some(navigator) = navigator { + let selector = TrackSelector::new(Rc::clone(&this.source)); + + selector.set_selected_cb(clone!(@strong this => move |selection| { + let mut tracks = Vec::new(); + + for index in selection { + let track = Track { + work_parts: Vec::new(), + }; + + let data = TrackData { + track_source: index, + work_parts: Vec::new(), + }; + + tracks.push(data); + } + + let length = tracks.len(); + this.tracks.replace(tracks); + this.track_list.update(length); + this.autofill_parts(); + })); + + navigator.push(selector); + } + })); + + this.track_list.set_make_widget(clone!(@strong this => move |index| { + let track = &this.tracks.borrow()[index]; + + let mut title_parts = Vec::::new(); + + if let Some(recording) = &*this.recording.borrow() { + for part in &track.work_parts { + title_parts.push(recording.work.parts[*part].title.clone()); + } + } + + let title = if title_parts.is_empty() { + gettext("Unknown") + } else { + title_parts.join(", ") + }; + + let number = this.source.tracks[track.track_source].number; + let subtitle = format!("Track {}", number); + + let edit_image = gtk::Image::from_icon_name(Some("document-edit-symbolic"), gtk::IconSize::Button); + let edit_button = gtk::Button::new(); + edit_button.set_relief(gtk::ReliefStyle::None); + edit_button.set_valign(gtk::Align::Center); + edit_button.add(&edit_image); + + let row = libhandy::ActionRow::new(); + row.set_activatable(true); + row.set_title(Some(&title)); + row.set_subtitle(Some(&subtitle)); + row.add(&edit_button); + row.set_activatable_widget(Some(&edit_button)); + row.show_all(); + + edit_button.connect_clicked(clone!(@strong this => move |_| { + let recording = this.recording.borrow().clone(); + let navigator = this.navigator.borrow().clone(); + + if let (Some(recording), Some(navigator)) = (recording, navigator) { + let track = &this.tracks.borrow()[index]; + + let editor = TrackEditor::new(recording, track.work_parts.clone()); + + editor.set_selected_cb(clone!(@strong this => move |selection| { + { + let mut tracks = this.tracks.borrow_mut(); + let mut track = &mut tracks[index]; + track.work_parts = selection; + }; + + this.update_tracks(); + })); + + navigator.push(editor); + } + })); + + row.upcast() + })); + + this + } + + /// Set the closure to be called when the user has created the track set. + pub fn set_done_cb(&self, cb: F) { + self.done_cb.replace(Some(Box::new(cb))); + } + + /// Set everything up after selecting a recording. + fn recording_selected(&self) { + if let Some(recording) = &*self.recording.borrow() { + self.recording_row.set_title(Some(&recording.work.get_title())); + self.recording_row.set_subtitle(Some(&recording.get_performers())); + self.save_button.set_sensitive(true); + } + + self.autofill_parts(); + } + + /// Automatically try to put work part information from the selected recording into the + /// selected tracks. + fn autofill_parts(&self) { + if let Some(recording) = &*self.recording.borrow() { + let mut tracks = self.tracks.borrow_mut(); + + for (index, _) in recording.work.parts.iter().enumerate() { + if let Some(mut track) = tracks.get_mut(index) { + track.work_parts = vec![index]; + } else { + break; + } + } + } + + self.update_tracks(); + } + + /// Update the track list. + fn update_tracks(&self) { + let length = self.tracks.borrow().len(); + self.track_list.update(length); + } +} + +impl NavigatorScreen for TrackSetEditor { + 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/meson.build b/musicus/src/meson.build index ec71ba2..ae59ef2 100644 --- a/musicus/src/meson.build +++ b/musicus/src/meson.build @@ -63,7 +63,6 @@ sources = files( 'editors/performance.rs', 'editors/person.rs', 'editors/recording.rs', - 'editors/track_set.rs', 'editors/work.rs', 'editors/work_part.rs', 'editors/work_section.rs',