fixed a bug with the second exercise in unit 1

This commit is contained in:
dvorakmortonxd 2026-09-05 22:48:58 +02:00
commit f7cfd36b90

View file

@ -56,22 +56,23 @@ static const char *ex_names[NUM_EXERCISES + 1] = {
"Länder", "Länder",
}; };
/* Scattered bubble positions on the unit-1 map (centre of each bubble). */ /* Scattered bubble positions on the unit-1 map, as fractions of the
static const double ex_pos[NUM_EXERCISES + 1][2] = { * available width/height so the map scales with the window. */
static const double scatter_pos[NUM_EXERCISES + 1][2] = {
{0.0, 0.0}, {0.0, 0.0},
{ 70.0, 100.0}, {0.100, 0.233},
{190.0, 45.0}, {0.271, 0.105},
{320.0, 90.0}, {0.457, 0.209},
{450.0, 40.0}, {0.643, 0.093},
{590.0, 95.0}, {0.843, 0.221},
{130.0, 210.0}, {0.186, 0.488},
{280.0, 185.0}, {0.400, 0.430},
{410.0, 215.0}, {0.586, 0.500},
{540.0, 190.0}, {0.771, 0.442},
{ 70.0, 320.0}, {0.100, 0.744},
{215.0, 330.0}, {0.307, 0.767},
{360.0, 325.0}, {0.514, 0.756},
{500.0, 335.0}, {0.714, 0.779},
}; };
static double path_x_at(double t) { static double path_x_at(double t) {
@ -301,6 +302,8 @@ static GtkWidget *top_bar(const char *back_target, const char *title,
GtkWidget *sub = gtk_label_new(subtitle); GtkWidget *sub = gtk_label_new(subtitle);
gtk_widget_set_halign(sub, GTK_ALIGN_CENTER); gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
gtk_widget_add_css_class(sub, "roadmap-sub"); 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); gtk_box_append(GTK_BOX(center), sub);
} }
@ -636,6 +639,77 @@ static GtkWidget *build_roadmap_page(void) {
return page; return page;
} }
/* ------------------------------------------------------------------ */
/* Scatter layout (scales bubble positions with the window) */
/* ------------------------------------------------------------------ */
#define SCATTER_MIN_W 600
#define SCATTER_MIN_H 380
#define SCATTER_NAT_W 760
#define SCATTER_NAT_H 440
typedef struct {
GtkWidget parent_instance;
GtkWidget *children[NUM_EXERCISES + 1];
} ScatterBox;
typedef struct {
GtkWidgetClass parent_class;
} ScatterBoxClass;
G_DEFINE_TYPE(ScatterBox, scatter_box, GTK_TYPE_WIDGET)
#define SCATTER_BOX(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), scatter_box_get_type(), ScatterBox))
static void scatter_box_measure(GtkWidget *widget, GtkOrientation orientation,
int for_size, int *minimum, int *natural,
int *minimum_baseline, int *natural_baseline) {
(void)widget;
(void)for_size;
if (orientation == GTK_ORIENTATION_HORIZONTAL) {
*minimum = SCATTER_MIN_W;
*natural = SCATTER_NAT_W;
} else {
*minimum = SCATTER_MIN_H;
*natural = SCATTER_NAT_H;
}
*minimum_baseline = -1;
*natural_baseline = -1;
}
static void scatter_box_size_allocate(GtkWidget *widget, int width, int height,
int baseline) {
ScatterBox *self = SCATTER_BOX(widget);
for (int i = 1; i <= NUM_EXERCISES; i++) {
GtkWidget *child = self->children[i];
GtkRequisition req;
GtkAllocation alloc;
if (!child)
continue;
gtk_widget_get_preferred_size(child, &req, NULL);
alloc.x = (int)(scatter_pos[i][0] * width) - req.width / 2;
alloc.y = (int)(scatter_pos[i][1] * height) - 32;
alloc.width = req.width;
alloc.height = req.height;
gtk_widget_size_allocate(child, &alloc, baseline);
}
}
static void scatter_box_init(ScatterBox *self) {
for (int i = 0; i <= NUM_EXERCISES; i++)
self->children[i] = NULL;
}
static void scatter_box_class_init(ScatterBoxClass *klass) {
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
widget_class->measure = scatter_box_measure;
widget_class->size_allocate = scatter_box_size_allocate;
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Unit 1 bubble map (scattered) */ /* Unit 1 bubble map (scattered) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@ -698,8 +772,7 @@ static GtkWidget *make_bubble(int n) {
static GtkWidget *build_unit1_page(void) { static GtkWidget *build_unit1_page(void) {
GtkWidget *page; GtkWidget *page;
GtkWidget *scroll; GtkWidget *scatter;
GtkWidget *fixed;
int i; int i;
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
@ -712,23 +785,15 @@ static GtkWidget *build_unit1_page(void) {
top_bar("roadmap", "Neue Freunde", top_bar("roadmap", "Neue Freunde",
"Vyberte cvičení a dokončete je.")); "Vyberte cvičení a dokončete je."));
scroll = gtk_scrolled_window_new(); scatter = g_object_new(scatter_box_get_type(), NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), gtk_widget_set_hexpand(scatter, TRUE);
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_widget_set_vexpand(scatter, TRUE);
gtk_widget_set_vexpand(scroll, TRUE); gtk_box_append(GTK_BOX(page), scatter);
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++) { for (i = 1; i <= NUM_EXERCISES; i++) {
GtkWidget *cell = make_bubble(i); GtkWidget *cell = make_bubble(i);
gtk_fixed_put(GTK_FIXED(fixed), cell, SCATTER_BOX(scatter)->children[i] = cell;
(int)(ex_pos[i][0] - 60.0), gtk_widget_set_parent(cell, scatter);
(int)(ex_pos[i][1] - 32.0));
} }
return page; return page;
@ -758,7 +823,7 @@ static GtkWidget *ex_page_shell(const char *title, const char *subtitle,
scroll = gtk_scrolled_window_new(); scroll = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_widget_set_vexpand(scroll, TRUE); gtk_widget_set_vexpand(scroll, TRUE);
gtk_widget_set_margin_top(scroll, 14); gtk_widget_set_margin_top(scroll, 14);
gtk_widget_set_margin_bottom(scroll, 14); gtk_widget_set_margin_bottom(scroll, 14);
@ -829,6 +894,24 @@ static GtkWidget *make_word_combo(const char **words, int n) {
return combo; 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) { static void combo_list_check(GtkButton *button, gpointer data) {
ComboListCtx *ctx = data; ComboListCtx *ctx = data;
guint total = ctx->combos->len; guint total = ctx->combos->len;
@ -844,10 +927,8 @@ static void combo_list_check(GtkButton *button, gpointer data) {
if (sel) { if (sel) {
gchar *t = normalize_answer(sel); gchar *t = normalize_answer(sel);
gchar *a = normalize_answer(ans); good = answer_accepts(t, ans);
good = (g_strcmp0(t, a) == 0);
g_free(t); g_free(t);
g_free(a);
g_free(sel); g_free(sel);
} }
answer_mark(GTK_WIDGET(combo), good); answer_mark(GTK_WIDGET(combo), good);
@ -883,12 +964,13 @@ typedef struct {
int n_words; int n_words;
} SentBuilder; } SentBuilder;
typedef struct {
SentBuilder *sb;
int idx;
} ChipRef;
static void sent_rebuild(SentBuilder *sb) { 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->pool));
flow_clear(GTK_FLOW_BOX(sb->target)); flow_clear(GTK_FLOW_BOX(sb->target));
@ -909,30 +991,91 @@ static void sent_rebuild(SentBuilder *sb) {
} }
} }
static void asm_chip_clicked(GtkButton *button, gpointer data) { typedef struct {
SentBuilder *sb;
int idx;
} ChipRef;
static GdkContentProvider *asm_drag_prepare(GtkDragSource *source,
double x, double y,
gpointer data) {
ChipRef *ref = data; ChipRef *ref = data;
SentBuilder *sb = ref->sb; (void)source;
int idx = ref->idx; (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; int pos = -1;
(void)button; (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++) { for (int i = 0; i < sb->placed_count; i++) {
if (sb->placed[i] == idx) { if (sb->placed[i] == idx) {
pos = i; pos = i;
break; break;
} }
} }
if (pos >= 0) { if (pos >= 0) {
for (int i = pos; i < sb->placed_count - 1; i++) for (int i = pos; i < sb->placed_count - 1; i++)
sb->placed[i] = sb->placed[i + 1]; sb->placed[i] = sb->placed[i + 1];
sb->placed_count--; sb->placed_count--;
} else { }
sb->placed[sb->placed_count++] = idx; sb->placed[sb->placed_count++] = idx;
return TRUE;
} }
sent_rebuild(sb); 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;
} }
typedef struct { typedef struct {
@ -1024,15 +1167,28 @@ static GtkWidget *build_assembly(const char *title, const char *subtitle,
sb->pool = pool; sb->pool = pool;
gtk_box_append(GTK_BOX(group), pool); gtk_box_append(GTK_BOX(group), pool);
{
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++) { for (int w = 0; w < item->n; w++) {
GtkWidget *chip = gtk_button_new_with_label(item->words[w]); GtkWidget *chip = gtk_button_new_with_label(item->words[w]);
GtkDragSource *src = gtk_drag_source_new();
ChipRef *ref = g_new(ChipRef, 1); ChipRef *ref = g_new(ChipRef, 1);
gtk_widget_add_css_class(chip, "chip"); gtk_widget_add_css_class(chip, "chip");
g_object_ref(chip); g_object_ref(chip);
sb->chips[w] = chip; sb->chips[w] = chip;
ref->sb = sb; ref->sb = sb;
ref->idx = w; ref->idx = w;
g_signal_connect(chip, "clicked", G_CALLBACK(asm_chip_clicked), ref); 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);
gtk_widget_add_controller(chip, GTK_EVENT_CONTROLLER(src));
} }
{ {
@ -1069,8 +1225,8 @@ typedef struct {
} Dialogue; } Dialogue;
static const DialogRow ex1_d1[] = { static const DialogRow ex1_d1[] = {
{"A:", "Hallo! Ich ", "bin", " Anna."}, {"A:", "Hallo! Ich ", "bin|heiße", " Anna."},
{"B:", "Hallo! Ich ", "heiße", " Peter."}, {"B:", "Hallo! Ich ", "bin|heiße", " Peter."},
{"A:", "Woher ", "kommst", " du?"}, {"A:", "Woher ", "kommst", " du?"},
{"B:", "Ich komme ", "aus", " Tschechien."}, {"B:", "Ich komme ", "aus", " Tschechien."},
}; };
@ -1721,6 +1877,12 @@ typedef struct {
} AssignChipRef; } AssignChipRef;
static void assign_rebuild(AssignCtx *ac) { static void assign_rebuild(AssignCtx *ac) {
for (int i = 0; i < ac->n_items; i++) {
GtkWidget *parent = gtk_widget_get_parent(ac->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(ac->pool)); flow_clear(GTK_FLOW_BOX(ac->pool));
for (int g = 0; g < ac->n_groups; g++) for (int g = 0; g < ac->n_groups; g++)
flow_clear(GTK_FLOW_BOX(ac->group_flow[g])); flow_clear(GTK_FLOW_BOX(ac->group_flow[g]));
@ -1928,7 +2090,7 @@ static void activate(GtkApplication *app, gpointer user_data) {
ex_pages[1] = build_ex1(); ex_pages[1] = build_ex1();
ex_pages[2] = build_assembly("Sätze bilden", ex_pages[2] = build_assembly("Sätze bilden",
"Seřaďte slova do správného pořadí.", "Přetáhněte slova do správného pořadí.",
2, ex2_items, G_N_ELEMENTS(ex2_items)); 2, ex2_items, G_N_ELEMENTS(ex2_items));
ex_pages[3] = build_choice("Was ist richtig?", "Vyberte správnou číslovku.", ex_pages[3] = build_choice("Was ist richtig?", "Vyberte správnou číslovku.",
3, ex3_questions, G_N_ELEMENTS(ex3_questions)); 3, ex3_questions, G_N_ELEMENTS(ex3_questions));
@ -1940,7 +2102,7 @@ static void activate(GtkApplication *app, gpointer user_data) {
ex_pages[9] = build_choice("Wer? Wie? Wo?", "Vyberte správné tázací slovo.", ex_pages[9] = build_choice("Wer? Wie? Wo?", "Vyberte správné tázací slovo.",
9, ex9_questions, G_N_ELEMENTS(ex9_questions)); 9, ex9_questions, G_N_ELEMENTS(ex9_questions));
ex_pages[10] = build_assembly("Wörter trennen", ex_pages[10] = build_assembly("Wörter trennen",
"Poskládejte slova do správného pořadí.", "Přetáhněte slova do správného pořadí.",
10, ex10_items, G_N_ELEMENTS(ex10_items)); 10, ex10_items, G_N_ELEMENTS(ex10_items));
ex_pages[11] = build_assign("Grußformen", "Roztřiďte pozdravy na pozdravy a rozloučení.", ex_pages[11] = build_assign("Grußformen", "Roztřiďte pozdravy na pozdravy a rozloučení.",
11, ex11_items, G_N_ELEMENTS(ex11_items), 11, ex11_items, G_N_ELEMENTS(ex11_items),