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); +}