From e47b7c2006bc1df530b089231136ab98391692c8 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Sat, 1 Mar 2025 15:52:59 +0100 Subject: [PATCH] editor: Implement drag and drop where it makes sense --- data/res/style.css | 14 +++ data/ui/editor/album/recording_row.blp | 23 ++++ data/ui/editor/recording/ensemble_row.blp | 9 ++ data/ui/editor/recording/performer_row.blp | 9 ++ data/ui/editor/tracks/track_row.blp | 14 ++- data/ui/editor/work/composer_row.blp | 13 +- data/ui/editor/work/instrument_row.blp | 23 ++++ data/ui/editor/work/part_row.blp | 11 +- src/editor/album.rs | 57 +++++---- src/editor/album/recording_row.rs | 140 +++++++++++++++++++++ src/editor/recording.rs | 28 +++++ src/editor/recording/ensemble_row.rs | 64 +++++++++- src/editor/recording/performer_row.rs | 64 +++++++++- src/editor/tracks.rs | 14 +++ src/editor/tracks/parts_popover.rs | 2 +- src/editor/tracks/track_row.rs | 72 ++++++++++- src/editor/work.rs | 85 +++++++++---- src/editor/work/composer_row.rs | 64 +++++++++- src/editor/work/instrument_row.rs | 139 ++++++++++++++++++++ src/editor/work/part_row.rs | 71 ++++++++++- src/main.rs | 1 - src/selector/ensemble.rs | 2 +- src/selector/instrument.rs | 2 +- src/selector/performer_role.rs | 2 +- src/selector/person.rs | 2 +- src/selector/recording.rs | 2 +- src/selector/role.rs | 2 +- src/selector/work.rs | 2 +- src/util.rs | 3 + src/{ => util}/activatable_row.rs | 0 src/util/drag_widget.rs | 41 ++++++ 31 files changed, 888 insertions(+), 87 deletions(-) create mode 100644 data/ui/editor/album/recording_row.blp create mode 100644 data/ui/editor/work/instrument_row.blp create mode 100644 src/editor/album/recording_row.rs create mode 100644 src/editor/work/instrument_row.rs rename src/{ => util}/activatable_row.rs (100%) create mode 100644 src/util/drag_widget.rs diff --git a/data/res/style.css b/data/res/style.css index 0a701b6..57514fa 100644 --- a/data/res/style.css +++ b/data/res/style.css @@ -118,4 +118,18 @@ .selector-list>row { padding: 6px; border-radius: 6px; +} + +.drag-handle { + color: color-mix(in srgb, var(--window-fg-color) 40%, transparent); +} + +.drag-handle:backdrop { + color: color-mix(in srgb, var(--window-fg-color) 40%, transparent); +} + +dragwidget { + background-color: var(--card-bg-color); + color: var(--card-fg-color); + border: 1px solid rgba(0, 0, 6, 0.07); } \ No newline at end of file diff --git a/data/ui/editor/album/recording_row.blp b/data/ui/editor/album/recording_row.blp new file mode 100644 index 0000000..da82d70 --- /dev/null +++ b/data/ui/editor/album/recording_row.blp @@ -0,0 +1,23 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusAlbumEditorRecordingRow: Adw.ActionRow { + [prefix] + Gtk.Image { + icon-name: "list-drag-handle-symbolic"; + + styles [ + "drag-handle", + ] + } + + Gtk.Button { + icon-name: "user-trash-symbolic"; + valign: center; + clicked => $remove() swapped; + + styles [ + "flat", + ] + } +} diff --git a/data/ui/editor/recording/ensemble_row.blp b/data/ui/editor/recording/ensemble_row.blp index ff9e49d..07019f9 100644 --- a/data/ui/editor/recording/ensemble_row.blp +++ b/data/ui/editor/recording/ensemble_row.blp @@ -2,6 +2,15 @@ using Gtk 4.0; using Adw 1; template $MusicusRecordingEditorEnsembleRow: Adw.ActionRow { + [prefix] + Gtk.Image { + icon-name: "list-drag-handle-symbolic"; + + styles [ + "drag-handle", + ] + } + Gtk.Button { icon-name: "user-trash-symbolic"; valign: center; diff --git a/data/ui/editor/recording/performer_row.blp b/data/ui/editor/recording/performer_row.blp index 559d16b..4789aec 100644 --- a/data/ui/editor/recording/performer_row.blp +++ b/data/ui/editor/recording/performer_row.blp @@ -2,6 +2,15 @@ using Gtk 4.0; using Adw 1; template $MusicusRecordingEditorPerformerRow: Adw.ActionRow { + [prefix] + Gtk.Image { + icon-name: "list-drag-handle-symbolic"; + + styles [ + "drag-handle", + ] + } + Gtk.Button { icon-name: "user-trash-symbolic"; valign: center; diff --git a/data/ui/editor/tracks/track_row.blp b/data/ui/editor/tracks/track_row.blp index 6426eca..6529b95 100644 --- a/data/ui/editor/tracks/track_row.blp +++ b/data/ui/editor/tracks/track_row.blp @@ -9,10 +9,18 @@ template $MusicusTracksEditorTrackRow: Adw.ActionRow { [prefix] Gtk.Box select_parts_box { Gtk.Image { - icon-name: "document-edit-symbolic"; + icon-name: "list-drag-handle-symbolic"; + + styles [ + "drag-handle", + ] } } + Gtk.Image edit_image { + icon-name: "document-edit-symbolic"; + } + Gtk.Button reset_button { icon-name: "edit-clear-symbolic"; tooltip-text: _("Clear selected work parts"); @@ -21,7 +29,7 @@ template $MusicusTracksEditorTrackRow: Adw.ActionRow { clicked => $reset() swapped; styles [ - "flat" + "flat", ] } @@ -32,7 +40,7 @@ template $MusicusTracksEditorTrackRow: Adw.ActionRow { clicked => $remove() swapped; styles [ - "flat" + "flat", ] } } diff --git a/data/ui/editor/work/composer_row.blp b/data/ui/editor/work/composer_row.blp index c7b414f..33bf407 100644 --- a/data/ui/editor/work/composer_row.blp +++ b/data/ui/editor/work/composer_row.blp @@ -2,13 +2,22 @@ using Gtk 4.0; using Adw 1; template $MusicusWorkEditorComposerRow: Adw.ActionRow { + [prefix] + Gtk.Image { + icon-name: "list-drag-handle-symbolic"; + + styles [ + "drag-handle", + ] + } + Gtk.Button { icon-name: "user-trash-symbolic"; valign: center; clicked => $remove() swapped; styles [ - "flat" + "flat", ] } @@ -17,7 +26,7 @@ template $MusicusWorkEditorComposerRow: Adw.ActionRow { clicked => $open_role_popover() swapped; styles [ - "flat" + "flat", ] Gtk.Box role_box { diff --git a/data/ui/editor/work/instrument_row.blp b/data/ui/editor/work/instrument_row.blp new file mode 100644 index 0000000..1a4266e --- /dev/null +++ b/data/ui/editor/work/instrument_row.blp @@ -0,0 +1,23 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusWorkEditorInstrumentRow: Adw.ActionRow { + [prefix] + Gtk.Image { + icon-name: "list-drag-handle-symbolic"; + + styles [ + "drag-handle", + ] + } + + Gtk.Button { + icon-name: "user-trash-symbolic"; + valign: center; + clicked => $remove() swapped; + + styles [ + "flat", + ] + } +} diff --git a/data/ui/editor/work/part_row.blp b/data/ui/editor/work/part_row.blp index dbbbb8e..0a2af18 100644 --- a/data/ui/editor/work/part_row.blp +++ b/data/ui/editor/work/part_row.blp @@ -5,6 +5,15 @@ template $MusicusWorkEditorPartRow: Adw.ActionRow { activatable: true; activated => $edit() swapped; + [prefix] + Gtk.Image { + icon-name: "list-drag-handle-symbolic"; + + styles [ + "drag-handle", + ] + } + Gtk.Image { icon-name: "document-edit-symbolic"; } @@ -15,7 +24,7 @@ template $MusicusWorkEditorPartRow: Adw.ActionRow { clicked => $remove() swapped; styles [ - "flat" + "flat", ] } } diff --git a/src/editor/album.rs b/src/editor/album.rs index 198ee46..1a5cff1 100644 --- a/src/editor/album.rs +++ b/src/editor/album.rs @@ -1,9 +1,12 @@ +mod recording_row; + use std::cell::{OnceCell, RefCell}; use adw::{prelude::*, subclass::prelude::*}; use gettextrs::gettext; use gtk::glib::{self, clone, subclass::Signal, Properties}; use once_cell::sync::Lazy; +use recording_row::RecordingRow; use crate::{ db::models::{Album, Recording}, @@ -25,7 +28,7 @@ mod imp { pub library: OnceCell, pub album_id: OnceCell, - pub recordings: RefCell>, + pub recording_rows: RefCell>, pub recordings_popover: OnceCell, @@ -143,40 +146,36 @@ impl AlbumEditor { } fn add_recording(&self, recording: Recording) { - let row = adw::ActionRow::builder() - .title(recording.work.to_string()) - .subtitle(recording.performers_string()) - .build(); + let row = RecordingRow::new(recording); - let remove_button = gtk::Button::builder() - .icon_name("user-trash-symbolic") - .valign(gtk::Align::Center) - .css_classes(["flat"]) - .build(); - - remove_button.connect_clicked(clone!( + row.connect_move(clone!( #[weak(rename_to = this)] self, - #[weak] - row, - #[strong] - recording, - move |_| { - this.imp().recordings_list.remove(&row); - this.imp() - .recordings - .borrow_mut() - .retain(|r| *r != recording); + move |target, source| { + let mut recording_rows = this.imp().recording_rows.borrow_mut(); + if let Some(index) = recording_rows.iter().position(|p| p == target) { + this.imp().recordings_list.remove(&source); + recording_rows.retain(|p| p != &source); + this.imp().recordings_list.insert(&source, index as i32); + recording_rows.insert(index, source); + } } )); - row.add_suffix(&remove_button); + row.connect_remove(clone!( + #[weak(rename_to = this)] + self, + move |row| { + this.imp().recordings_list.remove(row); + this.imp().recording_rows.borrow_mut().retain(|p| p != row); + } + )); self.imp() .recordings_list - .insert(&row, self.imp().recordings.borrow().len() as i32); + .insert(&row, self.imp().recording_rows.borrow().len() as i32); - self.imp().recordings.borrow_mut().push(recording); + self.imp().recording_rows.borrow_mut().push(row); } #[template_callback] @@ -184,7 +183,13 @@ impl AlbumEditor { let library = self.imp().library.get().unwrap(); let name = self.imp().name_editor.translation(); - let recordings = self.imp().recordings.borrow().clone(); + let recordings = self + .imp() + .recording_rows + .borrow() + .iter() + .map(|r| r.recording()) + .collect::>(); if let Some(album_id) = self.imp().album_id.get() { library.update_album(album_id, name, recordings).unwrap(); diff --git a/src/editor/album/recording_row.rs b/src/editor/album/recording_row.rs new file mode 100644 index 0000000..a60da23 --- /dev/null +++ b/src/editor/album/recording_row.rs @@ -0,0 +1,140 @@ +use std::cell::OnceCell; + +use adw::{prelude::*, subclass::prelude::*}; +use gtk::{ + gdk, + glib::{self, clone, subclass::Signal}, +}; +use once_cell::sync::Lazy; + +use crate::{db::models::Recording, util::drag_widget::DragWidget}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/editor/album/recording_row.blp")] + pub struct RecordingRow { + pub recording: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for RecordingRow { + const NAME: &'static str = "MusicusAlbumEditorRecordingRow"; + type Type = super::RecordingRow; + type ParentType = adw::ActionRow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for RecordingRow { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("remove").build(), + Signal::builder("move") + .param_types([super::RecordingRow::static_type()]) + .build(), + ] + }); + + SIGNALS.as_ref() + } + + fn constructed(&self) { + self.parent_constructed(); + + let drag_source = gtk::DragSource::builder() + .actions(gdk::DragAction::MOVE) + .content(&gdk::ContentProvider::for_value(&self.obj().to_value())) + .build(); + + drag_source.connect_drag_begin(clone!( + #[weak(rename_to = obj)] + self.obj(), + move |_, drag| { + let icon = gtk::DragIcon::for_drag(drag); + icon.set_child(Some(&DragWidget::new(&obj))); + } + )); + + self.obj().add_controller(drag_source); + + let drop_target = gtk::DropTarget::builder() + .actions(gdk::DragAction::MOVE) + .build(); + drop_target.set_types(&[Self::Type::static_type()]); + + drop_target.connect_drop(clone!( + #[weak(rename_to = obj)] + self.obj(), + #[upgrade_or] + false, + move |_, value, _, _| { + if let Ok(row) = value.get::() { + obj.emit_by_name::<()>("move", &[&row]); + true + } else { + false + } + } + )); + + self.obj().add_controller(drop_target); + } + } + + impl WidgetImpl for RecordingRow {} + impl ListBoxRowImpl for RecordingRow {} + impl PreferencesRowImpl for RecordingRow {} + impl ActionRowImpl for RecordingRow {} +} + +glib::wrapper! { + pub struct RecordingRow(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow; +} + +#[gtk::template_callbacks] +impl RecordingRow { + pub fn new(recording: Recording) -> Self { + let obj: Self = glib::Object::new(); + obj.set_title(&recording.work.to_string()); + obj.set_subtitle(&recording.performers_string()); + obj.imp().recording.set(recording).unwrap(); + obj + } + + pub fn connect_move(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("move", true, move |values| { + let obj = values[0].get::().unwrap(); + let source = values[1].get::().unwrap(); + f(&obj, source); + None + }) + } + + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("remove", true, move |values| { + let obj = values[0].get::().unwrap(); + f(&obj); + None + }) + } + + pub fn recording(&self) -> Recording { + self.imp().recording.get().unwrap().clone() + } + + #[template_callback] + fn remove(&self) { + self.emit_by_name::<()>("remove", &[]); + } +} diff --git a/src/editor/recording.rs b/src/editor/recording.rs index c128bd9..be7cb8f 100644 --- a/src/editor/recording.rs +++ b/src/editor/recording.rs @@ -266,6 +266,20 @@ impl RecordingEditor { fn add_performer_row(&self, performer: Performer) { let row = RecordingEditorPerformerRow::new(&self.navigation(), &self.library(), performer); + row.connect_move(clone!( + #[weak(rename_to = this)] + self, + move |target, source| { + let mut performer_rows = this.imp().performer_rows.borrow_mut(); + if let Some(index) = performer_rows.iter().position(|p| p == target) { + this.imp().performer_list.remove(&source); + performer_rows.retain(|p| p != &source); + this.imp().performer_list.insert(&source, index as i32); + performer_rows.insert(index, source); + } + } + )); + row.connect_remove(clone!( #[weak(rename_to = this)] self, @@ -298,6 +312,20 @@ impl RecordingEditor { ensemble_performer, ); + row.connect_move(clone!( + #[weak(rename_to = this)] + self, + move |target, source| { + let mut ensemble_rows = this.imp().ensemble_rows.borrow_mut(); + if let Some(index) = ensemble_rows.iter().position(|p| p == target) { + this.imp().ensemble_list.remove(&source); + ensemble_rows.retain(|p| p != &source); + this.imp().ensemble_list.insert(&source, index as i32); + ensemble_rows.insert(index, source); + } + } + )); + row.connect_remove(clone!( #[weak(rename_to = this)] self, diff --git a/src/editor/recording/ensemble_row.rs b/src/editor/recording/ensemble_row.rs index 22f3b86..bac5744 100644 --- a/src/editor/recording/ensemble_row.rs +++ b/src/editor/recording/ensemble_row.rs @@ -1,12 +1,15 @@ use std::cell::{OnceCell, RefCell}; use adw::{prelude::*, subclass::prelude::*}; -use gtk::glib::{self, clone, subclass::Signal, Properties}; +use gtk::{ + gdk, + glib::{self, clone, subclass::Signal, Properties}, +}; use once_cell::sync::Lazy; use crate::{ db::models::EnsemblePerformer, editor::role::RoleEditor, library::Library, - selector::role::RoleSelectorPopover, + selector::role::RoleSelectorPopover, util::drag_widget::DragWidget, }; mod imp { @@ -50,8 +53,14 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for RecordingEditorEnsembleRow { fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = - Lazy::new(|| vec![Signal::builder("remove").build()]); + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("remove").build(), + Signal::builder("move") + .param_types([super::RecordingEditorEnsembleRow::static_type()]) + .build(), + ] + }); SIGNALS.as_ref() } @@ -59,6 +68,44 @@ mod imp { fn constructed(&self) { self.parent_constructed(); + let drag_source = gtk::DragSource::builder() + .actions(gdk::DragAction::MOVE) + .content(&gdk::ContentProvider::for_value(&self.obj().to_value())) + .build(); + + drag_source.connect_drag_begin(clone!( + #[weak(rename_to = obj)] + self.obj(), + move |_, drag| { + let icon = gtk::DragIcon::for_drag(drag); + icon.set_child(Some(&DragWidget::new(&obj))); + } + )); + + self.obj().add_controller(drag_source); + + let drop_target = gtk::DropTarget::builder() + .actions(gdk::DragAction::MOVE) + .build(); + drop_target.set_types(&[Self::Type::static_type()]); + + drop_target.connect_drop(clone!( + #[weak(rename_to = obj)] + self.obj(), + #[upgrade_or] + false, + move |_, value, _, _| { + if let Ok(row) = value.get::() { + obj.emit_by_name::<()>("move", &[&row]); + true + } else { + false + } + } + )); + + self.obj().add_controller(drop_target); + let role_popover = RoleSelectorPopover::new(self.library.get().unwrap()); let obj = self.obj().to_owned(); @@ -118,6 +165,15 @@ impl RecordingEditorEnsembleRow { obj } + pub fn connect_move(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("move", true, move |values| { + let obj = values[0].get::().unwrap(); + let source = values[1].get::().unwrap(); + f(&obj, source); + None + }) + } + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { self.connect_local("remove", true, move |values| { let obj = values[0].get::().unwrap(); diff --git a/src/editor/recording/performer_row.rs b/src/editor/recording/performer_row.rs index 0596df3..1eac180 100644 --- a/src/editor/recording/performer_row.rs +++ b/src/editor/recording/performer_row.rs @@ -1,12 +1,15 @@ use std::cell::{OnceCell, RefCell}; use adw::{prelude::*, subclass::prelude::*}; -use gtk::glib::{self, clone, subclass::Signal, Properties}; +use gtk::{ + gdk, + glib::{self, clone, subclass::Signal, Properties}, +}; use once_cell::sync::Lazy; use crate::{ db::models::Performer, editor::role::RoleEditor, library::Library, - selector::performer_role::PerformerRoleSelectorPopover, + selector::performer_role::PerformerRoleSelectorPopover, util::drag_widget::DragWidget, }; mod imp { @@ -51,8 +54,14 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for RecordingEditorPerformerRow { fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = - Lazy::new(|| vec![Signal::builder("remove").build()]); + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("remove").build(), + Signal::builder("move") + .param_types([super::RecordingEditorPerformerRow::static_type()]) + .build(), + ] + }); SIGNALS.as_ref() } @@ -60,6 +69,44 @@ mod imp { fn constructed(&self) { self.parent_constructed(); + let drag_source = gtk::DragSource::builder() + .actions(gdk::DragAction::MOVE) + .content(&gdk::ContentProvider::for_value(&self.obj().to_value())) + .build(); + + drag_source.connect_drag_begin(clone!( + #[weak(rename_to = obj)] + self.obj(), + move |_, drag| { + let icon = gtk::DragIcon::for_drag(drag); + icon.set_child(Some(&DragWidget::new(&obj))); + } + )); + + self.obj().add_controller(drag_source); + + let drop_target = gtk::DropTarget::builder() + .actions(gdk::DragAction::MOVE) + .build(); + drop_target.set_types(&[Self::Type::static_type()]); + + drop_target.connect_drop(clone!( + #[weak(rename_to = obj)] + self.obj(), + #[upgrade_or] + false, + move |_, value, _, _| { + if let Ok(row) = value.get::() { + obj.emit_by_name::<()>("move", &[&row]); + true + } else { + false + } + } + )); + + self.obj().add_controller(drop_target); + let role_popover = PerformerRoleSelectorPopover::new(self.library.get().unwrap()); let obj = self.obj().to_owned(); @@ -142,6 +189,15 @@ impl RecordingEditorPerformerRow { obj } + pub fn connect_move(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("move", true, move |values| { + let obj = values[0].get::().unwrap(); + let source = values[1].get::().unwrap(); + f(&obj, source); + None + }) + } + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { self.connect_local("remove", true, move |values| { let obj = values[0].get::().unwrap(); diff --git a/src/editor/tracks.rs b/src/editor/tracks.rs index 832f3e2..7390a2c 100644 --- a/src/editor/tracks.rs +++ b/src/editor/tracks.rs @@ -268,6 +268,20 @@ impl TracksEditor { let track_row = TracksEditorTrackRow::new(&self.navigation(), &self.library(), recording, track_data); + track_row.connect_move(clone!( + #[weak(rename_to = this)] + self, + move |target, source| { + let mut track_rows = this.imp().track_rows.borrow_mut(); + if let Some(index) = track_rows.iter().position(|p| p == target) { + this.imp().track_list.remove(&source); + track_rows.retain(|p| p != &source); + this.imp().track_list.insert(&source, index as i32); + track_rows.insert(index, source); + } + } + )); + track_row.connect_remove(clone!( #[weak(rename_to = this)] self, diff --git a/src/editor/tracks/parts_popover.rs b/src/editor/tracks/parts_popover.rs index 0a42b1c..932edeb 100644 --- a/src/editor/tracks/parts_popover.rs +++ b/src/editor/tracks/parts_popover.rs @@ -7,7 +7,7 @@ use gtk::{ }; use once_cell::sync::Lazy; -use crate::{activatable_row::ActivatableRow, db::models::Work}; +use crate::{db::models::Work, util::activatable_row::ActivatableRow}; mod imp { use super::*; diff --git a/src/editor/tracks/track_row.rs b/src/editor/tracks/track_row.rs index 8579d07..5aeb707 100644 --- a/src/editor/tracks/track_row.rs +++ b/src/editor/tracks/track_row.rs @@ -6,13 +6,17 @@ use std::{ use adw::{prelude::*, subclass::prelude::*}; use formatx::formatx; use gettextrs::gettext; -use gtk::glib::{self, clone, subclass::Signal, Properties}; +use gtk::{ + gdk, + glib::{self, clone, subclass::Signal, Properties}, +}; use once_cell::sync::Lazy; use super::parts_popover::TracksEditorPartsPopover; use crate::{ db::models::{Recording, Track, Work}, library::Library, + util::drag_widget::DragWidget, }; mod imp { @@ -35,6 +39,8 @@ mod imp { #[template_child] pub select_parts_box: TemplateChild, #[template_child] + pub edit_image: TemplateChild, + #[template_child] pub reset_button: TemplateChild, } @@ -57,11 +63,59 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for TracksEditorTrackRow { fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = - Lazy::new(|| vec![Signal::builder("remove").build()]); + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("remove").build(), + Signal::builder("move") + .param_types([super::TracksEditorTrackRow::static_type()]) + .build(), + ] + }); SIGNALS.as_ref() } + + fn constructed(&self) { + self.parent_constructed(); + + let drag_source = gtk::DragSource::builder() + .actions(gdk::DragAction::MOVE) + .content(&gdk::ContentProvider::for_value(&self.obj().to_value())) + .build(); + + drag_source.connect_drag_begin(clone!( + #[weak(rename_to = obj)] + self.obj(), + move |_, drag| { + let icon = gtk::DragIcon::for_drag(drag); + icon.set_child(Some(&DragWidget::new(&obj))); + } + )); + + self.obj().add_controller(drag_source); + + let drop_target = gtk::DropTarget::builder() + .actions(gdk::DragAction::MOVE) + .build(); + drop_target.set_types(&[Self::Type::static_type()]); + + drop_target.connect_drop(clone!( + #[weak(rename_to = obj)] + self.obj(), + #[upgrade_or] + false, + move |_, value, _, _| { + if let Ok(row) = value.get::() { + obj.emit_by_name::<()>("move", &[&row]); + true + } else { + false + } + } + )); + + self.obj().add_controller(drop_target); + } } impl WidgetImpl for TracksEditorTrackRow {} @@ -89,6 +143,9 @@ impl TracksEditorTrackRow { .build(); obj.set_activatable(!recording.work.parts.is_empty()); + obj.imp() + .edit_image + .set_visible(!recording.work.parts.is_empty()); obj.set_subtitle(&match &track_data.location { TrackLocation::Undefined => String::new(), @@ -127,6 +184,15 @@ impl TracksEditorTrackRow { obj } + pub fn connect_move(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("move", true, move |values| { + let obj = values[0].get::().unwrap(); + let source = values[1].get::().unwrap(); + f(&obj, source); + None + }) + } + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { self.connect_local("remove", true, move |values| { let obj = values[0].get::().unwrap(); diff --git a/src/editor/work.rs b/src/editor/work.rs index 0b37d3c..b801f12 100644 --- a/src/editor/work.rs +++ b/src/editor/work.rs @@ -1,4 +1,5 @@ mod composer_row; +mod instrument_row; mod part_row; use std::cell::{Cell, OnceCell, RefCell}; @@ -19,6 +20,7 @@ use crate::{ library::Library, selector::{instrument::InstrumentSelectorPopover, person::PersonSelectorPopover}, }; +use instrument_row::InstrumentRow; mod imp { use super::*; @@ -41,8 +43,7 @@ mod imp { // handle all state related to the composer. pub composer_rows: RefCell>, pub part_rows: RefCell>, - - pub instruments: RefCell>, + pub instrument_rows: RefCell>, pub persons_popover: OnceCell, pub instruments_popover: OnceCell, @@ -240,6 +241,20 @@ impl WorkEditor { fn add_part_row(&self, part: Work) { let row = WorkEditorPartRow::new(&self.navigation(), &self.library(), part); + row.connect_move(clone!( + #[weak(rename_to = this)] + self, + move |target, source| { + let mut part_rows = this.imp().part_rows.borrow_mut(); + if let Some(index) = part_rows.iter().position(|p| p == target) { + this.imp().part_list.remove(&source); + part_rows.retain(|p| p != &source); + this.imp().part_list.insert(&source, index as i32); + part_rows.insert(index, source); + } + } + )); + row.connect_remove(clone!( #[weak(rename_to = this)] self, @@ -259,6 +274,20 @@ impl WorkEditor { fn add_composer_row(&self, composer: Composer) { let row = WorkEditorComposerRow::new(&self.navigation(), &self.library(), composer); + row.connect_move(clone!( + #[weak(rename_to = this)] + self, + move |target, source| { + let mut composer_rows = this.imp().composer_rows.borrow_mut(); + if let Some(index) = composer_rows.iter().position(|p| p == target) { + this.imp().composer_list.remove(&source); + composer_rows.retain(|p| p != &source); + this.imp().composer_list.insert(&source, index as i32); + composer_rows.insert(index, source); + } + } + )); + row.connect_remove(clone!( #[weak(rename_to = this)] self, @@ -276,39 +305,36 @@ impl WorkEditor { } fn add_instrument_row(&self, instrument: Instrument) { - let row = adw::ActionRow::builder() - .title(instrument.to_string()) - .build(); + let row = InstrumentRow::new(instrument); - let remove_button = gtk::Button::builder() - .icon_name("user-trash-symbolic") - .valign(gtk::Align::Center) - .css_classes(["flat"]) - .build(); - - remove_button.connect_clicked(clone!( + row.connect_move(clone!( #[weak(rename_to = this)] self, - #[weak] - row, - #[strong] - instrument, - move |_| { - this.imp().instrument_list.remove(&row); - this.imp() - .instruments - .borrow_mut() - .retain(|i| *i != instrument); + move |target, source| { + let mut instrument_rows = this.imp().instrument_rows.borrow_mut(); + if let Some(index) = instrument_rows.iter().position(|p| p == target) { + this.imp().instrument_list.remove(&source); + instrument_rows.retain(|p| p != &source); + this.imp().instrument_list.insert(&source, index as i32); + instrument_rows.insert(index, source); + } } )); - row.add_suffix(&remove_button); + row.connect_remove(clone!( + #[weak(rename_to = this)] + self, + move |row| { + this.imp().instrument_list.remove(row); + this.imp().instrument_rows.borrow_mut().retain(|p| p != row); + } + )); self.imp() .instrument_list - .insert(&row, self.imp().instruments.borrow().len() as i32); + .insert(&row, self.imp().instrument_rows.borrow().len() as i32); - self.imp().instruments.borrow_mut().push(instrument); + self.imp().instrument_rows.borrow_mut().push(row); } #[template_callback] @@ -332,7 +358,14 @@ impl WorkEditor { .iter() .map(|c| c.composer()) .collect::>(); - let instruments = self.imp().instruments.borrow().clone(); + + let instruments = self + .imp() + .instrument_rows + .borrow() + .iter() + .map(|r| r.instrument()) + .collect::>(); if self.imp().is_part_editor.get() { let work_id = self diff --git a/src/editor/work/composer_row.rs b/src/editor/work/composer_row.rs index cd05ca9..e3997e9 100644 --- a/src/editor/work/composer_row.rs +++ b/src/editor/work/composer_row.rs @@ -1,12 +1,15 @@ use std::cell::{OnceCell, RefCell}; use adw::{prelude::*, subclass::prelude::*}; -use gtk::glib::{self, clone, subclass::Signal, Properties}; +use gtk::{ + gdk, + glib::{self, clone, subclass::Signal, Properties}, +}; use once_cell::sync::Lazy; use crate::{ db::models::Composer, editor::role::RoleEditor, library::Library, - selector::role::RoleSelectorPopover, + selector::role::RoleSelectorPopover, util::drag_widget::DragWidget, }; mod imp { @@ -50,8 +53,14 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for WorkEditorComposerRow { fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = - Lazy::new(|| vec![Signal::builder("remove").build()]); + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("remove").build(), + Signal::builder("move") + .param_types([super::WorkEditorComposerRow::static_type()]) + .build(), + ] + }); SIGNALS.as_ref() } @@ -59,6 +68,44 @@ mod imp { fn constructed(&self) { self.parent_constructed(); + let drag_source = gtk::DragSource::builder() + .actions(gdk::DragAction::MOVE) + .content(&gdk::ContentProvider::for_value(&self.obj().to_value())) + .build(); + + drag_source.connect_drag_begin(clone!( + #[weak(rename_to = obj)] + self.obj(), + move |_, drag| { + let icon = gtk::DragIcon::for_drag(drag); + icon.set_child(Some(&DragWidget::new(&obj))); + } + )); + + self.obj().add_controller(drag_source); + + let drop_target = gtk::DropTarget::builder() + .actions(gdk::DragAction::MOVE) + .build(); + drop_target.set_types(&[Self::Type::static_type()]); + + drop_target.connect_drop(clone!( + #[weak(rename_to = obj)] + self.obj(), + #[upgrade_or] + false, + move |_, value, _, _| { + if let Ok(row) = value.get::() { + obj.emit_by_name::<()>("move", &[&row]); + true + } else { + false + } + } + )); + + self.obj().add_controller(drop_target); + let role_popover = RoleSelectorPopover::new(self.library.get().unwrap()); let obj = self.obj().to_owned(); @@ -114,6 +161,15 @@ impl WorkEditorComposerRow { obj } + pub fn connect_move(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("move", true, move |values| { + let obj = values[0].get::().unwrap(); + let source = values[1].get::().unwrap(); + f(&obj, source); + None + }) + } + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { self.connect_local("remove", true, move |values| { let obj = values[0].get::().unwrap(); diff --git a/src/editor/work/instrument_row.rs b/src/editor/work/instrument_row.rs new file mode 100644 index 0000000..c3b8eeb --- /dev/null +++ b/src/editor/work/instrument_row.rs @@ -0,0 +1,139 @@ +use std::cell::OnceCell; + +use adw::{prelude::*, subclass::prelude::*}; +use gtk::{ + gdk, + glib::{self, clone, subclass::Signal}, +}; +use once_cell::sync::Lazy; + +use crate::{db::models::Instrument, util::drag_widget::DragWidget}; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/editor/work/instrument_row.blp")] + pub struct InstrumentRow { + pub instrument: OnceCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for InstrumentRow { + const NAME: &'static str = "MusicusWorkEditorInstrumentRow"; + type Type = super::InstrumentRow; + type ParentType = adw::ActionRow; + + fn class_init(klass: &mut Self::Class) { + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for InstrumentRow { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("remove").build(), + Signal::builder("move") + .param_types([super::InstrumentRow::static_type()]) + .build(), + ] + }); + + SIGNALS.as_ref() + } + + fn constructed(&self) { + self.parent_constructed(); + + let drag_source = gtk::DragSource::builder() + .actions(gdk::DragAction::MOVE) + .content(&gdk::ContentProvider::for_value(&self.obj().to_value())) + .build(); + + drag_source.connect_drag_begin(clone!( + #[weak(rename_to = obj)] + self.obj(), + move |_, drag| { + let icon = gtk::DragIcon::for_drag(drag); + icon.set_child(Some(&DragWidget::new(&obj))); + } + )); + + self.obj().add_controller(drag_source); + + let drop_target = gtk::DropTarget::builder() + .actions(gdk::DragAction::MOVE) + .build(); + drop_target.set_types(&[Self::Type::static_type()]); + + drop_target.connect_drop(clone!( + #[weak(rename_to = obj)] + self.obj(), + #[upgrade_or] + false, + move |_, value, _, _| { + if let Ok(row) = value.get::() { + obj.emit_by_name::<()>("move", &[&row]); + true + } else { + false + } + } + )); + + self.obj().add_controller(drop_target); + } + } + + impl WidgetImpl for InstrumentRow {} + impl ListBoxRowImpl for InstrumentRow {} + impl PreferencesRowImpl for InstrumentRow {} + impl ActionRowImpl for InstrumentRow {} +} + +glib::wrapper! { + pub struct InstrumentRow(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::ActionRow; +} + +#[gtk::template_callbacks] +impl InstrumentRow { + pub fn new(instrument: Instrument) -> Self { + let obj: Self = glib::Object::new(); + obj.set_title(&instrument.to_string()); + obj.imp().instrument.set(instrument).unwrap(); + obj + } + + pub fn connect_move(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("move", true, move |values| { + let obj = values[0].get::().unwrap(); + let source = values[1].get::().unwrap(); + f(&obj, source); + None + }) + } + + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("remove", true, move |values| { + let obj = values[0].get::().unwrap(); + f(&obj); + None + }) + } + + pub fn instrument(&self) -> Instrument { + self.imp().instrument.get().unwrap().clone() + } + + #[template_callback] + fn remove(&self) { + self.emit_by_name::<()>("remove", &[]); + } +} diff --git a/src/editor/work/part_row.rs b/src/editor/work/part_row.rs index 6c51c5d..b7d3e42 100644 --- a/src/editor/work/part_row.rs +++ b/src/editor/work/part_row.rs @@ -1,13 +1,17 @@ use std::cell::{OnceCell, RefCell}; use adw::{prelude::*, subclass::prelude::*}; -use gtk::glib::{self, clone, subclass::Signal, Properties}; +use gtk::{ + gdk, + glib::{self, clone, subclass::Signal, Properties}, +}; use once_cell::sync::Lazy; -use crate::{db::models::Work, editor::work::WorkEditor, library::Library}; +use crate::{ + db::models::Work, editor::work::WorkEditor, library::Library, util::drag_widget::DragWidget, +}; mod imp { - use super::*; #[derive(Properties, Debug, Default, gtk::CompositeTemplate)] @@ -42,11 +46,59 @@ mod imp { #[glib::derived_properties] impl ObjectImpl for WorkEditorPartRow { fn signals() -> &'static [Signal] { - static SIGNALS: Lazy> = - Lazy::new(|| vec![Signal::builder("remove").build()]); + static SIGNALS: Lazy> = Lazy::new(|| { + vec![ + Signal::builder("remove").build(), + Signal::builder("move") + .param_types([super::WorkEditorPartRow::static_type()]) + .build(), + ] + }); SIGNALS.as_ref() } + + fn constructed(&self) { + self.parent_constructed(); + + let drag_source = gtk::DragSource::builder() + .actions(gdk::DragAction::MOVE) + .content(&gdk::ContentProvider::for_value(&self.obj().to_value())) + .build(); + + drag_source.connect_drag_begin(clone!( + #[weak(rename_to = obj)] + self.obj(), + move |_, drag| { + let icon = gtk::DragIcon::for_drag(drag); + icon.set_child(Some(&DragWidget::new(&obj))); + } + )); + + self.obj().add_controller(drag_source); + + let drop_target = gtk::DropTarget::builder() + .actions(gdk::DragAction::MOVE) + .build(); + drop_target.set_types(&[Self::Type::static_type()]); + + drop_target.connect_drop(clone!( + #[weak(rename_to = obj)] + self.obj(), + #[upgrade_or] + false, + move |_, value, _, _| { + if let Ok(row) = value.get::() { + obj.emit_by_name::<()>("move", &[&row]); + true + } else { + false + } + } + )); + + self.obj().add_controller(drop_target); + } } impl WidgetImpl for WorkEditorPartRow {} @@ -71,6 +123,15 @@ impl WorkEditorPartRow { obj } + pub fn connect_move(&self, f: F) -> glib::SignalHandlerId { + self.connect_local("move", true, move |values| { + let obj = values[0].get::().unwrap(); + let source = values[1].get::().unwrap(); + f(&obj, source); + None + }) + } + pub fn connect_remove(&self, f: F) -> glib::SignalHandlerId { self.connect_local("remove", true, move |values| { let obj = values[0].get::().unwrap(); diff --git a/src/main.rs b/src/main.rs index f6ea0d5..fea094e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -mod activatable_row; mod album_tile; mod application; mod config; diff --git a/src/selector/ensemble.rs b/src/selector/ensemble.rs index 63f203b..d0eb57e 100644 --- a/src/selector/ensemble.rs +++ b/src/selector/ensemble.rs @@ -8,7 +8,7 @@ use gtk::{ }; use once_cell::sync::Lazy; -use crate::{activatable_row::ActivatableRow, db::models::Ensemble, library::Library}; +use crate::{db::models::Ensemble, library::Library, util::activatable_row::ActivatableRow}; mod imp { use super::*; diff --git a/src/selector/instrument.rs b/src/selector/instrument.rs index 7117a80..7e5bd7f 100644 --- a/src/selector/instrument.rs +++ b/src/selector/instrument.rs @@ -8,7 +8,7 @@ use gtk::{ }; use once_cell::sync::Lazy; -use crate::{activatable_row::ActivatableRow, db::models::Instrument, library::Library}; +use crate::{db::models::Instrument, library::Library, util::activatable_row::ActivatableRow}; mod imp { use super::*; diff --git a/src/selector/performer_role.rs b/src/selector/performer_role.rs index 02a4488..2f9c735 100644 --- a/src/selector/performer_role.rs +++ b/src/selector/performer_role.rs @@ -10,9 +10,9 @@ use gtk::{ use once_cell::sync::Lazy; use crate::{ - activatable_row::ActivatableRow, db::models::{Instrument, Role}, library::Library, + util::activatable_row::ActivatableRow, }; mod imp { diff --git a/src/selector/person.rs b/src/selector/person.rs index aaf8fa2..4fd3250 100644 --- a/src/selector/person.rs +++ b/src/selector/person.rs @@ -8,7 +8,7 @@ use gtk::{ }; use once_cell::sync::Lazy; -use crate::{activatable_row::ActivatableRow, db::models::Person, library::Library}; +use crate::{db::models::Person, library::Library, util::activatable_row::ActivatableRow}; mod imp { use super::*; diff --git a/src/selector/recording.rs b/src/selector/recording.rs index f7e2adc..eb16c67 100644 --- a/src/selector/recording.rs +++ b/src/selector/recording.rs @@ -10,9 +10,9 @@ use gtk::{ use once_cell::sync::Lazy; use crate::{ - activatable_row::ActivatableRow, db::models::{Person, Recording, Work}, library::Library, + util::activatable_row::ActivatableRow, }; mod imp { diff --git a/src/selector/role.rs b/src/selector/role.rs index e6bce97..fd919bc 100644 --- a/src/selector/role.rs +++ b/src/selector/role.rs @@ -8,7 +8,7 @@ use gtk::{ }; use once_cell::sync::Lazy; -use crate::{activatable_row::ActivatableRow, db::models::Role, library::Library}; +use crate::{db::models::Role, library::Library, util::activatable_row::ActivatableRow}; mod imp { use super::*; diff --git a/src/selector/work.rs b/src/selector/work.rs index a0cd7e4..46e84f1 100644 --- a/src/selector/work.rs +++ b/src/selector/work.rs @@ -10,9 +10,9 @@ use gtk::{ use once_cell::sync::Lazy; use crate::{ - activatable_row::ActivatableRow, db::models::{Person, Work}, library::Library, + util::activatable_row::ActivatableRow, }; mod imp { diff --git a/src/util.rs b/src/util.rs index b33fb3c..bec2b2c 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,6 @@ +pub mod activatable_row; +pub mod drag_widget; + use gtk::glib; use lazy_static::lazy_static; diff --git a/src/activatable_row.rs b/src/util/activatable_row.rs similarity index 100% rename from src/activatable_row.rs rename to src/util/activatable_row.rs diff --git a/src/util/drag_widget.rs b/src/util/drag_widget.rs new file mode 100644 index 0000000..73880af --- /dev/null +++ b/src/util/drag_widget.rs @@ -0,0 +1,41 @@ +use adw::{prelude::*, subclass::prelude::*}; + +mod imp { + use super::*; + + #[derive(Default)] + pub struct DragWidget {} + + #[glib::object_subclass] + impl ObjectSubclass for DragWidget { + const NAME: &'static str = "MusicusDragWidget"; + type Type = super::DragWidget; + type ParentType = adw::Bin; + + fn class_init(klass: &mut Self::Class) { + klass.set_css_name("dragwidget"); + } + } + + impl ObjectImpl for DragWidget {} + impl WidgetImpl for DragWidget {} + impl BinImpl for DragWidget {} +} + +glib::wrapper! { + /// A simple helper widget for displaying a drag icon for a widget. + pub struct DragWidget(ObjectSubclass) + @extends gtk::Widget, adw::Bin; +} + +impl DragWidget { + pub fn new(widget: &W) -> Self + where + W: IsA, + { + let obj: Self = glib::Object::new(); + let picture = gtk::Picture::for_paintable(>k::WidgetPaintable::new(Some(widget))); + obj.set_child(Some(&picture)); + obj + } +}