mirror of
https://github.com/pepaskopec-blip/Graduately.git
synced 2026-10-27 07:04:35 +00:00
1647 lines
52 KiB
C
1647 lines
52 KiB
C
#include "graduately.h"
|
||
|
||
#include <string.h>
|
||
|
||
#ifdef __APPLE__
|
||
#include <objc/message.h>
|
||
#include <objc/runtime.h>
|
||
#endif
|
||
#ifdef G_OS_UNIX
|
||
#include <signal.h>
|
||
#include <sys/wait.h>
|
||
#endif
|
||
|
||
/* English across four gymnasium years (RVP G, foreign language, maturita
|
||
* skills). Lesson text lives in english_lessons.inc. */
|
||
|
||
#define EN_YEARS 4
|
||
#define EN_N 8
|
||
|
||
#include "english_lessons.inc"
|
||
|
||
typedef struct {
|
||
const ChoiceQ *q;
|
||
GtkToggleButton *tb[4];
|
||
GtkWidget *hint;
|
||
} EnPick;
|
||
|
||
typedef struct {
|
||
int year;
|
||
int index;
|
||
const EnLesson *lesson;
|
||
void (*on_done)(int year, int index);
|
||
GtkWidget *feedback;
|
||
GtkWidget *model;
|
||
GArray *picks;
|
||
GArray *entries;
|
||
GArray *gap_hints;
|
||
GtkWidget *essay;
|
||
GtkWidget *play_note;
|
||
GtkWidget *play_btn;
|
||
gboolean played;
|
||
gboolean busy;
|
||
gboolean paused;
|
||
gboolean fallback;
|
||
gboolean restarting;
|
||
gboolean voice_dirty;
|
||
gboolean heard;
|
||
double speed;
|
||
double gain;
|
||
const char *voice;
|
||
GPid pid;
|
||
guint apply_src;
|
||
guint poll;
|
||
#ifdef __APPLE__
|
||
id synth;
|
||
#endif
|
||
} EnEx;
|
||
|
||
typedef struct {
|
||
int year;
|
||
GtkWidget *scroll;
|
||
GtkWidget *fixed;
|
||
GtkWidget *rail;
|
||
GtkWidget *nodes[EN_N];
|
||
GtkWidget *labels[EN_N];
|
||
double cx[EN_N];
|
||
double cy[EN_N];
|
||
int cols, rows, cw, ch;
|
||
guint idle;
|
||
double last_avail;
|
||
int last_cols, last_rows, last_cw, last_ch;
|
||
cairo_surface_t *cache;
|
||
int cache_w, cache_h;
|
||
} EngMap;
|
||
|
||
static NetLesson en_rt[EN_YEARS][EN_N];
|
||
static EngMap en_maps[EN_YEARS];
|
||
static char en_map_page[EN_YEARS][16];
|
||
static char en_unit_page[EN_YEARS][EN_N][16];
|
||
static char en_ex_page[EN_YEARS][EN_N][16];
|
||
static char en_title_key[EN_YEARS][EN_N][16];
|
||
static char en_sub_key[EN_YEARS][EN_N][20];
|
||
static gboolean en_ready;
|
||
|
||
static void en_prepare(void) {
|
||
if (en_ready)
|
||
return;
|
||
en_ready = TRUE;
|
||
for (int y = 0; y < EN_YEARS; y++) {
|
||
g_snprintf(en_map_page[y], sizeof en_map_page[y], "en%dmap", y + 1);
|
||
for (int i = 0; i < EN_N; i++) {
|
||
g_snprintf(en_unit_page[y][i], sizeof en_unit_page[y][i],
|
||
"en%dunit%d", y + 1, i + 1);
|
||
g_snprintf(en_ex_page[y][i], sizeof en_ex_page[y][i],
|
||
"en%dex%d", y + 1, i + 1);
|
||
g_snprintf(en_title_key[y][i], sizeof en_title_key[y][i],
|
||
"en_y%d_u%d", y + 1, i + 1);
|
||
g_snprintf(en_sub_key[y][i], sizeof en_sub_key[y][i],
|
||
"en_y%d_u%d_sub", y + 1, i + 1);
|
||
en_rt[y][i].unit_page = en_unit_page[y][i];
|
||
en_rt[y][i].ex_page = en_ex_page[y][i];
|
||
en_rt[y][i].n_slides = (guint)en_lessons[y][i].n_slides;
|
||
}
|
||
}
|
||
}
|
||
|
||
static void en_save_progress(void) {
|
||
GKeyFile *kf = g_key_file_new();
|
||
gchar *data;
|
||
GError *err = NULL;
|
||
|
||
for (int y = 0; y < EN_YEARS; y++) {
|
||
for (int i = 0; i < EN_N; i++) {
|
||
char key[8];
|
||
|
||
g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1);
|
||
g_key_file_set_boolean(kf, "done", key, en_rt[y][i].done);
|
||
}
|
||
}
|
||
data = g_key_file_to_data(kf, NULL, NULL);
|
||
if (!g_file_set_contents(PROGRESS_EN, data, -1, &err)) {
|
||
g_warning("english progress: %s", err->message);
|
||
g_error_free(err);
|
||
}
|
||
g_free(data);
|
||
g_key_file_free(kf);
|
||
}
|
||
|
||
void en_load_progress(void) {
|
||
GKeyFile *kf = g_key_file_new();
|
||
GError *err = NULL;
|
||
|
||
en_prepare();
|
||
if (!g_key_file_load_from_file(kf, PROGRESS_EN, G_KEY_FILE_NONE, &err)) {
|
||
if (err)
|
||
g_error_free(err);
|
||
g_key_file_free(kf);
|
||
return;
|
||
}
|
||
for (int y = 0; y < EN_YEARS; y++) {
|
||
for (int i = 0; i < EN_N; i++) {
|
||
char key[8];
|
||
|
||
g_snprintf(key, sizeof key, "%d-%d", y + 1, i + 1);
|
||
en_rt[y][i].done = g_key_file_get_boolean(kf, "done", key, NULL);
|
||
}
|
||
}
|
||
g_key_file_free(kf);
|
||
}
|
||
|
||
static void en_refresh(void) {
|
||
for (int y = 0; y < EN_YEARS; y++) {
|
||
for (int i = 0; i < EN_N; i++) {
|
||
GtkWidget *node = en_maps[y].nodes[i];
|
||
GtkWidget *icon = en_rt[y][i].done_icon;
|
||
|
||
if (!node)
|
||
continue;
|
||
if (en_rt[y][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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
static void mark_en_done(int year, int index) {
|
||
if (year < 0 || year >= EN_YEARS || index < 0 || index >= EN_N)
|
||
return;
|
||
if (en_rt[year][index].done)
|
||
return;
|
||
en_rt[year][index].done = TRUE;
|
||
en_save_progress();
|
||
en_refresh();
|
||
refresh_stats_ui();
|
||
}
|
||
|
||
void progress_for_en(ProgressSum *out) {
|
||
out->total_ex += EN_YEARS * EN_N;
|
||
out->open_units += EN_YEARS * EN_N;
|
||
for (int y = 0; y < EN_YEARS; y++) {
|
||
for (int i = 0; i < EN_N; i++) {
|
||
if (en_rt[y][i].done) {
|
||
out->done_ex++;
|
||
out->done_units++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Path */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
static void en_point(EngMap *m, 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 >= EN_N - 1) { k = EN_N - 2; u = 1.0; }
|
||
|
||
p1x = m->cx[k]; p1y = m->cy[k];
|
||
p2x = m->cx[k + 1]; p2y = m->cy[k + 1];
|
||
if (k - 1 >= 0) { p0x = m->cx[k - 1]; p0y = m->cy[k - 1]; }
|
||
else { p0x = p1x - (p2x - p1x); p0y = p1y - (p2y - p1y); }
|
||
if (k + 2 < EN_N) { p3x = m->cx[k + 2]; p3y = m->cy[k + 2]; }
|
||
else { p3x = p2x + (p2x - p1x); p3y = p2y + (p2y - p1y); }
|
||
|
||
u2 = u * u;
|
||
u3 = u2 * u;
|
||
*ox = 0.5 * (2.0 * p1x + (-p0x + p2x) * u
|
||
+ (2.0 * p0x - 5.0 * p1x + 4.0 * p2x - p3x) * u2
|
||
+ (-p0x + 3.0 * p1x - 3.0 * p2x + p3x) * u3);
|
||
*oy = 0.5 * (2.0 * p1y + (-p0y + p2y) * u
|
||
+ (2.0 * p0y - 5.0 * p1y + 4.0 * p2y - p3y) * u2
|
||
+ (-p0y + 3.0 * p1y - 3.0 * p2y + p3y) * u3);
|
||
}
|
||
|
||
static void en_path(EngMap *m, cairo_t *cr, double t0, double t1) {
|
||
int n = (int)((t1 - t0) * 24.0);
|
||
double x, y;
|
||
|
||
if (n < 1)
|
||
n = 1;
|
||
en_point(m, t0, &x, &y);
|
||
cairo_move_to(cr, x, y);
|
||
for (int s = 1; s <= n; s++) {
|
||
en_point(m, t0 + (t1 - t0) * (double)s / (double)n, &x, &y);
|
||
cairo_line_to(cr, x, y);
|
||
}
|
||
}
|
||
|
||
static void en_geometry(EngMap *m, double avail) {
|
||
const double phase = 2.0 * G_PI * 1.7 / (double)(EN_N - 1);
|
||
int rows;
|
||
|
||
for (rows = 1; rows <= EN_N; rows++) {
|
||
int cols = (EN_N + rows - 1) / rows;
|
||
if (2.0 * ROAD_MX + (cols - 1) * PATH_SPAC <= avail + 1.0)
|
||
break;
|
||
}
|
||
if (rows > EN_N)
|
||
rows = EN_N;
|
||
m->rows = rows;
|
||
m->cols = (EN_N + rows - 1) / rows;
|
||
if (m->cols < 1)
|
||
m->cols = 1;
|
||
m->cw = (int)(2.0 * ROAD_MX + (m->cols - 1) * PATH_SPAC);
|
||
m->ch = (int)(2.0 * ROAD_MY + (rows - 1) * ROAD_GAP);
|
||
|
||
for (int r = 0; r < rows; r++) {
|
||
int base = r * m->cols;
|
||
int len = MIN(m->cols, EN_N - base);
|
||
int fwd = (r % 2) == 0;
|
||
|
||
for (int c = 0; c < len; c++) {
|
||
int i = base + c;
|
||
int cc = fwd ? c : (m->cols - 1 - c);
|
||
|
||
m->cx[i] = ROAD_MX + cc * PATH_SPAC;
|
||
m->cy[i] = ROAD_MY + r * ROAD_GAP
|
||
+ ROAD_WAVE * sin((double)i * phase);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void en_apply_layout(EngMap *m) {
|
||
if (!m->fixed)
|
||
return;
|
||
if (m->cols == m->last_cols && m->rows == m->last_rows &&
|
||
m->cw == m->last_cw && m->ch == m->last_ch)
|
||
return;
|
||
m->last_cols = m->cols;
|
||
m->last_rows = m->rows;
|
||
m->last_cw = m->cw;
|
||
m->last_ch = m->ch;
|
||
if (m->cache) {
|
||
cairo_surface_destroy(m->cache);
|
||
m->cache = NULL;
|
||
}
|
||
for (int i = 0; i < EN_N; i++) {
|
||
if (!m->nodes[i] || !m->labels[i])
|
||
continue;
|
||
gtk_fixed_move(GTK_FIXED(m->fixed), m->nodes[i],
|
||
(int)(m->cx[i] - NODE_SIZE / 2.0),
|
||
(int)(m->cy[i] - NODE_SIZE / 2.0));
|
||
gtk_fixed_move(GTK_FIXED(m->fixed), m->labels[i],
|
||
(int)(m->cx[i] - (PATH_SPAC - 20.0) / 2.0),
|
||
(int)(m->cy[i] + NODE_SIZE / 2.0 + 10.0));
|
||
}
|
||
gtk_widget_set_size_request(m->fixed, m->cw, m->ch);
|
||
gtk_widget_set_size_request(m->rail, m->cw, m->ch);
|
||
gtk_fixed_move(GTK_FIXED(m->fixed), m->rail, 0, 0);
|
||
gtk_widget_queue_draw(m->rail);
|
||
}
|
||
|
||
static void en_relayout(EngMap *m) {
|
||
GtkAdjustment *hadj;
|
||
double avail;
|
||
|
||
if (!m->scroll)
|
||
return;
|
||
hadj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(m->scroll));
|
||
avail = gtk_adjustment_get_page_size(hadj);
|
||
if (avail < 1.0)
|
||
return;
|
||
if (fabs(avail - m->last_avail) < 1.0)
|
||
return;
|
||
m->last_avail = avail;
|
||
en_geometry(m, avail);
|
||
en_apply_layout(m);
|
||
}
|
||
|
||
static gboolean en_relayout_idle(gpointer data) {
|
||
EngMap *m = data;
|
||
|
||
m->idle = 0;
|
||
en_relayout(m);
|
||
return G_SOURCE_REMOVE;
|
||
}
|
||
|
||
static void en_adjust_notify(GtkAdjustment *adj, GParamSpec *ps, gpointer data) {
|
||
EngMap *m = data;
|
||
|
||
(void)adj;
|
||
(void)ps;
|
||
if (m->idle == 0)
|
||
m->idle = g_idle_add(en_relayout_idle, m);
|
||
}
|
||
|
||
static void en_render_rail(EngMap *m, cairo_t *cr) {
|
||
const Rgb rail = color_from_hex(app_theme.rail);
|
||
const Rgb muted = color_from_hex(app_theme.subtext);
|
||
|
||
cairo_set_operator(cr, CAIRO_OPERATOR_CLEAR);
|
||
cairo_paint(cr);
|
||
cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
|
||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
|
||
cairo_set_line_join(cr, CAIRO_LINE_JOIN_ROUND);
|
||
cairo_new_path(cr);
|
||
en_path(m, cr, 0.0, (double)(EN_N - 1));
|
||
cairo_set_line_width(cr, 12);
|
||
cairo_set_source_rgb(cr, rail.r, rail.g, rail.b);
|
||
cairo_stroke(cr);
|
||
cairo_set_source_rgba(cr, muted.r, muted.g, muted.b, 0.17);
|
||
cairo_set_dash(cr, (double[]){2.0, 40.0}, 2, 0.0);
|
||
cairo_new_path(cr);
|
||
en_path(m, cr, 0.0, (double)(EN_N - 1));
|
||
cairo_stroke(cr);
|
||
cairo_set_dash(cr, NULL, 0, 0.0);
|
||
}
|
||
|
||
static void en_draw_rail(GtkDrawingArea *area, cairo_t *cr,
|
||
int width, int height, gpointer data) {
|
||
EngMap *m = data;
|
||
|
||
(void)area;
|
||
(void)width;
|
||
(void)height;
|
||
if (!m->cache || m->cache_w != m->cw || m->cache_h != m->ch) {
|
||
cairo_t *rcr;
|
||
|
||
if (m->cache)
|
||
cairo_surface_destroy(m->cache);
|
||
m->cache_w = m->cw > 0 ? m->cw : 1;
|
||
m->cache_h = m->ch > 0 ? m->ch : 1;
|
||
m->cache = cairo_image_surface_create(CAIRO_FORMAT_ARGB32,
|
||
m->cache_w, m->cache_h);
|
||
rcr = cairo_create(m->cache);
|
||
en_render_rail(m, rcr);
|
||
cairo_destroy(rcr);
|
||
}
|
||
cairo_set_source_surface(cr, m->cache, 0, 0);
|
||
cairo_paint(cr);
|
||
}
|
||
|
||
void en_rail_theme_reset(void) {
|
||
for (int y = 0; y < EN_YEARS; y++) {
|
||
if (en_maps[y].cache) {
|
||
cairo_surface_destroy(en_maps[y].cache);
|
||
en_maps[y].cache = NULL;
|
||
}
|
||
if (en_maps[y].rail)
|
||
gtk_widget_queue_draw(en_maps[y].rail);
|
||
}
|
||
}
|
||
|
||
static void en_open_unit(GtkButton *button, gpointer data) {
|
||
NetLesson *L = data;
|
||
|
||
(void)button;
|
||
if (!L)
|
||
return;
|
||
L->idx = 0;
|
||
if (L->stack) {
|
||
char name[16];
|
||
|
||
g_snprintf(name, sizeof name, "slide%u", L->idx);
|
||
gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name);
|
||
if (L->prev_btn)
|
||
gtk_widget_set_sensitive(L->prev_btn, FALSE);
|
||
if (L->next_btn)
|
||
gtk_button_set_label(GTK_BUTTON(L->next_btn), tr("net_slide_next"));
|
||
}
|
||
show_page(L->unit_page, 1);
|
||
}
|
||
|
||
static void en_add_node(EngMap *m, GtkFixed *fixed, int index) {
|
||
GtkWidget *card;
|
||
GtkWidget *vbox;
|
||
GtkWidget *number;
|
||
GtkWidget *icon;
|
||
GtkWidget *name;
|
||
char *text;
|
||
|
||
card = gtk_button_new();
|
||
gtk_widget_add_css_class(card, "unit-node");
|
||
gtk_widget_add_css_class(card, "current");
|
||
gtk_widget_set_can_focus(card, FALSE);
|
||
gtk_widget_set_size_request(card, (int)NODE_SIZE, (int)NODE_SIZE);
|
||
|
||
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", index + 1);
|
||
number = gtk_label_new(text);
|
||
g_free(text);
|
||
gtk_widget_add_css_class(number, "unit-number");
|
||
gtk_box_append(GTK_BOX(vbox), number);
|
||
|
||
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);
|
||
en_rt[m->year][index].done_icon = icon;
|
||
g_signal_connect(card, "clicked", G_CALLBACK(en_open_unit),
|
||
&en_rt[m->year][index]);
|
||
|
||
m->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");
|
||
i18n_bind(name, en_title_key[m->year][index], 0);
|
||
m->labels[index] = name;
|
||
gtk_fixed_put(fixed, name, 0, 0);
|
||
}
|
||
|
||
static GtkWidget *en_year_button(int year) {
|
||
static const char *keys[] = {"en_year1", "en_year2", "en_year3", "en_year4"};
|
||
static const char *subs[] = {"en_y1_sub", "en_y2_sub", "en_y3_sub", "en_y4_sub"};
|
||
GtkWidget *btn;
|
||
GtkWidget *row;
|
||
GtkWidget *num;
|
||
GtkWidget *texts;
|
||
GtkWidget *title;
|
||
GtkWidget *sub;
|
||
char *num_text;
|
||
|
||
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_hexpand(btn, TRUE);
|
||
gtk_widget_set_size_request(btn, -1, 112);
|
||
|
||
row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 14);
|
||
gtk_widget_set_margin_start(row, 18);
|
||
gtk_widget_set_margin_end(row, 18);
|
||
gtk_widget_set_margin_top(row, 8);
|
||
gtk_widget_set_margin_bottom(row, 8);
|
||
gtk_button_set_child(GTK_BUTTON(btn), row);
|
||
|
||
num_text = g_strdup_printf("%d", year);
|
||
num = gtk_label_new(num_text);
|
||
g_free(num_text);
|
||
gtk_widget_add_css_class(num, "unit-number");
|
||
gtk_widget_set_valign(num, GTK_ALIGN_CENTER);
|
||
gtk_box_append(GTK_BOX(row), num);
|
||
|
||
texts = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||
gtk_widget_set_hexpand(texts, TRUE);
|
||
gtk_box_append(GTK_BOX(row), texts);
|
||
|
||
title = gtk_label_new(NULL);
|
||
i18n_bind(title, keys[year - 1], 0);
|
||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||
gtk_widget_add_css_class(title, "unit-name");
|
||
gtk_box_append(GTK_BOX(texts), title);
|
||
|
||
sub = gtk_label_new(NULL);
|
||
i18n_bind(sub, subs[year - 1], 0);
|
||
gtk_widget_set_halign(sub, GTK_ALIGN_START);
|
||
gtk_label_set_wrap(GTK_LABEL(sub), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(sub), 0);
|
||
gtk_widget_add_css_class(sub, "meaning");
|
||
gtk_widget_set_visible(sub, TRUE);
|
||
gtk_box_append(GTK_BOX(texts), sub);
|
||
|
||
g_object_set_data_full(G_OBJECT(btn), "target",
|
||
g_strdup(en_map_page[year - 1]), g_free);
|
||
g_signal_connect(btn, "clicked", G_CALLBACK(on_nav_clicked), NULL);
|
||
return btn;
|
||
}
|
||
|
||
GtkWidget *build_enyears_page(void) {
|
||
GtkWidget *page;
|
||
GtkWidget *scroll;
|
||
GtkWidget *list;
|
||
|
||
en_prepare();
|
||
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", "English", "en_years_sub"));
|
||
|
||
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, 18);
|
||
gtk_box_append(GTK_BOX(page), scroll);
|
||
|
||
list = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
|
||
gtk_widget_set_halign(list, GTK_ALIGN_CENTER);
|
||
gtk_widget_set_size_request(list, 460, -1);
|
||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), list);
|
||
for (int y = 1; y <= EN_YEARS; y++)
|
||
gtk_box_append(GTK_BOX(list), en_year_button(y));
|
||
return page;
|
||
}
|
||
|
||
GtkWidget *build_enmap_page(int year) {
|
||
EngMap *m;
|
||
GtkWidget *page;
|
||
GtkWidget *scroll;
|
||
GtkWidget *wrap;
|
||
GtkWidget *fixed;
|
||
GtkWidget *rail;
|
||
GtkAdjustment *ha;
|
||
GtkAdjustment *va;
|
||
static const char *year_keys[] = {
|
||
"en_year1", "en_year2", "en_year3", "en_year4",
|
||
};
|
||
static const char *sub_keys[] = {
|
||
"en_y1_sub", "en_y2_sub", "en_y3_sub", "en_y4_sub",
|
||
};
|
||
|
||
en_prepare();
|
||
m = &en_maps[year];
|
||
m->year = year;
|
||
en_geometry(m, 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("enyears", year_keys[year], sub_keys[year]));
|
||
|
||
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);
|
||
m->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, m->cw, m->ch);
|
||
gtk_center_box_set_center_widget(GTK_CENTER_BOX(wrap), fixed);
|
||
m->fixed = fixed;
|
||
|
||
rail = gtk_drawing_area_new();
|
||
gtk_widget_set_size_request(rail, m->cw, m->ch);
|
||
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(rail), en_draw_rail, m, NULL);
|
||
gtk_fixed_put(GTK_FIXED(fixed), rail, 0, 0);
|
||
m->rail = rail;
|
||
|
||
for (int i = 0; i < EN_N; i++)
|
||
en_add_node(m, 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(en_adjust_notify), m);
|
||
g_signal_connect(va, "notify::page-size", G_CALLBACK(en_adjust_notify), m);
|
||
en_refresh();
|
||
return page;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Slides */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
void skill_slide_apply(NetLesson *L) {
|
||
char name[16];
|
||
|
||
if (!L || !L->stack)
|
||
return;
|
||
g_snprintf(name, sizeof name, "slide%u", L->idx);
|
||
gtk_stack_set_visible_child_name(GTK_STACK(L->stack), name);
|
||
if (L->prev_btn) {
|
||
gtk_widget_set_sensitive(L->prev_btn, L->idx > 0);
|
||
gtk_button_set_label(GTK_BUTTON(L->prev_btn), tr("net_slide_prev"));
|
||
}
|
||
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"));
|
||
}
|
||
|
||
static int en_note_w = -1;
|
||
static gboolean en_note_ready;
|
||
|
||
static void en_rescale_notes(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 y = 0; y < EN_YEARS; y++)
|
||
for (int i = 0; i < EN_N; i++)
|
||
net_rescale_lesson_notes(&en_rt[y][i], body, head, kick);
|
||
}
|
||
|
||
static gboolean en_note_tick(GtkWidget *w, GdkFrameClock *clock, gpointer data) {
|
||
int width = gtk_widget_get_allocated_width(w);
|
||
|
||
(void)clock;
|
||
(void)data;
|
||
if (width != en_note_w) {
|
||
en_note_w = width;
|
||
en_rescale_notes(width);
|
||
}
|
||
return G_SOURCE_CONTINUE;
|
||
}
|
||
|
||
static void en_slide_prev(GtkButton *button, gpointer data) {
|
||
NetLesson *L = data;
|
||
|
||
(void)button;
|
||
if (L && L->idx > 0) {
|
||
L->idx--;
|
||
skill_slide_apply(L);
|
||
}
|
||
}
|
||
|
||
static void en_slide_next(GtkButton *button, gpointer data) {
|
||
NetLesson *L = data;
|
||
|
||
(void)button;
|
||
if (!L)
|
||
return;
|
||
if (L->idx + 1 < L->n_slides) {
|
||
L->idx++;
|
||
skill_slide_apply(L);
|
||
} else {
|
||
show_page(L->ex_page, 1);
|
||
}
|
||
}
|
||
|
||
void en_lessons_apply_lang(void) {
|
||
for (int y = 0; y < EN_YEARS; y++)
|
||
for (int i = 0; i < EN_N; i++)
|
||
skill_slide_apply(&en_rt[y][i]);
|
||
}
|
||
|
||
GtkWidget *skill_unit_page(NetLesson *L, const EnLesson *lesson,
|
||
const char *back, const char *title_key,
|
||
const char *sub_key) {
|
||
GtkWidget *page;
|
||
GtkWidget *scroll;
|
||
GtkWidget *nav;
|
||
|
||
net_lesson_notes_ensure(L);
|
||
net_notes_target = L;
|
||
L->n_slides = (guint)lesson->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(back, 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);
|
||
|
||
for (guint s = 0; s < L->n_slides; s++) {
|
||
GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||
char name[16];
|
||
|
||
g_snprintf(name, sizeof name, "slide%u", s);
|
||
gtk_widget_set_margin_top(card, 8);
|
||
gtk_widget_set_margin_bottom(card, 8);
|
||
net_note_kicker(card, lesson->slides[s].kicker);
|
||
net_note_title(card, lesson->slides[s].title);
|
||
for (int i = 0; i < 8 && lesson->slides[s].line[i]; i++)
|
||
net_note_line(card, lesson->slides[s].line[i], FALSE);
|
||
if (lesson->slides[s].tip)
|
||
net_note_line(card, lesson->slides[s].tip, TRUE);
|
||
gtk_stack_add_named(GTK_STACK(L->stack), card, name);
|
||
}
|
||
|
||
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");
|
||
gtk_box_append(GTK_BOX(nav), L->prev_btn);
|
||
g_signal_connect(L->prev_btn, "clicked", G_CALLBACK(en_slide_prev), L);
|
||
L->next_btn = gtk_button_new_with_label(tr("net_slide_next"));
|
||
gtk_widget_add_css_class(L->next_btn, "btn-primary");
|
||
gtk_box_append(GTK_BOX(nav), L->next_btn);
|
||
g_signal_connect(L->next_btn, "clicked", G_CALLBACK(en_slide_next), L);
|
||
if (!en_note_ready) {
|
||
gtk_widget_add_tick_callback(L->stack, en_note_tick, NULL, NULL);
|
||
en_note_ready = TRUE;
|
||
}
|
||
en_rescale_notes(0);
|
||
net_notes_target = NULL;
|
||
skill_slide_apply(L);
|
||
return page;
|
||
}
|
||
|
||
/* ------------------------------------------------------------------ */
|
||
/* Exercise */
|
||
/* ------------------------------------------------------------------ */
|
||
|
||
static GtkWidget *en_heading(GtkWidget *body, const char *key) {
|
||
GtkWidget *label = gtk_label_new(NULL);
|
||
|
||
i18n_bind(label, key, 0);
|
||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||
gtk_widget_add_css_class(label, "quiz-heading");
|
||
gtk_widget_set_margin_top(label, 8);
|
||
gtk_box_append(GTK_BOX(body), label);
|
||
return label;
|
||
}
|
||
|
||
static GtkWidget *en_passage(GtkWidget *body, const char *text, gboolean hide) {
|
||
GtkWidget *label = gtk_label_new(text);
|
||
|
||
gtk_label_set_wrap(GTK_LABEL(label), TRUE);
|
||
gtk_label_set_wrap_mode(GTK_LABEL(label), PANGO_WRAP_WORD_CHAR);
|
||
gtk_label_set_selectable(GTK_LABEL(label), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(label), 0);
|
||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||
gtk_widget_add_css_class(label, "quiz-intro");
|
||
gtk_widget_set_visible(label, !hide);
|
||
gtk_box_append(GTK_BOX(body), label);
|
||
return label;
|
||
}
|
||
|
||
static void en_add_mcq(EnEx *ctx, GtkWidget *body, const ChoiceQ *qs,
|
||
const char **hints, int n) {
|
||
for (int i = 0; i < n; i++) {
|
||
EnPick pick;
|
||
GtkWidget *card;
|
||
|
||
memset(&pick, 0, sizeof pick);
|
||
pick.q = &qs[i];
|
||
card = mcq_append_question(body, i + 1, &qs[i], pick.tb);
|
||
if (hints && hints[i])
|
||
pick.hint = meaning_add(card, hints[i]);
|
||
g_array_append_val(ctx->picks, pick);
|
||
}
|
||
}
|
||
|
||
static int en_word_count(const char *norm) {
|
||
int n = 0;
|
||
const char *p = norm ? norm : "";
|
||
|
||
while (*p) {
|
||
while (*p == ' ')
|
||
p++;
|
||
if (!*p)
|
||
break;
|
||
n++;
|
||
while (*p && *p != ' ')
|
||
p++;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
static gboolean en_has_word(const char *norm, const char *word) {
|
||
char *key = normalize_answer(word);
|
||
size_t n;
|
||
const char *p;
|
||
gboolean ok = FALSE;
|
||
|
||
if (!key || !key[0]) {
|
||
g_free(key);
|
||
return TRUE;
|
||
}
|
||
n = strlen(key);
|
||
for (p = norm; p && (p = strstr(p, key)); p += n) {
|
||
gboolean left = p == norm || p[-1] == ' ';
|
||
gboolean right = p[n] == '\0' || p[n] == ' ';
|
||
|
||
if (left && right) {
|
||
ok = TRUE;
|
||
break;
|
||
}
|
||
}
|
||
g_free(key);
|
||
return ok;
|
||
}
|
||
|
||
static void en_sync_play(EnEx *ctx) {
|
||
const char *key = "en_play";
|
||
|
||
if (!ctx->play_btn)
|
||
return;
|
||
if (ctx->paused)
|
||
key = "en_resume";
|
||
else if (ctx->busy)
|
||
key = "en_pause";
|
||
gtk_button_set_label(GTK_BUTTON(ctx->play_btn), tr(key));
|
||
}
|
||
|
||
static void en_start_fallback(EnEx *ctx) {
|
||
ctx->fallback = TRUE;
|
||
ctx->played = TRUE;
|
||
ctx->busy = FALSE;
|
||
ctx->paused = FALSE;
|
||
if (ctx->play_note) {
|
||
gtk_label_set_text(GTK_LABEL(ctx->play_note), tr("en_listen_fallback"));
|
||
gtk_widget_set_visible(ctx->play_note, TRUE);
|
||
}
|
||
en_sync_play(ctx);
|
||
}
|
||
|
||
static gboolean en_launch(EnEx *ctx);
|
||
|
||
static void en_voice_done(GPid pid, gint status, gpointer data) {
|
||
EnEx *ctx = data;
|
||
gboolean ok = FALSE;
|
||
gboolean restart = ctx->restarting;
|
||
|
||
g_spawn_close_pid(pid);
|
||
if (ctx->pid == pid)
|
||
ctx->pid = 0;
|
||
if (restart) {
|
||
ctx->restarting = FALSE;
|
||
en_launch(ctx);
|
||
return;
|
||
}
|
||
#ifdef G_OS_UNIX
|
||
ok = WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||
#else
|
||
ok = status == 0;
|
||
#endif
|
||
ctx->busy = FALSE;
|
||
ctx->paused = FALSE;
|
||
en_sync_play(ctx);
|
||
if (!ok)
|
||
en_start_fallback(ctx);
|
||
}
|
||
|
||
static gboolean en_spawn(char **argv, GPid *pid) {
|
||
GError *err = NULL;
|
||
|
||
if (!argv[0] || !g_find_program_in_path(argv[0]))
|
||
return FALSE;
|
||
if (!g_spawn_async(NULL, argv, NULL,
|
||
G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
|
||
NULL, NULL, pid, &err)) {
|
||
g_clear_error(&err);
|
||
return FALSE;
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
#ifdef __APPLE__
|
||
static id en_ns(const char *s) {
|
||
return ((id (*)(Class, SEL, const char *))objc_msgSend)(
|
||
objc_getClass("NSString"), sel_registerName("stringWithUTF8String:"), s);
|
||
}
|
||
|
||
static void en_mac_levels(EnEx *ctx) {
|
||
float rate = (float)(180.0 * (ctx->speed > 0.2 ? ctx->speed : 0.9));
|
||
float vol = (float)(ctx->gain < 0 ? 0 : (ctx->gain > 1 ? 1 : ctx->gain));
|
||
|
||
((void (*)(id, SEL, float))objc_msgSend)(
|
||
ctx->synth, sel_registerName("setRate:"), rate);
|
||
((void (*)(id, SEL, float))objc_msgSend)(
|
||
ctx->synth, sel_registerName("setVolume:"), vol);
|
||
}
|
||
|
||
static gboolean en_mac_try_voice(id synth, const char *name) {
|
||
return ((BOOL (*)(id, SEL, id))objc_msgSend)(
|
||
synth, sel_registerName("setVoice:"), en_ns(name));
|
||
}
|
||
|
||
static void en_mac_choose(id synth, const char *lang) {
|
||
static const char *en_voices[] = {
|
||
"com.apple.voice.compact.en-GB.Daniel",
|
||
"com.apple.speech.synthesis.voice.daniel",
|
||
"com.apple.voice.compact.en-US.Samantha",
|
||
"com.apple.speech.synthesis.voice.samantha",
|
||
NULL
|
||
};
|
||
static const char *de_voices[] = {
|
||
"com.apple.voice.compact.de-DE.Anna",
|
||
"com.apple.speech.synthesis.voice.anna",
|
||
"com.apple.voice.compact.de-DE.Helena",
|
||
"com.apple.speech.synthesis.voice.petra",
|
||
NULL
|
||
};
|
||
gboolean german = lang && g_str_has_prefix(lang, "de");
|
||
const char **list = german ? de_voices : en_voices;
|
||
const char *needle = german ? "de-DE" : "en-GB";
|
||
Class cls;
|
||
id voices;
|
||
unsigned long n;
|
||
|
||
for (int i = 0; list[i]; i++) {
|
||
if (en_mac_try_voice(synth, list[i]))
|
||
return;
|
||
}
|
||
cls = objc_getClass("NSSpeechSynthesizer");
|
||
if (!cls)
|
||
return;
|
||
voices = ((id (*)(Class, SEL))objc_msgSend)(
|
||
cls, sel_registerName("availableVoices"));
|
||
if (!voices)
|
||
return;
|
||
n = ((unsigned long (*)(id, SEL))objc_msgSend)(
|
||
voices, sel_registerName("count"));
|
||
for (unsigned long i = 0; i < n; i++) {
|
||
id name = ((id (*)(id, SEL, unsigned long))objc_msgSend)(
|
||
voices, sel_registerName("objectAtIndex:"), i);
|
||
const char *utf = ((const char *(*)(id, SEL))objc_msgSend)(
|
||
name, sel_registerName("UTF8String"));
|
||
|
||
if (utf && strstr(utf, needle) && en_mac_try_voice(synth, utf))
|
||
return;
|
||
}
|
||
}
|
||
|
||
static gboolean en_mac_ensure(EnEx *ctx) {
|
||
Class cls;
|
||
id obj;
|
||
|
||
if (ctx->synth)
|
||
return TRUE;
|
||
cls = objc_getClass("NSSpeechSynthesizer");
|
||
if (!cls)
|
||
return FALSE;
|
||
obj = ((id (*)(Class, SEL))objc_msgSend)(cls, sel_registerName("alloc"));
|
||
obj = ((id (*)(id, SEL))objc_msgSend)(obj, sel_registerName("init"));
|
||
if (!obj)
|
||
return FALSE;
|
||
ctx->synth = obj;
|
||
en_mac_choose(obj, ctx->voice);
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean en_mac_speaking(EnEx *ctx) {
|
||
if (!ctx->synth)
|
||
return FALSE;
|
||
return ((BOOL (*)(id, SEL))objc_msgSend)(
|
||
ctx->synth, sel_registerName("isSpeaking"));
|
||
}
|
||
|
||
static void en_mac_stop(EnEx *ctx) {
|
||
if (ctx->synth)
|
||
((void (*)(id, SEL))objc_msgSend)(
|
||
ctx->synth, sel_registerName("stopSpeaking"));
|
||
}
|
||
|
||
static gboolean en_mac_start(EnEx *ctx) {
|
||
if (!en_mac_ensure(ctx))
|
||
return FALSE;
|
||
en_mac_levels(ctx);
|
||
ctx->heard = FALSE;
|
||
return ((BOOL (*)(id, SEL, id))objc_msgSend)(
|
||
ctx->synth, sel_registerName("startSpeakingString:"),
|
||
en_ns(ctx->lesson->listening));
|
||
}
|
||
|
||
static void en_mac_pause(EnEx *ctx) {
|
||
((BOOL (*)(id, SEL, long))objc_msgSend)(
|
||
ctx->synth, sel_registerName("pauseSpeakingAtBoundary:"), 1L);
|
||
}
|
||
|
||
static void en_mac_resume(EnEx *ctx) {
|
||
if (ctx->voice_dirty) {
|
||
ctx->voice_dirty = FALSE;
|
||
en_mac_stop(ctx);
|
||
en_mac_start(ctx);
|
||
return;
|
||
}
|
||
((BOOL (*)(id, SEL))objc_msgSend)(
|
||
ctx->synth, sel_registerName("continueSpeaking"));
|
||
}
|
||
|
||
static gboolean en_poll_speech(gpointer data) {
|
||
EnEx *ctx = data;
|
||
|
||
if (ctx->paused || ctx->restarting)
|
||
return G_SOURCE_CONTINUE;
|
||
if (en_mac_speaking(ctx)) {
|
||
ctx->heard = TRUE;
|
||
return G_SOURCE_CONTINUE;
|
||
}
|
||
if (!ctx->heard)
|
||
return G_SOURCE_CONTINUE;
|
||
ctx->poll = 0;
|
||
ctx->busy = FALSE;
|
||
ctx->paused = FALSE;
|
||
en_sync_play(ctx);
|
||
return G_SOURCE_REMOVE;
|
||
}
|
||
#endif
|
||
|
||
static gboolean en_launch(EnEx *ctx) {
|
||
char *text = (char *)ctx->lesson->listening;
|
||
double speed = ctx->speed > 0.2 ? ctx->speed : 0.9;
|
||
char rate[16];
|
||
char amp[16];
|
||
|
||
ctx->played = TRUE;
|
||
ctx->busy = TRUE;
|
||
ctx->paused = FALSE;
|
||
ctx->voice_dirty = FALSE;
|
||
#ifdef __APPLE__
|
||
if (en_mac_start(ctx)) {
|
||
if (!ctx->poll)
|
||
ctx->poll = g_timeout_add(200, en_poll_speech, ctx);
|
||
en_sync_play(ctx);
|
||
return TRUE;
|
||
}
|
||
#endif
|
||
g_snprintf(rate, sizeof rate, "%d", (int)(160.0 * speed));
|
||
g_snprintf(amp, sizeof amp, "%d", (int)(ctx->gain * 200.0));
|
||
{
|
||
const char *voice = ctx->voice && ctx->voice[0] ? ctx->voice : "en";
|
||
char *a[] = {"espeak-ng", "-v", (char *)voice, "-s", rate, "-a", amp, text, NULL};
|
||
char *b[] = {"espeak", "-v", (char *)voice, "-s", rate, "-a", amp, text, NULL};
|
||
#ifdef __APPLE__
|
||
char *c_en[] = {"say", "-r", rate, text, NULL};
|
||
char *c_de[] = {"say", "-v", "Anna", "-r", rate, text, NULL};
|
||
char **tries[] = {g_str_has_prefix(voice, "de") ? c_de : c_en};
|
||
int n = 1;
|
||
#else
|
||
char *c[] = {"spd-say", "-l", (char *)voice, "-r", rate, text, NULL};
|
||
char **tries[] = {a, b, c};
|
||
int n = 3;
|
||
#endif
|
||
(void)a;
|
||
(void)b;
|
||
for (int i = 0; i < n; i++) {
|
||
if (en_spawn(tries[i], &ctx->pid)) {
|
||
g_child_watch_add(ctx->pid, en_voice_done, ctx);
|
||
en_sync_play(ctx);
|
||
return TRUE;
|
||
}
|
||
}
|
||
}
|
||
ctx->busy = FALSE;
|
||
en_start_fallback(ctx);
|
||
return FALSE;
|
||
}
|
||
|
||
static void en_pause_voice(EnEx *ctx) {
|
||
ctx->paused = TRUE;
|
||
#ifdef __APPLE__
|
||
if (ctx->synth && en_mac_speaking(ctx)) {
|
||
en_mac_pause(ctx);
|
||
en_sync_play(ctx);
|
||
return;
|
||
}
|
||
#endif
|
||
#ifdef G_OS_UNIX
|
||
if (ctx->pid) {
|
||
kill((pid_t)ctx->pid, SIGSTOP);
|
||
en_sync_play(ctx);
|
||
return;
|
||
}
|
||
#endif
|
||
en_sync_play(ctx);
|
||
}
|
||
|
||
static void en_resume_voice(EnEx *ctx) {
|
||
ctx->paused = FALSE;
|
||
#ifdef __APPLE__
|
||
if (ctx->synth) {
|
||
en_mac_resume(ctx);
|
||
if (!ctx->poll)
|
||
ctx->poll = g_timeout_add(200, en_poll_speech, ctx);
|
||
en_sync_play(ctx);
|
||
return;
|
||
}
|
||
#endif
|
||
if (ctx->voice_dirty) {
|
||
ctx->voice_dirty = FALSE;
|
||
if (ctx->pid) {
|
||
ctx->restarting = TRUE;
|
||
#ifdef G_OS_UNIX
|
||
kill((pid_t)ctx->pid, SIGKILL);
|
||
#endif
|
||
}
|
||
en_sync_play(ctx);
|
||
return;
|
||
}
|
||
#ifdef G_OS_UNIX
|
||
if (ctx->pid) {
|
||
kill((pid_t)ctx->pid, SIGCONT);
|
||
en_sync_play(ctx);
|
||
return;
|
||
}
|
||
#endif
|
||
en_sync_play(ctx);
|
||
}
|
||
|
||
static void en_restart_voice(EnEx *ctx) {
|
||
ctx->voice_dirty = FALSE;
|
||
ctx->paused = FALSE;
|
||
#ifdef __APPLE__
|
||
if (ctx->synth) {
|
||
ctx->restarting = TRUE;
|
||
en_mac_stop(ctx);
|
||
ctx->restarting = FALSE;
|
||
en_mac_start(ctx);
|
||
if (!ctx->poll)
|
||
ctx->poll = g_timeout_add(200, en_poll_speech, ctx);
|
||
en_sync_play(ctx);
|
||
return;
|
||
}
|
||
#endif
|
||
if (ctx->fallback)
|
||
return;
|
||
if (ctx->pid) {
|
||
ctx->restarting = TRUE;
|
||
#ifdef G_OS_UNIX
|
||
kill((pid_t)ctx->pid, SIGKILL);
|
||
#endif
|
||
}
|
||
}
|
||
|
||
static gboolean en_apply_voice(gpointer data) {
|
||
EnEx *ctx = data;
|
||
|
||
ctx->apply_src = 0;
|
||
if (ctx->busy && !ctx->paused)
|
||
en_restart_voice(ctx);
|
||
return G_SOURCE_REMOVE;
|
||
}
|
||
|
||
static void en_level_changed(GtkRange *range, gpointer data) {
|
||
EnEx *ctx = data;
|
||
const char *which = g_object_get_data(G_OBJECT(range), "en-level");
|
||
|
||
if (g_strcmp0(which, "speed") == 0)
|
||
ctx->speed = gtk_range_get_value(range);
|
||
else
|
||
ctx->gain = gtk_range_get_value(range) / 100.0;
|
||
ctx->voice_dirty = TRUE;
|
||
if (ctx->apply_src)
|
||
g_source_remove(ctx->apply_src);
|
||
ctx->apply_src = g_timeout_add(180, en_apply_voice, ctx);
|
||
}
|
||
|
||
static void en_play_clicked(GtkButton *button, gpointer data) {
|
||
EnEx *ctx = data;
|
||
|
||
(void)button;
|
||
if (!ctx->lesson->listening)
|
||
return;
|
||
ctx->played = TRUE;
|
||
if (!ctx->busy) {
|
||
en_launch(ctx);
|
||
return;
|
||
}
|
||
if (ctx->paused)
|
||
en_resume_voice(ctx);
|
||
else
|
||
en_pause_voice(ctx);
|
||
}
|
||
|
||
static char *en_format_level(GtkScale *scale, double value, gpointer data) {
|
||
const char *which = g_object_get_data(G_OBJECT(scale), "en-level");
|
||
|
||
(void)data;
|
||
if (g_strcmp0(which, "speed") == 0)
|
||
return g_strdup_printf("%.1f×", value);
|
||
return g_strdup_printf("%.0f %%", value);
|
||
}
|
||
|
||
static GtkWidget *en_level_control(EnEx *ctx, const char *key, const char *which,
|
||
double min, double max, double value) {
|
||
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||
GtkWidget *lab = gtk_label_new(NULL);
|
||
GtkWidget *scale = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL,
|
||
min, max, max > 10 ? 1 : 0.1);
|
||
|
||
i18n_bind(lab, key, 0);
|
||
gtk_widget_add_css_class(lab, "listen-caption");
|
||
gtk_label_set_xalign(GTK_LABEL(lab), 0);
|
||
gtk_widget_set_halign(lab, GTK_ALIGN_START);
|
||
g_object_set_data(G_OBJECT(scale), "en-level", (gpointer)which);
|
||
gtk_scale_set_format_value_func(GTK_SCALE(scale), en_format_level, NULL, NULL);
|
||
gtk_scale_set_draw_value(GTK_SCALE(scale), TRUE);
|
||
gtk_scale_set_value_pos(GTK_SCALE(scale), GTK_POS_RIGHT);
|
||
gtk_range_set_value(GTK_RANGE(scale), value);
|
||
gtk_widget_add_css_class(scale, "listen-scale");
|
||
gtk_widget_set_size_request(scale, 156, -1);
|
||
gtk_widget_set_hexpand(scale, FALSE);
|
||
gtk_widget_set_halign(scale, GTK_ALIGN_FILL);
|
||
g_signal_connect(scale, "value-changed", G_CALLBACK(en_level_changed), ctx);
|
||
gtk_box_append(GTK_BOX(box), lab);
|
||
gtk_box_append(GTK_BOX(box), scale);
|
||
gtk_widget_set_hexpand(box, FALSE);
|
||
gtk_widget_set_halign(box, GTK_ALIGN_FILL);
|
||
return box;
|
||
}
|
||
|
||
/* Transcript stays on screen. Speed and volume sit in a short column
|
||
* beside it, so the sliders do not run the width of the page. */
|
||
static void en_add_listen(EnEx *ctx, GtkWidget *parent) {
|
||
GtkWidget *card;
|
||
GtkWidget *text_col;
|
||
GtkWidget *script;
|
||
GtkWidget *controls;
|
||
GtkWidget *play;
|
||
GtkWidget *note;
|
||
|
||
en_heading(parent, "en_sec_listen");
|
||
|
||
note = gtk_label_new(NULL);
|
||
i18n_bind(note, "en_listen_note", 0);
|
||
gtk_label_set_wrap(GTK_LABEL(note), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(note), 0);
|
||
gtk_widget_set_halign(note, GTK_ALIGN_START);
|
||
gtk_widget_add_css_class(note, "quiz-intro");
|
||
gtk_box_append(GTK_BOX(parent), note);
|
||
|
||
card = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 22);
|
||
gtk_widget_add_css_class(card, "quiz-card");
|
||
gtk_widget_add_css_class(card, "listen-card");
|
||
|
||
controls = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
|
||
gtk_widget_set_hexpand(controls, FALSE);
|
||
gtk_widget_set_halign(controls, GTK_ALIGN_START);
|
||
gtk_widget_set_valign(controls, GTK_ALIGN_START);
|
||
gtk_widget_set_size_request(controls, 168, -1);
|
||
|
||
ctx->speed = 0.9;
|
||
ctx->gain = 1.0;
|
||
play = gtk_button_new_with_label(tr("en_play"));
|
||
gtk_widget_add_css_class(play, "btn-primary");
|
||
gtk_widget_set_hexpand(play, TRUE);
|
||
gtk_widget_set_halign(play, GTK_ALIGN_FILL);
|
||
ctx->play_btn = play;
|
||
g_signal_connect(play, "clicked", G_CALLBACK(en_play_clicked), ctx);
|
||
gtk_box_append(GTK_BOX(controls), play);
|
||
gtk_box_append(GTK_BOX(controls),
|
||
en_level_control(ctx, "en_speed", "speed",
|
||
0.6, 1.5, ctx->speed));
|
||
gtk_box_append(GTK_BOX(controls),
|
||
en_level_control(ctx, "en_volume", "volume",
|
||
0, 100, ctx->gain * 100.0));
|
||
|
||
text_col = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||
gtk_widget_set_hexpand(text_col, TRUE);
|
||
gtk_widget_set_valign(text_col, GTK_ALIGN_CENTER);
|
||
|
||
script = gtk_label_new(ctx->lesson->listening);
|
||
gtk_label_set_wrap(GTK_LABEL(script), TRUE);
|
||
gtk_label_set_wrap_mode(GTK_LABEL(script), PANGO_WRAP_WORD_CHAR);
|
||
gtk_label_set_width_chars(GTK_LABEL(script), 36);
|
||
gtk_label_set_max_width_chars(GTK_LABEL(script), 72);
|
||
gtk_label_set_selectable(GTK_LABEL(script), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(script), 0);
|
||
gtk_widget_set_halign(script, GTK_ALIGN_FILL);
|
||
gtk_widget_set_hexpand(script, TRUE);
|
||
gtk_widget_add_css_class(script, "listen-script");
|
||
gtk_box_append(GTK_BOX(text_col), script);
|
||
|
||
ctx->play_note = gtk_label_new("");
|
||
gtk_label_set_wrap(GTK_LABEL(ctx->play_note), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(ctx->play_note), 0);
|
||
gtk_widget_set_halign(ctx->play_note, GTK_ALIGN_START);
|
||
gtk_widget_add_css_class(ctx->play_note, "meaning");
|
||
gtk_widget_set_visible(ctx->play_note, FALSE);
|
||
gtk_box_append(GTK_BOX(text_col), ctx->play_note);
|
||
|
||
gtk_box_append(GTK_BOX(card), controls);
|
||
gtk_box_append(GTK_BOX(card), text_col);
|
||
gtk_box_append(GTK_BOX(parent), card);
|
||
}
|
||
|
||
static void en_check(GtkButton *button, gpointer data) {
|
||
EnEx *ctx = data;
|
||
int mcq_ok = 0;
|
||
int gap_ok = 0;
|
||
gboolean write_ok = TRUE;
|
||
gboolean all;
|
||
|
||
(void)button;
|
||
if (ctx->lesson->listening && !ctx->played) {
|
||
set_feedback(ctx->feedback, FALSE, tr("en_listen_first"));
|
||
return;
|
||
}
|
||
|
||
for (guint i = 0; i < ctx->picks->len; i++) {
|
||
EnPick *p = &g_array_index(ctx->picks, EnPick, i);
|
||
int active = -1;
|
||
|
||
for (int o = 0; o < p->q->n_options; o++) {
|
||
gtk_widget_remove_css_class(GTK_WIDGET(p->tb[o]), "ok");
|
||
gtk_widget_remove_css_class(GTK_WIDGET(p->tb[o]), "wrong");
|
||
if (gtk_toggle_button_get_active(p->tb[o]))
|
||
active = o;
|
||
}
|
||
if (active == p->q->correct) {
|
||
mcq_ok++;
|
||
if (active >= 0)
|
||
gtk_widget_add_css_class(GTK_WIDGET(p->tb[active]), "ok");
|
||
} else if (active >= 0) {
|
||
gtk_widget_add_css_class(GTK_WIDGET(p->tb[active]), "wrong");
|
||
}
|
||
if (p->hint)
|
||
gtk_widget_set_visible(p->hint, TRUE);
|
||
}
|
||
|
||
for (guint i = 0; i < ctx->entries->len; i++) {
|
||
GtkWidget *entry = g_array_index(ctx->entries, GtkWidget *, i);
|
||
const char *txt = gtk_editable_get_text(GTK_EDITABLE(entry));
|
||
char *norm = normalize_answer(txt);
|
||
gboolean ok = answer_accepts(norm, ctx->lesson->gaps[i].answers);
|
||
|
||
gtk_widget_remove_css_class(entry, "answer-ok");
|
||
gtk_widget_remove_css_class(entry, "answer-wrong");
|
||
gtk_widget_add_css_class(entry, ok ? "answer-ok" : "answer-wrong");
|
||
if (ok)
|
||
gap_ok++;
|
||
g_free(norm);
|
||
if (i < ctx->gap_hints->len) {
|
||
GtkWidget *hint = g_array_index(ctx->gap_hints, GtkWidget *, i);
|
||
|
||
if (hint)
|
||
gtk_widget_set_visible(hint, TRUE);
|
||
}
|
||
}
|
||
|
||
if (ctx->essay && ctx->lesson->write_prompt) {
|
||
GtkTextBuffer *buf = gtk_text_view_get_buffer(GTK_TEXT_VIEW(ctx->essay));
|
||
GtkTextIter a, b;
|
||
char *raw;
|
||
char *norm;
|
||
gboolean keys = TRUE;
|
||
|
||
gtk_text_buffer_get_bounds(buf, &a, &b);
|
||
raw = gtk_text_buffer_get_text(buf, &a, &b, FALSE);
|
||
norm = normalize_answer(raw);
|
||
if (ctx->lesson->write_keys) {
|
||
gchar **parts = g_strsplit(ctx->lesson->write_keys, "|", -1);
|
||
|
||
for (int i = 0; parts[i]; i++) {
|
||
if (parts[i][0] && !en_has_word(norm, parts[i]))
|
||
keys = FALSE;
|
||
}
|
||
g_strfreev(parts);
|
||
}
|
||
write_ok = keys && en_word_count(norm) >= ctx->lesson->write_min;
|
||
g_free(norm);
|
||
g_free(raw);
|
||
}
|
||
|
||
all = mcq_ok == (int)ctx->picks->len
|
||
&& gap_ok == ctx->lesson->n_gaps
|
||
&& write_ok;
|
||
if (all) {
|
||
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
|
||
if (ctx->model)
|
||
gtk_widget_set_visible(ctx->model, TRUE);
|
||
if (ctx->on_done)
|
||
ctx->on_done(ctx->year, ctx->index);
|
||
} else if (ctx->essay && !write_ok &&
|
||
mcq_ok == (int)ctx->picks->len &&
|
||
gap_ok == ctx->lesson->n_gaps) {
|
||
set_feedback(ctx->feedback, FALSE, tr("en_write_short"));
|
||
} else {
|
||
set_feedback(ctx->feedback, FALSE, tr("feedback_retry_short"));
|
||
}
|
||
}
|
||
|
||
GtkWidget *skill_ex_page(const EnLesson *lesson, int year, int index,
|
||
void (*on_done)(int, int),
|
||
const char *back, const char *title_key,
|
||
const char *voice) {
|
||
EnEx *ctx = g_new0(EnEx, 1);
|
||
GtkWidget *page;
|
||
GtkWidget *scroll;
|
||
GtkWidget *body;
|
||
GtkWidget *check;
|
||
|
||
ctx->year = year;
|
||
ctx->index = index;
|
||
ctx->lesson = lesson;
|
||
ctx->on_done = on_done;
|
||
ctx->voice = voice && voice[0] ? voice : "en";
|
||
ctx->picks = g_array_new(FALSE, FALSE, sizeof(EnPick));
|
||
ctx->entries = g_array_new(FALSE, FALSE, sizeof(GtkWidget *));
|
||
ctx->gap_hints = g_array_new(FALSE, TRUE, sizeof(GtkWidget *));
|
||
|
||
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, title_key, NULL));
|
||
|
||
/* Keep the recording in view while the questions scroll. Lessons that
|
||
* also have a reading passage leave it in the scroll, above the player. */
|
||
if (lesson->listening && !lesson->reading)
|
||
en_add_listen(ctx, page);
|
||
|
||
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, 12);
|
||
gtk_widget_set_hexpand(body, TRUE);
|
||
gtk_widget_set_margin_bottom(body, 12);
|
||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroll), body);
|
||
|
||
if (lesson->reading) {
|
||
en_heading(body, lesson->n_read ? "en_sec_read" : "en_sec_read");
|
||
en_passage(body, lesson->reading, FALSE);
|
||
if (lesson->n_read)
|
||
en_add_mcq(ctx, body, lesson->readq, lesson->read_hints, lesson->n_read);
|
||
}
|
||
|
||
if (lesson->listening && lesson->reading)
|
||
en_add_listen(ctx, body);
|
||
if (lesson->n_listen)
|
||
en_add_mcq(ctx, body, lesson->listenq, lesson->listen_hints,
|
||
lesson->n_listen);
|
||
|
||
if (lesson->n_gaps) {
|
||
en_heading(body, "en_sec_gap");
|
||
for (int i = 0; i < lesson->n_gaps; i++) {
|
||
GtkWidget *card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||
GtkWidget *prompt = gtk_label_new(NULL);
|
||
GtkWidget *entry = gtk_entry_new();
|
||
GtkWidget *hint = NULL;
|
||
char *text = g_strdup_printf("%d.) %s", i + 1, lesson->gaps[i].prompt);
|
||
|
||
gtk_widget_add_css_class(card, "quiz-card");
|
||
gtk_label_set_text(GTK_LABEL(prompt), text);
|
||
g_free(text);
|
||
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(prompt), 0);
|
||
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
|
||
gtk_box_append(GTK_BOX(card), prompt);
|
||
gtk_editable_set_width_chars(GTK_EDITABLE(entry), 24);
|
||
gtk_widget_set_halign(entry, GTK_ALIGN_START);
|
||
gtk_box_append(GTK_BOX(card), entry);
|
||
if (lesson->gaps[i].meaning)
|
||
hint = meaning_add(card, lesson->gaps[i].meaning);
|
||
gtk_box_append(GTK_BOX(body), card);
|
||
g_array_append_val(ctx->entries, entry);
|
||
g_array_append_val(ctx->gap_hints, hint);
|
||
}
|
||
}
|
||
|
||
if (lesson->write_prompt) {
|
||
GtkWidget *prompt;
|
||
GtkWidget *view;
|
||
|
||
en_heading(body, "en_sec_write");
|
||
prompt = gtk_label_new(lesson->write_prompt);
|
||
gtk_label_set_wrap(GTK_LABEL(prompt), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(prompt), 0);
|
||
gtk_widget_set_halign(prompt, GTK_ALIGN_START);
|
||
gtk_widget_add_css_class(prompt, "quiz-q");
|
||
gtk_box_append(GTK_BOX(body), prompt);
|
||
|
||
view = gtk_text_view_new();
|
||
gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(view), GTK_WRAP_WORD_CHAR);
|
||
gtk_widget_set_size_request(view, -1, 140);
|
||
gtk_widget_add_css_class(view, "quiz-card");
|
||
gtk_box_append(GTK_BOX(body), view);
|
||
ctx->essay = view;
|
||
|
||
ctx->model = gtk_label_new(lesson->write_model);
|
||
gtk_label_set_wrap(GTK_LABEL(ctx->model), TRUE);
|
||
gtk_label_set_selectable(GTK_LABEL(ctx->model), TRUE);
|
||
gtk_label_set_xalign(GTK_LABEL(ctx->model), 0);
|
||
gtk_widget_set_halign(ctx->model, GTK_ALIGN_START);
|
||
gtk_widget_add_css_class(ctx->model, "meaning");
|
||
gtk_widget_set_visible(ctx->model, FALSE);
|
||
gtk_box_append(GTK_BOX(body), ctx->model);
|
||
}
|
||
|
||
if (lesson->n_quiz) {
|
||
en_heading(body, "en_sec_quiz");
|
||
en_add_mcq(ctx, body, lesson->quiz, lesson->quiz_hints, lesson->n_quiz);
|
||
}
|
||
|
||
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_widget_set_margin_top(check, 8);
|
||
gtk_box_append(GTK_BOX(body), check);
|
||
g_signal_connect(check, "clicked", G_CALLBACK(en_check), ctx);
|
||
|
||
ctx->feedback = gtk_label_new("");
|
||
gtk_widget_set_halign(ctx->feedback, GTK_ALIGN_START);
|
||
gtk_label_set_wrap(GTK_LABEL(ctx->feedback), TRUE);
|
||
gtk_box_append(GTK_BOX(body), ctx->feedback);
|
||
return page;
|
||
}
|
||
|
||
static GtkWidget *build_en_unit_page(int year, int index) {
|
||
return skill_unit_page(&en_rt[year][index], &en_lessons[year][index],
|
||
en_map_page[year], en_title_key[year][index],
|
||
en_sub_key[year][index]);
|
||
}
|
||
|
||
static GtkWidget *build_en_ex_page(int year, int index) {
|
||
return skill_ex_page(&en_lessons[year][index], year, index, mark_en_done,
|
||
en_unit_page[year][index], en_title_key[year][index],
|
||
"en");
|
||
}
|
||
|
||
void add_en_pages(GtkStack *stack) {
|
||
en_prepare();
|
||
for (int y = 0; y < EN_YEARS; y++) {
|
||
gtk_stack_add_named(stack, build_enmap_page(y), en_map_page[y]);
|
||
for (int i = 0; i < EN_N; i++) {
|
||
gtk_stack_add_named(stack, build_en_unit_page(y, i),
|
||
en_unit_page[y][i]);
|
||
gtk_stack_add_named(stack, build_en_ex_page(y, i),
|
||
en_ex_page[y][i]);
|
||
}
|
||
}
|
||
}
|
||
|
||
void draw_uk_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 w = (double)width;
|
||
double h = (double)height;
|
||
|
||
(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_arc(cr, cx, cy, r, 0.0, 2.0 * G_PI);
|
||
cairo_clip(cr);
|
||
|
||
cairo_set_source_rgb(cr, 0.01, 0.14, 0.40);
|
||
cairo_paint(cr);
|
||
|
||
cairo_set_line_cap(cr, CAIRO_LINE_CAP_SQUARE);
|
||
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
|
||
cairo_set_line_width(cr, w * 0.16);
|
||
cairo_move_to(cr, 0, 0);
|
||
cairo_line_to(cr, w, h);
|
||
cairo_move_to(cr, w, 0);
|
||
cairo_line_to(cr, 0, h);
|
||
cairo_stroke(cr);
|
||
|
||
cairo_set_source_rgb(cr, 0.80, 0.05, 0.15);
|
||
cairo_set_line_width(cr, w * 0.055);
|
||
cairo_move_to(cr, 0, 0);
|
||
cairo_line_to(cr, w, h);
|
||
cairo_move_to(cr, w, 0);
|
||
cairo_line_to(cr, 0, h);
|
||
cairo_stroke(cr);
|
||
|
||
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
|
||
cairo_rectangle(cr, 0, h * 0.38, w, h * 0.24);
|
||
cairo_rectangle(cr, w * 0.38, 0, w * 0.24, h);
|
||
cairo_fill(cr);
|
||
|
||
cairo_set_source_rgb(cr, 0.80, 0.05, 0.15);
|
||
cairo_rectangle(cr, 0, h * 0.43, w, h * 0.14);
|
||
cairo_rectangle(cr, w * 0.43, 0, w * 0.14, h);
|
||
cairo_fill(cr);
|
||
|
||
cairo_restore(cr);
|
||
}
|