Add widgets for translating data

This commit is contained in:
Elias Projahn 2024-04-01 16:57:34 +02:00
parent 3c8f5b8c31
commit 783d548bc2
10 changed files with 350 additions and 4 deletions

15
data/ui/person_editor.blp Normal file
View 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 {}
}
}
}

View 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 {}
}
}
}
}
}

View 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");
}
}

View file

@ -39,9 +39,9 @@ pub fn connect(file_name: &str) -> Result<SqliteConnection> {
}
/// 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<String, String>);
pub struct TranslatedString(pub HashMap<String, String>);
impl TranslatedString {
/// Get the best translation for the user's current locale.

3
src/editor/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod person_editor;
pub mod translation_entry;
pub mod translation_section;

View 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()
}
}

View 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, _: &gtk::Button) {
self.imp().lang_popover.popup();
self.imp().lang_entry.grab_focus();
}
#[template_callback]
fn remove(&self, _: &gtk::Button) {
self.emit_by_name::<()>("remove", &[]);
}
}

View 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, _: &gtk::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);
}
}

View file

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

View file

@ -1,6 +1,7 @@
mod application;
mod config;
mod db;
mod editor;
mod home_page;
mod library_manager;
mod library;