MAJOR REDESIGN, TURNED FROM A GERMAN LEARNING APP TO EVERYTHING IN HIGH SCHOOL BUT STILL LOCKED THE OTHER SUBJECTS SINCE THEYRE NOT FINISHED YET
This commit is contained in:
parent
4e907d8a14
commit
6ed5c5876b
4 changed files with 412 additions and 32 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,9 +1,10 @@
|
||||||
# ---> C
|
# ---> C
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
*.d
|
#.d
|
||||||
|
|
||||||
# binary
|
# binary
|
||||||
sprechen
|
sprechen
|
||||||
|
maturita
|
||||||
error.txt
|
error.txt
|
||||||
error.log
|
error.log
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|
|
||||||
4
Makefile
4
Makefile
|
|
@ -16,8 +16,8 @@ GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4)
|
||||||
|
|
||||||
CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS)
|
CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS)
|
||||||
LIBS = $(GTK_LIBS) -lm
|
LIBS = $(GTK_LIBS) -lm
|
||||||
TARGET = sprechen
|
TARGET = maturita
|
||||||
SRC = sprechen.c
|
SRC = maturita.c
|
||||||
|
|
||||||
all: $(TARGET)
|
all: $(TARGET)
|
||||||
|
|
||||||
|
|
|
||||||
44
README.md
44
README.md
|
|
@ -1,18 +1,24 @@
|
||||||
# Sprechen.c
|
# maturita.c
|
||||||
|
|
||||||
A German learning application written in C using GTK 4.
|
An educational application written in C using GTK 4.
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
|
|
||||||
Sprechen.c is an educational program designed for practicing German language
|
maturita.c is an educational program for high-school and gymnasium students
|
||||||
skills, aimed at high school and gymnasium students. The UI is available in
|
preparing for their maturita exam. The UI is available in Czech and English and
|
||||||
Czech and English and is built as a single C file (`sprechen.c`) that renders a
|
is built as a single C file (`maturita.c`) that renders a modern, CSS-styled
|
||||||
modern, CSS-styled GTK 4 interface.
|
GTK 4 interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Welcome screen** – branded greeting with a description of the program and a
|
- **Welcome screen** – branded greeting with a description of the program and a
|
||||||
continue button to get started
|
continue button to get started
|
||||||
|
- **Subjects ("Předměty")** – adaptive serpentine subject map with 13 subjects
|
||||||
|
- only **Deutsch** is open (round bubble with the German flag); picking it
|
||||||
|
opens the German learning path
|
||||||
|
- the other subjects are dimmed/locked placeholders until their content is
|
||||||
|
added
|
||||||
|
- layout folds into multiple rows when the window is narrow
|
||||||
- **Learning roadmap** – adaptive serpentine learning path with 10 units
|
- **Learning roadmap** – adaptive serpentine learning path with 10 units
|
||||||
- each unit numbered and labeled with its topic (e.g. "Neue Freunde")
|
- each unit numbered and labeled with its topic (e.g. "Neue Freunde")
|
||||||
- units 1 and 2 are open; completed units shown in green with a checkmark
|
- units 1 and 2 are open; completed units shown in green with a checkmark
|
||||||
|
|
@ -115,7 +121,7 @@ make
|
||||||
Or manually:
|
Or manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gcc -o sprechen sprechen.c `pkg-config --cflags --libs gtk4` -lm
|
gcc -o maturita maturita.c `pkg-config --cflags --libs gtk4` -lm
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the Application
|
### Running the Application
|
||||||
|
|
@ -127,22 +133,24 @@ make run
|
||||||
or
|
or
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./sprechen
|
./maturita
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
When launched, the application shows the welcome screen. From there:
|
When launched, the application shows the welcome screen. From there:
|
||||||
|
|
||||||
1. Click **"Pokračuj" / "Continue"** to open the learning roadmap.
|
1. Click **"Pokračuj" / "Continue"** to open the **Předměty / Subjects** map.
|
||||||
2. Browse the units along the path; the roadmap reflows into rows and scrolls
|
2. Click the **Deutsch** bubble (German flag) to open the German learning
|
||||||
|
roadmap. The other subjects are locked for now.
|
||||||
|
3. Browse the units along the path; the roadmap reflows into rows and scrolls
|
||||||
if the window is too narrow.
|
if the window is too narrow.
|
||||||
3. Click the **"Neue Freunde"** node to open unit 1 or **"Aus aller Welt"** to
|
4. Click the **"Neue Freunde"** node to open unit 1 or **"Aus aller Welt"** to
|
||||||
open unit 2, then pick any bubble to start an exercise. Unit 2 is available
|
open unit 2, then pick any bubble to start an exercise. Unit 2 is available
|
||||||
even if unit 1 is not finished yet.
|
even if unit 1 is not finished yet.
|
||||||
4. Use the **back button** in the top-left corner to return to the previous
|
5. Use the **back button** in the top-left corner to return to the previous
|
||||||
screen.
|
screen.
|
||||||
5. Open **Nastavení / Settings** (gear icon, top-right) to switch dark/light
|
6. Open **Nastavení / Settings** (gear icon, top-right) to switch dark/light
|
||||||
mode, pick a color theme, or change the interface language.
|
mode, pick a color theme, or change the interface language.
|
||||||
|
|
||||||
Units 1 and 2 are currently implemented. Locked units do nothing – they are
|
Units 1 and 2 are currently implemented. Locked units do nothing – they are
|
||||||
|
|
@ -160,7 +168,7 @@ to the app on first finish).
|
||||||
## Project structure
|
## Project structure
|
||||||
|
|
||||||
```
|
```
|
||||||
sprechen.c entire application (UI, navigation, themes, exercises)
|
maturita.c entire application (UI, navigation, themes, exercises)
|
||||||
Makefile build & run targets
|
Makefile build & run targets
|
||||||
README.md this file
|
README.md this file
|
||||||
LICENSE MIT license
|
LICENSE MIT license
|
||||||
|
|
@ -174,18 +182,18 @@ progress/ created at runtime
|
||||||
|
|
||||||
The app ships with ten palettes – Catppuccin, Nord, Dracula, Rose Pine, Ocean,
|
The app ships with ten palettes – Catppuccin, Nord, Dracula, Rose Pine, Ocean,
|
||||||
Gruvbox, Solarized, Everforest, Monokai and One Dark – each available in dark
|
Gruvbox, Solarized, Everforest, Monokai and One Dark – each available in dark
|
||||||
and light mode. Colors are defined as theme palettes in `sprechen.c` and
|
and light mode. Colors are defined as theme palettes in `maturita.c` and
|
||||||
applied through a generated CSS stylesheet (plus matching Cairo colors for the
|
applied through a generated CSS stylesheet (plus matching Cairo colors for the
|
||||||
roadmap rails and icons).
|
roadmap rails and icons).
|
||||||
|
|
||||||
Change the look from the in-app settings panel, or edit the palette tables /
|
Change the look from the in-app settings panel, or edit the palette tables /
|
||||||
`build_theme_css()` in `sprechen.c` and rebuild.
|
`build_theme_css()` in `maturita.c` and rebuild.
|
||||||
|
|
||||||
## Languages
|
## Languages
|
||||||
|
|
||||||
The interface can be switched between Czech and English from the settings
|
The interface can be switched between Czech and English from the settings
|
||||||
panel; the choice takes effect immediately and is remembered between runs.
|
panel; the choice takes effect immediately and is remembered between runs.
|
||||||
Translations live in two tables in `sprechen.c`: `tr_ui` holds interface
|
Translations live in two tables in `maturita.c`: `tr_ui` holds interface
|
||||||
strings under short keys, while `tr_content` translates the Czech meanings
|
strings under short keys, while `tr_content` translates the Czech meanings
|
||||||
shown next to the German exercise items and uses the Czech text itself as the
|
shown next to the German exercise items and uses the Czech text itself as the
|
||||||
key. Widgets are registered with `i18n_bind()` so `apply_language()` can
|
key. Widgets are registered with `i18n_bind()` so `apply_language()` can
|
||||||
|
|
@ -196,7 +204,7 @@ German in both languages.
|
||||||
|
|
||||||
## To-Do Plans
|
## To-Do Plans
|
||||||
|
|
||||||
Future enhancements planned for Sprechen.c:
|
Future enhancements planned for maturita.c:
|
||||||
|
|
||||||
- [ ] Wire unit nodes to actual German vocabulary/grammar exercises
|
- [ ] Wire unit nodes to actual German vocabulary/grammar exercises
|
||||||
- [x] Unit 1 ("Neue Freunde") – 13 interactive exercises with progress saving
|
- [x] Unit 1 ("Neue Freunde") – 13 interactive exercises with progress saving
|
||||||
|
|
|
||||||
|
|
@ -358,11 +358,11 @@ static const TrEntry tr_ui[] = {
|
||||||
{"theme", "TÉMA", "THEME"},
|
{"theme", "TÉMA", "THEME"},
|
||||||
{"language", "JAZYK", "LANGUAGE"},
|
{"language", "JAZYK", "LANGUAGE"},
|
||||||
{"welcome_body",
|
{"welcome_body",
|
||||||
"Sprechen.C je vzdělávací program na procvičování Němčiny, určený "
|
"maturita.C je vzdělávací program pro studenty středních škol a "
|
||||||
"pro studenty středních škol a gymnázií.\n\n"
|
"gymnázií na přípravu k maturitě.\n\n"
|
||||||
"Program je napsaný v Céčku studentama ze SSŠVT!",
|
"Program je napsaný v Céčku studentama ze SSŠVT!",
|
||||||
"Sprechen.C is an educational app for practicing German, made for "
|
"maturita.C is an educational app for high-school and gymnasium "
|
||||||
"high-school and gymnasium students.\n\n"
|
"students to prepare for their maturita exam.\n\n"
|
||||||
"The program is written in C by students from SSŠVT!"},
|
"The program is written in C by students from SSŠVT!"},
|
||||||
{"continue", "Pokračuj", "Continue"},
|
{"continue", "Pokračuj", "Continue"},
|
||||||
{"roadmap_title", "Učební plán", "Learning path"},
|
{"roadmap_title", "Učební plán", "Learning path"},
|
||||||
|
|
@ -458,6 +458,25 @@ static const TrEntry tr_ui[] = {
|
||||||
{"hm_done", "Výborně! Uhodli jste všechna povolání.",
|
{"hm_done", "Výborně! Uhodli jste všechna povolání.",
|
||||||
"Great! You guessed all the jobs."},
|
"Great! You guessed all the jobs."},
|
||||||
{"hm_guess", "Hádejte písmeno", "Guess a letter"},
|
{"hm_guess", "Hádejte písmeno", "Guess a letter"},
|
||||||
|
{"subjects_title", "Předměty", "Subjects"},
|
||||||
|
{"subjects_sub", "Zatím je dostupný předmět Deutsch.",
|
||||||
|
"Only Deutsch is available so far."},
|
||||||
|
{"Český jazyk a literatura", "Český jazyk a literatura",
|
||||||
|
"Czech Language and Literature"},
|
||||||
|
{"Občanská nauka", "Občanská nauka", "Civics"},
|
||||||
|
{"English", "English", "English"},
|
||||||
|
{"Deutsch", "Deutsch", "Deutsch"},
|
||||||
|
{"Matematika", "Matematika", "Mathematics"},
|
||||||
|
{"Fyzika", "Fyzika", "Physics"},
|
||||||
|
{"Základy Přírodopisných věd", "Základy Přírodopisných věd",
|
||||||
|
"Fundamentals of Natural Sciences"},
|
||||||
|
{"Technická grafika", "Technická grafika", "Technical Drawing"},
|
||||||
|
{"Prezentační grafika", "Prezentační grafika", "Presentation Graphics"},
|
||||||
|
{"Správa počítačových sítí", "Správa počítačových sítí",
|
||||||
|
"Computer Network Administration"},
|
||||||
|
{"Programování", "Programování", "Programming"},
|
||||||
|
{"Technické vybavení", "Technické vybavení", "Computer Hardware"},
|
||||||
|
{"Programové vybavení", "Programové vybavení", "Software"},
|
||||||
{NULL, NULL, NULL}
|
{NULL, NULL, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -753,11 +772,11 @@ static void refresh_welcome_heading(void) {
|
||||||
return;
|
return;
|
||||||
if (app_lang == LANG_EN)
|
if (app_lang == LANG_EN)
|
||||||
markup = g_strdup_printf(
|
markup = g_strdup_printf(
|
||||||
"Welcome to <span color=\"#%06x\">Sprechen.C</span>!",
|
"Welcome to <span color=\"#%06x\">maturita.C</span>!",
|
||||||
app_theme.accent);
|
app_theme.accent);
|
||||||
else
|
else
|
||||||
markup = g_strdup_printf(
|
markup = g_strdup_printf(
|
||||||
"Vítejte ve <span color=\"#%06x\">Sprechen.C</span>!",
|
"Vítejte ve <span color=\"#%06x\">maturita.C</span>!",
|
||||||
app_theme.accent);
|
app_theme.accent);
|
||||||
gtk_label_set_markup(GTK_LABEL(welcome_heading), markup);
|
gtk_label_set_markup(GTK_LABEL(welcome_heading), markup);
|
||||||
g_free(markup);
|
g_free(markup);
|
||||||
|
|
@ -1829,7 +1848,7 @@ static void on_nav_clicked(GtkButton *button, gpointer user_data) {
|
||||||
static void on_continue_clicked(GtkButton *button, gpointer user_data) {
|
static void on_continue_clicked(GtkButton *button, gpointer user_data) {
|
||||||
(void)button;
|
(void)button;
|
||||||
(void)user_data;
|
(void)user_data;
|
||||||
gtk_stack_set_visible_child_name(main_stack, "roadmap");
|
gtk_stack_set_visible_child_name(main_stack, "subjects");
|
||||||
}
|
}
|
||||||
|
|
||||||
static GtkWidget *make_back_button(const char *target) {
|
static GtkWidget *make_back_button(const char *target) {
|
||||||
|
|
@ -2347,7 +2366,7 @@ static GtkWidget *build_roadmap_page(void) {
|
||||||
gtk_widget_set_margin_bottom(page, 24);
|
gtk_widget_set_margin_bottom(page, 24);
|
||||||
|
|
||||||
gtk_box_append(GTK_BOX(page),
|
gtk_box_append(GTK_BOX(page),
|
||||||
top_bar("welcome", "roadmap_title", "roadmap_sub"));
|
top_bar("subjects", "roadmap_title", "roadmap_sub"));
|
||||||
|
|
||||||
scroll = gtk_scrolled_window_new();
|
scroll = gtk_scrolled_window_new();
|
||||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||||
|
|
@ -2391,6 +2410,357 @@ static GtkWidget *build_roadmap_page(void) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
/* Subjects page ("Předměty") - serpentine subject map */
|
||||||
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
#define NUM_SUBJECTS 13
|
||||||
|
#define SUB_BUBBLE NODE_SIZE /* round node, matches .unit-node */
|
||||||
|
#define SUB_MX 110.0
|
||||||
|
#define SUB_MY 150.0
|
||||||
|
#define SUB_SPAC 220.0
|
||||||
|
#define SUB_GAP 210.0
|
||||||
|
#define SUB_WAVE 34.0
|
||||||
|
|
||||||
|
/* Index 0 is Deutsch and is the only unlocked subject for now. */
|
||||||
|
static const char *sub_keys[NUM_SUBJECTS] = {
|
||||||
|
"Deutsch",
|
||||||
|
"Český jazyk a literatura",
|
||||||
|
"Občanská nauka",
|
||||||
|
"English",
|
||||||
|
"Matematika",
|
||||||
|
"Fyzika",
|
||||||
|
"Základy Přírodopisných věd",
|
||||||
|
"Technická grafika",
|
||||||
|
"Prezentační grafika",
|
||||||
|
"Správa počítačových sítí",
|
||||||
|
"Programování",
|
||||||
|
"Technické vybavení",
|
||||||
|
"Programové vybavení",
|
||||||
|
};
|
||||||
|
|
||||||
|
static GtkWidget *sub_scroll;
|
||||||
|
static GtkWidget *sub_fixed;
|
||||||
|
static GtkWidget *sub_rail;
|
||||||
|
static GtkWidget *sub_cells[NUM_SUBJECTS];
|
||||||
|
static GtkWidget *sub_labels[NUM_SUBJECTS];
|
||||||
|
static double sub_cx[NUM_SUBJECTS];
|
||||||
|
static double sub_cy[NUM_SUBJECTS];
|
||||||
|
static int sub_rows = 1;
|
||||||
|
static int sub_cols = NUM_SUBJECTS;
|
||||||
|
static int sub_cw = (int)(2.0 * SUB_MX + (NUM_SUBJECTS - 1) * SUB_SPAC);
|
||||||
|
static int sub_ch = (int)(2.0 * SUB_MY);
|
||||||
|
static guint sub_idle;
|
||||||
|
|
||||||
|
static 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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);
|
||||||
|
const Rgb mauve = color_from_hex(app_theme.accent);
|
||||||
|
double hx, hy;
|
||||||
|
|
||||||
|
(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);
|
||||||
|
|
||||||
|
sub_point(0.0, &hx, &hy);
|
||||||
|
draw_node_halo(cr, hx, hy, SUB_BUBBLE * 1.05,
|
||||||
|
mauve.r, mauve.g, mauve.b, 0.18);
|
||||||
|
|
||||||
|
cairo_new_path(cr);
|
||||||
|
sub_path(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
static gboolean sub_relayout_idle(gpointer data) {
|
||||||
|
(void)data;
|
||||||
|
sub_idle = 0;
|
||||||
|
sub_relayout();
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sub_relayout_later(void) {
|
||||||
|
if (sub_idle == 0)
|
||||||
|
sub_idle = g_idle_add(sub_relayout_idle, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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. */
|
||||||
|
static 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static 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", "subjects_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);
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* Unit 1 exercise path (roadmap-style serpentine) */
|
/* Unit 1 exercise path (roadmap-style serpentine) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
@ -6063,12 +6433,12 @@ static void activate(GtkApplication *app, gpointer user_data) {
|
||||||
|
|
||||||
window = GTK_WINDOW(gtk_application_window_new(app));
|
window = GTK_WINDOW(gtk_application_window_new(app));
|
||||||
main_window = window;
|
main_window = window;
|
||||||
gtk_window_set_title(window, "Sprechen.c");
|
gtk_window_set_title(window, "maturita.c");
|
||||||
gtk_window_set_default_size(window, 760, 560);
|
gtk_window_set_default_size(window, 760, 560);
|
||||||
|
|
||||||
headerbar = gtk_header_bar_new();
|
headerbar = gtk_header_bar_new();
|
||||||
gtk_widget_add_css_class(headerbar, "titlebar");
|
gtk_widget_add_css_class(headerbar, "titlebar");
|
||||||
title_label = gtk_label_new("Sprechen.c");
|
title_label = gtk_label_new("maturita.c");
|
||||||
gtk_widget_add_css_class(title_label, "app-title");
|
gtk_widget_add_css_class(title_label, "app-title");
|
||||||
gtk_header_bar_set_title_widget(GTK_HEADER_BAR(headerbar), title_label);
|
gtk_header_bar_set_title_widget(GTK_HEADER_BAR(headerbar), title_label);
|
||||||
gtk_window_set_titlebar(window, headerbar);
|
gtk_window_set_titlebar(window, headerbar);
|
||||||
|
|
@ -6083,6 +6453,7 @@ static void activate(GtkApplication *app, gpointer user_data) {
|
||||||
welcome_page = build_welcome_page();
|
welcome_page = build_welcome_page();
|
||||||
roadmap_page = build_roadmap_page();
|
roadmap_page = build_roadmap_page();
|
||||||
gtk_stack_add_named(main_stack, welcome_page, "welcome");
|
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, roadmap_page, "roadmap");
|
||||||
|
|
||||||
for (int i = 0; i < NUM_UNLOCKED; i++) {
|
for (int i = 0; i < NUM_UNLOCKED; i++) {
|
||||||
|
|
@ -6123,7 +6494,7 @@ int main(int argc, char **argv) {
|
||||||
GtkApplication *app;
|
GtkApplication *app;
|
||||||
int status;
|
int status;
|
||||||
|
|
||||||
app = gtk_application_new("org.example.Sprechen", G_APPLICATION_DEFAULT_FLAGS);
|
app = gtk_application_new("org.example.maturita", G_APPLICATION_DEFAULT_FLAGS);
|
||||||
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
||||||
status = g_application_run(G_APPLICATION(app), argc, argv);
|
status = g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
g_object_unref(app);
|
g_object_unref(app);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue