mirror of
				https://github.com/johrpan/musicus.git
				synced 2025-10-26 11:47:25 +01:00 
			
		
		
		
	Add widgets for translating data
This commit is contained in:
		
							parent
							
								
									3c8f5b8c31
								
							
						
					
					
						commit
						783d548bc2
					
				
					 10 changed files with 350 additions and 4 deletions
				
			
		
							
								
								
									
										15
									
								
								data/ui/person_editor.blp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								data/ui/person_editor.blp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 {} | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								data/ui/translation_entry.blp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								data/ui/translation_entry.blp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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 <a href=\"https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes\">ISO 639</a> 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 {} | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								data/ui/translation_section.blp
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								data/ui/translation_section.blp
									
										
									
									
									
										Normal file
									
								
							|  | @ -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"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -39,9 +39,9 @@ pub fn connect(file_name: &str) -> Result<SqliteConnection> { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// A single translated string value.
 | /// A single translated string value.
 | ||||||
| #[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Debug)] | #[derive(Serialize, Deserialize, AsExpression, FromSqlRow, Clone, Default, Debug)] | ||||||
| #[diesel(sql_type = Text)] | #[diesel(sql_type = Text)] | ||||||
| pub struct TranslatedString(HashMap<String, String>); | pub struct TranslatedString(pub HashMap<String, String>); | ||||||
| 
 | 
 | ||||||
| impl TranslatedString { | impl TranslatedString { | ||||||
|     /// Get the best translation for the user's current locale.
 |     /// Get the best translation for the user's current locale.
 | ||||||
|  |  | ||||||
							
								
								
									
										3
									
								
								src/editor/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/editor/mod.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | pub mod person_editor; | ||||||
|  | pub mod translation_entry; | ||||||
|  | pub mod translation_section; | ||||||
							
								
								
									
										45
									
								
								src/editor/person_editor.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/editor/person_editor.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<Self>) { | ||||||
|  |             obj.init_template(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl ObjectImpl for MusicusPersonEditor {} | ||||||
|  |     impl WidgetImpl for MusicusPersonEditor {} | ||||||
|  |     impl NavigationPageImpl for MusicusPersonEditor {} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | glib::wrapper! { | ||||||
|  |     pub struct MusicusPersonEditor(ObjectSubclass<imp::MusicusPersonEditor>) | ||||||
|  |         @extends gtk::Widget, adw::NavigationPage; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[gtk::template_callbacks] | ||||||
|  | impl MusicusPersonEditor { | ||||||
|  |     pub fn new() -> Self { | ||||||
|  |         glib::Object::new() | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										94
									
								
								src/editor/translation_entry.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/editor/translation_entry.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<gtk::Popover>, | ||||||
|  |         #[template_child] | ||||||
|  |         pub lang_entry: TemplateChild<gtk::Entry>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[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<Self>) { | ||||||
|  |             obj.init_template(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     impl ObjectImpl for MusicusTranslationEntry { | ||||||
|  |         fn signals() -> &'static [Signal] { | ||||||
|  |             static SIGNALS: Lazy<Vec<Signal>> = | ||||||
|  |                 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<imp::MusicusTranslationEntry>) | ||||||
|  |         @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<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 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", &[]); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										103
									
								
								src/editor/translation_section.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								src/editor/translation_section.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -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<adw::EntryRow>, | ||||||
|  | 
 | ||||||
|  |         pub translation_entries: RefCell<Vec<MusicusTranslationEntry>>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[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<Self>) { | ||||||
|  |             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<imp::MusicusTranslationSection>) | ||||||
|  |         @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::<String, String>::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); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| use crate::library::MusicusLibrary; |  | ||||||
| use adw::{ | use adw::{ | ||||||
|     prelude::*, |     prelude::*, | ||||||
|     subclass::{navigation_page::NavigationPageImpl, prelude::*}, |     subclass::{navigation_page::NavigationPageImpl, prelude::*}, | ||||||
|  | @ -6,6 +5,9 @@ use adw::{ | ||||||
| use gtk::glib::{self, Properties}; | use gtk::glib::{self, Properties}; | ||||||
| use std::cell::OnceCell; | use std::cell::OnceCell; | ||||||
| 
 | 
 | ||||||
|  | use crate::editor::person_editor::MusicusPersonEditor; | ||||||
|  | use crate::library::MusicusLibrary; | ||||||
|  | 
 | ||||||
| mod imp { | mod imp { | ||||||
|     use super::*; |     use super::*; | ||||||
| 
 | 
 | ||||||
|  | @ -34,7 +36,12 @@ mod imp { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     #[glib::derived_properties] |     #[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 WidgetImpl for LibraryManager {} | ||||||
|     impl NavigationPageImpl for LibraryManager {} |     impl NavigationPageImpl for LibraryManager {} | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| mod application; | mod application; | ||||||
| mod config; | mod config; | ||||||
| mod db; | mod db; | ||||||
|  | mod editor; | ||||||
| mod home_page; | mod home_page; | ||||||
| mod library_manager; | mod library_manager; | ||||||
| mod library; | mod library; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue