Split the UI code into multiple files

This commit is contained in:
Martin Dvořák 2026-09-11 11:42:13 +02:00
commit 56fe5ae6fc
23 changed files with 10140 additions and 9559 deletions

979
exercise_u1.c Normal file
View file

@ -0,0 +1,979 @@
#include "maturita.h"
/* ------------------------------------------------------------------ */
/* Choice exercises (3 + 9) */
/* ------------------------------------------------------------------ */
const ChoiceQ ex3_questions[] = {
{"Welche Zahl ist „vierzehn“?", {"4", "14", "40"}, 3, 1},
{"Welche Zahl ist „siebzehn“?", {"7", "17", "70"}, 3, 1},
{"Welche Zahl ist „zwanzig“?", {"2", "12", "20"}, 3, 2},
{"Welche Zahl ist „sechs“?", {"6", "16", "60"}, 3, 0},
};
const char *ex3_meaning[] = {
"„vierzehn“ = čtrnáct (14)",
"„siebzehn“ = sedmnáct (17)",
"„zwanzig“ = dvacet (20)",
"„sechs“ = šest (6)",
};
const ChoiceQ ex9_questions[] = {
{"___ heißt du?", {"Wer", "Wie", "Wo"}, 3, 1},
{"___ kommst du?", {"Woher", "Wie", "Wer"}, 3, 0},
{"___ wohnst du?", {"Wo", "Wer", "Was"}, 3, 0},
{"___ ist das?", {"Wer", "Wo", "Wie"}, 3, 0},
{"___ alt bist du?", {"Wie", "Woher", "Was"}, 3, 0},
};
const char *ex9_meaning[] = {
"Jak se jmenuješ?",
"Odkud jsi?",
"Kde bydlíš?",
"Kdo to je?",
"Kolik je ti let?",
};
void choice_check(GtkButton *button, gpointer data) {
ChoiceCtx *ctx = data;
int ok = 0;
(void)button;
for (int i = 0; i < ctx->n; i++) {
int active = -1;
for (int o = 0; o < ctx->n_opts; o++) {
GtkToggleButton *tb = ctx->toggles[i * ctx->n_opts + o];
gtk_widget_remove_css_class(GTK_WIDGET(tb), "ok");
gtk_widget_remove_css_class(GTK_WIDGET(tb), "wrong");
if (gtk_toggle_button_get_active(tb))
active = o;
}
if (active == ctx->qs[i].correct) {
ok++;
gtk_widget_add_css_class(GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "ok");
} else if (active >= 0) {
gtk_widget_add_css_class(GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "wrong");
}
}
if (ok == ctx->n) {
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
mark_done(ctx->unit, ctx->ex_num);
} else {
set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short"));
}
meaning_reveal_all(ctx->trans, ctx->n);
}
GtkWidget *build_choice(UnitCtx *unit, const char *title,
const char *subtitle, int ex_num,
const ChoiceQ *qs, const char **meanings, int n) {
GtkWidget *body, *feedback, *check;
GtkWidget *page = ex_page_shell(unit->page, title, subtitle, "check",
&body, &feedback, &check);
ChoiceCtx *ctx = g_new0(ChoiceCtx, 1);
int n_opts = qs[0].n_options;
ctx->qs = qs;
ctx->n = n;
ctx->n_opts = n_opts;
ctx->unit = unit;
ctx->ex_num = ex_num;
ctx->feedback = feedback;
ctx->toggles = g_new0(GtkToggleButton *, n * n_opts);
ctx->trans = g_new0(GtkWidget *, n);
for (int i = 0; i < n; i++) {
GtkWidget *prompt = gtk_label_new(qs[i].prompt);
GtkWidget *row;
GtkToggleButton *first = NULL;
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
gtk_widget_add_css_class(prompt, "ex-prompt");
gtk_box_append(GTK_BOX(body), prompt);
row = gtk_flow_box_new();
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE);
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts);
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), row);
for (int o = 0; o < qs[i].n_options; o++) {
GtkWidget *tb = gtk_toggle_button_new_with_label(qs[i].options[o]);
gtk_widget_add_css_class(tb, "pill");
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first);
if (!first)
first = GTK_TOGGLE_BUTTON(tb);
gtk_flow_box_append(GTK_FLOW_BOX(row), tb);
ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb);
}
if (meanings[i])
ctx->trans[i] = meaning_add(body, meanings[i]);
}
g_signal_connect(check, "clicked", G_CALLBACK(choice_check), ctx);
return page;
}
/* ------------------------------------------------------------------ */
/* Exercise 4: Freie Antwort */
/* ------------------------------------------------------------------ */
const FreeQ ex4_questions[] = {
{"Wie heißt du?", "Jak se jmenuješ?",
"Ich heiße Anna.", "Jmenuji se Anna."},
{"Woher kommst du?", "Odkud pocházíš?",
"Ich komme aus Tschechien.", "Pocházím z Česka."},
{"Wie alt bist du?", "Kolik je ti let?",
"Ich bin sechzehn Jahre alt.", "Je mi šestnáct let."},
{"Wie geht es dir?", "Jak se máš?",
"Mir geht es gut, danke.", "Mám se dobře, děkuji."},
};
void ex4_fill_sample(GtkWidget *label) {
const FreeQ *q = g_object_get_data(G_OBJECT(label), "fq");
char *txt;
if (!q)
return;
txt = g_strdup_printf(tr("sample_fmt"), tr(q->q_cs), q->sample,
tr(q->a_cs));
gtk_label_set_text(GTK_LABEL(label), txt);
g_free(txt);
}
void ex4_reveal(GtkButton *button, gpointer data) {
GtkWidget *sample = data;
(void)button;
ex4_fill_sample(sample);
gtk_widget_set_visible(sample, !gtk_widget_get_visible(sample));
}
void ex4_finish(GtkButton *button, gpointer data) {
Ex4Ctx *ctx = data;
(void)button;
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
for (int i = 0; i < ctx->n; i++) {
ex4_fill_sample(ctx->samples[i]);
gtk_widget_set_visible(ctx->samples[i], TRUE);
}
mark_done(ctx->unit, ctx->ex_num);
}
GtkWidget *build_free_answer(UnitCtx *unit, int ex_num, const char *title,
const char *subtitle, const char *tip_key,
const FreeQ *qs, int n) {
GtkWidget *body, *feedback, *check;
Ex4Ctx *ctx;
GtkWidget *page = ex_page_shell(unit->page, title, subtitle, "finish",
&body, &feedback, &check);
ctx = g_new0(Ex4Ctx, 1);
ctx->unit = unit;
ctx->ex_num = ex_num;
ctx->feedback = feedback;
ctx->n = n;
ctx->samples = g_new0(GtkWidget *, n);
if (tip_key) {
GtkWidget *tip = gtk_label_new(NULL);
i18n_bind(tip, tip_key, 0);
gtk_widget_set_halign(tip, GTK_ALIGN_START);
gtk_widget_add_css_class(tip, "hint");
gtk_box_append(GTK_BOX(body), tip);
}
for (int i = 0; i < n; i++) {
const FreeQ *q = &qs[i];
GtkWidget *ql = gtk_label_new(q->question);
GtkWidget *entry;
GtkWidget *reveal;
GtkWidget *sample;
gtk_widget_set_halign(ql, GTK_ALIGN_START);
gtk_widget_add_css_class(ql, "ex-prompt");
gtk_box_append(GTK_BOX(body), ql);
entry = gtk_entry_new();
i18n_bind(entry, "your_answer", 3);
gtk_box_append(GTK_BOX(body), entry);
reveal = gtk_button_new();
i18n_bind(reveal, "show_sample", 1);
gtk_widget_add_css_class(reveal, "pill");
gtk_widget_set_halign(reveal, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(body), reveal);
sample = gtk_label_new(NULL);
g_object_set_data(G_OBJECT(sample), "fq", (gpointer)q);
gtk_widget_set_halign(sample, GTK_ALIGN_START);
gtk_widget_add_css_class(sample, "hint");
gtk_label_set_wrap(GTK_LABEL(sample), TRUE);
gtk_widget_set_visible(sample, FALSE);
gtk_box_append(GTK_BOX(body), sample);
ctx->samples[i] = sample;
g_signal_connect(reveal, "clicked", G_CALLBACK(ex4_reveal), sample);
}
g_signal_connect(check, "clicked", G_CALLBACK(ex4_finish), ctx);
return page;
}
GtkWidget *build_ex4(UnitCtx *unit) {
return build_free_answer(unit, 4, "Freie Antwort", "sub_free", "tip_ss",
ex4_questions,
(int)G_N_ELEMENTS(ex4_questions));
}
/* ------------------------------------------------------------------ */
/* Exercise 5: Zahlen (digits -> word) */
/* ------------------------------------------------------------------ */
const NumberQ ex5_data[] = {
{"14", "vierzehn"},
{"17", "siebzehn"},
{"20", "zwanzig"},
{"6", "sechs"},
{"13", "dreizehn"},
};
const char *ex5_pool[] = {
"vierzehn", "siebzehn", "zwanzig", "sechs", "dreizehn",
};
const char *ex5_meaning[] = {
"čtrnáct",
"sedmnáct",
"dvacet",
"šest",
"třináct",
};
GtkWidget *build_ex5(UnitCtx *unit) {
GtkWidget *body, *feedback, *check;
GtkWidget *page = ex_page_shell(unit->page, "Zahlen",
"sub_zahlen",
"check", &body, &feedback, &check);
ComboListCtx *ctx = combo_list_ctx_new(unit, 5, feedback);
for (guint i = 0; i < G_N_ELEMENTS(ex5_data); i++) {
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
GtkWidget *digits = gtk_label_new(ex5_data[i].digits);
GtkWidget *eq = gtk_label_new("=");
GtkWidget *combo;
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_widget_add_css_class(digits, "number-big");
gtk_widget_set_valign(digits, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), digits);
gtk_widget_set_valign(eq, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(eq, "ex-prompt");
gtk_box_append(GTK_BOX(row), eq);
combo = make_word_combo(ex5_pool, G_N_ELEMENTS(ex5_pool));
gtk_widget_set_size_request(combo, 140, -1);
gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), combo);
combo_list_add(ctx, combo, ex5_data[i].answer);
gtk_box_append(GTK_BOX(body), row);
combo_list_add_trans(ctx, meaning_add(body, ex5_meaning[i]));
}
g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
return page;
}
/* ------------------------------------------------------------------ */
/* Exercise 6: Wie viel? */
/* ------------------------------------------------------------------ */
const CountQ ex6_data[] = {
{"🍎", 2, "Äpfel", "zwei"},
{"⭐", 5, "Sterne", "fünf"},
{"🐱", 7, "Katzen", "sieben"},
{"🌸", 9, "Blumen", "neun"},
{"🚗", 3, "Autos", "drei"},
{"📚", 11, "Bücher", "elf"},
{"🕯️", 4, "Kerzen", "vier"},
{"⚽", 8, "Bälle", "acht"},
{"🐦", 6, "Vögel", "sechs"},
};
const char *ex6_pool[] = {
"zwei", "fünf", "sieben", "neun", "drei", "elf", "vier", "acht", "sechs",
};
const char *ex6_meaning[] = {
"dvě jablka",
"pět hvězd",
"sedm koček",
"devět květin",
"tři auta",
"jedenáct knih",
"čtyři svíčky",
"osm míčů",
"šest ptáků",
};
GtkWidget *build_ex6(UnitCtx *unit) {
GtkWidget *body, *feedback, *check;
GtkWidget *page = ex_page_shell(unit->page, "Wie viel?",
"sub_wieviel",
"check", &body, &feedback, &check);
ComboListCtx *ctx = combo_list_ctx_new(unit, 6, feedback);
for (guint i = 0; i < G_N_ELEMENTS(ex6_data); i++) {
const CountQ *q = &ex6_data[i];
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
GString *em = g_string_new(NULL);
GtkWidget *icons;
GtkWidget *noun;
GtkWidget *combo;
for (int c = 0; c < q->count; c++) {
if (c)
g_string_append_c(em, ' ');
g_string_append(em, q->emoji);
}
icons = gtk_label_new(em->str);
g_string_free(em, TRUE);
gtk_widget_set_valign(icons, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), icons);
noun = gtk_label_new(q->noun);
gtk_widget_set_valign(noun, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(noun, "ex-prompt");
gtk_box_append(GTK_BOX(row), noun);
combo = make_word_combo(ex6_pool, G_N_ELEMENTS(ex6_pool));
gtk_widget_set_size_request(combo, 130, -1);
gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), combo);
combo_list_add(ctx, combo, q->answer);
gtk_box_append(GTK_BOX(body), row);
combo_list_add_trans(ctx, meaning_add(body, ex6_meaning[i]));
}
g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
return page;
}
/* ------------------------------------------------------------------ */
/* Exercise 7: Zahlenreihe */
/* ------------------------------------------------------------------ */
const SeqQ ex7_data[] = {
{"sechs, sieben, ", ", neun", "acht"},
{"zwölf, dreizehn, ", ", fünfzehn", "vierzehn"},
{"achtzehn, neunzehn, ", ", einundzwanzig", "zwanzig"},
{"vier, fünf, ", ", sieben", "sechs"},
{"zehn, elf, ", ", dreizehn", "zwölf"},
};
const char *ex7_pool[] = {
"acht", "vierzehn", "zwanzig", "sechs", "zwölf",
};
const char *ex7_meaning[] = {
"šest, sedm, osm, devět",
"dvanáct, třináct, čtrnáct, patnáct",
"osmnáct, devatenáct, dvacet, dvacet jedna",
"čtyři, pět, šest, sedm",
"deset, jedenáct, dvanáct, třináct",
};
GtkWidget *build_ex7(UnitCtx *unit) {
GtkWidget *body, *feedback, *check;
GtkWidget *page = ex_page_shell(unit->page, "Zahlenreihe",
"sub_reihe",
"check", &body, &feedback, &check);
ComboListCtx *ctx = combo_list_ctx_new(unit, 7, feedback);
for (guint i = 0; i < G_N_ELEMENTS(ex7_data); i++) {
const SeqQ *q = &ex7_data[i];
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *before = gtk_label_new(q->before);
GtkWidget *after = gtk_label_new(q->after);
GtkWidget *combo;
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_widget_set_valign(before, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(before, "ex-prompt");
gtk_box_append(GTK_BOX(row), before);
combo = make_word_combo(ex7_pool, G_N_ELEMENTS(ex7_pool));
gtk_widget_set_size_request(combo, 140, -1);
gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), combo);
combo_list_add(ctx, combo, q->answer);
gtk_widget_set_valign(after, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(after, "ex-prompt");
gtk_box_append(GTK_BOX(row), after);
gtk_box_append(GTK_BOX(body), row);
combo_list_add_trans(ctx, meaning_add(body, ex7_meaning[i]));
}
g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
return page;
}
/* ------------------------------------------------------------------ */
/* Exercise 8: Verb einsetzen (word selection) */
/* ------------------------------------------------------------------ */
/* Complete German sentence for a before/after gap row (with any trailing
* "(…)" hint such as "(kommen)" removed) – shown after Check. */
char *verbq_german(const char *before, const char *answer,
const char *after) {
GString *s;
const char *p;
gboolean inside = FALSE;
char *out;
s = g_string_new(NULL);
if (before && before[0])
g_string_append(s, before);
if (answer && answer[0])
g_string_append(s, answer);
if (after && after[0])
g_string_append(s, after);
out = s->str;
s->str = NULL;
g_string_free(s, TRUE);
s = g_string_new(NULL);
for (p = out; *p; p++) {
if (*p == '(') {
inside = TRUE;
continue;
}
if (*p == ')') {
inside = FALSE;
continue;
}
if (!inside)
g_string_append_c(s, *p);
}
g_free(out);
while (s->len > 0 && g_ascii_isspace(s->str[s->len - 1]))
g_string_truncate(s, s->len - 1);
if (s->len == 0) {
g_string_free(s, TRUE);
return NULL;
}
return g_string_free(s, FALSE);
}
const VerbQ ex8_data[] = {
{"Ich ", " Peter.", "heiße"},
{"Woher ", " du?", "kommst"},
{"Ich ", " in Prag.", "wohne"},
{"Wie alt ", " du?", "bist"},
{"Ich ", " Fußball.", "spiele"},
{"Was ", " du gern?", "machst"},
};
const char *ex8_pool[] = {
"heißen", "kommen", "wohnen", "sein", "spielen", "machen",
"heiße", "kommst", "wohne", "bist", "spiele", "machst",
};
const char *ex8_meaning[] = {
"Jmenuji se Petr.",
"Odkud jsi?",
"Bydlím v Praze.",
"Kolik je ti let?",
"Hraji fotbal.",
"Co rád/a děláš?",
};
GtkWidget *build_ex8(UnitCtx *unit) {
GtkWidget *body, *feedback, *check;
GtkWidget *page = ex_page_shell(unit->page, "Verb einsetzen",
"sub_verb",
"check", &body, &feedback, &check);
ComboListCtx *ctx = combo_list_ctx_new(unit, 8, feedback);
ctx->trans_upfront = TRUE;
ctx->reveal_german = TRUE;
for (guint i = 0; i < G_N_ELEMENTS(ex8_data); i++) {
const VerbQ *q = &ex8_data[i];
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *before = gtk_label_new(q->before);
GtkWidget *after = gtk_label_new(q->after);
GtkWidget *combo;
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_widget_set_valign(before, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(before, "ex-prompt");
gtk_box_append(GTK_BOX(row), before);
combo = make_word_combo(ex8_pool, G_N_ELEMENTS(ex8_pool));
gtk_widget_set_size_request(combo, 120, -1);
gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), combo);
combo_list_add(ctx, combo, q->answer);
gtk_widget_set_valign(after, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(after, "ex-prompt");
gtk_box_append(GTK_BOX(row), after);
gtk_box_append(GTK_BOX(body), row);
{
GtkWidget *mean = meaning_add(body, ex8_meaning[i]);
GtkWidget *w;
char *sentence;
gtk_widget_set_visible(mean, TRUE);
combo_list_add_trans(ctx, mean);
sentence = verbq_german(q->before, q->answer, q->after);
if (sentence && sentence[0]) {
w = model_answer_add(body, sentence);
g_array_append_val(ctx->models, w);
}
g_free(sentence);
}
}
g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
return page;
}
/* ------------------------------------------------------------------ */
/* Exercise 10: Wörter trennen (word arrangement) */
/* ------------------------------------------------------------------ */
const AssemblyItem ex10_items[] = {
{"wiespätistes", {"wie", "spät", "ist", "es"}, 4},
{"ichheißeanna", {"ich", "heiße", "anna"}, 3},
{"woherkommstdu", {"woher", "kommst", "du"}, 3},
{"aufwiedersehen", {"auf", "wiedersehen"}, 2},
{"dankeschön", {"danke", "schön"}, 2},
};
const char *ex10_meaning[] = {
"Kolik je hodin?",
"Jmenuji se Anna.",
"Odkud jsi?",
"Na shledanou.",
"Moc děkuji.",
};
/* ------------------------------------------------------------------ */
/* Exercise 12: Was macht er/sie? */
/* ------------------------------------------------------------------ */
const VerbClueQ ex12_data[] = {
{"🏊", "Er ", " im See.", "schwimmt"},
{"🎤", "Sie ", " ein Lied.", "singt"},
{"🍳", "Er ", " Suppe.", "kocht"},
{"📖", "Sie ", " ein Buch.", "liest"},
{"🚗", "Er ", " Auto.", "fährt"},
{"🎨", "Sie ", " ein Bild.", "malt"},
{"⚽", "Er ", " Fußball.", "spielt"},
{"🛏️", "Sie ", " .", "schläft"},
};
const char *ex12_pool[] = {
"schwimmt", "singt", "kocht", "liest", "fährt", "malt", "spielt", "schläft",
};
const char *ex12_meaning[] = {
"Plave v jezeře.",
"Zpívá píseň.",
"Vaří polévku.",
"Čte knihu.",
"Řídí auto.",
"Maluje obraz.",
"Hraje fotbal.",
"Spí.",
};
GtkWidget *build_ex12(UnitCtx *unit) {
GtkWidget *body, *feedback, *check;
GtkWidget *page = ex_page_shell(unit->page, "Was macht er/sie?",
"sub_bild",
"check", &body, &feedback, &check);
ComboListCtx *ctx = combo_list_ctx_new(unit, 12, feedback);
ctx->trans_upfront = TRUE;
ctx->reveal_german = TRUE;
for (guint i = 0; i < G_N_ELEMENTS(ex12_data); i++) {
const VerbClueQ *q = &ex12_data[i];
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *emoji = gtk_label_new(q->emoji);
GtkWidget *before = gtk_label_new(q->before);
GtkWidget *after = gtk_label_new(q->after);
GtkWidget *combo;
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_widget_set_valign(emoji, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), emoji);
gtk_widget_set_valign(before, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(before, "ex-prompt");
gtk_box_append(GTK_BOX(row), before);
combo = make_word_combo(ex12_pool, G_N_ELEMENTS(ex12_pool));
gtk_widget_set_size_request(combo, 120, -1);
gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(row), combo);
combo_list_add(ctx, combo, q->answer);
gtk_widget_set_valign(after, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(after, "ex-prompt");
gtk_box_append(GTK_BOX(row), after);
gtk_box_append(GTK_BOX(body), row);
{
GtkWidget *mean = meaning_add(body, ex12_meaning[i]);
GtkWidget *w;
char *sentence;
gtk_widget_set_visible(mean, TRUE);
combo_list_add_trans(ctx, mean);
sentence = verbq_german(q->before, q->answer, q->after);
if (sentence && sentence[0]) {
w = model_answer_add(body, sentence);
g_array_append_val(ctx->models, w);
}
g_free(sentence);
}
}
g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
return page;
}
/* ------------------------------------------------------------------ */
/* Assign exercises (11 + 13) */
/* ------------------------------------------------------------------ */
const AssignItem ex11_items[] = {
{NULL, "Hallo", 0},
{NULL, "Guten Morgen", 0},
{NULL, "Guten Tag", 0},
{NULL, "Guten Abend", 0},
{NULL, "Grüß Gott", 0},
{NULL, "Servus", 0},
{NULL, "Tschüss", 1},
{NULL, "Auf Wiedersehen", 1},
{NULL, "Bis bald", 1},
{NULL, "Bis morgen", 1},
{NULL, "Gute Nacht", 1},
{NULL, "Bis später", 1},
};
const AssignItem ex13_items[] = {
{"🥨", "Brezel", 0},
{"🌭", "Bratwurst", 0},
{"🏰", "Brandenburger Tor", 0},
{"🚗", "Volkswagen", 0},
{"🎻", "Mozart", 1},
{"🍰", "Sachertorte", 1},
{"🥩", "Wiener Schnitzel", 1},
{"🧀", "Käse", 2},
{"⛰️", "Matterhorn", 2},
{"🍫", "Schokolade", 2},
};
const char *ex11_groups[] = {"Begrüßung", "Verabschiedung"};
const char *ex13_groups[] = {"Deutschland (D)", "Österreich (A)", "Schweiz (CH)"};
const char *ex11_meaning[] = {
"Ahoj",
"Dobré ráno",
"Dobrý den",
"Dobrý večer",
"Dobrý den (Bavorsko, Rakousko)",
"Ahoj / Čau",
"Čau",
"Na shledanou",
"Brzy na viděnou",
"Do zítřka",
"Dobrou noc",
"Zatím / Na viděnou",
};
const char *ex13_meaning[] = {
"Preclík",
"Klobása",
"Braniborská brána",
"Značka aut",
"Rakouský skladatel",
"Čokoládový dort (Vídeň)",
"Vídeňský řízek",
"Sýr",
"Švýcarská hora",
"Čokoláda",
};
void assign_rebuild(AssignCtx *ac) {
for (int i = 0; i < ac->n_items; i++) {
GtkWidget *parent = gtk_widget_get_parent(ac->items[i]);
if (parent != NULL && GTK_IS_FLOW_BOX_CHILD(parent))
gtk_flow_box_child_set_child(GTK_FLOW_BOX_CHILD(parent), NULL);
}
flow_clear(GTK_FLOW_BOX(ac->pool));
for (int g = 0; g < ac->n_groups; g++)
flow_clear(GTK_FLOW_BOX(ac->group_flow[g]));
for (int i = 0; i < ac->n_items; i++) {
if (ac->current_group[i] < 0)
gtk_flow_box_insert(GTK_FLOW_BOX(ac->pool), ac->items[i], -1);
else
gtk_flow_box_insert(GTK_FLOW_BOX(ac->group_flow[ac->current_group[i]]),
ac->items[i], -1);
}
}
void assign_chip_clicked(GtkButton *button, gpointer data) {
AssignChipRef *ref = data;
AssignCtx *ac = ref->ac;
int idx = ref->idx;
double sx = 0.0, sy = 0.0;
GtkWidget *chip = GTK_WIDGET(button);
(void)button;
if (ac->flying)
return;
if (ac->stage &&
gtk_widget_translate_coordinates(chip, ac->stage, 0, 0, &sx, &sy)) {
if (ac->current_group[idx] < 0)
ac->current_group[idx] = ac->active_group;
else
ac->current_group[idx] = -1;
assign_rebuild(ac);
asm_begin_fly(chip, ac->stage, &ac->flying, sx, sy);
} else {
/* No usable animation layer yet – just move instantly. */
if (ac->current_group[idx] < 0)
ac->current_group[idx] = ac->active_group;
else
ac->current_group[idx] = -1;
assign_rebuild(ac);
}
}
void assign_group_toggled(GtkToggleButton *button, gpointer data) {
AssignCtx *ac = data;
if (!gtk_toggle_button_get_active(button))
return;
for (int g = 0; g < ac->n_groups; g++) {
if (GTK_TOGGLE_BUTTON(ac->group_btn[g]) == button) {
ac->active_group = g;
break;
}
}
}
void assign_check(GtkButton *button, gpointer data) {
AssignCtx *ac = data;
int ok = 0;
(void)button;
for (int i = 0; i < ac->n_items; i++)
if (ac->current_group[i] == ac->items_data[i].correct_group)
ok++;
if (ok == ac->n_items) {
set_feedback(ac->feedback, TRUE, tr("feedback_ok"));
mark_done(ac->unit, ac->ex_num);
} else {
set_feedback(ac->feedback, FALSE, tr("feedback_retry"));
}
for (int i = 0; i < ac->n_items; i++)
if (ac->trans[i])
gtk_widget_set_visible(ac->trans[i], TRUE);
}
GtkWidget *build_assign(UnitCtx *unit, const char *title,
const char *subtitle, int ex_num,
const AssignItem *items, int n_items,
const char **group_labels, int n_groups,
const char **meanings) {
GtkWidget *body, *feedback, *check;
GtkWidget *page = ex_page_shell(unit->page, title, subtitle, "check",
&body, &feedback, &check);
AssignCtx *ac = g_new0(AssignCtx, 1);
GtkWidget *hint;
GtkWidget *row;
GtkWidget *holder;
GtkWidget *content;
GtkWidget *stage;
GtkToggleButton *first = NULL;
int *order;
ac->n_items = n_items;
ac->n_groups = n_groups;
ac->active_group = 0;
ac->unit = unit;
ac->ex_num = ex_num;
ac->feedback = feedback;
ac->items_data = items;
ac->items = g_new0(GtkWidget *, n_items);
ac->trans = g_new0(GtkWidget *, n_items);
ac->current_group = g_new0(int, n_items);
for (int i = 0; i < n_items; i++)
ac->current_group[i] = -1;
holder = gtk_overlay_new();
gtk_widget_set_hexpand(holder, TRUE);
gtk_box_append(GTK_BOX(body), holder);
content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
gtk_widget_set_halign(content, GTK_ALIGN_FILL);
gtk_overlay_set_child(GTK_OVERLAY(holder), content);
stage = gtk_fixed_new();
gtk_widget_set_halign(stage, GTK_ALIGN_FILL);
gtk_widget_set_valign(stage, GTK_ALIGN_FILL);
gtk_widget_set_can_target(stage, FALSE);
gtk_overlay_add_overlay(GTK_OVERLAY(holder), stage);
ac->stage = stage;
hint = gtk_label_new(NULL);
i18n_bind(hint, "assign_hint", 0);
gtk_label_set_wrap(GTK_LABEL(hint), TRUE);
gtk_widget_set_halign(hint, GTK_ALIGN_START);
gtk_widget_add_css_class(hint, "ex-sub");
gtk_box_append(GTK_BOX(content), hint);
row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
gtk_widget_set_halign(row, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(content), row);
for (int g = 0; g < n_groups; g++) {
GtkWidget *gb = gtk_toggle_button_new_with_label(group_labels[g]);
gtk_widget_add_css_class(gb, "group-btn");
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(gb), first);
if (!first)
first = GTK_TOGGLE_BUTTON(gb);
gtk_box_append(GTK_BOX(row), gb);
ac->group_btn[g] = gb;
g_signal_connect(gb, "toggled", G_CALLBACK(assign_group_toggled), ac);
}
gtk_toggle_button_set_active(first, TRUE);
ac->pool = gtk_flow_box_new();
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(ac->pool), GTK_SELECTION_NONE);
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(ac->pool), 2);
gtk_widget_set_size_request(ac->pool, -1, 48);
gtk_box_append(GTK_BOX(content), ac->pool);
for (int g = 0; g < n_groups; g++) {
GtkWidget *panel = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
GtkWidget *gl = gtk_label_new(group_labels[g]);
GtkWidget *gf = gtk_flow_box_new();
gtk_widget_add_css_class(panel, "group-panel");
gtk_widget_set_halign(gl, GTK_ALIGN_START);
gtk_widget_add_css_class(gl, "group-panel-label");
gtk_box_append(GTK_BOX(panel), gl);
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(gf), GTK_SELECTION_NONE);
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(gf), 2);
gtk_widget_set_size_request(gf, -1, 48);
ac->group_flow[g] = gf;
gtk_box_append(GTK_BOX(panel), gf);
gtk_box_append(GTK_BOX(content), panel);
}
order = g_new0(int, n_items);
for (int i = 0; i < n_items; i++)
order[i] = i;
shuffle_indices(order, n_items);
for (int k = 0; k < n_items; k++) {
int i = order[k];
GString *lab = g_string_new(NULL);
GtkWidget *box;
GtkWidget *chip;
GtkWidget *m = NULL;
AssignChipRef *ref = g_new(AssignChipRef, 1);
if (items[i].emoji) {
g_string_append(lab, items[i].emoji);
g_string_append(lab, " ");
}
g_string_append(lab, items[i].label);
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
chip = gtk_button_new_with_label(lab->str);
g_string_free(lab, TRUE);
gtk_widget_add_css_class(chip, "chip");
gtk_widget_set_halign(chip, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(box), chip);
if (meanings[i]) {
m = gtk_label_new(NULL);
i18n_bind(m, meanings[i], 0);
gtk_label_set_xalign(GTK_LABEL(m), 0.5);
gtk_widget_set_halign(m, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(m, "meaning");
gtk_label_set_wrap(GTK_LABEL(m), TRUE);
gtk_label_set_max_width_chars(GTK_LABEL(m), 16);
gtk_widget_set_visible(m, FALSE);
gtk_box_append(GTK_BOX(box), m);
ac->trans[i] = m;
}
g_object_ref(box);
ac->items[i] = box;
ref->ac = ac;
ref->idx = i;
g_signal_connect(chip, "clicked", G_CALLBACK(assign_chip_clicked), ref);
gtk_flow_box_insert(GTK_FLOW_BOX(ac->pool), box, -1);
}
g_free(order);
g_signal_connect(check, "clicked", G_CALLBACK(assign_check), ac);
return page;
}