Implement program editor

This commit is contained in:
Elias Projahn 2025-03-22 18:55:29 +01:00
parent fa94d61e1e
commit 8950b04ed2
8 changed files with 590 additions and 81 deletions

View file

@ -2,6 +2,7 @@ pub mod album;
pub mod ensemble;
pub mod instrument;
pub mod person;
pub mod program;
pub mod recording;
pub mod role;
pub mod tracks;

181
src/editor/program.rs Normal file
View file

@ -0,0 +1,181 @@
use std::{cell::OnceCell, str::FromStr};
use adw::{prelude::*, subclass::prelude::*};
use gtk::{
gio,
glib::{self, subclass::Signal},
};
use once_cell::sync::Lazy;
use crate::{
program::{Program, ProgramDesign},
slider_row::SliderRow,
};
mod imp {
use super::*;
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(file = "data/ui/editor/program.blp")]
pub struct ProgramEditor {
pub navigation: OnceCell<adw::NavigationView>,
pub action_group: OnceCell<gio::SimpleActionGroup>,
#[template_child]
pub title_row: TemplateChild<adw::EntryRow>,
#[template_child]
pub description_row: TemplateChild<adw::EntryRow>,
#[template_child]
pub prefer_least_recently_played_adjustment: TemplateChild<gtk::Adjustment>,
#[template_child]
pub prefer_recently_added_adjustment: TemplateChild<gtk::Adjustment>,
#[template_child]
pub avoid_repeated_composers_adjustment: TemplateChild<gtk::Adjustment>,
#[template_child]
pub avoid_repeated_instruments_adjustment: TemplateChild<gtk::Adjustment>,
#[template_child]
pub play_full_recordings_row: TemplateChild<adw::SwitchRow>,
}
#[glib::object_subclass]
impl ObjectSubclass for ProgramEditor {
const NAME: &'static str = "MusicusProgramEditor";
type Type = super::ProgramEditor;
type ParentType = adw::NavigationPage;
fn class_init(klass: &mut Self::Class) {
SliderRow::static_type();
klass.bind_template();
klass.bind_template_instance_callbacks();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ProgramEditor {
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder("save")
.param_types([Program::static_type()])
.build()]
});
SIGNALS.as_ref()
}
fn constructed(&self) {
self.parent_constructed();
let set_design_action = gio::ActionEntry::builder("set-design")
.parameter_type(Some(&glib::VariantTy::STRING))
.state(glib::Variant::from("program-1"))
.build();
let actions = gio::SimpleActionGroup::new();
actions.add_action_entries([set_design_action]);
self.obj().insert_action_group("program", Some(&actions));
self.action_group.set(actions).unwrap();
}
}
impl WidgetImpl for ProgramEditor {}
impl NavigationPageImpl for ProgramEditor {}
}
glib::wrapper! {
pub struct ProgramEditor(ObjectSubclass<imp::ProgramEditor>)
@extends gtk::Widget, adw::NavigationPage;
}
#[gtk::template_callbacks]
impl ProgramEditor {
pub fn new(navigation: &adw::NavigationView, program: Option<&Program>) -> Self {
let obj: Self = glib::Object::new();
if let Some(program) = program {
if let Some(title) = program.title() {
obj.imp().title_row.set_text(&title);
}
if let Some(description) = program.description() {
obj.imp().description_row.set_text(&description);
}
if let Err(err) = obj.activate_action(
"program.set-design",
Some(&glib::Variant::from(&program.design().to_string())),
) {
log::warn!("Failed to initialize program design buttons: {err:?}");
}
obj.imp()
.prefer_least_recently_played_adjustment
.set_value(program.prefer_least_recently_played() * 100.0);
obj.imp()
.prefer_recently_added_adjustment
.set_value(program.prefer_recently_added() * 100.0);
obj.imp()
.avoid_repeated_composers_adjustment
.set_value(program.avoid_repeated_composers() as f64);
obj.imp()
.avoid_repeated_instruments_adjustment
.set_value(program.avoid_repeated_instruments() as f64);
obj.imp()
.play_full_recordings_row
.set_active(program.play_full_recordings());
}
obj.imp().navigation.set(navigation.to_owned()).unwrap();
obj
}
pub fn connect_save<F: Fn(&Self, Program) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("save", true, move |values| {
let obj = values[0].get::<Self>().unwrap();
let program = values[1].get::<Program>().unwrap();
f(&obj, program);
None
})
}
#[template_callback]
fn save(&self) {
let program = Program::new(
&self.imp().title_row.text(),
&self.imp().description_row.text(),
ProgramDesign::from_str(
&self
.imp()
.action_group
.get()
.unwrap()
.action_state("set-design")
.map(|v| v.get::<String>().unwrap_or_default())
.unwrap_or_default(),
)
.unwrap_or_default(),
);
program.set_prefer_least_recently_played(
self.imp().prefer_least_recently_played_adjustment.value() / 100.0,
);
program
.set_prefer_recently_added(self.imp().prefer_recently_added_adjustment.value() / 100.0);
program.set_avoid_repeated_composers(
self.imp().avoid_repeated_composers_adjustment.value() as i32,
);
program.set_avoid_repeated_instruments(
self.imp().avoid_repeated_instruments_adjustment.value() as i32,
);
program.set_play_full_recordings(self.imp().play_full_recordings_row.is_active());
self.emit_by_name::<()>("save", &[&program]);
self.imp().navigation.get().unwrap().pop();
}
}

View file

@ -1,4 +1,7 @@
use std::cell::{Cell, RefCell};
use std::{
cell::{Cell, RefCell},
str::FromStr,
};
use anyhow::Result;
use gtk::{gio, glib, glib::Properties, prelude::*, subclass::prelude::*};
@ -156,10 +159,15 @@ impl Program {
}
}
impl Default for Program {
fn default() -> Self {
glib::Object::new()
}
}
#[derive(glib::Enum, Serialize, Deserialize, Eq, PartialEq, Clone, Copy, Debug)]
#[enum_type(name = "MusicusProgramDesign")]
pub enum ProgramDesign {
Generic,
Program1,
Program2,
Program3,
@ -168,8 +176,43 @@ pub enum ProgramDesign {
Program6,
}
impl Default for ProgramDesign {
fn default() -> Self {
Self::Generic
impl ProgramDesign {
pub fn css_class(&self) -> String {
self.to_string()
}
}
impl Default for ProgramDesign {
fn default() -> Self {
Self::Program1
}
}
impl ToString for ProgramDesign {
fn to_string(&self) -> String {
String::from(match self {
ProgramDesign::Program1 => "program-1",
ProgramDesign::Program2 => "program-2",
ProgramDesign::Program3 => "program-3",
ProgramDesign::Program4 => "program-4",
ProgramDesign::Program5 => "program-5",
ProgramDesign::Program6 => "program-6",
})
}
}
impl FromStr for ProgramDesign {
type Err = ();
fn from_str(s: &str) -> std::result::Result<Self, ()> {
match s {
"program-1" => Ok(ProgramDesign::Program1),
"program-2" => Ok(ProgramDesign::Program2),
"program-3" => Ok(ProgramDesign::Program3),
"program-4" => Ok(ProgramDesign::Program4),
"program-5" => Ok(ProgramDesign::Program5),
"program-6" => Ok(ProgramDesign::Program6),
_ => Err(()),
}
}
}

View file

@ -1,12 +1,13 @@
use std::cell::OnceCell;
use std::cell::{OnceCell, RefCell};
use gtk::{
glib::{self, Properties},
gio,
glib::{self, clone, Properties},
prelude::*,
subclass::prelude::*,
};
use crate::program::{Program, ProgramDesign};
use crate::{config, editor::program::ProgramEditor, program::Program};
mod imp {
use super::*;
@ -16,7 +17,11 @@ mod imp {
#[template(file = "data/ui/program_tile.blp")]
pub struct ProgramTile {
#[property(get, construct_only)]
pub program: OnceCell<Program>,
pub navigation: OnceCell<adw::NavigationView>,
#[property(get, construct_only)]
pub key: OnceCell<String>,
#[property(get, set = Self::set_program)]
pub program: RefCell<Program>,
#[template_child]
pub edit_button: TemplateChild<gtk::Button>,
@ -34,6 +39,7 @@ mod imp {
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_instance_callbacks();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
@ -42,10 +48,46 @@ mod imp {
}
#[glib::derived_properties]
impl ObjectImpl for ProgramTile {}
impl ObjectImpl for ProgramTile {
fn constructed(&self) {
self.parent_constructed();
let settings = gio::Settings::new(config::APP_ID);
self.set_program_from_settings(&settings);
let obj = self.obj().to_owned();
settings.connect_changed(Some(&self.key.get().unwrap()), move |settings, _| {
obj.imp().set_program_from_settings(settings);
});
}
}
impl WidgetImpl for ProgramTile {}
impl FlowBoxChildImpl for ProgramTile {}
impl ProgramTile {
fn set_program_from_settings(&self, settings: &gio::Settings) {
match Program::deserialize(&settings.string(self.key.get().unwrap())) {
Ok(program) => self.set_program(&program),
Err(err) => log::error!("Failed to deserialize program from settings: {err:?}"),
}
}
fn set_program(&self, program: &Program) {
self.obj()
.set_css_classes(&["program-tile", &program.design().css_class()]);
if let Some(title) = program.title() {
self.title_label.set_label(&title);
}
if let Some(description) = program.description() {
self.description_label.set_label(&description);
}
self.program.replace(program.to_owned());
}
}
}
glib::wrapper! {
@ -53,35 +95,33 @@ glib::wrapper! {
@extends gtk::Widget, gtk::FlowBoxChild;
}
#[gtk::template_callbacks]
impl ProgramTile {
pub fn new(program: Program) -> Self {
pub fn new_for_setting(navigation: &adw::NavigationView, key: &str) -> Self {
let obj: Self = glib::Object::builder()
.property("program", &program)
.property("navigation", navigation)
.property("key", key)
.build();
let imp = obj.imp();
if program.design() != ProgramDesign::Generic {
obj.add_css_class("highlight");
obj.add_css_class(match program.design() {
ProgramDesign::Generic => "generic",
ProgramDesign::Program1 => "program1",
ProgramDesign::Program2 => "program2",
ProgramDesign::Program3 => "program3",
ProgramDesign::Program4 => "program4",
ProgramDesign::Program5 => "program5",
ProgramDesign::Program6 => "program6",
})
}
if let Some(title) = program.title() {
imp.title_label.set_label(&title);
}
if let Some(description) = program.description() {
imp.description_label.set_label(&description);
}
obj
}
#[template_callback]
fn edit_button_clicked(&self) {
let editor = ProgramEditor::new(&self.navigation(), Some(&self.program()));
editor.connect_save(clone!(
#[weak(rename_to = obj)]
self,
move |_, program| {
let settings = gio::Settings::new(config::APP_ID);
if let Err(err) = settings.set_string(&obj.key(), &program.serialize()) {
log::error!("Failed to save program to settings: {err:?}");
};
obj.set_program(&program);
}
));
self.navigation().push(&editor);
}
}

View file

@ -12,7 +12,6 @@ use gtk::{
use crate::{
album_page::AlbumPage,
album_tile::AlbumTile,
config,
db::models::*,
editor::{
ensemble::EnsembleEditor, instrument::InstrumentEditor, person::PersonEditor,
@ -50,7 +49,7 @@ mod imp {
pub query: OnceCell<LibraryQuery>,
pub highlight: RefCell<Option<Tag>>,
pub programs: RefCell<Vec<Program>>,
pub program_tiles: RefCell<Vec<ProgramTile>>,
pub composers: RefCell<Vec<Person>>,
pub performers: RefCell<Vec<Person>>,
pub ensembles: RefCell<Vec<Ensemble>>,
@ -180,21 +179,11 @@ impl SearchPage {
.build();
if query.is_empty() {
let settings = gio::Settings::new(&config::APP_ID);
let programs = vec![
Program::deserialize(&settings.string("program1")).unwrap(),
Program::deserialize(&settings.string("program2")).unwrap(),
Program::deserialize(&settings.string("program3")).unwrap(),
];
for program in &programs {
for key in &["program1", "program2", "program3"] {
obj.imp()
.programs_flow_box
.append(&ProgramTile::new(program.to_owned()));
.append(&ProgramTile::new_for_setting(navigation, key));
}
obj.imp().programs.replace(programs);
}
obj.imp().query.set(query).unwrap();
@ -326,9 +315,11 @@ impl SearchPage {
let imp = self.imp();
if imp.programs_flow_box.is_visible() {
if let Some(program) = imp.programs.borrow().first().cloned() {
self.player().set_program(program);
self.player().play_from_program();
if let Some(widget) = imp.programs_flow_box.first_child() {
if let Ok(program_tile) = widget.downcast::<ProgramTile>() {
self.player().set_program(program_tile.program());
self.player().play_from_program();
}
}
} else {
let mut new_query = self.imp().query.get().unwrap().clone();