feat: add time zone configuration applet to System Configuration
This commit is contained in:
+11
-4
@@ -59,7 +59,7 @@ BINDIR := bin
|
|||||||
PROGRAMS := $(notdir $(wildcard src/*))
|
PROGRAMS := $(notdir $(wildcard src/*))
|
||||||
|
|
||||||
# Programs with custom Makefiles (built separately).
|
# Programs with custom Makefiles (built separately).
|
||||||
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs test_dialogs test_dl libloader libhello crashpad
|
CUSTOM_BUILDS := doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator desktop login shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs test_dialogs test_dl libloader libhello crashpad
|
||||||
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
SYSTEM_PROGRAMS := $(filter-out $(CUSTOM_BUILDS),$(PROGRAMS))
|
||||||
|
|
||||||
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
# Build targets: system programs go to bin/os/, apps go to bin/apps/<name>/.
|
||||||
@@ -86,9 +86,9 @@ CONFIGDST := $(patsubst $(CONFIGDIR)/%,$(BINDIR)/config/%,$(CONFIGSRC))
|
|||||||
# Common shared assets (wallpapers, etc.)
|
# Common shared assets (wallpapers, etc.)
|
||||||
COMMONKEEP := $(BINDIR)/common/.keep
|
COMMONKEEP := $(BINDIR)/common/.keep
|
||||||
|
|
||||||
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad
|
.PHONY: all clean doom fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator login desktop shell rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs icons fonts configs bearssl libc tls libjpeg libjpegwrite install-apps libloader libs libhello test_dialogs test_dl crashpad
|
||||||
|
|
||||||
all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP)
|
all: bearssl libc libjpeg libjpegwrite tls libloader libs libhello $(TARGETS) fetch wiki wikipedia weather imageviewer fontpreview spreadsheet wordprocessor pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator doom rpgdemo paint tcc lua screenshot texteditor mandelbrot printers timezone printd printctl dialogs login desktop shell icons fonts install-apps test_dialogs test_dl crashpad $(MANDST) $(WWWDST) $(CA_CERTS) $(CONFIGDST) $(COMMONKEEP)
|
||||||
|
|
||||||
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
# Build BearSSL static library (cross-compiled for freestanding x86_64).
|
||||||
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
BEARSSL_INCLUDES := -isystem $(shell cd .. && pwd)/kernel/freestnd-c-hdrs/x86_64/include -isystem $(abspath include/libc)
|
||||||
@@ -117,6 +117,7 @@ libloader: libc
|
|||||||
libs: libloader
|
libs: libloader
|
||||||
$(MAKE) -C libs/libmath
|
$(MAKE) -C libs/libmath
|
||||||
$(MAKE) -C libs/libprintersapplet
|
$(MAKE) -C libs/libprintersapplet
|
||||||
|
$(MAKE) -C libs/libtimezonesapplet
|
||||||
|
|
||||||
# Build libhello shared GUI library.
|
# Build libhello shared GUI library.
|
||||||
libhello: libloader
|
libhello: libloader
|
||||||
@@ -214,6 +215,10 @@ calculator: libc
|
|||||||
printers: bearssl libc tls
|
printers: bearssl libc tls
|
||||||
$(MAKE) -C src/printers
|
$(MAKE) -C src/printers
|
||||||
|
|
||||||
|
# Build time zone standalone GUI tool (depends on libc).
|
||||||
|
timezone: libc
|
||||||
|
$(MAKE) -C src/timezone
|
||||||
|
|
||||||
# Build login screen via its own Makefile (depends on bearssl and libc).
|
# Build login screen via its own Makefile (depends on bearssl and libc).
|
||||||
login: bearssl libc
|
login: bearssl libc
|
||||||
$(MAKE) -C src/login
|
$(MAKE) -C src/login
|
||||||
@@ -299,7 +304,7 @@ test_dialogs: libc libloader dialogs
|
|||||||
$(MAKE) -C src/test_dialogs
|
$(MAKE) -C src/test_dialogs
|
||||||
|
|
||||||
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
# Install app bundles (manifests, icons, data files) into bin/apps/<name>/.
|
||||||
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers crashpad
|
install-apps: doom rpgdemo paint spreadsheet wordprocessor weather wikipedia imageviewer fontpreview pdfviewer disks devexplorer installer volume music video bluetooth terminal klog procmgr calculator screenshot texteditor mandelbrot printers timezone crashpad
|
||||||
../scripts/install_apps.sh
|
../scripts/install_apps.sh
|
||||||
|
|
||||||
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
# Copy man pages into bin/man/ so mkramdisk.sh picks them up.
|
||||||
@@ -364,6 +369,7 @@ clean:
|
|||||||
$(MAKE) -C src/procmgr clean
|
$(MAKE) -C src/procmgr clean
|
||||||
$(MAKE) -C src/calculator clean
|
$(MAKE) -C src/calculator clean
|
||||||
$(MAKE) -C src/printers clean
|
$(MAKE) -C src/printers clean
|
||||||
|
$(MAKE) -C src/timezone clean
|
||||||
$(MAKE) -C src/paint clean
|
$(MAKE) -C src/paint clean
|
||||||
$(MAKE) -C src/rpgdemo clean
|
$(MAKE) -C src/rpgdemo clean
|
||||||
$(MAKE) -C src/screenshot clean
|
$(MAKE) -C src/screenshot clean
|
||||||
@@ -376,5 +382,6 @@ clean:
|
|||||||
$(MAKE) -C src/libloader clean
|
$(MAKE) -C src/libloader clean
|
||||||
$(MAKE) -C libs/libmath clean
|
$(MAKE) -C libs/libmath clean
|
||||||
$(MAKE) -C libs/libhello clean
|
$(MAKE) -C libs/libhello clean
|
||||||
|
$(MAKE) -C libs/libtimezonesapplet clean
|
||||||
$(MAKE) -C src/test_dialogs clean
|
$(MAKE) -C src/test_dialogs clean
|
||||||
$(MAKE) -C src/test_dl clean
|
$(MAKE) -C src/test_dl clean
|
||||||
|
|||||||
@@ -3,3 +3,9 @@ label = "Printers"
|
|||||||
library = "0:/os/libprintersapplet.lib"
|
library = "0:/os/libprintersapplet.lib"
|
||||||
icon = "0:/icons/printer.svg"
|
icon = "0:/icons/printer.svg"
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|
||||||
|
[applets.timezone]
|
||||||
|
label = "Time Zone"
|
||||||
|
library = "0:/os/libtimezonesapplet.lib"
|
||||||
|
icon = "0:/icons/preferences-system-time.svg"
|
||||||
|
enabled = true
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,69 @@
|
|||||||
|
# Makefile for the Time Zone shared desktop applet
|
||||||
|
# Copyright (c) 2026 Daniel Hammer
|
||||||
|
|
||||||
|
MAKEFLAGS += -rR
|
||||||
|
.SUFFIXES:
|
||||||
|
|
||||||
|
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||||
|
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||||
|
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||||
|
LD := $(TOOLCHAIN_PREFIX)ld
|
||||||
|
else
|
||||||
|
CXX := g++
|
||||||
|
LD := ld
|
||||||
|
endif
|
||||||
|
|
||||||
|
OUTDIR := ../../bin/os
|
||||||
|
OBJDIR := obj
|
||||||
|
TARGET := $(OUTDIR)/libtimezonesapplet.lib
|
||||||
|
|
||||||
|
CXXFLAGS := \
|
||||||
|
-std=gnu++20 \
|
||||||
|
-g -O2 -pipe \
|
||||||
|
-Wall \
|
||||||
|
-Wextra \
|
||||||
|
-nostdinc \
|
||||||
|
-ffreestanding \
|
||||||
|
-fno-stack-protector \
|
||||||
|
-fno-stack-check \
|
||||||
|
-fPIC \
|
||||||
|
-fno-rtti \
|
||||||
|
-fno-exceptions \
|
||||||
|
-ffunction-sections \
|
||||||
|
-fdata-sections \
|
||||||
|
-m64 \
|
||||||
|
-march=x86-64 \
|
||||||
|
-mno-red-zone \
|
||||||
|
-mcmodel=small \
|
||||||
|
-MMD -MP \
|
||||||
|
-I ../../include \
|
||||||
|
-isystem ../../include/libc \
|
||||||
|
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||||
|
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||||
|
|
||||||
|
LDFLAGS := \
|
||||||
|
-shared \
|
||||||
|
--build-id=none \
|
||||||
|
--hash-style=sysv \
|
||||||
|
-m elf_x86_64 \
|
||||||
|
-z max-page-size=0x1000
|
||||||
|
|
||||||
|
SRCS := src/libtimezonesapplet.cpp
|
||||||
|
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS) Makefile
|
||||||
|
mkdir -p $(OUTDIR)
|
||||||
|
$(LD) $(LDFLAGS) $(OBJS) -o $@
|
||||||
|
|
||||||
|
$(OBJDIR)/%.o: %.cpp Makefile
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
-include $(OBJS:.o=.d)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(OBJDIR) $(TARGET)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
obj/src/libtimezonesapplet.o: src/libtimezonesapplet.cpp \
|
||||||
|
../../include/gui/desktop_applet.hpp ../../include/montauk/syscall.h \
|
||||||
|
../../include/Api/Syscall.hpp
|
||||||
|
../../include/gui/desktop_applet.hpp:
|
||||||
|
../../include/montauk/syscall.h:
|
||||||
|
../../include/Api/Syscall.hpp:
|
||||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* libtimezonesapplet.cpp
|
||||||
|
* Shared desktop applet that launches the Time Zone configuration tool
|
||||||
|
* Copyright (c) 2026 Daniel Hammer
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <gui/desktop_applet.hpp>
|
||||||
|
#include <montauk/syscall.h>
|
||||||
|
|
||||||
|
using gui::desktop_applet::Descriptor;
|
||||||
|
using gui::desktop_applet::Host;
|
||||||
|
|
||||||
|
static constexpr Descriptor kAppletDescriptor = {
|
||||||
|
gui::desktop_applet::ABI_VERSION,
|
||||||
|
gui::desktop_applet::CAP_SYSTEM_CONFIGURATION,
|
||||||
|
"timezone",
|
||||||
|
"Time Zone",
|
||||||
|
"0:/icons/preferences-system-time.svg",
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C" const Descriptor* desktop_applet_query_v1() {
|
||||||
|
return &kAppletDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" bool desktop_applet_open_v1(const Host* host) {
|
||||||
|
(void)host;
|
||||||
|
return montauk::spawn("0:/apps/timezone/timezone.elf") >= 0;
|
||||||
|
}
|
||||||
@@ -53,32 +53,6 @@ void settings_draw_display(Canvas& c, SettingsState* st) {
|
|||||||
mtk::widget_state(s.ui_scale == 1), theme);
|
mtk::widget_state(s.ui_scale == 1), theme);
|
||||||
mtk::draw_button(c, {bx + (sbw + 8) * 2, y, sbw, btn_h}, "Large", mtk::BUTTON_SECONDARY,
|
mtk::draw_button(c, {bx + (sbw + 8) * 2, y, sbw, btn_h}, "Large", mtk::BUTTON_SECONDARY,
|
||||||
mtk::widget_state(s.ui_scale == 2), theme);
|
mtk::widget_state(s.ui_scale == 2), theme);
|
||||||
y += btn_h + 20;
|
|
||||||
|
|
||||||
mtk::draw_separator(c, x, y, c.w - 2 * x, theme);
|
|
||||||
y += 16;
|
|
||||||
|
|
||||||
c.text(x, y + 6, "UTC Offset", theme.text);
|
|
||||||
Rect minus_btn = {bx, y, 36, btn_h};
|
|
||||||
Rect plus_btn = {bx + 36 + 90, y, 36, btn_h};
|
|
||||||
mtk::draw_button(c, minus_btn, "-", mtk::BUTTON_SECONDARY, mtk::widget_state(), theme);
|
|
||||||
mtk::draw_button(c, plus_btn, "+", mtk::BUTTON_SECONDARY, mtk::widget_state(), theme);
|
|
||||||
|
|
||||||
int off = s.tz_offset_minutes;
|
|
||||||
int off_h = off / 60;
|
|
||||||
int off_m = off % 60;
|
|
||||||
if (off_m < 0) off_m = -off_m;
|
|
||||||
|
|
||||||
char tz_label[16];
|
|
||||||
if (off_m) {
|
|
||||||
snprintf(tz_label, sizeof(tz_label), "UTC%s%d:%02d", off_h >= 0 ? "+" : "", off_h, off_m);
|
|
||||||
} else {
|
|
||||||
snprintf(tz_label, sizeof(tz_label), "UTC%s%d", off_h >= 0 ? "+" : "", off_h);
|
|
||||||
}
|
|
||||||
|
|
||||||
int tw = text_width(tz_label);
|
|
||||||
int label_w = 90;
|
|
||||||
c.text(bx + 36 + (label_w - tw) / 2, y + 6, tz_label, theme.text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool settings_handle_display_click(SettingsState* st, int mx, int cy) {
|
bool settings_handle_display_click(SettingsState* st, int mx, int cy) {
|
||||||
@@ -142,26 +116,6 @@ bool settings_handle_display_click(SettingsState* st, int mx, int cy) {
|
|||||||
settings_persist(st);
|
settings_persist(st);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
y += btn_h + 20 + 16;
|
|
||||||
|
|
||||||
Rect minus_btn = {bx, y, 36, btn_h};
|
|
||||||
Rect plus_btn = {bx + 36 + 90, y, 36, btn_h};
|
|
||||||
if (minus_btn.contains(mx, cy)) {
|
|
||||||
if (s.tz_offset_minutes > -720) {
|
|
||||||
s.tz_offset_minutes -= 60;
|
|
||||||
montauk::settz(s.tz_offset_minutes);
|
|
||||||
settings_persist_tz(st);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (plus_btn.contains(mx, cy)) {
|
|
||||||
if (s.tz_offset_minutes < 840) {
|
|
||||||
s.tz_offset_minutes += 60;
|
|
||||||
montauk::settz(s.tz_offset_minutes);
|
|
||||||
settings_persist_tz(st);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,12 +45,4 @@ void settings_persist(SettingsState* st) {
|
|||||||
doc.destroy();
|
doc.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
void settings_persist_tz(SettingsState* st) {
|
|
||||||
montauk::toml::Doc doc;
|
|
||||||
doc.init();
|
|
||||||
montauk::config::set_int(&doc, "timezone.offset_minutes", st->desktop->settings.tz_offset_minutes);
|
|
||||||
montauk::config::save("timezone", &doc);
|
|
||||||
doc.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace settings_app
|
} // namespace settings_app
|
||||||
|
|||||||
@@ -91,6 +91,5 @@ bool settings_handle_display_click(SettingsState* st, int mx, int cy);
|
|||||||
void settings_draw_about(Canvas& c, SettingsState* st);
|
void settings_draw_about(Canvas& c, SettingsState* st);
|
||||||
|
|
||||||
void settings_persist(SettingsState* st);
|
void settings_persist(SettingsState* st);
|
||||||
void settings_persist_tz(SettingsState* st);
|
|
||||||
|
|
||||||
} // namespace settings_app
|
} // namespace settings_app
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Makefile for timezone (standalone Time Zone configuration app) on MontaukOS
|
||||||
|
# Copyright (c) 2026 Daniel Hammer
|
||||||
|
|
||||||
|
MAKEFLAGS += -rR
|
||||||
|
.SUFFIXES:
|
||||||
|
|
||||||
|
TOOLCHAIN_PREFIX := $(shell cd ../../.. && pwd)/toolchain/local/bin/x86_64-elf-
|
||||||
|
ifneq ($(wildcard $(TOOLCHAIN_PREFIX)gcc),)
|
||||||
|
CXX := $(TOOLCHAIN_PREFIX)g++
|
||||||
|
else
|
||||||
|
CXX := g++
|
||||||
|
endif
|
||||||
|
|
||||||
|
PROG_INC := ../../include
|
||||||
|
LINK_LD := ../../link.ld
|
||||||
|
BINDIR := ../../bin
|
||||||
|
OBJDIR := obj
|
||||||
|
LIBDIR := ../../lib
|
||||||
|
|
||||||
|
CXXFLAGS := \
|
||||||
|
-std=gnu++20 \
|
||||||
|
-g -O2 -pipe \
|
||||||
|
-Wall \
|
||||||
|
-Wextra \
|
||||||
|
-Wno-unused-parameter \
|
||||||
|
-Wno-unused-function \
|
||||||
|
-nostdinc \
|
||||||
|
-ffreestanding \
|
||||||
|
-fno-stack-protector \
|
||||||
|
-fno-stack-check \
|
||||||
|
-fno-PIC \
|
||||||
|
-fno-rtti \
|
||||||
|
-fno-exceptions \
|
||||||
|
-ffunction-sections \
|
||||||
|
-fdata-sections \
|
||||||
|
-m64 \
|
||||||
|
-march=x86-64 \
|
||||||
|
-msse \
|
||||||
|
-msse2 \
|
||||||
|
-mno-red-zone \
|
||||||
|
-mcmodel=small \
|
||||||
|
-MMD -MP \
|
||||||
|
-I $(PROG_INC) \
|
||||||
|
-isystem $(PROG_INC)/libc \
|
||||||
|
-isystem ../../../kernel/freestnd-c-hdrs/x86_64/include \
|
||||||
|
-isystem ../../../kernel/freestnd-cxx-hdrs/x86_64/include
|
||||||
|
|
||||||
|
LDFLAGS := \
|
||||||
|
-nostdlib \
|
||||||
|
-static \
|
||||||
|
-Wl,--build-id=none \
|
||||||
|
-Wl,--gc-sections \
|
||||||
|
-Wl,-m,elf_x86_64 \
|
||||||
|
-z max-page-size=0x1000 \
|
||||||
|
-T $(LINK_LD)
|
||||||
|
|
||||||
|
SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp
|
||||||
|
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
|
||||||
|
|
||||||
|
TARGET := $(BINDIR)/apps/timezone/timezone.elf
|
||||||
|
LIBS := $(LIBDIR)/libc/liblibc.a
|
||||||
|
|
||||||
|
.PHONY: all clean
|
||||||
|
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
$(TARGET): $(OBJS) $(LINK_LD) Makefile $(LIBS)
|
||||||
|
mkdir -p $(BINDIR)/apps/timezone
|
||||||
|
$(CXX) $(CXXFLAGS) $(LDFLAGS) $(OBJS) $(LIBS) -o $@
|
||||||
|
|
||||||
|
$(OBJDIR)/%.o: %.cpp Makefile
|
||||||
|
mkdir -p $(dir $@)
|
||||||
|
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
-include $(OBJS:.o=.d)
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(OBJDIR) $(TARGET)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "../desktop/font_data.cpp"
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[app]
|
||||||
|
name = "Time Zone"
|
||||||
|
binary = "timezone.elf"
|
||||||
|
icon = "preferences-system-time.svg"
|
||||||
|
|
||||||
|
[menu]
|
||||||
|
category = "System"
|
||||||
|
visible = false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
#include "../desktop/stb_truetype_impl.cpp"
|
||||||
@@ -52,6 +52,7 @@ ICONS=(
|
|||||||
"apps/scalable/system-file-manager.svg"
|
"apps/scalable/system-file-manager.svg"
|
||||||
"apps/scalable/preferences-desktop-apps.svg"
|
"apps/scalable/preferences-desktop-apps.svg"
|
||||||
"categories/scalable/preferences-system.svg"
|
"categories/scalable/preferences-system.svg"
|
||||||
|
"categories/scalable/preferences-system-time.svg"
|
||||||
"apps/scalable/accessories-calculator.svg"
|
"apps/scalable/accessories-calculator.svg"
|
||||||
"apps/scalable/web-browser.svg"
|
"apps/scalable/web-browser.svg"
|
||||||
"places/scalable/folder.svg"
|
"places/scalable/folder.svg"
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ APPS=(
|
|||||||
"procmgr|apps/scalable/system-monitor.svg"
|
"procmgr|apps/scalable/system-monitor.svg"
|
||||||
"calculator|apps/scalable/accessories-calculator.svg"
|
"calculator|apps/scalable/accessories-calculator.svg"
|
||||||
"printers|devices/scalable/preferences-devices-printer.svg"
|
"printers|devices/scalable/preferences-devices-printer.svg"
|
||||||
|
"timezone|categories/scalable/preferences-system-time.svg"
|
||||||
"rpgdemo|apps/scalable/utilities-terminal.svg"
|
"rpgdemo|apps/scalable/utilities-terminal.svg"
|
||||||
"paint|apps/scalable/kolourpaint.svg"
|
"paint|apps/scalable/kolourpaint.svg"
|
||||||
"screenshot|apps/scalable/gnome-screenshot.svg"
|
"screenshot|apps/scalable/gnome-screenshot.svg"
|
||||||
|
|||||||
Reference in New Issue
Block a user