editor: Implement drag and drop where it makes sense

This commit is contained in:
Elias Projahn 2025-03-01 15:52:59 +01:00
parent a13e406e99
commit e47b7c2006
31 changed files with 888 additions and 87 deletions

View file

@ -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);
}

View file

@ -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",
]
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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",
]
}
}

View file

@ -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 {

View file

@ -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",
]
}
}

View file

@ -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",
]
}
}

View file

@ -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<Library>,
pub album_id: OnceCell<String>,
pub recordings: RefCell<Vec<Recording>>,
pub recording_rows: RefCell<Vec<RecordingRow>>,
pub recordings_popover: OnceCell<RecordingSelectorPopover>,
@ -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::<Vec<Recording>>();
if let Some(album_id) = self.imp().album_id.get() {
library.update_album(album_id, name, recordings).unwrap();

View file

@ -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<Recording>,
}
#[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<Self>) {
obj.init_template();
}
}
impl ObjectImpl for RecordingRow {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = 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::<Self::Type>() {
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<imp::RecordingRow>)
@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<F: Fn(&Self, Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("move", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let source = values[1].get::<Self>().unwrap();
f(&obj, source);
None
})
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().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", &[]);
}
}

View file

@ -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,

View file

@ -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<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
static SIGNALS: Lazy<Vec<Signal>> = 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::<Self::Type>() {
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<F: Fn(&Self, Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("move", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let source = values[1].get::<Self>().unwrap();
f(&obj, source);
None
})
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();

View file

@ -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<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
static SIGNALS: Lazy<Vec<Signal>> = 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::<Self::Type>() {
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<F: Fn(&Self, Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("move", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let source = values[1].get::<Self>().unwrap();
f(&obj, source);
None
})
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();

View file

@ -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,

View file

@ -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::*;

View file

@ -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<gtk::Box>,
#[template_child]
pub edit_image: TemplateChild<gtk::Image>,
#[template_child]
pub reset_button: TemplateChild<gtk::Button>,
}
@ -57,11 +63,59 @@ mod imp {
#[glib::derived_properties]
impl ObjectImpl for TracksEditorTrackRow {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
static SIGNALS: Lazy<Vec<Signal>> = 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::<Self::Type>() {
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<F: Fn(&Self, Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("move", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let source = values[1].get::<Self>().unwrap();
f(&obj, source);
None
})
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();

View file

@ -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<Vec<WorkEditorComposerRow>>,
pub part_rows: RefCell<Vec<WorkEditorPartRow>>,
pub instruments: RefCell<Vec<Instrument>>,
pub instrument_rows: RefCell<Vec<InstrumentRow>>,
pub persons_popover: OnceCell<PersonSelectorPopover>,
pub instruments_popover: OnceCell<InstrumentSelectorPopover>,
@ -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::<Vec<Composer>>();
let instruments = self.imp().instruments.borrow().clone();
let instruments = self
.imp()
.instrument_rows
.borrow()
.iter()
.map(|r| r.instrument())
.collect::<Vec<Instrument>>();
if self.imp().is_part_editor.get() {
let work_id = self

View file

@ -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<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
static SIGNALS: Lazy<Vec<Signal>> = 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::<Self::Type>() {
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<F: Fn(&Self, Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("move", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let source = values[1].get::<Self>().unwrap();
f(&obj, source);
None
})
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();

View file

@ -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<Instrument>,
}
#[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<Self>) {
obj.init_template();
}
}
impl ObjectImpl for InstrumentRow {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = 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::<Self::Type>() {
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<imp::InstrumentRow>)
@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<F: Fn(&Self, Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("move", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let source = values[1].get::<Self>().unwrap();
f(&obj, source);
None
})
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().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", &[]);
}
}

View file

@ -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<Vec<Signal>> =
Lazy::new(|| vec![Signal::builder("remove").build()]);
static SIGNALS: Lazy<Vec<Signal>> = 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::<Self::Type>() {
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<F: Fn(&Self, Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("move", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let source = values[1].get::<Self>().unwrap();
f(&obj, source);
None
})
}
pub fn connect_remove<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("remove", true, move |values| {
let obj = values[0].get::<Self>().unwrap();

View file

@ -1,4 +1,3 @@
mod activatable_row;
mod album_tile;
mod application;
mod config;

View file

@ -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::*;

View file

@ -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::*;

View file

@ -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 {

View file

@ -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::*;

View file

@ -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 {

View file

@ -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::*;

View file

@ -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 {

View file

@ -1,3 +1,6 @@
pub mod activatable_row;
pub mod drag_widget;
use gtk::glib;
use lazy_static::lazy_static;

41
src/util/drag_widget.rs Normal file
View file

@ -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<imp::DragWidget>)
@extends gtk::Widget, adw::Bin;
}
impl DragWidget {
pub fn new<W>(widget: &W) -> Self
where
W: IsA<gtk::Widget>,
{
let obj: Self = glib::Object::new();
let picture = gtk::Picture::for_paintable(&gtk::WidgetPaintable::new(Some(widget)));
obj.set_child(Some(&picture));
obj
}
}