Compare commits

...

10 Commits

Author SHA1 Message Date
eb65d8582f Bump copyright years
All checks were successful
Alpine 3.22 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-20 12:50:21 +01:00
c93a1fb9b7 Indicate dimensions in the image scale dialog
It looks a bit odd, but it serves a particular purpose.
2025-11-20 12:48:36 +01:00
577de6bfb7 Add a dialog to set precise image scale 2025-11-20 12:48:31 +01:00
690e60cd74 Build an application bundle on macOS
All checks were successful
Alpine 3.22 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
This is far from done, but nonetheless constitutes a big improvement.

macOS application bundles are more or less necessary for:
 - showing a nice icon;
 - having spawned off instances actually be brought to the foreground;
 - file associations (yet files currently do not open properly);
 - having a reasonable method of distribution.

Also resolving a bunch of minor issues:
 - The context menu had duplicate items,
   and might needlessly end up with (null) labels.
2025-11-11 19:28:45 +01:00
a7ff9f220d Make Cmd/Ctrl/Shift+click/Enter open new windows
All checks were successful
Alpine 3.22 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-07 22:45:32 +01:00
2234fd008d MSYS2: add a comment about realpath
All checks were successful
Alpine 3.21 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-03 18:05:41 +01:00
0fceaf7728 Bump Wuffs
All checks were successful
Alpine 3.21 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2025-11-02 02:09:28 +01:00
c46fc73c34 Prefill the 'Enter location' dialog 2025-11-02 02:09:28 +01:00
bdd18fc898 Very slightly improve file updates on macOS
Some checks failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
openSUSE 15.5 Success
OpenBSD 7.7 Success
OpenBSD 7.6 Unsupported
Alpine 3.21 Success
2025-10-18 17:47:25 +01:00
cf6ded1d03 Make browser Cmd+click open new windows on macOS
Some checks failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.6 Scripts failed
openSUSE 15.5 Success
Alpine 3.21 Success
2025-10-18 15:24:36 +01:00
16 changed files with 616 additions and 42 deletions

View File

@@ -1,4 +1,4 @@
Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
Copyright (c) 2021 - 2025, Přemysl Eric Janouch <p@janouch.name>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

View File

@@ -2,7 +2,7 @@ fiv
===
'fiv' is a slightly unconventional, general-purpose image browser and viewer
for Linux and Windows (macOS still has major issues).
for Linux and Windows (macOS also kind of works).
image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
@@ -91,6 +91,14 @@ _mingw-w64-lcms2_ with the following change:
sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
macOS
~~~~~
Support for this operating system isn't as good.
If you install Homebrew, you can get an application bundle with:
$ sh -e macos-configure.sh builddir
$ meson install -C builddir
Documentation
-------------
For information concerning usage, refer to link:docs/fiv.html[the user guide],

View File

@@ -1,7 +1,7 @@
//
// fiv-browser.c: filesystem browsing widget
//
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2025, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -1307,6 +1307,15 @@ fiv_browser_button_press_event(GtkWidget *widget, GdkEventButton *event)
return GDK_EVENT_PROPAGATE;
}
static gboolean
modifier_state_opens_new_window(GtkWidget *widget, guint state)
{
GdkModifierType primary = gdk_keymap_get_modifier_mask(
gdk_keymap_get_for_display(gtk_widget_get_display(widget)),
GDK_MODIFIER_INTENT_PRIMARY_ACCELERATOR);
return state == primary || state == GDK_SHIFT_MASK;
}
static gboolean
fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
{
@@ -1323,11 +1332,13 @@ fiv_browser_button_release_event(GtkWidget *widget, GdkEventButton *event)
if (!entry || entry != entry_at(self, event->x, event->y))
return GDK_EVENT_PROPAGATE;
guint state = event->state & gtk_accelerator_get_default_mod_mask();
if ((event->button == GDK_BUTTON_PRIMARY && state == 0))
return open_entry(widget, entry, FALSE);
if ((event->button == GDK_BUTTON_PRIMARY && state == GDK_CONTROL_MASK) ||
(event->button == GDK_BUTTON_MIDDLE && state == 0))
if ((event->button == GDK_BUTTON_MIDDLE && state == 0) ||
(event->button == GDK_BUTTON_PRIMARY &&
modifier_state_opens_new_window(widget, state)))
return open_entry(widget, entry, TRUE);
return GDK_EVENT_PROPAGATE;
}
@@ -1578,7 +1589,8 @@ static gboolean
fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
{
FivBrowser *self = FIV_BROWSER(widget);
switch ((event->state & gtk_accelerator_get_default_mod_mask())) {
guint state = event->state & gtk_accelerator_get_default_mod_mask();
switch (state) {
case 0:
switch (event->keyval) {
case GDK_KEY_Delete:
@@ -1635,6 +1647,15 @@ fiv_browser_key_press_event(GtkWidget *widget, GdkEventKey *event)
}
}
if (modifier_state_opens_new_window(widget, state)) {
switch (event->keyval) {
case GDK_KEY_Return:
if (self->selected)
return open_entry(widget, self->selected, TRUE);
return GDK_EVENT_STOP;
}
}
return GTK_WIDGET_CLASS(fiv_browser_parent_class)
->key_press_event(widget, event);
}

View File

@@ -380,8 +380,11 @@ append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)
ctx->app_info = opener;
// On Linux, this prefers the obsoleted X-GNOME-FullName.
gchar *name =
g_strdup_printf("Open With %s", g_app_info_get_display_name(opener));
const char *display_name = g_app_info_get_display_name(opener);
// Ironically, GIO reads CFBundleName and can't read CFBundleDisplayName.
if (!display_name)
display_name = g_app_info_get_executable(opener);
gchar *name = g_strdup_printf("Open With %s", display_name);
// It's documented that we can touch the child, if we want to use markup.
#if 0
@@ -503,8 +506,6 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
GAppInfo *default_ =
g_app_info_get_default_for_type(ctx->content_type, FALSE);
GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
GtkWidget *menu = gtk_menu_new();
if (default_) {
@@ -513,6 +514,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
}
GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
for (GList *iter = recommended; iter; iter = iter->next) {
if (!default_ || !g_app_info_equal(iter->data, default_))
append_opener(menu, iter->data, ctx);
@@ -525,6 +527,10 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
}
// The implementation returns the same data for both,
// we'd have to filter out the recommended ones from here.
#ifndef __APPLE__
GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
for (GList *iter = fallback; iter; iter = iter->next) {
if (!default_ || !g_app_info_equal(iter->data, default_))
append_opener(menu, iter->data, ctx);
@@ -536,6 +542,7 @@ fiv_context_menu_new(GtkWidget *widget, GFile *file)
gtk_menu_shell_append(
GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
}
#endif
GtkWidget *item = gtk_menu_item_new_with_label("Open With...");
g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate),

View File

@@ -382,6 +382,9 @@ on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
switch (event_type) {
case G_FILE_MONITOR_EVENT_CHANGED:
case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
// On macOS, we seem to not receive _CHANGED for child files.
// And while this seems to arrive too early, it's a mild improvement.
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
event = MONITOR_CHANGING;
new_entry_file = file;
break;
@@ -400,8 +403,6 @@ on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
new_entry_file = file;
break;
case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
// TODO(p): Figure out if we can't make use of _CHANGES_DONE_HINT.
case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
case G_FILE_MONITOR_EVENT_UNMOUNTED:
// TODO(p): Figure out how to handle _UNMOUNTED sensibly.

View File

@@ -537,6 +537,13 @@ on_show_enter_location(
g_signal_connect(entry, "changed",
G_CALLBACK(on_enter_location_changed), self);
GFile *location = fiv_io_model_get_location(self->model);
if (location) {
gchar *parse_name = g_file_get_parse_name(location);
gtk_entry_set_text(GTK_ENTRY(entry), parse_name);
g_free(parse_name);
}
// Can't have it ellipsized and word-wrapped at the same time.
GtkWidget *protocols = gtk_label_new("");
gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);

View File

@@ -1,7 +1,7 @@
//
// fiv-view.c: image viewing widget
//
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2025, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -1438,6 +1438,127 @@ get_toplevel(GtkWidget *widget)
return NULL;
}
struct zoom_ask_context {
GtkWidget *result_left, *result_right;
Dimensions dimensions;
};
static void
on_zoom_ask_spin_changed(GtkSpinButton *spin, G_GNUC_UNUSED gpointer user_data)
{
// We don't want to call gtk_spin_button_update(),
// that would immediately replace whatever the user has typed in.
gdouble scale = -1;
const gchar *text = gtk_entry_get_text(GTK_ENTRY(spin));
if (*text) {
gchar *end = NULL;
gdouble value = g_strtod(text, &end);
if (!*end)
scale = value / 100.;
}
struct zoom_ask_context *data = user_data;
GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(spin));
if (scale <= 0) {
gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING);
gtk_label_set_text(GTK_LABEL(data->result_left), "");
gtk_label_set_text(GTK_LABEL(data->result_right), "");
} else {
gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING);
gchar *left = g_strdup_printf("%.0f", data->dimensions.width * scale);
gchar *right = g_strdup_printf("%.0f", data->dimensions.height * scale);
gtk_label_set_text(GTK_LABEL(data->result_left), left);
gtk_label_set_text(GTK_LABEL(data->result_right), right);
g_free(left);
g_free(right);
}
}
static void
zoom_ask(FivView *self)
{
GtkWidget *dialog = gtk_dialog_new_with_buttons("Set zoom level",
get_toplevel(GTK_WIDGET(self)),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL |
GTK_DIALOG_USE_HEADER_BAR,
"_OK", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
Dimensions dimensions = get_surface_dimensions(self);
gchar *original_width = g_strdup_printf("%.0f", dimensions.width);
gchar *original_height = g_strdup_printf("%.0f", dimensions.height);
GtkWidget *original_left = gtk_label_new(original_width);
GtkWidget *original_middle = gtk_label_new("×");
GtkWidget *original_right = gtk_label_new(original_height);
g_free(original_width);
g_free(original_height);
gtk_label_set_xalign(GTK_LABEL(original_left), 1.);
gtk_label_set_xalign(GTK_LABEL(original_right), 0.);
GtkWidget *original_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_pack_start(
GTK_BOX(original_box), original_left, TRUE, TRUE, 0);
gtk_box_pack_start(
GTK_BOX(original_box), original_middle, FALSE, FALSE, 0);
gtk_box_pack_start(
GTK_BOX(original_box), original_right, TRUE, TRUE, 0);
// FIXME: This widget's behaviour is absolutely miserable.
// For example, we would like to be flexible with decimal spaces.
GtkAdjustment *adjustment = gtk_adjustment_new(
self->scale * 100, 0., 100000., 1., 10., 0.);
GtkWidget *spin = gtk_spin_button_new(adjustment, 1., 0);
gtk_spin_button_set_update_policy(
GTK_SPIN_BUTTON(spin), GTK_UPDATE_IF_VALID);
gtk_entry_set_activates_default(GTK_ENTRY(spin), TRUE);
GtkWidget *zoom_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget *zoom_label = gtk_label_new_with_mnemonic("_Zoom:");
gtk_label_set_mnemonic_widget(GTK_LABEL(zoom_label), spin);
gtk_box_pack_start(GTK_BOX(zoom_box), zoom_label, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(zoom_box), spin, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(zoom_box), gtk_label_new("%"), FALSE, FALSE, 0);
GtkWidget *result_left = gtk_label_new(NULL);
GtkWidget *result_middle = gtk_label_new("×");
GtkWidget *result_right = gtk_label_new(NULL);
gtk_label_set_xalign(GTK_LABEL(result_left), 1.);
gtk_label_set_xalign(GTK_LABEL(result_right), 0.);
GtkSizeGroup *group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
gtk_size_group_add_widget(group, original_left);
gtk_size_group_add_widget(group, original_right);
gtk_size_group_add_widget(group, result_left);
gtk_size_group_add_widget(group, result_right);
GtkWidget *result_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_box_pack_start(GTK_BOX(result_box), result_left, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(result_box), result_middle, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(result_box), result_right, TRUE, TRUE, 0);
struct zoom_ask_context data = { result_left, result_right, dimensions };
g_signal_connect(spin, "changed",
G_CALLBACK(on_zoom_ask_spin_changed), &data);
on_zoom_ask_spin_changed(GTK_SPIN_BUTTON(spin), &data);
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
g_object_set(content, "margin", 12, NULL);
gtk_box_set_spacing(GTK_BOX(content), 6);
gtk_container_add(GTK_CONTAINER(content), original_box);
gtk_container_add(GTK_CONTAINER(content), zoom_box);
gtk_container_add(GTK_CONTAINER(content), result_box);
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
gtk_widget_show_all(dialog);
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));
if (value > 0)
set_scale(self, value / 100., NULL);
}
gtk_widget_destroy(dialog);
g_object_unref(group);
}
static void
copy(FivView *self)
{
@@ -2028,6 +2149,8 @@ fiv_view_command(FivView *self, FivViewCommand command)
set_scale(self, self->scale / SCALE_STEP, NULL);
break; case FIV_VIEW_COMMAND_ZOOM_1:
set_scale(self, 1.0, NULL);
break; case FIV_VIEW_COMMAND_ZOOM_ASK:
zoom_ask(self);
break; case FIV_VIEW_COMMAND_FIT_WIDTH:
set_scale_to_fit_width(self);
break; case FIV_VIEW_COMMAND_FIT_HEIGHT:

View File

@@ -59,6 +59,7 @@ typedef enum _FivViewCommand {
XX(FIV_VIEW_COMMAND_ZOOM_IN, "zoom-in") \
XX(FIV_VIEW_COMMAND_ZOOM_OUT, "zoom-out") \
XX(FIV_VIEW_COMMAND_ZOOM_1, "zoom-1") \
XX(FIV_VIEW_COMMAND_ZOOM_ASK, "zoom-ask") \
XX(FIV_VIEW_COMMAND_FIT_WIDTH, "fit-width") \
XX(FIV_VIEW_COMMAND_FIT_HEIGHT, "fit-height") \
XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit") \

192
fiv.c
View File

@@ -1,7 +1,7 @@
//
// fiv.c: fuck-if-I-know-how-to-name-it image browser and viewer
//
// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
// Copyright (c) 2021 - 2025, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
@@ -73,6 +73,138 @@ slist_to_strv(GSList *slist)
return strv;
}
// --- macOS utilities ---------------------------------------------------------
#ifdef __APPLE__
#include <CoreFoundation/CoreFoundation.h>
static gchar *
cfurlref_to_path(CFURLRef urlref)
{
CFStringRef path = CFURLCopyFileSystemPath(urlref, kCFURLPOSIXPathStyle);
if (!path)
return NULL;
CFIndex size = CFStringGetMaximumSizeForEncoding(
CFStringGetLength(path), kCFStringEncodingUTF8) + 1;
gchar *string = g_malloc(size);
Boolean ok = CFStringGetCString(path, string, size, kCFStringEncodingUTF8);
CFRelease(path);
if (!ok) {
g_free(string);
return NULL;
}
return string;
}
static gchar *
get_application_bundle_path(void)
{
gchar *result = NULL;
CFBundleRef bundle = CFBundleGetMainBundle();
if (!bundle)
goto fail_1;
// When launched from outside a bundle, it will make up one,
// but these paths will then be equal.
CFURLRef bundle_url = CFBundleCopyBundleURL(bundle);
if (!bundle_url)
goto fail_1;
CFURLRef resources_url = CFBundleCopyResourcesDirectoryURL(bundle);
if (!resources_url)
goto fail_2;
if (!CFEqual(bundle_url, resources_url))
result = cfurlref_to_path(bundle_url);
CFRelease(resources_url);
fail_2:
CFRelease(bundle_url);
fail_1:
return result;
}
static gchar *
prepend_path_string(const gchar *prepended, const gchar *original)
{
if (!prepended)
return g_strdup(original ? original : "");
if (!original || !*original)
return g_strdup(prepended);
GHashTable *seen = g_hash_table_new(g_str_hash, g_str_equal);
GPtrArray *unique = g_ptr_array_new();
g_ptr_array_add(unique, (gpointer) prepended);
g_hash_table_add(seen, (gpointer) prepended);
gchar **components = g_strsplit(original, ":", -1);
for (gchar **p = components; *p; p++) {
if (g_hash_table_contains(seen, *p))
continue;
g_ptr_array_add(unique, *p);
g_hash_table_add(seen, *p);
}
g_ptr_array_add(unique, NULL);
gchar *result = g_strjoinv(":", (gchar **) unique->pdata);
g_hash_table_destroy(seen);
g_ptr_array_free(unique, TRUE);
g_strfreev(components);
return result;
}
// We reuse foreign dependencies, so we need to prevent them from loading
// any system-wide files, and point them in the right direction.
static void
adjust_environment(void)
{
gchar *bundle_dir = get_application_bundle_path();
if (!bundle_dir)
return;
gchar *contents_dir = g_build_filename(bundle_dir, "Contents", NULL);
gchar *macos_dir = g_build_filename(contents_dir, "MacOS", NULL);
gchar *resources_dir = g_build_filename(contents_dir, "Resources", NULL);
gchar *datadir = g_build_filename(resources_dir, "share", NULL);
gchar *libdir = g_build_filename(resources_dir, "lib", NULL);
g_free(bundle_dir);
gchar *new_path = prepend_path_string(macos_dir, g_getenv("PATH"));
g_setenv("PATH", new_path, TRUE);
g_free(new_path);
const gchar *data_dirs = g_getenv("XDG_DATA_DIRS");
gchar *new_data_dirs = data_dirs && *data_dirs
? prepend_path_string(datadir, data_dirs)
: prepend_path_string(datadir, "/usr/local/share:/usr/share");
g_setenv("XDG_DATA_DIRS", new_data_dirs, TRUE);
g_free(new_data_dirs);
gchar *schemas_dir = g_build_filename(datadir, "glib-2.0", "schemas", NULL);
g_setenv("GSETTINGS_SCHEMA_DIR", schemas_dir, TRUE);
g_free(schemas_dir);
gchar *gdk_pixbuf_module_file =
g_build_filename(libdir, "gdk-pixbuf-2.0", "loaders.cache", NULL);
g_setenv("GDK_PIXBUF_MODULE_FILE", gdk_pixbuf_module_file, TRUE);
g_free(gdk_pixbuf_module_file);
// GTK+ is smart enough to also consider application bundles,
// but let there be a single source of truth.
g_setenv("GTK_EXE_PREFIX", resources_dir, TRUE);
g_free(libdir);
g_free(datadir);
g_free(resources_dir);
g_free(macos_dir);
g_free(contents_dir);
}
#endif
// --- Keyboard shortcuts ------------------------------------------------------
// Fuck XML, this can be easily represented in static structures.
// Though it would be nice if the accelerators could be customized.
@@ -667,7 +799,7 @@ enum {
XX(S3, gtk_separator_new(GTK_ORIENTATION_HORIZONTAL)) \
XX(FIXATE, T("pin2-symbolic", "Keep zoom and position")) \
XX(MINUS, B("zoom-out-symbolic", "Zoom out")) \
XX(SCALE, gtk_label_new("")) \
XX(SCALE, B(NULL, "Set zoom level")) \
XX(PLUS, B("zoom-in-symbolic", "Zoom in")) \
XX(ONE, B("zoom-original-symbolic", "Original size")) \
XX(FIT, T("zoom-fit-best-symbolic", "Scale to fit")) \
@@ -1092,9 +1224,22 @@ on_next(void)
static gchar **
build_spawn_argv(const char *uri)
{
// Because we only pass URIs, there is no need to prepend "--" here.
GPtrArray *a = g_ptr_array_new();
g_ptr_array_add(a, g_strdup(PROJECT_NAME));
#ifdef __APPLE__
// Otherwise we would always launch ourselves in the background.
gchar *bundle_dir = get_application_bundle_path();
if (bundle_dir) {
g_ptr_array_add(a, g_strdup("open"));
g_ptr_array_add(a, g_strdup("-a"));
g_ptr_array_add(a, bundle_dir);
// At least with G_APPLICATION_NON_UNIQUE, this is necessary:
g_ptr_array_add(a, g_strdup("-n"));
g_ptr_array_add(a, g_strdup("--args"));
}
#endif
// Because we only pass URIs, there is no need to prepend "--" after this.
if (!a->len)
g_ptr_array_add(a, g_strdup(PROJECT_NAME));
// Process-local VFS URIs need to be resolved to globally accessible URIs.
// It doesn't seem possible to reliably tell if a GFile is process-local,
@@ -1403,15 +1548,24 @@ on_window_state_event(G_GNUC_UNUSED GtkWidget *widget,
static void
show_help_contents(void)
{
gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME);
#ifdef G_OS_WIN32
gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
#elif defined __APPLE__
gchar *prefix = get_application_bundle_path();
if (!prefix) {
show_error_dialog(g_error_new(
G_FILE_ERROR, G_FILE_ERROR_FAILED, "Cannot locate bundle"));
return;
}
#else
gchar *prefix = g_strdup(PROJECT_PREFIX);
#endif
gchar *filename = g_strdup_printf("%s.html", PROJECT_NAME);
gchar *path = g_build_filename(prefix, PROJECT_DOCDIR, filename, NULL);
g_free(prefix);
#else
gchar *path = g_build_filename(PROJECT_DOCDIR, filename, NULL);
#endif
g_free(filename);
GError *error = NULL;
gchar *uri = g_filename_to_uri(path, NULL, &error);
g_free(path);
@@ -1661,8 +1815,10 @@ static GtkWidget *
make_toolbar_button(const char *symbolic, const char *tooltip)
{
GtkWidget *button = gtk_button_new();
gtk_button_set_image(GTK_BUTTON(button),
gtk_image_new_from_icon_name(symbolic, GTK_ICON_SIZE_BUTTON));
if (symbolic) {
gtk_button_set_image(GTK_BUTTON(button),
gtk_image_new_from_icon_name(symbolic, GTK_ICON_SIZE_BUTTON));
}
gtk_widget_set_tooltip_text(button, tooltip);
gtk_widget_set_focus_on_click(button, FALSE);
gtk_style_context_add_class(
@@ -1808,7 +1964,8 @@ on_notify_view_scale(
g_object_get(object, g_param_spec_get_name(param_spec), &scale, NULL);
gchar *scale_str = g_strdup_printf("%.0f%%", round(scale * 100));
gtk_label_set_text(GTK_LABEL(g.toolbar[TOOLBAR_SCALE]), scale_str);
gtk_label_set_text(GTK_LABEL(
gtk_bin_get_child(GTK_BIN(g.toolbar[TOOLBAR_SCALE]))), scale_str);
g_free(scale_str);
// FIXME: The label doesn't immediately assume its new width.
@@ -1893,13 +2050,11 @@ make_view_toolbar(void)
TOOLBAR(XX)
#undef XX
gtk_widget_set_margin_start(g.toolbar[TOOLBAR_SCALE], 5);
gtk_widget_set_margin_end(g.toolbar[TOOLBAR_SCALE], 5);
GtkWidget *scale_label = gtk_label_new("");
gtk_container_add(GTK_CONTAINER(g.toolbar[TOOLBAR_SCALE]), scale_label);
// So that the width doesn't jump around in the usual zoom range.
// Ideally, we'd measure the widest digit and use width(NNN%).
gtk_label_set_width_chars(GTK_LABEL(g.toolbar[TOOLBAR_SCALE]), 5);
gtk_widget_set_halign(g.toolbar[TOOLBAR_SCALE], GTK_ALIGN_CENTER);
gtk_label_set_width_chars(GTK_LABEL(scale_label), 5);
// GtkStatusBar solves a problem we do not have here.
GtkWidget *view_toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
@@ -1930,6 +2085,7 @@ make_view_toolbar(void)
toolbar_command(TOOLBAR_PLAY_PAUSE, FIV_VIEW_COMMAND_TOGGLE_PLAYBACK);
toolbar_command(TOOLBAR_SEEK_FORWARD, FIV_VIEW_COMMAND_FRAME_NEXT);
toolbar_command(TOOLBAR_MINUS, FIV_VIEW_COMMAND_ZOOM_OUT);
toolbar_command(TOOLBAR_SCALE, FIV_VIEW_COMMAND_ZOOM_ASK);
toolbar_command(TOOLBAR_PLUS, FIV_VIEW_COMMAND_ZOOM_IN);
toolbar_command(TOOLBAR_ONE, FIV_VIEW_COMMAND_ZOOM_1);
toolbar_toggler(TOOLBAR_FIT, "scale-to-fit");
@@ -2640,6 +2796,10 @@ main(int argc, char *argv[])
{},
};
#ifdef __APPLE__
adjust_environment();
#endif
// We never get the ::open signal, thanks to G_OPTION_ARG_FILENAME_ARRAY.
GtkApplication *app = gtk_application_new(NULL, G_APPLICATION_NON_UNIQUE);
g_application_set_option_context_parameter_string(

52
macos-Info.plist.in Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>@ProjectName@</string>
<key>CFBundleIdentifier</key>
<string>@ProjectNS@@ProjectName@</string>
<key>CFBundleName</key>
<string>@ProjectName@</string>
<key>CFBundleIconFile</key>
<string>@ProjectName@.icns</string>
<key>CFBundleShortVersionString</key>
<string>@ProjectVersion@</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<!-- Although mostly static, this should eventually be generated. -->
<!-- In particular, we should expand image/x-dcraw, -->
<!-- using information we can collect from shared-mime-info. -->
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Image File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Default</string>
<key>LSItemContentTypes</key>
<array>
<string>com.apple.icns</string>
<string>com.apple.quicktime-image</string>
<string>com.compuserve.gif</string>
<string>com.microsoft.bmp</string>
<string>com.microsoft.ico</string>
<string>org.webmproject.webp</string>
<string>public.avif</string>
<string>public.heic</string>
<string>public.heif</string>
<string>public.jpeg</string>
<string>public.png</string>
<string>public.svg-image</string>
<string>public.tiff</string>
<string>public.xbitmap-image</string>
</array>
</dict>
</array>
</dict>
</plist>

25
macos-configure.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/sh -e
# macos-configure.sh: set up a Homebrew-based macOS build
#
# Meson has no special support for macOS application bundles whatsoever.
#
# gtk-mac-bundler doesn't do anything particularly miraculous,
# and it doesn't directly support Homebrew.
#
# It would be cleaner and more reproducible to set up a special HOMEBREW_PREFIX,
# though right now we're happy to build an app bundle at all.
#
# It would also allow us to make a custom Little CMS build that includes
# the fast float plugin, which is a bit of a big deal.
# TODO: exiftool (Perl is part of macOS, at least for now)
HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_ASK=1 brew install
coreutils meson pkgconf shared-mime-info adwaita-icon-theme \
gtk+3 jpeg-xl libavif libheif libraw librsvg little-cms2 webp
sourcedir=$(grealpath "${2:-$(dirname "$0")}")
builddir=$(grealpath "${1:-builddir}")
appdir=$builddir/fiv.app
meson setup --buildtype=debugoptimized --prefix="$appdir" \
--bindir=Contents/MacOS --libdir=Contents/Resources/lib \
--datadir=Contents/Resources/share "$builddir" "$sourcedir"

115
macos-install.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/bin/sh -e
export LC_ALL=C
cd "$MESON_INSTALL_DESTDIR_PREFIX"
# Input: Half-baked application bundle linked against Homebrew.
# Output: Portable application bundle.
source=/opt/homebrew
bindir=Contents/MacOS
libdir=Contents/Resources/lib
datadir=Contents/Resources/share
mkdir -p "$datadir"/glib-2.0/schemas
cp -p "$source"/share/glib-2.0/schemas/org.gtk.Settings.* \
"$datadir"/glib-2.0/schemas
mkdir -p "$datadir"/icons
cp -pRL "$source"/share/icons/Adwaita "$datadir/"icons
mkdir -p "$datadir"/icons/hicolor
cp -p "$source"/share/icons/hicolor/index.theme "$datadir"/icons/hicolor
mkdir -p "$datadir/mime"
# GIO doesn't use the database on macOS, this subset is for us.
find "$source"/share/mime/ -maxdepth 1 -type f -exec cp -p {} "$datadir"/mime \;
# Copy binaries we directly or indirectly depend on.
#
# Homebrew is a bit chaotic in that some libraries are linked against locations
# in /opt/homebrew/Cellar, and some against /opt/homebrew/opt symlinks.
# We'll process things in such a way that it does not matter.
#
# As a side note, libraries in /usr/lib are now actually being served from
# a shared cache by the dynamic linker and aren't visible on the filesystem.
# There is an alternative to "otool -L" which can see them but it isn't
# particularly nicer to parse: "dyld_info -dependents/-linked_dylibs".
rm -rf "$libdir"
mkdir -p "$libdir"
pixbufdir=$libdir/gdk-pixbuf-2.0
loadersdir=$pixbufdir/loaders
cp -RL "$source"/lib/gdk-pixbuf-2.0/* "$pixbufdir"
# Fix a piece of crap loader that needs to be special.
svg=$loadersdir/libpixbufloader_svg.so
rm -f "$loadersdir"/libpixbufloader_svg.dylib
otool -L "$svg" | grep -o '@rpath/[^ ]*' | while IFS= read -r bad
do install_name_tool -change "$bad" "$source/lib/$(basename "$bad")" "$svg"
done
GDK_PIXBUF_MODULEDIR=$loadersdir gdk-pixbuf-query-loaders \
| sed "s,$libdir,@rpath," > "$pixbufdir/loaders.cache"
gtkdir=$libdir/gtk-3.0
printbackendsdir=$gtkdir/printbackends
cp -RL "$source"/lib/gtk-3.0/* "$gtkdir"
# TODO: Figure out how to make gtk-query-immodules-3.0 pick up exactly
# what it needs to. So far I'm not sure if this is at all even useful.
rm -rf "$gtkdir"/immodules*
find "$bindir" "$loadersdir" "$printbackendsdir" -type f -maxdepth 1 | awk '
function collect(binary, command, line) {
if (seen[binary]++)
return
command = "otool -L \"" binary "\""
while ((command | getline line) > 0)
if (match(line, /^\t\/opt\/.+ \(/))
collect(substr(line, RSTART + 1, RLENGTH - 3))
close(command)
} {
collect($0)
delete seen[$0]
} END {
for (library in seen)
print library
}
' | while IFS= read -r binary
do test -f "$libdir/$(basename "$binary")" || cp "$binary" "$libdir"
done
# Now redirect all binaries to internal linking.
# A good overview of how this works is "man dyld" and:
# https://itwenty.me/posts/01-understanding-rpath/
rewrite() {
otool -L "$1" | sed -n 's,^\t\(.*\) (.*,\1,p' | grep '^/opt/' \
| while IFS= read -r lib
do install_name_tool -change "$lib" "@rpath/$(basename "$lib")" "$1"
done
}
find "$bindir" -type f -maxdepth 1 | while IFS= read -r binary
do
install_name_tool -add_rpath @executable_path/../Resources/lib "$binary"
rewrite "$binary"
done
find "$libdir" -type f \( -name '*.so' -o -name '*.dylib' \) \
| while IFS= read -r binary
do
chmod 644 "$binary"
install_name_tool -id "@rpath/${binary#$libdir/}" "$binary"
rewrite "$binary"
# Discard pointless @loader_path/../lib and absolute Homebrew paths.
otool -l "$binary" | awk '
$1 == "cmd" { command = $2 }
command == "LC_RPATH" && $1 == "path" { print $2 }
' | xargs -R 1 -I % install_name_tool -delete_rpath % "$binary"
# Replace freshly invalidated code signatures with ad-hoc ones.
codesign --force --sign - "$binary"
done
glib-compile-schemas "$datadir"/glib-2.0/schemas
# This may speed up program start-up a little bit.
gtk-update-icon-cache "$datadir"/icons/Adwaita

22
macos-svg2icns.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh -e
# macos-svg2icns.sh: convert an SVG to the macOS .icns format
if [ $# -ne 2 ]
then
echo >&2 "Usage: $0 INPUT.svg OUTPUT.icns"
exit 2
fi
svg=$1 icns=$2 tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
iconset="$tmpdir/$(basename "$icns" .icns).iconset"
mkdir -p "$iconset"
for size in 16 32 128 256 512
do
size2x=$((size * 2))
rsvg-convert --output="$iconset/icon_${size}x${size}.png" \
--width=$size --height=$size "$svg"
rsvg-convert --output="$iconset/icon_${size}x${size}@2x.png" \
--width=$size2x --height=$size2x "$svg"
done
iconutil -c icns -o "$icns" "$iconset"

View File

@@ -18,6 +18,8 @@ add_project_arguments(
#endif
win32 = host_machine.system() == 'windows'
macos = host_machine.system() == 'darwin' \
and host_machine.subsystem() == 'macos'
# The likelihood of this being installed is nearly zero. Enable the wrap.
libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
@@ -97,15 +99,20 @@ docdir = get_option('datadir') / 'doc' / meson.project_name()
application_ns = 'name.janouch.'
application_url = 'https://janouch.name/p/' + meson.project_name()
rawconf = configuration_data({
'ProjectName' : meson.project_name(),
'ProjectVersion' : meson.project_version(),
'ProjectNS' : application_ns,
'ProjectURL' : application_url,
})
conf = configuration_data()
conf.set_quoted('PROJECT_NAME', meson.project_name())
conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
conf.set_quoted('PROJECT_NS', application_ns)
conf.set_quoted('PROJECT_URL', application_url)
conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
if win32
conf.set_quoted('PROJECT_DOCDIR', docdir)
endif
conf.set_quoted('PROJECT_PREFIX', get_option('prefix'))
conf.set_quoted('PROJECT_DOCDIR', docdir)
conf.set('HAVE_JPEG_QS', libjpegqs.found())
conf.set('HAVE_LCMS2', lcms2.found())
@@ -147,6 +154,15 @@ if win32
output : 'fiv.ico', input : icon_png_list,
command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
rc += windows.compile_resources('fiv.rc', depends : icon_ico)
elif macos
# Meson is really extremely brain-dead and retarded.
# There is no real reason why this would have to be a shell script.
svg2icns = find_program('macos-svg2icns.sh')
icon_icns = custom_target('fiv.icns',
output : 'fiv.icns', input : 'fiv.svg',
command : [svg2icns, '@INPUT@', '@OUTPUT@'],
install : true,
install_dir : 'Contents/Resources')
endif
gnome = import('gnome')
@@ -214,13 +230,12 @@ foreach schema : gsettings_schemas
input : schema,
output : application_ns + schema,
copy : true,
install: true,
install : true,
install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
endforeach
# For the purposes of development: make the program find its GSettings schemas.
gnome.compile_schemas(depend_files : files(gsettings_schemas))
gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
# Meson is broken on Windows and removes the backslashes, so this ends up empty.
symbolics = run_command(find_program('sed', required : false, disabler : true),
@@ -256,7 +271,26 @@ install_data('fiv.svg',
install_subdir('docs',
install_dir : docdir, strip_directory : true)
if not win32
if macos
# We're going all in on application bundles, seeing as it doesn't make
# much sense to install the application as in the block below.
#
# macOS has other mechanisms we can use to launch the JPEG cropper,
# or the reverse search utilities.
configure_file(
input : 'macos-Info.plist.in',
output : 'Info.plist',
configuration : rawconf,
install : true,
install_dir : 'Contents')
meson.add_install_script('macos-install.sh')
elif not win32
gnome.post_install(
glib_compile_schemas : true,
gtk_update_icon_cache : true,
)
asciidoctor = find_program('asciidoctor', required : false)
a2x = find_program('a2x', required : false)
if not asciidoctor.found() and not a2x.found()
@@ -357,11 +391,7 @@ elif meson.is_cross_build()
wxs = configure_file(
input : 'fiv.wxs.in',
output : 'fiv.wxs',
configuration : configuration_data({
'ProjectName' : meson.project_name(),
'ProjectVersion' : meson.project_version(),
'ProjectURL' : application_url,
}),
configuration : rawconf,
)
msi = meson.project_name() + '-' + meson.project_version() + \
'-' + host_machine.cpu() + '.msi'

View File

@@ -130,6 +130,8 @@ setup() {
--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
}
# Note: you may need GNU coreutils realpath for non-existent build directories
# (macOS and busybox will probably not work).
sourcedir=$(realpath "${2:-$(dirname "$0")}")
builddir=$(realpath "${1:-builddir}")
toolchain=$builddir/msys2-cross-toolchain.meson