|
|
|
@@ -0,0 +1,832 @@
|
|
|
|
|
/*
|
|
|
|
|
* main.cpp
|
|
|
|
|
* MontaukOS Time Zone configuration applet
|
|
|
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <montauk/config.h>
|
|
|
|
|
#include <montauk/heap.h>
|
|
|
|
|
#include <montauk/string.h>
|
|
|
|
|
#include <montauk/syscall.h>
|
|
|
|
|
#include <gui/gui.hpp>
|
|
|
|
|
#include <gui/canvas.hpp>
|
|
|
|
|
#include <gui/mtk.hpp>
|
|
|
|
|
#include <gui/standalone.hpp>
|
|
|
|
|
#include <gui/truetype.hpp>
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using namespace gui;
|
|
|
|
|
|
|
|
|
|
static constexpr int INIT_W = 640;
|
|
|
|
|
static constexpr int INIT_H = 460;
|
|
|
|
|
static constexpr int HEADER_H = 14;
|
|
|
|
|
static constexpr int FOOTER_H = 58;
|
|
|
|
|
static constexpr int PAD = 16;
|
|
|
|
|
static constexpr int GAP = 12;
|
|
|
|
|
static constexpr int LABEL_H = 24;
|
|
|
|
|
static constexpr int ROW_H = 30;
|
|
|
|
|
static constexpr int MAX_COUNTRIES = 320;
|
|
|
|
|
|
|
|
|
|
struct CountryEntry {
|
|
|
|
|
char key[64]; // Full TOML table key, e.g. countries.NO
|
|
|
|
|
char code[24]; // Country code suffix, e.g. NO
|
|
|
|
|
const char* name;
|
|
|
|
|
const char* default_tz;
|
|
|
|
|
const char* note;
|
|
|
|
|
bool dst_enabled;
|
|
|
|
|
char dst_rule[32];
|
|
|
|
|
montauk::toml::Value* zones;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static WsWindow g_win;
|
|
|
|
|
static montauk::toml::Doc g_data;
|
|
|
|
|
static bool g_data_loaded = false;
|
|
|
|
|
static CountryEntry g_countries[MAX_COUNTRIES];
|
|
|
|
|
static int g_country_count = 0;
|
|
|
|
|
|
|
|
|
|
static int g_selected_country = 0;
|
|
|
|
|
static int g_selected_zone = 0;
|
|
|
|
|
static int g_saved_country = 0;
|
|
|
|
|
static int g_saved_zone = 0;
|
|
|
|
|
static int g_country_scroll = 0;
|
|
|
|
|
static int g_zone_scroll = 0;
|
|
|
|
|
static int g_focus_pane = 0; // 0=country list, 1=zone list
|
|
|
|
|
static int g_mouse_x = -1;
|
|
|
|
|
static int g_mouse_y = -1;
|
|
|
|
|
static bool g_dirty = false;
|
|
|
|
|
static char g_status[160] = {};
|
|
|
|
|
static uint64_t g_status_time = 0;
|
|
|
|
|
static Color g_accent = colors::ACCENT;
|
|
|
|
|
|
|
|
|
|
static void safe_copy(char* dst, int cap, const char* src) {
|
|
|
|
|
montauk::strncpy(dst, src ? src : "", cap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int str_compare_ci(const char* a, const char* b) {
|
|
|
|
|
while (*a && *b) {
|
|
|
|
|
char ca = (*a >= 'A' && *a <= 'Z') ? (*a + 32) : *a;
|
|
|
|
|
char cb = (*b >= 'A' && *b <= 'Z') ? (*b + 32) : *b;
|
|
|
|
|
if (ca != cb) return ca - cb;
|
|
|
|
|
a++;
|
|
|
|
|
b++;
|
|
|
|
|
}
|
|
|
|
|
return (unsigned char)*a - (unsigned char)*b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void set_status(const char* msg) {
|
|
|
|
|
safe_copy(g_status, sizeof(g_status), msg);
|
|
|
|
|
g_status_time = montauk::get_milliseconds();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool status_visible() {
|
|
|
|
|
return g_status[0] && (montauk::get_milliseconds() - g_status_time < 5000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static mtk::Theme app_theme() {
|
|
|
|
|
mtk::Theme theme = mtk::make_theme(g_accent);
|
|
|
|
|
theme.disabled_bg = Color::from_rgb(0xE4, 0xE4, 0xE4);
|
|
|
|
|
theme.disabled_fg = Color::from_rgb(0x9A, 0x9A, 0x9A);
|
|
|
|
|
return theme;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void apply_scale(int scale) {
|
|
|
|
|
switch (scale) {
|
|
|
|
|
case 0:
|
|
|
|
|
fonts::UI_SIZE = 14;
|
|
|
|
|
fonts::TITLE_SIZE = 14;
|
|
|
|
|
fonts::TERM_SIZE = 14;
|
|
|
|
|
fonts::LARGE_SIZE = 22;
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
fonts::UI_SIZE = 22;
|
|
|
|
|
fonts::TITLE_SIZE = 22;
|
|
|
|
|
fonts::TERM_SIZE = 22;
|
|
|
|
|
fonts::LARGE_SIZE = 34;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
fonts::UI_SIZE = 18;
|
|
|
|
|
fonts::TITLE_SIZE = 18;
|
|
|
|
|
fonts::TERM_SIZE = 18;
|
|
|
|
|
fonts::LARGE_SIZE = 28;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void load_accent() {
|
|
|
|
|
g_accent = colors::ACCENT;
|
|
|
|
|
|
|
|
|
|
char user[64] = {};
|
|
|
|
|
if (montauk::getuser(user, sizeof(user)) > 0 && user[0]) {
|
|
|
|
|
auto doc = montauk::config::load_user(user, "desktop");
|
|
|
|
|
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
|
|
|
|
if (accent >= 0) {
|
|
|
|
|
g_accent = Color::from_rgb((uint8_t)((accent >> 16) & 0xFF),
|
|
|
|
|
(uint8_t)((accent >> 8) & 0xFF),
|
|
|
|
|
(uint8_t)(accent & 0xFF));
|
|
|
|
|
doc.destroy();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
doc.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto doc = montauk::config::load("desktop");
|
|
|
|
|
int64_t accent = doc.get_int("appearance.accent_color", -1);
|
|
|
|
|
if (accent >= 0) {
|
|
|
|
|
g_accent = Color::from_rgb((uint8_t)((accent >> 16) & 0xFF),
|
|
|
|
|
(uint8_t)((accent >> 8) & 0xFF),
|
|
|
|
|
(uint8_t)(accent & 0xFF));
|
|
|
|
|
}
|
|
|
|
|
doc.destroy();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void build_key(char* out, int cap, const char* base, const char* field) {
|
|
|
|
|
snprintf(out, cap, "%s.%s", base, field);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char* zone_name(const CountryEntry& country, int index) {
|
|
|
|
|
if (!country.zones || index < 0 || index >= country.zones->array.count) return "";
|
|
|
|
|
montauk::toml::Value* item = country.zones->array.items[index];
|
|
|
|
|
if (!item || item->type != montauk::toml::Type::String) return "";
|
|
|
|
|
|
|
|
|
|
if (country.zones->type == montauk::toml::Type::Array)
|
|
|
|
|
return item->str ? item->str : "";
|
|
|
|
|
|
|
|
|
|
char prefix[96];
|
|
|
|
|
snprintf(prefix, sizeof(prefix), "%s.timezones.", country.key);
|
|
|
|
|
return (item->key && montauk::starts_with(item->key, prefix))
|
|
|
|
|
? item->key + montauk::slen(prefix)
|
|
|
|
|
: "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int zone_count(const CountryEntry& country) {
|
|
|
|
|
return country.zones ? country.zones->array.count : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int find_zone_index(const CountryEntry& country, const char* zone) {
|
|
|
|
|
if (!zone || !zone[0]) return -1;
|
|
|
|
|
int count = zone_count(country);
|
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
|
|
|
if (montauk::streq(zone_name(country, i), zone))
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int default_zone_index(const CountryEntry& country) {
|
|
|
|
|
int index = find_zone_index(country, country.default_tz);
|
|
|
|
|
return index >= 0 ? index : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char* zone_offset_string(const CountryEntry& country, int index) {
|
|
|
|
|
if (!country.zones || country.zones->type != montauk::toml::Type::Table)
|
|
|
|
|
return "";
|
|
|
|
|
if (index < 0 || index >= country.zones->array.count)
|
|
|
|
|
return "";
|
|
|
|
|
|
|
|
|
|
montauk::toml::Value* item = country.zones->array.items[index];
|
|
|
|
|
if (!item || item->type != montauk::toml::Type::String)
|
|
|
|
|
return "";
|
|
|
|
|
return item->str ? item->str : "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool parse_offset_minutes(const char* value, int* out_minutes) {
|
|
|
|
|
if (!value || !value[0]) return false;
|
|
|
|
|
|
|
|
|
|
int sign = 1;
|
|
|
|
|
int pos = 0;
|
|
|
|
|
if (value[pos] == '+') {
|
|
|
|
|
pos++;
|
|
|
|
|
} else if (value[pos] == '-') {
|
|
|
|
|
sign = -1;
|
|
|
|
|
pos++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value[pos] < '0' || value[pos] > '9') return false;
|
|
|
|
|
int hours = 0;
|
|
|
|
|
while (value[pos] >= '0' && value[pos] <= '9') {
|
|
|
|
|
hours = hours * 10 + (value[pos] - '0');
|
|
|
|
|
pos++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int minutes = 0;
|
|
|
|
|
if (value[pos] == ':') {
|
|
|
|
|
pos++;
|
|
|
|
|
if (value[pos] < '0' || value[pos] > '9') return false;
|
|
|
|
|
while (value[pos] >= '0' && value[pos] <= '9') {
|
|
|
|
|
minutes = minutes * 10 + (value[pos] - '0');
|
|
|
|
|
pos++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (value[pos] != '\0') return false;
|
|
|
|
|
if (hours > 24 || minutes > 59) return false;
|
|
|
|
|
|
|
|
|
|
if (out_minutes) *out_minutes = sign * (hours * 60 + minutes);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int visible_rows(const Rect& list) {
|
|
|
|
|
int rows = list.h / ROW_H;
|
|
|
|
|
return rows > 0 ? rows : 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void clamp_scrolls() {
|
|
|
|
|
Rect country_list = {};
|
|
|
|
|
Rect zone_list = {};
|
|
|
|
|
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
|
|
|
|
|
int list_y = HEADER_H + LABEL_H;
|
|
|
|
|
int list_h = gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H);
|
|
|
|
|
country_list = {PAD, list_y, left_w, list_h};
|
|
|
|
|
zone_list = {PAD + left_w + GAP, list_y,
|
|
|
|
|
g_win.width - PAD * 2 - GAP - left_w, list_h};
|
|
|
|
|
|
|
|
|
|
int country_max = gui_max(0, g_country_count - visible_rows(country_list));
|
|
|
|
|
if (g_country_scroll < 0) g_country_scroll = 0;
|
|
|
|
|
if (g_country_scroll > country_max) g_country_scroll = country_max;
|
|
|
|
|
|
|
|
|
|
int zcount = (g_selected_country >= 0 && g_selected_country < g_country_count)
|
|
|
|
|
? zone_count(g_countries[g_selected_country]) : 0;
|
|
|
|
|
int zone_max = gui_max(0, zcount - visible_rows(zone_list));
|
|
|
|
|
if (g_zone_scroll < 0) g_zone_scroll = 0;
|
|
|
|
|
if (g_zone_scroll > zone_max) g_zone_scroll = zone_max;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ensure_country_visible() {
|
|
|
|
|
Rect list = {PAD, HEADER_H + LABEL_H, 250,
|
|
|
|
|
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
|
|
|
|
int rows = visible_rows(list);
|
|
|
|
|
if (g_selected_country < g_country_scroll)
|
|
|
|
|
g_country_scroll = g_selected_country;
|
|
|
|
|
else if (g_selected_country >= g_country_scroll + rows)
|
|
|
|
|
g_country_scroll = g_selected_country - rows + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void ensure_zone_visible() {
|
|
|
|
|
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
|
|
|
|
|
Rect list = {PAD + left_w + GAP, HEADER_H + LABEL_H,
|
|
|
|
|
g_win.width - PAD * 2 - GAP - left_w,
|
|
|
|
|
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
|
|
|
|
int rows = visible_rows(list);
|
|
|
|
|
if (g_selected_zone < g_zone_scroll)
|
|
|
|
|
g_zone_scroll = g_selected_zone;
|
|
|
|
|
else if (g_selected_zone >= g_zone_scroll + rows)
|
|
|
|
|
g_zone_scroll = g_selected_zone - rows + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int find_country_by_code(const char* code) {
|
|
|
|
|
if (!code || !code[0]) return -1;
|
|
|
|
|
for (int i = 0; i < g_country_count; i++) {
|
|
|
|
|
if (montauk::streq(g_countries[i].code, code))
|
|
|
|
|
return i;
|
|
|
|
|
}
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void sort_countries() {
|
|
|
|
|
for (int i = 1; i < g_country_count; i++) {
|
|
|
|
|
CountryEntry tmp = g_countries[i];
|
|
|
|
|
int j = i - 1;
|
|
|
|
|
while (j >= 0 && str_compare_ci(tmp.name, g_countries[j].name) < 0) {
|
|
|
|
|
g_countries[j + 1] = g_countries[j];
|
|
|
|
|
j--;
|
|
|
|
|
}
|
|
|
|
|
g_countries[j + 1] = tmp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void load_timezone_data() {
|
|
|
|
|
g_data = montauk::config::load("timezonedata");
|
|
|
|
|
g_data_loaded = true;
|
|
|
|
|
g_country_count = 0;
|
|
|
|
|
|
|
|
|
|
static constexpr const char* prefix = "countries.";
|
|
|
|
|
int prefix_len = montauk::slen(prefix);
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < g_data.entries.count && g_country_count < MAX_COUNTRIES; i++) {
|
|
|
|
|
montauk::toml::Value* item = g_data.entries.items[i];
|
|
|
|
|
if (!item || item->type != montauk::toml::Type::Table || !item->key) continue;
|
|
|
|
|
if (!montauk::starts_with(item->key, prefix)) continue;
|
|
|
|
|
|
|
|
|
|
CountryEntry entry = {};
|
|
|
|
|
safe_copy(entry.key, sizeof(entry.key), item->key);
|
|
|
|
|
safe_copy(entry.code, sizeof(entry.code), item->key + prefix_len);
|
|
|
|
|
|
|
|
|
|
char key[128];
|
|
|
|
|
build_key(key, sizeof(key), item->key, "name");
|
|
|
|
|
entry.name = g_data.get_string(key, entry.code);
|
|
|
|
|
|
|
|
|
|
build_key(key, sizeof(key), item->key, "timezones");
|
|
|
|
|
entry.zones = g_data.get(key);
|
|
|
|
|
if (!entry.zones || entry.zones->array.count <= 0) continue;
|
|
|
|
|
if (entry.zones->type != montauk::toml::Type::Array &&
|
|
|
|
|
entry.zones->type != montauk::toml::Type::Table) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
build_key(key, sizeof(key), item->key, "default_tz");
|
|
|
|
|
entry.default_tz = g_data.get_string(key, zone_name(entry, 0));
|
|
|
|
|
|
|
|
|
|
build_key(key, sizeof(key), item->key, "dst_enabled");
|
|
|
|
|
entry.dst_enabled = g_data.get_bool(key, false);
|
|
|
|
|
|
|
|
|
|
build_key(key, sizeof(key), item->key, "dst_rules.rule");
|
|
|
|
|
safe_copy(entry.dst_rule, sizeof(entry.dst_rule), g_data.get_string(key, ""));
|
|
|
|
|
|
|
|
|
|
build_key(key, sizeof(key), item->key, "note");
|
|
|
|
|
entry.note = g_data.get_string(key, "");
|
|
|
|
|
|
|
|
|
|
g_countries[g_country_count++] = entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sort_countries();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void load_saved_selection() {
|
|
|
|
|
char saved_code[48] = "NO";
|
|
|
|
|
char saved_zone[96] = "";
|
|
|
|
|
|
|
|
|
|
auto cfg = montauk::config::load("timezone");
|
|
|
|
|
safe_copy(saved_code, sizeof(saved_code),
|
|
|
|
|
cfg.get_string("timezone.country_code", saved_code));
|
|
|
|
|
safe_copy(saved_zone, sizeof(saved_zone),
|
|
|
|
|
cfg.get_string("timezone.zone", ""));
|
|
|
|
|
cfg.destroy();
|
|
|
|
|
|
|
|
|
|
int country = find_country_by_code(saved_code);
|
|
|
|
|
if (country < 0) country = find_country_by_code("NO");
|
|
|
|
|
if (country < 0 && g_country_count > 0) country = 0;
|
|
|
|
|
|
|
|
|
|
g_selected_country = country >= 0 ? country : 0;
|
|
|
|
|
if (g_country_count > 0) {
|
|
|
|
|
CountryEntry& c = g_countries[g_selected_country];
|
|
|
|
|
int zone = find_zone_index(c, saved_zone);
|
|
|
|
|
if (zone < 0) zone = default_zone_index(c);
|
|
|
|
|
g_selected_zone = zone;
|
|
|
|
|
} else {
|
|
|
|
|
g_selected_zone = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_saved_country = g_selected_country;
|
|
|
|
|
g_saved_zone = g_selected_zone;
|
|
|
|
|
g_dirty = false;
|
|
|
|
|
ensure_country_visible();
|
|
|
|
|
ensure_zone_visible();
|
|
|
|
|
clamp_scrolls();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void fit_text(char* out, int cap, const char* text, int max_w) {
|
|
|
|
|
safe_copy(out, cap, text);
|
|
|
|
|
if (text_width(out) <= max_w) return;
|
|
|
|
|
|
|
|
|
|
static constexpr const char* ellipsis = "...";
|
|
|
|
|
int len = montauk::slen(out);
|
|
|
|
|
while (len > 0) {
|
|
|
|
|
out[--len] = '\0';
|
|
|
|
|
char candidate[160];
|
|
|
|
|
snprintf(candidate, sizeof(candidate), "%s%s", out, ellipsis);
|
|
|
|
|
if (text_width(candidate) <= max_w) {
|
|
|
|
|
safe_copy(out, cap, candidate);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
safe_copy(out, cap, ellipsis);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void format_offset(int minutes, char* out, int cap) {
|
|
|
|
|
int h = minutes / 60;
|
|
|
|
|
int m = minutes % 60;
|
|
|
|
|
if (m < 0) m = -m;
|
|
|
|
|
if (m) snprintf(out, cap, "UTC%s%d:%02d", h >= 0 ? "+" : "", h, m);
|
|
|
|
|
else snprintf(out, cap, "UTC%s%d", h >= 0 ? "+" : "", h);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool selected_offset_minutes(int* out_minutes) {
|
|
|
|
|
if (g_selected_country < 0 || g_selected_country >= g_country_count)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
CountryEntry& country = g_countries[g_selected_country];
|
|
|
|
|
char key[128];
|
|
|
|
|
|
|
|
|
|
const char* offset = zone_offset_string(country, g_selected_zone);
|
|
|
|
|
if (parse_offset_minutes(offset, out_minutes))
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
build_key(key, sizeof(key), country.key, "offsets");
|
|
|
|
|
montauk::toml::Value* offsets = g_data.get_array(key);
|
|
|
|
|
if (offsets && g_selected_zone >= 0 && g_selected_zone < offsets->array.count) {
|
|
|
|
|
montauk::toml::Value* item = offsets->array.items[g_selected_zone];
|
|
|
|
|
if (item && item->type == montauk::toml::Type::Int) {
|
|
|
|
|
if (out_minutes) *out_minutes = (int)item->ival;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
build_key(key, sizeof(key), country.key, "offset_minutes");
|
|
|
|
|
montauk::toml::Value* single = g_data.get(key);
|
|
|
|
|
if (single && single->type == montauk::toml::Type::Int) {
|
|
|
|
|
if (out_minutes) *out_minutes = (int)single->ival;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int dst_offset_change_minutes(const CountryEntry& country) {
|
|
|
|
|
if (!country.dst_enabled || !country.dst_rule[0])
|
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
|
|
char key[96];
|
|
|
|
|
snprintf(key, sizeof(key), "dst_rules.%s.offset_change", country.dst_rule);
|
|
|
|
|
return (int)(g_data.get_int(key, 0) / 60);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void save_selection() {
|
|
|
|
|
if (g_country_count <= 0) return;
|
|
|
|
|
CountryEntry& country = g_countries[g_selected_country];
|
|
|
|
|
const char* zone = zone_name(country, g_selected_zone);
|
|
|
|
|
|
|
|
|
|
auto cfg = montauk::config::load("timezone");
|
|
|
|
|
montauk::config::set_string(&cfg, "timezone.country_code", country.code);
|
|
|
|
|
montauk::config::set_string(&cfg, "timezone.country", country.name);
|
|
|
|
|
montauk::config::set_string(&cfg, "timezone.zone", zone);
|
|
|
|
|
montauk::config::set_bool(&cfg, "timezone.dst_enabled", country.dst_enabled);
|
|
|
|
|
if (country.dst_rule[0])
|
|
|
|
|
montauk::config::set_string(&cfg, "timezone.dst_rule", country.dst_rule);
|
|
|
|
|
else
|
|
|
|
|
montauk::config::unset(&cfg, "timezone.dst_rule");
|
|
|
|
|
|
|
|
|
|
int dst_change = dst_offset_change_minutes(country);
|
|
|
|
|
if (country.dst_enabled && dst_change != 0)
|
|
|
|
|
montauk::config::set_int(&cfg, "timezone.dst_offset_minutes", dst_change);
|
|
|
|
|
else
|
|
|
|
|
montauk::config::unset(&cfg, "timezone.dst_offset_minutes");
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
bool has_offset = selected_offset_minutes(&offset);
|
|
|
|
|
if (has_offset) {
|
|
|
|
|
const char* offset_text = zone_offset_string(country, g_selected_zone);
|
|
|
|
|
if (offset_text[0])
|
|
|
|
|
montauk::config::set_string(&cfg, "timezone.offset", offset_text);
|
|
|
|
|
montauk::config::set_int(&cfg, "timezone.offset_minutes", offset);
|
|
|
|
|
montauk::settz(offset);
|
|
|
|
|
} else {
|
|
|
|
|
montauk::config::unset(&cfg, "timezone.offset");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ret = montauk::config::save("timezone", &cfg);
|
|
|
|
|
cfg.destroy();
|
|
|
|
|
|
|
|
|
|
if (ret < 0) {
|
|
|
|
|
set_status("Could not save time zone");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_saved_country = g_selected_country;
|
|
|
|
|
g_saved_zone = g_selected_zone;
|
|
|
|
|
g_dirty = false;
|
|
|
|
|
set_status(has_offset ? "Saved and applied" : "Saved selection; clock offset unchanged");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void revert_selection() {
|
|
|
|
|
g_selected_country = g_saved_country;
|
|
|
|
|
g_selected_zone = g_saved_zone;
|
|
|
|
|
g_dirty = false;
|
|
|
|
|
ensure_country_visible();
|
|
|
|
|
ensure_zone_visible();
|
|
|
|
|
clamp_scrolls();
|
|
|
|
|
set_status("Selection reverted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Rect country_list_rect() {
|
|
|
|
|
int left_w = gui_min(250, (g_win.width - PAD * 2 - GAP) / 2);
|
|
|
|
|
return {PAD, HEADER_H + LABEL_H, left_w,
|
|
|
|
|
gui_max(g_win.height - HEADER_H - FOOTER_H - LABEL_H - PAD, ROW_H)};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Rect zone_list_rect() {
|
|
|
|
|
Rect country = country_list_rect();
|
|
|
|
|
return {country.x + country.w + GAP, country.y,
|
|
|
|
|
g_win.width - PAD * 2 - GAP - country.w, country.h};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Rect apply_button_rect() {
|
|
|
|
|
return {g_win.width - PAD - 104, g_win.height - FOOTER_H + 14, 104, 30};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Rect revert_button_rect() {
|
|
|
|
|
return {g_win.width - PAD - 104 - GAP - 92, g_win.height - FOOTER_H + 14, 92, 30};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool mouse_in_rect(const Rect& rect) {
|
|
|
|
|
return rect.contains(g_mouse_x, g_mouse_y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static mtk::WidgetState button_state(const Rect& rect, bool enabled, bool active = false) {
|
|
|
|
|
return mtk::widget_state(active, mouse_in_rect(rect), enabled);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool hover_changed(const Rect& rect, bool enabled,
|
|
|
|
|
int prev_x, int prev_y, int next_x, int next_y) {
|
|
|
|
|
if (!enabled) return false;
|
|
|
|
|
return rect.contains(prev_x, prev_y) != rect.contains(next_x, next_y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void draw_list_frame(Canvas& canvas, const Rect& list, const mtk::Theme& theme) {
|
|
|
|
|
canvas.fill_rect(list.x, list.y, list.w, list.h, theme.surface_alt);
|
|
|
|
|
canvas.rect(list.x, list.y, list.w, list.h, theme.border);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void draw_country_list(Canvas& canvas, const Rect& list, const mtk::Theme& theme) {
|
|
|
|
|
draw_list_frame(canvas, list, theme);
|
|
|
|
|
int rows = visible_rows(list);
|
|
|
|
|
for (int row = 0; row < rows; row++) {
|
|
|
|
|
int index = g_country_scroll + row;
|
|
|
|
|
if (index >= g_country_count) break;
|
|
|
|
|
|
|
|
|
|
Rect item = {list.x + 2, list.y + 2 + row * ROW_H, list.w - 4, ROW_H - 2};
|
|
|
|
|
bool selected = index == g_selected_country;
|
|
|
|
|
mtk::draw_list_row(canvas, item, selected, (index & 1) != 0, theme);
|
|
|
|
|
|
|
|
|
|
char label[128];
|
|
|
|
|
fit_text(label, sizeof(label), g_countries[index].name, item.w - 16);
|
|
|
|
|
canvas.text(item.x + 8, item.y + (item.h - system_font_height()) / 2,
|
|
|
|
|
label, selected ? theme.accent_fg : theme.text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void draw_zone_list(Canvas& canvas, const Rect& list, const mtk::Theme& theme) {
|
|
|
|
|
draw_list_frame(canvas, list, theme);
|
|
|
|
|
if (g_selected_country < 0 || g_selected_country >= g_country_count) return;
|
|
|
|
|
|
|
|
|
|
CountryEntry& country = g_countries[g_selected_country];
|
|
|
|
|
int count = zone_count(country);
|
|
|
|
|
int rows = visible_rows(list);
|
|
|
|
|
for (int row = 0; row < rows; row++) {
|
|
|
|
|
int index = g_zone_scroll + row;
|
|
|
|
|
if (index >= count) break;
|
|
|
|
|
|
|
|
|
|
Rect item = {list.x + 2, list.y + 2 + row * ROW_H, list.w - 4, ROW_H - 2};
|
|
|
|
|
bool selected = index == g_selected_zone;
|
|
|
|
|
mtk::draw_list_row(canvas, item, selected, (index & 1) != 0, theme);
|
|
|
|
|
|
|
|
|
|
char label[128];
|
|
|
|
|
fit_text(label, sizeof(label), zone_name(country, index), item.w - 16);
|
|
|
|
|
canvas.text(item.x + 8, item.y + (item.h - system_font_height()) / 2,
|
|
|
|
|
label, selected ? theme.accent_fg : theme.text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void draw_details(Canvas& canvas, const mtk::Theme& theme) {
|
|
|
|
|
if (g_country_count <= 0) return;
|
|
|
|
|
|
|
|
|
|
Rect country = country_list_rect();
|
|
|
|
|
Rect zone = zone_list_rect();
|
|
|
|
|
int x = zone.x;
|
|
|
|
|
int y = zone.y + zone.h + 8;
|
|
|
|
|
if (y > g_win.height - FOOTER_H - 24) return;
|
|
|
|
|
|
|
|
|
|
CountryEntry& entry = g_countries[g_selected_country];
|
|
|
|
|
char line[192];
|
|
|
|
|
snprintf(line, sizeof(line), "%s / %s", entry.name, zone_name(entry, g_selected_zone));
|
|
|
|
|
fit_text(line, sizeof(line), line, g_win.width - x - PAD);
|
|
|
|
|
canvas.text(x, y, line, theme.text);
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
char right[96];
|
|
|
|
|
if (selected_offset_minutes(&offset)) {
|
|
|
|
|
format_offset(offset, right, sizeof(right));
|
|
|
|
|
} else {
|
|
|
|
|
format_offset(montauk::gettz(), right, sizeof(right));
|
|
|
|
|
char tmp[96];
|
|
|
|
|
snprintf(tmp, sizeof(tmp), "Clock: %s", right);
|
|
|
|
|
safe_copy(right, sizeof(right), tmp);
|
|
|
|
|
}
|
|
|
|
|
canvas.text(country.x, y, right, theme.text_subtle);
|
|
|
|
|
|
|
|
|
|
y += system_font_height() + 6;
|
|
|
|
|
if (entry.dst_enabled) {
|
|
|
|
|
int dst_change = dst_offset_change_minutes(entry);
|
|
|
|
|
if (dst_change)
|
|
|
|
|
snprintf(line, sizeof(line), "DST: %s (+%d min)",
|
|
|
|
|
entry.dst_rule[0] ? entry.dst_rule : "enabled", dst_change);
|
|
|
|
|
else
|
|
|
|
|
snprintf(line, sizeof(line), "DST: %s",
|
|
|
|
|
entry.dst_rule[0] ? entry.dst_rule : "enabled");
|
|
|
|
|
} else {
|
|
|
|
|
safe_copy(line, sizeof(line), "DST: not used");
|
|
|
|
|
}
|
|
|
|
|
canvas.text(x, y, line, theme.text_subtle);
|
|
|
|
|
|
|
|
|
|
if (entry.note && entry.note[0]) {
|
|
|
|
|
char note[192];
|
|
|
|
|
fit_text(note, sizeof(note), entry.note, g_win.width - x - PAD);
|
|
|
|
|
canvas.text(x, y + system_font_height() + 4, note, theme.text_subtle);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void render() {
|
|
|
|
|
mtk::StandaloneHost host(&g_win);
|
|
|
|
|
Canvas canvas = host.canvas();
|
|
|
|
|
mtk::Theme theme = app_theme();
|
|
|
|
|
|
|
|
|
|
canvas.fill(theme.window_bg);
|
|
|
|
|
|
|
|
|
|
if (g_country_count <= 0) {
|
|
|
|
|
canvas.text(PAD, HEADER_H + 24,
|
|
|
|
|
"No time zone data was found in 0:/config/timezonedata.toml",
|
|
|
|
|
theme.danger);
|
|
|
|
|
host.present();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Rect country = country_list_rect();
|
|
|
|
|
Rect zone = zone_list_rect();
|
|
|
|
|
|
|
|
|
|
canvas.text(country.x, HEADER_H + 6, "Country", theme.text_subtle);
|
|
|
|
|
canvas.text(zone.x, HEADER_H + 6, "Time Zone", theme.text_subtle);
|
|
|
|
|
|
|
|
|
|
draw_country_list(canvas, country, theme);
|
|
|
|
|
draw_zone_list(canvas, zone, theme);
|
|
|
|
|
draw_details(canvas, theme);
|
|
|
|
|
|
|
|
|
|
Rect footer = {0, g_win.height - FOOTER_H, g_win.width, FOOTER_H};
|
|
|
|
|
canvas.fill_rect(footer.x, footer.y, footer.w, footer.h, theme.surface);
|
|
|
|
|
mtk::draw_separator(canvas, 0, footer.y, g_win.width, theme);
|
|
|
|
|
|
|
|
|
|
const char* footer_text = status_visible()
|
|
|
|
|
? g_status
|
|
|
|
|
: (g_dirty ? "Unsaved time zone selection" : "Selection saved");
|
|
|
|
|
int max_footer_text = revert_button_rect().x - PAD - GAP;
|
|
|
|
|
char footer_label[160];
|
|
|
|
|
fit_text(footer_label, sizeof(footer_label), footer_text, max_footer_text);
|
|
|
|
|
canvas.text(PAD, footer.y + (FOOTER_H - system_font_height()) / 2,
|
|
|
|
|
footer_label, g_dirty ? theme.text : theme.text_subtle);
|
|
|
|
|
|
|
|
|
|
Rect revert = revert_button_rect();
|
|
|
|
|
Rect apply = apply_button_rect();
|
|
|
|
|
mtk::draw_button(canvas, revert, "Revert", mtk::BUTTON_SECONDARY,
|
|
|
|
|
button_state(revert, g_dirty), theme);
|
|
|
|
|
mtk::draw_button(canvas, apply, "Apply", mtk::BUTTON_PRIMARY,
|
|
|
|
|
button_state(apply, g_dirty), theme);
|
|
|
|
|
|
|
|
|
|
host.present();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool handle_mouse(const Montauk::WinEvent& ev) {
|
|
|
|
|
int prev_x = g_mouse_x;
|
|
|
|
|
int prev_y = g_mouse_y;
|
|
|
|
|
g_mouse_x = ev.mouse.x;
|
|
|
|
|
g_mouse_y = ev.mouse.y;
|
|
|
|
|
|
|
|
|
|
Rect country = country_list_rect();
|
|
|
|
|
Rect zone = zone_list_rect();
|
|
|
|
|
Rect revert = revert_button_rect();
|
|
|
|
|
Rect apply = apply_button_rect();
|
|
|
|
|
bool redraw = false;
|
|
|
|
|
|
|
|
|
|
redraw |= hover_changed(revert, g_dirty, prev_x, prev_y, g_mouse_x, g_mouse_y);
|
|
|
|
|
redraw |= hover_changed(apply, g_dirty, prev_x, prev_y, g_mouse_x, g_mouse_y);
|
|
|
|
|
|
|
|
|
|
if (ev.mouse.scroll != 0) {
|
|
|
|
|
if (country.contains(g_mouse_x, g_mouse_y)) {
|
|
|
|
|
g_country_scroll -= ev.mouse.scroll * 3;
|
|
|
|
|
clamp_scrolls();
|
|
|
|
|
redraw = true;
|
|
|
|
|
} else if (zone.contains(g_mouse_x, g_mouse_y)) {
|
|
|
|
|
g_zone_scroll -= ev.mouse.scroll * 3;
|
|
|
|
|
clamp_scrolls();
|
|
|
|
|
redraw = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool clicked = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
|
|
|
|
if (!clicked) return redraw;
|
|
|
|
|
|
|
|
|
|
if (country.contains(g_mouse_x, g_mouse_y)) {
|
|
|
|
|
int row = (g_mouse_y - country.y - 2) / ROW_H;
|
|
|
|
|
int index = g_country_scroll + row;
|
|
|
|
|
if (index >= 0 && index < g_country_count) {
|
|
|
|
|
g_selected_country = index;
|
|
|
|
|
g_selected_zone = default_zone_index(g_countries[g_selected_country]);
|
|
|
|
|
g_zone_scroll = 0;
|
|
|
|
|
g_focus_pane = 0;
|
|
|
|
|
g_dirty = (g_selected_country != g_saved_country || g_selected_zone != g_saved_zone);
|
|
|
|
|
clamp_scrolls();
|
|
|
|
|
redraw = true;
|
|
|
|
|
}
|
|
|
|
|
return redraw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (zone.contains(g_mouse_x, g_mouse_y)) {
|
|
|
|
|
int row = (g_mouse_y - zone.y - 2) / ROW_H;
|
|
|
|
|
int index = g_zone_scroll + row;
|
|
|
|
|
if (g_selected_country >= 0 && g_selected_country < g_country_count &&
|
|
|
|
|
index >= 0 && index < zone_count(g_countries[g_selected_country])) {
|
|
|
|
|
g_selected_zone = index;
|
|
|
|
|
g_focus_pane = 1;
|
|
|
|
|
g_dirty = (g_selected_country != g_saved_country || g_selected_zone != g_saved_zone);
|
|
|
|
|
redraw = true;
|
|
|
|
|
}
|
|
|
|
|
return redraw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (apply.contains(g_mouse_x, g_mouse_y)) {
|
|
|
|
|
if (g_dirty) save_selection();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (revert.contains(g_mouse_x, g_mouse_y)) {
|
|
|
|
|
if (g_dirty) revert_selection();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return redraw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool handle_key(const Montauk::KeyEvent& key) {
|
|
|
|
|
if (!key.pressed || g_country_count <= 0) return false;
|
|
|
|
|
|
|
|
|
|
if (key.ascii == '\n' || key.ascii == '\r') {
|
|
|
|
|
if (g_dirty) save_selection();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (key.ascii == '\033') {
|
|
|
|
|
if (g_dirty) revert_selection();
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int* selected = g_focus_pane == 0 ? &g_selected_country : &g_selected_zone;
|
|
|
|
|
int count = g_focus_pane == 0
|
|
|
|
|
? g_country_count
|
|
|
|
|
: zone_count(g_countries[g_selected_country]);
|
|
|
|
|
|
|
|
|
|
int old_country = g_selected_country;
|
|
|
|
|
|
|
|
|
|
if (key.scancode == 0x4B) g_focus_pane = 0; // Left
|
|
|
|
|
else if (key.scancode == 0x4D) g_focus_pane = 1; // Right
|
|
|
|
|
else if (key.scancode == 0x48 && *selected > 0) (*selected)--; // Up
|
|
|
|
|
else if (key.scancode == 0x50 && *selected < count - 1) (*selected)++; // Down
|
|
|
|
|
else if (key.scancode == 0x49) *selected = gui_max(0, *selected - 8); // Page Up
|
|
|
|
|
else if (key.scancode == 0x51) *selected = gui_min(count - 1, *selected + 8); // Page Down
|
|
|
|
|
else return false;
|
|
|
|
|
|
|
|
|
|
if (old_country != g_selected_country) {
|
|
|
|
|
g_selected_zone = default_zone_index(g_countries[g_selected_country]);
|
|
|
|
|
g_zone_scroll = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ensure_country_visible();
|
|
|
|
|
ensure_zone_visible();
|
|
|
|
|
clamp_scrolls();
|
|
|
|
|
|
|
|
|
|
g_dirty = (g_selected_country != g_saved_country || g_selected_zone != g_saved_zone);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extern "C" void _start() {
|
|
|
|
|
if (!fonts::init())
|
|
|
|
|
montauk::exit(1);
|
|
|
|
|
|
|
|
|
|
apply_scale(montauk::win_getscale());
|
|
|
|
|
load_accent();
|
|
|
|
|
load_timezone_data();
|
|
|
|
|
|
|
|
|
|
if (!g_win.create("Time Zone", INIT_W, INIT_H))
|
|
|
|
|
montauk::exit(1);
|
|
|
|
|
|
|
|
|
|
load_saved_selection();
|
|
|
|
|
render();
|
|
|
|
|
|
|
|
|
|
while (g_win.id >= 0 && !g_win.closed) {
|
|
|
|
|
Montauk::WinEvent ev;
|
|
|
|
|
int r = g_win.poll(&ev);
|
|
|
|
|
bool redraw = false;
|
|
|
|
|
|
|
|
|
|
if (r < 0) break;
|
|
|
|
|
if (r == 0) {
|
|
|
|
|
montauk::sleep_ms(16);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ev.type == 3) break;
|
|
|
|
|
if (ev.type == 2) {
|
|
|
|
|
redraw = true;
|
|
|
|
|
} else if (ev.type == 4) {
|
|
|
|
|
apply_scale(g_win.scale_factor);
|
|
|
|
|
redraw = true;
|
|
|
|
|
} else if (ev.type == 1) {
|
|
|
|
|
redraw = handle_mouse(ev);
|
|
|
|
|
} else if (ev.type == 0) {
|
|
|
|
|
redraw = handle_key(ev.key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (redraw) render();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (g_data_loaded) g_data.destroy();
|
|
|
|
|
g_win.destroy();
|
|
|
|
|
montauk::exit(0);
|
|
|
|
|
}
|