Make the app icon work on macOS, Windows, and Linux.
Co-authored-by: Cursor <cursoragent@cursor.com>
|
|
@ -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
|
||||
$<TARGET_FILE_DIR:maturita>/share)
|
||||
|
|
|
|||
17
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)"
|
||||
|
|
|
|||
BIN
assets/app-icon.ico
Normal file
|
After Width: | Height: | Size: 75 KiB |
|
|
@ -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.
|
||||
|
|
|
|||
97
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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
3
maturita.rc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
/* Windows application icon embedded into maturita.exe.
|
||||
* The first ICON resource becomes the Explorer / taskbar icon. */
|
||||
1 ICON "assets/app-icon.ico"
|
||||
85
scripts/gen-icons.py
Executable file
|
|
@ -0,0 +1,85 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Regenerate assets/app-icon.ico and share/icons from assets/app-icon.png.
|
||||
|
||||
Uses macOS `sips` when available; otherwise requires Pillow.
|
||||
Run from the repository root.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SRC = ROOT / "assets" / "app-icon.png"
|
||||
ICO = ROOT / "assets" / "app-icon.ico"
|
||||
ICO_SIZES = [16, 32, 48, 64, 128, 256]
|
||||
SHARE_SIZES = [16, 32, 48, 64, 128, 256, 512]
|
||||
|
||||
|
||||
def resize_png(src: Path, dest: Path, size: int) -> None:
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
if subprocess.call(["which", "sips"], stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL) == 0:
|
||||
subprocess.check_call(
|
||||
["sips", "-z", str(size), str(size), str(src), "--out", str(dest)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
return
|
||||
try:
|
||||
from PIL import Image
|
||||
except ImportError as exc:
|
||||
raise SystemExit(
|
||||
"Need macOS sips or Pillow to resize icons"
|
||||
) from exc
|
||||
img = Image.open(src).convert("RGBA")
|
||||
img = img.resize((size, size), Image.Resampling.LANCZOS)
|
||||
img.save(dest, format="PNG")
|
||||
|
||||
|
||||
def write_ico(entries: list[tuple[int, bytes]], dest: Path) -> None:
|
||||
count = len(entries)
|
||||
header = struct.pack("<HHH", 0, 1, count)
|
||||
offset = 6 + 16 * count
|
||||
direntries = []
|
||||
blobs = []
|
||||
for size, data in entries:
|
||||
w = 0 if size >= 256 else size
|
||||
h = 0 if size >= 256 else size
|
||||
direntries.append(
|
||||
struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, len(data), offset)
|
||||
)
|
||||
blobs.append(data)
|
||||
offset += len(data)
|
||||
dest.write_bytes(header + b"".join(direntries) + b"".join(blobs))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SRC.is_file():
|
||||
print(f"missing {SRC}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="maturita-ico-") as tmp:
|
||||
entries: list[tuple[int, bytes]] = []
|
||||
for size in ICO_SIZES:
|
||||
png = Path(tmp) / f"icon_{size}.png"
|
||||
resize_png(SRC, png, size)
|
||||
entries.append((size, png.read_bytes()))
|
||||
write_ico(entries, ICO)
|
||||
print(f"wrote {ICO.relative_to(ROOT)}")
|
||||
|
||||
for size in SHARE_SIZES:
|
||||
dest = ROOT / "share" / "icons" / "hicolor" / f"{size}x{size}" / "apps" / "maturita.png"
|
||||
resize_png(SRC, dest, size)
|
||||
print(f"wrote {dest.relative_to(ROOT)}")
|
||||
|
||||
assets_icon = ROOT / "assets" / "icons" / "hicolor" / "512x512" / "apps" / "maturita.png"
|
||||
resize_png(SRC, assets_icon, 512)
|
||||
print(f"wrote {assets_icon.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
14
share/applications/org.maturita.Maturita.desktop
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Version=1.0
|
||||
Name=maturita.c
|
||||
GenericName=Maturita study app
|
||||
GenericName[cs]=Příprava na maturitu
|
||||
Comment=Educational GTK app for maturita exam preparation
|
||||
Comment[cs]=Vzdělávací GTK aplikace pro přípravu na maturitu
|
||||
Exec=maturita
|
||||
Icon=maturita
|
||||
Terminal=false
|
||||
Categories=Education;GTK;
|
||||
StartupNotify=true
|
||||
StartupWMClass=org.maturita.Maturita
|
||||
BIN
share/icons/hicolor/128x128/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
share/icons/hicolor/16x16/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
share/icons/hicolor/256x256/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
share/icons/hicolor/32x32/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
share/icons/hicolor/48x48/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
share/icons/hicolor/512x512/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
share/icons/hicolor/64x64/apps/maturita.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |