diff --git a/hw.c b/hw.c new file mode 100644 index 0000000..59b7de7 --- /dev/null +++ b/hw.c @@ -0,0 +1,765 @@ +#include "maturita.h" + +/* ---- Technical equipment (Technické vybavení) --------------------- */ + +NetLesson hw_lessons[HW_LESSONS] = { + { .n_slides = HW_SLIDES, .unit_page = "hwunit1", .ex_page = "hwex1" }, +}; + +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 mauve = color_from_hex(app_theme.accent); + const Rgb rail = color_from_hex(app_theme.rail); + const Rgb muted = color_from_hex(app_theme.subtext); + double hx, hy; + + cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR); + cairo_paint(cr); + cairo_set_operator(cr, CAIRO_OPERATOR_OVER); + cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND); + cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND); + + hw_point(0.0, &hx, &hy); + draw_node_halo(cr, hx, hy, NODE_SIZE * 1.05, + mauve.r, mauve.g, mauve.b, 0.18); + + cairo_new_path(cr); + hw_path(cr, 0.0, t_end); + cairo_set_line_width(cr, 16); + cairo_set_source_rgb(cr, rail.r, rail.g, rail.b); + cairo_stroke(cr); + + cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17); + cairo_set_dash(cr, (double[]){2.0, 40.0}, 2, 0.0); + cairo_new_path(cr); + 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" + }; + + 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_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", + "ALS = aritmeticko-logická jednotka", + { + "Von Neumannovo schéma má pět hlavních částí:", + "ALS – 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, 10); + 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, 6); + gtk_widget_add_css_class(head, "ex-sub"); + 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, "ex-prompt"); + 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 *prompt; + GtkWidget *row; + GtkToggleButton *first = NULL; + char *qtext; + + qtext = g_strdup_printf("%d.) %s", i + 1, qs[i].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_widget_add_css_class(prompt, "ex-prompt"); + gtk_widget_set_margin_top(prompt, 8); + gtk_box_append(GTK_BOX(body), prompt); + + row = gtk_flow_box_new(); + gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(row), GTK_SELECTION_NONE); + gtk_flow_box_set_min_children_per_line(GTK_FLOW_BOX(row), n_opts); + gtk_flow_box_set_max_children_per_line(GTK_FLOW_BOX(row), n_opts); + gtk_widget_set_halign(row, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(body), row); + + for (int o = 0; o < n_opts; o++) { + GtkWidget *tb = gtk_toggle_button_new_with_label(qs[i].options[o]); + gtk_widget_add_css_class(tb, "pill"); + gtk_toggle_button_set_group(GTK_TOGGLE_BUTTON(tb), first); + if (!first) + first = GTK_TOGGLE_BUTTON(tb); + gtk_flow_box_append(GTK_FLOW_BOX(row), tb); + ctx->toggles[i * n_opts + o] = GTK_TOGGLE_BUTTON(tb); + } + ctx->hints[i] = meaning_add(body, hints[i]); + } + + btns = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); + gtk_widget_set_margin_top(btns, 12); + gtk_box_append(GTK_BOX(body), btns); + + check = gtk_button_new(); + i18n_bind(check, "check", 1); + gtk_widget_add_css_class(check, "pill"); + gtk_widget_set_halign(check, GTK_ALIGN_START); + gtk_box_append(GTK_BOX(btns), check); + g_signal_connect(check, "clicked", G_CALLBACK(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 ALS?", + {"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 ALS"}, + 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", + "ALS = 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); +} diff --git a/i18n.c b/i18n.c index 878cc7c..d14000f 100644 --- a/i18n.c +++ b/i18n.c @@ -281,6 +281,15 @@ const TrEntry tr_ui[] = { {"net_slide_prev", "Zpět", "Back"}, {"net_slide_start", "Přejít na cvičení", "Go to the exercise"}, {"net_solution", "Ukázat řešení", "Show solution"}, + {"hw_sub", "Vyberte lekci na cestě a začněte procvičovat.", + "Pick a unit on the path and start practicing."}, + {"hw_unit1", "Architektura počítače", "Computer architecture"}, + {"hw_unit1_sub", "3 snímky o Von Neumannovi a schématu a pak kvíz.", + "3 slides about von Neumann and the model, then a quiz."}, + {"hw_ex1_title", "Kvíz: architektura počítače", + "Quiz: computer architecture"}, + {"hw_quiz1_head", "Kvíz k architektuře počítače", + "Quiz on computer architecture"}, {NULL, NULL, NULL} }; @@ -917,6 +926,17 @@ const TrEntry tr_content[] = { {"Ahoj! (při loučení)", NULL, "Bye!"}, {"shledání", NULL, "reunion"}, {"Na shledanou!", NULL, "Goodbye!"}, + {"John von Neumann – americký matematik", NULL, + "John von Neumann – American mathematician"}, + {"Americký matematik s objevy v digitálních počítačích", NULL, + "American mathematician with discoveries in digital computers"}, + {"ALS = aritmeticko-logická jednotka", NULL, + "ALU = arithmetic-logic unit"}, + {"Řadič řídí činnost ostatních částí", NULL, + "The control unit directs the other parts"}, + {"Operační paměť uchovává program i data", NULL, + "Main memory stores both the program and data"}, + {"Byl tvůrcem teorie her", NULL, "He created game theory"}, {NULL, NULL, NULL} }; @@ -1006,6 +1026,7 @@ void refresh_welcome_heading(void) { void refresh_stats_ui(void); void net_lessons_apply_lang(void); +void hw_lessons_apply_lang(void); void net_rail_theme_reset(void); void apply_language(void) { @@ -1017,4 +1038,5 @@ void apply_language(void) { refresh_welcome_heading(); refresh_stats_ui(); net_lessons_apply_lang(); + hw_lessons_apply_lang(); } diff --git a/icons.c b/icons.c index bc7cb30..79ff688 100644 --- a/icons.c +++ b/icons.c @@ -94,6 +94,54 @@ void draw_wifi_icon(GtkDrawingArea *area, cairo_t *cr, 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); + } +} + void draw_back_icon(GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data) { const double size = 15.0; /* fixed logical icon size */ diff --git a/main.c b/main.c index 776c319..f77d7c3 100644 --- a/main.c +++ b/main.c @@ -171,6 +171,9 @@ void activate(GtkApplication *app, gpointer user_data) { gtk_stack_add_named(main_stack, roadmap_page, "roadmap"); gtk_stack_add_named(main_stack, build_stats_page(), "stats"); gtk_stack_add_named(main_stack, build_netmap_page(), "netmap"); + gtk_stack_add_named(main_stack, build_hwmap_page(), "hwmap"); + gtk_stack_add_named(main_stack, build_hw_unit1_page(), "hwunit1"); + gtk_stack_add_named(main_stack, build_hw_unit1_exercise_page(), "hwex1"); { typedef GtkWidget *(*NetBuilder)(void); static const struct { diff --git a/maturita.h b/maturita.h index 3433600..513c5fc 100644 --- a/maturita.h +++ b/maturita.h @@ -12,6 +12,7 @@ #define MAX_UNIT_EX 20 /* largest exercise count any unit may have */ #define NUM_SUBJECTS 13 #define NET_SUBJ 1 /* index of "Správa počítačových sítí" */ +#define HW_SUBJ 11 /* index of "Technické vybavení" */ #define CSS_FILE "style.css" #define PROGRESS_DIR "progress" #define PROGRESS_U1 "progress/unit1.conf" @@ -19,6 +20,7 @@ #define PROGRESS_U3 "progress/unit3.conf" #define SETTINGS_FILE "progress/settings.conf" #define PROGRESS_NET "progress/net.conf" +#define PROGRESS_HW "progress/hw.conf" #define NODE_SIZE 88.0 #define PATH_SPAC 240.0 #define ROAD_MX 150.0 /* horizontal canvas margin */ @@ -56,6 +58,9 @@ #define NET9_SLIDES 4 #define NET10_SLIDES 3 #define NET_LESSONS 10 +#define HW_UNITS 10 +#define HW_LESSONS 1 +#define HW_SLIDES 3 typedef enum { THEME_CATPPUCCIN = 0, @@ -668,6 +673,9 @@ extern const U3Fill ex13_u3_rows[]; extern const U3Kw ex14_u3_qs[]; extern const U3Let ex15_u3_rows[]; extern NetLesson net_lessons[NET_LESSONS]; +extern NetLesson *net_notes_target; +extern NetLesson hw_lessons[HW_LESSONS]; +extern GtkWidget *hw_nodes[HW_UNITS]; extern GtkWidget *net_note_host; extern int net_note_last_w; extern GtkWidget *net_scroll; @@ -745,6 +753,20 @@ void net_save_progress(void); void mark_net_done(int lesson_id); void refresh_net_completion_ui(void); void progress_for_net(ProgressSum *out); +void hw_load_progress(void); +void hw_save_progress(void); +void mark_hw_done(int lesson_id); +void refresh_hw_completion_ui(void); +void progress_for_hw(ProgressSum *out); +void hw_rail_theme_reset(void); +void hw_lessons_apply_lang(void); +GtkWidget *build_hwmap_page(void); +GtkWidget *build_hw_unit1_page(void); +GtkWidget *build_hw_unit1_exercise_page(void); +void draw_chip_icon(GtkDrawingArea *area, cairo_t *cr, + int width, int height, gpointer data); +void net_lesson_notes_ensure(NetLesson *L); +void net_rescale_lesson_notes(NetLesson *L, int body, int head, int kick); GtkWidget *make_back_button(const char *target); void on_nav_clicked(GtkButton *button, gpointer user_data); int unit_done_count(const UnitCtx *u); diff --git a/net.c b/net.c index 3114b72..da06d7e 100644 --- a/net.c +++ b/net.c @@ -40,7 +40,7 @@ void net_heading(GtkWidget *box, const char *text) { /* Note-card slides with text that scales with the window. */ GtkWidget *net_note_host; int net_note_last_w = -1; -static NetLesson *net_notes_target; +NetLesson *net_notes_target; void net_lesson_notes_ensure(NetLesson *L) { if (L->note_kicks) diff --git a/progress.c b/progress.c index 6ac3437..377f7f5 100644 --- a/progress.c +++ b/progress.c @@ -53,6 +53,7 @@ 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. */ @@ -124,6 +125,7 @@ void refresh_completion_ui(void) { all_rails_redraw(); refresh_net_completion_ui(); + refresh_hw_completion_ui(); refresh_stats_ui(); } @@ -225,3 +227,94 @@ void progress_for_net(ProgressSum *out) { } } } + +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++; + } + } +} diff --git a/stats.c b/stats.c index e31ae56..08fe40c 100644 --- a/stats.c +++ b/stats.c @@ -56,6 +56,8 @@ void refresh_stats_ui(void) { 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 progress_for_units(sub_unit_start[s], sub_unit_count[s], &all); } @@ -83,12 +85,15 @@ void refresh_stats_ui(void) { GtkWidget *count = stats_ui.subj[s].count; GtkWidget *bar = stats_ui.subj[s].bar; ProgressSum sp = {0}; - gboolean has_content = (s == NET_SUBJ) || sub_unit_count[s] > 0; + gboolean has_content = (s == NET_SUBJ) || (s == HW_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 progress_for_units(sub_unit_start[s], sub_unit_count[s], &sp); if (has_content) { @@ -353,7 +358,8 @@ GtkWidget *build_stats_page(void) { GtkWidget *name; GtkWidget *count; GtkWidget *bar; - gboolean has_units = (s == NET_SUBJ) || sub_unit_count[s] > 0; + gboolean has_units = (s == NET_SUBJ) || (s == HW_SUBJ) + || sub_unit_count[s] > 0; gboolean has_deutsch_units = sub_unit_count[s] > 0; subject = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); diff --git a/subjects.c b/subjects.c index 0ef397b..ed363f4 100644 --- a/subjects.c +++ b/subjects.c @@ -5,7 +5,7 @@ /* ------------------------------------------------------------------ */ -/* Index 0 (Deutsch) and NET_SUBJ (computer networks) are unlocked. */ +/* Index 0 (Deutsch), NET_SUBJ (networks) and HW_SUBJ (hardware) are unlocked. */ const char *sub_keys[NUM_SUBJECTS] = { "Deutsch", "Správa počítačových sítí", @@ -326,6 +326,17 @@ GtkWidget *build_subjects_page(void) { g_object_set_data_full(G_OBJECT(card), "target", g_strdup("netmap"), g_free); g_signal_connect(card, "clicked", G_CALLBACK(on_nav_clicked), NULL); + } else 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 { GtkWidget *lock; gtk_widget_add_css_class(card, "locked"); @@ -345,7 +356,7 @@ GtkWidget *build_subjects_page(void) { 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) + if (i != 0 && i != NET_SUBJ && i != HW_SUBJ) gtk_widget_add_css_class(lbl, "unit-name-locked"); sub_labels[i] = lbl; gtk_fixed_put(GTK_FIXED(fixed), lbl, 0, 0); diff --git a/theme.c b/theme.c index 8fabb00..c8b8b93 100644 --- a/theme.c +++ b/theme.c @@ -359,4 +359,5 @@ void apply_theme(void) { gtk_widget_queue_draw(theme_swatch_areas[i]); } net_rail_theme_reset(); + hw_rail_theme_reset(); }