diff --git a/.gitignore b/.gitignore index 391e12a..99e75c7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ sprechen error.txt error.log CLAUDE.md +progress/ # Object files *.o diff --git a/Makefile b/Makefile index 246785d..fdb6b7a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ CC = gcc -CFLAGS = `pkg-config --cflags gtk4` +CFLAGS = -Wall -Wextra -Wno-deprecated-declarations `pkg-config --cflags gtk4` LIBS = `pkg-config --libs gtk4` -lm TARGET = sprechen SRC = sprechen.c diff --git a/README.md b/README.md index 315c568..a64362c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,11 @@ CSS-styled GTK 4 interface with a Catppuccin Mocha dark theme. - finish flag at the end of the path - **Back navigation** – themed circular back button (top-left) returns to the welcome screen +- **Unit 1 exercises** – "Neue Freunde" opens an interactive bubble map with 13 + exercises (dialogs, sentence building, numbers, verb conjugation, greeting + sorting and more), each with German prompts and a "Zkontrolovat" check +- **Progress tracking** – completed exercises are marked green; progress is + saved to `progress/unit1.conf` and restored on startup - **Catppuccin Mocha theme** – gradient background, rounded cards, glowing buttons and nodes, all driven by an embedded CSS stylesheet - **Keyboard quit shortcuts** – `Super/Cmd+Q` or `Alt+F4` closes the app @@ -122,11 +127,14 @@ When launched, the application shows the welcome screen. From there: 1. Click **"Pokračuj"** to open the learning roadmap. 2. Browse the units along the path; the roadmap scrolls horizontally if the window is too narrow. -3. Use the **back button** in the top-left corner to return to the welcome +3. Click the **"Neue Freunde"** node to open the unit's exercise map, then pick + any bubble to start an exercise. +4. Use the **back button** in the top-left corner to return to the previous screen. -Only the first unit is currently unlocked. Clicking a locked unit does nothing – -units are placeholders until lessons are implemented. +Only the first unit is currently implemented. Locked units do nothing – they +are placeholders until lessons are added. Completed exercises stay green and +are saved to `progress/unit1.conf` (created next to the app on first finish). ### Keyboard shortcuts @@ -156,6 +164,7 @@ that stylesheet and rebuild. Future enhancements planned for Sprechen.c: - [ ] Wire unit nodes to actual German vocabulary/grammar exercises + - [x] Unit 1 ("Neue Freunde") – 13 interactive exercises with progress saving - [ ] Lesson progression and unlock system for the remaining units - [ ] Add audio pronunciation features - [ ] Add user settings panel diff --git a/sprechen.c b/sprechen.c index a934224..cd9a4bf 100644 --- a/sprechen.c +++ b/sprechen.c @@ -1,9 +1,14 @@ #include #include +#include #include #define NUM_UNITS 10 #define FIRST_LOCKED 1 +#define NUM_EXERCISES 13 + +#define PROGRESS_DIR "progress" +#define PROGRESS_FILE "progress/unit1.conf" #define NODE_SIZE 88.0 #define PATH_SPAC 240.0 @@ -14,6 +19,13 @@ static GtkStack *main_stack; +static gboolean ex_done[NUM_EXERCISES + 1]; /* indexed 1..13 */ + +static GtkWidget *ex_bubbles[NUM_EXERCISES + 1]; +static GtkWidget *ex_done_icons[NUM_EXERCISES + 1]; +static GtkWidget *unit1_node; +static GtkWidget *unit1_done_icon; + static const char *unit_names[] = { "Neue Freunde", "Aus aller Welt", @@ -27,6 +39,41 @@ static const char *unit_names[] = { "Urlaub in Österreich", }; +static const char *ex_names[NUM_EXERCISES + 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", +}; + +/* Scattered bubble positions on the unit-1 map (centre of each bubble). */ +static const double ex_pos[NUM_EXERCISES + 1][2] = { + {0.0, 0.0}, + { 70.0, 100.0}, + {190.0, 45.0}, + {320.0, 90.0}, + {450.0, 40.0}, + {590.0, 95.0}, + {130.0, 210.0}, + {280.0, 185.0}, + {410.0, 215.0}, + {540.0, 190.0}, + { 70.0, 320.0}, + {215.0, 330.0}, + {360.0, 325.0}, + {500.0, 335.0}, +}; + static double path_x_at(double t) { return PATH_MARGIN + (NODE_SIZE / 2.0) + t * PATH_SPAC; } @@ -47,12 +94,217 @@ static Rgb color_from_hex(unsigned int hex) { return c; } +/* ------------------------------------------------------------------ */ +/* 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); +} + +/* ------------------------------------------------------------------ */ +/* Progress */ +/* ------------------------------------------------------------------ */ + +static void save_progress(void) { + GKeyFile *kf = g_key_file_new(); + for (int i = 1; i <= NUM_EXERCISES; i++) { + gchar key[8]; + g_snprintf(key, sizeof(key), "%d", i); + g_key_file_set_boolean(kf, "done", key, ex_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(PROGRESS_FILE, data, -1, &err); + if (err) + g_error_free(err); + g_free(data); + } +} + +static void load_progress(void) { + GKeyFile *kf = g_key_file_new(); + GError *err = NULL; + + if (!g_key_file_load_from_file(kf, PROGRESS_FILE, G_KEY_FILE_NONE, &err)) { + if (err) + g_error_free(err); + g_key_file_free(kf); + return; + } + + for (int i = 1; i <= NUM_EXERCISES; i++) { + gchar key[8]; + g_snprintf(key, sizeof(key), "%d", i); + ex_done[i] = g_key_file_get_boolean(kf, "done", key, NULL); + } + + g_key_file_free(kf); +} + +static void refresh_completion_ui(void) { + gboolean all = TRUE; + + for (int i = 1; i <= NUM_EXERCISES; i++) { + if (!ex_done[i]) + all = FALSE; + if (ex_bubbles[i]) { + if (ex_done[i]) { + gtk_widget_add_css_class(ex_bubbles[i], "done"); + gtk_widget_set_visible(ex_done_icons[i], TRUE); + } else { + gtk_widget_remove_css_class(ex_bubbles[i], "done"); + gtk_widget_set_visible(ex_done_icons[i], FALSE); + } + } + } + + if (unit1_node) { + if (all) { + gtk_widget_remove_css_class(unit1_node, "current"); + gtk_widget_add_css_class(unit1_node, "done"); + gtk_widget_set_visible(unit1_done_icon, TRUE); + } else { + gtk_widget_remove_css_class(unit1_node, "done"); + gtk_widget_add_css_class(unit1_node, "current"); + gtk_widget_set_visible(unit1_done_icon, FALSE); + } + } +} + +static void mark_done(int n) { + if (n < 1 || n > NUM_EXERCISES) + return; + ex_done[n] = TRUE; + refresh_completion_ui(); + save_progress(); +} + +/* ------------------------------------------------------------------ */ +/* 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, "roadmap"); } -static void on_back_clicked(GtkButton *button, gpointer user_data) { - gtk_stack_set_visible_child_name(main_stack, "welcome"); +static GtkWidget *make_back_button(const char *target) { + GtkWidget *back = gtk_button_new_from_icon_name("go-previous-symbolic"); + gtk_widget_add_css_class(back, "back-btn"); + gtk_widget_set_valign(back, GTK_ALIGN_CENTER); + gtk_widget_set_tooltip_text(back, "Zpět"); + 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(title); + 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(subtitle); + gtk_widget_set_halign(sub, GTK_ALIGN_CENTER); + gtk_widget_add_css_class(sub, "roadmap-sub"); + gtk_box_append(GTK_BOX(center), sub); + } + + return top; } static gboolean on_window_key_pressed(GtkEventControllerKey *controller, @@ -77,20 +329,22 @@ static gboolean on_window_key_pressed(GtkEventControllerKey *controller, return GDK_EVENT_PROPAGATE; } +/* ------------------------------------------------------------------ */ +/* Welcome page */ +/* ------------------------------------------------------------------ */ + static GtkWidget *build_welcome_page(void) { GtkWidget *box; GtkWidget *heading; GtkWidget *card; GtkWidget *button; - // Create a vertical box to hold contents 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 heading = gtk_label_new(NULL); gtk_label_set_markup(GTK_LABEL(heading), "Vítejte ve Sprechen.C!"); @@ -98,7 +352,6 @@ static GtkWidget *build_welcome_page(void) { gtk_widget_add_css_class(heading, "heading"); gtk_box_append(GTK_BOX(box), heading); - // Card with the welcome message card = gtk_label_new( "Sprechen.C je vzdělávací program na procvičování Němčiny, určený " "pro studenty středních škol a gymnázií.\n\n" @@ -109,7 +362,6 @@ static GtkWidget *build_welcome_page(void) { gtk_widget_add_css_class(card, "card"); gtk_box_append(GTK_BOX(box), card); - // Continue button button = gtk_button_new_with_label("Pokračuj"); gtk_widget_set_halign(button, GTK_ALIGN_CENTER); gtk_widget_set_margin_top(button, 6); @@ -121,6 +373,10 @@ static GtkWidget *build_welcome_page(void) { 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) { @@ -298,6 +554,18 @@ static void add_path_node(GtkFixed *fixed, int index) { gtk_image_set_pixel_size(GTK_IMAGE(lock), 16); gtk_widget_add_css_class(lock, "lock-icon"); gtk_box_append(GTK_BOX(vbox), lock); + } else if (index == 0) { + GtkWidget *icon; + icon = gtk_image_new_from_icon_name("object-select-symbolic"); + gtk_image_set_pixel_size(GTK_IMAGE(icon), 18); + gtk_widget_add_css_class(icon, "state-icon"); + gtk_widget_set_visible(icon, FALSE); + gtk_box_append(GTK_BOX(vbox), icon); + unit1_done_icon = icon; + unit1_node = card; + g_object_set_data_full(G_OBJECT(card), "target", + g_strdup("unit1"), g_free); + g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL); } gtk_fixed_put(fixed, card, @@ -319,11 +587,6 @@ static void add_path_node(GtkFixed *fixed, int index) { static GtkWidget *build_roadmap_page(void) { GtkWidget *page; - GtkWidget *top; - GtkWidget *back; - GtkWidget *center; - GtkWidget *heading; - GtkWidget *sub; GtkWidget *scroll; GtkWidget *wrap; GtkWidget *fixed; @@ -341,31 +604,9 @@ static GtkWidget *build_roadmap_page(void) { gtk_widget_set_margin_top(page, 24); gtk_widget_set_margin_bottom(page, 24); - top = gtk_center_box_new(); - gtk_widget_set_margin_top(top, 2); - gtk_widget_set_margin_bottom(top, 6); - gtk_box_append(GTK_BOX(page), top); - - back = gtk_button_new_from_icon_name("go-previous-symbolic"); - gtk_widget_add_css_class(back, "back-btn"); - gtk_widget_set_valign(back, GTK_ALIGN_CENTER); - gtk_widget_set_tooltip_text(back, "Zpět"); - g_signal_connect(back, "clicked", G_CALLBACK(on_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("Učební plán"); - 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("Vyberte jednotku na cestě a začněte procvičovat."); - gtk_widget_set_halign(sub, GTK_ALIGN_CENTER); - gtk_widget_add_css_class(sub, "roadmap-sub"); - gtk_box_append(GTK_BOX(center), sub); + gtk_box_append(GTK_BOX(page), + top_bar("welcome", "Učební plán", + "Vyberte jednotku na cestě a začněte procvičovat.")); scroll = gtk_scrolled_window_new(); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), @@ -395,30 +636,1287 @@ static GtkWidget *build_roadmap_page(void) { return page; } +/* ------------------------------------------------------------------ */ +/* Unit 1 bubble map (scattered) */ +/* ------------------------------------------------------------------ */ + +static GtkWidget *make_bubble(int n) { + GtkWidget *cell = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + GtkWidget *btn; + GtkWidget *vbox; + GtkWidget *num_label; + GtkWidget *icon; + GtkWidget *name_label; + char *num; + char *target; + + gtk_widget_set_halign(cell, GTK_ALIGN_CENTER); + gtk_widget_set_valign(cell, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(cell, 120, -1); + + btn = gtk_button_new(); + gtk_widget_add_css_class(btn, "ex-bubble"); + gtk_widget_set_size_request(btn, 64, 64); + if (ex_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_add_css_class(num_label, "bubble-number"); + gtk_box_append(GTK_BOX(vbox), num_label); + + icon = gtk_image_new_from_icon_name("object-select-symbolic"); + gtk_image_set_pixel_size(GTK_IMAGE(icon), 16); + gtk_widget_add_css_class(icon, "bubble-icon"); + gtk_widget_set_visible(icon, ex_done[n]); + gtk_box_append(GTK_BOX(vbox), icon); + + gtk_box_append(GTK_BOX(cell), btn); + + name_label = gtk_label_new(ex_names[n]); + gtk_widget_set_halign(name_label, GTK_ALIGN_CENTER); + gtk_widget_add_css_class(name_label, "ex-label"); + gtk_label_set_wrap(GTK_LABEL(name_label), TRUE); + gtk_label_set_max_width_chars(GTK_LABEL(name_label), 12); + gtk_box_append(GTK_BOX(cell), name_label); + + ex_bubbles[n] = btn; + ex_done_icons[n] = icon; + + target = g_strdup_printf("ex%d", 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 cell; +} + +static GtkWidget *build_unit1_page(void) { + GtkWidget *page; + GtkWidget *scroll; + GtkWidget *fixed; + int i; + + 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("roadmap", "Neue Freunde", + "Vyberte cvičení a dokončete je.")); + + 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); + + fixed = gtk_fixed_new(); + gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER); + gtk_widget_set_size_request(fixed, 700, 430); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), fixed); + + for (i = 1; i <= NUM_EXERCISES; i++) { + GtkWidget *cell = make_bubble(i); + gtk_fixed_put(GTK_FIXED(fixed), cell, + (int)(ex_pos[i][0] - 60.0), + (int)(ex_pos[i][1] - 32.0)); + } + + return page; +} + +/* ------------------------------------------------------------------ */ +/* Exercise page shell */ +/* ------------------------------------------------------------------ */ + +static GtkWidget *ex_page_shell(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("unit1", title, subtitle)); + + 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, 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_with_label(btn_label); + 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* */ + int ex_num; + GtkWidget *feedback; +} ComboListCtx; + +static ComboListCtx *combo_list_ctx_new(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->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 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 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); + gchar *a = normalize_answer(ans); + good = (g_strcmp0(t, a) == 0); + g_free(t); + g_free(a); + g_free(sel); + } + answer_mark(GTK_WIDGET(combo), good); + if (good) + ok++; + } + + if (ok == total) { + set_feedback(ctx->feedback, TRUE, "Výborně!"); + mark_done(ctx->ex_num); + } else { + set_feedback(ctx->feedback, FALSE, "Ještě to není správně. Zkuste to znovu."); + } +} + +/* ------------------------------------------------------------------ */ +/* 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 *chips[6]; + int placed[6]; + int placed_count; + int n_words; +} SentBuilder; + +typedef struct { + SentBuilder *sb; + int idx; +} ChipRef; + +static void sent_rebuild(SentBuilder *sb) { + 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++) { + gboolean in_target = FALSE; + for (int j = 0; j < sb->placed_count; j++) { + if (sb->placed[j] == i) { + in_target = TRUE; + break; + } + } + if (!in_target) + gtk_flow_box_insert(GTK_FLOW_BOX(sb->pool), sb->chips[i], -1); + } +} + +static void asm_chip_clicked(GtkButton *button, gpointer data) { + ChipRef *ref = data; + SentBuilder *sb = ref->sb; + int idx = ref->idx; + int pos = -1; + + (void)button; + + 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--; + } else { + sb->placed[sb->placed_count++] = idx; + } + + sent_rebuild(sb); +} + +typedef struct { + SentBuilder **sbs; + int n; + 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"); + } + } + + if (ok == ctx->n) { + set_feedback(ctx->feedback, TRUE, "Výborně!"); + mark_done(ctx->ex_num); + } else { + set_feedback(ctx->feedback, FALSE, "Některé věty nejsou správně. Zkuste to znovu."); + } +} + +static GtkWidget *build_assembly(const char *title, const char *subtitle, + int ex_num, const AssemblyItem *items, int n) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell(title, subtitle, "Zkontrolovat", + &body, &feedback, &check); + AsmCtx *ctx = g_new0(AsmCtx, 1); + + ctx->sbs = g_new0(SentBuilder *, n); + ctx->n = n; + 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 *group; + GtkWidget *target; + GtkWidget *pool; + + 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); + } + + group = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); + gtk_widget_add_css_class(group, "sent-group"); + sb->group = 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); + + for (int w = 0; w < item->n; w++) { + GtkWidget *chip = gtk_button_new_with_label(item->words[w]); + 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; + g_signal_connect(chip, "clicked", G_CALLBACK(asm_chip_clicked), ref); + } + + { + int order[6]; + for (int w = 0; w < item->n; w++) + order[w] = w; + shuffle_indices(order, item->n); + for (int w = 0; w < item->n; w++) + gtk_flow_box_insert(GTK_FLOW_BOX(pool), sb->chips[order[w]], -1); + } + + gtk_box_append(GTK_BOX(body), group); + } + + 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", " Anna."}, + {"B:", "Hallo! Ich ", "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", +}; + +static GtkWidget *build_ex1(void) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell("Dialog", + "Doplňte chybějící slova v dialogu.", + "Zkontrolovat", &body, &feedback, &check); + ComboListCtx *ctx = combo_list_ctx_new(1, feedback); + + 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); + } + } + } + + 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", "?"}, 4}, +}; + +/* ------------------------------------------------------------------ */ +/* Choice exercises (3 + 9) */ +/* ------------------------------------------------------------------ */ + +typedef struct { + const char *prompt; + const char *options[3]; + int correct; +} ChoiceQ; + +static const ChoiceQ ex3_questions[] = { + {"Welche Zahl ist „vierzehn“?", {"4", "14", "40"}, 1}, + {"Welche Zahl ist „siebzehn“?", {"7", "17", "70"}, 1}, + {"Welche Zahl ist „zwanzig“?", {"2", "12", "20"}, 2}, + {"Welche Zahl ist „sechs“?", {"6", "16", "60"}, 0}, +}; + +static const ChoiceQ ex9_questions[] = { + {"___ heißt du?", {"Wer", "Wie", "Wo"}, 1}, + {"___ kommst du?", {"Woher", "Wie", "Wer"}, 0}, + {"___ wohnst du?", {"Wo", "Wer", "Was"}, 0}, + {"___ ist das?", {"Wer", "Wo", "Wie"}, 0}, + {"___ alt bist du?", {"Wie", "Woher", "Was"}, 0}, +}; + +typedef struct { + const ChoiceQ *qs; + int n; + int ex_num; + GtkWidget *feedback; + GtkToggleButton **toggles; +} 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 < 3; o++) { + GtkToggleButton *tb = ctx->toggles[i * 3 + 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 * 3 + active]), "ok"); + } else if (active >= 0) { + gtk_widget_add_css_class(GTK_WIDGET(ctx->toggles[i * 3 + active]), "wrong"); + } + } + + if (ok == ctx->n) { + set_feedback(ctx->feedback, TRUE, "Výborně!"); + mark_done(ctx->ex_num); + } else { + set_feedback(ctx->feedback, FALSE, "Ještě to není správně."); + } +} + +static GtkWidget *build_choice(const char *title, const char *subtitle, int ex_num, + const ChoiceQ *qs, int n) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell(title, subtitle, "Zkontrolovat", + &body, &feedback, &check); + ChoiceCtx *ctx = g_new0(ChoiceCtx, 1); + + ctx->qs = qs; + ctx->n = n; + ctx->ex_num = ex_num; + ctx->feedback = feedback; + ctx->toggles = g_new0(GtkToggleButton *, n * 3); + + 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_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), row); + + for (int o = 0; o < 3; 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_box_append(GTK_BOX(row), tb); + ctx->toggles[i * 3 + o] = GTK_TOGGLE_BUTTON(tb); + } + } + + g_signal_connect(check, "clicked", G_CALLBACK(choice_check), ctx); + return page; +} + +/* ------------------------------------------------------------------ */ +/* Exercise 4: Freie Antwort */ +/* ------------------------------------------------------------------ */ + +typedef struct { + const char *question; + const char *sample; +} FreeQ; + +static const FreeQ ex4_questions[] = { + {"Wie heißt du?", "Ich heiße Anna."}, + {"Woher kommst du?", "Ich komme aus Tschechien."}, + {"Wie alt bist du?", "Ich bin sechzehn Jahre alt."}, + {"Wie geht es dir?", "Mir geht es gut, danke."}, +}; + +static void ex4_reveal(GtkButton *button, gpointer data) { + GtkWidget *sample = data; + (void)button; + gtk_widget_set_visible(sample, !gtk_widget_get_visible(sample)); +} + +static void ex4_finish(GtkButton *button, gpointer data) { + GtkWidget *feedback = data; + (void)button; + set_feedback(feedback, TRUE, "Výborně!"); + mark_done(4); +} + +static GtkWidget *build_ex4(void) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell("Freie Antwort", + "Odpovězte vlastními slovy, poté klikněte na Dokončit.", + "Dokončit", &body, &feedback, &check); + + for (guint i = 0; i < G_N_ELEMENTS(ex4_questions); i++) { + GtkWidget *q = gtk_label_new(ex4_questions[i].question); + GtkWidget *entry; + GtkWidget *reveal; + GtkWidget *sample; + + gtk_widget_set_halign(q, GTK_ALIGN_START); + gtk_widget_add_css_class(q, "ex-prompt"); + gtk_box_append(GTK_BOX(body), q); + + entry = gtk_entry_new(); + gtk_entry_set_placeholder_text(GTK_ENTRY(entry), "Vaše odpověď…"); + gtk_box_append(GTK_BOX(body), entry); + + reveal = gtk_button_new_with_label("Ukázat vzor"); + 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(ex4_questions[i].sample); + gtk_widget_set_halign(sample, GTK_ALIGN_START); + gtk_widget_add_css_class(sample, "hint"); + gtk_widget_set_visible(sample, FALSE); + gtk_box_append(GTK_BOX(body), sample); + + g_signal_connect(reveal, "clicked", G_CALLBACK(ex4_reveal), sample); + } + + g_signal_connect(check, "clicked", G_CALLBACK(ex4_finish), feedback); + return page; +} + +/* ------------------------------------------------------------------ */ +/* 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 GtkWidget *build_ex5(void) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell("Zahlen", + "Vyberte správné slovo pro dané číslo.", + "Zkontrolovat", &body, &feedback, &check); + ComboListCtx *ctx = combo_list_ctx_new(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); + } + + 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 GtkWidget *build_ex6(void) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell("Wie viel?", + "Spočítejte předměty a vyberte počet.", + "Zkontrolovat", &body, &feedback, &check); + ComboListCtx *ctx = combo_list_ctx_new(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); + } + + 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 GtkWidget *build_ex7(void) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell("Zahlenreihe", + "Doplňte chybějící číslovku.", + "Zkontrolovat", &body, &feedback, &check); + ComboListCtx *ctx = combo_list_ctx_new(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); + } + + 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; + +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 GtkWidget *build_ex8(void) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell("Verb einsetzen", + "Vyberte správný tvar slovesa.", + "Zkontrolovat", &body, &feedback, &check); + ComboListCtx *ctx = combo_list_ctx_new(8, feedback); + + 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); + } + + 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}, +}; + +/* ------------------------------------------------------------------ */ +/* 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 GtkWidget *build_ex12(void) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell("Was macht er/sie?", + "Vyberte správný tvar slovesa podle obrázku.", + "Zkontrolovat", &body, &feedback, &check); + ComboListCtx *ctx = combo_list_ctx_new(12, feedback); + + 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); + } + + 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)"}; + +typedef struct { + GtkWidget *pool; + GtkWidget *group_flow[3]; + GtkWidget *group_btn[3]; + int n_groups; + int active_group; + GtkWidget **chips; + int n_items; + int *current_group; + const AssignItem *items; + int ex_num; + GtkWidget *feedback; +} AssignCtx; + +typedef struct { + AssignCtx *ac; + int idx; +} AssignChipRef; + +static void assign_rebuild(AssignCtx *ac) { + 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->chips[i], -1); + else + gtk_flow_box_insert(GTK_FLOW_BOX(ac->group_flow[ac->current_group[i]]), + ac->chips[i], -1); + } +} + +static void assign_chip_clicked(GtkButton *button, gpointer data) { + AssignChipRef *ref = data; + AssignCtx *ac = ref->ac; + int idx = ref->idx; + + (void)button; + + 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[i].correct_group) + ok++; + + if (ok == ac->n_items) { + set_feedback(ac->feedback, TRUE, "Výborně!"); + mark_done(ac->ex_num); + } else { + set_feedback(ac->feedback, FALSE, "Ještě to není správně. Zkuste to znovu."); + } +} + +static GtkWidget *build_assign(const char *title, const char *subtitle, int ex_num, + const AssignItem *items, int n_items, + const char **group_labels, int n_groups) { + GtkWidget *body, *feedback, *check; + GtkWidget *page = ex_page_shell(title, subtitle, "Zkontrolovat", + &body, &feedback, &check); + AssignCtx *ac = g_new0(AssignCtx, 1); + GtkWidget *hint; + GtkWidget *row; + GtkToggleButton *first = NULL; + int *order; + + ac->n_items = n_items; + ac->n_groups = n_groups; + ac->active_group = 0; + ac->ex_num = ex_num; + ac->feedback = feedback; + ac->items = items; + ac->chips = 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; + + hint = gtk_label_new( + "Vyberte skupinu a klikněte na kartu. Kliknutím na kartu ve skupině ji vrátíte zpět."); + gtk_widget_set_halign(hint, GTK_ALIGN_START); + gtk_widget_add_css_class(hint, "ex-sub"); + gtk_box_append(GTK_BOX(body), hint); + + row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), 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(body), 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(body), 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 *chip; + 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); + + chip = gtk_button_new_with_label(lab->str); + g_string_free(lab, TRUE); + gtk_widget_add_css_class(chip, "chip"); + g_object_ref(chip); + ac->chips[i] = chip; + + 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), chip, -1); + } + g_free(order); + + g_signal_connect(check, "clicked", G_CALLBACK(assign_check), ac); + return page; +} + +/* ------------------------------------------------------------------ */ +/* Activate */ +/* ------------------------------------------------------------------ */ + static void activate(GtkApplication *app, gpointer user_data) { GtkWindow *window; GtkWidget *headerbar; GtkWidget *title_label; GtkWidget *welcome_page; GtkWidget *roadmap_page; + GtkWidget *unit1_page; + GtkWidget *ex_pages[NUM_EXERCISES + 1]; GtkEventController *keys; GtkCssProvider *provider; + char name[16]; + + (void)user_data; + + load_progress(); - // Create a new application window window = GTK_WINDOW(gtk_application_window_new(app)); gtk_window_set_title(window, "Sprechen.c"); gtk_window_set_default_size(window, 760, 560); - // Create a styled headerbar (titlebar) headerbar = gtk_header_bar_new(); gtk_widget_add_css_class(headerbar, "titlebar"); title_label = gtk_label_new("Sprechen.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); - // Stack navigation between welcome and roadmap pages main_stack = GTK_STACK(gtk_stack_new()); gtk_stack_set_transition_type(GTK_STACK(main_stack), GTK_STACK_TRANSITION_TYPE_CROSSFADE); @@ -426,11 +1924,43 @@ static void activate(GtkApplication *app, gpointer user_data) { welcome_page = build_welcome_page(); roadmap_page = build_roadmap_page(); + unit1_page = build_unit1_page(); + + ex_pages[1] = build_ex1(); + ex_pages[2] = build_assembly("Sätze bilden", + "Seřaďte slova do správného pořadí.", + 2, ex2_items, G_N_ELEMENTS(ex2_items)); + ex_pages[3] = build_choice("Was ist richtig?", "Vyberte správnou číslovku.", + 3, ex3_questions, G_N_ELEMENTS(ex3_questions)); + ex_pages[4] = build_ex4(); + ex_pages[5] = build_ex5(); + ex_pages[6] = build_ex6(); + ex_pages[7] = build_ex7(); + ex_pages[8] = build_ex8(); + ex_pages[9] = build_choice("Wer? Wie? Wo?", "Vyberte správné tázací slovo.", + 9, ex9_questions, G_N_ELEMENTS(ex9_questions)); + ex_pages[10] = build_assembly("Wörter trennen", + "Poskládejte slova do správného pořadí.", + 10, ex10_items, G_N_ELEMENTS(ex10_items)); + ex_pages[11] = build_assign("Grußformen", "Roztřiďte pozdravy na pozdravy a rozloučení.", + 11, ex11_items, G_N_ELEMENTS(ex11_items), + ex11_groups, G_N_ELEMENTS(ex11_groups)); + ex_pages[12] = build_ex12(); + ex_pages[13] = build_assign("Länder", "Přiřaďte symboly ke správné zemi.", + 13, ex13_items, G_N_ELEMENTS(ex13_items), + ex13_groups, G_N_ELEMENTS(ex13_groups)); gtk_stack_add_named(main_stack, welcome_page, "welcome"); gtk_stack_add_named(main_stack, roadmap_page, "roadmap"); + gtk_stack_add_named(main_stack, unit1_page, "unit1"); + for (int i = 1; i <= NUM_EXERCISES; i++) { + g_snprintf(name, sizeof(name), "ex%d", i); + gtk_stack_add_named(main_stack, ex_pages[i], 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); @@ -438,13 +1968,15 @@ static void activate(GtkApplication *app, gpointer user_data) { G_CALLBACK(on_window_key_pressed), window); gtk_widget_add_controller(GTK_WIDGET(window), GTK_EVENT_CONTROLLER(keys)); - // Load Catppuccin Mocha CSS provider = gtk_css_provider_new(); gtk_css_provider_load_from_string(provider, "window {" " background-color: #1e1e2e;" " background-image: linear-gradient(160deg, #11111b 0%, #1e1e2e 40%, #313244 100%);" "}" + "button {" + " background-image: none;" + "}" ".titlebar {" " background-color: #1e1e2e;" " background-image: none;" @@ -596,6 +2128,206 @@ static void activate(GtkApplication *app, gpointer user_data) { ".unit-name-locked {" " color: #6c7086;" "}" + ".ex-bubble {" + " min-width: 64px;" + " min-height: 64px;" + " padding: 0;" + " border-radius: 999px;" + " background-color: #2b2d40;" + " color: #cdd6f4;" + " border: 2px solid #3b3e56;" + " box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);" + " transition: border-color 150ms ease, box-shadow 150ms ease;" + "}" + ".ex-bubble:hover {" + " border-color: #cba6f7;" + " box-shadow: 0 6px 20px rgba(203, 166, 247, 0.35);" + "}" + ".ex-bubble.done {" + " background-image: linear-gradient(135deg, #a6e3a1 0%, #94e2d5 100%);" + " border-color: #a6e3a1;" + " color: #1e1e2e;" + " box-shadow: 0 4px 16px rgba(166, 227, 161, 0.35);" + "}" + ".ex-bubble.done:hover {" + " box-shadow: 0 8px 22px rgba(166, 227, 161, 0.5);" + "}" + ".ex-bubble:focus," + ".ex-bubble:focus-visible {" + " outline: none;" + "}" + ".bubble-number {" + " font-size: 22px;" + " font-weight: 800;" + "}" + ".ex-bubble.done .bubble-number {" + " color: #1e1e2e;" + "}" + ".bubble-icon {" + " color: #1e1e2e;" + "}" + ".ex-label {" + " color: #cdd6f4;" + " font-size: 12px;" + " font-weight: 600;" + "}" + ".ex-prompt {" + " color: #cdd6f4;" + " font-size: 16px;" + " font-weight: 500;" + "}" + ".ex-sub {" + " color: #a6adc8;" + " font-size: 13px;" + " font-weight: 700;" + "}" + ".hint {" + " color: #a6adc8;" + " font-size: 13px;" + " font-style: italic;" + "}" + ".dlg-speaker {" + " color: #cba6f7;" + " font-weight: 700;" + " font-size: 15px;" + "}" + ".number-big {" + " color: #f9e2af;" + " font-size: 26px;" + " font-weight: 800;" + "}" + ".chain {" + " color: #f9e2af;" + " font-size: 20px;" + " font-weight: 700;" + " letter-spacing: 1px;" + "}" + "entry {" + " background-color: #181825;" + " color: #cdd6f4;" + " border: 1px solid #45475a;" + " border-radius: 10px;" + " padding: 8px 12px;" + " font-size: 15px;" + " caret-color: #cba6f7;" + "}" + "entry:focus {" + " border-color: #cba6f7;" + "}" + "combobox {" + " background-color: #181825;" + " color: #cdd6f4;" + "}" + "combobox button {" + " background-image: none;" + " background-color: #181825;" + " color: #cdd6f4;" + " border: 1px solid #45475a;" + " border-radius: 10px;" + " padding: 4px 10px;" + "}" + "combobox button:hover {" + " border-color: #cba6f7;" + "}" + "combobox.answer-ok button {" + " border-color: #a6e3a1;" + " background-color: rgba(166, 227, 161, 0.12);" + "}" + "combobox.answer-wrong button {" + " border-color: #f38ba8;" + " background-color: rgba(243, 139, 168, 0.12);" + "}" + ".pill {" + " background-color: #2b2d40;" + " color: #cdd6f4;" + " border: 2px solid #3b3e56;" + " border-radius: 999px;" + " padding: 6px 18px;" + " font-size: 15px;" + " transition: border-color 150ms ease, box-shadow 150ms ease;" + "}" + ".pill:hover {" + " border-color: #cba6f7;" + "}" + ".pill:checked {" + " background-image: linear-gradient(135deg, #cba6f7 0%, #b4befe 100%);" + " color: #1e1e2e;" + " border-color: #cba6f7;" + " font-weight: 700;" + "}" + ".pill.ok {" + " border-color: #a6e3a1;" + " box-shadow: 0 0 0 2px rgba(166, 227, 161, 0.4);" + "}" + ".pill.wrong {" + " border-color: #f38ba8;" + " box-shadow: 0 0 0 2px rgba(243, 139, 168, 0.4);" + "}" + ".chip {" + " background-color: #313244;" + " color: #cdd6f4;" + " border: 1px solid #45475a;" + " border-radius: 999px;" + " padding: 6px 14px;" + " font-size: 14px;" + " font-weight: 600;" + " transition: background-color 150ms ease, border-color 150ms ease;" + "}" + ".chip:hover {" + " background-color: #45475a;" + " border-color: #cba6f7;" + "}" + ".group-btn {" + " background-color: #2b2d40;" + " color: #cdd6f4;" + " border: 2px solid #3b3e56;" + " border-radius: 12px;" + " padding: 8px 18px;" + " font-size: 14px;" + " font-weight: 700;" + " transition: border-color 150ms ease, box-shadow 150ms ease;" + "}" + ".group-btn:hover {" + " border-color: #cba6f7;" + "}" + ".group-btn:checked {" + " background-image: linear-gradient(135deg, #89b4fa 0%, #89dceb 100%);" + " color: #1e1e2e;" + " border-color: #89b4fa;" + "}" + ".group-panel {" + " background-color: rgba(49, 50, 68, 0.5);" + " border: 2px dashed #45475a;" + " border-radius: 14px;" + " padding: 10px;" + "}" + ".group-panel-label {" + " color: #a6adc8;" + " font-size: 13px;" + " font-weight: 700;" + "}" + ".sent-group {" + " background-color: rgba(49, 50, 68, 0.5);" + " border: 1px solid #45475a;" + " border-radius: 14px;" + " padding: 12px;" + "}" + ".sent-group.ok {" + " border-color: #a6e3a1;" + "}" + ".sent-group.wrong {" + " border-color: #f38ba8;" + "}" + ".feedback-ok {" + " color: #a6e3a1;" + " font-size: 15px;" + " font-weight: 700;" + "}" + ".feedback-err {" + " color: #f38ba8;" + " font-size: 15px;" + " font-weight: 700;" + "}" "tooltip {" " background-color: #181825;" " border: 1px solid #45475a;" @@ -604,7 +2336,6 @@ static void activate(GtkApplication *app, gpointer user_data) { "}" ); - // Apply CSS to the window gtk_style_context_add_provider_for_display( gdk_display_get_default(), GTK_STYLE_PROVIDER(provider),