diff --git a/CMakeLists.txt b/CMakeLists.txt index e82dc71..2fd957d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,7 +46,8 @@ endif() # so MSVC needs mainCRTStartup as the entry point. file(GLOB MATURITA_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.c") if(WIN32) - add_executable(maturita WIN32 ${MATURITA_SOURCES}) + add_executable(maturita WIN32 ${MATURITA_SOURCES} + "${CMAKE_CURRENT_SOURCE_DIR}/maturita.rc") else() add_executable(maturita ${MATURITA_SOURCES}) endif() @@ -66,8 +67,8 @@ else() endif() endif() -# The GTK stylesheet and window icon are loaded at runtime from the working -# directory (or next to the binary), so keep copies beside the built binary. +# The GTK stylesheet, window icon and Freedesktop share/ tree are loaded at +# runtime from the working directory (or next to the binary). add_custom_command(TARGET maturita POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/style.css @@ -81,4 +82,7 @@ add_custom_command(TARGET maturita POST_BUILD ${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_SOURCE_DIR}/assets/app-icon.png - ${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps/maturita.png) + ${CMAKE_CURRENT_SOURCE_DIR}/assets/icons/hicolor/512x512/apps/maturita.png + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_CURRENT_SOURCE_DIR}/share + $/share) diff --git a/Makefile b/Makefile index 9d30fe4..65c418f 100644 --- a/Makefile +++ b/Makefile @@ -17,12 +17,15 @@ GTK_LIBS := $(shell $(PKG_CONFIG) --libs gtk4) CFLAGS = -Wall -Wextra -Wno-deprecated-declarations $(GTK_CFLAGS) LIBS = $(GTK_LIBS) -lm -# Windows (MSYS2 / MinGW): .exe suffix and no console window for the GUI. +# Windows (MSYS2 / MinGW): .exe suffix, no console window, embed .ico. ifeq ($(OS),Windows_NT) TARGET = maturita.exe LIBS += -mwindows +WINDRES ?= windres +RC_OBJ = maturita_rc.o else TARGET = maturita +RC_OBJ = # macOS Dock icon is set via AppKit (see macos_dock.c). ifeq ($(shell uname -s 2>/dev/null),Darwin) LIBS += -framework AppKit -framework Foundation @@ -34,6 +37,7 @@ OBJ = $(SRC:.c=.o) ICON_SRC = assets/app-icon.png ICON_DST = assets/icons/hicolor/512x512/apps/maturita.png +ICON_ICO = assets/app-icon.ico all: $(TARGET) @@ -41,11 +45,19 @@ $(ICON_DST): $(ICON_SRC) mkdir -p $(dir $@) cp "$(ICON_SRC)" "$@" +ifeq ($(OS),Windows_NT) +$(RC_OBJ): maturita.rc $(ICON_ICO) + $(WINDRES) maturita.rc -o $(RC_OBJ) + +$(TARGET): $(SRC) $(ICON_DST) $(RC_OBJ) + $(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(RC_OBJ) $(LIBS) +else $(TARGET): $(SRC) $(ICON_DST) $(CC) $(CFLAGS) -o $(TARGET) $(SRC) $(LIBS) +endif clean: - rm -f $(TARGET) maturita.exe $(OBJ) + rm -f $(TARGET) maturita.exe $(OBJ) $(RC_OBJ) maturita_rc.o run: $(TARGET) ./$(TARGET) @@ -61,6 +73,7 @@ bundle: $(TARGET) @cp style.css dist/ @mkdir -p dist/icons/hicolor/512x512/apps @cp "$(ICON_DST)" dist/icons/hicolor/512x512/apps/ + @cp -R share dist/ @ldd $(TARGET) | grep -Ei '/(ucrt64|mingw64)/bin/' | awk '{print $$3}' \ | xargs -r -I{} cp -f {} dist/ @echo "Bundled into dist/ - run dist/$(TARGET)" diff --git a/assets/app-icon.ico b/assets/app-icon.ico new file mode 100644 index 0000000..79162ae Binary files /dev/null and b/assets/app-icon.ico differ diff --git a/build-windows.bat b/build-windows.bat index d5ed29e..23ceb97 100644 --- a/build-windows.bat +++ b/build-windows.bat @@ -1,5 +1,5 @@ @echo off -setlocal +setlocal EnableExtensions REM --------------------------------------------------------------------------- REM Native Windows build with MSVC. No MSYS2 or MinGW shell required. @@ -67,10 +67,22 @@ if not defined GTK_CFLAGS ( exit /b 1 ) -REM --- 4. Compile ----------------------------------------------------------- +if not exist "assets\app-icon.ico" ( + echo [error] assets\app-icon.ico is missing. + echo Generate it with: python scripts\gen-icons.py + exit /b 1 +) + +REM --- 4. Compile resources + sources --------------------------------------- echo Building maturita.exe with MSVC... +rc /nologo /fo maturita.res maturita.rc +if errorlevel 1 ( + echo [error] Resource compile failed. + exit /b 1 +) + cl /nologo /O2 /MD /W3 /std:c11 /D_CRT_SECURE_NO_WARNINGS ^ - %GTK_CFLAGS% maturita.c ^ + %GTK_CFLAGS% *.c maturita.res ^ /Fe:maturita.exe ^ /link /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup %GTK_LIBS% @@ -79,7 +91,10 @@ if errorlevel 1 ( exit /b 1 ) +if not exist icons\hicolor\512x512\apps mkdir icons\hicolor\512x512\apps +copy /Y assets\app-icon.png icons\hicolor\512x512\apps\maturita.png >nul + echo. -echo Built maturita.exe +echo Built maturita.exe ^(icon embedded from assets\app-icon.ico^) echo Run it from this terminal with: maturita.exe echo Or use run-windows.bat / double-click maturita.exe. diff --git a/main.c b/main.c index 1aa538c..086418e 100644 --- a/main.c +++ b/main.c @@ -189,6 +189,7 @@ static const char *find_app_icon_file(void) { static const char *paths[] = { ICON_FILE, ICON_THEME_DIR "/hicolor/512x512/apps/" ICON_NAME ".png", + SHARE_DIR "/icons/hicolor/512x512/apps/" ICON_NAME ".png", ICON_FILE_ALT, NULL, }; @@ -201,12 +202,84 @@ static const char *find_app_icon_file(void) { return NULL; } -/* GTK4 has no gtk_window_set_icon_from_file(); register a local hicolor - * theme dir and set the window icon by name instead. On macOS the Dock - * tile ignores that, so also push the PNG through AppKit. */ +/* Kept alive for gdk_toplevel_set_icon_list() which does not take ownership. */ +static GList *app_icon_textures; + +static void clear_app_icon_textures(void) { + g_list_free_full(app_icon_textures, g_object_unref); + app_icon_textures = NULL; +} + +/* X11 / some compositors use the toplevel icon list for the taskbar tile. */ +static void apply_toplevel_icon(GtkWindow *window) { + GdkSurface *surface; + const char *path; + GError *err = NULL; + GdkTexture *tex; + + surface = gtk_native_get_surface(GTK_NATIVE(window)); + if (!surface || !GDK_IS_TOPLEVEL(surface)) + return; + + path = find_app_icon_file(); + if (!path) + return; + + tex = gdk_texture_new_from_filename(path, &err); + if (!tex) { + g_clear_error(&err); + return; + } + + clear_app_icon_textures(); + app_icon_textures = g_list_append(NULL, tex); + gdk_toplevel_set_icon_list(GDK_TOPLEVEL(surface), app_icon_textures); +} + +static void on_window_realize_icon(GtkWidget *widget, gpointer data) { + (void)data; + apply_toplevel_icon(GTK_WINDOW(widget)); +} + +/* Prepend ./share to XDG_DATA_DIRS so Wayland can find our .desktop file and + * hicolor icons without a system install (matches APP_ID). */ +static void setup_portable_share_dir(void) { + char *abs; + const char *old; + char *neu; + + if (!g_file_test(SHARE_DIR "/applications", G_FILE_TEST_IS_DIR) && + !g_file_test(SHARE_DIR "/icons", G_FILE_TEST_IS_DIR)) + return; + + abs = g_canonicalize_filename(SHARE_DIR, NULL); + if (!abs) + return; + + old = g_getenv("XDG_DATA_DIRS"); + if (old && old[0]) + neu = g_strconcat(abs, G_SEARCHPATH_SEPARATOR_S, old, NULL); + else + neu = g_strconcat(abs, + G_SEARCHPATH_SEPARATOR_S "/usr/local/share" + G_SEARCHPATH_SEPARATOR_S "/usr/share", + NULL); + g_setenv("XDG_DATA_DIRS", neu, TRUE); + g_free(neu); + g_free(abs); +} + +/* Register icon theme paths + window icon-name (Linux), AppKit Dock (macOS), + * and toplevel textures (X11 / fallbacks). Windows uses the .ico embedded + * via maturita.rc at link time. */ static void setup_window_icon(GtkWindow *window) { GtkIconTheme *theme; - static const char *dirs[] = { ICON_THEME_DIR, ICON_THEME_DIR_ALT, NULL }; + static const char *dirs[] = { + ICON_THEME_DIR, + ICON_THEME_DIR_ALT, + SHARE_DIR "/icons", + NULL, + }; const char *icon_file; int i; @@ -220,16 +293,21 @@ static void setup_window_icon(GtkWindow *window) { gtk_window_set_default_icon_name(ICON_NAME); gtk_window_set_icon_name(window, ICON_NAME); } else { - g_warning("App icon '%s' not found (tried %s/ and %s/)", - ICON_NAME, ICON_THEME_DIR, ICON_THEME_DIR_ALT); + g_warning("App icon '%s' not found (tried %s/, %s/, %s/icons/)", + ICON_NAME, ICON_THEME_DIR, ICON_THEME_DIR_ALT, SHARE_DIR); } icon_file = find_app_icon_file(); if (icon_file) macos_set_dock_icon(icon_file); +#ifdef __APPLE__ else g_warning("Dock icon PNG not found (tried %s and %s)", ICON_FILE, ICON_FILE_ALT); +#endif + + g_signal_connect(window, "realize", + G_CALLBACK(on_window_realize_icon), NULL); } void activate(GtkApplication *app, gpointer user_data) { @@ -398,9 +476,14 @@ int main(int argc, char **argv) { GtkApplication *app; int status; - app = gtk_application_new("org.example.maturita", G_APPLICATION_DEFAULT_FLAGS); + /* Must run before GtkApplication so Wayland can resolve our .desktop + * file / icons via the application id. */ + setup_portable_share_dir(); + + app = gtk_application_new(APP_ID, G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); status = g_application_run(G_APPLICATION(app), argc, argv); + clear_app_icon_textures(); g_object_unref(app); return status; diff --git a/maturita.h b/maturita.h index b4ce285..46fe7d5 100644 --- a/maturita.h +++ b/maturita.h @@ -15,11 +15,13 @@ #define HW_SUBJ 2 /* index of "Technické vybavení" */ #define CZ_SUBJ 3 /* index of "Český jazyk a literatura" */ #define CSS_FILE "style.css" +#define APP_ID "org.maturita.Maturita" #define ICON_NAME "maturita" #define ICON_FILE "assets/app-icon.png" #define ICON_THEME_DIR "assets/icons" /* cwd when run from source tree */ #define ICON_THEME_DIR_ALT "icons" /* next to the binary after install */ #define ICON_FILE_ALT "icons/hicolor/512x512/apps/maturita.png" +#define SHARE_DIR "share" /* portable Freedesktop data root */ #define PROGRESS_DIR "progress" #define PROGRESS_U1 "progress/unit1.conf" #define PROGRESS_U2 "progress/unit2.conf" diff --git a/maturita.rc b/maturita.rc new file mode 100644 index 0000000..e8c7905 --- /dev/null +++ b/maturita.rc @@ -0,0 +1,3 @@ +/* Windows application icon embedded into maturita.exe. + * The first ICON resource becomes the Explorer / taskbar icon. */ +1 ICON "assets/app-icon.ico" diff --git a/scripts/gen-icons.py b/scripts/gen-icons.py new file mode 100755 index 0000000..1cf5e4a --- /dev/null +++ b/scripts/gen-icons.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Regenerate assets/app-icon.ico and share/icons from assets/app-icon.png. + +Uses macOS `sips` when available; otherwise requires Pillow. +Run from the repository root. +""" +from __future__ import annotations + +import struct +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "assets" / "app-icon.png" +ICO = ROOT / "assets" / "app-icon.ico" +ICO_SIZES = [16, 32, 48, 64, 128, 256] +SHARE_SIZES = [16, 32, 48, 64, 128, 256, 512] + + +def resize_png(src: Path, dest: Path, size: int) -> None: + dest.parent.mkdir(parents=True, exist_ok=True) + if subprocess.call(["which", "sips"], stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) == 0: + subprocess.check_call( + ["sips", "-z", str(size), str(size), str(src), "--out", str(dest)], + stdout=subprocess.DEVNULL, + ) + return + try: + from PIL import Image + except ImportError as exc: + raise SystemExit( + "Need macOS sips or Pillow to resize icons" + ) from exc + img = Image.open(src).convert("RGBA") + img = img.resize((size, size), Image.Resampling.LANCZOS) + img.save(dest, format="PNG") + + +def write_ico(entries: list[tuple[int, bytes]], dest: Path) -> None: + count = len(entries) + header = struct.pack("= 256 else size + h = 0 if size >= 256 else size + direntries.append( + struct.pack(" int: + if not SRC.is_file(): + print(f"missing {SRC}", file=sys.stderr) + return 1 + + with tempfile.TemporaryDirectory(prefix="maturita-ico-") as tmp: + entries: list[tuple[int, bytes]] = [] + for size in ICO_SIZES: + png = Path(tmp) / f"icon_{size}.png" + resize_png(SRC, png, size) + entries.append((size, png.read_bytes())) + write_ico(entries, ICO) + print(f"wrote {ICO.relative_to(ROOT)}") + + for size in SHARE_SIZES: + dest = ROOT / "share" / "icons" / "hicolor" / f"{size}x{size}" / "apps" / "maturita.png" + resize_png(SRC, dest, size) + print(f"wrote {dest.relative_to(ROOT)}") + + assets_icon = ROOT / "assets" / "icons" / "hicolor" / "512x512" / "apps" / "maturita.png" + resize_png(SRC, assets_icon, 512) + print(f"wrote {assets_icon.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/share/applications/org.maturita.Maturita.desktop b/share/applications/org.maturita.Maturita.desktop new file mode 100644 index 0000000..859c4ef --- /dev/null +++ b/share/applications/org.maturita.Maturita.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Type=Application +Version=1.0 +Name=maturita.c +GenericName=Maturita study app +GenericName[cs]=Příprava na maturitu +Comment=Educational GTK app for maturita exam preparation +Comment[cs]=Vzdělávací GTK aplikace pro přípravu na maturitu +Exec=maturita +Icon=maturita +Terminal=false +Categories=Education;GTK; +StartupNotify=true +StartupWMClass=org.maturita.Maturita diff --git a/share/icons/hicolor/128x128/apps/maturita.png b/share/icons/hicolor/128x128/apps/maturita.png new file mode 100644 index 0000000..67770f5 Binary files /dev/null and b/share/icons/hicolor/128x128/apps/maturita.png differ diff --git a/share/icons/hicolor/16x16/apps/maturita.png b/share/icons/hicolor/16x16/apps/maturita.png new file mode 100644 index 0000000..9bb78af Binary files /dev/null and b/share/icons/hicolor/16x16/apps/maturita.png differ diff --git a/share/icons/hicolor/256x256/apps/maturita.png b/share/icons/hicolor/256x256/apps/maturita.png new file mode 100644 index 0000000..4bf5a3b Binary files /dev/null and b/share/icons/hicolor/256x256/apps/maturita.png differ diff --git a/share/icons/hicolor/32x32/apps/maturita.png b/share/icons/hicolor/32x32/apps/maturita.png new file mode 100644 index 0000000..fc046df Binary files /dev/null and b/share/icons/hicolor/32x32/apps/maturita.png differ diff --git a/share/icons/hicolor/48x48/apps/maturita.png b/share/icons/hicolor/48x48/apps/maturita.png new file mode 100644 index 0000000..c089401 Binary files /dev/null and b/share/icons/hicolor/48x48/apps/maturita.png differ diff --git a/share/icons/hicolor/512x512/apps/maturita.png b/share/icons/hicolor/512x512/apps/maturita.png new file mode 100644 index 0000000..e0360d2 Binary files /dev/null and b/share/icons/hicolor/512x512/apps/maturita.png differ diff --git a/share/icons/hicolor/64x64/apps/maturita.png b/share/icons/hicolor/64x64/apps/maturita.png new file mode 100644 index 0000000..9bd2735 Binary files /dev/null and b/share/icons/hicolor/64x64/apps/maturita.png differ