Move the GTK application sources into desktop/.
Co-authored-by: Cursor <cursoragent@cursor.com>
88
desktop/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(maturita C)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# GTK4 is found through pkg-config everywhere: Linux/macOS packages, MSYS2,
|
||||
# vcpkg and the native MSVC (gvsbuild) package all ship .pc files.
|
||||
if(MSVC)
|
||||
# On MSVC the Unix-style flags pkg-config prints are not understood, so
|
||||
# ask for the native "/I" and ".lib" form instead.
|
||||
find_program(PKG_CONFIG_EXECUTABLE
|
||||
NAMES pkg-config pkg-config.exe pkgconf pkgconf.exe)
|
||||
if(NOT PKG_CONFIG_EXECUTABLE)
|
||||
message(FATAL_ERROR
|
||||
"pkg-config was not found. A native GTK4 install for MSVC "
|
||||
"(e.g. gvsbuild) provides it; set PKG_CONFIG_PATH to "
|
||||
"<GTK>/lib/pkgconfig. See README.md.")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND "${PKG_CONFIG_EXECUTABLE}" --cflags gtk4 --msvc-syntax
|
||||
OUTPUT_VARIABLE GTK4_CFLAGS_RAW
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE GTK4_CFLAGS_RESULT)
|
||||
execute_process(
|
||||
COMMAND "${PKG_CONFIG_EXECUTABLE}" --libs gtk4 --msvc-syntax
|
||||
OUTPUT_VARIABLE GTK4_LIBS_RAW
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
RESULT_VARIABLE GTK4_LIBS_RESULT)
|
||||
|
||||
if(NOT GTK4_CFLAGS_RESULT EQUAL 0 OR NOT GTK4_LIBS_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR
|
||||
"pkg-config could not find GTK4. Set PKG_CONFIG_PATH to "
|
||||
"<GTK>/lib/pkgconfig (for example C:\\gtk\\lib\\pkgconfig).")
|
||||
endif()
|
||||
|
||||
separate_arguments(GTK4_CFLAGS WINDOWS_COMMAND "${GTK4_CFLAGS_RAW}")
|
||||
separate_arguments(GTK4_LIBS WINDOWS_COMMAND "${GTK4_LIBS_RAW}")
|
||||
else()
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK4 REQUIRED IMPORTED_TARGET gtk4)
|
||||
endif()
|
||||
|
||||
# WIN32 suppresses the console window on Windows; the app still uses main(),
|
||||
# so MSVC needs mainCRTStartup as the entry point.
|
||||
file(GLOB MATURITA_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.c")
|
||||
if(WIN32)
|
||||
add_executable(maturita WIN32 ${MATURITA_SOURCES}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/maturita.rc")
|
||||
else()
|
||||
add_executable(maturita ${MATURITA_SOURCES})
|
||||
endif()
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(maturita PRIVATE ${GTK4_CFLAGS})
|
||||
target_link_options(maturita PRIVATE ${GTK4_LIBS})
|
||||
target_link_options(maturita PRIVATE /ENTRY:mainCRTStartup)
|
||||
target_compile_definitions(maturita PRIVATE _CRT_SECURE_NO_WARNINGS)
|
||||
else()
|
||||
target_link_libraries(maturita PRIVATE PkgConfig::GTK4 m)
|
||||
target_compile_options(maturita PRIVATE
|
||||
-Wall -Wextra -Wno-deprecated-declarations)
|
||||
if(APPLE)
|
||||
target_link_libraries(maturita PRIVATE
|
||||
"-framework AppKit" "-framework Foundation")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# The GTK stylesheet, window icon and Freedesktop share/ tree are loaded at
|
||||
# runtime from the working directory (or next to the binary).
|
||||
add_custom_command(TARGET maturita POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/style.css
|
||||
$<TARGET_FILE_DIR:maturita>/style.css
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
$<TARGET_FILE_DIR:maturita>/icons/hicolor/512x512/apps
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.png
|
||||
$<TARGET_FILE_DIR:maturita>/icons/hicolor/512x512/apps/maturita.png
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.png
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps/maturita.png
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/share
|
||||
$<TARGET_FILE_DIR:maturita>/share)
|
||||
101
desktop/Makefile
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
CC = gcc
|
||||
|
||||
# Make sure pkg-config is found even when make runs with a minimal PATH
|
||||
# (e.g. launched from an IDE such as CLion on macOS).
|
||||
PKG_CONFIG ?= $(firstword $(wildcard \
|
||||
$(addsuffix /pkg-config,$(subst :, ,$(PATH)) /opt/homebrew/bin /usr/local/bin)))
|
||||
ifeq ($(PKG_CONFIG),)
|
||||
PKG_CONFIG := pkg-config
|
||||
endif
|
||||
|
||||
# Use $(shell ...) instead of backticks so the flags are expanded by make
|
||||
# itself; IDEs that import the Makefile via a dry run then see the real
|
||||
# include paths.
|
||||
GTK_CFLAGS := $(shell $(PKG_CONFIG) --cflags gtk4)
|
||||
GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4)
|
||||
|
||||
CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS)
|
||||
LIBS = $(GTK_LIBS) -lm
|
||||
|
||||
# Windows (MSYS2 / MinGW): .exe suffix, no console window, embed .ico.
|
||||
ifeq ($(OS),Windows_NT)
|
||||
TARGET = maturita.exe
|
||||
LIBS += -mwindows
|
||||
WINDRES ?= windres
|
||||
RC_OBJ = maturita_rc.o
|
||||
UNAME_S :=
|
||||
else
|
||||
TARGET = maturita
|
||||
RC_OBJ =
|
||||
UNAME_S := $(shell uname -s 2>/dev/null)
|
||||
# macOS Dock icon is set via AppKit (see macos_dock.c).
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
LIBS += -framework AppKit -framework Foundation
|
||||
endif
|
||||
endif
|
||||
|
||||
SRC = $(wildcard *.c)
|
||||
OBJ = $(SRC:.c=.o)
|
||||
|
||||
ICON_SRC = assets/app-icon.png
|
||||
# Always use the real 512×512 asset — copying app-icon.png (1024×1024) breaks
|
||||
# linuxdeploy, which rejects non-standard icon resolutions.
|
||||
ICON_DST = share/icons/hicolor/512x512/apps/maturita.png
|
||||
ICON_RUNTIME = assets/icons/hicolor/512x512/apps/maturita.png
|
||||
ICON_ICO = assets/app-icon.ico
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(ICON_RUNTIME): $(ICON_DST)
|
||||
mkdir -p $(dir $@)
|
||||
cp "$(ICON_DST)" "$@"
|
||||
|
||||
ifeq ($(OS),Windows_NT)
|
||||
$(RC_OBJ): maturita.rc $(ICON_ICO)
|
||||
$(WINDRES) maturita.rc -o $(RC_OBJ)
|
||||
|
||||
$(TARGET): $(SRC) $(ICON_RUNTIME) $(RC_OBJ)
|
||||
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(RC_OBJ) $(LIBS)
|
||||
else
|
||||
$(TARGET): $(SRC) $(ICON_RUNTIME)
|
||||
$(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LIBS)
|
||||
endif
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET) maturita.exe $(OBJ) $(RC_OBJ) maturita_rc.o
|
||||
|
||||
run: $(TARGET)
|
||||
./$(TARGET)
|
||||
|
||||
# Self-contained package for the current platform into dist/.
|
||||
ifeq ($(OS),Windows_NT)
|
||||
bundle: $(TARGET)
|
||||
@rm -rf dist
|
||||
@mkdir -p dist
|
||||
@cp $(TARGET) dist/
|
||||
@cp style.css dist/
|
||||
@mkdir -p dist/icons/hicolor/512x512/apps
|
||||
@cp "$(ICON_DST)" dist/icons/hicolor/512x512/apps/
|
||||
@cp -R share dist/
|
||||
@ldd $(TARGET) | grep -Ei '/(ucrt64|mingw64)/bin/' | awk '{print $$3}' \
|
||||
| xargs -r -I{} cp -f {} dist/
|
||||
@# GTK runtime data (schemas / pixbuf loaders) next to the exe.
|
||||
@GTK_PREFIX="$$($(PKG_CONFIG) --variable=prefix gtk4)"; \
|
||||
if [ -d "$$GTK_PREFIX/share/glib-2.0" ]; then \
|
||||
mkdir -p dist/share && cp -R "$$GTK_PREFIX/share/glib-2.0" dist/share/; \
|
||||
fi; \
|
||||
if [ -d "$$GTK_PREFIX/lib/gdk-pixbuf-2.0" ]; then \
|
||||
mkdir -p dist/lib && cp -R "$$GTK_PREFIX/lib/gdk-pixbuf-2.0" dist/lib/; \
|
||||
fi
|
||||
@echo "Bundled into dist/ - run dist/$(TARGET)"
|
||||
else ifeq ($(UNAME_S),Darwin)
|
||||
bundle:
|
||||
@chmod +x scripts/bundle-macos.sh
|
||||
@./scripts/bundle-macos.sh
|
||||
else
|
||||
bundle:
|
||||
@chmod +x scripts/bundle-linux.sh
|
||||
@./scripts/bundle-linux.sh
|
||||
endif
|
||||
|
||||
.PHONY: all clean run bundle
|
||||
BIN
desktop/assets/app-icon.ico
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
desktop/assets/app-icon.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
BIN
desktop/assets/germanlection.png
Normal file
|
After Width: | Height: | Size: 339 KiB |
BIN
desktop/assets/subjectsscreen.png
Normal file
|
After Width: | Height: | Size: 442 KiB |
BIN
desktop/assets/welcomescreen.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
100
desktop/build-windows.bat
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
@echo off
|
||||
setlocal EnableExtensions
|
||||
|
||||
REM ---------------------------------------------------------------------------
|
||||
REM Native Windows build with MSVC. No MSYS2 or MinGW shell required.
|
||||
REM
|
||||
REM Requirements:
|
||||
REM * Visual Studio 2019/2022 with the "Desktop development with C++"
|
||||
REM workload installed.
|
||||
REM * A GTK4 build for MSVC, for example the gvsbuild prebuilt package
|
||||
REM unzipped to C:\gtk (see README.md).
|
||||
REM
|
||||
REM If GTK lives somewhere other than C:\gtk, set GTK_ROOT first, e.g.
|
||||
REM set GTK_ROOT=D:\gtk
|
||||
REM ---------------------------------------------------------------------------
|
||||
|
||||
if not defined GTK_ROOT set "GTK_ROOT=C:\gtk"
|
||||
|
||||
REM --- 1. Locate Visual Studio and enter the 64-bit MSVC environment --------
|
||||
set "VSWHERE=%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if not exist "%VSWHERE%" (
|
||||
echo [error] vswhere.exe not found.
|
||||
echo Install Visual Studio with the "Desktop development with C++"
|
||||
echo workload, then run this script again.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
set "VSPATH="
|
||||
for /f "usebackq tokens=*" %%i in (`"%VSWHERE%" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`) do set "VSPATH=%%i"
|
||||
if not defined VSPATH (
|
||||
echo [error] No Visual Studio C++ installation found.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat" >nul
|
||||
if errorlevel 1 (
|
||||
echo [error] Failed to initialise the MSVC environment.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM --- 2. Point the build at the GTK4 installation --------------------------
|
||||
set "PATH=%GTK_ROOT%\bin;%PATH%"
|
||||
set "PKG_CONFIG_PATH=%GTK_ROOT%\lib\pkgconfig;%PKG_CONFIG_PATH%"
|
||||
|
||||
set "PKG_CONFIG=pkg-config"
|
||||
where pkg-config >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
where pkgconf >nul 2>nul
|
||||
if errorlevel 1 (
|
||||
echo [error] pkg-config/pkgconf was not found.
|
||||
echo Make sure GTK_ROOT points at a GTK4 install that contains
|
||||
echo bin\pkg-config.exe. Current GTK_ROOT: %GTK_ROOT%
|
||||
exit /b 1
|
||||
)
|
||||
set "PKG_CONFIG=pkgconf"
|
||||
)
|
||||
|
||||
REM --- 3. Collect the GTK4 flags in MSVC syntax -----------------------------
|
||||
set "GTK_CFLAGS="
|
||||
set "GTK_LIBS="
|
||||
for /f "usebackq delims=" %%c in (`%PKG_CONFIG% --cflags gtk4 --msvc-syntax`) do set "GTK_CFLAGS=%%c"
|
||||
for /f "usebackq delims=" %%l in (`%PKG_CONFIG% --libs gtk4 --msvc-syntax`) do set "GTK_LIBS=%%l"
|
||||
|
||||
if not defined GTK_CFLAGS (
|
||||
echo [error] pkg-config could not find gtk4.
|
||||
echo Check that "%PKG_CONFIG_PATH%" contains gtk4.pc.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "assets\app-icon.ico" (
|
||||
echo [error] assets\app-icon.ico is missing.
|
||||
echo Generate it with: python scripts\gen-icons.py
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM --- 4. Compile resources + sources ---------------------------------------
|
||||
echo Building maturita.exe with MSVC...
|
||||
rc /nologo /fo maturita.res maturita.rc
|
||||
if errorlevel 1 (
|
||||
echo [error] Resource compile failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
cl /nologo /O2 /MD /W3 /std:c11 /D_CRT_SECURE_NO_WARNINGS ^
|
||||
%GTK_CFLAGS% *.c maturita.res ^
|
||||
/Fe:maturita.exe ^
|
||||
/link /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup %GTK_LIBS%
|
||||
|
||||
if errorlevel 1 (
|
||||
echo [error] Build failed.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist icons\hicolor\512x512\apps mkdir icons\hicolor\512x512\apps
|
||||
copy /Y assets\app-icon.png icons\hicolor\512x512\apps\maturita.png >nul
|
||||
|
||||
echo.
|
||||
echo Built maturita.exe ^(icon embedded from assets\app-icon.ico^)
|
||||
echo Run it from this terminal with: maturita.exe
|
||||
echo Or use run-windows.bat / double-click maturita.exe.
|
||||
1601
desktop/cetba.c
Normal file
162
desktop/czech.c
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
#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) {
|
||||
GtkWidget *btn;
|
||||
GtkWidget *label;
|
||||
|
||||
btn = gtk_button_new();
|
||||
gtk_widget_add_css_class(btn, "unit-node");
|
||||
gtk_widget_add_css_class(btn, "current");
|
||||
gtk_widget_set_can_focus(btn, FALSE);
|
||||
gtk_widget_set_size_request(btn, (int)CZ_BUBBLE, (int)CZ_BUBBLE);
|
||||
|
||||
label = gtk_label_new(cz_initials[index]);
|
||||
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++) {
|
||||
GtkWidget *btn = cz_make_node(i);
|
||||
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");
|
||||
|
||||
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;
|
||||
}
|
||||
833
desktop/exercise_common.c
Normal file
|
|
@ -0,0 +1,833 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Exercise page shell */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
GtkWidget *ex_page_shell(const char *back_target, const char *title,
|
||||
const char *subtitle, const char *btn_label,
|
||||
GtkWidget **body_out, GtkWidget **feedback_out,
|
||||
GtkWidget **btn_out) {
|
||||
GtkWidget *page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
GtkWidget *scroll;
|
||||
GtkWidget *body;
|
||||
GtkWidget *bottom;
|
||||
GtkWidget *feedback;
|
||||
GtkWidget *btn;
|
||||
|
||||
gtk_widget_set_margin_start(page, 32);
|
||||
gtk_widget_set_margin_end(page, 32);
|
||||
gtk_widget_set_margin_top(page, 24);
|
||||
gtk_widget_set_margin_bottom(page, 24);
|
||||
|
||||
gtk_box_append(GTK_BOX(page), top_bar(back_target, title, subtitle));
|
||||
|
||||
scroll = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
|
||||
GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
|
||||
gtk_widget_set_vexpand(scroll, TRUE);
|
||||
gtk_widget_set_margin_top(scroll, 14);
|
||||
gtk_widget_set_margin_bottom(scroll, 14);
|
||||
gtk_box_append(GTK_BOX(page), scroll);
|
||||
|
||||
body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 14);
|
||||
gtk_widget_set_vexpand(body, TRUE);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
|
||||
*body_out = body;
|
||||
|
||||
bottom = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
||||
gtk_widget_set_margin_top(bottom, 4);
|
||||
gtk_box_append(GTK_BOX(page), bottom);
|
||||
|
||||
feedback = gtk_label_new("");
|
||||
gtk_widget_set_halign(feedback, GTK_ALIGN_START);
|
||||
gtk_widget_set_valign(feedback, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_hexpand(feedback, TRUE);
|
||||
gtk_box_append(GTK_BOX(bottom), feedback);
|
||||
*feedback_out = feedback;
|
||||
|
||||
btn = gtk_button_new();
|
||||
i18n_bind(btn, btn_label, 1);
|
||||
gtk_widget_add_css_class(btn, "btn-primary");
|
||||
gtk_widget_set_valign(btn, GTK_ALIGN_CENTER);
|
||||
gtk_box_append(GTK_BOX(bottom), btn);
|
||||
*btn_out = btn;
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Word-selection (combo box) exercise */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
|
||||
/* Build a hidden translation label ("meaning") appended under an item. */
|
||||
GtkWidget *meaning_add(GtkWidget *body, const char *text) {
|
||||
GtkWidget *lbl = gtk_label_new(NULL);
|
||||
|
||||
i18n_bind(lbl, text, 0);
|
||||
gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
|
||||
gtk_widget_set_halign(lbl, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(lbl, "meaning");
|
||||
gtk_label_set_wrap(GTK_LABEL(lbl), TRUE);
|
||||
gtk_widget_set_visible(lbl, FALSE);
|
||||
gtk_widget_set_margin_top(lbl, 2);
|
||||
gtk_widget_set_margin_bottom(lbl, 2);
|
||||
gtk_widget_set_margin_start(lbl, 8);
|
||||
gtk_box_append(GTK_BOX(body), lbl);
|
||||
return lbl;
|
||||
}
|
||||
|
||||
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
desktop/exercise_u1.c
Normal file
1540
desktop/exercise_u2.c
Normal file
1547
desktop/exercise_u3.c
Normal file
83
desktop/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);
|
||||
911
desktop/hw.c
Normal file
|
|
@ -0,0 +1,911 @@
|
|||
#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 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", "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_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);
|
||||
}
|
||||
1488
desktop/i18n.c
Normal file
467
desktop/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
desktop/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
|
||||
494
desktop/main.c
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
#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);
|
||||
}
|
||||
|
||||
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);
|
||||
gtk_window_set_child(window, GTK_WIDGET(main_stack));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
gtk_stack_set_visible_child_name(main_stack, "welcome");
|
||||
|
||||
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);
|
||||
|
||||
gtk_window_present(window);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
1220
desktop/maturita.h
Normal file
3
desktop/maturita.rc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* Windows application icon embedded into maturita.exe.
|
||||
* The first ICON resource becomes the Explorer / taskbar icon. */
|
||||
1 ICON "assets/app-icon.ico"
|
||||
783
desktop/mluvnice.c
Normal file
|
|
@ -0,0 +1,783 @@
|
|||
#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 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);
|
||||
}
|
||||
132
desktop/navigation.c
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#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 *heading;
|
||||
GtkWidget *card;
|
||||
GtkWidget *button;
|
||||
|
||||
box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 18);
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
card = gtk_label_new(NULL);
|
||||
i18n_bind(card, "welcome_body", 0);
|
||||
gtk_label_set_justify(GTK_LABEL(card), GTK_JUSTIFY_CENTER);
|
||||
gtk_label_set_wrap(GTK_LABEL(card), TRUE);
|
||||
gtk_label_set_max_width_chars(GTK_LABEL(card), 48);
|
||||
gtk_widget_add_css_class(card, "card");
|
||||
gtk_box_append(GTK_BOX(box), card);
|
||||
|
||||
button = gtk_button_new();
|
||||
i18n_bind(button, "continue", 1);
|
||||
gtk_widget_set_halign(button, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_margin_top(button, 6);
|
||||
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;
|
||||
}
|
||||
3987
desktop/net.c
Normal file
321
desktop/progress.c
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
#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);
|
||||
}
|
||||
}
|
||||
|
||||
void refresh_completion_ui(void) {
|
||||
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 marker */
|
||||
if (u->node) {
|
||||
if (all && u->n_ex > 0) {
|
||||
gtk_widget_remove_css_class(u->node, "current");
|
||||
gtk_widget_add_css_class(u->node, "done");
|
||||
if (u->node_done_icon)
|
||||
gtk_widget_set_visible(u->node_done_icon, TRUE);
|
||||
} else {
|
||||
gtk_widget_remove_css_class(u->node, "done");
|
||||
gtk_widget_add_css_class(u->node, "current");
|
||||
if (u->node_done_icon)
|
||||
gtk_widget_set_visible(u->node_done_icon, FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
446
desktop/roadmap.c
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Roadmap page */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void draw_node_halo(cairo_t *cr, double cx, double cy,
|
||||
double radius, double r, double g, double b,
|
||||
double alpha) {
|
||||
cairo_pattern_t *grad;
|
||||
grad = cairo_pattern_create_radial(cx, cy, 6.0, cx, cy, radius);
|
||||
cairo_pattern_add_color_stop_rgba(grad, 0.0, r, g, b, alpha);
|
||||
cairo_pattern_add_color_stop_rgba(grad, 0.55, r, g, b, alpha * 0.30);
|
||||
cairo_pattern_add_color_stop_rgba(grad, 1.0, r, g, b, 0.0);
|
||||
cairo_set_source(cr, grad);
|
||||
cairo_paint(cr);
|
||||
cairo_pattern_destroy(grad);
|
||||
}
|
||||
|
||||
/* Draw the checkered "finish line" node: checkerboard behind the unit
|
||||
* number and the same lock glyph the locked units use. */
|
||||
void draw_finish_cell(GtkDrawingArea *area, cairo_t *cr,
|
||||
int width, int height, gpointer data) {
|
||||
const double s = 11.0;
|
||||
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;
|
||||
double start_x = cx - r;
|
||||
double start_y = cy - r;
|
||||
int cols = (int)ceil(2.0 * r / s);
|
||||
int rows = (int)ceil(2.0 * r / s);
|
||||
char buf[8];
|
||||
PangoLayout *layout;
|
||||
PangoFontDescription *fd;
|
||||
PangoRectangle ink;
|
||||
double num_cy;
|
||||
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, start_x, start_y, 2.0 * r, 2.0 * r);
|
||||
cairo_set_source_rgb(cr, fin_dark.r, fin_dark.g, fin_dark.b);
|
||||
cairo_fill(cr);
|
||||
|
||||
for (iy = 0; iy < rows; iy++) {
|
||||
for (ix = 0; ix < cols; ix++) {
|
||||
if (((ix + iy) & 1) == 0)
|
||||
continue;
|
||||
cairo_rectangle(cr, start_x + ix * s, start_y + iy * s, s, s);
|
||||
cairo_set_source_rgb(cr, fin_light.r, fin_light.g, fin_light.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);
|
||||
|
||||
/* lock glyph below the number, as on the other locked units */
|
||||
lock_paint(cr, cx - 8.0, num_cy + ink.height / 2.0 + 6.0, 16.0, &gray);
|
||||
|
||||
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 double x0 = ROAD_MX;
|
||||
const double x1 = (double)road_cw - ROAD_MX;
|
||||
const Rgb mauve = color_from_hex(app_theme.accent);
|
||||
const Rgb blue = color_from_hex(app_theme.accent3);
|
||||
const Rgb rail = color_from_hex(app_theme.rail);
|
||||
const Rgb muted = color_from_hex(app_theme.subtext);
|
||||
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);
|
||||
|
||||
road_point((double)(NUM_UNLOCKED - 1), &hx, &hy);
|
||||
draw_node_halo(cr, hx, hy, NODE_SIZE * 1.05,
|
||||
mauve.r, mauve.g, mauve.b, 0.18);
|
||||
|
||||
cairo_new_path(cr);
|
||||
road_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);
|
||||
|
||||
if (t_lit > 0.0) {
|
||||
cairo_pattern_t *grad;
|
||||
|
||||
cairo_new_path(cr);
|
||||
road_path(cr, 0.0, t_lit);
|
||||
|
||||
grad = cairo_pattern_create_linear(x0, 0.0, x1, 0.0);
|
||||
cairo_pattern_add_color_stop_rgba(grad, 0.0,
|
||||
mauve.r, mauve.g, mauve.b, 0.16);
|
||||
cairo_pattern_add_color_stop_rgba(grad, 1.0,
|
||||
blue.r, blue.g, blue.b, 0.16);
|
||||
cairo_set_source(cr, grad);
|
||||
cairo_set_line_width(cr, 34);
|
||||
cairo_stroke_preserve(cr);
|
||||
cairo_pattern_destroy(grad);
|
||||
|
||||
grad = cairo_pattern_create_linear(x0, 0.0, x1, 0.0);
|
||||
cairo_pattern_add_color_stop_rgba(grad, 0.0,
|
||||
mauve.r, mauve.g, mauve.b, 1.0);
|
||||
cairo_pattern_add_color_stop_rgba(grad, 1.0,
|
||||
blue.r, blue.g, blue.b, 1.0);
|
||||
cairo_set_source(cr, grad);
|
||||
cairo_set_line_width(cr, 16);
|
||||
cairo_stroke_preserve(cr);
|
||||
cairo_pattern_destroy(grad);
|
||||
}
|
||||
|
||||
{
|
||||
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;
|
||||
|
||||
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);
|
||||
} 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_relayout_later();
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
17
desktop/run-windows.bat
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
@echo off
|
||||
setlocal
|
||||
|
||||
REM Launch a locally built maturita.exe with the GTK4 runtime on PATH, so the
|
||||
REM app can be started by double-clicking even when GTK is not installed
|
||||
REM system-wide.
|
||||
|
||||
if not defined GTK_ROOT set "GTK_ROOT=C:\gtk"
|
||||
set "PATH=%GTK_ROOT%\bin;%PATH%"
|
||||
|
||||
if not exist "%~dp0maturita.exe" (
|
||||
echo [error] maturita.exe not found next to this script.
|
||||
echo Build it first with build-windows.bat.
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
start "" /D "%~dp0" "%~dp0maturita.exe"
|
||||
123
desktop/scripts/bundle-linux.sh
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build a self-contained Linux AppImage (x86_64).
|
||||
# Requires: make, pkg-config, GTK4, curl, and fuse/appimagetool (pulled as needed).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH=x86_64 ;;
|
||||
aarch64|arm64) ARCH=aarch64 ;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
OUT_NAME="maturita-linux-${ARCH}.AppImage"
|
||||
APPDIR="$ROOT/dist/AppDir"
|
||||
TOOLS="$ROOT/dist/tools"
|
||||
# Real 512×512 PNG (assets/app-icon.png is 1024×1024 and linuxdeploy rejects it).
|
||||
ICON_512="$ROOT/share/icons/hicolor/512x512/apps/maturita.png"
|
||||
|
||||
echo "==> Building maturita"
|
||||
make -C "$ROOT" clean
|
||||
make -C "$ROOT"
|
||||
|
||||
echo "==> Preparing AppDir"
|
||||
rm -rf "$APPDIR"
|
||||
mkdir -p "$APPDIR/usr/bin" \
|
||||
"$APPDIR/usr/share/applications" \
|
||||
"$APPDIR/usr/share/icons/hicolor/512x512/apps" \
|
||||
"$APPDIR/icons/hicolor/512x512/apps" \
|
||||
"$APPDIR/share"
|
||||
|
||||
cp -f "$ROOT/maturita" "$APPDIR/usr/bin/maturita"
|
||||
cp -f "$ROOT/style.css" "$APPDIR/style.css"
|
||||
cp -f "$ICON_512" "$APPDIR/icons/hicolor/512x512/apps/maturita.png"
|
||||
cp -f "$ICON_512" "$APPDIR/usr/share/icons/hicolor/512x512/apps/maturita.png"
|
||||
cp -f "$ICON_512" "$APPDIR/maturita.png"
|
||||
cp -R "$ROOT/share/." "$APPDIR/share/"
|
||||
cp -R "$ROOT/share/." "$APPDIR/usr/share/"
|
||||
cp -f "$ROOT/share/applications/org.maturita.Maturita.desktop" \
|
||||
"$APPDIR/usr/share/applications/org.maturita.Maturita.desktop"
|
||||
# AppImage desktop entry must live at AppDir root and Exec= must be AppRun-compatible.
|
||||
cp -f "$ROOT/share/applications/org.maturita.Maturita.desktop" \
|
||||
"$APPDIR/org.maturita.Maturita.desktop"
|
||||
# linuxdeploy expects Icon= without path and a matching PNG at AppDir root.
|
||||
sed -i.bak 's|^Exec=.*|Exec=maturita|' "$APPDIR/org.maturita.Maturita.desktop"
|
||||
sed -i.bak 's|^Icon=.*|Icon=maturita|' "$APPDIR/org.maturita.Maturita.desktop"
|
||||
rm -f "$APPDIR/org.maturita.Maturita.desktop.bak"
|
||||
|
||||
mkdir -p "$TOOLS"
|
||||
LINUXDEPLOY="$TOOLS/linuxdeploy-${ARCH}.AppImage"
|
||||
GTK_PLUGIN="$TOOLS/linuxdeploy-plugin-gtk.sh"
|
||||
|
||||
if [[ ! -x "$LINUXDEPLOY" ]]; then
|
||||
echo "==> Downloading linuxdeploy"
|
||||
curl -fsSL -o "$LINUXDEPLOY" \
|
||||
"https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-${ARCH}.AppImage"
|
||||
chmod +x "$LINUXDEPLOY"
|
||||
fi
|
||||
if [[ ! -f "$GTK_PLUGIN" ]]; then
|
||||
echo "==> Downloading linuxdeploy-plugin-gtk"
|
||||
curl -fsSL -o "$GTK_PLUGIN" \
|
||||
"https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh"
|
||||
chmod +x "$GTK_PLUGIN"
|
||||
fi
|
||||
|
||||
# Extract linuxdeploy if FUSE is unavailable (common on CI).
|
||||
run_linuxdeploy() {
|
||||
if "$LINUXDEPLOY" --appimage-help >/dev/null 2>&1; then
|
||||
"$LINUXDEPLOY" "$@"
|
||||
else
|
||||
local extract_dir="$TOOLS/linuxdeploy-extracted"
|
||||
rm -rf "$extract_dir"
|
||||
cd "$TOOLS"
|
||||
"$LINUXDEPLOY" --appimage-extract >/dev/null
|
||||
mv squashfs-root "$extract_dir"
|
||||
cd "$ROOT"
|
||||
"$extract_dir/AppRun" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "==> Bundling GTK libraries into AppDir"
|
||||
export LDAI_OUTPUT="$ROOT/dist/$OUT_NAME"
|
||||
export LINUXDEPLOY_OUTPUT_VERSION="${VERSION:-continuous}"
|
||||
# Place plugin on PATH so linuxdeploy can find it.
|
||||
export PATH="$TOOLS:$PATH"
|
||||
|
||||
run_linuxdeploy \
|
||||
--appdir "$APPDIR" \
|
||||
--executable "$APPDIR/usr/bin/maturita" \
|
||||
--desktop-file "$APPDIR/org.maturita.Maturita.desktop" \
|
||||
--icon-file "$APPDIR/maturita.png" \
|
||||
--plugin gtk \
|
||||
--output appimage
|
||||
|
||||
# linuxdeploy may write the AppImage into the cwd under another name;
|
||||
# only rename when it is not already at the expected path (mv same→same
|
||||
# fails under `set -e` and was aborting an otherwise successful build).
|
||||
DEST="$ROOT/dist/$OUT_NAME"
|
||||
if [[ ! -f "$DEST" ]]; then
|
||||
shopt -s nullglob
|
||||
for f in "$ROOT"/*.AppImage "$ROOT/dist"/*.AppImage; do
|
||||
case "$(basename "$f")" in
|
||||
linuxdeploy*|appimagetool*) continue ;;
|
||||
esac
|
||||
mv -f "$f" "$DEST"
|
||||
break
|
||||
done
|
||||
shopt -u nullglob
|
||||
fi
|
||||
|
||||
if [[ ! -f "$DEST" ]]; then
|
||||
echo "AppImage was not created" >&2
|
||||
find "$ROOT" "$ROOT/dist" -maxdepth 2 -name '*.AppImage' -print >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$DEST"
|
||||
echo "Created dist/$OUT_NAME"
|
||||
280
desktop/scripts/bundle-macos.sh
Executable file
|
|
@ -0,0 +1,280 @@
|
|||
#!/usr/bin/env bash
|
||||
# Build a self-contained macOS .app with GTK dylibs relocated into the bundle.
|
||||
# Requires: make, pkg-config, GTK4 (Homebrew).
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
ARCH="$(uname -m)"
|
||||
case "$ARCH" in
|
||||
arm64|aarch64) ARCH_TAG=arm64 ;;
|
||||
x86_64) ARCH_TAG=x86_64 ;;
|
||||
*)
|
||||
echo "Unsupported architecture: $ARCH" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
APP_NAME="Maturita.app"
|
||||
APP="$ROOT/dist/$APP_NAME"
|
||||
MACOS="$APP/Contents/MacOS"
|
||||
RES="$APP/Contents/Resources"
|
||||
FW="$APP/Contents/Frameworks"
|
||||
ICON_512="$ROOT/share/icons/hicolor/512x512/apps/maturita.png"
|
||||
OUT_ZIP="maturita-macos-${ARCH_TAG}.zip"
|
||||
BREW_PREFIX="$(brew --prefix 2>/dev/null || echo /opt/homebrew)"
|
||||
|
||||
echo "==> Building maturita"
|
||||
make -C "$ROOT" clean
|
||||
make -C "$ROOT"
|
||||
|
||||
echo "==> Creating $APP_NAME"
|
||||
rm -rf "$APP"
|
||||
mkdir -p "$MACOS" "$RES/icons/hicolor/512x512/apps" "$RES/share" "$FW"
|
||||
|
||||
cp -f "$ROOT/maturita" "$MACOS/maturita-bin"
|
||||
cp -f "$ROOT/style.css" "$RES/style.css"
|
||||
cp -f "$ICON_512" "$RES/icons/hicolor/512x512/apps/maturita.png"
|
||||
cp -R "$ROOT/share/." "$RES/share/"
|
||||
|
||||
# Resolve an absolute Homebrew-style dependency to a real file on disk.
|
||||
resolve_lib() {
|
||||
local dep="$1"
|
||||
local candidate
|
||||
|
||||
if [[ -f "$dep" ]]; then
|
||||
realpath "$dep"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try common Homebrew layouts for the basename.
|
||||
local base
|
||||
base="$(basename "$dep")"
|
||||
for candidate in \
|
||||
"$BREW_PREFIX/lib/$base" \
|
||||
"$BREW_PREFIX/opt"/*/lib/"$base" \
|
||||
"$BREW_PREFIX/Cellar"/*/*/lib/"$base"; do
|
||||
if [[ -f "$candidate" ]]; then
|
||||
realpath "$candidate"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Recursively copy non-system dylibs into Frameworks and rewrite load paths.
|
||||
# Unlike dylibbundler, this never prompts interactively and fails loudly.
|
||||
bundle_deps() {
|
||||
local target="$1"
|
||||
local line dep name dest real
|
||||
|
||||
while IFS= read -r line; do
|
||||
dep="$(echo "$line" | awk '{print $1}')"
|
||||
case "$dep" in
|
||||
/System/*|/usr/lib/*|@executable_path/*|@loader_path/*)
|
||||
continue
|
||||
;;
|
||||
@rpath/*)
|
||||
name="$(basename "$dep")"
|
||||
if ! real="$(resolve_lib "$BREW_PREFIX/lib/$name")"; then
|
||||
echo "error: cannot resolve @rpath dependency '$dep' needed by $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
/*)
|
||||
name="$(basename "$dep")"
|
||||
case "$name" in
|
||||
libjpeg*.dylib) name="libjpeg.8.dylib" ;;
|
||||
esac
|
||||
if [[ -f "$dep" ]]; then
|
||||
real="$(realpath "$dep")"
|
||||
elif ! real="$(resolve_lib "$dep")"; then
|
||||
echo "error: cannot resolve dependency '$dep' needed by $target" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
dest="$FW/$name"
|
||||
|
||||
if [[ ! -f "$dest" ]]; then
|
||||
echo " + $name (from $real)"
|
||||
cp -f "$real" "$dest"
|
||||
chmod u+w "$dest"
|
||||
install_name_tool -id "@executable_path/../Frameworks/$name" "$dest" 2>/dev/null || true
|
||||
codesign --force --sign - "$dest" >/dev/null 2>&1 || true
|
||||
bundle_deps "$dest"
|
||||
fi
|
||||
|
||||
# Rewrite matching absolute / @rpath references on the target.
|
||||
while IFS= read -r changed; do
|
||||
changed="$(echo "$changed" | awk '{print $1}')"
|
||||
case "$changed" in
|
||||
/*|@rpath/*)
|
||||
if [[ "$(basename "$changed")" == "$name" ]] || \
|
||||
[[ "$(basename "$changed")" == "$(basename "$real")" ]] || \
|
||||
{ [[ "$name" == "libjpeg.8.dylib" ]] && [[ "$(basename "$changed")" == libjpeg* ]]; }; then
|
||||
install_name_tool -change "$changed" \
|
||||
"@executable_path/../Frameworks/$name" "$target" 2>/dev/null || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < <(otool -L "$target" | tail -n +2)
|
||||
done < <(otool -L "$target" | tail -n +2)
|
||||
}
|
||||
|
||||
echo "==> Bundling GTK / GLib dylibs into Frameworks"
|
||||
bundle_deps "$MACOS/maturita-bin"
|
||||
codesign --force --sign - "$MACOS/maturita-bin" >/dev/null 2>&1 || true
|
||||
|
||||
echo "==> Verifying Frameworks completeness"
|
||||
MISSING=0
|
||||
check_file_deps() {
|
||||
local file="$1"
|
||||
local line dep name
|
||||
while IFS= read -r line; do
|
||||
dep="$(echo "$line" | awk '{print $1}')"
|
||||
case "$dep" in
|
||||
@executable_path/../Frameworks/*)
|
||||
name="$(basename "$dep")"
|
||||
if [[ ! -f "$FW/$name" ]]; then
|
||||
echo "MISSING: $name (required by $(basename "$file"))" >&2
|
||||
MISSING=1
|
||||
fi
|
||||
;;
|
||||
@rpath/*)
|
||||
echo "UNRESOLVED @rpath: $dep (in $(basename "$file"))" >&2
|
||||
MISSING=1
|
||||
;;
|
||||
/opt/homebrew/*|/usr/local/*)
|
||||
echo "UNBUNDLED absolute dep still present: $dep (in $(basename "$file"))" >&2
|
||||
MISSING=1
|
||||
;;
|
||||
esac
|
||||
done < <(otool -L "$file" | tail -n +2)
|
||||
}
|
||||
check_file_deps "$MACOS/maturita-bin"
|
||||
for f in "$FW"/*.dylib; do
|
||||
check_file_deps "$f"
|
||||
done
|
||||
if [[ "$MISSING" -ne 0 ]]; then
|
||||
echo "error: macOS bundle is incomplete" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo " Frameworks: $(ls "$FW" | wc -l | tr -d ' ') dylibs"
|
||||
|
||||
# Copy GLib / GTK runtime data (schemas, modules, icon themes).
|
||||
copy_pkg_share() {
|
||||
local prefix
|
||||
prefix="$(pkg-config --variable=prefix "$1" 2>/dev/null || true)"
|
||||
[[ -n "$prefix" && -d "$prefix/share" ]] || return 0
|
||||
mkdir -p "$RES/share"
|
||||
for sub in glib-2.0 gtk-4.0 icons themes locale; do
|
||||
if [[ -d "$prefix/share/$sub" ]]; then
|
||||
rsync -a "$prefix/share/$sub" "$RES/share/" 2>/dev/null \
|
||||
|| cp -R "$prefix/share/$sub" "$RES/share/"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
copy_pkg_share gtk4
|
||||
copy_pkg_share glib-2.0
|
||||
|
||||
if [[ -d "$RES/share/glib-2.0/schemas" ]] && command -v glib-compile-schemas >/dev/null 2>&1; then
|
||||
glib-compile-schemas "$RES/share/glib-2.0/schemas" || true
|
||||
fi
|
||||
|
||||
# Launcher sets GTK search paths then execs the real binary.
|
||||
cat > "$MACOS/maturita" <<'EOF'
|
||||
#!/bin/bash
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
export DYLD_LIBRARY_PATH="$ROOT/Frameworks${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
|
||||
export XDG_DATA_DIRS="$ROOT/Resources/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
|
||||
export GSETTINGS_SCHEMA_DIR="$ROOT/Resources/share/glib-2.0/schemas"
|
||||
export GTK_DATA_PREFIX="$ROOT/Resources"
|
||||
export GTK_EXE_PREFIX="$ROOT"
|
||||
export XDG_DATA_HOME="$ROOT/Resources"
|
||||
exec "$ROOT/MacOS/maturita-bin" "$@"
|
||||
EOF
|
||||
chmod +x "$MACOS/maturita"
|
||||
|
||||
cat > "$APP/Contents/Info.plist" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleName</key>
|
||||
<string>maturita.c</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>maturita.c</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.maturita.Maturita</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${VERSION:-1.0}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${VERSION:-1.0}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>maturita</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>AppIcon</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then
|
||||
ICONSET="$RES/AppIcon.iconset"
|
||||
mkdir -p "$ICONSET"
|
||||
for sz in 16 32 64 128 256 512; do
|
||||
sips -z "$sz" "$sz" "$ICON_512" --out "$ICONSET/icon_${sz}x${sz}.png" >/dev/null
|
||||
sips -z $((sz * 2)) $((sz * 2)) "$ICON_512" \
|
||||
--out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
|
||||
done
|
||||
iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true
|
||||
fi
|
||||
|
||||
# Ad-hoc sign the .app so Gatekeeper is less likely to kill dyld loads.
|
||||
codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || true
|
||||
|
||||
echo "==> Smoke-testing launch"
|
||||
if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then
|
||||
echo " (CI: skipping GUI smoke test; Frameworks verification already passed)"
|
||||
else
|
||||
SMOKE_LOG="$(mktemp)"
|
||||
"$MACOS/maturita" >"$SMOKE_LOG" 2>&1 &
|
||||
SMOKE_PID=$!
|
||||
sleep 3
|
||||
if ! kill -0 "$SMOKE_PID" 2>/dev/null; then
|
||||
if grep -q "Library not loaded" "$SMOKE_LOG"; then
|
||||
echo "error: Maturita.app missing bundled libraries" >&2
|
||||
cat "$SMOKE_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
echo "error: Maturita.app exited during smoke test" >&2
|
||||
cat "$SMOKE_LOG" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
kill "$SMOKE_PID" 2>/dev/null || true
|
||||
wait "$SMOKE_PID" 2>/dev/null || true
|
||||
rm -f "$SMOKE_LOG"
|
||||
echo " smoke test OK (process stayed up)"
|
||||
fi
|
||||
|
||||
echo "==> Zipping"
|
||||
rm -f "$ROOT/dist/$OUT_ZIP"
|
||||
(
|
||||
cd "$ROOT/dist"
|
||||
zip -qry "$OUT_ZIP" "$APP_NAME"
|
||||
)
|
||||
|
||||
echo "Created dist/$OUT_ZIP"
|
||||
85
desktop/scripts/gen-icons.py
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regenerate assets/app-icon.ico and share/icons from assets/app-icon.png.
|
||||
|
||||
Uses macOS `sips` when available; otherwise requires Pillow.
|
||||
Run from the repository root.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "assets" / "app-icon.png"
|
||||
ICO = ROOT / "assets" / "app-icon.ico"
|
||||
ICO_SIZES = [16, 32, 48, 64, 128, 256]
|
||||
SHARE_SIZES = [16, 32, 48, 64, 128, 256, 512]
|
||||
|
||||
|
||||
def resize_png(src: Path, dest: Path, size: int) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if subprocess.call(["which", "sips"], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL) == 0:
|
||||
subprocess.check_call(
|
||||
["sips", "-z", str(size), str(size), str(src), "--out", str(dest)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
return
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc:
|
||||
raise SystemExit(
|
||||
"Need macOS sips or Pillow to resize icons"
|
||||
) from exc
|
||||
img = Image.open(src).convert("RGBA")
|
||||
img = img.resize((size, size), Image.Resampling.LANCZOS)
|
||||
img.save(dest, format="PNG")
|
||||
|
||||
|
||||
def write_ico(entries: list[tuple[int, bytes]], dest: Path) -> None:
|
||||
count = len(entries)
|
||||
header = struct.pack("<HHH", 0, 1, count)
|
||||
offset = 6 + 16 * count
|
||||
direntries = []
|
||||
blobs = []
|
||||
for size, data in entries:
|
||||
w = 0 if size >= 256 else size
|
||||
h = 0 if size >= 256 else size
|
||||
direntries.append(
|
||||
struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(data), offset)
|
||||
)
|
||||
blobs.append(data)
|
||||
offset += len(data)
|
||||
dest.write_bytes(header + b"".join(direntries) + b"".join(blobs))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SRC.is_file():
|
||||
print(f"missing {SRC}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="maturita-ico-") as tmp:
|
||||
entries: list[tuple[int, bytes]] = []
|
||||
for size in ICO_SIZES:
|
||||
png = Path(tmp) / f"icon_{size}.png"
|
||||
resize_png(SRC, png, size)
|
||||
entries.append((size, png.read_bytes()))
|
||||
write_ico(entries, ICO)
|
||||
print(f"wrote {ICO.relative_to(ROOT)}")
|
||||
|
||||
for size in SHARE_SIZES:
|
||||
dest = ROOT / "share" / "icons" / "hicolor" / f"{size}x{size}" / "apps" / "maturita.png"
|
||||
resize_png(SRC, dest, size)
|
||||
print(f"wrote {dest.relative_to(ROOT)}")
|
||||
|
||||
assets_icon = ROOT / "assets" / "icons" / "hicolor" / "512x512" / "apps" / "maturita.png"
|
||||
resize_png(SRC, assets_icon, 512)
|
||||
print(f"wrote {assets_icon.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
342
desktop/settings.c
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
#include "maturita.h"
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Settings */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
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();
|
||||
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);
|
||||
|
||||
{
|
||||
GtkWidget *sw = gtk_scrolled_window_new();
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(sw),
|
||||
GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
|
||||
gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(sw), 470);
|
||||
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;
|
||||
}
|
||||
14
desktop/share/applications/org.maturita.Maturita.desktop
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
Name=maturita.c
|
||||
GenericName=Maturita study app
|
||||
GenericName[cs]=Příprava na maturitu
|
||||
Comment=Educational GTK app for maturita exam preparation
|
||||
Comment[cs]=Vzdělávací GTK aplikace pro přípravu na maturitu
|
||||
Exec=maturita
|
||||
Icon=maturita
|
||||
Terminal=false
|
||||
Categories=Education;GTK;
|
||||
StartupNotify=true
|
||||
StartupWMClass=org.maturita.Maturita
|
||||
BIN
desktop/share/icons/hicolor/128x128/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
desktop/share/icons/hicolor/16x16/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
desktop/share/icons/hicolor/256x256/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
desktop/share/icons/hicolor/32x32/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
desktop/share/icons/hicolor/48x48/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
desktop/share/icons/hicolor/512x512/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
desktop/share/icons/hicolor/64x64/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
414
desktop/stats.c
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
#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++;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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);
|
||||
|
||||
gtk_progress_bar_set_fraction(
|
||||
GTK_PROGRESS_BAR(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
|
||||
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);
|
||||
gtk_progress_bar_set_fraction(
|
||||
GTK_PROGRESS_BAR(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"));
|
||||
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(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"));
|
||||
gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(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);
|
||||
gtk_progress_bar_set_fraction(
|
||||
GTK_PROGRESS_BAR(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);
|
||||
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;
|
||||
}
|
||||
937
desktop/style.css
Normal file
|
|
@ -0,0 +1,937 @@
|
|||
window {
|
||||
background-color: @bg_base;
|
||||
background-image: linear-gradient(160deg, @bg_crust 0%, @bg_base 40%, @bg_surface0 100%);
|
||||
}
|
||||
button {
|
||||
background-image: none;
|
||||
}
|
||||
.app-title {
|
||||
color: @fg_text;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
.settings-btn,
|
||||
.settings-btn:hover,
|
||||
.settings-btn:active,
|
||||
.settings-btn:checked,
|
||||
.settings-btn:focus,
|
||||
.settings-btn:focus-visible {
|
||||
min-width: 34px;
|
||||
min-height: 34px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
color: @fg_text;
|
||||
}
|
||||
.settings-btn:hover {
|
||||
color: @accent;
|
||||
background-color: alpha(@bg_surface0, 0.55);
|
||||
}
|
||||
.settings-btn:active {
|
||||
color: @accent;
|
||||
background-color: alpha(@bg_surface1, 0.4);
|
||||
}
|
||||
popover,
|
||||
popover.background,
|
||||
.settings-popover {
|
||||
background: @bg_mantle;
|
||||
background-color: @bg_mantle;
|
||||
color: @fg_text;
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 18px 40px alpha(@bg_crust, 0.45);
|
||||
}
|
||||
popover > contents,
|
||||
.settings-popover > contents {
|
||||
background: transparent;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
.settings-box {
|
||||
background-color: transparent;
|
||||
color: @fg_text;
|
||||
min-width: 292px;
|
||||
padding: 16px 16px 14px 16px;
|
||||
}
|
||||
.settings-title {
|
||||
color: @fg_text;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
.settings-label {
|
||||
color: @fg_overlay;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
}
|
||||
.settings-seg {
|
||||
background-color: alpha(@bg_surface0, 0.55);
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 3px;
|
||||
}
|
||||
.mode-chip {
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
color: @fg_subtext;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
box-shadow: none;
|
||||
transition: background-color 140ms ease, color 140ms ease,
|
||||
box-shadow 140ms ease;
|
||||
}
|
||||
.mode-chip:hover {
|
||||
background-color: alpha(@bg_surface1, 0.28);
|
||||
color: @fg_text;
|
||||
}
|
||||
.mode-chip:checked {
|
||||
background-image: linear-gradient(135deg, @accent 0%, @accent2 100%);
|
||||
color: @on_accent;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4px 12px alpha(@accent, 0.25);
|
||||
}
|
||||
.mode-chip:checked:hover {
|
||||
background-image: linear-gradient(135deg, @accent 0%, @accent2 100%);
|
||||
color: @on_accent;
|
||||
}
|
||||
.theme-grid {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
flowboxchild {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.theme-card {
|
||||
background-image: none;
|
||||
background-color: transparent;
|
||||
color: @fg_text;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 6px 4px;
|
||||
min-width: 0;
|
||||
box-shadow: none;
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
.theme-card:hover {
|
||||
background-color: alpha(@bg_surface0, 0.45);
|
||||
}
|
||||
.theme-card:checked {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.theme-card:checked:hover {
|
||||
background-color: alpha(@bg_surface0, 0.35);
|
||||
}
|
||||
.theme-card-name {
|
||||
color: @fg_subtext;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.theme-card:checked .theme-card-name {
|
||||
color: @fg_text;
|
||||
font-weight: 700;
|
||||
}
|
||||
headerbar,
|
||||
headerbar.titlebar,
|
||||
.titlebar {
|
||||
background-color: @bg_base;
|
||||
background-image: none;
|
||||
color: @fg_text;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
windowcontrols button:not(:hover) {
|
||||
color: @fg_text;
|
||||
}
|
||||
windowcontrols button:not(:hover) image {
|
||||
color: @fg_text;
|
||||
}
|
||||
.heading {
|
||||
color: @fg_text;
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.card {
|
||||
background-color: alpha(@bg_surface0, 0.92);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 18px;
|
||||
padding: 30px 34px;
|
||||
color: @fg_text;
|
||||
font-size: 16px;
|
||||
}
|
||||
.btn-primary {
|
||||
background-image: linear-gradient(135deg, @accent 0%, @accent2 100%);
|
||||
color: @on_accent;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
padding: 12px 40px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 6px 18px alpha(@accent, 0.25);
|
||||
transition: box-shadow 150ms ease, background-image 150ms ease;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
box-shadow: 0 8px 24px alpha(@accent, 0.45);
|
||||
background-image: linear-gradient(135deg, @accent2 0%, @accent3 100%);
|
||||
}
|
||||
.btn-primary:active {
|
||||
box-shadow: 0 3px 10px alpha(@accent, 0.35);
|
||||
}
|
||||
.roadmap-title {
|
||||
color: @fg_text;
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.roadmap-sub {
|
||||
color: @fg_subtext;
|
||||
font-size: 14px;
|
||||
}
|
||||
.back-btn {
|
||||
min-width: 42px;
|
||||
min-height: 42px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
background-color: alpha(@bg_surface0, 0.9);
|
||||
border: 1px solid @bg_surface1;
|
||||
color: @fg_text;
|
||||
box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);
|
||||
transition: background-color 150ms ease, color 150ms ease,
|
||||
border-color 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background-color: @bg_surface1;
|
||||
border-color: @bg_surface2;
|
||||
color: @accent;
|
||||
box-shadow: 0 6px 20px alpha(@accent, 0.35);
|
||||
}
|
||||
.back-btn:active {
|
||||
background-color: @bg_surface0;
|
||||
box-shadow: 0 2px 8px alpha(@bg_crust, 0.35);
|
||||
}
|
||||
.unit-node {
|
||||
background-image: none;
|
||||
background-color: @node_bg;
|
||||
color: @fg_text;
|
||||
border: 2px solid @locked_border;
|
||||
border-radius: 999px;
|
||||
padding: 0;
|
||||
box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);
|
||||
transition: transform 150ms ease, box-shadow 150ms ease,
|
||||
border-color 150ms ease;
|
||||
}
|
||||
.unit-node.done {
|
||||
background-image: linear-gradient(135deg, @success 0%, @success2 100%);
|
||||
border-color: @success;
|
||||
color: @on_accent;
|
||||
box-shadow: 0 4px 16px alpha(@success, 0.35);
|
||||
}
|
||||
.unit-node.current {
|
||||
background-image: linear-gradient(135deg, @accent 0%, @accent2 55%, @accent3 100%);
|
||||
border-color: @accent;
|
||||
color: @on_accent;
|
||||
box-shadow: 0 4px 18px alpha(@accent, 0.5),
|
||||
0 0 0 4px alpha(@bg_base, 0.9),
|
||||
0 0 0 7px alpha(@accent, 0.45);
|
||||
}
|
||||
.unit-node.done:hover,
|
||||
.unit-node.current:hover {
|
||||
transition: box-shadow 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
.unit-node.done:hover {
|
||||
box-shadow: 0 8px 22px alpha(@success, 0.5);
|
||||
}
|
||||
.unit-node.current:hover {
|
||||
box-shadow: 0 8px 26px alpha(@accent, 0.65),
|
||||
0 0 0 4px alpha(@bg_base, 0.9),
|
||||
0 0 0 7px alpha(@accent, 0.55);
|
||||
}
|
||||
.unit-node.locked {
|
||||
background-image: none;
|
||||
background-color: @locked_bg;
|
||||
border: 2px solid @locked_border;
|
||||
color: @fg_overlay;
|
||||
box-shadow: none;
|
||||
}
|
||||
.unit-node.locked:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.unit-node.finish {
|
||||
background-color: transparent;
|
||||
background-image: none;
|
||||
border: none;
|
||||
color: @fg_overlay;
|
||||
box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);
|
||||
}
|
||||
.unit-node.finish:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);
|
||||
}
|
||||
.unit-node:focus,
|
||||
.unit-node:focus-visible,
|
||||
.unit-node:hover {
|
||||
outline: none;
|
||||
}
|
||||
.unit-number {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.unit-node.done .unit-number,
|
||||
.unit-node.current .unit-number {
|
||||
color: @on_accent;
|
||||
}
|
||||
.unit-node.locked .unit-number {
|
||||
color: @fg_overlay;
|
||||
}
|
||||
.state-icon {
|
||||
color: @on_accent;
|
||||
}
|
||||
.unit-node.locked .lock-icon {
|
||||
color: @bg_surface2;
|
||||
}
|
||||
.unit-name {
|
||||
color: @fg_text;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.unit-name-locked {
|
||||
color: @fg_overlay;
|
||||
}
|
||||
.ex-bubble {
|
||||
min-width: 64px;
|
||||
min-height: 64px;
|
||||
padding: 0;
|
||||
border-radius: 999px;
|
||||
background-color: @node_bg;
|
||||
color: @fg_text;
|
||||
border: 2px solid @locked_border;
|
||||
box-shadow: 0 4px 14px alpha(@bg_crust, 0.35);
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
.ex-bubble:hover {
|
||||
background-image: none;
|
||||
border-color: @accent;
|
||||
box-shadow: 0 6px 20px alpha(@accent, 0.35);
|
||||
}
|
||||
.ex-bubble.done {
|
||||
background-image: linear-gradient(135deg, @success 0%, @success2 100%);
|
||||
border-color: @success;
|
||||
color: @on_accent;
|
||||
box-shadow: 0 4px 16px alpha(@success, 0.35);
|
||||
}
|
||||
.ex-bubble.done:hover {
|
||||
box-shadow: 0 8px 22px alpha(@success, 0.5);
|
||||
}
|
||||
.ex-bubble:focus,
|
||||
.ex-bubble:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
.ex-branch {
|
||||
border-style: dashed;
|
||||
border-color: @accent;
|
||||
}
|
||||
.ex-branch:hover {
|
||||
border-style: solid;
|
||||
}
|
||||
.bubble-number {
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.ex-bubble.done .bubble-number {
|
||||
color: @on_accent;
|
||||
}
|
||||
.bubble-icon {
|
||||
color: @on_accent;
|
||||
}
|
||||
.ex-label {
|
||||
color: @fg_text;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cz-label {
|
||||
background-color: @bg_base;
|
||||
padding: 1px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.book-card {
|
||||
background-image: none;
|
||||
background-color: alpha(@bg_surface0, 0.92);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 18px;
|
||||
padding: 20px 26px;
|
||||
color: @fg_text;
|
||||
box-shadow: 0 6px 18px alpha(@bg_crust, 0.22);
|
||||
transition: box-shadow 160ms ease, border-color 160ms ease,
|
||||
background-color 160ms ease;
|
||||
}
|
||||
.book-card:hover {
|
||||
border-color: @accent;
|
||||
background-color: alpha(@accent, 0.10);
|
||||
box-shadow: 0 12px 30px alpha(@accent, 0.32);
|
||||
}
|
||||
.book-card:active {
|
||||
background-color: alpha(@accent, 0.18);
|
||||
box-shadow: 0 4px 12px alpha(@accent, 0.25);
|
||||
}
|
||||
.reading-hero {
|
||||
opacity: 0.22;
|
||||
}
|
||||
.book-title {
|
||||
color: @fg_text;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.book-author {
|
||||
color: @accent2;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.book-sub {
|
||||
color: @fg_subtext;
|
||||
font-size: 14px;
|
||||
}
|
||||
.lit-link {
|
||||
background-image: none;
|
||||
background-color: alpha(@bg_surface0, 0.92);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 16px;
|
||||
padding: 14px 18px;
|
||||
color: @fg_text;
|
||||
box-shadow: 0 5px 16px alpha(@bg_crust, 0.18);
|
||||
transition: box-shadow 160ms ease, border-color 160ms ease,
|
||||
background-color 160ms ease;
|
||||
}
|
||||
.lit-link:hover {
|
||||
border-color: @accent;
|
||||
background-color: alpha(@accent, 0.10);
|
||||
box-shadow: 0 10px 26px alpha(@accent, 0.30);
|
||||
}
|
||||
.lit-link:active {
|
||||
background-color: alpha(@accent, 0.18);
|
||||
box-shadow: 0 4px 12px alpha(@accent, 0.25);
|
||||
}
|
||||
.lit-link-title {
|
||||
color: @fg_text;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.lit-link-sub {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
}
|
||||
.lit-progress {
|
||||
color: @accent2;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.lit-score {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.lit-prompt {
|
||||
color: @fg_text;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.lit-expl {
|
||||
color: @success2;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
background-color: alpha(@bg_surface0, 0.85);
|
||||
border-left: 4px solid @success;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
.kahoot-opt {
|
||||
background-image: none;
|
||||
color: @on_accent;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 16px;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 14px alpha(@bg_crust, 0.30);
|
||||
}
|
||||
.kahoot-opt:hover {
|
||||
box-shadow: 0 7px 22px alpha(@bg_crust, 0.42);
|
||||
}
|
||||
.kahoot-0 {
|
||||
background-color: @accent;
|
||||
}
|
||||
.kahoot-1 {
|
||||
background-color: @error;
|
||||
}
|
||||
.kahoot-2 {
|
||||
background-color: @warning;
|
||||
}
|
||||
.kahoot-3 {
|
||||
background-color: @success;
|
||||
}
|
||||
.kahoot-opt.ok {
|
||||
border-color: @fg_text;
|
||||
box-shadow: 0 0 0 3px alpha(@fg_text, 0.35);
|
||||
}
|
||||
.kahoot-opt.wrong {
|
||||
opacity: 0.35;
|
||||
}
|
||||
.ex-prompt {
|
||||
color: @fg_text;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.trans-word {
|
||||
color: @fg_text;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.ex-sub {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.hint {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
.meaning {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
.u3-answer {
|
||||
color: @success;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.notes-card {
|
||||
background-color: alpha(@bg_surface0, 0.9);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 24px;
|
||||
padding: 24px 34px;
|
||||
box-shadow: 0 10px 30px alpha(@bg_crust, 0.20);
|
||||
}
|
||||
.notes-kicker {
|
||||
color: @accent2;
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.notes-title {
|
||||
color: @fg_text;
|
||||
font-weight: 800;
|
||||
}
|
||||
.notes-body {
|
||||
color: @fg_text;
|
||||
}
|
||||
.notes-tip {
|
||||
color: @success2;
|
||||
font-weight: 700;
|
||||
}
|
||||
.dlg-speaker {
|
||||
color: @accent;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
}
|
||||
.number-big {
|
||||
color: @warning;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.hm-word {
|
||||
color: @fg_text;
|
||||
font-family: monospace;
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.chain {
|
||||
color: @warning;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
entry {
|
||||
background-color: @bg_mantle;
|
||||
color: @fg_text;
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 10px;
|
||||
padding: 8px 12px;
|
||||
font-size: 15px;
|
||||
caret-color: @accent;
|
||||
}
|
||||
entry:focus {
|
||||
border-color: @accent;
|
||||
}
|
||||
combobox {
|
||||
background-color: @bg_mantle;
|
||||
color: @fg_text;
|
||||
}
|
||||
combobox button {
|
||||
background-image: none;
|
||||
background-color: @bg_mantle;
|
||||
color: @fg_text;
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 10px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
combobox button:hover {
|
||||
background-image: none;
|
||||
border-color: @accent;
|
||||
}
|
||||
combobox.answer-ok button {
|
||||
border-color: @success;
|
||||
background-color: alpha(@success, 0.12);
|
||||
}
|
||||
combobox.answer-wrong button {
|
||||
border-color: @error;
|
||||
background-color: alpha(@error, 0.12);
|
||||
}
|
||||
entry.answer-ok {
|
||||
border-color: @success;
|
||||
background-color: alpha(@success, 0.08);
|
||||
}
|
||||
entry.answer-wrong {
|
||||
border-color: @error;
|
||||
background-color: alpha(@error, 0.08);
|
||||
}
|
||||
.pill {
|
||||
background-image: none;
|
||||
background-color: @node_bg;
|
||||
color: @fg_text;
|
||||
border: 2px solid @locked_border;
|
||||
border-radius: 999px;
|
||||
padding: 6px 18px;
|
||||
font-size: 15px;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease,
|
||||
background-color 150ms ease;
|
||||
}
|
||||
.pill:hover {
|
||||
background-image: none;
|
||||
background-color: @node_bg;
|
||||
border-color: @accent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.pill:active {
|
||||
background-image: none;
|
||||
background-color: @bg_surface1;
|
||||
box-shadow: none;
|
||||
}
|
||||
.pill:checked {
|
||||
background-image: linear-gradient(135deg, @accent 0%, @accent2 100%);
|
||||
color: @on_accent;
|
||||
border-color: @accent;
|
||||
font-weight: 700;
|
||||
}
|
||||
.pill.ok,
|
||||
.pill.ok:checked {
|
||||
background-image: linear-gradient(135deg, @success 0%, @success2 100%);
|
||||
color: @on_accent;
|
||||
border-color: @success;
|
||||
box-shadow: 0 0 0 2px alpha(@success, 0.35);
|
||||
}
|
||||
.pill.wrong,
|
||||
.pill.wrong:checked {
|
||||
background-image: linear-gradient(135deg, @error 0%, #c45c5c 100%);
|
||||
color: @on_accent;
|
||||
border-color: @error;
|
||||
box-shadow: 0 0 0 2px alpha(@error, 0.35);
|
||||
}
|
||||
.pill.hm-key {
|
||||
padding: 4px 5px;
|
||||
min-width: 32px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.quiz-card {
|
||||
background-color: alpha(@bg_surface0, 0.85);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 18px;
|
||||
padding: 16px 18px 14px 18px;
|
||||
box-shadow: 0 6px 18px alpha(@bg_crust, 0.18);
|
||||
}
|
||||
.quiz-heading {
|
||||
color: @fg_text;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.quiz-intro {
|
||||
color: @fg_subtext;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
}
|
||||
.quiz-q {
|
||||
color: @fg_text;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.quiz-options {
|
||||
margin-top: 2px;
|
||||
}
|
||||
.quiz-pill {
|
||||
border-radius: 14px;
|
||||
padding: 12px 16px;
|
||||
min-height: 44px;
|
||||
}
|
||||
.quiz-pill label {
|
||||
font-weight: 500;
|
||||
}
|
||||
.quiz-pill:checked label {
|
||||
font-weight: 700;
|
||||
}
|
||||
.chip {
|
||||
background-image: none;
|
||||
background-color: @bg_surface1;
|
||||
color: @fg_text;
|
||||
border: 1px solid @bg_surface2;
|
||||
border-radius: 999px;
|
||||
padding: 6px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
transition: background-color 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
.chip:hover {
|
||||
background-image: none;
|
||||
background-color: @bg_surface2;
|
||||
border-color: @accent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.chip:active {
|
||||
background-image: none;
|
||||
background-color: @bg_surface2;
|
||||
box-shadow: none;
|
||||
}
|
||||
.chip.ghost-host {
|
||||
opacity: 0;
|
||||
}
|
||||
.group-btn {
|
||||
background-image: none;
|
||||
background-color: @node_bg;
|
||||
color: @fg_text;
|
||||
border: 2px solid @locked_border;
|
||||
border-radius: 12px;
|
||||
padding: 8px 18px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
transition: border-color 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
.group-btn:hover {
|
||||
background-image: none;
|
||||
background-color: @node_bg;
|
||||
border-color: @accent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.group-btn:active {
|
||||
background-image: none;
|
||||
background-color: @bg_surface1;
|
||||
box-shadow: none;
|
||||
}
|
||||
.group-btn:checked {
|
||||
background-image: linear-gradient(135deg, @accent3 0%, @success2 100%);
|
||||
color: @on_accent;
|
||||
border-color: @accent3;
|
||||
}
|
||||
.group-panel {
|
||||
background-color: alpha(@bg_surface0, 0.5);
|
||||
border: 2px dashed @bg_surface1;
|
||||
border-radius: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
.group-panel-label {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.sent-group {
|
||||
background-color: alpha(@bg_surface0, 0.5);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 14px;
|
||||
padding: 12px;
|
||||
}
|
||||
.sent-group.ok {
|
||||
border-color: @success;
|
||||
}
|
||||
.sent-group.wrong {
|
||||
border-color: @error;
|
||||
}
|
||||
.feedback-ok {
|
||||
color: @success;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.feedback-err {
|
||||
color: @error;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.feedback-warn {
|
||||
color: @warning;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.stats-page {
|
||||
padding: 8px 28px 24px 28px;
|
||||
}
|
||||
.stats-summary {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.stats-metric {
|
||||
background-color: alpha(@bg_surface0, 0.92);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 16px;
|
||||
padding: 16px 18px;
|
||||
min-width: 120px;
|
||||
}
|
||||
.stats-metric-value {
|
||||
color: @fg_text;
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.4px;
|
||||
}
|
||||
.stats-metric-label {
|
||||
color: @fg_overlay;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.stats-overall-bar,
|
||||
.stats-overall-bar trough,
|
||||
.stats-unit-bar,
|
||||
.stats-unit-bar trough {
|
||||
min-height: 10px;
|
||||
min-width: 1px;
|
||||
border-radius: 6px;
|
||||
background-color: alpha(@bg_surface1, 0.85);
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.stats-overall-bar {
|
||||
margin-bottom: 22px;
|
||||
min-height: 12px;
|
||||
}
|
||||
.stats-overall-bar progress,
|
||||
.stats-unit-bar progress {
|
||||
min-width: 1px;
|
||||
min-height: 10px;
|
||||
border-radius: 6px;
|
||||
background-image: linear-gradient(90deg, @accent 0%, @accent2 100%);
|
||||
background-color: @accent;
|
||||
border: none;
|
||||
}
|
||||
.stats-unit-bar.done progress {
|
||||
background-image: linear-gradient(90deg, @success 0%, @success2 100%);
|
||||
background-color: @success;
|
||||
}
|
||||
.stats-section {
|
||||
color: @fg_overlay;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.stats-subject {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.stats-subject-name {
|
||||
color: @fg_text;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.stats-subject-count {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.stats-subject-count.locked {
|
||||
color: @fg_overlay;
|
||||
}
|
||||
.stats-subject-bar,
|
||||
.stats-subject-bar trough {
|
||||
min-height: 12px;
|
||||
min-width: 1px;
|
||||
border-radius: 6px;
|
||||
background-color: alpha(@bg_surface1, 0.85);
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.stats-subject-bar {
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
.stats-subject-bar progress {
|
||||
min-width: 1px;
|
||||
min-height: 10px;
|
||||
border-radius: 6px;
|
||||
background-image: linear-gradient(90deg, @accent 0%, @accent2 100%);
|
||||
background-color: @accent;
|
||||
border: none;
|
||||
}
|
||||
.stats-units {
|
||||
margin-top: 8px;
|
||||
border-left: 2px solid alpha(@fg_overlay, 0.22);
|
||||
padding-left: 12px;
|
||||
}
|
||||
.stats-unit-row {
|
||||
background-color: alpha(@bg_surface0, 0.72);
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.stats-unit-row.locked {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.stats-unit-row.done {
|
||||
border-color: alpha(@success, 0.55);
|
||||
}
|
||||
.stats-unit-index {
|
||||
color: @fg_overlay;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
min-width: 22px;
|
||||
}
|
||||
.stats-unit-name {
|
||||
color: @fg_text;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.stats-unit-count {
|
||||
color: @fg_subtext;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.stats-unit-badge {
|
||||
color: @success;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
tooltip {
|
||||
background-color: @bg_mantle;
|
||||
border: 1px solid @bg_surface1;
|
||||
border-radius: 10px;
|
||||
color: @fg_text;
|
||||
}
|
||||
|
||||
/* no-scale */
|
||||
/* Rules below keep a fixed size (symbols/icons) even when the window grows. */
|
||||
.bubble-number.branch-star {
|
||||
font-size: 22px;
|
||||
}
|
||||
430
desktop/subjects.c
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
#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);
|
||||
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);
|
||||
}
|
||||
|
||||
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
desktop/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
desktop/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;
|
||||
}
|
||||
333
desktop/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. */
|
||||
static char *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 = 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);
|
||||
}
|
||||