diff --git a/CMakeLists.txt b/CMakeLists.txt
index 802ed6b..2804da5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -44,10 +44,11 @@ endif()
# WIN32 suppresses the console window on Windows; the app still uses main(),
# so MSVC needs mainCRTStartup as the entry point.
+file(GLOB MATURITA_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.c")
if(WIN32)
- add_executable(maturita WIN32 maturita.c)
+ add_executable(maturita WIN32 ${MATURITA_SOURCES})
else()
- add_executable(maturita maturita.c)
+ add_executable(maturita ${MATURITA_SOURCES})
endif()
if(MSVC)
diff --git a/Makefile b/Makefile
index ca1d1f6..a816ed5 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,8 @@ else
TARGET = maturita
endif
-SRC = maturita.c
+SRC = $(wildcard *.c)
+OBJ = $(SRC:.c=.o)
all: $(TARGET)
@@ -33,7 +34,7 @@ $(TARGET): $(SRC)
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LIBS)
clean:
- rm -f $(TARGET) maturita.exe
+ rm -f $(TARGET) maturita.exe $(OBJ)
run: $(TARGET)
./$(TARGET)
diff --git a/README.md b/README.md
index d060d7b..15bdc6d 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,8 @@ An educational GTK 4 app written in C. / Vzdělávací GTK 4 aplikace napsaná v
maturita.c is an educational program for high-school and gymnasium students
preparing for their maturita exam. The UI is available in Czech and English and
-is built as a single C file (`maturita.c`) that renders a modern, CSS-styled
+is built from a set of small C modules (see
+[Project structure](#project-structure)) that render a modern, CSS-styled
GTK 4 interface.
### Screenshots
@@ -78,7 +79,7 @@ GTK 4 interface.
- **language**: Czech or English, applied instantly without a restart
- all three preferences are stored in `progress/settings.conf`
- **Keyboard quit shortcuts** – `Super/Cmd+Q` or `Alt+F4` closes the app
-- Single-file C codebase, cross-platform (Linux, macOS and Windows)
+- Modular C codebase split across several files, cross-platform (Linux, macOS and Windows)
### Installation
@@ -160,10 +161,10 @@ Or manually:
```bash
# Linux / macOS
-gcc -o maturita maturita.c $(pkg-config --cflags --libs gtk4) -lm
+gcc -o maturita *.c $(pkg-config --cflags --libs gtk4) -lm
# Windows (MSYS2 UCRT64)
-gcc -o maturita.exe maturita.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
+gcc -o maturita.exe *.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
```
##### Building with CMake (optional, Linux / macOS)
@@ -228,7 +229,7 @@ Follow these steps to compile and run on Windows.
```bash
cd /c/Users/Josef/maturita.c
- gcc -o maturita.exe maturita.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
+ gcc -o maturita.exe *.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
```
Path conversion examples:
@@ -299,7 +300,25 @@ saved per unit to `progress/unit1.conf` / `progress/unit2.conf` /
### Project structure
```
-maturita.c entire application (UI, navigation, themes, exercises)
+maturita.h shared types, macros, globals and prototypes
+globals.c shared global state (window, stack, units, theme, …)
+theme.c theme palettes, stylesheet injection and settings file
+i18n.c Czech/English translation tables and language switching
+icons.c Cairo-drawn vector icons (check, lock, wifi, gear, …)
+util.c small helpers (answer normalisation, feedback, umlaut note)
+progress.c per-unit exercise progress (load/save/completion)
+stats.c statistics page and progress aggregation
+navigation.c back button, top bar, welcome page, key shortcuts
+roadmap.c German learning-path roadmap
+subjects.c subjects map and subject catalogue
+unitmap.c per-unit exercise bubble map
+exercise_common.c shared exercise shell + combo/assembly/choice widgets
+exercise_u1.c unit 1 ("Neue Freunde") exercises
+exercise_u2.c unit 2 ("Aus aller Welt") exercises
+exercise_u3.c unit 3 ("Bei uns zu Hause") exercises
+settings.c settings popover (mode, theme, language)
+net.c computer-networks path, slides and practice sheet
+main.c application entry point and page wiring
style.css GTK stylesheet (palette colors are injected at runtime)
Makefile build & run targets (Linux, macOS, MSYS2); `make bundle` on Windows
CMakeLists.txt optional CMake build (Linux / macOS)
@@ -318,19 +337,19 @@ progress/ created at runtime
The app ships with ten palettes – Catppuccin, Nord, Dracula, Rose Pine, Ocean,
Gruvbox, Solarized, Everforest, Monokai and One Dark – each available in dark
-and light mode. Colors are defined as theme palettes in `maturita.c`, injected
+and light mode. Colors are defined as theme palettes in `theme.c`, injected
as `@define-color` bindings, and combined with the rules in the external
`style.css` stylesheet (plus matching Cairo colors for the roadmap rails and
icons).
Change the look from the in-app settings panel, or edit the palette tables in
-`maturita.c` / the rules in `style.css` and rebuild.
+`theme.c` / the rules in `style.css` and rebuild.
### Languages
The interface can be switched between Czech and English from the settings
panel; the choice takes effect immediately and is remembered between runs.
-Translations live in two tables in `maturita.c`: `tr_ui` holds interface
+Translations live in two tables in `i18n.c`: `tr_ui` holds interface
strings under short keys, while `tr_content` translates the Czech meanings
shown next to the German exercise items and uses the Czech text itself as the
key. Widgets are registered with `i18n_bind()` so `apply_language()` can
@@ -353,7 +372,7 @@ German in both languages.
- [ ] Add writing practice with feedback
- [x] Expand progress tracking with statistics
- [x] Move CSS styling to an external file
-- [ ] Split the UI code into multiple files
+- [x] Split the UI code into multiple files
- [x] Add internationalization support (Czech and English)
- [ ] Add more interface languages
- [ ] Add unit testing framework
@@ -376,8 +395,9 @@ details.
### Popis
maturita.c je vzdělávací program pro středoškoláky a gymnazisty připravující
-se na maturitu. Rozhraní je v češtině a angličtině a celá aplikace je v jednom
-C souboru (`maturita.c`) s moderním GTK 4 rozhraním stylovaným přes CSS.
+se na maturitu. Rozhraní je v češtině a angličtině a aplikace je rozdělena do
+několika malých C modulů (viz [Struktura projektu](#struktura-projektu)) s
+moderním GTK 4 rozhraním stylovaným přes CSS.
### Screenshoty
@@ -430,7 +450,7 @@ C souboru (`maturita.c`) s moderním GTK 4 rozhraním stylovaným přes CSS.
- **jazyk**: čeština nebo angličtina, bez restartu
- vše se ukládá do `progress/settings.conf`
- **Klávesové zkratky pro ukončení** – `Super/Cmd+Q` nebo `Alt+F4`
-- Jeden C soubor, multiplatformní (Linux, macOS i Windows)
+- Modulární C kód v několika souborech, multiplatformní (Linux, macOS i Windows)
### Instalace
@@ -513,10 +533,10 @@ Nebo ručně:
```bash
# Linux / macOS
-gcc -o maturita maturita.c $(pkg-config --cflags --libs gtk4) -lm
+gcc -o maturita *.c $(pkg-config --cflags --libs gtk4) -lm
# Windows (MSYS2 UCRT64)
-gcc -o maturita.exe maturita.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
+gcc -o maturita.exe *.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
```
##### Sestavení přes CMake (volitelné, Linux / macOS)
@@ -581,7 +601,7 @@ Následujte tyto kroky pro rychlé zkompilování a spuštění na Windows.
```bash
cd /c/Users/Josef/maturita.c
- gcc -o maturita.exe maturita.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
+ gcc -o maturita.exe *.c $(pkg-config --cflags --libs gtk4) -lm -mwindows
```
Příklady převodu cesty:
@@ -648,7 +668,25 @@ prvním dokončení).
### Struktura projektu
```
-maturita.c celá aplikace (UI, navigace, témata, cvičení)
+maturita.h sdílené typy, makra, globální proměnné a prototypy
+globals.c sdílený globální stav (okno, stack, jednotky, téma, …)
+theme.c palety témat, vkládání stylu a soubor nastavení
+i18n.c české/anglické překlady a přepínání jazyka
+icons.c vektorové ikony kreslené Cairem (fajfka, zámek, wifi, …)
+util.c pomocné funkce (normalizace odpovědí, feedback, poznámka)
+progress.c postup cvičení jednotek (načtení/uložení/dokončení)
+stats.c stránka statistik a agregace pokroku
+navigation.c tlačítko zpět, horní lišta, úvodní stránka, zkratky
+roadmap.c německá učební cesta
+subjects.c mapa předmětů a katalog předmětů
+unitmap.c mapa cvičení jednotlivých jednotek
+exercise_common.c společné jádro cvičení + combo/skládání/výběr
+exercise_u1.c cvičení jednotky 1 („Neue Freunde“)
+exercise_u2.c cvičení jednotky 2 („Aus aller Welt“)
+exercise_u3.c cvičení jednotky 3 („Bei uns zu Hause“)
+settings.c panel nastavení (režim, téma, jazyk)
+net.c cesta počítačových sítí, snímky a cvičení
+main.c vstupní bod aplikace a zapojení stránek
style.css GTK styl (barvy palety se vkládají za běhu)
Makefile build & run (Linux, macOS, MSYS2); `make bundle` na Windows
CMakeLists.txt volitelný CMake build (Linux / macOS)
@@ -667,17 +705,17 @@ progress/ vzniká za běhu
Aplikace má deset palet – Catppuccin, Nord, Dracula, Rose Pine, Ocean, Gruvbox,
Solarized, Everforest, Monokai a One Dark – každou ve světlém i tmavém režimu.
-Barvy jsou v `maturita.c`, vkládají se jako `@define-color` a spojují se s
+Barvy jsou v `theme.c`, vkládají se jako `@define-color` a spojují se s
pravidly v externím souboru `style.css` (a Cairo barvy pro koleje a ikony na
mapě).
-Vzhled změníte v nastavení, nebo upravte tabulky palet v `maturita.c` / pravidla
+Vzhled změníte v nastavení, nebo upravte tabulky palet v `theme.c` / pravidla
v `style.css` a znovu zkompilujte.
### Jazyky
Rozhraní přepnete mezi češtinou a angličtinou v nastavení; změna platí hned a
-zapamatuje se. Překlady jsou ve dvou tabulkách v `maturita.c`: `tr_ui` drží
+zapamatuje se. Překlady jsou ve dvou tabulkách v `i18n.c`: `tr_ui` drží
texty rozhraní pod krátkými klíči, `tr_content` překládá české významy u
německých cvičení (klíčem je samotný český text). Widgety se registrují přes
`i18n_bind()`, aby je `apply_language()` přeložila na místě.
@@ -699,7 +737,7 @@ rozhraní.
- [ ] Psaní s feedbackem
- [x] Statistiky postupu
- [x] Přesunout CSS do externího souboru
-- [ ] Rozdělit UI kód do více souborů
+- [x] Rozdělit UI kód do více souborů
- [x] Internacionalizace (čeština a angličtina)
- [ ] Další jazyky rozhraní
- [ ] Unit testy
diff --git a/exercise_common.c b/exercise_common.c
new file mode 100644
index 0000000..d3ddc7e
--- /dev/null
+++ b/exercise_common.c
@@ -0,0 +1,733 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Exercise page shell */
+/* ------------------------------------------------------------------ */
+
+GtkWidget *ex_page_shell(const char *back_target, const char *title,
+ const char *subtitle, const char *btn_label,
+ GtkWidget **body_out, GtkWidget **feedback_out,
+ GtkWidget **btn_out) {
+ GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ GtkWidget *scroll;
+ GtkWidget *body;
+ GtkWidget *bottom;
+ GtkWidget *feedback;
+ GtkWidget *btn;
+
+ gtk_widget_set_margin_start(page, 32);
+ gtk_widget_set_margin_end(page, 32);
+ gtk_widget_set_margin_top(page, 24);
+ gtk_widget_set_margin_bottom(page, 24);
+
+ gtk_box_append(GTK_BOX(page), top_bar(back_target, title, subtitle));
+
+ scroll = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_margin_top(scroll, 14);
+ gtk_widget_set_margin_bottom(scroll, 14);
+ gtk_box_append(GTK_BOX(page), scroll);
+
+ body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
+ gtk_widget_set_vexpand(body, TRUE);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
+ *body_out = body;
+
+ bottom = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
+ gtk_widget_set_margin_top(bottom, 4);
+ gtk_box_append(GTK_BOX(page), bottom);
+
+ feedback = gtk_label_new("");
+ gtk_widget_set_halign(feedback, GTK_ALIGN_START);
+ gtk_widget_set_valign(feedback, GTK_ALIGN_CENTER);
+ gtk_widget_set_hexpand(feedback, TRUE);
+ gtk_box_append(GTK_BOX(bottom), feedback);
+ *feedback_out = feedback;
+
+ btn = gtk_button_new();
+ i18n_bind(btn, btn_label, 1);
+ gtk_widget_add_css_class(btn, "btn-primary");
+ gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(bottom), btn);
+ *btn_out = btn;
+
+ return page;
+}
+
+/* ------------------------------------------------------------------ */
+/* Word-selection (combo box) exercise */
+/* ------------------------------------------------------------------ */
+
+
+/* Build a hidden translation label ("meaning") appended under an item. */
+GtkWidget *meaning_add(GtkWidget *body, const char *text) {
+ GtkWidget *lbl = gtk_label_new(NULL);
+
+ i18n_bind(lbl, text, 0);
+ gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
+ gtk_widget_set_halign(lbl, GTK_ALIGN_START);
+ gtk_widget_add_css_class(lbl, "meaning");
+ gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
+ gtk_widget_set_visible(lbl, FALSE);
+ gtk_widget_set_margin_top(lbl, 2);
+ gtk_widget_set_margin_bottom(lbl, 2);
+ gtk_widget_set_margin_start(lbl, 8);
+ gtk_box_append(GTK_BOX(body), lbl);
+ return lbl;
+}
+
+/* A hidden green label that shows the model German sentence after Check. */
+GtkWidget *model_answer_add(GtkWidget *body, const char *german) {
+ GtkWidget *lbl = gtk_label_new(german);
+
+ gtk_widget_set_halign(lbl, GTK_ALIGN_START);
+ gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
+ gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
+ gtk_widget_set_margin_top(lbl, 2);
+ gtk_widget_set_margin_bottom(lbl, 2);
+ gtk_widget_set_margin_start(lbl, 8);
+ gtk_widget_add_css_class(lbl, "u3-answer");
+ gtk_widget_set_visible(lbl, FALSE);
+ gtk_box_append(GTK_BOX(body), lbl);
+ return lbl;
+}
+
+void meaning_reveal_all(GtkWidget **labels, int n) {
+ for (int i = 0; i < n; i++)
+ if (labels[i])
+ gtk_widget_set_visible(labels[i], TRUE);
+}
+
+ComboListCtx *combo_list_ctx_new(UnitCtx *unit, int ex_num,
+ GtkWidget *feedback) {
+ ComboListCtx *ctx = g_new0(ComboListCtx, 1);
+ ctx->combos = g_array_new(FALSE, FALSE, sizeof(GtkComboBox *));
+ ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
+ ctx->trans = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ ctx->models = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ ctx->unit = unit;
+ ctx->ex_num = ex_num;
+ ctx->feedback = feedback;
+ return ctx;
+}
+
+void combo_list_add(ComboListCtx *ctx, GtkWidget *combo, const char *ans) {
+ g_array_append_val(ctx->combos, combo);
+ g_array_append_val(ctx->answers, ans);
+}
+
+void combo_list_add_trans(ComboListCtx *ctx, GtkWidget *label) {
+ g_array_append_val(ctx->trans, label);
+}
+
+GtkWidget *make_word_combo(const char **words, int n) {
+ GtkWidget *combo = gtk_combo_box_text_new();
+ int *order = g_new0(int, n);
+
+ for (int i = 0; i < n; i++)
+ order[i] = i;
+ shuffle_indices(order, n);
+ for (int i = 0; i < n; i++)
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo), NULL,
+ words[order[i]]);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(combo), -1);
+ g_free(order);
+ return combo;
+}
+
+gboolean answer_accepts(const char *sel_norm, const char *ans) {
+ gchar **parts = g_strsplit(ans, "|", -1);
+ gboolean ok = FALSE;
+
+ for (int i = 0; parts[i] != NULL; i++) {
+ gchar *a = normalize_answer(parts[i]);
+ if (g_strcmp0(sel_norm, a) == 0) {
+ ok = TRUE;
+ g_free(a);
+ break;
+ }
+ g_free(a);
+ }
+
+ g_strfreev(parts);
+ return ok;
+}
+
+void combo_list_check(GtkButton *button, gpointer data) {
+ ComboListCtx *ctx = data;
+ guint total = ctx->combos->len;
+ guint ok = 0;
+
+ (void)button;
+
+ for (guint i = 0; i < total; i++) {
+ GtkComboBox *combo = g_array_index(ctx->combos, GtkComboBox *, i);
+ const char *ans = g_array_index(ctx->answers, const char *, i);
+ gchar *sel = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(combo));
+ gboolean good = FALSE;
+
+ if (sel) {
+ gchar *t = normalize_answer(sel);
+ good = answer_accepts(t, ans);
+ g_free(t);
+ g_free(sel);
+ }
+ answer_mark(GTK_WIDGET(combo), good);
+ if (good)
+ ok++;
+ }
+
+ if (ctx->trans->len > 0) {
+ for (guint i = 0; i < ctx->trans->len; i++) {
+ GtkWidget *lbl = g_array_index(ctx->trans, GtkWidget *, i);
+ if (lbl)
+ gtk_widget_set_visible(lbl, TRUE);
+ }
+ }
+
+ if (ctx->models->len > 0) {
+ for (guint i = 0; i < ctx->models->len; i++) {
+ GtkWidget *lbl = g_array_index(ctx->models, GtkWidget *, i);
+ if (lbl)
+ gtk_widget_set_visible(lbl, TRUE);
+ }
+ }
+
+ if (ok == total) {
+ set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
+ mark_done(ctx->unit, ctx->ex_num);
+ } else {
+ set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Word-arrangement (chips) exercise */
+/* ------------------------------------------------------------------ */
+
+
+
+void sent_rebuild(SentBuilder *sb) {
+ for (int i = 0; i < sb->n_words; i++) {
+ GtkWidget *parent = gtk_widget_get_parent(sb->chips[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(sb->pool));
+ flow_clear(GTK_FLOW_BOX(sb->target));
+
+ for (int i = 0; i < sb->placed_count; i++)
+ gtk_flow_box_insert(GTK_FLOW_BOX(sb->target),
+ sb->chips[sb->placed[i]], -1);
+
+ for (int i = 0; i < sb->n_words; i++) {
+ int w = sb->pool_order[i];
+ gboolean in_target = FALSE;
+ for (int j = 0; j < sb->placed_count; j++) {
+ if (sb->placed[j] == w) {
+ in_target = TRUE;
+ break;
+ }
+ }
+ if (!in_target)
+ gtk_flow_box_insert(GTK_FLOW_BOX(sb->pool), sb->chips[w], -1);
+ }
+}
+
+
+GdkContentProvider *asm_drag_prepare(GtkDragSource *source,
+ double x, double y,
+ gpointer data) {
+ ChipRef *ref = data;
+ (void)source;
+ (void)x;
+ (void)y;
+ return gdk_content_provider_new_typed(G_TYPE_INT, ref->idx);
+}
+
+gboolean asm_rebuild_idle(gpointer data) {
+ sent_rebuild(data);
+ return G_SOURCE_REMOVE;
+}
+
+void asm_drag_end(GtkDragSource *source, GdkDrag *drag,
+ gboolean delete_data, gpointer data) {
+ ChipRef *ref = data;
+ (void)source;
+ (void)drag;
+ (void)delete_data;
+ g_idle_add(asm_rebuild_idle, ref->sb);
+}
+
+gboolean asm_drop_to_sentence(GtkDropTarget *target, const GValue *value,
+ double x, double y, gpointer data) {
+ SentBuilder *sb = data;
+ int idx;
+ int pos = -1;
+
+ (void)target;
+ (void)x;
+ (void)y;
+
+ if (!G_VALUE_HOLDS_INT(value))
+ return FALSE;
+
+ idx = g_value_get_int(value);
+ for (int i = 0; i < sb->placed_count; i++) {
+ if (sb->placed[i] == idx) {
+ pos = i;
+ break;
+ }
+ }
+ if (pos >= 0) {
+ for (int i = pos; i < sb->placed_count - 1; i++)
+ sb->placed[i] = sb->placed[i + 1];
+ sb->placed_count--;
+ }
+ sb->placed[sb->placed_count++] = idx;
+ return TRUE;
+}
+
+gboolean asm_drop_to_pool(GtkDropTarget *target, const GValue *value,
+ double x, double y, gpointer data) {
+ SentBuilder *sb = data;
+ int idx;
+ int pos = -1;
+
+ (void)target;
+ (void)x;
+ (void)y;
+
+ if (!G_VALUE_HOLDS_INT(value))
+ return FALSE;
+
+ idx = g_value_get_int(value);
+ for (int i = 0; i < sb->placed_count; i++) {
+ if (sb->placed[i] == idx) {
+ pos = i;
+ break;
+ }
+ }
+ if (pos < 0)
+ return FALSE;
+
+ for (int i = pos; i < sb->placed_count - 1; i++)
+ sb->placed[i] = sb->placed[i + 1];
+ sb->placed_count--;
+ return TRUE;
+}
+
+/* ------------------------------------------------------------------ */
+/* Click-to-move with a fly ("glide") animation */
+/* ------------------------------------------------------------------ */
+
+
+void asm_finish_fly(FlyCtx *fc) {
+ gtk_widget_remove_css_class(fc->chip, "ghost-host");
+ gtk_fixed_remove(GTK_FIXED(fc->stage), fc->ghost);
+ if (fc->flying)
+ *fc->flying = FALSE;
+ g_free(fc);
+}
+
+gboolean asm_fly_tick(gpointer data) {
+ FlyCtx *fc = data;
+ gint64 now = g_get_monotonic_time() / 1000;
+
+ if (fc->t0 < 0) {
+ double nx = 0.0, ny = 0.0;
+ gboolean ok;
+
+ fc->wait++;
+ ok = gtk_widget_translate_coordinates(fc->chip, fc->stage,
+ 0, 0, &nx, &ny);
+ if (fc->wait < 2)
+ return G_SOURCE_CONTINUE;
+
+ if (ok && fc->wait < 14 &&
+ fabs(nx - fc->sx) + fabs(ny - fc->sy) < 0.5)
+ return G_SOURCE_CONTINUE; /* still waiting for relayout */
+
+ if (ok) {
+ fc->tx = nx;
+ fc->ty = ny;
+ } else {
+ fc->tx = fc->sx;
+ fc->ty = fc->sy;
+ }
+ fc->t0 = now;
+ return G_SOURCE_CONTINUE;
+ }
+
+ {
+ double t = (double)(now - fc->t0) / 240.0;
+ double e, x, y;
+
+ if (t > 1.0)
+ t = 1.0;
+ e = 1.0 - pow(1.0 - t, 3.0); /* ease-out cubic */
+ x = fc->sx + (fc->tx - fc->sx) * e;
+ y = fc->sy + (fc->ty - fc->sy) * e;
+ gtk_fixed_move(GTK_FIXED(fc->stage), fc->ghost, (int)x, (int)y);
+
+ if (t >= 1.0) {
+ asm_finish_fly(fc);
+ return G_SOURCE_REMOVE;
+ }
+ }
+ return G_SOURCE_CONTINUE;
+}
+
+/* Start a fly animation: hide `chip` in place and animate a ghost from
+ * (sx, sy) to the chip's new location. */
+void asm_begin_fly(GtkWidget *chip, GtkWidget *stage,
+ gboolean *flying, double sx, double sy) {
+ GtkWidget *child = gtk_button_get_child(GTK_BUTTON(chip));
+ const char *txt = (child && GTK_IS_LABEL(child))
+ ? gtk_label_get_text(GTK_LABEL(child)) : NULL;
+ GtkWidget *ghost = gtk_button_new_with_label(txt ? txt : "");
+ FlyCtx *fc;
+
+ gtk_widget_add_css_class(ghost, "chip");
+ gtk_widget_set_sensitive(ghost, FALSE);
+ gtk_widget_set_can_focus(ghost, FALSE);
+ gtk_fixed_put(GTK_FIXED(stage), ghost, (int)sx, (int)sy);
+
+ fc = g_new0(FlyCtx, 1);
+ fc->chip = chip;
+ fc->ghost = ghost;
+ fc->stage = stage;
+ fc->flying = flying;
+ fc->sx = sx;
+ fc->sy = sy;
+ fc->t0 = -1;
+
+ if (flying)
+ *flying = TRUE;
+ gtk_widget_add_css_class(chip, "ghost-host");
+
+ g_timeout_add(16, asm_fly_tick, fc);
+}
+
+void asm_move_by_click(GtkButton *button, gpointer data) {
+ ChipRef *ref = data;
+ SentBuilder *sb = ref->sb;
+ int idx = ref->idx;
+ int pos = -1;
+ double sx = 0.0, sy = 0.0;
+ GtkWidget *chip = GTK_WIDGET(button);
+
+ (void)button;
+
+ if (sb->flying)
+ return;
+
+ for (int i = 0; i < sb->placed_count; i++) {
+ if (sb->placed[i] == idx) {
+ pos = i;
+ break;
+ }
+ }
+
+ if (sb->stage &&
+ gtk_widget_translate_coordinates(chip, sb->stage, 0, 0, &sx, &sy)) {
+ if (pos >= 0) {
+ for (int i = pos; i < sb->placed_count - 1; i++)
+ sb->placed[i] = sb->placed[i + 1];
+ sb->placed_count--;
+ } else {
+ sb->placed[sb->placed_count++] = idx;
+ }
+
+ sent_rebuild(sb);
+ asm_begin_fly(chip, sb->stage, &sb->flying, sx, sy);
+ } else {
+ /* No usable animation layer yet – just move instantly. */
+ if (pos >= 0) {
+ for (int i = pos; i < sb->placed_count - 1; i++)
+ sb->placed[i] = sb->placed[i + 1];
+ sb->placed_count--;
+ } else {
+ sb->placed[sb->placed_count++] = idx;
+ }
+ sent_rebuild(sb);
+ }
+}
+
+
+void assembly_check(GtkButton *button, gpointer data) {
+ AsmCtx *ctx = data;
+ int ok = 0;
+
+ (void)button;
+
+ for (int i = 0; i < ctx->n; i++) {
+ SentBuilder *sb = ctx->sbs[i];
+ gboolean good = (sb->placed_count == sb->n_words);
+ if (good) {
+ for (int j = 0; j < sb->placed_count; j++) {
+ if (sb->placed[j] != j) {
+ good = FALSE;
+ break;
+ }
+ }
+ }
+ if (good) {
+ ok++;
+ gtk_widget_remove_css_class(sb->group, "wrong");
+ gtk_widget_add_css_class(sb->group, "ok");
+ } else {
+ gtk_widget_remove_css_class(sb->group, "ok");
+ gtk_widget_add_css_class(sb->group, "wrong");
+ }
+ }
+
+ for (int i = 0; i < ctx->n; i++) {
+ if (ctx->sbs[i]->trans)
+ gtk_widget_set_visible(ctx->sbs[i]->trans, TRUE);
+ }
+
+ 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_sentences"));
+ }
+}
+
+GtkWidget *build_assembly(UnitCtx *unit, const char *title,
+ const char *subtitle, int ex_num,
+ const AssemblyItem *items,
+ const char **meanings, int n) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, title, subtitle, "check",
+ &body, &feedback, &check);
+ AsmCtx *ctx = g_new0(AsmCtx, 1);
+
+ ctx->sbs = g_new0(SentBuilder *, n);
+ ctx->n = n;
+ ctx->unit = unit;
+ ctx->ex_num = ex_num;
+ ctx->feedback = feedback;
+
+ for (int s = 0; s < n; s++) {
+ const AssemblyItem *item = &items[s];
+ SentBuilder *sb = g_new0(SentBuilder, 1);
+ GtkWidget *holder;
+ GtkWidget *group;
+ GtkWidget *target;
+ GtkWidget *pool;
+ GtkWidget *stage;
+
+ sb->n_words = item->n;
+ ctx->sbs[s] = sb;
+
+ if (item->prompt) {
+ GtkWidget *prompt = gtk_label_new(item->prompt);
+ gtk_widget_set_halign(prompt, GTK_ALIGN_START);
+ gtk_widget_add_css_class(prompt, "chain");
+ gtk_box_append(GTK_BOX(body), prompt);
+ }
+
+ holder = gtk_overlay_new();
+ gtk_widget_set_hexpand(holder, TRUE);
+
+ group = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
+ gtk_widget_add_css_class(group, "sent-group");
+ sb->group = group;
+ gtk_overlay_set_child(GTK_OVERLAY(holder), group);
+
+ target = gtk_flow_box_new();
+ gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(target), GTK_SELECTION_NONE);
+ gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(target), 2);
+ gtk_widget_set_size_request(target, -1, 44);
+ sb->target = target;
+ gtk_box_append(GTK_BOX(group), target);
+
+ pool = gtk_flow_box_new();
+ gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(pool), GTK_SELECTION_NONE);
+ gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(pool), 2);
+ gtk_widget_set_size_request(pool, -1, 44);
+ sb->pool = pool;
+ gtk_box_append(GTK_BOX(group), pool);
+
+ 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);
+ sb->stage = stage;
+
+ {
+ GtkDropTarget *dt = gtk_drop_target_new(G_TYPE_INT, GDK_ACTION_COPY);
+ GtkDropTarget *dp = gtk_drop_target_new(G_TYPE_INT, GDK_ACTION_COPY);
+ g_signal_connect(dt, "drop", G_CALLBACK(asm_drop_to_sentence), sb);
+ gtk_widget_add_controller(target, GTK_EVENT_CONTROLLER(dt));
+ g_signal_connect(dp, "drop", G_CALLBACK(asm_drop_to_pool), sb);
+ gtk_widget_add_controller(pool, GTK_EVENT_CONTROLLER(dp));
+ }
+
+ for (int w = 0; w < item->n; w++) {
+ GtkWidget *chip = gtk_button_new_with_label(item->words[w]);
+ GtkDragSource *src = gtk_drag_source_new();
+ ChipRef *ref = g_new(ChipRef, 1);
+ gtk_widget_add_css_class(chip, "chip");
+ g_object_ref(chip);
+ sb->chips[w] = chip;
+ ref->sb = sb;
+ ref->idx = w;
+ gtk_drag_source_set_actions(src, GDK_ACTION_COPY);
+ g_signal_connect(src, "prepare", G_CALLBACK(asm_drag_prepare), ref);
+ g_signal_connect(src, "drag-end", G_CALLBACK(asm_drag_end), ref);
+ g_signal_connect(chip, "clicked", G_CALLBACK(asm_move_by_click), ref);
+ gtk_widget_add_controller(chip, GTK_EVENT_CONTROLLER(src));
+ }
+
+ {
+ for (int w = 0; w < item->n; w++)
+ sb->pool_order[w] = w;
+ shuffle_indices(sb->pool_order, item->n);
+ for (int w = 0; w < item->n; w++)
+ gtk_flow_box_insert(GTK_FLOW_BOX(pool),
+ sb->chips[sb->pool_order[w]], -1);
+ }
+
+ gtk_box_append(GTK_BOX(body), holder);
+
+ if (meanings[s])
+ sb->trans = meaning_add(body, meanings[s]);
+ }
+
+ g_signal_connect(check, "clicked", G_CALLBACK(assembly_check), ctx);
+ return page;
+}
+
+/* ------------------------------------------------------------------ */
+/* Exercise 1: Dialog (word selection) */
+/* ------------------------------------------------------------------ */
+
+
+
+const DialogRow ex1_d1[] = {
+ {"A:", "Hallo! Ich ", "bin|heiße", " Anna."},
+ {"B:", "Hallo! Ich ", "bin|heiße", " Peter."},
+ {"A:", "Woher ", "kommst", " du?"},
+ {"B:", "Ich komme ", "aus", " Tschechien."},
+};
+
+const DialogRow ex1_d2[] = {
+ {"A:", "Guten ", "Morgen", "!"},
+ {"B:", "Wie ", "geht", " es dir?"},
+ {"A:", "Mir geht es gut, ", "danke", "."},
+};
+
+const DialogRow ex1_d3[] = {
+ {"A:", "Ich muss gehen. Auf ", "Wiedersehen", "!"},
+ {"B:", "", "Bis", " bald!"},
+ {"A:", "", "Tschüss", "!"},
+};
+
+const Dialogue ex1_dialogues[] = {
+ {"Sich vorstellen", ex1_d1, G_N_ELEMENTS(ex1_d1)},
+ {"Begrüßung", ex1_d2, G_N_ELEMENTS(ex1_d2)},
+ {"Verabschiedung", ex1_d3, G_N_ELEMENTS(ex1_d3)},
+};
+
+const char *ex1_pool[] = {
+ "bin", "heiße", "kommst", "aus", "Morgen",
+ "geht", "danke", "Wiedersehen", "Bis", "Tschüss",
+};
+
+/* One Czech meaning per dialog line, in the same order as the rows. */
+const char *ex1_meaning[] = {
+ "Ahoj! Jmenuji se Anna.",
+ "Ahoj! Jmenuji se Petr.",
+ "Odkud jsi?",
+ "Pocházím z Česka.",
+ "Dobré ráno!",
+ "Jak se máš?",
+ "Mám se dobře, děkuji.",
+ "Musím jít. Na shledanou!",
+ "Brzy na viděnou!",
+ "Čau!",
+};
+
+GtkWidget *build_ex1(UnitCtx *unit) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, "Dialog",
+ "sub_dialog",
+ "check", &body, &feedback, &check);
+ ComboListCtx *ctx = combo_list_ctx_new(unit, 1, feedback);
+ int ri = 0;
+
+ for (guint d = 0; d < G_N_ELEMENTS(ex1_dialogues); d++) {
+ const Dialogue *diag = &ex1_dialogues[d];
+ GtkWidget *name = gtk_label_new(diag->name);
+ gtk_widget_set_halign(name, GTK_ALIGN_START);
+ gtk_widget_add_css_class(name, "ex-sub");
+ gtk_widget_set_margin_top(name, d > 0 ? 10 : 0);
+ gtk_box_append(GTK_BOX(body), name);
+
+ for (int r = 0; r < diag->n; r++) {
+ const DialogRow *row = &diag->rows[r];
+ GtkWidget *line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ GtkWidget *combo;
+ gtk_widget_set_halign(line, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), line);
+
+ if (row->speaker && row->speaker[0]) {
+ GtkWidget *spk = gtk_label_new(row->speaker);
+ gtk_widget_add_css_class(spk, "dlg-speaker");
+ gtk_widget_set_valign(spk, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(line), spk);
+ }
+ if (row->before && row->before[0]) {
+ GtkWidget *lb = gtk_label_new(row->before);
+ gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(lb, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), lb);
+ }
+
+ combo = make_word_combo(ex1_pool, G_N_ELEMENTS(ex1_pool));
+ gtk_widget_set_size_request(combo, 130, -1);
+ gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(line), combo);
+ combo_list_add(ctx, combo, row->answer);
+
+ if (row->after && row->after[0]) {
+ GtkWidget *la = gtk_label_new(row->after);
+ gtk_widget_set_valign(la, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(la, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), la);
+ }
+
+ if (ri < (int)G_N_ELEMENTS(ex1_meaning))
+ combo_list_add_trans(ctx, meaning_add(body, ex1_meaning[ri]));
+ ri++;
+ }
+ }
+
+ g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
+ return page;
+}
+
+/* ------------------------------------------------------------------ */
+/* Exercise 2: Sätze bilden (word arrangement) */
+/* ------------------------------------------------------------------ */
+
+const AssemblyItem ex2_items[] = {
+ {NULL, {"Ich", "heiße", "Anna", "."}, 4},
+ {NULL, {"Woher", "kommst", "du", "?"}, 4},
+ {NULL, {"Ich", "komme", "aus", "Tschechien", "."}, 5},
+ {NULL, {"Wie", "geht", "es", "dir", "?"}, 5},
+};
+
+const char *ex2_meaning[] = {
+ "Jmenuji se Anna.",
+ "Odkud jsi?",
+ "Pocházím z Česka.",
+ "Jak se máš?",
+};
diff --git a/exercise_u1.c b/exercise_u1.c
new file mode 100644
index 0000000..dfd906a
--- /dev/null
+++ b/exercise_u1.c
@@ -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;
+}
diff --git a/exercise_u2.c b/exercise_u2.c
new file mode 100644
index 0000000..fec2008
--- /dev/null
+++ b/exercise_u2.c
@@ -0,0 +1,1307 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Unit 2 exercises ("Aus aller Welt") */
+/* ------------------------------------------------------------------ */
+
+/* ---- generic word-row helper (single blank, shared word bank) ----- */
+
+void verb_rows_add(ComboListCtx *ctx, GtkWidget *body, int *counter,
+ const VerbQ *qs, int n,
+ const char **pool, int pool_n,
+ const char **meanings, gboolean pre_meaning) {
+ for (int i = 0; i < n; i++) {
+ GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ GtkWidget *before = gtk_label_new(qs[i].before);
+ GtkWidget *after = gtk_label_new(qs[i].after);
+ GtkWidget *combo;
+ char *numtxt;
+
+ (*counter)++;
+ numtxt = g_strdup_printf("%d. ", *counter);
+ GtkWidget *num = gtk_label_new(numtxt);
+ g_free(numtxt);
+
+ gtk_widget_set_halign(row, GTK_ALIGN_START);
+ gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(num, "ex-prompt");
+ gtk_box_append(GTK_BOX(row), num);
+
+ 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(pool, pool_n);
+ gtk_widget_set_size_request(combo, 150, -1);
+ gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(row), combo);
+ combo_list_add(ctx, combo, qs[i].answer);
+
+ if (qs[i].after && qs[i].after[0]) {
+ 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);
+
+ if (meanings && meanings[i]) {
+ GtkWidget *mean = meaning_add(body, meanings[i]);
+ if (pre_meaning || ctx->trans_upfront)
+ gtk_widget_set_visible(mean, TRUE);
+ combo_list_add_trans(ctx, mean);
+ }
+
+ if (ctx->reveal_german) {
+ char *sentence = verbq_german(qs[i].before, qs[i].answer,
+ qs[i].after);
+
+ if (sentence && sentence[0]) {
+ GtkWidget *w = model_answer_add(body, sentence);
+
+ g_array_append_val(ctx->models, w);
+ }
+ g_free(sentence);
+ }
+ }
+}
+
+
+void verb_note_reveal(VerbNoteCtx *nc) {
+ GString *s;
+ int i;
+
+ if (!nc->note || !nc->gloss)
+ return;
+
+ s = g_string_new(NULL);
+ for (i = 0; i < nc->n; i++) {
+ if (i > 0)
+ g_string_append_c(s, '\n');
+ g_string_append_printf(s, "%s – %s", nc->qs[i].answer,
+ tr(nc->gloss[i]));
+ }
+ gtk_label_set_text(GTK_LABEL(nc->note), s->str);
+ g_string_free(s, TRUE);
+ gtk_widget_set_visible(nc->note, TRUE);
+}
+
+void verb_ex_check(GtkButton *button, gpointer data) {
+ VerbNoteCtx *nc = data;
+
+ combo_list_check(button, nc->ctx);
+ verb_note_reveal(nc);
+}
+
+GtkWidget *build_verb_ex(UnitCtx *unit, int ex_num,
+ const char *title, const char *sub,
+ const VerbQ *qs, int n,
+ const char **pool, int pool_n,
+ const char **meanings, gboolean pre_meaning,
+ const char **gloss) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
+ &body, &feedback, &check);
+ ComboListCtx *ctx = combo_list_ctx_new(unit, ex_num, feedback);
+ VerbNoteCtx *nc = NULL;
+ int counter = 0;
+
+ ctx->trans_upfront = TRUE;
+ ctx->reveal_german = TRUE;
+
+ verb_rows_add(ctx, body, &counter, qs, n, pool, pool_n, meanings,
+ pre_meaning);
+
+ if (gloss) {
+ GtkWidget *note = gtk_label_new(NULL);
+ gtk_widget_set_halign(note, GTK_ALIGN_START);
+ gtk_widget_set_margin_top(note, 6);
+ gtk_widget_add_css_class(note, "meaning");
+ gtk_label_set_wrap(GTK_LABEL(note), TRUE);
+ gtk_widget_set_visible(note, FALSE);
+ gtk_box_append(GTK_BOX(body), note);
+
+ nc = g_new0(VerbNoteCtx, 1);
+ nc->ctx = ctx;
+ nc->note = note;
+ nc->qs = qs;
+ nc->n = n;
+ nc->gloss = gloss;
+ g_signal_connect(check, "clicked", G_CALLBACK(verb_ex_check), nc);
+ } else {
+ g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
+ }
+
+ return page;
+}
+
+/* ---- typed-entry builder with auto-check --------------------------- */
+
+
+
+void typed_check(GtkButton *button, gpointer data) {
+ TypedCtx *ctx = data;
+ int ok = 0;
+
+ (void)button;
+
+ for (int i = 0; i < ctx->n; i++) {
+ const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(ctx->entries[i]));
+ gchar *norm = normalize_answer(txt);
+ gboolean good = txt && txt[0] && answer_accepts(norm, ctx->qs[i].answers);
+ g_free(norm);
+ answer_mark(ctx->entries[i], good);
+ if (good)
+ ok++;
+ }
+
+ if (ctx->trans) {
+ for (int i = 0; i < ctx->n; i++)
+ if (ctx->trans[i])
+ gtk_widget_set_visible(ctx->trans[i], TRUE);
+ }
+
+ 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"));
+ }
+}
+
+
+/* Capitalise the first (canonical) answer alternative for display. */
+char *typed_answer_display(const char *answers) {
+ const char *pipe = strchr(answers, '|');
+ char *base = pipe ? g_strndup(answers, (gsize)(pipe - answers))
+ : g_strdup(answers);
+
+ if (base[0]) {
+ gunichar ch = g_utf8_get_char(base);
+ gunichar up = g_unichar_toupper(ch);
+ if (up != ch) {
+ char *first = g_ucs4_to_utf8(&up, 1, NULL, NULL, NULL);
+ char *res = g_strconcat(first, g_utf8_next_char(base), NULL);
+ g_free(first);
+ g_free(base);
+ return res;
+ }
+ }
+ return base;
+}
+
+void typed_note_reveal(TypedNoteCtx *nc) {
+ GString *s;
+ int i;
+
+ if (!nc->note)
+ return;
+
+ s = g_string_new(NULL);
+ for (i = 0; i < nc->n; i++) {
+ char *word = typed_answer_display(nc->qs[i].answers);
+ if (i > 0)
+ g_string_append_c(s, '\n');
+ if (nc->qs[i].meaning)
+ g_string_append_printf(s, "%s – %s", word, tr(nc->qs[i].meaning));
+ else
+ g_string_append(s, word);
+ g_free(word);
+ }
+ gtk_label_set_text(GTK_LABEL(nc->note), s->str);
+ g_string_free(s, TRUE);
+ gtk_widget_set_visible(nc->note, TRUE);
+}
+
+void typed_ex_check(GtkButton *button, gpointer data) {
+ TypedNoteCtx *nc = data;
+
+ typed_check(button, nc->ctx);
+ typed_note_reveal(nc);
+}
+
+GtkWidget *build_typed(UnitCtx *unit, int ex_num,
+ const char *title, const char *sub,
+ const TypedQ *qs, int n,
+ const char *word_bank, gboolean answers_note) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
+ &body, &feedback, &check);
+ TypedCtx *ctx = g_new0(TypedCtx, 1);
+ TypedNoteCtx *nc = NULL;
+
+ ctx->qs = qs;
+ ctx->n = n;
+ ctx->unit = unit;
+ ctx->ex_num = ex_num;
+ ctx->feedback = feedback;
+ ctx->entries = g_new0(GtkWidget *, n);
+ ctx->trans = g_new0(GtkWidget *, n);
+
+ {
+ gboolean need = FALSE;
+
+ for (int i = 0; i < n; i++) {
+ if (text_has_german_umlaut(qs[i].prompt) ||
+ text_has_german_umlaut(qs[i].answers)) {
+ need = TRUE;
+ break;
+ }
+ }
+ if (need)
+ add_umlaut_note(body);
+ }
+
+ if (word_bank) {
+ GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ GtkWidget *hdr = gtk_label_new(NULL);
+ GtkWidget *words = gtk_label_new(word_bank);
+
+ gtk_widget_set_halign(row, GTK_ALIGN_START);
+ i18n_bind(hdr, "wordbank", 0);
+ gtk_widget_add_css_class(hdr, "hint");
+ gtk_widget_add_css_class(words, "hint");
+ gtk_box_append(GTK_BOX(row), hdr);
+ gtk_box_append(GTK_BOX(row), words);
+ gtk_box_append(GTK_BOX(body), row);
+ }
+
+ for (int i = 0; i < n; i++) {
+ GtkWidget *prompt = gtk_label_new(qs[i].prompt);
+ GtkWidget *entry;
+
+ gtk_widget_set_halign(prompt, GTK_ALIGN_START);
+ gtk_widget_add_css_class(prompt, "ex-prompt");
+ gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
+ gtk_box_append(GTK_BOX(body), prompt);
+
+ entry = gtk_entry_new();
+ gtk_widget_set_hexpand(entry, FALSE);
+ gtk_widget_set_halign(entry, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), entry);
+ ctx->entries[i] = entry;
+
+ if (qs[i].meaning)
+ ctx->trans[i] = meaning_add(body, qs[i].meaning);
+ }
+
+ if (answers_note) {
+ GtkWidget *note = gtk_label_new(NULL);
+ gtk_widget_set_halign(note, GTK_ALIGN_START);
+ gtk_widget_set_margin_top(note, 6);
+ gtk_widget_add_css_class(note, "meaning");
+ gtk_label_set_wrap(GTK_LABEL(note), TRUE);
+ gtk_widget_set_visible(note, FALSE);
+ gtk_box_append(GTK_BOX(body), note);
+
+ nc = g_new0(TypedNoteCtx, 1);
+ nc->ctx = ctx;
+ nc->note = note;
+ nc->qs = qs;
+ nc->n = n;
+ g_signal_connect(check, "clicked", G_CALLBACK(typed_ex_check), nc);
+ } else {
+ g_signal_connect(check, "clicked", G_CALLBACK(typed_check), ctx);
+ }
+
+ return page;
+}
+
+/* ---- G05/G06/G07/G11/S03 typed content ----------------------------- */
+
+const char *g05_bank =
+ "Tscheche, Tschechin, Spanier, Spanierin, Türke, Türkin, Kroate, "
+ "Kroatin, Deutscher, Deutsche, Österreicher, Österreicherin, "
+ "Slowake, Slowakin";
+
+const TypedQ g05_rows[] = {
+ {"Er kommt aus Tschechien. Er ist _________.", "tscheche", "Čech"},
+ {"Sie kommt aus Spanien. Sie ist _________.", "spanierin", "Španělka"},
+ {"Er kommt aus der Türkei. Er ist _________.", "türke|turke", "Turek"},
+ {"Sie kommt aus Kroatien. Sie ist _________.", "kroatin", "Chorvatka"},
+ {"Sie kommt aus Deutschland. Sie ist _________.", "deutsche", "Němka"},
+ {"Sie kommt aus Österreich. Sie ist _________.", "österreicherin|osterreicherin",
+ "Rakušanka"},
+ {"Er kommt aus der Slowakei. Er ist _________.", "slowake", "Slovák"},
+};
+
+const TypedQ g06_rows[] = {
+ {"Ich heiße Thomas. Ich spreche Deutsch. Ich komme aus D________.",
+ "deutschland|eutschland", "Deutschland – Německo"},
+ {"Ich heiße Martina. Ich spreche Slowakisch. Ich komme aus der S________.",
+ "slowakei|lowakei", "Slowakei – Slovensko"},
+ {"Ich heiße Petra. Ich spreche Deutsch. Ich komme aus Ö________.",
+ "österreich|osterreich|sterreich", "Österreich – Rakousko"},
+ {"Ich heiße Miguel. Ich spreche Spanisch. Ich komme aus S________.",
+ "spanien|panien", "Spanien – Španělsko"},
+ {"Ich heiße George. Ich spreche Englisch. Ich komme aus E________.",
+ "england|ngland", "England – Anglie"},
+ {"Ich heiße Maria. Ich spreche Polnisch. Ich komme aus P________.",
+ "polen|olen", "Polen – Polsko"},
+};
+
+const TypedQ g07_rows[] = {
+ {"1. Ru__land", "russland|ss", "Russland – Rusko"},
+ {"2. T_rkei", "türkei|turkei|ü|u", "Türkei – Turecko"},
+ {"3. Slo__kei", "slowakei|wa", "Slowakei – Slovensko"},
+ {"4. Kro__ien", "kroatien|at", "Kroatien – Chorvatsko"},
+ {"5. Deut___land", "deutschland|sch", "Deutschland – Německo"},
+ {"6. Öst__reich", "österreich|osterreich|er", "Österreich – Rakousko"},
+ {"7. Gr__chenland", "griechenland|ie", "Griechenland – Řecko"},
+ {"8. Ita__en", "italien|li", "Italien – Itálie"},
+};
+
+const TypedQ g11_rows[] = {
+ {"tergehvatereikauwauzeh", "vater", "otec"},
+ {"berelternnemrerhobschaz", "eltern", "rodiče"},
+ {"nefrastschechinreiabtab", "tschechin", "Češka"},
+ {"hejkosstudentaldwsahujoi", "student", "student"},
+};
+
+const TypedQ s03_rows[] = {
+ {"1. 🇨🇿 Bist du Russe? – Nein, ich bin _________.", "tscheche",
+ "Čech"},
+ {"2. 🇦🇹 Bist du Tschechin? – Nein, ich bin _________.", "österreicherin|osterreicherin",
+ "Rakušanka"},
+ {"3. 🇨🇭 Bist du Deutsche? – Nein, ich bin _________.", "schweizerin",
+ "Švýcarka"},
+ {"4. 🇩🇪 Bist du Engländer? – Nein, ich bin _________.", "deutscher",
+ "Němec"},
+ {"5. 🇨🇭 Bist du Amerikanerin? – Nein, ich bin _________.", "schweizerin",
+ "Švýcarka"},
+};
+
+/* ---- G01 content ---------------------------------------------------- */
+
+const VerbQ g01_peter[] = {
+ {"Er ", " aus Österreich. (kommen)", "kommt"},
+ {"Er ", " in Graz. (wohnen)", "wohnt"},
+ {"Er ", " gern Golf. (spielen)", "spielt"},
+ {"Er ", " Deutsch, Englisch und Italienisch. (sprechen)", "spricht"},
+};
+
+const VerbQ g01_jana[] = {
+ {"Sie ", " aus der Slowakei. (kommen)", "kommen"},
+ {"Sie ", " in Bratislava. (wohnen)", "wohnen"},
+ {"Sie ", " gern. (tanzen)", "tanzt"},
+ {"Er ", " gern. (reisen)", "reist"},
+ {"Sie ", " gute Freunde. (sein)", "sind"},
+};
+
+const char *g01_pool[] = {
+ "kommt", "kommst", "komme", "kommen",
+ "wohnt", "wohnst", "wohne", "wohnen",
+ "spielt", "spielst", "spiele", "spielen",
+ "spricht", "sprichst", "spreche", "sprechen",
+ "tanzt", "tanze", "tanzen",
+ "reist", "reise", "reisen",
+ "ist", "bist", "bin", "sind", "seid",
+};
+
+const char *g01_peter_mean[] = {
+ "Pochází z Rakouska.",
+ "Bydlí ve Štýrském Hradci (Graz).",
+ "Rád hraje golf.",
+ "Mluví německy, anglicky a italsky.",
+};
+
+const char *g01_jana_mean[] = {
+ "Pocházejí ze Slovenska.",
+ "Bydlí v Bratislavě (ona).",
+ "Ona ráda tancuje.",
+ "On rád cestuje.",
+ "Jsou dobří přátelé.",
+};
+
+/* ---- G04 sprechen --------------------------------------------------- */
+
+const VerbQ g04_rows[] = {
+ {"• Ich ", " Deutsch und du?", "spreche"},
+ {"• Welche Sprachen ", " Sie?", "sprechen"},
+ {"• ", " du Französisch?", "sprichst"},
+ {"• Wir ", " Polnisch und ihr?", "sprechen"},
+ {"• ", " ihr Englisch?", "sprecht"},
+ {"• Er ", " kein Wort Türkisch.", "spricht"},
+};
+
+const char *g04_pool[] = {
+ "spreche", "sprichst", "spricht", "sprechen", "sprecht",
+};
+
+const char *g04_mean[] = {
+ "Mluvím německy. A ty?",
+ "Kterými jazyky mluvíte?",
+ "Mluvíš francouzsky? – Ano, trochu.",
+ "My mluvíme polsky a vy?",
+ "Mluvíte anglicky? – Ano, velmi dobře.",
+ "Nemluví ani slovo turecky.",
+};
+
+/* ---- G08 Verben einsetzen ------------------------------------------- */
+
+const VerbQ g08_rows[] = {
+ {"Was ", " er? → Ein Buch. (lesen)", "liest"},
+ {"Was ", " sie? → Medizin. (studieren)", "studiert"},
+ {"Was ", " Jan gern? → Auto. (fahren)", "fährt"},
+ {"Wo ", " ihr? → In Wien. (leben)", "lebt"},
+ {"Wer ", " bei Siemens? → Meine Freundin Vera. (arbeiten)",
+ "arbeitet"},
+};
+
+const char *g08_pool[] = {
+ "liest", "lese", "lest", "lesen",
+ "studiert", "studiere", "studiert", "studieren",
+ "fährt", "fahre", "fahrt", "fahren",
+ "lebt", "lebe", "lebt", "leben",
+ "arbeitet", "arbeite", "arbeitet", "arbeiten",
+};
+
+const char *g08_mean[] = {
+ "Co čte? – Knihu.",
+ "Co studuje? – Medicínu.",
+ "Čím Jan rád jezdí? – Autem.",
+ "Kde bydlíte? – Ve Vídni.",
+ "Kdo pracuje u Siemensu? – Moje kamarádka Věra.",
+};
+
+/* ---- G10 Euro -------------------------------------------------------- */
+
+const VerbQ g10_rows[] = {
+ {"Julia hat ", " Euro.", "dreiundsechzig"},
+ {"Tina hat ", " Euro.", "achtunddreißig"},
+ {"Peter hat ", " Euro.", "fünfundvierzig"},
+ {"Hans hat ", " Euro.", "siebenundfünfzig"},
+ {"Barbara hat ", " Euro.", "neunundneunzig"},
+};
+
+const char *g10_pool[] = {
+ "dreiundsechzig", "achtunddreißig", "fünfundvierzig",
+ "siebenundfünfzig", "neunundneunzig",
+};
+
+const char *g10_mean[] = {
+ "šedesát tři eur",
+ "třicet osm eur",
+ "čtyřicet pět eur",
+ "padesát sedm eur",
+ "devadesát devět eur",
+};
+
+/* ---- G14 Lückentext -------------------------------------------------- */
+
+const VerbQ g14_rows[] = {
+ {"Agnieszka Kowalski kommt ", " Polen, aus Kraków.", "aus"},
+ {"Sie ist 18 Jahre alt und ", " dieses Jahr Abitur.", "macht"},
+ {"Sie chattet gerne mit ihrem deutschen ", ".", "Freund"},
+ {"Er heißt Stefan Böhmermann und ", " in Dresden.", "lebt"},
+ {"Agnieszka findet die ", " cool.", "Sprache"},
+ {"Sie spricht schon sehr ", " Deutsch.", "gut"},
+};
+
+const char *g14_pool[] = {
+ "aus", "macht", "Freund", "lebt", "Sprache", "gut",
+};
+
+const char *g14_mean[] = {
+ "Agnieszka Kowalski pochází z Polska, z Krakova.",
+ "Je jí 18 let a letos dělá maturitu.",
+ "Ráda chatuje se svým německým přítelem.",
+ "Jmenuje se Stefan Böhmermann a bydlí v Drážďanech.",
+ "Agnieszce přijde ten jazyk super.",
+ "Už mluví velmi dobře německy.",
+};
+
+const char *g14_gloss[] = {
+ "z", "dělá", "kamarád", "žije", "jazyk", "dobře",
+};
+
+/* ---- G15 Verbinde ---------------------------------------------------- */
+
+const VerbQ g15_rows[] = {
+ {"ein Handy ", "", "brauchen"},
+ {"bei BMW ", "", "arbeiten"},
+ {"Auto ", "", "fahren"},
+ {"das Gymnasium ", "", "besuchen"},
+ {"Spanisch ", "", "sprechen"},
+ {"Krimis ", "", "lesen"},
+};
+
+const char *g15_pool[] = {
+ "brauchen", "arbeiten", "fahren", "besuchen", "sprechen", "lesen",
+};
+
+const char *g15_mean[] = {
+ "potřebovat mobil",
+ "pracovat ve BMW",
+ "řídit auto",
+ "navštěvovat gymnázium",
+ "mluvit španělsky",
+ "číst detektivky",
+};
+
+const char *g15_gloss[] = {
+ "potřebovat", "pracovat", "řídit",
+ "navštěvovat", "mluvit", "číst",
+};
+
+/* ---- G16 Zahlenpaare -------------------------------------------------- */
+
+const VerbQ g16_rows[] = {
+ {"54 = ", "", "vierundfünfzig"},
+ {"45 = ", "", "fünfundvierzig"},
+ {"369 = ", "", "dreihundertneunundsechzig"},
+ {"112 = ", "", "hundertzwölf"},
+ {"122 = ", "", "hundertzweiundzwanzig"},
+ {"68 = ", "", "achtundsechzig"},
+};
+
+const char *g16_pool[] = {
+ "vierundfünfzig", "fünfundvierzig", "dreihundertneunundsechzig",
+ "hundertzwölf", "hundertzweiundzwanzig", "achtundsechzig",
+};
+
+const char *g16_mean[] = {
+ "padesát čtyři",
+ "čtyřicet pět",
+ "tři sta šedesát devět",
+ "sto dvanáct",
+ "sto dvacet dva",
+ "šedesát osm",
+};
+
+/* ---- G02 choice ------------------------------------------------------ */
+
+const ChoiceQ g02_rows[] = {
+ {"1. Ich komme ___ Athen.", {"aus", "in"}, 2, 0},
+ {"2. Peter wohnt ___ Salzburg.", {"aus", "in"}, 2, 1},
+ {"3. Berlin liegt ___ Deutschland.", {"aus", "in"}, 2, 1},
+ {"4. Wir wohnen ___ Paris.", {"aus", "in"}, 2, 1},
+ {"5. Sie kommen ___ Griechenland.", {"aus", "in"}, 2, 0},
+};
+
+const char *g02_mean[] = {
+ "Pocházím z Atén.",
+ "Petr bydlí v Salcburku.",
+ "Berlín leží v Německu.",
+ "Bydlíme v Paříži.",
+ "Pocházejí z Řecka.",
+};
+
+/* ---- G03 Fragewörter -------------------------------------------------- */
+
+const ChoiceQ g03_rows[] = {
+ {"1. ___ kommt er? → Aus London.", {"Wer", "Wie", "Wo", "Woher", "Was"},
+ 5, 3},
+ {"2. ___ heißt sie? → Andrea.", {"Wer", "Wie", "Wo", "Woher", "Was"},
+ 5, 1},
+ {"3. ___ kommt aus Russland? → Alexander.",
+ {"Wer", "Wie", "Wo", "Woher", "Was"}, 5, 0},
+ {"4. ___ liegt Prag? → In Tschechien.", {"Wer", "Wie", "Wo", "Woher", "Was"},
+ 5, 2},
+ {"5. ___ wohnen sie? → In Bratislava.",
+ {"Wer", "Wie", "Wo", "Woher", "Was"}, 5, 2},
+ {"6. ___ macht ihr? → Wir studieren Germanistik.",
+ {"Wer", "Wie", "Wo", "Woher", "Was"}, 5, 4},
+};
+
+const char *g03_mean[] = {
+ "Odkud je? – Z Londýna.",
+ "Jak se jmenuje? – Andrea.",
+ "Kdo pochází z Ruska? – Alexandr.",
+ "Kde leží Praha? – V Česku.",
+ "Kde bydlí? – V Bratislavě.",
+ "Co děláte? – Studujeme germanistiku.",
+};
+
+/* ---- G12 Unterstreiche ------------------------------------------------ */
+
+const ChoiceQ g12_rows[] = {
+ {"1. Peter ___ gern Bücher.", {"liest", "lest", "lesen"}, 3, 0},
+ {"2. Filip ___ in Salzburg.", {"arbeit", "arbeitet", "arbeite"}, 3, 1},
+ {"3. Magda ___ gut Englisch.", {"spricht", "sprecht", "sprechen"}, 3, 0},
+ {"4. Marek ___ das Gymnasium.", {"besuchst", "besucht", "besuchen"}, 3, 1},
+ {"5. Dominika ___ 15 Jahre alt.", {"ist", "seid", "bin"}, 3, 0},
+ {"6. Meine Schwester ___ Marta.", {"heiße", "heißt", "heißen"}, 3, 1},
+};
+
+const char *g12_mean[] = {
+ "Petr rád čte knihy.",
+ "Filip pracuje v Salcburku.",
+ "Magda dobře mluví anglicky.",
+ "Marek navštěvuje gymnázium.",
+ "Dominice je 15 let.",
+ "Moje sestra se jmenuje Marta.",
+};
+
+/* ---- G13 Ordne zu ----------------------------------------------------- */
+
+
+const OrdneRow g13_rows[] = {
+ {"heißen sie?", "Wie", "Jana und Michael."},
+ {"spricht Polnisch?", "Wer", "Jacek."},
+ {"wohnt Eva?", "Wo", "In Plzeň."},
+ {"kommt Markus?", "Woher", "Aus Deutschland."},
+ {"macht ihr?", "Was", "Wir studieren Germanistik."},
+ {"spricht Jana?", "Was", "Slowakisch."},
+};
+
+const char *g13_fw_pool[] = {"Was", "Wer", "Wie", "Wo", "Woher"};
+
+const char *g13_ans_pool[] = {
+ "Jana und Michael.", "Jacek.", "In Plzeň.", "Aus Deutschland.",
+ "Wir studieren Germanistik.", "Slowakisch.",
+};
+
+const char *g13_mean[] = {
+ "Jak se jmenují? – Jana a Michael.",
+ "Kdo mluví polsky? – Jacek.",
+ "Kde bydlí Eva? – V Plzni.",
+ "Odkud pochází Markus? – Z Německa.",
+ "Co děláte? – Studujeme germanistiku.",
+ "Jakým jazykem mluví Jana? – Slovensky.",
+};
+
+/* ---- G09 content (Freie Antwort) ------------------------------------- */
+
+const FreeQ ex9_free_qs[] = {
+ {"Wie heißt du?", "Jak se jmenuješ?",
+ "Ich heiße Lukas.", "Jmenuji se Lukáš."},
+ {"Wo wohnst du?", "Kde bydlíš?",
+ "Ich wohne in Prag.", "Bydlím v Praze."},
+ {"Wohin fährst du?", "Kam jedeš?",
+ "Ich fahre nach Deutschland.", "Jedu do Německa."},
+ {"Was liest du gern?", "Co rád/a čteš?",
+ "Ich lese gern Krimis.", "Rád/a čtu detektivky."},
+ {"Was spielst du gern?", "Co rád/a hraješ?",
+ "Ich spiele gern Golf.", "Rád/a hraju golf."},
+};
+
+/* ---- S01 Steckbrief --------------------------------------------------- */
+
+
+const ProfileQ s01_rows[] = {
+ {"Ich heiße …", "Ich heiße Petra."},
+ {"Ich komme …", "Ich komme aus Tschechien."},
+ {"Ich lebe …", "Ich lebe in Prag."},
+ {"Ich bin …", "Ich bin fünfzehn Jahre alt."},
+ {"Ich spreche …", "Ich spreche Tschechisch und Deutsch."},
+ {"Ich besuche …", "Ich besuche das Gymnasium."},
+ {"Ich finde …", "Ich finde Deutsch toll."},
+ {"Ich arbeite …", "Ich arbeite noch nicht."},
+};
+
+void s01_fill_sample(GtkWidget *label) {
+ const ProfileQ *q = g_object_get_data(G_OBJECT(label), "pq");
+ char *txt;
+
+ if (!q)
+ return;
+ txt = g_strdup_printf(tr("sample_line"), q->sample);
+ gtk_label_set_text(GTK_LABEL(label), txt);
+ g_free(txt);
+}
+
+
+void s01_reveal(GtkButton *button, gpointer data) {
+ GtkWidget *sample = data;
+ (void)button;
+ s01_fill_sample(sample);
+ gtk_widget_set_visible(sample, !gtk_widget_get_visible(sample));
+}
+
+void s01_finish(GtkButton *button, gpointer data) {
+ ProfileCtx *ctx = data;
+ (void)button;
+ set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
+ for (int i = 0; i < ctx->n; i++) {
+ s01_fill_sample(ctx->samples[i]);
+ gtk_widget_set_visible(ctx->samples[i], TRUE);
+ }
+ mark_done(ctx->unit, ctx->ex_num);
+}
+
+GtkWidget *build_profile(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub, const ProfileQ *qs, int n) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, title, sub, "finish",
+ &body, &feedback, &check);
+ ProfileCtx *ctx = g_new0(ProfileCtx, 1);
+
+ ctx->unit = unit;
+ ctx->ex_num = ex_num;
+ ctx->feedback = feedback;
+ ctx->n = n;
+ ctx->samples = g_new0(GtkWidget *, n);
+
+ for (int i = 0; i < n; i++) {
+ GtkWidget *prompt = gtk_label_new(qs[i].stem);
+ GtkWidget *entry;
+ GtkWidget *reveal;
+ GtkWidget *sample;
+
+ gtk_widget_set_halign(prompt, GTK_ALIGN_START);
+ gtk_widget_add_css_class(prompt, "ex-prompt");
+ gtk_box_append(GTK_BOX(body), prompt);
+
+ 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), "pq", (gpointer)&qs[i]);
+ 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(s01_reveal), sample);
+ }
+
+ g_signal_connect(check, "clicked", G_CALLBACK(s01_finish), ctx);
+ return page;
+}
+
+/* ---- S02 Hangman (Berufe) --------------------------------------------- */
+
+
+const char *hm_words[HM_WORDS] = {
+ "KOCH", "ARZT", "LEHRER", "POLIZIST", "VERKÄUFER",
+};
+
+const char *hm_tips[HM_WORDS] = {
+ "vaří v restauraci",
+ "léčí nemocné lidi",
+ "učí ve škole",
+ "pracuje u policie",
+ "prodává v obchodě",
+};
+
+/* QWERTY order, Ä/Ö/Ü in their own bottom row. */
+const char *hm_letters[] = {
+ "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
+ "A", "S", "D", "F", "G", "H", "J", "K", "L",
+ "Z", "X", "C", "V", "B", "N", "M",
+ "Ä", "Ö", "Ü",
+};
+
+const gunichar hm_letters_u[] = {
+ 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P',
+ 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L',
+ 'Z', 'X', 'C', 'V', 'B', 'N', 'M',
+ 0x00c4, 0x00d6, 0x00dc,
+};
+
+
+int hm_char_index(gunichar ch) {
+ for (int i = 0; i < HM_N_LETTERS; i++)
+ if (hm_letters_u[i] == ch)
+ return i;
+ return -1;
+}
+
+void hm_update_slots(HangmanCtx *hm) {
+ const char *w = hm_words[hm->word_index];
+ const char *p = w;
+ GString *out = g_string_new(NULL);
+
+ while (*p) {
+ gunichar ch = g_utf8_get_char(p);
+ p = g_utf8_next_char(p);
+ if (out->len > 0)
+ g_string_append_c(out, ' ');
+ int idx = hm_char_index(ch);
+ if (idx >= 0 && hm->guessed[idx])
+ g_string_append_unichar(out, ch);
+ else
+ g_string_append_c(out, '_');
+ }
+ gtk_label_set_text(GTK_LABEL(hm->slots), out->str);
+ g_string_free(out, TRUE);
+}
+
+gboolean hm_word_solved(HangmanCtx *hm) {
+ const char *w = hm_words[hm->word_index];
+ const char *p = w;
+
+ while (*p) {
+ gunichar ch = g_utf8_get_char(p);
+ p = g_utf8_next_char(p);
+ int idx = hm_char_index(ch);
+ if (idx < 0 || !hm->guessed[idx])
+ return FALSE;
+ }
+ return TRUE;
+}
+
+void hm_set_letters_enabled(HangmanCtx *hm, gboolean enabled) {
+ for (int i = 0; i < HM_N_LETTERS; i++) {
+ if (hm->letter_btns[i])
+ gtk_widget_set_sensitive(hm->letter_btns[i], enabled);
+ }
+}
+
+void hm_redraw(HangmanCtx *hm) {
+ if (hm->draw)
+ gtk_widget_queue_draw(hm->draw);
+}
+
+void hm_clear_feedback(HangmanCtx *hm) {
+ gtk_widget_remove_css_class(hm->feedback, "feedback-ok");
+ gtk_widget_remove_css_class(hm->feedback, "feedback-err");
+ gtk_label_set_text(GTK_LABEL(hm->feedback), "");
+}
+
+gboolean hm_advance(gpointer data) {
+ HangmanCtx *hm = data;
+
+ hm->locked = FALSE;
+ if (hm->word_index + 1 >= HM_WORDS) {
+ hm->finished = TRUE;
+ set_feedback(hm->feedback, TRUE, tr("hm_done"));
+ mark_done(hm->unit, hm->ex_num);
+ return G_SOURCE_REMOVE;
+ }
+ hm->word_index++;
+ hm->misses = 0;
+ for (int i = 0; i < HM_N_LETTERS; i++)
+ hm->guessed[i] = FALSE;
+ hm_set_letters_enabled(hm, TRUE);
+ hm_update_slots(hm);
+ gtk_label_set_text(GTK_LABEL(hm->hint_lbl), tr(hm_tips[hm->word_index]));
+ {
+ char *t = g_strdup_printf(tr("hm_progress"), hm->word_index + 1,
+ HM_WORDS);
+ gtk_label_set_text(GTK_LABEL(hm->counter_lbl), t);
+ g_free(t);
+ }
+ hm_clear_feedback(hm);
+ hm_redraw(hm);
+ return G_SOURCE_REMOVE;
+}
+
+void hm_reset_word(gpointer data) {
+ HangmanCtx *hm = data;
+ hm->misses = 0;
+ for (int i = 0; i < HM_N_LETTERS; i++)
+ hm->guessed[i] = FALSE;
+ hm->locked = FALSE;
+ hm_set_letters_enabled(hm, TRUE);
+ hm_update_slots(hm);
+ hm_clear_feedback(hm);
+ hm_redraw(hm);
+}
+
+gboolean hm_retry_word(gpointer data) {
+ hm_reset_word(data);
+ return G_SOURCE_REMOVE;
+}
+
+void hm_guess_letter(GtkButton *button, gpointer data) {
+ HangmanCtx *hm = data;
+ int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "li"));
+ const char *w = hm_words[hm->word_index];
+ const char *p = w;
+ gboolean correct = FALSE;
+
+ (void)button;
+ if (hm->finished || hm->locked)
+ return;
+ gtk_widget_set_sensitive(GTK_WIDGET(button), FALSE);
+ hm->guessed[idx] = TRUE;
+
+ while (*p) {
+ gunichar ch = g_utf8_get_char(p);
+ p = g_utf8_next_char(p);
+ if (hm_char_index(ch) == idx) {
+ correct = TRUE;
+ break;
+ }
+ }
+
+ if (correct) {
+ hm_update_slots(hm);
+ if (hm_word_solved(hm)) {
+ hm->locked = TRUE;
+ hm_set_letters_enabled(hm, FALSE);
+ set_feedback(hm->feedback, TRUE, tr("hm_wrong"));
+ g_timeout_add(1100, hm_advance, hm);
+ }
+ } else {
+ hm->misses++;
+ hm_redraw(hm);
+ if (hm->misses >= HM_MAX_MISSES) {
+ char *msg = g_strdup_printf(tr("hm_fail"), w);
+ hm->locked = TRUE;
+ hm_set_letters_enabled(hm, FALSE);
+ set_feedback(hm->feedback, FALSE, msg);
+ g_free(msg);
+ /* reveal the word, then restart it with empty letters */
+ for (int i = 0; i < HM_N_LETTERS; i++)
+ hm->guessed[i] = TRUE;
+ hm_update_slots(hm);
+ hm_redraw(hm);
+ g_timeout_add(1600, hm_retry_word, hm);
+ }
+ }
+}
+
+void draw_hangman(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ HangmanCtx *hm = data;
+ Rgb c = color_from_hex(app_theme.subtext);
+ double cx = width / 2.0 - 30.0;
+ double ground = height - 10.0;
+ int parts = hm->misses;
+
+ (void)area;
+ cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cr);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+ cairo_set_source_rgb(cr, c.r, c.g, c.b);
+ cairo_set_line_width(cr, 3.0);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+
+ /* scaffold */
+ cairo_new_path(cr);
+ cairo_move_to(cr, width * 0.15, ground);
+ cairo_line_to(cr, width * 0.85, ground);
+ cairo_move_to(cr, cx, ground);
+ cairo_line_to(cr, cx, 20.0);
+ cairo_line_to(cr, cx + 70.0, 20.0);
+ cairo_move_to(cr, cx + 70.0, 20.0);
+ cairo_line_to(cr, cx + 70.0, 42.0);
+ cairo_stroke(cr);
+
+ if (parts >= 1) { /* head */
+ cairo_new_path(cr);
+ cairo_arc(cr, cx + 70.0, 54.0, 12.0, 0.0, 2.0 * G_PI);
+ cairo_stroke(cr);
+ }
+ if (parts >= 2) { /* body */
+ cairo_new_path(cr);
+ cairo_move_to(cr, cx + 70.0, 66.0);
+ cairo_line_to(cr, cx + 70.0, 108.0);
+ cairo_stroke(cr);
+ }
+ if (parts >= 3) { /* left arm */
+ cairo_new_path(cr);
+ cairo_move_to(cr, cx + 70.0, 76.0);
+ cairo_line_to(cr, cx + 50.0, 96.0);
+ cairo_stroke(cr);
+ }
+ if (parts >= 4) { /* right arm */
+ cairo_new_path(cr);
+ cairo_move_to(cr, cx + 70.0, 76.0);
+ cairo_line_to(cr, cx + 90.0, 96.0);
+ cairo_stroke(cr);
+ }
+ if (parts >= 5) { /* left leg */
+ cairo_new_path(cr);
+ cairo_move_to(cr, cx + 70.0, 108.0);
+ cairo_line_to(cr, cx + 50.0, 132.0);
+ cairo_stroke(cr);
+ }
+ if (parts >= 6) { /* right leg */
+ cairo_new_path(cr);
+ cairo_move_to(cr, cx + 70.0, 108.0);
+ cairo_line_to(cr, cx + 90.0, 132.0);
+ cairo_stroke(cr);
+ }
+}
+
+GtkWidget *build_hangman(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub) {
+ GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ GtkWidget *wrap = gtk_center_box_new();
+ GtkWidget *content;
+ HangmanCtx *hm = g_new0(HangmanCtx, 1);
+ GtkWidget *hint_row;
+ GtkWidget *counter;
+ GtkWidget *slots;
+ GtkWidget *tip_hdr;
+ GtkWidget *hint_lbl;
+ GtkWidget *letters;
+ GtkWidget *head;
+ GtkWidget *guess_lbl;
+ char *tmp;
+
+ hm->unit = unit;
+ hm->ex_num = ex_num;
+
+ gtk_widget_set_hexpand(page, TRUE);
+ gtk_widget_set_vexpand(page, TRUE);
+ gtk_widget_set_margin_start(page, 32);
+ gtk_widget_set_margin_end(page, 32);
+ gtk_widget_set_margin_top(page, 24);
+ gtk_widget_set_margin_bottom(page, 24);
+ gtk_box_append(GTK_BOX(page), top_bar(unit->page, title, sub));
+
+ gtk_widget_set_vexpand(wrap, TRUE);
+ gtk_widget_set_hexpand(wrap, TRUE);
+ gtk_box_append(GTK_BOX(page), wrap);
+
+ content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
+ gtk_widget_set_halign(content, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(content, GTK_ALIGN_CENTER);
+ gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), content);
+
+ counter = gtk_label_new(NULL);
+ hm->counter_lbl = counter;
+ gtk_widget_add_css_class(counter, "ex-sub");
+ gtk_box_append(GTK_BOX(content), counter);
+
+ head = gtk_drawing_area_new();
+ gtk_widget_set_size_request(head, 220, 170);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(head), draw_hangman,
+ hm, NULL);
+ hm->draw = head;
+ gtk_box_append(GTK_BOX(content), head);
+
+ slots = gtk_label_new(NULL);
+ gtk_widget_add_css_class(slots, "hm-word");
+ hm->slots = slots;
+ gtk_box_append(GTK_BOX(content), slots);
+
+ hint_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_set_halign(hint_row, GTK_ALIGN_CENTER);
+ tip_hdr = gtk_label_new(NULL);
+ i18n_bind(tip_hdr, "hm_hint", 0);
+ gtk_widget_add_css_class(tip_hdr, "hint");
+ hint_lbl = gtk_label_new(NULL);
+ gtk_widget_add_css_class(hint_lbl, "hint");
+ hm->hint_lbl = hint_lbl;
+ gtk_box_append(GTK_BOX(hint_row), tip_hdr);
+ gtk_box_append(GTK_BOX(hint_row), hint_lbl);
+ gtk_box_append(GTK_BOX(content), hint_row);
+
+ guess_lbl = gtk_label_new(NULL);
+ i18n_bind(guess_lbl, "hm_guess", 0);
+ gtk_widget_add_css_class(guess_lbl, "ex-sub");
+ gtk_box_append(GTK_BOX(content), guess_lbl);
+
+ letters = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_box_append(GTK_BOX(content), letters);
+
+ hm->letter_btns = g_new0(GtkWidget *, HM_N_LETTERS);
+ {
+ static const int kb_start[4] = { 0, HM_ROW2, HM_ROW3, HM_ROW4 };
+ static const int kb_end[4] = { HM_ROW2, HM_ROW3, HM_ROW4,
+ HM_N_LETTERS };
+ for (int r = 0; r < 4; r++) {
+ GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
+ gtk_widget_set_halign(row, GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top(row, r == 3 ? 4 : 0);
+ for (int i = kb_start[r]; i < kb_end[r]; i++) {
+ GtkWidget *b = gtk_button_new_with_label(hm_letters[i]);
+ gtk_widget_add_css_class(b, "pill");
+ gtk_widget_add_css_class(b, "hm-key");
+ g_object_set_data(G_OBJECT(b), "li", GINT_TO_POINTER(i));
+ g_signal_connect(b, "clicked", G_CALLBACK(hm_guess_letter), hm);
+ gtk_box_append(GTK_BOX(row), b);
+ hm->letter_btns[i] = b;
+ }
+ gtk_box_append(GTK_BOX(letters), row);
+ }
+ }
+
+ hm->feedback = gtk_label_new(NULL);
+ gtk_widget_set_halign(hm->feedback, GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top(hm->feedback, 6);
+ gtk_box_append(GTK_BOX(content), hm->feedback);
+
+ gtk_label_set_text(GTK_LABEL(hint_lbl), tr(hm_tips[0]));
+ tmp = g_strdup_printf(tr("hm_progress"), 1, HM_WORDS);
+ gtk_label_set_text(GTK_LABEL(counter), tmp);
+ g_free(tmp);
+ hm_update_slots(hm);
+
+ return page;
+}
+
+/* ---- per-exercise builders for unit 2 ------------------------------- */
+
+GtkWidget *u2ex1(UnitCtx *unit) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, "Verben konjugieren",
+ "sub_verben", "check",
+ &body, &feedback, &check);
+ ComboListCtx *ctx = combo_list_ctx_new(unit, 1, feedback);
+ int counter = 0;
+
+ ctx->trans_upfront = TRUE;
+ ctx->reveal_german = TRUE;
+
+ GtkWidget *h1 = gtk_label_new("Peter Fritsch:");
+ gtk_widget_set_halign(h1, GTK_ALIGN_START);
+ gtk_widget_add_css_class(h1, "ex-sub");
+ gtk_box_append(GTK_BOX(body), h1);
+ verb_rows_add(ctx, body, &counter, g01_peter, G_N_ELEMENTS(g01_peter),
+ g01_pool, G_N_ELEMENTS(g01_pool), g01_peter_mean, FALSE);
+
+ GtkWidget *h2 = gtk_label_new("Jana Nová und Pavol Korčák:");
+ gtk_widget_set_halign(h2, GTK_ALIGN_START);
+ gtk_widget_add_css_class(h2, "ex-sub");
+ gtk_widget_set_margin_top(h2, 10);
+ gtk_box_append(GTK_BOX(body), h2);
+ verb_rows_add(ctx, body, &counter, g01_jana, G_N_ELEMENTS(g01_jana),
+ g01_pool, G_N_ELEMENTS(g01_pool), g01_jana_mean, FALSE);
+
+ g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
+ return page;
+}
+
+GtkWidget *u2ex2(UnitCtx *unit) {
+ return build_choice(unit, "aus oder in", "sub_ausin", 2,
+ g02_rows, g02_mean, G_N_ELEMENTS(g02_rows));
+}
+
+GtkWidget *u2ex3(UnitCtx *unit) {
+ return build_choice(unit, "Fragewörter", "sub_wer", 3,
+ g03_rows, g03_mean, G_N_ELEMENTS(g03_rows));
+}
+
+GtkWidget *u2ex4(UnitCtx *unit) {
+ return build_verb_ex(unit, 4, "sprechen", "sub_sprich",
+ g04_rows, G_N_ELEMENTS(g04_rows),
+ g04_pool, G_N_ELEMENTS(g04_pool), g04_mean, FALSE,
+ NULL);
+}
+
+GtkWidget *u2ex5(UnitCtx *unit) {
+ return build_typed(unit, 5, "Nationalitäten", "sub_nation",
+ g05_rows, G_N_ELEMENTS(g05_rows), g05_bank, FALSE);
+}
+
+GtkWidget *u2ex6(UnitCtx *unit) {
+ return build_typed(unit, 6, "Woher?", "sub_woher",
+ g06_rows, G_N_ELEMENTS(g06_rows), NULL, FALSE);
+}
+
+GtkWidget *u2ex7(UnitCtx *unit) {
+ return build_typed(unit, 7, "Länder schreiben", "sub_laender",
+ g07_rows, G_N_ELEMENTS(g07_rows), NULL, FALSE);
+}
+
+GtkWidget *u2ex8(UnitCtx *unit) {
+ return build_verb_ex(unit, 8, "Verben einsetzen", "sub_verb2",
+ g08_rows, G_N_ELEMENTS(g08_rows),
+ g08_pool, G_N_ELEMENTS(g08_pool), g08_mean, FALSE,
+ NULL);
+}
+
+GtkWidget *u2ex9(UnitCtx *unit) {
+ return build_free_answer(unit, 9, "Freie Antwort", "sub_free", "tip_ss",
+ ex9_free_qs, (int)G_N_ELEMENTS(ex9_free_qs));
+}
+
+GtkWidget *u2ex10(UnitCtx *unit) {
+ return build_verb_ex(unit, 10, "Euro", "sub_euro",
+ g10_rows, G_N_ELEMENTS(g10_rows),
+ g10_pool, G_N_ELEMENTS(g10_pool), g10_mean, TRUE,
+ NULL);
+}
+
+GtkWidget *u2ex11(UnitCtx *unit) {
+ return build_typed(unit, 11, "Wörter suchen", "sub_wortsuchen",
+ g11_rows, G_N_ELEMENTS(g11_rows), NULL, FALSE);
+}
+
+GtkWidget *u2ex12(UnitCtx *unit) {
+ return build_choice(unit, "Was ist richtig?", "sub_verb2", 12,
+ g12_rows, g12_mean, G_N_ELEMENTS(g12_rows));
+}
+
+GtkWidget *u2ex13(UnitCtx *unit) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, "Ordne zu", "sub_ordne",
+ "check", &body, &feedback, &check);
+ ComboListCtx *ctx = combo_list_ctx_new(unit, 13, feedback);
+
+ for (guint i = 0; i < G_N_ELEMENTS(g13_rows); i++) {
+ const OrdneRow *r = &g13_rows[i];
+ GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ GtkWidget *num = gtk_label_new(NULL);
+ GtkWidget *mid = gtk_label_new(r->mid);
+ GtkWidget *arrow = gtk_label_new("→");
+ GtkWidget *fw = make_word_combo(g13_fw_pool,
+ G_N_ELEMENTS(g13_fw_pool));
+ GtkWidget *ans = make_word_combo(g13_ans_pool,
+ G_N_ELEMENTS(g13_ans_pool));
+ char *numtxt = g_strdup_printf("%d.", (int)(i + 1));
+
+ gtk_widget_set_halign(row, GTK_ALIGN_START);
+ gtk_label_set_text(GTK_LABEL(num), numtxt);
+ g_free(numtxt);
+ gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(num, "ex-prompt");
+ gtk_box_append(GTK_BOX(row), num);
+
+ gtk_widget_set_valign(fw, GTK_ALIGN_CENTER);
+ gtk_widget_set_size_request(fw, 110, -1);
+ gtk_box_append(GTK_BOX(row), fw);
+ combo_list_add(ctx, fw, r->fw);
+
+ gtk_widget_set_valign(mid, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(mid, "ex-prompt");
+ gtk_box_append(GTK_BOX(row), mid);
+
+ gtk_widget_set_valign(arrow, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(arrow, "ex-prompt");
+ gtk_box_append(GTK_BOX(row), arrow);
+
+ gtk_widget_set_valign(ans, GTK_ALIGN_CENTER);
+ gtk_widget_set_size_request(ans, 190, -1);
+ gtk_box_append(GTK_BOX(row), ans);
+ combo_list_add(ctx, ans, r->ans);
+
+ gtk_box_append(GTK_BOX(body), row);
+ combo_list_add_trans(ctx, meaning_add(body, g13_mean[i]));
+ }
+
+ g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
+ return page;
+}
+
+GtkWidget *u2ex14(UnitCtx *unit) {
+ return build_verb_ex(unit, 14, "Lückentext", "sub_luecke",
+ g14_rows, G_N_ELEMENTS(g14_rows),
+ g14_pool, G_N_ELEMENTS(g14_pool), g14_mean, TRUE,
+ g14_gloss);
+}
+
+GtkWidget *u2ex15(UnitCtx *unit) {
+ return build_verb_ex(unit, 15, "Verbinde", "sub_verbinde",
+ g15_rows, G_N_ELEMENTS(g15_rows),
+ g15_pool, G_N_ELEMENTS(g15_pool), g15_mean, TRUE,
+ g15_gloss);
+}
+
+GtkWidget *u2ex16(UnitCtx *unit) {
+ return build_verb_ex(unit, 16, "Zahlen", "sub_zahlpaar",
+ g16_rows, G_N_ELEMENTS(g16_rows),
+ g16_pool, G_N_ELEMENTS(g16_pool), g16_mean, FALSE,
+ NULL);
+}
+
+GtkWidget *u2ex17(UnitCtx *unit) {
+ return build_profile(unit, 17, "Steckbrief", "sub_steckbrief",
+ s01_rows, G_N_ELEMENTS(s01_rows));
+}
+
+GtkWidget *u2ex18(UnitCtx *unit) {
+ return build_hangman(unit, 18, "Berufe", "sub_berufe");
+}
+
+GtkWidget *u2ex19(UnitCtx *unit) {
+ return build_typed(unit, 19, "Nationalität", "sub_bistdu",
+ s03_rows, G_N_ELEMENTS(s03_rows), NULL, TRUE);
+}
+
diff --git a/exercise_u3.c b/exercise_u3.c
new file mode 100644
index 0000000..b2a9528
--- /dev/null
+++ b/exercise_u3.c
@@ -0,0 +1,1279 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Unit 3 exercises ("Bei uns zu Hause") */
+/* ------------------------------------------------------------------ */
+
+/* Sentence fill-ins: a row is made of literal text segments that
+ * alternate with one or more blank widgets (dropdowns). `num` is the
+ * optional item number shown at the start of the line. */
+
+/* Build the complete German sentence of a fill row (segments spliced
+ * with the correct answers) – used as the model shown after Check. */
+char *u3_fill_sentence(const U3Fill *f) {
+ const char *segs[4];
+ const char *ans[3];
+ GString *s;
+ int i, g;
+
+ segs[0] = f->s0;
+ segs[1] = f->s1;
+ segs[2] = f->s2;
+ segs[3] = f->s3;
+ ans[0] = f->a0;
+ ans[1] = f->a1;
+ ans[2] = f->a2;
+ g = 0;
+ while (g < 3 && ans[g])
+ g++;
+
+ s = g_string_new(NULL);
+ for (i = 0; i <= g; i++) {
+ if (segs[i] && segs[i][0])
+ g_string_append(s, segs[i]);
+ if (i < g && ans[i])
+ g_string_append(s, ans[i]);
+ }
+ if (s->len == 0) {
+ g_string_free(s, TRUE);
+ return NULL;
+ }
+ return g_string_free(s, FALSE);
+}
+
+/* Append one horizontal fill row to `body`. The answer of blank i sits
+ * between text segments s[i] and s[i+1]; empty segments are skipped.
+ * `gloss` (may be NULL) is an i18n key for a small grey note shown at
+ * the end of the line, e.g. the meaning of the expected possessive. */
+void u3_fill_row(ComboListCtx *ctx, GtkWidget *body, const U3Fill *f,
+ const char **pool, int pool_n, const char *gloss) {
+ const char *segs[4];
+ const char *ans[3];
+ int i, g;
+ GtkWidget *line;
+
+ segs[0] = f->s0;
+ segs[1] = f->s1;
+ segs[2] = f->s2;
+ segs[3] = f->s3;
+ ans[0] = f->a0;
+ ans[1] = f->a1;
+ ans[2] = f->a2;
+ g = 0;
+ while (g < 3 && ans[g])
+ g++;
+
+ line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_set_halign(line, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), line);
+
+ if (f->num && f->num[0]) {
+ GtkWidget *nl = gtk_label_new(f->num);
+ gtk_widget_set_valign(nl, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(nl, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), nl);
+ }
+ for (i = 0; i <= g; i++) {
+ if (segs[i] && segs[i][0]) {
+ GtkWidget *lb = gtk_label_new(segs[i]);
+ gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(lb, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), lb);
+ }
+ if (i < g) {
+ GtkWidget *combo = make_word_combo(pool, pool_n);
+ gtk_widget_set_size_request(combo, 96, -1);
+ gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(line), combo);
+ combo_list_add(ctx, combo, ans[i]);
+ }
+ }
+
+ if (gloss && gloss[0]) {
+ GtkWidget *gl = gtk_label_new(NULL);
+ i18n_bind(gl, gloss, 0);
+ gtk_widget_set_valign(gl, GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_start(gl, 8);
+ gtk_widget_add_css_class(gl, "meaning");
+ gtk_box_append(GTK_BOX(line), gl);
+ }
+
+ if (f->mean && f->mean[0]) {
+ GtkWidget *m = meaning_add(body, f->mean);
+ combo_list_add_trans(ctx, m);
+ if (ctx->trans_upfront)
+ gtk_widget_set_visible(m, TRUE);
+ }
+
+ if (ctx->reveal_german) {
+ char *sentence = u3_fill_sentence(f);
+
+ if (sentence && sentence[0]) {
+ GtkWidget *w = model_answer_add(body, sentence);
+
+ g_array_append_val(ctx->models, w);
+ }
+ g_free(sentence);
+ }
+}
+
+void u3_sample_line(GtkWidget *body, const char *text) {
+ GtkWidget *sl = gtk_label_new(text);
+
+ gtk_widget_set_halign(sl, GTK_ALIGN_START);
+ gtk_widget_set_margin_top(sl, 2);
+ gtk_widget_set_margin_bottom(sl, 4);
+ gtk_widget_add_css_class(sl, "hint");
+ gtk_label_set_wrap(GTK_LABEL(sl), TRUE);
+ gtk_box_append(GTK_BOX(body), sl);
+}
+
+GtkWidget *u3_build_drop(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub, const char *sample,
+ const U3Fill *rows, int n,
+ const char **pool, int pool_n,
+ const char **glosses, gboolean show_german) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
+ &body, &feedback, &check);
+ ComboListCtx *ctx = combo_list_ctx_new(unit, ex_num, feedback);
+
+ ctx->trans_upfront = show_german;
+ ctx->reveal_german = show_german;
+
+ if (sample && sample[0])
+ u3_sample_line(body, sample);
+
+ for (int i = 0; i < n; i++)
+ u3_fill_row(ctx, body, &rows[i], pool, pool_n,
+ glosses ? glosses[i] : NULL);
+
+ g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
+ return page;
+}
+
+/* ---- ex1: Familienpaare (match the pair) ---------------------------- */
+
+const char *ex1_u3_pool[] = {
+ "die Cousine", "die Tante", "die Oma", "der Bruder",
+ "die Freundin", "der Sohn",
+};
+
+const U3Fill ex1_u3_rows[] = {
+ {"1.", "der Cousin", "die Cousine", NULL, NULL, NULL, NULL, NULL,
+ "bratranec / sestřenice"},
+ {"2.", "der Onkel", "die Tante", NULL, NULL, NULL, NULL, NULL,
+ "strýc / teta"},
+ {"3.", "der Opa", "die Oma", NULL, NULL, NULL, NULL, NULL,
+ "dědeček / babička"},
+ {"4.", "die Schwester", "der Bruder", NULL, NULL, NULL, NULL, NULL,
+ "sestra / bratr"},
+ {"5.", "der Freund", "die Freundin", NULL, NULL, NULL, NULL, NULL,
+ "kamarád / kamarádka"},
+ {"6.", "die Tochter", "der Sohn", NULL, NULL, NULL, NULL, NULL,
+ "dcera / syn"},
+};
+
+GtkWidget *u3ex1(UnitCtx *unit) {
+ return u3_build_drop(unit, 1, "Familienpaare", "sub3_pair",
+ "Beispiel: der Vater ↔ die Mutter",
+ ex1_u3_rows, G_N_ELEMENTS(ex1_u3_rows),
+ ex1_u3_pool, G_N_ELEMENTS(ex1_u3_pool), NULL, FALSE);
+}
+
+/* ---- ex2: Possessivpronomen im Nominativ ----------------------------- */
+
+const char *ex2_u3_pool[] = {
+ "dein", "deine", "mein", "meine", "sein", "seine",
+};
+
+/* The Czech sentence is shown straight away, and pressing "Check" only
+ * adds the translations of the words the student picked in the blanks. */
+
+
+const Ex2Row ex2_u3_rows2[] = {
+ {"2.", {"Ist das ", " Mutter? – Nein, das ist nicht ", " Mutter.", NULL},
+ {"deine", "meine", NULL}, 2},
+ {NULL, {"Das ist ", " Tante.", NULL, NULL}, {"seine", NULL, NULL}, 1},
+};
+
+const Ex2Row ex2_u3_rows3[] = {
+ {"3.", {"Ist das ", " Bruder? – Nein, das ist nicht ", " Bruder.", NULL},
+ {"dein", "mein", NULL}, 2},
+ {NULL, {"Das ist ", " Cousin.", NULL, NULL}, {"sein", NULL, NULL}, 1},
+};
+
+const Ex2Row ex2_u3_rows4[] = {
+ {"4.",
+ {"Sind das ", " Eltern? – Nein, das sind nicht ",
+ " Eltern. Das sind die Eltern von Markus.", NULL},
+ {"deine", "meine", NULL}, 2},
+};
+
+const Ex2Item ex2_u3_items[] = {
+ {"Je to tvoje matka? – Ne, to není moje matka. To je jeho teta.",
+ ex2_u3_rows2, G_N_ELEMENTS(ex2_u3_rows2)},
+ {"Je to tvůj bratr? – Ne, to není můj bratr. To je jeho bratranec.",
+ ex2_u3_rows3, G_N_ELEMENTS(ex2_u3_rows3)},
+ {"Jsou to tvoji rodiče? – Ne, to nejsou moji rodiče. To jsou rodiče "
+ "Markuse.",
+ ex2_u3_rows4, G_N_ELEMENTS(ex2_u3_rows4)},
+};
+
+
+const char *ex2_word_cs(const char *w) {
+ if (!w)
+ return NULL;
+ if (g_strcmp0(w, "dein") == 0) return "tvůj";
+ if (g_strcmp0(w, "deine") == 0) return "tvoje";
+ if (g_strcmp0(w, "mein") == 0) return "můj";
+ if (g_strcmp0(w, "meine") == 0) return "moje";
+ if (g_strcmp0(w, "sein") == 0) return "jeho";
+ if (g_strcmp0(w, "seine") == 0) return "jeho";
+ return NULL;
+}
+
+void ex2_check(GtkButton *button, gpointer data) {
+ Ex2Ctx *ctx = data;
+ guint total = ctx->combos->len;
+ guint ok = 0;
+
+ (void)button;
+
+ for (guint i = 0; i < total; i++) {
+ GtkComboBox *combo = g_array_index(ctx->combos, GtkComboBox *, i);
+ const char *ans = g_array_index(ctx->answers, const char *, i);
+ gchar *sel = gtk_combo_box_text_get_active_text(
+ GTK_COMBO_BOX_TEXT(combo));
+ gboolean good = FALSE;
+ if (sel) {
+ gchar *t = normalize_answer(sel);
+ good = answer_accepts(t, ans);
+ g_free(t);
+ g_free(sel);
+ }
+ answer_mark(GTK_WIDGET(combo), good);
+ if (good)
+ ok++;
+ }
+
+ for (int it = 0; it < ctx->n_items; it++) {
+ GArray *item = ctx->item_combos[it];
+ GtkWidget *note = ctx->notes[it];
+ GString *s = g_string_new(NULL);
+ for (guint k = 0; k < item->len; k++) {
+ GtkComboBox *combo = g_array_index(item, GtkComboBox *, k);
+ gchar *sel = gtk_combo_box_text_get_active_text(
+ GTK_COMBO_BOX_TEXT(combo));
+ const char *cs;
+ if (sel) {
+ cs = ex2_word_cs(sel);
+ if (cs) {
+ if (s->len > 0)
+ g_string_append(s, " · ");
+ g_string_append_printf(s, "%s = %s", sel, tr(cs));
+ }
+ g_free(sel);
+ }
+ }
+ gtk_label_set_text(GTK_LABEL(note), s->str);
+ g_string_free(s, TRUE);
+ gtk_widget_set_visible(note, TRUE);
+ }
+
+ if (ok == total) {
+ set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
+ mark_done(ctx->unit, ctx->ex_num);
+ } else {
+ set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
+ }
+}
+
+GtkWidget *u3ex2(UnitCtx *unit) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, "mein oder dein", "sub3_poss",
+ "check", &body, &feedback, &check);
+ Ex2Ctx *ctx = g_new0(Ex2Ctx, 1);
+
+ ctx->combos = g_array_new(FALSE, FALSE, sizeof(GtkComboBox *));
+ ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
+ ctx->n_items = G_N_ELEMENTS(ex2_u3_items);
+ ctx->notes = g_new0(GtkWidget *, ctx->n_items);
+ ctx->item_combos = g_new0(GArray *, ctx->n_items);
+ ctx->unit = unit;
+ ctx->ex_num = 2;
+ ctx->feedback = feedback;
+
+ u3_sample_line(body, "Beispiel: Ist das dein Vater? – Nein, das ist "
+ "nicht mein Vater. Das ist sein Onkel.");
+
+ for (int it = 0; it < ctx->n_items; it++) {
+ const Ex2Item *item = &ex2_u3_items[it];
+ GtkWidget *mean;
+ GtkWidget *note;
+ GArray *item_combos = g_array_new(FALSE, FALSE,
+ sizeof(GtkComboBox *));
+ ctx->item_combos[it] = item_combos;
+
+ for (int r = 0; r < item->nrows; r++) {
+ const Ex2Row *row = &item->rows[r];
+ GtkWidget *line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+
+ gtk_widget_set_halign(line, GTK_ALIGN_START);
+ if (row->num && row->num[0]) {
+ GtkWidget *nl = gtk_label_new(row->num);
+ gtk_widget_set_valign(nl, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(nl, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), nl);
+ }
+ for (int i = 0; i <= row->gaps; i++) {
+ if (row->seg[i] && row->seg[i][0]) {
+ GtkWidget *lb = gtk_label_new(row->seg[i]);
+ gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(lb, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), lb);
+ }
+ if (i < row->gaps) {
+ GtkWidget *combo = make_word_combo(ex2_u3_pool,
+ G_N_ELEMENTS(ex2_u3_pool));
+ gtk_widget_set_size_request(combo, 96, -1);
+ gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(line), combo);
+ g_array_append_val(ctx->combos, combo);
+ g_array_append_val(item_combos, combo);
+ g_array_append_val(ctx->answers, row->ans[i]);
+ }
+ }
+ gtk_box_append(GTK_BOX(body), line);
+ }
+
+ /* Czech sentence is available before checking. */
+ mean = meaning_add(body, item->czech);
+ gtk_widget_set_visible(mean, TRUE);
+
+ /* word translations appear only after pressing "Check". */
+ note = gtk_label_new(NULL);
+ gtk_widget_set_halign(note, GTK_ALIGN_START);
+ gtk_widget_set_margin_bottom(note, 2);
+ gtk_widget_add_css_class(note, "meaning");
+ gtk_label_set_wrap(GTK_LABEL(note), TRUE);
+ gtk_widget_set_visible(note, FALSE);
+ gtk_box_append(GTK_BOX(body), note);
+ ctx->notes[it] = note;
+ }
+
+ g_signal_connect(check, "clicked", G_CALLBACK(ex2_check), ctx);
+ return page;
+}
+
+/* ---- ex3: Haustiere – ein/kein --------------------------------------- */
+
+const char *ex3_u3_pool[] = {
+ "ein", "eine", "einen", "kein", "keine", "keinen",
+};
+
+const U3Fill ex3_u3_rows[] = {
+ {"2.", "Hast du ", "einen",
+ " Hamster? – Nein, ich habe ", "keinen", " Hamster.", NULL, NULL,
+ NULL},
+ {NULL, "Ich habe ", "eine", " Schildkröte.", NULL, NULL, NULL, NULL,
+ "Máš křečka? – Ne, nemám křečka. Mám želvu."},
+ {"3.", "Hast du ", "ein",
+ " Pferd? – Nein, ich habe ", "kein", " Pferd.", NULL, NULL, NULL},
+ {NULL, "Ich habe ", "ein", " Kaninchen.", NULL, NULL, NULL, NULL,
+ "Máš koně? – Ne, nemám koně. Mám králíka."},
+ {"4.", "Hast du ", "einen",
+ " Fisch? – Nein, ich habe ", "keinen", " Fisch.", NULL, NULL, NULL},
+ {NULL, "Ich habe ", "einen", " Vogel.", NULL, NULL, NULL, NULL,
+ "Máš rybu? – Ne, nemám rybu. Mám ptáka."},
+ {"5.", "Hast du ", "ein",
+ " Meerschweinchen? – Nein, ich habe ", "kein",
+ " Meerschweinchen.", NULL, NULL, NULL},
+ {NULL, "Ich habe ", "einen", " Wellensittich.", NULL, NULL, NULL, NULL,
+ "Máš morče? – Ne, nemám morče. Mám andulku."},
+};
+
+GtkWidget *u3ex3(UnitCtx *unit) {
+ return u3_build_drop(unit, 3, "ein / kein", "sub3_haustier",
+ "Beispiel: Hast du eine Katze? – Nein, ich habe "
+ "keine Katze. Ich habe einen Hund.",
+ ex3_u3_rows, G_N_ELEMENTS(ex3_u3_rows),
+ ex3_u3_pool, G_N_ELEMENTS(ex3_u3_pool), NULL, TRUE);
+}
+
+/* ---- inline letter-gap rows (ex15) ----------------------------------- */
+
+/* One sentence row whose text is split into segments with up to four
+ * small text entries (one per missing-letter group) inserted. */
+
+
+U3LetCtx *u3_let_ctx_new(UnitCtx *unit, int ex_num,
+ GtkWidget *feedback) {
+ U3LetCtx *ctx = g_new0(U3LetCtx, 1);
+ ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
+ ctx->trans = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ ctx->models = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ ctx->unit = unit;
+ ctx->ex_num = ex_num;
+ ctx->feedback = feedback;
+ return ctx;
+}
+
+/* Rebuild the full German sentence (letter gaps filled in). */
+char *u3_let_sentence(const U3Let *row) {
+ const char *segs[5];
+ const char *ans[4];
+ GString *s;
+ int i, g;
+
+ segs[0] = row->s0;
+ segs[1] = row->s1;
+ segs[2] = row->s2;
+ segs[3] = row->s3;
+ segs[4] = row->s4;
+ ans[0] = row->a0;
+ ans[1] = row->a1;
+ ans[2] = row->a2;
+ ans[3] = row->a3;
+ g = 0;
+ while (g < 4 && ans[g])
+ g++;
+
+ s = g_string_new(NULL);
+ for (i = 0; i <= g; i++) {
+ if (segs[i] && segs[i][0])
+ g_string_append(s, segs[i]);
+ if (i < g && ans[i])
+ g_string_append(s, ans[i]);
+ }
+ if (s->len == 0) {
+ g_string_free(s, TRUE);
+ return NULL;
+ }
+ return g_string_free(s, FALSE);
+}
+
+void u3_let_check(GtkButton *button, gpointer data) {
+ U3LetCtx *ctx = data;
+ guint total = ctx->entries->len;
+ guint ok = 0;
+
+ (void)button;
+
+ for (guint i = 0; i < total; i++) {
+ GtkWidget *e = g_array_index(ctx->entries, GtkWidget *, i);
+ const char *ans = g_array_index(ctx->answers, const char *, i);
+ const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(e));
+ gchar *norm = normalize_answer(txt);
+ gboolean good = txt && txt[0] && answer_accepts(norm, ans);
+ g_free(norm);
+ answer_mark(e, good);
+ if (good)
+ ok++;
+ }
+
+ for (guint i = 0; i < ctx->trans->len; i++) {
+ GtkWidget *lbl = g_array_index(ctx->trans, GtkWidget *, i);
+ if (lbl)
+ gtk_widget_set_visible(lbl, TRUE);
+ }
+
+ for (guint i = 0; i < ctx->models->len; i++) {
+ GtkWidget *lbl = g_array_index(ctx->models, GtkWidget *, i);
+ if (lbl)
+ gtk_widget_set_visible(lbl, TRUE);
+ }
+
+ if (ok == total) {
+ set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
+ mark_done(ctx->unit, ctx->ex_num);
+ } else {
+ set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
+ }
+}
+
+void u3_let_row(U3LetCtx *ctx, GtkWidget *body, const U3Let *row) {
+ const char *segs[5];
+ const char *ans[4];
+ int i, g;
+ GtkWidget *line;
+
+ segs[0] = row->s0;
+ segs[1] = row->s1;
+ segs[2] = row->s2;
+ segs[3] = row->s3;
+ segs[4] = row->s4;
+ ans[0] = row->a0;
+ ans[1] = row->a1;
+ ans[2] = row->a2;
+ ans[3] = row->a3;
+ g = 0;
+ while (g < 4 && ans[g])
+ g++;
+
+ line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_widget_set_halign(line, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), line);
+
+ if (row->num && row->num[0]) {
+ GtkWidget *nl = gtk_label_new(row->num);
+ gtk_widget_set_valign(nl, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(nl, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), nl);
+ }
+ for (i = 0; i <= g; i++) {
+ if (segs[i] && segs[i][0]) {
+ GtkWidget *lb = gtk_label_new(segs[i]);
+ gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(lb, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), lb);
+ }
+ if (i < g) {
+ GtkWidget *e = gtk_entry_new();
+ gtk_editable_set_width_chars(GTK_EDITABLE(e), 4);
+ gtk_widget_set_valign(e, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(line), e);
+ g_array_append_val(ctx->entries, e);
+ g_array_append_val(ctx->answers, ans[i]);
+ }
+ }
+
+ if (row->mean && row->mean[0]) {
+ GtkWidget *m = meaning_add(body, row->mean);
+ gtk_widget_set_visible(m, TRUE);
+ g_array_append_val(ctx->trans, m);
+ }
+
+ {
+ char *sentence = u3_let_sentence(row);
+
+ if (sentence && sentence[0]) {
+ GtkWidget *w = model_answer_add(body, sentence);
+
+ g_array_append_val(ctx->models, w);
+ }
+ g_free(sentence);
+ }
+}
+
+GtkWidget *u3_build_letters(UnitCtx *unit, int ex_num,
+ const char *title, const char *sub,
+ const U3Let *rows, int n) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
+ &body, &feedback, &check);
+ U3LetCtx *ctx = u3_let_ctx_new(unit, ex_num, feedback);
+
+ {
+ gboolean need = FALSE;
+
+ for (int i = 0; i < n && !need; i++) {
+ const U3Let *r = &rows[i];
+
+ need = text_has_german_umlaut(r->s0) ||
+ text_has_german_umlaut(r->s1) ||
+ text_has_german_umlaut(r->s2) ||
+ text_has_german_umlaut(r->s3) ||
+ text_has_german_umlaut(r->s4) ||
+ text_has_german_umlaut(r->a0) ||
+ text_has_german_umlaut(r->a1) ||
+ text_has_german_umlaut(r->a2) ||
+ text_has_german_umlaut(r->a3);
+ }
+ if (need)
+ add_umlaut_note(body);
+ }
+
+ for (int i = 0; i < n; i++)
+ u3_let_row(ctx, body, &rows[i]);
+
+ g_signal_connect(check, "clicked", G_CALLBACK(u3_let_check), ctx);
+ return page;
+}
+
+/* ---- ex4: missing letters in a paragraph ----------------------------- */
+
+/* The Czech sentence is shown straight away; pressing "Check" only adds
+ * the translations of the words the student had to complete. */
+
+const Ex4Row ex4_u3_rows[] = {
+ {{"Wir hab", "n Fre", "nde in Öster", "ich. Sie wo", "nen in Wien."},
+ {"e", "u", "re", "h"},
+ "Máme přátele v Rakousku. Bydlí ve Vídni.",
+ {"haben", "Freunde", "Österreich", "wohnen"},
+ {"mít", "přátelé", "Rakousko", "bydlet"}, 4},
+ {{"Sie hei", "en Elfriede und Jiri. Sie haben auch ", "inder.", NULL,
+ NULL},
+ {"ß", "K", NULL, NULL},
+ "Jmenují se Elfriede a Jiří. Mají také děti.",
+ {"heißen", "Kinder", NULL, NULL},
+ {"jmenovat se", "děti", NULL, NULL}, 2},
+ {{"Ihr S", "hn heißt Philipp und ihr", " Tochter heißt Sabine.", NULL,
+ NULL},
+ {"o", "e", NULL, NULL},
+ "Jejich syn se jmenuje Philipp a jejich dcera Sabine.",
+ {"Sohn", "ihre", NULL, NULL},
+ {"syn", "jejich", NULL, NULL}, 2},
+ {{"Die Famili", " re", "st gern.", NULL, NULL},
+ {"e", "i", NULL, NULL},
+ "Rodina ráda cestuje.",
+ {"Familie", "reist", NULL, NULL},
+ {"rodina", "cestuje", NULL, NULL}, 2},
+ {{"Sie ist o", "t bei uns zu Bes", "ch in Tsche", "ien.", NULL},
+ {"f", "u", "ch", NULL},
+ "Často je u nás na návštěvě v Česku.",
+ {"oft", "Besuch", "Tschechien", NULL},
+ {"často", "návštěva", "Česko", NULL}, 3},
+ {{"Die Kinder ler", "en auch Tsche", "isch.", NULL, NULL},
+ {"n", "ch", NULL, NULL},
+ "Děti se učí také česky.",
+ {"lernen", "Tschechisch", NULL, NULL},
+ {"učit se", "čeština", NULL, NULL}, 2},
+};
+
+
+void ex4_check(GtkButton *button, gpointer data) {
+ Ex4LetterCtx *ctx = data;
+ guint total = ctx->entries->len;
+ guint ok = 0;
+
+ (void)button;
+
+ for (guint i = 0; i < total; i++) {
+ GtkWidget *e = g_array_index(ctx->entries, GtkWidget *, i);
+ const char *ans = g_array_index(ctx->answers, const char *, i);
+ const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(e));
+ gchar *norm = normalize_answer(txt);
+ gboolean good = txt && txt[0] && answer_accepts(norm, ans);
+ g_free(norm);
+ answer_mark(e, good);
+ if (good)
+ ok++;
+ }
+
+ for (int r = 0; r < ctx->n_rows; r++) {
+ const Ex4Row *row = &ctx->rows[r];
+ GtkWidget *note = ctx->notes[r];
+ GString *s = g_string_new(NULL);
+ for (int k = 0; k < row->gaps; k++) {
+ if (k > 0)
+ g_string_append(s, " · ");
+ if (row->word[k])
+ g_string_append_printf(s, "%s = %s", row->word[k],
+ tr(row->gmean[k]));
+ }
+ gtk_label_set_text(GTK_LABEL(note), s->str);
+ g_string_free(s, TRUE);
+ gtk_widget_set_visible(note, TRUE);
+ }
+
+ if (ok == total) {
+ set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
+ mark_done(ctx->unit, ctx->ex_num);
+ } else {
+ set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
+ }
+}
+
+GtkWidget *u3ex4(UnitCtx *unit) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, "Lückentext", "sub3_buchst",
+ "check", &body, &feedback, &check);
+ Ex4LetterCtx *ctx = g_new0(Ex4LetterCtx, 1);
+
+ ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
+ ctx->rows = ex4_u3_rows;
+ ctx->n_rows = G_N_ELEMENTS(ex4_u3_rows);
+ ctx->notes = g_new0(GtkWidget *, ctx->n_rows);
+ ctx->unit = unit;
+ ctx->ex_num = 4;
+ ctx->feedback = feedback;
+
+ {
+ gboolean need = FALSE;
+
+ for (int r = 0; r < ctx->n_rows && !need; r++) {
+ for (int i = 0; i < ctx->rows[r].gaps; i++) {
+ if (text_has_german_umlaut(ctx->rows[r].ans[i]) ||
+ text_has_german_umlaut(ctx->rows[r].word[i])) {
+ need = TRUE;
+ break;
+ }
+ }
+ }
+ if (need)
+ add_umlaut_note(body);
+ }
+
+ for (int r = 0; r < ctx->n_rows; r++) {
+ const Ex4Row *row = &ctx->rows[r];
+ GtkWidget *line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ GtkWidget *mean;
+ GtkWidget *note;
+
+ gtk_widget_set_halign(line, GTK_ALIGN_START);
+ for (int i = 0; i <= row->gaps; i++) {
+ if (row->seg[i] && row->seg[i][0]) {
+ GtkWidget *lb = gtk_label_new(row->seg[i]);
+ gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(lb, "ex-prompt");
+ gtk_box_append(GTK_BOX(line), lb);
+ }
+ if (i < row->gaps) {
+ GtkWidget *e = gtk_entry_new();
+ gtk_editable_set_width_chars(GTK_EDITABLE(e), 4);
+ gtk_widget_set_valign(e, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(line), e);
+ g_array_append_val(ctx->entries, e);
+ g_array_append_val(ctx->answers, row->ans[i]);
+ }
+ }
+ gtk_box_append(GTK_BOX(body), line);
+
+ /* the Czech sentence is available before checking */
+ mean = meaning_add(body, row->czech);
+ gtk_widget_set_visible(mean, TRUE);
+
+ /* gapped-word translations appear only after "Check" */
+ note = gtk_label_new(NULL);
+ gtk_widget_set_halign(note, GTK_ALIGN_START);
+ gtk_widget_set_margin_bottom(note, 2);
+ gtk_widget_add_css_class(note, "meaning");
+ gtk_label_set_wrap(GTK_LABEL(note), TRUE);
+ gtk_widget_set_visible(note, FALSE);
+ gtk_box_append(GTK_BOX(body), note);
+ ctx->notes[r] = note;
+ }
+
+ g_signal_connect(check, "clicked", G_CALLBACK(ex4_check), ctx);
+ return page;
+}
+
+/* ---- ex5: Marcos Familie (word-bank typing) -------------------------- */
+
+const char *ex5_u3_bank =
+ "Tanten | Job | Vater | Ferien | Zwillinge | Eltern | Kroatien";
+
+const TypedQ ex5_u3_rows[] = {
+ {"Wir haben eine Männer-Wohngemeinschaft: Mein ________ Johann (50), "
+ "Ivo (16) und ich.",
+ "vater", "Máme mužskou domácnost: Můj otec Johann (50), Ivo (16) a já."},
+ {"Ivo und ich sind gleich alt – wir sind nämlich ________.",
+ "zwillinge", "Ivo a já jsme stejně staří – jsme totiž dvojčata."},
+ {"Jetzt wohnen wir in Hannover, unser Vater hat hier einen neuen "
+ "________.",
+ "job", "Náš otec tu má novou práci."},
+ {"Unsere ________ sind geschieden.",
+ "eltern", "Naši rodiče jsou rozvedení."},
+ {"Unsere Mutter Maja (50) lebt in Wien, aber sie kommt aus ________.",
+ "kroatien", "Naše matka Maja (50) žije ve Vídni, ale pochází z "
+ "Chorvatska."},
+ {"Wir haben viele Onkel, ________, Cousinen und Cousins in Kroatien.",
+ "tanten", "Máme v Chorvatsku hodně strýců, tet, sestřenic a bratranců."},
+ {"Wir sehen sie manchmal in den ________.",
+ "ferien", "Vídáme je někdy o prázdninách."},
+};
+
+GtkWidget *u3ex5(UnitCtx *unit) {
+ return build_typed(unit, 5, "Marcos Familie", "sub3_marco",
+ ex5_u3_rows, G_N_ELEMENTS(ex5_u3_rows),
+ ex5_u3_bank, FALSE);
+}
+
+/* ---- ex6: Wortschatz sortieren --------------------------------------- */
+
+const AssignItem ex6_u3_items[] = {
+ {NULL, "die Oma", 0},
+ {NULL, "der Bruder", 0},
+ {NULL, "der Opa", 0},
+ {NULL, "die Tochter", 0},
+ {NULL, "die Tante", 0},
+ {NULL, "der Hamster", 1},
+ {NULL, "das Pferd", 1},
+ {NULL, "die Katze", 1},
+ {NULL, "der Hund", 1},
+ {NULL, "der Wellensittich", 1},
+};
+
+const char *ex6_u3_groups[] = {"Familienmitglieder", "Haustiere"};
+
+const char *ex6_u3_meaning[] = {
+ "babička", "bratr", "dědeček", "dcera", "teta",
+ "křeček", "kůň", "kočka", "pes", "andulka",
+};
+
+GtkWidget *u3ex6(UnitCtx *unit) {
+ return build_assign(unit, "Sortieren", "sub3_sort", 6,
+ ex6_u3_items, G_N_ELEMENTS(ex6_u3_items),
+ ex6_u3_groups, G_N_ELEMENTS(ex6_u3_groups),
+ ex6_u3_meaning);
+}
+
+/* ---- ex7: Possessivpronomen-Tabelle ---------------------------------- */
+
+
+void u3_table_check(GtkButton *button, gpointer data) {
+ U3TableCtx *ctx = data;
+ int ok = 0;
+
+ (void)button;
+
+ for (int i = 0; i < ctx->n; i++) {
+ const gchar *txt = gtk_editable_get_text(
+ GTK_EDITABLE(ctx->entries[i]));
+ gchar *norm = normalize_answer(txt);
+ gboolean good = txt && txt[0] &&
+ g_strcmp0(norm, ctx->answers[i]) == 0;
+ g_free(norm);
+ answer_mark(ctx->entries[i], good);
+ if (good)
+ ok++;
+ }
+
+ 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"));
+ }
+}
+
+GtkWidget *u3ex7(UnitCtx *unit) {
+ static const char *persons[] = {
+ "ich", "du", "er", "sie", "es", "wir", "ihr", "sie / Sie",
+ };
+ static const char *nouns[] = {
+ "der Garten", "die Idee", "das Fest", "die Geschwister",
+ };
+ static const char *ans[4][8] = {
+ {"mein", "dein", "sein", "ihr", "sein", "unser", "euer",
+ "ihr"},
+ {"meine", "deine", "seine", "ihre", "seine", "unsere", "eure",
+ "ihre"},
+ {"mein", "dein", "sein", "ihr", "sein", "unser", "euer",
+ "ihr"},
+ {"meine", "deine", "seine", "ihre", "seine", "unsere", "eure",
+ "ihre"},
+ };
+ const int np = G_N_ELEMENTS(persons);
+ const int nn = G_N_ELEMENTS(nouns);
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, "Possessivtabelle",
+ "sub3_tabelle", "check",
+ &body, &feedback, &check);
+ GtkWidget *grid;
+ GtkWidget *sample_note;
+ GtkWidget *bank_row;
+ GtkWidget *bank_hdr;
+ GtkWidget *bank1;
+ GtkWidget *bank2;
+ U3TableCtx *ctx = g_new0(U3TableCtx, 1);
+
+ ctx->n = nn * np;
+ ctx->entries = g_new0(GtkWidget *, ctx->n);
+ ctx->answers = g_new0(const char *, ctx->n);
+ ctx->unit = unit;
+ ctx->ex_num = 7;
+ ctx->feedback = feedback;
+
+ sample_note = gtk_label_new("Beispiel: mein (ich × der Garten)");
+ gtk_widget_set_halign(sample_note, GTK_ALIGN_START);
+ gtk_widget_set_margin_top(sample_note, 2);
+ gtk_widget_set_margin_bottom(sample_note, 4);
+ gtk_widget_add_css_class(sample_note, "hint");
+ gtk_box_append(GTK_BOX(body), sample_note);
+
+ bank_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_set_halign(bank_row, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), bank_row);
+ bank_hdr = gtk_label_new(NULL);
+ i18n_bind(bank_hdr, "wordbank", 0);
+ gtk_widget_add_css_class(bank_hdr, "hint");
+ gtk_box_append(GTK_BOX(bank_row), bank_hdr);
+ bank1 = gtk_label_new("(der Garten / das Fest) "
+ "mein · dein · sein · ihr · unser · euer");
+ gtk_widget_add_css_class(bank1, "hint");
+ gtk_box_append(GTK_BOX(bank_row), bank1);
+
+ bank_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
+ gtk_widget_set_halign(bank_row, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), bank_row);
+ bank2 = gtk_label_new("(die Idee / die Geschwister) "
+ "meine · deine · seine · ihre · unsere · eure");
+ gtk_widget_add_css_class(bank2, "hint");
+ gtk_box_append(GTK_BOX(bank_row), bank2);
+
+ grid = gtk_grid_new();
+ gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
+ gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
+ gtk_widget_set_halign(grid, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), grid);
+
+ for (int c = 0; c < np; c++) {
+ GtkWidget *h = gtk_label_new(persons[c]);
+ gtk_widget_set_halign(h, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(h, "ex-sub");
+ gtk_grid_attach(GTK_GRID(grid), h, c + 1, 0, 1, 1);
+ }
+ for (int r = 0; r < nn; r++) {
+ GtkWidget *h = gtk_label_new(nouns[r]);
+ gtk_widget_set_halign(h, GTK_ALIGN_START);
+ gtk_widget_add_css_class(h, "ex-sub");
+ gtk_grid_attach(GTK_GRID(grid), h, 0, r + 1, 1, 1);
+ }
+ for (int r = 0; r < nn; r++) {
+ for (int c = 0; c < np; c++) {
+ GtkWidget *e = gtk_entry_new();
+ gtk_editable_set_width_chars(GTK_EDITABLE(e), 6);
+ gtk_widget_set_halign(e, GTK_ALIGN_CENTER);
+ gtk_grid_attach(GTK_GRID(grid), e, c + 1, r + 1, 1, 1);
+ ctx->entries[r * np + c] = e;
+ ctx->answers[r * np + c] = ans[r][c];
+ }
+ }
+ /* pre-filled sample cell (ich × der Garten) */
+ gtk_editable_set_text(GTK_EDITABLE(ctx->entries[0]), "mein");
+ gtk_widget_set_sensitive(ctx->entries[0], FALSE);
+
+ g_signal_connect(check, "clicked", G_CALLBACK(u3_table_check), ctx);
+ return page;
+}
+
+/* ---- ex8: Possessivpronomen im Akkusativ ------------------------------ */
+
+const char *ex8_u3_pool[] = {
+ "mein", "meine", "meinen", "dein", "deine", "deinen",
+ "sein", "seine", "seinen", "ihr", "ihre", "ihren",
+ "unser", "unsere", "unseren", "euer", "eure", "euren",
+};
+
+const U3Fill ex8_u3_rows[] = {
+ {"2.", "das Handy → Du brauchst ", "dein", " Handy.", NULL, NULL, NULL,
+ NULL, "Potřebuješ svůj mobil."},
+ {"3.", "das Pferd → Er hat ", "sein", " Pferd.", NULL, NULL, NULL, NULL,
+ "Má svého koně."},
+ {"4.", "die Party → Sie macht ", "ihre", " Party.", NULL, NULL, NULL,
+ NULL, "Dělá svou párty."},
+ {"5.", "der Lehrer → Es fragt ", "seinen", " Lehrer.", NULL, NULL, NULL,
+ NULL, "Ptá se svého učitele."},
+ {"6.", "das Wochenendhaus → Wir besuchen ", "unser", " Wochenendhaus.",
+ NULL, NULL, NULL, NULL, "Navštěvujeme náš víkendový dům."},
+ {"7.", "der Hund → Ihr seht ", "euren", " Hund.", NULL, NULL, NULL, NULL,
+ "Vy vidíte svého psa."},
+ {"8.", "die Instrumente → Sie haben ", "ihre", " Instrumente.", NULL,
+ NULL, NULL, NULL, "Mají své nástroje."},
+};
+
+const char *ex8_u3_gloss[] = {
+ "u3g_dein", "u3g_sein", "u3g_ihr_sie", "u3g_sein_es",
+ "u3g_unser", "u3g_euer", "u3g_ihr_sie_pl",
+};
+
+GtkWidget *u3ex8(UnitCtx *unit) {
+ return u3_build_drop(unit, 8, "Akkusativ", "sub3_akk",
+ "Beispiel: die Inliner (ich) → Ich brauche meine "
+ "Inliner.",
+ ex8_u3_rows, G_N_ELEMENTS(ex8_u3_rows),
+ ex8_u3_pool, G_N_ELEMENTS(ex8_u3_pool),
+ ex8_u3_gloss, TRUE);
+}
+
+/* ---- ex9: kein / keine / keinen / nicht ------------------------------ */
+
+const char *ex9_u3_pool[] = {
+ "kein", "keine", "keinen", "nicht",
+};
+
+const U3Fill ex9_u3_rows[] = {
+ {"1.", "● Sprichst du Spanisch? ○ Nein, ich spreche ", "kein",
+ " Spanisch.", NULL, NULL, NULL, NULL,
+ "Mluvíš španělsky? – Ne, nemluvím španělsky."},
+ {"2.", "● Hast du einen Bruder? ○ Nein, ich habe ", "keinen",
+ " Bruder.", NULL, NULL, NULL, NULL, "Máš bratra? – Ne, nemám bratra."},
+ {"3.", "● Gibt es in Opava einen Zoo? ○ Nein, in Opava gibt es ",
+ "keinen", " Zoo.", NULL, NULL, NULL, NULL,
+ "Je v Opavě zoo? – Ne, v Opavě žádná zoo není."},
+ {"4.", "● Gehst du ins Schwimmbad? ○ Nein, ich gehe ", "nicht",
+ " ins Schwimmbad.", NULL, NULL, NULL, NULL,
+ "Jdeš do bazénu? – Ne, do bazénu nejdu."},
+ {"5.", "● Wandert er gern? ○ Nein, er wandert ", "nicht", " gern.",
+ NULL, NULL, NULL, NULL, "Rád chodí na túry? – Ne, nerad chodí na túry."},
+ {"6.", "● Besuchst du heute ein Schloss? ○ Nein, heute besuche ich ",
+ "kein", " Schloss.", NULL, NULL, NULL, NULL,
+ "Navštívíš dnes zámek? – Ne, dnes nenavštívím žádný zámek."},
+};
+
+GtkWidget *u3ex9(UnitCtx *unit) {
+ return u3_build_drop(unit, 9, "kein / nicht", "sub3_nicht", NULL,
+ ex9_u3_rows, G_N_ELEMENTS(ex9_u3_rows),
+ ex9_u3_pool, G_N_ELEMENTS(ex9_u3_pool), NULL, TRUE);
+}
+
+/* ---- ex10: Sätze bauen ------------------------------------------------ */
+
+const AssemblyItem ex10_u3_items[] = {
+ {NULL, {"Sie", "haben", "eine", "Schildkröte", "."}, 5},
+ {NULL, {"Was", "macht", "deine", "Freundin", "am", "Wochenende?"}, 6},
+ {NULL, {"Mein", "Vater", "ist", "Lehrer", "von", "Beruf."}, 6},
+ {NULL, {"Seid", "ihr", "viele", "zu", "Hause?"}, 5},
+ {NULL, {"Julia", "stellt", "ihren", "Hund", "Trixi", "vor."}, 6},
+};
+
+const char *ex10_u3_meaning[] = {
+ "Mají želvu.",
+ "Co dělá tvoje kamarádka o víkendu?",
+ "Můj otec je povoláním učitel.",
+ "Je vás doma hodně?",
+ "Julie představuje svého psa Trixiho.",
+};
+
+GtkWidget *u3ex10(UnitCtx *unit) {
+ return build_assembly(unit, "Sätze bauen", "sub3_satz", 10,
+ ex10_u3_items, ex10_u3_meaning,
+ G_N_ELEMENTS(ex10_u3_items));
+}
+
+/* ---- keyword-typed builder (ex11 + ex14) ----------------------------- */
+
+
+
+void u3_kw_check(GtkButton *button, gpointer data) {
+ U3KwCtx *ctx = data;
+ int ok = 0;
+
+ (void)button;
+
+ for (int i = 0; i < ctx->n; i++) {
+ const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(ctx->entries[i]));
+ gchar *norm = normalize_answer(txt);
+ gboolean good = txt && txt[0];
+ if (good) {
+ gchar **parts = g_strsplit(ctx->qs[i].answers, "|", -1);
+ good = FALSE;
+ for (int j = 0; parts[j]; j++) {
+ gchar *a = normalize_answer(parts[j]);
+ if (a[0] && g_strrstr(norm, a)) {
+ good = TRUE;
+ g_free(a);
+ break;
+ }
+ g_free(a);
+ }
+ g_strfreev(parts);
+ }
+ g_free(norm);
+ answer_mark(ctx->entries[i], good);
+ if (good)
+ ok++;
+ }
+
+ for (int i = 0; i < ctx->n; i++)
+ if (ctx->models[i])
+ gtk_widget_set_visible(ctx->models[i], TRUE);
+
+ 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"));
+ }
+}
+
+GtkWidget *u3_build_kw(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub, const char *sample,
+ const U3Kw *qs, int n) {
+ GtkWidget *body, *feedback, *check;
+ GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
+ &body, &feedback, &check);
+ U3KwCtx *ctx = g_new0(U3KwCtx, 1);
+
+ ctx->qs = qs;
+ ctx->n = n;
+ ctx->unit = unit;
+ ctx->ex_num = ex_num;
+ ctx->feedback = feedback;
+ ctx->entries = g_new0(GtkWidget *, n);
+ ctx->trans = g_new0(GtkWidget *, n);
+ ctx->models = g_new0(GtkWidget *, n);
+
+ {
+ gboolean need = FALSE;
+
+ for (int i = 0; i < n && !need; i++) {
+ need = text_has_german_umlaut(qs[i].prompt) ||
+ text_has_german_umlaut(qs[i].answers) ||
+ text_has_german_umlaut(qs[i].german);
+ }
+ if (need)
+ add_umlaut_note(body);
+ }
+
+ if (sample && sample[0])
+ u3_sample_line(body, sample);
+
+ for (int i = 0; i < n; i++) {
+ GtkWidget *prompt = gtk_label_new(qs[i].prompt);
+ GtkWidget *entry;
+
+ gtk_widget_set_halign(prompt, GTK_ALIGN_START);
+ gtk_widget_add_css_class(prompt, "ex-prompt");
+ gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
+ gtk_box_append(GTK_BOX(body), prompt);
+
+ entry = gtk_entry_new();
+ gtk_widget_set_hexpand(entry, FALSE);
+ gtk_widget_set_halign(entry, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(body), entry);
+ ctx->entries[i] = entry;
+
+ if (qs[i].mean)
+ ctx->trans[i] = meaning_add(body, qs[i].mean);
+
+ /* Czech meaning acts as a hint and stays visible. */
+ if (ctx->trans[i])
+ gtk_widget_set_visible(ctx->trans[i], TRUE);
+
+ /* The model German sentence is only shown after clicking Check. */
+ if (qs[i].german && qs[i].german[0]) {
+ GtkWidget *model = gtk_label_new(qs[i].german);
+
+ gtk_widget_set_halign(model, GTK_ALIGN_START);
+ gtk_label_set_xalign(GTK_LABEL(model), 0.0);
+ gtk_label_set_wrap(GTK_LABEL(model), TRUE);
+ gtk_widget_set_margin_top(model, 2);
+ gtk_widget_set_margin_bottom(model, 2);
+ gtk_widget_set_margin_start(model, 8);
+ gtk_widget_add_css_class(model, "u3-answer");
+ gtk_widget_set_visible(model, FALSE);
+ gtk_box_append(GTK_BOX(body), model);
+ ctx->models[i] = model;
+ }
+ }
+
+ g_signal_connect(check, "clicked", G_CALLBACK(u3_kw_check), ctx);
+ return page;
+}
+
+/* ---- ex11: Was siehst du? -------------------------------------------- */
+
+const U3Kw ex11_u3_qs[] = {
+ {"Ein altes Gebäude aus dem Mittelalter. Es steht auf einem Berg.",
+ "burg|schloss", "Vidím hrad / zámek.",
+ "Ich sehe eine Burg. / Ich sehe ein Schloss."},
+ {"Wasser, Bäume und Berge. Es ist sehr ruhig.",
+ "see", "Vidím jezero.", "Ich sehe einen See."},
+ {"Eine Frau, ein Mann und zwei Kinder essen zusammen.",
+ "familie", "Vidím rodinu.", "Ich sehe eine Familie."},
+};
+
+GtkWidget *u3ex11(UnitCtx *unit) {
+ return u3_build_kw(unit, 11, "Was siehst du?", "sub3_sehen",
+ "Beispiel: Ich sehe eine Burg. / Ich sehe ein Schloss.",
+ ex11_u3_qs, G_N_ELEMENTS(ex11_u3_qs));
+}
+
+/* ---- ex12: Wem gehört das? ------------------------------------------- */
+
+const char *ex12_u3_pool[] = {
+ "mein", "meine", "dein", "deine", "sein", "seine",
+ "ihr", "ihre", "unser", "unsere", "euer", "eure",
+};
+
+const U3Fill ex12_u3_rows[] = {
+ {"1.", "Das ist Tom. → Das ist ", "sein", " Handy.", NULL, NULL, NULL,
+ NULL, "To je jeho mobil."},
+ {"2.", "Das ist Maria. → Das ist ", "ihr", " Kaninchen.", NULL, NULL,
+ NULL, NULL, "To je její králík."},
+ {"3.", "Das sind Max und Lisa. → Das sind ", "ihre", " Inliner.", NULL,
+ NULL, NULL, NULL, "To jsou jejich brusle."},
+ {"4.", "Das ist Frau Berger. → Das ist ", "ihr", " Kuchen.", NULL, NULL,
+ NULL, NULL, "To je její dort."},
+ {"5.", "Das ist Herr Müller. → Das ist ", "sein", " Haus.", NULL, NULL,
+ NULL, NULL, "To je jeho dům."},
+ {"6.", "Das sind die Schmidts. → Das sind ", "ihre", " Kinder.", NULL,
+ NULL, NULL, NULL, "To jsou jejich děti."},
+};
+
+GtkWidget *u3ex12(UnitCtx *unit) {
+ return u3_build_drop(unit, 12, "Wem gehört das?", "sub3_wem", NULL,
+ ex12_u3_rows, G_N_ELEMENTS(ex12_u3_rows),
+ ex12_u3_pool, G_N_ELEMENTS(ex12_u3_pool), NULL, TRUE);
+}
+
+/* ---- ex13: Es gibt + unbestimmter Artikel ---------------------------- */
+
+const char *ex13_u3_pool[] = {"ein", "eine", "einen"};
+
+const U3Fill ex13_u3_rows[] = {
+ {"1.", "(das Konzert) → Es gibt ", "ein", " Konzert.", NULL, NULL, NULL,
+ NULL, "Je tu koncert."},
+ {"2.", "(die Ausstellung) → Es gibt ", "eine", " Ausstellung.", NULL,
+ NULL, NULL, NULL, "Je tu výstava."},
+ {"3.", "(das Sportfest) → Es gibt ", "ein", " Sportfest.", NULL, NULL,
+ NULL, NULL, "Je tu sportovní slavnost."},
+ {"4.", "(das Familientreffen) → Es gibt ", "ein", " Familientreffen.",
+ NULL, NULL, NULL, NULL, "Je tu rodinné setkání."},
+ {"5.", "(die Theatervorstellung) → Es gibt ", "eine",
+ " Theatervorstellung.", NULL, NULL, NULL, NULL,
+ "Je tu divadelní představení."},
+ {"6.", "(der Flohmarkt) → Es gibt ", "einen", " Flohmarkt.", NULL, NULL,
+ NULL, NULL, "Je tu bleší trh."},
+};
+
+GtkWidget *u3ex13(UnitCtx *unit) {
+ return u3_build_drop(unit, 13, "Es gibt …", "sub3_gibt", NULL,
+ ex13_u3_rows, G_N_ELEMENTS(ex13_u3_rows),
+ ex13_u3_pool, G_N_ELEMENTS(ex13_u3_pool), NULL, TRUE);
+}
+
+/* ---- ex14: Beschreiben (sentence pairs) ------------------------------ */
+
+const U3Kw ex14_u3_qs[] = {
+ {"Eine Frau fährt Fahrrad. → Ich sehe ________.",
+ "eine frau|frau", "Vidím ženu.", "Ich sehe eine Frau."},
+ {"→ Sie ________.",
+ "sie fährt fahrrad|sie fährt|fährt fahrrad|fährt", "Jezdí na kole.",
+ "Sie fährt Fahrrad."},
+ {"Ein Mann wandert im Wald. → Ich sehe ________.",
+ "einen mann|mann", "Vidím muže.", "Ich sehe einen Mann."},
+ {"→ Er ________.",
+ "er wandert im wald|er wandert|wandert im wald|wandert",
+ "Chodí po lese.", "Er wandert im Wald."},
+ {"Zwei Kinder spielen Fußball. → Ich sehe ________.",
+ "zwei kinder|kinder", "Vidím dvě děti.", "Ich sehe zwei Kinder."},
+ {"→ Sie ________.",
+ "sie spielen fußball|sie spielen fussball|spielen fußball|spielen "
+ "fussball|spielen", "Hrají fotbal.", "Sie spielen Fußball."},
+};
+
+GtkWidget *u3ex14(UnitCtx *unit) {
+ return u3_build_kw(unit, 14, "Beschreiben", "sub3_saetze",
+ "Beispiel: Ich sehe einen Jungen. Er liest ein Buch.",
+ ex14_u3_qs, G_N_ELEMENTS(ex14_u3_qs));
+}
+
+/* ---- ex15: missing letters (Wochenende) ------------------------------ */
+
+const U3Let ex15_u3_rows[] = {
+ {"1.", "Meine Gr", "oß", "eltern fahr", "en", " ins Wochen", "end",
+ "haus.", NULL, NULL,
+ "Moji prarodiče jedou o víkendu do víkendového domu."},
+ {"2.", "Mein ", "Va", "ter und mein", "e", " Mutter besuch", "en",
+ " ein Konz", "er", "t.",
+ "Můj otec a moje matka navštíví koncert."},
+ {"3.", "Mein", "e", " Sch", "w", "ester geh", "t", " ins K", "in", "o.",
+ "Moje sestra jde do kina."},
+ {"4.", "Mein Bru", "d", "er f", "äh", "rt an ", "ei", "nen S", "ee",
+ ".",
+ "Můj bratr jede k jezeru."},
+ {"5.", "Und ich? Ich besuch", "e", " ein Einka", "ufs", "zentrum.",
+ NULL, NULL, NULL, NULL,
+ "A já? Navštívím obchodní centrum."},
+};
+
+GtkWidget *u3ex15(UnitCtx *unit) {
+ return u3_build_letters(unit, 15, "Wochenende", "sub3_wochen",
+ ex15_u3_rows, G_N_ELEMENTS(ex15_u3_rows));
+}
diff --git a/globals.c b/globals.c
new file mode 100644
index 0000000..7251a3f
--- /dev/null
+++ b/globals.c
@@ -0,0 +1,81 @@
+#include "maturita.h"
+
+GtkStack *main_stack;
+
+GtkWindow *main_window;
+
+GtkWidget *welcome_heading;
+
+GtkCssProvider *theme_provider;
+
+ThemeId app_theme_id = THEME_CATPPUCCIN;
+
+ColorMode app_color_mode = MODE_DARK;
+
+UiLang app_lang = LANG_CS;
+
+ThemePalette app_theme;
+
+const char *theme_names[THEME_COUNT] = {
+ "Catppuccin",
+ "Nord",
+ "Dracula",
+ "Rose Pine",
+ "Ocean",
+ "Gruvbox",
+ "Solarized",
+ "Everforest",
+ "Monokai",
+ "One Dark",
+};
+
+GtkWidget *theme_swatch_areas[THEME_COUNT];
+
+UnitCtx units[NUM_UNITS] = {0};
+
+const char *const u1_ex_names[MAX_UNIT_EX + 1] = {
+ NULL,
+ "Dialog", "Sätze bilden", "Was ist richtig?", "Freie Antwort",
+ "Zahlen", "Wie viel?", "Zahlenreihe", "Verb einsetzen",
+ "Wer? Wie? Wo?", "Wörter trennen", "Grußformen",
+ "Was macht er/sie?", "Länder",
+};
+
+const char *const u2_ex_names[MAX_UNIT_EX + 1] = {
+ NULL,
+ "Verben konjugieren", "aus oder in", "Fragewörter", "sprechen",
+ "Nationalitäten", "Woher?", "Länder schreiben", "Verben einsetzen",
+ "Freie Antwort", "Euro", "Wörter suchen", "Was ist richtig?",
+ "Ordne zu", "Lückentext", "Verbinde", "Zahlen",
+ "Steckbrief", "Berufe", "Nationalität",
+};
+
+const char *const u3_ex_names[MAX_UNIT_EX + 1] = {
+ NULL,
+ "Familienpaare", "mein oder dein", "ein / kein", "Lückentext",
+ "Marcos Familie", "Sortieren", "Possessivtabelle", "Akkusativ",
+ "kein / nicht", "Sätze bauen", "Was siehst du?", "Wem gehört das?",
+ "Es gibt …", "Beschreiben", "Wochenende",
+};
+
+GtkWidget *road_scroll;
+
+GtkWidget *road_fixed;
+
+GtkWidget *road_rail;
+
+GtkWidget *road_nodes[NUM_UNITS];
+
+GtkWidget *road_labels[NUM_UNITS];
+
+double road_cx[NUM_UNITS];
+
+double road_cy[NUM_UNITS];
+
+int road_cols = NUM_UNITS;
+
+int road_rows = 1;
+
+int road_cw = (int)(2.0 * ROAD_MX + (NUM_UNITS - 1) * PATH_SPAC);
+
+int road_ch = (int)(2.0 * ROAD_MY);
diff --git a/i18n.c b/i18n.c
new file mode 100644
index 0000000..8fa9cbf
--- /dev/null
+++ b/i18n.c
@@ -0,0 +1,695 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* i18n */
+/* ------------------------------------------------------------------ */
+
+
+void ex4_fill_sample(GtkWidget *label);
+
+GArray *i18n_binds;
+
+
+const TrEntry tr_ui[] = {
+ {"back", "Zpět", "Back"},
+ {"settings", "Nastavení", "Settings"},
+ {"settings_title", "Nastavení", "Settings"},
+ {"mode", "REŽIM", "MODE"},
+ {"mode_dark", "Tmavý", "Dark"},
+ {"mode_light", "Světlý", "Light"},
+ {"theme", "TÉMA", "THEME"},
+ {"language", "JAZYK", "LANGUAGE"},
+ {"welcome_body",
+ "maturita.C je vzdělávací program pro studenty středních škol a "
+ "gymnázií na přípravu k maturitě.\n\n"
+ "Program je napsaný v Céčku studentama ze SSŠVT!",
+ "maturita.C is an educational app for high-school and gymnasium "
+ "students to prepare for their maturita exam.\n\n"
+ "The program is written in C by students from SSŠVT!"},
+ {"continue", "Pokračuj", "Continue"},
+ {"roadmap_title", "Učební plán", "Learning path"},
+ {"roadmap_sub", "Vyberte jednotku na cestě a začněte procvičovat.",
+ "Pick a unit on the path and start practicing."},
+ {"unit1_sub", "Vyberte cvičení a dokončete je.",
+ "Choose an exercise and complete it."},
+ {"check", "Zkontrolovat", "Check"},
+ {"finish", "Dokončit", "Finish"},
+ {"show_sample", "Ukázat vzor", "Show sample"},
+ {"your_answer", "Vaše odpověď…", "Your answer…"},
+ {"feedback_ok", "Výborně!", "Great!"},
+ {"feedback_retry", "Ještě to není správně. Zkuste to znovu.",
+ "Not quite right yet. Try again."},
+ {"feedback_retry_short", "Ještě to není správně.", "Not quite right yet."},
+ {"feedback_sentences", "Některé věty nejsou správně. Zkuste to znovu.",
+ "Some sentences are not correct. Try again."},
+ {"tip_ss", "Písmeno „ß“ se dá na klávesnici zaměnit za „ss“!",
+ "You can type “ss” instead of “ß” on the keyboard!"},
+ {"hint_umlauts",
+ "Na klávesnici nemáte německé znaky? Můžete psát ä jako ae, ö jako oe, "
+ "ü jako ue a ß jako ss.",
+ "No German keyboard? You can type ä as ae, ö as oe, ü as ue and ß as "
+ "ss."},
+ {"sample_fmt", "Otázka: %s\nVzor: %s (česky: %s)",
+ "Question: %s\nSample: %s (English: %s)"},
+ {"sub_dialog", "Doplňte chybějící slova v dialogu.",
+ "Fill in the missing words in the dialogue."},
+ {"sub_assembly", "Přetáhněte slova do správného pořadí.",
+ "Drag the words into the correct order."},
+ {"sub_choice_num", "Vyberte správnou číslovku.",
+ "Choose the correct numeral."},
+ {"sub_free", "Odpovězte vlastními slovy, poté klikněte na Dokončit.",
+ "Answer in your own words, then click Finish."},
+ {"sub_zahlen", "Vyberte správné slovo pro dané číslo.",
+ "Choose the correct word for the given number."},
+ {"sub_wieviel", "Spočítejte předměty a vyberte počet.",
+ "Count the items and choose the amount."},
+ {"sub_reihe", "Doplňte chybějící číslovku.",
+ "Fill in the missing numeral."},
+ {"sub_verb", "Vyberte správný tvar slovesa.",
+ "Choose the correct verb form."},
+ {"sub_wer", "Vyberte správné tázací slovo.",
+ "Choose the correct question word."},
+ {"sub_bild", "Vyberte správný tvar slovesa podle obrázku.",
+ "Choose the correct verb form for the picture."},
+ {"sub_gruss", "Roztřiďte pozdravy na pozdravy a rozloučení.",
+ "Sort the phrases into greetings and farewells."},
+ {"sub_land", "Přiřaďte symboly ke správné zemi.",
+ "Match the symbols to the correct country."},
+ {"assign_hint",
+ "Vyberte skupinu a klikněte na kartu. Kliknutím na kartu ve skupině "
+ "ji vrátíte zpět.",
+ "Pick a group and click a card. Click a card inside a group to send "
+ "it back."},
+ {"unit2_sub", "Vyberte cvičení a dokončete je.",
+ "Choose an exercise and complete it."},
+ {"Bei uns zu Hause", "Bei uns zu Hause", "At Home with Us"},
+ {"unit3_sub", "Vyberte cvičení a dokončete je.",
+ "Choose an exercise and complete it."},
+ {"sub3_pair", "Přiřaďte každému slovu jeho protějšek.",
+ "Match each word with its counterpart."},
+ {"sub3_poss", "Doplňte přivlastňovací zájmena (1. pád).",
+ "Fill in the possessive pronouns (nominative)."},
+ {"sub3_haustier", "Doplňte ein/kein ve správném tvaru.",
+ "Fill in ein/kein in the correct form."},
+ {"sub3_buchst", "Doplňte chybějící písmena.",
+ "Fill in the missing letters."},
+ {"sub3_marco", "Doplňte slova z nabídky.",
+ "Fill in the words from the box."},
+ {"sub3_sort", "Roztřiďte slova do dvou skupin.",
+ "Sort the words into two groups."},
+ {"sub3_tabelle", "Doplňte přivlastňovací zájmena do tabulky.",
+ "Fill the possessive pronouns into the table."},
+ {"sub3_akk", "Doplňte přivlastňovací zájmena (4. pád).",
+ "Fill in the possessive pronouns (accusative)."},
+ {"sub3_nicht", "Vyberte kein/keine/keinen/nicht.",
+ "Choose kein/keine/keinen/nicht."},
+ {"sub3_satz", "Sestavte správné věty.",
+ "Build the correct sentences."},
+ {"sub3_sehen", "Napište větu „Ich sehe…“.",
+ "Write an “Ich sehe…” sentence."},
+ {"sub3_wem", "Komu to patří? Doplňte zájmeno.",
+ "Whose is it? Fill in the pronoun."},
+ {"sub3_gibt", "Doplňte „Es gibt“ + ein/eine/einen.",
+ "Fill in “Es gibt” + ein/eine/einen."},
+ {"sub3_saetze", "Doplňte věty podle vzoru.",
+ "Complete the sentences following the example."},
+ {"sub3_wochen", "Doplňte chybějící písmena.",
+ "Fill in the missing letters."},
+ {"wordbank", "Nabídka slov:", "Word bank:"},
+ {"sample_line", "Příklad: %s", "Sample: %s"},
+ {"sub_verben", "Doplňte sloveso ve správném tvaru.",
+ "Fill in the verb in the correct form."},
+ {"sub_ausin", "Vyberte správnou předložku: aus nebo in.",
+ "Choose the correct preposition: aus or in."},
+ {"sub_sprich", "Doplňte sloveso „sprechen“.",
+ "Fill in the verb “sprechen”."},
+ {"sub_nation", "Doplňte národnost – použijte slova z nabídky.",
+ "Fill in the nationality – use the words in the box."},
+ {"sub_woher", "Odkud lidé pocházejí? Napište zemi.",
+ "Where are the people from? Type the country."},
+ {"sub_laender", "Doplňte chybějící písmena v názvech zemí.",
+ "Type the full country name (the missing letters are shown as _)."},
+ {"sub_verb2", "Vyberte správný tvar slovesa.",
+ "Choose the correct verb form."},
+ {"sub_euro", "Napište částku slovy.",
+ "Write the amount in words."},
+ {"sub_wortsuchen", "Najděte skryté slovo a napište ho.",
+ "Find the hidden word and type it."},
+ {"sub_ordne", "Přiřaďte správné tázací slovo a odpověď.",
+ "Match each phrase with the right question word and answer."},
+ {"sub_luecke", "Doplňte chybějící slova do textu.",
+ "Fill the missing words into the text."},
+ {"sub_verbinde", "Spojte každé podstatné jméno se správným slovesem.",
+ "Match each noun with the right verb."},
+ {"sub_zahlpaar", "Spojte čísla s německými slovy.",
+ "Match the numbers with the German words."},
+ {"sub_steckbrief", "Doplňte údaje o sobě.",
+ "Complete the information about yourself."},
+ {"sub_berufe", "Hádejte povolání dřív, než bude oběšenec hotový.",
+ "Guess each job before the hangman drawing is finished."},
+ {"sub_bistdu", "Odpovězte podle vlajky – jakou máte národnost?",
+ "Answer according to the flag – what is your nationality?"},
+ {"hm_progress", "Slovo %d / %d", "Word %d / %d"},
+ {"hm_hint", "Nápověda", "Hint"},
+ {"hm_wrong", "Slovo je uhodnuto!", "Word guessed!"},
+ {"hm_fail", "Oběšenec! Slovo bylo „%s“.",
+ "Hanged! The word was “%s”."},
+ {"hm_retry", "Zkusit znovu", "Try again"},
+ {"hm_done", "Výborně! Uhodli jste všechna povolání.",
+ "Great! You guessed all the jobs."},
+ {"hm_guess", "Hádejte písmeno", "Guess a letter"},
+ {"stats", "Statistiky", "Statistics"},
+ {"stats_title", "Statistiky", "Statistics"},
+ {"stats_sub", "Přehled vašeho pokroku.", "Overview of your progress."},
+ {"stats_ex_label", "Cvičení", "Exercises"},
+ {"stats_pct_label", "Hotovo", "Done"},
+ {"stats_units_label", "Jednotky", "Units"},
+ {"stats_section", "Předměty", "Subjects"},
+ {"stats_locked", "Zamčeno", "Locked"},
+ {"stats_complete", "Hotovo", "Complete"},
+ {"stats_ex_fmt", "%d / %d", "%d / %d"},
+ {"stats_pct_fmt", "%d %%", "%d %%"},
+ {"stats_units_fmt", "%d / %d", "%d / %d"},
+ {"subjects_title", "Předměty", "Subjects"},
+ {"subjects_sub", "Zatím je dostupný předmět Deutsch.",
+ "Only Deutsch is available so far."},
+ {"Český jazyk a literatura", "Český jazyk a literatura",
+ "Czech Language and Literature"},
+ {"Občanská nauka", "Občanská nauka", "Civics"},
+ {"English", "English", "English"},
+ {"Deutsch", "Deutsch", "Deutsch"},
+ {"Matematika", "Matematika", "Mathematics"},
+ {"Fyzika", "Fyzika", "Physics"},
+ {"Základy Přírodopisných věd", "Základy Přírodopisných věd",
+ "Fundamentals of Natural Sciences"},
+ {"Technická grafika", "Technická grafika", "Technical Drawing"},
+ {"Prezentační grafika", "Prezentační grafika", "Presentation Graphics"},
+ {"Správa počítačových sítí", "Správa počítačových sítí",
+ "Computer Network Administration"},
+ {"Programování", "Programování", "Programming"},
+ {"Technické vybavení", "Technické vybavení", "Computer Hardware"},
+ {"Programové vybavení", "Programové vybavení", "Software"},
+ {"net_sub", "Vyberte lekci na cestě a začněte procvičovat.",
+ "Pick a unit on the path and start practicing."},
+ {"net_unit1", "Výpočet IP adres", "IP Address Calculation"},
+ {"net_unit1_sub", "4 snímky s postupem a pak cvičení.",
+ "4 slides with the method, then an exercise."},
+ {"net_ex_title", "Cvičení: výpočet IP adres",
+ "Exercise: IP address calculation"},
+ {"net_slide_next", "Další", "Next"},
+ {"net_slide_prev", "Zpět", "Back"},
+ {"net_slide_start", "Přejít na cvičení", "Go to the exercise"},
+ {"net_solution", "Ukázat řešení", "Show solution"},
+ {NULL, NULL, NULL}
+};
+
+/* Exercise content: the Czech text doubles as the lookup key, so `cs`
+ * stays NULL and tr() falls back to the key itself. */
+const TrEntry tr_content[] = {
+ {"Ahoj! Jmenuji se Anna.", NULL, "Hi! My name is Anna."},
+ {"Ahoj! Jmenuji se Petr.", NULL, "Hi! My name is Petr."},
+ {"Jmenuji se Anna.", NULL, "My name is Anna."},
+ {"Jmenuji se Petr.", NULL, "My name is Petr."},
+ {"Odkud jsi?", NULL, "Where are you from?"},
+ {"Odkud pocházíš?", NULL, "Where do you come from?"},
+ {"Pocházím z Česka.", NULL, "I come from Czechia."},
+ {"Dobré ráno!", NULL, "Good morning!"},
+ {"Jak se máš?", NULL, "How are you?"},
+ {"Mám se dobře, děkuji.", NULL, "I am fine, thank you."},
+ {"Musím jít. Na shledanou!", NULL, "I have to go. Goodbye!"},
+ {"Brzy na viděnou!", NULL, "See you soon!"},
+ {"Čau!", NULL, "Bye!"},
+ {"Jak se jmenuješ?", NULL, "What is your name?"},
+ {"Kde bydlíš?", NULL, "Where do you live?"},
+ {"Kdo to je?", NULL, "Who is that?"},
+ {"Kolik je ti let?", NULL, "How old are you?"},
+ {"Je mi šestnáct let.", NULL, "I am sixteen years old."},
+ {"Bydlím v Praze.", NULL, "I live in Prague."},
+ {"Hraji fotbal.", NULL, "I play football."},
+ {"Co rád/a děláš?", NULL, "What do you like to do?"},
+ {"Kolik je hodin?", NULL, "What time is it?"},
+ {"Na shledanou.", NULL, "Goodbye."},
+ {"Moc děkuji.", NULL, "Thank you very much."},
+ {"„vierzehn“ = čtrnáct (14)", NULL, "“vierzehn” = fourteen (14)"},
+ {"„siebzehn“ = sedmnáct (17)", NULL, "“siebzehn” = seventeen (17)"},
+ {"„zwanzig“ = dvacet (20)", NULL, "“zwanzig” = twenty (20)"},
+ {"„sechs“ = šest (6)", NULL, "“sechs” = six (6)"},
+ {"čtrnáct", NULL, "fourteen"},
+ {"sedmnáct", NULL, "seventeen"},
+ {"dvacet", NULL, "twenty"},
+ {"šest", NULL, "six"},
+ {"třináct", NULL, "thirteen"},
+ {"dvě jablka", NULL, "two apples"},
+ {"pět hvězd", NULL, "five stars"},
+ {"sedm koček", NULL, "seven cats"},
+ {"devět květin", NULL, "nine flowers"},
+ {"tři auta", NULL, "three cars"},
+ {"jedenáct knih", NULL, "eleven books"},
+ {"čtyři svíčky", NULL, "four candles"},
+ {"osm míčů", NULL, "eight balls"},
+ {"šest ptáků", NULL, "six birds"},
+ {"šest, sedm, osm, devět", NULL, "six, seven, eight, nine"},
+ {"dvanáct, třináct, čtrnáct, patnáct", NULL,
+ "twelve, thirteen, fourteen, fifteen"},
+ {"osmnáct, devatenáct, dvacet, dvacet jedna", NULL,
+ "eighteen, nineteen, twenty, twenty-one"},
+ {"čtyři, pět, šest, sedm", NULL, "four, five, six, seven"},
+ {"deset, jedenáct, dvanáct, třináct", NULL,
+ "ten, eleven, twelve, thirteen"},
+ {"Plave v jezeře.", NULL, "He/She is swimming in the lake."},
+ {"Zpívá píseň.", NULL, "He/She is singing a song."},
+ {"Vaří polévku.", NULL, "He/She is cooking soup."},
+ {"Čte knihu.", NULL, "He/She is reading a book."},
+ {"Řídí auto.", NULL, "He/She is driving a car."},
+ {"Maluje obraz.", NULL, "He/She is painting a picture."},
+ {"Hraje fotbal.", NULL, "He/She is playing football."},
+ {"Spí.", NULL, "He/She is sleeping."},
+ {"Ahoj", NULL, "Hi"},
+ {"Dobré ráno", NULL, "Good morning"},
+ {"Dobrý den", NULL, "Good day"},
+ {"Dobrý večer", NULL, "Good evening"},
+ {"Dobrý den (Bavorsko, Rakousko)", NULL, "Good day (Bavaria, Austria)"},
+ {"Ahoj / Čau", NULL, "Hi / Hey"},
+ {"Čau", NULL, "Bye"},
+ {"Na shledanou", NULL, "Goodbye"},
+ {"Brzy na viděnou", NULL, "See you soon"},
+ {"Do zítřka", NULL, "See you tomorrow"},
+ {"Dobrou noc", NULL, "Good night"},
+ {"Zatím / Na viděnou", NULL, "See you later"},
+ {"Preclík", NULL, "Pretzel"},
+ {"Klobása", NULL, "Sausage"},
+ {"Braniborská brána", NULL, "Brandenburg Gate"},
+ {"Značka aut", NULL, "Car brand"},
+ {"Rakouský skladatel", NULL, "Austrian composer"},
+ {"Čokoládový dort (Vídeň)", NULL, "Chocolate cake (Vienna)"},
+ {"Vídeňský řízek", NULL, "Viennese schnitzel"},
+ {"Sýr", NULL, "Cheese"},
+ {"Švýcarská hora", NULL, "Swiss mountain"},
+ {"Čokoláda", NULL, "Chocolate"},
+ /* ---- unit 2: grammar & vocabulary meanings ---- */
+ {"Pochází z Rakouska.", NULL, "He is from Austria."},
+ {"Bydlí ve Štýrském Hradci (Graz).", NULL, "He lives in Graz."},
+ {"Rád hraje golf.", NULL, "He likes playing golf."},
+ {"Mluví německy, anglicky a italsky.", NULL,
+ "He speaks German, English and Italian."},
+ {"Pocházejí ze Slovenska.", NULL, "They are from Slovakia."},
+ {"Bydlí v Bratislavě (ona).", NULL, "She lives in Bratislava."},
+ {"Ona ráda tancuje.", NULL, "She likes dancing."},
+ {"On rád cestuje.", NULL, "He likes travelling."},
+ {"Jsou dobří přátelé.", NULL, "They are good friends."},
+ {"Pocházím z Atén.", NULL, "I am from Athens."},
+ {"Petr bydlí v Salcburku.", NULL, "Peter lives in Salzburg."},
+ {"Berlín leží v Německu.", NULL, "Berlin is in Germany."},
+ {"Bydlíme v Paříži.", NULL, "We live in Paris."},
+ {"Pocházejí z Řecka.", NULL, "They are from Greece."},
+ {"Odkud je? – Z Londýna.", NULL, "Where is he from? – From London."},
+ {"Jak se jmenuje? – Andrea.", NULL, "What is her name? – Andrea."},
+ {"Kdo pochází z Ruska? – Alexandr.", NULL,
+ "Who comes from Russia? – Alexander."},
+ {"Kde leží Praha? – V Česku.", NULL, "Where is Prague? – In Czechia."},
+ {"Kde bydlí? – V Bratislavě.", NULL, "Where do they live? – In Bratislava."},
+ {"Co děláte? – Studujeme germanistiku.", NULL,
+ "What are you doing? – We're studying German studies."},
+ {"Mluvím německy. A ty?", NULL, "I speak German. And you?"},
+ {"Kterými jazyky mluvíte?", NULL, "Which languages do you speak?"},
+ {"Mluvíš francouzsky? – Ano, trochu.", NULL,
+ "Do you speak French? – Yes, a little."},
+ {"My mluvíme polsky a vy?", NULL, "We speak Polish and you?"},
+ {"Mluvíte anglicky? – Ano, velmi dobře.", NULL,
+ "Do you speak English? – Yes, very well."},
+ {"Nemluví ani slovo turecky.", NULL,
+ "He doesn't speak a word of Turkish."},
+ {"Čech", NULL, "Czech man"},
+ {"Španělka", NULL, "Spanish woman"},
+ {"Turek", NULL, "Turk (man)"},
+ {"Chorvatka", NULL, "Croatian woman"},
+ {"Němka", NULL, "German woman"},
+ {"Rakušanka", NULL, "Austrian woman"},
+ {"Slovák", NULL, "Slovak man"},
+ {"Švýcarka", NULL, "Swiss woman"},
+ {"Němec", NULL, "German man"},
+ {"Německo", NULL, "Germany"},
+ {"Deutschland – Německo", NULL, "Deutschland – Germany"},
+ {"Slowakei – Slovensko", NULL, "Slowakei – Slovakia"},
+ {"Österreich – Rakousko", NULL, "Österreich – Austria"},
+ {"Spanien – Španělsko", NULL, "Spanien – Spain"},
+ {"England – Anglie", NULL, "England – England"},
+ {"Polen – Polsko", NULL, "Polen – Poland"},
+ {"Slovensko", NULL, "Slovakia"},
+ {"Rakousko", NULL, "Austria"},
+ {"Španělsko", NULL, "Spain"},
+ {"Anglie", NULL, "England"},
+ {"Polsko", NULL, "Poland"},
+ {"Rusko", NULL, "Russia"},
+ {"Russland – Rusko", NULL, "Russland – Russia"},
+ {"Türkei – Turecko", NULL, "Türkei – Turkey"},
+ {"Slowakei – Slovensko", NULL, "Slowakei – Slovakia"},
+ {"Kroatien – Chorvatsko", NULL, "Kroatien – Croatia"},
+ {"Griechenland – Řecko", NULL, "Griechenland – Greece"},
+ {"Italien – Itálie", NULL, "Italien – Italy"},
+ {"Turecko", NULL, "Turkey"},
+ {"Chorvatsko", NULL, "Croatia"},
+ {"Řecko", NULL, "Greece"},
+ {"Itálie", NULL, "Italy"},
+ {"otec", NULL, "father"},
+ {"rodiče", NULL, "parents"},
+ {"Češka", NULL, "Czech woman"},
+ {"student", NULL, "student"},
+ {"Co čte? – Knihu.", NULL, "What is he reading? – A book."},
+ {"Co studuje? – Medicínu.", NULL, "What is she studying? – Medicine."},
+ {"Čím Jan rád jezdí? – Autem.", NULL,
+ "What does Jan like to drive? – A car."},
+ {"Kde bydlíte? – Ve Vídni.", NULL, "Where do you live? – In Vienna."},
+ {"Kdo pracuje u Siemensu? – Moje kamarádka Věra.", NULL,
+ "Who works at Siemens? – My friend Vera."},
+ {"Kam jedeš?", NULL, "Where are you going?"},
+ {"Co rád/a čteš?", NULL, "What do you like to read?"},
+ {"Co rád/a hraješ?", NULL, "What do you like to play?"},
+ {"Jmenuji se Lukáš.", NULL, "My name is Lukáš."},
+ {"Jedu do Německa.", NULL, "I'm going to Germany."},
+ {"Rád/a čtu detektivky.", NULL, "I like reading crime novels."},
+ {"Rád/a hraju golf.", NULL, "I like playing golf."},
+ {"šedesát tři eur", NULL, "sixty-three euros"},
+ {"třicet osm eur", NULL, "thirty-eight euros"},
+ {"čtyřicet pět eur", NULL, "forty-five euros"},
+ {"padesát sedm eur", NULL, "fifty-seven euros"},
+ {"devadesát devět eur", NULL, "ninety-nine euros"},
+ {"Agnieszka Kowalski pochází z Polska, z Krakova.", NULL,
+ "Agnieszka Kowalski is from Kraków, Poland."},
+ {"Je jí 18 let a letos dělá maturitu.", NULL,
+ "She is 18 years old and is taking her A-levels this year."},
+ {"Ráda chatuje se svým německým přítelem.", NULL,
+ "She likes chatting with her German friend."},
+ {"Jmenuje se Stefan Böhmermann a bydlí v Drážďanech.", NULL,
+ "His name is Stefan Böhmermann and he lives in Dresden."},
+ {"Agnieszce přijde ten jazyk super.", NULL,
+ "Agnieszka thinks the language is cool."},
+ {"Už mluví velmi dobře německy.", NULL,
+ "She already speaks German very well."},
+ {"potřebovat mobil", NULL, "to need a mobile phone"},
+ {"pracovat ve BMW", NULL, "to work at BMW"},
+ {"řídit auto", NULL, "to drive a car"},
+ {"navštěvovat gymnázium", NULL, "to attend a grammar school"},
+ {"mluvit španělsky", NULL, "to speak Spanish"},
+ {"číst detektivky", NULL, "to read crime novels"},
+ {"padesát čtyři", NULL, "fifty-four"},
+ {"čtyřicet pět", NULL, "forty-five"},
+ {"tři sta šedesát devět", NULL, "three hundred and sixty-nine"},
+ {"sto dvanáct", NULL, "one hundred and twelve"},
+ {"sto dvacet dva", NULL, "one hundred and twenty-two"},
+ {"šedesát osm", NULL, "sixty-eight"},
+ {"Petr rád čte knihy.", NULL, "Peter likes reading books."},
+ {"Filip pracuje v Salcburku.", NULL, "Filip works in Salzburg."},
+ {"Magda dobře mluví anglicky.", NULL, "Magda speaks English well."},
+ {"Marek navštěvuje gymnázium.", NULL, "Marek attends the grammar school."},
+ {"Dominice je 15 let.", NULL, "Dominika is 15 years old."},
+ {"Moje sestra se jmenuje Marta.", NULL, "My sister is called Marta."},
+ {"Jak se jmenují? – Jana a Michael.", NULL,
+ "What are their names? – Jana and Michael."},
+ {"Kdo mluví polsky? – Jacek.", NULL, "Who speaks Polish? – Jacek."},
+ {"Kde bydlí Eva? – V Plzni.", NULL, "Where does Eva live? – In Plzeň."},
+ {"Odkud pochází Markus? – Z Německa.", NULL,
+ "Where is Markus from? – From Germany."},
+ {"Jakým jazykem mluví Jana? – Slovensky.", NULL,
+ "What language does Jana speak? – Slovak."},
+ /* ---- unit 2: hangman hints ---- */
+ {"vaří v restauraci", NULL, "he cooks in a restaurant"},
+ {"léčí nemocné lidi", NULL, "he treats sick people"},
+ {"učí ve škole", NULL, "he teaches at school"},
+ {"pracuje u policie", NULL, "he works for the police"},
+ {"prodává v obchodě", NULL, "he sells in a shop"},
+ /* ---- unit 2: word-level glosses for ex14/ex15 ---- */
+ {"z", NULL, "from"},
+ {"dělá", NULL, "does"},
+ {"kamarád", NULL, "friend"},
+ {"žije", NULL, "lives"},
+ {"jazyk", NULL, "language"},
+ {"dobře", NULL, "well"},
+ {"potřebovat", NULL, "to need"},
+ {"pracovat", NULL, "to work"},
+ {"řídit", NULL, "to drive"},
+ {"navštěvovat", NULL, "to attend"},
+ {"mluvit", NULL, "to speak"},
+ {"číst", NULL, "to read"},
+ /* ---- unit 3: Bei uns zu Hause ---- */
+ /* ex1 family word pairs */
+ {"bratranec / sestřenice", NULL, "male cousin / female cousin"},
+ {"strýc / teta", NULL, "uncle / aunt"},
+ {"dědeček / babička", NULL, "grandfather / grandmother"},
+ {"sestra / bratr", NULL, "sister / brother"},
+ {"kamarád / kamarádka", NULL, "friend (m) / friend (f)"},
+ {"dcera / syn", NULL, "daughter / son"},
+ /* ex2 possessive pronouns (nominative) */
+ {"Je to tvoje matka? – Ne, to není moje matka. To je jeho teta.", NULL,
+ "Is that your mother? – No, that is not my mother. That is his aunt."},
+ /* ex2 word-level glosses for the selected possessive pronouns */
+ {"tvůj", NULL, "yours (m.)"},
+ {"tvoje", NULL, "yours (f.)"},
+ {"můj", NULL, "mine (m.)"},
+ {"moje", NULL, "mine (f.)"},
+ {"jeho", NULL, "his"},
+ {"Je to tvůj bratr? – Ne, to není můj bratr. To je jeho bratranec.", NULL,
+ "Is that your brother? – No, that is not my brother. That is his cousin."},
+ {"Jsou to tvoji rodiče? – Ne, to nejsou moji rodiče. To jsou rodiče Markuse.",
+ NULL, "Are those your parents? – No, those are not my parents. "
+ "Those are Markus's parents."},
+ /* ex3 ein/kein (pets) */
+ {"Máš křečka? – Ne, nemám křečka. Mám želvu.", NULL,
+ "Have you got a hamster? – No, I haven't got a hamster. I have a tortoise."},
+ {"Máš koně? – Ne, nemám koně. Mám králíka.", NULL,
+ "Have you got a horse? – No, I haven't got a horse. I have a rabbit."},
+ {"Máš rybu? – Ne, nemám rybu. Mám ptáka.", NULL,
+ "Have you got a fish? – No, I haven't got a fish. I have a bird."},
+ {"Máš morče? – Ne, nemám morče. Mám andulku.", NULL,
+ "Have you got a guinea pig? – No, I haven't got a guinea pig. I have a budgie."},
+ /* ex4 missing letters (text) */
+ {"Máme přátele v Rakousku. Bydlí ve Vídni.", NULL,
+ "We have friends in Austria. They live in Vienna."},
+ {"Jmenují se Elfriede a Jiří. Mají také děti.", NULL,
+ "They are called Elfriede and Jiri. They also have children."},
+ {"Jejich syn se jmenuje Philipp a jejich dcera Sabine.", NULL,
+ "Their son is called Philipp and their daughter Sabine."},
+ {"Rodina ráda cestuje.", NULL, "The family likes travelling."},
+ {"Často je u nás na návštěvě v Česku.", NULL,
+ "They often visit us in Czechia."},
+ {"Rodina ráda cestuje. Často je u nás na návštěvě v Česku.", NULL,
+ "The family likes travelling. They often visit us in Czechia."},
+ {"Děti se učí také česky.", NULL,
+ "The children are also learning Czech."},
+ /* ex4 word-level glosses for the gapped words */
+ {"mít", NULL, "to have"},
+ {"přátelé", NULL, "friends"},
+ {"bydlet", NULL, "to live"},
+ {"jmenovat se", NULL, "to be called"},
+ {"děti", NULL, "children"},
+ {"syn", NULL, "son"},
+ {"jejich", NULL, "their"},
+ {"rodina", NULL, "family"},
+ {"cestuje", NULL, "travels"},
+ {"často", NULL, "often"},
+ {"návštěva", NULL, "visit"},
+ {"Česko", NULL, "Czechia"},
+ {"učit se", NULL, "to learn"},
+ {"čeština", NULL, "Czech"},
+ /* ex5 Marco's family */
+ {"Máme mužskou domácnost: Můj otec Johann (50), Ivo (16) a já.", NULL,
+ "We live in an all-male household: my father Johann (50), Ivo (16) and me."},
+ {"Ivo a já jsme stejně staří – jsme totiž dvojčata.", NULL,
+ "Ivo and I are the same age – we are twins."},
+ {"Náš otec tu má novou práci.", NULL,
+ "Our father has a new job here."},
+ {"Naši rodiče jsou rozvedení.", NULL,
+ "Our parents are divorced."},
+ {"Naše matka Maja (50) žije ve Vídni, ale pochází z Chorvatska.", NULL,
+ "Our mother Maja (50) lives in Vienna, but she comes from Croatia."},
+ {"Máme v Chorvatsku hodně strýců, tet, sestřenic a bratranců.", NULL,
+ "We have many uncles, aunts, male and female cousins in Croatia."},
+ {"Vídáme je někdy o prázdninách.", NULL,
+ "We sometimes see them on holiday."},
+ /* ex6 sorting vocabulary */
+ {"křeček", NULL, "hamster"},
+ {"babička", NULL, "grandmother"},
+ {"kůň", NULL, "horse"},
+ {"bratr", NULL, "brother"},
+ {"kočka", NULL, "cat"},
+ {"dědeček", NULL, "grandfather"},
+ {"dcera", NULL, "daughter"},
+ {"pes", NULL, "dog"},
+ {"teta", NULL, "aunt"},
+ {"andulka", NULL, "budgerigar"},
+ /* ex8 possessive pronouns (accusative) */
+ {"Potřebuješ svůj mobil.", NULL, "You need your mobile."},
+ {"Má svého koně.", NULL, "He has his horse."},
+ {"Dělá svou párty.", NULL, "She is throwing her party."},
+ {"Ptá se svého učitele.", NULL, "It is asking its teacher."},
+ {"Navštěvujeme náš víkendový dům.", NULL,
+ "We are visiting our weekend house."},
+ {"Vy vidíte svého psa.", NULL, "You see your dog."},
+ {"Mají své nástroje.", NULL, "They have their instruments."},
+ /* ex8 grey notes showing which possessive is expected */
+ {"u3g_dein", "(tvůj)", "(your)"},
+ {"u3g_sein", "(jeho)", "(his)"},
+ {"u3g_ihr_sie", "(její)", "(her)"},
+ {"u3g_sein_es", "(jeho)", "(its)"},
+ {"u3g_unser", "(náš)", "(our)"},
+ {"u3g_euer", "(váš)", "(your)"},
+ {"u3g_ihr_sie_pl", "(jejich)", "(their)"},
+ /* ex9 kein / nicht */
+ {"Mluvíš španělsky? – Ne, nemluvím španělsky.", NULL,
+ "Do you speak Spanish? – No, I don't speak any Spanish."},
+ {"Máš bratra? – Ne, nemám bratra.", NULL,
+ "Have you got a brother? – No, I haven't got a brother."},
+ {"Je v Opavě zoo? – Ne, v Opavě žádná zoo není.", NULL,
+ "Is there a zoo in Opava? – No, there is no zoo in Opava."},
+ {"Jdeš do bazénu? – Ne, do bazénu nejdu.", NULL,
+ "Are you going to the swimming pool? – No, I am not going to the swimming pool."},
+ {"Rád chodí na túry? – Ne, nerad chodí na túry.", NULL,
+ "Does he like hiking? – No, he does not like hiking."},
+ {"Navštívíš dnes zámek? – Ne, dnes nenavštívím žádný zámek.", NULL,
+ "Are you visiting a castle today? – No, I am not visiting a castle today."},
+ /* ex10 sentence order */
+ {"Mají želvu.", NULL, "They have a tortoise."},
+ {"Co dělá tvoje kamarádka o víkendu?", NULL,
+ "What does your friend do at the weekend?"},
+ {"Můj otec je povoláním učitel.", NULL,
+ "My father is a teacher by profession."},
+ {"Je vás doma hodně?", NULL, "Are there many of you at home?"},
+ {"Julie představuje svého psa Trixiho.", NULL,
+ "Julia is introducing her dog Trixi."},
+ /* ex11 what do you see */
+ {"Vidím hrad / zámek.", NULL, "I see a castle."},
+ {"Vidím jezero.", NULL, "I see a lake."},
+ {"Vidím rodinu.", NULL, "I see a family."},
+ /* ex12 whose is it */
+ {"To je jeho mobil.", NULL, "That is his mobile."},
+ {"To je její králík.", NULL, "That is her rabbit."},
+ {"To jsou jejich brusle.", NULL, "Those are their inline skates."},
+ {"To je její dort.", NULL, "That is her cake."},
+ {"To je jeho dům.", NULL, "That is his house."},
+ {"To jsou jejich děti.", NULL, "Those are their children."},
+ /* ex13 es gibt */
+ {"Je tu koncert.", NULL, "There is a concert."},
+ {"Je tu výstava.", NULL, "There is an exhibition."},
+ {"Je tu sportovní slavnost.", NULL, "There is a sports festival."},
+ {"Je tu rodinné setkání.", NULL, "There is a family reunion."},
+ {"Je tu divadelní představení.", NULL, "There is a theatre performance."},
+ {"Je tu bleší trh.", NULL, "There is a flea market."},
+ /* ex14 describe (sentence pairs) */
+ {"Vidím ženu.", NULL, "I see a woman."},
+ {"Jezdí na kole.", NULL, "She is riding a bike."},
+ {"Vidím muže.", NULL, "I see a man."},
+ {"Chodí po lese.", NULL, "He is hiking in the forest."},
+ {"Vidím dvě děti.", NULL, "I see two children."},
+ {"Hrají fotbal.", NULL, "They are playing football."},
+ /* ex15 missing letters (weekend) */
+ {"Moji prarodiče jedou o víkendu do víkendového domu.", NULL,
+ "My grandparents are driving to their weekend house."},
+ {"Můj otec a moje matka navštíví koncert.", NULL,
+ "My father and my mother are going to a concert."},
+ {"Moje sestra jde do kina.", NULL,
+ "My sister is going to the cinema."},
+ {"Můj bratr jede k jezeru.", NULL,
+ "My brother is driving to a lake."},
+ {"A já? Navštívím obchodní centrum.", NULL,
+ "And me? I am going to a shopping centre."},
+ {NULL, NULL, NULL}
+};
+
+const char *tr(const char *key) {
+ static GHashTable *idx;
+ const TrEntry *e;
+ int i;
+
+ if (!key)
+ return NULL;
+
+ if (!idx) {
+ idx = g_hash_table_new(g_str_hash, g_str_equal);
+ for (i = 0; tr_ui[i].key; i++)
+ g_hash_table_insert(idx, (gpointer)tr_ui[i].key,
+ (gpointer)&tr_ui[i]);
+ for (i = 0; tr_content[i].key; i++)
+ g_hash_table_insert(idx, (gpointer)tr_content[i].key,
+ (gpointer)&tr_content[i]);
+ }
+
+ e = g_hash_table_lookup(idx, key);
+ if (!e)
+ return key;
+ if (app_lang == LANG_EN)
+ return e->en;
+ return e->cs ? e->cs : e->key;
+}
+
+void i18n_ensure(void) {
+ if (!i18n_binds)
+ i18n_binds = g_array_new(FALSE, FALSE, sizeof(I18nBind));
+}
+
+void i18n_apply_one(const I18nBind *b) {
+ if (!b->widget || !b->key)
+ return;
+ switch (b->kind) {
+ case 0:
+ gtk_label_set_text(GTK_LABEL(b->widget), tr(b->key));
+ break;
+ case 1:
+ gtk_button_set_label(GTK_BUTTON(b->widget), tr(b->key));
+ break;
+ case 2:
+ gtk_widget_set_tooltip_text(b->widget, tr(b->key));
+ break;
+ case 3:
+ gtk_entry_set_placeholder_text(GTK_ENTRY(b->widget), tr(b->key));
+ break;
+ case 4:
+ ex4_fill_sample(b->widget);
+ break;
+ default:
+ break;
+ }
+}
+
+void i18n_bind(GtkWidget *widget, const char *key, int kind) {
+ I18nBind b;
+
+ i18n_ensure();
+ b.widget = widget;
+ b.key = key;
+ b.kind = kind;
+ g_array_append_val(i18n_binds, b);
+
+ i18n_apply_one(&b);
+}
+
+void refresh_welcome_heading(void) {
+ char *markup;
+
+ if (!welcome_heading)
+ return;
+ if (app_lang == LANG_EN)
+ markup = g_strdup_printf(
+ "Welcome to maturita.C!",
+ app_theme.accent);
+ else
+ markup = g_strdup_printf(
+ "Vítejte ve maturita.C!",
+ app_theme.accent);
+ gtk_label_set_markup(GTK_LABEL(welcome_heading), markup);
+ g_free(markup);
+}
+
+void refresh_stats_ui(void);
+void net_slide_apply(void);
+void net_rail_theme_reset(void);
+
+void apply_language(void) {
+ guint i;
+
+ i18n_ensure();
+ for (i = 0; i < i18n_binds->len; i++)
+ i18n_apply_one(&g_array_index(i18n_binds, I18nBind, i));
+ refresh_welcome_heading();
+ refresh_stats_ui();
+ net_slide_apply();
+}
diff --git a/icons.c b/icons.c
new file mode 100644
index 0000000..bc7cb30
--- /dev/null
+++ b/icons.c
@@ -0,0 +1,210 @@
+#include "maturita.h"
+
+/* Vector icons drawn with cairo so they never depend on the system
+ * icon theme (which is missing on some installs). */
+
+void draw_check_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ Rgb c = color_from_hex(app_theme.on_accent);
+
+ (void)area;
+ (void)data;
+ cairo_set_source_rgb(cr, c.r, c.g, c.b);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
+ cairo_set_line_width(cr, width * 0.16);
+ cairo_move_to(cr, width * 0.24, height * 0.56);
+ cairo_line_to(cr, width * 0.43, height * 0.74);
+ cairo_line_to(cr, width * 0.78, height * 0.30);
+ cairo_stroke(cr);
+}
+
+/* Paint a padlock centred in the size x size box with top-left (x0,y0). */
+void lock_paint(cairo_t *cr, double x0, double y0, double size,
+ const Rgb *c) {
+ double sl = MAX(1.6, size * 0.13); /* shackle stroke width */
+ double bw = size * 0.60; /* body width */
+ double bh = size * 0.44; /* body height */
+ double cx = x0 + size / 2.0;
+ double body_top = y0 + size - bh - size * 0.04;
+ double bx = cx - bw / 2.0;
+ double r = bw / 2.0 - sl / 2.0; /* shackle arc radius */
+
+ cairo_set_source_rgb(cr, c->r, c->g, c->b);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_width(cr, sl);
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, body_top, r, G_PI, 2.0 * G_PI);
+ cairo_stroke(cr);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, bx, body_top, bw, bh);
+ cairo_fill(cr);
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, body_top + bh * 0.42, size * 0.07, 0.0, 2.0 * G_PI);
+ {
+ Rgb hole = color_from_hex(app_theme.crust);
+ cairo_set_source_rgb(cr, hole.r, hole.g, hole.b);
+ }
+ cairo_fill(cr);
+}
+
+void draw_lock_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ Rgb c = color_from_hex(app_theme.overlay);
+ (void)area;
+ (void)data;
+ lock_paint(cr, 0.0, 0.0, MIN(width, height), &c);
+}
+
+/* Wireless icon: three upward arcs that fan out from a base dot. */
+void draw_wifi_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ const double s = MIN(width, height);
+ const double cx = width / 2.0;
+ const double cy = height * 0.68;
+ const double dot_r = s * 0.06;
+ const double radii[3] = {s * 0.15, s * 0.27, s * 0.39};
+ const double phi = 0.75;
+ /* Keep the icon legible on the accent-gradient bubble while giving it a
+ * hint of the active theme: blend the contrasting on-accent colour a
+ * little way towards the palette's middle accent. */
+ const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent),
+ color_from_hex(app_theme.accent2), 0.22);
+
+ (void)area;
+ (void)data;
+
+ cairo_set_source_rgb(cr, c.r, c.g, c.b);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_width(cr, MAX(1.5, s * 0.07));
+
+ for (int i = 0; i < 3; i++) {
+ double r = radii[i];
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, cy, r, -G_PI / 2.0 - phi, -G_PI / 2.0 + phi);
+ cairo_stroke(cr);
+ }
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, cy, dot_r, 0.0, 2.0 * G_PI);
+ cairo_fill(cr);
+}
+
+void draw_back_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ const double size = 15.0; /* fixed logical icon size */
+ const double stroke = 1.8;
+ double ox = (width - size) / 2.0;
+ double oy = (height - size) / 2.0;
+ GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(area));
+ GdkRGBA color;
+
+ (void)data;
+ gtk_style_context_get_color(gtk_widget_get_style_context(parent), &color);
+ cairo_set_source_rgb(cr, color.red, color.green, color.blue);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
+ cairo_set_line_width(cr, stroke);
+ cairo_move_to(cr, ox + size * 0.72, oy + size * 0.24);
+ cairo_line_to(cr, ox + size * 0.30, oy + size * 0.50);
+ cairo_line_to(cr, ox + size * 0.72, oy + size * 0.76);
+ cairo_stroke(cr);
+}
+
+void draw_settings_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ const int teeth = 8;
+ const double size = 16.0;
+ double cx = width / 2.0;
+ double cy = height / 2.0;
+ double r_outer = size * 0.48;
+ double r_inner = size * 0.34;
+ double r_hole = size * 0.16;
+ double tooth_half = G_PI / (double)teeth;
+ GtkWidget *color_w = data ? GTK_WIDGET(data)
+ : gtk_widget_get_parent(GTK_WIDGET(area));
+ GdkRGBA color;
+ int i;
+
+ gtk_style_context_get_color(gtk_widget_get_style_context(color_w), &color);
+ cairo_set_source_rgb(cr, color.red, color.green, color.blue);
+ cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
+
+ cairo_new_path(cr);
+ for (i = 0; i < teeth; i++) {
+ double a = -G_PI / 2.0 + (2.0 * G_PI * (double)i) / (double)teeth;
+ double a0 = a - tooth_half * 0.55;
+ double a1 = a - tooth_half * 0.28;
+ double a2 = a + tooth_half * 0.28;
+ double a3 = a + tooth_half * 0.55;
+
+ if (i == 0)
+ cairo_move_to(cr, cx + cos(a0) * r_inner, cy + sin(a0) * r_inner);
+ else
+ cairo_line_to(cr, cx + cos(a0) * r_inner, cy + sin(a0) * r_inner);
+
+ cairo_line_to(cr, cx + cos(a1) * r_outer, cy + sin(a1) * r_outer);
+ cairo_line_to(cr, cx + cos(a2) * r_outer, cy + sin(a2) * r_outer);
+ cairo_line_to(cr, cx + cos(a3) * r_inner, cy + sin(a3) * r_inner);
+ }
+ cairo_close_path(cr);
+ cairo_new_sub_path(cr);
+ cairo_arc(cr, cx, cy, r_hole, 0.0, 2.0 * G_PI);
+ cairo_fill(cr);
+}
+
+void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ GtkWidget *color_w = data ? GTK_WIDGET(data)
+ : gtk_widget_get_parent(GTK_WIDGET(area));
+ GdkRGBA color;
+ const double bar_w = 3.2;
+ const double gap = 2.6;
+ const double total_w = 3.0 * bar_w + 2.0 * gap;
+ const double heights[3] = {0.42, 0.72, 1.0};
+ double base_x = (width - total_w) / 2.0;
+ double base_y = height * 0.82;
+ double max_h = height * 0.58;
+ int i;
+
+ (void)area;
+ gtk_style_context_get_color(gtk_widget_get_style_context(color_w), &color);
+ cairo_set_source_rgb(cr, color.red, color.green, color.blue);
+ cairo_set_line_width(cr, 1.5);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+
+ for (i = 0; i < 3; i++) {
+ double x = base_x + (double)i * (bar_w + gap);
+ double h = max_h * heights[i];
+ double y = base_y - h;
+ double r = 1.2;
+
+ cairo_new_sub_path(cr);
+ cairo_arc(cr, x + r, y + r, r, G_PI, 1.5 * G_PI);
+ cairo_arc(cr, x + bar_w - r, y + r, r, 1.5 * G_PI, 2.0 * G_PI);
+ cairo_line_to(cr, x + bar_w, base_y);
+ cairo_line_to(cr, x, base_y);
+ cairo_close_path(cr);
+ cairo_fill(cr);
+ }
+}
+
+GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
+ double r, double g, double b, int px) {
+ Rgb *col = g_new(Rgb, 1);
+ GtkWidget *d = gtk_drawing_area_new();
+
+ col->r = r;
+ col->g = g;
+ col->b = b;
+ gtk_widget_set_size_request(d, px, px);
+ gtk_widget_set_halign(d, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(d, GTK_ALIGN_CENTER);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(d), fn,
+ col, (GDestroyNotify)g_free);
+ return d;
+}
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..9bba4f2
--- /dev/null
+++ b/main.c
@@ -0,0 +1,215 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Activate */
+/* ------------------------------------------------------------------ */
+
+void unit_meta_init(void) {
+ static const char *titles[NUM_UNITS] = {
+ "Neue Freunde", "Aus aller Welt", "Bei uns zu Hause",
+ "Schule und Freizeit", "Guten Appetit!", "Mein Tagesablauf",
+ "Meine Freunde", "Wir treffen uns in Salzburg",
+ "Mein Haus ist meine Burg", "Urlaub in Österreich",
+ };
+ static const char *pages[NUM_UNITS] = {
+ "unit1", "unit2", "unit3", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ };
+ static const char *tags[NUM_UNITS] = {
+ "u1", "u2", "u3", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+ };
+
+ for (int i = 0; i < NUM_UNITS; i++) {
+ units[i].title = titles[i];
+ units[i].page = pages[i];
+ units[i].ex_tag = tags[i];
+ units[i].sub_key = NULL;
+ units[i].unlocked = (i < NUM_UNLOCKED);
+ }
+ units[0].sub_key = "unit1_sub";
+ units[0].progress_file = PROGRESS_U1;
+ units[1].sub_key = "unit2_sub";
+ units[1].progress_file = PROGRESS_U2;
+ units[2].sub_key = "unit3_sub";
+ units[2].progress_file = PROGRESS_U3;
+}
+
+void unit_configure(int idx, const char *const *names, int n) {
+ UnitCtx *u = &units[idx];
+
+ u->n_ex = n;
+ for (int i = 1; i <= n; i++)
+ u->ex_names[i] = names[i];
+}
+
+GtkWidget *build_exercise_page(UnitCtx *u, int n) {
+ if (u->ex_tag[1] == '1') {
+ switch (n) {
+ case 1: return build_ex1(u);
+ case 2: return build_assembly(u, "Sätze bilden", "sub_assembly", 2,
+ ex2_items, ex2_meaning,
+ 4);
+ case 3: return build_choice(u, "Was ist richtig?", "sub_choice_num",
+ 3, ex3_questions, ex3_meaning,
+ 4);
+ case 4: return build_ex4(u);
+ case 5: return build_ex5(u);
+ case 6: return build_ex6(u);
+ case 7: return build_ex7(u);
+ case 8: return build_ex8(u);
+ case 9: return build_choice(u, "Wer? Wie? Wo?", "sub_wer",
+ 9, ex9_questions, ex9_meaning,
+ 5);
+ case 10: return build_assembly(u, "Wörter trennen", "sub_assembly",
+ 10, ex10_items, ex10_meaning,
+ 5);
+ case 11: return build_assign(u, "Grußformen", "sub_gruss",
+ 11, ex11_items,
+ 12,
+ ex11_groups, 2,
+ ex11_meaning);
+ case 12: return build_ex12(u);
+ case 13: return build_assign(u, "Länder", "sub_land",
+ 13, ex13_items,
+ 10,
+ ex13_groups, 3,
+ ex13_meaning);
+ default: return NULL;
+ }
+ } else if (u->ex_tag[1] == '2') {
+ switch (n) {
+ case 1: return u2ex1(u);
+ case 2: return u2ex2(u);
+ case 3: return u2ex3(u);
+ case 4: return u2ex4(u);
+ case 5: return u2ex5(u);
+ case 6: return u2ex6(u);
+ case 7: return u2ex7(u);
+ case 8: return u2ex8(u);
+ case 9: return u2ex9(u);
+ case 10: return u2ex10(u);
+ case 11: return u2ex11(u);
+ case 12: return u2ex12(u);
+ case 13: return u2ex13(u);
+ case 14: return u2ex14(u);
+ case 15: return u2ex15(u);
+ case 16: return u2ex16(u);
+ case 17: return u2ex17(u);
+ case 18: return u2ex18(u);
+ case 19: return u2ex19(u);
+ default: return NULL;
+ }
+ } else {
+ switch (n) {
+ case 1: return u3ex1(u);
+ case 2: return u3ex2(u);
+ case 3: return u3ex3(u);
+ case 4: return u3ex4(u);
+ case 5: return u3ex5(u);
+ case 6: return u3ex6(u);
+ case 7: return u3ex7(u);
+ case 8: return u3ex8(u);
+ case 9: return u3ex9(u);
+ case 10: return u3ex10(u);
+ case 11: return u3ex11(u);
+ case 12: return u3ex12(u);
+ case 13: return u3ex13(u);
+ case 14: return u3ex14(u);
+ case 15: return u3ex15(u);
+ default: return NULL;
+ }
+ }
+}
+
+void activate(GtkApplication *app, gpointer user_data) {
+ GtkWindow *window;
+ GtkWidget *headerbar;
+ GtkWidget *title_label;
+ GtkWidget *welcome_page;
+ GtkWidget *roadmap_page;
+ GtkEventController *keys;
+
+ (void)user_data;
+
+ load_settings();
+ unit_meta_init();
+ unit_configure(0, u1_ex_names, 13);
+ unit_configure(1, u2_ex_names, 19);
+ unit_configure(2, u3_ex_names, 15);
+ load_progress();
+ app_theme = theme_palette(app_theme_id, app_color_mode);
+
+ window = GTK_WINDOW(gtk_application_window_new(app));
+ main_window = window;
+ gtk_window_set_title(window, "maturita.c");
+ gtk_window_set_default_size(window, 760, 560);
+
+ headerbar = gtk_header_bar_new();
+ gtk_widget_add_css_class(headerbar, "titlebar");
+ title_label = gtk_label_new("maturita.c");
+ gtk_widget_add_css_class(title_label, "app-title");
+ gtk_header_bar_set_title_widget(GTK_HEADER_BAR(headerbar), title_label);
+ gtk_window_set_titlebar(window, headerbar);
+ apply_theme();
+ gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_settings_button());
+ gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_stats_button());
+
+ main_stack = GTK_STACK(gtk_stack_new());
+ gtk_stack_set_transition_type(GTK_STACK(main_stack),
+ GTK_STACK_TRANSITION_TYPE_CROSSFADE);
+ gtk_window_set_child(window, GTK_WIDGET(main_stack));
+
+ welcome_page = build_welcome_page();
+ roadmap_page = build_roadmap_page();
+ gtk_stack_add_named(main_stack, welcome_page, "welcome");
+ gtk_stack_add_named(main_stack, build_subjects_page(), "subjects");
+ gtk_stack_add_named(main_stack, roadmap_page, "roadmap");
+ gtk_stack_add_named(main_stack, build_stats_page(), "stats");
+ gtk_stack_add_named(main_stack, build_netmap_page(), "netmap");
+ gtk_stack_add_named(main_stack, build_net_unit1_page(), "netunit1");
+ gtk_stack_add_named(main_stack, build_net_exercise_page(), "netex");
+
+ for (int i = 0; i < NUM_UNLOCKED; i++) {
+ UnitCtx *u = &units[i];
+ GtkWidget *map = build_unit_page(u);
+
+ if (map)
+ gtk_stack_add_named(main_stack, map, u->page);
+ for (int n = 1; n <= u->n_ex; n++) {
+ GtkWidget *page = build_exercise_page(u, n);
+ char name[16];
+
+ if (!page)
+ continue;
+ g_snprintf(name, sizeof(name), "%se%d", u->ex_tag, n);
+ gtk_stack_add_named(main_stack, page, name);
+ }
+ }
+
+ gtk_stack_set_visible_child_name(main_stack, "welcome");
+
+ refresh_completion_ui();
+
+ keys = gtk_event_controller_key_new();
+ gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(keys),
+ GTK_PHASE_CAPTURE);
+ g_signal_connect(keys, "key-pressed",
+ G_CALLBACK(on_window_key_pressed), window);
+ gtk_widget_add_controller(GTK_WIDGET(window), GTK_EVENT_CONTROLLER(keys));
+
+ apply_theme();
+ apply_language();
+
+ gtk_window_present(window);
+}
+
+int main(int argc, char **argv) {
+ GtkApplication *app;
+ int status;
+
+ app = gtk_application_new("org.example.maturita", G_APPLICATION_DEFAULT_FLAGS);
+ g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
+ status = g_application_run(G_APPLICATION(app), argc, argv);
+ g_object_unref(app);
+
+ return status;
+}
diff --git a/maturita.c b/maturita.c
deleted file mode 100644
index 634a8c0..0000000
--- a/maturita.c
+++ /dev/null
@@ -1,9534 +0,0 @@
-#include
-#include
-#include
-#include
-#include
-
-#define NUM_UNITS 10
-#define NUM_UNLOCKED 3 /* first NUM_UNLOCKED units (index 0..2) are open */
-#define MAX_UNIT_EX 20 /* largest exercise count any unit may have */
-#define NUM_SUBJECTS 13
-#define NET_SUBJ 1 /* index of "Správa počítačových sítí" */
-
-#define CSS_FILE "style.css"
-#define PROGRESS_DIR "progress"
-#define PROGRESS_U1 "progress/unit1.conf"
-#define PROGRESS_U2 "progress/unit2.conf"
-#define PROGRESS_U3 "progress/unit3.conf"
-#define SETTINGS_FILE "progress/settings.conf"
-
-#define NODE_SIZE 88.0
-#define PATH_SPAC 240.0
-
-/* Serpentine roadmap layout */
-#define ROAD_MX 150.0 /* horizontal canvas margin */
-#define ROAD_MY 150.0 /* vertical canvas margin */
-#define ROAD_GAP 240.0 /* vertical space between folded rows */
-#define ROAD_WAVE 44.0 /* wavy vertical offset of the nodes */
-
-static GtkStack *main_stack;
-static GtkWindow *main_window;
-static GtkWidget *welcome_heading;
-static GtkCssProvider *theme_provider;
-
-typedef enum {
- THEME_CATPPUCCIN = 0,
- THEME_NORD,
- THEME_DRACULA,
- THEME_ROSE_PINE,
- THEME_OCEAN,
- THEME_GRUVBOX,
- THEME_SOLARIZED,
- THEME_EVERFOREST,
- THEME_MONOKAI,
- THEME_ONE_DARK,
- THEME_COUNT
-} ThemeId;
-
-typedef enum {
- MODE_DARK = 0,
- MODE_LIGHT
-} ColorMode;
-
-typedef enum {
- LANG_CS = 0,
- LANG_EN
-} UiLang;
-
-typedef struct {
- unsigned crust;
- unsigned base;
- unsigned mantle;
- unsigned surface0;
- unsigned surface1;
- unsigned surface2;
- unsigned overlay;
- unsigned text;
- unsigned subtext;
- unsigned accent;
- unsigned accent2;
- unsigned accent3;
- unsigned success;
- unsigned success2;
- unsigned warning;
- unsigned error;
- unsigned on_accent;
- unsigned node;
- unsigned locked_bg;
- unsigned locked_border;
- unsigned rail;
- unsigned finish_dark;
- unsigned finish_light;
- unsigned finish_stroke;
-} ThemePalette;
-
-static ThemeId app_theme_id = THEME_CATPPUCCIN;
-static ColorMode app_color_mode = MODE_DARK;
-static UiLang app_lang = LANG_CS;
-static ThemePalette app_theme;
-
-static const char *theme_names[THEME_COUNT] = {
- "Catppuccin",
- "Nord",
- "Dracula",
- "Rose Pine",
- "Ocean",
- "Gruvbox",
- "Solarized",
- "Everforest",
- "Monokai",
- "One Dark",
-};
-
-static GtkWidget *theme_swatch_areas[THEME_COUNT];
-
-/* Per-unit context. Every playable unit owns its exercise state, its
- * exercise-map page and the related geometry, so future units (3..10) can
- * be added without more duplicated machinery. */
-typedef struct {
- const char *title; /* German unit title */
- const char *page; /* roadmap-stack child name, e.g. "unit1" */
- const char *ex_tag; /* exercise page prefix, e.g. "u1" */
- const char *sub_key; /* i18n key for the exercise-map subtitle */
- const char *progress_file;
- gboolean unlocked;
- gboolean done[MAX_UNIT_EX + 1]; /* exercise state, 1..n_ex */
- const char *ex_names[MAX_UNIT_EX + 1]; /* bubble labels, 1..n_ex */
- int n_ex;
- GtkWidget *node; /* roadmap node button */
- GtkWidget *node_done_icon;
- /* exercise-map page widgets + serpentine geometry */
- GtkWidget *ex_scroll;
- GtkWidget *ex_fixed;
- GtkWidget *ex_rail;
- GtkWidget *ex_cells[MAX_UNIT_EX]; /* 0..n_ex-1 */
- GtkWidget *ex_labels[MAX_UNIT_EX]; /* 0..n_ex-1 */
- GtkWidget *ex_icons[MAX_UNIT_EX + 1]; /* per-bubble check icon 1..n */
- double ex_cx[MAX_UNIT_EX];
- double ex_cy[MAX_UNIT_EX];
- int ex_rows, ex_cols, ex_cw, ex_ch;
- guint ex_idle;
-} UnitCtx;
-
-static UnitCtx units[NUM_UNITS] = {0};
-
-/* Subject catalogue shared by the subjects page and the statistics page.
- * The full definitions live in the "Subjects page" section further down. */
-static const char *sub_keys[NUM_SUBJECTS];
-static const int sub_unit_start[NUM_SUBJECTS];
-static const int sub_unit_count[NUM_SUBJECTS];
-
-/* Short German bubble titles, indexed 1..n_ex (entry 0 unused). */
-static const char *const u1_ex_names[MAX_UNIT_EX + 1] = {
- NULL,
- "Dialog", "Sätze bilden", "Was ist richtig?", "Freie Antwort",
- "Zahlen", "Wie viel?", "Zahlenreihe", "Verb einsetzen",
- "Wer? Wie? Wo?", "Wörter trennen", "Grußformen",
- "Was macht er/sie?", "Länder",
-};
-
-static const char *const u2_ex_names[MAX_UNIT_EX + 1] = {
- NULL,
- "Verben konjugieren", "aus oder in", "Fragewörter", "sprechen",
- "Nationalitäten", "Woher?", "Länder schreiben", "Verben einsetzen",
- "Freie Antwort", "Euro", "Wörter suchen", "Was ist richtig?",
- "Ordne zu", "Lückentext", "Verbinde", "Zahlen",
- "Steckbrief", "Berufe", "Nationalität",
-};
-
-static const char *const u3_ex_names[MAX_UNIT_EX + 1] = {
- NULL,
- "Familienpaare", "mein oder dein", "ein / kein", "Lückentext",
- "Marcos Familie", "Sortieren", "Possessivtabelle", "Akkusativ",
- "kein / nicht", "Sätze bauen", "Was siehst du?", "Wem gehört das?",
- "Es gibt …", "Beschreiben", "Wochenende",
-};
-
-/* Adaptive serpentine roadmap geometry */
-static GtkWidget *road_scroll;
-static GtkWidget *road_fixed;
-static GtkWidget *road_rail;
-static GtkWidget *road_nodes[NUM_UNITS];
-static GtkWidget *road_labels[NUM_UNITS];
-static double road_cx[NUM_UNITS];
-static double road_cy[NUM_UNITS];
-static int road_cols = NUM_UNITS;
-static int road_rows = 1;
-static int road_cw = (int)(2.0 * ROAD_MX + (NUM_UNITS - 1) * PATH_SPAC);
-static int road_ch = (int)(2.0 * ROAD_MY);
-
-
-typedef struct { double r, g, b; } Rgb;
-
-static Rgb color_from_hex(unsigned int hex) {
- Rgb c;
- c.r = (double)((hex >> 16) & 0xFF) / 255.0;
- c.g = (double)((hex >> 8) & 0xFF) / 255.0;
- c.b = (double)(hex & 0xFF) / 255.0;
- return c;
-}
-
-/* Linear blend of two colours: t = 0 keeps a, t = 1 returns b. */
-static Rgb mix_rgb(Rgb a, Rgb b, double t) {
- Rgb c;
- c.r = a.r + (b.r - a.r) * t;
- c.g = a.g + (b.g - a.g) * t;
- c.b = a.b + (b.b - a.b) * t;
- return c;
-}
-
-static ThemePalette theme_palette(ThemeId id, ColorMode mode) {
- /* Catppuccin Mocha / Latte */
- static const ThemePalette cat_dark = {
- 0x11111b, 0x1e1e2e, 0x181825, 0x313244, 0x45475a, 0x585b70,
- 0x6c7086, 0xcdd6f4, 0xa6adc8, 0xcba6f7, 0xb4befe, 0x89b4fa,
- 0xa6e3a1, 0x94e2d5, 0xf9e2af, 0xf38ba8, 0x1e1e2e,
- 0x2b2d40, 0x24263a, 0x383b52, 0x27293c, 0x1f202e, 0x3b3e58, 0x52556b
- };
- static const ThemePalette cat_light = {
- 0xdce0e8, 0xeff1f5, 0xe6e9ef, 0xccd0da, 0xbcc0cc, 0xacb0be,
- 0x9ca0b0, 0x4c4f69, 0x6c6f85, 0x8839ef, 0x7287fd, 0x1e66f5,
- 0x40a02b, 0x179299, 0xdf8e1d, 0xd20f39, 0xeff1f5,
- 0xe6e9ef, 0xdce0e8, 0xbcc0cc, 0xccd0da, 0xdce0e8, 0xbcc0cc, 0x9ca0b0
- };
- /* Nord */
- static const ThemePalette nord_dark = {
- 0x2e3440, 0x3b4252, 0x2e3440, 0x434c5e, 0x4c566a, 0x616e88,
- 0x7b88a1, 0xeceff4, 0xd8dee9, 0x88c0d0, 0x81a1c1, 0x5e81ac,
- 0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0x2e3440,
- 0x434c5e, 0x3b4252, 0x4c566a, 0x2e3440, 0x2e3440, 0x434c5e, 0x4c566a
- };
- static const ThemePalette nord_light = {
- 0xd8dee9, 0xeceff4, 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xb0b8c8,
- 0x7b88a1, 0x2e3440, 0x4c566a, 0x5e81ac, 0x81a1c1, 0x5e81ac,
- 0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0xeceff4,
- 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xd8dee9, 0xd8dee9, 0xc7ceda, 0x7b88a1
- };
- /* Dracula / light soft */
- static const ThemePalette dra_dark = {
- 0x191a21, 0x282a36, 0x21222c, 0x44475a, 0x6272a4, 0x707eb0,
- 0x6272a4, 0xf8f8f2, 0xbfbfb2, 0xbd93f9, 0xff79c6, 0x8be9fd,
- 0x50fa7b, 0x8be9fd, 0xf1fa8c, 0xff5555, 0x282a36,
- 0x383a4a, 0x2f3240, 0x44475a, 0x21222c, 0x191a21, 0x383a4a, 0x6272a4
- };
- static const ThemePalette dra_light = {
- 0xd5d7e0, 0xf8f8f2, 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xbfbfb2,
- 0x9a9a8c, 0x282a36, 0x44475a, 0x7c3aed, 0xd946a6, 0x0891b2,
- 0x16a34a, 0x0e7490, 0xca8a04, 0xdc2626, 0xf8f8f2,
- 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xe2e2d8, 0xd5d7e0, 0xd0d0c4, 0x9a9a8c
- };
- /* Rose Pine / Dawn */
- static const ThemePalette rose_dark = {
- 0x191724, 0x1f1d2e, 0x1f1d2e, 0x26233a, 0x403d52, 0x524f67,
- 0x6e6a86, 0xe0def4, 0x908caa, 0xc4a7e7, 0xebbcba, 0x9ccfd8,
- 0x31748f, 0x9ccfd8, 0xf6c177, 0xeb6f92, 0x191724,
- 0x26233a, 0x21202e, 0x403d52, 0x191724, 0x191724, 0x26233a, 0x524f67
- };
- static const ThemePalette rose_light = {
- 0xf2e9e1, 0xfaf4ed, 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xcecacd,
- 0x9893a5, 0x575279, 0x797593, 0x907aa9, 0xd7827e, 0x56949f,
- 0x286983, 0x56949f, 0xea9d34, 0xb4637a, 0xfaf4ed,
- 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xf2e9e1, 0xf2e9e1, 0xdfdad9, 0x9893a5
- };
- /* Ocean / Tokyo Night inspired */
- static const ThemePalette ocean_dark = {
- 0x16161e, 0x1a1b26, 0x16161e, 0x24283b, 0x414868, 0x565f89,
- 0x565f89, 0xc0caf5, 0xa9b1d6, 0x7aa2f7, 0xbb9af7, 0x7dcfff,
- 0x9ece6a, 0x73daca, 0xe0af68, 0xf7768e, 0x1a1b26,
- 0x24283b, 0x1f2335, 0x3b4261, 0x16161e, 0x16161e, 0x24283b, 0x414868
- };
- static const ThemePalette ocean_light = {
- 0xd6dae8, 0xe1e2ec, 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0x9aa0b5,
- 0x7a8199, 0x2e3446, 0x4a5168, 0x2e7de9, 0x7847bd, 0x007197,
- 0x387068, 0x007197, 0x8c6c3e, 0xc64343, 0xe1e2ec,
- 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0xc8c9d6, 0xd6dae8, 0xb4b6c5, 0x7a8199
- };
-
- /* Gruvbox */
- static const ThemePalette gruv_dark = {
- 0x1d2021, 0x282828, 0x1d2021, 0x3c3836, 0x504945, 0x665c54,
- 0x928374, 0xebdbb2, 0xa89984, 0xfe8019, 0xd3869b, 0x83a598,
- 0xb8bb26, 0x8ec07c, 0xfabd2f, 0xfb4934, 0x1d2021,
- 0x3c3836, 0x32302f, 0x504945, 0x1d2021, 0x1d2021, 0x3c3836, 0x504945
- };
- static const ThemePalette gruv_light = {
- 0xebdbb2, 0xfbf1c7, 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xbdae93,
- 0x928374, 0x3c3836, 0x504945, 0xd65d0e, 0xb16286, 0x076678,
- 0x79740e, 0x427b58, 0xb57614, 0x9d0006, 0xfbf1c7,
- 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xebdbb2, 0xebdbb2, 0xd5c4a1, 0x928374
- };
- /* Solarized */
- static const ThemePalette sol_dark = {
- 0x002b36, 0x073642, 0x002b36, 0x586e75, 0x657b83, 0x839496,
- 0x93a1a1, 0xfdf6e3, 0x93a1a1, 0x268bd2, 0x6c71c4, 0x2aa198,
- 0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0x002b36,
- 0x073642, 0x002b36, 0x586e75, 0x002b36, 0x002b36, 0x073642, 0x586e75
- };
- static const ThemePalette sol_light = {
- 0xeee8d5, 0xfdf6e3, 0xeee8d5, 0xeee8d5, 0xe2dbc6, 0xc9c0a6,
- 0x657b83, 0x586e75, 0x657b83, 0x268bd2, 0x6c71c4, 0x2aa198,
- 0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0xfdf6e3,
- 0xeee8d5, 0xe6dfc8, 0x93a1a1, 0xeee8d5, 0xeee8d5, 0x93a1a1, 0x586e75
- };
- /* Everforest */
- static const ThemePalette ever_dark = {
- 0x1e2326, 0x272e33, 0x2d353b, 0x374145, 0x4f5b58, 0x7a8478,
- 0x859289, 0xd3c6aa, 0xa7c080, 0xa7c080, 0xd699b6, 0x7fbbb3,
- 0xa7c080, 0x83c092, 0xdbbc7f, 0xe67e80, 0x272e33,
- 0x374145, 0x2d353b, 0x4f5b58, 0x1e2326, 0x1e2326, 0x374145, 0x4f5b58
- };
- static const ThemePalette ever_light = {
- 0xe6e2cc, 0xfdf6e3, 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xa6b0a0,
- 0x939f91, 0x5c6a72, 0x829181, 0x8da101, 0xdf69ba, 0x35a77c,
- 0x8da101, 0x35a77c, 0xdfa000, 0xf85552, 0xfdf6e3,
- 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xe6e2cc, 0xe6e2cc, 0xe0dcc7, 0x939f91
- };
- /* Monokai */
- static const ThemePalette mono_dark = {
- 0x1d1e19, 0x272822, 0x1e1f1a, 0x3e3d32, 0x49483e, 0x75715e,
- 0x75715e, 0xf8f8f2, 0xa59f85, 0xf92672, 0xae81ff, 0x66d9ef,
- 0xa6e22e, 0xa1efe4, 0xe6db74, 0xf92672, 0x272822,
- 0x3e3d32, 0x2e2f29, 0x49483e, 0x1d1e19, 0x1d1e19, 0x3e3d32, 0x49483e
- };
- static const ThemePalette mono_light = {
- 0xe6e3d4, 0xfaf8f0, 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xb8b49e,
- 0x8a8674, 0x2d2a2e, 0x5b5854, 0xe14775, 0x7c5cbf, 0x1c7d9b,
- 0x5b8c2a, 0x1c7d9b, 0xb08900, 0xe14775, 0xfaf8f0,
- 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xe6e3d4, 0xe6e3d4, 0xd6d2c0, 0x8a8674
- };
- /* One Dark / One Light */
- static const ThemePalette one_dark = {
- 0x21252b, 0x282c34, 0x21252b, 0x3b4048, 0x4b5263, 0x5c6370,
- 0x5c6370, 0xabb2bf, 0x7f848e, 0xc678dd, 0x61afef, 0x56b6c2,
- 0x98c379, 0x56b6c2, 0xe5c07b, 0xe06c75, 0x282c34,
- 0x3b4048, 0x2c313a, 0x4b5263, 0x21252b, 0x21252b, 0x3b4048, 0x4b5263
- };
- static const ThemePalette one_light = {
- 0xe5e5e6, 0xfafafa, 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
- 0xa0a1a7, 0x383a42, 0x696c77, 0xa626a4, 0x4078f2, 0x0184bc,
- 0x50a14f, 0x0184bc, 0xc18401, 0xe45649, 0xfafafa,
- 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xe5e5e6, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7
- };
-
- switch (id) {
- case THEME_NORD:
- return mode == MODE_LIGHT ? nord_light : nord_dark;
- case THEME_DRACULA:
- return mode == MODE_LIGHT ? dra_light : dra_dark;
- case THEME_ROSE_PINE:
- return mode == MODE_LIGHT ? rose_light : rose_dark;
- case THEME_OCEAN:
- return mode == MODE_LIGHT ? ocean_light : ocean_dark;
- case THEME_GRUVBOX:
- return mode == MODE_LIGHT ? gruv_light : gruv_dark;
- case THEME_SOLARIZED:
- return mode == MODE_LIGHT ? sol_light : sol_dark;
- case THEME_EVERFOREST:
- return mode == MODE_LIGHT ? ever_light : ever_dark;
- case THEME_MONOKAI:
- return mode == MODE_LIGHT ? mono_light : mono_dark;
- case THEME_ONE_DARK:
- return mode == MODE_LIGHT ? one_light : one_dark;
- case THEME_CATPPUCCIN:
- default:
- return mode == MODE_LIGHT ? cat_light : cat_dark;
- }
-}
-
-/* ------------------------------------------------------------------ */
-/* i18n */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- GtkWidget *widget;
- const char *key;
- int kind; /* 0 label, 1 button, 2 tooltip, 3 placeholder, 4 ex4 sample */
-} I18nBind;
-
-static void ex4_fill_sample(GtkWidget *label);
-
-static GArray *i18n_binds;
-
-typedef struct {
- const char *key;
- const char *cs;
- const char *en;
-} TrEntry;
-
-static const TrEntry tr_ui[] = {
- {"back", "Zpět", "Back"},
- {"settings", "Nastavení", "Settings"},
- {"settings_title", "Nastavení", "Settings"},
- {"mode", "REŽIM", "MODE"},
- {"mode_dark", "Tmavý", "Dark"},
- {"mode_light", "Světlý", "Light"},
- {"theme", "TÉMA", "THEME"},
- {"language", "JAZYK", "LANGUAGE"},
- {"welcome_body",
- "maturita.C je vzdělávací program pro studenty středních škol a "
- "gymnázií na přípravu k maturitě.\n\n"
- "Program je napsaný v Céčku studentama ze SSŠVT!",
- "maturita.C is an educational app for high-school and gymnasium "
- "students to prepare for their maturita exam.\n\n"
- "The program is written in C by students from SSŠVT!"},
- {"continue", "Pokračuj", "Continue"},
- {"roadmap_title", "Učební plán", "Learning path"},
- {"roadmap_sub", "Vyberte jednotku na cestě a začněte procvičovat.",
- "Pick a unit on the path and start practicing."},
- {"unit1_sub", "Vyberte cvičení a dokončete je.",
- "Choose an exercise and complete it."},
- {"check", "Zkontrolovat", "Check"},
- {"finish", "Dokončit", "Finish"},
- {"show_sample", "Ukázat vzor", "Show sample"},
- {"your_answer", "Vaše odpověď…", "Your answer…"},
- {"feedback_ok", "Výborně!", "Great!"},
- {"feedback_retry", "Ještě to není správně. Zkuste to znovu.",
- "Not quite right yet. Try again."},
- {"feedback_retry_short", "Ještě to není správně.", "Not quite right yet."},
- {"feedback_sentences", "Některé věty nejsou správně. Zkuste to znovu.",
- "Some sentences are not correct. Try again."},
- {"tip_ss", "Písmeno „ß“ se dá na klávesnici zaměnit za „ss“!",
- "You can type “ss” instead of “ß” on the keyboard!"},
- {"hint_umlauts",
- "Na klávesnici nemáte německé znaky? Můžete psát ä jako ae, ö jako oe, "
- "ü jako ue a ß jako ss.",
- "No German keyboard? You can type ä as ae, ö as oe, ü as ue and ß as "
- "ss."},
- {"sample_fmt", "Otázka: %s\nVzor: %s (česky: %s)",
- "Question: %s\nSample: %s (English: %s)"},
- {"sub_dialog", "Doplňte chybějící slova v dialogu.",
- "Fill in the missing words in the dialogue."},
- {"sub_assembly", "Přetáhněte slova do správného pořadí.",
- "Drag the words into the correct order."},
- {"sub_choice_num", "Vyberte správnou číslovku.",
- "Choose the correct numeral."},
- {"sub_free", "Odpovězte vlastními slovy, poté klikněte na Dokončit.",
- "Answer in your own words, then click Finish."},
- {"sub_zahlen", "Vyberte správné slovo pro dané číslo.",
- "Choose the correct word for the given number."},
- {"sub_wieviel", "Spočítejte předměty a vyberte počet.",
- "Count the items and choose the amount."},
- {"sub_reihe", "Doplňte chybějící číslovku.",
- "Fill in the missing numeral."},
- {"sub_verb", "Vyberte správný tvar slovesa.",
- "Choose the correct verb form."},
- {"sub_wer", "Vyberte správné tázací slovo.",
- "Choose the correct question word."},
- {"sub_bild", "Vyberte správný tvar slovesa podle obrázku.",
- "Choose the correct verb form for the picture."},
- {"sub_gruss", "Roztřiďte pozdravy na pozdravy a rozloučení.",
- "Sort the phrases into greetings and farewells."},
- {"sub_land", "Přiřaďte symboly ke správné zemi.",
- "Match the symbols to the correct country."},
- {"assign_hint",
- "Vyberte skupinu a klikněte na kartu. Kliknutím na kartu ve skupině "
- "ji vrátíte zpět.",
- "Pick a group and click a card. Click a card inside a group to send "
- "it back."},
- {"unit2_sub", "Vyberte cvičení a dokončete je.",
- "Choose an exercise and complete it."},
- {"Bei uns zu Hause", "Bei uns zu Hause", "At Home with Us"},
- {"unit3_sub", "Vyberte cvičení a dokončete je.",
- "Choose an exercise and complete it."},
- {"sub3_pair", "Přiřaďte každému slovu jeho protějšek.",
- "Match each word with its counterpart."},
- {"sub3_poss", "Doplňte přivlastňovací zájmena (1. pád).",
- "Fill in the possessive pronouns (nominative)."},
- {"sub3_haustier", "Doplňte ein/kein ve správném tvaru.",
- "Fill in ein/kein in the correct form."},
- {"sub3_buchst", "Doplňte chybějící písmena.",
- "Fill in the missing letters."},
- {"sub3_marco", "Doplňte slova z nabídky.",
- "Fill in the words from the box."},
- {"sub3_sort", "Roztřiďte slova do dvou skupin.",
- "Sort the words into two groups."},
- {"sub3_tabelle", "Doplňte přivlastňovací zájmena do tabulky.",
- "Fill the possessive pronouns into the table."},
- {"sub3_akk", "Doplňte přivlastňovací zájmena (4. pád).",
- "Fill in the possessive pronouns (accusative)."},
- {"sub3_nicht", "Vyberte kein/keine/keinen/nicht.",
- "Choose kein/keine/keinen/nicht."},
- {"sub3_satz", "Sestavte správné věty.",
- "Build the correct sentences."},
- {"sub3_sehen", "Napište větu „Ich sehe…“.",
- "Write an “Ich sehe…” sentence."},
- {"sub3_wem", "Komu to patří? Doplňte zájmeno.",
- "Whose is it? Fill in the pronoun."},
- {"sub3_gibt", "Doplňte „Es gibt“ + ein/eine/einen.",
- "Fill in “Es gibt” + ein/eine/einen."},
- {"sub3_saetze", "Doplňte věty podle vzoru.",
- "Complete the sentences following the example."},
- {"sub3_wochen", "Doplňte chybějící písmena.",
- "Fill in the missing letters."},
- {"wordbank", "Nabídka slov:", "Word bank:"},
- {"sample_line", "Příklad: %s", "Sample: %s"},
- {"sub_verben", "Doplňte sloveso ve správném tvaru.",
- "Fill in the verb in the correct form."},
- {"sub_ausin", "Vyberte správnou předložku: aus nebo in.",
- "Choose the correct preposition: aus or in."},
- {"sub_sprich", "Doplňte sloveso „sprechen“.",
- "Fill in the verb “sprechen”."},
- {"sub_nation", "Doplňte národnost – použijte slova z nabídky.",
- "Fill in the nationality – use the words in the box."},
- {"sub_woher", "Odkud lidé pocházejí? Napište zemi.",
- "Where are the people from? Type the country."},
- {"sub_laender", "Doplňte chybějící písmena v názvech zemí.",
- "Type the full country name (the missing letters are shown as _)."},
- {"sub_verb2", "Vyberte správný tvar slovesa.",
- "Choose the correct verb form."},
- {"sub_euro", "Napište částku slovy.",
- "Write the amount in words."},
- {"sub_wortsuchen", "Najděte skryté slovo a napište ho.",
- "Find the hidden word and type it."},
- {"sub_ordne", "Přiřaďte správné tázací slovo a odpověď.",
- "Match each phrase with the right question word and answer."},
- {"sub_luecke", "Doplňte chybějící slova do textu.",
- "Fill the missing words into the text."},
- {"sub_verbinde", "Spojte každé podstatné jméno se správným slovesem.",
- "Match each noun with the right verb."},
- {"sub_zahlpaar", "Spojte čísla s německými slovy.",
- "Match the numbers with the German words."},
- {"sub_steckbrief", "Doplňte údaje o sobě.",
- "Complete the information about yourself."},
- {"sub_berufe", "Hádejte povolání dřív, než bude oběšenec hotový.",
- "Guess each job before the hangman drawing is finished."},
- {"sub_bistdu", "Odpovězte podle vlajky – jakou máte národnost?",
- "Answer according to the flag – what is your nationality?"},
- {"hm_progress", "Slovo %d / %d", "Word %d / %d"},
- {"hm_hint", "Nápověda", "Hint"},
- {"hm_wrong", "Slovo je uhodnuto!", "Word guessed!"},
- {"hm_fail", "Oběšenec! Slovo bylo „%s“.",
- "Hanged! The word was “%s”."},
- {"hm_retry", "Zkusit znovu", "Try again"},
- {"hm_done", "Výborně! Uhodli jste všechna povolání.",
- "Great! You guessed all the jobs."},
- {"hm_guess", "Hádejte písmeno", "Guess a letter"},
- {"stats", "Statistiky", "Statistics"},
- {"stats_title", "Statistiky", "Statistics"},
- {"stats_sub", "Přehled vašeho pokroku.", "Overview of your progress."},
- {"stats_ex_label", "Cvičení", "Exercises"},
- {"stats_pct_label", "Hotovo", "Done"},
- {"stats_units_label", "Jednotky", "Units"},
- {"stats_section", "Předměty", "Subjects"},
- {"stats_locked", "Zamčeno", "Locked"},
- {"stats_complete", "Hotovo", "Complete"},
- {"stats_ex_fmt", "%d / %d", "%d / %d"},
- {"stats_pct_fmt", "%d %%", "%d %%"},
- {"stats_units_fmt", "%d / %d", "%d / %d"},
- {"subjects_title", "Předměty", "Subjects"},
- {"subjects_sub", "Zatím je dostupný předmět Deutsch.",
- "Only Deutsch is available so far."},
- {"Český jazyk a literatura", "Český jazyk a literatura",
- "Czech Language and Literature"},
- {"Občanská nauka", "Občanská nauka", "Civics"},
- {"English", "English", "English"},
- {"Deutsch", "Deutsch", "Deutsch"},
- {"Matematika", "Matematika", "Mathematics"},
- {"Fyzika", "Fyzika", "Physics"},
- {"Základy Přírodopisných věd", "Základy Přírodopisných věd",
- "Fundamentals of Natural Sciences"},
- {"Technická grafika", "Technická grafika", "Technical Drawing"},
- {"Prezentační grafika", "Prezentační grafika", "Presentation Graphics"},
- {"Správa počítačových sítí", "Správa počítačových sítí",
- "Computer Network Administration"},
- {"Programování", "Programování", "Programming"},
- {"Technické vybavení", "Technické vybavení", "Computer Hardware"},
- {"Programové vybavení", "Programové vybavení", "Software"},
- {"net_sub", "Vyberte lekci na cestě a začněte procvičovat.",
- "Pick a unit on the path and start practicing."},
- {"net_unit1", "Výpočet IP adres", "IP Address Calculation"},
- {"net_unit1_sub", "4 snímky s postupem a pak cvičení.",
- "4 slides with the method, then an exercise."},
- {"net_ex_title", "Cvičení: výpočet IP adres",
- "Exercise: IP address calculation"},
- {"net_slide_next", "Další", "Next"},
- {"net_slide_prev", "Zpět", "Back"},
- {"net_slide_start", "Přejít na cvičení", "Go to the exercise"},
- {"net_solution", "Ukázat řešení", "Show solution"},
- {NULL, NULL, NULL}
-};
-
-/* Exercise content: the Czech text doubles as the lookup key, so `cs`
- * stays NULL and tr() falls back to the key itself. */
-static const TrEntry tr_content[] = {
- {"Ahoj! Jmenuji se Anna.", NULL, "Hi! My name is Anna."},
- {"Ahoj! Jmenuji se Petr.", NULL, "Hi! My name is Petr."},
- {"Jmenuji se Anna.", NULL, "My name is Anna."},
- {"Jmenuji se Petr.", NULL, "My name is Petr."},
- {"Odkud jsi?", NULL, "Where are you from?"},
- {"Odkud pocházíš?", NULL, "Where do you come from?"},
- {"Pocházím z Česka.", NULL, "I come from Czechia."},
- {"Dobré ráno!", NULL, "Good morning!"},
- {"Jak se máš?", NULL, "How are you?"},
- {"Mám se dobře, děkuji.", NULL, "I am fine, thank you."},
- {"Musím jít. Na shledanou!", NULL, "I have to go. Goodbye!"},
- {"Brzy na viděnou!", NULL, "See you soon!"},
- {"Čau!", NULL, "Bye!"},
- {"Jak se jmenuješ?", NULL, "What is your name?"},
- {"Kde bydlíš?", NULL, "Where do you live?"},
- {"Kdo to je?", NULL, "Who is that?"},
- {"Kolik je ti let?", NULL, "How old are you?"},
- {"Je mi šestnáct let.", NULL, "I am sixteen years old."},
- {"Bydlím v Praze.", NULL, "I live in Prague."},
- {"Hraji fotbal.", NULL, "I play football."},
- {"Co rád/a děláš?", NULL, "What do you like to do?"},
- {"Kolik je hodin?", NULL, "What time is it?"},
- {"Na shledanou.", NULL, "Goodbye."},
- {"Moc děkuji.", NULL, "Thank you very much."},
- {"„vierzehn“ = čtrnáct (14)", NULL, "“vierzehn” = fourteen (14)"},
- {"„siebzehn“ = sedmnáct (17)", NULL, "“siebzehn” = seventeen (17)"},
- {"„zwanzig“ = dvacet (20)", NULL, "“zwanzig” = twenty (20)"},
- {"„sechs“ = šest (6)", NULL, "“sechs” = six (6)"},
- {"čtrnáct", NULL, "fourteen"},
- {"sedmnáct", NULL, "seventeen"},
- {"dvacet", NULL, "twenty"},
- {"šest", NULL, "six"},
- {"třináct", NULL, "thirteen"},
- {"dvě jablka", NULL, "two apples"},
- {"pět hvězd", NULL, "five stars"},
- {"sedm koček", NULL, "seven cats"},
- {"devět květin", NULL, "nine flowers"},
- {"tři auta", NULL, "three cars"},
- {"jedenáct knih", NULL, "eleven books"},
- {"čtyři svíčky", NULL, "four candles"},
- {"osm míčů", NULL, "eight balls"},
- {"šest ptáků", NULL, "six birds"},
- {"šest, sedm, osm, devět", NULL, "six, seven, eight, nine"},
- {"dvanáct, třináct, čtrnáct, patnáct", NULL,
- "twelve, thirteen, fourteen, fifteen"},
- {"osmnáct, devatenáct, dvacet, dvacet jedna", NULL,
- "eighteen, nineteen, twenty, twenty-one"},
- {"čtyři, pět, šest, sedm", NULL, "four, five, six, seven"},
- {"deset, jedenáct, dvanáct, třináct", NULL,
- "ten, eleven, twelve, thirteen"},
- {"Plave v jezeře.", NULL, "He/She is swimming in the lake."},
- {"Zpívá píseň.", NULL, "He/She is singing a song."},
- {"Vaří polévku.", NULL, "He/She is cooking soup."},
- {"Čte knihu.", NULL, "He/She is reading a book."},
- {"Řídí auto.", NULL, "He/She is driving a car."},
- {"Maluje obraz.", NULL, "He/She is painting a picture."},
- {"Hraje fotbal.", NULL, "He/She is playing football."},
- {"Spí.", NULL, "He/She is sleeping."},
- {"Ahoj", NULL, "Hi"},
- {"Dobré ráno", NULL, "Good morning"},
- {"Dobrý den", NULL, "Good day"},
- {"Dobrý večer", NULL, "Good evening"},
- {"Dobrý den (Bavorsko, Rakousko)", NULL, "Good day (Bavaria, Austria)"},
- {"Ahoj / Čau", NULL, "Hi / Hey"},
- {"Čau", NULL, "Bye"},
- {"Na shledanou", NULL, "Goodbye"},
- {"Brzy na viděnou", NULL, "See you soon"},
- {"Do zítřka", NULL, "See you tomorrow"},
- {"Dobrou noc", NULL, "Good night"},
- {"Zatím / Na viděnou", NULL, "See you later"},
- {"Preclík", NULL, "Pretzel"},
- {"Klobása", NULL, "Sausage"},
- {"Braniborská brána", NULL, "Brandenburg Gate"},
- {"Značka aut", NULL, "Car brand"},
- {"Rakouský skladatel", NULL, "Austrian composer"},
- {"Čokoládový dort (Vídeň)", NULL, "Chocolate cake (Vienna)"},
- {"Vídeňský řízek", NULL, "Viennese schnitzel"},
- {"Sýr", NULL, "Cheese"},
- {"Švýcarská hora", NULL, "Swiss mountain"},
- {"Čokoláda", NULL, "Chocolate"},
- /* ---- unit 2: grammar & vocabulary meanings ---- */
- {"Pochází z Rakouska.", NULL, "He is from Austria."},
- {"Bydlí ve Štýrském Hradci (Graz).", NULL, "He lives in Graz."},
- {"Rád hraje golf.", NULL, "He likes playing golf."},
- {"Mluví německy, anglicky a italsky.", NULL,
- "He speaks German, English and Italian."},
- {"Pocházejí ze Slovenska.", NULL, "They are from Slovakia."},
- {"Bydlí v Bratislavě (ona).", NULL, "She lives in Bratislava."},
- {"Ona ráda tancuje.", NULL, "She likes dancing."},
- {"On rád cestuje.", NULL, "He likes travelling."},
- {"Jsou dobří přátelé.", NULL, "They are good friends."},
- {"Pocházím z Atén.", NULL, "I am from Athens."},
- {"Petr bydlí v Salcburku.", NULL, "Peter lives in Salzburg."},
- {"Berlín leží v Německu.", NULL, "Berlin is in Germany."},
- {"Bydlíme v Paříži.", NULL, "We live in Paris."},
- {"Pocházejí z Řecka.", NULL, "They are from Greece."},
- {"Odkud je? – Z Londýna.", NULL, "Where is he from? – From London."},
- {"Jak se jmenuje? – Andrea.", NULL, "What is her name? – Andrea."},
- {"Kdo pochází z Ruska? – Alexandr.", NULL,
- "Who comes from Russia? – Alexander."},
- {"Kde leží Praha? – V Česku.", NULL, "Where is Prague? – In Czechia."},
- {"Kde bydlí? – V Bratislavě.", NULL, "Where do they live? – In Bratislava."},
- {"Co děláte? – Studujeme germanistiku.", NULL,
- "What are you doing? – We're studying German studies."},
- {"Mluvím německy. A ty?", NULL, "I speak German. And you?"},
- {"Kterými jazyky mluvíte?", NULL, "Which languages do you speak?"},
- {"Mluvíš francouzsky? – Ano, trochu.", NULL,
- "Do you speak French? – Yes, a little."},
- {"My mluvíme polsky a vy?", NULL, "We speak Polish and you?"},
- {"Mluvíte anglicky? – Ano, velmi dobře.", NULL,
- "Do you speak English? – Yes, very well."},
- {"Nemluví ani slovo turecky.", NULL,
- "He doesn't speak a word of Turkish."},
- {"Čech", NULL, "Czech man"},
- {"Španělka", NULL, "Spanish woman"},
- {"Turek", NULL, "Turk (man)"},
- {"Chorvatka", NULL, "Croatian woman"},
- {"Němka", NULL, "German woman"},
- {"Rakušanka", NULL, "Austrian woman"},
- {"Slovák", NULL, "Slovak man"},
- {"Švýcarka", NULL, "Swiss woman"},
- {"Němec", NULL, "German man"},
- {"Německo", NULL, "Germany"},
- {"Deutschland – Německo", NULL, "Deutschland – Germany"},
- {"Slowakei – Slovensko", NULL, "Slowakei – Slovakia"},
- {"Österreich – Rakousko", NULL, "Österreich – Austria"},
- {"Spanien – Španělsko", NULL, "Spanien – Spain"},
- {"England – Anglie", NULL, "England – England"},
- {"Polen – Polsko", NULL, "Polen – Poland"},
- {"Slovensko", NULL, "Slovakia"},
- {"Rakousko", NULL, "Austria"},
- {"Španělsko", NULL, "Spain"},
- {"Anglie", NULL, "England"},
- {"Polsko", NULL, "Poland"},
- {"Rusko", NULL, "Russia"},
- {"Russland – Rusko", NULL, "Russland – Russia"},
- {"Türkei – Turecko", NULL, "Türkei – Turkey"},
- {"Slowakei – Slovensko", NULL, "Slowakei – Slovakia"},
- {"Kroatien – Chorvatsko", NULL, "Kroatien – Croatia"},
- {"Griechenland – Řecko", NULL, "Griechenland – Greece"},
- {"Italien – Itálie", NULL, "Italien – Italy"},
- {"Turecko", NULL, "Turkey"},
- {"Chorvatsko", NULL, "Croatia"},
- {"Řecko", NULL, "Greece"},
- {"Itálie", NULL, "Italy"},
- {"otec", NULL, "father"},
- {"rodiče", NULL, "parents"},
- {"Češka", NULL, "Czech woman"},
- {"student", NULL, "student"},
- {"Co čte? – Knihu.", NULL, "What is he reading? – A book."},
- {"Co studuje? – Medicínu.", NULL, "What is she studying? – Medicine."},
- {"Čím Jan rád jezdí? – Autem.", NULL,
- "What does Jan like to drive? – A car."},
- {"Kde bydlíte? – Ve Vídni.", NULL, "Where do you live? – In Vienna."},
- {"Kdo pracuje u Siemensu? – Moje kamarádka Věra.", NULL,
- "Who works at Siemens? – My friend Vera."},
- {"Kam jedeš?", NULL, "Where are you going?"},
- {"Co rád/a čteš?", NULL, "What do you like to read?"},
- {"Co rád/a hraješ?", NULL, "What do you like to play?"},
- {"Jmenuji se Lukáš.", NULL, "My name is Lukáš."},
- {"Jedu do Německa.", NULL, "I'm going to Germany."},
- {"Rád/a čtu detektivky.", NULL, "I like reading crime novels."},
- {"Rád/a hraju golf.", NULL, "I like playing golf."},
- {"šedesát tři eur", NULL, "sixty-three euros"},
- {"třicet osm eur", NULL, "thirty-eight euros"},
- {"čtyřicet pět eur", NULL, "forty-five euros"},
- {"padesát sedm eur", NULL, "fifty-seven euros"},
- {"devadesát devět eur", NULL, "ninety-nine euros"},
- {"Agnieszka Kowalski pochází z Polska, z Krakova.", NULL,
- "Agnieszka Kowalski is from Kraków, Poland."},
- {"Je jí 18 let a letos dělá maturitu.", NULL,
- "She is 18 years old and is taking her A-levels this year."},
- {"Ráda chatuje se svým německým přítelem.", NULL,
- "She likes chatting with her German friend."},
- {"Jmenuje se Stefan Böhmermann a bydlí v Drážďanech.", NULL,
- "His name is Stefan Böhmermann and he lives in Dresden."},
- {"Agnieszce přijde ten jazyk super.", NULL,
- "Agnieszka thinks the language is cool."},
- {"Už mluví velmi dobře německy.", NULL,
- "She already speaks German very well."},
- {"potřebovat mobil", NULL, "to need a mobile phone"},
- {"pracovat ve BMW", NULL, "to work at BMW"},
- {"řídit auto", NULL, "to drive a car"},
- {"navštěvovat gymnázium", NULL, "to attend a grammar school"},
- {"mluvit španělsky", NULL, "to speak Spanish"},
- {"číst detektivky", NULL, "to read crime novels"},
- {"padesát čtyři", NULL, "fifty-four"},
- {"čtyřicet pět", NULL, "forty-five"},
- {"tři sta šedesát devět", NULL, "three hundred and sixty-nine"},
- {"sto dvanáct", NULL, "one hundred and twelve"},
- {"sto dvacet dva", NULL, "one hundred and twenty-two"},
- {"šedesát osm", NULL, "sixty-eight"},
- {"Petr rád čte knihy.", NULL, "Peter likes reading books."},
- {"Filip pracuje v Salcburku.", NULL, "Filip works in Salzburg."},
- {"Magda dobře mluví anglicky.", NULL, "Magda speaks English well."},
- {"Marek navštěvuje gymnázium.", NULL, "Marek attends the grammar school."},
- {"Dominice je 15 let.", NULL, "Dominika is 15 years old."},
- {"Moje sestra se jmenuje Marta.", NULL, "My sister is called Marta."},
- {"Jak se jmenují? – Jana a Michael.", NULL,
- "What are their names? – Jana and Michael."},
- {"Kdo mluví polsky? – Jacek.", NULL, "Who speaks Polish? – Jacek."},
- {"Kde bydlí Eva? – V Plzni.", NULL, "Where does Eva live? – In Plzeň."},
- {"Odkud pochází Markus? – Z Německa.", NULL,
- "Where is Markus from? – From Germany."},
- {"Jakým jazykem mluví Jana? – Slovensky.", NULL,
- "What language does Jana speak? – Slovak."},
- /* ---- unit 2: hangman hints ---- */
- {"vaří v restauraci", NULL, "he cooks in a restaurant"},
- {"léčí nemocné lidi", NULL, "he treats sick people"},
- {"učí ve škole", NULL, "he teaches at school"},
- {"pracuje u policie", NULL, "he works for the police"},
- {"prodává v obchodě", NULL, "he sells in a shop"},
- /* ---- unit 2: word-level glosses for ex14/ex15 ---- */
- {"z", NULL, "from"},
- {"dělá", NULL, "does"},
- {"kamarád", NULL, "friend"},
- {"žije", NULL, "lives"},
- {"jazyk", NULL, "language"},
- {"dobře", NULL, "well"},
- {"potřebovat", NULL, "to need"},
- {"pracovat", NULL, "to work"},
- {"řídit", NULL, "to drive"},
- {"navštěvovat", NULL, "to attend"},
- {"mluvit", NULL, "to speak"},
- {"číst", NULL, "to read"},
- /* ---- unit 3: Bei uns zu Hause ---- */
- /* ex1 family word pairs */
- {"bratranec / sestřenice", NULL, "male cousin / female cousin"},
- {"strýc / teta", NULL, "uncle / aunt"},
- {"dědeček / babička", NULL, "grandfather / grandmother"},
- {"sestra / bratr", NULL, "sister / brother"},
- {"kamarád / kamarádka", NULL, "friend (m) / friend (f)"},
- {"dcera / syn", NULL, "daughter / son"},
- /* ex2 possessive pronouns (nominative) */
- {"Je to tvoje matka? – Ne, to není moje matka. To je jeho teta.", NULL,
- "Is that your mother? – No, that is not my mother. That is his aunt."},
- /* ex2 word-level glosses for the selected possessive pronouns */
- {"tvůj", NULL, "yours (m.)"},
- {"tvoje", NULL, "yours (f.)"},
- {"můj", NULL, "mine (m.)"},
- {"moje", NULL, "mine (f.)"},
- {"jeho", NULL, "his"},
- {"Je to tvůj bratr? – Ne, to není můj bratr. To je jeho bratranec.", NULL,
- "Is that your brother? – No, that is not my brother. That is his cousin."},
- {"Jsou to tvoji rodiče? – Ne, to nejsou moji rodiče. To jsou rodiče Markuse.",
- NULL, "Are those your parents? – No, those are not my parents. "
- "Those are Markus's parents."},
- /* ex3 ein/kein (pets) */
- {"Máš křečka? – Ne, nemám křečka. Mám želvu.", NULL,
- "Have you got a hamster? – No, I haven't got a hamster. I have a tortoise."},
- {"Máš koně? – Ne, nemám koně. Mám králíka.", NULL,
- "Have you got a horse? – No, I haven't got a horse. I have a rabbit."},
- {"Máš rybu? – Ne, nemám rybu. Mám ptáka.", NULL,
- "Have you got a fish? – No, I haven't got a fish. I have a bird."},
- {"Máš morče? – Ne, nemám morče. Mám andulku.", NULL,
- "Have you got a guinea pig? – No, I haven't got a guinea pig. I have a budgie."},
- /* ex4 missing letters (text) */
- {"Máme přátele v Rakousku. Bydlí ve Vídni.", NULL,
- "We have friends in Austria. They live in Vienna."},
- {"Jmenují se Elfriede a Jiří. Mají také děti.", NULL,
- "They are called Elfriede and Jiri. They also have children."},
- {"Jejich syn se jmenuje Philipp a jejich dcera Sabine.", NULL,
- "Their son is called Philipp and their daughter Sabine."},
- {"Rodina ráda cestuje.", NULL, "The family likes travelling."},
- {"Často je u nás na návštěvě v Česku.", NULL,
- "They often visit us in Czechia."},
- {"Rodina ráda cestuje. Často je u nás na návštěvě v Česku.", NULL,
- "The family likes travelling. They often visit us in Czechia."},
- {"Děti se učí také česky.", NULL,
- "The children are also learning Czech."},
- /* ex4 word-level glosses for the gapped words */
- {"mít", NULL, "to have"},
- {"přátelé", NULL, "friends"},
- {"bydlet", NULL, "to live"},
- {"jmenovat se", NULL, "to be called"},
- {"děti", NULL, "children"},
- {"syn", NULL, "son"},
- {"jejich", NULL, "their"},
- {"rodina", NULL, "family"},
- {"cestuje", NULL, "travels"},
- {"často", NULL, "often"},
- {"návštěva", NULL, "visit"},
- {"Česko", NULL, "Czechia"},
- {"učit se", NULL, "to learn"},
- {"čeština", NULL, "Czech"},
- /* ex5 Marco's family */
- {"Máme mužskou domácnost: Můj otec Johann (50), Ivo (16) a já.", NULL,
- "We live in an all-male household: my father Johann (50), Ivo (16) and me."},
- {"Ivo a já jsme stejně staří – jsme totiž dvojčata.", NULL,
- "Ivo and I are the same age – we are twins."},
- {"Náš otec tu má novou práci.", NULL,
- "Our father has a new job here."},
- {"Naši rodiče jsou rozvedení.", NULL,
- "Our parents are divorced."},
- {"Naše matka Maja (50) žije ve Vídni, ale pochází z Chorvatska.", NULL,
- "Our mother Maja (50) lives in Vienna, but she comes from Croatia."},
- {"Máme v Chorvatsku hodně strýců, tet, sestřenic a bratranců.", NULL,
- "We have many uncles, aunts, male and female cousins in Croatia."},
- {"Vídáme je někdy o prázdninách.", NULL,
- "We sometimes see them on holiday."},
- /* ex6 sorting vocabulary */
- {"křeček", NULL, "hamster"},
- {"babička", NULL, "grandmother"},
- {"kůň", NULL, "horse"},
- {"bratr", NULL, "brother"},
- {"kočka", NULL, "cat"},
- {"dědeček", NULL, "grandfather"},
- {"dcera", NULL, "daughter"},
- {"pes", NULL, "dog"},
- {"teta", NULL, "aunt"},
- {"andulka", NULL, "budgerigar"},
- /* ex8 possessive pronouns (accusative) */
- {"Potřebuješ svůj mobil.", NULL, "You need your mobile."},
- {"Má svého koně.", NULL, "He has his horse."},
- {"Dělá svou párty.", NULL, "She is throwing her party."},
- {"Ptá se svého učitele.", NULL, "It is asking its teacher."},
- {"Navštěvujeme náš víkendový dům.", NULL,
- "We are visiting our weekend house."},
- {"Vy vidíte svého psa.", NULL, "You see your dog."},
- {"Mají své nástroje.", NULL, "They have their instruments."},
- /* ex8 grey notes showing which possessive is expected */
- {"u3g_dein", "(tvůj)", "(your)"},
- {"u3g_sein", "(jeho)", "(his)"},
- {"u3g_ihr_sie", "(její)", "(her)"},
- {"u3g_sein_es", "(jeho)", "(its)"},
- {"u3g_unser", "(náš)", "(our)"},
- {"u3g_euer", "(váš)", "(your)"},
- {"u3g_ihr_sie_pl", "(jejich)", "(their)"},
- /* ex9 kein / nicht */
- {"Mluvíš španělsky? – Ne, nemluvím španělsky.", NULL,
- "Do you speak Spanish? – No, I don't speak any Spanish."},
- {"Máš bratra? – Ne, nemám bratra.", NULL,
- "Have you got a brother? – No, I haven't got a brother."},
- {"Je v Opavě zoo? – Ne, v Opavě žádná zoo není.", NULL,
- "Is there a zoo in Opava? – No, there is no zoo in Opava."},
- {"Jdeš do bazénu? – Ne, do bazénu nejdu.", NULL,
- "Are you going to the swimming pool? – No, I am not going to the swimming pool."},
- {"Rád chodí na túry? – Ne, nerad chodí na túry.", NULL,
- "Does he like hiking? – No, he does not like hiking."},
- {"Navštívíš dnes zámek? – Ne, dnes nenavštívím žádný zámek.", NULL,
- "Are you visiting a castle today? – No, I am not visiting a castle today."},
- /* ex10 sentence order */
- {"Mají želvu.", NULL, "They have a tortoise."},
- {"Co dělá tvoje kamarádka o víkendu?", NULL,
- "What does your friend do at the weekend?"},
- {"Můj otec je povoláním učitel.", NULL,
- "My father is a teacher by profession."},
- {"Je vás doma hodně?", NULL, "Are there many of you at home?"},
- {"Julie představuje svého psa Trixiho.", NULL,
- "Julia is introducing her dog Trixi."},
- /* ex11 what do you see */
- {"Vidím hrad / zámek.", NULL, "I see a castle."},
- {"Vidím jezero.", NULL, "I see a lake."},
- {"Vidím rodinu.", NULL, "I see a family."},
- /* ex12 whose is it */
- {"To je jeho mobil.", NULL, "That is his mobile."},
- {"To je její králík.", NULL, "That is her rabbit."},
- {"To jsou jejich brusle.", NULL, "Those are their inline skates."},
- {"To je její dort.", NULL, "That is her cake."},
- {"To je jeho dům.", NULL, "That is his house."},
- {"To jsou jejich děti.", NULL, "Those are their children."},
- /* ex13 es gibt */
- {"Je tu koncert.", NULL, "There is a concert."},
- {"Je tu výstava.", NULL, "There is an exhibition."},
- {"Je tu sportovní slavnost.", NULL, "There is a sports festival."},
- {"Je tu rodinné setkání.", NULL, "There is a family reunion."},
- {"Je tu divadelní představení.", NULL, "There is a theatre performance."},
- {"Je tu bleší trh.", NULL, "There is a flea market."},
- /* ex14 describe (sentence pairs) */
- {"Vidím ženu.", NULL, "I see a woman."},
- {"Jezdí na kole.", NULL, "She is riding a bike."},
- {"Vidím muže.", NULL, "I see a man."},
- {"Chodí po lese.", NULL, "He is hiking in the forest."},
- {"Vidím dvě děti.", NULL, "I see two children."},
- {"Hrají fotbal.", NULL, "They are playing football."},
- /* ex15 missing letters (weekend) */
- {"Moji prarodiče jedou o víkendu do víkendového domu.", NULL,
- "My grandparents are driving to their weekend house."},
- {"Můj otec a moje matka navštíví koncert.", NULL,
- "My father and my mother are going to a concert."},
- {"Moje sestra jde do kina.", NULL,
- "My sister is going to the cinema."},
- {"Můj bratr jede k jezeru.", NULL,
- "My brother is driving to a lake."},
- {"A já? Navštívím obchodní centrum.", NULL,
- "And me? I am going to a shopping centre."},
- {NULL, NULL, NULL}
-};
-
-static const char *tr(const char *key) {
- static GHashTable *idx;
- const TrEntry *e;
- int i;
-
- if (!key)
- return NULL;
-
- if (!idx) {
- idx = g_hash_table_new(g_str_hash, g_str_equal);
- for (i = 0; tr_ui[i].key; i++)
- g_hash_table_insert(idx, (gpointer)tr_ui[i].key,
- (gpointer)&tr_ui[i]);
- for (i = 0; tr_content[i].key; i++)
- g_hash_table_insert(idx, (gpointer)tr_content[i].key,
- (gpointer)&tr_content[i]);
- }
-
- e = g_hash_table_lookup(idx, key);
- if (!e)
- return key;
- if (app_lang == LANG_EN)
- return e->en;
- return e->cs ? e->cs : e->key;
-}
-
-static void i18n_ensure(void) {
- if (!i18n_binds)
- i18n_binds = g_array_new(FALSE, FALSE, sizeof(I18nBind));
-}
-
-static void i18n_apply_one(const I18nBind *b) {
- if (!b->widget || !b->key)
- return;
- switch (b->kind) {
- case 0:
- gtk_label_set_text(GTK_LABEL(b->widget), tr(b->key));
- break;
- case 1:
- gtk_button_set_label(GTK_BUTTON(b->widget), tr(b->key));
- break;
- case 2:
- gtk_widget_set_tooltip_text(b->widget, tr(b->key));
- break;
- case 3:
- gtk_entry_set_placeholder_text(GTK_ENTRY(b->widget), tr(b->key));
- break;
- case 4:
- ex4_fill_sample(b->widget);
- break;
- default:
- break;
- }
-}
-
-static void i18n_bind(GtkWidget *widget, const char *key, int kind) {
- I18nBind b;
-
- i18n_ensure();
- b.widget = widget;
- b.key = key;
- b.kind = kind;
- g_array_append_val(i18n_binds, b);
-
- i18n_apply_one(&b);
-}
-
-static void refresh_welcome_heading(void) {
- char *markup;
-
- if (!welcome_heading)
- return;
- if (app_lang == LANG_EN)
- markup = g_strdup_printf(
- "Welcome to maturita.C!",
- app_theme.accent);
- else
- markup = g_strdup_printf(
- "Vítejte ve maturita.C!",
- app_theme.accent);
- gtk_label_set_markup(GTK_LABEL(welcome_heading), markup);
- g_free(markup);
-}
-
-static void refresh_stats_ui(void);
-static void net_slide_apply(void);
-static void net_rail_theme_reset(void);
-
-static void apply_language(void) {
- guint i;
-
- i18n_ensure();
- for (i = 0; i < i18n_binds->len; i++)
- i18n_apply_one(&g_array_index(i18n_binds, I18nBind, i));
- refresh_welcome_heading();
- refresh_stats_ui();
- net_slide_apply();
-}
-
-static char *read_css_file(void) {
- gchar *contents = NULL;
- GError *err = NULL;
-
- if (!g_file_get_contents(CSS_FILE, &contents, NULL, &err)) {
- g_warning("Could not load stylesheet '%s': %s", CSS_FILE,
- err ? err->message : "unknown error");
- if (err)
- g_error_free(err);
- return g_strdup("");
- }
- return contents;
-}
-
-/* The palette is data-driven, so its @define-color bindings are generated
- * here and prepended to the rules loaded from the external stylesheet. */
-static char *build_theme_css(const ThemePalette *p) {
- char *palette = g_strdup_printf(
- "@define-color bg_crust #%06x;\n"
- "@define-color bg_base #%06x;\n"
- "@define-color bg_mantle #%06x;\n"
- "@define-color bg_surface0 #%06x;\n"
- "@define-color bg_surface1 #%06x;\n"
- "@define-color bg_surface2 #%06x;\n"
- "@define-color fg_overlay #%06x;\n"
- "@define-color fg_text #%06x;\n"
- "@define-color fg_subtext #%06x;\n"
- "@define-color accent #%06x;\n"
- "@define-color accent2 #%06x;\n"
- "@define-color accent3 #%06x;\n"
- "@define-color success #%06x;\n"
- "@define-color success2 #%06x;\n"
- "@define-color warning #%06x;\n"
- "@define-color error #%06x;\n"
- "@define-color on_accent #%06x;\n"
- "@define-color node_bg #%06x;\n"
- "@define-color locked_bg #%06x;\n"
- "@define-color locked_border #%06x;\n",
- p->crust, p->base, p->mantle, p->surface0, p->surface1, p->surface2,
- p->overlay, p->text, p->subtext, p->accent, p->accent2, p->accent3,
- p->success, p->success2, p->warning, p->error, p->on_accent,
- p->node, p->locked_bg, p->locked_border);
- char *rules = read_css_file();
- char *css = g_strconcat(palette, rules, NULL);
-
- g_free(palette);
- g_free(rules);
- return css;
-}
-
-static void save_settings(void) {
- GKeyFile *kf = g_key_file_new();
- gchar *data;
-
- g_key_file_set_string(kf, "ui", "theme", theme_names[app_theme_id]);
- g_key_file_set_string(kf, "ui", "mode",
- app_color_mode == MODE_LIGHT ? "light" : "dark");
- g_key_file_set_string(kf, "ui", "lang",
- app_lang == LANG_EN ? "en" : "cs");
-
- if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
- g_key_file_free(kf);
- return;
- }
-
- data = g_key_file_to_data(kf, NULL, NULL);
- g_key_file_free(kf);
- if (data) {
- GError *err = NULL;
- g_file_set_contents(SETTINGS_FILE, data, -1, &err);
- if (err)
- g_error_free(err);
- g_free(data);
- }
-}
-
-static void load_settings(void) {
- GKeyFile *kf = g_key_file_new();
- GError *err = NULL;
- gchar *theme = NULL;
- gchar *mode = NULL;
-
- if (!g_key_file_load_from_file(kf, SETTINGS_FILE, G_KEY_FILE_NONE, &err)) {
- if (err)
- g_error_free(err);
- g_key_file_free(kf);
- return;
- }
-
- theme = g_key_file_get_string(kf, "ui", "theme", NULL);
- mode = g_key_file_get_string(kf, "ui", "mode", NULL);
- if (theme) {
- for (int i = 0; i < THEME_COUNT; i++) {
- if (g_ascii_strcasecmp(theme, theme_names[i]) == 0) {
- app_theme_id = (ThemeId)i;
- break;
- }
- }
- g_free(theme);
- }
- if (mode) {
- if (g_ascii_strcasecmp(mode, "light") == 0 ||
- g_ascii_strcasecmp(mode, "white") == 0)
- app_color_mode = MODE_LIGHT;
- else
- app_color_mode = MODE_DARK;
- g_free(mode);
- }
- {
- gchar *lang = g_key_file_get_string(kf, "ui", "lang", NULL);
- if (lang) {
- if (g_ascii_strcasecmp(lang, "en") == 0 ||
- g_ascii_strcasecmp(lang, "english") == 0)
- app_lang = LANG_EN;
- else
- app_lang = LANG_CS;
- g_free(lang);
- }
- }
-
- g_key_file_free(kf);
-}
-
-/* GTK only re-resolves the style of widgets that are actually drawn after a
- * CSS provider reload. Pages that are not the current stack child keep their
- * previously resolved colors, so after switching the theme any page you then
- * open still shows the old palette (e.g. near-white text in light mode).
- * Adding and immediately removing a dummy class forces GTK to invalidate the
- * style node of every widget, including hidden pages. */
-static void force_restyle_tree(GtkWidget *w) {
- GtkWidget *child;
-
- if (!w)
- return;
-
- gtk_widget_add_css_class(w, "m4-restyle");
- gtk_widget_remove_css_class(w, "m4-restyle");
-
- for (child = gtk_widget_get_first_child(w); child;
- child = gtk_widget_get_next_sibling(child))
- force_restyle_tree(child);
-}
-
-static void apply_theme(void) {
- char *css;
- GtkSettings *settings = gtk_settings_get_default();
-
- app_theme = theme_palette(app_theme_id, app_color_mode);
-
- if (settings) {
- g_object_set(settings,
- "gtk-application-prefer-dark-theme",
- app_color_mode == MODE_DARK,
- NULL);
- }
-
- css = build_theme_css(&app_theme);
-
- if (!theme_provider) {
- theme_provider = gtk_css_provider_new();
- gtk_style_context_add_provider_for_display(
- gdk_display_get_default(),
- GTK_STYLE_PROVIDER(theme_provider),
- GTK_STYLE_PROVIDER_PRIORITY_USER);
- }
-
- gtk_css_provider_load_from_string(theme_provider, css);
- g_free(css);
-
- refresh_welcome_heading();
-
- if (main_window) {
- gtk_widget_queue_draw(GTK_WIDGET(main_window));
- force_restyle_tree(GTK_WIDGET(main_window));
- }
- if (road_rail)
- gtk_widget_queue_draw(road_rail);
- for (int i = 0; i < NUM_UNLOCKED; i++)
- if (units[i].ex_rail)
- gtk_widget_queue_draw(units[i].ex_rail);
- for (int i = 0; i < THEME_COUNT; i++) {
- if (theme_swatch_areas[i])
- gtk_widget_queue_draw(theme_swatch_areas[i]);
- }
- net_rail_theme_reset();
-}
-
-/* Vector icons drawn with cairo so they never depend on the system
- * icon theme (which is missing on some installs). */
-
-static void draw_check_icon(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- Rgb c = color_from_hex(app_theme.on_accent);
-
- (void)area;
- (void)data;
- cairo_set_source_rgb(cr, c.r, c.g, c.b);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
- cairo_set_line_width(cr, width * 0.16);
- cairo_move_to(cr, width * 0.24, height * 0.56);
- cairo_line_to(cr, width * 0.43, height * 0.74);
- cairo_line_to(cr, width * 0.78, height * 0.30);
- cairo_stroke(cr);
-}
-
-/* Paint a padlock centred in the size x size box with top-left (x0,y0). */
-static void lock_paint(cairo_t *cr, double x0, double y0, double size,
- const Rgb *c) {
- double sl = MAX(1.6, size * 0.13); /* shackle stroke width */
- double bw = size * 0.60; /* body width */
- double bh = size * 0.44; /* body height */
- double cx = x0 + size / 2.0;
- double body_top = y0 + size - bh - size * 0.04;
- double bx = cx - bw / 2.0;
- double r = bw / 2.0 - sl / 2.0; /* shackle arc radius */
-
- cairo_set_source_rgb(cr, c->r, c->g, c->b);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_width(cr, sl);
-
- cairo_new_path(cr);
- cairo_arc(cr, cx, body_top, r, G_PI, 2.0 * G_PI);
- cairo_stroke(cr);
-
- cairo_new_path(cr);
- cairo_rectangle(cr, bx, body_top, bw, bh);
- cairo_fill(cr);
-
- cairo_new_path(cr);
- cairo_arc(cr, cx, body_top + bh * 0.42, size * 0.07, 0.0, 2.0 * G_PI);
- {
- Rgb hole = color_from_hex(app_theme.crust);
- cairo_set_source_rgb(cr, hole.r, hole.g, hole.b);
- }
- cairo_fill(cr);
-}
-
-static void draw_lock_icon(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- Rgb c = color_from_hex(app_theme.overlay);
- (void)area;
- (void)data;
- lock_paint(cr, 0.0, 0.0, MIN(width, height), &c);
-}
-
-/* Wireless icon: three upward arcs that fan out from a base dot. */
-static void draw_wifi_icon(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- const double s = MIN(width, height);
- const double cx = width / 2.0;
- const double cy = height * 0.68;
- const double dot_r = s * 0.06;
- const double radii[3] = {s * 0.15, s * 0.27, s * 0.39};
- const double phi = 0.75;
- /* Keep the icon legible on the accent-gradient bubble while giving it a
- * hint of the active theme: blend the contrasting on-accent colour a
- * little way towards the palette's middle accent. */
- const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent),
- color_from_hex(app_theme.accent2), 0.22);
-
- (void)area;
- (void)data;
-
- cairo_set_source_rgb(cr, c.r, c.g, c.b);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_width(cr, MAX(1.5, s * 0.07));
-
- for (int i = 0; i < 3; i++) {
- double r = radii[i];
-
- cairo_new_path(cr);
- cairo_arc(cr, cx, cy, r, -G_PI / 2.0 - phi, -G_PI / 2.0 + phi);
- cairo_stroke(cr);
- }
-
- cairo_new_path(cr);
- cairo_arc(cr, cx, cy, dot_r, 0.0, 2.0 * G_PI);
- cairo_fill(cr);
-}
-
-static void draw_back_icon(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- const double size = 15.0; /* fixed logical icon size */
- const double stroke = 1.8;
- double ox = (width - size) / 2.0;
- double oy = (height - size) / 2.0;
- GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(area));
- GdkRGBA color;
-
- (void)data;
- gtk_style_context_get_color(gtk_widget_get_style_context(parent), &color);
- cairo_set_source_rgb(cr, color.red, color.green, color.blue);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
- cairo_set_line_width(cr, stroke);
- cairo_move_to(cr, ox + size * 0.72, oy + size * 0.24);
- cairo_line_to(cr, ox + size * 0.30, oy + size * 0.50);
- cairo_line_to(cr, ox + size * 0.72, oy + size * 0.76);
- cairo_stroke(cr);
-}
-
-static void draw_settings_icon(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- const int teeth = 8;
- const double size = 16.0;
- double cx = width / 2.0;
- double cy = height / 2.0;
- double r_outer = size * 0.48;
- double r_inner = size * 0.34;
- double r_hole = size * 0.16;
- double tooth_half = G_PI / (double)teeth;
- GtkWidget *color_w = data ? GTK_WIDGET(data)
- : gtk_widget_get_parent(GTK_WIDGET(area));
- GdkRGBA color;
- int i;
-
- gtk_style_context_get_color(gtk_widget_get_style_context(color_w), &color);
- cairo_set_source_rgb(cr, color.red, color.green, color.blue);
- cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
-
- cairo_new_path(cr);
- for (i = 0; i < teeth; i++) {
- double a = -G_PI / 2.0 + (2.0 * G_PI * (double)i) / (double)teeth;
- double a0 = a - tooth_half * 0.55;
- double a1 = a - tooth_half * 0.28;
- double a2 = a + tooth_half * 0.28;
- double a3 = a + tooth_half * 0.55;
-
- if (i == 0)
- cairo_move_to(cr, cx + cos(a0) * r_inner, cy + sin(a0) * r_inner);
- else
- cairo_line_to(cr, cx + cos(a0) * r_inner, cy + sin(a0) * r_inner);
-
- cairo_line_to(cr, cx + cos(a1) * r_outer, cy + sin(a1) * r_outer);
- cairo_line_to(cr, cx + cos(a2) * r_outer, cy + sin(a2) * r_outer);
- cairo_line_to(cr, cx + cos(a3) * r_inner, cy + sin(a3) * r_inner);
- }
- cairo_close_path(cr);
- cairo_new_sub_path(cr);
- cairo_arc(cr, cx, cy, r_hole, 0.0, 2.0 * G_PI);
- cairo_fill(cr);
-}
-
-static void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- GtkWidget *color_w = data ? GTK_WIDGET(data)
- : gtk_widget_get_parent(GTK_WIDGET(area));
- GdkRGBA color;
- const double bar_w = 3.2;
- const double gap = 2.6;
- const double total_w = 3.0 * bar_w + 2.0 * gap;
- const double heights[3] = {0.42, 0.72, 1.0};
- double base_x = (width - total_w) / 2.0;
- double base_y = height * 0.82;
- double max_h = height * 0.58;
- int i;
-
- (void)area;
- gtk_style_context_get_color(gtk_widget_get_style_context(color_w), &color);
- cairo_set_source_rgb(cr, color.red, color.green, color.blue);
- cairo_set_line_width(cr, 1.5);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
-
- for (i = 0; i < 3; i++) {
- double x = base_x + (double)i * (bar_w + gap);
- double h = max_h * heights[i];
- double y = base_y - h;
- double r = 1.2;
-
- cairo_new_sub_path(cr);
- cairo_arc(cr, x + r, y + r, r, G_PI, 1.5 * G_PI);
- cairo_arc(cr, x + bar_w - r, y + r, r, 1.5 * G_PI, 2.0 * G_PI);
- cairo_line_to(cr, x + bar_w, base_y);
- cairo_line_to(cr, x, base_y);
- cairo_close_path(cr);
- cairo_fill(cr);
- }
-}
-
-static GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
- double r, double g, double b, int px) {
- Rgb *col = g_new(Rgb, 1);
- GtkWidget *d = gtk_drawing_area_new();
-
- col->r = r;
- col->g = g;
- col->b = b;
- gtk_widget_set_size_request(d, px, px);
- gtk_widget_set_halign(d, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(d, GTK_ALIGN_CENTER);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(d), fn,
- col, (GDestroyNotify)g_free);
- return d;
-}
-
-/* ------------------------------------------------------------------ */
-/* Utilities */
-/* ------------------------------------------------------------------ */
-
-static char *normalize_answer(const char *input) {
- GString *out = g_string_new(NULL);
- const gchar *down = g_utf8_strdown(input, -1);
- const gchar *p = down;
- gboolean prev_space = FALSE;
-
- while (*p) {
- gunichar ch = g_utf8_get_char(p);
- p = g_utf8_next_char(p);
- if (g_unichar_isspace(ch)) {
- if (out->len > 0 && !prev_space)
- g_string_append_c(out, ' ');
- prev_space = TRUE;
- continue;
- }
- prev_space = FALSE;
- switch (ch) {
- case 0x00e4: g_string_append(out, "ae"); break; /* ä */
- case 0x00f6: g_string_append(out, "oe"); break; /* ö */
- case 0x00fc: g_string_append(out, "ue"); break; /* ü */
- case 0x00df: g_string_append(out, "ss"); break; /* ß */
- default: g_string_append_unichar(out, ch); break;
- }
- }
-
- if (out->len > 0 && out->str[out->len - 1] == ' ')
- g_string_truncate(out, out->len - 1);
-
- g_free((gpointer)down);
- return g_string_free(out, FALSE);
-}
-
-static void shuffle_indices(int *arr, int n) {
- for (int i = n - 1; i > 0; i--) {
- int j = g_random_int_range(0, i + 1);
- int tmp = arr[i];
- arr[i] = arr[j];
- arr[j] = tmp;
- }
-}
-
-static void flow_clear(GtkFlowBox *fb) {
- GtkWidget *c = gtk_widget_get_first_child(GTK_WIDGET(fb));
- while (c) {
- GtkWidget *next = gtk_widget_get_next_sibling(c);
- gtk_flow_box_remove(fb, c);
- c = next;
- }
-}
-
-static void answer_mark(GtkWidget *widget, gboolean ok) {
- if (ok) {
- gtk_widget_remove_css_class(widget, "answer-wrong");
- gtk_widget_add_css_class(widget, "answer-ok");
- } else {
- gtk_widget_remove_css_class(widget, "answer-ok");
- gtk_widget_add_css_class(widget, "answer-wrong");
- }
-}
-
-static void set_feedback(GtkWidget *label, gboolean ok, const char *text) {
- gtk_widget_remove_css_class(label, "feedback-ok");
- gtk_widget_remove_css_class(label, "feedback-err");
- gtk_widget_add_css_class(label, ok ? "feedback-ok" : "feedback-err");
- gtk_label_set_text(GTK_LABEL(label), text);
-}
-
-/* True if `s` contains any German special letter (ä, ö, ü or ß). */
-static gboolean text_has_german_umlaut(const char *s) {
- const char *p = s ? s : "";
-
- while (*p) {
- gunichar c = g_utf8_get_char(p);
-
- if (c == 0x00e4 || c == 0x00f6 || c == 0x00fc || c == 0x00df)
- return TRUE;
- p = g_utf8_next_char(p);
- }
- return FALSE;
-}
-
-/* Small bilingual note shown under the exercise title whenever the answer
- * the student has to type may contain a German special letter. */
-static void add_umlaut_note(GtkWidget *body) {
- GtkWidget *n = gtk_label_new(NULL);
-
- i18n_bind(n, "hint_umlauts", 0);
- gtk_widget_set_halign(n, GTK_ALIGN_START);
- gtk_label_set_wrap(GTK_LABEL(n), TRUE);
- gtk_widget_set_margin_bottom(n, 2);
- gtk_widget_add_css_class(n, "hint");
- gtk_box_append(GTK_BOX(body), n);
-}
-
-/* ------------------------------------------------------------------ */
-/* Progress */
-/* ------------------------------------------------------------------ */
-
-static void unit_save_progress(UnitCtx *u) {
- GKeyFile *kf = g_key_file_new();
- for (int i = 1; i <= u->n_ex; i++) {
- gchar key[8];
- g_snprintf(key, sizeof(key), "%d", i);
- g_key_file_set_boolean(kf, "done", key, u->done[i]);
- }
-
- if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
- g_key_file_free(kf);
- return;
- }
-
- gchar *data = g_key_file_to_data(kf, NULL, NULL);
- g_key_file_free(kf);
-
- if (data) {
- GError *err = NULL;
- g_file_set_contents(u->progress_file, data, -1, &err);
- if (err)
- g_error_free(err);
- g_free(data);
- }
-}
-
-static void unit_load_progress(UnitCtx *u) {
- GKeyFile *kf = g_key_file_new();
- GError *err = NULL;
-
- if (!g_key_file_load_from_file(kf, u->progress_file, G_KEY_FILE_NONE, &err)) {
- if (err)
- g_error_free(err);
- g_key_file_free(kf);
- return;
- }
-
- for (int i = 1; i <= u->n_ex; i++) {
- gchar key[8];
- g_snprintf(key, sizeof(key), "%d", i);
- u->done[i] = g_key_file_get_boolean(kf, "done", key, NULL);
- }
-
- g_key_file_free(kf);
-}
-
-static void load_progress(void) {
- for (int i = 0; i < NUM_UNLOCKED; i++)
- unit_load_progress(&units[i]);
-}
-
-/* Redraw the exercise rails of every unit that already has a page. */
-static void all_rails_redraw(void) {
- for (int i = 0; i < NUM_UNLOCKED; i++) {
- if (units[i].ex_rail)
- gtk_widget_queue_draw(units[i].ex_rail);
- }
-}
-
-static void refresh_completion_ui(void) {
- for (int uidx = 0; uidx < NUM_UNLOCKED; uidx++) {
- UnitCtx *u = &units[uidx];
- gboolean all = TRUE;
-
- for (int i = 1; i <= u->n_ex; i++) {
- if (!u->done[i]) {
- all = FALSE;
- break;
- }
- }
-
- /* per-bubble markers */
- if (u->ex_cells[0] != NULL) {
- for (int i = 1; i <= u->n_ex; i++) {
- GtkWidget *btn = u->ex_cells[i - 1];
- if (!btn)
- continue;
- if (u->done[i]) {
- gtk_widget_add_css_class(btn, "done");
- if (u->ex_icons[i])
- gtk_widget_set_visible(u->ex_icons[i], TRUE);
- } else {
- gtk_widget_remove_css_class(btn, "done");
- if (u->ex_icons[i])
- gtk_widget_set_visible(u->ex_icons[i], FALSE);
- }
- }
- }
-
- /* roadmap node marker */
- if (u->node) {
- if (all && u->n_ex > 0) {
- gtk_widget_remove_css_class(u->node, "current");
- gtk_widget_add_css_class(u->node, "done");
- if (u->node_done_icon)
- gtk_widget_set_visible(u->node_done_icon, TRUE);
- } else {
- gtk_widget_remove_css_class(u->node, "done");
- gtk_widget_add_css_class(u->node, "current");
- if (u->node_done_icon)
- gtk_widget_set_visible(u->node_done_icon, FALSE);
- }
- }
- }
-
- all_rails_redraw();
- refresh_stats_ui();
-}
-
-static void mark_done(UnitCtx *u, int n) {
- if (!u || n < 1 || n > u->n_ex)
- return;
- u->done[n] = TRUE;
- refresh_completion_ui();
- unit_save_progress(u);
-}
-
-/* ------------------------------------------------------------------ */
-/* Statistics */
-/* ------------------------------------------------------------------ */
-
-static GtkWidget *make_back_button(const char *target);
-static void on_nav_clicked(GtkButton *button, gpointer user_data);
-
-typedef struct {
- GtkWidget *ex_value;
- GtkWidget *pct_value;
- GtkWidget *units_value;
- GtkWidget *overall_bar;
-
- /* one progress block per subject (index matches sub_keys[]) */
- struct {
- GtkWidget *name; /* subject heading label */
- GtkWidget *count; /* "13 / 47" or the locked text */
- GtkWidget *bar; /* per-subject overall progress bar */
- GtkWidget *units_box; /* holds the unit rows, NULL if no units */
- } subj[NUM_SUBJECTS];
-
- /* per-unit rows, indexed by the global units[] index */
- GtkWidget *unit_count[NUM_UNITS];
- GtkWidget *unit_badge[NUM_UNITS];
- GtkWidget *unit_bar[NUM_UNITS];
- GtkWidget *unit_row[NUM_UNITS];
-} StatsUi;
-
-static StatsUi stats_ui;
-static char *stats_return_page;
-
-static int unit_done_count(const UnitCtx *u) {
- int n = 0;
-
- if (!u)
- return 0;
- for (int i = 1; i <= u->n_ex; i++) {
- if (u->done[i])
- n++;
- }
- return n;
-}
-
-typedef struct {
- int done_ex;
- int total_ex;
- int done_units;
- int open_units;
-} ProgressSum;
-
-/* Sum progress over units[first .. first+n-1]. Locked placeholder units and
- * the finish node (no playable exercises) are ignored. */
-static void progress_for_units(int first, int n, ProgressSum *out) {
- for (int j = 0; j < n; j++) {
- UnitCtx *u;
- int d;
-
- if (first < 0 || first + j >= NUM_UNITS)
- continue;
- u = &units[first + j];
- if (!u->unlocked || u->n_ex <= 0)
- continue;
- d = unit_done_count(u);
- out->done_ex += d;
- out->total_ex += u->n_ex;
- out->open_units++;
- if (d == u->n_ex)
- out->done_units++;
- }
-}
-
-static void refresh_stats_ui(void) {
- ProgressSum all = {0};
- int pct;
- char *tmp;
-
- if (!stats_ui.ex_value)
- return;
-
- for (int s = 0; s < NUM_SUBJECTS; s++)
- progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
-
- pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0;
-
- tmp = g_strdup_printf(tr("stats_ex_fmt"), all.done_ex, all.total_ex);
- gtk_label_set_text(GTK_LABEL(stats_ui.ex_value), tmp);
- g_free(tmp);
-
- tmp = g_strdup_printf(tr("stats_pct_fmt"), pct);
- gtk_label_set_text(GTK_LABEL(stats_ui.pct_value), tmp);
- g_free(tmp);
-
- tmp = g_strdup_printf(tr("stats_units_fmt"), all.done_units,
- all.open_units);
- gtk_label_set_text(GTK_LABEL(stats_ui.units_value), tmp);
- g_free(tmp);
-
- gtk_progress_bar_set_fraction(
- GTK_PROGRESS_BAR(stats_ui.overall_bar),
- all.total_ex > 0 ? (double)all.done_ex / (double)all.total_ex : 0.0);
-
- for (int s = 0; s < NUM_SUBJECTS; s++) {
- GtkWidget *count = stats_ui.subj[s].count;
- GtkWidget *bar = stats_ui.subj[s].bar;
- ProgressSum sp = {0};
-
- if (!count || !bar)
- continue;
- progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp);
- if (sub_unit_count[s] > 0) {
- tmp = g_strdup_printf(tr("stats_ex_fmt"), sp.done_ex, sp.total_ex);
- gtk_label_set_text(GTK_LABEL(count), tmp);
- g_free(tmp);
- gtk_progress_bar_set_fraction(
- GTK_PROGRESS_BAR(bar),
- sp.total_ex > 0 ? (double)sp.done_ex / (double)sp.total_ex
- : 0.0);
- } else {
- gtk_label_set_text(GTK_LABEL(count), tr("stats_locked"));
- gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), 0.0);
- }
- }
-
- for (int i = 0; i < NUM_UNITS; i++) {
- UnitCtx *u = &units[i];
- GtkWidget *count = stats_ui.unit_count[i];
- GtkWidget *badge = stats_ui.unit_badge[i];
- GtkWidget *bar = stats_ui.unit_bar[i];
- GtkWidget *row = stats_ui.unit_row[i];
- gboolean unlocked = u->unlocked && u->n_ex > 0;
- int d = unlocked ? unit_done_count(u) : 0;
- gboolean complete = unlocked && d == u->n_ex;
-
- if (!count || !bar || !row)
- continue;
-
- gtk_widget_remove_css_class(row, "done");
- gtk_widget_remove_css_class(row, "locked");
- gtk_widget_remove_css_class(bar, "done");
-
- if (!unlocked) {
- gtk_widget_add_css_class(row, "locked");
- gtk_label_set_text(GTK_LABEL(count), tr("stats_locked"));
- gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), 0.0);
- if (badge)
- gtk_widget_set_visible(badge, FALSE);
- continue;
- }
-
- tmp = g_strdup_printf(tr("stats_ex_fmt"), d, u->n_ex);
- gtk_label_set_text(GTK_LABEL(count), tmp);
- g_free(tmp);
- gtk_progress_bar_set_fraction(
- GTK_PROGRESS_BAR(bar),
- u->n_ex > 0 ? (double)d / (double)u->n_ex : 0.0);
-
- if (complete) {
- gtk_widget_add_css_class(row, "done");
- gtk_widget_add_css_class(bar, "done");
- if (badge) {
- gtk_label_set_text(GTK_LABEL(badge), tr("stats_complete"));
- gtk_widget_set_visible(badge, TRUE);
- }
- } else if (badge) {
- gtk_widget_set_visible(badge, FALSE);
- }
- }
-}
-
-static void on_stats_back_clicked(GtkButton *button, gpointer user_data) {
- const char *target = stats_return_page;
-
- (void)button;
- (void)user_data;
- if (!target || !target[0])
- target = "subjects";
- gtk_stack_set_visible_child_name(main_stack, target);
-}
-
-static void on_stats_clicked(GtkButton *button, gpointer user_data) {
- const char *cur;
-
- (void)button;
- (void)user_data;
-
- cur = gtk_stack_get_visible_child_name(main_stack);
- if (cur && g_strcmp0(cur, "stats") != 0) {
- g_free(stats_return_page);
- stats_return_page = g_strdup(cur);
- }
- refresh_stats_ui();
- gtk_stack_set_visible_child_name(main_stack, "stats");
-}
-
-static GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out) {
- GtkWidget *card;
- GtkWidget *value;
- GtkWidget *label;
-
- card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_add_css_class(card, "stats-metric");
- gtk_widget_set_hexpand(card, TRUE);
-
- value = gtk_label_new("0");
- gtk_widget_set_halign(value, GTK_ALIGN_START);
- gtk_widget_add_css_class(value, "stats-metric-value");
- gtk_box_append(GTK_BOX(card), value);
-
- label = gtk_label_new(NULL);
- i18n_bind(label, label_key, 0);
- gtk_widget_set_halign(label, GTK_ALIGN_START);
- gtk_widget_add_css_class(label, "stats-metric-label");
- gtk_box_append(GTK_BOX(card), label);
-
- *value_out = value;
- return card;
-}
-
-/* Build one unit row (index, name, badge, count, bar) for the stats list.
- * `num` is the row number shown (1-based within its subject). */
-static GtkWidget *make_unit_stats_row(UnitCtx *u, int num, int global) {
- GtkWidget *row;
- GtkWidget *head;
- GtkWidget *left;
- GtkWidget *idx;
- GtkWidget *name;
- GtkWidget *right;
- GtkWidget *count;
- GtkWidget *badge;
- GtkWidget *bar;
- char text[8];
-
- row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
- gtk_widget_add_css_class(row, "stats-unit-row");
- stats_ui.unit_row[global] = row;
-
- head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
- gtk_box_append(GTK_BOX(row), head);
-
- left = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
- gtk_widget_set_hexpand(left, TRUE);
- gtk_box_append(GTK_BOX(head), left);
-
- g_snprintf(text, sizeof(text), "%d", num);
- idx = gtk_label_new(text);
- gtk_widget_add_css_class(idx, "stats-unit-index");
- gtk_widget_set_valign(idx, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(left), idx);
-
- name = gtk_label_new(u->title);
- gtk_widget_set_halign(name, GTK_ALIGN_START);
- gtk_widget_set_hexpand(name, TRUE);
- gtk_label_set_xalign(GTK_LABEL(name), 0.0);
- gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
- gtk_widget_add_css_class(name, "stats-unit-name");
- gtk_box_append(GTK_BOX(left), name);
-
- right = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
- gtk_widget_set_valign(right, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(head), right);
-
- badge = gtk_label_new(NULL);
- gtk_widget_add_css_class(badge, "stats-unit-badge");
- gtk_widget_set_visible(badge, FALSE);
- gtk_box_append(GTK_BOX(right), badge);
- stats_ui.unit_badge[global] = badge;
-
- count = gtk_label_new("");
- gtk_widget_add_css_class(count, "stats-unit-count");
- gtk_box_append(GTK_BOX(right), count);
- stats_ui.unit_count[global] = count;
-
- bar = gtk_progress_bar_new();
- gtk_widget_add_css_class(bar, "stats-unit-bar");
- gtk_widget_set_hexpand(bar, TRUE);
- gtk_box_append(GTK_BOX(row), bar);
- stats_ui.unit_bar[global] = bar;
-
- return row;
-}
-
-static GtkWidget *build_stats_page(void) {
- GtkWidget *outer;
- GtkWidget *scroll;
- GtkWidget *page;
- GtkWidget *top;
- GtkWidget *back;
- GtkWidget *center;
- GtkWidget *heading;
- GtkWidget *sub;
- GtkWidget *summary;
- GtkWidget *section;
- GtkWidget *list;
-
- outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_add_css_class(outer, "stats-page");
-
- top = gtk_center_box_new();
- gtk_widget_set_margin_top(top, 2);
- gtk_widget_set_margin_bottom(top, 6);
- gtk_box_append(GTK_BOX(outer), top);
-
- back = make_back_button("subjects");
- g_signal_handlers_disconnect_matched(back, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_nav_clicked, NULL);
- g_signal_connect(back, "clicked", G_CALLBACK(on_stats_back_clicked), NULL);
- gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
-
- center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
- gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
- gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
-
- heading = gtk_label_new(NULL);
- i18n_bind(heading, "stats_title", 0);
- gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(heading, "roadmap-title");
- gtk_box_append(GTK_BOX(center), heading);
-
- sub = gtk_label_new(NULL);
- i18n_bind(sub, "stats_sub", 0);
- gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(sub, "roadmap-sub");
- gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
- gtk_box_append(GTK_BOX(center), sub);
-
- scroll = gtk_scrolled_window_new();
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_hexpand(scroll, TRUE);
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
- gtk_box_append(GTK_BOX(outer), scroll);
-
- page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_set_halign(page, GTK_ALIGN_FILL);
- gtk_widget_set_margin_start(page, 4);
- gtk_widget_set_margin_end(page, 4);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), page);
-
- summary = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
- gtk_widget_add_css_class(summary, "stats-summary");
- gtk_widget_set_halign(summary, GTK_ALIGN_FILL);
- gtk_box_append(GTK_BOX(page), summary);
-
- gtk_box_append(GTK_BOX(summary),
- stats_metric_card("stats_ex_label", &stats_ui.ex_value));
- gtk_box_append(GTK_BOX(summary),
- stats_metric_card("stats_pct_label", &stats_ui.pct_value));
- gtk_box_append(GTK_BOX(summary),
- stats_metric_card("stats_units_label",
- &stats_ui.units_value));
-
- stats_ui.overall_bar = gtk_progress_bar_new();
- gtk_widget_add_css_class(stats_ui.overall_bar, "stats-overall-bar");
- gtk_widget_set_hexpand(stats_ui.overall_bar, TRUE);
- gtk_box_append(GTK_BOX(page), stats_ui.overall_bar);
-
- section = gtk_label_new(NULL);
- i18n_bind(section, "stats_section", 0);
- gtk_widget_set_halign(section, GTK_ALIGN_START);
- gtk_widget_add_css_class(section, "stats-section");
- gtk_box_append(GTK_BOX(page), section);
-
- list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_box_append(GTK_BOX(page), list);
-
- for (int s = 0; s < NUM_SUBJECTS; s++) {
- GtkWidget *subject;
- GtkWidget *head;
- GtkWidget *name;
- GtkWidget *count;
- GtkWidget *bar;
- gboolean has_units = sub_unit_count[s] > 0;
-
- subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_add_css_class(subject, "stats-subject");
- gtk_box_append(GTK_BOX(list), subject);
-
- head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
- gtk_box_append(GTK_BOX(subject), head);
-
- name = gtk_label_new(NULL);
- i18n_bind(name, sub_keys[s], 0);
- gtk_widget_set_halign(name, GTK_ALIGN_START);
- gtk_widget_set_hexpand(name, TRUE);
- gtk_label_set_xalign(GTK_LABEL(name), 0.0);
- gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
- gtk_widget_add_css_class(name, "stats-subject-name");
- if (!has_units)
- gtk_widget_add_css_class(name, "unit-name-locked");
- gtk_box_append(GTK_BOX(head), name);
- stats_ui.subj[s].name = name;
-
- count = gtk_label_new(NULL);
- gtk_widget_add_css_class(count, "stats-subject-count");
- if (!has_units)
- gtk_widget_add_css_class(count, "locked");
- gtk_box_append(GTK_BOX(head), count);
- stats_ui.subj[s].count = count;
-
- bar = gtk_progress_bar_new();
- gtk_widget_add_css_class(bar, "stats-subject-bar");
- gtk_widget_set_hexpand(bar, TRUE);
- gtk_box_append(GTK_BOX(subject), bar);
- stats_ui.subj[s].bar = bar;
-
- if (has_units) {
- GtkWidget *units_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-
- gtk_widget_add_css_class(units_box, "stats-units");
- gtk_box_append(GTK_BOX(subject), units_box);
- stats_ui.subj[s].units_box = units_box;
-
- for (int j = 0; j < sub_unit_count[s]; j++) {
- int gi = sub_unit_start[s] + j;
-
- gtk_box_append(GTK_BOX(units_box),
- make_unit_stats_row(&units[gi], j + 1, gi));
- }
- }
- }
-
- return outer;
-}
-
-/* ------------------------------------------------------------------ */
-/* Navigation */
-/* ------------------------------------------------------------------ */
-
-static void on_nav_clicked(GtkButton *button, gpointer user_data) {
- const char *target = g_object_get_data(G_OBJECT(button), "target");
- (void)user_data;
- if (target)
- gtk_stack_set_visible_child_name(main_stack, target);
-}
-
-static void on_continue_clicked(GtkButton *button, gpointer user_data) {
- (void)button;
- (void)user_data;
- gtk_stack_set_visible_child_name(main_stack, "subjects");
-}
-
-static GtkWidget *make_back_button(const char *target) {
- GtkWidget *back = gtk_button_new();
- GtkWidget *icon = gtk_drawing_area_new();
-
- gtk_widget_set_size_request(icon, 14, 14);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon), draw_back_icon,
- NULL, NULL);
- gtk_button_set_child(GTK_BUTTON(back), icon);
- gtk_widget_add_css_class(back, "back-btn");
- gtk_widget_set_valign(back, GTK_ALIGN_CENTER);
- i18n_bind(back, "back", 2);
- g_signal_connect_swapped(back, "state-flags-changed",
- G_CALLBACK(gtk_widget_queue_draw), icon);
- g_object_set_data_full(G_OBJECT(back), "target", g_strdup(target), g_free);
- g_signal_connect(back, "clicked", G_CALLBACK(on_nav_clicked), NULL);
- return back;
-}
-
-static GtkWidget *top_bar(const char *back_target, const char *title,
- const char *subtitle) {
- GtkWidget *top = gtk_center_box_new();
- gtk_widget_set_margin_top(top, 2);
- gtk_widget_set_margin_bottom(top, 6);
-
- GtkWidget *back = make_back_button(back_target);
- gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
-
- GtkWidget *center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
- gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
- gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
-
- GtkWidget *heading = gtk_label_new(NULL);
- i18n_bind(heading, title, 0);
- gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(heading, "roadmap-title");
- gtk_box_append(GTK_BOX(center), heading);
-
- if (subtitle) {
- GtkWidget *sub = gtk_label_new(NULL);
- i18n_bind(sub, subtitle, 0);
- gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(sub, "roadmap-sub");
- gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
- gtk_label_set_max_width_chars(GTK_LABEL(sub), 60);
- gtk_box_append(GTK_BOX(center), sub);
- }
-
- return top;
-}
-
-static gboolean on_window_key_pressed(GtkEventControllerKey *controller,
- guint keyval, guint keycode,
- GdkModifierType state,
- gpointer user_data) {
- GtkWindow *window = user_data;
-
- (void)controller;
- (void)keycode;
-
- if ((keyval == GDK_KEY_q &&
- (state & (GDK_SUPER_MASK | GDK_META_MASK)) != 0) ||
- (keyval == GDK_KEY_F4 &&
- (state & GDK_ALT_MASK) != 0)) {
- GtkApplication *app = gtk_window_get_application(window);
- if (app != NULL)
- g_application_quit(G_APPLICATION(app));
- return GDK_EVENT_STOP;
- }
-
- return GDK_EVENT_PROPAGATE;
-}
-
-/* ------------------------------------------------------------------ */
-/* Welcome page */
-/* ------------------------------------------------------------------ */
-
-static GtkWidget *build_welcome_page(void) {
- GtkWidget *box;
- GtkWidget *heading;
- GtkWidget *card;
- GtkWidget *button;
-
- box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18);
- gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(box, GTK_ALIGN_CENTER);
- gtk_widget_set_margin_top(box, 40);
- gtk_widget_set_margin_bottom(box, 40);
-
- heading = gtk_label_new(NULL);
- welcome_heading = heading;
- gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(heading, "heading");
- gtk_box_append(GTK_BOX(box), heading);
-
- card = gtk_label_new(NULL);
- i18n_bind(card, "welcome_body", 0);
- gtk_label_set_justify(GTK_LABEL(card), GTK_JUSTIFY_CENTER);
- gtk_label_set_wrap(GTK_LABEL(card), TRUE);
- gtk_label_set_max_width_chars(GTK_LABEL(card), 48);
- gtk_widget_add_css_class(card, "card");
- gtk_box_append(GTK_BOX(box), card);
-
- button = gtk_button_new();
- i18n_bind(button, "continue", 1);
- gtk_widget_set_halign(button, GTK_ALIGN_CENTER);
- gtk_widget_set_margin_top(button, 6);
- gtk_widget_add_css_class(button, "btn-primary");
- gtk_box_append(GTK_BOX(box), button);
-
- g_signal_connect(button, "clicked", G_CALLBACK(on_continue_clicked), NULL);
-
- return box;
-}
-
-/* ------------------------------------------------------------------ */
-/* Roadmap page */
-/* ------------------------------------------------------------------ */
-
-static void draw_node_halo(cairo_t *cr, double cx, double cy,
- double radius, double r, double g, double b,
- double alpha) {
- cairo_pattern_t *grad;
- grad = cairo_pattern_create_radial(cx, cy, 6.0, cx, cy, radius);
- cairo_pattern_add_color_stop_rgba(grad, 0.0, r, g, b, alpha);
- cairo_pattern_add_color_stop_rgba(grad, 0.55, r, g, b, alpha * 0.30);
- cairo_pattern_add_color_stop_rgba(grad, 1.0, r, g, b, 0.0);
- cairo_set_source(cr, grad);
- cairo_paint(cr);
- cairo_pattern_destroy(grad);
-}
-
-/* Draw the checkered "finish line" node: checkerboard behind the unit
- * number and the same lock glyph the locked units use. */
-static void draw_finish_cell(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- const double s = 11.0;
- const Rgb gray = color_from_hex(app_theme.overlay);
- const Rgb fin_dark = color_from_hex(app_theme.finish_dark);
- const Rgb fin_light = color_from_hex(app_theme.finish_light);
- const Rgb fin_stroke = color_from_hex(app_theme.finish_stroke);
- int n = GPOINTER_TO_INT(data);
- double cx = width / 2.0;
- double cy = height / 2.0;
- double r = MIN(width, height) / 2.0 - 2.0;
- double start_x = cx - r;
- double start_y = cy - r;
- int cols = (int)ceil(2.0 * r / s);
- int rows = (int)ceil(2.0 * r / s);
- char buf[8];
- PangoLayout *layout;
- PangoFontDescription *fd;
- PangoRectangle ink;
- double num_cy;
- int ix, iy;
-
- (void)area;
-
- cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
- cairo_paint(cr);
- cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
-
- cairo_save(cr);
- cairo_new_path(cr);
- cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
- cairo_clip(cr);
-
- cairo_rectangle(cr, start_x, start_y, 2.0 * r, 2.0 * r);
- cairo_set_source_rgb(cr, fin_dark.r, fin_dark.g, fin_dark.b);
- cairo_fill(cr);
-
- for (iy = 0; iy < rows; iy++) {
- for (ix = 0; ix < cols; ix++) {
- if (((ix + iy) & 1) == 0)
- continue;
- cairo_rectangle(cr, start_x + ix * s, start_y + iy * s, s, s);
- cairo_set_source_rgb(cr, fin_light.r, fin_light.g, fin_light.b);
- cairo_fill(cr);
- }
- }
-
- /* unit number, styled like the other locked nodes */
- g_snprintf(buf, sizeof(buf), "%d", n);
- layout = pango_cairo_create_layout(cr);
- fd = pango_font_description_new();
- pango_font_description_set_family(fd, "sans");
- pango_font_description_set_weight(fd, PANGO_WEIGHT_ULTRABOLD);
- pango_font_description_set_size(fd, 22 * PANGO_SCALE);
- pango_layout_set_font_description(layout, fd);
- pango_layout_set_text(layout, buf, -1);
- pango_layout_get_pixel_extents(layout, &ink, NULL);
-
- cairo_set_source_rgb(cr, gray.r, gray.g, gray.b);
- num_cy = cy - 3.0;
- cairo_move_to(cr, cx - ink.width / 2.0 - ink.x,
- num_cy - ink.height / 2.0 - ink.y);
- pango_cairo_show_layout(cr, layout);
-
- /* lock glyph below the number, as on the other locked units */
- lock_paint(cr, cx - 8.0, num_cy + ink.height / 2.0 + 6.0, 16.0, &gray);
-
- g_object_unref(layout);
- pango_font_description_free(fd);
- cairo_restore(cr);
-
- cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
- cairo_set_source_rgb(cr, fin_stroke.r, fin_stroke.g, fin_stroke.b);
- cairo_set_line_width(cr, 2.0);
- cairo_stroke(cr);
-}
-
-static void road_point(double t, double *ox, double *oy) {
- int k = (int)t;
- double u = t - (double)k;
- double u2, u3;
- double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
-
- if (k < 0) { k = 0; u = 0.0; }
- if (k >= NUM_UNITS - 1) { k = NUM_UNITS - 2; u = 1.0; }
-
- p1x = road_cx[k]; p1y = road_cy[k];
- p2x = road_cx[k + 1]; p2y = road_cy[k + 1];
- if (k - 1 >= 0) { p0x = road_cx[k - 1]; p0y = road_cy[k - 1]; }
- else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
- if (k + 2 < NUM_UNITS) { p3x = road_cx[k + 2]; p3y = road_cy[k + 2]; }
- else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
-
- u2 = u * u;
- u3 = u2 * u;
- *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
- + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
- + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
- *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
- + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
- + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
-}
-
-static void road_path(cairo_t *cr, double t0, double t1) {
- const double steps_per_unit = 48.0;
- int n = (int)((t1 - t0) * steps_per_unit);
- double x, y;
- int s;
-
- if (n < 1)
- n = 1;
- road_point(t0, &x, &y);
- cairo_move_to(cr, x, y);
- for (s = 1; s <= n; s++) {
- road_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
- cairo_line_to(cr, x, y);
- }
-}
-
-/* Compute the serpentine node layout for the given available width. */
-static void roadmap_geometry(double avail) {
- const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_UNITS - 1);
- int rows;
- int r, c, i;
-
- for (rows = 1; rows <= NUM_UNITS; rows++) {
- int cols = (NUM_UNITS + rows - 1) / rows;
- if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0)
- break;
- }
- if (rows > NUM_UNITS)
- rows = NUM_UNITS;
- road_rows = rows;
- road_cols = (NUM_UNITS + rows - 1) / rows;
- if (road_cols < 1)
- road_cols = 1;
- road_cw = (int)(2.0 * ROAD_MX + (road_cols - 1) * PATH_SPAC);
- road_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP);
-
- for (r = 0; r < rows; r++) {
- int base = r * road_cols;
- int len = MIN(road_cols, NUM_UNITS - base);
- int fwd = (r % 2) == 0;
- for (c = 0; c < len; c++) {
- i = base + c;
- int cc = fwd ? c : (road_cols - 1 - c);
- road_cx[i] = ROAD_MX + cc * PATH_SPAC;
- road_cy[i] = ROAD_MY + r * ROAD_GAP
- + ROAD_WAVE * sin((double)i * phase);
- }
- }
-}
-
-static void road_apply_layout(void) {
- int i;
-
- if (!road_fixed)
- return;
-
- for (i = 0; i < NUM_UNITS; i++) {
- if (!road_nodes[i] || !road_labels[i])
- continue;
- gtk_fixed_move(GTK_FIXED(road_fixed), road_nodes[i],
- (int)(road_cx[i] - NODE_SIZE / 2.0),
- (int)(road_cy[i] - NODE_SIZE / 2.0));
- gtk_fixed_move(GTK_FIXED(road_fixed), road_labels[i],
- (int)(road_cx[i] - (PATH_SPAC - 20.0) / 2.0),
- (int)(road_cy[i] + NODE_SIZE / 2.0 + 10.0));
- }
-
- gtk_widget_set_size_request(road_fixed, road_cw, road_ch);
- gtk_widget_set_size_request(road_rail, road_cw, road_ch);
- gtk_fixed_move(GTK_FIXED(road_fixed), road_rail, 0, 0);
- gtk_widget_queue_draw(road_rail);
-}
-
-static void road_relayout(void) {
- GtkAdjustment *hadj;
- double avail;
-
- if (!road_scroll)
- return;
- hadj = gtk_scrolled_window_get_hadjustment(
- GTK_SCROLLED_WINDOW(road_scroll));
- avail = gtk_adjustment_get_page_size(hadj);
- if (avail < 1.0)
- return;
- roadmap_geometry(avail);
- road_apply_layout();
-}
-
-static guint road_idle;
-
-static gboolean road_relayout_idle(gpointer data) {
- (void)data;
- road_idle = 0;
- road_relayout();
- return G_SOURCE_REMOVE;
-}
-
-static void road_relayout_later(void) {
- if (road_idle == 0)
- road_idle = g_idle_add(road_relayout_idle, NULL);
-}
-
-static void road_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
- gpointer data) {
- (void)adj;
- (void)ps;
- (void)data;
- road_relayout_later();
-}
-
-static void draw_rail(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer user_data) {
- const double t_lit = (double)(NUM_UNLOCKED - 1);
- const double t_end = (double)(NUM_UNITS - 1);
- const double x0 = ROAD_MX;
- const double x1 = (double)road_cw - ROAD_MX;
- const Rgb mauve = color_from_hex(app_theme.accent);
- const Rgb blue = color_from_hex(app_theme.accent3);
- const Rgb rail = color_from_hex(app_theme.rail);
- const Rgb muted = color_from_hex(app_theme.subtext);
- double hx, hy;
-
- (void)area;
- (void)width;
- (void)height;
- (void)user_data;
-
- cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
- cairo_paint(cr);
- cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
-
- road_point((double)(NUM_UNLOCKED - 1), &hx, &hy);
- draw_node_halo(cr, hx, hy, NODE_SIZE * 1.05,
- mauve.r, mauve.g, mauve.b, 0.18);
-
- cairo_new_path(cr);
- road_path(cr, 0.0, t_end);
- cairo_set_line_width(cr, 16);
- cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
- cairo_stroke(cr);
-
- if (t_lit > 0.0) {
- cairo_pattern_t *grad;
-
- cairo_new_path(cr);
- road_path(cr, 0.0, t_lit);
-
- grad = cairo_pattern_create_linear(x0, 0.0, x1, 0.0);
- cairo_pattern_add_color_stop_rgba(grad, 0.0,
- mauve.r, mauve.g, mauve.b, 0.16);
- cairo_pattern_add_color_stop_rgba(grad, 1.0,
- blue.r, blue.g, blue.b, 0.16);
- cairo_set_source(cr, grad);
- cairo_set_line_width(cr, 34);
- cairo_stroke_preserve(cr);
- cairo_pattern_destroy(grad);
-
- grad = cairo_pattern_create_linear(x0, 0.0, x1, 0.0);
- cairo_pattern_add_color_stop_rgba(grad, 0.0,
- mauve.r, mauve.g, mauve.b, 1.0);
- cairo_pattern_add_color_stop_rgba(grad, 1.0,
- blue.r, blue.g, blue.b, 1.0);
- cairo_set_source(cr, grad);
- cairo_set_line_width(cr, 16);
- cairo_stroke_preserve(cr);
- cairo_pattern_destroy(grad);
- }
-
- {
- double start_t = t_lit > 0.0 ? t_lit : 0.0;
-
- cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17);
- cairo_set_dash(cr, (double[]){1.0, 26.0}, 2, 0.0);
- cairo_new_path(cr);
- road_path(cr, start_t, t_end);
- cairo_stroke(cr);
- cairo_set_dash(cr, NULL, 0, 0.0);
- }
-}
-
-static void add_path_node(GtkFixed *fixed, int index) {
- int num = index + 1;
- UnitCtx *u = &units[index];
- gboolean locked = !u->unlocked;
- GtkWidget *card;
- GtkWidget *name;
- char *text;
-
- card = gtk_button_new();
- gtk_widget_add_css_class(card, "unit-node");
- gtk_widget_set_can_focus(card, FALSE);
- gtk_widget_set_sensitive(card, !locked);
- gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE);
-
- if (index == NUM_UNITS - 1) {
- GtkWidget *fin = gtk_drawing_area_new();
-
- gtk_widget_add_css_class(card, "finish");
- gtk_widget_add_css_class(card, "locked");
- gtk_widget_set_size_request(fin, (int)NODE_SIZE, (int)NODE_SIZE);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(fin),
- draw_finish_cell,
- GINT_TO_POINTER(num), NULL);
- gtk_button_set_child(GTK_BUTTON(card), fin);
- } else {
- GtkWidget *vbox;
- GtkWidget *number;
- GtkWidget *icon;
-
- if (locked)
- gtk_widget_add_css_class(card, "locked");
- else
- gtk_widget_add_css_class(card, "current");
-
- vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
- gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
- gtk_button_set_child(GTK_BUTTON(card), vbox);
-
- text = g_strdup_printf("%d", num);
- number = gtk_label_new(text);
- g_free(text);
- gtk_widget_add_css_class(number, "unit-number");
- gtk_box_append(GTK_BOX(vbox), number);
-
- if (locked) {
- icon = icon_area_new(draw_lock_icon,
- 0.3451, 0.3569, 0.4392, 16);
- } else {
- icon = icon_area_new(draw_check_icon,
- 0.1176, 0.1176, 0.1804, 18);
- gtk_widget_set_visible(icon, FALSE);
- u->node = card;
- u->node_done_icon = icon;
- g_object_set_data_full(G_OBJECT(card), "target",
- g_strdup(u->page), g_free);
- g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
- }
- gtk_box_append(GTK_BOX(vbox), icon);
- }
-
- road_nodes[index] = card;
- gtk_fixed_put(fixed, card, 0, 0);
-
- name = gtk_label_new(u->title);
- gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1);
- gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
- gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER);
- gtk_label_set_wrap(GTK_LABEL(name), TRUE);
- gtk_widget_add_css_class(name, "unit-name");
- if (locked)
- gtk_widget_add_css_class(name, "unit-name-locked");
- road_labels[index] = name;
- gtk_fixed_put(fixed, name, 0, 0);
-}
-
-static GtkWidget *build_roadmap_page(void) {
- GtkWidget *page;
- GtkWidget *scroll;
- GtkWidget *wrap;
- GtkWidget *fixed;
- GtkWidget *rail;
- GtkAdjustment *ha;
- GtkAdjustment *va;
- int i;
-
- roadmap_geometry(1000.0);
-
- page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_set_hexpand(page, TRUE);
- gtk_widget_set_vexpand(page, TRUE);
- gtk_widget_set_margin_start(page, 32);
- gtk_widget_set_margin_end(page, 32);
- gtk_widget_set_margin_top(page, 24);
- gtk_widget_set_margin_bottom(page, 24);
-
- gtk_box_append(GTK_BOX(page),
- top_bar("subjects", "roadmap_title", "roadmap_sub"));
-
- scroll = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_margin_top(scroll, 14);
- gtk_box_append(GTK_BOX(page), scroll);
- road_scroll = scroll;
-
- wrap = gtk_center_box_new();
- gtk_widget_set_vexpand(wrap, TRUE);
- gtk_widget_set_hexpand(wrap, TRUE);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
-
- fixed = gtk_fixed_new();
- gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_size_request(fixed, road_cw, road_ch);
- gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
- road_fixed = fixed;
-
- rail = gtk_drawing_area_new();
- gtk_widget_set_size_request(rail, road_cw, road_ch);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_rail, NULL, NULL);
- gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
- road_rail = rail;
-
- for (i = 0; i < NUM_UNITS; i++)
- add_path_node(GTK_FIXED(fixed), i);
-
- ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
- va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
- g_signal_connect(ha, "notify::page-size",
- G_CALLBACK(road_adjust_notify), NULL);
- g_signal_connect(va, "notify::page-size",
- G_CALLBACK(road_adjust_notify), NULL);
-
- road_relayout_later();
-
- return page;
-}
-
-
-/* ------------------------------------------------------------------ */
-/* Subjects page ("Předměty") - serpentine subject map */
-/* ------------------------------------------------------------------ */
-
-#define SUB_BUBBLE NODE_SIZE /* round node, matches .unit-node */
-#define SUB_MX 110.0
-#define SUB_MY 150.0
-#define SUB_SPAC 220.0
-#define SUB_GAP 210.0
-#define SUB_WAVE 34.0
-
-/* Index 0 is Deutsch and is the only unlocked subject for now. */
-static const char *sub_keys[NUM_SUBJECTS] = {
- "Deutsch",
- "Správa počítačových sítí",
- "Občanská nauka",
- "English",
- "Matematika",
- "Fyzika",
- "Základy Přírodopisných věd",
- "Technická grafika",
- "Prezentační grafika",
- "Český jazyk a literatura",
- "Programování",
- "Technické vybavení",
- "Programové vybavení",
-};
-
-/* Slice of the global `units[]` table that each subject owns. A start of -1
- * (or a count of 0) means the subject has no content yet; such subjects show
- * as locked on the statistics page until their units are added. */
-static const int sub_unit_start[NUM_SUBJECTS] = {
- 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-};
-static const int sub_unit_count[NUM_SUBJECTS] = {
- NUM_UNITS, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-};
-
-static GtkWidget *sub_scroll;
-static GtkWidget *sub_fixed;
-static GtkWidget *sub_rail;
-static GtkWidget *sub_cells[NUM_SUBJECTS];
-static GtkWidget *sub_labels[NUM_SUBJECTS];
-static double sub_cx[NUM_SUBJECTS];
-static double sub_cy[NUM_SUBJECTS];
-static int sub_rows = 1;
-static int sub_cols = NUM_SUBJECTS;
-static int sub_cw = (int)(2.0 * SUB_MX + (NUM_SUBJECTS - 1) * SUB_SPAC);
-static int sub_ch = (int)(2.0 * SUB_MY);
-static guint sub_idle;
-
-static void sub_layout_geometry(double avail) {
- const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_SUBJECTS - 1);
- int rows;
- int r, c, i;
-
- for (rows = 1; rows <= NUM_SUBJECTS; rows++) {
- int cols = (NUM_SUBJECTS + rows - 1) / rows;
- if (2.0 * SUB_MX + (cols - 1) * SUB_SPAC <= avail + 1.0)
- break;
- }
- if (rows > NUM_SUBJECTS)
- rows = NUM_SUBJECTS;
- sub_rows = rows;
- sub_cols = (NUM_SUBJECTS + rows - 1) / rows;
- if (sub_cols < 1)
- sub_cols = 1;
- sub_cw = (int)(2.0 * SUB_MX + (sub_cols - 1) * SUB_SPAC);
- sub_ch = (int)(2.0 * SUB_MY + (rows - 1) * SUB_GAP);
-
- for (r = 0; r < rows; r++) {
- int base = r * sub_cols;
- int len = MIN(sub_cols, NUM_SUBJECTS - base);
- int fwd = (r % 2) == 0;
- for (c = 0; c < len; c++) {
- i = base + c;
- int cc = fwd ? c : (sub_cols - 1 - c);
- sub_cx[i] = SUB_MX + cc * SUB_SPAC;
- sub_cy[i] = SUB_MY + r * SUB_GAP
- + SUB_WAVE * sin((double)i * phase);
- }
- }
-}
-
-static void sub_point(double t, double *ox, double *oy) {
- int k = (int)t;
- double u = t - (double)k;
- double u2, u3;
- double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
-
- if (k < 0) { k = 0; u = 0.0; }
- if (k >= NUM_SUBJECTS - 1) { k = NUM_SUBJECTS - 2; u = 1.0; }
-
- p1x = sub_cx[k]; p1y = sub_cy[k];
- p2x = sub_cx[k + 1]; p2y = sub_cy[k + 1];
- if (k - 1 >= 0) { p0x = sub_cx[k - 1]; p0y = sub_cy[k - 1]; }
- else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
- if (k + 2 < NUM_SUBJECTS) { p3x = sub_cx[k + 2]; p3y = sub_cy[k + 2]; }
- else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
-
- u2 = u * u;
- u3 = u2 * u;
- *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
- + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
- + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
- *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
- + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
- + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
-}
-
-static void sub_path(cairo_t *cr, double t0, double t1) {
- const double steps_per_unit = 48.0;
- int n = (int)((t1 - t0) * steps_per_unit);
- double x, y;
- int s;
-
- if (n < 1)
- n = 1;
- sub_point(t0, &x, &y);
- cairo_move_to(cr, x, y);
- for (s = 1; s <= n; s++) {
- sub_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
- cairo_line_to(cr, x, y);
- }
-}
-
-static void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer user_data) {
- const double t_end = (double)(NUM_SUBJECTS - 1);
- const Rgb rail = color_from_hex(app_theme.rail);
- const Rgb mauve = color_from_hex(app_theme.accent);
- double hx, hy;
-
- (void)area;
- (void)width;
- (void)height;
- (void)user_data;
-
- cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
- cairo_paint(cr);
- cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
-
- sub_point(0.0, &hx, &hy);
- draw_node_halo(cr, hx, hy, SUB_BUBBLE * 1.05,
- mauve.r, mauve.g, mauve.b, 0.18);
-
- cairo_new_path(cr);
- sub_path(cr, 0.0, t_end);
- cairo_set_line_width(cr, 14);
- cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
- cairo_stroke(cr);
-}
-
-static void sub_apply_layout(void) {
- int i;
-
- if (!sub_fixed)
- return;
-
- for (i = 0; i < NUM_SUBJECTS; i++) {
- if (sub_cells[i])
- gtk_fixed_move(GTK_FIXED(sub_fixed), sub_cells[i],
- (int)(sub_cx[i] - SUB_BUBBLE / 2.0),
- (int)(sub_cy[i] - SUB_BUBBLE / 2.0));
- if (sub_labels[i])
- gtk_fixed_move(GTK_FIXED(sub_fixed), sub_labels[i],
- (int)(sub_cx[i] - (SUB_SPAC - 24.0) / 2.0),
- (int)(sub_cy[i] + SUB_BUBBLE / 2.0 + 10.0));
- }
-
- gtk_widget_set_size_request(sub_fixed, sub_cw, sub_ch);
- gtk_widget_set_size_request(sub_rail, sub_cw, sub_ch);
- gtk_fixed_move(GTK_FIXED(sub_fixed), sub_rail, 0, 0);
- gtk_widget_queue_draw(sub_rail);
-}
-
-static void sub_relayout(void) {
- GtkAdjustment *hadj;
- double avail;
-
- if (!sub_scroll)
- return;
- hadj = gtk_scrolled_window_get_hadjustment(
- GTK_SCROLLED_WINDOW(sub_scroll));
- avail = gtk_adjustment_get_page_size(hadj);
- if (avail < 1.0)
- return;
- sub_layout_geometry(avail);
- sub_apply_layout();
-}
-
-static gboolean sub_relayout_idle(gpointer data) {
- (void)data;
- sub_idle = 0;
- sub_relayout();
- return G_SOURCE_REMOVE;
-}
-
-static void sub_relayout_later(void) {
- if (sub_idle == 0)
- sub_idle = g_idle_add(sub_relayout_idle, NULL);
-}
-
-static void sub_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
- gpointer data) {
- (void)adj;
- (void)ps;
- (void)data;
- sub_relayout_later();
-}
-
-/* German flag (black / red / gold) drawn in a clipped circle. */
-static void draw_german_flag(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- double cx = width / 2.0;
- double cy = height / 2.0;
- double r = MIN(width, height) / 2.0 - 2.0;
- double band = (double)height / 3.0;
-
- (void)area;
- (void)data;
-
- cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
- cairo_paint(cr);
- cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
-
- cairo_save(cr);
- cairo_new_path(cr);
- cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
- cairo_clip(cr);
-
- cairo_new_path(cr);
- cairo_rectangle(cr, 0.0, 0.0, (double)width, band);
- cairo_set_source_rgb(cr, 0.18, 0.18, 0.19);
- cairo_fill(cr);
-
- cairo_new_path(cr);
- cairo_rectangle(cr, 0.0, band, (double)width, band);
- cairo_set_source_rgb(cr, 0.72, 0.27, 0.24);
- cairo_fill(cr);
-
- cairo_new_path(cr);
- cairo_rectangle(cr, 0.0, 2.0 * band, (double)width, band);
- cairo_set_source_rgb(cr, 0.84, 0.70, 0.32);
- cairo_fill(cr);
-
- cairo_restore(cr);
-}
-
-static GtkWidget *build_subjects_page(void) {
- GtkWidget *page;
- GtkWidget *scroll;
- GtkWidget *wrap;
- GtkWidget *fixed;
- GtkWidget *rail;
- GtkAdjustment *ha;
- GtkAdjustment *va;
- int i;
-
- sub_layout_geometry(1000.0);
-
- page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_set_hexpand(page, TRUE);
- gtk_widget_set_vexpand(page, TRUE);
- gtk_widget_set_margin_start(page, 32);
- gtk_widget_set_margin_end(page, 32);
- gtk_widget_set_margin_top(page, 24);
- gtk_widget_set_margin_bottom(page, 24);
-
- gtk_box_append(GTK_BOX(page),
- top_bar("welcome", "subjects_title", "subjects_sub"));
-
- scroll = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_margin_top(scroll, 14);
- gtk_box_append(GTK_BOX(page), scroll);
- sub_scroll = scroll;
-
- wrap = gtk_center_box_new();
- gtk_widget_set_vexpand(wrap, TRUE);
- gtk_widget_set_hexpand(wrap, TRUE);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
-
- fixed = gtk_fixed_new();
- gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_size_request(fixed, sub_cw, sub_ch);
- gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
- sub_fixed = fixed;
-
- rail = gtk_drawing_area_new();
- gtk_widget_set_size_request(rail, sub_cw, sub_ch);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_sub_rail,
- NULL, NULL);
- gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
- sub_rail = rail;
-
- for (i = 0; i < NUM_SUBJECTS; i++) {
- GtkWidget *card;
- GtkWidget *lbl;
-
- card = gtk_button_new();
- gtk_widget_add_css_class(card, "unit-node");
- gtk_widget_set_can_focus(card, FALSE);
- gtk_widget_set_size_request(card, (int)SUB_BUBBLE, (int)SUB_BUBBLE);
-
- if (i == 0) {
- GtkWidget *flag = gtk_drawing_area_new();
- gtk_widget_add_css_class(card, "current");
- gtk_widget_set_size_request(flag, (int)SUB_BUBBLE,
- (int)SUB_BUBBLE);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(flag),
- draw_german_flag, NULL, NULL);
- gtk_button_set_child(GTK_BUTTON(card), flag);
- g_object_set_data_full(G_OBJECT(card), "target",
- g_strdup("roadmap"), g_free);
- g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
- } else if (i == NET_SUBJ) {
- GtkWidget *wifi = gtk_drawing_area_new();
- gtk_widget_add_css_class(card, "current");
- gtk_widget_set_size_request(wifi, (int)SUB_BUBBLE,
- (int)SUB_BUBBLE);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(wifi),
- draw_wifi_icon, NULL, NULL);
- gtk_button_set_child(GTK_BUTTON(card), wifi);
- g_object_set_data_full(G_OBJECT(card), "target",
- g_strdup("netmap"), g_free);
- g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
- } else {
- GtkWidget *lock;
- gtk_widget_add_css_class(card, "locked");
- gtk_widget_set_sensitive(card, FALSE);
- lock = icon_area_new(draw_lock_icon,
- 0.3451, 0.3569, 0.4392, 26);
- gtk_button_set_child(GTK_BUTTON(card), lock);
- }
-
- sub_cells[i] = card;
- gtk_fixed_put(GTK_FIXED(fixed), card, 0, 0);
-
- lbl = gtk_label_new(NULL);
- i18n_bind(lbl, sub_keys[i], 0);
- gtk_widget_set_size_request(lbl, (int)(SUB_SPAC - 24.0), -1);
- gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER);
- gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER);
- gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
- gtk_widget_add_css_class(lbl, "unit-name");
- if (i != 0 && i != NET_SUBJ)
- gtk_widget_add_css_class(lbl, "unit-name-locked");
- sub_labels[i] = lbl;
- gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0);
- }
-
- ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
- va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
- g_signal_connect(ha, "notify::page-size",
- G_CALLBACK(sub_adjust_notify), NULL);
- g_signal_connect(va, "notify::page-size",
- G_CALLBACK(sub_adjust_notify), NULL);
-
- sub_apply_layout();
- sub_relayout_later();
-
- return page;
-}
-
-
-/* ------------------------------------------------------------------ */
-/* Unit 1 exercise path (roadmap-style serpentine) */
-/* ------------------------------------------------------------------ */
-
-#define EX_BUBBLE 64.0 /* exercise bubble size (matches .ex-bubble) */
-#define EX_MX 80.0 /* horizontal canvas margin */
-#define EX_MY 120.0 /* vertical canvas margin */
-#define EX_SPAC 150.0 /* horizontal distance between bubbles */
-#define EX_GAP 200.0 /* vertical space between folded rows */
-#define EX_WAVE 26.0 /* wavy vertical offset of the bubbles */
-
-/* Compute the serpentine exercise layout for the given available width. */
-static void ex_layout_geometry(UnitCtx *u, double avail) {
- const int n = u->n_ex;
- const double phase = 2.0 * G_PI * 1.7 / (double)(n > 1 ? n - 1 : 1);
- int rows;
- int r, c, i;
-
- if (n < 1)
- return;
- for (rows = 1; rows <= n; rows++) {
- int cols = (n + rows - 1) / rows;
- if (2.0 * EX_MX + (cols - 1) * EX_SPAC <= avail + 1.0)
- break;
- }
- if (rows > n)
- rows = n;
- u->ex_rows = rows;
- u->ex_cols = (n + rows - 1) / rows;
- if (u->ex_cols < 1)
- u->ex_cols = 1;
- u->ex_cw = (int)(2.0 * EX_MX + (u->ex_cols - 1) * EX_SPAC);
- u->ex_ch = (int)(2.0 * EX_MY + (rows - 1) * EX_GAP);
-
- for (r = 0; r < rows; r++) {
- int base = r * u->ex_cols;
- int len = MIN(u->ex_cols, n - base);
- int fwd = (r % 2) == 0;
- for (c = 0; c < len; c++) {
- i = base + c;
- int cc = fwd ? c : (u->ex_cols - 1 - c);
- u->ex_cx[i] = EX_MX + cc * EX_SPAC;
- u->ex_cy[i] = EX_MY + r * EX_GAP
- + EX_WAVE * sin((double)i * phase);
- }
- }
-}
-
-static void ex_point(UnitCtx *u, double t, double *ox, double *oy) {
- const int n = u->n_ex;
- int k = (int)t;
- double uu = t - (double)k;
- double u2, u3;
- double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
-
- if (n < 2) { *ox = u->ex_cx[0]; *oy = u->ex_cy[0]; return; }
- if (k < 0) { k = 0; uu = 0.0; }
- if (k >= n - 1) { k = n - 2; uu = 1.0; }
-
- p1x = u->ex_cx[k]; p1y = u->ex_cy[k];
- p2x = u->ex_cx[k + 1]; p2y = u->ex_cy[k + 1];
- if (k - 1 >= 0) { p0x = u->ex_cx[k - 1]; p0y = u->ex_cy[k - 1]; }
- else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
- if (k + 2 < n) { p3x = u->ex_cx[k + 2]; p3y = u->ex_cy[k + 2]; }
- else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
-
- u2 = uu * uu;
- u3 = u2 * uu;
- *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * uu
- + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
- + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
- *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * uu
- + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
- + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
-}
-
-static void ex_path(UnitCtx *u, cairo_t *cr, double t0, double t1) {
- const double steps_per_unit = 48.0;
- int n = (int)((t1 - t0) * steps_per_unit);
- double x, y;
- int s;
-
- if (n < 1)
- n = 1;
- ex_point(u, t0, &x, &y);
- cairo_move_to(cr, x, y);
- for (s = 1; s <= n; s++) {
- ex_point(u, t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
- cairo_line_to(cr, x, y);
- }
-}
-
-static void draw_ex_rail(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer user_data) {
- UnitCtx *u = user_data;
- const double t_end = (double)(u->n_ex - 1);
- const Rgb slate = color_from_hex(app_theme.surface1);
- const Rgb green = color_from_hex(app_theme.success);
- const Rgb rail = color_from_hex(app_theme.rail);
-
- (void)area;
- (void)width;
- (void)height;
-
- cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
- cairo_paint(cr);
- cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
-
- cairo_new_path(cr);
- ex_path(u, cr, 0.0, t_end);
- cairo_set_line_width(cr, 14);
- cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
- cairo_stroke(cr);
-
- cairo_new_path(cr);
- ex_path(u, cr, 0.0, t_end);
- cairo_set_line_width(cr, 9);
- cairo_set_source_rgb(cr, slate.r, slate.g, slate.b);
- cairo_stroke(cr);
-
- for (int k = 0; k < u->n_ex - 1; k++) {
- if (!(u->done[k + 1] || u->done[k + 2]))
- continue;
- cairo_new_path(cr);
- ex_path(u, cr, (double)k, (double)(k + 1));
- cairo_set_source_rgba(cr, green.r, green.g, green.b, 0.85);
- cairo_set_line_width(cr, 9);
- cairo_stroke(cr);
- }
-}
-
-static void ex_apply_layout(UnitCtx *u) {
- int i;
-
- if (!u->ex_fixed)
- return;
-
- for (i = 0; i < u->n_ex; i++) {
- if (u->ex_cells[i])
- gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_cells[i],
- (int)(u->ex_cx[i] - EX_BUBBLE / 2.0),
- (int)(u->ex_cy[i] - EX_BUBBLE / 2.0));
- if (u->ex_labels[i])
- gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_labels[i],
- (int)(u->ex_cx[i] - (EX_SPAC - 20.0) / 2.0),
- (int)(u->ex_cy[i] + EX_BUBBLE / 2.0 + 8.0));
- }
-
- gtk_widget_set_size_request(u->ex_fixed, u->ex_cw, u->ex_ch);
- gtk_widget_set_size_request(u->ex_rail, u->ex_cw, u->ex_ch);
- gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_rail, 0, 0);
- gtk_widget_queue_draw(u->ex_rail);
-}
-
-static void ex_relayout(UnitCtx *u) {
- GtkAdjustment *hadj;
- double avail;
-
- if (!u->ex_scroll)
- return;
- hadj = gtk_scrolled_window_get_hadjustment(
- GTK_SCROLLED_WINDOW(u->ex_scroll));
- avail = gtk_adjustment_get_page_size(hadj);
- if (avail < 1.0)
- return;
- ex_layout_geometry(u, avail);
- ex_apply_layout(u);
-}
-
-static gboolean ex_relayout_idle(gpointer data) {
- UnitCtx *u = data;
- u->ex_idle = 0;
- ex_relayout(u);
- return G_SOURCE_REMOVE;
-}
-
-static void ex_relayout_later(UnitCtx *u) {
- if (u->ex_idle == 0)
- u->ex_idle = g_idle_add(ex_relayout_idle, u);
-}
-
-static void ex_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
- gpointer data) {
- (void)adj;
- (void)ps;
- ex_relayout_later(data);
-}
-
-static GtkWidget *make_bubble(UnitCtx *u, int n) {
- GtkWidget *btn;
- GtkWidget *vbox;
- GtkWidget *num_label;
- GtkWidget *icon;
- char *num;
- char *target;
-
- btn = gtk_button_new();
- gtk_widget_add_css_class(btn, "ex-bubble");
- gtk_widget_set_size_request(btn, (int)EX_BUBBLE, (int)EX_BUBBLE);
- gtk_widget_set_can_focus(btn, FALSE);
- if (u->done[n])
- gtk_widget_add_css_class(btn, "done");
-
- vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
- gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
- gtk_button_set_child(GTK_BUTTON(btn), vbox);
-
- num = g_strdup_printf("%d", n);
- num_label = gtk_label_new(num);
- g_free(num);
- gtk_widget_set_halign(num_label, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(num_label, "bubble-number");
- gtk_box_append(GTK_BOX(vbox), num_label);
-
- icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 16);
- gtk_widget_set_visible(icon, u->done[n]);
- gtk_box_append(GTK_BOX(vbox), icon);
-
- u->ex_cells[n - 1] = btn;
- u->ex_icons[n] = icon;
-
- target = g_strdup_printf("%se%d", u->ex_tag, n);
- g_object_set_data_full(G_OBJECT(btn), "target", target, g_free);
- g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
-
- return btn;
-}
-
-static GtkWidget *build_unit_page(UnitCtx *u) {
- GtkWidget *page;
- GtkWidget *scroll;
- GtkWidget *wrap;
- GtkWidget *fixed;
- GtkWidget *rail;
- GtkAdjustment *ha;
- GtkAdjustment *va;
- int i;
-
- if (u->n_ex < 1)
- return NULL;
- ex_layout_geometry(u, 1000.0);
-
- page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_set_hexpand(page, TRUE);
- gtk_widget_set_vexpand(page, TRUE);
- gtk_widget_set_margin_start(page, 32);
- gtk_widget_set_margin_end(page, 32);
- gtk_widget_set_margin_top(page, 24);
- gtk_widget_set_margin_bottom(page, 24);
-
- gtk_box_append(GTK_BOX(page),
- top_bar("roadmap", u->title, u->sub_key));
-
- scroll = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_margin_top(scroll, 14);
- gtk_box_append(GTK_BOX(page), scroll);
- u->ex_scroll = scroll;
-
- wrap = gtk_center_box_new();
- gtk_widget_set_vexpand(wrap, TRUE);
- gtk_widget_set_hexpand(wrap, TRUE);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
-
- fixed = gtk_fixed_new();
- gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_size_request(fixed, u->ex_cw, u->ex_ch);
- gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
- u->ex_fixed = fixed;
-
- rail = gtk_drawing_area_new();
- gtk_widget_set_size_request(rail, u->ex_cw, u->ex_ch);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_ex_rail,
- u, NULL);
- gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
- u->ex_rail = rail;
-
- for (i = 0; i < u->n_ex; i++) {
- GtkWidget *btn = make_bubble(u, i + 1);
- GtkWidget *lbl = gtk_label_new(u->ex_names[i + 1]);
-
- u->ex_labels[i] = lbl;
-
- gtk_widget_set_size_request(lbl, (int)(EX_SPAC - 20.0), -1);
- gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER);
- gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER);
- gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
- gtk_widget_add_css_class(lbl, "ex-label");
-
- gtk_fixed_put(GTK_FIXED(fixed), btn, 0, 0);
- gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0);
- }
-
- ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
- va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
- g_signal_connect(ha, "notify::page-size",
- G_CALLBACK(ex_adjust_notify), u);
- g_signal_connect(va, "notify::page-size",
- G_CALLBACK(ex_adjust_notify), u);
-
- ex_apply_layout(u);
- ex_relayout_later(u);
-
- return page;
-}
-
-/* ------------------------------------------------------------------ */
-/* Exercise page shell */
-/* ------------------------------------------------------------------ */
-
-static GtkWidget *ex_page_shell(const char *back_target, const char *title,
- const char *subtitle, const char *btn_label,
- GtkWidget **body_out, GtkWidget **feedback_out,
- GtkWidget **btn_out) {
- GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- GtkWidget *scroll;
- GtkWidget *body;
- GtkWidget *bottom;
- GtkWidget *feedback;
- GtkWidget *btn;
-
- gtk_widget_set_margin_start(page, 32);
- gtk_widget_set_margin_end(page, 32);
- gtk_widget_set_margin_top(page, 24);
- gtk_widget_set_margin_bottom(page, 24);
-
- gtk_box_append(GTK_BOX(page), top_bar(back_target, title, subtitle));
-
- scroll = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_margin_top(scroll, 14);
- gtk_widget_set_margin_bottom(scroll, 14);
- gtk_box_append(GTK_BOX(page), scroll);
-
- body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
- gtk_widget_set_vexpand(body, TRUE);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
- *body_out = body;
-
- bottom = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
- gtk_widget_set_margin_top(bottom, 4);
- gtk_box_append(GTK_BOX(page), bottom);
-
- feedback = gtk_label_new("");
- gtk_widget_set_halign(feedback, GTK_ALIGN_START);
- gtk_widget_set_valign(feedback, GTK_ALIGN_CENTER);
- gtk_widget_set_hexpand(feedback, TRUE);
- gtk_box_append(GTK_BOX(bottom), feedback);
- *feedback_out = feedback;
-
- btn = gtk_button_new();
- i18n_bind(btn, btn_label, 1);
- gtk_widget_add_css_class(btn, "btn-primary");
- gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(bottom), btn);
- *btn_out = btn;
-
- return page;
-}
-
-/* ------------------------------------------------------------------ */
-/* Word-selection (combo box) exercise */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- GArray *combos; /* GtkComboBox* */
- GArray *answers; /* const char* */
- GArray *trans; /* GtkWidget* hidden meaning labels */
- GArray *models; /* GtkWidget* hidden German answer labels */
- gboolean trans_upfront; /* meanings shown before Check */
- gboolean reveal_german; /* German answer shown on Check */
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
-} ComboListCtx;
-
-/* Build a hidden translation label ("meaning") appended under an item. */
-static GtkWidget *meaning_add(GtkWidget *body, const char *text) {
- GtkWidget *lbl = gtk_label_new(NULL);
-
- i18n_bind(lbl, text, 0);
- gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
- gtk_widget_set_halign(lbl, GTK_ALIGN_START);
- gtk_widget_add_css_class(lbl, "meaning");
- gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
- gtk_widget_set_visible(lbl, FALSE);
- gtk_widget_set_margin_top(lbl, 2);
- gtk_widget_set_margin_bottom(lbl, 2);
- gtk_widget_set_margin_start(lbl, 8);
- gtk_box_append(GTK_BOX(body), lbl);
- return lbl;
-}
-
-/* A hidden green label that shows the model German sentence after Check. */
-static GtkWidget *model_answer_add(GtkWidget *body, const char *german) {
- GtkWidget *lbl = gtk_label_new(german);
-
- gtk_widget_set_halign(lbl, GTK_ALIGN_START);
- gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
- gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
- gtk_widget_set_margin_top(lbl, 2);
- gtk_widget_set_margin_bottom(lbl, 2);
- gtk_widget_set_margin_start(lbl, 8);
- gtk_widget_add_css_class(lbl, "u3-answer");
- gtk_widget_set_visible(lbl, FALSE);
- gtk_box_append(GTK_BOX(body), lbl);
- return lbl;
-}
-
-static void meaning_reveal_all(GtkWidget **labels, int n) {
- for (int i = 0; i < n; i++)
- if (labels[i])
- gtk_widget_set_visible(labels[i], TRUE);
-}
-
-static ComboListCtx *combo_list_ctx_new(UnitCtx *unit, int ex_num,
- GtkWidget *feedback) {
- ComboListCtx *ctx = g_new0(ComboListCtx, 1);
- ctx->combos = g_array_new(FALSE, FALSE, sizeof(GtkComboBox *));
- ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
- ctx->trans = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- ctx->models = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- ctx->unit = unit;
- ctx->ex_num = ex_num;
- ctx->feedback = feedback;
- return ctx;
-}
-
-static void combo_list_add(ComboListCtx *ctx, GtkWidget *combo, const char *ans) {
- g_array_append_val(ctx->combos, combo);
- g_array_append_val(ctx->answers, ans);
-}
-
-static void combo_list_add_trans(ComboListCtx *ctx, GtkWidget *label) {
- g_array_append_val(ctx->trans, label);
-}
-
-static GtkWidget *make_word_combo(const char **words, int n) {
- GtkWidget *combo = gtk_combo_box_text_new();
- int *order = g_new0(int, n);
-
- for (int i = 0; i < n; i++)
- order[i] = i;
- shuffle_indices(order, n);
- for (int i = 0; i < n; i++)
- gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(combo), NULL,
- words[order[i]]);
- gtk_combo_box_set_active(GTK_COMBO_BOX(combo), -1);
- g_free(order);
- return combo;
-}
-
-static gboolean answer_accepts(const char *sel_norm, const char *ans) {
- gchar **parts = g_strsplit(ans, "|", -1);
- gboolean ok = FALSE;
-
- for (int i = 0; parts[i] != NULL; i++) {
- gchar *a = normalize_answer(parts[i]);
- if (g_strcmp0(sel_norm, a) == 0) {
- ok = TRUE;
- g_free(a);
- break;
- }
- g_free(a);
- }
-
- g_strfreev(parts);
- return ok;
-}
-
-static void combo_list_check(GtkButton *button, gpointer data) {
- ComboListCtx *ctx = data;
- guint total = ctx->combos->len;
- guint ok = 0;
-
- (void)button;
-
- for (guint i = 0; i < total; i++) {
- GtkComboBox *combo = g_array_index(ctx->combos, GtkComboBox *, i);
- const char *ans = g_array_index(ctx->answers, const char *, i);
- gchar *sel = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(combo));
- gboolean good = FALSE;
-
- if (sel) {
- gchar *t = normalize_answer(sel);
- good = answer_accepts(t, ans);
- g_free(t);
- g_free(sel);
- }
- answer_mark(GTK_WIDGET(combo), good);
- if (good)
- ok++;
- }
-
- if (ctx->trans->len > 0) {
- for (guint i = 0; i < ctx->trans->len; i++) {
- GtkWidget *lbl = g_array_index(ctx->trans, GtkWidget *, i);
- if (lbl)
- gtk_widget_set_visible(lbl, TRUE);
- }
- }
-
- if (ctx->models->len > 0) {
- for (guint i = 0; i < ctx->models->len; i++) {
- GtkWidget *lbl = g_array_index(ctx->models, GtkWidget *, i);
- if (lbl)
- gtk_widget_set_visible(lbl, TRUE);
- }
- }
-
- if (ok == total) {
- set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
- mark_done(ctx->unit, ctx->ex_num);
- } else {
- set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
- }
-}
-
-/* ------------------------------------------------------------------ */
-/* Word-arrangement (chips) exercise */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *prompt; /* optional label shown above the sentence */
- const char *words[6];
- int n;
-} AssemblyItem;
-
-typedef struct {
- GtkWidget *pool;
- GtkWidget *target;
- GtkWidget *group;
- GtkWidget *stage; /* transparent overlay layer for the fly sprite */
- GtkWidget *trans; /* hidden meaning label under the group */
- GtkWidget *chips[6];
- int pool_order[6]; /* fixed shuffled order of the word pool */
- int placed[6];
- int placed_count;
- int n_words;
- gboolean flying; /* a click-move animation is in progress */
-} SentBuilder;
-
-static void sent_rebuild(SentBuilder *sb) {
- for (int i = 0; i < sb->n_words; i++) {
- GtkWidget *parent = gtk_widget_get_parent(sb->chips[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(sb->pool));
- flow_clear(GTK_FLOW_BOX(sb->target));
-
- for (int i = 0; i < sb->placed_count; i++)
- gtk_flow_box_insert(GTK_FLOW_BOX(sb->target),
- sb->chips[sb->placed[i]], -1);
-
- for (int i = 0; i < sb->n_words; i++) {
- int w = sb->pool_order[i];
- gboolean in_target = FALSE;
- for (int j = 0; j < sb->placed_count; j++) {
- if (sb->placed[j] == w) {
- in_target = TRUE;
- break;
- }
- }
- if (!in_target)
- gtk_flow_box_insert(GTK_FLOW_BOX(sb->pool), sb->chips[w], -1);
- }
-}
-
-typedef struct {
- SentBuilder *sb;
- int idx;
-} ChipRef;
-
-static GdkContentProvider *asm_drag_prepare(GtkDragSource *source,
- double x, double y,
- gpointer data) {
- ChipRef *ref = data;
- (void)source;
- (void)x;
- (void)y;
- return gdk_content_provider_new_typed(G_TYPE_INT, ref->idx);
-}
-
-static gboolean asm_rebuild_idle(gpointer data) {
- sent_rebuild(data);
- return G_SOURCE_REMOVE;
-}
-
-static void asm_drag_end(GtkDragSource *source, GdkDrag *drag,
- gboolean delete_data, gpointer data) {
- ChipRef *ref = data;
- (void)source;
- (void)drag;
- (void)delete_data;
- g_idle_add(asm_rebuild_idle, ref->sb);
-}
-
-static gboolean asm_drop_to_sentence(GtkDropTarget *target, const GValue *value,
- double x, double y, gpointer data) {
- SentBuilder *sb = data;
- int idx;
- int pos = -1;
-
- (void)target;
- (void)x;
- (void)y;
-
- if (!G_VALUE_HOLDS_INT(value))
- return FALSE;
-
- idx = g_value_get_int(value);
- for (int i = 0; i < sb->placed_count; i++) {
- if (sb->placed[i] == idx) {
- pos = i;
- break;
- }
- }
- if (pos >= 0) {
- for (int i = pos; i < sb->placed_count - 1; i++)
- sb->placed[i] = sb->placed[i + 1];
- sb->placed_count--;
- }
- sb->placed[sb->placed_count++] = idx;
- return TRUE;
-}
-
-static gboolean asm_drop_to_pool(GtkDropTarget *target, const GValue *value,
- double x, double y, gpointer data) {
- SentBuilder *sb = data;
- int idx;
- int pos = -1;
-
- (void)target;
- (void)x;
- (void)y;
-
- if (!G_VALUE_HOLDS_INT(value))
- return FALSE;
-
- idx = g_value_get_int(value);
- for (int i = 0; i < sb->placed_count; i++) {
- if (sb->placed[i] == idx) {
- pos = i;
- break;
- }
- }
- if (pos < 0)
- return FALSE;
-
- for (int i = pos; i < sb->placed_count - 1; i++)
- sb->placed[i] = sb->placed[i + 1];
- sb->placed_count--;
- return TRUE;
-}
-
-/* ------------------------------------------------------------------ */
-/* Click-to-move with a fly ("glide") animation */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- GtkWidget *chip; /* real chip, kept invisible in its final slot */
- GtkWidget *ghost; /* sprite flying from start to finish */
- GtkWidget *stage;
- gboolean *flying; /* pointer to the owner's "animating" flag */
- double sx, sy; /* start position (stage coords) */
- double tx, ty; /* target position (stage coords) */
- guint wait; /* ticks waited for relayout */
- gint64 t0; /* < 0 while still measuring the target */
-} FlyCtx;
-
-static void asm_finish_fly(FlyCtx *fc) {
- gtk_widget_remove_css_class(fc->chip, "ghost-host");
- gtk_fixed_remove(GTK_FIXED(fc->stage), fc->ghost);
- if (fc->flying)
- *fc->flying = FALSE;
- g_free(fc);
-}
-
-static gboolean asm_fly_tick(gpointer data) {
- FlyCtx *fc = data;
- gint64 now = g_get_monotonic_time() / 1000;
-
- if (fc->t0 < 0) {
- double nx = 0.0, ny = 0.0;
- gboolean ok;
-
- fc->wait++;
- ok = gtk_widget_translate_coordinates(fc->chip, fc->stage,
- 0, 0, &nx, &ny);
- if (fc->wait < 2)
- return G_SOURCE_CONTINUE;
-
- if (ok && fc->wait < 14 &&
- fabs(nx - fc->sx) + fabs(ny - fc->sy) < 0.5)
- return G_SOURCE_CONTINUE; /* still waiting for relayout */
-
- if (ok) {
- fc->tx = nx;
- fc->ty = ny;
- } else {
- fc->tx = fc->sx;
- fc->ty = fc->sy;
- }
- fc->t0 = now;
- return G_SOURCE_CONTINUE;
- }
-
- {
- double t = (double)(now - fc->t0) / 240.0;
- double e, x, y;
-
- if (t > 1.0)
- t = 1.0;
- e = 1.0 - pow(1.0 - t, 3.0); /* ease-out cubic */
- x = fc->sx + (fc->tx - fc->sx) * e;
- y = fc->sy + (fc->ty - fc->sy) * e;
- gtk_fixed_move(GTK_FIXED(fc->stage), fc->ghost, (int)x, (int)y);
-
- if (t >= 1.0) {
- asm_finish_fly(fc);
- return G_SOURCE_REMOVE;
- }
- }
- return G_SOURCE_CONTINUE;
-}
-
-/* Start a fly animation: hide `chip` in place and animate a ghost from
- * (sx, sy) to the chip's new location. */
-static void asm_begin_fly(GtkWidget *chip, GtkWidget *stage,
- gboolean *flying, double sx, double sy) {
- GtkWidget *child = gtk_button_get_child(GTK_BUTTON(chip));
- const char *txt = (child && GTK_IS_LABEL(child))
- ? gtk_label_get_text(GTK_LABEL(child)) : NULL;
- GtkWidget *ghost = gtk_button_new_with_label(txt ? txt : "");
- FlyCtx *fc;
-
- gtk_widget_add_css_class(ghost, "chip");
- gtk_widget_set_sensitive(ghost, FALSE);
- gtk_widget_set_can_focus(ghost, FALSE);
- gtk_fixed_put(GTK_FIXED(stage), ghost, (int)sx, (int)sy);
-
- fc = g_new0(FlyCtx, 1);
- fc->chip = chip;
- fc->ghost = ghost;
- fc->stage = stage;
- fc->flying = flying;
- fc->sx = sx;
- fc->sy = sy;
- fc->t0 = -1;
-
- if (flying)
- *flying = TRUE;
- gtk_widget_add_css_class(chip, "ghost-host");
-
- g_timeout_add(16, asm_fly_tick, fc);
-}
-
-static void asm_move_by_click(GtkButton *button, gpointer data) {
- ChipRef *ref = data;
- SentBuilder *sb = ref->sb;
- int idx = ref->idx;
- int pos = -1;
- double sx = 0.0, sy = 0.0;
- GtkWidget *chip = GTK_WIDGET(button);
-
- (void)button;
-
- if (sb->flying)
- return;
-
- for (int i = 0; i < sb->placed_count; i++) {
- if (sb->placed[i] == idx) {
- pos = i;
- break;
- }
- }
-
- if (sb->stage &&
- gtk_widget_translate_coordinates(chip, sb->stage, 0, 0, &sx, &sy)) {
- if (pos >= 0) {
- for (int i = pos; i < sb->placed_count - 1; i++)
- sb->placed[i] = sb->placed[i + 1];
- sb->placed_count--;
- } else {
- sb->placed[sb->placed_count++] = idx;
- }
-
- sent_rebuild(sb);
- asm_begin_fly(chip, sb->stage, &sb->flying, sx, sy);
- } else {
- /* No usable animation layer yet – just move instantly. */
- if (pos >= 0) {
- for (int i = pos; i < sb->placed_count - 1; i++)
- sb->placed[i] = sb->placed[i + 1];
- sb->placed_count--;
- } else {
- sb->placed[sb->placed_count++] = idx;
- }
- sent_rebuild(sb);
- }
-}
-
-typedef struct {
- SentBuilder **sbs;
- int n;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
-} AsmCtx;
-
-static void assembly_check(GtkButton *button, gpointer data) {
- AsmCtx *ctx = data;
- int ok = 0;
-
- (void)button;
-
- for (int i = 0; i < ctx->n; i++) {
- SentBuilder *sb = ctx->sbs[i];
- gboolean good = (sb->placed_count == sb->n_words);
- if (good) {
- for (int j = 0; j < sb->placed_count; j++) {
- if (sb->placed[j] != j) {
- good = FALSE;
- break;
- }
- }
- }
- if (good) {
- ok++;
- gtk_widget_remove_css_class(sb->group, "wrong");
- gtk_widget_add_css_class(sb->group, "ok");
- } else {
- gtk_widget_remove_css_class(sb->group, "ok");
- gtk_widget_add_css_class(sb->group, "wrong");
- }
- }
-
- for (int i = 0; i < ctx->n; i++) {
- if (ctx->sbs[i]->trans)
- gtk_widget_set_visible(ctx->sbs[i]->trans, TRUE);
- }
-
- 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_sentences"));
- }
-}
-
-static GtkWidget *build_assembly(UnitCtx *unit, const char *title,
- const char *subtitle, int ex_num,
- const AssemblyItem *items,
- const char **meanings, int n) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, title, subtitle, "check",
- &body, &feedback, &check);
- AsmCtx *ctx = g_new0(AsmCtx, 1);
-
- ctx->sbs = g_new0(SentBuilder *, n);
- ctx->n = n;
- ctx->unit = unit;
- ctx->ex_num = ex_num;
- ctx->feedback = feedback;
-
- for (int s = 0; s < n; s++) {
- const AssemblyItem *item = &items[s];
- SentBuilder *sb = g_new0(SentBuilder, 1);
- GtkWidget *holder;
- GtkWidget *group;
- GtkWidget *target;
- GtkWidget *pool;
- GtkWidget *stage;
-
- sb->n_words = item->n;
- ctx->sbs[s] = sb;
-
- if (item->prompt) {
- GtkWidget *prompt = gtk_label_new(item->prompt);
- gtk_widget_set_halign(prompt, GTK_ALIGN_START);
- gtk_widget_add_css_class(prompt, "chain");
- gtk_box_append(GTK_BOX(body), prompt);
- }
-
- holder = gtk_overlay_new();
- gtk_widget_set_hexpand(holder, TRUE);
-
- group = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
- gtk_widget_add_css_class(group, "sent-group");
- sb->group = group;
- gtk_overlay_set_child(GTK_OVERLAY(holder), group);
-
- target = gtk_flow_box_new();
- gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(target), GTK_SELECTION_NONE);
- gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(target), 2);
- gtk_widget_set_size_request(target, -1, 44);
- sb->target = target;
- gtk_box_append(GTK_BOX(group), target);
-
- pool = gtk_flow_box_new();
- gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(pool), GTK_SELECTION_NONE);
- gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(pool), 2);
- gtk_widget_set_size_request(pool, -1, 44);
- sb->pool = pool;
- gtk_box_append(GTK_BOX(group), pool);
-
- 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);
- sb->stage = stage;
-
- {
- GtkDropTarget *dt = gtk_drop_target_new(G_TYPE_INT, GDK_ACTION_COPY);
- GtkDropTarget *dp = gtk_drop_target_new(G_TYPE_INT, GDK_ACTION_COPY);
- g_signal_connect(dt, "drop", G_CALLBACK(asm_drop_to_sentence), sb);
- gtk_widget_add_controller(target, GTK_EVENT_CONTROLLER(dt));
- g_signal_connect(dp, "drop", G_CALLBACK(asm_drop_to_pool), sb);
- gtk_widget_add_controller(pool, GTK_EVENT_CONTROLLER(dp));
- }
-
- for (int w = 0; w < item->n; w++) {
- GtkWidget *chip = gtk_button_new_with_label(item->words[w]);
- GtkDragSource *src = gtk_drag_source_new();
- ChipRef *ref = g_new(ChipRef, 1);
- gtk_widget_add_css_class(chip, "chip");
- g_object_ref(chip);
- sb->chips[w] = chip;
- ref->sb = sb;
- ref->idx = w;
- gtk_drag_source_set_actions(src, GDK_ACTION_COPY);
- g_signal_connect(src, "prepare", G_CALLBACK(asm_drag_prepare), ref);
- g_signal_connect(src, "drag-end", G_CALLBACK(asm_drag_end), ref);
- g_signal_connect(chip, "clicked", G_CALLBACK(asm_move_by_click), ref);
- gtk_widget_add_controller(chip, GTK_EVENT_CONTROLLER(src));
- }
-
- {
- for (int w = 0; w < item->n; w++)
- sb->pool_order[w] = w;
- shuffle_indices(sb->pool_order, item->n);
- for (int w = 0; w < item->n; w++)
- gtk_flow_box_insert(GTK_FLOW_BOX(pool),
- sb->chips[sb->pool_order[w]], -1);
- }
-
- gtk_box_append(GTK_BOX(body), holder);
-
- if (meanings[s])
- sb->trans = meaning_add(body, meanings[s]);
- }
-
- g_signal_connect(check, "clicked", G_CALLBACK(assembly_check), ctx);
- return page;
-}
-
-/* ------------------------------------------------------------------ */
-/* Exercise 1: Dialog (word selection) */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *speaker;
- const char *before;
- const char *answer;
- const char *after;
-} DialogRow;
-
-typedef struct {
- const char *name;
- const DialogRow *rows;
- int n;
-} Dialogue;
-
-static const DialogRow ex1_d1[] = {
- {"A:", "Hallo! Ich ", "bin|heiße", " Anna."},
- {"B:", "Hallo! Ich ", "bin|heiße", " Peter."},
- {"A:", "Woher ", "kommst", " du?"},
- {"B:", "Ich komme ", "aus", " Tschechien."},
-};
-
-static const DialogRow ex1_d2[] = {
- {"A:", "Guten ", "Morgen", "!"},
- {"B:", "Wie ", "geht", " es dir?"},
- {"A:", "Mir geht es gut, ", "danke", "."},
-};
-
-static const DialogRow ex1_d3[] = {
- {"A:", "Ich muss gehen. Auf ", "Wiedersehen", "!"},
- {"B:", "", "Bis", " bald!"},
- {"A:", "", "Tschüss", "!"},
-};
-
-static const Dialogue ex1_dialogues[] = {
- {"Sich vorstellen", ex1_d1, G_N_ELEMENTS(ex1_d1)},
- {"Begrüßung", ex1_d2, G_N_ELEMENTS(ex1_d2)},
- {"Verabschiedung", ex1_d3, G_N_ELEMENTS(ex1_d3)},
-};
-
-static const char *ex1_pool[] = {
- "bin", "heiße", "kommst", "aus", "Morgen",
- "geht", "danke", "Wiedersehen", "Bis", "Tschüss",
-};
-
-/* One Czech meaning per dialog line, in the same order as the rows. */
-static const char *ex1_meaning[] = {
- "Ahoj! Jmenuji se Anna.",
- "Ahoj! Jmenuji se Petr.",
- "Odkud jsi?",
- "Pocházím z Česka.",
- "Dobré ráno!",
- "Jak se máš?",
- "Mám se dobře, děkuji.",
- "Musím jít. Na shledanou!",
- "Brzy na viděnou!",
- "Čau!",
-};
-
-static GtkWidget *build_ex1(UnitCtx *unit) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, "Dialog",
- "sub_dialog",
- "check", &body, &feedback, &check);
- ComboListCtx *ctx = combo_list_ctx_new(unit, 1, feedback);
- int ri = 0;
-
- for (guint d = 0; d < G_N_ELEMENTS(ex1_dialogues); d++) {
- const Dialogue *diag = &ex1_dialogues[d];
- GtkWidget *name = gtk_label_new(diag->name);
- gtk_widget_set_halign(name, GTK_ALIGN_START);
- gtk_widget_add_css_class(name, "ex-sub");
- gtk_widget_set_margin_top(name, d > 0 ? 10 : 0);
- gtk_box_append(GTK_BOX(body), name);
-
- for (int r = 0; r < diag->n; r++) {
- const DialogRow *row = &diag->rows[r];
- GtkWidget *line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- GtkWidget *combo;
- gtk_widget_set_halign(line, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), line);
-
- if (row->speaker && row->speaker[0]) {
- GtkWidget *spk = gtk_label_new(row->speaker);
- gtk_widget_add_css_class(spk, "dlg-speaker");
- gtk_widget_set_valign(spk, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(line), spk);
- }
- if (row->before && row->before[0]) {
- GtkWidget *lb = gtk_label_new(row->before);
- gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(lb, "ex-prompt");
- gtk_box_append(GTK_BOX(line), lb);
- }
-
- combo = make_word_combo(ex1_pool, G_N_ELEMENTS(ex1_pool));
- gtk_widget_set_size_request(combo, 130, -1);
- gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(line), combo);
- combo_list_add(ctx, combo, row->answer);
-
- if (row->after && row->after[0]) {
- GtkWidget *la = gtk_label_new(row->after);
- gtk_widget_set_valign(la, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(la, "ex-prompt");
- gtk_box_append(GTK_BOX(line), la);
- }
-
- if (ri < (int)G_N_ELEMENTS(ex1_meaning))
- combo_list_add_trans(ctx, meaning_add(body, ex1_meaning[ri]));
- ri++;
- }
- }
-
- g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
- return page;
-}
-
-/* ------------------------------------------------------------------ */
-/* Exercise 2: Sätze bilden (word arrangement) */
-/* ------------------------------------------------------------------ */
-
-static const AssemblyItem ex2_items[] = {
- {NULL, {"Ich", "heiße", "Anna", "."}, 4},
- {NULL, {"Woher", "kommst", "du", "?"}, 4},
- {NULL, {"Ich", "komme", "aus", "Tschechien", "."}, 5},
- {NULL, {"Wie", "geht", "es", "dir", "?"}, 5},
-};
-
-static const char *ex2_meaning[] = {
- "Jmenuji se Anna.",
- "Odkud jsi?",
- "Pocházím z Česka.",
- "Jak se máš?",
-};
-
-/* ------------------------------------------------------------------ */
-/* Choice exercises (3 + 9) */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *prompt;
- const char *options[5];
- int n_options;
- int correct;
-} ChoiceQ;
-
-static 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},
-};
-
-static const char *ex3_meaning[] = {
- "„vierzehn“ = čtrnáct (14)",
- "„siebzehn“ = sedmnáct (17)",
- "„zwanzig“ = dvacet (20)",
- "„sechs“ = šest (6)",
-};
-
-static 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},
-};
-
-static const char *ex9_meaning[] = {
- "Jak se jmenuješ?",
- "Odkud jsi?",
- "Kde bydlíš?",
- "Kdo to je?",
- "Kolik je ti let?",
-};
-
-typedef struct {
- const ChoiceQ *qs;
- int n;
- int n_opts;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
- GtkToggleButton **toggles;
- GtkWidget **trans; /* hidden meaning labels, one per question */
-} ChoiceCtx;
-
-static 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);
-}
-
-static 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 */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *question;
- const char *q_cs; /* Czech translation of the question */
- const char *sample;
- const char *a_cs; /* Czech translation of the sample answer */
-} FreeQ;
-
-static 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."},
-};
-
-static 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);
-}
-
-static 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));
-}
-
-typedef struct {
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
- GtkWidget **samples;
- int n;
-} Ex4Ctx;
-
-static 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);
-}
-
-static 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;
-}
-
-static 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) */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *digits;
- const char *answer;
-} NumberQ;
-
-static const NumberQ ex5_data[] = {
- {"14", "vierzehn"},
- {"17", "siebzehn"},
- {"20", "zwanzig"},
- {"6", "sechs"},
- {"13", "dreizehn"},
-};
-
-static const char *ex5_pool[] = {
- "vierzehn", "siebzehn", "zwanzig", "sechs", "dreizehn",
-};
-
-static const char *ex5_meaning[] = {
- "čtrnáct",
- "sedmnáct",
- "dvacet",
- "šest",
- "třináct",
-};
-
-static 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? */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *emoji;
- int count;
- const char *noun;
- const char *answer;
-} CountQ;
-
-static 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"},
-};
-
-static const char *ex6_pool[] = {
- "zwei", "fünf", "sieben", "neun", "drei", "elf", "vier", "acht", "sechs",
-};
-
-static 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ů",
-};
-
-static 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 */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *before;
- const char *after;
- const char *answer;
-} SeqQ;
-
-static 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"},
-};
-
-static const char *ex7_pool[] = {
- "acht", "vierzehn", "zwanzig", "sechs", "zwölf",
-};
-
-static 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",
-};
-
-static 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) */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *before;
- const char *after;
- const char *answer;
-} VerbQ;
-
-/* Complete German sentence for a before/after gap row (with any trailing
- * "(…)" hint such as "(kommen)" removed) – shown after Check. */
-static 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);
-}
-
-static 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"},
-};
-
-static const char *ex8_pool[] = {
- "heißen", "kommen", "wohnen", "sein", "spielen", "machen",
- "heiße", "kommst", "wohne", "bist", "spiele", "machst",
-};
-
-static 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áš?",
-};
-
-static 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) */
-/* ------------------------------------------------------------------ */
-
-static 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},
-};
-
-static const char *ex10_meaning[] = {
- "Kolik je hodin?",
- "Jmenuji se Anna.",
- "Odkud jsi?",
- "Na shledanou.",
- "Moc děkuji.",
-};
-
-/* ------------------------------------------------------------------ */
-/* Exercise 12: Was macht er/sie? */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *emoji;
- const char *before;
- const char *after;
- const char *answer;
-} VerbClueQ;
-
-static 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"},
-};
-
-static const char *ex12_pool[] = {
- "schwimmt", "singt", "kocht", "liest", "fährt", "malt", "spielt", "schläft",
-};
-
-static const char *ex12_meaning[] = {
- "Plave v jezeře.",
- "Zpívá píseň.",
- "Vaří polévku.",
- "Čte knihu.",
- "Řídí auto.",
- "Maluje obraz.",
- "Hraje fotbal.",
- "Spí.",
-};
-
-static 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) */
-/* ------------------------------------------------------------------ */
-
-typedef struct {
- const char *emoji;
- const char *label;
- int correct_group;
-} AssignItem;
-
-static 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},
-};
-
-static 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},
-};
-
-static const char *ex11_groups[] = {"Begrüßung", "Verabschiedung"};
-static const char *ex13_groups[] = {"Deutschland (D)", "Österreich (A)", "Schweiz (CH)"};
-
-static 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",
-};
-
-static 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",
-};
-
-typedef struct {
- GtkWidget *pool;
- GtkWidget *group_flow[3];
- GtkWidget *group_btn[3];
- int n_groups;
- int active_group;
- GtkWidget **items; /* per-item boxes (chip + hidden meaning) */
- GtkWidget **trans; /* hidden meaning labels */
- GtkWidget *stage; /* transparent overlay layer for the fly */
- gboolean flying; /* a click-move animation is in progress */
- int n_items;
- int *current_group;
- const AssignItem *items_data;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
-} AssignCtx;
-
-typedef struct {
- AssignCtx *ac;
- int idx;
-} AssignChipRef;
-
-static 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);
- }
-}
-
-static 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);
- }
-}
-
-static 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;
- }
- }
-}
-
-static 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);
-}
-
-static 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;
-}
-
-/* ------------------------------------------------------------------ */
-/* Unit 2 exercises ("Aus aller Welt") */
-/* ------------------------------------------------------------------ */
-
-/* ---- generic word-row helper (single blank, shared word bank) ----- */
-
-static void verb_rows_add(ComboListCtx *ctx, GtkWidget *body, int *counter,
- const VerbQ *qs, int n,
- const char **pool, int pool_n,
- const char **meanings, gboolean pre_meaning) {
- for (int i = 0; i < n; i++) {
- GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- GtkWidget *before = gtk_label_new(qs[i].before);
- GtkWidget *after = gtk_label_new(qs[i].after);
- GtkWidget *combo;
- char *numtxt;
-
- (*counter)++;
- numtxt = g_strdup_printf("%d. ", *counter);
- GtkWidget *num = gtk_label_new(numtxt);
- g_free(numtxt);
-
- gtk_widget_set_halign(row, GTK_ALIGN_START);
- gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(num, "ex-prompt");
- gtk_box_append(GTK_BOX(row), num);
-
- 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(pool, pool_n);
- gtk_widget_set_size_request(combo, 150, -1);
- gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(row), combo);
- combo_list_add(ctx, combo, qs[i].answer);
-
- if (qs[i].after && qs[i].after[0]) {
- 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);
-
- if (meanings && meanings[i]) {
- GtkWidget *mean = meaning_add(body, meanings[i]);
- if (pre_meaning || ctx->trans_upfront)
- gtk_widget_set_visible(mean, TRUE);
- combo_list_add_trans(ctx, mean);
- }
-
- if (ctx->reveal_german) {
- char *sentence = verbq_german(qs[i].before, qs[i].answer,
- qs[i].after);
-
- if (sentence && sentence[0]) {
- GtkWidget *w = model_answer_add(body, sentence);
-
- g_array_append_val(ctx->models, w);
- }
- g_free(sentence);
- }
- }
-}
-
-typedef struct {
- ComboListCtx *ctx;
- GtkWidget *note;
- const VerbQ *qs;
- int n;
- const char **gloss; /* per-row word-level meaning keys (current lang) */
-} VerbNoteCtx;
-
-static void verb_note_reveal(VerbNoteCtx *nc) {
- GString *s;
- int i;
-
- if (!nc->note || !nc->gloss)
- return;
-
- s = g_string_new(NULL);
- for (i = 0; i < nc->n; i++) {
- if (i > 0)
- g_string_append_c(s, '\n');
- g_string_append_printf(s, "%s – %s", nc->qs[i].answer,
- tr(nc->gloss[i]));
- }
- gtk_label_set_text(GTK_LABEL(nc->note), s->str);
- g_string_free(s, TRUE);
- gtk_widget_set_visible(nc->note, TRUE);
-}
-
-static void verb_ex_check(GtkButton *button, gpointer data) {
- VerbNoteCtx *nc = data;
-
- combo_list_check(button, nc->ctx);
- verb_note_reveal(nc);
-}
-
-static GtkWidget *build_verb_ex(UnitCtx *unit, int ex_num,
- const char *title, const char *sub,
- const VerbQ *qs, int n,
- const char **pool, int pool_n,
- const char **meanings, gboolean pre_meaning,
- const char **gloss) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
- &body, &feedback, &check);
- ComboListCtx *ctx = combo_list_ctx_new(unit, ex_num, feedback);
- VerbNoteCtx *nc = NULL;
- int counter = 0;
-
- ctx->trans_upfront = TRUE;
- ctx->reveal_german = TRUE;
-
- verb_rows_add(ctx, body, &counter, qs, n, pool, pool_n, meanings,
- pre_meaning);
-
- if (gloss) {
- GtkWidget *note = gtk_label_new(NULL);
- gtk_widget_set_halign(note, GTK_ALIGN_START);
- gtk_widget_set_margin_top(note, 6);
- gtk_widget_add_css_class(note, "meaning");
- gtk_label_set_wrap(GTK_LABEL(note), TRUE);
- gtk_widget_set_visible(note, FALSE);
- gtk_box_append(GTK_BOX(body), note);
-
- nc = g_new0(VerbNoteCtx, 1);
- nc->ctx = ctx;
- nc->note = note;
- nc->qs = qs;
- nc->n = n;
- nc->gloss = gloss;
- g_signal_connect(check, "clicked", G_CALLBACK(verb_ex_check), nc);
- } else {
- g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
- }
-
- return page;
-}
-
-/* ---- typed-entry builder with auto-check --------------------------- */
-
-typedef struct {
- const char *prompt; /* German prompt text */
- const char *answers; /* accepted answers separated by '|' */
- const char *meaning; /* i18n key of the revealed meaning or NULL */
-} TypedQ;
-
-typedef struct {
- const TypedQ *qs;
- int n;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
- GtkWidget **entries;
- GtkWidget **trans;
-} TypedCtx;
-
-static void typed_check(GtkButton *button, gpointer data) {
- TypedCtx *ctx = data;
- int ok = 0;
-
- (void)button;
-
- for (int i = 0; i < ctx->n; i++) {
- const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(ctx->entries[i]));
- gchar *norm = normalize_answer(txt);
- gboolean good = txt && txt[0] && answer_accepts(norm, ctx->qs[i].answers);
- g_free(norm);
- answer_mark(ctx->entries[i], good);
- if (good)
- ok++;
- }
-
- if (ctx->trans) {
- for (int i = 0; i < ctx->n; i++)
- if (ctx->trans[i])
- gtk_widget_set_visible(ctx->trans[i], TRUE);
- }
-
- 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"));
- }
-}
-
-typedef struct {
- TypedCtx *ctx;
- GtkWidget *note;
- const TypedQ *qs;
- int n;
-} TypedNoteCtx;
-
-/* Capitalise the first (canonical) answer alternative for display. */
-static char *typed_answer_display(const char *answers) {
- const char *pipe = strchr(answers, '|');
- char *base = pipe ? g_strndup(answers, (gsize)(pipe - answers))
- : g_strdup(answers);
-
- if (base[0]) {
- gunichar ch = g_utf8_get_char(base);
- gunichar up = g_unichar_toupper(ch);
- if (up != ch) {
- char *first = g_ucs4_to_utf8(&up, 1, NULL, NULL, NULL);
- char *res = g_strconcat(first, g_utf8_next_char(base), NULL);
- g_free(first);
- g_free(base);
- return res;
- }
- }
- return base;
-}
-
-static void typed_note_reveal(TypedNoteCtx *nc) {
- GString *s;
- int i;
-
- if (!nc->note)
- return;
-
- s = g_string_new(NULL);
- for (i = 0; i < nc->n; i++) {
- char *word = typed_answer_display(nc->qs[i].answers);
- if (i > 0)
- g_string_append_c(s, '\n');
- if (nc->qs[i].meaning)
- g_string_append_printf(s, "%s – %s", word, tr(nc->qs[i].meaning));
- else
- g_string_append(s, word);
- g_free(word);
- }
- gtk_label_set_text(GTK_LABEL(nc->note), s->str);
- g_string_free(s, TRUE);
- gtk_widget_set_visible(nc->note, TRUE);
-}
-
-static void typed_ex_check(GtkButton *button, gpointer data) {
- TypedNoteCtx *nc = data;
-
- typed_check(button, nc->ctx);
- typed_note_reveal(nc);
-}
-
-static GtkWidget *build_typed(UnitCtx *unit, int ex_num,
- const char *title, const char *sub,
- const TypedQ *qs, int n,
- const char *word_bank, gboolean answers_note) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
- &body, &feedback, &check);
- TypedCtx *ctx = g_new0(TypedCtx, 1);
- TypedNoteCtx *nc = NULL;
-
- ctx->qs = qs;
- ctx->n = n;
- ctx->unit = unit;
- ctx->ex_num = ex_num;
- ctx->feedback = feedback;
- ctx->entries = g_new0(GtkWidget *, n);
- ctx->trans = g_new0(GtkWidget *, n);
-
- {
- gboolean need = FALSE;
-
- for (int i = 0; i < n; i++) {
- if (text_has_german_umlaut(qs[i].prompt) ||
- text_has_german_umlaut(qs[i].answers)) {
- need = TRUE;
- break;
- }
- }
- if (need)
- add_umlaut_note(body);
- }
-
- if (word_bank) {
- GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- GtkWidget *hdr = gtk_label_new(NULL);
- GtkWidget *words = gtk_label_new(word_bank);
-
- gtk_widget_set_halign(row, GTK_ALIGN_START);
- i18n_bind(hdr, "wordbank", 0);
- gtk_widget_add_css_class(hdr, "hint");
- gtk_widget_add_css_class(words, "hint");
- gtk_box_append(GTK_BOX(row), hdr);
- gtk_box_append(GTK_BOX(row), words);
- gtk_box_append(GTK_BOX(body), row);
- }
-
- for (int i = 0; i < n; i++) {
- GtkWidget *prompt = gtk_label_new(qs[i].prompt);
- GtkWidget *entry;
-
- gtk_widget_set_halign(prompt, GTK_ALIGN_START);
- gtk_widget_add_css_class(prompt, "ex-prompt");
- gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
- gtk_box_append(GTK_BOX(body), prompt);
-
- entry = gtk_entry_new();
- gtk_widget_set_hexpand(entry, FALSE);
- gtk_widget_set_halign(entry, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), entry);
- ctx->entries[i] = entry;
-
- if (qs[i].meaning)
- ctx->trans[i] = meaning_add(body, qs[i].meaning);
- }
-
- if (answers_note) {
- GtkWidget *note = gtk_label_new(NULL);
- gtk_widget_set_halign(note, GTK_ALIGN_START);
- gtk_widget_set_margin_top(note, 6);
- gtk_widget_add_css_class(note, "meaning");
- gtk_label_set_wrap(GTK_LABEL(note), TRUE);
- gtk_widget_set_visible(note, FALSE);
- gtk_box_append(GTK_BOX(body), note);
-
- nc = g_new0(TypedNoteCtx, 1);
- nc->ctx = ctx;
- nc->note = note;
- nc->qs = qs;
- nc->n = n;
- g_signal_connect(check, "clicked", G_CALLBACK(typed_ex_check), nc);
- } else {
- g_signal_connect(check, "clicked", G_CALLBACK(typed_check), ctx);
- }
-
- return page;
-}
-
-/* ---- G05/G06/G07/G11/S03 typed content ----------------------------- */
-
-static const char *g05_bank =
- "Tscheche, Tschechin, Spanier, Spanierin, Türke, Türkin, Kroate, "
- "Kroatin, Deutscher, Deutsche, Österreicher, Österreicherin, "
- "Slowake, Slowakin";
-
-static const TypedQ g05_rows[] = {
- {"Er kommt aus Tschechien. Er ist _________.", "tscheche", "Čech"},
- {"Sie kommt aus Spanien. Sie ist _________.", "spanierin", "Španělka"},
- {"Er kommt aus der Türkei. Er ist _________.", "türke|turke", "Turek"},
- {"Sie kommt aus Kroatien. Sie ist _________.", "kroatin", "Chorvatka"},
- {"Sie kommt aus Deutschland. Sie ist _________.", "deutsche", "Němka"},
- {"Sie kommt aus Österreich. Sie ist _________.", "österreicherin|osterreicherin",
- "Rakušanka"},
- {"Er kommt aus der Slowakei. Er ist _________.", "slowake", "Slovák"},
-};
-
-static const TypedQ g06_rows[] = {
- {"Ich heiße Thomas. Ich spreche Deutsch. Ich komme aus D________.",
- "deutschland|eutschland", "Deutschland – Německo"},
- {"Ich heiße Martina. Ich spreche Slowakisch. Ich komme aus der S________.",
- "slowakei|lowakei", "Slowakei – Slovensko"},
- {"Ich heiße Petra. Ich spreche Deutsch. Ich komme aus Ö________.",
- "österreich|osterreich|sterreich", "Österreich – Rakousko"},
- {"Ich heiße Miguel. Ich spreche Spanisch. Ich komme aus S________.",
- "spanien|panien", "Spanien – Španělsko"},
- {"Ich heiße George. Ich spreche Englisch. Ich komme aus E________.",
- "england|ngland", "England – Anglie"},
- {"Ich heiße Maria. Ich spreche Polnisch. Ich komme aus P________.",
- "polen|olen", "Polen – Polsko"},
-};
-
-static const TypedQ g07_rows[] = {
- {"1. Ru__land", "russland|ss", "Russland – Rusko"},
- {"2. T_rkei", "türkei|turkei|ü|u", "Türkei – Turecko"},
- {"3. Slo__kei", "slowakei|wa", "Slowakei – Slovensko"},
- {"4. Kro__ien", "kroatien|at", "Kroatien – Chorvatsko"},
- {"5. Deut___land", "deutschland|sch", "Deutschland – Německo"},
- {"6. Öst__reich", "österreich|osterreich|er", "Österreich – Rakousko"},
- {"7. Gr__chenland", "griechenland|ie", "Griechenland – Řecko"},
- {"8. Ita__en", "italien|li", "Italien – Itálie"},
-};
-
-static const TypedQ g11_rows[] = {
- {"tergehvatereikauwauzeh", "vater", "otec"},
- {"berelternnemrerhobschaz", "eltern", "rodiče"},
- {"nefrastschechinreiabtab", "tschechin", "Češka"},
- {"hejkosstudentaldwsahujoi", "student", "student"},
-};
-
-static const TypedQ s03_rows[] = {
- {"1. 🇨🇿 Bist du Russe? – Nein, ich bin _________.", "tscheche",
- "Čech"},
- {"2. 🇦🇹 Bist du Tschechin? – Nein, ich bin _________.", "österreicherin|osterreicherin",
- "Rakušanka"},
- {"3. 🇨🇭 Bist du Deutsche? – Nein, ich bin _________.", "schweizerin",
- "Švýcarka"},
- {"4. 🇩🇪 Bist du Engländer? – Nein, ich bin _________.", "deutscher",
- "Němec"},
- {"5. 🇨🇭 Bist du Amerikanerin? – Nein, ich bin _________.", "schweizerin",
- "Švýcarka"},
-};
-
-/* ---- G01 content ---------------------------------------------------- */
-
-static const VerbQ g01_peter[] = {
- {"Er ", " aus Österreich. (kommen)", "kommt"},
- {"Er ", " in Graz. (wohnen)", "wohnt"},
- {"Er ", " gern Golf. (spielen)", "spielt"},
- {"Er ", " Deutsch, Englisch und Italienisch. (sprechen)", "spricht"},
-};
-
-static const VerbQ g01_jana[] = {
- {"Sie ", " aus der Slowakei. (kommen)", "kommen"},
- {"Sie ", " in Bratislava. (wohnen)", "wohnen"},
- {"Sie ", " gern. (tanzen)", "tanzt"},
- {"Er ", " gern. (reisen)", "reist"},
- {"Sie ", " gute Freunde. (sein)", "sind"},
-};
-
-static const char *g01_pool[] = {
- "kommt", "kommst", "komme", "kommen",
- "wohnt", "wohnst", "wohne", "wohnen",
- "spielt", "spielst", "spiele", "spielen",
- "spricht", "sprichst", "spreche", "sprechen",
- "tanzt", "tanze", "tanzen",
- "reist", "reise", "reisen",
- "ist", "bist", "bin", "sind", "seid",
-};
-
-static const char *g01_peter_mean[] = {
- "Pochází z Rakouska.",
- "Bydlí ve Štýrském Hradci (Graz).",
- "Rád hraje golf.",
- "Mluví německy, anglicky a italsky.",
-};
-
-static const char *g01_jana_mean[] = {
- "Pocházejí ze Slovenska.",
- "Bydlí v Bratislavě (ona).",
- "Ona ráda tancuje.",
- "On rád cestuje.",
- "Jsou dobří přátelé.",
-};
-
-/* ---- G04 sprechen --------------------------------------------------- */
-
-static const VerbQ g04_rows[] = {
- {"• Ich ", " Deutsch und du?", "spreche"},
- {"• Welche Sprachen ", " Sie?", "sprechen"},
- {"• ", " du Französisch?", "sprichst"},
- {"• Wir ", " Polnisch und ihr?", "sprechen"},
- {"• ", " ihr Englisch?", "sprecht"},
- {"• Er ", " kein Wort Türkisch.", "spricht"},
-};
-
-static const char *g04_pool[] = {
- "spreche", "sprichst", "spricht", "sprechen", "sprecht",
-};
-
-static const char *g04_mean[] = {
- "Mluvím německy. A ty?",
- "Kterými jazyky mluvíte?",
- "Mluvíš francouzsky? – Ano, trochu.",
- "My mluvíme polsky a vy?",
- "Mluvíte anglicky? – Ano, velmi dobře.",
- "Nemluví ani slovo turecky.",
-};
-
-/* ---- G08 Verben einsetzen ------------------------------------------- */
-
-static const VerbQ g08_rows[] = {
- {"Was ", " er? → Ein Buch. (lesen)", "liest"},
- {"Was ", " sie? → Medizin. (studieren)", "studiert"},
- {"Was ", " Jan gern? → Auto. (fahren)", "fährt"},
- {"Wo ", " ihr? → In Wien. (leben)", "lebt"},
- {"Wer ", " bei Siemens? → Meine Freundin Vera. (arbeiten)",
- "arbeitet"},
-};
-
-static const char *g08_pool[] = {
- "liest", "lese", "lest", "lesen",
- "studiert", "studiere", "studiert", "studieren",
- "fährt", "fahre", "fahrt", "fahren",
- "lebt", "lebe", "lebt", "leben",
- "arbeitet", "arbeite", "arbeitet", "arbeiten",
-};
-
-static const char *g08_mean[] = {
- "Co čte? – Knihu.",
- "Co studuje? – Medicínu.",
- "Čím Jan rád jezdí? – Autem.",
- "Kde bydlíte? – Ve Vídni.",
- "Kdo pracuje u Siemensu? – Moje kamarádka Věra.",
-};
-
-/* ---- G10 Euro -------------------------------------------------------- */
-
-static const VerbQ g10_rows[] = {
- {"Julia hat ", " Euro.", "dreiundsechzig"},
- {"Tina hat ", " Euro.", "achtunddreißig"},
- {"Peter hat ", " Euro.", "fünfundvierzig"},
- {"Hans hat ", " Euro.", "siebenundfünfzig"},
- {"Barbara hat ", " Euro.", "neunundneunzig"},
-};
-
-static const char *g10_pool[] = {
- "dreiundsechzig", "achtunddreißig", "fünfundvierzig",
- "siebenundfünfzig", "neunundneunzig",
-};
-
-static const char *g10_mean[] = {
- "šedesát tři eur",
- "třicet osm eur",
- "čtyřicet pět eur",
- "padesát sedm eur",
- "devadesát devět eur",
-};
-
-/* ---- G14 Lückentext -------------------------------------------------- */
-
-static const VerbQ g14_rows[] = {
- {"Agnieszka Kowalski kommt ", " Polen, aus Kraków.", "aus"},
- {"Sie ist 18 Jahre alt und ", " dieses Jahr Abitur.", "macht"},
- {"Sie chattet gerne mit ihrem deutschen ", ".", "Freund"},
- {"Er heißt Stefan Böhmermann und ", " in Dresden.", "lebt"},
- {"Agnieszka findet die ", " cool.", "Sprache"},
- {"Sie spricht schon sehr ", " Deutsch.", "gut"},
-};
-
-static const char *g14_pool[] = {
- "aus", "macht", "Freund", "lebt", "Sprache", "gut",
-};
-
-static const char *g14_mean[] = {
- "Agnieszka Kowalski pochází z Polska, z Krakova.",
- "Je jí 18 let a letos dělá maturitu.",
- "Ráda chatuje se svým německým přítelem.",
- "Jmenuje se Stefan Böhmermann a bydlí v Drážďanech.",
- "Agnieszce přijde ten jazyk super.",
- "Už mluví velmi dobře německy.",
-};
-
-static const char *g14_gloss[] = {
- "z", "dělá", "kamarád", "žije", "jazyk", "dobře",
-};
-
-/* ---- G15 Verbinde ---------------------------------------------------- */
-
-static const VerbQ g15_rows[] = {
- {"ein Handy ", "", "brauchen"},
- {"bei BMW ", "", "arbeiten"},
- {"Auto ", "", "fahren"},
- {"das Gymnasium ", "", "besuchen"},
- {"Spanisch ", "", "sprechen"},
- {"Krimis ", "", "lesen"},
-};
-
-static const char *g15_pool[] = {
- "brauchen", "arbeiten", "fahren", "besuchen", "sprechen", "lesen",
-};
-
-static const char *g15_mean[] = {
- "potřebovat mobil",
- "pracovat ve BMW",
- "řídit auto",
- "navštěvovat gymnázium",
- "mluvit španělsky",
- "číst detektivky",
-};
-
-static const char *g15_gloss[] = {
- "potřebovat", "pracovat", "řídit",
- "navštěvovat", "mluvit", "číst",
-};
-
-/* ---- G16 Zahlenpaare -------------------------------------------------- */
-
-static const VerbQ g16_rows[] = {
- {"54 = ", "", "vierundfünfzig"},
- {"45 = ", "", "fünfundvierzig"},
- {"369 = ", "", "dreihundertneunundsechzig"},
- {"112 = ", "", "hundertzwölf"},
- {"122 = ", "", "hundertzweiundzwanzig"},
- {"68 = ", "", "achtundsechzig"},
-};
-
-static const char *g16_pool[] = {
- "vierundfünfzig", "fünfundvierzig", "dreihundertneunundsechzig",
- "hundertzwölf", "hundertzweiundzwanzig", "achtundsechzig",
-};
-
-static const char *g16_mean[] = {
- "padesát čtyři",
- "čtyřicet pět",
- "tři sta šedesát devět",
- "sto dvanáct",
- "sto dvacet dva",
- "šedesát osm",
-};
-
-/* ---- G02 choice ------------------------------------------------------ */
-
-static const ChoiceQ g02_rows[] = {
- {"1. Ich komme ___ Athen.", {"aus", "in"}, 2, 0},
- {"2. Peter wohnt ___ Salzburg.", {"aus", "in"}, 2, 1},
- {"3. Berlin liegt ___ Deutschland.", {"aus", "in"}, 2, 1},
- {"4. Wir wohnen ___ Paris.", {"aus", "in"}, 2, 1},
- {"5. Sie kommen ___ Griechenland.", {"aus", "in"}, 2, 0},
-};
-
-static const char *g02_mean[] = {
- "Pocházím z Atén.",
- "Petr bydlí v Salcburku.",
- "Berlín leží v Německu.",
- "Bydlíme v Paříži.",
- "Pocházejí z Řecka.",
-};
-
-/* ---- G03 Fragewörter -------------------------------------------------- */
-
-static const ChoiceQ g03_rows[] = {
- {"1. ___ kommt er? → Aus London.", {"Wer", "Wie", "Wo", "Woher", "Was"},
- 5, 3},
- {"2. ___ heißt sie? → Andrea.", {"Wer", "Wie", "Wo", "Woher", "Was"},
- 5, 1},
- {"3. ___ kommt aus Russland? → Alexander.",
- {"Wer", "Wie", "Wo", "Woher", "Was"}, 5, 0},
- {"4. ___ liegt Prag? → In Tschechien.", {"Wer", "Wie", "Wo", "Woher", "Was"},
- 5, 2},
- {"5. ___ wohnen sie? → In Bratislava.",
- {"Wer", "Wie", "Wo", "Woher", "Was"}, 5, 2},
- {"6. ___ macht ihr? → Wir studieren Germanistik.",
- {"Wer", "Wie", "Wo", "Woher", "Was"}, 5, 4},
-};
-
-static const char *g03_mean[] = {
- "Odkud je? – Z Londýna.",
- "Jak se jmenuje? – Andrea.",
- "Kdo pochází z Ruska? – Alexandr.",
- "Kde leží Praha? – V Česku.",
- "Kde bydlí? – V Bratislavě.",
- "Co děláte? – Studujeme germanistiku.",
-};
-
-/* ---- G12 Unterstreiche ------------------------------------------------ */
-
-static const ChoiceQ g12_rows[] = {
- {"1. Peter ___ gern Bücher.", {"liest", "lest", "lesen"}, 3, 0},
- {"2. Filip ___ in Salzburg.", {"arbeit", "arbeitet", "arbeite"}, 3, 1},
- {"3. Magda ___ gut Englisch.", {"spricht", "sprecht", "sprechen"}, 3, 0},
- {"4. Marek ___ das Gymnasium.", {"besuchst", "besucht", "besuchen"}, 3, 1},
- {"5. Dominika ___ 15 Jahre alt.", {"ist", "seid", "bin"}, 3, 0},
- {"6. Meine Schwester ___ Marta.", {"heiße", "heißt", "heißen"}, 3, 1},
-};
-
-static const char *g12_mean[] = {
- "Petr rád čte knihy.",
- "Filip pracuje v Salcburku.",
- "Magda dobře mluví anglicky.",
- "Marek navštěvuje gymnázium.",
- "Dominice je 15 let.",
- "Moje sestra se jmenuje Marta.",
-};
-
-/* ---- G13 Ordne zu ----------------------------------------------------- */
-
-typedef struct {
- const char *mid; /* middle part of the question, without the FW */
- const char *fw; /* correct Fragewort */
- const char *ans; /* correct answer */
-} OrdneRow;
-
-static const OrdneRow g13_rows[] = {
- {"heißen sie?", "Wie", "Jana und Michael."},
- {"spricht Polnisch?", "Wer", "Jacek."},
- {"wohnt Eva?", "Wo", "In Plzeň."},
- {"kommt Markus?", "Woher", "Aus Deutschland."},
- {"macht ihr?", "Was", "Wir studieren Germanistik."},
- {"spricht Jana?", "Was", "Slowakisch."},
-};
-
-static const char *g13_fw_pool[] = {"Was", "Wer", "Wie", "Wo", "Woher"};
-
-static const char *g13_ans_pool[] = {
- "Jana und Michael.", "Jacek.", "In Plzeň.", "Aus Deutschland.",
- "Wir studieren Germanistik.", "Slowakisch.",
-};
-
-static const char *g13_mean[] = {
- "Jak se jmenují? – Jana a Michael.",
- "Kdo mluví polsky? – Jacek.",
- "Kde bydlí Eva? – V Plzni.",
- "Odkud pochází Markus? – Z Německa.",
- "Co děláte? – Studujeme germanistiku.",
- "Jakým jazykem mluví Jana? – Slovensky.",
-};
-
-/* ---- G09 content (Freie Antwort) ------------------------------------- */
-
-static const FreeQ ex9_free_qs[] = {
- {"Wie heißt du?", "Jak se jmenuješ?",
- "Ich heiße Lukas.", "Jmenuji se Lukáš."},
- {"Wo wohnst du?", "Kde bydlíš?",
- "Ich wohne in Prag.", "Bydlím v Praze."},
- {"Wohin fährst du?", "Kam jedeš?",
- "Ich fahre nach Deutschland.", "Jedu do Německa."},
- {"Was liest du gern?", "Co rád/a čteš?",
- "Ich lese gern Krimis.", "Rád/a čtu detektivky."},
- {"Was spielst du gern?", "Co rád/a hraješ?",
- "Ich spiele gern Golf.", "Rád/a hraju golf."},
-};
-
-/* ---- S01 Steckbrief --------------------------------------------------- */
-
-typedef struct {
- const char *stem; /* German sentence beginning with an ellipsis */
- const char *sample; /* finished German sample line */
-} ProfileQ;
-
-static const ProfileQ s01_rows[] = {
- {"Ich heiße …", "Ich heiße Petra."},
- {"Ich komme …", "Ich komme aus Tschechien."},
- {"Ich lebe …", "Ich lebe in Prag."},
- {"Ich bin …", "Ich bin fünfzehn Jahre alt."},
- {"Ich spreche …", "Ich spreche Tschechisch und Deutsch."},
- {"Ich besuche …", "Ich besuche das Gymnasium."},
- {"Ich finde …", "Ich finde Deutsch toll."},
- {"Ich arbeite …", "Ich arbeite noch nicht."},
-};
-
-static void s01_fill_sample(GtkWidget *label) {
- const ProfileQ *q = g_object_get_data(G_OBJECT(label), "pq");
- char *txt;
-
- if (!q)
- return;
- txt = g_strdup_printf(tr("sample_line"), q->sample);
- gtk_label_set_text(GTK_LABEL(label), txt);
- g_free(txt);
-}
-
-typedef struct {
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
- GtkWidget **samples;
- int n;
-} ProfileCtx;
-
-static void s01_reveal(GtkButton *button, gpointer data) {
- GtkWidget *sample = data;
- (void)button;
- s01_fill_sample(sample);
- gtk_widget_set_visible(sample, !gtk_widget_get_visible(sample));
-}
-
-static void s01_finish(GtkButton *button, gpointer data) {
- ProfileCtx *ctx = data;
- (void)button;
- set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
- for (int i = 0; i < ctx->n; i++) {
- s01_fill_sample(ctx->samples[i]);
- gtk_widget_set_visible(ctx->samples[i], TRUE);
- }
- mark_done(ctx->unit, ctx->ex_num);
-}
-
-static GtkWidget *build_profile(UnitCtx *unit, int ex_num, const char *title,
- const char *sub, const ProfileQ *qs, int n) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, title, sub, "finish",
- &body, &feedback, &check);
- ProfileCtx *ctx = g_new0(ProfileCtx, 1);
-
- ctx->unit = unit;
- ctx->ex_num = ex_num;
- ctx->feedback = feedback;
- ctx->n = n;
- ctx->samples = g_new0(GtkWidget *, n);
-
- for (int i = 0; i < n; i++) {
- GtkWidget *prompt = gtk_label_new(qs[i].stem);
- GtkWidget *entry;
- GtkWidget *reveal;
- GtkWidget *sample;
-
- gtk_widget_set_halign(prompt, GTK_ALIGN_START);
- gtk_widget_add_css_class(prompt, "ex-prompt");
- gtk_box_append(GTK_BOX(body), prompt);
-
- 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), "pq", (gpointer)&qs[i]);
- 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(s01_reveal), sample);
- }
-
- g_signal_connect(check, "clicked", G_CALLBACK(s01_finish), ctx);
- return page;
-}
-
-/* ---- S02 Hangman (Berufe) --------------------------------------------- */
-
-#define HM_WORDS 5
-#define HM_MAX_MISSES 6
-
-static const char *hm_words[HM_WORDS] = {
- "KOCH", "ARZT", "LEHRER", "POLIZIST", "VERKÄUFER",
-};
-
-static const char *hm_tips[HM_WORDS] = {
- "vaří v restauraci",
- "léčí nemocné lidi",
- "učí ve škole",
- "pracuje u policie",
- "prodává v obchodě",
-};
-
-/* QWERTY order, Ä/Ö/Ü in their own bottom row. */
-static const char *hm_letters[] = {
- "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
- "A", "S", "D", "F", "G", "H", "J", "K", "L",
- "Z", "X", "C", "V", "B", "N", "M",
- "Ä", "Ö", "Ü",
-};
-#define HM_N_LETTERS (int)(G_N_ELEMENTS(hm_letters))
-#define HM_ROW2 10 /* start of the ASDF row */
-#define HM_ROW3 19 /* start of the ZXCV row */
-#define HM_ROW4 26 /* start of the umlaut row */
-
-static const gunichar hm_letters_u[] = {
- 'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P',
- 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L',
- 'Z', 'X', 'C', 'V', 'B', 'N', 'M',
- 0x00c4, 0x00d6, 0x00dc,
-};
-
-typedef struct {
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
- GtkWidget *draw;
- GtkWidget *slots;
- GtkWidget *hint_lbl;
- GtkWidget *counter_lbl;
- GtkWidget **letter_btns;
- gboolean guessed[HM_N_LETTERS];
- gboolean locked; /* true during the "word solved" pause */
- gboolean finished;
- int misses;
- int word_index;
-} HangmanCtx;
-
-static int hm_char_index(gunichar ch) {
- for (int i = 0; i < HM_N_LETTERS; i++)
- if (hm_letters_u[i] == ch)
- return i;
- return -1;
-}
-
-static void hm_update_slots(HangmanCtx *hm) {
- const char *w = hm_words[hm->word_index];
- const char *p = w;
- GString *out = g_string_new(NULL);
-
- while (*p) {
- gunichar ch = g_utf8_get_char(p);
- p = g_utf8_next_char(p);
- if (out->len > 0)
- g_string_append_c(out, ' ');
- int idx = hm_char_index(ch);
- if (idx >= 0 && hm->guessed[idx])
- g_string_append_unichar(out, ch);
- else
- g_string_append_c(out, '_');
- }
- gtk_label_set_text(GTK_LABEL(hm->slots), out->str);
- g_string_free(out, TRUE);
-}
-
-static gboolean hm_word_solved(HangmanCtx *hm) {
- const char *w = hm_words[hm->word_index];
- const char *p = w;
-
- while (*p) {
- gunichar ch = g_utf8_get_char(p);
- p = g_utf8_next_char(p);
- int idx = hm_char_index(ch);
- if (idx < 0 || !hm->guessed[idx])
- return FALSE;
- }
- return TRUE;
-}
-
-static void hm_set_letters_enabled(HangmanCtx *hm, gboolean enabled) {
- for (int i = 0; i < HM_N_LETTERS; i++) {
- if (hm->letter_btns[i])
- gtk_widget_set_sensitive(hm->letter_btns[i], enabled);
- }
-}
-
-static void hm_redraw(HangmanCtx *hm) {
- if (hm->draw)
- gtk_widget_queue_draw(hm->draw);
-}
-
-static void hm_clear_feedback(HangmanCtx *hm) {
- gtk_widget_remove_css_class(hm->feedback, "feedback-ok");
- gtk_widget_remove_css_class(hm->feedback, "feedback-err");
- gtk_label_set_text(GTK_LABEL(hm->feedback), "");
-}
-
-static gboolean hm_advance(gpointer data) {
- HangmanCtx *hm = data;
-
- hm->locked = FALSE;
- if (hm->word_index + 1 >= HM_WORDS) {
- hm->finished = TRUE;
- set_feedback(hm->feedback, TRUE, tr("hm_done"));
- mark_done(hm->unit, hm->ex_num);
- return G_SOURCE_REMOVE;
- }
- hm->word_index++;
- hm->misses = 0;
- for (int i = 0; i < HM_N_LETTERS; i++)
- hm->guessed[i] = FALSE;
- hm_set_letters_enabled(hm, TRUE);
- hm_update_slots(hm);
- gtk_label_set_text(GTK_LABEL(hm->hint_lbl), tr(hm_tips[hm->word_index]));
- {
- char *t = g_strdup_printf(tr("hm_progress"), hm->word_index + 1,
- HM_WORDS);
- gtk_label_set_text(GTK_LABEL(hm->counter_lbl), t);
- g_free(t);
- }
- hm_clear_feedback(hm);
- hm_redraw(hm);
- return G_SOURCE_REMOVE;
-}
-
-static void hm_reset_word(gpointer data) {
- HangmanCtx *hm = data;
- hm->misses = 0;
- for (int i = 0; i < HM_N_LETTERS; i++)
- hm->guessed[i] = FALSE;
- hm->locked = FALSE;
- hm_set_letters_enabled(hm, TRUE);
- hm_update_slots(hm);
- hm_clear_feedback(hm);
- hm_redraw(hm);
-}
-
-static gboolean hm_retry_word(gpointer data) {
- hm_reset_word(data);
- return G_SOURCE_REMOVE;
-}
-
-static void hm_guess_letter(GtkButton *button, gpointer data) {
- HangmanCtx *hm = data;
- int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "li"));
- const char *w = hm_words[hm->word_index];
- const char *p = w;
- gboolean correct = FALSE;
-
- (void)button;
- if (hm->finished || hm->locked)
- return;
- gtk_widget_set_sensitive(GTK_WIDGET(button), FALSE);
- hm->guessed[idx] = TRUE;
-
- while (*p) {
- gunichar ch = g_utf8_get_char(p);
- p = g_utf8_next_char(p);
- if (hm_char_index(ch) == idx) {
- correct = TRUE;
- break;
- }
- }
-
- if (correct) {
- hm_update_slots(hm);
- if (hm_word_solved(hm)) {
- hm->locked = TRUE;
- hm_set_letters_enabled(hm, FALSE);
- set_feedback(hm->feedback, TRUE, tr("hm_wrong"));
- g_timeout_add(1100, hm_advance, hm);
- }
- } else {
- hm->misses++;
- hm_redraw(hm);
- if (hm->misses >= HM_MAX_MISSES) {
- char *msg = g_strdup_printf(tr("hm_fail"), w);
- hm->locked = TRUE;
- hm_set_letters_enabled(hm, FALSE);
- set_feedback(hm->feedback, FALSE, msg);
- g_free(msg);
- /* reveal the word, then restart it with empty letters */
- for (int i = 0; i < HM_N_LETTERS; i++)
- hm->guessed[i] = TRUE;
- hm_update_slots(hm);
- hm_redraw(hm);
- g_timeout_add(1600, hm_retry_word, hm);
- }
- }
-}
-
-static void draw_hangman(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- HangmanCtx *hm = data;
- Rgb c = color_from_hex(app_theme.subtext);
- double cx = width / 2.0 - 30.0;
- double ground = height - 10.0;
- int parts = hm->misses;
-
- (void)area;
- cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
- cairo_paint(cr);
- cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
- cairo_set_source_rgb(cr, c.r, c.g, c.b);
- cairo_set_line_width(cr, 3.0);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
-
- /* scaffold */
- cairo_new_path(cr);
- cairo_move_to(cr, width * 0.15, ground);
- cairo_line_to(cr, width * 0.85, ground);
- cairo_move_to(cr, cx, ground);
- cairo_line_to(cr, cx, 20.0);
- cairo_line_to(cr, cx + 70.0, 20.0);
- cairo_move_to(cr, cx + 70.0, 20.0);
- cairo_line_to(cr, cx + 70.0, 42.0);
- cairo_stroke(cr);
-
- if (parts >= 1) { /* head */
- cairo_new_path(cr);
- cairo_arc(cr, cx + 70.0, 54.0, 12.0, 0.0, 2.0 * G_PI);
- cairo_stroke(cr);
- }
- if (parts >= 2) { /* body */
- cairo_new_path(cr);
- cairo_move_to(cr, cx + 70.0, 66.0);
- cairo_line_to(cr, cx + 70.0, 108.0);
- cairo_stroke(cr);
- }
- if (parts >= 3) { /* left arm */
- cairo_new_path(cr);
- cairo_move_to(cr, cx + 70.0, 76.0);
- cairo_line_to(cr, cx + 50.0, 96.0);
- cairo_stroke(cr);
- }
- if (parts >= 4) { /* right arm */
- cairo_new_path(cr);
- cairo_move_to(cr, cx + 70.0, 76.0);
- cairo_line_to(cr, cx + 90.0, 96.0);
- cairo_stroke(cr);
- }
- if (parts >= 5) { /* left leg */
- cairo_new_path(cr);
- cairo_move_to(cr, cx + 70.0, 108.0);
- cairo_line_to(cr, cx + 50.0, 132.0);
- cairo_stroke(cr);
- }
- if (parts >= 6) { /* right leg */
- cairo_new_path(cr);
- cairo_move_to(cr, cx + 70.0, 108.0);
- cairo_line_to(cr, cx + 90.0, 132.0);
- cairo_stroke(cr);
- }
-}
-
-static GtkWidget *build_hangman(UnitCtx *unit, int ex_num, const char *title,
- const char *sub) {
- GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- GtkWidget *wrap = gtk_center_box_new();
- GtkWidget *content;
- HangmanCtx *hm = g_new0(HangmanCtx, 1);
- GtkWidget *hint_row;
- GtkWidget *counter;
- GtkWidget *slots;
- GtkWidget *tip_hdr;
- GtkWidget *hint_lbl;
- GtkWidget *letters;
- GtkWidget *head;
- GtkWidget *guess_lbl;
- char *tmp;
-
- hm->unit = unit;
- hm->ex_num = ex_num;
-
- gtk_widget_set_hexpand(page, TRUE);
- gtk_widget_set_vexpand(page, TRUE);
- gtk_widget_set_margin_start(page, 32);
- gtk_widget_set_margin_end(page, 32);
- gtk_widget_set_margin_top(page, 24);
- gtk_widget_set_margin_bottom(page, 24);
- gtk_box_append(GTK_BOX(page), top_bar(unit->page, title, sub));
-
- gtk_widget_set_vexpand(wrap, TRUE);
- gtk_widget_set_hexpand(wrap, TRUE);
- gtk_box_append(GTK_BOX(page), wrap);
-
- content = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
- gtk_widget_set_halign(content, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(content, GTK_ALIGN_CENTER);
- gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), content);
-
- counter = gtk_label_new(NULL);
- hm->counter_lbl = counter;
- gtk_widget_add_css_class(counter, "ex-sub");
- gtk_box_append(GTK_BOX(content), counter);
-
- head = gtk_drawing_area_new();
- gtk_widget_set_size_request(head, 220, 170);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(head), draw_hangman,
- hm, NULL);
- hm->draw = head;
- gtk_box_append(GTK_BOX(content), head);
-
- slots = gtk_label_new(NULL);
- gtk_widget_add_css_class(slots, "hm-word");
- hm->slots = slots;
- gtk_box_append(GTK_BOX(content), slots);
-
- hint_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- gtk_widget_set_halign(hint_row, GTK_ALIGN_CENTER);
- tip_hdr = gtk_label_new(NULL);
- i18n_bind(tip_hdr, "hm_hint", 0);
- gtk_widget_add_css_class(tip_hdr, "hint");
- hint_lbl = gtk_label_new(NULL);
- gtk_widget_add_css_class(hint_lbl, "hint");
- hm->hint_lbl = hint_lbl;
- gtk_box_append(GTK_BOX(hint_row), tip_hdr);
- gtk_box_append(GTK_BOX(hint_row), hint_lbl);
- gtk_box_append(GTK_BOX(content), hint_row);
-
- guess_lbl = gtk_label_new(NULL);
- i18n_bind(guess_lbl, "hm_guess", 0);
- gtk_widget_add_css_class(guess_lbl, "ex-sub");
- gtk_box_append(GTK_BOX(content), guess_lbl);
-
- letters = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
- gtk_box_append(GTK_BOX(content), letters);
-
- hm->letter_btns = g_new0(GtkWidget *, HM_N_LETTERS);
- {
- static const int kb_start[4] = { 0, HM_ROW2, HM_ROW3, HM_ROW4 };
- static const int kb_end[4] = { HM_ROW2, HM_ROW3, HM_ROW4,
- HM_N_LETTERS };
- for (int r = 0; r < 4; r++) {
- GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5);
- gtk_widget_set_halign(row, GTK_ALIGN_CENTER);
- gtk_widget_set_margin_top(row, r == 3 ? 4 : 0);
- for (int i = kb_start[r]; i < kb_end[r]; i++) {
- GtkWidget *b = gtk_button_new_with_label(hm_letters[i]);
- gtk_widget_add_css_class(b, "pill");
- gtk_widget_add_css_class(b, "hm-key");
- g_object_set_data(G_OBJECT(b), "li", GINT_TO_POINTER(i));
- g_signal_connect(b, "clicked", G_CALLBACK(hm_guess_letter), hm);
- gtk_box_append(GTK_BOX(row), b);
- hm->letter_btns[i] = b;
- }
- gtk_box_append(GTK_BOX(letters), row);
- }
- }
-
- hm->feedback = gtk_label_new(NULL);
- gtk_widget_set_halign(hm->feedback, GTK_ALIGN_CENTER);
- gtk_widget_set_margin_top(hm->feedback, 6);
- gtk_box_append(GTK_BOX(content), hm->feedback);
-
- gtk_label_set_text(GTK_LABEL(hint_lbl), tr(hm_tips[0]));
- tmp = g_strdup_printf(tr("hm_progress"), 1, HM_WORDS);
- gtk_label_set_text(GTK_LABEL(counter), tmp);
- g_free(tmp);
- hm_update_slots(hm);
-
- return page;
-}
-
-/* ---- per-exercise builders for unit 2 ------------------------------- */
-
-static GtkWidget *u2ex1(UnitCtx *unit) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, "Verben konjugieren",
- "sub_verben", "check",
- &body, &feedback, &check);
- ComboListCtx *ctx = combo_list_ctx_new(unit, 1, feedback);
- int counter = 0;
-
- ctx->trans_upfront = TRUE;
- ctx->reveal_german = TRUE;
-
- GtkWidget *h1 = gtk_label_new("Peter Fritsch:");
- gtk_widget_set_halign(h1, GTK_ALIGN_START);
- gtk_widget_add_css_class(h1, "ex-sub");
- gtk_box_append(GTK_BOX(body), h1);
- verb_rows_add(ctx, body, &counter, g01_peter, G_N_ELEMENTS(g01_peter),
- g01_pool, G_N_ELEMENTS(g01_pool), g01_peter_mean, FALSE);
-
- GtkWidget *h2 = gtk_label_new("Jana Nová und Pavol Korčák:");
- gtk_widget_set_halign(h2, GTK_ALIGN_START);
- gtk_widget_add_css_class(h2, "ex-sub");
- gtk_widget_set_margin_top(h2, 10);
- gtk_box_append(GTK_BOX(body), h2);
- verb_rows_add(ctx, body, &counter, g01_jana, G_N_ELEMENTS(g01_jana),
- g01_pool, G_N_ELEMENTS(g01_pool), g01_jana_mean, FALSE);
-
- g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
- return page;
-}
-
-static GtkWidget *u2ex2(UnitCtx *unit) {
- return build_choice(unit, "aus oder in", "sub_ausin", 2,
- g02_rows, g02_mean, G_N_ELEMENTS(g02_rows));
-}
-
-static GtkWidget *u2ex3(UnitCtx *unit) {
- return build_choice(unit, "Fragewörter", "sub_wer", 3,
- g03_rows, g03_mean, G_N_ELEMENTS(g03_rows));
-}
-
-static GtkWidget *u2ex4(UnitCtx *unit) {
- return build_verb_ex(unit, 4, "sprechen", "sub_sprich",
- g04_rows, G_N_ELEMENTS(g04_rows),
- g04_pool, G_N_ELEMENTS(g04_pool), g04_mean, FALSE,
- NULL);
-}
-
-static GtkWidget *u2ex5(UnitCtx *unit) {
- return build_typed(unit, 5, "Nationalitäten", "sub_nation",
- g05_rows, G_N_ELEMENTS(g05_rows), g05_bank, FALSE);
-}
-
-static GtkWidget *u2ex6(UnitCtx *unit) {
- return build_typed(unit, 6, "Woher?", "sub_woher",
- g06_rows, G_N_ELEMENTS(g06_rows), NULL, FALSE);
-}
-
-static GtkWidget *u2ex7(UnitCtx *unit) {
- return build_typed(unit, 7, "Länder schreiben", "sub_laender",
- g07_rows, G_N_ELEMENTS(g07_rows), NULL, FALSE);
-}
-
-static GtkWidget *u2ex8(UnitCtx *unit) {
- return build_verb_ex(unit, 8, "Verben einsetzen", "sub_verb2",
- g08_rows, G_N_ELEMENTS(g08_rows),
- g08_pool, G_N_ELEMENTS(g08_pool), g08_mean, FALSE,
- NULL);
-}
-
-static GtkWidget *u2ex9(UnitCtx *unit) {
- return build_free_answer(unit, 9, "Freie Antwort", "sub_free", "tip_ss",
- ex9_free_qs, (int)G_N_ELEMENTS(ex9_free_qs));
-}
-
-static GtkWidget *u2ex10(UnitCtx *unit) {
- return build_verb_ex(unit, 10, "Euro", "sub_euro",
- g10_rows, G_N_ELEMENTS(g10_rows),
- g10_pool, G_N_ELEMENTS(g10_pool), g10_mean, TRUE,
- NULL);
-}
-
-static GtkWidget *u2ex11(UnitCtx *unit) {
- return build_typed(unit, 11, "Wörter suchen", "sub_wortsuchen",
- g11_rows, G_N_ELEMENTS(g11_rows), NULL, FALSE);
-}
-
-static GtkWidget *u2ex12(UnitCtx *unit) {
- return build_choice(unit, "Was ist richtig?", "sub_verb2", 12,
- g12_rows, g12_mean, G_N_ELEMENTS(g12_rows));
-}
-
-static GtkWidget *u2ex13(UnitCtx *unit) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, "Ordne zu", "sub_ordne",
- "check", &body, &feedback, &check);
- ComboListCtx *ctx = combo_list_ctx_new(unit, 13, feedback);
-
- for (guint i = 0; i < G_N_ELEMENTS(g13_rows); i++) {
- const OrdneRow *r = &g13_rows[i];
- GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- GtkWidget *num = gtk_label_new(NULL);
- GtkWidget *mid = gtk_label_new(r->mid);
- GtkWidget *arrow = gtk_label_new("→");
- GtkWidget *fw = make_word_combo(g13_fw_pool,
- G_N_ELEMENTS(g13_fw_pool));
- GtkWidget *ans = make_word_combo(g13_ans_pool,
- G_N_ELEMENTS(g13_ans_pool));
- char *numtxt = g_strdup_printf("%d.", (int)(i + 1));
-
- gtk_widget_set_halign(row, GTK_ALIGN_START);
- gtk_label_set_text(GTK_LABEL(num), numtxt);
- g_free(numtxt);
- gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(num, "ex-prompt");
- gtk_box_append(GTK_BOX(row), num);
-
- gtk_widget_set_valign(fw, GTK_ALIGN_CENTER);
- gtk_widget_set_size_request(fw, 110, -1);
- gtk_box_append(GTK_BOX(row), fw);
- combo_list_add(ctx, fw, r->fw);
-
- gtk_widget_set_valign(mid, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(mid, "ex-prompt");
- gtk_box_append(GTK_BOX(row), mid);
-
- gtk_widget_set_valign(arrow, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(arrow, "ex-prompt");
- gtk_box_append(GTK_BOX(row), arrow);
-
- gtk_widget_set_valign(ans, GTK_ALIGN_CENTER);
- gtk_widget_set_size_request(ans, 190, -1);
- gtk_box_append(GTK_BOX(row), ans);
- combo_list_add(ctx, ans, r->ans);
-
- gtk_box_append(GTK_BOX(body), row);
- combo_list_add_trans(ctx, meaning_add(body, g13_mean[i]));
- }
-
- g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
- return page;
-}
-
-static GtkWidget *u2ex14(UnitCtx *unit) {
- return build_verb_ex(unit, 14, "Lückentext", "sub_luecke",
- g14_rows, G_N_ELEMENTS(g14_rows),
- g14_pool, G_N_ELEMENTS(g14_pool), g14_mean, TRUE,
- g14_gloss);
-}
-
-static GtkWidget *u2ex15(UnitCtx *unit) {
- return build_verb_ex(unit, 15, "Verbinde", "sub_verbinde",
- g15_rows, G_N_ELEMENTS(g15_rows),
- g15_pool, G_N_ELEMENTS(g15_pool), g15_mean, TRUE,
- g15_gloss);
-}
-
-static GtkWidget *u2ex16(UnitCtx *unit) {
- return build_verb_ex(unit, 16, "Zahlen", "sub_zahlpaar",
- g16_rows, G_N_ELEMENTS(g16_rows),
- g16_pool, G_N_ELEMENTS(g16_pool), g16_mean, FALSE,
- NULL);
-}
-
-static GtkWidget *u2ex17(UnitCtx *unit) {
- return build_profile(unit, 17, "Steckbrief", "sub_steckbrief",
- s01_rows, G_N_ELEMENTS(s01_rows));
-}
-
-static GtkWidget *u2ex18(UnitCtx *unit) {
- return build_hangman(unit, 18, "Berufe", "sub_berufe");
-}
-
-static GtkWidget *u2ex19(UnitCtx *unit) {
- return build_typed(unit, 19, "Nationalität", "sub_bistdu",
- s03_rows, G_N_ELEMENTS(s03_rows), NULL, TRUE);
-}
-
-/* ------------------------------------------------------------------ */
-/* Unit 3 exercises ("Bei uns zu Hause") */
-/* ------------------------------------------------------------------ */
-
-/* Sentence fill-ins: a row is made of literal text segments that
- * alternate with one or more blank widgets (dropdowns). `num` is the
- * optional item number shown at the start of the line. */
-typedef struct {
- const char *num;
- const char *s0, *a0, *s1, *a1, *s2, *a2, *s3; /* up to 3 blanks */
- const char *mean; /* Czech meaning */
-} U3Fill;
-
-/* Build the complete German sentence of a fill row (segments spliced
- * with the correct answers) – used as the model shown after Check. */
-static char *u3_fill_sentence(const U3Fill *f) {
- const char *segs[4];
- const char *ans[3];
- GString *s;
- int i, g;
-
- segs[0] = f->s0;
- segs[1] = f->s1;
- segs[2] = f->s2;
- segs[3] = f->s3;
- ans[0] = f->a0;
- ans[1] = f->a1;
- ans[2] = f->a2;
- g = 0;
- while (g < 3 && ans[g])
- g++;
-
- s = g_string_new(NULL);
- for (i = 0; i <= g; i++) {
- if (segs[i] && segs[i][0])
- g_string_append(s, segs[i]);
- if (i < g && ans[i])
- g_string_append(s, ans[i]);
- }
- if (s->len == 0) {
- g_string_free(s, TRUE);
- return NULL;
- }
- return g_string_free(s, FALSE);
-}
-
-/* Append one horizontal fill row to `body`. The answer of blank i sits
- * between text segments s[i] and s[i+1]; empty segments are skipped.
- * `gloss` (may be NULL) is an i18n key for a small grey note shown at
- * the end of the line, e.g. the meaning of the expected possessive. */
-static void u3_fill_row(ComboListCtx *ctx, GtkWidget *body, const U3Fill *f,
- const char **pool, int pool_n, const char *gloss) {
- const char *segs[4];
- const char *ans[3];
- int i, g;
- GtkWidget *line;
-
- segs[0] = f->s0;
- segs[1] = f->s1;
- segs[2] = f->s2;
- segs[3] = f->s3;
- ans[0] = f->a0;
- ans[1] = f->a1;
- ans[2] = f->a2;
- g = 0;
- while (g < 3 && ans[g])
- g++;
-
- line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- gtk_widget_set_halign(line, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), line);
-
- if (f->num && f->num[0]) {
- GtkWidget *nl = gtk_label_new(f->num);
- gtk_widget_set_valign(nl, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(nl, "ex-prompt");
- gtk_box_append(GTK_BOX(line), nl);
- }
- for (i = 0; i <= g; i++) {
- if (segs[i] && segs[i][0]) {
- GtkWidget *lb = gtk_label_new(segs[i]);
- gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(lb, "ex-prompt");
- gtk_box_append(GTK_BOX(line), lb);
- }
- if (i < g) {
- GtkWidget *combo = make_word_combo(pool, pool_n);
- gtk_widget_set_size_request(combo, 96, -1);
- gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(line), combo);
- combo_list_add(ctx, combo, ans[i]);
- }
- }
-
- if (gloss && gloss[0]) {
- GtkWidget *gl = gtk_label_new(NULL);
- i18n_bind(gl, gloss, 0);
- gtk_widget_set_valign(gl, GTK_ALIGN_CENTER);
- gtk_widget_set_margin_start(gl, 8);
- gtk_widget_add_css_class(gl, "meaning");
- gtk_box_append(GTK_BOX(line), gl);
- }
-
- if (f->mean && f->mean[0]) {
- GtkWidget *m = meaning_add(body, f->mean);
- combo_list_add_trans(ctx, m);
- if (ctx->trans_upfront)
- gtk_widget_set_visible(m, TRUE);
- }
-
- if (ctx->reveal_german) {
- char *sentence = u3_fill_sentence(f);
-
- if (sentence && sentence[0]) {
- GtkWidget *w = model_answer_add(body, sentence);
-
- g_array_append_val(ctx->models, w);
- }
- g_free(sentence);
- }
-}
-
-static void u3_sample_line(GtkWidget *body, const char *text) {
- GtkWidget *sl = gtk_label_new(text);
-
- gtk_widget_set_halign(sl, GTK_ALIGN_START);
- gtk_widget_set_margin_top(sl, 2);
- gtk_widget_set_margin_bottom(sl, 4);
- gtk_widget_add_css_class(sl, "hint");
- gtk_label_set_wrap(GTK_LABEL(sl), TRUE);
- gtk_box_append(GTK_BOX(body), sl);
-}
-
-static GtkWidget *u3_build_drop(UnitCtx *unit, int ex_num, const char *title,
- const char *sub, const char *sample,
- const U3Fill *rows, int n,
- const char **pool, int pool_n,
- const char **glosses, gboolean show_german) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
- &body, &feedback, &check);
- ComboListCtx *ctx = combo_list_ctx_new(unit, ex_num, feedback);
-
- ctx->trans_upfront = show_german;
- ctx->reveal_german = show_german;
-
- if (sample && sample[0])
- u3_sample_line(body, sample);
-
- for (int i = 0; i < n; i++)
- u3_fill_row(ctx, body, &rows[i], pool, pool_n,
- glosses ? glosses[i] : NULL);
-
- g_signal_connect(check, "clicked", G_CALLBACK(combo_list_check), ctx);
- return page;
-}
-
-/* ---- ex1: Familienpaare (match the pair) ---------------------------- */
-
-static const char *ex1_u3_pool[] = {
- "die Cousine", "die Tante", "die Oma", "der Bruder",
- "die Freundin", "der Sohn",
-};
-
-static const U3Fill ex1_u3_rows[] = {
- {"1.", "der Cousin", "die Cousine", NULL, NULL, NULL, NULL, NULL,
- "bratranec / sestřenice"},
- {"2.", "der Onkel", "die Tante", NULL, NULL, NULL, NULL, NULL,
- "strýc / teta"},
- {"3.", "der Opa", "die Oma", NULL, NULL, NULL, NULL, NULL,
- "dědeček / babička"},
- {"4.", "die Schwester", "der Bruder", NULL, NULL, NULL, NULL, NULL,
- "sestra / bratr"},
- {"5.", "der Freund", "die Freundin", NULL, NULL, NULL, NULL, NULL,
- "kamarád / kamarádka"},
- {"6.", "die Tochter", "der Sohn", NULL, NULL, NULL, NULL, NULL,
- "dcera / syn"},
-};
-
-static GtkWidget *u3ex1(UnitCtx *unit) {
- return u3_build_drop(unit, 1, "Familienpaare", "sub3_pair",
- "Beispiel: der Vater ↔ die Mutter",
- ex1_u3_rows, G_N_ELEMENTS(ex1_u3_rows),
- ex1_u3_pool, G_N_ELEMENTS(ex1_u3_pool), NULL, FALSE);
-}
-
-/* ---- ex2: Possessivpronomen im Nominativ ----------------------------- */
-
-static const char *ex2_u3_pool[] = {
- "dein", "deine", "mein", "meine", "sein", "seine",
-};
-
-/* The Czech sentence is shown straight away, and pressing "Check" only
- * adds the translations of the words the student picked in the blanks. */
-typedef struct {
- const char *num; /* item number on the first line of the item */
- const char *seg[4]; /* literal text segments, gaps+1 of them */
- const char *ans[3]; /* correct word per blank */
- int gaps;
-} Ex2Row;
-
-typedef struct {
- const char *czech; /* whole-sentence translation, always shown */
- const Ex2Row *rows;
- int nrows;
-} Ex2Item;
-
-static const Ex2Row ex2_u3_rows2[] = {
- {"2.", {"Ist das ", " Mutter? – Nein, das ist nicht ", " Mutter.", NULL},
- {"deine", "meine", NULL}, 2},
- {NULL, {"Das ist ", " Tante.", NULL, NULL}, {"seine", NULL, NULL}, 1},
-};
-
-static const Ex2Row ex2_u3_rows3[] = {
- {"3.", {"Ist das ", " Bruder? – Nein, das ist nicht ", " Bruder.", NULL},
- {"dein", "mein", NULL}, 2},
- {NULL, {"Das ist ", " Cousin.", NULL, NULL}, {"sein", NULL, NULL}, 1},
-};
-
-static const Ex2Row ex2_u3_rows4[] = {
- {"4.",
- {"Sind das ", " Eltern? – Nein, das sind nicht ",
- " Eltern. Das sind die Eltern von Markus.", NULL},
- {"deine", "meine", NULL}, 2},
-};
-
-static const Ex2Item ex2_u3_items[] = {
- {"Je to tvoje matka? – Ne, to není moje matka. To je jeho teta.",
- ex2_u3_rows2, G_N_ELEMENTS(ex2_u3_rows2)},
- {"Je to tvůj bratr? – Ne, to není můj bratr. To je jeho bratranec.",
- ex2_u3_rows3, G_N_ELEMENTS(ex2_u3_rows3)},
- {"Jsou to tvoji rodiče? – Ne, to nejsou moji rodiče. To jsou rodiče "
- "Markuse.",
- ex2_u3_rows4, G_N_ELEMENTS(ex2_u3_rows4)},
-};
-
-typedef struct {
- GArray *combos; /* every blank combo (checking order) */
- GArray *answers;
- GtkWidget **notes; /* one hidden word-translation note per item */
- GArray **item_combos; /* blank combos of each item */
- int n_items;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
-} Ex2Ctx;
-
-static const char *ex2_word_cs(const char *w) {
- if (!w)
- return NULL;
- if (g_strcmp0(w, "dein") == 0) return "tvůj";
- if (g_strcmp0(w, "deine") == 0) return "tvoje";
- if (g_strcmp0(w, "mein") == 0) return "můj";
- if (g_strcmp0(w, "meine") == 0) return "moje";
- if (g_strcmp0(w, "sein") == 0) return "jeho";
- if (g_strcmp0(w, "seine") == 0) return "jeho";
- return NULL;
-}
-
-static void ex2_check(GtkButton *button, gpointer data) {
- Ex2Ctx *ctx = data;
- guint total = ctx->combos->len;
- guint ok = 0;
-
- (void)button;
-
- for (guint i = 0; i < total; i++) {
- GtkComboBox *combo = g_array_index(ctx->combos, GtkComboBox *, i);
- const char *ans = g_array_index(ctx->answers, const char *, i);
- gchar *sel = gtk_combo_box_text_get_active_text(
- GTK_COMBO_BOX_TEXT(combo));
- gboolean good = FALSE;
- if (sel) {
- gchar *t = normalize_answer(sel);
- good = answer_accepts(t, ans);
- g_free(t);
- g_free(sel);
- }
- answer_mark(GTK_WIDGET(combo), good);
- if (good)
- ok++;
- }
-
- for (int it = 0; it < ctx->n_items; it++) {
- GArray *item = ctx->item_combos[it];
- GtkWidget *note = ctx->notes[it];
- GString *s = g_string_new(NULL);
- for (guint k = 0; k < item->len; k++) {
- GtkComboBox *combo = g_array_index(item, GtkComboBox *, k);
- gchar *sel = gtk_combo_box_text_get_active_text(
- GTK_COMBO_BOX_TEXT(combo));
- const char *cs;
- if (sel) {
- cs = ex2_word_cs(sel);
- if (cs) {
- if (s->len > 0)
- g_string_append(s, " · ");
- g_string_append_printf(s, "%s = %s", sel, tr(cs));
- }
- g_free(sel);
- }
- }
- gtk_label_set_text(GTK_LABEL(note), s->str);
- g_string_free(s, TRUE);
- gtk_widget_set_visible(note, TRUE);
- }
-
- if (ok == total) {
- set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
- mark_done(ctx->unit, ctx->ex_num);
- } else {
- set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
- }
-}
-
-static GtkWidget *u3ex2(UnitCtx *unit) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, "mein oder dein", "sub3_poss",
- "check", &body, &feedback, &check);
- Ex2Ctx *ctx = g_new0(Ex2Ctx, 1);
-
- ctx->combos = g_array_new(FALSE, FALSE, sizeof(GtkComboBox *));
- ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
- ctx->n_items = G_N_ELEMENTS(ex2_u3_items);
- ctx->notes = g_new0(GtkWidget *, ctx->n_items);
- ctx->item_combos = g_new0(GArray *, ctx->n_items);
- ctx->unit = unit;
- ctx->ex_num = 2;
- ctx->feedback = feedback;
-
- u3_sample_line(body, "Beispiel: Ist das dein Vater? – Nein, das ist "
- "nicht mein Vater. Das ist sein Onkel.");
-
- for (int it = 0; it < ctx->n_items; it++) {
- const Ex2Item *item = &ex2_u3_items[it];
- GtkWidget *mean;
- GtkWidget *note;
- GArray *item_combos = g_array_new(FALSE, FALSE,
- sizeof(GtkComboBox *));
- ctx->item_combos[it] = item_combos;
-
- for (int r = 0; r < item->nrows; r++) {
- const Ex2Row *row = &item->rows[r];
- GtkWidget *line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
-
- gtk_widget_set_halign(line, GTK_ALIGN_START);
- if (row->num && row->num[0]) {
- GtkWidget *nl = gtk_label_new(row->num);
- gtk_widget_set_valign(nl, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(nl, "ex-prompt");
- gtk_box_append(GTK_BOX(line), nl);
- }
- for (int i = 0; i <= row->gaps; i++) {
- if (row->seg[i] && row->seg[i][0]) {
- GtkWidget *lb = gtk_label_new(row->seg[i]);
- gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(lb, "ex-prompt");
- gtk_box_append(GTK_BOX(line), lb);
- }
- if (i < row->gaps) {
- GtkWidget *combo = make_word_combo(ex2_u3_pool,
- G_N_ELEMENTS(ex2_u3_pool));
- gtk_widget_set_size_request(combo, 96, -1);
- gtk_widget_set_valign(combo, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(line), combo);
- g_array_append_val(ctx->combos, combo);
- g_array_append_val(item_combos, combo);
- g_array_append_val(ctx->answers, row->ans[i]);
- }
- }
- gtk_box_append(GTK_BOX(body), line);
- }
-
- /* Czech sentence is available before checking. */
- mean = meaning_add(body, item->czech);
- gtk_widget_set_visible(mean, TRUE);
-
- /* word translations appear only after pressing "Check". */
- note = gtk_label_new(NULL);
- gtk_widget_set_halign(note, GTK_ALIGN_START);
- gtk_widget_set_margin_bottom(note, 2);
- gtk_widget_add_css_class(note, "meaning");
- gtk_label_set_wrap(GTK_LABEL(note), TRUE);
- gtk_widget_set_visible(note, FALSE);
- gtk_box_append(GTK_BOX(body), note);
- ctx->notes[it] = note;
- }
-
- g_signal_connect(check, "clicked", G_CALLBACK(ex2_check), ctx);
- return page;
-}
-
-/* ---- ex3: Haustiere – ein/kein --------------------------------------- */
-
-static const char *ex3_u3_pool[] = {
- "ein", "eine", "einen", "kein", "keine", "keinen",
-};
-
-static const U3Fill ex3_u3_rows[] = {
- {"2.", "Hast du ", "einen",
- " Hamster? – Nein, ich habe ", "keinen", " Hamster.", NULL, NULL,
- NULL},
- {NULL, "Ich habe ", "eine", " Schildkröte.", NULL, NULL, NULL, NULL,
- "Máš křečka? – Ne, nemám křečka. Mám želvu."},
- {"3.", "Hast du ", "ein",
- " Pferd? – Nein, ich habe ", "kein", " Pferd.", NULL, NULL, NULL},
- {NULL, "Ich habe ", "ein", " Kaninchen.", NULL, NULL, NULL, NULL,
- "Máš koně? – Ne, nemám koně. Mám králíka."},
- {"4.", "Hast du ", "einen",
- " Fisch? – Nein, ich habe ", "keinen", " Fisch.", NULL, NULL, NULL},
- {NULL, "Ich habe ", "einen", " Vogel.", NULL, NULL, NULL, NULL,
- "Máš rybu? – Ne, nemám rybu. Mám ptáka."},
- {"5.", "Hast du ", "ein",
- " Meerschweinchen? – Nein, ich habe ", "kein",
- " Meerschweinchen.", NULL, NULL, NULL},
- {NULL, "Ich habe ", "einen", " Wellensittich.", NULL, NULL, NULL, NULL,
- "Máš morče? – Ne, nemám morče. Mám andulku."},
-};
-
-static GtkWidget *u3ex3(UnitCtx *unit) {
- return u3_build_drop(unit, 3, "ein / kein", "sub3_haustier",
- "Beispiel: Hast du eine Katze? – Nein, ich habe "
- "keine Katze. Ich habe einen Hund.",
- ex3_u3_rows, G_N_ELEMENTS(ex3_u3_rows),
- ex3_u3_pool, G_N_ELEMENTS(ex3_u3_pool), NULL, TRUE);
-}
-
-/* ---- inline letter-gap rows (ex15) ----------------------------------- */
-
-/* One sentence row whose text is split into segments with up to four
- * small text entries (one per missing-letter group) inserted. */
-typedef struct {
- const char *num;
- const char *s0, *a0, *s1, *a1, *s2, *a2, *s3, *a3, *s4;
- const char *mean;
-} U3Let;
-
-typedef struct {
- GArray *entries;
- GArray *answers;
- GArray *trans;
- GArray *models;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
-} U3LetCtx;
-
-static U3LetCtx *u3_let_ctx_new(UnitCtx *unit, int ex_num,
- GtkWidget *feedback) {
- U3LetCtx *ctx = g_new0(U3LetCtx, 1);
- ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
- ctx->trans = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- ctx->models = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- ctx->unit = unit;
- ctx->ex_num = ex_num;
- ctx->feedback = feedback;
- return ctx;
-}
-
-/* Rebuild the full German sentence (letter gaps filled in). */
-static char *u3_let_sentence(const U3Let *row) {
- const char *segs[5];
- const char *ans[4];
- GString *s;
- int i, g;
-
- segs[0] = row->s0;
- segs[1] = row->s1;
- segs[2] = row->s2;
- segs[3] = row->s3;
- segs[4] = row->s4;
- ans[0] = row->a0;
- ans[1] = row->a1;
- ans[2] = row->a2;
- ans[3] = row->a3;
- g = 0;
- while (g < 4 && ans[g])
- g++;
-
- s = g_string_new(NULL);
- for (i = 0; i <= g; i++) {
- if (segs[i] && segs[i][0])
- g_string_append(s, segs[i]);
- if (i < g && ans[i])
- g_string_append(s, ans[i]);
- }
- if (s->len == 0) {
- g_string_free(s, TRUE);
- return NULL;
- }
- return g_string_free(s, FALSE);
-}
-
-static void u3_let_check(GtkButton *button, gpointer data) {
- U3LetCtx *ctx = data;
- guint total = ctx->entries->len;
- guint ok = 0;
-
- (void)button;
-
- for (guint i = 0; i < total; i++) {
- GtkWidget *e = g_array_index(ctx->entries, GtkWidget *, i);
- const char *ans = g_array_index(ctx->answers, const char *, i);
- const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(e));
- gchar *norm = normalize_answer(txt);
- gboolean good = txt && txt[0] && answer_accepts(norm, ans);
- g_free(norm);
- answer_mark(e, good);
- if (good)
- ok++;
- }
-
- for (guint i = 0; i < ctx->trans->len; i++) {
- GtkWidget *lbl = g_array_index(ctx->trans, GtkWidget *, i);
- if (lbl)
- gtk_widget_set_visible(lbl, TRUE);
- }
-
- for (guint i = 0; i < ctx->models->len; i++) {
- GtkWidget *lbl = g_array_index(ctx->models, GtkWidget *, i);
- if (lbl)
- gtk_widget_set_visible(lbl, TRUE);
- }
-
- if (ok == total) {
- set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
- mark_done(ctx->unit, ctx->ex_num);
- } else {
- set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
- }
-}
-
-static void u3_let_row(U3LetCtx *ctx, GtkWidget *body, const U3Let *row) {
- const char *segs[5];
- const char *ans[4];
- int i, g;
- GtkWidget *line;
-
- segs[0] = row->s0;
- segs[1] = row->s1;
- segs[2] = row->s2;
- segs[3] = row->s3;
- segs[4] = row->s4;
- ans[0] = row->a0;
- ans[1] = row->a1;
- ans[2] = row->a2;
- ans[3] = row->a3;
- g = 0;
- while (g < 4 && ans[g])
- g++;
-
- line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
- gtk_widget_set_halign(line, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), line);
-
- if (row->num && row->num[0]) {
- GtkWidget *nl = gtk_label_new(row->num);
- gtk_widget_set_valign(nl, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(nl, "ex-prompt");
- gtk_box_append(GTK_BOX(line), nl);
- }
- for (i = 0; i <= g; i++) {
- if (segs[i] && segs[i][0]) {
- GtkWidget *lb = gtk_label_new(segs[i]);
- gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(lb, "ex-prompt");
- gtk_box_append(GTK_BOX(line), lb);
- }
- if (i < g) {
- GtkWidget *e = gtk_entry_new();
- gtk_editable_set_width_chars(GTK_EDITABLE(e), 4);
- gtk_widget_set_valign(e, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(line), e);
- g_array_append_val(ctx->entries, e);
- g_array_append_val(ctx->answers, ans[i]);
- }
- }
-
- if (row->mean && row->mean[0]) {
- GtkWidget *m = meaning_add(body, row->mean);
- gtk_widget_set_visible(m, TRUE);
- g_array_append_val(ctx->trans, m);
- }
-
- {
- char *sentence = u3_let_sentence(row);
-
- if (sentence && sentence[0]) {
- GtkWidget *w = model_answer_add(body, sentence);
-
- g_array_append_val(ctx->models, w);
- }
- g_free(sentence);
- }
-}
-
-static GtkWidget *u3_build_letters(UnitCtx *unit, int ex_num,
- const char *title, const char *sub,
- const U3Let *rows, int n) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
- &body, &feedback, &check);
- U3LetCtx *ctx = u3_let_ctx_new(unit, ex_num, feedback);
-
- {
- gboolean need = FALSE;
-
- for (int i = 0; i < n && !need; i++) {
- const U3Let *r = &rows[i];
-
- need = text_has_german_umlaut(r->s0) ||
- text_has_german_umlaut(r->s1) ||
- text_has_german_umlaut(r->s2) ||
- text_has_german_umlaut(r->s3) ||
- text_has_german_umlaut(r->s4) ||
- text_has_german_umlaut(r->a0) ||
- text_has_german_umlaut(r->a1) ||
- text_has_german_umlaut(r->a2) ||
- text_has_german_umlaut(r->a3);
- }
- if (need)
- add_umlaut_note(body);
- }
-
- for (int i = 0; i < n; i++)
- u3_let_row(ctx, body, &rows[i]);
-
- g_signal_connect(check, "clicked", G_CALLBACK(u3_let_check), ctx);
- return page;
-}
-
-/* ---- ex4: missing letters in a paragraph ----------------------------- */
-
-/* The Czech sentence is shown straight away; pressing "Check" only adds
- * the translations of the words the student had to complete. */
-typedef struct {
- const char *seg[5]; /* literal text segments (gaps + 1) */
- const char *ans[4]; /* missing letters per gap */
- const char *czech; /* whole-sentence translation, always shown */
- const char *word[4]; /* completed German word per gap */
- const char *gmean[4]; /* Czech meaning of that word */
- int gaps;
-} Ex4Row;
-
-static const Ex4Row ex4_u3_rows[] = {
- {{"Wir hab", "n Fre", "nde in Öster", "ich. Sie wo", "nen in Wien."},
- {"e", "u", "re", "h"},
- "Máme přátele v Rakousku. Bydlí ve Vídni.",
- {"haben", "Freunde", "Österreich", "wohnen"},
- {"mít", "přátelé", "Rakousko", "bydlet"}, 4},
- {{"Sie hei", "en Elfriede und Jiri. Sie haben auch ", "inder.", NULL,
- NULL},
- {"ß", "K", NULL, NULL},
- "Jmenují se Elfriede a Jiří. Mají také děti.",
- {"heißen", "Kinder", NULL, NULL},
- {"jmenovat se", "děti", NULL, NULL}, 2},
- {{"Ihr S", "hn heißt Philipp und ihr", " Tochter heißt Sabine.", NULL,
- NULL},
- {"o", "e", NULL, NULL},
- "Jejich syn se jmenuje Philipp a jejich dcera Sabine.",
- {"Sohn", "ihre", NULL, NULL},
- {"syn", "jejich", NULL, NULL}, 2},
- {{"Die Famili", " re", "st gern.", NULL, NULL},
- {"e", "i", NULL, NULL},
- "Rodina ráda cestuje.",
- {"Familie", "reist", NULL, NULL},
- {"rodina", "cestuje", NULL, NULL}, 2},
- {{"Sie ist o", "t bei uns zu Bes", "ch in Tsche", "ien.", NULL},
- {"f", "u", "ch", NULL},
- "Často je u nás na návštěvě v Česku.",
- {"oft", "Besuch", "Tschechien", NULL},
- {"často", "návštěva", "Česko", NULL}, 3},
- {{"Die Kinder ler", "en auch Tsche", "isch.", NULL, NULL},
- {"n", "ch", NULL, NULL},
- "Děti se učí také česky.",
- {"lernen", "Tschechisch", NULL, NULL},
- {"učit se", "čeština", NULL, NULL}, 2},
-};
-
-typedef struct {
- GArray *entries; /* every small letter entry (checking order) */
- GArray *answers;
- const Ex4Row *rows;
- int n_rows;
- GtkWidget **notes; /* hidden word-translation note per row */
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
-} Ex4LetterCtx;
-
-static void ex4_check(GtkButton *button, gpointer data) {
- Ex4LetterCtx *ctx = data;
- guint total = ctx->entries->len;
- guint ok = 0;
-
- (void)button;
-
- for (guint i = 0; i < total; i++) {
- GtkWidget *e = g_array_index(ctx->entries, GtkWidget *, i);
- const char *ans = g_array_index(ctx->answers, const char *, i);
- const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(e));
- gchar *norm = normalize_answer(txt);
- gboolean good = txt && txt[0] && answer_accepts(norm, ans);
- g_free(norm);
- answer_mark(e, good);
- if (good)
- ok++;
- }
-
- for (int r = 0; r < ctx->n_rows; r++) {
- const Ex4Row *row = &ctx->rows[r];
- GtkWidget *note = ctx->notes[r];
- GString *s = g_string_new(NULL);
- for (int k = 0; k < row->gaps; k++) {
- if (k > 0)
- g_string_append(s, " · ");
- if (row->word[k])
- g_string_append_printf(s, "%s = %s", row->word[k],
- tr(row->gmean[k]));
- }
- gtk_label_set_text(GTK_LABEL(note), s->str);
- g_string_free(s, TRUE);
- gtk_widget_set_visible(note, TRUE);
- }
-
- if (ok == total) {
- set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
- mark_done(ctx->unit, ctx->ex_num);
- } else {
- set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
- }
-}
-
-static GtkWidget *u3ex4(UnitCtx *unit) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, "Lückentext", "sub3_buchst",
- "check", &body, &feedback, &check);
- Ex4LetterCtx *ctx = g_new0(Ex4LetterCtx, 1);
-
- ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- ctx->answers = g_array_new(FALSE, FALSE, sizeof(const char *));
- ctx->rows = ex4_u3_rows;
- ctx->n_rows = G_N_ELEMENTS(ex4_u3_rows);
- ctx->notes = g_new0(GtkWidget *, ctx->n_rows);
- ctx->unit = unit;
- ctx->ex_num = 4;
- ctx->feedback = feedback;
-
- {
- gboolean need = FALSE;
-
- for (int r = 0; r < ctx->n_rows && !need; r++) {
- for (int i = 0; i < ctx->rows[r].gaps; i++) {
- if (text_has_german_umlaut(ctx->rows[r].ans[i]) ||
- text_has_german_umlaut(ctx->rows[r].word[i])) {
- need = TRUE;
- break;
- }
- }
- }
- if (need)
- add_umlaut_note(body);
- }
-
- for (int r = 0; r < ctx->n_rows; r++) {
- const Ex4Row *row = &ctx->rows[r];
- GtkWidget *line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
- GtkWidget *mean;
- GtkWidget *note;
-
- gtk_widget_set_halign(line, GTK_ALIGN_START);
- for (int i = 0; i <= row->gaps; i++) {
- if (row->seg[i] && row->seg[i][0]) {
- GtkWidget *lb = gtk_label_new(row->seg[i]);
- gtk_widget_set_valign(lb, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(lb, "ex-prompt");
- gtk_box_append(GTK_BOX(line), lb);
- }
- if (i < row->gaps) {
- GtkWidget *e = gtk_entry_new();
- gtk_editable_set_width_chars(GTK_EDITABLE(e), 4);
- gtk_widget_set_valign(e, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(line), e);
- g_array_append_val(ctx->entries, e);
- g_array_append_val(ctx->answers, row->ans[i]);
- }
- }
- gtk_box_append(GTK_BOX(body), line);
-
- /* the Czech sentence is available before checking */
- mean = meaning_add(body, row->czech);
- gtk_widget_set_visible(mean, TRUE);
-
- /* gapped-word translations appear only after "Check" */
- note = gtk_label_new(NULL);
- gtk_widget_set_halign(note, GTK_ALIGN_START);
- gtk_widget_set_margin_bottom(note, 2);
- gtk_widget_add_css_class(note, "meaning");
- gtk_label_set_wrap(GTK_LABEL(note), TRUE);
- gtk_widget_set_visible(note, FALSE);
- gtk_box_append(GTK_BOX(body), note);
- ctx->notes[r] = note;
- }
-
- g_signal_connect(check, "clicked", G_CALLBACK(ex4_check), ctx);
- return page;
-}
-
-/* ---- ex5: Marcos Familie (word-bank typing) -------------------------- */
-
-static const char *ex5_u3_bank =
- "Tanten | Job | Vater | Ferien | Zwillinge | Eltern | Kroatien";
-
-static const TypedQ ex5_u3_rows[] = {
- {"Wir haben eine Männer-Wohngemeinschaft: Mein ________ Johann (50), "
- "Ivo (16) und ich.",
- "vater", "Máme mužskou domácnost: Můj otec Johann (50), Ivo (16) a já."},
- {"Ivo und ich sind gleich alt – wir sind nämlich ________.",
- "zwillinge", "Ivo a já jsme stejně staří – jsme totiž dvojčata."},
- {"Jetzt wohnen wir in Hannover, unser Vater hat hier einen neuen "
- "________.",
- "job", "Náš otec tu má novou práci."},
- {"Unsere ________ sind geschieden.",
- "eltern", "Naši rodiče jsou rozvedení."},
- {"Unsere Mutter Maja (50) lebt in Wien, aber sie kommt aus ________.",
- "kroatien", "Naše matka Maja (50) žije ve Vídni, ale pochází z "
- "Chorvatska."},
- {"Wir haben viele Onkel, ________, Cousinen und Cousins in Kroatien.",
- "tanten", "Máme v Chorvatsku hodně strýců, tet, sestřenic a bratranců."},
- {"Wir sehen sie manchmal in den ________.",
- "ferien", "Vídáme je někdy o prázdninách."},
-};
-
-static GtkWidget *u3ex5(UnitCtx *unit) {
- return build_typed(unit, 5, "Marcos Familie", "sub3_marco",
- ex5_u3_rows, G_N_ELEMENTS(ex5_u3_rows),
- ex5_u3_bank, FALSE);
-}
-
-/* ---- ex6: Wortschatz sortieren --------------------------------------- */
-
-static const AssignItem ex6_u3_items[] = {
- {NULL, "die Oma", 0},
- {NULL, "der Bruder", 0},
- {NULL, "der Opa", 0},
- {NULL, "die Tochter", 0},
- {NULL, "die Tante", 0},
- {NULL, "der Hamster", 1},
- {NULL, "das Pferd", 1},
- {NULL, "die Katze", 1},
- {NULL, "der Hund", 1},
- {NULL, "der Wellensittich", 1},
-};
-
-static const char *ex6_u3_groups[] = {"Familienmitglieder", "Haustiere"};
-
-static const char *ex6_u3_meaning[] = {
- "babička", "bratr", "dědeček", "dcera", "teta",
- "křeček", "kůň", "kočka", "pes", "andulka",
-};
-
-static GtkWidget *u3ex6(UnitCtx *unit) {
- return build_assign(unit, "Sortieren", "sub3_sort", 6,
- ex6_u3_items, G_N_ELEMENTS(ex6_u3_items),
- ex6_u3_groups, G_N_ELEMENTS(ex6_u3_groups),
- ex6_u3_meaning);
-}
-
-/* ---- ex7: Possessivpronomen-Tabelle ---------------------------------- */
-
-typedef struct {
- GtkWidget **entries;
- const char **answers;
- int n;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
-} U3TableCtx;
-
-static void u3_table_check(GtkButton *button, gpointer data) {
- U3TableCtx *ctx = data;
- int ok = 0;
-
- (void)button;
-
- for (int i = 0; i < ctx->n; i++) {
- const gchar *txt = gtk_editable_get_text(
- GTK_EDITABLE(ctx->entries[i]));
- gchar *norm = normalize_answer(txt);
- gboolean good = txt && txt[0] &&
- g_strcmp0(norm, ctx->answers[i]) == 0;
- g_free(norm);
- answer_mark(ctx->entries[i], good);
- if (good)
- ok++;
- }
-
- 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"));
- }
-}
-
-static GtkWidget *u3ex7(UnitCtx *unit) {
- static const char *persons[] = {
- "ich", "du", "er", "sie", "es", "wir", "ihr", "sie / Sie",
- };
- static const char *nouns[] = {
- "der Garten", "die Idee", "das Fest", "die Geschwister",
- };
- static const char *ans[4][8] = {
- {"mein", "dein", "sein", "ihr", "sein", "unser", "euer",
- "ihr"},
- {"meine", "deine", "seine", "ihre", "seine", "unsere", "eure",
- "ihre"},
- {"mein", "dein", "sein", "ihr", "sein", "unser", "euer",
- "ihr"},
- {"meine", "deine", "seine", "ihre", "seine", "unsere", "eure",
- "ihre"},
- };
- const int np = G_N_ELEMENTS(persons);
- const int nn = G_N_ELEMENTS(nouns);
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, "Possessivtabelle",
- "sub3_tabelle", "check",
- &body, &feedback, &check);
- GtkWidget *grid;
- GtkWidget *sample_note;
- GtkWidget *bank_row;
- GtkWidget *bank_hdr;
- GtkWidget *bank1;
- GtkWidget *bank2;
- U3TableCtx *ctx = g_new0(U3TableCtx, 1);
-
- ctx->n = nn * np;
- ctx->entries = g_new0(GtkWidget *, ctx->n);
- ctx->answers = g_new0(const char *, ctx->n);
- ctx->unit = unit;
- ctx->ex_num = 7;
- ctx->feedback = feedback;
-
- sample_note = gtk_label_new("Beispiel: mein (ich × der Garten)");
- gtk_widget_set_halign(sample_note, GTK_ALIGN_START);
- gtk_widget_set_margin_top(sample_note, 2);
- gtk_widget_set_margin_bottom(sample_note, 4);
- gtk_widget_add_css_class(sample_note, "hint");
- gtk_box_append(GTK_BOX(body), sample_note);
-
- bank_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- gtk_widget_set_halign(bank_row, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), bank_row);
- bank_hdr = gtk_label_new(NULL);
- i18n_bind(bank_hdr, "wordbank", 0);
- gtk_widget_add_css_class(bank_hdr, "hint");
- gtk_box_append(GTK_BOX(bank_row), bank_hdr);
- bank1 = gtk_label_new("(der Garten / das Fest) "
- "mein · dein · sein · ihr · unser · euer");
- gtk_widget_add_css_class(bank1, "hint");
- gtk_box_append(GTK_BOX(bank_row), bank1);
-
- bank_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
- gtk_widget_set_halign(bank_row, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), bank_row);
- bank2 = gtk_label_new("(die Idee / die Geschwister) "
- "meine · deine · seine · ihre · unsere · eure");
- gtk_widget_add_css_class(bank2, "hint");
- gtk_box_append(GTK_BOX(bank_row), bank2);
-
- grid = gtk_grid_new();
- gtk_grid_set_column_spacing(GTK_GRID(grid), 8);
- gtk_grid_set_row_spacing(GTK_GRID(grid), 8);
- gtk_widget_set_halign(grid, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), grid);
-
- for (int c = 0; c < np; c++) {
- GtkWidget *h = gtk_label_new(persons[c]);
- gtk_widget_set_halign(h, GTK_ALIGN_CENTER);
- gtk_widget_add_css_class(h, "ex-sub");
- gtk_grid_attach(GTK_GRID(grid), h, c + 1, 0, 1, 1);
- }
- for (int r = 0; r < nn; r++) {
- GtkWidget *h = gtk_label_new(nouns[r]);
- gtk_widget_set_halign(h, GTK_ALIGN_START);
- gtk_widget_add_css_class(h, "ex-sub");
- gtk_grid_attach(GTK_GRID(grid), h, 0, r + 1, 1, 1);
- }
- for (int r = 0; r < nn; r++) {
- for (int c = 0; c < np; c++) {
- GtkWidget *e = gtk_entry_new();
- gtk_editable_set_width_chars(GTK_EDITABLE(e), 6);
- gtk_widget_set_halign(e, GTK_ALIGN_CENTER);
- gtk_grid_attach(GTK_GRID(grid), e, c + 1, r + 1, 1, 1);
- ctx->entries[r * np + c] = e;
- ctx->answers[r * np + c] = ans[r][c];
- }
- }
- /* pre-filled sample cell (ich × der Garten) */
- gtk_editable_set_text(GTK_EDITABLE(ctx->entries[0]), "mein");
- gtk_widget_set_sensitive(ctx->entries[0], FALSE);
-
- g_signal_connect(check, "clicked", G_CALLBACK(u3_table_check), ctx);
- return page;
-}
-
-/* ---- ex8: Possessivpronomen im Akkusativ ------------------------------ */
-
-static const char *ex8_u3_pool[] = {
- "mein", "meine", "meinen", "dein", "deine", "deinen",
- "sein", "seine", "seinen", "ihr", "ihre", "ihren",
- "unser", "unsere", "unseren", "euer", "eure", "euren",
-};
-
-static const U3Fill ex8_u3_rows[] = {
- {"2.", "das Handy → Du brauchst ", "dein", " Handy.", NULL, NULL, NULL,
- NULL, "Potřebuješ svůj mobil."},
- {"3.", "das Pferd → Er hat ", "sein", " Pferd.", NULL, NULL, NULL, NULL,
- "Má svého koně."},
- {"4.", "die Party → Sie macht ", "ihre", " Party.", NULL, NULL, NULL,
- NULL, "Dělá svou párty."},
- {"5.", "der Lehrer → Es fragt ", "seinen", " Lehrer.", NULL, NULL, NULL,
- NULL, "Ptá se svého učitele."},
- {"6.", "das Wochenendhaus → Wir besuchen ", "unser", " Wochenendhaus.",
- NULL, NULL, NULL, NULL, "Navštěvujeme náš víkendový dům."},
- {"7.", "der Hund → Ihr seht ", "euren", " Hund.", NULL, NULL, NULL, NULL,
- "Vy vidíte svého psa."},
- {"8.", "die Instrumente → Sie haben ", "ihre", " Instrumente.", NULL,
- NULL, NULL, NULL, "Mají své nástroje."},
-};
-
-static const char *ex8_u3_gloss[] = {
- "u3g_dein", "u3g_sein", "u3g_ihr_sie", "u3g_sein_es",
- "u3g_unser", "u3g_euer", "u3g_ihr_sie_pl",
-};
-
-static GtkWidget *u3ex8(UnitCtx *unit) {
- return u3_build_drop(unit, 8, "Akkusativ", "sub3_akk",
- "Beispiel: die Inliner (ich) → Ich brauche meine "
- "Inliner.",
- ex8_u3_rows, G_N_ELEMENTS(ex8_u3_rows),
- ex8_u3_pool, G_N_ELEMENTS(ex8_u3_pool),
- ex8_u3_gloss, TRUE);
-}
-
-/* ---- ex9: kein / keine / keinen / nicht ------------------------------ */
-
-static const char *ex9_u3_pool[] = {
- "kein", "keine", "keinen", "nicht",
-};
-
-static const U3Fill ex9_u3_rows[] = {
- {"1.", "● Sprichst du Spanisch? ○ Nein, ich spreche ", "kein",
- " Spanisch.", NULL, NULL, NULL, NULL,
- "Mluvíš španělsky? – Ne, nemluvím španělsky."},
- {"2.", "● Hast du einen Bruder? ○ Nein, ich habe ", "keinen",
- " Bruder.", NULL, NULL, NULL, NULL, "Máš bratra? – Ne, nemám bratra."},
- {"3.", "● Gibt es in Opava einen Zoo? ○ Nein, in Opava gibt es ",
- "keinen", " Zoo.", NULL, NULL, NULL, NULL,
- "Je v Opavě zoo? – Ne, v Opavě žádná zoo není."},
- {"4.", "● Gehst du ins Schwimmbad? ○ Nein, ich gehe ", "nicht",
- " ins Schwimmbad.", NULL, NULL, NULL, NULL,
- "Jdeš do bazénu? – Ne, do bazénu nejdu."},
- {"5.", "● Wandert er gern? ○ Nein, er wandert ", "nicht", " gern.",
- NULL, NULL, NULL, NULL, "Rád chodí na túry? – Ne, nerad chodí na túry."},
- {"6.", "● Besuchst du heute ein Schloss? ○ Nein, heute besuche ich ",
- "kein", " Schloss.", NULL, NULL, NULL, NULL,
- "Navštívíš dnes zámek? – Ne, dnes nenavštívím žádný zámek."},
-};
-
-static GtkWidget *u3ex9(UnitCtx *unit) {
- return u3_build_drop(unit, 9, "kein / nicht", "sub3_nicht", NULL,
- ex9_u3_rows, G_N_ELEMENTS(ex9_u3_rows),
- ex9_u3_pool, G_N_ELEMENTS(ex9_u3_pool), NULL, TRUE);
-}
-
-/* ---- ex10: Sätze bauen ------------------------------------------------ */
-
-static const AssemblyItem ex10_u3_items[] = {
- {NULL, {"Sie", "haben", "eine", "Schildkröte", "."}, 5},
- {NULL, {"Was", "macht", "deine", "Freundin", "am", "Wochenende?"}, 6},
- {NULL, {"Mein", "Vater", "ist", "Lehrer", "von", "Beruf."}, 6},
- {NULL, {"Seid", "ihr", "viele", "zu", "Hause?"}, 5},
- {NULL, {"Julia", "stellt", "ihren", "Hund", "Trixi", "vor."}, 6},
-};
-
-static const char *ex10_u3_meaning[] = {
- "Mají želvu.",
- "Co dělá tvoje kamarádka o víkendu?",
- "Můj otec je povoláním učitel.",
- "Je vás doma hodně?",
- "Julie představuje svého psa Trixiho.",
-};
-
-static GtkWidget *u3ex10(UnitCtx *unit) {
- return build_assembly(unit, "Sätze bauen", "sub3_satz", 10,
- ex10_u3_items, ex10_u3_meaning,
- G_N_ELEMENTS(ex10_u3_items));
-}
-
-/* ---- keyword-typed builder (ex11 + ex14) ----------------------------- */
-
-typedef struct {
- const char *prompt;
- const char *answers; /* accepted substrings separated by '|' */
- const char *mean; /* Czech meaning shown under the entry */
- const char *german; /* German sentence(s) revealed after Check */
-} U3Kw;
-
-typedef struct {
- const U3Kw *qs;
- int n;
- UnitCtx *unit;
- int ex_num;
- GtkWidget *feedback;
- GtkWidget **entries;
- GtkWidget **trans;
- GtkWidget **models;
-} U3KwCtx;
-
-static void u3_kw_check(GtkButton *button, gpointer data) {
- U3KwCtx *ctx = data;
- int ok = 0;
-
- (void)button;
-
- for (int i = 0; i < ctx->n; i++) {
- const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(ctx->entries[i]));
- gchar *norm = normalize_answer(txt);
- gboolean good = txt && txt[0];
- if (good) {
- gchar **parts = g_strsplit(ctx->qs[i].answers, "|", -1);
- good = FALSE;
- for (int j = 0; parts[j]; j++) {
- gchar *a = normalize_answer(parts[j]);
- if (a[0] && g_strrstr(norm, a)) {
- good = TRUE;
- g_free(a);
- break;
- }
- g_free(a);
- }
- g_strfreev(parts);
- }
- g_free(norm);
- answer_mark(ctx->entries[i], good);
- if (good)
- ok++;
- }
-
- for (int i = 0; i < ctx->n; i++)
- if (ctx->models[i])
- gtk_widget_set_visible(ctx->models[i], TRUE);
-
- 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"));
- }
-}
-
-static GtkWidget *u3_build_kw(UnitCtx *unit, int ex_num, const char *title,
- const char *sub, const char *sample,
- const U3Kw *qs, int n) {
- GtkWidget *body, *feedback, *check;
- GtkWidget *page = ex_page_shell(unit->page, title, sub, "check",
- &body, &feedback, &check);
- U3KwCtx *ctx = g_new0(U3KwCtx, 1);
-
- ctx->qs = qs;
- ctx->n = n;
- ctx->unit = unit;
- ctx->ex_num = ex_num;
- ctx->feedback = feedback;
- ctx->entries = g_new0(GtkWidget *, n);
- ctx->trans = g_new0(GtkWidget *, n);
- ctx->models = g_new0(GtkWidget *, n);
-
- {
- gboolean need = FALSE;
-
- for (int i = 0; i < n && !need; i++) {
- need = text_has_german_umlaut(qs[i].prompt) ||
- text_has_german_umlaut(qs[i].answers) ||
- text_has_german_umlaut(qs[i].german);
- }
- if (need)
- add_umlaut_note(body);
- }
-
- if (sample && sample[0])
- u3_sample_line(body, sample);
-
- for (int i = 0; i < n; i++) {
- GtkWidget *prompt = gtk_label_new(qs[i].prompt);
- GtkWidget *entry;
-
- gtk_widget_set_halign(prompt, GTK_ALIGN_START);
- gtk_widget_add_css_class(prompt, "ex-prompt");
- gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
- gtk_box_append(GTK_BOX(body), prompt);
-
- entry = gtk_entry_new();
- gtk_widget_set_hexpand(entry, FALSE);
- gtk_widget_set_halign(entry, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(body), entry);
- ctx->entries[i] = entry;
-
- if (qs[i].mean)
- ctx->trans[i] = meaning_add(body, qs[i].mean);
-
- /* Czech meaning acts as a hint and stays visible. */
- if (ctx->trans[i])
- gtk_widget_set_visible(ctx->trans[i], TRUE);
-
- /* The model German sentence is only shown after clicking Check. */
- if (qs[i].german && qs[i].german[0]) {
- GtkWidget *model = gtk_label_new(qs[i].german);
-
- gtk_widget_set_halign(model, GTK_ALIGN_START);
- gtk_label_set_xalign(GTK_LABEL(model), 0.0);
- gtk_label_set_wrap(GTK_LABEL(model), TRUE);
- gtk_widget_set_margin_top(model, 2);
- gtk_widget_set_margin_bottom(model, 2);
- gtk_widget_set_margin_start(model, 8);
- gtk_widget_add_css_class(model, "u3-answer");
- gtk_widget_set_visible(model, FALSE);
- gtk_box_append(GTK_BOX(body), model);
- ctx->models[i] = model;
- }
- }
-
- g_signal_connect(check, "clicked", G_CALLBACK(u3_kw_check), ctx);
- return page;
-}
-
-/* ---- ex11: Was siehst du? -------------------------------------------- */
-
-static const U3Kw ex11_u3_qs[] = {
- {"Ein altes Gebäude aus dem Mittelalter. Es steht auf einem Berg.",
- "burg|schloss", "Vidím hrad / zámek.",
- "Ich sehe eine Burg. / Ich sehe ein Schloss."},
- {"Wasser, Bäume und Berge. Es ist sehr ruhig.",
- "see", "Vidím jezero.", "Ich sehe einen See."},
- {"Eine Frau, ein Mann und zwei Kinder essen zusammen.",
- "familie", "Vidím rodinu.", "Ich sehe eine Familie."},
-};
-
-static GtkWidget *u3ex11(UnitCtx *unit) {
- return u3_build_kw(unit, 11, "Was siehst du?", "sub3_sehen",
- "Beispiel: Ich sehe eine Burg. / Ich sehe ein Schloss.",
- ex11_u3_qs, G_N_ELEMENTS(ex11_u3_qs));
-}
-
-/* ---- ex12: Wem gehört das? ------------------------------------------- */
-
-static const char *ex12_u3_pool[] = {
- "mein", "meine", "dein", "deine", "sein", "seine",
- "ihr", "ihre", "unser", "unsere", "euer", "eure",
-};
-
-static const U3Fill ex12_u3_rows[] = {
- {"1.", "Das ist Tom. → Das ist ", "sein", " Handy.", NULL, NULL, NULL,
- NULL, "To je jeho mobil."},
- {"2.", "Das ist Maria. → Das ist ", "ihr", " Kaninchen.", NULL, NULL,
- NULL, NULL, "To je její králík."},
- {"3.", "Das sind Max und Lisa. → Das sind ", "ihre", " Inliner.", NULL,
- NULL, NULL, NULL, "To jsou jejich brusle."},
- {"4.", "Das ist Frau Berger. → Das ist ", "ihr", " Kuchen.", NULL, NULL,
- NULL, NULL, "To je její dort."},
- {"5.", "Das ist Herr Müller. → Das ist ", "sein", " Haus.", NULL, NULL,
- NULL, NULL, "To je jeho dům."},
- {"6.", "Das sind die Schmidts. → Das sind ", "ihre", " Kinder.", NULL,
- NULL, NULL, NULL, "To jsou jejich děti."},
-};
-
-static GtkWidget *u3ex12(UnitCtx *unit) {
- return u3_build_drop(unit, 12, "Wem gehört das?", "sub3_wem", NULL,
- ex12_u3_rows, G_N_ELEMENTS(ex12_u3_rows),
- ex12_u3_pool, G_N_ELEMENTS(ex12_u3_pool), NULL, TRUE);
-}
-
-/* ---- ex13: Es gibt + unbestimmter Artikel ---------------------------- */
-
-static const char *ex13_u3_pool[] = {"ein", "eine", "einen"};
-
-static const U3Fill ex13_u3_rows[] = {
- {"1.", "(das Konzert) → Es gibt ", "ein", " Konzert.", NULL, NULL, NULL,
- NULL, "Je tu koncert."},
- {"2.", "(die Ausstellung) → Es gibt ", "eine", " Ausstellung.", NULL,
- NULL, NULL, NULL, "Je tu výstava."},
- {"3.", "(das Sportfest) → Es gibt ", "ein", " Sportfest.", NULL, NULL,
- NULL, NULL, "Je tu sportovní slavnost."},
- {"4.", "(das Familientreffen) → Es gibt ", "ein", " Familientreffen.",
- NULL, NULL, NULL, NULL, "Je tu rodinné setkání."},
- {"5.", "(die Theatervorstellung) → Es gibt ", "eine",
- " Theatervorstellung.", NULL, NULL, NULL, NULL,
- "Je tu divadelní představení."},
- {"6.", "(der Flohmarkt) → Es gibt ", "einen", " Flohmarkt.", NULL, NULL,
- NULL, NULL, "Je tu bleší trh."},
-};
-
-static GtkWidget *u3ex13(UnitCtx *unit) {
- return u3_build_drop(unit, 13, "Es gibt …", "sub3_gibt", NULL,
- ex13_u3_rows, G_N_ELEMENTS(ex13_u3_rows),
- ex13_u3_pool, G_N_ELEMENTS(ex13_u3_pool), NULL, TRUE);
-}
-
-/* ---- ex14: Beschreiben (sentence pairs) ------------------------------ */
-
-static const U3Kw ex14_u3_qs[] = {
- {"Eine Frau fährt Fahrrad. → Ich sehe ________.",
- "eine frau|frau", "Vidím ženu.", "Ich sehe eine Frau."},
- {"→ Sie ________.",
- "sie fährt fahrrad|sie fährt|fährt fahrrad|fährt", "Jezdí na kole.",
- "Sie fährt Fahrrad."},
- {"Ein Mann wandert im Wald. → Ich sehe ________.",
- "einen mann|mann", "Vidím muže.", "Ich sehe einen Mann."},
- {"→ Er ________.",
- "er wandert im wald|er wandert|wandert im wald|wandert",
- "Chodí po lese.", "Er wandert im Wald."},
- {"Zwei Kinder spielen Fußball. → Ich sehe ________.",
- "zwei kinder|kinder", "Vidím dvě děti.", "Ich sehe zwei Kinder."},
- {"→ Sie ________.",
- "sie spielen fußball|sie spielen fussball|spielen fußball|spielen "
- "fussball|spielen", "Hrají fotbal.", "Sie spielen Fußball."},
-};
-
-static GtkWidget *u3ex14(UnitCtx *unit) {
- return u3_build_kw(unit, 14, "Beschreiben", "sub3_saetze",
- "Beispiel: Ich sehe einen Jungen. Er liest ein Buch.",
- ex14_u3_qs, G_N_ELEMENTS(ex14_u3_qs));
-}
-
-/* ---- ex15: missing letters (Wochenende) ------------------------------ */
-
-static const U3Let ex15_u3_rows[] = {
- {"1.", "Meine Gr", "oß", "eltern fahr", "en", " ins Wochen", "end",
- "haus.", NULL, NULL,
- "Moji prarodiče jedou o víkendu do víkendového domu."},
- {"2.", "Mein ", "Va", "ter und mein", "e", " Mutter besuch", "en",
- " ein Konz", "er", "t.",
- "Můj otec a moje matka navštíví koncert."},
- {"3.", "Mein", "e", " Sch", "w", "ester geh", "t", " ins K", "in", "o.",
- "Moje sestra jde do kina."},
- {"4.", "Mein Bru", "d", "er f", "äh", "rt an ", "ei", "nen S", "ee",
- ".",
- "Můj bratr jede k jezeru."},
- {"5.", "Und ich? Ich besuch", "e", " ein Einka", "ufs", "zentrum.",
- NULL, NULL, NULL, NULL,
- "A já? Navštívím obchodní centrum."},
-};
-
-static GtkWidget *u3ex15(UnitCtx *unit) {
- return u3_build_letters(unit, 15, "Wochenende", "sub3_wochen",
- ex15_u3_rows, G_N_ELEMENTS(ex15_u3_rows));
-}
-
-/* ------------------------------------------------------------------ */
-/* Settings */
-/* ------------------------------------------------------------------ */
-
-static void draw_theme_swatch(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- ThemeId id = (ThemeId)GPOINTER_TO_INT(data);
- ThemePalette p = theme_palette(id, app_color_mode);
- Rgb base = color_from_hex(p.base);
- Rgb a1 = color_from_hex(p.accent);
- Rgb a2 = color_from_hex(p.accent3);
- Rgb ok = color_from_hex(p.success);
- Rgb accent = color_from_hex(app_theme.accent);
- gboolean selected = (app_theme_id == id);
- double size = MIN(width, height);
- double cx = width / 2.0;
- double cy = height / 2.0;
- double r = size / 2.0 - (selected ? 3.0 : 1.0);
- double dot = MAX(3.5, r * 0.26);
- double gap = dot * 2.05;
-
- (void)area;
-
- if (selected) {
- cairo_new_path(cr);
- cairo_arc(cr, cx, cy, size / 2.0 - 0.5, 0.0, 2.0 * G_PI);
- cairo_set_source_rgb(cr, accent.r, accent.g, accent.b);
- cairo_set_line_width(cr, 2.2);
- cairo_stroke(cr);
- }
-
- cairo_new_path(cr);
- cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
- cairo_set_source_rgb(cr, base.r, base.g, base.b);
- cairo_fill(cr);
-
- cairo_new_path(cr);
- cairo_arc(cr, cx - gap, cy, dot, 0.0, 2.0 * G_PI);
- cairo_set_source_rgb(cr, a1.r, a1.g, a1.b);
- cairo_fill(cr);
-
- cairo_new_path(cr);
- cairo_arc(cr, cx, cy, dot, 0.0, 2.0 * G_PI);
- cairo_set_source_rgb(cr, a2.r, a2.g, a2.b);
- cairo_fill(cr);
-
- cairo_new_path(cr);
- cairo_arc(cr, cx + gap, cy, dot, 0.0, 2.0 * G_PI);
- cairo_set_source_rgb(cr, ok.r, ok.g, ok.b);
- cairo_fill(cr);
-}
-
-static void on_mode_toggled(GtkToggleButton *btn, gpointer user_data) {
- ColorMode mode = (ColorMode)GPOINTER_TO_INT(user_data);
-
- if (!gtk_toggle_button_get_active(btn))
- return;
- if (app_color_mode == mode)
- return;
- app_color_mode = mode;
- apply_theme();
- save_settings();
-}
-
-static void on_theme_toggled(GtkToggleButton *btn, gpointer user_data) {
- ThemeId id = (ThemeId)GPOINTER_TO_INT(user_data);
-
- if (!gtk_toggle_button_get_active(btn))
- return;
- if (app_theme_id == id)
- return;
- app_theme_id = id;
- apply_theme();
- save_settings();
-}
-
-static void on_lang_toggled(GtkToggleButton *btn, gpointer user_data) {
- UiLang lang = (UiLang)GPOINTER_TO_INT(user_data);
-
- if (!gtk_toggle_button_get_active(btn))
- return;
- if (app_lang == lang)
- return;
- app_lang = lang;
- apply_language();
- save_settings();
-}
-
-static GtkWidget *make_lang_chip(const char *label, UiLang lang,
- GtkToggleButton *group) {
- GtkWidget *btn = gtk_toggle_button_new_with_label(label);
-
- gtk_widget_add_css_class(btn, "mode-chip");
- gtk_widget_set_hexpand(btn, TRUE);
- if (group)
- gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
- g_signal_connect(btn, "toggled", G_CALLBACK(on_lang_toggled),
- GINT_TO_POINTER(lang));
- return btn;
-}
-
-static GtkWidget *make_mode_chip(const char *label, ColorMode mode,
- GtkToggleButton *group) {
- GtkWidget *btn = gtk_toggle_button_new();
-
- i18n_bind(btn, label, 1);
- gtk_widget_add_css_class(btn, "mode-chip");
- gtk_widget_set_hexpand(btn, TRUE);
- if (group)
- gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
- g_signal_connect(btn, "toggled", G_CALLBACK(on_mode_toggled),
- GINT_TO_POINTER(mode));
- return btn;
-}
-
-static GtkWidget *make_theme_card(ThemeId id, GtkToggleButton *group) {
- GtkWidget *btn;
- GtkWidget *vbox;
- GtkWidget *swatch;
- GtkWidget *name;
-
- btn = gtk_toggle_button_new();
- gtk_widget_add_css_class(btn, "theme-card");
- gtk_widget_set_hexpand(btn, TRUE);
- if (group)
- gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
-
- vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
- gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
- gtk_button_set_child(GTK_BUTTON(btn), vbox);
-
- swatch = gtk_drawing_area_new();
- gtk_widget_set_size_request(swatch, 44, 44);
- gtk_widget_set_halign(swatch, GTK_ALIGN_CENTER);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(swatch),
- draw_theme_swatch,
- GINT_TO_POINTER(id), NULL);
- theme_swatch_areas[id] = swatch;
- gtk_box_append(GTK_BOX(vbox), swatch);
-
- name = gtk_label_new(theme_names[id]);
- gtk_widget_add_css_class(name, "theme-card-name");
- gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(vbox), name);
-
- g_signal_connect(btn, "toggled", G_CALLBACK(on_theme_toggled),
- GINT_TO_POINTER(id));
- return btn;
-}
-
-static void on_settings_clicked(GtkButton *button, gpointer user_data) {
- GtkPopover *popover = GTK_POPOVER(user_data);
-
- (void)button;
- gtk_popover_popup(popover);
-}
-
-static GtkWidget *build_stats_button(void) {
- GtkWidget *btn;
- GtkWidget *icon;
-
- btn = gtk_button_new();
- gtk_widget_add_css_class(btn, "settings-btn");
- gtk_widget_add_css_class(btn, "flat");
- i18n_bind(btn, "stats", 2);
- gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
- gtk_widget_set_focus_on_click(btn, FALSE);
-
- icon = gtk_drawing_area_new();
- gtk_widget_set_size_request(icon, 18, 18);
- gtk_widget_set_can_target(icon, FALSE);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon),
- draw_stats_icon, NULL, NULL);
- gtk_button_set_child(GTK_BUTTON(btn), icon);
- g_signal_connect_swapped(btn, "state-flags-changed",
- G_CALLBACK(gtk_widget_queue_draw), icon);
- g_signal_connect(btn, "clicked", G_CALLBACK(on_stats_clicked), NULL);
-
- return btn;
-}
-
-static GtkWidget *build_settings_button(void) {
- GtkWidget *btn;
- GtkWidget *icon;
- GtkWidget *popover;
- GtkWidget *box;
- GtkWidget *title;
- GtkWidget *label;
- GtkWidget *mode_row;
- GtkWidget *lang_row;
- GtkWidget *theme_grid;
- GtkWidget *dark_btn;
- GtkWidget *light_btn;
- GtkWidget *cs_btn;
- GtkWidget *en_btn;
- GtkWidget *first_theme = NULL;
- int i;
-
- btn = gtk_button_new();
- gtk_widget_add_css_class(btn, "settings-btn");
- gtk_widget_add_css_class(btn, "flat");
- i18n_bind(btn, "settings", 2);
- gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
- gtk_widget_set_focus_on_click(btn, FALSE);
-
- icon = gtk_drawing_area_new();
- gtk_widget_set_size_request(icon, 18, 18);
- gtk_widget_set_can_target(icon, FALSE);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon),
- draw_settings_icon, NULL, NULL);
- gtk_button_set_child(GTK_BUTTON(btn), icon);
- g_signal_connect_swapped(btn, "state-flags-changed",
- G_CALLBACK(gtk_widget_queue_draw), icon);
-
- popover = gtk_popover_new();
- gtk_widget_add_css_class(popover, "settings-popover");
- gtk_popover_set_has_arrow(GTK_POPOVER(popover), TRUE);
- gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
- gtk_widget_set_parent(popover, btn);
- g_signal_connect_swapped(btn, "destroy",
- G_CALLBACK(gtk_widget_unparent), popover);
- g_signal_connect(btn, "clicked", G_CALLBACK(on_settings_clicked), popover);
-
- box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
- gtk_widget_add_css_class(box, "settings-box");
-
- title = gtk_label_new(NULL);
- i18n_bind(title, "settings_title", 0);
- gtk_widget_set_halign(title, GTK_ALIGN_START);
- gtk_widget_add_css_class(title, "settings-title");
- gtk_box_append(GTK_BOX(box), title);
-
- label = gtk_label_new(NULL);
- i18n_bind(label, "mode", 0);
- gtk_widget_set_halign(label, GTK_ALIGN_START);
- gtk_widget_add_css_class(label, "settings-label");
- gtk_widget_set_margin_top(label, 4);
- gtk_box_append(GTK_BOX(box), label);
-
- mode_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
- gtk_widget_add_css_class(mode_row, "settings-seg");
- gtk_box_append(GTK_BOX(box), mode_row);
-
- dark_btn = make_mode_chip("mode_dark", MODE_DARK, NULL);
- light_btn = make_mode_chip("mode_light", MODE_LIGHT,
- GTK_TOGGLE_BUTTON(dark_btn));
- g_signal_handlers_block_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_mode_toggled, NULL);
- g_signal_handlers_block_matched(light_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_mode_toggled, NULL);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dark_btn),
- app_color_mode == MODE_DARK);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(light_btn),
- app_color_mode == MODE_LIGHT);
- g_signal_handlers_unblock_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_mode_toggled, NULL);
- g_signal_handlers_unblock_matched(light_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_mode_toggled, NULL);
- gtk_box_append(GTK_BOX(mode_row), dark_btn);
- gtk_box_append(GTK_BOX(mode_row), light_btn);
-
- label = gtk_label_new(NULL);
- i18n_bind(label, "theme", 0);
- gtk_widget_set_halign(label, GTK_ALIGN_START);
- gtk_widget_add_css_class(label, "settings-label");
- gtk_widget_set_margin_top(label, 2);
- gtk_box_append(GTK_BOX(box), label);
-
- theme_grid = gtk_flow_box_new();
- gtk_widget_add_css_class(theme_grid, "theme-grid");
- gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(theme_grid),
- GTK_SELECTION_NONE);
- gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
- gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
- gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(theme_grid), 8);
- gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(theme_grid), 8);
- gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(theme_grid), TRUE);
- gtk_widget_set_hexpand(theme_grid, TRUE);
- gtk_box_append(GTK_BOX(box), theme_grid);
-
- for (i = 0; i < THEME_COUNT; i++) {
- GtkWidget *card = make_theme_card(
- (ThemeId)i,
- first_theme ? GTK_TOGGLE_BUTTON(first_theme) : NULL);
- if (!first_theme)
- first_theme = card;
- if (app_theme_id == (ThemeId)i) {
- g_signal_handlers_block_matched(card, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_theme_toggled, NULL);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(card), TRUE);
- g_signal_handlers_unblock_matched(card, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_theme_toggled, NULL);
- }
- gtk_flow_box_insert(GTK_FLOW_BOX(theme_grid), card, -1);
- }
-
- label = gtk_label_new(NULL);
- i18n_bind(label, "language", 0);
- gtk_widget_set_halign(label, GTK_ALIGN_START);
- gtk_widget_add_css_class(label, "settings-label");
- gtk_widget_set_margin_top(label, 4);
- gtk_box_append(GTK_BOX(box), label);
-
- lang_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
- gtk_widget_add_css_class(lang_row, "settings-seg");
- gtk_box_append(GTK_BOX(box), lang_row);
-
- cs_btn = make_lang_chip("Čeština", LANG_CS, NULL);
- en_btn = make_lang_chip("English", LANG_EN, GTK_TOGGLE_BUTTON(cs_btn));
- g_signal_handlers_block_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_lang_toggled, NULL);
- g_signal_handlers_block_matched(en_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_lang_toggled, NULL);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs_btn),
- app_lang == LANG_CS);
- gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(en_btn),
- app_lang == LANG_EN);
- g_signal_handlers_unblock_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_lang_toggled, NULL);
- g_signal_handlers_unblock_matched(en_btn, G_SIGNAL_MATCH_FUNC,
- 0, 0, NULL, on_lang_toggled, NULL);
- gtk_box_append(GTK_BOX(lang_row), cs_btn);
- gtk_box_append(GTK_BOX(lang_row), en_btn);
-
- {
- GtkWidget *sw = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
- GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
- gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(sw), 470);
- gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(sw), FALSE);
- gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(sw), TRUE);
- gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(sw), TRUE);
- gtk_widget_set_hexpand(sw, FALSE);
- gtk_widget_set_vexpand(sw, FALSE);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), box);
- gtk_popover_set_child(GTK_POPOVER(popover), sw);
- }
-
- return btn;
-}
-
-/* ------------------------------------------------------------------ */
-/* Activate */
-/* ------------------------------------------------------------------ */
-
-static void unit_meta_init(void) {
- static const char *titles[NUM_UNITS] = {
- "Neue Freunde", "Aus aller Welt", "Bei uns zu Hause",
- "Schule und Freizeit", "Guten Appetit!", "Mein Tagesablauf",
- "Meine Freunde", "Wir treffen uns in Salzburg",
- "Mein Haus ist meine Burg", "Urlaub in Österreich",
- };
- static const char *pages[NUM_UNITS] = {
- "unit1", "unit2", "unit3", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
- };
- static const char *tags[NUM_UNITS] = {
- "u1", "u2", "u3", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
- };
-
- for (int i = 0; i < NUM_UNITS; i++) {
- units[i].title = titles[i];
- units[i].page = pages[i];
- units[i].ex_tag = tags[i];
- units[i].sub_key = NULL;
- units[i].unlocked = (i < NUM_UNLOCKED);
- }
- units[0].sub_key = "unit1_sub";
- units[0].progress_file = PROGRESS_U1;
- units[1].sub_key = "unit2_sub";
- units[1].progress_file = PROGRESS_U2;
- units[2].sub_key = "unit3_sub";
- units[2].progress_file = PROGRESS_U3;
-}
-
-static void unit_configure(int idx, const char *const *names, int n) {
- UnitCtx *u = &units[idx];
-
- u->n_ex = n;
- for (int i = 1; i <= n; i++)
- u->ex_names[i] = names[i];
-}
-
-static GtkWidget *build_exercise_page(UnitCtx *u, int n) {
- if (u->ex_tag[1] == '1') {
- switch (n) {
- case 1: return build_ex1(u);
- case 2: return build_assembly(u, "Sätze bilden", "sub_assembly", 2,
- ex2_items, ex2_meaning,
- G_N_ELEMENTS(ex2_items));
- case 3: return build_choice(u, "Was ist richtig?", "sub_choice_num",
- 3, ex3_questions, ex3_meaning,
- G_N_ELEMENTS(ex3_questions));
- case 4: return build_ex4(u);
- case 5: return build_ex5(u);
- case 6: return build_ex6(u);
- case 7: return build_ex7(u);
- case 8: return build_ex8(u);
- case 9: return build_choice(u, "Wer? Wie? Wo?", "sub_wer",
- 9, ex9_questions, ex9_meaning,
- G_N_ELEMENTS(ex9_questions));
- case 10: return build_assembly(u, "Wörter trennen", "sub_assembly",
- 10, ex10_items, ex10_meaning,
- G_N_ELEMENTS(ex10_items));
- case 11: return build_assign(u, "Grußformen", "sub_gruss",
- 11, ex11_items,
- G_N_ELEMENTS(ex11_items),
- ex11_groups, G_N_ELEMENTS(ex11_groups),
- ex11_meaning);
- case 12: return build_ex12(u);
- case 13: return build_assign(u, "Länder", "sub_land",
- 13, ex13_items,
- G_N_ELEMENTS(ex13_items),
- ex13_groups, G_N_ELEMENTS(ex13_groups),
- ex13_meaning);
- default: return NULL;
- }
- } else if (u->ex_tag[1] == '2') {
- switch (n) {
- case 1: return u2ex1(u);
- case 2: return u2ex2(u);
- case 3: return u2ex3(u);
- case 4: return u2ex4(u);
- case 5: return u2ex5(u);
- case 6: return u2ex6(u);
- case 7: return u2ex7(u);
- case 8: return u2ex8(u);
- case 9: return u2ex9(u);
- case 10: return u2ex10(u);
- case 11: return u2ex11(u);
- case 12: return u2ex12(u);
- case 13: return u2ex13(u);
- case 14: return u2ex14(u);
- case 15: return u2ex15(u);
- case 16: return u2ex16(u);
- case 17: return u2ex17(u);
- case 18: return u2ex18(u);
- case 19: return u2ex19(u);
- default: return NULL;
- }
- } else {
- switch (n) {
- case 1: return u3ex1(u);
- case 2: return u3ex2(u);
- case 3: return u3ex3(u);
- case 4: return u3ex4(u);
- case 5: return u3ex5(u);
- case 6: return u3ex6(u);
- case 7: return u3ex7(u);
- case 8: return u3ex8(u);
- case 9: return u3ex9(u);
- case 10: return u3ex10(u);
- case 11: return u3ex11(u);
- case 12: return u3ex12(u);
- case 13: return u3ex13(u);
- case 14: return u3ex14(u);
- case 15: return u3ex15(u);
- default: return NULL;
- }
- }
-}
-
-/* ---- Computer networks: subject path, lesson & exercise ------------ */
-
-#define NET_UNITS 30
-#define NET_GRID_COLS 6
-#define NET_SLIDES 4
-
-static GtkWidget *net_slide_stack;
-static GtkWidget *net_prev_btn;
-static GtkWidget *net_next_btn;
-static guint net_slide_idx;
-
-static void net_paragraph(GtkWidget *box, const char *text) {
- GtkWidget *l = gtk_label_new(text);
-
- gtk_widget_set_halign(l, GTK_ALIGN_START);
- gtk_label_set_wrap(GTK_LABEL(l), TRUE);
- gtk_widget_add_css_class(l, "ex-prompt");
- gtk_box_append(GTK_BOX(box), l);
-}
-
-static void net_heading(GtkWidget *box, const char *text) {
- GtkWidget *l = gtk_label_new(text);
-
- gtk_widget_set_halign(l, GTK_ALIGN_START);
- gtk_label_set_wrap(GTK_LABEL(l), TRUE);
- gtk_widget_set_margin_top(l, 6);
- gtk_widget_add_css_class(l, "ex-sub");
- gtk_box_append(GTK_BOX(box), l);
-}
-
-/* Note-card slides with text that scales with the window. */
-static GtkWidget *net_note_host;
-static GArray *net_note_kicks;
-static GArray *net_note_titles;
-static GArray *net_note_bodies;
-static int net_note_last_w = -1;
-
-static void net_note_font(GtkWidget *l, int px) {
- PangoAttrList *attrs = pango_attr_list_new();
-
- pango_attr_list_insert(attrs, pango_attr_size_new(px * PANGO_SCALE));
- gtk_label_set_attributes(GTK_LABEL(l), attrs);
- pango_attr_list_unref(attrs);
-}
-
-static void net_rescale_notes(void) {
- int w = net_note_host ? gtk_widget_get_allocated_width(net_note_host) : 0;
- int body, head, kick;
-
- if (w < 160)
- w = 160;
- body = w / 46;
- if (body < 14) body = 14;
- if (body > 26) body = 26;
- head = body + 14;
- if (head > 46) head = 46;
- kick = body - 3;
- if (kick < 11) kick = 11;
-
- for (guint i = 0; i < net_note_kicks->len; i++) {
- net_note_font(g_array_index(net_note_kicks, GtkWidget *, i), kick);
- net_note_font(g_array_index(net_note_titles, GtkWidget *, i), head);
- net_note_font(g_array_index(net_note_bodies, GtkWidget *, i), body);
- }
-}
-
-static gboolean net_note_tick(GtkWidget *w, GdkFrameClock *clock,
- gpointer data) {
- int width = gtk_widget_get_allocated_width(w);
-
- (void)clock;
- (void)data;
- if (width != net_note_last_w) {
- net_note_last_w = width;
- net_rescale_notes();
- }
- return G_SOURCE_CONTINUE;
-}
-
-static GtkWidget *net_slide_card(const char *cls) {
- GtkWidget *l = gtk_label_new(NULL);
-
- gtk_widget_add_css_class(l, cls);
- gtk_label_set_wrap(GTK_LABEL(l), TRUE);
- gtk_label_set_justify(GTK_LABEL(l), GTK_JUSTIFY_CENTER);
- gtk_widget_set_hexpand(l, TRUE);
- gtk_widget_set_halign(l, GTK_ALIGN_FILL);
- return l;
-}
-
-static void net_note_kicker(GtkWidget *box, const char *text) {
- GtkWidget *l = net_slide_card("notes-kicker");
-
- gtk_label_set_text(GTK_LABEL(l), text);
- gtk_box_append(GTK_BOX(box), l);
- g_array_append_val(net_note_kicks, l);
-}
-
-static void net_note_title(GtkWidget *box, const char *text) {
- GtkWidget *l = net_slide_card("notes-title");
-
- gtk_label_set_text(GTK_LABEL(l), text);
- gtk_box_append(GTK_BOX(box), l);
- g_array_append_val(net_note_titles, l);
-}
-
-static void net_note_line(GtkWidget *box, const char *text, gboolean tip) {
- GtkWidget *l = net_slide_card(tip ? "notes-tip" : "notes-body");
-
- gtk_label_set_text(GTK_LABEL(l), text);
- gtk_widget_set_margin_top(l, 2);
- gtk_box_append(GTK_BOX(box), l);
- g_array_append_val(net_note_bodies, l);
-}
-
-static void net_slide_apply(void) {
- char name[16];
-
- if (!net_slide_stack)
- return;
-
- g_snprintf(name, sizeof(name), "net_slide%u", net_slide_idx);
- gtk_stack_set_visible_child_name(GTK_STACK(net_slide_stack), name);
-
- if (net_prev_btn) {
- gtk_button_set_label(GTK_BUTTON(net_prev_btn), tr("net_slide_prev"));
- gtk_widget_set_sensitive(net_prev_btn, net_slide_idx > 0);
- }
- if (net_next_btn) {
- gtk_button_set_label(
- GTK_BUTTON(net_next_btn),
- net_slide_idx + 1 >= NET_SLIDES ? tr("net_slide_start")
- : tr("net_slide_next"));
- }
-}
-
-static void net_open_unit(GtkButton *button, gpointer data) {
- (void)button;
- (void)data;
- net_slide_idx = 0;
- net_slide_apply();
- gtk_stack_set_visible_child_name(main_stack, "netunit1");
-}
-
-static void net_slide_prev(GtkButton *button, gpointer data) {
- (void)button;
- (void)data;
- if (net_slide_idx > 0) {
- net_slide_idx--;
- net_slide_apply();
- }
-}
-
-static void net_slide_next(GtkButton *button, gpointer data) {
- (void)button;
- (void)data;
- if (net_slide_idx + 1 < NET_SLIDES) {
- net_slide_idx++;
- net_slide_apply();
- } else {
- gtk_stack_set_visible_child_name(main_stack, "netex");
- }
-}
-
-static GtkWidget *net_scroll;
-static GtkWidget *net_fixed;
-static GtkWidget *net_rail;
-static GtkWidget *net_nodes[NET_UNITS];
-static GtkWidget *net_labels[NET_UNITS];
-static double net_cx[NET_UNITS];
-static double net_cy[NET_UNITS];
-static int net_cols = 1;
-static int net_rows = 1;
-static int net_cw = (int)(2.0 * ROAD_MX + (NET_UNITS - 1) * PATH_SPAC);
-static int net_ch = (int)(2.0 * ROAD_MY);
-static guint net_idle;
-static double net_last_avail = -1.0;
-static int net_last_cols = -1;
-static int net_last_rows = -1;
-static int net_last_cw = -1;
-static int net_last_ch = -1;
-static cairo_surface_t *net_rail_cache;
-static int net_cache_w;
-static int net_cache_h;
-
-static void net_point(double t, double *ox, double *oy) {
- int k = (int)t;
- double u = t - (double)k;
- double u2, u3;
- double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
-
- if (k < 0) { k = 0; u = 0.0; }
- if (k >= NET_UNITS - 1) { k = NET_UNITS - 2; u = 1.0; }
-
- p1x = net_cx[k]; p1y = net_cy[k];
- p2x = net_cx[k + 1]; p2y = net_cy[k + 1];
- if (k - 1 >= 0) { p0x = net_cx[k - 1]; p0y = net_cy[k - 1]; }
- else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
- if (k + 2 < NET_UNITS) { p3x = net_cx[k + 2]; p3y = net_cy[k + 2]; }
- else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
-
- u2 = u * u;
- u3 = u2 * u;
- *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
- + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
- + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
- *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
- + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
- + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
-}
-
-static void net_path(cairo_t *cr, double t0, double t1) {
- const double steps_per_unit = 24.0;
- int n = (int)((t1 - t0) * steps_per_unit);
- double x, y;
- int s;
-
- if (n < 1)
- n = 1;
- net_point(t0, &x, &y);
- cairo_move_to(cr, x, y);
- for (s = 1; s <= n; s++) {
- net_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
- cairo_line_to(cr, x, y);
- }
-}
-
-static void net_geometry(double avail) {
- const double phase = 2.0 * G_PI * 1.7 / (double)(NET_UNITS - 1);
- int rows;
- int r, c, i;
-
- for (rows = 1; rows <= NET_UNITS; rows++) {
- int cols = (NET_UNITS + rows - 1) / rows;
- if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0)
- break;
- }
- if (rows > NET_UNITS)
- rows = NET_UNITS;
- net_rows = rows;
- net_cols = (NET_UNITS + rows - 1) / rows;
- if (net_cols < 1)
- net_cols = 1;
- net_cw = (int)(2.0 * ROAD_MX + (net_cols - 1) * PATH_SPAC);
- net_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP);
-
- for (r = 0; r < rows; r++) {
- int base = r * net_cols;
- int len = MIN(net_cols, NET_UNITS - base);
- int fwd = (r % 2) == 0;
- for (c = 0; c < len; c++) {
- i = base + c;
- int cc = fwd ? c : (net_cols - 1 - c);
- net_cx[i] = ROAD_MX + cc * PATH_SPAC;
- net_cy[i] = ROAD_MY + r * ROAD_GAP
- + ROAD_WAVE * sin((double)i * phase);
- }
- }
-}
-
-static void net_apply_layout(void) {
- if (!net_fixed)
- return;
-
- if (net_cols == net_last_cols && net_rows == net_last_rows &&
- net_cw == net_last_cw && net_ch == net_last_ch)
- return;
-
- net_last_cols = net_cols;
- net_last_rows = net_rows;
- net_last_cw = net_cw;
- net_last_ch = net_ch;
- if (net_rail_cache) {
- cairo_surface_destroy(net_rail_cache);
- net_rail_cache = NULL;
- }
-
- for (int i = 0; i < NET_UNITS; i++) {
- if (!net_nodes[i] || !net_labels[i])
- continue;
- gtk_fixed_move(GTK_FIXED(net_fixed), net_nodes[i],
- (int)(net_cx[i] - NODE_SIZE / 2.0),
- (int)(net_cy[i] - NODE_SIZE / 2.0));
- gtk_fixed_move(GTK_FIXED(net_fixed), net_labels[i],
- (int)(net_cx[i] - (PATH_SPAC - 20.0) / 2.0),
- (int)(net_cy[i] + NODE_SIZE / 2.0 + 10.0));
- }
-
- gtk_widget_set_size_request(net_fixed, net_cw, net_ch);
- gtk_widget_set_size_request(net_rail, net_cw, net_ch);
- gtk_fixed_move(GTK_FIXED(net_fixed), net_rail, 0, 0);
- gtk_widget_queue_draw(net_rail);
-}
-
-static void net_relayout(void) {
- GtkAdjustment *hadj;
- double avail;
-
- if (!net_scroll)
- return;
- hadj = gtk_scrolled_window_get_hadjustment(
- GTK_SCROLLED_WINDOW(net_scroll));
- avail = gtk_adjustment_get_page_size(hadj);
- if (avail < 1.0)
- return;
- if (fabs(avail - net_last_avail) < 1.0)
- return;
- net_last_avail = avail;
- net_geometry(avail);
- net_apply_layout();
-}
-
-static gboolean net_relayout_idle(gpointer data) {
- (void)data;
- net_idle = 0;
- net_relayout();
- return G_SOURCE_REMOVE;
-}
-
-static void net_relayout_later(void) {
- if (net_idle == 0)
- net_idle = g_idle_add(net_relayout_idle, NULL);
-}
-
-static void net_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
- gpointer data) {
- (void)adj;
- (void)ps;
- (void)data;
- net_relayout_later();
-}
-
-static void net_render_rail_to(cairo_t *cr) {
- const double t_end = (double)(NET_UNITS - 1);
- const Rgb mauve = color_from_hex(app_theme.accent);
- const Rgb rail = color_from_hex(app_theme.rail);
- const Rgb muted = color_from_hex(app_theme.subtext);
- double hx, hy;
-
- cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
- cairo_paint(cr);
- cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
- cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
- cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
-
- net_point(0.0, &hx, &hy);
- draw_node_halo(cr, hx, hy, NODE_SIZE * 1.05,
- mauve.r, mauve.g, mauve.b, 0.18);
-
- cairo_new_path(cr);
- net_path(cr, 0.0, t_end);
- cairo_set_line_width(cr, 16);
- cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
- cairo_stroke(cr);
-
- cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17);
- cairo_set_dash(cr, (double[]){2.0, 40.0}, 2, 0.0);
- cairo_new_path(cr);
- net_path(cr, 0.0, t_end);
- cairo_stroke(cr);
- cairo_set_dash(cr, NULL, 0, 0.0);
-}
-
-static void net_draw_rail(GtkDrawingArea *area, cairo_t *cr,
- int width, int height, gpointer data) {
- (void)area;
- (void)width;
- (void)height;
- (void)data;
-
- if (!net_rail_cache || net_cache_w != net_cw || net_cache_h != net_ch) {
- cairo_t *rcr;
-
- if (net_rail_cache)
- cairo_surface_destroy(net_rail_cache);
- net_cache_w = net_cw;
- net_cache_h = net_ch;
- if (net_cache_w < 1)
- net_cache_w = 1;
- if (net_cache_h < 1)
- net_cache_h = 1;
- net_rail_cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
- net_cache_w, net_cache_h);
- rcr = cairo_create(net_rail_cache);
- net_render_rail_to(rcr);
- cairo_destroy(rcr);
- }
-
- cairo_set_source_surface(cr, net_rail_cache, 0, 0);
- cairo_paint(cr);
-}
-
-static void net_rail_theme_reset(void) {
- if (net_rail_cache) {
- cairo_surface_destroy(net_rail_cache);
- net_rail_cache = NULL;
- }
- if (net_rail)
- gtk_widget_queue_draw(net_rail);
-}
-
-static void net_add_node(GtkFixed *fixed, int index) {
- int num = index + 1;
- gboolean locked = index != 0;
- GtkWidget *card;
- GtkWidget *vbox;
- GtkWidget *number;
- GtkWidget *icon;
- GtkWidget *name;
- char *text;
-
- card = gtk_button_new();
- gtk_widget_add_css_class(card, "unit-node");
- gtk_widget_set_can_focus(card, FALSE);
- gtk_widget_set_sensitive(card, !locked);
- gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE);
-
- if (locked)
- gtk_widget_add_css_class(card, "locked");
- else
- gtk_widget_add_css_class(card, "current");
-
- vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
- gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
- gtk_button_set_child(GTK_BUTTON(card), vbox);
-
- text = g_strdup_printf("%d", num);
- number = gtk_label_new(text);
- g_free(text);
- gtk_widget_add_css_class(number, "unit-number");
- gtk_box_append(GTK_BOX(vbox), number);
-
- if (locked) {
- icon = icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 16);
- gtk_box_append(GTK_BOX(vbox), icon);
- } else {
- icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18);
- gtk_widget_set_visible(icon, FALSE);
- gtk_box_append(GTK_BOX(vbox), icon);
- g_signal_connect(card, "clicked", G_CALLBACK(net_open_unit), NULL);
- }
-
- net_nodes[index] = card;
- gtk_fixed_put(fixed, card, 0, 0);
-
- name = gtk_label_new(NULL);
- gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1);
- gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
- gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER);
- gtk_label_set_wrap(GTK_LABEL(name), TRUE);
- gtk_widget_add_css_class(name, "unit-name");
- if (index == 0) {
- i18n_bind(name, "net_unit1", 0);
- } else {
- gtk_widget_add_css_class(name, "unit-name-locked");
- }
- net_labels[index] = name;
- gtk_fixed_put(fixed, name, 0, 0);
-}
-
-static GtkWidget *build_netmap_page(void) {
- GtkWidget *page;
- GtkWidget *scroll;
- GtkWidget *wrap;
- GtkWidget *fixed;
- GtkWidget *rail;
- GtkAdjustment *ha;
- GtkAdjustment *va;
-
- net_geometry(1000.0);
-
- page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_set_hexpand(page, TRUE);
- gtk_widget_set_vexpand(page, TRUE);
- gtk_widget_set_margin_start(page, 32);
- gtk_widget_set_margin_end(page, 32);
- gtk_widget_set_margin_top(page, 24);
- gtk_widget_set_margin_bottom(page, 24);
-
- gtk_box_append(GTK_BOX(page),
- top_bar("subjects", "Správa počítačových sítí", "net_sub"));
-
- scroll = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_margin_top(scroll, 14);
- gtk_box_append(GTK_BOX(page), scroll);
- net_scroll = scroll;
-
- wrap = gtk_center_box_new();
- gtk_widget_set_vexpand(wrap, TRUE);
- gtk_widget_set_hexpand(wrap, TRUE);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
-
- fixed = gtk_fixed_new();
- gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
- gtk_widget_set_size_request(fixed, net_cw, net_ch);
- gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
- net_fixed = fixed;
-
- rail = gtk_drawing_area_new();
- gtk_widget_set_size_request(rail, net_cw, net_ch);
- gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), net_draw_rail,
- NULL, NULL);
- gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
- net_rail = rail;
-
- for (int i = 0; i < NET_UNITS; i++)
- net_add_node(GTK_FIXED(fixed), i);
-
- ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
- va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
- g_signal_connect(ha, "notify::page-size",
- G_CALLBACK(net_adjust_notify), NULL);
- g_signal_connect(va, "notify::page-size",
- G_CALLBACK(net_adjust_notify), NULL);
-
- net_relayout_later();
-
- return page;
-}
-
-static GtkWidget *build_net_unit1_page(void) {
- typedef struct {
- const char *kicker;
- const char *title;
- const char *tip;
- const char *line[7];
- } NetSlide;
- static const NetSlide slides[NET_SLIDES] = {
- {
- "1 / 4 • VLSM", "Podsíťování sítí",
- "Tip: prefix /n = počet jedniček v masce. /24 = 255.255.255.0",
- {
- "Cíl: rozdělit jednu IP síť na menší podsítě podle počtu uzlů.",
- "Pamatuj si tři role adres v každém bloku:",
- "síťová = první adresa bloku",
- "broadcast = poslední adresa bloku",
- "uzly = vše mezi nimi (síť + 1 až broadcast − 1)",
- NULL,
- },
- },
- {
- "2 / 4 • Velikosti", "Jak velký blok potřebuji?",
- "Počet bitů pro uzly h = log₂(velikost), prefix = 32 − h",
- {
- "Pro X uzlů potřebuješ blok, do kterého se vejde X + 2 adresy "
- "(síť a broadcast).",
- "Velikost bloku je vždy mocnina dvojky – vezmi nejmenší blok, "
- "který je větší nebo roven X + 2.",
- "60 uzlů → 62 → blok 64 → /26",
- "30 uzlů → 32 → blok 32 → /27",
- "14 uzlů → 16 → blok 16 → /28",
- "7 uzlů → 9 → blok 16 → /28",
- NULL,
- },
- },
- {
- "3 / 4 • Postup", "Jak na to krok za krokem",
- NULL,
- {
- "1. Seřaď podsítě od největší po nejmenší.",
- "2. Pro každou najdi potřebný prefix.",
- "3. Začni na síťové adrese ze zadání.",
- "4. Rozsah podsítě = od síťové adresy po broadcast.",
- "5. Rozsah uzlů = síťová + 1 až broadcast − 1.",
- "6. Posuň se hned za broadcast a pokračuj dál.",
- NULL,
- },
- },
- {
- "4 / 4 • Příklad", "Ukázka: 10.0.0.0/24",
- "A: 60 uzlů, B: 30, C: 14, D: 7",
- {
- "A → 10.0.0.0/26 uzly 10.0.0.1–10.0.0.62 "
- "broadcast 10.0.0.63",
- "B → 10.0.0.64/27 uzly 10.0.0.65–10.0.0.94 "
- "broadcast 10.0.0.95",
- "C → 10.0.0.96/28 uzly 10.0.0.97–10.0.0.110 "
- "broadcast 10.0.0.111",
- "D → 10.0.0.112/28 uzly 10.0.0.113–10.0.0.126 "
- "broadcast 10.0.0.127",
- NULL,
- },
- },
- };
- GtkWidget *page;
- GtkWidget *scroll;
- GtkWidget *nav;
-
- net_note_kicks = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- net_note_titles = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
- net_note_bodies = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
-
- page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_set_margin_start(page, 28);
- gtk_widget_set_margin_end(page, 28);
- gtk_widget_set_margin_top(page, 20);
- gtk_widget_set_margin_bottom(page, 20);
-
- gtk_box_append(GTK_BOX(page),
- top_bar("netmap", "net_unit1", NULL));
-
- scroll = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_margin_top(scroll, 10);
- gtk_box_append(GTK_BOX(page), scroll);
-
- net_slide_stack = gtk_stack_new();
- gtk_stack_set_transition_type(GTK_STACK(net_slide_stack),
- GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll),
- net_slide_stack);
- net_note_host = net_slide_stack;
-
- for (int s = 0; s < NET_SLIDES; s++) {
- GtkWidget *stage = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- GtkWidget *card;
- char name[16];
-
- gtk_widget_set_vexpand(stage, TRUE);
- g_snprintf(name, sizeof(name), "net_slide%d", s);
- gtk_stack_add_named(GTK_STACK(net_slide_stack), stage, name);
-
- card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
- gtk_widget_add_css_class(card, "notes-card");
- gtk_widget_set_valign(card, GTK_ALIGN_CENTER);
- gtk_box_append(GTK_BOX(stage), card);
-
- net_note_kicker(card, slides[s].kicker);
- net_note_title(card, slides[s].title);
-
- for (int i = 0; i < 7 && slides[s].line[i]; i++)
- net_note_line(card, slides[s].line[i], FALSE);
-
- if (slides[s].tip)
- net_note_line(card, slides[s].tip, TRUE);
- }
-
- gtk_widget_add_tick_callback(net_slide_stack, net_note_tick, NULL, NULL);
- net_note_last_w = -1;
- net_rescale_notes();
-
- nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
- gtk_box_set_homogeneous(GTK_BOX(nav), TRUE);
- gtk_widget_set_margin_top(nav, 10);
- gtk_box_append(GTK_BOX(page), nav);
-
- net_prev_btn = gtk_button_new_with_label(tr("net_slide_prev"));
- gtk_widget_add_css_class(net_prev_btn, "btn-primary");
- g_signal_connect(net_prev_btn, "clicked", G_CALLBACK(net_slide_prev), NULL);
- gtk_box_append(GTK_BOX(nav), net_prev_btn);
-
- net_next_btn = gtk_button_new_with_label(tr("net_slide_next"));
- gtk_widget_add_css_class(net_next_btn, "btn-primary");
- g_signal_connect(net_next_btn, "clicked", G_CALLBACK(net_slide_next), NULL);
- gtk_box_append(GTK_BOX(nav), net_next_btn);
-
- net_slide_idx = 0;
- net_slide_apply();
-
- return page;
-}
-
-/* Solutions below are the canonical "largest subnet first, in order A–D"
- * allocation. Scenario 3 does not fit into a /24 as written, which is why
- * its "solution" explains that instead. */
-typedef struct {
- const char *prompt;
- const char *solution;
-} NetTask;
-
-static const NetTask net_tasks[] = {
- {
- "1.) Síť má adresu 10.0.0.0/24. Rozdělte ji do podsítí s následujícími "
- "požadavky:\n\n"
- "Podsíť A: 60 uzlů\nPodsíť B: 30 uzlů\nPodsíť C: 14 uzlů\n"
- "Podsíť D: 7 uzlů",
- "A: 10.0.0.0/26 síť 10.0.0.0 uzly 10.0.0.1–10.0.0.62 "
- "broadcast 10.0.0.63\n"
- "B: 10.0.0.64/27 síť 10.0.0.64 uzly 10.0.0.65–10.0.0.94 "
- "broadcast 10.0.0.95\n"
- "C: 10.0.0.96/28 síť 10.0.0.96 uzly 10.0.0.97–10.0.0.110 "
- "broadcast 10.0.0.111\n"
- "D: 10.0.0.112/28 síť 10.0.0.112 uzly 10.0.0.113–10.0.0.126 "
- "broadcast 10.0.0.127",
- },
- {
- "2.) Síť má adresu 172.16.0.0/16. Rozdělte ji do podsítí s "
- "následujícími požadavky:\n\n"
- "Podsíť A: 500 uzlů\nPodsíť B: 200 uzlů\nPodsíť C: 100 uzlů\n"
- "Podsíť D: 50 uzlů",
- "A: 172.16.0.0/23 síť 172.16.0.0 uzly 172.16.0.1–172.16.1.254 "
- "broadcast 172.16.1.255\n"
- "B: 172.16.2.0/24 síť 172.16.2.0 uzly 172.16.2.1–172.16.2.254 "
- "broadcast 172.16.2.255\n"
- "C: 172.16.3.0/25 síť 172.16.3.0 uzly 172.16.3.1–172.16.3.126 "
- "broadcast 172.16.3.127\n"
- "D: 172.16.3.128/26 síť 172.16.3.128 uzly 172.16.3.129–172.16.3.190 "
- "broadcast 172.16.3.191",
- },
- {
- "3.) Síť má adresu 192.168.10.0/24. Rozdělte ji do podsítí s "
- "následujícími požadavky:\n\n"
- "Podsíť A: 120 uzlů\nPodsíť B: 70 uzlů\nPodsíť C: 35 uzlů\n"
- "Podsíť D: 15 uzlů",
- "Do sítě /24 se tyto požadavky nevejdou: A i B potřebují /25 "
- "(2 × 128 = 256 adres), na C a D už nezbude žádný blok. "
- "Při zachování velikostí by bylo nutné větší síť (např. /23).",
- },
- {
- "4.) Síť má adresu 192.168.2.0/25. Rozdělte ji do podsítí s "
- "následujícími požadavky:\n\n"
- "Podsíť A: 50 uzlů\nPodsíť B: 25 uzlů\nPodsíť C: 12 uzlů\n"
- "Podsíť D: 5 uzlů",
- "A: 192.168.2.0/26 síť 192.168.2.0 uzly 192.168.2.1–192.168.2.62 "
- "broadcast 192.168.2.63\n"
- "B: 192.168.2.64/27 síť 192.168.2.64 "
- "uzly 192.168.2.65–192.168.2.94 broadcast 192.168.2.95\n"
- "C: 192.168.2.96/28 síť 192.168.2.96 "
- "uzly 192.168.2.97–192.168.2.110 broadcast 192.168.2.111\n"
- "D: 192.168.2.112/28 síť 192.168.2.112 "
- "uzly 192.168.2.113–192.168.2.126 broadcast 192.168.2.127",
- },
-};
-
-/* Interactive subnet calculator for the practice sheet: the student
- * chooses a prefix and types the network address, broadcast and the
- * usable host range. */
-typedef struct {
- int pfx;
- const char *net;
- const char *bcast;
- const char *lo;
- const char *hi;
-} NetAns;
-
-static const NetAns net_ans[4][4] = {
- { /* 10.0.0.0/24 */
- {26, "10.0.0.0", "10.0.0.63", "10.0.0.1", "10.0.0.62"},
- {27, "10.0.0.64", "10.0.0.95", "10.0.0.65", "10.0.0.94"},
- {28, "10.0.0.96", "10.0.0.111", "10.0.0.97", "10.0.0.110"},
- {28, "10.0.0.112", "10.0.0.127", "10.0.0.113", "10.0.0.126"},
- },
- { /* 172.16.0.0/16 */
- {23, "172.16.0.0", "172.16.1.255", "172.16.0.1", "172.16.1.254"},
- {24, "172.16.2.0", "172.16.2.255", "172.16.2.1", "172.16.2.254"},
- {25, "172.16.3.0", "172.16.3.127", "172.16.3.1", "172.16.3.126"},
- {26, "172.16.3.128", "172.16.3.191", "172.16.3.129", "172.16.3.190"},
- },
- { /* does not fit into a /24 */
- {-1, NULL, NULL, NULL, NULL},
- {-1, NULL, NULL, NULL, NULL},
- {-1, NULL, NULL, NULL, NULL},
- {-1, NULL, NULL, NULL, NULL},
- },
- { /* 192.168.2.0/25 */
- {26, "192.168.2.0", "192.168.2.63", "192.168.2.1", "192.168.2.62"},
- {27, "192.168.2.64", "192.168.2.95", "192.168.2.65", "192.168.2.94"},
- {28, "192.168.2.96", "192.168.2.111", "192.168.2.97", "192.168.2.110"},
- {28, "192.168.2.112", "192.168.2.127", "192.168.2.113", "192.168.2.126"},
- },
-};
-
-typedef struct {
- GtkWidget *combo[4];
- GtkWidget *net[4];
- GtkWidget *bcast[4];
- GtkWidget *lo[4];
- GtkWidget *hi[4];
- GtkWidget *feed;
- GtkWidget *note;
- int id;
- gboolean infeasible;
-} NetQz;
-
-static GtkWidget *net_qz_combo(void) {
- GtkWidget *c = gtk_combo_box_text_new();
-
- for (int p = 8; p <= 30; p++) {
- char buf[8];
-
- g_snprintf(buf, sizeof(buf), "/%d", p);
- gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(c), NULL, buf);
- }
- gtk_combo_box_set_active(GTK_COMBO_BOX(c), -1);
- return c;
-}
-
-static void net_qz_set_prefix(GtkComboBoxText *c, int pfx) {
- if (pfx >= 8 && pfx <= 30)
- gtk_combo_box_set_active(GTK_COMBO_BOX(c), pfx - 8);
- else
- gtk_combo_box_set_active(GTK_COMBO_BOX(c), -1);
-}
-
-static int net_qz_prefix(GtkComboBoxText *c) {
- gchar *txt = gtk_combo_box_text_get_active_text(c);
- int p = txt ? atoi(txt + (txt[0] == '/' ? 1 : 0)) : -1;
-
- g_free(txt);
- return p;
-}
-
-/* Case- and space-insensitive comparison of typed address answers. */
-static gboolean net_txt_eq(const char *a, const char *b) {
- const char *x, *y;
-
- if (!a || !b)
- return a == b;
- x = a;
- y = b;
- while (*x && *y) {
- while (*x == ' ' || *x == '\t')
- x++;
- while (*y == ' ' || *y == '\t')
- y++;
- if (g_ascii_tolower((guchar)*x) != g_ascii_tolower((guchar)*y))
- return FALSE;
- if (*x)
- x++;
- if (*y)
- y++;
- }
- while (*x == ' ' || *x == '\t')
- x++;
- while (*y == ' ' || *y == '\t')
- y++;
- return *x == '\0' && *y == '\0';
-}
-
-static gboolean net_qz_entry_ok(GtkWidget *entry, const char *want) {
- const gchar *got = gtk_editable_get_text(GTK_EDITABLE(entry));
-
- return got && net_txt_eq(got, want);
-}
-
-static void net_qz_check(GtkButton *button, gpointer data) {
- NetQz *q = data;
- int ok = 0;
-
- (void)button;
-
- if (q->infeasible) {
- set_feedback(q->feed, FALSE,
- "Toto zadání se do sítě /24 nevejde – vyzkoušej "
- "„Ukázat řešení“ a přečti si proč.");
- return;
- }
-
- for (int i = 0; i < 4; i++) {
- const NetAns *a = &net_ans[q->id][i];
- gboolean p_ok = a->pfx > 0 &&
- net_qz_prefix(GTK_COMBO_BOX_TEXT(q->combo[i])) == a->pfx;
- gboolean net_ok = net_qz_entry_ok(q->net[i], a->net);
- gboolean bc_ok = net_qz_entry_ok(q->bcast[i], a->bcast);
- gboolean lo_ok = net_qz_entry_ok(q->lo[i], a->lo);
- gboolean hi_ok = net_qz_entry_ok(q->hi[i], a->hi);
-
- answer_mark(q->combo[i], p_ok);
- answer_mark(q->net[i], net_ok);
- answer_mark(q->bcast[i], bc_ok);
- answer_mark(q->lo[i], lo_ok);
- answer_mark(q->hi[i], hi_ok);
- if (p_ok && net_ok && bc_ok && lo_ok && hi_ok)
- ok++;
- }
-
- if (ok == 4)
- set_feedback(q->feed, TRUE, tr("feedback_ok"));
- else
- set_feedback(q->feed, FALSE, tr("feedback_retry"));
-}
-
-static void net_qz_fill(GtkButton *button, gpointer data) {
- NetQz *q = data;
-
- (void)button;
- for (int i = 0; i < 4; i++) {
- const NetAns *a = &net_ans[q->id][i];
-
- if (a->pfx <= 0)
- continue;
- net_qz_set_prefix(GTK_COMBO_BOX_TEXT(q->combo[i]), a->pfx);
- gtk_editable_set_text(GTK_EDITABLE(q->net[i]), a->net);
- gtk_editable_set_text(GTK_EDITABLE(q->bcast[i]), a->bcast);
- gtk_editable_set_text(GTK_EDITABLE(q->lo[i]), a->lo);
- gtk_editable_set_text(GTK_EDITABLE(q->hi[i]), a->hi);
- }
- if (q->note)
- gtk_widget_set_visible(q->note, TRUE);
-}
-
-static void net_qz_header(GtkWidget *box, const char *text) {
- GtkWidget *l = gtk_label_new(text);
-
- gtk_widget_add_css_class(l, "meaning");
- gtk_widget_set_halign(l, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(box), l);
-}
-
-static GtkWidget *net_qz_make_entry(void) {
- GtkWidget *e = gtk_entry_new();
-
- gtk_widget_set_hexpand(e, TRUE);
- gtk_editable_set_width_chars(GTK_EDITABLE(e), 15);
- return e;
-}
-
-static GtkWidget *build_net_exercise_page(void) {
- GtkWidget *page;
- GtkWidget *scroll;
- GtkWidget *body;
-
- page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
- gtk_widget_set_margin_start(page, 32);
- gtk_widget_set_margin_end(page, 32);
- gtk_widget_set_margin_top(page, 24);
- gtk_widget_set_margin_bottom(page, 24);
-
- gtk_box_append(GTK_BOX(page),
- top_bar("netunit1", "net_ex_title", NULL));
-
- scroll = gtk_scrolled_window_new();
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
- gtk_widget_set_vexpand(scroll, TRUE);
- gtk_widget_set_margin_top(scroll, 12);
- gtk_box_append(GTK_BOX(page), scroll);
-
- body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
- gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
-
- net_heading(body,
- "Zadání pro výpočet IP adres – k procvičení");
- net_paragraph(body,
- "Určete pro každou podsíť:\n"
- "• Maska podsítě (prefix)\n"
- "• Rozsah IP adres podsítě\n"
- "• Rozsah IP adres pro uzly\n"
- "• Síťovou adresu\n"
- "• Broadcast adresu");
-
- for (guint t = 0; t < G_N_ELEMENTS(net_tasks); t++) {
- GtkWidget *task = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
- GtkWidget *table;
- GtkWidget *btns;
- GtkWidget *reveal;
- GtkWidget *check;
- GtkWidget *sol;
- NetQz *q = g_new0(NetQz, 1);
-
- q->infeasible = (t == 2);
- q->id = (int)t;
- gtk_widget_set_margin_top(task, 10);
- gtk_box_append(GTK_BOX(body), task);
-
- net_paragraph(task, net_tasks[t].prompt);
-
- if (q->infeasible) {
- net_qz_header(task, net_tasks[t].solution);
- } else {
- static const char *cols[] = {"Prefix", "Síťová adresa",
- "Broadcast", "Uzly od", "Uzly do"};
- table = gtk_grid_new();
- gtk_grid_set_row_spacing(GTK_GRID(table), 6);
- gtk_grid_set_column_spacing(GTK_GRID(table), 8);
- gtk_widget_set_margin_top(table, 6);
- gtk_box_append(GTK_BOX(task), table);
-
- for (guint c = 1; c < 6; c++) {
- GtkWidget *h = gtk_label_new(cols[c - 1]);
- gtk_widget_add_css_class(h, "meaning");
- gtk_grid_attach(GTK_GRID(table), h, (int)c, 0, 1, 1);
- }
-
- for (int i = 0; i < 4; i++) {
- char letter[8];
- GtkWidget *lab = gtk_label_new(NULL);
- int row = i + 1;
-
- g_snprintf(letter, sizeof(letter), "%c", 'A' + i);
- gtk_label_set_text(GTK_LABEL(lab), letter);
- gtk_widget_add_css_class(lab, "ex-prompt");
- gtk_grid_attach(GTK_GRID(table), lab, 0, row, 1, 1);
-
- q->combo[i] = net_qz_combo();
- gtk_grid_attach(GTK_GRID(table), q->combo[i], 1, row, 1, 1);
- q->net[i] = net_qz_make_entry();
- gtk_grid_attach(GTK_GRID(table), q->net[i], 2, row, 1, 1);
- q->bcast[i] = net_qz_make_entry();
- gtk_grid_attach(GTK_GRID(table), q->bcast[i], 3, row, 1, 1);
- q->lo[i] = net_qz_make_entry();
- gtk_grid_attach(GTK_GRID(table), q->lo[i], 4, row, 1, 1);
- q->hi[i] = net_qz_make_entry();
- gtk_grid_attach(GTK_GRID(table), q->hi[i], 5, row, 1, 1);
- }
- }
-
- btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
- gtk_box_append(GTK_BOX(task), btns);
-
- if (!q->infeasible) {
- check = gtk_button_new();
- i18n_bind(check, "check", 1);
- gtk_widget_add_css_class(check, "pill");
- gtk_widget_set_halign(check, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(btns), check);
- g_signal_connect(check, "clicked", G_CALLBACK(net_qz_check), q);
- }
-
- reveal = gtk_button_new();
- i18n_bind(reveal, "net_solution", 1);
- gtk_widget_add_css_class(reveal, "pill");
- gtk_widget_set_halign(reveal, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(btns), reveal);
- g_signal_connect(reveal, "clicked", G_CALLBACK(net_qz_fill), q);
-
- q->feed = gtk_label_new("");
- gtk_widget_set_halign(q->feed, GTK_ALIGN_START);
- gtk_box_append(GTK_BOX(task), q->feed);
-
- sol = gtk_label_new(net_tasks[t].solution);
- gtk_widget_set_halign(sol, GTK_ALIGN_START);
- gtk_label_set_xalign(GTK_LABEL(sol), 0.0);
- gtk_label_set_wrap(GTK_LABEL(sol), TRUE);
- gtk_widget_set_margin_start(sol, 8);
- gtk_widget_add_css_class(sol, "meaning");
- gtk_widget_set_visible(sol, FALSE);
- gtk_box_append(GTK_BOX(task), sol);
- q->note = sol;
- }
-
- return page;
-}
-
-static void activate(GtkApplication *app, gpointer user_data) {
- GtkWindow *window;
- GtkWidget *headerbar;
- GtkWidget *title_label;
- GtkWidget *welcome_page;
- GtkWidget *roadmap_page;
- GtkEventController *keys;
-
- (void)user_data;
-
- load_settings();
- unit_meta_init();
- unit_configure(0, u1_ex_names, 13);
- unit_configure(1, u2_ex_names, 19);
- unit_configure(2, u3_ex_names, 15);
- load_progress();
- app_theme = theme_palette(app_theme_id, app_color_mode);
-
- window = GTK_WINDOW(gtk_application_window_new(app));
- main_window = window;
- gtk_window_set_title(window, "maturita.c");
- gtk_window_set_default_size(window, 760, 560);
-
- headerbar = gtk_header_bar_new();
- gtk_widget_add_css_class(headerbar, "titlebar");
- title_label = gtk_label_new("maturita.c");
- gtk_widget_add_css_class(title_label, "app-title");
- gtk_header_bar_set_title_widget(GTK_HEADER_BAR(headerbar), title_label);
- gtk_window_set_titlebar(window, headerbar);
- apply_theme();
- gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_settings_button());
- gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_stats_button());
-
- main_stack = GTK_STACK(gtk_stack_new());
- gtk_stack_set_transition_type(GTK_STACK(main_stack),
- GTK_STACK_TRANSITION_TYPE_CROSSFADE);
- gtk_window_set_child(window, GTK_WIDGET(main_stack));
-
- welcome_page = build_welcome_page();
- roadmap_page = build_roadmap_page();
- gtk_stack_add_named(main_stack, welcome_page, "welcome");
- gtk_stack_add_named(main_stack, build_subjects_page(), "subjects");
- gtk_stack_add_named(main_stack, roadmap_page, "roadmap");
- gtk_stack_add_named(main_stack, build_stats_page(), "stats");
- gtk_stack_add_named(main_stack, build_netmap_page(), "netmap");
- gtk_stack_add_named(main_stack, build_net_unit1_page(), "netunit1");
- gtk_stack_add_named(main_stack, build_net_exercise_page(), "netex");
-
- for (int i = 0; i < NUM_UNLOCKED; i++) {
- UnitCtx *u = &units[i];
- GtkWidget *map = build_unit_page(u);
-
- if (map)
- gtk_stack_add_named(main_stack, map, u->page);
- for (int n = 1; n <= u->n_ex; n++) {
- GtkWidget *page = build_exercise_page(u, n);
- char name[16];
-
- if (!page)
- continue;
- g_snprintf(name, sizeof(name), "%se%d", u->ex_tag, n);
- gtk_stack_add_named(main_stack, page, name);
- }
- }
-
- gtk_stack_set_visible_child_name(main_stack, "welcome");
-
- refresh_completion_ui();
-
- keys = gtk_event_controller_key_new();
- gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(keys),
- GTK_PHASE_CAPTURE);
- g_signal_connect(keys, "key-pressed",
- G_CALLBACK(on_window_key_pressed), window);
- gtk_widget_add_controller(GTK_WIDGET(window), GTK_EVENT_CONTROLLER(keys));
-
- apply_theme();
- apply_language();
-
- gtk_window_present(window);
-}
-
-int main(int argc, char **argv) {
- GtkApplication *app;
- int status;
-
- app = gtk_application_new("org.example.maturita", G_APPLICATION_DEFAULT_FLAGS);
- g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
- status = g_application_run(G_APPLICATION(app), argc, argv);
- g_object_unref(app);
-
- return status;
-}
diff --git a/maturita.h b/maturita.h
new file mode 100644
index 0000000..996f7a6
--- /dev/null
+++ b/maturita.h
@@ -0,0 +1,985 @@
+#ifndef MATURITA_H
+#define MATURITA_H
+
+#include
+#include
+#include
+#include
+#include
+
+#define NUM_UNITS 10
+#define NUM_UNLOCKED 3 /* first NUM_UNLOCKED units (index 0..2) are open */
+#define MAX_UNIT_EX 20 /* largest exercise count any unit may have */
+#define NUM_SUBJECTS 13
+#define NET_SUBJ 1 /* index of "Správa počítačových sítí" */
+#define CSS_FILE "style.css"
+#define PROGRESS_DIR "progress"
+#define PROGRESS_U1 "progress/unit1.conf"
+#define PROGRESS_U2 "progress/unit2.conf"
+#define PROGRESS_U3 "progress/unit3.conf"
+#define SETTINGS_FILE "progress/settings.conf"
+#define NODE_SIZE 88.0
+#define PATH_SPAC 240.0
+#define ROAD_MX 150.0 /* horizontal canvas margin */
+#define ROAD_MY 150.0 /* vertical canvas margin */
+#define ROAD_GAP 240.0 /* vertical space between folded rows */
+#define ROAD_WAVE 44.0 /* wavy vertical offset of the nodes */
+#define SUB_BUBBLE NODE_SIZE /* round node, matches .unit-node */
+#define SUB_MX 110.0
+#define SUB_MY 150.0
+#define SUB_SPAC 220.0
+#define SUB_GAP 210.0
+#define SUB_WAVE 34.0
+#define EX_BUBBLE 64.0 /* exercise bubble size (matches .ex-bubble) */
+#define EX_MX 80.0 /* horizontal canvas margin */
+#define EX_MY 120.0 /* vertical canvas margin */
+#define EX_SPAC 150.0 /* horizontal distance between bubbles */
+#define EX_GAP 200.0 /* vertical space between folded rows */
+#define EX_WAVE 26.0 /* wavy vertical offset of the bubbles */
+#define HM_WORDS 5
+#define HM_MAX_MISSES 6
+#define HM_N_LETTERS 29 /* QWERTY rows + Ä/Ö/Ü (see hm_letters[]) */
+#define HM_ROW2 10 /* start of the ASDF row */
+#define HM_ROW3 19 /* start of the ZXCV row */
+#define HM_ROW4 26 /* start of the umlaut row */
+#define NET_UNITS 30
+#define NET_GRID_COLS 6
+#define NET_SLIDES 4
+
+typedef enum {
+ THEME_CATPPUCCIN = 0,
+ THEME_NORD,
+ THEME_DRACULA,
+ THEME_ROSE_PINE,
+ THEME_OCEAN,
+ THEME_GRUVBOX,
+ THEME_SOLARIZED,
+ THEME_EVERFOREST,
+ THEME_MONOKAI,
+ THEME_ONE_DARK,
+ THEME_COUNT
+} ThemeId;
+typedef enum {
+ MODE_DARK = 0,
+ MODE_LIGHT
+} ColorMode;
+typedef enum {
+ LANG_CS = 0,
+ LANG_EN
+} UiLang;
+typedef struct {
+ unsigned crust;
+ unsigned base;
+ unsigned mantle;
+ unsigned surface0;
+ unsigned surface1;
+ unsigned surface2;
+ unsigned overlay;
+ unsigned text;
+ unsigned subtext;
+ unsigned accent;
+ unsigned accent2;
+ unsigned accent3;
+ unsigned success;
+ unsigned success2;
+ unsigned warning;
+ unsigned error;
+ unsigned on_accent;
+ unsigned node;
+ unsigned locked_bg;
+ unsigned locked_border;
+ unsigned rail;
+ unsigned finish_dark;
+ unsigned finish_light;
+ unsigned finish_stroke;
+} ThemePalette;
+typedef struct {
+ const char *title; /* German unit title */
+ const char *page; /* roadmap-stack child name, e.g. "unit1" */
+ const char *ex_tag; /* exercise page prefix, e.g. "u1" */
+ const char *sub_key; /* i18n key for the exercise-map subtitle */
+ const char *progress_file;
+ gboolean unlocked;
+ gboolean done[MAX_UNIT_EX + 1]; /* exercise state, 1..n_ex */
+ const char *ex_names[MAX_UNIT_EX + 1]; /* bubble labels, 1..n_ex */
+ int n_ex;
+ GtkWidget *node; /* roadmap node button */
+ GtkWidget *node_done_icon;
+ /* exercise-map page widgets + serpentine geometry */
+ GtkWidget *ex_scroll;
+ GtkWidget *ex_fixed;
+ GtkWidget *ex_rail;
+ GtkWidget *ex_cells[MAX_UNIT_EX]; /* 0..n_ex-1 */
+ GtkWidget *ex_labels[MAX_UNIT_EX]; /* 0..n_ex-1 */
+ GtkWidget *ex_icons[MAX_UNIT_EX + 1]; /* per-bubble check icon 1..n */
+ double ex_cx[MAX_UNIT_EX];
+ double ex_cy[MAX_UNIT_EX];
+ int ex_rows, ex_cols, ex_cw, ex_ch;
+ guint ex_idle;
+} UnitCtx;
+typedef struct { double r, g, b; } Rgb;
+typedef struct {
+ GtkWidget *widget;
+ const char *key;
+ int kind; /* 0 label, 1 button, 2 tooltip, 3 placeholder, 4 ex4 sample */
+} I18nBind;
+typedef struct {
+ const char *key;
+ const char *cs;
+ const char *en;
+} TrEntry;
+typedef struct {
+ GtkWidget *ex_value;
+ GtkWidget *pct_value;
+ GtkWidget *units_value;
+ GtkWidget *overall_bar;
+
+ /* one progress block per subject (index matches sub_keys[]) */
+ struct {
+ GtkWidget *name; /* subject heading label */
+ GtkWidget *count; /* "13 / 47" or the locked text */
+ GtkWidget *bar; /* per-subject overall progress bar */
+ GtkWidget *units_box; /* holds the unit rows, NULL if no units */
+ } subj[NUM_SUBJECTS];
+
+ /* per-unit rows, indexed by the global units[] index */
+ GtkWidget *unit_count[NUM_UNITS];
+ GtkWidget *unit_badge[NUM_UNITS];
+ GtkWidget *unit_bar[NUM_UNITS];
+ GtkWidget *unit_row[NUM_UNITS];
+} StatsUi;
+typedef struct {
+ int done_ex;
+ int total_ex;
+ int done_units;
+ int open_units;
+} ProgressSum;
+typedef struct {
+ GArray *combos; /* GtkComboBox* */
+ GArray *answers; /* const char* */
+ GArray *trans; /* GtkWidget* hidden meaning labels */
+ GArray *models; /* GtkWidget* hidden German answer labels */
+ gboolean trans_upfront; /* meanings shown before Check */
+ gboolean reveal_german; /* German answer shown on Check */
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+} ComboListCtx;
+typedef struct {
+ const char *prompt; /* optional label shown above the sentence */
+ const char *words[6];
+ int n;
+} AssemblyItem;
+typedef struct {
+ GtkWidget *pool;
+ GtkWidget *target;
+ GtkWidget *group;
+ GtkWidget *stage; /* transparent overlay layer for the fly sprite */
+ GtkWidget *trans; /* hidden meaning label under the group */
+ GtkWidget *chips[6];
+ int pool_order[6]; /* fixed shuffled order of the word pool */
+ int placed[6];
+ int placed_count;
+ int n_words;
+ gboolean flying; /* a click-move animation is in progress */
+} SentBuilder;
+typedef struct {
+ SentBuilder *sb;
+ int idx;
+} ChipRef;
+typedef struct {
+ GtkWidget *chip; /* real chip, kept invisible in its final slot */
+ GtkWidget *ghost; /* sprite flying from start to finish */
+ GtkWidget *stage;
+ gboolean *flying; /* pointer to the owner's "animating" flag */
+ double sx, sy; /* start position (stage coords) */
+ double tx, ty; /* target position (stage coords) */
+ guint wait; /* ticks waited for relayout */
+ gint64 t0; /* < 0 while still measuring the target */
+} FlyCtx;
+typedef struct {
+ SentBuilder **sbs;
+ int n;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+} AsmCtx;
+typedef struct {
+ const char *speaker;
+ const char *before;
+ const char *answer;
+ const char *after;
+} DialogRow;
+typedef struct {
+ const char *name;
+ const DialogRow *rows;
+ int n;
+} Dialogue;
+typedef struct {
+ const char *prompt;
+ const char *options[5];
+ int n_options;
+ int correct;
+} ChoiceQ;
+typedef struct {
+ const ChoiceQ *qs;
+ int n;
+ int n_opts;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+ GtkToggleButton **toggles;
+ GtkWidget **trans; /* hidden meaning labels, one per question */
+} ChoiceCtx;
+typedef struct {
+ const char *question;
+ const char *q_cs; /* Czech translation of the question */
+ const char *sample;
+ const char *a_cs; /* Czech translation of the sample answer */
+} FreeQ;
+typedef struct {
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+ GtkWidget **samples;
+ int n;
+} Ex4Ctx;
+typedef struct {
+ const char *digits;
+ const char *answer;
+} NumberQ;
+typedef struct {
+ const char *emoji;
+ int count;
+ const char *noun;
+ const char *answer;
+} CountQ;
+typedef struct {
+ const char *before;
+ const char *after;
+ const char *answer;
+} SeqQ;
+typedef struct {
+ const char *before;
+ const char *after;
+ const char *answer;
+} VerbQ;
+typedef struct {
+ const char *emoji;
+ const char *before;
+ const char *after;
+ const char *answer;
+} VerbClueQ;
+typedef struct {
+ const char *emoji;
+ const char *label;
+ int correct_group;
+} AssignItem;
+typedef struct {
+ GtkWidget *pool;
+ GtkWidget *group_flow[3];
+ GtkWidget *group_btn[3];
+ int n_groups;
+ int active_group;
+ GtkWidget **items; /* per-item boxes (chip + hidden meaning) */
+ GtkWidget **trans; /* hidden meaning labels */
+ GtkWidget *stage; /* transparent overlay layer for the fly */
+ gboolean flying; /* a click-move animation is in progress */
+ int n_items;
+ int *current_group;
+ const AssignItem *items_data;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+} AssignCtx;
+typedef struct {
+ AssignCtx *ac;
+ int idx;
+} AssignChipRef;
+typedef struct {
+ ComboListCtx *ctx;
+ GtkWidget *note;
+ const VerbQ *qs;
+ int n;
+ const char **gloss; /* per-row word-level meaning keys (current lang) */
+} VerbNoteCtx;
+typedef struct {
+ const char *prompt; /* German prompt text */
+ const char *answers; /* accepted answers separated by '|' */
+ const char *meaning; /* i18n key of the revealed meaning or NULL */
+} TypedQ;
+typedef struct {
+ const TypedQ *qs;
+ int n;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+ GtkWidget **entries;
+ GtkWidget **trans;
+} TypedCtx;
+typedef struct {
+ TypedCtx *ctx;
+ GtkWidget *note;
+ const TypedQ *qs;
+ int n;
+} TypedNoteCtx;
+typedef struct {
+ const char *mid; /* middle part of the question, without the FW */
+ const char *fw; /* correct Fragewort */
+ const char *ans; /* correct answer */
+} OrdneRow;
+typedef struct {
+ const char *stem; /* German sentence beginning with an ellipsis */
+ const char *sample; /* finished German sample line */
+} ProfileQ;
+typedef struct {
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+ GtkWidget **samples;
+ int n;
+} ProfileCtx;
+typedef struct {
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+ GtkWidget *draw;
+ GtkWidget *slots;
+ GtkWidget *hint_lbl;
+ GtkWidget *counter_lbl;
+ GtkWidget **letter_btns;
+ gboolean guessed[HM_N_LETTERS];
+ gboolean locked; /* true during the "word solved" pause */
+ gboolean finished;
+ int misses;
+ int word_index;
+} HangmanCtx;
+typedef struct {
+ const char *num;
+ const char *s0, *a0, *s1, *a1, *s2, *a2, *s3; /* up to 3 blanks */
+ const char *mean; /* Czech meaning */
+} U3Fill;
+typedef struct {
+ const char *num; /* item number on the first line of the item */
+ const char *seg[4]; /* literal text segments, gaps+1 of them */
+ const char *ans[3]; /* correct word per blank */
+ int gaps;
+} Ex2Row;
+typedef struct {
+ const char *czech; /* whole-sentence translation, always shown */
+ const Ex2Row *rows;
+ int nrows;
+} Ex2Item;
+typedef struct {
+ GArray *combos; /* every blank combo (checking order) */
+ GArray *answers;
+ GtkWidget **notes; /* one hidden word-translation note per item */
+ GArray **item_combos; /* blank combos of each item */
+ int n_items;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+} Ex2Ctx;
+typedef struct {
+ const char *num;
+ const char *s0, *a0, *s1, *a1, *s2, *a2, *s3, *a3, *s4;
+ const char *mean;
+} U3Let;
+typedef struct {
+ GArray *entries;
+ GArray *answers;
+ GArray *trans;
+ GArray *models;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+} U3LetCtx;
+typedef struct {
+ const char *seg[5]; /* literal text segments (gaps + 1) */
+ const char *ans[4]; /* missing letters per gap */
+ const char *czech; /* whole-sentence translation, always shown */
+ const char *word[4]; /* completed German word per gap */
+ const char *gmean[4]; /* Czech meaning of that word */
+ int gaps;
+} Ex4Row;
+typedef struct {
+ GArray *entries; /* every small letter entry (checking order) */
+ GArray *answers;
+ const Ex4Row *rows;
+ int n_rows;
+ GtkWidget **notes; /* hidden word-translation note per row */
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+} Ex4LetterCtx;
+typedef struct {
+ GtkWidget **entries;
+ const char **answers;
+ int n;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+} U3TableCtx;
+typedef struct {
+ const char *prompt;
+ const char *answers; /* accepted substrings separated by '|' */
+ const char *mean; /* Czech meaning shown under the entry */
+ const char *german; /* German sentence(s) revealed after Check */
+} U3Kw;
+typedef struct {
+ const U3Kw *qs;
+ int n;
+ UnitCtx *unit;
+ int ex_num;
+ GtkWidget *feedback;
+ GtkWidget **entries;
+ GtkWidget **trans;
+ GtkWidget **models;
+} U3KwCtx;
+typedef struct {
+ const char *prompt;
+ const char *solution;
+} NetTask;
+typedef struct {
+ int pfx;
+ const char *net;
+ const char *bcast;
+ const char *lo;
+ const char *hi;
+} NetAns;
+typedef struct {
+ GtkWidget *combo[4];
+ GtkWidget *net[4];
+ GtkWidget *bcast[4];
+ GtkWidget *lo[4];
+ GtkWidget *hi[4];
+ GtkWidget *feed;
+ GtkWidget *note;
+ int id;
+ gboolean infeasible;
+} NetQz;
+
+extern GtkStack *main_stack;
+extern GtkWindow *main_window;
+extern GtkWidget *welcome_heading;
+extern GtkCssProvider *theme_provider;
+extern ThemeId app_theme_id;
+extern ColorMode app_color_mode;
+extern UiLang app_lang;
+extern ThemePalette app_theme;
+extern const char *theme_names[THEME_COUNT];
+extern GtkWidget *theme_swatch_areas[THEME_COUNT];
+extern UnitCtx units[NUM_UNITS];
+extern const char *sub_keys[NUM_SUBJECTS];
+extern const int sub_unit_start[NUM_SUBJECTS];
+extern const int sub_unit_count[NUM_SUBJECTS];
+extern const char *const u1_ex_names[MAX_UNIT_EX + 1];
+extern const char *const u2_ex_names[MAX_UNIT_EX + 1];
+extern const char *const u3_ex_names[MAX_UNIT_EX + 1];
+extern GtkWidget *road_scroll;
+extern GtkWidget *road_fixed;
+extern GtkWidget *road_rail;
+extern GtkWidget *road_nodes[NUM_UNITS];
+extern GtkWidget *road_labels[NUM_UNITS];
+extern double road_cx[NUM_UNITS];
+extern double road_cy[NUM_UNITS];
+extern int road_cols;
+extern int road_rows;
+extern int road_cw;
+extern int road_ch;
+extern GArray *i18n_binds;
+extern const TrEntry tr_ui[];
+extern const TrEntry tr_content[];
+extern StatsUi stats_ui;
+extern char *stats_return_page;
+extern guint road_idle;
+extern GtkWidget *sub_scroll;
+extern GtkWidget *sub_fixed;
+extern GtkWidget *sub_rail;
+extern GtkWidget *sub_cells[NUM_SUBJECTS];
+extern GtkWidget *sub_labels[NUM_SUBJECTS];
+extern double sub_cx[NUM_SUBJECTS];
+extern double sub_cy[NUM_SUBJECTS];
+extern int sub_rows;
+extern int sub_cols;
+extern int sub_cw;
+extern int sub_ch;
+extern guint sub_idle;
+extern const DialogRow ex1_d1[];
+extern const DialogRow ex1_d2[];
+extern const DialogRow ex1_d3[];
+extern const Dialogue ex1_dialogues[];
+extern const char *ex1_pool[];
+extern const char *ex1_meaning[];
+extern const AssemblyItem ex2_items[];
+extern const char *ex2_meaning[];
+extern const ChoiceQ ex3_questions[];
+extern const char *ex3_meaning[];
+extern const ChoiceQ ex9_questions[];
+extern const char *ex9_meaning[];
+extern const FreeQ ex4_questions[];
+extern const NumberQ ex5_data[];
+extern const char *ex5_pool[];
+extern const char *ex5_meaning[];
+extern const CountQ ex6_data[];
+extern const char *ex6_pool[];
+extern const char *ex6_meaning[];
+extern const SeqQ ex7_data[];
+extern const char *ex7_pool[];
+extern const char *ex7_meaning[];
+extern const VerbQ ex8_data[];
+extern const char *ex8_pool[];
+extern const char *ex8_meaning[];
+extern const AssemblyItem ex10_items[];
+extern const char *ex10_meaning[];
+extern const VerbClueQ ex12_data[];
+extern const char *ex12_pool[];
+extern const char *ex12_meaning[];
+extern const AssignItem ex11_items[];
+extern const AssignItem ex13_items[];
+extern const char *ex11_groups[];
+extern const char *ex13_groups[];
+extern const char *ex11_meaning[];
+extern const char *ex13_meaning[];
+extern const char *g05_bank;
+extern const TypedQ g05_rows[];
+extern const TypedQ g06_rows[];
+extern const TypedQ g07_rows[];
+extern const TypedQ g11_rows[];
+extern const TypedQ s03_rows[];
+extern const VerbQ g01_peter[];
+extern const VerbQ g01_jana[];
+extern const char *g01_pool[];
+extern const char *g01_peter_mean[];
+extern const char *g01_jana_mean[];
+extern const VerbQ g04_rows[];
+extern const char *g04_pool[];
+extern const char *g04_mean[];
+extern const VerbQ g08_rows[];
+extern const char *g08_pool[];
+extern const char *g08_mean[];
+extern const VerbQ g10_rows[];
+extern const char *g10_pool[];
+extern const char *g10_mean[];
+extern const VerbQ g14_rows[];
+extern const char *g14_pool[];
+extern const char *g14_mean[];
+extern const char *g14_gloss[];
+extern const VerbQ g15_rows[];
+extern const char *g15_pool[];
+extern const char *g15_mean[];
+extern const char *g15_gloss[];
+extern const VerbQ g16_rows[];
+extern const char *g16_pool[];
+extern const char *g16_mean[];
+extern const ChoiceQ g02_rows[];
+extern const char *g02_mean[];
+extern const ChoiceQ g03_rows[];
+extern const char *g03_mean[];
+extern const ChoiceQ g12_rows[];
+extern const char *g12_mean[];
+extern const OrdneRow g13_rows[];
+extern const char *g13_fw_pool[];
+extern const char *g13_ans_pool[];
+extern const char *g13_mean[];
+extern const FreeQ ex9_free_qs[];
+extern const ProfileQ s01_rows[];
+extern const char *hm_words[HM_WORDS];
+extern const char *hm_tips[HM_WORDS];
+extern const char *hm_letters[];
+extern const gunichar hm_letters_u[];
+extern const char *ex1_u3_pool[];
+extern const U3Fill ex1_u3_rows[];
+extern const char *ex2_u3_pool[];
+extern const Ex2Row ex2_u3_rows2[];
+extern const Ex2Row ex2_u3_rows3[];
+extern const Ex2Row ex2_u3_rows4[];
+extern const Ex2Item ex2_u3_items[];
+extern const char *ex3_u3_pool[];
+extern const U3Fill ex3_u3_rows[];
+extern const Ex4Row ex4_u3_rows[];
+extern const char *ex5_u3_bank;
+extern const TypedQ ex5_u3_rows[];
+extern const AssignItem ex6_u3_items[];
+extern const char *ex6_u3_groups[];
+extern const char *ex6_u3_meaning[];
+extern const char *ex8_u3_pool[];
+extern const U3Fill ex8_u3_rows[];
+extern const char *ex8_u3_gloss[];
+extern const char *ex9_u3_pool[];
+extern const U3Fill ex9_u3_rows[];
+extern const AssemblyItem ex10_u3_items[];
+extern const char *ex10_u3_meaning[];
+extern const U3Kw ex11_u3_qs[];
+extern const char *ex12_u3_pool[];
+extern const U3Fill ex12_u3_rows[];
+extern const char *ex13_u3_pool[];
+extern const U3Fill ex13_u3_rows[];
+extern const U3Kw ex14_u3_qs[];
+extern const U3Let ex15_u3_rows[];
+extern GtkWidget *net_slide_stack;
+extern GtkWidget *net_prev_btn;
+extern GtkWidget *net_next_btn;
+extern guint net_slide_idx;
+extern GtkWidget *net_note_host;
+extern GArray *net_note_kicks;
+extern GArray *net_note_titles;
+extern GArray *net_note_bodies;
+extern int net_note_last_w;
+extern GtkWidget *net_scroll;
+extern GtkWidget *net_fixed;
+extern GtkWidget *net_rail;
+extern GtkWidget *net_nodes[NET_UNITS];
+extern GtkWidget *net_labels[NET_UNITS];
+extern double net_cx[NET_UNITS];
+extern double net_cy[NET_UNITS];
+extern int net_cols;
+extern int net_rows;
+extern int net_cw;
+extern int net_ch;
+extern guint net_idle;
+extern double net_last_avail;
+extern int net_last_cols;
+extern int net_last_rows;
+extern int net_last_cw;
+extern int net_last_ch;
+extern cairo_surface_t *net_rail_cache;
+extern int net_cache_w;
+extern int net_cache_h;
+extern const NetTask net_tasks[];
+extern const NetAns net_ans[4][4];
+
+Rgb color_from_hex(unsigned int hex);
+Rgb mix_rgb(Rgb a, Rgb b, double t);
+ThemePalette theme_palette(ThemeId id, ColorMode mode);
+void ex4_fill_sample(GtkWidget *label);
+const char *tr(const char *key);
+void i18n_ensure(void);
+void i18n_apply_one(const I18nBind *b);
+void i18n_bind(GtkWidget *widget, const char *key, int kind);
+void refresh_welcome_heading(void);
+void refresh_stats_ui(void);
+void net_slide_apply(void);
+void net_rail_theme_reset(void);
+void apply_language(void);
+char *read_css_file(void);
+char *build_theme_css(const ThemePalette *p);
+void save_settings(void);
+void load_settings(void);
+void force_restyle_tree(GtkWidget *w);
+void apply_theme(void);
+void draw_check_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void lock_paint(cairo_t *cr, double x0, double y0, double size,
+ const Rgb *c);
+void draw_lock_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void draw_wifi_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void draw_back_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void draw_settings_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
+ double r, double g, double b, int px);
+char *normalize_answer(const char *input);
+void shuffle_indices(int *arr, int n);
+void flow_clear(GtkFlowBox *fb);
+void answer_mark(GtkWidget *widget, gboolean ok);
+void set_feedback(GtkWidget *label, gboolean ok, const char *text);
+gboolean text_has_german_umlaut(const char *s);
+void add_umlaut_note(GtkWidget *body);
+void unit_save_progress(UnitCtx *u);
+void unit_load_progress(UnitCtx *u);
+void load_progress(void);
+void all_rails_redraw(void);
+void refresh_completion_ui(void);
+void mark_done(UnitCtx *u, int n);
+GtkWidget *make_back_button(const char *target);
+void on_nav_clicked(GtkButton *button, gpointer user_data);
+int unit_done_count(const UnitCtx *u);
+void progress_for_units(int first, int n, ProgressSum *out);
+void refresh_stats_ui(void);
+void on_stats_back_clicked(GtkButton *button, gpointer user_data);
+void on_stats_clicked(GtkButton *button, gpointer user_data);
+GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out);
+GtkWidget *make_unit_stats_row(UnitCtx *u, int num, int global);
+GtkWidget *build_stats_page(void);
+void on_nav_clicked(GtkButton *button, gpointer user_data);
+void on_continue_clicked(GtkButton *button, gpointer user_data);
+GtkWidget *make_back_button(const char *target);
+GtkWidget *top_bar(const char *back_target, const char *title,
+ const char *subtitle);
+gboolean on_window_key_pressed(GtkEventControllerKey *controller,
+ guint keyval, guint keycode,
+ GdkModifierType state,
+ gpointer user_data);
+GtkWidget *build_welcome_page(void);
+void draw_node_halo(cairo_t *cr, double cx, double cy,
+ double radius, double r, double g, double b,
+ double alpha);
+void draw_finish_cell(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void road_point(double t, double *ox, double *oy);
+void road_path(cairo_t *cr, double t0, double t1);
+void roadmap_geometry(double avail);
+void road_apply_layout(void);
+void road_relayout(void);
+gboolean road_relayout_idle(gpointer data);
+void road_relayout_later(void);
+void road_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data);
+void draw_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer user_data);
+void add_path_node(GtkFixed *fixed, int index);
+GtkWidget *build_roadmap_page(void);
+void sub_layout_geometry(double avail);
+void sub_point(double t, double *ox, double *oy);
+void sub_path(cairo_t *cr, double t0, double t1);
+void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer user_data);
+void sub_apply_layout(void);
+void sub_relayout(void);
+gboolean sub_relayout_idle(gpointer data);
+void sub_relayout_later(void);
+void sub_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data);
+void draw_german_flag(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+GtkWidget *build_subjects_page(void);
+void ex_layout_geometry(UnitCtx *u, double avail);
+void ex_point(UnitCtx *u, double t, double *ox, double *oy);
+void ex_path(UnitCtx *u, cairo_t *cr, double t0, double t1);
+void draw_ex_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer user_data);
+void ex_apply_layout(UnitCtx *u);
+void ex_relayout(UnitCtx *u);
+gboolean ex_relayout_idle(gpointer data);
+void ex_relayout_later(UnitCtx *u);
+void ex_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data);
+GtkWidget *make_bubble(UnitCtx *u, int n);
+GtkWidget *build_unit_page(UnitCtx *u);
+GtkWidget *ex_page_shell(const char *back_target, const char *title,
+ const char *subtitle, const char *btn_label,
+ GtkWidget **body_out, GtkWidget **feedback_out,
+ GtkWidget **btn_out);
+GtkWidget *meaning_add(GtkWidget *body, const char *text);
+GtkWidget *model_answer_add(GtkWidget *body, const char *german);
+void meaning_reveal_all(GtkWidget **labels, int n);
+ComboListCtx *combo_list_ctx_new(UnitCtx *unit, int ex_num,
+ GtkWidget *feedback);
+void combo_list_add(ComboListCtx *ctx, GtkWidget *combo, const char *ans);
+void combo_list_add_trans(ComboListCtx *ctx, GtkWidget *label);
+GtkWidget *make_word_combo(const char **words, int n);
+gboolean answer_accepts(const char *sel_norm, const char *ans);
+void combo_list_check(GtkButton *button, gpointer data);
+void sent_rebuild(SentBuilder *sb);
+GdkContentProvider *asm_drag_prepare(GtkDragSource *source,
+ double x, double y,
+ gpointer data);
+gboolean asm_rebuild_idle(gpointer data);
+void asm_drag_end(GtkDragSource *source, GdkDrag *drag,
+ gboolean delete_data, gpointer data);
+gboolean asm_drop_to_sentence(GtkDropTarget *target, const GValue *value,
+ double x, double y, gpointer data);
+gboolean asm_drop_to_pool(GtkDropTarget *target, const GValue *value,
+ double x, double y, gpointer data);
+void asm_finish_fly(FlyCtx *fc);
+gboolean asm_fly_tick(gpointer data);
+void asm_begin_fly(GtkWidget *chip, GtkWidget *stage,
+ gboolean *flying, double sx, double sy);
+void asm_move_by_click(GtkButton *button, gpointer data);
+void assembly_check(GtkButton *button, gpointer data);
+GtkWidget *build_assembly(UnitCtx *unit, const char *title,
+ const char *subtitle, int ex_num,
+ const AssemblyItem *items,
+ const char **meanings, int n);
+GtkWidget *build_ex1(UnitCtx *unit);
+void choice_check(GtkButton *button, gpointer data);
+GtkWidget *build_choice(UnitCtx *unit, const char *title,
+ const char *subtitle, int ex_num,
+ const ChoiceQ *qs, const char **meanings, int n);
+void ex4_fill_sample(GtkWidget *label);
+void ex4_reveal(GtkButton *button, gpointer data);
+void ex4_finish(GtkButton *button, gpointer data);
+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 *build_ex4(UnitCtx *unit);
+GtkWidget *build_ex5(UnitCtx *unit);
+GtkWidget *build_ex6(UnitCtx *unit);
+GtkWidget *build_ex7(UnitCtx *unit);
+char *verbq_german(const char *before, const char *answer,
+ const char *after);
+GtkWidget *build_ex8(UnitCtx *unit);
+GtkWidget *build_ex12(UnitCtx *unit);
+void assign_rebuild(AssignCtx *ac);
+void assign_chip_clicked(GtkButton *button, gpointer data);
+void assign_group_toggled(GtkToggleButton *button, gpointer data);
+void assign_check(GtkButton *button, gpointer data);
+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);
+void verb_rows_add(ComboListCtx *ctx, GtkWidget *body, int *counter,
+ const VerbQ *qs, int n,
+ const char **pool, int pool_n,
+ const char **meanings, gboolean pre_meaning);
+void verb_note_reveal(VerbNoteCtx *nc);
+void verb_ex_check(GtkButton *button, gpointer data);
+GtkWidget *build_verb_ex(UnitCtx *unit, int ex_num,
+ const char *title, const char *sub,
+ const VerbQ *qs, int n,
+ const char **pool, int pool_n,
+ const char **meanings, gboolean pre_meaning,
+ const char **gloss);
+void typed_check(GtkButton *button, gpointer data);
+char *typed_answer_display(const char *answers);
+void typed_note_reveal(TypedNoteCtx *nc);
+void typed_ex_check(GtkButton *button, gpointer data);
+GtkWidget *build_typed(UnitCtx *unit, int ex_num,
+ const char *title, const char *sub,
+ const TypedQ *qs, int n,
+ const char *word_bank, gboolean answers_note);
+void s01_fill_sample(GtkWidget *label);
+void s01_reveal(GtkButton *button, gpointer data);
+void s01_finish(GtkButton *button, gpointer data);
+GtkWidget *build_profile(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub, const ProfileQ *qs, int n);
+int hm_char_index(gunichar ch);
+void hm_update_slots(HangmanCtx *hm);
+gboolean hm_word_solved(HangmanCtx *hm);
+void hm_set_letters_enabled(HangmanCtx *hm, gboolean enabled);
+void hm_redraw(HangmanCtx *hm);
+void hm_clear_feedback(HangmanCtx *hm);
+gboolean hm_advance(gpointer data);
+void hm_reset_word(gpointer data);
+gboolean hm_retry_word(gpointer data);
+void hm_guess_letter(GtkButton *button, gpointer data);
+void draw_hangman(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+GtkWidget *build_hangman(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub);
+GtkWidget *u2ex1(UnitCtx *unit);
+GtkWidget *u2ex2(UnitCtx *unit);
+GtkWidget *u2ex3(UnitCtx *unit);
+GtkWidget *u2ex4(UnitCtx *unit);
+GtkWidget *u2ex5(UnitCtx *unit);
+GtkWidget *u2ex6(UnitCtx *unit);
+GtkWidget *u2ex7(UnitCtx *unit);
+GtkWidget *u2ex8(UnitCtx *unit);
+GtkWidget *u2ex9(UnitCtx *unit);
+GtkWidget *u2ex10(UnitCtx *unit);
+GtkWidget *u2ex11(UnitCtx *unit);
+GtkWidget *u2ex12(UnitCtx *unit);
+GtkWidget *u2ex13(UnitCtx *unit);
+GtkWidget *u2ex14(UnitCtx *unit);
+GtkWidget *u2ex15(UnitCtx *unit);
+GtkWidget *u2ex16(UnitCtx *unit);
+GtkWidget *u2ex17(UnitCtx *unit);
+GtkWidget *u2ex18(UnitCtx *unit);
+GtkWidget *u2ex19(UnitCtx *unit);
+char *u3_fill_sentence(const U3Fill *f);
+void u3_fill_row(ComboListCtx *ctx, GtkWidget *body, const U3Fill *f,
+ const char **pool, int pool_n, const char *gloss);
+void u3_sample_line(GtkWidget *body, const char *text);
+GtkWidget *u3_build_drop(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub, const char *sample,
+ const U3Fill *rows, int n,
+ const char **pool, int pool_n,
+ const char **glosses, gboolean show_german);
+GtkWidget *u3ex1(UnitCtx *unit);
+const char *ex2_word_cs(const char *w);
+void ex2_check(GtkButton *button, gpointer data);
+GtkWidget *u3ex2(UnitCtx *unit);
+GtkWidget *u3ex3(UnitCtx *unit);
+U3LetCtx *u3_let_ctx_new(UnitCtx *unit, int ex_num,
+ GtkWidget *feedback);
+char *u3_let_sentence(const U3Let *row);
+void u3_let_check(GtkButton *button, gpointer data);
+void u3_let_row(U3LetCtx *ctx, GtkWidget *body, const U3Let *row);
+GtkWidget *u3_build_letters(UnitCtx *unit, int ex_num,
+ const char *title, const char *sub,
+ const U3Let *rows, int n);
+void ex4_check(GtkButton *button, gpointer data);
+GtkWidget *u3ex4(UnitCtx *unit);
+GtkWidget *u3ex5(UnitCtx *unit);
+GtkWidget *u3ex6(UnitCtx *unit);
+void u3_table_check(GtkButton *button, gpointer data);
+GtkWidget *u3ex7(UnitCtx *unit);
+GtkWidget *u3ex8(UnitCtx *unit);
+GtkWidget *u3ex9(UnitCtx *unit);
+GtkWidget *u3ex10(UnitCtx *unit);
+void u3_kw_check(GtkButton *button, gpointer data);
+GtkWidget *u3_build_kw(UnitCtx *unit, int ex_num, const char *title,
+ const char *sub, const char *sample,
+ const U3Kw *qs, int n);
+GtkWidget *u3ex11(UnitCtx *unit);
+GtkWidget *u3ex12(UnitCtx *unit);
+GtkWidget *u3ex13(UnitCtx *unit);
+GtkWidget *u3ex14(UnitCtx *unit);
+GtkWidget *u3ex15(UnitCtx *unit);
+void draw_theme_swatch(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void on_mode_toggled(GtkToggleButton *btn, gpointer user_data);
+void on_theme_toggled(GtkToggleButton *btn, gpointer user_data);
+void on_lang_toggled(GtkToggleButton *btn, gpointer user_data);
+GtkWidget *make_lang_chip(const char *label, UiLang lang,
+ GtkToggleButton *group);
+GtkWidget *make_mode_chip(const char *label, ColorMode mode,
+ GtkToggleButton *group);
+GtkWidget *make_theme_card(ThemeId id, GtkToggleButton *group);
+void on_settings_clicked(GtkButton *button, gpointer user_data);
+GtkWidget *build_stats_button(void);
+GtkWidget *build_settings_button(void);
+void unit_meta_init(void);
+void unit_configure(int idx, const char *const *names, int n);
+GtkWidget *build_exercise_page(UnitCtx *u, int n);
+void net_paragraph(GtkWidget *box, const char *text);
+void net_heading(GtkWidget *box, const char *text);
+void net_note_font(GtkWidget *l, int px);
+void net_rescale_notes(void);
+gboolean net_note_tick(GtkWidget *w, GdkFrameClock *clock,
+ gpointer data);
+GtkWidget *net_slide_card(const char *cls);
+void net_note_kicker(GtkWidget *box, const char *text);
+void net_note_title(GtkWidget *box, const char *text);
+void net_note_line(GtkWidget *box, const char *text, gboolean tip);
+void net_slide_apply(void);
+void net_open_unit(GtkButton *button, gpointer data);
+void net_slide_prev(GtkButton *button, gpointer data);
+void net_slide_next(GtkButton *button, gpointer data);
+void net_point(double t, double *ox, double *oy);
+void net_path(cairo_t *cr, double t0, double t1);
+void net_geometry(double avail);
+void net_apply_layout(void);
+void net_relayout(void);
+gboolean net_relayout_idle(gpointer data);
+void net_relayout_later(void);
+void net_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data);
+void net_render_rail_to(cairo_t *cr);
+void net_draw_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data);
+void net_rail_theme_reset(void);
+void net_add_node(GtkFixed *fixed, int index);
+GtkWidget *build_netmap_page(void);
+GtkWidget *build_net_unit1_page(void);
+GtkWidget *net_qz_combo(void);
+void net_qz_set_prefix(GtkComboBoxText *c, int pfx);
+int net_qz_prefix(GtkComboBoxText *c);
+gboolean net_txt_eq(const char *a, const char *b);
+gboolean net_qz_entry_ok(GtkWidget *entry, const char *want);
+void net_qz_check(GtkButton *button, gpointer data);
+void net_qz_fill(GtkButton *button, gpointer data);
+void net_qz_header(GtkWidget *box, const char *text);
+GtkWidget *net_qz_make_entry(void);
+GtkWidget *build_net_exercise_page(void);
+void activate(GtkApplication *app, gpointer user_data);
+int main(int argc, char **argv);
+
+#endif /* MATURITA_H */
diff --git a/navigation.c b/navigation.c
new file mode 100644
index 0000000..ed03392
--- /dev/null
+++ b/navigation.c
@@ -0,0 +1,132 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Navigation */
+/* ------------------------------------------------------------------ */
+
+void on_nav_clicked(GtkButton *button, gpointer user_data) {
+ const char *target = g_object_get_data(G_OBJECT(button), "target");
+ (void)user_data;
+ if (target)
+ gtk_stack_set_visible_child_name(main_stack, target);
+}
+
+void on_continue_clicked(GtkButton *button, gpointer user_data) {
+ (void)button;
+ (void)user_data;
+ gtk_stack_set_visible_child_name(main_stack, "subjects");
+}
+
+GtkWidget *make_back_button(const char *target) {
+ GtkWidget *back = gtk_button_new();
+ GtkWidget *icon = gtk_drawing_area_new();
+
+ gtk_widget_set_size_request(icon, 14, 14);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon), draw_back_icon,
+ NULL, NULL);
+ gtk_button_set_child(GTK_BUTTON(back), icon);
+ gtk_widget_add_css_class(back, "back-btn");
+ gtk_widget_set_valign(back, GTK_ALIGN_CENTER);
+ i18n_bind(back, "back", 2);
+ g_signal_connect_swapped(back, "state-flags-changed",
+ G_CALLBACK(gtk_widget_queue_draw), icon);
+ g_object_set_data_full(G_OBJECT(back), "target", g_strdup(target), g_free);
+ g_signal_connect(back, "clicked", G_CALLBACK(on_nav_clicked), NULL);
+ return back;
+}
+
+GtkWidget *top_bar(const char *back_target, const char *title,
+ const char *subtitle) {
+ GtkWidget *top = gtk_center_box_new();
+ gtk_widget_set_margin_top(top, 2);
+ gtk_widget_set_margin_bottom(top, 6);
+
+ GtkWidget *back = make_back_button(back_target);
+ gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
+
+ GtkWidget *center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
+ gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
+
+ GtkWidget *heading = gtk_label_new(NULL);
+ i18n_bind(heading, title, 0);
+ gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(heading, "roadmap-title");
+ gtk_box_append(GTK_BOX(center), heading);
+
+ if (subtitle) {
+ GtkWidget *sub = gtk_label_new(NULL);
+ i18n_bind(sub, subtitle, 0);
+ gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(sub, "roadmap-sub");
+ gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
+ gtk_label_set_max_width_chars(GTK_LABEL(sub), 60);
+ gtk_box_append(GTK_BOX(center), sub);
+ }
+
+ return top;
+}
+
+gboolean on_window_key_pressed(GtkEventControllerKey *controller,
+ guint keyval, guint keycode,
+ GdkModifierType state,
+ gpointer user_data) {
+ GtkWindow *window = user_data;
+
+ (void)controller;
+ (void)keycode;
+
+ if ((keyval == GDK_KEY_q &&
+ (state & (GDK_SUPER_MASK | GDK_META_MASK)) != 0) ||
+ (keyval == GDK_KEY_F4 &&
+ (state & GDK_ALT_MASK) != 0)) {
+ GtkApplication *app = gtk_window_get_application(window);
+ if (app != NULL)
+ g_application_quit(G_APPLICATION(app));
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+/* ------------------------------------------------------------------ */
+/* Welcome page */
+/* ------------------------------------------------------------------ */
+
+GtkWidget *build_welcome_page(void) {
+ GtkWidget *box;
+ GtkWidget *heading;
+ GtkWidget *card;
+ GtkWidget *button;
+
+ box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18);
+ gtk_widget_set_halign(box, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(box, GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top(box, 40);
+ gtk_widget_set_margin_bottom(box, 40);
+
+ heading = gtk_label_new(NULL);
+ welcome_heading = heading;
+ gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(heading, "heading");
+ gtk_box_append(GTK_BOX(box), heading);
+
+ card = gtk_label_new(NULL);
+ i18n_bind(card, "welcome_body", 0);
+ gtk_label_set_justify(GTK_LABEL(card), GTK_JUSTIFY_CENTER);
+ gtk_label_set_wrap(GTK_LABEL(card), TRUE);
+ gtk_label_set_max_width_chars(GTK_LABEL(card), 48);
+ gtk_widget_add_css_class(card, "card");
+ gtk_box_append(GTK_BOX(box), card);
+
+ button = gtk_button_new();
+ i18n_bind(button, "continue", 1);
+ gtk_widget_set_halign(button, GTK_ALIGN_CENTER);
+ gtk_widget_set_margin_top(button, 6);
+ gtk_widget_add_css_class(button, "btn-primary");
+ gtk_box_append(GTK_BOX(box), button);
+
+ g_signal_connect(button, "clicked", G_CALLBACK(on_continue_clicked), NULL);
+
+ return box;
+}
diff --git a/net.c b/net.c
new file mode 100644
index 0000000..bfeb479
--- /dev/null
+++ b/net.c
@@ -0,0 +1,1017 @@
+#include "maturita.h"
+
+/* ---- Computer networks: subject path, lesson & exercise ------------ */
+
+
+GtkWidget *net_slide_stack;
+GtkWidget *net_prev_btn;
+GtkWidget *net_next_btn;
+guint net_slide_idx;
+
+void net_paragraph(GtkWidget *box, const char *text) {
+ GtkWidget *l = gtk_label_new(text);
+
+ gtk_widget_set_halign(l, GTK_ALIGN_START);
+ gtk_label_set_wrap(GTK_LABEL(l), TRUE);
+ gtk_widget_add_css_class(l, "ex-prompt");
+ gtk_box_append(GTK_BOX(box), l);
+}
+
+void net_heading(GtkWidget *box, const char *text) {
+ GtkWidget *l = gtk_label_new(text);
+
+ gtk_widget_set_halign(l, GTK_ALIGN_START);
+ gtk_label_set_wrap(GTK_LABEL(l), TRUE);
+ gtk_widget_set_margin_top(l, 6);
+ gtk_widget_add_css_class(l, "ex-sub");
+ gtk_box_append(GTK_BOX(box), l);
+}
+
+/* Note-card slides with text that scales with the window. */
+GtkWidget *net_note_host;
+GArray *net_note_kicks;
+GArray *net_note_titles;
+GArray *net_note_bodies;
+int net_note_last_w = -1;
+
+void net_note_font(GtkWidget *l, int px) {
+ PangoAttrList *attrs = pango_attr_list_new();
+
+ pango_attr_list_insert(attrs, pango_attr_size_new(px * PANGO_SCALE));
+ gtk_label_set_attributes(GTK_LABEL(l), attrs);
+ pango_attr_list_unref(attrs);
+}
+
+void net_rescale_notes(void) {
+ int w = net_note_host ? gtk_widget_get_allocated_width(net_note_host) : 0;
+ int body, head, kick;
+
+ if (w < 160)
+ w = 160;
+ body = w / 46;
+ if (body < 14) body = 14;
+ if (body > 26) body = 26;
+ head = body + 14;
+ if (head > 46) head = 46;
+ kick = body - 3;
+ if (kick < 11) kick = 11;
+
+ for (guint i = 0; i < net_note_kicks->len; i++) {
+ net_note_font(g_array_index(net_note_kicks, GtkWidget *, i), kick);
+ net_note_font(g_array_index(net_note_titles, GtkWidget *, i), head);
+ net_note_font(g_array_index(net_note_bodies, GtkWidget *, i), body);
+ }
+}
+
+gboolean net_note_tick(GtkWidget *w, GdkFrameClock *clock,
+ gpointer data) {
+ int width = gtk_widget_get_allocated_width(w);
+
+ (void)clock;
+ (void)data;
+ if (width != net_note_last_w) {
+ net_note_last_w = width;
+ net_rescale_notes();
+ }
+ return G_SOURCE_CONTINUE;
+}
+
+GtkWidget *net_slide_card(const char *cls) {
+ GtkWidget *l = gtk_label_new(NULL);
+
+ gtk_widget_add_css_class(l, cls);
+ gtk_label_set_wrap(GTK_LABEL(l), TRUE);
+ gtk_label_set_justify(GTK_LABEL(l), GTK_JUSTIFY_CENTER);
+ gtk_widget_set_hexpand(l, TRUE);
+ gtk_widget_set_halign(l, GTK_ALIGN_FILL);
+ return l;
+}
+
+void net_note_kicker(GtkWidget *box, const char *text) {
+ GtkWidget *l = net_slide_card("notes-kicker");
+
+ gtk_label_set_text(GTK_LABEL(l), text);
+ gtk_box_append(GTK_BOX(box), l);
+ g_array_append_val(net_note_kicks, l);
+}
+
+void net_note_title(GtkWidget *box, const char *text) {
+ GtkWidget *l = net_slide_card("notes-title");
+
+ gtk_label_set_text(GTK_LABEL(l), text);
+ gtk_box_append(GTK_BOX(box), l);
+ g_array_append_val(net_note_titles, l);
+}
+
+void net_note_line(GtkWidget *box, const char *text, gboolean tip) {
+ GtkWidget *l = net_slide_card(tip ? "notes-tip" : "notes-body");
+
+ gtk_label_set_text(GTK_LABEL(l), text);
+ gtk_widget_set_margin_top(l, 2);
+ gtk_box_append(GTK_BOX(box), l);
+ g_array_append_val(net_note_bodies, l);
+}
+
+void net_slide_apply(void) {
+ char name[16];
+
+ if (!net_slide_stack)
+ return;
+
+ g_snprintf(name, sizeof(name), "net_slide%u", net_slide_idx);
+ gtk_stack_set_visible_child_name(GTK_STACK(net_slide_stack), name);
+
+ if (net_prev_btn) {
+ gtk_button_set_label(GTK_BUTTON(net_prev_btn), tr("net_slide_prev"));
+ gtk_widget_set_sensitive(net_prev_btn, net_slide_idx > 0);
+ }
+ if (net_next_btn) {
+ gtk_button_set_label(
+ GTK_BUTTON(net_next_btn),
+ net_slide_idx + 1 >= NET_SLIDES ? tr("net_slide_start")
+ : tr("net_slide_next"));
+ }
+}
+
+void net_open_unit(GtkButton *button, gpointer data) {
+ (void)button;
+ (void)data;
+ net_slide_idx = 0;
+ net_slide_apply();
+ gtk_stack_set_visible_child_name(main_stack, "netunit1");
+}
+
+void net_slide_prev(GtkButton *button, gpointer data) {
+ (void)button;
+ (void)data;
+ if (net_slide_idx > 0) {
+ net_slide_idx--;
+ net_slide_apply();
+ }
+}
+
+void net_slide_next(GtkButton *button, gpointer data) {
+ (void)button;
+ (void)data;
+ if (net_slide_idx + 1 < NET_SLIDES) {
+ net_slide_idx++;
+ net_slide_apply();
+ } else {
+ gtk_stack_set_visible_child_name(main_stack, "netex");
+ }
+}
+
+GtkWidget *net_scroll;
+GtkWidget *net_fixed;
+GtkWidget *net_rail;
+GtkWidget *net_nodes[NET_UNITS];
+GtkWidget *net_labels[NET_UNITS];
+double net_cx[NET_UNITS];
+double net_cy[NET_UNITS];
+int net_cols = 1;
+int net_rows = 1;
+int net_cw = (int)(2.0 * ROAD_MX + (NET_UNITS - 1) * PATH_SPAC);
+int net_ch = (int)(2.0 * ROAD_MY);
+guint net_idle;
+double net_last_avail = -1.0;
+int net_last_cols = -1;
+int net_last_rows = -1;
+int net_last_cw = -1;
+int net_last_ch = -1;
+cairo_surface_t *net_rail_cache;
+int net_cache_w;
+int net_cache_h;
+
+void net_point(double t, double *ox, double *oy) {
+ int k = (int)t;
+ double u = t - (double)k;
+ double u2, u3;
+ double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
+
+ if (k < 0) { k = 0; u = 0.0; }
+ if (k >= NET_UNITS - 1) { k = NET_UNITS - 2; u = 1.0; }
+
+ p1x = net_cx[k]; p1y = net_cy[k];
+ p2x = net_cx[k + 1]; p2y = net_cy[k + 1];
+ if (k - 1 >= 0) { p0x = net_cx[k - 1]; p0y = net_cy[k - 1]; }
+ else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
+ if (k + 2 < NET_UNITS) { p3x = net_cx[k + 2]; p3y = net_cy[k + 2]; }
+ else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
+
+ u2 = u * u;
+ u3 = u2 * u;
+ *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
+ + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
+ + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
+ *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
+ + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
+ + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
+}
+
+void net_path(cairo_t *cr, double t0, double t1) {
+ const double steps_per_unit = 24.0;
+ int n = (int)((t1 - t0) * steps_per_unit);
+ double x, y;
+ int s;
+
+ if (n < 1)
+ n = 1;
+ net_point(t0, &x, &y);
+ cairo_move_to(cr, x, y);
+ for (s = 1; s <= n; s++) {
+ net_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
+ cairo_line_to(cr, x, y);
+ }
+}
+
+void net_geometry(double avail) {
+ const double phase = 2.0 * G_PI * 1.7 / (double)(NET_UNITS - 1);
+ int rows;
+ int r, c, i;
+
+ for (rows = 1; rows <= NET_UNITS; rows++) {
+ int cols = (NET_UNITS + rows - 1) / rows;
+ if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0)
+ break;
+ }
+ if (rows > NET_UNITS)
+ rows = NET_UNITS;
+ net_rows = rows;
+ net_cols = (NET_UNITS + rows - 1) / rows;
+ if (net_cols < 1)
+ net_cols = 1;
+ net_cw = (int)(2.0 * ROAD_MX + (net_cols - 1) * PATH_SPAC);
+ net_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP);
+
+ for (r = 0; r < rows; r++) {
+ int base = r * net_cols;
+ int len = MIN(net_cols, NET_UNITS - base);
+ int fwd = (r % 2) == 0;
+ for (c = 0; c < len; c++) {
+ i = base + c;
+ int cc = fwd ? c : (net_cols - 1 - c);
+ net_cx[i] = ROAD_MX + cc * PATH_SPAC;
+ net_cy[i] = ROAD_MY + r * ROAD_GAP
+ + ROAD_WAVE * sin((double)i * phase);
+ }
+ }
+}
+
+void net_apply_layout(void) {
+ if (!net_fixed)
+ return;
+
+ if (net_cols == net_last_cols && net_rows == net_last_rows &&
+ net_cw == net_last_cw && net_ch == net_last_ch)
+ return;
+
+ net_last_cols = net_cols;
+ net_last_rows = net_rows;
+ net_last_cw = net_cw;
+ net_last_ch = net_ch;
+ if (net_rail_cache) {
+ cairo_surface_destroy(net_rail_cache);
+ net_rail_cache = NULL;
+ }
+
+ for (int i = 0; i < NET_UNITS; i++) {
+ if (!net_nodes[i] || !net_labels[i])
+ continue;
+ gtk_fixed_move(GTK_FIXED(net_fixed), net_nodes[i],
+ (int)(net_cx[i] - NODE_SIZE / 2.0),
+ (int)(net_cy[i] - NODE_SIZE / 2.0));
+ gtk_fixed_move(GTK_FIXED(net_fixed), net_labels[i],
+ (int)(net_cx[i] - (PATH_SPAC - 20.0) / 2.0),
+ (int)(net_cy[i] + NODE_SIZE / 2.0 + 10.0));
+ }
+
+ gtk_widget_set_size_request(net_fixed, net_cw, net_ch);
+ gtk_widget_set_size_request(net_rail, net_cw, net_ch);
+ gtk_fixed_move(GTK_FIXED(net_fixed), net_rail, 0, 0);
+ gtk_widget_queue_draw(net_rail);
+}
+
+void net_relayout(void) {
+ GtkAdjustment *hadj;
+ double avail;
+
+ if (!net_scroll)
+ return;
+ hadj = gtk_scrolled_window_get_hadjustment(
+ GTK_SCROLLED_WINDOW(net_scroll));
+ avail = gtk_adjustment_get_page_size(hadj);
+ if (avail < 1.0)
+ return;
+ if (fabs(avail - net_last_avail) < 1.0)
+ return;
+ net_last_avail = avail;
+ net_geometry(avail);
+ net_apply_layout();
+}
+
+gboolean net_relayout_idle(gpointer data) {
+ (void)data;
+ net_idle = 0;
+ net_relayout();
+ return G_SOURCE_REMOVE;
+}
+
+void net_relayout_later(void) {
+ if (net_idle == 0)
+ net_idle = g_idle_add(net_relayout_idle, NULL);
+}
+
+void net_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data) {
+ (void)adj;
+ (void)ps;
+ (void)data;
+ net_relayout_later();
+}
+
+void net_render_rail_to(cairo_t *cr) {
+ const double t_end = (double)(NET_UNITS - 1);
+ const Rgb mauve = color_from_hex(app_theme.accent);
+ const Rgb rail = color_from_hex(app_theme.rail);
+ const Rgb muted = color_from_hex(app_theme.subtext);
+ double hx, hy;
+
+ cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cr);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
+
+ net_point(0.0, &hx, &hy);
+ draw_node_halo(cr, hx, hy, NODE_SIZE * 1.05,
+ mauve.r, mauve.g, mauve.b, 0.18);
+
+ cairo_new_path(cr);
+ net_path(cr, 0.0, t_end);
+ cairo_set_line_width(cr, 16);
+ cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
+ cairo_stroke(cr);
+
+ cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17);
+ cairo_set_dash(cr, (double[]){2.0, 40.0}, 2, 0.0);
+ cairo_new_path(cr);
+ net_path(cr, 0.0, t_end);
+ cairo_stroke(cr);
+ cairo_set_dash(cr, NULL, 0, 0.0);
+}
+
+void net_draw_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ (void)area;
+ (void)width;
+ (void)height;
+ (void)data;
+
+ if (!net_rail_cache || net_cache_w != net_cw || net_cache_h != net_ch) {
+ cairo_t *rcr;
+
+ if (net_rail_cache)
+ cairo_surface_destroy(net_rail_cache);
+ net_cache_w = net_cw;
+ net_cache_h = net_ch;
+ if (net_cache_w < 1)
+ net_cache_w = 1;
+ if (net_cache_h < 1)
+ net_cache_h = 1;
+ net_rail_cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
+ net_cache_w, net_cache_h);
+ rcr = cairo_create(net_rail_cache);
+ net_render_rail_to(rcr);
+ cairo_destroy(rcr);
+ }
+
+ cairo_set_source_surface(cr, net_rail_cache, 0, 0);
+ cairo_paint(cr);
+}
+
+void net_rail_theme_reset(void) {
+ if (net_rail_cache) {
+ cairo_surface_destroy(net_rail_cache);
+ net_rail_cache = NULL;
+ }
+ if (net_rail)
+ gtk_widget_queue_draw(net_rail);
+}
+
+void net_add_node(GtkFixed *fixed, int index) {
+ int num = index + 1;
+ gboolean locked = index != 0;
+ GtkWidget *card;
+ GtkWidget *vbox;
+ GtkWidget *number;
+ GtkWidget *icon;
+ GtkWidget *name;
+ char *text;
+
+ card = gtk_button_new();
+ gtk_widget_add_css_class(card, "unit-node");
+ gtk_widget_set_can_focus(card, FALSE);
+ gtk_widget_set_sensitive(card, !locked);
+ gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE);
+
+ if (locked)
+ gtk_widget_add_css_class(card, "locked");
+ else
+ gtk_widget_add_css_class(card, "current");
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
+ gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
+ gtk_button_set_child(GTK_BUTTON(card), vbox);
+
+ text = g_strdup_printf("%d", num);
+ number = gtk_label_new(text);
+ g_free(text);
+ gtk_widget_add_css_class(number, "unit-number");
+ gtk_box_append(GTK_BOX(vbox), number);
+
+ if (locked) {
+ icon = icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 16);
+ gtk_box_append(GTK_BOX(vbox), icon);
+ } else {
+ icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18);
+ gtk_widget_set_visible(icon, FALSE);
+ gtk_box_append(GTK_BOX(vbox), icon);
+ g_signal_connect(card, "clicked", G_CALLBACK(net_open_unit), NULL);
+ }
+
+ net_nodes[index] = card;
+ gtk_fixed_put(fixed, card, 0, 0);
+
+ name = gtk_label_new(NULL);
+ gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1);
+ gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
+ gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER);
+ gtk_label_set_wrap(GTK_LABEL(name), TRUE);
+ gtk_widget_add_css_class(name, "unit-name");
+ if (index == 0) {
+ i18n_bind(name, "net_unit1", 0);
+ } else {
+ gtk_widget_add_css_class(name, "unit-name-locked");
+ }
+ net_labels[index] = name;
+ gtk_fixed_put(fixed, name, 0, 0);
+}
+
+GtkWidget *build_netmap_page(void) {
+ GtkWidget *page;
+ GtkWidget *scroll;
+ GtkWidget *wrap;
+ GtkWidget *fixed;
+ GtkWidget *rail;
+ GtkAdjustment *ha;
+ GtkAdjustment *va;
+
+ net_geometry(1000.0);
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_hexpand(page, TRUE);
+ gtk_widget_set_vexpand(page, TRUE);
+ gtk_widget_set_margin_start(page, 32);
+ gtk_widget_set_margin_end(page, 32);
+ gtk_widget_set_margin_top(page, 24);
+ gtk_widget_set_margin_bottom(page, 24);
+
+ gtk_box_append(GTK_BOX(page),
+ top_bar("subjects", "Správa počítačových sítí", "net_sub"));
+
+ scroll = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_margin_top(scroll, 14);
+ gtk_box_append(GTK_BOX(page), scroll);
+ net_scroll = scroll;
+
+ wrap = gtk_center_box_new();
+ gtk_widget_set_vexpand(wrap, TRUE);
+ gtk_widget_set_hexpand(wrap, TRUE);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
+
+ fixed = gtk_fixed_new();
+ gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_size_request(fixed, net_cw, net_ch);
+ gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
+ net_fixed = fixed;
+
+ rail = gtk_drawing_area_new();
+ gtk_widget_set_size_request(rail, net_cw, net_ch);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), net_draw_rail,
+ NULL, NULL);
+ gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
+ net_rail = rail;
+
+ for (int i = 0; i < NET_UNITS; i++)
+ net_add_node(GTK_FIXED(fixed), i);
+
+ ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
+ va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
+ g_signal_connect(ha, "notify::page-size",
+ G_CALLBACK(net_adjust_notify), NULL);
+ g_signal_connect(va, "notify::page-size",
+ G_CALLBACK(net_adjust_notify), NULL);
+
+ net_relayout_later();
+
+ return page;
+}
+
+GtkWidget *build_net_unit1_page(void) {
+ typedef struct {
+ const char *kicker;
+ const char *title;
+ const char *tip;
+ const char *line[7];
+ } NetSlide;
+ static const NetSlide slides[NET_SLIDES] = {
+ {
+ "1 / 4 • VLSM", "Podsíťování sítí",
+ "Tip: prefix /n = počet jedniček v masce. /24 = 255.255.255.0",
+ {
+ "Cíl: rozdělit jednu IP síť na menší podsítě podle počtu uzlů.",
+ "Pamatuj si tři role adres v každém bloku:",
+ "síťová = první adresa bloku",
+ "broadcast = poslední adresa bloku",
+ "uzly = vše mezi nimi (síť + 1 až broadcast − 1)",
+ NULL,
+ },
+ },
+ {
+ "2 / 4 • Velikosti", "Jak velký blok potřebuji?",
+ "Počet bitů pro uzly h = log₂(velikost), prefix = 32 − h",
+ {
+ "Pro X uzlů potřebuješ blok, do kterého se vejde X + 2 adresy "
+ "(síť a broadcast).",
+ "Velikost bloku je vždy mocnina dvojky – vezmi nejmenší blok, "
+ "který je větší nebo roven X + 2.",
+ "60 uzlů → 62 → blok 64 → /26",
+ "30 uzlů → 32 → blok 32 → /27",
+ "14 uzlů → 16 → blok 16 → /28",
+ "7 uzlů → 9 → blok 16 → /28",
+ NULL,
+ },
+ },
+ {
+ "3 / 4 • Postup", "Jak na to krok za krokem",
+ NULL,
+ {
+ "1. Seřaď podsítě od největší po nejmenší.",
+ "2. Pro každou najdi potřebný prefix.",
+ "3. Začni na síťové adrese ze zadání.",
+ "4. Rozsah podsítě = od síťové adresy po broadcast.",
+ "5. Rozsah uzlů = síťová + 1 až broadcast − 1.",
+ "6. Posuň se hned za broadcast a pokračuj dál.",
+ NULL,
+ },
+ },
+ {
+ "4 / 4 • Příklad", "Ukázka: 10.0.0.0/24",
+ "A: 60 uzlů, B: 30, C: 14, D: 7",
+ {
+ "A → 10.0.0.0/26 uzly 10.0.0.1–10.0.0.62 "
+ "broadcast 10.0.0.63",
+ "B → 10.0.0.64/27 uzly 10.0.0.65–10.0.0.94 "
+ "broadcast 10.0.0.95",
+ "C → 10.0.0.96/28 uzly 10.0.0.97–10.0.0.110 "
+ "broadcast 10.0.0.111",
+ "D → 10.0.0.112/28 uzly 10.0.0.113–10.0.0.126 "
+ "broadcast 10.0.0.127",
+ NULL,
+ },
+ },
+ };
+ GtkWidget *page;
+ GtkWidget *scroll;
+ GtkWidget *nav;
+
+ net_note_kicks = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ net_note_titles = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+ net_note_bodies = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_margin_start(page, 28);
+ gtk_widget_set_margin_end(page, 28);
+ gtk_widget_set_margin_top(page, 20);
+ gtk_widget_set_margin_bottom(page, 20);
+
+ gtk_box_append(GTK_BOX(page),
+ top_bar("netmap", "net_unit1", NULL));
+
+ scroll = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_margin_top(scroll, 10);
+ gtk_box_append(GTK_BOX(page), scroll);
+
+ net_slide_stack = gtk_stack_new();
+ gtk_stack_set_transition_type(GTK_STACK(net_slide_stack),
+ GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll),
+ net_slide_stack);
+ net_note_host = net_slide_stack;
+
+ for (int s = 0; s < NET_SLIDES; s++) {
+ GtkWidget *stage = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ GtkWidget *card;
+ char name[16];
+
+ gtk_widget_set_vexpand(stage, TRUE);
+ g_snprintf(name, sizeof(name), "net_slide%d", s);
+ gtk_stack_add_named(GTK_STACK(net_slide_stack), stage, name);
+
+ card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
+ gtk_widget_add_css_class(card, "notes-card");
+ gtk_widget_set_valign(card, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(stage), card);
+
+ net_note_kicker(card, slides[s].kicker);
+ net_note_title(card, slides[s].title);
+
+ for (int i = 0; i < 7 && slides[s].line[i]; i++)
+ net_note_line(card, slides[s].line[i], FALSE);
+
+ if (slides[s].tip)
+ net_note_line(card, slides[s].tip, TRUE);
+ }
+
+ gtk_widget_add_tick_callback(net_slide_stack, net_note_tick, NULL, NULL);
+ net_note_last_w = -1;
+ net_rescale_notes();
+
+ nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_box_set_homogeneous(GTK_BOX(nav), TRUE);
+ gtk_widget_set_margin_top(nav, 10);
+ gtk_box_append(GTK_BOX(page), nav);
+
+ net_prev_btn = gtk_button_new_with_label(tr("net_slide_prev"));
+ gtk_widget_add_css_class(net_prev_btn, "btn-primary");
+ g_signal_connect(net_prev_btn, "clicked", G_CALLBACK(net_slide_prev), NULL);
+ gtk_box_append(GTK_BOX(nav), net_prev_btn);
+
+ net_next_btn = gtk_button_new_with_label(tr("net_slide_next"));
+ gtk_widget_add_css_class(net_next_btn, "btn-primary");
+ g_signal_connect(net_next_btn, "clicked", G_CALLBACK(net_slide_next), NULL);
+ gtk_box_append(GTK_BOX(nav), net_next_btn);
+
+ net_slide_idx = 0;
+ net_slide_apply();
+
+ return page;
+}
+
+/* Solutions below are the canonical "largest subnet first, in order A–D"
+ * allocation. Scenario 3 does not fit into a /24 as written, which is why
+ * its "solution" explains that instead. */
+
+const NetTask net_tasks[] = {
+ {
+ "1.) Síť má adresu 10.0.0.0/24. Rozdělte ji do podsítí s následujícími "
+ "požadavky:\n\n"
+ "Podsíť A: 60 uzlů\nPodsíť B: 30 uzlů\nPodsíť C: 14 uzlů\n"
+ "Podsíť D: 7 uzlů",
+ "A: 10.0.0.0/26 síť 10.0.0.0 uzly 10.0.0.1–10.0.0.62 "
+ "broadcast 10.0.0.63\n"
+ "B: 10.0.0.64/27 síť 10.0.0.64 uzly 10.0.0.65–10.0.0.94 "
+ "broadcast 10.0.0.95\n"
+ "C: 10.0.0.96/28 síť 10.0.0.96 uzly 10.0.0.97–10.0.0.110 "
+ "broadcast 10.0.0.111\n"
+ "D: 10.0.0.112/28 síť 10.0.0.112 uzly 10.0.0.113–10.0.0.126 "
+ "broadcast 10.0.0.127",
+ },
+ {
+ "2.) Síť má adresu 172.16.0.0/16. Rozdělte ji do podsítí s "
+ "následujícími požadavky:\n\n"
+ "Podsíť A: 500 uzlů\nPodsíť B: 200 uzlů\nPodsíť C: 100 uzlů\n"
+ "Podsíť D: 50 uzlů",
+ "A: 172.16.0.0/23 síť 172.16.0.0 uzly 172.16.0.1–172.16.1.254 "
+ "broadcast 172.16.1.255\n"
+ "B: 172.16.2.0/24 síť 172.16.2.0 uzly 172.16.2.1–172.16.2.254 "
+ "broadcast 172.16.2.255\n"
+ "C: 172.16.3.0/25 síť 172.16.3.0 uzly 172.16.3.1–172.16.3.126 "
+ "broadcast 172.16.3.127\n"
+ "D: 172.16.3.128/26 síť 172.16.3.128 uzly 172.16.3.129–172.16.3.190 "
+ "broadcast 172.16.3.191",
+ },
+ {
+ "3.) Síť má adresu 192.168.10.0/24. Rozdělte ji do podsítí s "
+ "následujícími požadavky:\n\n"
+ "Podsíť A: 120 uzlů\nPodsíť B: 70 uzlů\nPodsíť C: 35 uzlů\n"
+ "Podsíť D: 15 uzlů",
+ "Do sítě /24 se tyto požadavky nevejdou: A i B potřebují /25 "
+ "(2 × 128 = 256 adres), na C a D už nezbude žádný blok. "
+ "Při zachování velikostí by bylo nutné větší síť (např. /23).",
+ },
+ {
+ "4.) Síť má adresu 192.168.2.0/25. Rozdělte ji do podsítí s "
+ "následujícími požadavky:\n\n"
+ "Podsíť A: 50 uzlů\nPodsíť B: 25 uzlů\nPodsíť C: 12 uzlů\n"
+ "Podsíť D: 5 uzlů",
+ "A: 192.168.2.0/26 síť 192.168.2.0 uzly 192.168.2.1–192.168.2.62 "
+ "broadcast 192.168.2.63\n"
+ "B: 192.168.2.64/27 síť 192.168.2.64 "
+ "uzly 192.168.2.65–192.168.2.94 broadcast 192.168.2.95\n"
+ "C: 192.168.2.96/28 síť 192.168.2.96 "
+ "uzly 192.168.2.97–192.168.2.110 broadcast 192.168.2.111\n"
+ "D: 192.168.2.112/28 síť 192.168.2.112 "
+ "uzly 192.168.2.113–192.168.2.126 broadcast 192.168.2.127",
+ },
+};
+
+/* Interactive subnet calculator for the practice sheet: the student
+ * chooses a prefix and types the network address, broadcast and the
+ * usable host range. */
+
+const NetAns net_ans[4][4] = {
+ { /* 10.0.0.0/24 */
+ {26, "10.0.0.0", "10.0.0.63", "10.0.0.1", "10.0.0.62"},
+ {27, "10.0.0.64", "10.0.0.95", "10.0.0.65", "10.0.0.94"},
+ {28, "10.0.0.96", "10.0.0.111", "10.0.0.97", "10.0.0.110"},
+ {28, "10.0.0.112", "10.0.0.127", "10.0.0.113", "10.0.0.126"},
+ },
+ { /* 172.16.0.0/16 */
+ {23, "172.16.0.0", "172.16.1.255", "172.16.0.1", "172.16.1.254"},
+ {24, "172.16.2.0", "172.16.2.255", "172.16.2.1", "172.16.2.254"},
+ {25, "172.16.3.0", "172.16.3.127", "172.16.3.1", "172.16.3.126"},
+ {26, "172.16.3.128", "172.16.3.191", "172.16.3.129", "172.16.3.190"},
+ },
+ { /* does not fit into a /24 */
+ {-1, NULL, NULL, NULL, NULL},
+ {-1, NULL, NULL, NULL, NULL},
+ {-1, NULL, NULL, NULL, NULL},
+ {-1, NULL, NULL, NULL, NULL},
+ },
+ { /* 192.168.2.0/25 */
+ {26, "192.168.2.0", "192.168.2.63", "192.168.2.1", "192.168.2.62"},
+ {27, "192.168.2.64", "192.168.2.95", "192.168.2.65", "192.168.2.94"},
+ {28, "192.168.2.96", "192.168.2.111", "192.168.2.97", "192.168.2.110"},
+ {28, "192.168.2.112", "192.168.2.127", "192.168.2.113", "192.168.2.126"},
+ },
+};
+
+
+GtkWidget *net_qz_combo(void) {
+ GtkWidget *c = gtk_combo_box_text_new();
+
+ for (int p = 8; p <= 30; p++) {
+ char buf[8];
+
+ g_snprintf(buf, sizeof(buf), "/%d", p);
+ gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(c), NULL, buf);
+ }
+ gtk_combo_box_set_active(GTK_COMBO_BOX(c), -1);
+ return c;
+}
+
+void net_qz_set_prefix(GtkComboBoxText *c, int pfx) {
+ if (pfx >= 8 && pfx <= 30)
+ gtk_combo_box_set_active(GTK_COMBO_BOX(c), pfx - 8);
+ else
+ gtk_combo_box_set_active(GTK_COMBO_BOX(c), -1);
+}
+
+int net_qz_prefix(GtkComboBoxText *c) {
+ gchar *txt = gtk_combo_box_text_get_active_text(c);
+ int p = txt ? atoi(txt + (txt[0] == '/' ? 1 : 0)) : -1;
+
+ g_free(txt);
+ return p;
+}
+
+/* Case- and space-insensitive comparison of typed address answers. */
+gboolean net_txt_eq(const char *a, const char *b) {
+ const char *x, *y;
+
+ if (!a || !b)
+ return a == b;
+ x = a;
+ y = b;
+ while (*x && *y) {
+ while (*x == ' ' || *x == '\t')
+ x++;
+ while (*y == ' ' || *y == '\t')
+ y++;
+ if (g_ascii_tolower((guchar)*x) != g_ascii_tolower((guchar)*y))
+ return FALSE;
+ if (*x)
+ x++;
+ if (*y)
+ y++;
+ }
+ while (*x == ' ' || *x == '\t')
+ x++;
+ while (*y == ' ' || *y == '\t')
+ y++;
+ return *x == '\0' && *y == '\0';
+}
+
+gboolean net_qz_entry_ok(GtkWidget *entry, const char *want) {
+ const gchar *got = gtk_editable_get_text(GTK_EDITABLE(entry));
+
+ return got && net_txt_eq(got, want);
+}
+
+void net_qz_check(GtkButton *button, gpointer data) {
+ NetQz *q = data;
+ int ok = 0;
+
+ (void)button;
+
+ if (q->infeasible) {
+ set_feedback(q->feed, FALSE,
+ "Toto zadání se do sítě /24 nevejde – vyzkoušej "
+ "„Ukázat řešení“ a přečti si proč.");
+ return;
+ }
+
+ for (int i = 0; i < 4; i++) {
+ const NetAns *a = &net_ans[q->id][i];
+ gboolean p_ok = a->pfx > 0 &&
+ net_qz_prefix(GTK_COMBO_BOX_TEXT(q->combo[i])) == a->pfx;
+ gboolean net_ok = net_qz_entry_ok(q->net[i], a->net);
+ gboolean bc_ok = net_qz_entry_ok(q->bcast[i], a->bcast);
+ gboolean lo_ok = net_qz_entry_ok(q->lo[i], a->lo);
+ gboolean hi_ok = net_qz_entry_ok(q->hi[i], a->hi);
+
+ answer_mark(q->combo[i], p_ok);
+ answer_mark(q->net[i], net_ok);
+ answer_mark(q->bcast[i], bc_ok);
+ answer_mark(q->lo[i], lo_ok);
+ answer_mark(q->hi[i], hi_ok);
+ if (p_ok && net_ok && bc_ok && lo_ok && hi_ok)
+ ok++;
+ }
+
+ if (ok == 4)
+ set_feedback(q->feed, TRUE, tr("feedback_ok"));
+ else
+ set_feedback(q->feed, FALSE, tr("feedback_retry"));
+}
+
+void net_qz_fill(GtkButton *button, gpointer data) {
+ NetQz *q = data;
+
+ (void)button;
+ for (int i = 0; i < 4; i++) {
+ const NetAns *a = &net_ans[q->id][i];
+
+ if (a->pfx <= 0)
+ continue;
+ net_qz_set_prefix(GTK_COMBO_BOX_TEXT(q->combo[i]), a->pfx);
+ gtk_editable_set_text(GTK_EDITABLE(q->net[i]), a->net);
+ gtk_editable_set_text(GTK_EDITABLE(q->bcast[i]), a->bcast);
+ gtk_editable_set_text(GTK_EDITABLE(q->lo[i]), a->lo);
+ gtk_editable_set_text(GTK_EDITABLE(q->hi[i]), a->hi);
+ }
+ if (q->note)
+ gtk_widget_set_visible(q->note, TRUE);
+}
+
+void net_qz_header(GtkWidget *box, const char *text) {
+ GtkWidget *l = gtk_label_new(text);
+
+ gtk_widget_add_css_class(l, "meaning");
+ gtk_widget_set_halign(l, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(box), l);
+}
+
+GtkWidget *net_qz_make_entry(void) {
+ GtkWidget *e = gtk_entry_new();
+
+ gtk_widget_set_hexpand(e, TRUE);
+ gtk_editable_set_width_chars(GTK_EDITABLE(e), 15);
+ return e;
+}
+
+GtkWidget *build_net_exercise_page(void) {
+ GtkWidget *page;
+ GtkWidget *scroll;
+ GtkWidget *body;
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_margin_start(page, 32);
+ gtk_widget_set_margin_end(page, 32);
+ gtk_widget_set_margin_top(page, 24);
+ gtk_widget_set_margin_bottom(page, 24);
+
+ gtk_box_append(GTK_BOX(page),
+ top_bar("netunit1", "net_ex_title", NULL));
+
+ scroll = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_margin_top(scroll, 12);
+ gtk_box_append(GTK_BOX(page), scroll);
+
+ body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
+
+ net_heading(body,
+ "Zadání pro výpočet IP adres – k procvičení");
+ net_paragraph(body,
+ "Určete pro každou podsíť:\n"
+ "• Maska podsítě (prefix)\n"
+ "• Rozsah IP adres podsítě\n"
+ "• Rozsah IP adres pro uzly\n"
+ "• Síťovou adresu\n"
+ "• Broadcast adresu");
+
+ for (guint t = 0; t < G_N_ELEMENTS(net_tasks); t++) {
+ GtkWidget *task = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
+ GtkWidget *table;
+ GtkWidget *btns;
+ GtkWidget *reveal;
+ GtkWidget *check;
+ GtkWidget *sol;
+ NetQz *q = g_new0(NetQz, 1);
+
+ q->infeasible = (t == 2);
+ q->id = (int)t;
+ gtk_widget_set_margin_top(task, 10);
+ gtk_box_append(GTK_BOX(body), task);
+
+ net_paragraph(task, net_tasks[t].prompt);
+
+ if (q->infeasible) {
+ net_qz_header(task, net_tasks[t].solution);
+ } else {
+ static const char *cols[] = {"Prefix", "Síťová adresa",
+ "Broadcast", "Uzly od", "Uzly do"};
+ table = gtk_grid_new();
+ gtk_grid_set_row_spacing(GTK_GRID(table), 6);
+ gtk_grid_set_column_spacing(GTK_GRID(table), 8);
+ gtk_widget_set_margin_top(table, 6);
+ gtk_box_append(GTK_BOX(task), table);
+
+ for (guint c = 1; c < 6; c++) {
+ GtkWidget *h = gtk_label_new(cols[c - 1]);
+ gtk_widget_add_css_class(h, "meaning");
+ gtk_grid_attach(GTK_GRID(table), h, (int)c, 0, 1, 1);
+ }
+
+ for (int i = 0; i < 4; i++) {
+ char letter[8];
+ GtkWidget *lab = gtk_label_new(NULL);
+ int row = i + 1;
+
+ g_snprintf(letter, sizeof(letter), "%c", 'A' + i);
+ gtk_label_set_text(GTK_LABEL(lab), letter);
+ gtk_widget_add_css_class(lab, "ex-prompt");
+ gtk_grid_attach(GTK_GRID(table), lab, 0, row, 1, 1);
+
+ q->combo[i] = net_qz_combo();
+ gtk_grid_attach(GTK_GRID(table), q->combo[i], 1, row, 1, 1);
+ q->net[i] = net_qz_make_entry();
+ gtk_grid_attach(GTK_GRID(table), q->net[i], 2, row, 1, 1);
+ q->bcast[i] = net_qz_make_entry();
+ gtk_grid_attach(GTK_GRID(table), q->bcast[i], 3, row, 1, 1);
+ q->lo[i] = net_qz_make_entry();
+ gtk_grid_attach(GTK_GRID(table), q->lo[i], 4, row, 1, 1);
+ q->hi[i] = net_qz_make_entry();
+ gtk_grid_attach(GTK_GRID(table), q->hi[i], 5, row, 1, 1);
+ }
+ }
+
+ btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
+ gtk_box_append(GTK_BOX(task), btns);
+
+ if (!q->infeasible) {
+ check = gtk_button_new();
+ i18n_bind(check, "check", 1);
+ gtk_widget_add_css_class(check, "pill");
+ gtk_widget_set_halign(check, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(btns), check);
+ g_signal_connect(check, "clicked", G_CALLBACK(net_qz_check), q);
+ }
+
+ reveal = gtk_button_new();
+ i18n_bind(reveal, "net_solution", 1);
+ gtk_widget_add_css_class(reveal, "pill");
+ gtk_widget_set_halign(reveal, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(btns), reveal);
+ g_signal_connect(reveal, "clicked", G_CALLBACK(net_qz_fill), q);
+
+ q->feed = gtk_label_new("");
+ gtk_widget_set_halign(q->feed, GTK_ALIGN_START);
+ gtk_box_append(GTK_BOX(task), q->feed);
+
+ sol = gtk_label_new(net_tasks[t].solution);
+ gtk_widget_set_halign(sol, GTK_ALIGN_START);
+ gtk_label_set_xalign(GTK_LABEL(sol), 0.0);
+ gtk_label_set_wrap(GTK_LABEL(sol), TRUE);
+ gtk_widget_set_margin_start(sol, 8);
+ gtk_widget_add_css_class(sol, "meaning");
+ gtk_widget_set_visible(sol, FALSE);
+ gtk_box_append(GTK_BOX(task), sol);
+ q->note = sol;
+ }
+
+ return page;
+}
diff --git a/progress.c b/progress.c
new file mode 100644
index 0000000..0ce0b0d
--- /dev/null
+++ b/progress.c
@@ -0,0 +1,121 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Progress */
+/* ------------------------------------------------------------------ */
+
+void unit_save_progress(UnitCtx *u) {
+ GKeyFile *kf = g_key_file_new();
+ for (int i = 1; i <= u->n_ex; i++) {
+ gchar key[8];
+ g_snprintf(key, sizeof(key), "%d", i);
+ g_key_file_set_boolean(kf, "done", key, u->done[i]);
+ }
+
+ if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
+ g_key_file_free(kf);
+ return;
+ }
+
+ gchar *data = g_key_file_to_data(kf, NULL, NULL);
+ g_key_file_free(kf);
+
+ if (data) {
+ GError *err = NULL;
+ g_file_set_contents(u->progress_file, data, -1, &err);
+ if (err)
+ g_error_free(err);
+ g_free(data);
+ }
+}
+
+void unit_load_progress(UnitCtx *u) {
+ GKeyFile *kf = g_key_file_new();
+ GError *err = NULL;
+
+ if (!g_key_file_load_from_file(kf, u->progress_file, G_KEY_FILE_NONE, &err)) {
+ if (err)
+ g_error_free(err);
+ g_key_file_free(kf);
+ return;
+ }
+
+ for (int i = 1; i <= u->n_ex; i++) {
+ gchar key[8];
+ g_snprintf(key, sizeof(key), "%d", i);
+ u->done[i] = g_key_file_get_boolean(kf, "done", key, NULL);
+ }
+
+ g_key_file_free(kf);
+}
+
+void load_progress(void) {
+ for (int i = 0; i < NUM_UNLOCKED; i++)
+ unit_load_progress(&units[i]);
+}
+
+/* Redraw the exercise rails of every unit that already has a page. */
+void all_rails_redraw(void) {
+ for (int i = 0; i < NUM_UNLOCKED; i++) {
+ if (units[i].ex_rail)
+ gtk_widget_queue_draw(units[i].ex_rail);
+ }
+}
+
+void refresh_completion_ui(void) {
+ for (int uidx = 0; uidx < NUM_UNLOCKED; uidx++) {
+ UnitCtx *u = &units[uidx];
+ gboolean all = TRUE;
+
+ for (int i = 1; i <= u->n_ex; i++) {
+ if (!u->done[i]) {
+ all = FALSE;
+ break;
+ }
+ }
+
+ /* per-bubble markers */
+ if (u->ex_cells[0] != NULL) {
+ for (int i = 1; i <= u->n_ex; i++) {
+ GtkWidget *btn = u->ex_cells[i - 1];
+ if (!btn)
+ continue;
+ if (u->done[i]) {
+ gtk_widget_add_css_class(btn, "done");
+ if (u->ex_icons[i])
+ gtk_widget_set_visible(u->ex_icons[i], TRUE);
+ } else {
+ gtk_widget_remove_css_class(btn, "done");
+ if (u->ex_icons[i])
+ gtk_widget_set_visible(u->ex_icons[i], FALSE);
+ }
+ }
+ }
+
+ /* roadmap node marker */
+ if (u->node) {
+ if (all && u->n_ex > 0) {
+ gtk_widget_remove_css_class(u->node, "current");
+ gtk_widget_add_css_class(u->node, "done");
+ if (u->node_done_icon)
+ gtk_widget_set_visible(u->node_done_icon, TRUE);
+ } else {
+ gtk_widget_remove_css_class(u->node, "done");
+ gtk_widget_add_css_class(u->node, "current");
+ if (u->node_done_icon)
+ gtk_widget_set_visible(u->node_done_icon, FALSE);
+ }
+ }
+ }
+
+ all_rails_redraw();
+ refresh_stats_ui();
+}
+
+void mark_done(UnitCtx *u, int n) {
+ if (!u || n < 1 || n > u->n_ex)
+ return;
+ u->done[n] = TRUE;
+ refresh_completion_ui();
+ unit_save_progress(u);
+}
diff --git a/roadmap.c b/roadmap.c
new file mode 100644
index 0000000..22e99f9
--- /dev/null
+++ b/roadmap.c
@@ -0,0 +1,446 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Roadmap page */
+/* ------------------------------------------------------------------ */
+
+void draw_node_halo(cairo_t *cr, double cx, double cy,
+ double radius, double r, double g, double b,
+ double alpha) {
+ cairo_pattern_t *grad;
+ grad = cairo_pattern_create_radial(cx, cy, 6.0, cx, cy, radius);
+ cairo_pattern_add_color_stop_rgba(grad, 0.0, r, g, b, alpha);
+ cairo_pattern_add_color_stop_rgba(grad, 0.55, r, g, b, alpha * 0.30);
+ cairo_pattern_add_color_stop_rgba(grad, 1.0, r, g, b, 0.0);
+ cairo_set_source(cr, grad);
+ cairo_paint(cr);
+ cairo_pattern_destroy(grad);
+}
+
+/* Draw the checkered "finish line" node: checkerboard behind the unit
+ * number and the same lock glyph the locked units use. */
+void draw_finish_cell(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ const double s = 11.0;
+ const Rgb gray = color_from_hex(app_theme.overlay);
+ const Rgb fin_dark = color_from_hex(app_theme.finish_dark);
+ const Rgb fin_light = color_from_hex(app_theme.finish_light);
+ const Rgb fin_stroke = color_from_hex(app_theme.finish_stroke);
+ int n = GPOINTER_TO_INT(data);
+ double cx = width / 2.0;
+ double cy = height / 2.0;
+ double r = MIN(width, height) / 2.0 - 2.0;
+ double start_x = cx - r;
+ double start_y = cy - r;
+ int cols = (int)ceil(2.0 * r / s);
+ int rows = (int)ceil(2.0 * r / s);
+ char buf[8];
+ PangoLayout *layout;
+ PangoFontDescription *fd;
+ PangoRectangle ink;
+ double num_cy;
+ int ix, iy;
+
+ (void)area;
+
+ cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cr);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+
+ cairo_save(cr);
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
+ cairo_clip(cr);
+
+ cairo_rectangle(cr, start_x, start_y, 2.0 * r, 2.0 * r);
+ cairo_set_source_rgb(cr, fin_dark.r, fin_dark.g, fin_dark.b);
+ cairo_fill(cr);
+
+ for (iy = 0; iy < rows; iy++) {
+ for (ix = 0; ix < cols; ix++) {
+ if (((ix + iy) & 1) == 0)
+ continue;
+ cairo_rectangle(cr, start_x + ix * s, start_y + iy * s, s, s);
+ cairo_set_source_rgb(cr, fin_light.r, fin_light.g, fin_light.b);
+ cairo_fill(cr);
+ }
+ }
+
+ /* unit number, styled like the other locked nodes */
+ g_snprintf(buf, sizeof(buf), "%d", n);
+ layout = pango_cairo_create_layout(cr);
+ fd = pango_font_description_new();
+ pango_font_description_set_family(fd, "sans");
+ pango_font_description_set_weight(fd, PANGO_WEIGHT_ULTRABOLD);
+ pango_font_description_set_size(fd, 22 * PANGO_SCALE);
+ pango_layout_set_font_description(layout, fd);
+ pango_layout_set_text(layout, buf, -1);
+ pango_layout_get_pixel_extents(layout, &ink, NULL);
+
+ cairo_set_source_rgb(cr, gray.r, gray.g, gray.b);
+ num_cy = cy - 3.0;
+ cairo_move_to(cr, cx - ink.width / 2.0 - ink.x,
+ num_cy - ink.height / 2.0 - ink.y);
+ pango_cairo_show_layout(cr, layout);
+
+ /* lock glyph below the number, as on the other locked units */
+ lock_paint(cr, cx - 8.0, num_cy + ink.height / 2.0 + 6.0, 16.0, &gray);
+
+ g_object_unref(layout);
+ pango_font_description_free(fd);
+ cairo_restore(cr);
+
+ cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
+ cairo_set_source_rgb(cr, fin_stroke.r, fin_stroke.g, fin_stroke.b);
+ cairo_set_line_width(cr, 2.0);
+ cairo_stroke(cr);
+}
+
+void road_point(double t, double *ox, double *oy) {
+ int k = (int)t;
+ double u = t - (double)k;
+ double u2, u3;
+ double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
+
+ if (k < 0) { k = 0; u = 0.0; }
+ if (k >= NUM_UNITS - 1) { k = NUM_UNITS - 2; u = 1.0; }
+
+ p1x = road_cx[k]; p1y = road_cy[k];
+ p2x = road_cx[k + 1]; p2y = road_cy[k + 1];
+ if (k - 1 >= 0) { p0x = road_cx[k - 1]; p0y = road_cy[k - 1]; }
+ else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
+ if (k + 2 < NUM_UNITS) { p3x = road_cx[k + 2]; p3y = road_cy[k + 2]; }
+ else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
+
+ u2 = u * u;
+ u3 = u2 * u;
+ *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
+ + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
+ + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
+ *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
+ + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
+ + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
+}
+
+void road_path(cairo_t *cr, double t0, double t1) {
+ const double steps_per_unit = 48.0;
+ int n = (int)((t1 - t0) * steps_per_unit);
+ double x, y;
+ int s;
+
+ if (n < 1)
+ n = 1;
+ road_point(t0, &x, &y);
+ cairo_move_to(cr, x, y);
+ for (s = 1; s <= n; s++) {
+ road_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
+ cairo_line_to(cr, x, y);
+ }
+}
+
+/* Compute the serpentine node layout for the given available width. */
+void roadmap_geometry(double avail) {
+ const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_UNITS - 1);
+ int rows;
+ int r, c, i;
+
+ for (rows = 1; rows <= NUM_UNITS; rows++) {
+ int cols = (NUM_UNITS + rows - 1) / rows;
+ if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0)
+ break;
+ }
+ if (rows > NUM_UNITS)
+ rows = NUM_UNITS;
+ road_rows = rows;
+ road_cols = (NUM_UNITS + rows - 1) / rows;
+ if (road_cols < 1)
+ road_cols = 1;
+ road_cw = (int)(2.0 * ROAD_MX + (road_cols - 1) * PATH_SPAC);
+ road_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP);
+
+ for (r = 0; r < rows; r++) {
+ int base = r * road_cols;
+ int len = MIN(road_cols, NUM_UNITS - base);
+ int fwd = (r % 2) == 0;
+ for (c = 0; c < len; c++) {
+ i = base + c;
+ int cc = fwd ? c : (road_cols - 1 - c);
+ road_cx[i] = ROAD_MX + cc * PATH_SPAC;
+ road_cy[i] = ROAD_MY + r * ROAD_GAP
+ + ROAD_WAVE * sin((double)i * phase);
+ }
+ }
+}
+
+void road_apply_layout(void) {
+ int i;
+
+ if (!road_fixed)
+ return;
+
+ for (i = 0; i < NUM_UNITS; i++) {
+ if (!road_nodes[i] || !road_labels[i])
+ continue;
+ gtk_fixed_move(GTK_FIXED(road_fixed), road_nodes[i],
+ (int)(road_cx[i] - NODE_SIZE / 2.0),
+ (int)(road_cy[i] - NODE_SIZE / 2.0));
+ gtk_fixed_move(GTK_FIXED(road_fixed), road_labels[i],
+ (int)(road_cx[i] - (PATH_SPAC - 20.0) / 2.0),
+ (int)(road_cy[i] + NODE_SIZE / 2.0 + 10.0));
+ }
+
+ gtk_widget_set_size_request(road_fixed, road_cw, road_ch);
+ gtk_widget_set_size_request(road_rail, road_cw, road_ch);
+ gtk_fixed_move(GTK_FIXED(road_fixed), road_rail, 0, 0);
+ gtk_widget_queue_draw(road_rail);
+}
+
+void road_relayout(void) {
+ GtkAdjustment *hadj;
+ double avail;
+
+ if (!road_scroll)
+ return;
+ hadj = gtk_scrolled_window_get_hadjustment(
+ GTK_SCROLLED_WINDOW(road_scroll));
+ avail = gtk_adjustment_get_page_size(hadj);
+ if (avail < 1.0)
+ return;
+ roadmap_geometry(avail);
+ road_apply_layout();
+}
+
+guint road_idle;
+
+gboolean road_relayout_idle(gpointer data) {
+ (void)data;
+ road_idle = 0;
+ road_relayout();
+ return G_SOURCE_REMOVE;
+}
+
+void road_relayout_later(void) {
+ if (road_idle == 0)
+ road_idle = g_idle_add(road_relayout_idle, NULL);
+}
+
+void road_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data) {
+ (void)adj;
+ (void)ps;
+ (void)data;
+ road_relayout_later();
+}
+
+void draw_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer user_data) {
+ const double t_lit = (double)(NUM_UNLOCKED - 1);
+ const double t_end = (double)(NUM_UNITS - 1);
+ const double x0 = ROAD_MX;
+ const double x1 = (double)road_cw - ROAD_MX;
+ const Rgb mauve = color_from_hex(app_theme.accent);
+ const Rgb blue = color_from_hex(app_theme.accent3);
+ const Rgb rail = color_from_hex(app_theme.rail);
+ const Rgb muted = color_from_hex(app_theme.subtext);
+ double hx, hy;
+
+ (void)area;
+ (void)width;
+ (void)height;
+ (void)user_data;
+
+ cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cr);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
+
+ road_point((double)(NUM_UNLOCKED - 1), &hx, &hy);
+ draw_node_halo(cr, hx, hy, NODE_SIZE * 1.05,
+ mauve.r, mauve.g, mauve.b, 0.18);
+
+ cairo_new_path(cr);
+ road_path(cr, 0.0, t_end);
+ cairo_set_line_width(cr, 16);
+ cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
+ cairo_stroke(cr);
+
+ if (t_lit > 0.0) {
+ cairo_pattern_t *grad;
+
+ cairo_new_path(cr);
+ road_path(cr, 0.0, t_lit);
+
+ grad = cairo_pattern_create_linear(x0, 0.0, x1, 0.0);
+ cairo_pattern_add_color_stop_rgba(grad, 0.0,
+ mauve.r, mauve.g, mauve.b, 0.16);
+ cairo_pattern_add_color_stop_rgba(grad, 1.0,
+ blue.r, blue.g, blue.b, 0.16);
+ cairo_set_source(cr, grad);
+ cairo_set_line_width(cr, 34);
+ cairo_stroke_preserve(cr);
+ cairo_pattern_destroy(grad);
+
+ grad = cairo_pattern_create_linear(x0, 0.0, x1, 0.0);
+ cairo_pattern_add_color_stop_rgba(grad, 0.0,
+ mauve.r, mauve.g, mauve.b, 1.0);
+ cairo_pattern_add_color_stop_rgba(grad, 1.0,
+ blue.r, blue.g, blue.b, 1.0);
+ cairo_set_source(cr, grad);
+ cairo_set_line_width(cr, 16);
+ cairo_stroke_preserve(cr);
+ cairo_pattern_destroy(grad);
+ }
+
+ {
+ double start_t = t_lit > 0.0 ? t_lit : 0.0;
+
+ cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17);
+ cairo_set_dash(cr, (double[]){1.0, 26.0}, 2, 0.0);
+ cairo_new_path(cr);
+ road_path(cr, start_t, t_end);
+ cairo_stroke(cr);
+ cairo_set_dash(cr, NULL, 0, 0.0);
+ }
+}
+
+void add_path_node(GtkFixed *fixed, int index) {
+ int num = index + 1;
+ UnitCtx *u = &units[index];
+ gboolean locked = !u->unlocked;
+ GtkWidget *card;
+ GtkWidget *name;
+ char *text;
+
+ card = gtk_button_new();
+ gtk_widget_add_css_class(card, "unit-node");
+ gtk_widget_set_can_focus(card, FALSE);
+ gtk_widget_set_sensitive(card, !locked);
+ gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE);
+
+ if (index == NUM_UNITS - 1) {
+ GtkWidget *fin = gtk_drawing_area_new();
+
+ gtk_widget_add_css_class(card, "finish");
+ gtk_widget_add_css_class(card, "locked");
+ gtk_widget_set_size_request(fin, (int)NODE_SIZE, (int)NODE_SIZE);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(fin),
+ draw_finish_cell,
+ GINT_TO_POINTER(num), NULL);
+ gtk_button_set_child(GTK_BUTTON(card), fin);
+ } else {
+ GtkWidget *vbox;
+ GtkWidget *number;
+ GtkWidget *icon;
+
+ if (locked)
+ gtk_widget_add_css_class(card, "locked");
+ else
+ gtk_widget_add_css_class(card, "current");
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
+ gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
+ gtk_button_set_child(GTK_BUTTON(card), vbox);
+
+ text = g_strdup_printf("%d", num);
+ number = gtk_label_new(text);
+ g_free(text);
+ gtk_widget_add_css_class(number, "unit-number");
+ gtk_box_append(GTK_BOX(vbox), number);
+
+ if (locked) {
+ icon = icon_area_new(draw_lock_icon,
+ 0.3451, 0.3569, 0.4392, 16);
+ } else {
+ icon = icon_area_new(draw_check_icon,
+ 0.1176, 0.1176, 0.1804, 18);
+ gtk_widget_set_visible(icon, FALSE);
+ u->node = card;
+ u->node_done_icon = icon;
+ g_object_set_data_full(G_OBJECT(card), "target",
+ g_strdup(u->page), g_free);
+ g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
+ }
+ gtk_box_append(GTK_BOX(vbox), icon);
+ }
+
+ road_nodes[index] = card;
+ gtk_fixed_put(fixed, card, 0, 0);
+
+ name = gtk_label_new(u->title);
+ gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1);
+ gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
+ gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER);
+ gtk_label_set_wrap(GTK_LABEL(name), TRUE);
+ gtk_widget_add_css_class(name, "unit-name");
+ if (locked)
+ gtk_widget_add_css_class(name, "unit-name-locked");
+ road_labels[index] = name;
+ gtk_fixed_put(fixed, name, 0, 0);
+}
+
+GtkWidget *build_roadmap_page(void) {
+ GtkWidget *page;
+ GtkWidget *scroll;
+ GtkWidget *wrap;
+ GtkWidget *fixed;
+ GtkWidget *rail;
+ GtkAdjustment *ha;
+ GtkAdjustment *va;
+ int i;
+
+ roadmap_geometry(1000.0);
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_hexpand(page, TRUE);
+ gtk_widget_set_vexpand(page, TRUE);
+ gtk_widget_set_margin_start(page, 32);
+ gtk_widget_set_margin_end(page, 32);
+ gtk_widget_set_margin_top(page, 24);
+ gtk_widget_set_margin_bottom(page, 24);
+
+ gtk_box_append(GTK_BOX(page),
+ top_bar("subjects", "roadmap_title", "roadmap_sub"));
+
+ scroll = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_margin_top(scroll, 14);
+ gtk_box_append(GTK_BOX(page), scroll);
+ road_scroll = scroll;
+
+ wrap = gtk_center_box_new();
+ gtk_widget_set_vexpand(wrap, TRUE);
+ gtk_widget_set_hexpand(wrap, TRUE);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
+
+ fixed = gtk_fixed_new();
+ gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_size_request(fixed, road_cw, road_ch);
+ gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
+ road_fixed = fixed;
+
+ rail = gtk_drawing_area_new();
+ gtk_widget_set_size_request(rail, road_cw, road_ch);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_rail, NULL, NULL);
+ gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
+ road_rail = rail;
+
+ for (i = 0; i < NUM_UNITS; i++)
+ add_path_node(GTK_FIXED(fixed), i);
+
+ ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
+ va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
+ g_signal_connect(ha, "notify::page-size",
+ G_CALLBACK(road_adjust_notify), NULL);
+ g_signal_connect(va, "notify::page-size",
+ G_CALLBACK(road_adjust_notify), NULL);
+
+ road_relayout_later();
+
+ return page;
+}
+
diff --git a/settings.c b/settings.c
new file mode 100644
index 0000000..f42577e
--- /dev/null
+++ b/settings.c
@@ -0,0 +1,342 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Settings */
+/* ------------------------------------------------------------------ */
+
+void draw_theme_swatch(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ ThemeId id = (ThemeId)GPOINTER_TO_INT(data);
+ ThemePalette p = theme_palette(id, app_color_mode);
+ Rgb base = color_from_hex(p.base);
+ Rgb a1 = color_from_hex(p.accent);
+ Rgb a2 = color_from_hex(p.accent3);
+ Rgb ok = color_from_hex(p.success);
+ Rgb accent = color_from_hex(app_theme.accent);
+ gboolean selected = (app_theme_id == id);
+ double size = MIN(width, height);
+ double cx = width / 2.0;
+ double cy = height / 2.0;
+ double r = size / 2.0 - (selected ? 3.0 : 1.0);
+ double dot = MAX(3.5, r * 0.26);
+ double gap = dot * 2.05;
+
+ (void)area;
+
+ if (selected) {
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, cy, size / 2.0 - 0.5, 0.0, 2.0 * G_PI);
+ cairo_set_source_rgb(cr, accent.r, accent.g, accent.b);
+ cairo_set_line_width(cr, 2.2);
+ cairo_stroke(cr);
+ }
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
+ cairo_set_source_rgb(cr, base.r, base.g, base.b);
+ cairo_fill(cr);
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx - gap, cy, dot, 0.0, 2.0 * G_PI);
+ cairo_set_source_rgb(cr, a1.r, a1.g, a1.b);
+ cairo_fill(cr);
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, cy, dot, 0.0, 2.0 * G_PI);
+ cairo_set_source_rgb(cr, a2.r, a2.g, a2.b);
+ cairo_fill(cr);
+
+ cairo_new_path(cr);
+ cairo_arc(cr, cx + gap, cy, dot, 0.0, 2.0 * G_PI);
+ cairo_set_source_rgb(cr, ok.r, ok.g, ok.b);
+ cairo_fill(cr);
+}
+
+void on_mode_toggled(GtkToggleButton *btn, gpointer user_data) {
+ ColorMode mode = (ColorMode)GPOINTER_TO_INT(user_data);
+
+ if (!gtk_toggle_button_get_active(btn))
+ return;
+ if (app_color_mode == mode)
+ return;
+ app_color_mode = mode;
+ apply_theme();
+ save_settings();
+}
+
+void on_theme_toggled(GtkToggleButton *btn, gpointer user_data) {
+ ThemeId id = (ThemeId)GPOINTER_TO_INT(user_data);
+
+ if (!gtk_toggle_button_get_active(btn))
+ return;
+ if (app_theme_id == id)
+ return;
+ app_theme_id = id;
+ apply_theme();
+ save_settings();
+}
+
+void on_lang_toggled(GtkToggleButton *btn, gpointer user_data) {
+ UiLang lang = (UiLang)GPOINTER_TO_INT(user_data);
+
+ if (!gtk_toggle_button_get_active(btn))
+ return;
+ if (app_lang == lang)
+ return;
+ app_lang = lang;
+ apply_language();
+ save_settings();
+}
+
+GtkWidget *make_lang_chip(const char *label, UiLang lang,
+ GtkToggleButton *group) {
+ GtkWidget *btn = gtk_toggle_button_new_with_label(label);
+
+ gtk_widget_add_css_class(btn, "mode-chip");
+ gtk_widget_set_hexpand(btn, TRUE);
+ if (group)
+ gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
+ g_signal_connect(btn, "toggled", G_CALLBACK(on_lang_toggled),
+ GINT_TO_POINTER(lang));
+ return btn;
+}
+
+GtkWidget *make_mode_chip(const char *label, ColorMode mode,
+ GtkToggleButton *group) {
+ GtkWidget *btn = gtk_toggle_button_new();
+
+ i18n_bind(btn, label, 1);
+ gtk_widget_add_css_class(btn, "mode-chip");
+ gtk_widget_set_hexpand(btn, TRUE);
+ if (group)
+ gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
+ g_signal_connect(btn, "toggled", G_CALLBACK(on_mode_toggled),
+ GINT_TO_POINTER(mode));
+ return btn;
+}
+
+GtkWidget *make_theme_card(ThemeId id, GtkToggleButton *group) {
+ GtkWidget *btn;
+ GtkWidget *vbox;
+ GtkWidget *swatch;
+ GtkWidget *name;
+
+ btn = gtk_toggle_button_new();
+ gtk_widget_add_css_class(btn, "theme-card");
+ gtk_widget_set_hexpand(btn, TRUE);
+ if (group)
+ gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
+ gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
+ gtk_button_set_child(GTK_BUTTON(btn), vbox);
+
+ swatch = gtk_drawing_area_new();
+ gtk_widget_set_size_request(swatch, 44, 44);
+ gtk_widget_set_halign(swatch, GTK_ALIGN_CENTER);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(swatch),
+ draw_theme_swatch,
+ GINT_TO_POINTER(id), NULL);
+ theme_swatch_areas[id] = swatch;
+ gtk_box_append(GTK_BOX(vbox), swatch);
+
+ name = gtk_label_new(theme_names[id]);
+ gtk_widget_add_css_class(name, "theme-card-name");
+ gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(vbox), name);
+
+ g_signal_connect(btn, "toggled", G_CALLBACK(on_theme_toggled),
+ GINT_TO_POINTER(id));
+ return btn;
+}
+
+void on_settings_clicked(GtkButton *button, gpointer user_data) {
+ GtkPopover *popover = GTK_POPOVER(user_data);
+
+ (void)button;
+ gtk_popover_popup(popover);
+}
+
+GtkWidget *build_stats_button(void) {
+ GtkWidget *btn;
+ GtkWidget *icon;
+
+ btn = gtk_button_new();
+ gtk_widget_add_css_class(btn, "settings-btn");
+ gtk_widget_add_css_class(btn, "flat");
+ i18n_bind(btn, "stats", 2);
+ gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
+ gtk_widget_set_focus_on_click(btn, FALSE);
+
+ icon = gtk_drawing_area_new();
+ gtk_widget_set_size_request(icon, 18, 18);
+ gtk_widget_set_can_target(icon, FALSE);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon),
+ draw_stats_icon, NULL, NULL);
+ gtk_button_set_child(GTK_BUTTON(btn), icon);
+ g_signal_connect_swapped(btn, "state-flags-changed",
+ G_CALLBACK(gtk_widget_queue_draw), icon);
+ g_signal_connect(btn, "clicked", G_CALLBACK(on_stats_clicked), NULL);
+
+ return btn;
+}
+
+GtkWidget *build_settings_button(void) {
+ GtkWidget *btn;
+ GtkWidget *icon;
+ GtkWidget *popover;
+ GtkWidget *box;
+ GtkWidget *title;
+ GtkWidget *label;
+ GtkWidget *mode_row;
+ GtkWidget *lang_row;
+ GtkWidget *theme_grid;
+ GtkWidget *dark_btn;
+ GtkWidget *light_btn;
+ GtkWidget *cs_btn;
+ GtkWidget *en_btn;
+ GtkWidget *first_theme = NULL;
+ int i;
+
+ btn = gtk_button_new();
+ gtk_widget_add_css_class(btn, "settings-btn");
+ gtk_widget_add_css_class(btn, "flat");
+ i18n_bind(btn, "settings", 2);
+ gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
+ gtk_widget_set_focus_on_click(btn, FALSE);
+
+ icon = gtk_drawing_area_new();
+ gtk_widget_set_size_request(icon, 18, 18);
+ gtk_widget_set_can_target(icon, FALSE);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon),
+ draw_settings_icon, NULL, NULL);
+ gtk_button_set_child(GTK_BUTTON(btn), icon);
+ g_signal_connect_swapped(btn, "state-flags-changed",
+ G_CALLBACK(gtk_widget_queue_draw), icon);
+
+ popover = gtk_popover_new();
+ gtk_widget_add_css_class(popover, "settings-popover");
+ gtk_popover_set_has_arrow(GTK_POPOVER(popover), TRUE);
+ gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
+ gtk_widget_set_parent(popover, btn);
+ g_signal_connect_swapped(btn, "destroy",
+ G_CALLBACK(gtk_widget_unparent), popover);
+ g_signal_connect(btn, "clicked", G_CALLBACK(on_settings_clicked), popover);
+
+ box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
+ gtk_widget_add_css_class(box, "settings-box");
+
+ title = gtk_label_new(NULL);
+ i18n_bind(title, "settings_title", 0);
+ gtk_widget_set_halign(title, GTK_ALIGN_START);
+ gtk_widget_add_css_class(title, "settings-title");
+ gtk_box_append(GTK_BOX(box), title);
+
+ label = gtk_label_new(NULL);
+ i18n_bind(label, "mode", 0);
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_widget_add_css_class(label, "settings-label");
+ gtk_widget_set_margin_top(label, 4);
+ gtk_box_append(GTK_BOX(box), label);
+
+ mode_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_widget_add_css_class(mode_row, "settings-seg");
+ gtk_box_append(GTK_BOX(box), mode_row);
+
+ dark_btn = make_mode_chip("mode_dark", MODE_DARK, NULL);
+ light_btn = make_mode_chip("mode_light", MODE_LIGHT,
+ GTK_TOGGLE_BUTTON(dark_btn));
+ g_signal_handlers_block_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_mode_toggled, NULL);
+ g_signal_handlers_block_matched(light_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_mode_toggled, NULL);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dark_btn),
+ app_color_mode == MODE_DARK);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(light_btn),
+ app_color_mode == MODE_LIGHT);
+ g_signal_handlers_unblock_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_mode_toggled, NULL);
+ g_signal_handlers_unblock_matched(light_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_mode_toggled, NULL);
+ gtk_box_append(GTK_BOX(mode_row), dark_btn);
+ gtk_box_append(GTK_BOX(mode_row), light_btn);
+
+ label = gtk_label_new(NULL);
+ i18n_bind(label, "theme", 0);
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_widget_add_css_class(label, "settings-label");
+ gtk_widget_set_margin_top(label, 2);
+ gtk_box_append(GTK_BOX(box), label);
+
+ theme_grid = gtk_flow_box_new();
+ gtk_widget_add_css_class(theme_grid, "theme-grid");
+ gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(theme_grid),
+ GTK_SELECTION_NONE);
+ gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
+ gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
+ gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(theme_grid), 8);
+ gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(theme_grid), 8);
+ gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(theme_grid), TRUE);
+ gtk_widget_set_hexpand(theme_grid, TRUE);
+ gtk_box_append(GTK_BOX(box), theme_grid);
+
+ for (i = 0; i < THEME_COUNT; i++) {
+ GtkWidget *card = make_theme_card(
+ (ThemeId)i,
+ first_theme ? GTK_TOGGLE_BUTTON(first_theme) : NULL);
+ if (!first_theme)
+ first_theme = card;
+ if (app_theme_id == (ThemeId)i) {
+ g_signal_handlers_block_matched(card, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_theme_toggled, NULL);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(card), TRUE);
+ g_signal_handlers_unblock_matched(card, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_theme_toggled, NULL);
+ }
+ gtk_flow_box_insert(GTK_FLOW_BOX(theme_grid), card, -1);
+ }
+
+ label = gtk_label_new(NULL);
+ i18n_bind(label, "language", 0);
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_widget_add_css_class(label, "settings-label");
+ gtk_widget_set_margin_top(label, 4);
+ gtk_box_append(GTK_BOX(box), label);
+
+ lang_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
+ gtk_widget_add_css_class(lang_row, "settings-seg");
+ gtk_box_append(GTK_BOX(box), lang_row);
+
+ cs_btn = make_lang_chip("Čeština", LANG_CS, NULL);
+ en_btn = make_lang_chip("English", LANG_EN, GTK_TOGGLE_BUTTON(cs_btn));
+ g_signal_handlers_block_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_lang_toggled, NULL);
+ g_signal_handlers_block_matched(en_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_lang_toggled, NULL);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs_btn),
+ app_lang == LANG_CS);
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(en_btn),
+ app_lang == LANG_EN);
+ g_signal_handlers_unblock_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_lang_toggled, NULL);
+ g_signal_handlers_unblock_matched(en_btn, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_lang_toggled, NULL);
+ gtk_box_append(GTK_BOX(lang_row), cs_btn);
+ gtk_box_append(GTK_BOX(lang_row), en_btn);
+
+ {
+ GtkWidget *sw = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(sw), 470);
+ gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(sw), FALSE);
+ gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(sw), TRUE);
+ gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(sw), TRUE);
+ gtk_widget_set_hexpand(sw, FALSE);
+ gtk_widget_set_vexpand(sw, FALSE);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), box);
+ gtk_popover_set_child(GTK_POPOVER(popover), sw);
+ }
+
+ return btn;
+}
diff --git a/stats.c b/stats.c
new file mode 100644
index 0000000..12ae25c
--- /dev/null
+++ b/stats.c
@@ -0,0 +1,399 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Statistics */
+/* ------------------------------------------------------------------ */
+
+GtkWidget *make_back_button(const char *target);
+void on_nav_clicked(GtkButton *button, gpointer user_data);
+
+
+StatsUi stats_ui;
+char *stats_return_page;
+
+int unit_done_count(const UnitCtx *u) {
+ int n = 0;
+
+ if (!u)
+ return 0;
+ for (int i = 1; i <= u->n_ex; i++) {
+ if (u->done[i])
+ n++;
+ }
+ return n;
+}
+
+
+/* Sum progress over units[first .. first+n-1]. Locked placeholder units and
+ * the finish node (no playable exercises) are ignored. */
+void progress_for_units(int first, int n, ProgressSum *out) {
+ for (int j = 0; j < n; j++) {
+ UnitCtx *u;
+ int d;
+
+ if (first < 0 || first + j >= NUM_UNITS)
+ continue;
+ u = &units[first + j];
+ if (!u->unlocked || u->n_ex <= 0)
+ continue;
+ d = unit_done_count(u);
+ out->done_ex += d;
+ out->total_ex += u->n_ex;
+ out->open_units++;
+ if (d == u->n_ex)
+ out->done_units++;
+ }
+}
+
+void refresh_stats_ui(void) {
+ ProgressSum all = {0};
+ int pct;
+ char *tmp;
+
+ if (!stats_ui.ex_value)
+ return;
+
+ for (int s = 0; s < NUM_SUBJECTS; s++)
+ progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
+
+ pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0;
+
+ tmp = g_strdup_printf(tr("stats_ex_fmt"), all.done_ex, all.total_ex);
+ gtk_label_set_text(GTK_LABEL(stats_ui.ex_value), tmp);
+ g_free(tmp);
+
+ tmp = g_strdup_printf(tr("stats_pct_fmt"), pct);
+ gtk_label_set_text(GTK_LABEL(stats_ui.pct_value), tmp);
+ g_free(tmp);
+
+ tmp = g_strdup_printf(tr("stats_units_fmt"), all.done_units,
+ all.open_units);
+ gtk_label_set_text(GTK_LABEL(stats_ui.units_value), tmp);
+ g_free(tmp);
+
+ gtk_progress_bar_set_fraction(
+ GTK_PROGRESS_BAR(stats_ui.overall_bar),
+ all.total_ex > 0 ? (double)all.done_ex / (double)all.total_ex : 0.0);
+
+ for (int s = 0; s < NUM_SUBJECTS; s++) {
+ GtkWidget *count = stats_ui.subj[s].count;
+ GtkWidget *bar = stats_ui.subj[s].bar;
+ ProgressSum sp = {0};
+
+ if (!count || !bar)
+ continue;
+ progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp);
+ if (sub_unit_count[s] > 0) {
+ tmp = g_strdup_printf(tr("stats_ex_fmt"), sp.done_ex, sp.total_ex);
+ gtk_label_set_text(GTK_LABEL(count), tmp);
+ g_free(tmp);
+ gtk_progress_bar_set_fraction(
+ GTK_PROGRESS_BAR(bar),
+ sp.total_ex > 0 ? (double)sp.done_ex / (double)sp.total_ex
+ : 0.0);
+ } else {
+ gtk_label_set_text(GTK_LABEL(count), tr("stats_locked"));
+ gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), 0.0);
+ }
+ }
+
+ for (int i = 0; i < NUM_UNITS; i++) {
+ UnitCtx *u = &units[i];
+ GtkWidget *count = stats_ui.unit_count[i];
+ GtkWidget *badge = stats_ui.unit_badge[i];
+ GtkWidget *bar = stats_ui.unit_bar[i];
+ GtkWidget *row = stats_ui.unit_row[i];
+ gboolean unlocked = u->unlocked && u->n_ex > 0;
+ int d = unlocked ? unit_done_count(u) : 0;
+ gboolean complete = unlocked && d == u->n_ex;
+
+ if (!count || !bar || !row)
+ continue;
+
+ gtk_widget_remove_css_class(row, "done");
+ gtk_widget_remove_css_class(row, "locked");
+ gtk_widget_remove_css_class(bar, "done");
+
+ if (!unlocked) {
+ gtk_widget_add_css_class(row, "locked");
+ gtk_label_set_text(GTK_LABEL(count), tr("stats_locked"));
+ gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), 0.0);
+ if (badge)
+ gtk_widget_set_visible(badge, FALSE);
+ continue;
+ }
+
+ tmp = g_strdup_printf(tr("stats_ex_fmt"), d, u->n_ex);
+ gtk_label_set_text(GTK_LABEL(count), tmp);
+ g_free(tmp);
+ gtk_progress_bar_set_fraction(
+ GTK_PROGRESS_BAR(bar),
+ u->n_ex > 0 ? (double)d / (double)u->n_ex : 0.0);
+
+ if (complete) {
+ gtk_widget_add_css_class(row, "done");
+ gtk_widget_add_css_class(bar, "done");
+ if (badge) {
+ gtk_label_set_text(GTK_LABEL(badge), tr("stats_complete"));
+ gtk_widget_set_visible(badge, TRUE);
+ }
+ } else if (badge) {
+ gtk_widget_set_visible(badge, FALSE);
+ }
+ }
+}
+
+void on_stats_back_clicked(GtkButton *button, gpointer user_data) {
+ const char *target = stats_return_page;
+
+ (void)button;
+ (void)user_data;
+ if (!target || !target[0])
+ target = "subjects";
+ gtk_stack_set_visible_child_name(main_stack, target);
+}
+
+void on_stats_clicked(GtkButton *button, gpointer user_data) {
+ const char *cur;
+
+ (void)button;
+ (void)user_data;
+
+ cur = gtk_stack_get_visible_child_name(main_stack);
+ if (cur && g_strcmp0(cur, "stats") != 0) {
+ g_free(stats_return_page);
+ stats_return_page = g_strdup(cur);
+ }
+ refresh_stats_ui();
+ gtk_stack_set_visible_child_name(main_stack, "stats");
+}
+
+GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out) {
+ GtkWidget *card;
+ GtkWidget *value;
+ GtkWidget *label;
+
+ card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_add_css_class(card, "stats-metric");
+ gtk_widget_set_hexpand(card, TRUE);
+
+ value = gtk_label_new("0");
+ gtk_widget_set_halign(value, GTK_ALIGN_START);
+ gtk_widget_add_css_class(value, "stats-metric-value");
+ gtk_box_append(GTK_BOX(card), value);
+
+ label = gtk_label_new(NULL);
+ i18n_bind(label, label_key, 0);
+ gtk_widget_set_halign(label, GTK_ALIGN_START);
+ gtk_widget_add_css_class(label, "stats-metric-label");
+ gtk_box_append(GTK_BOX(card), label);
+
+ *value_out = value;
+ return card;
+}
+
+/* Build one unit row (index, name, badge, count, bar) for the stats list.
+ * `num` is the row number shown (1-based within its subject). */
+GtkWidget *make_unit_stats_row(UnitCtx *u, int num, int global) {
+ GtkWidget *row;
+ GtkWidget *head;
+ GtkWidget *left;
+ GtkWidget *idx;
+ GtkWidget *name;
+ GtkWidget *right;
+ GtkWidget *count;
+ GtkWidget *badge;
+ GtkWidget *bar;
+ char text[8];
+
+ row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
+ gtk_widget_add_css_class(row, "stats-unit-row");
+ stats_ui.unit_row[global] = row;
+
+ head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_box_append(GTK_BOX(row), head);
+
+ left = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
+ gtk_widget_set_hexpand(left, TRUE);
+ gtk_box_append(GTK_BOX(head), left);
+
+ g_snprintf(text, sizeof(text), "%d", num);
+ idx = gtk_label_new(text);
+ gtk_widget_add_css_class(idx, "stats-unit-index");
+ gtk_widget_set_valign(idx, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(left), idx);
+
+ name = gtk_label_new(u->title);
+ gtk_widget_set_halign(name, GTK_ALIGN_START);
+ gtk_widget_set_hexpand(name, TRUE);
+ gtk_label_set_xalign(GTK_LABEL(name), 0.0);
+ gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
+ gtk_widget_add_css_class(name, "stats-unit-name");
+ gtk_box_append(GTK_BOX(left), name);
+
+ right = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
+ gtk_widget_set_valign(right, GTK_ALIGN_CENTER);
+ gtk_box_append(GTK_BOX(head), right);
+
+ badge = gtk_label_new(NULL);
+ gtk_widget_add_css_class(badge, "stats-unit-badge");
+ gtk_widget_set_visible(badge, FALSE);
+ gtk_box_append(GTK_BOX(right), badge);
+ stats_ui.unit_badge[global] = badge;
+
+ count = gtk_label_new("");
+ gtk_widget_add_css_class(count, "stats-unit-count");
+ gtk_box_append(GTK_BOX(right), count);
+ stats_ui.unit_count[global] = count;
+
+ bar = gtk_progress_bar_new();
+ gtk_widget_add_css_class(bar, "stats-unit-bar");
+ gtk_widget_set_hexpand(bar, TRUE);
+ gtk_box_append(GTK_BOX(row), bar);
+ stats_ui.unit_bar[global] = bar;
+
+ return row;
+}
+
+GtkWidget *build_stats_page(void) {
+ GtkWidget *outer;
+ GtkWidget *scroll;
+ GtkWidget *page;
+ GtkWidget *top;
+ GtkWidget *back;
+ GtkWidget *center;
+ GtkWidget *heading;
+ GtkWidget *sub;
+ GtkWidget *summary;
+ GtkWidget *section;
+ GtkWidget *list;
+
+ outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_add_css_class(outer, "stats-page");
+
+ top = gtk_center_box_new();
+ gtk_widget_set_margin_top(top, 2);
+ gtk_widget_set_margin_bottom(top, 6);
+ gtk_box_append(GTK_BOX(outer), top);
+
+ back = make_back_button("subjects");
+ g_signal_handlers_disconnect_matched(back, G_SIGNAL_MATCH_FUNC,
+ 0, 0, NULL, on_nav_clicked, NULL);
+ g_signal_connect(back, "clicked", G_CALLBACK(on_stats_back_clicked), NULL);
+ gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
+
+ center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
+ gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
+ gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
+
+ heading = gtk_label_new(NULL);
+ i18n_bind(heading, "stats_title", 0);
+ gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(heading, "roadmap-title");
+ gtk_box_append(GTK_BOX(center), heading);
+
+ sub = gtk_label_new(NULL);
+ i18n_bind(sub, "stats_sub", 0);
+ gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(sub, "roadmap-sub");
+ gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
+ gtk_box_append(GTK_BOX(center), sub);
+
+ scroll = gtk_scrolled_window_new();
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_hexpand(scroll, TRUE);
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
+ gtk_box_append(GTK_BOX(outer), scroll);
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_halign(page, GTK_ALIGN_FILL);
+ gtk_widget_set_margin_start(page, 4);
+ gtk_widget_set_margin_end(page, 4);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), page);
+
+ summary = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_widget_add_css_class(summary, "stats-summary");
+ gtk_widget_set_halign(summary, GTK_ALIGN_FILL);
+ gtk_box_append(GTK_BOX(page), summary);
+
+ gtk_box_append(GTK_BOX(summary),
+ stats_metric_card("stats_ex_label", &stats_ui.ex_value));
+ gtk_box_append(GTK_BOX(summary),
+ stats_metric_card("stats_pct_label", &stats_ui.pct_value));
+ gtk_box_append(GTK_BOX(summary),
+ stats_metric_card("stats_units_label",
+ &stats_ui.units_value));
+
+ stats_ui.overall_bar = gtk_progress_bar_new();
+ gtk_widget_add_css_class(stats_ui.overall_bar, "stats-overall-bar");
+ gtk_widget_set_hexpand(stats_ui.overall_bar, TRUE);
+ gtk_box_append(GTK_BOX(page), stats_ui.overall_bar);
+
+ section = gtk_label_new(NULL);
+ i18n_bind(section, "stats_section", 0);
+ gtk_widget_set_halign(section, GTK_ALIGN_START);
+ gtk_widget_add_css_class(section, "stats-section");
+ gtk_box_append(GTK_BOX(page), section);
+
+ list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_box_append(GTK_BOX(page), list);
+
+ for (int s = 0; s < NUM_SUBJECTS; s++) {
+ GtkWidget *subject;
+ GtkWidget *head;
+ GtkWidget *name;
+ GtkWidget *count;
+ GtkWidget *bar;
+ gboolean has_units = sub_unit_count[s] > 0;
+
+ subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_add_css_class(subject, "stats-subject");
+ gtk_box_append(GTK_BOX(list), subject);
+
+ head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
+ gtk_box_append(GTK_BOX(subject), head);
+
+ name = gtk_label_new(NULL);
+ i18n_bind(name, sub_keys[s], 0);
+ gtk_widget_set_halign(name, GTK_ALIGN_START);
+ gtk_widget_set_hexpand(name, TRUE);
+ gtk_label_set_xalign(GTK_LABEL(name), 0.0);
+ gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
+ gtk_widget_add_css_class(name, "stats-subject-name");
+ if (!has_units)
+ gtk_widget_add_css_class(name, "unit-name-locked");
+ gtk_box_append(GTK_BOX(head), name);
+ stats_ui.subj[s].name = name;
+
+ count = gtk_label_new(NULL);
+ gtk_widget_add_css_class(count, "stats-subject-count");
+ if (!has_units)
+ gtk_widget_add_css_class(count, "locked");
+ gtk_box_append(GTK_BOX(head), count);
+ stats_ui.subj[s].count = count;
+
+ bar = gtk_progress_bar_new();
+ gtk_widget_add_css_class(bar, "stats-subject-bar");
+ gtk_widget_set_hexpand(bar, TRUE);
+ gtk_box_append(GTK_BOX(subject), bar);
+ stats_ui.subj[s].bar = bar;
+
+ if (has_units) {
+ GtkWidget *units_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+
+ gtk_widget_add_css_class(units_box, "stats-units");
+ gtk_box_append(GTK_BOX(subject), units_box);
+ stats_ui.subj[s].units_box = units_box;
+
+ for (int j = 0; j < sub_unit_count[s]; j++) {
+ int gi = sub_unit_start[s] + j;
+
+ gtk_box_append(GTK_BOX(units_box),
+ make_unit_stats_row(&units[gi], j + 1, gi));
+ }
+ }
+ }
+
+ return outer;
+}
diff --git a/subjects.c b/subjects.c
new file mode 100644
index 0000000..95fd331
--- /dev/null
+++ b/subjects.c
@@ -0,0 +1,366 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Subjects page ("Předměty") - serpentine subject map */
+/* ------------------------------------------------------------------ */
+
+
+/* Index 0 is Deutsch and is the only unlocked subject for now. */
+const char *sub_keys[NUM_SUBJECTS] = {
+ "Deutsch",
+ "Správa počítačových sítí",
+ "Občanská nauka",
+ "English",
+ "Matematika",
+ "Fyzika",
+ "Základy Přírodopisných věd",
+ "Technická grafika",
+ "Prezentační grafika",
+ "Český jazyk a literatura",
+ "Programování",
+ "Technické vybavení",
+ "Programové vybavení",
+};
+
+/* Slice of the global `units[]` table that each subject owns. A start of -1
+ * (or a count of 0) means the subject has no content yet; such subjects show
+ * as locked on the statistics page until their units are added. */
+const int sub_unit_start[NUM_SUBJECTS] = {
+ 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
+};
+const int sub_unit_count[NUM_SUBJECTS] = {
+ NUM_UNITS, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+GtkWidget *sub_scroll;
+GtkWidget *sub_fixed;
+GtkWidget *sub_rail;
+GtkWidget *sub_cells[NUM_SUBJECTS];
+GtkWidget *sub_labels[NUM_SUBJECTS];
+double sub_cx[NUM_SUBJECTS];
+double sub_cy[NUM_SUBJECTS];
+int sub_rows = 1;
+int sub_cols = NUM_SUBJECTS;
+int sub_cw = (int)(2.0 * SUB_MX + (NUM_SUBJECTS - 1) * SUB_SPAC);
+int sub_ch = (int)(2.0 * SUB_MY);
+guint sub_idle;
+
+void sub_layout_geometry(double avail) {
+ const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_SUBJECTS - 1);
+ int rows;
+ int r, c, i;
+
+ for (rows = 1; rows <= NUM_SUBJECTS; rows++) {
+ int cols = (NUM_SUBJECTS + rows - 1) / rows;
+ if (2.0 * SUB_MX + (cols - 1) * SUB_SPAC <= avail + 1.0)
+ break;
+ }
+ if (rows > NUM_SUBJECTS)
+ rows = NUM_SUBJECTS;
+ sub_rows = rows;
+ sub_cols = (NUM_SUBJECTS + rows - 1) / rows;
+ if (sub_cols < 1)
+ sub_cols = 1;
+ sub_cw = (int)(2.0 * SUB_MX + (sub_cols - 1) * SUB_SPAC);
+ sub_ch = (int)(2.0 * SUB_MY + (rows - 1) * SUB_GAP);
+
+ for (r = 0; r < rows; r++) {
+ int base = r * sub_cols;
+ int len = MIN(sub_cols, NUM_SUBJECTS - base);
+ int fwd = (r % 2) == 0;
+ for (c = 0; c < len; c++) {
+ i = base + c;
+ int cc = fwd ? c : (sub_cols - 1 - c);
+ sub_cx[i] = SUB_MX + cc * SUB_SPAC;
+ sub_cy[i] = SUB_MY + r * SUB_GAP
+ + SUB_WAVE * sin((double)i * phase);
+ }
+ }
+}
+
+void sub_point(double t, double *ox, double *oy) {
+ int k = (int)t;
+ double u = t - (double)k;
+ double u2, u3;
+ double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
+
+ if (k < 0) { k = 0; u = 0.0; }
+ if (k >= NUM_SUBJECTS - 1) { k = NUM_SUBJECTS - 2; u = 1.0; }
+
+ p1x = sub_cx[k]; p1y = sub_cy[k];
+ p2x = sub_cx[k + 1]; p2y = sub_cy[k + 1];
+ if (k - 1 >= 0) { p0x = sub_cx[k - 1]; p0y = sub_cy[k - 1]; }
+ else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
+ if (k + 2 < NUM_SUBJECTS) { p3x = sub_cx[k + 2]; p3y = sub_cy[k + 2]; }
+ else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
+
+ u2 = u * u;
+ u3 = u2 * u;
+ *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
+ + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
+ + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
+ *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
+ + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
+ + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
+}
+
+void sub_path(cairo_t *cr, double t0, double t1) {
+ const double steps_per_unit = 48.0;
+ int n = (int)((t1 - t0) * steps_per_unit);
+ double x, y;
+ int s;
+
+ if (n < 1)
+ n = 1;
+ sub_point(t0, &x, &y);
+ cairo_move_to(cr, x, y);
+ for (s = 1; s <= n; s++) {
+ sub_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
+ cairo_line_to(cr, x, y);
+ }
+}
+
+void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer user_data) {
+ const double t_end = (double)(NUM_SUBJECTS - 1);
+ const Rgb rail = color_from_hex(app_theme.rail);
+ const Rgb mauve = color_from_hex(app_theme.accent);
+ double hx, hy;
+
+ (void)area;
+ (void)width;
+ (void)height;
+ (void)user_data;
+
+ cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cr);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
+
+ sub_point(0.0, &hx, &hy);
+ draw_node_halo(cr, hx, hy, SUB_BUBBLE * 1.05,
+ mauve.r, mauve.g, mauve.b, 0.18);
+
+ cairo_new_path(cr);
+ sub_path(cr, 0.0, t_end);
+ cairo_set_line_width(cr, 14);
+ cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
+ cairo_stroke(cr);
+}
+
+void sub_apply_layout(void) {
+ int i;
+
+ if (!sub_fixed)
+ return;
+
+ for (i = 0; i < NUM_SUBJECTS; i++) {
+ if (sub_cells[i])
+ gtk_fixed_move(GTK_FIXED(sub_fixed), sub_cells[i],
+ (int)(sub_cx[i] - SUB_BUBBLE / 2.0),
+ (int)(sub_cy[i] - SUB_BUBBLE / 2.0));
+ if (sub_labels[i])
+ gtk_fixed_move(GTK_FIXED(sub_fixed), sub_labels[i],
+ (int)(sub_cx[i] - (SUB_SPAC - 24.0) / 2.0),
+ (int)(sub_cy[i] + SUB_BUBBLE / 2.0 + 10.0));
+ }
+
+ gtk_widget_set_size_request(sub_fixed, sub_cw, sub_ch);
+ gtk_widget_set_size_request(sub_rail, sub_cw, sub_ch);
+ gtk_fixed_move(GTK_FIXED(sub_fixed), sub_rail, 0, 0);
+ gtk_widget_queue_draw(sub_rail);
+}
+
+void sub_relayout(void) {
+ GtkAdjustment *hadj;
+ double avail;
+
+ if (!sub_scroll)
+ return;
+ hadj = gtk_scrolled_window_get_hadjustment(
+ GTK_SCROLLED_WINDOW(sub_scroll));
+ avail = gtk_adjustment_get_page_size(hadj);
+ if (avail < 1.0)
+ return;
+ sub_layout_geometry(avail);
+ sub_apply_layout();
+}
+
+gboolean sub_relayout_idle(gpointer data) {
+ (void)data;
+ sub_idle = 0;
+ sub_relayout();
+ return G_SOURCE_REMOVE;
+}
+
+void sub_relayout_later(void) {
+ if (sub_idle == 0)
+ sub_idle = g_idle_add(sub_relayout_idle, NULL);
+}
+
+void sub_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data) {
+ (void)adj;
+ (void)ps;
+ (void)data;
+ sub_relayout_later();
+}
+
+/* German flag (black / red / gold) drawn in a clipped circle. */
+void draw_german_flag(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer data) {
+ double cx = width / 2.0;
+ double cy = height / 2.0;
+ double r = MIN(width, height) / 2.0 - 2.0;
+ double band = (double)height / 3.0;
+
+ (void)area;
+ (void)data;
+
+ cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cr);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+
+ cairo_save(cr);
+ cairo_new_path(cr);
+ cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
+ cairo_clip(cr);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0.0, 0.0, (double)width, band);
+ cairo_set_source_rgb(cr, 0.18, 0.18, 0.19);
+ cairo_fill(cr);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0.0, band, (double)width, band);
+ cairo_set_source_rgb(cr, 0.72, 0.27, 0.24);
+ cairo_fill(cr);
+
+ cairo_new_path(cr);
+ cairo_rectangle(cr, 0.0, 2.0 * band, (double)width, band);
+ cairo_set_source_rgb(cr, 0.84, 0.70, 0.32);
+ cairo_fill(cr);
+
+ cairo_restore(cr);
+}
+
+GtkWidget *build_subjects_page(void) {
+ GtkWidget *page;
+ GtkWidget *scroll;
+ GtkWidget *wrap;
+ GtkWidget *fixed;
+ GtkWidget *rail;
+ GtkAdjustment *ha;
+ GtkAdjustment *va;
+ int i;
+
+ sub_layout_geometry(1000.0);
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_hexpand(page, TRUE);
+ gtk_widget_set_vexpand(page, TRUE);
+ gtk_widget_set_margin_start(page, 32);
+ gtk_widget_set_margin_end(page, 32);
+ gtk_widget_set_margin_top(page, 24);
+ gtk_widget_set_margin_bottom(page, 24);
+
+ gtk_box_append(GTK_BOX(page),
+ top_bar("welcome", "subjects_title", "subjects_sub"));
+
+ scroll = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_margin_top(scroll, 14);
+ gtk_box_append(GTK_BOX(page), scroll);
+ sub_scroll = scroll;
+
+ wrap = gtk_center_box_new();
+ gtk_widget_set_vexpand(wrap, TRUE);
+ gtk_widget_set_hexpand(wrap, TRUE);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
+
+ fixed = gtk_fixed_new();
+ gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_size_request(fixed, sub_cw, sub_ch);
+ gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
+ sub_fixed = fixed;
+
+ rail = gtk_drawing_area_new();
+ gtk_widget_set_size_request(rail, sub_cw, sub_ch);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_sub_rail,
+ NULL, NULL);
+ gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
+ sub_rail = rail;
+
+ for (i = 0; i < NUM_SUBJECTS; i++) {
+ GtkWidget *card;
+ GtkWidget *lbl;
+
+ card = gtk_button_new();
+ gtk_widget_add_css_class(card, "unit-node");
+ gtk_widget_set_can_focus(card, FALSE);
+ gtk_widget_set_size_request(card, (int)SUB_BUBBLE, (int)SUB_BUBBLE);
+
+ if (i == 0) {
+ GtkWidget *flag = gtk_drawing_area_new();
+ gtk_widget_add_css_class(card, "current");
+ gtk_widget_set_size_request(flag, (int)SUB_BUBBLE,
+ (int)SUB_BUBBLE);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(flag),
+ draw_german_flag, NULL, NULL);
+ gtk_button_set_child(GTK_BUTTON(card), flag);
+ g_object_set_data_full(G_OBJECT(card), "target",
+ g_strdup("roadmap"), g_free);
+ g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
+ } else if (i == NET_SUBJ) {
+ GtkWidget *wifi = gtk_drawing_area_new();
+ gtk_widget_add_css_class(card, "current");
+ gtk_widget_set_size_request(wifi, (int)SUB_BUBBLE,
+ (int)SUB_BUBBLE);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(wifi),
+ draw_wifi_icon, NULL, NULL);
+ gtk_button_set_child(GTK_BUTTON(card), wifi);
+ g_object_set_data_full(G_OBJECT(card), "target",
+ g_strdup("netmap"), g_free);
+ g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
+ } else {
+ GtkWidget *lock;
+ gtk_widget_add_css_class(card, "locked");
+ gtk_widget_set_sensitive(card, FALSE);
+ lock = icon_area_new(draw_lock_icon,
+ 0.3451, 0.3569, 0.4392, 26);
+ gtk_button_set_child(GTK_BUTTON(card), lock);
+ }
+
+ sub_cells[i] = card;
+ gtk_fixed_put(GTK_FIXED(fixed), card, 0, 0);
+
+ lbl = gtk_label_new(NULL);
+ i18n_bind(lbl, sub_keys[i], 0);
+ gtk_widget_set_size_request(lbl, (int)(SUB_SPAC - 24.0), -1);
+ gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER);
+ gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER);
+ gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
+ gtk_widget_add_css_class(lbl, "unit-name");
+ if (i != 0 && i != NET_SUBJ)
+ gtk_widget_add_css_class(lbl, "unit-name-locked");
+ sub_labels[i] = lbl;
+ gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0);
+ }
+
+ ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
+ va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
+ g_signal_connect(ha, "notify::page-size",
+ G_CALLBACK(sub_adjust_notify), NULL);
+ g_signal_connect(va, "notify::page-size",
+ G_CALLBACK(sub_adjust_notify), NULL);
+
+ sub_apply_layout();
+ sub_relayout_later();
+
+ return page;
+}
+
diff --git a/theme.c b/theme.c
new file mode 100644
index 0000000..8fabb00
--- /dev/null
+++ b/theme.c
@@ -0,0 +1,362 @@
+#include "maturita.h"
+
+Rgb color_from_hex(unsigned int hex) {
+ Rgb c;
+ c.r = (double)((hex >> 16) & 0xFF) / 255.0;
+ c.g = (double)((hex >> 8) & 0xFF) / 255.0;
+ c.b = (double)(hex & 0xFF) / 255.0;
+ return c;
+}
+
+/* Linear blend of two colours: t = 0 keeps a, t = 1 returns b. */
+Rgb mix_rgb(Rgb a, Rgb b, double t) {
+ Rgb c;
+ c.r = a.r + (b.r - a.r) * t;
+ c.g = a.g + (b.g - a.g) * t;
+ c.b = a.b + (b.b - a.b) * t;
+ return c;
+}
+
+ThemePalette theme_palette(ThemeId id, ColorMode mode) {
+ /* Catppuccin Mocha / Latte */
+ static const ThemePalette cat_dark = {
+ 0x11111b, 0x1e1e2e, 0x181825, 0x313244, 0x45475a, 0x585b70,
+ 0x6c7086, 0xcdd6f4, 0xa6adc8, 0xcba6f7, 0xb4befe, 0x89b4fa,
+ 0xa6e3a1, 0x94e2d5, 0xf9e2af, 0xf38ba8, 0x1e1e2e,
+ 0x2b2d40, 0x24263a, 0x383b52, 0x27293c, 0x1f202e, 0x3b3e58, 0x52556b
+ };
+ static const ThemePalette cat_light = {
+ 0xdce0e8, 0xeff1f5, 0xe6e9ef, 0xccd0da, 0xbcc0cc, 0xacb0be,
+ 0x9ca0b0, 0x4c4f69, 0x6c6f85, 0x8839ef, 0x7287fd, 0x1e66f5,
+ 0x40a02b, 0x179299, 0xdf8e1d, 0xd20f39, 0xeff1f5,
+ 0xe6e9ef, 0xdce0e8, 0xbcc0cc, 0xccd0da, 0xdce0e8, 0xbcc0cc, 0x9ca0b0
+ };
+ /* Nord */
+ static const ThemePalette nord_dark = {
+ 0x2e3440, 0x3b4252, 0x2e3440, 0x434c5e, 0x4c566a, 0x616e88,
+ 0x7b88a1, 0xeceff4, 0xd8dee9, 0x88c0d0, 0x81a1c1, 0x5e81ac,
+ 0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0x2e3440,
+ 0x434c5e, 0x3b4252, 0x4c566a, 0x2e3440, 0x2e3440, 0x434c5e, 0x4c566a
+ };
+ static const ThemePalette nord_light = {
+ 0xd8dee9, 0xeceff4, 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xb0b8c8,
+ 0x7b88a1, 0x2e3440, 0x4c566a, 0x5e81ac, 0x81a1c1, 0x5e81ac,
+ 0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0xeceff4,
+ 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xd8dee9, 0xd8dee9, 0xc7ceda, 0x7b88a1
+ };
+ /* Dracula / light soft */
+ static const ThemePalette dra_dark = {
+ 0x191a21, 0x282a36, 0x21222c, 0x44475a, 0x6272a4, 0x707eb0,
+ 0x6272a4, 0xf8f8f2, 0xbfbfb2, 0xbd93f9, 0xff79c6, 0x8be9fd,
+ 0x50fa7b, 0x8be9fd, 0xf1fa8c, 0xff5555, 0x282a36,
+ 0x383a4a, 0x2f3240, 0x44475a, 0x21222c, 0x191a21, 0x383a4a, 0x6272a4
+ };
+ static const ThemePalette dra_light = {
+ 0xd5d7e0, 0xf8f8f2, 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xbfbfb2,
+ 0x9a9a8c, 0x282a36, 0x44475a, 0x7c3aed, 0xd946a6, 0x0891b2,
+ 0x16a34a, 0x0e7490, 0xca8a04, 0xdc2626, 0xf8f8f2,
+ 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xe2e2d8, 0xd5d7e0, 0xd0d0c4, 0x9a9a8c
+ };
+ /* Rose Pine / Dawn */
+ static const ThemePalette rose_dark = {
+ 0x191724, 0x1f1d2e, 0x1f1d2e, 0x26233a, 0x403d52, 0x524f67,
+ 0x6e6a86, 0xe0def4, 0x908caa, 0xc4a7e7, 0xebbcba, 0x9ccfd8,
+ 0x31748f, 0x9ccfd8, 0xf6c177, 0xeb6f92, 0x191724,
+ 0x26233a, 0x21202e, 0x403d52, 0x191724, 0x191724, 0x26233a, 0x524f67
+ };
+ static const ThemePalette rose_light = {
+ 0xf2e9e1, 0xfaf4ed, 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xcecacd,
+ 0x9893a5, 0x575279, 0x797593, 0x907aa9, 0xd7827e, 0x56949f,
+ 0x286983, 0x56949f, 0xea9d34, 0xb4637a, 0xfaf4ed,
+ 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xf2e9e1, 0xf2e9e1, 0xdfdad9, 0x9893a5
+ };
+ /* Ocean / Tokyo Night inspired */
+ static const ThemePalette ocean_dark = {
+ 0x16161e, 0x1a1b26, 0x16161e, 0x24283b, 0x414868, 0x565f89,
+ 0x565f89, 0xc0caf5, 0xa9b1d6, 0x7aa2f7, 0xbb9af7, 0x7dcfff,
+ 0x9ece6a, 0x73daca, 0xe0af68, 0xf7768e, 0x1a1b26,
+ 0x24283b, 0x1f2335, 0x3b4261, 0x16161e, 0x16161e, 0x24283b, 0x414868
+ };
+ static const ThemePalette ocean_light = {
+ 0xd6dae8, 0xe1e2ec, 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0x9aa0b5,
+ 0x7a8199, 0x2e3446, 0x4a5168, 0x2e7de9, 0x7847bd, 0x007197,
+ 0x387068, 0x007197, 0x8c6c3e, 0xc64343, 0xe1e2ec,
+ 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0xc8c9d6, 0xd6dae8, 0xb4b6c5, 0x7a8199
+ };
+
+ /* Gruvbox */
+ static const ThemePalette gruv_dark = {
+ 0x1d2021, 0x282828, 0x1d2021, 0x3c3836, 0x504945, 0x665c54,
+ 0x928374, 0xebdbb2, 0xa89984, 0xfe8019, 0xd3869b, 0x83a598,
+ 0xb8bb26, 0x8ec07c, 0xfabd2f, 0xfb4934, 0x1d2021,
+ 0x3c3836, 0x32302f, 0x504945, 0x1d2021, 0x1d2021, 0x3c3836, 0x504945
+ };
+ static const ThemePalette gruv_light = {
+ 0xebdbb2, 0xfbf1c7, 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xbdae93,
+ 0x928374, 0x3c3836, 0x504945, 0xd65d0e, 0xb16286, 0x076678,
+ 0x79740e, 0x427b58, 0xb57614, 0x9d0006, 0xfbf1c7,
+ 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xebdbb2, 0xebdbb2, 0xd5c4a1, 0x928374
+ };
+ /* Solarized */
+ static const ThemePalette sol_dark = {
+ 0x002b36, 0x073642, 0x002b36, 0x586e75, 0x657b83, 0x839496,
+ 0x93a1a1, 0xfdf6e3, 0x93a1a1, 0x268bd2, 0x6c71c4, 0x2aa198,
+ 0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0x002b36,
+ 0x073642, 0x002b36, 0x586e75, 0x002b36, 0x002b36, 0x073642, 0x586e75
+ };
+ static const ThemePalette sol_light = {
+ 0xeee8d5, 0xfdf6e3, 0xeee8d5, 0xeee8d5, 0xe2dbc6, 0xc9c0a6,
+ 0x657b83, 0x586e75, 0x657b83, 0x268bd2, 0x6c71c4, 0x2aa198,
+ 0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0xfdf6e3,
+ 0xeee8d5, 0xe6dfc8, 0x93a1a1, 0xeee8d5, 0xeee8d5, 0x93a1a1, 0x586e75
+ };
+ /* Everforest */
+ static const ThemePalette ever_dark = {
+ 0x1e2326, 0x272e33, 0x2d353b, 0x374145, 0x4f5b58, 0x7a8478,
+ 0x859289, 0xd3c6aa, 0xa7c080, 0xa7c080, 0xd699b6, 0x7fbbb3,
+ 0xa7c080, 0x83c092, 0xdbbc7f, 0xe67e80, 0x272e33,
+ 0x374145, 0x2d353b, 0x4f5b58, 0x1e2326, 0x1e2326, 0x374145, 0x4f5b58
+ };
+ static const ThemePalette ever_light = {
+ 0xe6e2cc, 0xfdf6e3, 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xa6b0a0,
+ 0x939f91, 0x5c6a72, 0x829181, 0x8da101, 0xdf69ba, 0x35a77c,
+ 0x8da101, 0x35a77c, 0xdfa000, 0xf85552, 0xfdf6e3,
+ 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xe6e2cc, 0xe6e2cc, 0xe0dcc7, 0x939f91
+ };
+ /* Monokai */
+ static const ThemePalette mono_dark = {
+ 0x1d1e19, 0x272822, 0x1e1f1a, 0x3e3d32, 0x49483e, 0x75715e,
+ 0x75715e, 0xf8f8f2, 0xa59f85, 0xf92672, 0xae81ff, 0x66d9ef,
+ 0xa6e22e, 0xa1efe4, 0xe6db74, 0xf92672, 0x272822,
+ 0x3e3d32, 0x2e2f29, 0x49483e, 0x1d1e19, 0x1d1e19, 0x3e3d32, 0x49483e
+ };
+ static const ThemePalette mono_light = {
+ 0xe6e3d4, 0xfaf8f0, 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xb8b49e,
+ 0x8a8674, 0x2d2a2e, 0x5b5854, 0xe14775, 0x7c5cbf, 0x1c7d9b,
+ 0x5b8c2a, 0x1c7d9b, 0xb08900, 0xe14775, 0xfaf8f0,
+ 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xe6e3d4, 0xe6e3d4, 0xd6d2c0, 0x8a8674
+ };
+ /* One Dark / One Light */
+ static const ThemePalette one_dark = {
+ 0x21252b, 0x282c34, 0x21252b, 0x3b4048, 0x4b5263, 0x5c6370,
+ 0x5c6370, 0xabb2bf, 0x7f848e, 0xc678dd, 0x61afef, 0x56b6c2,
+ 0x98c379, 0x56b6c2, 0xe5c07b, 0xe06c75, 0x282c34,
+ 0x3b4048, 0x2c313a, 0x4b5263, 0x21252b, 0x21252b, 0x3b4048, 0x4b5263
+ };
+ static const ThemePalette one_light = {
+ 0xe5e5e6, 0xfafafa, 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
+ 0xa0a1a7, 0x383a42, 0x696c77, 0xa626a4, 0x4078f2, 0x0184bc,
+ 0x50a14f, 0x0184bc, 0xc18401, 0xe45649, 0xfafafa,
+ 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xe5e5e6, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7
+ };
+
+ switch (id) {
+ case THEME_NORD:
+ return mode == MODE_LIGHT ? nord_light : nord_dark;
+ case THEME_DRACULA:
+ return mode == MODE_LIGHT ? dra_light : dra_dark;
+ case THEME_ROSE_PINE:
+ return mode == MODE_LIGHT ? rose_light : rose_dark;
+ case THEME_OCEAN:
+ return mode == MODE_LIGHT ? ocean_light : ocean_dark;
+ case THEME_GRUVBOX:
+ return mode == MODE_LIGHT ? gruv_light : gruv_dark;
+ case THEME_SOLARIZED:
+ return mode == MODE_LIGHT ? sol_light : sol_dark;
+ case THEME_EVERFOREST:
+ return mode == MODE_LIGHT ? ever_light : ever_dark;
+ case THEME_MONOKAI:
+ return mode == MODE_LIGHT ? mono_light : mono_dark;
+ case THEME_ONE_DARK:
+ return mode == MODE_LIGHT ? one_light : one_dark;
+ case THEME_CATPPUCCIN:
+ default:
+ return mode == MODE_LIGHT ? cat_light : cat_dark;
+ }
+}
+char *read_css_file(void) {
+ gchar *contents = NULL;
+ GError *err = NULL;
+
+ if (!g_file_get_contents(CSS_FILE, &contents, NULL, &err)) {
+ g_warning("Could not load stylesheet '%s': %s", CSS_FILE,
+ err ? err->message : "unknown error");
+ if (err)
+ g_error_free(err);
+ return g_strdup("");
+ }
+ return contents;
+}
+
+/* The palette is data-driven, so its @define-color bindings are generated
+ * here and prepended to the rules loaded from the external stylesheet. */
+char *build_theme_css(const ThemePalette *p) {
+ char *palette = g_strdup_printf(
+ "@define-color bg_crust #%06x;\n"
+ "@define-color bg_base #%06x;\n"
+ "@define-color bg_mantle #%06x;\n"
+ "@define-color bg_surface0 #%06x;\n"
+ "@define-color bg_surface1 #%06x;\n"
+ "@define-color bg_surface2 #%06x;\n"
+ "@define-color fg_overlay #%06x;\n"
+ "@define-color fg_text #%06x;\n"
+ "@define-color fg_subtext #%06x;\n"
+ "@define-color accent #%06x;\n"
+ "@define-color accent2 #%06x;\n"
+ "@define-color accent3 #%06x;\n"
+ "@define-color success #%06x;\n"
+ "@define-color success2 #%06x;\n"
+ "@define-color warning #%06x;\n"
+ "@define-color error #%06x;\n"
+ "@define-color on_accent #%06x;\n"
+ "@define-color node_bg #%06x;\n"
+ "@define-color locked_bg #%06x;\n"
+ "@define-color locked_border #%06x;\n",
+ p->crust, p->base, p->mantle, p->surface0, p->surface1, p->surface2,
+ p->overlay, p->text, p->subtext, p->accent, p->accent2, p->accent3,
+ p->success, p->success2, p->warning, p->error, p->on_accent,
+ p->node, p->locked_bg, p->locked_border);
+ char *rules = read_css_file();
+ char *css = g_strconcat(palette, rules, NULL);
+
+ g_free(palette);
+ g_free(rules);
+ return css;
+}
+
+void save_settings(void) {
+ GKeyFile *kf = g_key_file_new();
+ gchar *data;
+
+ g_key_file_set_string(kf, "ui", "theme", theme_names[app_theme_id]);
+ g_key_file_set_string(kf, "ui", "mode",
+ app_color_mode == MODE_LIGHT ? "light" : "dark");
+ g_key_file_set_string(kf, "ui", "lang",
+ app_lang == LANG_EN ? "en" : "cs");
+
+ if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
+ g_key_file_free(kf);
+ return;
+ }
+
+ data = g_key_file_to_data(kf, NULL, NULL);
+ g_key_file_free(kf);
+ if (data) {
+ GError *err = NULL;
+ g_file_set_contents(SETTINGS_FILE, data, -1, &err);
+ if (err)
+ g_error_free(err);
+ g_free(data);
+ }
+}
+
+void load_settings(void) {
+ GKeyFile *kf = g_key_file_new();
+ GError *err = NULL;
+ gchar *theme = NULL;
+ gchar *mode = NULL;
+
+ if (!g_key_file_load_from_file(kf, SETTINGS_FILE, G_KEY_FILE_NONE, &err)) {
+ if (err)
+ g_error_free(err);
+ g_key_file_free(kf);
+ return;
+ }
+
+ theme = g_key_file_get_string(kf, "ui", "theme", NULL);
+ mode = g_key_file_get_string(kf, "ui", "mode", NULL);
+ if (theme) {
+ for (int i = 0; i < THEME_COUNT; i++) {
+ if (g_ascii_strcasecmp(theme, theme_names[i]) == 0) {
+ app_theme_id = (ThemeId)i;
+ break;
+ }
+ }
+ g_free(theme);
+ }
+ if (mode) {
+ if (g_ascii_strcasecmp(mode, "light") == 0 ||
+ g_ascii_strcasecmp(mode, "white") == 0)
+ app_color_mode = MODE_LIGHT;
+ else
+ app_color_mode = MODE_DARK;
+ g_free(mode);
+ }
+ {
+ gchar *lang = g_key_file_get_string(kf, "ui", "lang", NULL);
+ if (lang) {
+ if (g_ascii_strcasecmp(lang, "en") == 0 ||
+ g_ascii_strcasecmp(lang, "english") == 0)
+ app_lang = LANG_EN;
+ else
+ app_lang = LANG_CS;
+ g_free(lang);
+ }
+ }
+
+ g_key_file_free(kf);
+}
+
+/* GTK only re-resolves the style of widgets that are actually drawn after a
+ * CSS provider reload. Pages that are not the current stack child keep their
+ * previously resolved colors, so after switching the theme any page you then
+ * open still shows the old palette (e.g. near-white text in light mode).
+ * Adding and immediately removing a dummy class forces GTK to invalidate the
+ * style node of every widget, including hidden pages. */
+void force_restyle_tree(GtkWidget *w) {
+ GtkWidget *child;
+
+ if (!w)
+ return;
+
+ gtk_widget_add_css_class(w, "m4-restyle");
+ gtk_widget_remove_css_class(w, "m4-restyle");
+
+ for (child = gtk_widget_get_first_child(w); child;
+ child = gtk_widget_get_next_sibling(child))
+ force_restyle_tree(child);
+}
+
+void apply_theme(void) {
+ char *css;
+ GtkSettings *settings = gtk_settings_get_default();
+
+ app_theme = theme_palette(app_theme_id, app_color_mode);
+
+ if (settings) {
+ g_object_set(settings,
+ "gtk-application-prefer-dark-theme",
+ app_color_mode == MODE_DARK,
+ NULL);
+ }
+
+ css = build_theme_css(&app_theme);
+
+ if (!theme_provider) {
+ theme_provider = gtk_css_provider_new();
+ gtk_style_context_add_provider_for_display(
+ gdk_display_get_default(),
+ GTK_STYLE_PROVIDER(theme_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+ }
+
+ gtk_css_provider_load_from_string(theme_provider, css);
+ g_free(css);
+
+ refresh_welcome_heading();
+
+ if (main_window) {
+ gtk_widget_queue_draw(GTK_WIDGET(main_window));
+ force_restyle_tree(GTK_WIDGET(main_window));
+ }
+ if (road_rail)
+ gtk_widget_queue_draw(road_rail);
+ for (int i = 0; i < NUM_UNLOCKED; i++)
+ if (units[i].ex_rail)
+ gtk_widget_queue_draw(units[i].ex_rail);
+ for (int i = 0; i < THEME_COUNT; i++) {
+ if (theme_swatch_areas[i])
+ gtk_widget_queue_draw(theme_swatch_areas[i]);
+ }
+ net_rail_theme_reset();
+}
diff --git a/unitmap.c b/unitmap.c
new file mode 100644
index 0000000..a3af874
--- /dev/null
+++ b/unitmap.c
@@ -0,0 +1,307 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Unit 1 exercise path (roadmap-style serpentine) */
+/* ------------------------------------------------------------------ */
+
+
+/* Compute the serpentine exercise layout for the given available width. */
+void ex_layout_geometry(UnitCtx *u, double avail) {
+ const int n = u->n_ex;
+ const double phase = 2.0 * G_PI * 1.7 / (double)(n > 1 ? n - 1 : 1);
+ int rows;
+ int r, c, i;
+
+ if (n < 1)
+ return;
+ for (rows = 1; rows <= n; rows++) {
+ int cols = (n + rows - 1) / rows;
+ if (2.0 * EX_MX + (cols - 1) * EX_SPAC <= avail + 1.0)
+ break;
+ }
+ if (rows > n)
+ rows = n;
+ u->ex_rows = rows;
+ u->ex_cols = (n + rows - 1) / rows;
+ if (u->ex_cols < 1)
+ u->ex_cols = 1;
+ u->ex_cw = (int)(2.0 * EX_MX + (u->ex_cols - 1) * EX_SPAC);
+ u->ex_ch = (int)(2.0 * EX_MY + (rows - 1) * EX_GAP);
+
+ for (r = 0; r < rows; r++) {
+ int base = r * u->ex_cols;
+ int len = MIN(u->ex_cols, n - base);
+ int fwd = (r % 2) == 0;
+ for (c = 0; c < len; c++) {
+ i = base + c;
+ int cc = fwd ? c : (u->ex_cols - 1 - c);
+ u->ex_cx[i] = EX_MX + cc * EX_SPAC;
+ u->ex_cy[i] = EX_MY + r * EX_GAP
+ + EX_WAVE * sin((double)i * phase);
+ }
+ }
+}
+
+void ex_point(UnitCtx *u, double t, double *ox, double *oy) {
+ const int n = u->n_ex;
+ int k = (int)t;
+ double uu = t - (double)k;
+ double u2, u3;
+ double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
+
+ if (n < 2) { *ox = u->ex_cx[0]; *oy = u->ex_cy[0]; return; }
+ if (k < 0) { k = 0; uu = 0.0; }
+ if (k >= n - 1) { k = n - 2; uu = 1.0; }
+
+ p1x = u->ex_cx[k]; p1y = u->ex_cy[k];
+ p2x = u->ex_cx[k + 1]; p2y = u->ex_cy[k + 1];
+ if (k - 1 >= 0) { p0x = u->ex_cx[k - 1]; p0y = u->ex_cy[k - 1]; }
+ else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
+ if (k + 2 < n) { p3x = u->ex_cx[k + 2]; p3y = u->ex_cy[k + 2]; }
+ else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
+
+ u2 = uu * uu;
+ u3 = u2 * uu;
+ *ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * uu
+ + (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
+ + (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
+ *oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * uu
+ + (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
+ + (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
+}
+
+void ex_path(UnitCtx *u, cairo_t *cr, double t0, double t1) {
+ const double steps_per_unit = 48.0;
+ int n = (int)((t1 - t0) * steps_per_unit);
+ double x, y;
+ int s;
+
+ if (n < 1)
+ n = 1;
+ ex_point(u, t0, &x, &y);
+ cairo_move_to(cr, x, y);
+ for (s = 1; s <= n; s++) {
+ ex_point(u, t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
+ cairo_line_to(cr, x, y);
+ }
+}
+
+void draw_ex_rail(GtkDrawingArea *area, cairo_t *cr,
+ int width, int height, gpointer user_data) {
+ UnitCtx *u = user_data;
+ const double t_end = (double)(u->n_ex - 1);
+ const Rgb slate = color_from_hex(app_theme.surface1);
+ const Rgb green = color_from_hex(app_theme.success);
+ const Rgb rail = color_from_hex(app_theme.rail);
+
+ (void)area;
+ (void)width;
+ (void)height;
+
+ cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
+ cairo_paint(cr);
+ cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
+ cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
+ cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
+
+ cairo_new_path(cr);
+ ex_path(u, cr, 0.0, t_end);
+ cairo_set_line_width(cr, 14);
+ cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
+ cairo_stroke(cr);
+
+ cairo_new_path(cr);
+ ex_path(u, cr, 0.0, t_end);
+ cairo_set_line_width(cr, 9);
+ cairo_set_source_rgb(cr, slate.r, slate.g, slate.b);
+ cairo_stroke(cr);
+
+ for (int k = 0; k < u->n_ex - 1; k++) {
+ if (!(u->done[k + 1] || u->done[k + 2]))
+ continue;
+ cairo_new_path(cr);
+ ex_path(u, cr, (double)k, (double)(k + 1));
+ cairo_set_source_rgba(cr, green.r, green.g, green.b, 0.85);
+ cairo_set_line_width(cr, 9);
+ cairo_stroke(cr);
+ }
+}
+
+void ex_apply_layout(UnitCtx *u) {
+ int i;
+
+ if (!u->ex_fixed)
+ return;
+
+ for (i = 0; i < u->n_ex; i++) {
+ if (u->ex_cells[i])
+ gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_cells[i],
+ (int)(u->ex_cx[i] - EX_BUBBLE / 2.0),
+ (int)(u->ex_cy[i] - EX_BUBBLE / 2.0));
+ if (u->ex_labels[i])
+ gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_labels[i],
+ (int)(u->ex_cx[i] - (EX_SPAC - 20.0) / 2.0),
+ (int)(u->ex_cy[i] + EX_BUBBLE / 2.0 + 8.0));
+ }
+
+ gtk_widget_set_size_request(u->ex_fixed, u->ex_cw, u->ex_ch);
+ gtk_widget_set_size_request(u->ex_rail, u->ex_cw, u->ex_ch);
+ gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_rail, 0, 0);
+ gtk_widget_queue_draw(u->ex_rail);
+}
+
+void ex_relayout(UnitCtx *u) {
+ GtkAdjustment *hadj;
+ double avail;
+
+ if (!u->ex_scroll)
+ return;
+ hadj = gtk_scrolled_window_get_hadjustment(
+ GTK_SCROLLED_WINDOW(u->ex_scroll));
+ avail = gtk_adjustment_get_page_size(hadj);
+ if (avail < 1.0)
+ return;
+ ex_layout_geometry(u, avail);
+ ex_apply_layout(u);
+}
+
+gboolean ex_relayout_idle(gpointer data) {
+ UnitCtx *u = data;
+ u->ex_idle = 0;
+ ex_relayout(u);
+ return G_SOURCE_REMOVE;
+}
+
+void ex_relayout_later(UnitCtx *u) {
+ if (u->ex_idle == 0)
+ u->ex_idle = g_idle_add(ex_relayout_idle, u);
+}
+
+void ex_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
+ gpointer data) {
+ (void)adj;
+ (void)ps;
+ ex_relayout_later(data);
+}
+
+GtkWidget *make_bubble(UnitCtx *u, int n) {
+ GtkWidget *btn;
+ GtkWidget *vbox;
+ GtkWidget *num_label;
+ GtkWidget *icon;
+ char *num;
+ char *target;
+
+ btn = gtk_button_new();
+ gtk_widget_add_css_class(btn, "ex-bubble");
+ gtk_widget_set_size_request(btn, (int)EX_BUBBLE, (int)EX_BUBBLE);
+ gtk_widget_set_can_focus(btn, FALSE);
+ if (u->done[n])
+ gtk_widget_add_css_class(btn, "done");
+
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
+ gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
+ gtk_button_set_child(GTK_BUTTON(btn), vbox);
+
+ num = g_strdup_printf("%d", n);
+ num_label = gtk_label_new(num);
+ g_free(num);
+ gtk_widget_set_halign(num_label, GTK_ALIGN_CENTER);
+ gtk_widget_add_css_class(num_label, "bubble-number");
+ gtk_box_append(GTK_BOX(vbox), num_label);
+
+ icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 16);
+ gtk_widget_set_visible(icon, u->done[n]);
+ gtk_box_append(GTK_BOX(vbox), icon);
+
+ u->ex_cells[n - 1] = btn;
+ u->ex_icons[n] = icon;
+
+ target = g_strdup_printf("%se%d", u->ex_tag, n);
+ g_object_set_data_full(G_OBJECT(btn), "target", target, g_free);
+ g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
+
+ return btn;
+}
+
+GtkWidget *build_unit_page(UnitCtx *u) {
+ GtkWidget *page;
+ GtkWidget *scroll;
+ GtkWidget *wrap;
+ GtkWidget *fixed;
+ GtkWidget *rail;
+ GtkAdjustment *ha;
+ GtkAdjustment *va;
+ int i;
+
+ if (u->n_ex < 1)
+ return NULL;
+ ex_layout_geometry(u, 1000.0);
+
+ page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_hexpand(page, TRUE);
+ gtk_widget_set_vexpand(page, TRUE);
+ gtk_widget_set_margin_start(page, 32);
+ gtk_widget_set_margin_end(page, 32);
+ gtk_widget_set_margin_top(page, 24);
+ gtk_widget_set_margin_bottom(page, 24);
+
+ gtk_box_append(GTK_BOX(page),
+ top_bar("roadmap", u->title, u->sub_key));
+
+ scroll = gtk_scrolled_window_new();
+ gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
+ GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
+ gtk_widget_set_vexpand(scroll, TRUE);
+ gtk_widget_set_margin_top(scroll, 14);
+ gtk_box_append(GTK_BOX(page), scroll);
+ u->ex_scroll = scroll;
+
+ wrap = gtk_center_box_new();
+ gtk_widget_set_vexpand(wrap, TRUE);
+ gtk_widget_set_hexpand(wrap, TRUE);
+ gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
+
+ fixed = gtk_fixed_new();
+ gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
+ gtk_widget_set_size_request(fixed, u->ex_cw, u->ex_ch);
+ gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
+ u->ex_fixed = fixed;
+
+ rail = gtk_drawing_area_new();
+ gtk_widget_set_size_request(rail, u->ex_cw, u->ex_ch);
+ gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_ex_rail,
+ u, NULL);
+ gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
+ u->ex_rail = rail;
+
+ for (i = 0; i < u->n_ex; i++) {
+ GtkWidget *btn = make_bubble(u, i + 1);
+ GtkWidget *lbl = gtk_label_new(u->ex_names[i + 1]);
+
+ u->ex_labels[i] = lbl;
+
+ gtk_widget_set_size_request(lbl, (int)(EX_SPAC - 20.0), -1);
+ gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER);
+ gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER);
+ gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
+ gtk_widget_add_css_class(lbl, "ex-label");
+
+ gtk_fixed_put(GTK_FIXED(fixed), btn, 0, 0);
+ gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0);
+ }
+
+ ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
+ va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
+ g_signal_connect(ha, "notify::page-size",
+ G_CALLBACK(ex_adjust_notify), u);
+ g_signal_connect(va, "notify::page-size",
+ G_CALLBACK(ex_adjust_notify), u);
+
+ ex_apply_layout(u);
+ ex_relayout_later(u);
+
+ return page;
+}
diff --git a/util.c b/util.c
new file mode 100644
index 0000000..56d941f
--- /dev/null
+++ b/util.c
@@ -0,0 +1,99 @@
+#include "maturita.h"
+
+/* ------------------------------------------------------------------ */
+/* Utilities */
+/* ------------------------------------------------------------------ */
+
+char *normalize_answer(const char *input) {
+ GString *out = g_string_new(NULL);
+ const gchar *down = g_utf8_strdown(input, -1);
+ const gchar *p = down;
+ gboolean prev_space = FALSE;
+
+ while (*p) {
+ gunichar ch = g_utf8_get_char(p);
+ p = g_utf8_next_char(p);
+ if (g_unichar_isspace(ch)) {
+ if (out->len > 0 && !prev_space)
+ g_string_append_c(out, ' ');
+ prev_space = TRUE;
+ continue;
+ }
+ prev_space = FALSE;
+ switch (ch) {
+ case 0x00e4: g_string_append(out, "ae"); break; /* ä */
+ case 0x00f6: g_string_append(out, "oe"); break; /* ö */
+ case 0x00fc: g_string_append(out, "ue"); break; /* ü */
+ case 0x00df: g_string_append(out, "ss"); break; /* ß */
+ default: g_string_append_unichar(out, ch); break;
+ }
+ }
+
+ if (out->len > 0 && out->str[out->len - 1] == ' ')
+ g_string_truncate(out, out->len - 1);
+
+ g_free((gpointer)down);
+ return g_string_free(out, FALSE);
+}
+
+void shuffle_indices(int *arr, int n) {
+ for (int i = n - 1; i > 0; i--) {
+ int j = g_random_int_range(0, i + 1);
+ int tmp = arr[i];
+ arr[i] = arr[j];
+ arr[j] = tmp;
+ }
+}
+
+void flow_clear(GtkFlowBox *fb) {
+ GtkWidget *c = gtk_widget_get_first_child(GTK_WIDGET(fb));
+ while (c) {
+ GtkWidget *next = gtk_widget_get_next_sibling(c);
+ gtk_flow_box_remove(fb, c);
+ c = next;
+ }
+}
+
+void answer_mark(GtkWidget *widget, gboolean ok) {
+ if (ok) {
+ gtk_widget_remove_css_class(widget, "answer-wrong");
+ gtk_widget_add_css_class(widget, "answer-ok");
+ } else {
+ gtk_widget_remove_css_class(widget, "answer-ok");
+ gtk_widget_add_css_class(widget, "answer-wrong");
+ }
+}
+
+void set_feedback(GtkWidget *label, gboolean ok, const char *text) {
+ gtk_widget_remove_css_class(label, "feedback-ok");
+ gtk_widget_remove_css_class(label, "feedback-err");
+ gtk_widget_add_css_class(label, ok ? "feedback-ok" : "feedback-err");
+ gtk_label_set_text(GTK_LABEL(label), text);
+}
+
+/* True if `s` contains any German special letter (ä, ö, ü or ß). */
+gboolean text_has_german_umlaut(const char *s) {
+ const char *p = s ? s : "";
+
+ while (*p) {
+ gunichar c = g_utf8_get_char(p);
+
+ if (c == 0x00e4 || c == 0x00f6 || c == 0x00fc || c == 0x00df)
+ return TRUE;
+ p = g_utf8_next_char(p);
+ }
+ return FALSE;
+}
+
+/* Small bilingual note shown under the exercise title whenever the answer
+ * the student has to type may contain a German special letter. */
+void add_umlaut_note(GtkWidget *body) {
+ GtkWidget *n = gtk_label_new(NULL);
+
+ i18n_bind(n, "hint_umlauts", 0);
+ gtk_widget_set_halign(n, GTK_ALIGN_START);
+ gtk_label_set_wrap(GTK_LABEL(n), TRUE);
+ gtk_widget_set_margin_bottom(n, 2);
+ gtk_widget_add_css_class(n, "hint");
+ gtk_box_append(GTK_BOX(body), n);
+}