mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 15:14:35 +00:00
Move sources into src/ and runtime files into data/.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
452dba4a8a
commit
c2a632c7d6
36 changed files with 0 additions and 0 deletions
1595
src/cetba.c
Normal file
1595
src/cetba.c
Normal file
File diff suppressed because it is too large
Load diff
174
src/czech.c
Normal file
174
src/czech.c
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ---- Czech language and literature (Český jazyk a literatura) ----- */
|
||||
|
||||
#define CZ_NODES 3
|
||||
#define CZ_BUBBLE SUB_BUBBLE
|
||||
#define CZ_LABEL_W 200.0
|
||||
#define CZ_CW 600
|
||||
#define CZ_CH 470
|
||||
|
||||
static const char *cz_keys[CZ_NODES] = {
|
||||
"Literatura", "Mluvnice", "Maturitní četba",
|
||||
};
|
||||
static const char *cz_initials[CZ_NODES] = {
|
||||
"L", "M", "Č",
|
||||
};
|
||||
|
||||
static GtkWidget *cz_rail;
|
||||
static double cz_cx[CZ_NODES];
|
||||
static double cz_cy[CZ_NODES];
|
||||
|
||||
static void cz_geometry(void) {
|
||||
cz_cx[0] = 300.0; cz_cy[0] = 120.0; /* Literatura (top) */
|
||||
cz_cx[1] = 140.0; cz_cy[1] = 350.0; /* Mluvnice (bottom-left) */
|
||||
cz_cx[2] = 460.0; cz_cy[2] = 350.0; /* Maturitní četba (bottom-right) */
|
||||
}
|
||||
|
||||
/* Three bubbles joined into a triangle by thick rails. */
|
||||
static void draw_cz_rail(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const Rgb rail = color_from_hex(app_theme.rail);
|
||||
const Rgb slate = color_from_hex(app_theme.surface1);
|
||||
|
||||
(void)area;
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)data;
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_move_to(cr, cz_cx[0], cz_cy[0]);
|
||||
cairo_line_to(cr, cz_cx[1], cz_cy[1]);
|
||||
cairo_line_to(cr, cz_cx[2], cz_cy[2]);
|
||||
cairo_close_path(cr);
|
||||
|
||||
cairo_set_line_width(cr, 14);
|
||||
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
|
||||
cairo_stroke_preserve(cr);
|
||||
|
||||
cairo_set_line_width(cr, 9);
|
||||
cairo_set_source_rgb(cr, slate.r, slate.g, slate.b);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
void czech_rail_theme_reset(void) {
|
||||
if (cz_rail)
|
||||
gtk_widget_queue_draw(cz_rail);
|
||||
}
|
||||
|
||||
static GtkWidget *cz_make_node(int index, gboolean locked) {
|
||||
GtkWidget *btn;
|
||||
|
||||
btn = gtk_button_new();
|
||||
gtk_widget_add_css_class(btn, "unit-node");
|
||||
gtk_widget_set_can_focus(btn, FALSE);
|
||||
gtk_widget_set_size_request(btn, (int)CZ_BUBBLE, (int)CZ_BUBBLE);
|
||||
|
||||
if (locked) {
|
||||
gtk_widget_add_css_class(btn, "locked");
|
||||
gtk_widget_set_sensitive(btn, FALSE);
|
||||
gtk_button_set_child(GTK_BUTTON(btn),
|
||||
icon_area_new(draw_lock_icon,
|
||||
0.3451, 0.3569, 0.4392, 26));
|
||||
} else {
|
||||
GtkWidget *label = gtk_label_new(cz_initials[index]);
|
||||
|
||||
gtk_widget_add_css_class(btn, "current");
|
||||
gtk_widget_add_css_class(label, "unit-number");
|
||||
gtk_button_set_child(GTK_BUTTON(btn), label);
|
||||
}
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
GtkWidget *build_czechmap_page(void) {
|
||||
GtkWidget *page;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *wrap;
|
||||
GtkWidget *fixed;
|
||||
GtkWidget *rail;
|
||||
|
||||
cz_geometry();
|
||||
|
||||
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_hexpand(page, TRUE);
|
||||
gtk_widget_set_vexpand(page, TRUE);
|
||||
gtk_widget_set_margin_start(page, 32);
|
||||
gtk_widget_set_margin_end(page, 32);
|
||||
gtk_widget_set_margin_top(page, 24);
|
||||
gtk_widget_set_margin_bottom(page, 24);
|
||||
|
||||
gtk_box_append(GTK_BOX(page),
|
||||
top_bar("subjects", "Český jazyk a literatura",
|
||||
"czech_sub"));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 14);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
|
||||
wrap = gtk_center_box_new();
|
||||
gtk_widget_set_vexpand(wrap, TRUE);
|
||||
gtk_widget_set_hexpand(wrap, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
|
||||
|
||||
fixed = gtk_fixed_new();
|
||||
gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_size_request(fixed, CZ_CW, CZ_CH);
|
||||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
|
||||
|
||||
rail = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(rail, CZ_CW, CZ_CH);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_cz_rail,
|
||||
NULL, NULL);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
|
||||
cz_rail = rail;
|
||||
|
||||
for (int i = 0; i < CZ_NODES; i++) {
|
||||
/* Literatura has no lessons yet; keep the node visible but locked. */
|
||||
gboolean locked = (i == 0);
|
||||
GtkWidget *btn = cz_make_node(i, locked);
|
||||
GtkWidget *name = gtk_label_new(NULL);
|
||||
|
||||
if (i == 2) {
|
||||
g_object_set_data_full(G_OBJECT(btn), "target",
|
||||
g_strdup("readinglist"), g_free);
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
} else if (i == 1) {
|
||||
g_object_set_data_full(G_OBJECT(btn), "target",
|
||||
g_strdup("mluvnice"), g_free);
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
}
|
||||
|
||||
i18n_bind(name, cz_keys[i], 0);
|
||||
gtk_widget_set_size_request(name, (int)CZ_LABEL_W, -1);
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
|
||||
gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER);
|
||||
gtk_label_set_wrap(GTK_LABEL(name), TRUE);
|
||||
gtk_widget_add_css_class(name, "unit-name");
|
||||
gtk_widget_add_css_class(name, "cz-label");
|
||||
if (locked)
|
||||
gtk_widget_add_css_class(name, "unit-name-locked");
|
||||
|
||||
gtk_fixed_put(GTK_FIXED(fixed), btn, 0, 0);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), name, 0, 0);
|
||||
|
||||
gtk_fixed_move(GTK_FIXED(fixed), btn,
|
||||
(int)(cz_cx[i] - CZ_BUBBLE / 2.0),
|
||||
(int)(cz_cy[i] - CZ_BUBBLE / 2.0));
|
||||
gtk_fixed_move(GTK_FIXED(fixed), name,
|
||||
(int)(cz_cx[i] - CZ_LABEL_W / 2.0),
|
||||
(int)(cz_cy[i] + CZ_BUBBLE / 2.0 + 10.0));
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
857
src/exercise_common.c
Normal file
857
src/exercise_common.c
Normal file
|
|
@ -0,0 +1,857 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exercise page shell */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Exercise text would otherwise run the full width of a maximised window,
|
||||
* which is tiring to read. Keep the body at a comfortable measure and centre
|
||||
* it once there is room to spare; below that it still fills the page. */
|
||||
#define EX_BODY_MAX_WIDTH 880
|
||||
|
||||
static void ex_body_clamp(GtkAdjustment *adj, GParamSpec *ps, gpointer data) {
|
||||
GtkWidget *body = data;
|
||||
double avail = gtk_adjustment_get_page_size(adj);
|
||||
|
||||
(void)ps;
|
||||
if (avail < 1.0)
|
||||
return;
|
||||
if (avail < (double)EX_BODY_MAX_WIDTH) {
|
||||
gtk_widget_set_halign(body, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_size_request(body, -1, -1);
|
||||
} else {
|
||||
gtk_widget_set_halign(body, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_size_request(body, EX_BODY_MAX_WIDTH, -1);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
g_signal_connect(gtk_scrolled_window_get_hadjustment(
|
||||
GTK_SCROLLED_WINDOW(scroll)),
|
||||
"notify::page-size", G_CALLBACK(ex_body_clamp), 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;
|
||||
}
|
||||
|
||||
GtkWidget *mcq_option_toggle(const char *text, int index,
|
||||
GtkToggleButton **group_first) {
|
||||
GtkWidget *tb;
|
||||
GtkWidget *lab;
|
||||
char *labeled;
|
||||
|
||||
labeled = g_strdup_printf("%c %s", 'A' + index, text);
|
||||
tb = gtk_toggle_button_new();
|
||||
lab = gtk_label_new(labeled);
|
||||
g_free(labeled);
|
||||
|
||||
gtk_label_set_wrap(GTK_LABEL(lab), TRUE);
|
||||
gtk_label_set_wrap_mode(GTK_LABEL(lab), PANGO_WRAP_WORD_CHAR);
|
||||
gtk_label_set_xalign(GTK_LABEL(lab), 0.0);
|
||||
gtk_widget_set_halign(lab, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_hexpand(lab, TRUE);
|
||||
gtk_button_set_child(GTK_BUTTON(tb), lab);
|
||||
|
||||
gtk_widget_add_css_class(tb, "pill");
|
||||
gtk_widget_add_css_class(tb, "quiz-pill");
|
||||
gtk_widget_set_hexpand(tb, TRUE);
|
||||
gtk_widget_set_halign(tb, GTK_ALIGN_FILL);
|
||||
|
||||
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), *group_first);
|
||||
if (!*group_first)
|
||||
*group_first = GTK_TOGGLE_BUTTON(tb);
|
||||
|
||||
return tb;
|
||||
}
|
||||
|
||||
GtkWidget *mcq_append_question(GtkWidget *parent, int num, const ChoiceQ *q,
|
||||
GtkToggleButton **out_toggles) {
|
||||
GtkWidget *card;
|
||||
GtkWidget *prompt;
|
||||
GtkWidget *opts;
|
||||
GtkToggleButton *first = NULL;
|
||||
char *qtext;
|
||||
int o;
|
||||
|
||||
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||||
gtk_widget_add_css_class(card, "quiz-card");
|
||||
gtk_widget_set_hexpand(card, TRUE);
|
||||
gtk_box_append(GTK_BOX(parent), card);
|
||||
|
||||
qtext = g_strdup_printf("%d.) %s", num, q->prompt);
|
||||
prompt = gtk_label_new(qtext);
|
||||
g_free(qtext);
|
||||
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(prompt), 0.0);
|
||||
gtk_widget_add_css_class(prompt, "quiz-q");
|
||||
gtk_box_append(GTK_BOX(card), prompt);
|
||||
|
||||
opts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_add_css_class(opts, "quiz-options");
|
||||
gtk_box_append(GTK_BOX(card), opts);
|
||||
|
||||
for (o = 0; o < q->n_options; o++) {
|
||||
GtkWidget *tb = mcq_option_toggle(q->options[o], o, &first);
|
||||
|
||||
gtk_box_append(GTK_BOX(opts), tb);
|
||||
out_toggles[o] = GTK_TOGGLE_BUTTON(tb);
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/* 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áš?",
|
||||
};
|
||||
1360
src/exercise_u1.c
Normal file
1360
src/exercise_u1.c
Normal file
File diff suppressed because it is too large
Load diff
1540
src/exercise_u2.c
Normal file
1540
src/exercise_u2.c
Normal file
File diff suppressed because it is too large
Load diff
1547
src/exercise_u3.c
Normal file
1547
src/exercise_u3.c
Normal file
File diff suppressed because it is too large
Load diff
83
src/globals.c
Normal file
83
src/globals.c
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
#include "maturita.h"
|
||||
|
||||
GtkStack *main_stack;
|
||||
|
||||
GtkWindow *main_window;
|
||||
|
||||
GtkWidget *welcome_heading;
|
||||
|
||||
GtkCssProvider *theme_provider;
|
||||
|
||||
ThemeId app_theme_id = THEME_CATPPUCCIN;
|
||||
|
||||
ColorMode app_color_mode = MODE_DARK;
|
||||
|
||||
UiLang app_lang = LANG_CS;
|
||||
|
||||
ThemePalette app_theme;
|
||||
|
||||
double app_ui_scale = 1.0;
|
||||
|
||||
const char *theme_names[THEME_COUNT] = {
|
||||
"Catppuccin",
|
||||
"Nord",
|
||||
"Dracula",
|
||||
"Rose Pine",
|
||||
"Ocean",
|
||||
"Gruvbox",
|
||||
"Solarized",
|
||||
"Everforest",
|
||||
"Monokai",
|
||||
"One Dark",
|
||||
};
|
||||
|
||||
GtkWidget *theme_swatch_areas[THEME_COUNT];
|
||||
|
||||
UnitCtx units[NUM_UNITS] = {0};
|
||||
|
||||
const char *const u1_ex_names[MAX_UNIT_EX + 1] = {
|
||||
NULL,
|
||||
"Dialog", "Sätze bilden", "Was ist richtig?", "Freie Antwort",
|
||||
"Zahlen", "Wie viel?", "Zahlenreihe", "Verb einsetzen",
|
||||
"Wer? Wie? Wo?", "Wörter trennen", "Grußformen",
|
||||
"Was macht er/sie?", "Länder",
|
||||
};
|
||||
|
||||
const char *const u2_ex_names[MAX_UNIT_EX + 1] = {
|
||||
NULL,
|
||||
"Verben konjugieren", "aus oder in", "Fragewörter", "sprechen",
|
||||
"Nationalitäten", "Woher?", "Länder schreiben", "Verben einsetzen",
|
||||
"Freie Antwort", "Euro", "Wörter suchen", "Was ist richtig?",
|
||||
"Ordne zu", "Lückentext", "Verbinde", "Zahlen",
|
||||
"Steckbrief", "Berufe", "Nationalität",
|
||||
};
|
||||
|
||||
const char *const u3_ex_names[MAX_UNIT_EX + 1] = {
|
||||
NULL,
|
||||
"Familienpaare", "mein oder dein", "ein / kein", "Lückentext",
|
||||
"Marcos Familie", "Sortieren", "Possessivtabelle", "Akkusativ",
|
||||
"kein / nicht", "Sätze bauen", "Was siehst du?", "Wem gehört das?",
|
||||
"Es gibt …", "Beschreiben", "Wochenende",
|
||||
};
|
||||
|
||||
GtkWidget *road_scroll;
|
||||
|
||||
GtkWidget *road_fixed;
|
||||
|
||||
GtkWidget *road_rail;
|
||||
|
||||
GtkWidget *road_nodes[NUM_UNITS];
|
||||
|
||||
GtkWidget *road_labels[NUM_UNITS];
|
||||
|
||||
double road_cx[NUM_UNITS];
|
||||
|
||||
double road_cy[NUM_UNITS];
|
||||
|
||||
int road_cols = NUM_UNITS;
|
||||
|
||||
int road_rows = 1;
|
||||
|
||||
int road_cw = (int)(2.0 * ROAD_MX + (NUM_UNITS - 1) * PATH_SPAC);
|
||||
|
||||
int road_ch = (int)(2.0 * ROAD_MY);
|
||||
906
src/hw.c
Normal file
906
src/hw.c
Normal file
|
|
@ -0,0 +1,906 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ---- Technical equipment (Technické vybavení) --------------------- */
|
||||
|
||||
NetLesson hw_lessons[HW_LESSONS] = {
|
||||
{ .n_slides = HW_SLIDES, .unit_page = "hwunit1", .ex_page = "hwex1" },
|
||||
{ .n_slides = HW2_SLIDES, .unit_page = "hwunit2", .ex_page = "hwex2" },
|
||||
{ .n_slides = HW3_SLIDES, .unit_page = "hwunit3", .ex_page = "hwex3" },
|
||||
};
|
||||
|
||||
void hw_rail_theme_reset(void);
|
||||
void refresh_hw_completion_ui(void);
|
||||
|
||||
GtkWidget *hw_scroll;
|
||||
GtkWidget *hw_fixed;
|
||||
GtkWidget *hw_rail;
|
||||
GtkWidget *hw_nodes[HW_UNITS];
|
||||
GtkWidget *hw_labels[HW_UNITS];
|
||||
double hw_cx[HW_UNITS];
|
||||
double hw_cy[HW_UNITS];
|
||||
int hw_cols = 1;
|
||||
int hw_rows = 1;
|
||||
int hw_cw = (int)(2.0 * ROAD_MX + (HW_UNITS - 1) * PATH_SPAC);
|
||||
int hw_ch = (int)(2.0 * ROAD_MY);
|
||||
guint hw_idle;
|
||||
double hw_last_avail = -1.0;
|
||||
int hw_last_cols = -1;
|
||||
int hw_last_rows = -1;
|
||||
int hw_last_cw = -1;
|
||||
int hw_last_ch = -1;
|
||||
cairo_surface_t *hw_rail_cache;
|
||||
int hw_cache_w;
|
||||
int hw_cache_h;
|
||||
|
||||
void hw_point(double t, double *ox, double *oy) {
|
||||
int k = (int)t;
|
||||
double u = t - (double)k;
|
||||
double u2, u3;
|
||||
double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
|
||||
|
||||
if (k < 0) { k = 0; u = 0.0; }
|
||||
if (k >= HW_UNITS - 1) { k = HW_UNITS - 2; u = 1.0; }
|
||||
|
||||
p1x = hw_cx[k]; p1y = hw_cy[k];
|
||||
p2x = hw_cx[k + 1]; p2y = hw_cy[k + 1];
|
||||
if (k - 1 >= 0) { p0x = hw_cx[k - 1]; p0y = hw_cy[k - 1]; }
|
||||
else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
|
||||
if (k + 2 < HW_UNITS) { p3x = hw_cx[k + 2]; p3y = hw_cy[k + 2]; }
|
||||
else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
|
||||
|
||||
u2 = u * u;
|
||||
u3 = u2 * u;
|
||||
*ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
|
||||
+ (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
|
||||
+ (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
|
||||
*oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
|
||||
+ (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
|
||||
+ (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
|
||||
}
|
||||
|
||||
void hw_path(cairo_t *cr, double t0, double t1) {
|
||||
const double steps_per_unit = 24.0;
|
||||
int n = (int)((t1 - t0) * steps_per_unit);
|
||||
double x, y;
|
||||
int s;
|
||||
|
||||
if (n < 1)
|
||||
n = 1;
|
||||
hw_point(t0, &x, &y);
|
||||
cairo_move_to(cr, x, y);
|
||||
for (s = 1; s <= n; s++) {
|
||||
hw_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
|
||||
cairo_line_to(cr, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void hw_geometry(double avail) {
|
||||
const double phase = 2.0 * G_PI * 1.7 / (double)(HW_UNITS - 1);
|
||||
int rows;
|
||||
int r, c, i;
|
||||
|
||||
for (rows = 1; rows <= HW_UNITS; rows++) {
|
||||
int cols = (HW_UNITS + rows - 1) / rows;
|
||||
if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0)
|
||||
break;
|
||||
}
|
||||
if (rows > HW_UNITS)
|
||||
rows = HW_UNITS;
|
||||
hw_rows = rows;
|
||||
hw_cols = (HW_UNITS + rows - 1) / rows;
|
||||
if (hw_cols < 1)
|
||||
hw_cols = 1;
|
||||
hw_cw = (int)(2.0 * ROAD_MX + (hw_cols - 1) * PATH_SPAC);
|
||||
hw_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP);
|
||||
|
||||
for (r = 0; r < rows; r++) {
|
||||
int base = r * hw_cols;
|
||||
int len = MIN(hw_cols, HW_UNITS - base);
|
||||
int fwd = (r % 2) == 0;
|
||||
for (c = 0; c < len; c++) {
|
||||
i = base + c;
|
||||
int cc = fwd ? c : (hw_cols - 1 - c);
|
||||
hw_cx[i] = ROAD_MX + cc * PATH_SPAC;
|
||||
hw_cy[i] = ROAD_MY + r * ROAD_GAP
|
||||
+ ROAD_WAVE * sin((double)i * phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void hw_apply_layout(void) {
|
||||
if (!hw_fixed)
|
||||
return;
|
||||
|
||||
if (hw_cols == hw_last_cols && hw_rows == hw_last_rows &&
|
||||
hw_cw == hw_last_cw && hw_ch == hw_last_ch)
|
||||
return;
|
||||
|
||||
hw_last_cols = hw_cols;
|
||||
hw_last_rows = hw_rows;
|
||||
hw_last_cw = hw_cw;
|
||||
hw_last_ch = hw_ch;
|
||||
if (hw_rail_cache) {
|
||||
cairo_surface_destroy(hw_rail_cache);
|
||||
hw_rail_cache = NULL;
|
||||
}
|
||||
|
||||
for (int i = 0; i < HW_UNITS; i++) {
|
||||
if (!hw_nodes[i] || !hw_labels[i])
|
||||
continue;
|
||||
gtk_fixed_move(GTK_FIXED(hw_fixed), hw_nodes[i],
|
||||
(int)(hw_cx[i] - NODE_SIZE / 2.0),
|
||||
(int)(hw_cy[i] - NODE_SIZE / 2.0));
|
||||
gtk_fixed_move(GTK_FIXED(hw_fixed), hw_labels[i],
|
||||
(int)(hw_cx[i] - (PATH_SPAC - 20.0) / 2.0),
|
||||
(int)(hw_cy[i] + NODE_SIZE / 2.0 + 10.0));
|
||||
}
|
||||
|
||||
gtk_widget_set_size_request(hw_fixed, hw_cw, hw_ch);
|
||||
gtk_widget_set_size_request(hw_rail, hw_cw, hw_ch);
|
||||
gtk_fixed_move(GTK_FIXED(hw_fixed), hw_rail, 0, 0);
|
||||
gtk_widget_queue_draw(hw_rail);
|
||||
}
|
||||
|
||||
void hw_relayout(void) {
|
||||
GtkAdjustment *hadj;
|
||||
double avail;
|
||||
|
||||
if (!hw_scroll)
|
||||
return;
|
||||
hadj = gtk_scrolled_window_get_hadjustment(
|
||||
GTK_SCROLLED_WINDOW(hw_scroll));
|
||||
avail = gtk_adjustment_get_page_size(hadj);
|
||||
if (avail < 1.0)
|
||||
return;
|
||||
if (fabs(avail - hw_last_avail) < 1.0)
|
||||
return;
|
||||
hw_last_avail = avail;
|
||||
hw_geometry(avail);
|
||||
hw_apply_layout();
|
||||
}
|
||||
|
||||
gboolean hw_relayout_idle(gpointer data) {
|
||||
(void)data;
|
||||
hw_idle = 0;
|
||||
hw_relayout();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void hw_relayout_later(void) {
|
||||
if (hw_idle == 0)
|
||||
hw_idle = g_idle_add(hw_relayout_idle, NULL);
|
||||
}
|
||||
|
||||
void hw_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
|
||||
gpointer data) {
|
||||
(void)adj;
|
||||
(void)ps;
|
||||
(void)data;
|
||||
hw_relayout_later();
|
||||
}
|
||||
|
||||
void hw_render_rail_to(cairo_t *cr) {
|
||||
const double t_end = (double)(HW_UNITS - 1);
|
||||
const Rgb rail = color_from_hex(app_theme.rail);
|
||||
const Rgb muted = color_from_hex(app_theme.subtext);
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
|
||||
cairo_new_path(cr);
|
||||
hw_path(cr, 0.0, t_end);
|
||||
cairo_set_line_width(cr, 12);
|
||||
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17);
|
||||
cairo_set_dash(cr, (double[]){2.0, 40.0}, 2, 0.0);
|
||||
cairo_new_path(cr);
|
||||
hw_path(cr, 0.0, t_end);
|
||||
cairo_stroke(cr);
|
||||
cairo_set_dash(cr, NULL, 0, 0.0);
|
||||
}
|
||||
|
||||
void hw_draw_rail(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
(void)area;
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)data;
|
||||
|
||||
if (!hw_rail_cache || hw_cache_w != hw_cw || hw_cache_h != hw_ch) {
|
||||
cairo_t *rcr;
|
||||
|
||||
if (hw_rail_cache)
|
||||
cairo_surface_destroy(hw_rail_cache);
|
||||
hw_cache_w = hw_cw;
|
||||
hw_cache_h = hw_ch;
|
||||
if (hw_cache_w < 1)
|
||||
hw_cache_w = 1;
|
||||
if (hw_cache_h < 1)
|
||||
hw_cache_h = 1;
|
||||
hw_rail_cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
|
||||
hw_cache_w, hw_cache_h);
|
||||
rcr = cairo_create(hw_rail_cache);
|
||||
hw_render_rail_to(rcr);
|
||||
cairo_destroy(rcr);
|
||||
}
|
||||
|
||||
cairo_set_source_surface(cr, hw_rail_cache, 0, 0);
|
||||
cairo_paint(cr);
|
||||
}
|
||||
|
||||
void hw_rail_theme_reset(void) {
|
||||
if (hw_rail_cache) {
|
||||
cairo_surface_destroy(hw_rail_cache);
|
||||
hw_rail_cache = NULL;
|
||||
}
|
||||
if (hw_rail)
|
||||
gtk_widget_queue_draw(hw_rail);
|
||||
}
|
||||
|
||||
void hw_add_node(GtkFixed *fixed, int index) {
|
||||
int num = index + 1;
|
||||
gboolean locked = index >= HW_LESSONS;
|
||||
GtkWidget *card;
|
||||
GtkWidget *vbox;
|
||||
GtkWidget *number;
|
||||
GtkWidget *icon;
|
||||
GtkWidget *name;
|
||||
char *text;
|
||||
static const char *unit_keys[HW_LESSONS] = {
|
||||
"hw_unit1", "hw_unit2", "hw_unit3"
|
||||
};
|
||||
|
||||
card = gtk_button_new();
|
||||
gtk_widget_add_css_class(card, "unit-node");
|
||||
gtk_widget_set_can_focus(card, FALSE);
|
||||
gtk_widget_set_sensitive(card, !locked);
|
||||
gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE);
|
||||
|
||||
if (locked)
|
||||
gtk_widget_add_css_class(card, "locked");
|
||||
else
|
||||
gtk_widget_add_css_class(card, "current");
|
||||
|
||||
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||||
gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
|
||||
gtk_button_set_child(GTK_BUTTON(card), vbox);
|
||||
|
||||
text = g_strdup_printf("%d", num);
|
||||
number = gtk_label_new(text);
|
||||
g_free(text);
|
||||
gtk_widget_add_css_class(number, "unit-number");
|
||||
gtk_box_append(GTK_BOX(vbox), number);
|
||||
|
||||
if (locked) {
|
||||
icon = icon_area_new(draw_lock_icon, 0.3451, 0.3569, 0.4392, 16);
|
||||
gtk_box_append(GTK_BOX(vbox), icon);
|
||||
} else {
|
||||
icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 18);
|
||||
gtk_widget_set_visible(icon, FALSE);
|
||||
gtk_box_append(GTK_BOX(vbox), icon);
|
||||
hw_lessons[index].done_icon = icon;
|
||||
g_signal_connect(card, "clicked", G_CALLBACK(net_open_unit),
|
||||
&hw_lessons[index]);
|
||||
}
|
||||
|
||||
hw_nodes[index] = card;
|
||||
gtk_fixed_put(fixed, card, 0, 0);
|
||||
|
||||
name = gtk_label_new(NULL);
|
||||
gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1);
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
|
||||
gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER);
|
||||
gtk_label_set_wrap(GTK_LABEL(name), TRUE);
|
||||
gtk_widget_add_css_class(name, "unit-name");
|
||||
if (index < HW_LESSONS) {
|
||||
i18n_bind(name, unit_keys[index], 0);
|
||||
} else {
|
||||
gtk_widget_add_css_class(name, "unit-name-locked");
|
||||
}
|
||||
hw_labels[index] = name;
|
||||
gtk_fixed_put(fixed, name, 0, 0);
|
||||
}
|
||||
|
||||
GtkWidget *build_hwmap_page(void) {
|
||||
GtkWidget *page;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *wrap;
|
||||
GtkWidget *fixed;
|
||||
GtkWidget *rail;
|
||||
GtkAdjustment *ha;
|
||||
GtkAdjustment *va;
|
||||
|
||||
hw_geometry(1000.0);
|
||||
|
||||
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_hexpand(page, TRUE);
|
||||
gtk_widget_set_vexpand(page, TRUE);
|
||||
gtk_widget_set_margin_start(page, 32);
|
||||
gtk_widget_set_margin_end(page, 32);
|
||||
gtk_widget_set_margin_top(page, 24);
|
||||
gtk_widget_set_margin_bottom(page, 24);
|
||||
|
||||
gtk_box_append(GTK_BOX(page),
|
||||
top_bar("subjects", "Technické vybavení", "hw_sub"));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 14);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
hw_scroll = scroll;
|
||||
|
||||
wrap = gtk_center_box_new();
|
||||
gtk_widget_set_vexpand(wrap, TRUE);
|
||||
gtk_widget_set_hexpand(wrap, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
|
||||
|
||||
fixed = gtk_fixed_new();
|
||||
gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_size_request(fixed, hw_cw, hw_ch);
|
||||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
|
||||
hw_fixed = fixed;
|
||||
|
||||
rail = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(rail, hw_cw, hw_ch);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), hw_draw_rail,
|
||||
NULL, NULL);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
|
||||
hw_rail = rail;
|
||||
|
||||
for (int i = 0; i < HW_UNITS; i++)
|
||||
hw_add_node(GTK_FIXED(fixed), i);
|
||||
|
||||
ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
g_signal_connect(ha, "notify::page-size",
|
||||
G_CALLBACK(hw_adjust_notify), NULL);
|
||||
g_signal_connect(va, "notify::page-size",
|
||||
G_CALLBACK(hw_adjust_notify), NULL);
|
||||
|
||||
hw_apply_layout();
|
||||
hw_relayout_later();
|
||||
refresh_hw_completion_ui();
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
|
||||
void hw_slide_apply(NetLesson *L);
|
||||
void hw_slide_prev(GtkButton *button, gpointer data);
|
||||
void hw_slide_next(GtkButton *button, gpointer data);
|
||||
|
||||
/* ---- Unit 1: Architektura počítače -------------------------------- */
|
||||
|
||||
static GtkWidget *hw_note_host;
|
||||
static int hw_note_last_w = -1;
|
||||
|
||||
static void hw_rescale_notes_at(int w) {
|
||||
int body, head, kick;
|
||||
|
||||
if (w < 160)
|
||||
w = 160;
|
||||
body = w / 46;
|
||||
if (body < 14) body = 14;
|
||||
if (body > 26) body = 26;
|
||||
head = body + 14;
|
||||
if (head > 46) head = 46;
|
||||
kick = body - 3;
|
||||
if (kick < 11) kick = 11;
|
||||
for (int i = 0; i < HW_LESSONS; i++)
|
||||
net_rescale_lesson_notes(&hw_lessons[i], body, head, kick);
|
||||
}
|
||||
|
||||
static gboolean hw_note_tick(GtkWidget *w, GdkFrameClock *clock,
|
||||
gpointer data) {
|
||||
int width = gtk_widget_get_allocated_width(w);
|
||||
|
||||
(void)clock;
|
||||
(void)data;
|
||||
if (width != hw_note_last_w) {
|
||||
hw_note_last_w = width;
|
||||
hw_rescale_notes_at(width);
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static GtkWidget *build_hw_unit_page(NetLesson *L, const char *title_key,
|
||||
const char *sub_key,
|
||||
const NetSlide *slides, guint n_slides) {
|
||||
GtkWidget *page;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *nav;
|
||||
|
||||
net_lesson_notes_ensure(L);
|
||||
net_notes_target = L;
|
||||
L->n_slides = n_slides;
|
||||
|
||||
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_margin_start(page, 28);
|
||||
gtk_widget_set_margin_end(page, 28);
|
||||
gtk_widget_set_margin_top(page, 20);
|
||||
gtk_widget_set_margin_bottom(page, 20);
|
||||
|
||||
gtk_box_append(GTK_BOX(page), top_bar("hwmap", title_key, sub_key));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 10);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
|
||||
L->stack = gtk_stack_new();
|
||||
gtk_stack_set_transition_type(GTK_STACK(L->stack),
|
||||
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), L->stack);
|
||||
hw_note_host = L->stack;
|
||||
|
||||
for (guint s = 0; s < n_slides; s++) {
|
||||
GtkWidget *stage = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
GtkWidget *card;
|
||||
char name[16];
|
||||
|
||||
gtk_widget_set_vexpand(stage, TRUE);
|
||||
g_snprintf(name, sizeof(name), "hw_slide%u", s);
|
||||
gtk_stack_add_named(GTK_STACK(L->stack), stage, name);
|
||||
|
||||
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 16);
|
||||
gtk_widget_add_css_class(card, "notes-card");
|
||||
gtk_widget_set_valign(card, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(stage), card);
|
||||
|
||||
net_note_kicker(card, slides[s].kicker);
|
||||
net_note_title(card, slides[s].title);
|
||||
for (int i = 0; i < 8 && slides[s].line[i]; i++)
|
||||
net_note_line(card, slides[s].line[i], FALSE);
|
||||
if (slides[s].tip)
|
||||
net_note_line(card, slides[s].tip, TRUE);
|
||||
}
|
||||
|
||||
gtk_widget_add_tick_callback(L->stack, hw_note_tick, NULL, NULL);
|
||||
hw_note_last_w = -1;
|
||||
hw_rescale_notes_at(0);
|
||||
net_notes_target = NULL;
|
||||
|
||||
nav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
gtk_box_set_homogeneous(GTK_BOX(nav), TRUE);
|
||||
gtk_widget_set_margin_top(nav, 10);
|
||||
gtk_box_append(GTK_BOX(page), nav);
|
||||
|
||||
L->prev_btn = gtk_button_new_with_label(tr("net_slide_prev"));
|
||||
gtk_widget_add_css_class(L->prev_btn, "btn-primary");
|
||||
g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(hw_slide_prev), L);
|
||||
gtk_box_append(GTK_BOX(nav), L->prev_btn);
|
||||
|
||||
L->next_btn = gtk_button_new_with_label(tr("net_slide_next"));
|
||||
gtk_widget_add_css_class(L->next_btn, "btn-primary");
|
||||
g_signal_connect(L->next_btn, "clicked", G_CALLBACK(hw_slide_next), L);
|
||||
gtk_box_append(GTK_BOX(nav), L->next_btn);
|
||||
|
||||
L->idx = 0;
|
||||
hw_slide_apply(L);
|
||||
return page;
|
||||
}
|
||||
|
||||
void hw_slide_apply(NetLesson *L) {
|
||||
char name[16];
|
||||
|
||||
if (!L || !L->stack)
|
||||
return;
|
||||
g_snprintf(name, sizeof(name), "hw_slide%u", L->idx);
|
||||
gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name);
|
||||
if (L->prev_btn) {
|
||||
gtk_button_set_label(GTK_BUTTON(L->prev_btn), tr("net_slide_prev"));
|
||||
gtk_widget_set_sensitive(L->prev_btn, L->idx > 0);
|
||||
}
|
||||
if (L->next_btn) {
|
||||
gtk_button_set_label(
|
||||
GTK_BUTTON(L->next_btn),
|
||||
L->idx + 1 >= L->n_slides ? tr("net_slide_start")
|
||||
: tr("net_slide_next"));
|
||||
}
|
||||
}
|
||||
|
||||
void hw_slide_prev(GtkButton *button, gpointer data) {
|
||||
NetLesson *L = data;
|
||||
(void)button;
|
||||
if (!L) return;
|
||||
if (L->idx > 0) {
|
||||
L->idx--;
|
||||
hw_slide_apply(L);
|
||||
}
|
||||
}
|
||||
|
||||
void hw_slide_next(GtkButton *button, gpointer data) {
|
||||
NetLesson *L = data;
|
||||
(void)button;
|
||||
if (!L) return;
|
||||
if (L->idx + 1 < L->n_slides) {
|
||||
L->idx++;
|
||||
hw_slide_apply(L);
|
||||
} else {
|
||||
gtk_stack_set_visible_child_name(main_stack, L->ex_page);
|
||||
}
|
||||
}
|
||||
|
||||
void hw_lessons_apply_lang(void) {
|
||||
for (int i = 0; i < HW_LESSONS; i++)
|
||||
hw_slide_apply(&hw_lessons[i]);
|
||||
}
|
||||
|
||||
GtkWidget *build_hw_unit1_page(void) {
|
||||
static const NetSlide slides[HW_SLIDES] = {
|
||||
{
|
||||
"1 / 3 • Úvod", "Architektura počítače",
|
||||
"Architektura popisuje, z jakých částí se počítač skládá "
|
||||
"a jak spolu spolupracují.",
|
||||
{
|
||||
"Architektura počítače je základní model, podle kterého "
|
||||
"jsou počítače navrhovány.",
|
||||
"Nejznámější model je Von Neumannovo schéma.",
|
||||
"Používá se dodnes u většiny běžných počítačů.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"2 / 3 • Osobnost", "John von Neumann",
|
||||
"Tip: zapamatuj si jméno – podle něj se jmenuje celé schéma.",
|
||||
{
|
||||
"John von Neumann byl americký matematik.",
|
||||
"Pracoval na poli digitálních počítačů a operační teorie "
|
||||
"kvantové mechaniky.",
|
||||
"Je autorem Von Neumannovy algebry.",
|
||||
"Byl tvůrcem teorie her.",
|
||||
"Podílel se na vývoji atomové a vodíkové bomby.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"3 / 3 • Schéma", "Von Neumannovo schéma",
|
||||
"ALU = aritmeticko-logická jednotka",
|
||||
{
|
||||
"Von Neumannovo schéma má pět hlavních částí:",
|
||||
"ALU – aritmeticko-logická jednotka (výpočty a logika)",
|
||||
"Řadič – řídí činnost ostatních částí",
|
||||
"Operační paměť – uchovává program i data",
|
||||
"Vstupní jednotka – přivádí data do počítače",
|
||||
"Výstupní jednotka – předává výsledky ven",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return build_hw_unit_page(&hw_lessons[0], "hw_unit1", "hw_unit1_sub",
|
||||
slides, HW_SLIDES);
|
||||
}
|
||||
|
||||
/* ---- Quiz --------------------------------------------------------- */
|
||||
|
||||
static void hw_mcq_check(GtkButton *button, gpointer data) {
|
||||
NetMcqCtx *ctx = data;
|
||||
int ok = 0;
|
||||
|
||||
(void)button;
|
||||
for (int i = 0; i < ctx->n; i++) {
|
||||
int active = -1;
|
||||
for (int o = 0; o < ctx->n_opts; o++) {
|
||||
GtkToggleButton *tb = ctx->toggles[i * ctx->n_opts + o];
|
||||
gtk_widget_remove_css_class(GTK_WIDGET(tb), "ok");
|
||||
gtk_widget_remove_css_class(GTK_WIDGET(tb), "wrong");
|
||||
if (gtk_toggle_button_get_active(tb))
|
||||
active = o;
|
||||
}
|
||||
if (active == ctx->qs[i].correct) {
|
||||
ok++;
|
||||
gtk_widget_add_css_class(
|
||||
GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "ok");
|
||||
} else if (active >= 0) {
|
||||
gtk_widget_add_css_class(
|
||||
GTK_WIDGET(ctx->toggles[i * ctx->n_opts + active]), "wrong");
|
||||
}
|
||||
if (ctx->hints && ctx->hints[i])
|
||||
gtk_widget_set_visible(ctx->hints[i], TRUE);
|
||||
}
|
||||
if (ok == ctx->n) {
|
||||
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
|
||||
mark_hw_done(ctx->lesson_id);
|
||||
} else {
|
||||
set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short"));
|
||||
}
|
||||
}
|
||||
|
||||
static GtkWidget *build_hw_mcq_page(int lesson_id, const char *back_page,
|
||||
const char *title_key,
|
||||
const char *heading_key,
|
||||
const ChoiceQ *qs, const char **hints,
|
||||
int n) {
|
||||
GtkWidget *page;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *body;
|
||||
GtkWidget *check;
|
||||
GtkWidget *btns;
|
||||
GtkWidget *head;
|
||||
GtkWidget *intro;
|
||||
NetMcqCtx *ctx = g_new0(NetMcqCtx, 1);
|
||||
int n_opts = qs[0].n_options;
|
||||
|
||||
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(back_page, title_key, NULL));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 12);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
|
||||
body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
|
||||
gtk_widget_set_halign(body, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_hexpand(body, TRUE);
|
||||
gtk_widget_set_margin_start(body, 4);
|
||||
gtk_widget_set_margin_end(body, 4);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
|
||||
|
||||
head = gtk_label_new(NULL);
|
||||
i18n_bind(head, heading_key, 0);
|
||||
gtk_widget_set_halign(head, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(head), TRUE);
|
||||
gtk_widget_set_margin_top(head, 4);
|
||||
gtk_widget_add_css_class(head, "quiz-heading");
|
||||
gtk_box_append(GTK_BOX(body), head);
|
||||
|
||||
intro = gtk_label_new(NULL);
|
||||
i18n_bind(intro, "net_quiz_intro", 0);
|
||||
gtk_widget_set_halign(intro, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(intro), TRUE);
|
||||
gtk_widget_add_css_class(intro, "quiz-intro");
|
||||
gtk_widget_set_margin_bottom(intro, 4);
|
||||
gtk_box_append(GTK_BOX(body), intro);
|
||||
|
||||
ctx->qs = qs;
|
||||
ctx->n = n;
|
||||
ctx->n_opts = n_opts;
|
||||
ctx->lesson_id = lesson_id;
|
||||
ctx->toggles = g_new0(GtkToggleButton *, n * n_opts);
|
||||
ctx->hints = g_new0(GtkWidget *, n);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
GtkWidget *card = mcq_append_question(body, i + 1, &qs[i],
|
||||
&ctx->toggles[i * n_opts]);
|
||||
|
||||
ctx->hints[i] = meaning_add(card, hints[i]);
|
||||
}
|
||||
|
||||
btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_margin_top(btns, 8);
|
||||
gtk_widget_set_margin_bottom(btns, 8);
|
||||
gtk_box_append(GTK_BOX(body), btns);
|
||||
|
||||
check = gtk_button_new();
|
||||
i18n_bind(check, "check", 1);
|
||||
gtk_widget_add_css_class(check, "btn-primary");
|
||||
gtk_widget_set_halign(check, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(btns), check);
|
||||
g_signal_connect(check, "clicked", G_CALLBACK(hw_mcq_check), ctx);
|
||||
|
||||
ctx->feedback = gtk_label_new("");
|
||||
gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(body), ctx->feedback);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
GtkWidget *build_hw_unit1_exercise_page(void) {
|
||||
static const ChoiceQ qs[] = {
|
||||
{"Kdo je autorem Von Neumannova schématu?",
|
||||
{"Alan Turing", "John von Neumann", "Bill Gates", "Steve Jobs"},
|
||||
4, 1},
|
||||
{"Čím byl John von Neumann?",
|
||||
{"Americký matematik", "Německý fyzik", "Anglický chemik",
|
||||
"Francouzský lékař"},
|
||||
4, 0},
|
||||
{"Co znamená zkratka ALU?",
|
||||
{"Automatická logická síť", "Aritmeticko-logická jednotka",
|
||||
"Analogová linková sestava", "Adresová listová sekce"},
|
||||
4, 1},
|
||||
{"Která část Von Neumannova schématu řídí ostatní části?",
|
||||
{"Operační paměť", "Vstupní jednotka", "Řadič", "Výstupní jednotka"},
|
||||
4, 2},
|
||||
{"Kde se podle Von Neumannova schématu uchovává program i data?",
|
||||
{"Jen na pevném disku", "V operační paměti", "Jen ve vstupní jednotce",
|
||||
"Jen v ALU"},
|
||||
4, 1},
|
||||
{"Čím se John von Neumann také zabýval?",
|
||||
{"Jen malířstvím", "Teorií her", "Jen botanikou", "Jen hudbou"},
|
||||
4, 1},
|
||||
};
|
||||
static const char *hints[] = {
|
||||
"John von Neumann – americký matematik",
|
||||
"Americký matematik s objevy v digitálních počítačích",
|
||||
"ALU = aritmeticko-logická jednotka",
|
||||
"Řadič řídí činnost ostatních částí",
|
||||
"Operační paměť uchovává program i data",
|
||||
"Byl tvůrcem teorie her",
|
||||
};
|
||||
return build_hw_mcq_page(0, "hwunit1", "hw_ex1_title", "hw_quiz1_head",
|
||||
qs, hints, 6);
|
||||
}
|
||||
|
||||
GtkWidget *build_hw_unit2_page(void) {
|
||||
static const NetSlide slides[HW2_SLIDES] = {
|
||||
{
|
||||
"1 / 6 • Přehled", "Historie počítačů",
|
||||
"Generace se dělí podle klíčové technologie své doby.",
|
||||
{
|
||||
"1. Předchůdci – do 30. let 19. století",
|
||||
"2. Nultá generace – 1938–1944",
|
||||
"3. První generace – 1944–1955",
|
||||
"4. Druhá generace – 1955–1964",
|
||||
"5. Třetí generace – 1964–1971",
|
||||
"Další snímky shrnují znaky jednotlivých generací.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"2 / 6 • 0. generace", "Počítače 0. generace (1938–1944)",
|
||||
"Tip: počítačů bylo málo a často sloužily armádě.",
|
||||
{
|
||||
"Existovalo jen několik počítačů na světě.",
|
||||
"Většinou měly vojenský účel.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"3 / 6 • 1. generace", "Počítače 1. generace (1944–1955)",
|
||||
"Klíčový objev: elektronka",
|
||||
{
|
||||
"Objev elektronky umožnil tuto generaci.",
|
||||
"Ještě neexistoval software v dnešním smyslu.",
|
||||
"Data a programy se zadávaly pomocí děrných štítků.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"4 / 6 • 2. generace", "Počítače 2. generace (1955–1964)",
|
||||
"Klíčový objev: tranzistor",
|
||||
{
|
||||
"Objev tranzistoru nahradil elektronky.",
|
||||
"Používala se hlavní externí paměť.",
|
||||
"Počítače měly menší rozměry než v 1. generaci.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"5 / 6 • 3. generace", "Počítače 3. generace (1964–1971)",
|
||||
"Klíčový objev: integrovaný obvod",
|
||||
{
|
||||
"Objev integrovaného obvodu.",
|
||||
"Stále se používala hlavní externí paměť.",
|
||||
"Rozměry počítačů se dál zmenšovaly.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"6 / 6 • 4. generace", "Počítače 4. generace",
|
||||
"Klíčové: programovatelné mikroprocesory",
|
||||
{
|
||||
"Nastupují programovatelné mikroprocesory.",
|
||||
"1972 – první počítačová hra",
|
||||
"1973 – první disketa",
|
||||
"1979 – první CD",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return build_hw_unit_page(&hw_lessons[1], "hw_unit2", "hw_unit2_sub",
|
||||
slides, HW2_SLIDES);
|
||||
}
|
||||
|
||||
GtkWidget *build_hw_unit2_exercise_page(void) {
|
||||
static const ChoiceQ qs[] = {
|
||||
{"Kdy probíhala nultá generace počítačů?",
|
||||
{"1938–1944", "1944–1955", "1955–1964", "1964–1971"}, 4, 0},
|
||||
{"Co bylo typické pro počítače 0. generace?",
|
||||
{"Byly v každé domácnosti", "Jen několik kusů, často vojenský účel",
|
||||
"Měly mikroprocesory", "Používaly CD"},
|
||||
4, 1},
|
||||
{"Která technologie patří k 1. generaci?",
|
||||
{"Tranzistor", "Elektronka", "Integrovaný obvod", "Mikroprocesor"},
|
||||
4, 1},
|
||||
{"Čím se zadávaly data v 1. generaci?",
|
||||
{"USB fleškou", "Děrnými štítky", "CD", "Disketou"}, 4, 1},
|
||||
{"Která technologie patří k 2. generaci?",
|
||||
{"Elektronka", "Tranzistor", "Mikroprocesor", "CD"}, 4, 1},
|
||||
{"Která technologie patří k 3. generaci?",
|
||||
{"Elektronka", "Tranzistor", "Integrovaný obvod", "Disketa"},
|
||||
4, 2},
|
||||
{"Co je typické pro 4. generaci?",
|
||||
{"Jen děrné štítky", "Programovatelné mikroprocesory",
|
||||
"Jen vojenské použití", "Žádný software"},
|
||||
4, 1},
|
||||
{"Kdy vznikla první počítačová hra (podle lekce)?",
|
||||
{"1964", "1972", "1973", "1979"}, 4, 1},
|
||||
};
|
||||
static const char *hints[] = {
|
||||
"Nultá generace: 1938–1944",
|
||||
"Jen několik počítačů, většinou vojenský účel",
|
||||
"1. generace – objev elektronky",
|
||||
"1. generace – děrné štítky",
|
||||
"2. generace – objev tranzistoru",
|
||||
"3. generace – integrovaný obvod",
|
||||
"4. generace – programovatelné mikroprocesory",
|
||||
"1972 – první počítačová hra",
|
||||
};
|
||||
return build_hw_mcq_page(1, "hwunit2", "hw_ex2_title", "hw_quiz2_head",
|
||||
qs, hints, 8);
|
||||
}
|
||||
|
||||
GtkWidget *build_hw_unit3_page(void) {
|
||||
static const NetSlide slides[HW3_SLIDES] = {
|
||||
{
|
||||
"1 / 2 • Bit", "Bit – nejmenší jednotka informace",
|
||||
"Bit = binary digit (dvojková číslice)",
|
||||
{
|
||||
"Bit je dvojková číslice – nabývá hodnoty 0 nebo 1.",
|
||||
"Je to nejmenší zobrazitelná jednotka informace.",
|
||||
"Z bitů se skládají větší jednotky (např. byte).",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
{
|
||||
"2 / 2 • Byte", "Byte – adresovatelná jednotka paměti",
|
||||
"Tip: 1 B = 8 b",
|
||||
{
|
||||
"Byte je nejmenší adresovatelná jednotka paměti.",
|
||||
"Počítač tedy paměť adresuje po bytech, ne po jednotlivých "
|
||||
"bitech.",
|
||||
"Jeden byte obvykle obsahuje 8 bitů.",
|
||||
NULL,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return build_hw_unit_page(&hw_lessons[2], "hw_unit3", "hw_unit3_sub",
|
||||
slides, HW3_SLIDES);
|
||||
}
|
||||
|
||||
GtkWidget *build_hw_unit3_exercise_page(void) {
|
||||
static const ChoiceQ qs[] = {
|
||||
{"Co je bit?",
|
||||
{"Největší jednotka paměti", "Dvojková číslice – nejmenší jednotka "
|
||||
"informace", "Jen klávesa na klávesnici", "Typ procesoru"},
|
||||
4, 1},
|
||||
{"Jaké hodnoty může nabývat bit?",
|
||||
{"Jen 0", "Jen 1", "0 nebo 1", "0 až 255"}, 4, 2},
|
||||
{"Co je byte?",
|
||||
{"Nejmenší adresovatelná jednotka paměti",
|
||||
"Nejmenší zobrazitelná jednotka informace",
|
||||
"Jen síťový kabel", "Jen grafická karta"},
|
||||
4, 0},
|
||||
{"Kolik bitů má obvykle jeden byte?",
|
||||
{"2", "4", "8", "16"}, 4, 2},
|
||||
};
|
||||
static const char *hints[] = {
|
||||
"Bit = dvojková číslice, nejmenší jednotka informace",
|
||||
"Bit nabývá hodnoty 0 nebo 1",
|
||||
"Byte = nejmenší adresovatelná jednotka paměti",
|
||||
"1 B = 8 b",
|
||||
};
|
||||
return build_hw_mcq_page(2, "hwunit3", "hw_ex3_title", "hw_quiz3_head",
|
||||
qs, hints, 4);
|
||||
}
|
||||
1510
src/i18n.c
Normal file
1510
src/i18n.c
Normal file
File diff suppressed because it is too large
Load diff
467
src/icons.c
Normal file
467
src/icons.c
Normal file
|
|
@ -0,0 +1,467 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* Vector icons drawn with cairo so they never depend on the system
|
||||
* icon theme (which is missing on some installs). */
|
||||
|
||||
void draw_check_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
Rgb c = color_from_hex(app_theme.on_accent);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
cairo_set_line_width(cr, width * 0.16);
|
||||
cairo_move_to(cr, width * 0.24, height * 0.56);
|
||||
cairo_line_to(cr, width * 0.43, height * 0.74);
|
||||
cairo_line_to(cr, width * 0.78, height * 0.30);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
/* Paint a padlock centred in the size x size box with top-left (x0,y0). */
|
||||
void lock_paint(cairo_t *cr, double x0, double y0, double size,
|
||||
const Rgb *c) {
|
||||
double sl = MAX(1.6, size * 0.13); /* shackle stroke width */
|
||||
double bw = size * 0.60; /* body width */
|
||||
double bh = size * 0.44; /* body height */
|
||||
double cx = x0 + size / 2.0;
|
||||
double body_top = y0 + size - bh - size * 0.04;
|
||||
double bx = cx - bw / 2.0;
|
||||
double r = bw / 2.0 - sl / 2.0; /* shackle arc radius */
|
||||
|
||||
cairo_set_source_rgb(cr, c->r, c->g, c->b);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_width(cr, sl);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, body_top, r, G_PI, 2.0 * G_PI);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_rectangle(cr, bx, body_top, bw, bh);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, body_top + bh * 0.42, size * 0.07, 0.0, 2.0 * G_PI);
|
||||
{
|
||||
Rgb hole = color_from_hex(app_theme.crust);
|
||||
cairo_set_source_rgb(cr, hole.r, hole.g, hole.b);
|
||||
}
|
||||
cairo_fill(cr);
|
||||
}
|
||||
|
||||
void draw_lock_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
Rgb c = color_from_hex(app_theme.overlay);
|
||||
(void)area;
|
||||
(void)data;
|
||||
lock_paint(cr, 0.0, 0.0, MIN(width, height), &c);
|
||||
}
|
||||
|
||||
/* Wireless icon: three upward arcs that fan out from a base dot. */
|
||||
void draw_wifi_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double s = MIN(width, height);
|
||||
const double cx = width / 2.0;
|
||||
const double cy = height * 0.68;
|
||||
const double dot_r = s * 0.06;
|
||||
const double radii[3] = {s * 0.15, s * 0.27, s * 0.39};
|
||||
const double phi = 0.75;
|
||||
/* Keep the icon legible on the accent-gradient bubble while giving it a
|
||||
* hint of the active theme: blend the contrasting on-accent colour a
|
||||
* little way towards the palette's middle accent. */
|
||||
const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent),
|
||||
color_from_hex(app_theme.accent2), 0.22);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_width(cr, MAX(1.5, s * 0.07));
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double r = radii[i];
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, r, -G_PI / 2.0 - phi, -G_PI / 2.0 + phi);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, dot_r, 0.0, 2.0 * G_PI);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
|
||||
/* CPU / chip icon for Technické vybavení. */
|
||||
void draw_chip_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double s = MIN(width, height);
|
||||
const double cx = width / 2.0;
|
||||
const double cy = height / 2.0;
|
||||
const double body = s * 0.42;
|
||||
const double pin_len = s * 0.12;
|
||||
const double pin_w = s * 0.055;
|
||||
const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent),
|
||||
color_from_hex(app_theme.accent2), 0.22);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
|
||||
cairo_rectangle(cr, cx - body / 2.0, cy - body / 2.0, body, body);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE);
|
||||
cairo_set_line_width(cr, pin_w);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
double t = (i + 0.5) / 4.0;
|
||||
double x = cx - body / 2.0 + t * body;
|
||||
double y = cy - body / 2.0 + t * body;
|
||||
|
||||
cairo_move_to(cr, x, cy - body / 2.0);
|
||||
cairo_line_to(cr, x, cy - body / 2.0 - pin_len);
|
||||
cairo_move_to(cr, x, cy + body / 2.0);
|
||||
cairo_line_to(cr, x, cy + body / 2.0 + pin_len);
|
||||
cairo_move_to(cr, cx - body / 2.0, y);
|
||||
cairo_line_to(cr, cx - body / 2.0 - pin_len, y);
|
||||
cairo_move_to(cr, cx + body / 2.0, y);
|
||||
cairo_line_to(cr, cx + body / 2.0 + pin_len, y);
|
||||
}
|
||||
cairo_stroke(cr);
|
||||
|
||||
{
|
||||
Rgb hole = color_from_hex(app_theme.crust);
|
||||
double core = body * 0.34;
|
||||
|
||||
cairo_set_source_rgb(cr, hole.r, hole.g, hole.b);
|
||||
cairo_rectangle(cr, cx - core / 2.0, cy - core / 2.0, core, core);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Open book glyph, painted in the given colour. */
|
||||
static void book_paint(cairo_t *cr, int width, int height, Rgb c) {
|
||||
const double w = width;
|
||||
const double h = height;
|
||||
const double mid = w * 0.5;
|
||||
const double gap = w * 0.045; /* spine gap so the pages read apart */
|
||||
const double lmid = mid - gap;
|
||||
const double rmid = mid + gap;
|
||||
const double left = w * 0.13;
|
||||
const double right = w * 0.87;
|
||||
const double top = h * 0.24;
|
||||
const double bot = h * 0.80;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_move_to(cr, lmid, top);
|
||||
cairo_curve_to(cr, w * 0.40, top - h * 0.07,
|
||||
w * 0.24, top - h * 0.02, left, top + h * 0.07);
|
||||
cairo_line_to(cr, left, bot - h * 0.05);
|
||||
cairo_curve_to(cr, w * 0.24, bot - h * 0.11,
|
||||
w * 0.40, bot - h * 0.05, lmid, bot);
|
||||
cairo_close_path(cr);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_move_to(cr, rmid, top);
|
||||
cairo_curve_to(cr, w * 0.60, top - h * 0.07,
|
||||
w * 0.76, top - h * 0.02, right, top + h * 0.07);
|
||||
cairo_line_to(cr, right, bot - h * 0.05);
|
||||
cairo_curve_to(cr, w * 0.76, bot - h * 0.11,
|
||||
w * 0.60, bot - h * 0.05, rmid, bot);
|
||||
cairo_close_path(cr);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
|
||||
/* Open book icon – used on light surfaces (cards, section headers). */
|
||||
void draw_book_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const Rgb c = mix_rgb(color_from_hex(app_theme.accent2),
|
||||
color_from_hex(app_theme.accent3), 0.35);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
book_paint(cr, width, height, c);
|
||||
}
|
||||
|
||||
/* Open book icon for the accent-gradient roadmap bubbles: uses the
|
||||
* contrasting on-accent colour so it stays visible on the gradient. */
|
||||
void draw_book_badge_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const Rgb c = mix_rgb(color_from_hex(app_theme.on_accent),
|
||||
color_from_hex(app_theme.accent2), 0.22);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
book_paint(cr, width, height, c);
|
||||
}
|
||||
|
||||
/* Question mark in a circle – quiz. */
|
||||
void draw_quiz_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const Rgb c = color_from_hex(app_theme.accent);
|
||||
PangoLayout *layout;
|
||||
PangoFontDescription *fd;
|
||||
int tw, th;
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
layout = pango_cairo_create_layout(cr);
|
||||
fd = pango_font_description_new();
|
||||
pango_font_description_set_weight(fd, PANGO_WEIGHT_HEAVY);
|
||||
pango_font_description_set_absolute_size(
|
||||
fd, MIN(width, height) * 1.05 * PANGO_SCALE);
|
||||
pango_layout_set_font_description(layout, fd);
|
||||
pango_layout_set_text(layout, "?", -1);
|
||||
pango_layout_get_pixel_size(layout, &tw, &th);
|
||||
cairo_move_to(cr, (width - tw) / 2.0, (height - th) / 2.0);
|
||||
pango_cairo_show_layout(cr, layout);
|
||||
pango_font_description_free(fd);
|
||||
g_object_unref(layout);
|
||||
}
|
||||
|
||||
/* Two opposing arrows – ordering / drag & drop. */
|
||||
void draw_order_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double w = width;
|
||||
const double h = height;
|
||||
const Rgb c = color_from_hex(app_theme.success2);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
cairo_set_line_width(cr, MAX(1.6, MIN(w, h) * 0.11));
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_move_to(cr, w * 0.36, h * 0.80);
|
||||
cairo_line_to(cr, w * 0.36, h * 0.24);
|
||||
cairo_move_to(cr, w * 0.22, h * 0.40);
|
||||
cairo_line_to(cr, w * 0.36, h * 0.24);
|
||||
cairo_line_to(cr, w * 0.50, h * 0.40);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_move_to(cr, w * 0.64, h * 0.20);
|
||||
cairo_line_to(cr, w * 0.64, h * 0.76);
|
||||
cairo_move_to(cr, w * 0.50, h * 0.60);
|
||||
cairo_line_to(cr, w * 0.64, h * 0.76);
|
||||
cairo_line_to(cr, w * 0.78, h * 0.60);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
/* Globe – the world / superpowers. */
|
||||
void draw_globe_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double cx = width / 2.0;
|
||||
const double cy = height / 2.0;
|
||||
const double r = MIN(width, height) * 0.36;
|
||||
const Rgb c = color_from_hex(app_theme.accent2);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
cairo_set_line_width(cr, MAX(1.4, r * 0.13));
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_translate(cr, cx, cy);
|
||||
cairo_scale(cr, 0.52, 1.0);
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, 0.0, 0.0, r, 0.0, 2.0 * G_PI);
|
||||
cairo_stroke(cr);
|
||||
cairo_restore(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_move_to(cr, cx - r, cy);
|
||||
cairo_line_to(cr, cx + r, cy);
|
||||
cairo_move_to(cr, cx - r * 0.86, cy - r * 0.52);
|
||||
cairo_line_to(cr, cx + r * 0.86, cy - r * 0.52);
|
||||
cairo_move_to(cr, cx - r * 0.86, cy + r * 0.52);
|
||||
cairo_line_to(cr, cx + r * 0.86, cy + r * 0.52);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
/* Two people – characters. */
|
||||
void draw_people_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const Rgb c = color_from_hex(app_theme.accent3);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, width * 0.38, height * 0.36, width * 0.15,
|
||||
0.0, 2.0 * G_PI);
|
||||
cairo_fill(cr);
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, width * 0.38, height * 1.02, width * 0.30,
|
||||
G_PI, 2.0 * G_PI);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, width * 0.67, height * 0.42, width * 0.13,
|
||||
0.0, 2.0 * G_PI);
|
||||
cairo_fill(cr);
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, width * 0.67, height * 1.06, width * 0.27,
|
||||
G_PI, 2.0 * G_PI);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
|
||||
/* Light bulb – key terms / ideas. */
|
||||
void draw_bulb_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double cx = width / 2.0;
|
||||
const double r = MIN(width, height) * 0.30;
|
||||
const double cy = height * 0.40;
|
||||
const Rgb c = color_from_hex(app_theme.warning);
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_source_rgb(cr, c.r, c.g, c.b);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_rectangle(cr, cx - r * 0.45, cy + r * 0.72,
|
||||
r * 0.90, r * 0.66);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy + r * 1.72, r * 0.52, 0.0, G_PI);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
|
||||
void draw_back_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double size = 15.0; /* fixed logical icon size */
|
||||
const double stroke = 1.8;
|
||||
double ox = (width - size) / 2.0;
|
||||
double oy = (height - size) / 2.0;
|
||||
GtkWidget *parent = gtk_widget_get_parent(GTK_WIDGET(area));
|
||||
GdkRGBA color;
|
||||
|
||||
(void)data;
|
||||
gtk_style_context_get_color(gtk_widget_get_style_context(parent), &color);
|
||||
cairo_set_source_rgb(cr, color.red, color.green, color.blue);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
cairo_set_line_width(cr, stroke);
|
||||
cairo_move_to(cr, ox + size * 0.72, oy + size * 0.24);
|
||||
cairo_line_to(cr, ox + size * 0.30, oy + size * 0.50);
|
||||
cairo_line_to(cr, ox + size * 0.72, oy + size * 0.76);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
void draw_settings_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const int teeth = 8;
|
||||
const double size = 16.0;
|
||||
double cx = width / 2.0;
|
||||
double cy = height / 2.0;
|
||||
double r_outer = size * 0.48;
|
||||
double r_inner = size * 0.34;
|
||||
double r_hole = size * 0.16;
|
||||
double tooth_half = G_PI / (double)teeth;
|
||||
GtkWidget *color_w = data ? GTK_WIDGET(data)
|
||||
: gtk_widget_get_parent(GTK_WIDGET(area));
|
||||
GdkRGBA color;
|
||||
int i;
|
||||
|
||||
gtk_style_context_get_color(gtk_widget_get_style_context(color_w), &color);
|
||||
cairo_set_source_rgb(cr, color.red, color.green, color.blue);
|
||||
cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
|
||||
|
||||
cairo_new_path(cr);
|
||||
for (i = 0; i < teeth; i++) {
|
||||
double a = -G_PI / 2.0 + (2.0 * G_PI * (double)i) / (double)teeth;
|
||||
double a0 = a - tooth_half * 0.55;
|
||||
double a1 = a - tooth_half * 0.28;
|
||||
double a2 = a + tooth_half * 0.28;
|
||||
double a3 = a + tooth_half * 0.55;
|
||||
|
||||
if (i == 0)
|
||||
cairo_move_to(cr, cx + cos(a0) * r_inner, cy + sin(a0) * r_inner);
|
||||
else
|
||||
cairo_line_to(cr, cx + cos(a0) * r_inner, cy + sin(a0) * r_inner);
|
||||
|
||||
cairo_line_to(cr, cx + cos(a1) * r_outer, cy + sin(a1) * r_outer);
|
||||
cairo_line_to(cr, cx + cos(a2) * r_outer, cy + sin(a2) * r_outer);
|
||||
cairo_line_to(cr, cx + cos(a3) * r_inner, cy + sin(a3) * r_inner);
|
||||
}
|
||||
cairo_close_path(cr);
|
||||
cairo_new_sub_path(cr);
|
||||
cairo_arc(cr, cx, cy, r_hole, 0.0, 2.0 * G_PI);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
|
||||
void draw_stats_icon(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
GtkWidget *color_w = data ? GTK_WIDGET(data)
|
||||
: gtk_widget_get_parent(GTK_WIDGET(area));
|
||||
GdkRGBA color;
|
||||
const double bar_w = 3.2;
|
||||
const double gap = 2.6;
|
||||
const double total_w = 3.0 * bar_w + 2.0 * gap;
|
||||
const double heights[3] = {0.42, 0.72, 1.0};
|
||||
double base_x = (width - total_w) / 2.0;
|
||||
double base_y = height * 0.82;
|
||||
double max_h = height * 0.58;
|
||||
int i;
|
||||
|
||||
(void)area;
|
||||
gtk_style_context_get_color(gtk_widget_get_style_context(color_w), &color);
|
||||
cairo_set_source_rgb(cr, color.red, color.green, color.blue);
|
||||
cairo_set_line_width(cr, 1.5);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
|
||||
for (i = 0; i < 3; i++) {
|
||||
double x = base_x + (double)i * (bar_w + gap);
|
||||
double h = max_h * heights[i];
|
||||
double y = base_y - h;
|
||||
double r = 1.2;
|
||||
|
||||
cairo_new_sub_path(cr);
|
||||
cairo_arc(cr, x + r, y + r, r, G_PI, 1.5 * G_PI);
|
||||
cairo_arc(cr, x + bar_w - r, y + r, r, 1.5 * G_PI, 2.0 * G_PI);
|
||||
cairo_line_to(cr, x + bar_w, base_y);
|
||||
cairo_line_to(cr, x, base_y);
|
||||
cairo_close_path(cr);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
}
|
||||
|
||||
GtkWidget *icon_area_new(GtkDrawingAreaDrawFunc fn,
|
||||
double r, double g, double b, int px) {
|
||||
Rgb *col = g_new(Rgb, 1);
|
||||
GtkWidget *d = gtk_drawing_area_new();
|
||||
|
||||
col->r = r;
|
||||
col->g = g;
|
||||
col->b = b;
|
||||
gtk_widget_set_size_request(d, px, px);
|
||||
gtk_widget_set_halign(d, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(d, GTK_ALIGN_CENTER);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(d), fn,
|
||||
col, (GDestroyNotify)g_free);
|
||||
return d;
|
||||
}
|
||||
55
src/macos_dock.c
Normal file
55
src/macos_dock.c
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#include "maturita.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include <objc/message.h>
|
||||
#include <objc/runtime.h>
|
||||
|
||||
/* Set the macOS Dock tile icon. GTK's window icon-name is ignored on
|
||||
* Quartz, so we talk to AppKit directly via the Objective-C runtime. */
|
||||
void macos_set_dock_icon(const char *path) {
|
||||
Class nsstring;
|
||||
Class nsimage;
|
||||
Class nsapp;
|
||||
id path_str;
|
||||
id image;
|
||||
id app;
|
||||
|
||||
if (!path || !path[0])
|
||||
return;
|
||||
|
||||
nsstring = objc_getClass("NSString");
|
||||
nsimage = objc_getClass("NSImage");
|
||||
nsapp = objc_getClass("NSApplication");
|
||||
if (!nsstring || !nsimage || !nsapp)
|
||||
return;
|
||||
|
||||
path_str = ((id (*)(Class, SEL, const char *))objc_msgSend)(
|
||||
nsstring, sel_registerName("stringWithUTF8String:"), path);
|
||||
if (!path_str)
|
||||
return;
|
||||
|
||||
/* NSImage has -initWithContentsOfFile:, not +imageWithContentsOfFile:. */
|
||||
image = ((id (*)(id, SEL, id))objc_msgSend)(
|
||||
((id (*)(Class, SEL))objc_msgSend)(
|
||||
nsimage, sel_registerName("alloc")),
|
||||
sel_registerName("initWithContentsOfFile:"),
|
||||
path_str);
|
||||
if (!image)
|
||||
return;
|
||||
|
||||
app = ((id (*)(Class, SEL))objc_msgSend)(
|
||||
nsapp, sel_registerName("sharedApplication"));
|
||||
if (!app) {
|
||||
((void (*)(id, SEL))objc_msgSend)(image, sel_registerName("release"));
|
||||
return;
|
||||
}
|
||||
|
||||
((void (*)(id, SEL, id))objc_msgSend)(
|
||||
app, sel_registerName("setApplicationIconImage:"), image);
|
||||
((void (*)(id, SEL))objc_msgSend)(image, sel_registerName("release"));
|
||||
}
|
||||
#else
|
||||
void macos_set_dock_icon(const char *path) {
|
||||
(void)path;
|
||||
}
|
||||
#endif
|
||||
652
src/main.c
Normal file
652
src/main.c
Normal file
|
|
@ -0,0 +1,652 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Activate */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void unit_meta_init(void) {
|
||||
static const char *titles[NUM_UNITS] = {
|
||||
"Neue Freunde", "Aus aller Welt", "Bei uns zu Hause",
|
||||
"Schule und Freizeit", "Guten Appetit!", "Mein Tagesablauf",
|
||||
"Meine Freunde", "Wir treffen uns in Salzburg",
|
||||
"Mein Haus ist meine Burg", "Urlaub in Österreich",
|
||||
};
|
||||
static const char *pages[NUM_UNITS] = {
|
||||
"unit1", "unit2", "unit3", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
};
|
||||
static const char *tags[NUM_UNITS] = {
|
||||
"u1", "u2", "u3", NULL, NULL, NULL, NULL, NULL, NULL, NULL,
|
||||
};
|
||||
|
||||
for (int i = 0; i < NUM_UNITS; i++) {
|
||||
units[i].title = titles[i];
|
||||
units[i].page = pages[i];
|
||||
units[i].ex_tag = tags[i];
|
||||
units[i].sub_key = NULL;
|
||||
units[i].unlocked = (i < NUM_UNLOCKED);
|
||||
}
|
||||
units[0].sub_key = "unit1_sub";
|
||||
units[0].progress_file = PROGRESS_U1;
|
||||
units[1].sub_key = "unit2_sub";
|
||||
units[1].progress_file = PROGRESS_U2;
|
||||
units[2].sub_key = "unit3_sub";
|
||||
units[2].progress_file = PROGRESS_U3;
|
||||
|
||||
/* Off-path branch hanging above exercise 2 of unit 1. */
|
||||
units[0].has_branch = TRUE;
|
||||
units[0].branch_name = "Vokabeltraining";
|
||||
units[0].branch_target = "u1vocab";
|
||||
units[0].branch_ex = MAX_UNIT_EX;
|
||||
units[0].trans_sections = u1_trans_sections;
|
||||
units[0].n_trans_sections = u1_trans_sections_n;
|
||||
|
||||
/* Same vocabulary-training branch for unit 2 ("Aus aller Welt"). */
|
||||
units[1].has_branch = TRUE;
|
||||
units[1].branch_name = "Vokabeltraining";
|
||||
units[1].branch_target = "u2vocab";
|
||||
units[1].branch_ex = MAX_UNIT_EX;
|
||||
units[1].trans_sections = u2_trans_sections;
|
||||
units[1].n_trans_sections = u2_trans_sections_n;
|
||||
|
||||
/* Same vocabulary-training branch for unit 3 ("Bei uns zu Hause"). */
|
||||
units[2].has_branch = TRUE;
|
||||
units[2].branch_name = "Vokabeltraining";
|
||||
units[2].branch_target = "u3vocab";
|
||||
units[2].branch_ex = MAX_UNIT_EX;
|
||||
units[2].trans_sections = u3_trans_sections;
|
||||
units[2].n_trans_sections = u3_trans_sections_n;
|
||||
}
|
||||
|
||||
void unit_configure(int idx, const char *const *names, int n) {
|
||||
UnitCtx *u = &units[idx];
|
||||
|
||||
u->n_ex = n;
|
||||
for (int i = 1; i <= n; i++)
|
||||
u->ex_names[i] = names[i];
|
||||
}
|
||||
|
||||
GtkWidget *build_exercise_page(UnitCtx *u, int n) {
|
||||
if (u->ex_tag[1] == '1') {
|
||||
switch (n) {
|
||||
case 1: return build_ex1(u);
|
||||
case 2: return build_assembly(u, "Sätze bilden", "sub_assembly", 2,
|
||||
ex2_items, ex2_meaning,
|
||||
4);
|
||||
case 3: return build_choice(u, "Was ist richtig?", "sub_choice_num",
|
||||
3, ex3_questions, ex3_meaning,
|
||||
4);
|
||||
case 4: return build_ex4(u);
|
||||
case 5: return build_ex5(u);
|
||||
case 6: return build_ex6(u);
|
||||
case 7: return build_ex7(u);
|
||||
case 8: return build_ex8(u);
|
||||
case 9: return build_choice(u, "Wer? Wie? Wo?", "sub_wer",
|
||||
9, ex9_questions, ex9_meaning,
|
||||
5);
|
||||
case 10: return build_assembly(u, "Wörter trennen", "sub_assembly",
|
||||
10, ex10_items, ex10_meaning,
|
||||
5);
|
||||
case 11: return build_assign(u, "Grußformen", "sub_gruss",
|
||||
11, ex11_items,
|
||||
12,
|
||||
ex11_groups, 2,
|
||||
ex11_meaning);
|
||||
case 12: return build_ex12(u);
|
||||
case 13: return build_assign(u, "Länder", "sub_land",
|
||||
13, ex13_items,
|
||||
10,
|
||||
ex13_groups, 3,
|
||||
ex13_meaning);
|
||||
default: return NULL;
|
||||
}
|
||||
} else if (u->ex_tag[1] == '2') {
|
||||
switch (n) {
|
||||
case 1: return u2ex1(u);
|
||||
case 2: return u2ex2(u);
|
||||
case 3: return u2ex3(u);
|
||||
case 4: return u2ex4(u);
|
||||
case 5: return u2ex5(u);
|
||||
case 6: return u2ex6(u);
|
||||
case 7: return u2ex7(u);
|
||||
case 8: return u2ex8(u);
|
||||
case 9: return u2ex9(u);
|
||||
case 10: return u2ex10(u);
|
||||
case 11: return u2ex11(u);
|
||||
case 12: return u2ex12(u);
|
||||
case 13: return u2ex13(u);
|
||||
case 14: return u2ex14(u);
|
||||
case 15: return u2ex15(u);
|
||||
case 16: return u2ex16(u);
|
||||
case 17: return u2ex17(u);
|
||||
case 18: return u2ex18(u);
|
||||
case 19: return u2ex19(u);
|
||||
default: return NULL;
|
||||
}
|
||||
} else {
|
||||
switch (n) {
|
||||
case 1: return u3ex1(u);
|
||||
case 2: return u3ex2(u);
|
||||
case 3: return u3ex3(u);
|
||||
case 4: return u3ex4(u);
|
||||
case 5: return u3ex5(u);
|
||||
case 6: return u3ex6(u);
|
||||
case 7: return u3ex7(u);
|
||||
case 8: return u3ex8(u);
|
||||
case 9: return u3ex9(u);
|
||||
case 10: return u3ex10(u);
|
||||
case 11: return u3ex11(u);
|
||||
case 12: return u3ex12(u);
|
||||
case 13: return u3ex13(u);
|
||||
case 14: return u3ex14(u);
|
||||
case 15: return u3ex15(u);
|
||||
default: return NULL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int ui_scale_last_w = -1;
|
||||
static guint ui_scale_timer = 0;
|
||||
static int ui_scale_pending_w = 0;
|
||||
|
||||
static gboolean ui_scale_apply(gpointer data) {
|
||||
double scale;
|
||||
|
||||
(void)data;
|
||||
ui_scale_timer = 0;
|
||||
scale = 1.0 + (ui_scale_pending_w - 760) / 1900.0;
|
||||
if (scale < 1.0)
|
||||
scale = 1.0;
|
||||
if (scale > 1.5)
|
||||
scale = 1.5;
|
||||
scale = (double)((int)(scale * 20.0 + 0.5)) / 20.0;
|
||||
if (scale != app_ui_scale) {
|
||||
app_ui_scale = scale;
|
||||
apply_theme();
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/* Grow the interface's text as the window gets wider, so it stays comfortable
|
||||
* when maximised on a large or high-DPI display. Debounced to avoid reloading
|
||||
* the stylesheet on every frame while dragging. */
|
||||
static gboolean ui_scale_tick(GtkWidget *w, GdkFrameClock *clock,
|
||||
gpointer data) {
|
||||
int width = gtk_widget_get_allocated_width(w);
|
||||
|
||||
(void)clock;
|
||||
(void)data;
|
||||
if (width < 200 || width == ui_scale_last_w)
|
||||
return G_SOURCE_CONTINUE;
|
||||
ui_scale_last_w = width;
|
||||
ui_scale_pending_w = width;
|
||||
if (ui_scale_timer)
|
||||
g_source_remove(ui_scale_timer);
|
||||
ui_scale_timer = g_timeout_add(120, ui_scale_apply, NULL);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static const char *find_app_icon_file(void) {
|
||||
static const char *paths[] = {
|
||||
ICON_FILE,
|
||||
ICON_THEME_DIR "/hicolor/512x512/apps/" ICON_NAME ".png",
|
||||
SHARE_DIR "/icons/hicolor/512x512/apps/" ICON_NAME ".png",
|
||||
ICON_FILE_ALT,
|
||||
NULL,
|
||||
};
|
||||
int i;
|
||||
|
||||
for (i = 0; paths[i]; i++) {
|
||||
if (g_file_test(paths[i], G_FILE_TEST_IS_REGULAR))
|
||||
return paths[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Kept alive for gdk_toplevel_set_icon_list() which does not take ownership. */
|
||||
static GList *app_icon_textures;
|
||||
|
||||
static void clear_app_icon_textures(void) {
|
||||
g_list_free_full(app_icon_textures, g_object_unref);
|
||||
app_icon_textures = NULL;
|
||||
}
|
||||
|
||||
/* X11 / some compositors use the toplevel icon list for the taskbar tile. */
|
||||
static void apply_toplevel_icon(GtkWindow *window) {
|
||||
GdkSurface *surface;
|
||||
const char *path;
|
||||
GError *err = NULL;
|
||||
GdkTexture *tex;
|
||||
|
||||
surface = gtk_native_get_surface(GTK_NATIVE(window));
|
||||
if (!surface || !GDK_IS_TOPLEVEL(surface))
|
||||
return;
|
||||
|
||||
path = find_app_icon_file();
|
||||
if (!path)
|
||||
return;
|
||||
|
||||
tex = gdk_texture_new_from_filename(path, &err);
|
||||
if (!tex) {
|
||||
g_clear_error(&err);
|
||||
return;
|
||||
}
|
||||
|
||||
clear_app_icon_textures();
|
||||
app_icon_textures = g_list_append(NULL, tex);
|
||||
gdk_toplevel_set_icon_list(GDK_TOPLEVEL(surface), app_icon_textures);
|
||||
}
|
||||
|
||||
static void on_window_realize_icon(GtkWidget *widget, gpointer data) {
|
||||
(void)data;
|
||||
apply_toplevel_icon(GTK_WINDOW(widget));
|
||||
}
|
||||
|
||||
/* Prepend ./share to XDG_DATA_DIRS so Wayland can find our .desktop file and
|
||||
* hicolor icons without a system install (matches APP_ID). */
|
||||
static void setup_portable_share_dir(void) {
|
||||
char *abs;
|
||||
const char *old;
|
||||
char *neu;
|
||||
|
||||
if (!g_file_test(SHARE_DIR "/applications", G_FILE_TEST_IS_DIR) &&
|
||||
!g_file_test(SHARE_DIR "/icons", G_FILE_TEST_IS_DIR))
|
||||
return;
|
||||
|
||||
abs = g_canonicalize_filename(SHARE_DIR, NULL);
|
||||
if (!abs)
|
||||
return;
|
||||
|
||||
old = g_getenv("XDG_DATA_DIRS");
|
||||
if (old && old[0])
|
||||
neu = g_strconcat(abs, G_SEARCHPATH_SEPARATOR_S, old, NULL);
|
||||
else
|
||||
neu = g_strconcat(abs,
|
||||
G_SEARCHPATH_SEPARATOR_S "/usr/local/share"
|
||||
G_SEARCHPATH_SEPARATOR_S "/usr/share",
|
||||
NULL);
|
||||
g_setenv("XDG_DATA_DIRS", neu, TRUE);
|
||||
g_free(neu);
|
||||
g_free(abs);
|
||||
}
|
||||
|
||||
/* Register icon theme paths + window icon-name (Linux), AppKit Dock (macOS),
|
||||
* and toplevel textures (X11 / fallbacks). Windows uses the .ico embedded
|
||||
* via maturita.rc at link time. */
|
||||
static void setup_window_icon(GtkWindow *window) {
|
||||
GtkIconTheme *theme;
|
||||
static const char *dirs[] = {
|
||||
ICON_THEME_DIR,
|
||||
ICON_THEME_DIR_ALT,
|
||||
SHARE_DIR "/icons",
|
||||
NULL,
|
||||
};
|
||||
const char *icon_file;
|
||||
int i;
|
||||
|
||||
theme = gtk_icon_theme_get_for_display(gdk_display_get_default());
|
||||
for (i = 0; dirs[i]; i++) {
|
||||
if (g_file_test(dirs[i], G_FILE_TEST_IS_DIR))
|
||||
gtk_icon_theme_add_search_path(theme, dirs[i]);
|
||||
}
|
||||
|
||||
if (gtk_icon_theme_has_icon(theme, ICON_NAME)) {
|
||||
gtk_window_set_default_icon_name(ICON_NAME);
|
||||
gtk_window_set_icon_name(window, ICON_NAME);
|
||||
} else {
|
||||
g_warning("App icon '%s' not found (tried %s/, %s/, %s/icons/)",
|
||||
ICON_NAME, ICON_THEME_DIR, ICON_THEME_DIR_ALT, SHARE_DIR);
|
||||
}
|
||||
|
||||
icon_file = find_app_icon_file();
|
||||
if (icon_file)
|
||||
macos_set_dock_icon(icon_file);
|
||||
#ifdef __APPLE__
|
||||
else
|
||||
g_warning("Dock icon PNG not found (tried %s and %s)",
|
||||
ICON_FILE, ICON_FILE_ALT);
|
||||
#endif
|
||||
|
||||
g_signal_connect(window, "realize",
|
||||
G_CALLBACK(on_window_realize_icon), NULL);
|
||||
}
|
||||
|
||||
static gboolean open_settings_later(gpointer data) {
|
||||
(void)data;
|
||||
settings_open();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gboolean save_window_png(const char *path) {
|
||||
GtkWidget *widget;
|
||||
int width, height;
|
||||
GdkPaintable *paintable;
|
||||
GtkSnapshot *snapshot;
|
||||
GskRenderNode *node;
|
||||
cairo_surface_t *surface;
|
||||
cairo_t *cr;
|
||||
cairo_status_t st;
|
||||
|
||||
if (!main_window || !path || !path[0])
|
||||
return FALSE;
|
||||
|
||||
widget = GTK_WIDGET(main_window);
|
||||
width = gtk_widget_get_width(widget);
|
||||
height = gtk_widget_get_height(widget);
|
||||
if (width < 80 || height < 80)
|
||||
return FALSE;
|
||||
|
||||
paintable = gtk_widget_paintable_new(widget);
|
||||
snapshot = gtk_snapshot_new();
|
||||
gdk_paintable_snapshot(paintable, snapshot, width, height);
|
||||
node = gtk_snapshot_free_to_node(snapshot);
|
||||
g_object_unref(paintable);
|
||||
if (!node)
|
||||
return FALSE;
|
||||
|
||||
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
|
||||
cr = cairo_create(surface);
|
||||
gsk_render_node_draw(node, cr);
|
||||
gsk_render_node_unref(node);
|
||||
cairo_destroy(cr);
|
||||
st = cairo_surface_write_to_png(surface, path);
|
||||
cairo_surface_destroy(surface);
|
||||
return st == CAIRO_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static void smoke_quit(int code) {
|
||||
GtkApplication *app = main_window
|
||||
? gtk_window_get_application(main_window) : NULL;
|
||||
|
||||
if (app)
|
||||
g_application_quit(G_APPLICATION(app));
|
||||
else
|
||||
exit(code);
|
||||
}
|
||||
|
||||
static gboolean shot_later(gpointer data) {
|
||||
const char *path = g_getenv("MATURITA_SHOT");
|
||||
int *tries = data;
|
||||
|
||||
if (!save_window_png(path)) {
|
||||
if (tries && *tries < 20) {
|
||||
(*tries)++;
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
g_printerr("MATURITA_SHOT: could not capture window\n");
|
||||
smoke_quit(1);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (g_getenv("MATURITA_SHOT_QUIT"))
|
||||
smoke_quit(0);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static gboolean run_smoke_test(gpointer data) {
|
||||
static const char *pages[] = {
|
||||
"welcome", "subjects", "roadmap", "stats",
|
||||
"netyears", "netmap", "hwmap", "czechmap",
|
||||
"mluvnice", "readinglist",
|
||||
"cetba1984", "cetba1984quiz", "cetba1984dej",
|
||||
"cetbaFuks", "cetbaFuksQuiz", "cetbaFuksDej",
|
||||
"hwunit1", "hwex1", "hwunit2", "hwex2", "hwunit3", "hwex3",
|
||||
"unit1", "unit2", "unit3",
|
||||
"u1e1", "u1e13", "u2e1", "u2e19", "u3e1", "u3e15",
|
||||
"u1vocab", "u2vocab", "u3vocab",
|
||||
"mluve1", "mluve20",
|
||||
"netunit1", "netex1", "netunit10", "netex10",
|
||||
"netunit27", "netex27",
|
||||
NULL
|
||||
};
|
||||
const char *report = g_getenv("MATURITA_SMOKE");
|
||||
FILE *f;
|
||||
int missing = 0;
|
||||
int i;
|
||||
|
||||
(void)data;
|
||||
f = report && report[0] ? fopen(report, "w") : stdout;
|
||||
if (!f) {
|
||||
g_printerr("MATURITA_SMOKE: cannot write %s\n", report);
|
||||
smoke_quit(1);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
fprintf(f, "commit=%s\n", APP_COMMIT);
|
||||
fprintf(f, "css=%s\n",
|
||||
g_file_test(CSS_FILE, G_FILE_TEST_IS_REGULAR) ? "ok" : "MISSING");
|
||||
fprintf(f, "progress_dir=%s\n", app_progress_dir ? app_progress_dir : "?");
|
||||
|
||||
for (i = 0; pages[i]; i++) {
|
||||
GtkWidget *child = gtk_stack_get_child_by_name(main_stack, pages[i]);
|
||||
|
||||
if (child) {
|
||||
fprintf(f, "OK %s\n", pages[i]);
|
||||
} else {
|
||||
fprintf(f, "MISSING %s\n", pages[i]);
|
||||
missing++;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(f, "missing=%d\n", missing);
|
||||
if (f != stdout)
|
||||
fclose(f);
|
||||
smoke_quit(missing ? 1 : 0);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void activate(GtkApplication *app, gpointer user_data) {
|
||||
GtkWindow *window;
|
||||
GtkWidget *headerbar;
|
||||
GtkWidget *title_label;
|
||||
GtkWidget *welcome_page;
|
||||
GtkWidget *roadmap_page;
|
||||
GtkEventController *keys;
|
||||
|
||||
(void)user_data;
|
||||
|
||||
load_settings();
|
||||
unit_meta_init();
|
||||
unit_configure(0, u1_ex_names, 13);
|
||||
unit_configure(1, u2_ex_names, 19);
|
||||
unit_configure(2, u3_ex_names, 15);
|
||||
load_progress();
|
||||
mluvnice_init();
|
||||
app_theme = theme_palette(app_theme_id, app_color_mode);
|
||||
|
||||
window = GTK_WINDOW(gtk_application_window_new(app));
|
||||
main_window = window;
|
||||
gtk_window_set_title(window, "maturita.c");
|
||||
gtk_window_set_default_size(window, 760, 560);
|
||||
setup_window_icon(window);
|
||||
|
||||
headerbar = gtk_header_bar_new();
|
||||
gtk_widget_add_css_class(headerbar, "titlebar");
|
||||
title_label = gtk_label_new("maturita.c");
|
||||
gtk_widget_add_css_class(title_label, "app-title");
|
||||
gtk_header_bar_set_title_widget(GTK_HEADER_BAR(headerbar), title_label);
|
||||
gtk_window_set_titlebar(window, headerbar);
|
||||
apply_theme();
|
||||
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_settings_button());
|
||||
gtk_header_bar_pack_end(GTK_HEADER_BAR(headerbar), build_stats_button());
|
||||
|
||||
main_stack = GTK_STACK(gtk_stack_new());
|
||||
gtk_stack_set_transition_type(GTK_STACK(main_stack),
|
||||
GTK_STACK_TRANSITION_TYPE_CROSSFADE);
|
||||
|
||||
/* The update banner sits above the pages and stays hidden until there is
|
||||
* something to report, so it never shifts the layout unprompted. */
|
||||
{
|
||||
GtkWidget *root = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
gtk_box_append(GTK_BOX(root), build_update_banner());
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(main_stack), TRUE);
|
||||
gtk_box_append(GTK_BOX(root), GTK_WIDGET(main_stack));
|
||||
gtk_window_set_child(window, root);
|
||||
}
|
||||
|
||||
welcome_page = build_welcome_page();
|
||||
roadmap_page = build_roadmap_page();
|
||||
gtk_stack_add_named(main_stack, welcome_page, "welcome");
|
||||
gtk_stack_add_named(main_stack, build_subjects_page(), "subjects");
|
||||
gtk_stack_add_named(main_stack, roadmap_page, "roadmap");
|
||||
gtk_stack_add_named(main_stack, build_stats_page(), "stats");
|
||||
gtk_stack_add_named(main_stack, build_netyears_page(), "netyears");
|
||||
gtk_stack_add_named(main_stack, build_netmap_page(), "netmap");
|
||||
gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap");
|
||||
gtk_stack_add_named(main_stack, build_czechmap_page(), "czechmap");
|
||||
gtk_stack_add_named(main_stack, build_mluvnice_page(), "mluvnice");
|
||||
for (int n = 1; n <= 20; n++) {
|
||||
char name[16];
|
||||
|
||||
g_snprintf(name, sizeof(name), "mluve%d", n);
|
||||
gtk_stack_add_named(main_stack,
|
||||
build_mluvnice_exercise_page(n), name);
|
||||
}
|
||||
gtk_stack_add_named(main_stack, build_readinglist_page(), "readinglist");
|
||||
gtk_stack_add_named(main_stack, build_cetba1984_page(), "cetba1984");
|
||||
gtk_stack_add_named(main_stack, build_cetba1984_quiz_page(),
|
||||
"cetba1984quiz");
|
||||
gtk_stack_add_named(main_stack, build_cetba1984_plot_page(),
|
||||
"cetba1984dej");
|
||||
gtk_stack_add_named(main_stack, build_cetba_fuks_page(), "cetbaFuks");
|
||||
gtk_stack_add_named(main_stack, build_cetba_fuks_quiz_page(),
|
||||
"cetbaFuksQuiz");
|
||||
gtk_stack_add_named(main_stack, build_cetba_fuks_plot_page(),
|
||||
"cetbaFuksDej");
|
||||
gtk_stack_add_named(main_stack, build_hw_unit1_page(), "hwunit1");
|
||||
gtk_stack_add_named(main_stack, build_hw_unit1_exercise_page(), "hwex1");
|
||||
gtk_stack_add_named(main_stack, build_hw_unit2_page(), "hwunit2");
|
||||
gtk_stack_add_named(main_stack, build_hw_unit2_exercise_page(), "hwex2");
|
||||
gtk_stack_add_named(main_stack, build_hw_unit3_page(), "hwunit3");
|
||||
gtk_stack_add_named(main_stack, build_hw_unit3_exercise_page(), "hwex3");
|
||||
{
|
||||
typedef GtkWidget *(*NetBuilder)(void);
|
||||
static const struct {
|
||||
const char *unit_name;
|
||||
const char *ex_name;
|
||||
NetBuilder build_unit;
|
||||
NetBuilder build_ex;
|
||||
} net_pages[] = {
|
||||
{"netunit1", "netex1", build_net_unit1_page, build_net_exercise_page},
|
||||
{"netunit2", "netex2", build_net_unit2_page, build_net_unit2_exercise_page},
|
||||
{"netunit3", "netex3", build_net_unit3_page, build_net_unit3_exercise_page},
|
||||
{"netunit4", "netex4", build_net_unit4_page, build_net_unit4_exercise_page},
|
||||
{"netunit5", "netex5", build_net_unit5_page, build_net_unit5_exercise_page},
|
||||
{"netunit6", "netex6", build_net_unit6_page, build_net_unit6_exercise_page},
|
||||
{"netunit7", "netex7", build_net_unit7_page, build_net_unit7_exercise_page},
|
||||
{"netunit8", "netex8", build_net_unit8_page, build_net_unit8_exercise_page},
|
||||
{"netunit9", "netex9", build_net_unit9_page, build_net_unit9_exercise_page},
|
||||
{"netunit10", "netex10", build_net_unit10_page, build_net_unit10_exercise_page},
|
||||
{"netunit11", "netex11", build_net_unit11_page, build_net_unit11_exercise_page},
|
||||
{"netunit12", "netex12", build_net_unit12_page, build_net_unit12_exercise_page},
|
||||
{"netunit13", "netex13", build_net_unit13_page, build_net_unit13_exercise_page},
|
||||
{"netunit14", "netex14", build_net_unit14_page, build_net_unit14_exercise_page},
|
||||
{"netunit15", "netex15", build_net_unit15_page, build_net_unit15_exercise_page},
|
||||
{"netunit16", "netex16", build_net_unit16_page, build_net_unit16_exercise_page},
|
||||
{"netunit17", "netex17", build_net_unit17_page, build_net_unit17_exercise_page},
|
||||
{"netunit18", "netex18", build_net_unit18_page, build_net_unit18_exercise_page},
|
||||
{"netunit19", "netex19", build_net_unit19_page, build_net_unit19_exercise_page},
|
||||
{"netunit20", "netex20", build_net_unit20_page, build_net_unit20_exercise_page},
|
||||
{"netunit21", "netex21", build_net_unit21_page, build_net_unit21_exercise_page},
|
||||
{"netunit22", "netex22", build_net_unit22_page, build_net_unit22_exercise_page},
|
||||
{"netunit23", "netex23", build_net_unit23_page, build_net_unit23_exercise_page},
|
||||
{"netunit24", "netex24", build_net_unit24_page, build_net_unit24_exercise_page},
|
||||
{"netunit25", "netex25", build_net_unit25_page, build_net_unit25_exercise_page},
|
||||
{"netunit26", "netex26", build_net_unit26_page, build_net_unit26_exercise_page},
|
||||
{"netunit27", "netex27", build_net_unit27_page, build_net_unit27_exercise_page},
|
||||
};
|
||||
|
||||
for (guint i = 0; i < G_N_ELEMENTS(net_pages); i++) {
|
||||
gtk_stack_add_named(main_stack, net_pages[i].build_unit(),
|
||||
net_pages[i].unit_name);
|
||||
gtk_stack_add_named(main_stack, net_pages[i].build_ex(),
|
||||
net_pages[i].ex_name);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_UNLOCKED; i++) {
|
||||
UnitCtx *u = &units[i];
|
||||
GtkWidget *map = build_unit_page(u);
|
||||
|
||||
if (map)
|
||||
gtk_stack_add_named(main_stack, map, u->page);
|
||||
for (int n = 1; n <= u->n_ex; n++) {
|
||||
GtkWidget *page = build_exercise_page(u, n);
|
||||
char name[16];
|
||||
|
||||
if (!page)
|
||||
continue;
|
||||
g_snprintf(name, sizeof(name), "%se%d", u->ex_tag, n);
|
||||
gtk_stack_add_named(main_stack, page, name);
|
||||
}
|
||||
if (u->has_branch && u->branch_target) {
|
||||
GtkWidget *branch = build_translate(u);
|
||||
|
||||
if (branch)
|
||||
gtk_stack_add_named(main_stack, branch, u->branch_target);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const char *start = g_getenv("MATURITA_PAGE");
|
||||
const char *size = g_getenv("MATURITA_SIZE");
|
||||
int w, h;
|
||||
|
||||
if (start && *start)
|
||||
gtk_stack_set_visible_child_name(main_stack, start);
|
||||
else
|
||||
gtk_stack_set_visible_child_name(main_stack, "welcome");
|
||||
if (size && sscanf(size, "%dx%d", &w, &h) == 2)
|
||||
gtk_window_set_default_size(window, w, h);
|
||||
}
|
||||
|
||||
refresh_completion_ui();
|
||||
|
||||
keys = gtk_event_controller_key_new();
|
||||
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(keys),
|
||||
GTK_PHASE_CAPTURE);
|
||||
g_signal_connect(keys, "key-pressed",
|
||||
G_CALLBACK(on_window_key_pressed), window);
|
||||
gtk_widget_add_controller(GTK_WIDGET(window), GTK_EVENT_CONTROLLER(keys));
|
||||
|
||||
apply_theme();
|
||||
apply_language();
|
||||
|
||||
gtk_widget_add_tick_callback(GTK_WIDGET(window), ui_scale_tick, NULL, NULL);
|
||||
|
||||
update_init();
|
||||
|
||||
gtk_window_present(window);
|
||||
|
||||
if (g_getenv("MATURITA_SETTINGS"))
|
||||
g_timeout_add(350, open_settings_later, NULL);
|
||||
if (g_getenv("MATURITA_SMOKE"))
|
||||
g_timeout_add(200, run_smoke_test, NULL);
|
||||
if (g_getenv("MATURITA_SHOT")) {
|
||||
static int shot_tries;
|
||||
|
||||
shot_tries = 0;
|
||||
g_timeout_add(g_getenv("MATURITA_SETTINGS") ? 700 : 400,
|
||||
shot_later, &shot_tries);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
GtkApplication *app;
|
||||
int status;
|
||||
|
||||
/* Resolve style.css / icons next to the binary (or AppImage / .app) and
|
||||
* point progress files at a writable location before any I/O. */
|
||||
setup_portable_paths();
|
||||
|
||||
/* Must run before GtkApplication so Wayland can resolve our .desktop
|
||||
* file / icons via the application id. */
|
||||
setup_portable_share_dir();
|
||||
|
||||
app = gtk_application_new(APP_ID, G_APPLICATION_DEFAULT_FLAGS);
|
||||
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
||||
status = g_application_run(G_APPLICATION(app), argc, argv);
|
||||
clear_app_icon_textures();
|
||||
g_object_unref(app);
|
||||
|
||||
return status;
|
||||
}
|
||||
1238
src/maturita.h
Normal file
1238
src/maturita.h
Normal file
File diff suppressed because it is too large
Load diff
796
src/mluvnice.c
Normal file
796
src/mluvnice.c
Normal file
|
|
@ -0,0 +1,796 @@
|
|||
#include "maturita.h"
|
||||
|
||||
#define MLUV_N_EX 20
|
||||
#define MLUV_MAX_BLANK 5
|
||||
#define MLUV_MAX_OPT 5
|
||||
|
||||
typedef struct {
|
||||
const char *prompt;
|
||||
const char *shown;
|
||||
int nblank;
|
||||
const char *hint[MLUV_MAX_BLANK];
|
||||
const char *answers[MLUV_MAX_BLANK];
|
||||
} MluvItem;
|
||||
|
||||
typedef struct {
|
||||
const char *prompt;
|
||||
const char *solution;
|
||||
} MluvRevealItem;
|
||||
|
||||
typedef struct {
|
||||
const MluvItem *items;
|
||||
int n;
|
||||
UnitCtx *unit;
|
||||
int ex_num;
|
||||
GtkWidget *feedback;
|
||||
GtkWidget **entries;
|
||||
GtkWidget **reveal;
|
||||
} MluvCtx;
|
||||
|
||||
typedef struct {
|
||||
const MluvRevealItem *items;
|
||||
int n;
|
||||
UnitCtx *unit;
|
||||
int ex_num;
|
||||
GtkWidget *feedback;
|
||||
GtkWidget **sol;
|
||||
} MluvRevealCtx;
|
||||
|
||||
typedef struct {
|
||||
const ChoiceQ *qs;
|
||||
const char **expls;
|
||||
int n;
|
||||
UnitCtx *unit;
|
||||
int ex_num;
|
||||
GtkWidget *feedback;
|
||||
GtkToggleButton **toggles;
|
||||
GtkWidget **expl_labels;
|
||||
} MluvChoiceCtx;
|
||||
|
||||
static UnitCtx mluv_unit;
|
||||
|
||||
static const char *mluv_names[MLUV_N_EX] = {
|
||||
"i/y", "Velká písmena", "Slovní druhy", "Podst. jména", "Slovesa",
|
||||
"Slovotvorba", "Větné členy", "Vedlejší věty", "Synonyma", "Antonyma",
|
||||
"s/z", "Najdi chybu", "Tvary", "Test A–D", "Přímá řeč",
|
||||
"Rozdíly", "Interpunkce", "Obrazná pojmen.", "Slohové útvary",
|
||||
"Opakování",
|
||||
};
|
||||
|
||||
void mluvnice_init(void) {
|
||||
mluv_unit.title = "Mluvnice";
|
||||
mluv_unit.page = "mluvnice";
|
||||
mluv_unit.ex_tag = "mluv";
|
||||
mluv_unit.sub_key = "Cvičení z mluvnice – styl maturita / "
|
||||
"přijímačky z ČJL";
|
||||
mluv_unit.back_target = "czechmap";
|
||||
mluv_unit.progress_file = app_progress_mluvnice;
|
||||
mluv_unit.n_ex = MLUV_N_EX;
|
||||
for (int i = 0; i < MLUV_N_EX; i++)
|
||||
mluv_unit.ex_names[i + 1] = mluv_names[i];
|
||||
unit_load_progress(&mluv_unit);
|
||||
}
|
||||
|
||||
void progress_for_mluvnice(ProgressSum *out) {
|
||||
int d;
|
||||
|
||||
if (!out || mluv_unit.n_ex <= 0)
|
||||
return;
|
||||
d = unit_done_count(&mluv_unit);
|
||||
out->total_ex += mluv_unit.n_ex;
|
||||
out->open_units += 1;
|
||||
out->done_ex += d;
|
||||
if (d == mluv_unit.n_ex)
|
||||
out->done_units++;
|
||||
}
|
||||
|
||||
void mluvnice_refresh_ui(void) {
|
||||
for (int i = 0; i < mluv_unit.n_ex; i++) {
|
||||
GtkWidget *btn = mluv_unit.ex_cells[i];
|
||||
|
||||
if (!btn)
|
||||
continue;
|
||||
if (mluv_unit.done[i + 1]) {
|
||||
gtk_widget_add_css_class(btn, "done");
|
||||
if (mluv_unit.ex_icons[i + 1])
|
||||
gtk_widget_set_visible(mluv_unit.ex_icons[i + 1], TRUE);
|
||||
} else {
|
||||
gtk_widget_remove_css_class(btn, "done");
|
||||
if (mluv_unit.ex_icons[i + 1])
|
||||
gtk_widget_set_visible(mluv_unit.ex_icons[i + 1], FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
if (mluv_unit.ex_rail)
|
||||
gtk_widget_queue_draw(mluv_unit.ex_rail);
|
||||
}
|
||||
|
||||
static void mluv_check(GtkButton *button, gpointer data) {
|
||||
MluvCtx *ctx = data;
|
||||
int total = 0;
|
||||
int ok = 0;
|
||||
|
||||
(void)button;
|
||||
|
||||
for (int i = 0; i < ctx->n; i++) {
|
||||
const MluvItem *it = &ctx->items[i];
|
||||
|
||||
for (int j = 0; j < it->nblank; j++) {
|
||||
GtkWidget *e = ctx->entries[i * MLUV_MAX_BLANK + j];
|
||||
const gchar *txt = gtk_editable_get_text(GTK_EDITABLE(e));
|
||||
gchar *norm = normalize_answer(txt);
|
||||
gboolean good = txt && txt[0] &&
|
||||
answer_accepts(norm, it->answers[j]);
|
||||
|
||||
g_free(norm);
|
||||
answer_mark(e, good);
|
||||
total++;
|
||||
if (good)
|
||||
ok++;
|
||||
}
|
||||
if (ctx->reveal[i])
|
||||
gtk_widget_set_visible(ctx->reveal[i], TRUE);
|
||||
}
|
||||
|
||||
if (total > 0 && ok == total) {
|
||||
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
|
||||
mark_done(ctx->unit, ctx->ex_num);
|
||||
} else {
|
||||
set_feedback(ctx->feedback, FALSE, tr("feedback_retry"));
|
||||
}
|
||||
}
|
||||
|
||||
static GtkWidget *mluv_typed_page(int ex_num, const char *title,
|
||||
const char *sub, const char *note,
|
||||
const MluvItem *items, int n) {
|
||||
GtkWidget *body, *feedback, *check;
|
||||
GtkWidget *page = ex_page_shell("mluvnice", title, sub, "check",
|
||||
&body, &feedback, &check);
|
||||
MluvCtx *ctx = g_new0(MluvCtx, 1);
|
||||
|
||||
ctx->items = items;
|
||||
ctx->n = n;
|
||||
ctx->unit = &mluv_unit;
|
||||
ctx->ex_num = ex_num;
|
||||
ctx->feedback = feedback;
|
||||
ctx->entries = g_new0(GtkWidget *, n * MLUV_MAX_BLANK);
|
||||
ctx->reveal = g_new0(GtkWidget *, n);
|
||||
|
||||
if (note) {
|
||||
GtkWidget *l = gtk_label_new(note);
|
||||
|
||||
gtk_widget_set_halign(l, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(l), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(l), 0.0);
|
||||
gtk_widget_add_css_class(l, "hint");
|
||||
gtk_widget_set_margin_bottom(l, 4);
|
||||
gtk_box_append(GTK_BOX(body), l);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const MluvItem *it = &items[i];
|
||||
GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
GtkWidget *prompt = gtk_label_new(it->prompt);
|
||||
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
char *rev = g_strdup_printf("Řešení: %s", it->shown);
|
||||
GtkWidget *rl = gtk_label_new(rev);
|
||||
|
||||
g_free(rev);
|
||||
gtk_widget_add_css_class(card, "notes-card");
|
||||
gtk_box_append(GTK_BOX(body), card);
|
||||
|
||||
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(prompt), 0.0);
|
||||
gtk_widget_add_css_class(prompt, "ex-prompt");
|
||||
gtk_box_append(GTK_BOX(card), prompt);
|
||||
|
||||
gtk_widget_set_halign(row, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(card), row);
|
||||
|
||||
for (int j = 0; j < it->nblank; j++) {
|
||||
GtkWidget *e = gtk_entry_new();
|
||||
|
||||
gtk_editable_set_width_chars(GTK_EDITABLE(e), 18);
|
||||
if (it->hint[j])
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(e), it->hint[j]);
|
||||
gtk_box_append(GTK_BOX(row), e);
|
||||
ctx->entries[i * MLUV_MAX_BLANK + j] = e;
|
||||
}
|
||||
|
||||
gtk_widget_set_halign(rl, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(rl), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(rl), 0.0);
|
||||
gtk_widget_add_css_class(rl, "meaning");
|
||||
gtk_widget_set_visible(rl, FALSE);
|
||||
gtk_box_append(GTK_BOX(card), rl);
|
||||
ctx->reveal[i] = rl;
|
||||
}
|
||||
|
||||
g_signal_connect(check, "clicked", G_CALLBACK(mluv_check), ctx);
|
||||
return page;
|
||||
}
|
||||
|
||||
static void mluv_reveal_check(GtkButton *button, gpointer data) {
|
||||
MluvRevealCtx *ctx = data;
|
||||
|
||||
(void)button;
|
||||
|
||||
for (int i = 0; i < ctx->n; i++)
|
||||
gtk_widget_set_visible(ctx->sol[i], TRUE);
|
||||
|
||||
set_feedback(ctx->feedback, TRUE, "Zobrazili jste si vzorové řešení.");
|
||||
mark_done(ctx->unit, ctx->ex_num);
|
||||
}
|
||||
|
||||
static GtkWidget *mluv_reveal_page(int ex_num, const char *title,
|
||||
const char *sub, const char *note,
|
||||
const MluvRevealItem *items, int n) {
|
||||
GtkWidget *body, *feedback, *check;
|
||||
GtkWidget *page = ex_page_shell("mluvnice", title, sub,
|
||||
"Zobrazit řešení",
|
||||
&body, &feedback, &check);
|
||||
MluvRevealCtx *ctx = g_new0(MluvRevealCtx, 1);
|
||||
|
||||
ctx->items = items;
|
||||
ctx->n = n;
|
||||
ctx->unit = &mluv_unit;
|
||||
ctx->ex_num = ex_num;
|
||||
ctx->feedback = feedback;
|
||||
ctx->sol = g_new0(GtkWidget *, n);
|
||||
|
||||
if (note) {
|
||||
GtkWidget *l = gtk_label_new(note);
|
||||
|
||||
gtk_widget_set_halign(l, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(l), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(l), 0.0);
|
||||
gtk_widget_add_css_class(l, "hint");
|
||||
gtk_widget_set_margin_bottom(l, 4);
|
||||
gtk_box_append(GTK_BOX(body), l);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
GtkWidget *prompt = gtk_label_new(items[i].prompt);
|
||||
GtkWidget *sol = gtk_label_new(items[i].solution);
|
||||
|
||||
gtk_widget_add_css_class(card, "notes-card");
|
||||
gtk_box_append(GTK_BOX(body), card);
|
||||
|
||||
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(prompt), 0.0);
|
||||
gtk_widget_add_css_class(prompt, "ex-prompt");
|
||||
gtk_box_append(GTK_BOX(card), prompt);
|
||||
|
||||
gtk_widget_set_halign(sol, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(sol), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(sol), 0.0);
|
||||
gtk_widget_add_css_class(sol, "meaning");
|
||||
gtk_widget_set_visible(sol, FALSE);
|
||||
gtk_box_append(GTK_BOX(card), sol);
|
||||
ctx->sol[i] = sol;
|
||||
}
|
||||
|
||||
g_signal_connect(check, "clicked", G_CALLBACK(mluv_reveal_check), ctx);
|
||||
return page;
|
||||
}
|
||||
|
||||
static void mluv_choice_check(GtkButton *button, gpointer data) {
|
||||
MluvChoiceCtx *ctx = data;
|
||||
int ok = 0;
|
||||
|
||||
(void)button;
|
||||
|
||||
for (int i = 0; i < ctx->n; i++) {
|
||||
const ChoiceQ *q = &ctx->qs[i];
|
||||
int active = -1;
|
||||
|
||||
for (int o = 0; o < q->n_options; o++) {
|
||||
GtkToggleButton *tb = ctx->toggles[i * MLUV_MAX_OPT + o];
|
||||
|
||||
gtk_widget_remove_css_class(GTK_WIDGET(tb), "ok");
|
||||
gtk_widget_remove_css_class(GTK_WIDGET(tb), "wrong");
|
||||
gtk_widget_set_sensitive(GTK_WIDGET(tb), FALSE);
|
||||
if (gtk_toggle_button_get_active(tb))
|
||||
active = o;
|
||||
}
|
||||
|
||||
if (active == q->correct) {
|
||||
ok++;
|
||||
gtk_widget_add_css_class(
|
||||
GTK_WIDGET(ctx->toggles[i * MLUV_MAX_OPT + active]), "ok");
|
||||
} else if (active >= 0) {
|
||||
gtk_widget_add_css_class(
|
||||
GTK_WIDGET(ctx->toggles[i * MLUV_MAX_OPT + active]), "wrong");
|
||||
gtk_widget_add_css_class(
|
||||
GTK_WIDGET(ctx->toggles[i * MLUV_MAX_OPT + q->correct]), "ok");
|
||||
} else {
|
||||
gtk_widget_add_css_class(
|
||||
GTK_WIDGET(ctx->toggles[i * MLUV_MAX_OPT + q->correct]), "ok");
|
||||
}
|
||||
|
||||
if (ctx->expl_labels && ctx->expl_labels[i])
|
||||
gtk_widget_set_visible(ctx->expl_labels[i], TRUE);
|
||||
}
|
||||
|
||||
if (ok == ctx->n) {
|
||||
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
|
||||
mark_done(ctx->unit, ctx->ex_num);
|
||||
} else {
|
||||
set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short"));
|
||||
}
|
||||
}
|
||||
|
||||
static GtkWidget *mluv_choice_page(int ex_num, const char *title,
|
||||
const char *sub, const char *note,
|
||||
const ChoiceQ *qs, const char **expls,
|
||||
int n) {
|
||||
GtkWidget *body, *feedback, *check;
|
||||
GtkWidget *page = ex_page_shell("mluvnice", title, sub, "check",
|
||||
&body, &feedback, &check);
|
||||
MluvChoiceCtx *ctx = g_new0(MluvChoiceCtx, 1);
|
||||
|
||||
ctx->qs = qs;
|
||||
ctx->expls = expls;
|
||||
ctx->n = n;
|
||||
ctx->unit = &mluv_unit;
|
||||
ctx->ex_num = ex_num;
|
||||
ctx->feedback = feedback;
|
||||
ctx->toggles = g_new0(GtkToggleButton *, n * MLUV_MAX_OPT);
|
||||
ctx->expl_labels = g_new0(GtkWidget *, n);
|
||||
|
||||
if (note) {
|
||||
GtkWidget *l = gtk_label_new(note);
|
||||
|
||||
gtk_widget_set_halign(l, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(l), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(l), 0.0);
|
||||
gtk_widget_add_css_class(l, "hint");
|
||||
gtk_widget_set_margin_bottom(l, 4);
|
||||
gtk_box_append(GTK_BOX(body), l);
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
GtkWidget *card = mcq_append_question(
|
||||
body, i + 1, &qs[i], &ctx->toggles[i * MLUV_MAX_OPT]);
|
||||
|
||||
if (expls && expls[i]) {
|
||||
GtkWidget *e = gtk_label_new(expls[i]);
|
||||
|
||||
gtk_widget_set_halign(e, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(e), TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(e), 0.0);
|
||||
gtk_widget_add_css_class(e, "meaning");
|
||||
gtk_widget_set_visible(e, FALSE);
|
||||
gtk_box_append(GTK_BOX(card), e);
|
||||
ctx->expl_labels[i] = e;
|
||||
}
|
||||
}
|
||||
|
||||
g_signal_connect(check, "clicked", G_CALLBACK(mluv_choice_check), ctx);
|
||||
return page;
|
||||
}
|
||||
|
||||
/* ---- Exercise content --------------------------------------------- */
|
||||
|
||||
static const MluvItem e1_items[] = {
|
||||
{"Stará bab_čka sed_la na lav_čce.", "babička, seděla, lavičce", 3,
|
||||
{"i/í/y/ý", "i/í/y/ý", "i/í/y/ý"},
|
||||
{"babička", "seděla|sedla", "lavičce"}},
|
||||
{"Na obloze sv_tily hv_zdy.", "svítily, hvězdy", 2,
|
||||
{"i/í/y/ý", "i/í/y/ý"},
|
||||
{"svítily", "hvězdy"}},
|
||||
{"M_sl_m, že m_š pravdu.", "Myslím, myš", 2,
|
||||
{"i/í/y/ý", "i/í/y/ý"},
|
||||
{"Myslím", "myš"}},
|
||||
{"V_soký muž nes_l těžký kuf_r.", "Vysoký, nesl, kufr", 3,
|
||||
{"i/í/y/ý", "i/í/y/ý", "i/í/y/ý"},
|
||||
{"Vysoký", "nesl", "kufr"}},
|
||||
{"Ryb_ář čekal cel_ den u řek_.", "Rybář, celý, řeky", 3,
|
||||
{"i/í/y/ý", "i/í/y/ý", "i/í/y/ý"},
|
||||
{"Rybář", "celý", "řeky"}},
|
||||
};
|
||||
|
||||
static const ChoiceQ e2_qs[] = {
|
||||
{"Narodil se v ______ v malé vesnici.",
|
||||
{"Čechách", "čechách"}, 2, 0},
|
||||
{"Přečetl jsem román od ______.",
|
||||
{"Boženy Němcové", "boženy němcové"}, 2, 0},
|
||||
{"Jedeme na ______.", {"Moravu", "moravu"}, 2, 0},
|
||||
{"V ______ žije přes milion lidí.", {"Praze", "praze"}, 2, 0},
|
||||
{"Navštívili jsme ______.",
|
||||
{"Národní muzeum", "národní muzeum"}, 2, 0},
|
||||
};
|
||||
|
||||
static const MluvItem e3_items[] = {
|
||||
{"Krásný den se chýlil ke konci. — slovo „Krásný“",
|
||||
"přídavné jméno", 1, {"slovní druh"},
|
||||
{"přídavné jméno|přídavné jmeno|adjektivum"}},
|
||||
{"Šel rychle domů. — slovo „rychle“",
|
||||
"příslovce", 1, {"slovní druh"},
|
||||
{"příslovce|adverbium"}},
|
||||
{"Sedím před školou. — slovo „před“",
|
||||
"předložka", 1, {"slovní druh"},
|
||||
{"předložka|prepozice"}},
|
||||
{"To je jeho kniha. — slovo „jeho“",
|
||||
"zájmeno", 1, {"slovní druh"},
|
||||
{"zájmeno|zájmeno|pronomen"}},
|
||||
{"Ach, to je škoda! — slovo „Ach“",
|
||||
"citoslovce", 1, {"slovní druh"},
|
||||
{"citoslovce|interjekce"}},
|
||||
{"Přišel jsem, ale bylo pozdě. — slovo „ale“",
|
||||
"spojka", 1, {"slovní druh"},
|
||||
{"spojka|konjunkce"}},
|
||||
{"Napočítal jsem pět jablek. — slovo „pět“",
|
||||
"číslovka", 1, {"slovní druh"},
|
||||
{"číslovka|cislovka|numerale"}},
|
||||
{"Učitel vysvětloval látku. — slovo „Učitel“",
|
||||
"podstatné jméno", 1, {"slovní druh"},
|
||||
{"podstatné jméno|podstatné jmeno|substantivum"}},
|
||||
};
|
||||
|
||||
static const MluvItem e4_items[] = {
|
||||
{"Kniha leží na stole. — slovo „stole“ (stůl)",
|
||||
"mužský rod, jednotné číslo, 6. pád (lokál)", 3,
|
||||
{"rod", "číslo", "pád"},
|
||||
{"mužský|mužský rod|maskulinum",
|
||||
"jednotné|jednotné číslo|singulár",
|
||||
"6|lokál|šestý|6. pád"}},
|
||||
{"Povídám si s kamarády. — slovo „kamarády“ (kamarád)",
|
||||
"mužský rod, množné číslo, 7. pád (instrumentál)", 3,
|
||||
{"rod", "číslo", "pád"},
|
||||
{"mužský|mužský rod|maskulinum",
|
||||
"množné|množné číslo|plurál",
|
||||
"7|instrumentál|sedmý|7. pád"}},
|
||||
{"Vidím krásnou horu. — slovo „horu“ (hora)",
|
||||
"ženský rod, jednotné číslo, 4. pád (akuzativ)", 3,
|
||||
{"rod", "číslo", "pád"},
|
||||
{"ženský|ženský rod|femininum",
|
||||
"jednotné|jednotné číslo|singulár",
|
||||
"4|akuzativ|čtvrtý|4. pád"}},
|
||||
{"To je dům mého souseda. — slovo „souseda“ (soused)",
|
||||
"mužský rod, jednotné číslo, 2. pád (genitiv)", 3,
|
||||
{"rod", "číslo", "pád"},
|
||||
{"mužský|mužský rod|maskulinum",
|
||||
"jednotné|jednotné číslo|singulár",
|
||||
"2|genitiv|druhý|2. pád"}},
|
||||
{"Děti si hrají na zahradě. — slovo „Děti“ (dítě)",
|
||||
"střední rod, množné číslo, 1. pád (nominativ)", 3,
|
||||
{"rod", "číslo", "pád"},
|
||||
{"střední|střední rod|neutrum",
|
||||
"množné|množné číslo|plurál",
|
||||
"1|nominativ|první|1. pád"}},
|
||||
};
|
||||
|
||||
static const MluvItem e5_items[] = {
|
||||
{"čteme", "1. osoba, množné číslo, přítomný čas, oznamovací způsob", 4,
|
||||
{"osoba", "číslo", "čas", "způsob"},
|
||||
{"1|1.|první|první osoba|1. osoba",
|
||||
"množné|množné číslo|plurál",
|
||||
"přítomný|přítomný čas|přítomný čas",
|
||||
"oznamovací|oznamovací způsob|indikativ"}},
|
||||
{"běžel", "3. osoba, jednotné číslo, minulý čas, oznamovací způsob", 4,
|
||||
{"osoba", "číslo", "čas", "způsob"},
|
||||
{"3|3.|třetí|třetí osoba|3. osoba",
|
||||
"jednotné|jednotné číslo|singulár",
|
||||
"minulý|minulý čas|minulý čas",
|
||||
"oznamovací|oznamovací způsob|indikativ"}},
|
||||
{"přečtěte",
|
||||
"2. osoba, množné číslo, rozkazovací způsob (čas se neurčuje)", 4,
|
||||
{"osoba", "číslo", "čas", "způsob"},
|
||||
{"2|2.|druhý|druhá osoba|2. osoba",
|
||||
"množné|množné číslo|plurál",
|
||||
"neurčitý|nestanovený|neurčený",
|
||||
"rozkazovací|rozkazovací způsob|imperativ"}},
|
||||
{"by zpíval",
|
||||
"3. osoba, jednotné číslo, podmiňovací způsob přítomný", 4,
|
||||
{"osoba", "číslo", "čas", "způsob"},
|
||||
{"3|3.|třetí|třetí osoba|3. osoba",
|
||||
"jednotné|jednotné číslo|singulár",
|
||||
"neurčitý|nestanovený|přítomný",
|
||||
"podmiňovací|podmiňovací způsob|kondicionál"}},
|
||||
{"budou pracovat",
|
||||
"3. osoba, množné číslo, budoucí čas, oznamovací způsob", 4,
|
||||
{"osoba", "číslo", "čas", "způsob"},
|
||||
{"3|3.|třetí|třetí osoba|3. osoba",
|
||||
"množné|množné číslo|plurál",
|
||||
"budoucí|budoucí čas|futurum",
|
||||
"oznamovací|oznamovací způsob|indikativ"}},
|
||||
};
|
||||
|
||||
static const MluvRevealItem e6_items[] = {
|
||||
{"napsali", "na- / ps- / -a- / -li"},
|
||||
{"přečíst", "pře- / číst"},
|
||||
{"zahradník", "za- / hrad- / -ník / -Ø"},
|
||||
{"nepřátelský", "ne- / přátel- / -ský / -Ø"},
|
||||
{"výběr", "vý- / bér- / -Ø / -Ø"},
|
||||
};
|
||||
|
||||
static const MluvItem e7_items[] = {
|
||||
{"Malá holčička plakala. — větný člen „Malá holčička“",
|
||||
"podmět", 1, {"větný člen"}, {"podmět|subjekt"}},
|
||||
{"Otec pracuje v garáži. — větný člen „v garáži“",
|
||||
"příslovečné určení místa", 1, {"větný člen"},
|
||||
{"příslovečné určení místa|příslovečné určení místa (PU místa)|"
|
||||
"příslovečné určení|adverbiále místa"}},
|
||||
{"Sestra je učitelka. — větný člen „je učitelka“",
|
||||
"přísudek jmenný se sponou", 1, {"větný člen"},
|
||||
{"přísudek jmenný se sponou|přísudek jmenný|jmenný přísudek se sponou|"
|
||||
"přísudek se sponou"}},
|
||||
{"Dal jsem knihu bratrovi. — větný člen „knihu“",
|
||||
"předmět", 1, {"větný člen"}, {"předmět|objekt"}},
|
||||
{"Přišel unavený. — větný člen „unavený“",
|
||||
"doplněk", 1, {"větný člen"}, {"doplněk"}},
|
||||
{"Pomalu šel domů. — větný člen „Pomalu“",
|
||||
"příslovečné určení způsobu", 1, {"větný člen"},
|
||||
{"příslovečné určení způsobu|příslovečné určení způsobu (PU způsobu)|"
|
||||
"příslovečné určení|adverbiále způsobu"}},
|
||||
};
|
||||
|
||||
static const MluvItem e8_items[] = {
|
||||
{"Věřím, že uspěješ.", "vedlejší věta předmětná", 1, {"druh vedlejší věty"},
|
||||
{"vedlejší věta předmětná|věta předmětná|předmětná"}},
|
||||
{"Udělám to, jak budu moci.", "vedlejší věta příslovečná způsobová", 1,
|
||||
{"druh vedlejší věty"},
|
||||
{"vedlejší věta příslovečná způsobová|příslovečná způsobová|způsobová"}},
|
||||
{"Přišel, protože chtěl pomoci.", "vedlejší věta příslovečná příčinná", 1,
|
||||
{"druh vedlejší věty"},
|
||||
{"vedlejší věta příslovečná příčinná|příslovečná příčinná|příčinná"}},
|
||||
{"Řekni mi, kde bydlíš.", "vedlejší věta předmětná", 1,
|
||||
{"druh vedlejší věty"},
|
||||
{"vedlejší věta předmětná|věta předmětná|předmětná"}},
|
||||
{"Byl tak unavený, že usnul okamžitě.",
|
||||
"vedlejší věta příslovečná následková", 1, {"druh vedlejší věty"},
|
||||
{"vedlejší věta příslovečná následková|příslovečná následková|následková"}},
|
||||
};
|
||||
|
||||
static const MluvItem e9_items[] = {
|
||||
{"rychlý", "hbitý, svižný", 1, {"synonymum"},
|
||||
{"hbitý|svižný|rychlý|čilý|bystrý|prudký"}},
|
||||
{"smutný", "nešťastný, zarmoucený", 1, {"synonymum"},
|
||||
{"nešťastný|zarmoucený|tesklivý|skličený|žalostný"}},
|
||||
{"začít", "zahájit, započít", 1, {"synonymum"},
|
||||
{"zahájit|započít|začínat|rozpoutat|zahajovat"}},
|
||||
{"krásný", "nádherný, překrásný", 1, {"synonymum"},
|
||||
{"nádherný|překrásný|kouzelný|půvabný|okouzlující"}},
|
||||
{"mluvit", "hovořit, promlouvat", 1, {"synonymum"},
|
||||
{"hovořit|promlouvat|vyprávět|říkat|povídat"}},
|
||||
};
|
||||
|
||||
static const MluvItem e10_items[] = {
|
||||
{"starý ×", "nový", 1, {"opak"}, {"nový|mladý"}},
|
||||
{"začátek ×", "konec", 1, {"opak"}, {"konec|závěr"}},
|
||||
{"přijít ×", "odejít", 1, {"opak"}, {"odejít|odjet|odcházet"}},
|
||||
{"radost ×", "smutek", 1, {"opak"}, {"smutek|žal|zármutek"}},
|
||||
{"vysoký ×", "nízký", 1, {"opak"}, {"nízký|malý"}},
|
||||
};
|
||||
|
||||
static const MluvItem e11_items[] = {
|
||||
{"(s/z)kočit dolů ze stromu", "skočit", 1, {"s-/z-"}, {"skočit"}},
|
||||
{"(s/z)epsat úkol", "sepsat", 1, {"s-/z-"}, {"sepsat"}},
|
||||
{"(s/z)budovat nový dům", "zbudovat", 1, {"s-/z-"}, {"zbudovat"}},
|
||||
{"(s/z)vednout těžký kámen", "zvednout", 1, {"s-/z-"}, {"zvednout"}},
|
||||
{"(s/z)pravit auto", "spravit", 1, {"s-/z-"}, {"spravit"}},
|
||||
};
|
||||
|
||||
static const MluvRevealItem e12_items[] = {
|
||||
{"Koupil jsem novej kabát.", "nový („novej“ je nespisovné)"},
|
||||
{"Přišli jsme tam, kdy jsme měli.",
|
||||
"kdy → když: „Přišli jsme tam, když jsme měli.“"},
|
||||
{"Ona mě o tom neřekla nic.",
|
||||
"mě → mi (dativ): „Ona mi o tom neřekla nic.“"},
|
||||
{"Je to nejlepší film, který jsem viděl vůbec.",
|
||||
"„vůbec nejlepší film“: „Je to vůbec nejlepší film, který jsem viděl.“"},
|
||||
{"Byl jsem na nákupu a koupil chléb.",
|
||||
"„Byl jsem nakupovat a koupil jsem chléb.“"},
|
||||
};
|
||||
|
||||
static const MluvItem e13_items[] = {
|
||||
{"Viděl jsem (ten starý muž) ______.", "toho starého muže", 1,
|
||||
{"správný tvar"}, {"toho starého muže|toho starého muže,"}},
|
||||
{"Mluvil jsem s (moje sestra) ______.", "mojí sestrou", 1,
|
||||
{"správný tvar"}, {"mojí sestrou|mou sestrou|svou sestrou"}},
|
||||
{"Jdu do (škola) ______.", "školy", 1, {"správný tvar"}, {"školy"}},
|
||||
{"Sedím vedle (kamarád) ______.", "kamaráda", 1,
|
||||
{"správný tvar"}, {"kamaráda"}},
|
||||
{"Vzpomínám na (dětství) ______.", "dětství", 1,
|
||||
{"správný tvar"}, {"dětství"}},
|
||||
};
|
||||
|
||||
static const ChoiceQ e14_qs[] = {
|
||||
{"Slovo „záříjový“ je:", {"správně", "chybně"}, 2, 1},
|
||||
{"Které slovo je přídavné jméno?",
|
||||
{"rychle", "rychlost", "rychlý", "zrychlení"}, 4, 2},
|
||||
{"Věta „Petr čte knihu.“ je:",
|
||||
{"souvětí", "věta jednoduchá", "věta bez podmětu", "věta zvolací"}, 4, 1},
|
||||
{"Slovo „příroda“ je utvořeno:",
|
||||
{"odvozením", "skládáním", "zkracováním", "přejímáním"}, 4, 0},
|
||||
{"Která dvojice jsou synonyma?",
|
||||
{"rychlý – pomalý", "jít – přijít", "smutný – nešťastný",
|
||||
"velký – malý"}, 4, 2},
|
||||
};
|
||||
|
||||
static const char *e14_expl[] = {
|
||||
"Správně je „zářijový“ (nikoli „záříjový“).",
|
||||
"„rychlý“ je přídavné jméno; „rychle“ je příslovce.",
|
||||
"Věta má jen jeden přísudek („čte“), jde o větu jednoduchou.",
|
||||
"„příroda“ je utvořena odvozením, není to složenina.",
|
||||
"Synonyma vyjadřují stejný význam: „smutný“ a „nešťastný“.",
|
||||
};
|
||||
|
||||
static const MluvRevealItem e15_items[] = {
|
||||
{"Matka řekla: „Pojď se najíst.“",
|
||||
"Matka řekla, abych přišel se najíst."},
|
||||
{"Učitel řekl: „Zítra bude písemka.“",
|
||||
"Učitel řekl, že zítra bude písemka."},
|
||||
{"Kamarád se zeptal: „Kde bydlíš?“",
|
||||
"Kamarád se zeptal, kde bydlím."},
|
||||
};
|
||||
|
||||
static const MluvRevealItem e16_items[] = {
|
||||
{"bydlet × žít",
|
||||
"bydlet = mít domov někde; žít = existovat, prožívat život"},
|
||||
{"říkat × vyprávět",
|
||||
"říkat = sdělovat informaci; vyprávět = podrobně popisovat příběh"},
|
||||
{"dívat se × vidět",
|
||||
"dívat se = záměrně sledovat; vidět = vnímat zrakem (i neúmyslně)"},
|
||||
{"učit × učit se",
|
||||
"učit = předávat znalosti druhým; učit se = sám nabývat znalosti"},
|
||||
{"přinést × donést",
|
||||
"přinést = přinést sem (k mluvčímu); donést = odnést tam (od mluvčího)"},
|
||||
};
|
||||
|
||||
static const MluvRevealItem e17_items[] = {
|
||||
{"Přišel jsem domů unavený hladový a promrzlý.",
|
||||
"Přišel jsem domů unavený, hladový a promrzlý."},
|
||||
{"Když dorazil vlak nastoupili všichni cestující.",
|
||||
"Když dorazil vlak, nastoupili všichni cestující."},
|
||||
{"Myslím že dnes bude pršet.", "Myslím, že dnes bude pršet."},
|
||||
{"Jana která je moje nejlepší kamarádka bydlí vedle.",
|
||||
"Jana, která je moje nejlepší kamarádka, bydlí vedle."},
|
||||
{"Pes štěkal ale nikdo nepřicházel.",
|
||||
"Pes štěkal, ale nikdo nepřicházel."},
|
||||
};
|
||||
|
||||
static const MluvItem e18_items[] = {
|
||||
{"Slunce se usmívalo na celé město.", "personifikace", 1,
|
||||
{"druh pojmenování"}, {"personifikace|zosobnění"}},
|
||||
{"Je silný jako býk.", "přirovnání", 1,
|
||||
{"druh pojmenování"}, {"přirovnání|komparace"}},
|
||||
{"Moře hněvu ho zaplavilo.", "metafora", 1,
|
||||
{"druh pojmenování"}, {"metafora"}},
|
||||
{"Čekal jsem na tebe věčnost.", "hyperbola", 1,
|
||||
{"druh pojmenování"}, {"hyperbola|nadsázka"}},
|
||||
{"Přečetl celého Shakespeara.", "metonymie", 1,
|
||||
{"druh pojmenování"}, {"metonymie"}},
|
||||
};
|
||||
|
||||
static const MluvItem e19_items[] = {
|
||||
{"Narodil se roku 1890 v Praze. Vystudoval práva a poté pracoval jako "
|
||||
"úředník…", "životopis", 1, {"slohový útvar"}, {"životopis|biografie"}},
|
||||
{"Byl to chlapec střední postavy, s hnědýma očima a klidným výrazem…",
|
||||
"charakteristika", 1, {"slohový útvar"}, {"charakteristika"}},
|
||||
{"Jednoho dne jsem šel lesem, když tu najednou jsem zaslechl podivný "
|
||||
"zvuk…", "vyprávění", 1, {"slohový útvar"}, {"vyprávění"}},
|
||||
{"Mnozí se ptají, zda má smysl číst knihy v době internetu. Domnívám "
|
||||
"se…", "úvaha", 1, {"slohový útvar"}, {"úvaha"}},
|
||||
{"Místnost byla malá, se dvěma okny na jih. U zdi stála stará skříň…",
|
||||
"popis", 1, {"slohový útvar"}, {"popis"}},
|
||||
};
|
||||
|
||||
static const ChoiceQ e20_qs[] = {
|
||||
{"Které slovo není příslovce?",
|
||||
{"rychle", "tam", "hezký", "brzy"}, 4, 2},
|
||||
{"Ve větě „Petr dal Pavlovi knihu“ je slovo „Pavlovi“:",
|
||||
{"podmět", "přísudek", "předmět", "příslovečné určení"}, 4, 2},
|
||||
{"Slovo „nejhezčí“ obsahuje:",
|
||||
{"předponu a příponu", "pouze předponu", "kořen a příponu",
|
||||
"pouze příponu"}, 4, 0},
|
||||
{"Která věta je souvětí souřadné slučovací?",
|
||||
{"Přišel, protože chtěl pomoci.", "Šel domů a spal.",
|
||||
"Věřím, že přijdeš.", "Udělám to, jak budeš chtít."}, 4, 1},
|
||||
{"Příslovce „rychle“ je stupňováno jako:",
|
||||
{"rychle – rychleji – nejrychleji",
|
||||
"rychle – více rychle – nejvíce rychle",
|
||||
"rychle – rychlé – nejrychlejší", "nelze stupňovat"}, 4, 0},
|
||||
};
|
||||
|
||||
GtkWidget *build_mluvnice_exercise_page(int n) {
|
||||
switch (n) {
|
||||
case 1:
|
||||
return mluv_typed_page(1,
|
||||
"Pravopis – i/y po obojetných souhláskách",
|
||||
"Doplňte i/í nebo y/ý.", NULL,
|
||||
e1_items, G_N_ELEMENTS(e1_items));
|
||||
case 2:
|
||||
return mluv_choice_page(2, "Pravopis – velká/malá písmena",
|
||||
"Vyberte správnou variantu.", NULL,
|
||||
e2_qs, NULL, G_N_ELEMENTS(e2_qs));
|
||||
case 3:
|
||||
return mluv_typed_page(3, "Slovní druhy",
|
||||
"Určete slovní druh podtržených slov.",
|
||||
"Do pole napište název slovního druhu.",
|
||||
e3_items, G_N_ELEMENTS(e3_items));
|
||||
case 4:
|
||||
return mluv_typed_page(4,
|
||||
"Mluvnické kategorie – podstatná jména",
|
||||
"Určete rod, číslo a pád podtržených slov.",
|
||||
"Vyplňte tři pole: rod, číslo a pád.",
|
||||
e4_items, G_N_ELEMENTS(e4_items));
|
||||
case 5:
|
||||
return mluv_typed_page(5, "Mluvnické kategorie – slovesa",
|
||||
"Určete osobu, číslo, čas a způsob u těchto sloves.",
|
||||
"U rozkazovacího a podmiňovacího způsobu napište do pole "
|
||||
"čas „neurčitý“.",
|
||||
e5_items, G_N_ELEMENTS(e5_items));
|
||||
case 6:
|
||||
return mluv_reveal_page(6, "Slovotvorba",
|
||||
"Rozeberte slova na morfémy.",
|
||||
"Určete předponu, kořen, příponu a koncovku.",
|
||||
e6_items, G_N_ELEMENTS(e6_items));
|
||||
case 7:
|
||||
return mluv_typed_page(7, "Větné členy",
|
||||
"Určete větný člen podtržených výrazů.", NULL,
|
||||
e7_items, G_N_ELEMENTS(e7_items));
|
||||
case 8:
|
||||
return mluv_typed_page(8, "Druhy vedlejších vět",
|
||||
"Určete druh vedlejší věty.", NULL,
|
||||
e8_items, G_N_ELEMENTS(e8_items));
|
||||
case 9:
|
||||
return mluv_typed_page(9, "Synonyma",
|
||||
"Ke každému slovu napište synonymum.", NULL,
|
||||
e9_items, G_N_ELEMENTS(e9_items));
|
||||
case 10:
|
||||
return mluv_typed_page(10, "Antonyma", "Napište opak.", NULL,
|
||||
e10_items, G_N_ELEMENTS(e10_items));
|
||||
case 11:
|
||||
return mluv_typed_page(11,
|
||||
"Pravopis – s/z na začátku slova",
|
||||
"Vyberte správnou předponu s-/z-.", NULL,
|
||||
e11_items, G_N_ELEMENTS(e11_items));
|
||||
case 12:
|
||||
return mluv_reveal_page(12, "Skladba – najděte chybu",
|
||||
"Každá věta obsahuje jednu chybu. Najděte ji a opravte.",
|
||||
NULL, e12_items, G_N_ELEMENTS(e12_items));
|
||||
case 13:
|
||||
return mluv_typed_page(13, "Tvarosloví – správný tvar",
|
||||
"Dejte slovo do správného tvaru.", NULL,
|
||||
e13_items, G_N_ELEMENTS(e13_items));
|
||||
case 14:
|
||||
return mluv_choice_page(14, "Výběr ze čtyř možností",
|
||||
"Vyberte správnou odpověď.", NULL,
|
||||
e14_qs, e14_expl, G_N_ELEMENTS(e14_qs));
|
||||
case 15:
|
||||
return mluv_reveal_page(15, "Přímá a nepřímá řeč",
|
||||
"Přepište přímou řeč na nepřímou.", NULL,
|
||||
e15_items, G_N_ELEMENTS(e15_items));
|
||||
case 16:
|
||||
return mluv_reveal_page(16, "Slovní zásoba – rozdíly",
|
||||
"Vysvětlete rozdíl mezi těmito dvojicemi slov.", NULL,
|
||||
e16_items, G_N_ELEMENTS(e16_items));
|
||||
case 17:
|
||||
return mluv_reveal_page(17, "Interpunkce",
|
||||
"Doplňte čárky tam, kde patří.", NULL,
|
||||
e17_items, G_N_ELEMENTS(e17_items));
|
||||
case 18:
|
||||
return mluv_typed_page(18, "Obrazná pojmenování",
|
||||
"Určete, o jaký druh obrazného pojmenování jde.", NULL,
|
||||
e18_items, G_N_ELEMENTS(e18_items));
|
||||
case 19:
|
||||
return mluv_typed_page(19, "Stylistika – slohové útvary",
|
||||
"Přiřaďte ukázku ke správnému slohovému útvaru.", NULL,
|
||||
e19_items, G_N_ELEMENTS(e19_items));
|
||||
case 20:
|
||||
return mluv_choice_page(20, "Souhrnné opakování",
|
||||
"Vyberte správnou odpověď.", NULL,
|
||||
e20_qs, NULL, G_N_ELEMENTS(e20_qs));
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
GtkWidget *build_mluvnice_page(void) {
|
||||
return build_unit_page(&mluv_unit);
|
||||
}
|
||||
140
src/navigation.c
Normal file
140
src/navigation.c
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Navigation */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void on_nav_clicked(GtkButton *button, gpointer user_data) {
|
||||
const char *target = g_object_get_data(G_OBJECT(button), "target");
|
||||
(void)user_data;
|
||||
if (target)
|
||||
gtk_stack_set_visible_child_name(main_stack, target);
|
||||
}
|
||||
|
||||
void on_continue_clicked(GtkButton *button, gpointer user_data) {
|
||||
(void)button;
|
||||
(void)user_data;
|
||||
gtk_stack_set_visible_child_name(main_stack, "subjects");
|
||||
}
|
||||
|
||||
GtkWidget *make_back_button(const char *target) {
|
||||
GtkWidget *back = gtk_button_new();
|
||||
GtkWidget *icon = gtk_drawing_area_new();
|
||||
|
||||
gtk_widget_set_size_request(icon, 14, 14);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon), draw_back_icon,
|
||||
NULL, NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(back), icon);
|
||||
gtk_widget_add_css_class(back, "back-btn");
|
||||
gtk_widget_set_valign(back, GTK_ALIGN_CENTER);
|
||||
i18n_bind(back, "back", 2);
|
||||
g_signal_connect_swapped(back, "state-flags-changed",
|
||||
G_CALLBACK(gtk_widget_queue_draw), icon);
|
||||
g_object_set_data_full(G_OBJECT(back), "target", g_strdup(target), g_free);
|
||||
g_signal_connect(back, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
return back;
|
||||
}
|
||||
|
||||
GtkWidget *top_bar(const char *back_target, const char *title,
|
||||
const char *subtitle) {
|
||||
GtkWidget *top = gtk_center_box_new();
|
||||
gtk_widget_set_margin_top(top, 2);
|
||||
gtk_widget_set_margin_bottom(top, 6);
|
||||
|
||||
GtkWidget *back = make_back_button(back_target);
|
||||
gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
|
||||
|
||||
GtkWidget *center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
|
||||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
|
||||
|
||||
GtkWidget *heading = gtk_label_new(NULL);
|
||||
i18n_bind(heading, title, 0);
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(heading, "roadmap-title");
|
||||
gtk_box_append(GTK_BOX(center), heading);
|
||||
|
||||
if (subtitle) {
|
||||
GtkWidget *sub = gtk_label_new(NULL);
|
||||
i18n_bind(sub, subtitle, 0);
|
||||
gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(sub, "roadmap-sub");
|
||||
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(sub), 60);
|
||||
gtk_box_append(GTK_BOX(center), sub);
|
||||
}
|
||||
|
||||
return top;
|
||||
}
|
||||
|
||||
gboolean on_window_key_pressed(GtkEventControllerKey *controller,
|
||||
guint keyval, guint keycode,
|
||||
GdkModifierType state,
|
||||
gpointer user_data) {
|
||||
GtkWindow *window = user_data;
|
||||
|
||||
(void)controller;
|
||||
(void)keycode;
|
||||
|
||||
if ((keyval == GDK_KEY_q &&
|
||||
(state & (GDK_SUPER_MASK | GDK_META_MASK)) != 0) ||
|
||||
(keyval == GDK_KEY_F4 &&
|
||||
(state & GDK_ALT_MASK) != 0)) {
|
||||
GtkApplication *app = gtk_window_get_application(window);
|
||||
if (app != NULL)
|
||||
g_application_quit(G_APPLICATION(app));
|
||||
return GDK_EVENT_STOP;
|
||||
}
|
||||
|
||||
return GDK_EVENT_PROPAGATE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Welcome page */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
GtkWidget *build_welcome_page(void) {
|
||||
GtkWidget *box;
|
||||
GtkWidget *icon;
|
||||
GtkWidget *heading;
|
||||
GtkWidget *body;
|
||||
GtkWidget *button;
|
||||
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
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);
|
||||
|
||||
icon = gtk_image_new_from_icon_name(ICON_NAME);
|
||||
gtk_image_set_pixel_size(GTK_IMAGE(icon), 88);
|
||||
gtk_widget_set_halign(icon, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_margin_bottom(icon, 22);
|
||||
gtk_box_append(GTK_BOX(box), icon);
|
||||
|
||||
heading = gtk_label_new(NULL);
|
||||
welcome_heading = heading;
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(heading, "heading");
|
||||
gtk_box_append(GTK_BOX(box), heading);
|
||||
|
||||
body = gtk_label_new(NULL);
|
||||
i18n_bind(body, "welcome_body", 0);
|
||||
gtk_label_set_justify(GTK_LABEL(body), GTK_JUSTIFY_CENTER);
|
||||
gtk_label_set_wrap(GTK_LABEL(body), TRUE);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(body), 46);
|
||||
gtk_widget_set_margin_top(body, 10);
|
||||
gtk_widget_add_css_class(body, "welcome-body");
|
||||
gtk_box_append(GTK_BOX(box), body);
|
||||
|
||||
button = gtk_button_new();
|
||||
i18n_bind(button, "continue", 1);
|
||||
gtk_widget_set_halign(button, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_margin_top(button, 28);
|
||||
gtk_widget_add_css_class(button, "btn-primary");
|
||||
gtk_box_append(GTK_BOX(box), button);
|
||||
|
||||
g_signal_connect(button, "clicked", G_CALLBACK(on_continue_clicked), NULL);
|
||||
|
||||
return box;
|
||||
}
|
||||
343
src/progress.c
Normal file
343
src/progress.c
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Progress */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void unit_save_progress(UnitCtx *u) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
for (int i = 1; i <= MAX_UNIT_EX; i++) {
|
||||
gchar key[8];
|
||||
g_snprintf(key, sizeof(key), "%d", i);
|
||||
g_key_file_set_boolean(kf, "done", key, u->done[i]);
|
||||
}
|
||||
|
||||
if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
|
||||
g_key_file_free(kf);
|
||||
return;
|
||||
}
|
||||
|
||||
gchar *data = g_key_file_to_data(kf, NULL, NULL);
|
||||
g_key_file_free(kf);
|
||||
|
||||
if (data) {
|
||||
GError *err = NULL;
|
||||
g_file_set_contents(u->progress_file, data, -1, &err);
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_free(data);
|
||||
}
|
||||
}
|
||||
|
||||
void unit_load_progress(UnitCtx *u) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
GError *err = NULL;
|
||||
|
||||
if (!g_key_file_load_from_file(kf, u->progress_file, G_KEY_FILE_NONE, &err)) {
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_key_file_free(kf);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 1; i <= MAX_UNIT_EX; i++) {
|
||||
gchar key[8];
|
||||
g_snprintf(key, sizeof(key), "%d", i);
|
||||
u->done[i] = g_key_file_get_boolean(kf, "done", key, NULL);
|
||||
}
|
||||
|
||||
g_key_file_free(kf);
|
||||
}
|
||||
|
||||
void load_progress(void) {
|
||||
for (int i = 0; i < NUM_UNLOCKED; i++)
|
||||
unit_load_progress(&units[i]);
|
||||
net_load_progress();
|
||||
hw_load_progress();
|
||||
}
|
||||
|
||||
/* Redraw the exercise rails of every unit that already has a page. */
|
||||
void all_rails_redraw(void) {
|
||||
for (int i = 0; i < NUM_UNLOCKED; i++) {
|
||||
if (units[i].ex_rail)
|
||||
gtk_widget_queue_draw(units[i].ex_rail);
|
||||
}
|
||||
}
|
||||
|
||||
/* Index of the unit the student should work on next: the first unlocked one
|
||||
* that is not finished yet. -1 once every open unit is complete. */
|
||||
static int current_unit_index(void) {
|
||||
for (int uidx = 0; uidx < NUM_UNLOCKED; uidx++) {
|
||||
UnitCtx *u = &units[uidx];
|
||||
|
||||
if (u->n_ex <= 0)
|
||||
continue;
|
||||
for (int i = 1; i <= u->n_ex; i++)
|
||||
if (!u->done[i])
|
||||
return uidx;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void refresh_completion_ui(void) {
|
||||
int current = current_unit_index();
|
||||
|
||||
for (int uidx = 0; uidx < NUM_UNLOCKED; uidx++) {
|
||||
UnitCtx *u = &units[uidx];
|
||||
gboolean all = TRUE;
|
||||
|
||||
for (int i = 1; i <= u->n_ex; i++) {
|
||||
if (!u->done[i]) {
|
||||
all = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* per-bubble markers */
|
||||
if (u->ex_cells[0] != NULL) {
|
||||
for (int i = 1; i <= u->n_ex; i++) {
|
||||
GtkWidget *btn = u->ex_cells[i - 1];
|
||||
if (!btn)
|
||||
continue;
|
||||
if (u->done[i]) {
|
||||
gtk_widget_add_css_class(btn, "done");
|
||||
if (u->ex_icons[i])
|
||||
gtk_widget_set_visible(u->ex_icons[i], TRUE);
|
||||
} else {
|
||||
gtk_widget_remove_css_class(btn, "done");
|
||||
if (u->ex_icons[i])
|
||||
gtk_widget_set_visible(u->ex_icons[i], FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* off-path branch marker */
|
||||
if (u->has_branch && u->branch_cell) {
|
||||
if (u->done[u->branch_ex]) {
|
||||
gtk_widget_add_css_class(u->branch_cell, "done");
|
||||
if (u->branch_icon)
|
||||
gtk_widget_set_visible(u->branch_icon, TRUE);
|
||||
} else {
|
||||
gtk_widget_remove_css_class(u->branch_cell, "done");
|
||||
if (u->branch_icon)
|
||||
gtk_widget_set_visible(u->branch_icon, FALSE);
|
||||
}
|
||||
}
|
||||
|
||||
/* Roadmap node: finished, the one step to take next, or simply open.
|
||||
* Highlighting every unlocked unit made three of them look equally
|
||||
* "current" and told the student nothing. */
|
||||
if (u->node) {
|
||||
gboolean done = all && u->n_ex > 0;
|
||||
|
||||
if (done)
|
||||
gtk_widget_add_css_class(u->node, "done");
|
||||
else
|
||||
gtk_widget_remove_css_class(u->node, "done");
|
||||
|
||||
if (!done && uidx == current)
|
||||
gtk_widget_add_css_class(u->node, "current");
|
||||
else
|
||||
gtk_widget_remove_css_class(u->node, "current");
|
||||
|
||||
if (u->node_done_icon)
|
||||
gtk_widget_set_visible(u->node_done_icon, done);
|
||||
}
|
||||
}
|
||||
|
||||
all_rails_redraw();
|
||||
refresh_net_completion_ui();
|
||||
refresh_hw_completion_ui();
|
||||
mluvnice_refresh_ui();
|
||||
refresh_stats_ui();
|
||||
}
|
||||
|
||||
void mark_done(UnitCtx *u, int n) {
|
||||
if (!u || n < 1 || n > MAX_UNIT_EX)
|
||||
return;
|
||||
u->done[n] = TRUE;
|
||||
refresh_completion_ui();
|
||||
unit_save_progress(u);
|
||||
}
|
||||
|
||||
void net_load_progress(void) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
GError *err = NULL;
|
||||
|
||||
if (!g_key_file_load_from_file(kf, PROGRESS_NET, G_KEY_FILE_NONE, &err)) {
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_key_file_free(kf);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < NET_LESSONS; i++) {
|
||||
gchar key[8];
|
||||
|
||||
g_snprintf(key, sizeof(key), "%d", i + 1);
|
||||
net_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
|
||||
}
|
||||
|
||||
g_key_file_free(kf);
|
||||
}
|
||||
|
||||
void net_save_progress(void) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
|
||||
for (int i = 0; i < NET_LESSONS; i++) {
|
||||
gchar key[8];
|
||||
|
||||
g_snprintf(key, sizeof(key), "%d", i + 1);
|
||||
g_key_file_set_boolean(kf, "done", key, net_lessons[i].done);
|
||||
}
|
||||
|
||||
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_NET, data, -1, &err);
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_free(data);
|
||||
}
|
||||
}
|
||||
|
||||
void refresh_net_completion_ui(void) {
|
||||
for (int i = 0; i < NET_LESSONS; i++) {
|
||||
GtkWidget *node = net_nodes[i];
|
||||
GtkWidget *icon = net_lessons[i].done_icon;
|
||||
|
||||
if (!node)
|
||||
continue;
|
||||
if (net_lessons[i].done) {
|
||||
gtk_widget_remove_css_class(node, "current");
|
||||
gtk_widget_add_css_class(node, "done");
|
||||
if (icon)
|
||||
gtk_widget_set_visible(icon, TRUE);
|
||||
} else {
|
||||
gtk_widget_remove_css_class(node, "done");
|
||||
gtk_widget_add_css_class(node, "current");
|
||||
if (icon)
|
||||
gtk_widget_set_visible(icon, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mark_net_done(int lesson_id) {
|
||||
if (lesson_id < 0 || lesson_id >= NET_LESSONS)
|
||||
return;
|
||||
if (net_lessons[lesson_id].done)
|
||||
return;
|
||||
net_lessons[lesson_id].done = TRUE;
|
||||
net_save_progress();
|
||||
refresh_net_completion_ui();
|
||||
refresh_stats_ui();
|
||||
}
|
||||
|
||||
void progress_for_net(ProgressSum *out) {
|
||||
out->total_ex += NET_LESSONS;
|
||||
out->open_units += NET_LESSONS;
|
||||
for (int i = 0; i < NET_LESSONS; i++) {
|
||||
if (net_lessons[i].done) {
|
||||
out->done_ex++;
|
||||
out->done_units++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void hw_load_progress(void) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
GError *err = NULL;
|
||||
|
||||
if (!g_key_file_load_from_file(kf, PROGRESS_HW, G_KEY_FILE_NONE, &err)) {
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_key_file_free(kf);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < HW_LESSONS; i++) {
|
||||
gchar key[8];
|
||||
|
||||
g_snprintf(key, sizeof(key), "%d", i + 1);
|
||||
hw_lessons[i].done = g_key_file_get_boolean(kf, "done", key, NULL);
|
||||
}
|
||||
|
||||
g_key_file_free(kf);
|
||||
}
|
||||
|
||||
void hw_save_progress(void) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
|
||||
for (int i = 0; i < HW_LESSONS; i++) {
|
||||
gchar key[8];
|
||||
|
||||
g_snprintf(key, sizeof(key), "%d", i + 1);
|
||||
g_key_file_set_boolean(kf, "done", key, hw_lessons[i].done);
|
||||
}
|
||||
|
||||
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_HW, data, -1, &err);
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_free(data);
|
||||
}
|
||||
}
|
||||
|
||||
void refresh_hw_completion_ui(void) {
|
||||
for (int i = 0; i < HW_LESSONS; i++) {
|
||||
GtkWidget *node = hw_nodes[i];
|
||||
GtkWidget *icon = hw_lessons[i].done_icon;
|
||||
|
||||
if (!node)
|
||||
continue;
|
||||
if (hw_lessons[i].done) {
|
||||
gtk_widget_remove_css_class(node, "current");
|
||||
gtk_widget_add_css_class(node, "done");
|
||||
if (icon)
|
||||
gtk_widget_set_visible(icon, TRUE);
|
||||
} else {
|
||||
gtk_widget_remove_css_class(node, "done");
|
||||
gtk_widget_add_css_class(node, "current");
|
||||
if (icon)
|
||||
gtk_widget_set_visible(icon, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void mark_hw_done(int lesson_id) {
|
||||
if (lesson_id < 0 || lesson_id >= HW_LESSONS)
|
||||
return;
|
||||
if (hw_lessons[lesson_id].done)
|
||||
return;
|
||||
hw_lessons[lesson_id].done = TRUE;
|
||||
hw_save_progress();
|
||||
refresh_hw_completion_ui();
|
||||
refresh_stats_ui();
|
||||
}
|
||||
|
||||
void progress_for_hw(ProgressSum *out) {
|
||||
out->total_ex += HW_LESSONS;
|
||||
out->open_units += HW_LESSONS;
|
||||
for (int i = 0; i < HW_LESSONS; i++) {
|
||||
if (hw_lessons[i].done) {
|
||||
out->done_ex++;
|
||||
out->done_units++;
|
||||
}
|
||||
}
|
||||
}
|
||||
409
src/roadmap.c
Normal file
409
src/roadmap.c
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Roadmap page */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Draw the "finish line" node: a locked-looking circle with the unit number
|
||||
* and a small chequered flag. The chequers used to fill the whole circle,
|
||||
* which read as the placeholder pattern for a missing image rather than as a
|
||||
* flag, so they are now a badge sitting on a plain node face. */
|
||||
void draw_finish_cell(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double cell = 6.0; /* chequer square */
|
||||
const int flag_cols = 3;
|
||||
const int flag_rows = 2;
|
||||
const Rgb gray = color_from_hex(app_theme.overlay);
|
||||
const Rgb fin_dark = color_from_hex(app_theme.finish_dark);
|
||||
const Rgb fin_light = color_from_hex(app_theme.finish_light);
|
||||
const Rgb fin_stroke = color_from_hex(app_theme.finish_stroke);
|
||||
int n = GPOINTER_TO_INT(data);
|
||||
double cx = width / 2.0;
|
||||
double cy = height / 2.0;
|
||||
double r = MIN(width, height) / 2.0 - 2.0;
|
||||
char buf[8];
|
||||
PangoLayout *layout;
|
||||
PangoFontDescription *fd;
|
||||
PangoRectangle ink;
|
||||
double num_cy;
|
||||
double flag_x, flag_y;
|
||||
int ix, iy;
|
||||
|
||||
(void)area;
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||
cairo_clip(cr);
|
||||
|
||||
cairo_rectangle(cr, cx - r, cy - r, 2.0 * r, 2.0 * r);
|
||||
cairo_set_source_rgb(cr, fin_dark.r, fin_dark.g, fin_dark.b);
|
||||
cairo_fill(cr);
|
||||
|
||||
/* unit number, styled like the other locked nodes */
|
||||
g_snprintf(buf, sizeof(buf), "%d", n);
|
||||
layout = pango_cairo_create_layout(cr);
|
||||
fd = pango_font_description_new();
|
||||
pango_font_description_set_family(fd, "sans");
|
||||
pango_font_description_set_weight(fd, PANGO_WEIGHT_ULTRABOLD);
|
||||
pango_font_description_set_size(fd, 22 * PANGO_SCALE);
|
||||
pango_layout_set_font_description(layout, fd);
|
||||
pango_layout_set_text(layout, buf, -1);
|
||||
pango_layout_get_pixel_extents(layout, &ink, NULL);
|
||||
|
||||
cairo_set_source_rgb(cr, gray.r, gray.g, gray.b);
|
||||
num_cy = cy - 3.0;
|
||||
cairo_move_to(cr, cx - ink.width / 2.0 - ink.x,
|
||||
num_cy - ink.height / 2.0 - ink.y);
|
||||
pango_cairo_show_layout(cr, layout);
|
||||
|
||||
/* chequered flag below the number, where locked units show a padlock */
|
||||
flag_x = cx - (flag_cols * cell) / 2.0;
|
||||
flag_y = num_cy + ink.height / 2.0 + 6.0;
|
||||
for (iy = 0; iy < flag_rows; iy++) {
|
||||
for (ix = 0; ix < flag_cols; ix++) {
|
||||
const Rgb *c = ((ix + iy) & 1) ? &fin_light : &fin_stroke;
|
||||
|
||||
cairo_rectangle(cr, flag_x + ix * cell, flag_y + iy * cell,
|
||||
cell, cell);
|
||||
cairo_set_source_rgb(cr, c->r, c->g, c->b);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
}
|
||||
|
||||
g_object_unref(layout);
|
||||
pango_font_description_free(fd);
|
||||
cairo_restore(cr);
|
||||
|
||||
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||
cairo_set_source_rgb(cr, fin_stroke.r, fin_stroke.g, fin_stroke.b);
|
||||
cairo_set_line_width(cr, 2.0);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
void road_point(double t, double *ox, double *oy) {
|
||||
int k = (int)t;
|
||||
double u = t - (double)k;
|
||||
double u2, u3;
|
||||
double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
|
||||
|
||||
if (k < 0) { k = 0; u = 0.0; }
|
||||
if (k >= NUM_UNITS - 1) { k = NUM_UNITS - 2; u = 1.0; }
|
||||
|
||||
p1x = road_cx[k]; p1y = road_cy[k];
|
||||
p2x = road_cx[k + 1]; p2y = road_cy[k + 1];
|
||||
if (k - 1 >= 0) { p0x = road_cx[k - 1]; p0y = road_cy[k - 1]; }
|
||||
else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
|
||||
if (k + 2 < NUM_UNITS) { p3x = road_cx[k + 2]; p3y = road_cy[k + 2]; }
|
||||
else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
|
||||
|
||||
u2 = u * u;
|
||||
u3 = u2 * u;
|
||||
*ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
|
||||
+ (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
|
||||
+ (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
|
||||
*oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
|
||||
+ (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
|
||||
+ (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
|
||||
}
|
||||
|
||||
void road_path(cairo_t *cr, double t0, double t1) {
|
||||
const double steps_per_unit = 48.0;
|
||||
int n = (int)((t1 - t0) * steps_per_unit);
|
||||
double x, y;
|
||||
int s;
|
||||
|
||||
if (n < 1)
|
||||
n = 1;
|
||||
road_point(t0, &x, &y);
|
||||
cairo_move_to(cr, x, y);
|
||||
for (s = 1; s <= n; s++) {
|
||||
road_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
|
||||
cairo_line_to(cr, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/* Compute the serpentine node layout for the given available width. */
|
||||
void roadmap_geometry(double avail) {
|
||||
const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_UNITS - 1);
|
||||
int rows;
|
||||
int r, c, i;
|
||||
|
||||
for (rows = 1; rows <= NUM_UNITS; rows++) {
|
||||
int cols = (NUM_UNITS + rows - 1) / rows;
|
||||
if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0)
|
||||
break;
|
||||
}
|
||||
if (rows > NUM_UNITS)
|
||||
rows = NUM_UNITS;
|
||||
road_rows = rows;
|
||||
road_cols = (NUM_UNITS + rows - 1) / rows;
|
||||
if (road_cols < 1)
|
||||
road_cols = 1;
|
||||
road_cw = (int)(2.0 * ROAD_MX + (road_cols - 1) * PATH_SPAC);
|
||||
road_ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP);
|
||||
|
||||
for (r = 0; r < rows; r++) {
|
||||
int base = r * road_cols;
|
||||
int len = MIN(road_cols, NUM_UNITS - base);
|
||||
int fwd = (r % 2) == 0;
|
||||
for (c = 0; c < len; c++) {
|
||||
i = base + c;
|
||||
int cc = fwd ? c : (road_cols - 1 - c);
|
||||
road_cx[i] = ROAD_MX + cc * PATH_SPAC;
|
||||
road_cy[i] = ROAD_MY + r * ROAD_GAP
|
||||
+ ROAD_WAVE * sin((double)i * phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void road_apply_layout(void) {
|
||||
int i;
|
||||
|
||||
if (!road_fixed)
|
||||
return;
|
||||
|
||||
for (i = 0; i < NUM_UNITS; i++) {
|
||||
if (!road_nodes[i] || !road_labels[i])
|
||||
continue;
|
||||
gtk_fixed_move(GTK_FIXED(road_fixed), road_nodes[i],
|
||||
(int)(road_cx[i] - NODE_SIZE / 2.0),
|
||||
(int)(road_cy[i] - NODE_SIZE / 2.0));
|
||||
gtk_fixed_move(GTK_FIXED(road_fixed), road_labels[i],
|
||||
(int)(road_cx[i] - (PATH_SPAC - 20.0) / 2.0),
|
||||
(int)(road_cy[i] + NODE_SIZE / 2.0 + 10.0));
|
||||
}
|
||||
|
||||
gtk_widget_set_size_request(road_fixed, road_cw, road_ch);
|
||||
gtk_widget_set_size_request(road_rail, road_cw, road_ch);
|
||||
gtk_fixed_move(GTK_FIXED(road_fixed), road_rail, 0, 0);
|
||||
gtk_widget_queue_draw(road_rail);
|
||||
}
|
||||
|
||||
void road_relayout(void) {
|
||||
GtkAdjustment *hadj;
|
||||
double avail;
|
||||
|
||||
if (!road_scroll)
|
||||
return;
|
||||
hadj = gtk_scrolled_window_get_hadjustment(
|
||||
GTK_SCROLLED_WINDOW(road_scroll));
|
||||
avail = gtk_adjustment_get_page_size(hadj);
|
||||
if (avail < 1.0)
|
||||
return;
|
||||
roadmap_geometry(avail);
|
||||
road_apply_layout();
|
||||
}
|
||||
|
||||
guint road_idle;
|
||||
|
||||
gboolean road_relayout_idle(gpointer data) {
|
||||
(void)data;
|
||||
road_idle = 0;
|
||||
road_relayout();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void road_relayout_later(void) {
|
||||
if (road_idle == 0)
|
||||
road_idle = g_idle_add(road_relayout_idle, NULL);
|
||||
}
|
||||
|
||||
void road_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
|
||||
gpointer data) {
|
||||
(void)adj;
|
||||
(void)ps;
|
||||
(void)data;
|
||||
road_relayout_later();
|
||||
}
|
||||
|
||||
void draw_rail(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer user_data) {
|
||||
const double t_lit = (double)(NUM_UNLOCKED - 1);
|
||||
const double t_end = (double)(NUM_UNITS - 1);
|
||||
const Rgb accent = color_from_hex(app_theme.accent);
|
||||
const Rgb rail = color_from_hex(app_theme.rail);
|
||||
const Rgb muted = color_from_hex(app_theme.subtext);
|
||||
|
||||
(void)area;
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)user_data;
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
|
||||
cairo_new_path(cr);
|
||||
road_path(cr, 0.0, t_end);
|
||||
cairo_set_line_width(cr, 12);
|
||||
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
|
||||
cairo_stroke(cr);
|
||||
|
||||
if (t_lit > 0.0) {
|
||||
cairo_new_path(cr);
|
||||
road_path(cr, 0.0, t_lit);
|
||||
cairo_set_source_rgb(cr, accent.r, accent.g, accent.b);
|
||||
cairo_set_line_width(cr, 12);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
{
|
||||
double start_t = t_lit > 0.0 ? t_lit : 0.0;
|
||||
|
||||
cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17);
|
||||
cairo_set_dash(cr, (double[]){1.0, 26.0}, 2, 0.0);
|
||||
cairo_new_path(cr);
|
||||
road_path(cr, start_t, t_end);
|
||||
cairo_stroke(cr);
|
||||
cairo_set_dash(cr, NULL, 0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
void add_path_node(GtkFixed *fixed, int index) {
|
||||
int num = index + 1;
|
||||
UnitCtx *u = &units[index];
|
||||
gboolean locked = !u->unlocked;
|
||||
GtkWidget *card;
|
||||
GtkWidget *name;
|
||||
char *text;
|
||||
|
||||
card = gtk_button_new();
|
||||
gtk_widget_add_css_class(card, "unit-node");
|
||||
gtk_widget_set_can_focus(card, FALSE);
|
||||
gtk_widget_set_sensitive(card, !locked);
|
||||
gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE);
|
||||
|
||||
if (index == NUM_UNITS - 1) {
|
||||
GtkWidget *fin = gtk_drawing_area_new();
|
||||
|
||||
gtk_widget_add_css_class(card, "finish");
|
||||
gtk_widget_add_css_class(card, "locked");
|
||||
gtk_widget_set_size_request(fin, (int)NODE_SIZE, (int)NODE_SIZE);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(fin),
|
||||
draw_finish_cell,
|
||||
GINT_TO_POINTER(num), NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(card), fin);
|
||||
} else {
|
||||
GtkWidget *vbox;
|
||||
GtkWidget *number;
|
||||
GtkWidget *icon;
|
||||
|
||||
/* Unlocked units start out plain; refresh_completion_ui() decides
|
||||
* which single one is the current step. */
|
||||
if (locked)
|
||||
gtk_widget_add_css_class(card, "locked");
|
||||
|
||||
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||||
gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
|
||||
gtk_button_set_child(GTK_BUTTON(card), vbox);
|
||||
|
||||
text = g_strdup_printf("%d", num);
|
||||
number = gtk_label_new(text);
|
||||
g_free(text);
|
||||
gtk_widget_add_css_class(number, "unit-number");
|
||||
gtk_box_append(GTK_BOX(vbox), number);
|
||||
|
||||
if (locked) {
|
||||
icon = icon_area_new(draw_lock_icon,
|
||||
0.3451, 0.3569, 0.4392, 16);
|
||||
} else {
|
||||
icon = icon_area_new(draw_check_icon,
|
||||
0.1176, 0.1176, 0.1804, 18);
|
||||
gtk_widget_set_visible(icon, FALSE);
|
||||
u->node = card;
|
||||
u->node_done_icon = icon;
|
||||
g_object_set_data_full(G_OBJECT(card), "target",
|
||||
g_strdup(u->page), g_free);
|
||||
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
}
|
||||
gtk_box_append(GTK_BOX(vbox), icon);
|
||||
}
|
||||
|
||||
road_nodes[index] = card;
|
||||
gtk_fixed_put(fixed, card, 0, 0);
|
||||
|
||||
name = gtk_label_new(u->title);
|
||||
gtk_widget_set_size_request(name, (int)(PATH_SPAC - 20.0), -1);
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
|
||||
gtk_label_set_justify(GTK_LABEL(name), GTK_JUSTIFY_CENTER);
|
||||
gtk_label_set_wrap(GTK_LABEL(name), TRUE);
|
||||
gtk_widget_add_css_class(name, "unit-name");
|
||||
if (locked)
|
||||
gtk_widget_add_css_class(name, "unit-name-locked");
|
||||
road_labels[index] = name;
|
||||
gtk_fixed_put(fixed, name, 0, 0);
|
||||
}
|
||||
|
||||
GtkWidget *build_roadmap_page(void) {
|
||||
GtkWidget *page;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *wrap;
|
||||
GtkWidget *fixed;
|
||||
GtkWidget *rail;
|
||||
GtkAdjustment *ha;
|
||||
GtkAdjustment *va;
|
||||
int i;
|
||||
|
||||
roadmap_geometry(1000.0);
|
||||
|
||||
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_hexpand(page, TRUE);
|
||||
gtk_widget_set_vexpand(page, TRUE);
|
||||
gtk_widget_set_margin_start(page, 32);
|
||||
gtk_widget_set_margin_end(page, 32);
|
||||
gtk_widget_set_margin_top(page, 24);
|
||||
gtk_widget_set_margin_bottom(page, 24);
|
||||
|
||||
gtk_box_append(GTK_BOX(page),
|
||||
top_bar("subjects", "roadmap_title", "roadmap_sub"));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 14);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
road_scroll = scroll;
|
||||
|
||||
wrap = gtk_center_box_new();
|
||||
gtk_widget_set_vexpand(wrap, TRUE);
|
||||
gtk_widget_set_hexpand(wrap, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
|
||||
|
||||
fixed = gtk_fixed_new();
|
||||
gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_size_request(fixed, road_cw, road_ch);
|
||||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
|
||||
road_fixed = fixed;
|
||||
|
||||
rail = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(rail, road_cw, road_ch);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_rail, NULL, NULL);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
|
||||
road_rail = rail;
|
||||
|
||||
for (i = 0; i < NUM_UNITS; i++)
|
||||
add_path_node(GTK_FIXED(fixed), i);
|
||||
|
||||
ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
g_signal_connect(ha, "notify::page-size",
|
||||
G_CALLBACK(road_adjust_notify), NULL);
|
||||
g_signal_connect(va, "notify::page-size",
|
||||
G_CALLBACK(road_adjust_notify), NULL);
|
||||
|
||||
road_apply_layout();
|
||||
road_relayout_later();
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
360
src/settings.c
Normal file
360
src/settings.c
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Settings */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static GtkWidget *settings_popover;
|
||||
|
||||
void settings_open(void) {
|
||||
if (settings_popover)
|
||||
gtk_popover_popup(GTK_POPOVER(settings_popover));
|
||||
}
|
||||
|
||||
void draw_theme_swatch(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
ThemeId id = (ThemeId)GPOINTER_TO_INT(data);
|
||||
ThemePalette p = theme_palette(id, app_color_mode);
|
||||
Rgb base = color_from_hex(p.base);
|
||||
Rgb a1 = color_from_hex(p.accent);
|
||||
Rgb a2 = color_from_hex(p.accent3);
|
||||
Rgb ok = color_from_hex(p.success);
|
||||
Rgb accent = color_from_hex(app_theme.accent);
|
||||
gboolean selected = (app_theme_id == id);
|
||||
double size = MIN(width, height);
|
||||
double cx = width / 2.0;
|
||||
double cy = height / 2.0;
|
||||
double r = size / 2.0 - (selected ? 3.0 : 1.0);
|
||||
double dot = MAX(3.5, r * 0.26);
|
||||
double gap = dot * 2.05;
|
||||
|
||||
(void)area;
|
||||
|
||||
if (selected) {
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, size / 2.0 - 0.5, 0.0, 2.0 * G_PI);
|
||||
cairo_set_source_rgb(cr, accent.r, accent.g, accent.b);
|
||||
cairo_set_line_width(cr, 2.2);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||
cairo_set_source_rgb(cr, base.r, base.g, base.b);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx - gap, cy, dot, 0.0, 2.0 * G_PI);
|
||||
cairo_set_source_rgb(cr, a1.r, a1.g, a1.b);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, dot, 0.0, 2.0 * G_PI);
|
||||
cairo_set_source_rgb(cr, a2.r, a2.g, a2.b);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx + gap, cy, dot, 0.0, 2.0 * G_PI);
|
||||
cairo_set_source_rgb(cr, ok.r, ok.g, ok.b);
|
||||
cairo_fill(cr);
|
||||
}
|
||||
|
||||
void on_mode_toggled(GtkToggleButton *btn, gpointer user_data) {
|
||||
ColorMode mode = (ColorMode)GPOINTER_TO_INT(user_data);
|
||||
|
||||
if (!gtk_toggle_button_get_active(btn))
|
||||
return;
|
||||
if (app_color_mode == mode)
|
||||
return;
|
||||
app_color_mode = mode;
|
||||
apply_theme();
|
||||
save_settings();
|
||||
}
|
||||
|
||||
void on_theme_toggled(GtkToggleButton *btn, gpointer user_data) {
|
||||
ThemeId id = (ThemeId)GPOINTER_TO_INT(user_data);
|
||||
|
||||
if (!gtk_toggle_button_get_active(btn))
|
||||
return;
|
||||
if (app_theme_id == id)
|
||||
return;
|
||||
app_theme_id = id;
|
||||
apply_theme();
|
||||
save_settings();
|
||||
}
|
||||
|
||||
void on_lang_toggled(GtkToggleButton *btn, gpointer user_data) {
|
||||
UiLang lang = (UiLang)GPOINTER_TO_INT(user_data);
|
||||
|
||||
if (!gtk_toggle_button_get_active(btn))
|
||||
return;
|
||||
if (app_lang == lang)
|
||||
return;
|
||||
app_lang = lang;
|
||||
apply_language();
|
||||
save_settings();
|
||||
}
|
||||
|
||||
GtkWidget *make_lang_chip(const char *label, UiLang lang,
|
||||
GtkToggleButton *group) {
|
||||
GtkWidget *btn = gtk_toggle_button_new_with_label(label);
|
||||
|
||||
gtk_widget_add_css_class(btn, "mode-chip");
|
||||
gtk_widget_set_hexpand(btn, TRUE);
|
||||
if (group)
|
||||
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
|
||||
g_signal_connect(btn, "toggled", G_CALLBACK(on_lang_toggled),
|
||||
GINT_TO_POINTER(lang));
|
||||
return btn;
|
||||
}
|
||||
|
||||
GtkWidget *make_mode_chip(const char *label, ColorMode mode,
|
||||
GtkToggleButton *group) {
|
||||
GtkWidget *btn = gtk_toggle_button_new();
|
||||
|
||||
i18n_bind(btn, label, 1);
|
||||
gtk_widget_add_css_class(btn, "mode-chip");
|
||||
gtk_widget_set_hexpand(btn, TRUE);
|
||||
if (group)
|
||||
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
|
||||
g_signal_connect(btn, "toggled", G_CALLBACK(on_mode_toggled),
|
||||
GINT_TO_POINTER(mode));
|
||||
return btn;
|
||||
}
|
||||
|
||||
GtkWidget *make_theme_card(ThemeId id, GtkToggleButton *group) {
|
||||
GtkWidget *btn;
|
||||
GtkWidget *vbox;
|
||||
GtkWidget *swatch;
|
||||
GtkWidget *name;
|
||||
|
||||
btn = gtk_toggle_button_new();
|
||||
gtk_widget_add_css_class(btn, "theme-card");
|
||||
gtk_widget_set_hexpand(btn, TRUE);
|
||||
if (group)
|
||||
gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(btn), group);
|
||||
|
||||
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
|
||||
gtk_button_set_child(GTK_BUTTON(btn), vbox);
|
||||
|
||||
swatch = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(swatch, 44, 44);
|
||||
gtk_widget_set_halign(swatch, GTK_ALIGN_CENTER);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(swatch),
|
||||
draw_theme_swatch,
|
||||
GINT_TO_POINTER(id), NULL);
|
||||
theme_swatch_areas[id] = swatch;
|
||||
gtk_box_append(GTK_BOX(vbox), swatch);
|
||||
|
||||
name = gtk_label_new(theme_names[id]);
|
||||
gtk_widget_add_css_class(name, "theme-card-name");
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(vbox), name);
|
||||
|
||||
g_signal_connect(btn, "toggled", G_CALLBACK(on_theme_toggled),
|
||||
GINT_TO_POINTER(id));
|
||||
return btn;
|
||||
}
|
||||
|
||||
void on_settings_clicked(GtkButton *button, gpointer user_data) {
|
||||
GtkPopover *popover = GTK_POPOVER(user_data);
|
||||
|
||||
(void)button;
|
||||
gtk_popover_popup(popover);
|
||||
}
|
||||
|
||||
GtkWidget *build_stats_button(void) {
|
||||
GtkWidget *btn;
|
||||
GtkWidget *icon;
|
||||
|
||||
btn = gtk_button_new();
|
||||
gtk_widget_add_css_class(btn, "settings-btn");
|
||||
gtk_widget_add_css_class(btn, "flat");
|
||||
i18n_bind(btn, "stats", 2);
|
||||
gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_focus_on_click(btn, FALSE);
|
||||
|
||||
icon = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(icon, 18, 18);
|
||||
gtk_widget_set_can_target(icon, FALSE);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon),
|
||||
draw_stats_icon, NULL, NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(btn), icon);
|
||||
g_signal_connect_swapped(btn, "state-flags-changed",
|
||||
G_CALLBACK(gtk_widget_queue_draw), icon);
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_stats_clicked), NULL);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
GtkWidget *build_settings_button(void) {
|
||||
GtkWidget *btn;
|
||||
GtkWidget *icon;
|
||||
GtkWidget *popover;
|
||||
GtkWidget *box;
|
||||
GtkWidget *title;
|
||||
GtkWidget *label;
|
||||
GtkWidget *mode_row;
|
||||
GtkWidget *lang_row;
|
||||
GtkWidget *theme_grid;
|
||||
GtkWidget *dark_btn;
|
||||
GtkWidget *light_btn;
|
||||
GtkWidget *cs_btn;
|
||||
GtkWidget *en_btn;
|
||||
GtkWidget *first_theme = NULL;
|
||||
int i;
|
||||
|
||||
btn = gtk_button_new();
|
||||
gtk_widget_add_css_class(btn, "settings-btn");
|
||||
gtk_widget_add_css_class(btn, "flat");
|
||||
i18n_bind(btn, "settings", 2);
|
||||
gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_focus_on_click(btn, FALSE);
|
||||
|
||||
icon = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(icon, 18, 18);
|
||||
gtk_widget_set_can_target(icon, FALSE);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(icon),
|
||||
draw_settings_icon, NULL, NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(btn), icon);
|
||||
g_signal_connect_swapped(btn, "state-flags-changed",
|
||||
G_CALLBACK(gtk_widget_queue_draw), icon);
|
||||
|
||||
popover = gtk_popover_new();
|
||||
settings_popover = popover;
|
||||
gtk_widget_add_css_class(popover, "settings-popover");
|
||||
gtk_popover_set_has_arrow(GTK_POPOVER(popover), TRUE);
|
||||
gtk_popover_set_position(GTK_POPOVER(popover), GTK_POS_BOTTOM);
|
||||
gtk_widget_set_parent(popover, btn);
|
||||
g_signal_connect_swapped(btn, "destroy",
|
||||
G_CALLBACK(gtk_widget_unparent), popover);
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_settings_clicked), popover);
|
||||
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
|
||||
gtk_widget_add_css_class(box, "settings-box");
|
||||
|
||||
title = gtk_label_new(NULL);
|
||||
i18n_bind(title, "settings_title", 0);
|
||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(title, "settings-title");
|
||||
gtk_box_append(GTK_BOX(box), title);
|
||||
|
||||
label = gtk_label_new(NULL);
|
||||
i18n_bind(label, "mode", 0);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(label, "settings-label");
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_box_append(GTK_BOX(box), label);
|
||||
|
||||
mode_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_add_css_class(mode_row, "settings-seg");
|
||||
gtk_box_append(GTK_BOX(box), mode_row);
|
||||
|
||||
dark_btn = make_mode_chip("mode_dark", MODE_DARK, NULL);
|
||||
light_btn = make_mode_chip("mode_light", MODE_LIGHT,
|
||||
GTK_TOGGLE_BUTTON(dark_btn));
|
||||
g_signal_handlers_block_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_mode_toggled, NULL);
|
||||
g_signal_handlers_block_matched(light_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_mode_toggled, NULL);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dark_btn),
|
||||
app_color_mode == MODE_DARK);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(light_btn),
|
||||
app_color_mode == MODE_LIGHT);
|
||||
g_signal_handlers_unblock_matched(dark_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_mode_toggled, NULL);
|
||||
g_signal_handlers_unblock_matched(light_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_mode_toggled, NULL);
|
||||
gtk_box_append(GTK_BOX(mode_row), dark_btn);
|
||||
gtk_box_append(GTK_BOX(mode_row), light_btn);
|
||||
|
||||
label = gtk_label_new(NULL);
|
||||
i18n_bind(label, "theme", 0);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(label, "settings-label");
|
||||
gtk_widget_set_margin_top(label, 2);
|
||||
gtk_box_append(GTK_BOX(box), label);
|
||||
|
||||
theme_grid = gtk_flow_box_new();
|
||||
gtk_widget_add_css_class(theme_grid, "theme-grid");
|
||||
gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(theme_grid),
|
||||
GTK_SELECTION_NONE);
|
||||
gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
|
||||
gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(theme_grid), 3);
|
||||
gtk_flow_box_set_column_spacing(GTK_FLOW_BOX(theme_grid), 8);
|
||||
gtk_flow_box_set_row_spacing(GTK_FLOW_BOX(theme_grid), 8);
|
||||
gtk_flow_box_set_homogeneous(GTK_FLOW_BOX(theme_grid), TRUE);
|
||||
gtk_widget_set_hexpand(theme_grid, TRUE);
|
||||
gtk_box_append(GTK_BOX(box), theme_grid);
|
||||
|
||||
for (i = 0; i < THEME_COUNT; i++) {
|
||||
GtkWidget *card = make_theme_card(
|
||||
(ThemeId)i,
|
||||
first_theme ? GTK_TOGGLE_BUTTON(first_theme) : NULL);
|
||||
if (!first_theme)
|
||||
first_theme = card;
|
||||
if (app_theme_id == (ThemeId)i) {
|
||||
g_signal_handlers_block_matched(card, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_theme_toggled, NULL);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(card), TRUE);
|
||||
g_signal_handlers_unblock_matched(card, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_theme_toggled, NULL);
|
||||
}
|
||||
gtk_flow_box_insert(GTK_FLOW_BOX(theme_grid), card, -1);
|
||||
}
|
||||
|
||||
label = gtk_label_new(NULL);
|
||||
i18n_bind(label, "language", 0);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(label, "settings-label");
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_box_append(GTK_BOX(box), label);
|
||||
|
||||
lang_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_add_css_class(lang_row, "settings-seg");
|
||||
gtk_box_append(GTK_BOX(box), lang_row);
|
||||
|
||||
cs_btn = make_lang_chip("Čeština", LANG_CS, NULL);
|
||||
en_btn = make_lang_chip("English", LANG_EN, GTK_TOGGLE_BUTTON(cs_btn));
|
||||
g_signal_handlers_block_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_lang_toggled, NULL);
|
||||
g_signal_handlers_block_matched(en_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_lang_toggled, NULL);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs_btn),
|
||||
app_lang == LANG_CS);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(en_btn),
|
||||
app_lang == LANG_EN);
|
||||
g_signal_handlers_unblock_matched(cs_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_lang_toggled, NULL);
|
||||
g_signal_handlers_unblock_matched(en_btn, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_lang_toggled, NULL);
|
||||
gtk_box_append(GTK_BOX(lang_row), cs_btn);
|
||||
gtk_box_append(GTK_BOX(lang_row), en_btn);
|
||||
|
||||
label = gtk_label_new(NULL);
|
||||
i18n_bind(label, "updates", 0);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(label, "settings-label");
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_box_append(GTK_BOX(box), label);
|
||||
gtk_box_append(GTK_BOX(box), build_update_box());
|
||||
|
||||
{
|
||||
GtkWidget *sw = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
/* Tall enough for every section, including updates at the bottom; GTK
|
||||
* still shrinks the popover to fit the window and scrolls the rest. */
|
||||
gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(sw), 820);
|
||||
gtk_scrolled_window_set_has_frame(GTK_SCROLLED_WINDOW(sw), FALSE);
|
||||
gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(sw), TRUE);
|
||||
gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(sw), TRUE);
|
||||
gtk_widget_set_hexpand(sw, FALSE);
|
||||
gtk_widget_set_vexpand(sw, FALSE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(sw), box);
|
||||
gtk_popover_set_child(GTK_POPOVER(popover), sw);
|
||||
}
|
||||
|
||||
return btn;
|
||||
}
|
||||
432
src/stats.c
Normal file
432
src/stats.c
Normal file
|
|
@ -0,0 +1,432 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Statistics */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
GtkWidget *make_back_button(const char *target);
|
||||
void on_nav_clicked(GtkButton *button, gpointer user_data);
|
||||
|
||||
|
||||
StatsUi stats_ui;
|
||||
char *stats_return_page;
|
||||
|
||||
int unit_done_count(const UnitCtx *u) {
|
||||
int n = 0;
|
||||
|
||||
if (!u)
|
||||
return 0;
|
||||
for (int i = 1; i <= u->n_ex; i++) {
|
||||
if (u->done[i])
|
||||
n++;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
|
||||
/* Sum progress over units[first .. first+n-1]. Locked placeholder units and
|
||||
* the finish node (no playable exercises) are ignored. */
|
||||
void progress_for_units(int first, int n, ProgressSum *out) {
|
||||
for (int j = 0; j < n; j++) {
|
||||
UnitCtx *u;
|
||||
int d;
|
||||
|
||||
if (first < 0 || first + j >= NUM_UNITS)
|
||||
continue;
|
||||
u = &units[first + j];
|
||||
if (!u->unlocked || u->n_ex <= 0)
|
||||
continue;
|
||||
d = unit_done_count(u);
|
||||
out->done_ex += d;
|
||||
out->total_ex += u->n_ex;
|
||||
out->open_units++;
|
||||
if (d == u->n_ex)
|
||||
out->done_units++;
|
||||
}
|
||||
}
|
||||
|
||||
/* GTK gives the progress node a minimum width, so a fraction of zero would
|
||||
* still paint a short stub of accent colour. Mark those bars so the
|
||||
* stylesheet can hide the fill entirely. */
|
||||
static void stats_set_fraction(GtkWidget *bar, double fraction) {
|
||||
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), fraction);
|
||||
if (fraction > 0.0)
|
||||
gtk_widget_remove_css_class(bar, "empty");
|
||||
else
|
||||
gtk_widget_add_css_class(bar, "empty");
|
||||
}
|
||||
|
||||
void refresh_stats_ui(void) {
|
||||
ProgressSum all = {0};
|
||||
int pct;
|
||||
char *tmp;
|
||||
|
||||
if (!stats_ui.ex_value)
|
||||
return;
|
||||
|
||||
for (int s = 0; s < NUM_SUBJECTS; s++) {
|
||||
if (s == NET_SUBJ)
|
||||
progress_for_net(&all);
|
||||
else if (s == HW_SUBJ)
|
||||
progress_for_hw(&all);
|
||||
else if (s == CZ_SUBJ)
|
||||
progress_for_mluvnice(&all);
|
||||
else
|
||||
progress_for_units(sub_unit_start[s], sub_unit_count[s], &all);
|
||||
}
|
||||
|
||||
pct = all.total_ex > 0 ? (all.done_ex * 100) / all.total_ex : 0;
|
||||
|
||||
tmp = g_strdup_printf(tr("stats_ex_fmt"), all.done_ex, all.total_ex);
|
||||
gtk_label_set_text(GTK_LABEL(stats_ui.ex_value), tmp);
|
||||
g_free(tmp);
|
||||
|
||||
tmp = g_strdup_printf(tr("stats_pct_fmt"), pct);
|
||||
gtk_label_set_text(GTK_LABEL(stats_ui.pct_value), tmp);
|
||||
g_free(tmp);
|
||||
|
||||
tmp = g_strdup_printf(tr("stats_units_fmt"), all.done_units,
|
||||
all.open_units);
|
||||
gtk_label_set_text(GTK_LABEL(stats_ui.units_value), tmp);
|
||||
g_free(tmp);
|
||||
|
||||
stats_set_fraction(stats_ui.overall_bar,
|
||||
all.total_ex > 0
|
||||
? (double)all.done_ex / (double)all.total_ex
|
||||
: 0.0);
|
||||
|
||||
for (int s = 0; s < NUM_SUBJECTS; s++) {
|
||||
GtkWidget *count = stats_ui.subj[s].count;
|
||||
GtkWidget *bar = stats_ui.subj[s].bar;
|
||||
ProgressSum sp = {0};
|
||||
gboolean has_content = (s == NET_SUBJ) || (s == HW_SUBJ)
|
||||
|| (s == CZ_SUBJ) || sub_unit_count[s] > 0;
|
||||
|
||||
if (!count || !bar)
|
||||
continue;
|
||||
if (s == NET_SUBJ)
|
||||
progress_for_net(&sp);
|
||||
else if (s == HW_SUBJ)
|
||||
progress_for_hw(&sp);
|
||||
else if (s == CZ_SUBJ)
|
||||
progress_for_mluvnice(&sp);
|
||||
else
|
||||
progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp);
|
||||
if (has_content) {
|
||||
tmp = g_strdup_printf(tr("stats_ex_fmt"), sp.done_ex, sp.total_ex);
|
||||
gtk_label_set_text(GTK_LABEL(count), tmp);
|
||||
g_free(tmp);
|
||||
stats_set_fraction(bar,
|
||||
sp.total_ex > 0
|
||||
? (double)sp.done_ex / (double)sp.total_ex
|
||||
: 0.0);
|
||||
} else {
|
||||
gtk_label_set_text(GTK_LABEL(count), tr("stats_locked"));
|
||||
stats_set_fraction(bar, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < NUM_UNITS; i++) {
|
||||
UnitCtx *u = &units[i];
|
||||
GtkWidget *count = stats_ui.unit_count[i];
|
||||
GtkWidget *badge = stats_ui.unit_badge[i];
|
||||
GtkWidget *bar = stats_ui.unit_bar[i];
|
||||
GtkWidget *row = stats_ui.unit_row[i];
|
||||
gboolean unlocked = u->unlocked && u->n_ex > 0;
|
||||
int d = unlocked ? unit_done_count(u) : 0;
|
||||
gboolean complete = unlocked && d == u->n_ex;
|
||||
|
||||
if (!count || !bar || !row)
|
||||
continue;
|
||||
|
||||
gtk_widget_remove_css_class(row, "done");
|
||||
gtk_widget_remove_css_class(row, "locked");
|
||||
gtk_widget_remove_css_class(bar, "done");
|
||||
|
||||
if (!unlocked) {
|
||||
gtk_widget_add_css_class(row, "locked");
|
||||
gtk_label_set_text(GTK_LABEL(count), tr("stats_locked"));
|
||||
stats_set_fraction(bar, 0.0);
|
||||
if (badge)
|
||||
gtk_widget_set_visible(badge, FALSE);
|
||||
continue;
|
||||
}
|
||||
|
||||
tmp = g_strdup_printf(tr("stats_ex_fmt"), d, u->n_ex);
|
||||
gtk_label_set_text(GTK_LABEL(count), tmp);
|
||||
g_free(tmp);
|
||||
stats_set_fraction(bar,
|
||||
u->n_ex > 0 ? (double)d / (double)u->n_ex : 0.0);
|
||||
|
||||
if (complete) {
|
||||
gtk_widget_add_css_class(row, "done");
|
||||
gtk_widget_add_css_class(bar, "done");
|
||||
if (badge) {
|
||||
gtk_label_set_text(GTK_LABEL(badge), tr("stats_complete"));
|
||||
gtk_widget_set_visible(badge, TRUE);
|
||||
}
|
||||
} else if (badge) {
|
||||
gtk_widget_set_visible(badge, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void on_stats_back_clicked(GtkButton *button, gpointer user_data) {
|
||||
const char *target = stats_return_page;
|
||||
|
||||
(void)button;
|
||||
(void)user_data;
|
||||
if (!target || !target[0])
|
||||
target = "subjects";
|
||||
gtk_stack_set_visible_child_name(main_stack, target);
|
||||
}
|
||||
|
||||
void on_stats_clicked(GtkButton *button, gpointer user_data) {
|
||||
const char *cur;
|
||||
|
||||
(void)button;
|
||||
(void)user_data;
|
||||
|
||||
cur = gtk_stack_get_visible_child_name(main_stack);
|
||||
if (cur && g_strcmp0(cur, "stats") != 0) {
|
||||
g_free(stats_return_page);
|
||||
stats_return_page = g_strdup(cur);
|
||||
}
|
||||
refresh_stats_ui();
|
||||
gtk_stack_set_visible_child_name(main_stack, "stats");
|
||||
}
|
||||
|
||||
GtkWidget *stats_metric_card(const char *label_key, GtkWidget **value_out) {
|
||||
GtkWidget *card;
|
||||
GtkWidget *value;
|
||||
GtkWidget *label;
|
||||
|
||||
card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_add_css_class(card, "stats-metric");
|
||||
gtk_widget_set_hexpand(card, TRUE);
|
||||
|
||||
value = gtk_label_new("0");
|
||||
gtk_widget_set_halign(value, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(value, "stats-metric-value");
|
||||
gtk_box_append(GTK_BOX(card), value);
|
||||
|
||||
label = gtk_label_new(NULL);
|
||||
i18n_bind(label, label_key, 0);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(label, "stats-metric-label");
|
||||
gtk_box_append(GTK_BOX(card), label);
|
||||
|
||||
*value_out = value;
|
||||
return card;
|
||||
}
|
||||
|
||||
/* Build one unit row (index, name, badge, count, bar) for the stats list.
|
||||
* `num` is the row number shown (1-based within its subject). */
|
||||
GtkWidget *make_unit_stats_row(UnitCtx *u, int num, int global) {
|
||||
GtkWidget *row;
|
||||
GtkWidget *head;
|
||||
GtkWidget *left;
|
||||
GtkWidget *idx;
|
||||
GtkWidget *name;
|
||||
GtkWidget *right;
|
||||
GtkWidget *count;
|
||||
GtkWidget *badge;
|
||||
GtkWidget *bar;
|
||||
char text[8];
|
||||
|
||||
row = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_add_css_class(row, "stats-unit-row");
|
||||
stats_ui.unit_row[global] = row;
|
||||
|
||||
head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
gtk_box_append(GTK_BOX(row), head);
|
||||
|
||||
left = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_hexpand(left, TRUE);
|
||||
gtk_box_append(GTK_BOX(head), left);
|
||||
|
||||
g_snprintf(text, sizeof(text), "%d", num);
|
||||
idx = gtk_label_new(text);
|
||||
gtk_widget_add_css_class(idx, "stats-unit-index");
|
||||
gtk_widget_set_valign(idx, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(left), idx);
|
||||
|
||||
name = gtk_label_new(u->title);
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(name, TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(name), 0.0);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
|
||||
gtk_widget_add_css_class(name, "stats-unit-name");
|
||||
gtk_box_append(GTK_BOX(left), name);
|
||||
|
||||
right = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_valign(right, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(head), right);
|
||||
|
||||
badge = gtk_label_new(NULL);
|
||||
gtk_widget_add_css_class(badge, "stats-unit-badge");
|
||||
gtk_widget_set_visible(badge, FALSE);
|
||||
gtk_box_append(GTK_BOX(right), badge);
|
||||
stats_ui.unit_badge[global] = badge;
|
||||
|
||||
count = gtk_label_new("");
|
||||
gtk_widget_add_css_class(count, "stats-unit-count");
|
||||
gtk_box_append(GTK_BOX(right), count);
|
||||
stats_ui.unit_count[global] = count;
|
||||
|
||||
bar = gtk_progress_bar_new();
|
||||
gtk_widget_add_css_class(bar, "stats-unit-bar");
|
||||
gtk_widget_set_hexpand(bar, TRUE);
|
||||
gtk_box_append(GTK_BOX(row), bar);
|
||||
stats_ui.unit_bar[global] = bar;
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
GtkWidget *build_stats_page(void) {
|
||||
GtkWidget *outer;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *page;
|
||||
GtkWidget *top;
|
||||
GtkWidget *back;
|
||||
GtkWidget *center;
|
||||
GtkWidget *heading;
|
||||
GtkWidget *sub;
|
||||
GtkWidget *summary;
|
||||
GtkWidget *section;
|
||||
GtkWidget *list;
|
||||
|
||||
outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_add_css_class(outer, "stats-page");
|
||||
|
||||
top = gtk_center_box_new();
|
||||
gtk_widget_set_margin_top(top, 2);
|
||||
gtk_widget_set_margin_bottom(top, 6);
|
||||
gtk_box_append(GTK_BOX(outer), top);
|
||||
|
||||
back = make_back_button("subjects");
|
||||
g_signal_handlers_disconnect_matched(back, G_SIGNAL_MATCH_FUNC,
|
||||
0, 0, NULL, on_nav_clicked, NULL);
|
||||
g_signal_connect(back, "clicked", G_CALLBACK(on_stats_back_clicked), NULL);
|
||||
gtk_center_box_set_start_widget(GTK_CENTER_BOX(top), back);
|
||||
|
||||
center = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_widget_set_valign(center, GTK_ALIGN_CENTER);
|
||||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(top), center);
|
||||
|
||||
heading = gtk_label_new(NULL);
|
||||
i18n_bind(heading, "stats_title", 0);
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(heading, "roadmap-title");
|
||||
gtk_box_append(GTK_BOX(center), heading);
|
||||
|
||||
sub = gtk_label_new(NULL);
|
||||
i18n_bind(sub, "stats_sub", 0);
|
||||
gtk_widget_set_halign(sub, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(sub, "roadmap-sub");
|
||||
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
|
||||
gtk_box_append(GTK_BOX(center), sub);
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_hexpand(scroll, TRUE);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_box_append(GTK_BOX(outer), scroll);
|
||||
|
||||
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_halign(page, GTK_ALIGN_FILL);
|
||||
gtk_widget_set_margin_start(page, 4);
|
||||
gtk_widget_set_margin_end(page, 4);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), page);
|
||||
|
||||
summary = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
gtk_widget_add_css_class(summary, "stats-summary");
|
||||
gtk_widget_set_halign(summary, GTK_ALIGN_FILL);
|
||||
/* Equal columns; hexpand alone only shares the leftover space, which
|
||||
* leaves the cards at different widths. */
|
||||
gtk_box_set_homogeneous(GTK_BOX(summary), TRUE);
|
||||
gtk_box_append(GTK_BOX(page), summary);
|
||||
|
||||
gtk_box_append(GTK_BOX(summary),
|
||||
stats_metric_card("stats_ex_label", &stats_ui.ex_value));
|
||||
gtk_box_append(GTK_BOX(summary),
|
||||
stats_metric_card("stats_pct_label", &stats_ui.pct_value));
|
||||
gtk_box_append(GTK_BOX(summary),
|
||||
stats_metric_card("stats_units_label",
|
||||
&stats_ui.units_value));
|
||||
|
||||
stats_ui.overall_bar = gtk_progress_bar_new();
|
||||
gtk_widget_add_css_class(stats_ui.overall_bar, "stats-overall-bar");
|
||||
gtk_widget_set_hexpand(stats_ui.overall_bar, TRUE);
|
||||
gtk_box_append(GTK_BOX(page), stats_ui.overall_bar);
|
||||
|
||||
section = gtk_label_new(NULL);
|
||||
i18n_bind(section, "stats_section", 0);
|
||||
gtk_widget_set_halign(section, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(section, "stats-section");
|
||||
gtk_box_append(GTK_BOX(page), section);
|
||||
|
||||
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_box_append(GTK_BOX(page), list);
|
||||
|
||||
for (int s = 0; s < NUM_SUBJECTS; s++) {
|
||||
GtkWidget *subject;
|
||||
GtkWidget *head;
|
||||
GtkWidget *name;
|
||||
GtkWidget *count;
|
||||
GtkWidget *bar;
|
||||
gboolean has_units = (s == NET_SUBJ) || (s == HW_SUBJ)
|
||||
|| (s == CZ_SUBJ) || sub_unit_count[s] > 0;
|
||||
gboolean has_deutsch_units = sub_unit_count[s] > 0;
|
||||
|
||||
subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_add_css_class(subject, "stats-subject");
|
||||
gtk_box_append(GTK_BOX(list), subject);
|
||||
|
||||
head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
gtk_box_append(GTK_BOX(subject), head);
|
||||
|
||||
name = gtk_label_new(NULL);
|
||||
i18n_bind(name, sub_keys[s], 0);
|
||||
gtk_widget_set_halign(name, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(name, TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(name), 0.0);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(name), PANGO_ELLIPSIZE_END);
|
||||
gtk_widget_add_css_class(name, "stats-subject-name");
|
||||
if (!has_units)
|
||||
gtk_widget_add_css_class(name, "unit-name-locked");
|
||||
gtk_box_append(GTK_BOX(head), name);
|
||||
stats_ui.subj[s].name = name;
|
||||
|
||||
count = gtk_label_new(NULL);
|
||||
gtk_widget_add_css_class(count, "stats-subject-count");
|
||||
if (!has_units)
|
||||
gtk_widget_add_css_class(count, "locked");
|
||||
gtk_box_append(GTK_BOX(head), count);
|
||||
stats_ui.subj[s].count = count;
|
||||
|
||||
bar = gtk_progress_bar_new();
|
||||
gtk_widget_add_css_class(bar, "stats-subject-bar");
|
||||
gtk_widget_set_hexpand(bar, TRUE);
|
||||
gtk_box_append(GTK_BOX(subject), bar);
|
||||
stats_ui.subj[s].bar = bar;
|
||||
|
||||
if (has_deutsch_units) {
|
||||
GtkWidget *units_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
gtk_widget_add_css_class(units_box, "stats-units");
|
||||
gtk_box_append(GTK_BOX(subject), units_box);
|
||||
stats_ui.subj[s].units_box = units_box;
|
||||
|
||||
for (int j = 0; j < sub_unit_count[s]; j++) {
|
||||
int gi = sub_unit_start[s] + j;
|
||||
|
||||
gtk_box_append(GTK_BOX(units_box),
|
||||
make_unit_stats_row(&units[gi], j + 1, gi));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return outer;
|
||||
}
|
||||
424
src/subjects.c
Normal file
424
src/subjects.c
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Subjects page ("Předměty") - serpentine subject map */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
|
||||
/* Index 0 (Deutsch), NET_SUBJ (networks), HW_SUBJ (hardware) and CZ_SUBJ
|
||||
* (Czech) are unlocked. */
|
||||
const char *sub_keys[NUM_SUBJECTS] = {
|
||||
"Deutsch",
|
||||
"Správa počítačových sítí",
|
||||
"Technické vybavení",
|
||||
"Český jazyk a literatura",
|
||||
"Občanská nauka",
|
||||
"English",
|
||||
"Matematika",
|
||||
"Fyzika",
|
||||
"Základy Přírodopisných věd",
|
||||
"Technická grafika",
|
||||
"Prezentační grafika",
|
||||
"Programování",
|
||||
"Programové vybavení",
|
||||
};
|
||||
|
||||
/* Slice of the global `units[]` table that each subject owns. A start of -1
|
||||
* (or a count of 0) means the subject has no content yet; such subjects show
|
||||
* as locked on the statistics page until their units are added. */
|
||||
const int sub_unit_start[NUM_SUBJECTS] = {
|
||||
0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
||||
};
|
||||
const int sub_unit_count[NUM_SUBJECTS] = {
|
||||
NUM_UNITS, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
};
|
||||
|
||||
GtkWidget *sub_scroll;
|
||||
GtkWidget *sub_fixed;
|
||||
GtkWidget *sub_rail;
|
||||
GtkWidget *sub_cells[NUM_SUBJECTS];
|
||||
GtkWidget *sub_labels[NUM_SUBJECTS];
|
||||
double sub_cx[NUM_SUBJECTS];
|
||||
double sub_cy[NUM_SUBJECTS];
|
||||
int sub_rows = 1;
|
||||
int sub_cols = NUM_SUBJECTS;
|
||||
int sub_cw = (int)(2.0 * SUB_MX + (NUM_SUBJECTS - 1) * SUB_SPAC);
|
||||
int sub_ch = (int)(2.0 * SUB_MY);
|
||||
guint sub_idle;
|
||||
|
||||
void sub_layout_geometry(double avail) {
|
||||
const double phase = 2.0 * G_PI * 1.7 / (double)(NUM_SUBJECTS - 1);
|
||||
int rows;
|
||||
int r, c, i;
|
||||
|
||||
for (rows = 1; rows <= NUM_SUBJECTS; rows++) {
|
||||
int cols = (NUM_SUBJECTS + rows - 1) / rows;
|
||||
if (2.0 * SUB_MX + (cols - 1) * SUB_SPAC <= avail + 1.0)
|
||||
break;
|
||||
}
|
||||
if (rows > NUM_SUBJECTS)
|
||||
rows = NUM_SUBJECTS;
|
||||
sub_rows = rows;
|
||||
sub_cols = (NUM_SUBJECTS + rows - 1) / rows;
|
||||
if (sub_cols < 1)
|
||||
sub_cols = 1;
|
||||
sub_cw = (int)(2.0 * SUB_MX + (sub_cols - 1) * SUB_SPAC);
|
||||
sub_ch = (int)(2.0 * SUB_MY + (rows - 1) * SUB_GAP);
|
||||
|
||||
for (r = 0; r < rows; r++) {
|
||||
int base = r * sub_cols;
|
||||
int len = MIN(sub_cols, NUM_SUBJECTS - base);
|
||||
int fwd = (r % 2) == 0;
|
||||
for (c = 0; c < len; c++) {
|
||||
i = base + c;
|
||||
int cc = fwd ? c : (sub_cols - 1 - c);
|
||||
sub_cx[i] = SUB_MX + cc * SUB_SPAC;
|
||||
sub_cy[i] = SUB_MY + r * SUB_GAP
|
||||
+ SUB_WAVE * sin((double)i * phase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sub_point(double t, double *ox, double *oy) {
|
||||
int k = (int)t;
|
||||
double u = t - (double)k;
|
||||
double u2, u3;
|
||||
double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
|
||||
|
||||
if (k < 0) { k = 0; u = 0.0; }
|
||||
if (k >= NUM_SUBJECTS - 1) { k = NUM_SUBJECTS - 2; u = 1.0; }
|
||||
|
||||
p1x = sub_cx[k]; p1y = sub_cy[k];
|
||||
p2x = sub_cx[k + 1]; p2y = sub_cy[k + 1];
|
||||
if (k - 1 >= 0) { p0x = sub_cx[k - 1]; p0y = sub_cy[k - 1]; }
|
||||
else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
|
||||
if (k + 2 < NUM_SUBJECTS) { p3x = sub_cx[k + 2]; p3y = sub_cy[k + 2]; }
|
||||
else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
|
||||
|
||||
u2 = u * u;
|
||||
u3 = u2 * u;
|
||||
*ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
|
||||
+ (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
|
||||
+ (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
|
||||
*oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
|
||||
+ (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
|
||||
+ (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
|
||||
}
|
||||
|
||||
void sub_path(cairo_t *cr, double t0, double t1) {
|
||||
const double steps_per_unit = 48.0;
|
||||
int n = (int)((t1 - t0) * steps_per_unit);
|
||||
double x, y;
|
||||
int s;
|
||||
|
||||
if (n < 1)
|
||||
n = 1;
|
||||
sub_point(t0, &x, &y);
|
||||
cairo_move_to(cr, x, y);
|
||||
for (s = 1; s <= n; s++) {
|
||||
sub_point(t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
|
||||
cairo_line_to(cr, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_sub_rail(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer user_data) {
|
||||
const double t_end = (double)(NUM_SUBJECTS - 1);
|
||||
const Rgb rail = color_from_hex(app_theme.rail);
|
||||
|
||||
(void)area;
|
||||
(void)width;
|
||||
(void)height;
|
||||
(void)user_data;
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
|
||||
cairo_new_path(cr);
|
||||
sub_path(cr, 0.0, t_end);
|
||||
cairo_set_line_width(cr, 12);
|
||||
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
void sub_apply_layout(void) {
|
||||
int i;
|
||||
|
||||
if (!sub_fixed)
|
||||
return;
|
||||
|
||||
for (i = 0; i < NUM_SUBJECTS; i++) {
|
||||
if (sub_cells[i])
|
||||
gtk_fixed_move(GTK_FIXED(sub_fixed), sub_cells[i],
|
||||
(int)(sub_cx[i] - SUB_BUBBLE / 2.0),
|
||||
(int)(sub_cy[i] - SUB_BUBBLE / 2.0));
|
||||
if (sub_labels[i])
|
||||
gtk_fixed_move(GTK_FIXED(sub_fixed), sub_labels[i],
|
||||
(int)(sub_cx[i] - (SUB_SPAC - 24.0) / 2.0),
|
||||
(int)(sub_cy[i] + SUB_BUBBLE / 2.0 + 10.0));
|
||||
}
|
||||
|
||||
gtk_widget_set_size_request(sub_fixed, sub_cw, sub_ch);
|
||||
gtk_widget_set_size_request(sub_rail, sub_cw, sub_ch);
|
||||
gtk_fixed_move(GTK_FIXED(sub_fixed), sub_rail, 0, 0);
|
||||
gtk_widget_queue_draw(sub_rail);
|
||||
}
|
||||
|
||||
void sub_relayout(void) {
|
||||
GtkAdjustment *hadj;
|
||||
double avail;
|
||||
|
||||
if (!sub_scroll)
|
||||
return;
|
||||
hadj = gtk_scrolled_window_get_hadjustment(
|
||||
GTK_SCROLLED_WINDOW(sub_scroll));
|
||||
avail = gtk_adjustment_get_page_size(hadj);
|
||||
if (avail < 1.0)
|
||||
return;
|
||||
sub_layout_geometry(avail);
|
||||
sub_apply_layout();
|
||||
}
|
||||
|
||||
gboolean sub_relayout_idle(gpointer data) {
|
||||
(void)data;
|
||||
sub_idle = 0;
|
||||
sub_relayout();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void sub_relayout_later(void) {
|
||||
if (sub_idle == 0)
|
||||
sub_idle = g_idle_add(sub_relayout_idle, NULL);
|
||||
}
|
||||
|
||||
void sub_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
|
||||
gpointer data) {
|
||||
(void)adj;
|
||||
(void)ps;
|
||||
(void)data;
|
||||
sub_relayout_later();
|
||||
}
|
||||
|
||||
/* German flag (black / red / gold) drawn in a clipped circle. */
|
||||
void draw_german_flag(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
double cx = width / 2.0;
|
||||
double cy = height / 2.0;
|
||||
double r = MIN(width, height) / 2.0 - 2.0;
|
||||
double band = (double)height / 3.0;
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||
cairo_clip(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_rectangle(cr, 0.0, 0.0, (double)width, band);
|
||||
cairo_set_source_rgb(cr, 0.18, 0.18, 0.19);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_rectangle(cr, 0.0, band, (double)width, band);
|
||||
cairo_set_source_rgb(cr, 0.72, 0.27, 0.24);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_rectangle(cr, 0.0, 2.0 * band, (double)width, band);
|
||||
cairo_set_source_rgb(cr, 0.84, 0.70, 0.32);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
/* Czech flag (white / red with a blue triangle) drawn in a clipped circle. */
|
||||
void draw_czech_flag(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
double cx = width / 2.0;
|
||||
double cy = height / 2.0;
|
||||
double r = MIN(width, height) / 2.0 - 2.0;
|
||||
|
||||
(void)area;
|
||||
(void)data;
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
|
||||
cairo_save(cr);
|
||||
cairo_new_path(cr);
|
||||
cairo_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||||
cairo_clip(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_rectangle(cr, 0.0, 0.0, (double)width, (double)height);
|
||||
cairo_set_source_rgb(cr, 0.98, 0.98, 0.98);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_rectangle(cr, 0.0, (double)height / 2.0,
|
||||
(double)width, (double)height / 2.0);
|
||||
cairo_set_source_rgb(cr, 0.83, 0.16, 0.18);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
cairo_move_to(cr, 0.0, 0.0);
|
||||
cairo_line_to(cr, (double)width * 0.55, (double)height / 2.0);
|
||||
cairo_line_to(cr, 0.0, (double)height);
|
||||
cairo_close_path(cr);
|
||||
cairo_set_source_rgb(cr, 0.07, 0.24, 0.55);
|
||||
cairo_fill(cr);
|
||||
|
||||
cairo_restore(cr);
|
||||
}
|
||||
|
||||
GtkWidget *build_subjects_page(void) {
|
||||
GtkWidget *page;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *wrap;
|
||||
GtkWidget *fixed;
|
||||
GtkWidget *rail;
|
||||
GtkAdjustment *ha;
|
||||
GtkAdjustment *va;
|
||||
int i;
|
||||
|
||||
sub_layout_geometry(1000.0);
|
||||
|
||||
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_hexpand(page, TRUE);
|
||||
gtk_widget_set_vexpand(page, TRUE);
|
||||
gtk_widget_set_margin_start(page, 32);
|
||||
gtk_widget_set_margin_end(page, 32);
|
||||
gtk_widget_set_margin_top(page, 24);
|
||||
gtk_widget_set_margin_bottom(page, 24);
|
||||
|
||||
gtk_box_append(GTK_BOX(page),
|
||||
top_bar("welcome", "subjects_title", NULL));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 14);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
sub_scroll = scroll;
|
||||
|
||||
wrap = gtk_center_box_new();
|
||||
gtk_widget_set_vexpand(wrap, TRUE);
|
||||
gtk_widget_set_hexpand(wrap, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
|
||||
|
||||
fixed = gtk_fixed_new();
|
||||
gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_size_request(fixed, sub_cw, sub_ch);
|
||||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
|
||||
sub_fixed = fixed;
|
||||
|
||||
rail = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(rail, sub_cw, sub_ch);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_sub_rail,
|
||||
NULL, NULL);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
|
||||
sub_rail = rail;
|
||||
|
||||
for (i = 0; i < NUM_SUBJECTS; i++) {
|
||||
GtkWidget *card;
|
||||
GtkWidget *lbl;
|
||||
|
||||
card = gtk_button_new();
|
||||
gtk_widget_add_css_class(card, "unit-node");
|
||||
gtk_widget_set_can_focus(card, FALSE);
|
||||
gtk_widget_set_size_request(card, (int)SUB_BUBBLE, (int)SUB_BUBBLE);
|
||||
|
||||
if (i == 0) {
|
||||
GtkWidget *flag = gtk_drawing_area_new();
|
||||
gtk_widget_add_css_class(card, "current");
|
||||
gtk_widget_set_size_request(flag, (int)SUB_BUBBLE,
|
||||
(int)SUB_BUBBLE);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(flag),
|
||||
draw_german_flag, NULL, NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(card), flag);
|
||||
g_object_set_data_full(G_OBJECT(card), "target",
|
||||
g_strdup("roadmap"), g_free);
|
||||
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
} else if (i == NET_SUBJ) {
|
||||
GtkWidget *wifi = gtk_drawing_area_new();
|
||||
gtk_widget_add_css_class(card, "current");
|
||||
gtk_widget_set_size_request(wifi, (int)SUB_BUBBLE,
|
||||
(int)SUB_BUBBLE);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(wifi),
|
||||
draw_wifi_icon, NULL, NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(card), wifi);
|
||||
g_object_set_data_full(G_OBJECT(card), "target",
|
||||
g_strdup("netyears"), g_free);
|
||||
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
} else if (i == HW_SUBJ) {
|
||||
GtkWidget *chip = gtk_drawing_area_new();
|
||||
gtk_widget_add_css_class(card, "current");
|
||||
gtk_widget_set_size_request(chip, (int)SUB_BUBBLE,
|
||||
(int)SUB_BUBBLE);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(chip),
|
||||
draw_chip_icon, NULL, NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(card), chip);
|
||||
g_object_set_data_full(G_OBJECT(card), "target",
|
||||
g_strdup("hwmap"), g_free);
|
||||
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
} else if (i == CZ_SUBJ) {
|
||||
GtkWidget *flag = gtk_drawing_area_new();
|
||||
gtk_widget_add_css_class(card, "current");
|
||||
gtk_widget_set_size_request(flag, (int)SUB_BUBBLE,
|
||||
(int)SUB_BUBBLE);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(flag),
|
||||
draw_czech_flag, NULL, NULL);
|
||||
gtk_button_set_child(GTK_BUTTON(card), flag);
|
||||
g_object_set_data_full(G_OBJECT(card), "target",
|
||||
g_strdup("czechmap"), g_free);
|
||||
g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
} else {
|
||||
GtkWidget *lock;
|
||||
gtk_widget_add_css_class(card, "locked");
|
||||
gtk_widget_set_sensitive(card, FALSE);
|
||||
lock = icon_area_new(draw_lock_icon,
|
||||
0.3451, 0.3569, 0.4392, 26);
|
||||
gtk_button_set_child(GTK_BUTTON(card), lock);
|
||||
}
|
||||
|
||||
sub_cells[i] = card;
|
||||
gtk_fixed_put(GTK_FIXED(fixed), card, 0, 0);
|
||||
|
||||
lbl = gtk_label_new(NULL);
|
||||
i18n_bind(lbl, sub_keys[i], 0);
|
||||
gtk_widget_set_size_request(lbl, (int)(SUB_SPAC - 24.0), -1);
|
||||
gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER);
|
||||
gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER);
|
||||
gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
|
||||
gtk_widget_add_css_class(lbl, "unit-name");
|
||||
if (i != 0 && i != NET_SUBJ && i != HW_SUBJ && i != CZ_SUBJ)
|
||||
gtk_widget_add_css_class(lbl, "unit-name-locked");
|
||||
sub_labels[i] = lbl;
|
||||
gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0);
|
||||
}
|
||||
|
||||
ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
g_signal_connect(ha, "notify::page-size",
|
||||
G_CALLBACK(sub_adjust_notify), NULL);
|
||||
g_signal_connect(va, "notify::page-size",
|
||||
G_CALLBACK(sub_adjust_notify), NULL);
|
||||
|
||||
sub_apply_layout();
|
||||
sub_relayout_later();
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
424
src/theme.c
Normal file
424
src/theme.c
Normal file
|
|
@ -0,0 +1,424 @@
|
|||
#include "maturita.h"
|
||||
|
||||
Rgb color_from_hex(unsigned int hex) {
|
||||
Rgb c;
|
||||
c.r = (double)((hex >> 16) & 0xFF) / 255.0;
|
||||
c.g = (double)((hex >> 8) & 0xFF) / 255.0;
|
||||
c.b = (double)(hex & 0xFF) / 255.0;
|
||||
return c;
|
||||
}
|
||||
|
||||
/* Linear blend of two colours: t = 0 keeps a, t = 1 returns b. */
|
||||
Rgb mix_rgb(Rgb a, Rgb b, double t) {
|
||||
Rgb c;
|
||||
c.r = a.r + (b.r - a.r) * t;
|
||||
c.g = a.g + (b.g - a.g) * t;
|
||||
c.b = a.b + (b.b - a.b) * t;
|
||||
return c;
|
||||
}
|
||||
|
||||
ThemePalette theme_palette(ThemeId id, ColorMode mode) {
|
||||
/* Catppuccin Mocha / Latte */
|
||||
static const ThemePalette cat_dark = {
|
||||
0x11111b, 0x1e1e2e, 0x181825, 0x313244, 0x45475a, 0x585b70,
|
||||
0x6c7086, 0xcdd6f4, 0xa6adc8, 0xcba6f7, 0xb4befe, 0x89b4fa,
|
||||
0xa6e3a1, 0x94e2d5, 0xf9e2af, 0xf38ba8, 0x1e1e2e,
|
||||
0x2b2d40, 0x24263a, 0x383b52, 0x27293c, 0x1f202e, 0x3b3e58, 0x52556b
|
||||
};
|
||||
static const ThemePalette cat_light = {
|
||||
0xdce0e8, 0xeff1f5, 0xe6e9ef, 0xccd0da, 0xbcc0cc, 0xacb0be,
|
||||
0x9ca0b0, 0x4c4f69, 0x6c6f85, 0x8839ef, 0x7287fd, 0x1e66f5,
|
||||
0x40a02b, 0x179299, 0xdf8e1d, 0xd20f39, 0xeff1f5,
|
||||
0xe6e9ef, 0xdce0e8, 0xbcc0cc, 0xccd0da, 0xdce0e8, 0xbcc0cc, 0x9ca0b0
|
||||
};
|
||||
/* Nord */
|
||||
static const ThemePalette nord_dark = {
|
||||
0x2e3440, 0x3b4252, 0x2e3440, 0x434c5e, 0x4c566a, 0x616e88,
|
||||
0x7b88a1, 0xeceff4, 0xd8dee9, 0x88c0d0, 0x81a1c1, 0x5e81ac,
|
||||
0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0x2e3440,
|
||||
0x434c5e, 0x3b4252, 0x4c566a, 0x2e3440, 0x2e3440, 0x434c5e, 0x4c566a
|
||||
};
|
||||
static const ThemePalette nord_light = {
|
||||
0xd8dee9, 0xeceff4, 0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xb0b8c8,
|
||||
0x7b88a1, 0x2e3440, 0x4c566a, 0x5e81ac, 0x81a1c1, 0x5e81ac,
|
||||
0xa3be8c, 0x8fbcbb, 0xebcb8b, 0xbf616a, 0xeceff4,
|
||||
0xe5e9f0, 0xd8dee9, 0xc7ceda, 0xd8dee9, 0xd8dee9, 0xc7ceda, 0x7b88a1
|
||||
};
|
||||
/* Dracula / light soft */
|
||||
static const ThemePalette dra_dark = {
|
||||
0x191a21, 0x282a36, 0x21222c, 0x44475a, 0x6272a4, 0x707eb0,
|
||||
0x6272a4, 0xf8f8f2, 0xbfbfb2, 0xbd93f9, 0xff79c6, 0x8be9fd,
|
||||
0x50fa7b, 0x8be9fd, 0xf1fa8c, 0xff5555, 0x282a36,
|
||||
0x383a4a, 0x2f3240, 0x44475a, 0x21222c, 0x191a21, 0x383a4a, 0x6272a4
|
||||
};
|
||||
static const ThemePalette dra_light = {
|
||||
0xd5d7e0, 0xf8f8f2, 0xefefea, 0xe2e2d8, 0xd0d0c4, 0xbfbfb2,
|
||||
0x9a9a8c, 0x282a36, 0x44475a, 0x7c3aed, 0xd946a6, 0x0891b2,
|
||||
0x16a34a, 0x0e7490, 0xca8a04, 0xdc2626, 0xf8f8f2,
|
||||
0xefefea, 0xe2e2d8, 0xd0d0c4, 0xe2e2d8, 0xd5d7e0, 0xd0d0c4, 0x9a9a8c
|
||||
};
|
||||
/* Rose Pine / Dawn */
|
||||
static const ThemePalette rose_dark = {
|
||||
0x191724, 0x1f1d2e, 0x1f1d2e, 0x26233a, 0x403d52, 0x524f67,
|
||||
0x6e6a86, 0xe0def4, 0x908caa, 0xc4a7e7, 0xebbcba, 0x9ccfd8,
|
||||
0x31748f, 0x9ccfd8, 0xf6c177, 0xeb6f92, 0x191724,
|
||||
0x26233a, 0x21202e, 0x403d52, 0x191724, 0x191724, 0x26233a, 0x524f67
|
||||
};
|
||||
static const ThemePalette rose_light = {
|
||||
0xf2e9e1, 0xfaf4ed, 0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xcecacd,
|
||||
0x9893a5, 0x575279, 0x797593, 0x907aa9, 0xd7827e, 0x56949f,
|
||||
0x286983, 0x56949f, 0xea9d34, 0xb4637a, 0xfaf4ed,
|
||||
0xfffaf3, 0xf2e9e1, 0xdfdad9, 0xf2e9e1, 0xf2e9e1, 0xdfdad9, 0x9893a5
|
||||
};
|
||||
/* Ocean / Tokyo Night inspired */
|
||||
static const ThemePalette ocean_dark = {
|
||||
0x16161e, 0x1a1b26, 0x16161e, 0x24283b, 0x414868, 0x565f89,
|
||||
0x565f89, 0xc0caf5, 0xa9b1d6, 0x7aa2f7, 0xbb9af7, 0x7dcfff,
|
||||
0x9ece6a, 0x73daca, 0xe0af68, 0xf7768e, 0x1a1b26,
|
||||
0x24283b, 0x1f2335, 0x3b4261, 0x16161e, 0x16161e, 0x24283b, 0x414868
|
||||
};
|
||||
static const ThemePalette ocean_light = {
|
||||
0xd6dae8, 0xe1e2ec, 0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0x9aa0b5,
|
||||
0x7a8199, 0x2e3446, 0x4a5168, 0x2e7de9, 0x7847bd, 0x007197,
|
||||
0x387068, 0x007197, 0x8c6c3e, 0xc64343, 0xe1e2ec,
|
||||
0xd5d6e2, 0xc8c9d6, 0xb4b6c5, 0xc8c9d6, 0xd6dae8, 0xb4b6c5, 0x7a8199
|
||||
};
|
||||
|
||||
/* Gruvbox */
|
||||
static const ThemePalette gruv_dark = {
|
||||
0x1d2021, 0x282828, 0x1d2021, 0x3c3836, 0x504945, 0x665c54,
|
||||
0x928374, 0xebdbb2, 0xa89984, 0xfe8019, 0xd3869b, 0x83a598,
|
||||
0xb8bb26, 0x8ec07c, 0xfabd2f, 0xfb4934, 0x1d2021,
|
||||
0x3c3836, 0x32302f, 0x504945, 0x1d2021, 0x1d2021, 0x3c3836, 0x504945
|
||||
};
|
||||
static const ThemePalette gruv_light = {
|
||||
0xebdbb2, 0xfbf1c7, 0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xbdae93,
|
||||
0x928374, 0x3c3836, 0x504945, 0xd65d0e, 0xb16286, 0x076678,
|
||||
0x79740e, 0x427b58, 0xb57614, 0x9d0006, 0xfbf1c7,
|
||||
0xf2e5bc, 0xebdbb2, 0xd5c4a1, 0xebdbb2, 0xebdbb2, 0xd5c4a1, 0x928374
|
||||
};
|
||||
/* Solarized */
|
||||
static const ThemePalette sol_dark = {
|
||||
0x002b36, 0x073642, 0x002b36, 0x586e75, 0x657b83, 0x839496,
|
||||
0x93a1a1, 0xfdf6e3, 0x93a1a1, 0x268bd2, 0x6c71c4, 0x2aa198,
|
||||
0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0x002b36,
|
||||
0x073642, 0x002b36, 0x586e75, 0x002b36, 0x002b36, 0x073642, 0x586e75
|
||||
};
|
||||
static const ThemePalette sol_light = {
|
||||
0xeee8d5, 0xfdf6e3, 0xeee8d5, 0xeee8d5, 0xe2dbc6, 0xc9c0a6,
|
||||
0x657b83, 0x586e75, 0x657b83, 0x268bd2, 0x6c71c4, 0x2aa198,
|
||||
0x859900, 0x2aa198, 0xb58900, 0xdc322f, 0xfdf6e3,
|
||||
0xeee8d5, 0xe6dfc8, 0x93a1a1, 0xeee8d5, 0xeee8d5, 0x93a1a1, 0x586e75
|
||||
};
|
||||
/* Everforest */
|
||||
static const ThemePalette ever_dark = {
|
||||
0x1e2326, 0x272e33, 0x2d353b, 0x374145, 0x4f5b58, 0x7a8478,
|
||||
0x859289, 0xd3c6aa, 0xa7c080, 0xa7c080, 0xd699b6, 0x7fbbb3,
|
||||
0xa7c080, 0x83c092, 0xdbbc7f, 0xe67e80, 0x272e33,
|
||||
0x374145, 0x2d353b, 0x4f5b58, 0x1e2326, 0x1e2326, 0x374145, 0x4f5b58
|
||||
};
|
||||
static const ThemePalette ever_light = {
|
||||
0xe6e2cc, 0xfdf6e3, 0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xa6b0a0,
|
||||
0x939f91, 0x5c6a72, 0x829181, 0x8da101, 0xdf69ba, 0x35a77c,
|
||||
0x8da101, 0x35a77c, 0xdfa000, 0xf85552, 0xfdf6e3,
|
||||
0xf4f0d9, 0xefebd4, 0xe0dcc7, 0xe6e2cc, 0xe6e2cc, 0xe0dcc7, 0x939f91
|
||||
};
|
||||
/* Monokai */
|
||||
static const ThemePalette mono_dark = {
|
||||
0x1d1e19, 0x272822, 0x1e1f1a, 0x3e3d32, 0x49483e, 0x75715e,
|
||||
0x75715e, 0xf8f8f2, 0xa59f85, 0xf92672, 0xae81ff, 0x66d9ef,
|
||||
0xa6e22e, 0xa1efe4, 0xe6db74, 0xf92672, 0x272822,
|
||||
0x3e3d32, 0x2e2f29, 0x49483e, 0x1d1e19, 0x1d1e19, 0x3e3d32, 0x49483e
|
||||
};
|
||||
static const ThemePalette mono_light = {
|
||||
0xe6e3d4, 0xfaf8f0, 0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xb8b49e,
|
||||
0x8a8674, 0x2d2a2e, 0x5b5854, 0xe14775, 0x7c5cbf, 0x1c7d9b,
|
||||
0x5b8c2a, 0x1c7d9b, 0xb08900, 0xe14775, 0xfaf8f0,
|
||||
0xf3f0e4, 0xe8e4d4, 0xd6d2c0, 0xe6e3d4, 0xe6e3d4, 0xd6d2c0, 0x8a8674
|
||||
};
|
||||
/* One Dark / One Light */
|
||||
static const ThemePalette one_dark = {
|
||||
0x21252b, 0x282c34, 0x21252b, 0x3b4048, 0x4b5263, 0x5c6370,
|
||||
0x5c6370, 0xabb2bf, 0x7f848e, 0xc678dd, 0x61afef, 0x56b6c2,
|
||||
0x98c379, 0x56b6c2, 0xe5c07b, 0xe06c75, 0x282c34,
|
||||
0x3b4048, 0x2c313a, 0x4b5263, 0x21252b, 0x21252b, 0x3b4048, 0x4b5263
|
||||
};
|
||||
static const ThemePalette one_light = {
|
||||
0xe5e5e6, 0xfafafa, 0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7,
|
||||
0xa0a1a7, 0x383a42, 0x696c77, 0xa626a4, 0x4078f2, 0x0184bc,
|
||||
0x50a14f, 0x0184bc, 0xc18401, 0xe45649, 0xfafafa,
|
||||
0xf0f0f0, 0xe5e5e6, 0xd0d0d2, 0xe5e5e6, 0xe5e5e6, 0xd0d0d2, 0xa0a1a7
|
||||
};
|
||||
|
||||
switch (id) {
|
||||
case THEME_NORD:
|
||||
return mode == MODE_LIGHT ? nord_light : nord_dark;
|
||||
case THEME_DRACULA:
|
||||
return mode == MODE_LIGHT ? dra_light : dra_dark;
|
||||
case THEME_ROSE_PINE:
|
||||
return mode == MODE_LIGHT ? rose_light : rose_dark;
|
||||
case THEME_OCEAN:
|
||||
return mode == MODE_LIGHT ? ocean_light : ocean_dark;
|
||||
case THEME_GRUVBOX:
|
||||
return mode == MODE_LIGHT ? gruv_light : gruv_dark;
|
||||
case THEME_SOLARIZED:
|
||||
return mode == MODE_LIGHT ? sol_light : sol_dark;
|
||||
case THEME_EVERFOREST:
|
||||
return mode == MODE_LIGHT ? ever_light : ever_dark;
|
||||
case THEME_MONOKAI:
|
||||
return mode == MODE_LIGHT ? mono_light : mono_dark;
|
||||
case THEME_ONE_DARK:
|
||||
return mode == MODE_LIGHT ? one_light : one_dark;
|
||||
case THEME_CATPPUCCIN:
|
||||
default:
|
||||
return mode == MODE_LIGHT ? cat_light : cat_dark;
|
||||
}
|
||||
}
|
||||
char *read_css_file(void) {
|
||||
gchar *contents = NULL;
|
||||
GError *err = NULL;
|
||||
|
||||
if (!g_file_get_contents(CSS_FILE, &contents, NULL, &err)) {
|
||||
g_warning("Could not load stylesheet '%s': %s", CSS_FILE,
|
||||
err ? err->message : "unknown error");
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
return g_strdup("");
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
double scale;
|
||||
} FontScale;
|
||||
|
||||
static gboolean scale_font_size_eval(const GMatchInfo *info, GString *result,
|
||||
gpointer user_data) {
|
||||
const FontScale *fs = user_data;
|
||||
char *space = g_match_info_fetch(info, 1);
|
||||
char *num = g_match_info_fetch(info, 2);
|
||||
int px = (int)g_ascii_strtoll(num, NULL, 10);
|
||||
int scaled = (int)(px * fs->scale + 0.5);
|
||||
|
||||
g_string_append_printf(result, "font-size:%s%dpx", space, scaled);
|
||||
g_free(space);
|
||||
g_free(num);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Scale every explicit `font-size: Npx` declaration so the interface grows on
|
||||
* large (e.g. maximised / high-DPI) windows. Layout metrics stay untouched.
|
||||
* Rules after the no-scale marker keep their fixed size, so symbol / icon
|
||||
* glyphs do not scale. */
|
||||
static char *scale_font_sizes(const char *css, double scale) {
|
||||
static const char marker[] = "/* no-scale */";
|
||||
const char *pos;
|
||||
char *head;
|
||||
char *scaled_head;
|
||||
char *out;
|
||||
GRegex *re;
|
||||
FontScale fs;
|
||||
|
||||
if (scale <= 1.0001)
|
||||
return g_strdup(css);
|
||||
|
||||
pos = g_strstr_len(css, -1, marker);
|
||||
head = pos ? g_strndup(css, (gsize)(pos - css)) : g_strdup(css);
|
||||
|
||||
fs.scale = scale;
|
||||
re = g_regex_new("font-size:([ \t]*)([0-9]+)px", 0, 0, NULL);
|
||||
scaled_head = g_regex_replace_eval(re, head, -1, 0, 0,
|
||||
scale_font_size_eval, &fs, NULL);
|
||||
g_regex_unref(re);
|
||||
g_free(head);
|
||||
|
||||
if (pos)
|
||||
out = g_strconcat(scaled_head, pos, NULL);
|
||||
else
|
||||
out = scaled_head;
|
||||
g_free(scaled_head);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* The palette is data-driven, so its @define-color bindings are generated
|
||||
* here and prepended to the rules loaded from the external stylesheet. */
|
||||
char *build_theme_css(const ThemePalette *p) {
|
||||
char *palette = g_strdup_printf(
|
||||
"@define-color bg_crust #%06x;\n"
|
||||
"@define-color bg_base #%06x;\n"
|
||||
"@define-color bg_mantle #%06x;\n"
|
||||
"@define-color bg_surface0 #%06x;\n"
|
||||
"@define-color bg_surface1 #%06x;\n"
|
||||
"@define-color bg_surface2 #%06x;\n"
|
||||
"@define-color fg_overlay #%06x;\n"
|
||||
"@define-color fg_text #%06x;\n"
|
||||
"@define-color fg_subtext #%06x;\n"
|
||||
"@define-color accent #%06x;\n"
|
||||
"@define-color accent2 #%06x;\n"
|
||||
"@define-color accent3 #%06x;\n"
|
||||
"@define-color success #%06x;\n"
|
||||
"@define-color success2 #%06x;\n"
|
||||
"@define-color warning #%06x;\n"
|
||||
"@define-color error #%06x;\n"
|
||||
"@define-color on_accent #%06x;\n"
|
||||
"@define-color node_bg #%06x;\n"
|
||||
"@define-color locked_bg #%06x;\n"
|
||||
"@define-color locked_border #%06x;\n",
|
||||
p->crust, p->base, p->mantle, p->surface0, p->surface1, p->surface2,
|
||||
p->overlay, p->text, p->subtext, p->accent, p->accent2, p->accent3,
|
||||
p->success, p->success2, p->warning, p->error, p->on_accent,
|
||||
p->node, p->locked_bg, p->locked_border);
|
||||
char *rules = read_css_file();
|
||||
char *scaled = scale_font_sizes(rules, app_ui_scale);
|
||||
char *css = g_strconcat(palette, scaled, NULL);
|
||||
|
||||
g_free(palette);
|
||||
g_free(rules);
|
||||
g_free(scaled);
|
||||
return css;
|
||||
}
|
||||
|
||||
void save_settings(void) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
gchar *data;
|
||||
|
||||
g_key_file_set_string(kf, "ui", "theme", theme_names[app_theme_id]);
|
||||
g_key_file_set_string(kf, "ui", "mode",
|
||||
app_color_mode == MODE_LIGHT ? "light" : "dark");
|
||||
g_key_file_set_string(kf, "ui", "lang",
|
||||
app_lang == LANG_EN ? "en" : "cs");
|
||||
|
||||
if (g_mkdir_with_parents(PROGRESS_DIR, 0755) != 0) {
|
||||
g_key_file_free(kf);
|
||||
return;
|
||||
}
|
||||
|
||||
data = g_key_file_to_data(kf, NULL, NULL);
|
||||
g_key_file_free(kf);
|
||||
if (data) {
|
||||
GError *err = NULL;
|
||||
g_file_set_contents(SETTINGS_FILE, data, -1, &err);
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_free(data);
|
||||
}
|
||||
}
|
||||
|
||||
void load_settings(void) {
|
||||
GKeyFile *kf = g_key_file_new();
|
||||
GError *err = NULL;
|
||||
gchar *theme = NULL;
|
||||
gchar *mode = NULL;
|
||||
|
||||
if (!g_key_file_load_from_file(kf, SETTINGS_FILE, G_KEY_FILE_NONE, &err)) {
|
||||
if (err)
|
||||
g_error_free(err);
|
||||
g_key_file_free(kf);
|
||||
return;
|
||||
}
|
||||
|
||||
theme = g_key_file_get_string(kf, "ui", "theme", NULL);
|
||||
mode = g_key_file_get_string(kf, "ui", "mode", NULL);
|
||||
if (theme) {
|
||||
for (int i = 0; i < THEME_COUNT; i++) {
|
||||
if (g_ascii_strcasecmp(theme, theme_names[i]) == 0) {
|
||||
app_theme_id = (ThemeId)i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_free(theme);
|
||||
}
|
||||
if (mode) {
|
||||
if (g_ascii_strcasecmp(mode, "light") == 0 ||
|
||||
g_ascii_strcasecmp(mode, "white") == 0)
|
||||
app_color_mode = MODE_LIGHT;
|
||||
else
|
||||
app_color_mode = MODE_DARK;
|
||||
g_free(mode);
|
||||
}
|
||||
{
|
||||
gchar *lang = g_key_file_get_string(kf, "ui", "lang", NULL);
|
||||
if (lang) {
|
||||
if (g_ascii_strcasecmp(lang, "en") == 0 ||
|
||||
g_ascii_strcasecmp(lang, "english") == 0)
|
||||
app_lang = LANG_EN;
|
||||
else
|
||||
app_lang = LANG_CS;
|
||||
g_free(lang);
|
||||
}
|
||||
}
|
||||
|
||||
g_key_file_free(kf);
|
||||
}
|
||||
|
||||
/* GTK only re-resolves the style of widgets that are actually drawn after a
|
||||
* CSS provider reload. Pages that are not the current stack child keep their
|
||||
* previously resolved colors, so after switching the theme any page you then
|
||||
* open still shows the old palette (e.g. near-white text in light mode).
|
||||
* Adding and immediately removing a dummy class forces GTK to invalidate the
|
||||
* style node of every widget, including hidden pages. */
|
||||
void force_restyle_tree(GtkWidget *w) {
|
||||
GtkWidget *child;
|
||||
|
||||
if (!w)
|
||||
return;
|
||||
|
||||
gtk_widget_add_css_class(w, "m4-restyle");
|
||||
gtk_widget_remove_css_class(w, "m4-restyle");
|
||||
|
||||
for (child = gtk_widget_get_first_child(w); child;
|
||||
child = gtk_widget_get_next_sibling(child))
|
||||
force_restyle_tree(child);
|
||||
}
|
||||
|
||||
void apply_theme(void) {
|
||||
char *css;
|
||||
GtkSettings *settings = gtk_settings_get_default();
|
||||
|
||||
app_theme = theme_palette(app_theme_id, app_color_mode);
|
||||
|
||||
if (settings) {
|
||||
g_object_set(settings,
|
||||
"gtk-application-prefer-dark-theme",
|
||||
app_color_mode == MODE_DARK,
|
||||
NULL);
|
||||
}
|
||||
|
||||
css = build_theme_css(&app_theme);
|
||||
|
||||
if (!theme_provider) {
|
||||
theme_provider = gtk_css_provider_new();
|
||||
gtk_style_context_add_provider_for_display(
|
||||
gdk_display_get_default(),
|
||||
GTK_STYLE_PROVIDER(theme_provider),
|
||||
GTK_STYLE_PROVIDER_PRIORITY_USER);
|
||||
}
|
||||
|
||||
#if GTK_CHECK_VERSION(4, 12, 0)
|
||||
gtk_css_provider_load_from_string(theme_provider, css);
|
||||
#else
|
||||
/* GTK 4.6 (Ubuntu 22.04) — load_from_string arrived in 4.12. */
|
||||
gtk_css_provider_load_from_data(theme_provider, css, -1);
|
||||
#endif
|
||||
g_free(css);
|
||||
|
||||
refresh_welcome_heading();
|
||||
|
||||
if (main_window) {
|
||||
gtk_widget_queue_draw(GTK_WIDGET(main_window));
|
||||
force_restyle_tree(GTK_WIDGET(main_window));
|
||||
}
|
||||
if (road_rail)
|
||||
gtk_widget_queue_draw(road_rail);
|
||||
for (int i = 0; i < NUM_UNLOCKED; i++)
|
||||
if (units[i].ex_rail)
|
||||
gtk_widget_queue_draw(units[i].ex_rail);
|
||||
for (int i = 0; i < THEME_COUNT; i++) {
|
||||
if (theme_swatch_areas[i])
|
||||
gtk_widget_queue_draw(theme_swatch_areas[i]);
|
||||
}
|
||||
net_rail_theme_reset();
|
||||
hw_rail_theme_reset();
|
||||
czech_rail_theme_reset();
|
||||
book_rail_theme_reset();
|
||||
}
|
||||
410
src/unitmap.c
Normal file
410
src/unitmap.c
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Unit 1 exercise path (roadmap-style serpentine) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
|
||||
/* Compute the serpentine exercise layout for the given available width. */
|
||||
void ex_layout_geometry(UnitCtx *u, double avail) {
|
||||
const int n = u->n_ex;
|
||||
const double phase = 2.0 * G_PI * 1.7 / (double)(n > 1 ? n - 1 : 1);
|
||||
const double lift = u->has_branch ? EX_BRANCH_LIFT : 0.0;
|
||||
int rows;
|
||||
int r, c, i;
|
||||
|
||||
if (n < 1)
|
||||
return;
|
||||
for (rows = 1; rows <= n; rows++) {
|
||||
int cols = (n + rows - 1) / rows;
|
||||
if (2.0 * EX_MX + (cols - 1) * EX_SPAC <= avail + 1.0)
|
||||
break;
|
||||
}
|
||||
if (rows > n)
|
||||
rows = n;
|
||||
u->ex_rows = rows;
|
||||
u->ex_cols = (n + rows - 1) / rows;
|
||||
if (u->ex_cols < 1)
|
||||
u->ex_cols = 1;
|
||||
u->ex_cw = (int)(2.0 * EX_MX + (u->ex_cols - 1) * EX_SPAC);
|
||||
u->ex_ch = (int)(2.0 * EX_MY + (rows - 1) * EX_GAP + lift);
|
||||
|
||||
for (r = 0; r < rows; r++) {
|
||||
int base = r * u->ex_cols;
|
||||
int len = MIN(u->ex_cols, n - base);
|
||||
int fwd = (r % 2) == 0;
|
||||
for (c = 0; c < len; c++) {
|
||||
i = base + c;
|
||||
int cc = fwd ? c : (u->ex_cols - 1 - c);
|
||||
u->ex_cx[i] = EX_MX + cc * EX_SPAC;
|
||||
u->ex_cy[i] = EX_MY + lift + r * EX_GAP
|
||||
+ EX_WAVE * sin((double)i * phase);
|
||||
}
|
||||
}
|
||||
|
||||
/* The branch hangs above exercise 2 (index 1), nudged sideways so its
|
||||
* connector is a slanted link rather than a dead-straight vertical line. */
|
||||
if (u->has_branch && n > 1) {
|
||||
u->branch_cx = u->ex_cx[1] + EX_BRANCH_DX;
|
||||
u->branch_cy = u->ex_cy[1] - EX_BRANCH_LIFT;
|
||||
}
|
||||
}
|
||||
|
||||
void ex_point(UnitCtx *u, double t, double *ox, double *oy) {
|
||||
const int n = u->n_ex;
|
||||
int k = (int)t;
|
||||
double uu = t - (double)k;
|
||||
double u2, u3;
|
||||
double p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y;
|
||||
|
||||
if (n < 2) { *ox = u->ex_cx[0]; *oy = u->ex_cy[0]; return; }
|
||||
if (k < 0) { k = 0; uu = 0.0; }
|
||||
if (k >= n - 1) { k = n - 2; uu = 1.0; }
|
||||
|
||||
p1x = u->ex_cx[k]; p1y = u->ex_cy[k];
|
||||
p2x = u->ex_cx[k + 1]; p2y = u->ex_cy[k + 1];
|
||||
if (k - 1 >= 0) { p0x = u->ex_cx[k - 1]; p0y = u->ex_cy[k - 1]; }
|
||||
else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
|
||||
if (k + 2 < n) { p3x = u->ex_cx[k + 2]; p3y = u->ex_cy[k + 2]; }
|
||||
else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
|
||||
|
||||
u2 = uu * uu;
|
||||
u3 = u2 * uu;
|
||||
*ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * uu
|
||||
+ (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
|
||||
+ (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
|
||||
*oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * uu
|
||||
+ (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
|
||||
+ (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
|
||||
}
|
||||
|
||||
void ex_path(UnitCtx *u, cairo_t *cr, double t0, double t1) {
|
||||
const double steps_per_unit = 48.0;
|
||||
int n = (int)((t1 - t0) * steps_per_unit);
|
||||
double x, y;
|
||||
int s;
|
||||
|
||||
if (n < 1)
|
||||
n = 1;
|
||||
ex_point(u, t0, &x, &y);
|
||||
cairo_move_to(cr, x, y);
|
||||
for (s = 1; s <= n; s++) {
|
||||
ex_point(u, t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
|
||||
cairo_line_to(cr, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wavy link from an exercise bubble up to the off-path branch. */
|
||||
static void branch_path(cairo_t *cr, double x0, double y0,
|
||||
double x1, double y1) {
|
||||
double dy = y0 - y1;
|
||||
double c1x = x0 + EX_WAVE * 1.3;
|
||||
double c1y = y0 - dy * 0.45;
|
||||
double c2x = x1 - EX_WAVE * 1.3;
|
||||
double c2y = y1 + dy * 0.45;
|
||||
|
||||
cairo_move_to(cr, x0, y0);
|
||||
cairo_curve_to(cr, c1x, c1y, c2x, c2y, x1, y1);
|
||||
}
|
||||
|
||||
void draw_ex_rail(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer user_data) {
|
||||
UnitCtx *u = user_data;
|
||||
const double t_end = (double)(u->n_ex - 1);
|
||||
const Rgb slate = color_from_hex(app_theme.surface1);
|
||||
const Rgb green = color_from_hex(app_theme.success);
|
||||
const Rgb rail = color_from_hex(app_theme.rail);
|
||||
|
||||
(void)area;
|
||||
(void)width;
|
||||
(void)height;
|
||||
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||||
cairo_paint(cr);
|
||||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||||
|
||||
cairo_new_path(cr);
|
||||
ex_path(u, cr, 0.0, t_end);
|
||||
cairo_set_line_width(cr, 14);
|
||||
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
ex_path(u, cr, 0.0, t_end);
|
||||
cairo_set_line_width(cr, 9);
|
||||
cairo_set_source_rgb(cr, slate.r, slate.g, slate.b);
|
||||
cairo_stroke(cr);
|
||||
|
||||
for (int k = 0; k < u->n_ex - 1; k++) {
|
||||
if (!(u->done[k + 1] || u->done[k + 2]))
|
||||
continue;
|
||||
cairo_new_path(cr);
|
||||
ex_path(u, cr, (double)k, (double)(k + 1));
|
||||
cairo_set_source_rgba(cr, green.r, green.g, green.b, 0.85);
|
||||
cairo_set_line_width(cr, 9);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
|
||||
/* Off-path branch connector up from exercise 2. */
|
||||
if (u->has_branch && u->n_ex > 1) {
|
||||
gboolean bdone = u->done[u->branch_ex];
|
||||
|
||||
cairo_new_path(cr);
|
||||
branch_path(cr, u->ex_cx[1], u->ex_cy[1], u->branch_cx, u->branch_cy);
|
||||
cairo_set_line_width(cr, 14);
|
||||
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
|
||||
cairo_stroke(cr);
|
||||
|
||||
cairo_new_path(cr);
|
||||
branch_path(cr, u->ex_cx[1], u->ex_cy[1], u->branch_cx, u->branch_cy);
|
||||
cairo_set_line_width(cr, 9);
|
||||
if (bdone)
|
||||
cairo_set_source_rgba(cr, green.r, green.g, green.b, 0.85);
|
||||
else
|
||||
cairo_set_source_rgb(cr, slate.r, slate.g, slate.b);
|
||||
cairo_stroke(cr);
|
||||
}
|
||||
}
|
||||
|
||||
void ex_apply_layout(UnitCtx *u) {
|
||||
int i;
|
||||
|
||||
if (!u->ex_fixed)
|
||||
return;
|
||||
|
||||
for (i = 0; i < u->n_ex; i++) {
|
||||
if (u->ex_cells[i])
|
||||
gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_cells[i],
|
||||
(int)(u->ex_cx[i] - EX_BUBBLE / 2.0),
|
||||
(int)(u->ex_cy[i] - EX_BUBBLE / 2.0));
|
||||
if (u->ex_labels[i])
|
||||
gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_labels[i],
|
||||
(int)(u->ex_cx[i] - (EX_SPAC - 20.0) / 2.0),
|
||||
(int)(u->ex_cy[i] + EX_BUBBLE / 2.0 + 8.0));
|
||||
}
|
||||
|
||||
if (u->has_branch && u->branch_cell) {
|
||||
gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->branch_cell,
|
||||
(int)(u->branch_cx - EX_BUBBLE / 2.0),
|
||||
(int)(u->branch_cy - EX_BUBBLE / 2.0));
|
||||
if (u->branch_label)
|
||||
gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->branch_label,
|
||||
(int)(u->branch_cx + EX_BUBBLE / 2.0 + 10.0),
|
||||
(int)(u->branch_cy - 14.0));
|
||||
}
|
||||
|
||||
gtk_widget_set_size_request(u->ex_fixed, u->ex_cw, u->ex_ch);
|
||||
gtk_widget_set_size_request(u->ex_rail, u->ex_cw, u->ex_ch);
|
||||
gtk_fixed_move(GTK_FIXED(u->ex_fixed), u->ex_rail, 0, 0);
|
||||
gtk_widget_queue_draw(u->ex_rail);
|
||||
}
|
||||
|
||||
void ex_relayout(UnitCtx *u) {
|
||||
GtkAdjustment *hadj;
|
||||
double avail;
|
||||
|
||||
if (!u->ex_scroll)
|
||||
return;
|
||||
hadj = gtk_scrolled_window_get_hadjustment(
|
||||
GTK_SCROLLED_WINDOW(u->ex_scroll));
|
||||
avail = gtk_adjustment_get_page_size(hadj);
|
||||
if (avail < 1.0)
|
||||
return;
|
||||
ex_layout_geometry(u, avail);
|
||||
ex_apply_layout(u);
|
||||
}
|
||||
|
||||
gboolean ex_relayout_idle(gpointer data) {
|
||||
UnitCtx *u = data;
|
||||
u->ex_idle = 0;
|
||||
ex_relayout(u);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void ex_relayout_later(UnitCtx *u) {
|
||||
if (u->ex_idle == 0)
|
||||
u->ex_idle = g_idle_add(ex_relayout_idle, u);
|
||||
}
|
||||
|
||||
void ex_adjust_notify(GtkAdjustment *adj, GParamSpec *ps,
|
||||
gpointer data) {
|
||||
(void)adj;
|
||||
(void)ps;
|
||||
ex_relayout_later(data);
|
||||
}
|
||||
|
||||
GtkWidget *make_bubble(UnitCtx *u, int n) {
|
||||
GtkWidget *btn;
|
||||
GtkWidget *vbox;
|
||||
GtkWidget *num_label;
|
||||
GtkWidget *icon;
|
||||
char *num;
|
||||
char *target;
|
||||
|
||||
btn = gtk_button_new();
|
||||
gtk_widget_add_css_class(btn, "ex-bubble");
|
||||
gtk_widget_set_size_request(btn, (int)EX_BUBBLE, (int)EX_BUBBLE);
|
||||
gtk_widget_set_can_focus(btn, FALSE);
|
||||
if (u->done[n])
|
||||
gtk_widget_add_css_class(btn, "done");
|
||||
|
||||
vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||||
gtk_widget_set_halign(vbox, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(vbox, GTK_ALIGN_CENTER);
|
||||
gtk_button_set_child(GTK_BUTTON(btn), vbox);
|
||||
|
||||
num = g_strdup_printf("%d", n);
|
||||
num_label = gtk_label_new(num);
|
||||
g_free(num);
|
||||
gtk_widget_set_halign(num_label, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(num_label, "bubble-number");
|
||||
gtk_box_append(GTK_BOX(vbox), num_label);
|
||||
|
||||
icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 16);
|
||||
gtk_widget_set_visible(icon, u->done[n]);
|
||||
gtk_box_append(GTK_BOX(vbox), icon);
|
||||
|
||||
u->ex_cells[n - 1] = btn;
|
||||
u->ex_icons[n] = icon;
|
||||
|
||||
target = g_strdup_printf("%se%d", u->ex_tag, n);
|
||||
g_object_set_data_full(G_OBJECT(btn), "target", target, g_free);
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
static GtkWidget *make_branch_bubble(UnitCtx *u) {
|
||||
GtkWidget *btn;
|
||||
GtkWidget *vbox;
|
||||
GtkWidget *star;
|
||||
GtkWidget *icon;
|
||||
|
||||
btn = gtk_button_new();
|
||||
gtk_widget_add_css_class(btn, "ex-bubble");
|
||||
gtk_widget_add_css_class(btn, "ex-branch");
|
||||
gtk_widget_set_size_request(btn, (int)EX_BUBBLE, (int)EX_BUBBLE);
|
||||
gtk_widget_set_can_focus(btn, FALSE);
|
||||
if (u->done[u->branch_ex])
|
||||
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);
|
||||
|
||||
star = gtk_label_new("★");
|
||||
gtk_widget_set_halign(star, GTK_ALIGN_CENTER);
|
||||
gtk_widget_add_css_class(star, "bubble-number");
|
||||
gtk_widget_add_css_class(star, "branch-star");
|
||||
gtk_box_append(GTK_BOX(vbox), star);
|
||||
|
||||
icon = icon_area_new(draw_check_icon, 0.1176, 0.1176, 0.1804, 16);
|
||||
gtk_widget_set_visible(icon, u->done[u->branch_ex]);
|
||||
gtk_box_append(GTK_BOX(vbox), icon);
|
||||
|
||||
u->branch_cell = btn;
|
||||
u->branch_icon = icon;
|
||||
|
||||
g_object_set_data_full(G_OBJECT(btn), "target",
|
||||
g_strdup(u->branch_target), g_free);
|
||||
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||||
return btn;
|
||||
}
|
||||
|
||||
GtkWidget *build_unit_page(UnitCtx *u) {
|
||||
GtkWidget *page;
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *wrap;
|
||||
GtkWidget *fixed;
|
||||
GtkWidget *rail;
|
||||
GtkAdjustment *ha;
|
||||
GtkAdjustment *va;
|
||||
int i;
|
||||
|
||||
if (u->n_ex < 1)
|
||||
return NULL;
|
||||
ex_layout_geometry(u, 1000.0);
|
||||
|
||||
page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_hexpand(page, TRUE);
|
||||
gtk_widget_set_vexpand(page, TRUE);
|
||||
gtk_widget_set_margin_start(page, 32);
|
||||
gtk_widget_set_margin_end(page, 32);
|
||||
gtk_widget_set_margin_top(page, 24);
|
||||
gtk_widget_set_margin_bottom(page, 24);
|
||||
|
||||
gtk_box_append(GTK_BOX(page),
|
||||
top_bar(u->back_target ? u->back_target : "roadmap",
|
||||
u->title, u->sub_key));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 14);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
u->ex_scroll = scroll;
|
||||
|
||||
wrap = gtk_center_box_new();
|
||||
gtk_widget_set_vexpand(wrap, TRUE);
|
||||
gtk_widget_set_hexpand(wrap, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), wrap);
|
||||
|
||||
fixed = gtk_fixed_new();
|
||||
gtk_widget_set_halign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_valign(fixed, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_size_request(fixed, u->ex_cw, u->ex_ch);
|
||||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
|
||||
u->ex_fixed = fixed;
|
||||
|
||||
rail = gtk_drawing_area_new();
|
||||
gtk_widget_set_size_request(rail, u->ex_cw, u->ex_ch);
|
||||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), draw_ex_rail,
|
||||
u, NULL);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
|
||||
u->ex_rail = rail;
|
||||
|
||||
for (i = 0; i < u->n_ex; i++) {
|
||||
GtkWidget *btn = make_bubble(u, i + 1);
|
||||
GtkWidget *lbl = gtk_label_new(u->ex_names[i + 1]);
|
||||
|
||||
u->ex_labels[i] = lbl;
|
||||
|
||||
gtk_widget_set_size_request(lbl, (int)(EX_SPAC - 20.0), -1);
|
||||
gtk_widget_set_halign(lbl, GTK_ALIGN_CENTER);
|
||||
gtk_label_set_justify(GTK_LABEL(lbl), GTK_JUSTIFY_CENTER);
|
||||
gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
|
||||
gtk_widget_add_css_class(lbl, "ex-label");
|
||||
|
||||
gtk_fixed_put(GTK_FIXED(fixed), btn, 0, 0);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0);
|
||||
}
|
||||
|
||||
if (u->has_branch && u->branch_target) {
|
||||
GtkWidget *btn = make_branch_bubble(u);
|
||||
GtkWidget *lbl = gtk_label_new(u->branch_name);
|
||||
|
||||
u->branch_label = lbl;
|
||||
gtk_widget_set_halign(lbl, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(lbl, "ex-label");
|
||||
gtk_widget_set_visible(lbl, TRUE);
|
||||
|
||||
gtk_fixed_put(GTK_FIXED(fixed), btn, 0, 0);
|
||||
gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0);
|
||||
}
|
||||
|
||||
ha = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
va = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
|
||||
g_signal_connect(ha, "notify::page-size",
|
||||
G_CALLBACK(ex_adjust_notify), u);
|
||||
g_signal_connect(va, "notify::page-size",
|
||||
G_CALLBACK(ex_adjust_notify), u);
|
||||
|
||||
ex_apply_layout(u);
|
||||
ex_relayout_later(u);
|
||||
|
||||
return page;
|
||||
}
|
||||
902
src/update.c
Normal file
902
src/update.c
Normal file
|
|
@ -0,0 +1,902 @@
|
|||
#include "maturita.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <process.h>
|
||||
#define update_getpid() ((long)_getpid())
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#define update_getpid() ((long)getpid())
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Self-update */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Networking runs through the `curl` command rather than a linked HTTP
|
||||
* library. curl already handles TLS, redirects and proxies, it ships with
|
||||
* macOS, with every desktop Linux and with Windows 10 and later, and using it
|
||||
* keeps the three bundling scripts free of another shared library to ship.
|
||||
* Every call is asynchronous, so a slow or unreachable network never blocks
|
||||
* the interface.
|
||||
*
|
||||
* Packages are not published as releases. They live on the `builds` branch of
|
||||
* the repository, which CI rewrites on every change to main, so an update is
|
||||
* simply whatever that branch currently holds. The branch also carries a
|
||||
* VERSION file naming the commit its packages were built from; comparing that
|
||||
* against the commit baked into this binary is the whole check. Serving it
|
||||
* from raw.githubusercontent.com avoids the REST API and its unauthenticated
|
||||
* rate limit, and needs no JSON parser.
|
||||
*
|
||||
* An application cannot reliably replace its own files while it is running, so
|
||||
* all three platforms follow the same shape: unpack the new version into a
|
||||
* staging directory beside the current install, write a small script that
|
||||
* waits for this process to exit before swapping it in, then launch that
|
||||
* script detached and quit. Staging beside the install keeps the final move on
|
||||
* one filesystem and proves up front that the location is writable. */
|
||||
|
||||
#define UPDATE_BASE_URL "https://raw.githubusercontent.com/" UPDATE_REPO \
|
||||
"/" UPDATE_BRANCH "/"
|
||||
#define UPDATE_VERSION_URL UPDATE_BASE_URL "VERSION"
|
||||
#define UPDATE_STAGING_DIR ".maturita-update"
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NULL_DEVICE "NUL"
|
||||
#else
|
||||
#define NULL_DEVICE "/dev/null"
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
UPDATE_IDLE,
|
||||
UPDATE_CHECKING,
|
||||
UPDATE_UP_TO_DATE,
|
||||
UPDATE_AVAILABLE,
|
||||
UPDATE_DOWNLOADING,
|
||||
UPDATE_STAGED,
|
||||
UPDATE_FAILED,
|
||||
} UpdateState;
|
||||
|
||||
/* How this copy is installed, which decides what the update has to replace: a
|
||||
* macOS bundle, a Windows install directory, or a single AppImage file. Builds
|
||||
* run straight from the source tree cannot update themselves. */
|
||||
typedef enum {
|
||||
INSTALL_UNSUPPORTED,
|
||||
INSTALL_MACOS_BUNDLE,
|
||||
INSTALL_WINDOWS_DIR,
|
||||
INSTALL_APPIMAGE,
|
||||
} InstallKind;
|
||||
|
||||
static struct {
|
||||
UpdateState state;
|
||||
gboolean interactive; /* user asked, so also report "up to date" */
|
||||
char *latest; /* commit the published packages were built from */
|
||||
char *staging; /* scratch directory beside the install */
|
||||
char *archive; /* asset downloaded into the staging directory */
|
||||
char *source; /* unpacked version the swap script moves in */
|
||||
char *script; /* swap script, run after this process exits */
|
||||
char *error;
|
||||
goffset total; /* asset size, 0 when the server did not say */
|
||||
guint poll_id;
|
||||
GtkWidget *banner;
|
||||
GtkWidget *banner_label;
|
||||
GtkWidget *banner_action;
|
||||
GtkWidget *status; /* status line in the settings popover */
|
||||
GtkWidget *action; /* check / update / restart button */
|
||||
GtkWidget *progress;
|
||||
} up;
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Build identity */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Commits are not ordered, so there is nothing to compare: the branch only
|
||||
* ever holds the current build, and anything else means this copy is behind.
|
||||
* Short and full forms count as the same commit, so the interface can show a
|
||||
* short one without confusing the check. */
|
||||
static gboolean same_commit(const char *a, const char *b) {
|
||||
size_t la, lb, n;
|
||||
|
||||
if (!a || !b || !*a || !*b)
|
||||
return FALSE;
|
||||
la = strlen(a);
|
||||
lb = strlen(b);
|
||||
n = MIN(la, lb);
|
||||
return n >= 7 && g_ascii_strncasecmp(a, b, n) == 0;
|
||||
}
|
||||
|
||||
/* Only builds published by CI carry a commit; see APP_COMMIT in maturita.h. */
|
||||
static gboolean is_published_build(void) {
|
||||
return g_ascii_isxdigit(APP_COMMIT[0]) && strlen(APP_COMMIT) >= 7;
|
||||
}
|
||||
|
||||
/* Commit hashes are unreadable at full length; seven characters is what git
|
||||
* itself shows. */
|
||||
static char *short_commit(const char *sha) {
|
||||
return g_strndup(sha ? sha : "", 7);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Install layout */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* The asset published for this platform, or NULL when the release carries
|
||||
* none. Only arm64 macOS and x86_64 Linux / Windows are built. */
|
||||
static const char *asset_name(void) {
|
||||
#if defined(_WIN32)
|
||||
return "maturita-windows-x64.zip";
|
||||
#elif defined(__APPLE__)
|
||||
#if defined(__aarch64__) || defined(__arm64__)
|
||||
return "maturita-macos-arm64.zip";
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
#elif defined(__x86_64__)
|
||||
return "maturita-linux-x86_64.AppImage";
|
||||
#else
|
||||
return NULL;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* What the update replaces, plus the directory holding it. */
|
||||
static InstallKind install_target(char **target_out, char **parent_out) {
|
||||
char *exe = app_executable_path();
|
||||
InstallKind kind = INSTALL_UNSUPPORTED;
|
||||
char *target = NULL;
|
||||
|
||||
*target_out = NULL;
|
||||
*parent_out = NULL;
|
||||
|
||||
#ifndef _WIN32
|
||||
{
|
||||
const char *appimage = g_getenv("APPIMAGE");
|
||||
|
||||
if (appimage && appimage[0]) {
|
||||
target = g_strdup(appimage);
|
||||
kind = INSTALL_APPIMAGE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
/* .../Maturita.app/Contents/MacOS/maturita -> .../Maturita.app */
|
||||
if (!target && exe) {
|
||||
char *macos_dir = g_path_get_dirname(exe);
|
||||
char *contents = g_path_get_dirname(macos_dir);
|
||||
char *bundle = g_path_get_dirname(contents);
|
||||
|
||||
if (g_str_has_suffix(macos_dir, "MacOS") &&
|
||||
g_str_has_suffix(bundle, ".app")) {
|
||||
target = g_strdup(bundle);
|
||||
kind = INSTALL_MACOS_BUNDLE;
|
||||
}
|
||||
g_free(bundle);
|
||||
g_free(contents);
|
||||
g_free(macos_dir);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
/* The Windows package is a plain directory holding the exe and the GTK
|
||||
* runtime, so the directory itself is what gets refreshed. */
|
||||
if (!target && exe) {
|
||||
target = g_path_get_dirname(exe);
|
||||
kind = INSTALL_WINDOWS_DIR;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (target) {
|
||||
*target_out = target;
|
||||
*parent_out = g_path_get_dirname(target);
|
||||
}
|
||||
g_free(exe);
|
||||
return kind;
|
||||
}
|
||||
|
||||
/* An update is only worth offering when a release carries an asset for this
|
||||
* platform and the running copy is one this code knows how to replace. */
|
||||
static gboolean update_supported(void) {
|
||||
char *target = NULL;
|
||||
char *parent = NULL;
|
||||
gboolean ok = install_target(&target, &parent) != INSTALL_UNSUPPORTED;
|
||||
|
||||
g_free(target);
|
||||
g_free(parent);
|
||||
return ok && asset_name() != NULL;
|
||||
}
|
||||
|
||||
/* Depth-first delete; GLib has no recursive remove. */
|
||||
static void remove_tree(const char *path) {
|
||||
GDir *dir;
|
||||
|
||||
if (!path || !g_file_test(path, G_FILE_TEST_EXISTS))
|
||||
return;
|
||||
|
||||
if (g_file_test(path, G_FILE_TEST_IS_DIR) &&
|
||||
!g_file_test(path, G_FILE_TEST_IS_SYMLINK)) {
|
||||
const char *name;
|
||||
|
||||
dir = g_dir_open(path, 0, NULL);
|
||||
if (dir) {
|
||||
while ((name = g_dir_read_name(dir))) {
|
||||
char *child = g_build_filename(path, name, NULL);
|
||||
|
||||
remove_tree(child);
|
||||
g_free(child);
|
||||
}
|
||||
g_dir_close(dir);
|
||||
}
|
||||
g_rmdir(path);
|
||||
} else {
|
||||
g_remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
/* An interrupted swap leaves the staging directory behind; drop it so it does
|
||||
* not sit next to the install forever. */
|
||||
void update_clear_staging(void) {
|
||||
char *target = NULL;
|
||||
char *parent = NULL;
|
||||
|
||||
if (install_target(&target, &parent) != INSTALL_UNSUPPORTED) {
|
||||
char *stale = g_build_filename(parent, UPDATE_STAGING_DIR, NULL);
|
||||
|
||||
remove_tree(stale);
|
||||
g_free(stale);
|
||||
}
|
||||
g_free(target);
|
||||
g_free(parent);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Interface state */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void update_set_state(UpdateState state);
|
||||
|
||||
static void update_fail(const char *message) {
|
||||
g_free(up.error);
|
||||
up.error = g_strdup(message);
|
||||
update_set_state(UPDATE_FAILED);
|
||||
}
|
||||
|
||||
static void update_refresh_ui(void) {
|
||||
gboolean busy = up.state == UPDATE_CHECKING ||
|
||||
up.state == UPDATE_DOWNLOADING;
|
||||
gboolean banner = up.state == UPDATE_AVAILABLE ||
|
||||
up.state == UPDATE_DOWNLOADING ||
|
||||
up.state == UPDATE_STAGED;
|
||||
const char *status = "";
|
||||
const char *action = "update_check";
|
||||
char *owned = NULL;
|
||||
|
||||
switch (up.state) {
|
||||
case UPDATE_IDLE:
|
||||
case UPDATE_UP_TO_DATE: {
|
||||
char *mine = short_commit(APP_COMMIT);
|
||||
|
||||
owned = g_strdup_printf(tr(up.state == UPDATE_IDLE
|
||||
? "update_current"
|
||||
: "update_uptodate"), mine);
|
||||
status = owned;
|
||||
g_free(mine);
|
||||
break;
|
||||
}
|
||||
case UPDATE_CHECKING:
|
||||
status = tr("update_checking");
|
||||
break;
|
||||
case UPDATE_AVAILABLE: {
|
||||
char *newest = short_commit(up.latest);
|
||||
|
||||
owned = g_strdup_printf(tr("update_available"), newest);
|
||||
status = owned;
|
||||
action = "update_install";
|
||||
g_free(newest);
|
||||
break;
|
||||
}
|
||||
case UPDATE_DOWNLOADING:
|
||||
status = tr("update_downloading");
|
||||
action = "update_install";
|
||||
break;
|
||||
case UPDATE_STAGED:
|
||||
status = tr("update_staged");
|
||||
action = "update_restart";
|
||||
break;
|
||||
case UPDATE_FAILED:
|
||||
status = up.error ? up.error : tr("update_failed");
|
||||
break;
|
||||
}
|
||||
|
||||
if (up.status)
|
||||
gtk_label_set_text(GTK_LABEL(up.status), status);
|
||||
if (up.action) {
|
||||
gtk_button_set_label(GTK_BUTTON(up.action), tr(action));
|
||||
gtk_widget_set_sensitive(up.action, !busy);
|
||||
}
|
||||
if (up.progress)
|
||||
gtk_widget_set_visible(up.progress, up.state == UPDATE_DOWNLOADING);
|
||||
|
||||
if (up.banner) {
|
||||
gtk_widget_set_visible(up.banner, banner);
|
||||
if (banner) {
|
||||
gtk_label_set_text(GTK_LABEL(up.banner_label), status);
|
||||
gtk_button_set_label(GTK_BUTTON(up.banner_action), tr(action));
|
||||
gtk_widget_set_sensitive(up.banner_action, !busy);
|
||||
}
|
||||
}
|
||||
|
||||
g_free(owned);
|
||||
}
|
||||
|
||||
static void update_set_state(UpdateState state) {
|
||||
up.state = state;
|
||||
update_refresh_ui();
|
||||
}
|
||||
|
||||
/* Called by apply_language() so switching language relabels the banner. */
|
||||
void update_apply_lang(void) {
|
||||
update_refresh_ui();
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Running curl */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Spawn curl and hand its stdout to `done`. FALSE means curl itself could not
|
||||
* be started, the one failure worth its own message. */
|
||||
static gboolean curl_async(const char *const *argv, GAsyncReadyCallback done) {
|
||||
GSubprocess *proc;
|
||||
|
||||
proc = g_subprocess_newv(argv,
|
||||
G_SUBPROCESS_FLAGS_STDOUT_PIPE |
|
||||
G_SUBPROCESS_FLAGS_STDERR_SILENCE,
|
||||
NULL);
|
||||
if (!proc)
|
||||
return FALSE;
|
||||
g_subprocess_communicate_utf8_async(proc, NULL, NULL, done, NULL);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Staging and the swap script */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* The macOS asset holds a single .app whose name we should not assume. */
|
||||
static char *find_app_bundle(const char *dir) {
|
||||
GDir *d = g_dir_open(dir, 0, NULL);
|
||||
const char *name;
|
||||
char *found = NULL;
|
||||
|
||||
if (!d)
|
||||
return NULL;
|
||||
while (!found && (name = g_dir_read_name(d))) {
|
||||
if (g_str_has_suffix(name, ".app"))
|
||||
found = g_build_filename(dir, name, NULL);
|
||||
}
|
||||
g_dir_close(d);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Each script takes: pid target source staging. They all wait for this
|
||||
* process to exit, swap the new version in, relaunch and clean up after
|
||||
* themselves. Rolling back on a failed move keeps a broken half-update from
|
||||
* leaving the user with nothing to start. */
|
||||
static const char *swap_script_body(InstallKind kind) {
|
||||
switch (kind) {
|
||||
case INSTALL_MACOS_BUNDLE:
|
||||
return "#!/bin/sh\n"
|
||||
"pid=$1; target=$2; source=$3; staging=$4\n"
|
||||
"n=0\n"
|
||||
"while kill -0 \"$pid\" 2>/dev/null && [ $n -lt 600 ]; do\n"
|
||||
" sleep 0.2; n=$((n+1))\n"
|
||||
"done\n"
|
||||
"rm -rf \"$target.old\"\n"
|
||||
"mv \"$target\" \"$target.old\" 2>/dev/null\n"
|
||||
"if mv \"$source\" \"$target\" 2>/dev/null; then\n"
|
||||
" xattr -cr \"$target\" 2>/dev/null\n"
|
||||
" rm -rf \"$target.old\"\n"
|
||||
"else\n"
|
||||
" mv \"$target.old\" \"$target\" 2>/dev/null\n"
|
||||
"fi\n"
|
||||
"open -n \"$target\"\n"
|
||||
"rm -rf \"$staging\"\n"
|
||||
"rm -f \"$0\"\n";
|
||||
case INSTALL_APPIMAGE:
|
||||
return "#!/bin/sh\n"
|
||||
"pid=$1; target=$2; source=$3; staging=$4\n"
|
||||
"n=0\n"
|
||||
"while kill -0 \"$pid\" 2>/dev/null && [ $n -lt 600 ]; do\n"
|
||||
" sleep 0.2; n=$((n+1))\n"
|
||||
"done\n"
|
||||
"if mv \"$source\" \"$target\" 2>/dev/null; then\n"
|
||||
" chmod +x \"$target\"\n"
|
||||
"fi\n"
|
||||
"\"$target\" >/dev/null 2>&1 &\n"
|
||||
"rm -rf \"$staging\"\n"
|
||||
"rm -f \"$0\"\n";
|
||||
case INSTALL_WINDOWS_DIR:
|
||||
/* Copies over the install without mirroring, so the progress files
|
||||
* kept inside it survive. ping is the sleep that works without a
|
||||
* console window. */
|
||||
return "@echo off\r\n"
|
||||
"set PID=%~1\r\n"
|
||||
"set TARGET=%~2\r\n"
|
||||
"set SOURCE=%~3\r\n"
|
||||
"set STAGING=%~4\r\n"
|
||||
":wait\r\n"
|
||||
"tasklist /FI \"PID eq %PID%\" 2>nul"
|
||||
" | find \"%PID%\" >nul\r\n"
|
||||
"if not errorlevel 1 (\r\n"
|
||||
" ping -n 2 127.0.0.1 >nul\r\n"
|
||||
" goto wait\r\n"
|
||||
")\r\n"
|
||||
"xcopy \"%SOURCE%\\*\" \"%TARGET%\\\" /E /I /Y /Q >nul\r\n"
|
||||
"start \"\" \"%TARGET%\\maturita.exe\"\r\n"
|
||||
"rmdir /s /q \"%STAGING%\"\r\n"
|
||||
"del \"%~f0\"\r\n";
|
||||
case INSTALL_UNSUPPORTED:
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean write_swap_script(InstallKind kind) {
|
||||
const char *body = swap_script_body(kind);
|
||||
const char *name = kind == INSTALL_WINDOWS_DIR ? "maturita-update.cmd"
|
||||
: "maturita-update.sh";
|
||||
|
||||
if (!body)
|
||||
return FALSE;
|
||||
|
||||
g_free(up.script);
|
||||
up.script = g_build_filename(g_get_tmp_dir(), name, NULL);
|
||||
if (!g_file_set_contents(up.script, body, -1, NULL))
|
||||
return FALSE;
|
||||
g_chmod(up.script, 0755);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Unpack the asset and write the swap script. Nothing outside the staging
|
||||
* directory is touched until the user restarts. */
|
||||
static void install_staged(void) {
|
||||
char *target = NULL;
|
||||
char *parent = NULL;
|
||||
InstallKind kind = install_target(&target, &parent);
|
||||
char *source = NULL;
|
||||
gboolean ok;
|
||||
|
||||
if (kind == INSTALL_APPIMAGE) {
|
||||
/* The asset already is the executable; there is nothing to unpack. */
|
||||
source = g_strdup(up.archive);
|
||||
ok = TRUE;
|
||||
} else {
|
||||
char *unpacked = g_build_filename(up.staging, "new", NULL);
|
||||
const char *argv[] = {"tar", "-xf", up.archive, "-C", unpacked, NULL};
|
||||
GSubprocess *proc;
|
||||
|
||||
/* bsdtar reads zip archives and ships with both macOS and Windows 10
|
||||
* and later, so no separate unzip tool is needed. */
|
||||
ok = g_mkdir_with_parents(unpacked, 0755) == 0;
|
||||
if (ok) {
|
||||
proc = g_subprocess_newv(argv, G_SUBPROCESS_FLAGS_STDERR_SILENCE,
|
||||
NULL);
|
||||
ok = proc && g_subprocess_wait_check(proc, NULL, NULL);
|
||||
g_clear_object(&proc);
|
||||
}
|
||||
if (ok && kind == INSTALL_MACOS_BUNDLE) {
|
||||
source = find_app_bundle(unpacked);
|
||||
ok = source != NULL;
|
||||
} else if (ok) {
|
||||
source = g_strdup(unpacked);
|
||||
}
|
||||
g_free(unpacked);
|
||||
}
|
||||
|
||||
if (ok)
|
||||
ok = write_swap_script(kind);
|
||||
|
||||
g_free(up.source);
|
||||
up.source = source;
|
||||
g_free(target);
|
||||
g_free(parent);
|
||||
|
||||
if (ok)
|
||||
update_set_state(UPDATE_STAGED);
|
||||
else
|
||||
update_fail(tr("update_err_stage"));
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Download */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* curl writes straight to disk, so the partial file's size is the progress. */
|
||||
static gboolean download_poll(gpointer data) {
|
||||
GStatBuf st;
|
||||
|
||||
(void)data;
|
||||
if (up.state != UPDATE_DOWNLOADING) {
|
||||
up.poll_id = 0;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (up.progress && up.archive && g_stat(up.archive, &st) == 0) {
|
||||
if (up.total > 0)
|
||||
gtk_progress_bar_set_fraction(
|
||||
GTK_PROGRESS_BAR(up.progress),
|
||||
CLAMP((double)st.st_size / (double)up.total, 0.0, 1.0));
|
||||
else
|
||||
gtk_progress_bar_pulse(GTK_PROGRESS_BAR(up.progress));
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static void download_done(GObject *src, GAsyncResult *res, gpointer data) {
|
||||
GSubprocess *proc = G_SUBPROCESS(src);
|
||||
gboolean ok;
|
||||
|
||||
(void)data;
|
||||
g_subprocess_communicate_utf8_finish(proc, res, NULL, NULL, NULL);
|
||||
ok = g_subprocess_get_successful(proc);
|
||||
g_object_unref(proc);
|
||||
|
||||
if (up.poll_id) {
|
||||
g_source_remove(up.poll_id);
|
||||
up.poll_id = 0;
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
update_fail(tr("update_err_download"));
|
||||
return;
|
||||
}
|
||||
install_staged();
|
||||
}
|
||||
|
||||
/* Content-Length of the asset, read from the headers of every redirect hop.
|
||||
* The last one is the file itself; the hops before it report zero. curl has no
|
||||
* --write-out variable for this, since a HEAD downloads no body. */
|
||||
static goffset content_length_from_headers(const char *headers) {
|
||||
char **lines = g_strsplit(headers ? headers : "", "\n", -1);
|
||||
goffset total = 0;
|
||||
|
||||
for (guint i = 0; lines[i]; i++) {
|
||||
if (g_ascii_strncasecmp(lines[i], "content-length:", 15) == 0) {
|
||||
goffset v = (goffset)g_ascii_strtoll(lines[i] + 15, NULL, 10);
|
||||
|
||||
if (v > 0)
|
||||
total = v;
|
||||
}
|
||||
}
|
||||
g_strfreev(lines);
|
||||
return total;
|
||||
}
|
||||
|
||||
/* The asset size only makes the progress bar meaningful, so a failure here
|
||||
* costs nothing: the bar falls back to pulsing. */
|
||||
static void size_probe_done(GObject *src, GAsyncResult *res, gpointer data) {
|
||||
GSubprocess *proc = G_SUBPROCESS(src);
|
||||
char *out = NULL;
|
||||
char *url;
|
||||
const char *argv[8];
|
||||
|
||||
(void)data;
|
||||
g_subprocess_communicate_utf8_finish(proc, res, &out, NULL, NULL);
|
||||
up.total = content_length_from_headers(out);
|
||||
g_free(out);
|
||||
g_object_unref(proc);
|
||||
|
||||
if (up.state != UPDATE_DOWNLOADING)
|
||||
return;
|
||||
|
||||
url = g_strconcat(UPDATE_BASE_URL, asset_name(), NULL);
|
||||
argv[0] = "curl";
|
||||
argv[1] = "-fsSL";
|
||||
argv[2] = "--max-time";
|
||||
argv[3] = "1800";
|
||||
argv[4] = "-o";
|
||||
argv[5] = up.archive;
|
||||
argv[6] = url;
|
||||
argv[7] = NULL;
|
||||
|
||||
if (!curl_async(argv, download_done))
|
||||
update_fail(tr("update_err_curl"));
|
||||
else
|
||||
up.poll_id = g_timeout_add(250, download_poll, NULL);
|
||||
g_free(url);
|
||||
}
|
||||
|
||||
static void update_start_download(void) {
|
||||
char *target = NULL;
|
||||
char *parent = NULL;
|
||||
char *url;
|
||||
const char *argv[10];
|
||||
|
||||
if (install_target(&target, &parent) == INSTALL_UNSUPPORTED ||
|
||||
!asset_name()) {
|
||||
g_free(target);
|
||||
g_free(parent);
|
||||
update_fail(tr("update_err_unsupported"));
|
||||
return;
|
||||
}
|
||||
|
||||
/* Staging beside the install keeps the final move on one filesystem, and
|
||||
* fails here, before anything is downloaded, if we cannot write there. */
|
||||
g_free(up.staging);
|
||||
up.staging = g_build_filename(parent, UPDATE_STAGING_DIR, NULL);
|
||||
remove_tree(up.staging);
|
||||
g_free(target);
|
||||
g_free(parent);
|
||||
if (g_mkdir_with_parents(up.staging, 0755) != 0) {
|
||||
update_fail(tr("update_err_readonly"));
|
||||
return;
|
||||
}
|
||||
|
||||
g_free(up.archive);
|
||||
up.archive = g_build_filename(up.staging, asset_name(), NULL);
|
||||
up.total = 0;
|
||||
update_set_state(UPDATE_DOWNLOADING);
|
||||
|
||||
/* HEAD the asset with its headers on stdout, to size the progress bar. */
|
||||
url = g_strconcat(UPDATE_BASE_URL, asset_name(), NULL);
|
||||
argv[0] = "curl";
|
||||
argv[1] = "-fsSLI";
|
||||
argv[2] = "--max-time";
|
||||
argv[3] = "20";
|
||||
argv[4] = "-D";
|
||||
argv[5] = "-";
|
||||
argv[6] = "-o";
|
||||
argv[7] = NULL_DEVICE;
|
||||
argv[8] = url;
|
||||
argv[9] = NULL;
|
||||
|
||||
if (!curl_async(argv, size_probe_done))
|
||||
update_fail(tr("update_err_curl"));
|
||||
g_free(url);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Check */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* A failed background check stays silent; only an explicit one reports. */
|
||||
static void check_giveup(void) {
|
||||
if (up.interactive)
|
||||
update_fail(tr("update_err_network"));
|
||||
else
|
||||
update_set_state(UPDATE_IDLE);
|
||||
}
|
||||
|
||||
static void check_done(GObject *src, GAsyncResult *res, gpointer data) {
|
||||
GSubprocess *proc = G_SUBPROCESS(src);
|
||||
char *out = NULL;
|
||||
gboolean ok;
|
||||
|
||||
(void)data;
|
||||
g_subprocess_communicate_utf8_finish(proc, res, &out, NULL, NULL);
|
||||
ok = g_subprocess_get_successful(proc);
|
||||
g_object_unref(proc);
|
||||
|
||||
if (out)
|
||||
g_strstrip(out);
|
||||
if (!ok || !out || !*out) {
|
||||
g_free(out);
|
||||
check_giveup();
|
||||
return;
|
||||
}
|
||||
|
||||
g_free(up.latest);
|
||||
up.latest = out;
|
||||
|
||||
if (same_commit(APP_COMMIT, up.latest))
|
||||
update_set_state(up.interactive ? UPDATE_UP_TO_DATE : UPDATE_IDLE);
|
||||
else if (update_supported())
|
||||
update_set_state(UPDATE_AVAILABLE);
|
||||
else if (up.interactive)
|
||||
/* A newer build exists, but this copy cannot swap itself out. */
|
||||
update_fail(tr("update_err_unsupported"));
|
||||
else
|
||||
update_set_state(UPDATE_IDLE);
|
||||
}
|
||||
|
||||
void update_check_async(gboolean interactive) {
|
||||
const char *argv[8];
|
||||
|
||||
if (up.state == UPDATE_CHECKING || up.state == UPDATE_DOWNLOADING ||
|
||||
up.state == UPDATE_STAGED)
|
||||
return;
|
||||
|
||||
up.interactive = interactive;
|
||||
|
||||
/* Nothing to compare against without a baked-in commit. */
|
||||
if (!is_published_build()) {
|
||||
if (interactive)
|
||||
update_fail(tr("update_err_devbuild"));
|
||||
return;
|
||||
}
|
||||
|
||||
update_set_state(UPDATE_CHECKING);
|
||||
|
||||
/* The CDN in front of raw.githubusercontent.com caches for a few minutes,
|
||||
* so ask it not to hand back a stale commit. */
|
||||
argv[0] = "curl";
|
||||
argv[1] = "-fsSL";
|
||||
argv[2] = "--max-time";
|
||||
argv[3] = "20";
|
||||
argv[4] = "-H";
|
||||
argv[5] = "Cache-Control: no-cache";
|
||||
argv[6] = UPDATE_VERSION_URL;
|
||||
argv[7] = NULL;
|
||||
|
||||
if (!curl_async(argv, check_done)) {
|
||||
if (interactive)
|
||||
update_fail(tr("update_err_curl"));
|
||||
else
|
||||
update_set_state(UPDATE_IDLE);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Restart into the new version */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void update_restart(void) {
|
||||
char *target = NULL;
|
||||
char *parent = NULL;
|
||||
InstallKind kind = install_target(&target, &parent);
|
||||
char *pid = g_strdup_printf("%ld", update_getpid());
|
||||
GSubprocess *proc;
|
||||
const char *argv[8];
|
||||
int i = 0;
|
||||
|
||||
if (kind == INSTALL_UNSUPPORTED || !up.script || !up.source ||
|
||||
!up.staging) {
|
||||
g_free(pid);
|
||||
g_free(target);
|
||||
g_free(parent);
|
||||
update_fail(tr("update_err_stage"));
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
argv[i++] = "cmd";
|
||||
argv[i++] = "/c";
|
||||
#else
|
||||
argv[i++] = "/bin/sh";
|
||||
#endif
|
||||
argv[i++] = up.script;
|
||||
argv[i++] = pid;
|
||||
argv[i++] = target;
|
||||
argv[i++] = up.source;
|
||||
argv[i++] = up.staging;
|
||||
argv[i] = NULL;
|
||||
|
||||
/* The child outlives us on purpose: it is what puts the new version in
|
||||
* place once this process is gone. */
|
||||
proc = g_subprocess_newv(argv, G_SUBPROCESS_FLAGS_NONE, NULL);
|
||||
g_free(pid);
|
||||
g_free(target);
|
||||
g_free(parent);
|
||||
|
||||
if (!proc) {
|
||||
update_fail(tr("update_err_stage"));
|
||||
return;
|
||||
}
|
||||
g_object_unref(proc);
|
||||
|
||||
if (main_window) {
|
||||
GtkApplication *app = gtk_window_get_application(main_window);
|
||||
|
||||
if (app)
|
||||
g_application_quit(G_APPLICATION(app));
|
||||
else
|
||||
gtk_window_close(main_window);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Widgets */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static void on_update_action(GtkButton *btn, gpointer data) {
|
||||
(void)btn;
|
||||
(void)data;
|
||||
|
||||
switch (up.state) {
|
||||
case UPDATE_AVAILABLE:
|
||||
update_start_download();
|
||||
break;
|
||||
case UPDATE_STAGED:
|
||||
update_restart();
|
||||
break;
|
||||
case UPDATE_DOWNLOADING:
|
||||
case UPDATE_CHECKING:
|
||||
break;
|
||||
default:
|
||||
update_check_async(TRUE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_update_dismiss(GtkButton *btn, gpointer data) {
|
||||
(void)btn;
|
||||
(void)data;
|
||||
if (up.banner)
|
||||
gtk_widget_set_visible(up.banner, FALSE);
|
||||
}
|
||||
|
||||
/* Strip shown above the page stack once an update is waiting. */
|
||||
GtkWidget *build_update_banner(void) {
|
||||
GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 10);
|
||||
GtkWidget *label = gtk_label_new("");
|
||||
GtkWidget *action = gtk_button_new_with_label("");
|
||||
GtkWidget *later = gtk_button_new();
|
||||
|
||||
gtk_widget_add_css_class(bar, "update-banner");
|
||||
gtk_widget_set_visible(bar, FALSE);
|
||||
|
||||
gtk_widget_set_hexpand(label, TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(label), 0.0);
|
||||
gtk_widget_add_css_class(label, "update-banner-text");
|
||||
gtk_box_append(GTK_BOX(bar), label);
|
||||
|
||||
gtk_widget_add_css_class(action, "update-banner-btn");
|
||||
gtk_widget_set_valign(action, GTK_ALIGN_CENTER);
|
||||
g_signal_connect(action, "clicked", G_CALLBACK(on_update_action), NULL);
|
||||
gtk_box_append(GTK_BOX(bar), action);
|
||||
|
||||
i18n_bind(later, "update_later", 1);
|
||||
gtk_widget_add_css_class(later, "update-banner-later");
|
||||
gtk_widget_set_valign(later, GTK_ALIGN_CENTER);
|
||||
g_signal_connect(later, "clicked", G_CALLBACK(on_update_dismiss), NULL);
|
||||
gtk_box_append(GTK_BOX(bar), later);
|
||||
|
||||
up.banner = bar;
|
||||
up.banner_label = label;
|
||||
up.banner_action = action;
|
||||
return bar;
|
||||
}
|
||||
|
||||
/* "Updates" section of the settings popover. */
|
||||
GtkWidget *build_update_box(void) {
|
||||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
GtkWidget *status = gtk_label_new("");
|
||||
GtkWidget *action = gtk_button_new_with_label("");
|
||||
GtkWidget *progress = gtk_progress_bar_new();
|
||||
|
||||
gtk_widget_set_hexpand(status, TRUE);
|
||||
gtk_label_set_xalign(GTK_LABEL(status), 0.0);
|
||||
gtk_label_set_wrap(GTK_LABEL(status), TRUE);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(status), 30);
|
||||
gtk_widget_add_css_class(status, "update-status");
|
||||
gtk_box_append(GTK_BOX(row), status);
|
||||
|
||||
gtk_widget_add_css_class(action, "update-btn");
|
||||
gtk_widget_set_valign(action, GTK_ALIGN_CENTER);
|
||||
g_signal_connect(action, "clicked", G_CALLBACK(on_update_action), NULL);
|
||||
gtk_box_append(GTK_BOX(row), action);
|
||||
gtk_box_append(GTK_BOX(box), row);
|
||||
|
||||
gtk_widget_add_css_class(progress, "update-progress");
|
||||
gtk_widget_set_visible(progress, FALSE);
|
||||
gtk_box_append(GTK_BOX(box), progress);
|
||||
|
||||
up.status = status;
|
||||
up.action = action;
|
||||
up.progress = progress;
|
||||
update_refresh_ui();
|
||||
return box;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Startup */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
static gboolean update_startup_check(gpointer data) {
|
||||
(void)data;
|
||||
update_check_async(FALSE);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/* Leaves the first seconds to the interface, then looks for a new release. */
|
||||
void update_init(void) {
|
||||
update_clear_staging();
|
||||
g_timeout_add_seconds(3, update_startup_check, NULL);
|
||||
}
|
||||
333
src/util.c
Normal file
333
src/util.c
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
#include "maturita.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <unistd.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Portable data / progress paths */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
char *app_progress_dir;
|
||||
char *app_progress_u1;
|
||||
char *app_progress_u2;
|
||||
char *app_progress_u3;
|
||||
char *app_settings_file;
|
||||
char *app_progress_net;
|
||||
char *app_progress_hw;
|
||||
char *app_progress_mluvnice;
|
||||
char *app_progress_cetba;
|
||||
|
||||
static char *dup_dirname(const char *path) {
|
||||
char *tmp = g_strdup(path);
|
||||
char *dir;
|
||||
|
||||
if (!tmp)
|
||||
return NULL;
|
||||
dir = g_path_get_dirname(tmp);
|
||||
g_free(tmp);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/* Absolute path of this process's executable, or NULL. */
|
||||
char *app_executable_path(void) {
|
||||
#ifdef _WIN32
|
||||
wchar_t wbuf[MAX_PATH];
|
||||
DWORD n = GetModuleFileNameW(NULL, wbuf, MAX_PATH);
|
||||
char *out;
|
||||
|
||||
if (n == 0 || n >= MAX_PATH)
|
||||
return NULL;
|
||||
out = g_utf16_to_utf8((const gunichar2 *)wbuf, -1, NULL, NULL, NULL);
|
||||
return out;
|
||||
#elif defined(__APPLE__)
|
||||
char *buf;
|
||||
uint32_t size = 0;
|
||||
|
||||
_NSGetExecutablePath(NULL, &size);
|
||||
if (size == 0)
|
||||
return NULL;
|
||||
buf = g_malloc(size);
|
||||
if (_NSGetExecutablePath(buf, &size) != 0) {
|
||||
g_free(buf);
|
||||
return NULL;
|
||||
}
|
||||
{
|
||||
char *real = g_canonicalize_filename(buf, NULL);
|
||||
|
||||
g_free(buf);
|
||||
return real ? real : NULL;
|
||||
}
|
||||
#else
|
||||
return g_file_read_link("/proc/self/exe", NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static gboolean dir_has_stylesheet(const char *dir) {
|
||||
char *css;
|
||||
gboolean ok;
|
||||
|
||||
if (!dir || !dir[0])
|
||||
return FALSE;
|
||||
css = g_build_filename(dir, CSS_FILE, NULL);
|
||||
ok = g_file_test(css, G_FILE_TEST_IS_REGULAR);
|
||||
g_free(css);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static char *macos_resources_dir(const char *exe) {
|
||||
char *macos_dir;
|
||||
char *contents;
|
||||
char *resources;
|
||||
|
||||
if (!exe)
|
||||
return NULL;
|
||||
macos_dir = dup_dirname(exe);
|
||||
if (!macos_dir)
|
||||
return NULL;
|
||||
if (!g_str_has_suffix(macos_dir, "MacOS")) {
|
||||
g_free(macos_dir);
|
||||
return NULL;
|
||||
}
|
||||
contents = dup_dirname(macos_dir);
|
||||
g_free(macos_dir);
|
||||
if (!contents)
|
||||
return NULL;
|
||||
resources = g_build_filename(contents, "Resources", NULL);
|
||||
g_free(contents);
|
||||
if (!dir_has_stylesheet(resources)) {
|
||||
g_free(resources);
|
||||
return NULL;
|
||||
}
|
||||
return resources;
|
||||
}
|
||||
|
||||
/* Directory next to Foo.app when exe is .../Foo.app/Contents/MacOS/maturita. */
|
||||
static char *macos_app_parent_dir(const char *exe) {
|
||||
char *macos_dir;
|
||||
char *contents;
|
||||
char *app_bundle;
|
||||
char *parent;
|
||||
|
||||
if (!exe)
|
||||
return NULL;
|
||||
macos_dir = dup_dirname(exe);
|
||||
if (!macos_dir || !g_str_has_suffix(macos_dir, "MacOS")) {
|
||||
g_free(macos_dir);
|
||||
return NULL;
|
||||
}
|
||||
contents = dup_dirname(macos_dir);
|
||||
g_free(macos_dir);
|
||||
if (!contents)
|
||||
return NULL;
|
||||
app_bundle = dup_dirname(contents);
|
||||
g_free(contents);
|
||||
if (!app_bundle)
|
||||
return NULL;
|
||||
parent = dup_dirname(app_bundle);
|
||||
g_free(app_bundle);
|
||||
return parent;
|
||||
}
|
||||
|
||||
static void set_progress_paths(const char *write_root) {
|
||||
g_free(app_progress_dir);
|
||||
g_free(app_progress_u1);
|
||||
g_free(app_progress_u2);
|
||||
g_free(app_progress_u3);
|
||||
g_free(app_settings_file);
|
||||
g_free(app_progress_net);
|
||||
g_free(app_progress_hw);
|
||||
g_free(app_progress_mluvnice);
|
||||
g_free(app_progress_cetba);
|
||||
|
||||
app_progress_dir = g_build_filename(write_root, PROGRESS_DIR_NAME, NULL);
|
||||
app_progress_u1 = g_build_filename(app_progress_dir, "unit1.conf", NULL);
|
||||
app_progress_u2 = g_build_filename(app_progress_dir, "unit2.conf", NULL);
|
||||
app_progress_u3 = g_build_filename(app_progress_dir, "unit3.conf", NULL);
|
||||
app_settings_file = g_build_filename(app_progress_dir, "settings.conf", NULL);
|
||||
app_progress_net = g_build_filename(app_progress_dir, "net.conf", NULL);
|
||||
app_progress_hw = g_build_filename(app_progress_dir, "hw.conf", NULL);
|
||||
app_progress_mluvnice = g_build_filename(app_progress_dir, "mluvnice.conf", NULL);
|
||||
app_progress_cetba = g_build_filename(app_progress_dir, "cetba.conf", NULL);
|
||||
}
|
||||
|
||||
/* Resolve read-only asset root, chdir there, and point progress at a writable
|
||||
* location (next to the AppImage / .app, or beside the binary / source tree). */
|
||||
void setup_portable_paths(void) {
|
||||
const char *appdir = g_getenv("APPDIR");
|
||||
const char *appimage = g_getenv("APPIMAGE");
|
||||
char *exe = app_executable_path();
|
||||
char *exe_dir = exe ? dup_dirname(exe) : NULL;
|
||||
char *data_dir = NULL;
|
||||
char *write_root = NULL;
|
||||
char *cwd;
|
||||
|
||||
if (appdir && dir_has_stylesheet(appdir))
|
||||
data_dir = g_strdup(appdir);
|
||||
if (!data_dir)
|
||||
data_dir = macos_resources_dir(exe);
|
||||
if (!data_dir && exe_dir && dir_has_stylesheet(exe_dir))
|
||||
data_dir = g_strdup(exe_dir);
|
||||
|
||||
cwd = g_get_current_dir();
|
||||
if (!data_dir && dir_has_stylesheet(cwd))
|
||||
data_dir = g_strdup(cwd);
|
||||
if (!data_dir && cwd)
|
||||
data_dir = g_strdup(cwd);
|
||||
|
||||
if (appimage && appimage[0])
|
||||
write_root = dup_dirname(appimage);
|
||||
if (!write_root)
|
||||
write_root = macos_app_parent_dir(exe);
|
||||
if (!write_root && exe_dir)
|
||||
write_root = g_strdup(exe_dir);
|
||||
if (!write_root && data_dir)
|
||||
write_root = g_strdup(data_dir);
|
||||
if (!write_root)
|
||||
write_root = g_strdup(".");
|
||||
|
||||
set_progress_paths(write_root);
|
||||
|
||||
if (data_dir && g_chdir(data_dir) != 0)
|
||||
g_warning("Could not chdir to data directory '%s'", data_dir);
|
||||
|
||||
g_free(cwd);
|
||||
g_free(write_root);
|
||||
g_free(data_dir);
|
||||
g_free(exe_dir);
|
||||
g_free(exe);
|
||||
}
|
||||
|
||||
char *normalize_answer(const char *input) {
|
||||
GString *pre = g_string_new(NULL);
|
||||
GString *out = g_string_new(NULL);
|
||||
const gchar *down = g_utf8_strdown(input ? input : "", -1);
|
||||
gchar *decomp;
|
||||
const gchar *p;
|
||||
gboolean prev_space = FALSE;
|
||||
|
||||
/* German special letters first, so ä/ö/ü/ß keep their two-letter
|
||||
* keyboard fallbacks (ae/oe/ue/ss). */
|
||||
for (p = down; *p; p = g_utf8_next_char(p)) {
|
||||
gunichar ch = g_utf8_get_char(p);
|
||||
|
||||
switch (ch) {
|
||||
case 0x00e4: g_string_append(pre, "ae"); break; /* ä */
|
||||
case 0x00f6: g_string_append(pre, "oe"); break; /* ö */
|
||||
case 0x00fc: g_string_append(pre, "ue"); break; /* ü */
|
||||
case 0x00df: g_string_append(pre, "ss"); break; /* ß */
|
||||
default: g_string_append_unichar(pre, ch); break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decompose and drop combining accent marks so Czech letters such as
|
||||
* é/ě/ř/ů/ž/š/č are treated as their base letters. */
|
||||
decomp = g_utf8_normalize(pre->str, -1, G_NORMALIZE_NFD);
|
||||
if (!decomp)
|
||||
decomp = g_strdup(pre->str);
|
||||
|
||||
for (p = decomp; *p; p = g_utf8_next_char(p)) {
|
||||
gunichar ch = g_utf8_get_char(p);
|
||||
|
||||
if (g_unichar_combining_class(ch) != 0)
|
||||
continue; /* accent mark */
|
||||
|
||||
if (g_unichar_isspace(ch)) {
|
||||
if (out->len > 0 && !prev_space)
|
||||
g_string_append_c(out, ' ');
|
||||
prev_space = TRUE;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!g_unichar_isalnum(ch))
|
||||
continue; /* ignore punctuation and symbols */
|
||||
|
||||
prev_space = FALSE;
|
||||
g_string_append_unichar(out, ch);
|
||||
}
|
||||
|
||||
if (out->len > 0 && out->str[out->len - 1] == ' ')
|
||||
g_string_truncate(out, out->len - 1);
|
||||
|
||||
g_free(decomp);
|
||||
g_free((gpointer)down);
|
||||
g_string_free(pre, TRUE);
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
void shuffle_indices(int *arr, int n) {
|
||||
for (int i = n - 1; i > 0; i--) {
|
||||
int j = g_random_int_range(0, i + 1);
|
||||
int tmp = arr[i];
|
||||
arr[i] = arr[j];
|
||||
arr[j] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
void flow_clear(GtkFlowBox *fb) {
|
||||
GtkWidget *c = gtk_widget_get_first_child(GTK_WIDGET(fb));
|
||||
while (c) {
|
||||
GtkWidget *next = gtk_widget_get_next_sibling(c);
|
||||
gtk_flow_box_remove(fb, c);
|
||||
c = next;
|
||||
}
|
||||
}
|
||||
|
||||
void answer_mark(GtkWidget *widget, gboolean ok) {
|
||||
if (ok) {
|
||||
gtk_widget_remove_css_class(widget, "answer-wrong");
|
||||
gtk_widget_add_css_class(widget, "answer-ok");
|
||||
} else {
|
||||
gtk_widget_remove_css_class(widget, "answer-ok");
|
||||
gtk_widget_add_css_class(widget, "answer-wrong");
|
||||
}
|
||||
}
|
||||
|
||||
void set_feedback(GtkWidget *label, gboolean ok, const char *text) {
|
||||
gtk_widget_remove_css_class(label, "feedback-ok");
|
||||
gtk_widget_remove_css_class(label, "feedback-err");
|
||||
gtk_widget_remove_css_class(label, "feedback-warn");
|
||||
gtk_widget_add_css_class(label, ok ? "feedback-ok" : "feedback-err");
|
||||
gtk_label_set_text(GTK_LABEL(label), text);
|
||||
}
|
||||
|
||||
/* Yellow "almost there" note: close to a correct answer but incomplete. */
|
||||
void set_feedback_warn(GtkWidget *label, const char *text) {
|
||||
gtk_widget_remove_css_class(label, "feedback-ok");
|
||||
gtk_widget_remove_css_class(label, "feedback-err");
|
||||
gtk_widget_remove_css_class(label, "feedback-warn");
|
||||
gtk_widget_add_css_class(label, "feedback-warn");
|
||||
gtk_label_set_text(GTK_LABEL(label), text);
|
||||
}
|
||||
|
||||
/* True if `s` contains any German special letter (ä, ö, ü or ß). */
|
||||
gboolean text_has_german_umlaut(const char *s) {
|
||||
const char *p = s ? s : "";
|
||||
|
||||
while (*p) {
|
||||
gunichar c = g_utf8_get_char(p);
|
||||
|
||||
if (c == 0x00e4 || c == 0x00f6 || c == 0x00fc || c == 0x00df)
|
||||
return TRUE;
|
||||
p = g_utf8_next_char(p);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Small bilingual note shown under the exercise title whenever the answer
|
||||
* the student has to type may contain a German special letter. */
|
||||
void add_umlaut_note(GtkWidget *body) {
|
||||
GtkWidget *n = gtk_label_new(NULL);
|
||||
|
||||
i18n_bind(n, "hint_umlauts", 0);
|
||||
gtk_widget_set_halign(n, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(n), TRUE);
|
||||
gtk_widget_set_margin_bottom(n, 2);
|
||||
gtk_widget_add_css_class(n, "hint");
|
||||
gtk_box_append(GTK_BOX(body), n);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue