From 783d548bc2ed54092311e59f16c5a806bea56de1 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Mon, 1 Apr 2024 16:57:34 +0200 Subject: [PATCH] Add widgets for translating data --- data/ui/person_editor.blp | 15 +++++ data/ui/translation_entry.blp | 59 +++++++++++++++++ data/ui/translation_section.blp | 19 ++++++ src/db/mod.rs | 4 +- src/editor/mod.rs | 3 + src/editor/person_editor.rs | 45 +++++++++++++ src/editor/translation_entry.rs | 94 +++++++++++++++++++++++++++ src/editor/translation_section.rs | 103 ++++++++++++++++++++++++++++++ src/library_manager.rs | 11 +++- src/main.rs | 1 + 10 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 data/ui/person_editor.blp create mode 100644 data/ui/translation_entry.blp create mode 100644 data/ui/translation_section.blp create mode 100644 src/editor/mod.rs create mode 100644 src/editor/person_editor.rs create mode 100644 src/editor/translation_entry.rs create mode 100644 src/editor/translation_section.rs diff --git a/data/ui/person_editor.blp b/data/ui/person_editor.blp new file mode 100644 index 0000000..6d69e37 --- /dev/null +++ b/data/ui/person_editor.blp @@ -0,0 +1,15 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusPersonEditor : Adw.NavigationPage { + title: _("Person"); + + Adw.ToolbarView { + [top] + Adw.HeaderBar header_bar {} + + Adw.Clamp { + $MusicusTranslationSection name_section {} + } + } +} diff --git a/data/ui/translation_entry.blp b/data/ui/translation_entry.blp new file mode 100644 index 0000000..3161f8e --- /dev/null +++ b/data/ui/translation_entry.blp @@ -0,0 +1,59 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusTranslationEntry : Adw.EntryRow { + title: _("Translated name"); + + Gtk.Button { + icon-name: "edit-delete-symbolic"; + valign: center; + clicked => $remove() swapped; + styles ["flat"] + } + + Gtk.Button { + valign: center; + clicked => $open_lang_popover() swapped; + + Gtk.Box { + spacing: 6; + + Gtk.Label { + label: bind lang_entry.text; + } + + Gtk.Image { + icon-name: "pan-down-symbolic"; + } + + Gtk.Popover lang_popover { + Gtk.Box { + orientation: vertical; + spacing: 6; + margin-start: 6; + margin-end: 6; + margin-top: 6; + margin-bottom: 6; + + Gtk.Label { + label: _("Language code"); + halign: start; + styles ["heading"] + } + + Gtk.Label { + width-request: 200; + label: _("Enter an ISO 639 two-letter language code identifying the language this translation uses."); + use-markup: true; + wrap: true; + max-width-chars: 40; + halign: start; + styles ["dim-label"] + } + + Gtk.Entry lang_entry {} + } + } + } + } +} \ No newline at end of file diff --git a/data/ui/translation_section.blp b/data/ui/translation_section.blp new file mode 100644 index 0000000..22c4644 --- /dev/null +++ b/data/ui/translation_section.blp @@ -0,0 +1,19 @@ +using Gtk 4.0; +using Adw 1; + +template $MusicusTranslationSection : Adw.PreferencesGroup { + title: _("Name"); + header-suffix: Gtk.Button add_button { + styles ["flat"] + clicked => $add_translation() swapped; + + Adw.ButtonContent { + icon-name: "list-add-symbolic"; + label: _("Translate"); + } + }; + + Adw.EntryRow entry_row { + title: _("Name"); + } +} \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index 5de4e74..b2fd98e 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -39,9 +39,9 @@ pub fn connect(file_name: &str) -> Result { } /// A single translated string value. -#[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Debug)] +#[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Default, Debug)] #[diesel(sql_type = Text)] -pub struct TranslatedString(HashMap); +pub struct TranslatedString(pub HashMap); impl TranslatedString { /// Get the best translation for the user's current locale. diff --git a/src/editor/mod.rs b/src/editor/mod.rs new file mode 100644 index 0000000..92cb362 --- /dev/null +++ b/src/editor/mod.rs @@ -0,0 +1,3 @@ +pub mod person_editor; +pub mod translation_entry; +pub mod translation_section; \ No newline at end of file diff --git a/src/editor/person_editor.rs b/src/editor/person_editor.rs new file mode 100644 index 0000000..4d764a4 --- /dev/null +++ b/src/editor/person_editor.rs @@ -0,0 +1,45 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gtk::glib; + +use crate::editor::translation_section::MusicusTranslationSection; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/person_editor.blp")] + pub struct MusicusPersonEditor {} + + #[glib::object_subclass] + impl ObjectSubclass for MusicusPersonEditor { + const NAME: &'static str = "MusicusPersonEditor"; + type Type = super::MusicusPersonEditor; + type ParentType = adw::NavigationPage; + + fn class_init(klass: &mut Self::Class) { + MusicusTranslationSection::static_type(); + klass.bind_template(); + klass.bind_template_instance_callbacks(); + } + + fn instance_init(obj: &glib::subclass::InitializingObject) { + obj.init_template(); + } + } + + impl ObjectImpl for MusicusPersonEditor {} + impl WidgetImpl for MusicusPersonEditor {} + impl NavigationPageImpl for MusicusPersonEditor {} +} + +glib::wrapper! { + pub struct MusicusPersonEditor(ObjectSubclass) + @extends gtk::Widget, adw::NavigationPage; +} + +#[gtk::template_callbacks] +impl MusicusPersonEditor { + pub fn new() -> Self { + glib::Object::new() + } +} diff --git a/src/editor/translation_entry.rs b/src/editor/translation_entry.rs new file mode 100644 index 0000000..df50956 --- /dev/null +++ b/src/editor/translation_entry.rs @@ -0,0 +1,94 @@ +use adw::{prelude::*, subclass::prelude::*}; +use gtk::glib::{self, subclass::Signal}; +use once_cell::sync::Lazy; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/translation_entry.blp")] + pub struct MusicusTranslationEntry { + #[template_child] + pub lang_popover: TemplateChild, + #[template_child] + pub lang_entry: TemplateChild, + } + + #[glib::object_subclass] + impl ObjectSubclass for MusicusTranslationEntry { + const NAME: &'static str = "MusicusTranslationEntry"; + type Type = super::MusicusTranslationEntry; + type ParentType = adw::EntryRow; + + 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 MusicusTranslationEntry { + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("remove").build()]); + + SIGNALS.as_ref() + } + + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl WidgetImpl for MusicusTranslationEntry {} + impl ListBoxRowImpl for MusicusTranslationEntry {} + impl PreferencesRowImpl for MusicusTranslationEntry {} + impl EntryRowImpl for MusicusTranslationEntry {} + impl EditableImpl for MusicusTranslationEntry {} +} + +glib::wrapper! { + pub struct MusicusTranslationEntry(ObjectSubclass) + @extends gtk::Widget, gtk::ListBoxRow, adw::PreferencesRow, adw::EntryRow, + @implements gtk::Editable; +} + +#[gtk::template_callbacks] +impl MusicusTranslationEntry { + pub fn new(lang: &str, translation: &str) -> Self { + let obj: Self = glib::Object::new(); + obj.set_text(translation); + obj.imp().lang_entry.set_text(lang); + obj + } + + 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 lang(&self) -> String { + self.imp().lang_entry.text().into() + } + + pub fn translation(&self) -> String { + self.imp().text().into() + } + + #[template_callback] + fn open_lang_popover(&self, _: >k::Button) { + self.imp().lang_popover.popup(); + self.imp().lang_entry.grab_focus(); + } + + #[template_callback] + fn remove(&self, _: >k::Button) { + self.emit_by_name::<()>("remove", &[]); + } +} diff --git a/src/editor/translation_section.rs b/src/editor/translation_section.rs new file mode 100644 index 0000000..5e1c795 --- /dev/null +++ b/src/editor/translation_section.rs @@ -0,0 +1,103 @@ +use std::cell::RefCell; +use std::collections::HashMap; + +use adw::{prelude::*, subclass::prelude::*}; +use gtk::glib; + +use crate::db::TranslatedString; +use crate::editor::translation_entry::MusicusTranslationEntry; +use crate::util; + +mod imp { + use super::*; + + #[derive(Debug, Default, gtk::CompositeTemplate)] + #[template(file = "data/ui/translation_section.blp")] + pub struct MusicusTranslationSection { + #[template_child] + pub entry_row: TemplateChild, + + pub translation_entries: RefCell>, + } + + #[glib::object_subclass] + impl ObjectSubclass for MusicusTranslationSection { + const NAME: &'static str = "MusicusTranslationSection"; + type Type = super::MusicusTranslationSection; + type ParentType = adw::PreferencesGroup; + + 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 MusicusTranslationSection { + fn constructed(&self) { + self.parent_constructed(); + } + } + + impl WidgetImpl for MusicusTranslationSection {} + impl PreferencesGroupImpl for MusicusTranslationSection {} +} + +glib::wrapper! { + pub struct MusicusTranslationSection(ObjectSubclass) + @extends gtk::Widget, adw::PreferencesGroup; +} + +#[gtk::template_callbacks] +impl MusicusTranslationSection { + pub fn new(translation: TranslatedString) -> Self { + let obj: Self = glib::Object::new(); + let mut translation = translation.0; + + obj.imp() + .entry_row + .set_text(&translation.remove("generic").unwrap_or_default()); + + for (lang, translation) in translation { + obj.add_entry(&lang, &translation); + } + + obj + } + + #[template_callback] + fn add_translation(&self, _: >k::Button) { + self.add_entry(&util::LANG, &self.imp().entry_row.text()); + } + + fn translation(&self) -> TranslatedString { + let imp = self.imp(); + let mut translation = HashMap::::new(); + + translation.insert(String::from("generic"), imp.entry_row.text().into()); + for entry in &*imp.translation_entries.borrow() { + translation.insert(entry.lang(), entry.translation()); + } + + TranslatedString(translation) + } + + fn add_entry(&self, lang: &str, translation: &str) { + let entry = MusicusTranslationEntry::new(lang, translation); + + let obj = self.clone(); + entry.connect_remove(move |entry| { + let mut entries = obj.imp().translation_entries.borrow_mut(); + if let Some(index) = entries.iter().position(|e| e == entry) { + entries.remove(index); + } + obj.remove(entry); + }); + + self.add(&entry); + self.imp().translation_entries.borrow_mut().push(entry); + } +} diff --git a/src/library_manager.rs b/src/library_manager.rs index cb07b49..9ff95e4 100644 --- a/src/library_manager.rs +++ b/src/library_manager.rs @@ -1,4 +1,3 @@ -use crate::library::MusicusLibrary; use adw::{ prelude::*, subclass::{navigation_page::NavigationPageImpl, prelude::*}, @@ -6,6 +5,9 @@ use adw::{ use gtk::glib::{self, Properties}; use std::cell::OnceCell; +use crate::editor::person_editor::MusicusPersonEditor; +use crate::library::MusicusLibrary; + mod imp { use super::*; @@ -34,7 +36,12 @@ mod imp { } #[glib::derived_properties] - impl ObjectImpl for LibraryManager {} + impl ObjectImpl for LibraryManager { + fn constructed(&self) { + self.parent_constructed(); + self.obj().set_child(Some(&MusicusPersonEditor::new())); + } + } impl WidgetImpl for LibraryManager {} impl NavigationPageImpl for LibraryManager {} diff --git a/src/main.rs b/src/main.rs index 906be97..f336ae4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod application; mod config; mod db; +mod editor; mod home_page; mod library_manager; mod library;