766 lines
24 KiB
C
766 lines
24 KiB
C
#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;
|
||
}
|
||
|
||
/* True when the typed answer matches the beginning of an accepted answer but
|
||
* is missing one or more trailing words (e.g. "jmenovat" for "jmenovat se").
|
||
* Used to warn about an incomplete answer before counting it wrong. */
|
||
gboolean answer_is_incomplete(const char *sel_norm, const char *ans) {
|
||
gchar **parts = g_strsplit(ans, "|", -1);
|
||
gboolean incomplete = FALSE;
|
||
|
||
for (int i = 0; parts[i] != NULL && !incomplete; i++) {
|
||
gchar *a = normalize_answer(parts[i]);
|
||
gchar **sel_w = g_strsplit(sel_norm, " ", -1);
|
||
gchar **ans_w = g_strsplit(a, " ", -1);
|
||
int nsel = g_strv_length(sel_w);
|
||
int nans = g_strv_length(ans_w);
|
||
int k;
|
||
|
||
if (sel_norm[0] && nsel > 0 && nsel < nans) {
|
||
incomplete = TRUE;
|
||
for (k = 0; k < nsel; k++) {
|
||
if (g_strcmp0(sel_w[k], ans_w[k]) != 0) {
|
||
incomplete = FALSE;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
g_strfreev(sel_w);
|
||
g_strfreev(ans_w);
|
||
g_free(a);
|
||
}
|
||
|
||
g_strfreev(parts);
|
||
return incomplete;
|
||
}
|
||
|
||
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áš?",
|
||
};
|