Compare commits

22 Commits

Author SHA1 Message Date
p cd5c7b9ef9 Download ExifTool from elsewhere
Alpine 3.24 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2026-08-05 05:07:35 +02:00
p 6e80a3b5c8 Extract a function
Alpine 3.24 Scripts failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2026-08-05 01:59:12 +02:00
p e424e903a4 Implement another animation frame disposal method 2026-08-05 01:48:41 +02:00
p 4a9c850105 Bump Wuffs
Apparently 0.3.5 changes about nothing, but still.
2026-08-05 01:48:41 +02:00
p d7dd8d0e38 Update README.adoc
Alpine 3.23 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2026-05-26 08:42:50 +02:00
p df648192a8 Fix project URL
Alpine 3.23 Scripts failed
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2026-03-29 23:13:10 +02:00
p f8c8943079 Fix macos-configure.sh
Alpine 3.23 Success
Arch Linux Success
Arch Linux AUR Success
Debian Bookworm Success
Fedora 39 Success
OpenBSD 7.8 Success
openSUSE 15.5 Success
2026-02-23 11:58:14 +01:00
p f2f69c7c46 Add support for the pinch gesture
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
Given the way our viewport works, it behaves a bit oddly.
2025-12-30 08:48:58 +01:00
p fd60959d41 README.adoc: more precise instructions
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-12-02 01:50:24 +01:00
p 6b0bd4ac5c Update .gitignore for newer Qt Creator 2025-11-30 18:20:03 +01:00
p f543a46118 README.adoc: make quick'n'dirty instructions work
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
Without the compiled schemas, fiv doesn't launch at all.
2025-11-29 23:45:31 +01:00
p 9de3747328 Image scale dialog: use two decimal places
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
GIMP does as well, and it provides better information.

It's not as if anyone is really going to use the spin button features
which behave weird with this, and it indicates that decimal numbers may
be written at all.
2025-11-21 06:30:25 +01:00
p eb65d8582f Bump copyright years
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
p 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
p 577de6bfb7 Add a dialog to set precise image scale 2025-11-20 12:48:31 +01:00
p 690e60cd74 Build an application bundle on macOS
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
p a7ff9f220d Make Cmd/Ctrl/Shift+click/Enter open new windows
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
p 2234fd008d MSYS2: add a comment about realpath
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
p 0fceaf7728 Bump Wuffs
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
p c46fc73c34 Prefill the 'Enter location' dialog 2025-11-02 02:09:28 +01:00
p bdd18fc898 Very slightly improve file updates on macOS
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
p cf6ded1d03 Make browser Cmd+click open new windows on macOS
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
18 changed files with 781 additions and 128 deletions
+1
View File
@@ -1,3 +1,4 @@
/.qtcreator
/meson.build.user
/subprojects/*
!/subprojects/*.wrap
+1 -1
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.
+16 -2
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"]
@@ -39,7 +39,8 @@ https://aur.archlinux.org/packages/fiv-git[AUR],
or as a https://git.janouch.name/p/nixexprs[Nix derivation].
https://janouch.name/cd[Windows installers can be found here],
you want the _x86_64_ version.
you want the _x86_64_ version (note that MSYS2 has discontinued _x86_,
and thus there will be no more such builds).
Building and Running
--------------------
@@ -69,6 +70,11 @@ you can get a quick and dirty installation package for testing purposes using:
$ meson compile deb
# dpkg -i fiv-*.deb
And in case you keep the default installation prefix rather than _/usr_,
it is necessary to:
# glib-compile-schemas /usr/local/share/glib-2.0/schemas
Windows
~~~~~~~
'fiv' can be cross-compiled for Windows, provided that you install a bunch of
@@ -91,6 +97,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],
+25 -4
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);
}
+11 -4
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),
+3 -2
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.
+86 -67
View File
@@ -390,8 +390,90 @@ struct load_wuffs_frame_context {
FivIoImage *result; ///< The resulting image (referenced)
FivIoImage *result_tail; ///< The final animation frame
FivIoImage *restore_previous; ///< Canvas before the previous frame
};
static bool
load_wuffs_frame_compose(struct load_wuffs_frame_context *ctx,
FivIoImage **image, const wuffs_base__frame_config *fc, GError **error)
{
// Copy the previous frame to a new image.
FivIoImage *prev = ctx->result_tail, *canvas = fiv_io_image_new(
prev->format, prev->width, prev->height);
if (!canvas) {
set_error(error, "image allocation failure");
return false;
}
FivIoImage *base = ctx->restore_previous ? ctx->restore_previous : prev;
memcpy(canvas->data, base->data, base->stride * base->height);
g_clear_pointer(&ctx->restore_previous, fiv_io_image_unref);
// Apply that frame's disposal method.
// XXX: We do not expect opaque pictures to receive holes this way.
wuffs_base__rect_ie_u32 bounds =
wuffs_base__frame_config__bounds(&ctx->last_fc);
// TODO(p): This field needs to be colour-managed.
wuffs_base__color_u32_argb_premul bg =
wuffs_base__frame_config__background_color(&ctx->last_fc);
double a = (bg >> 24) / 255., r = 0, g = 0, b = 0;
if (a) {
r = (uint8_t) (bg >> 16) / 255. / a;
g = (uint8_t) (bg >> 8) / 255. / a;
b = (uint8_t) (bg) / 255. / a;
}
cairo_surface_t *surface = fiv_io_image_to_surface_noref(canvas);
cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
if (wuffs_base__frame_config__disposal(&ctx->last_fc) ==
WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_BACKGROUND) {
cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y,
bounds.max_excl_x - bounds.min_incl_x,
bounds.max_excl_y - bounds.min_incl_y);
cairo_set_source_rgba(cr, r, g, b, a);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_fill(cr);
}
if (wuffs_base__frame_config__disposal(fc) ==
WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS) {
ctx->restore_previous = fiv_io_image_new(
canvas->format, canvas->width, canvas->height);
if (!ctx->restore_previous) {
cairo_destroy(cr);
fiv_io_image_unref(canvas);
set_error(error, "image allocation failure");
return false;
}
memcpy(ctx->restore_previous->data, canvas->data,
canvas->stride * canvas->height);
}
// Paint the current frame over that, within its bounds.
bounds = wuffs_base__frame_config__bounds(fc);
cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y,
bounds.max_excl_x - bounds.min_incl_x,
bounds.max_excl_y - bounds.min_incl_y);
cairo_clip(cr);
cairo_set_operator(cr,
wuffs_base__frame_config__overwrite_instead_of_blend(fc)
? CAIRO_OPERATOR_SOURCE
: CAIRO_OPERATOR_OVER);
surface = fiv_io_image_to_surface_noref(*image);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
cairo_paint(cr);
cairo_destroy(cr);
fiv_io_image_unref(*image);
*image = canvas;
return true;
}
static bool
load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
{
@@ -490,73 +572,9 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
}
// Single-frame images get a fast path, animations are are handled slowly:
if (wuffs_base__frame_config__index(&fc) > 0) {
// Copy the previous frame to a new image.
FivIoImage *prev = ctx->result_tail, *canvas = fiv_io_image_new(
prev->format, prev->width, prev->height);
if (!canvas) {
set_error(error, "image allocation failure");
goto fail;
}
memcpy(canvas->data, prev->data, prev->stride * prev->height);
// Apply that frame's disposal method.
// XXX: We do not expect opaque pictures to receive holes this way.
wuffs_base__rect_ie_u32 bounds =
wuffs_base__frame_config__bounds(&ctx->last_fc);
// TODO(p): This field needs to be colour-managed.
wuffs_base__color_u32_argb_premul bg =
wuffs_base__frame_config__background_color(&ctx->last_fc);
double a = (bg >> 24) / 255., r = 0, g = 0, b = 0;
if (a) {
r = (uint8_t) (bg >> 16) / 255. / a;
g = (uint8_t) (bg >> 8) / 255. / a;
b = (uint8_t) (bg) / 255. / a;
}
cairo_surface_t *surface = fiv_io_image_to_surface_noref(canvas);
cairo_t *cr = cairo_create(surface);
cairo_surface_destroy(surface);
switch (wuffs_base__frame_config__disposal(&ctx->last_fc)) {
case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_BACKGROUND:
cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y,
bounds.max_excl_x - bounds.min_incl_x,
bounds.max_excl_y - bounds.min_incl_y);
cairo_set_source_rgba(cr, r, g, b, a);
cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
cairo_fill(cr);
break;
case WUFFS_BASE__ANIMATION_DISPOSAL__RESTORE_PREVIOUS:
// TODO(p): Implement, it seems tricky.
// Might need another surface to keep track of the state.
break;
case WUFFS_BASE__ANIMATION_DISPOSAL__NONE:
break;
}
// Paint the current frame over that, within its bounds.
bounds = wuffs_base__frame_config__bounds(&fc);
cairo_rectangle(cr, bounds.min_incl_x, bounds.min_incl_y,
bounds.max_excl_x - bounds.min_incl_x,
bounds.max_excl_y - bounds.min_incl_y);
cairo_clip(cr);
cairo_set_operator(cr,
wuffs_base__frame_config__overwrite_instead_of_blend(&fc)
? CAIRO_OPERATOR_SOURCE
: CAIRO_OPERATOR_OVER);
surface = fiv_io_image_to_surface_noref(image);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
cairo_paint(cr);
cairo_destroy(cr);
fiv_io_image_unref(image);
image = canvas;
}
if (wuffs_base__frame_config__index(&fc) > 0 &&
!load_wuffs_frame_compose(ctx, &image, &fc, error))
goto fail;
if (ctx->meta_exif)
image->exif = g_bytes_ref(ctx->meta_exif);
@@ -763,6 +781,7 @@ fail:
g_clear_pointer(&ctx.meta_exif, g_bytes_unref);
g_clear_pointer(&ctx.meta_iccp, g_bytes_unref);
g_clear_pointer(&ctx.meta_xmp, g_bytes_unref);
g_clear_pointer(&ctx.restore_previous, fiv_io_image_unref);
g_clear_pointer(&ctx.source, fiv_io_profile_free);
return ctx.result;
}
+7
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);
+192 -17
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.
@@ -77,6 +77,9 @@ struct _FivView {
bool fixate : 1; ///< Keep zoom and position
double scale; ///< Scaling factor
double drag_start[2]; ///< Adjustment values for drag origin
double zoom_gesture_initial_scale; ///< Pinch gesture initial scale
double zoom_gesture_center[2]; ///< Pinch gesture widget coordinates
double zoom_gesture_surface[2]; ///< Pinch gesture surface coordinates
FivIoImage *enhance_swap; ///< Quick swap in/out
FivIoProfile *screen_cms_profile; ///< Target colour profile for widget
@@ -1128,28 +1131,14 @@ widget_to_surface(FivView *self, double *x, double *y)
}
static gboolean
set_scale(FivView *self, double scale, const GdkEvent *event)
set_scale_with_focus(FivView *self, double scale,
double focus_x, double focus_y, double surface_x, double surface_y)
{
// FIXME: Zooming to exactly 1:1 breaks rendering with some images
// when using a native X11 Window. This is a silly workaround.
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
if (window && gdk_window_has_native(window) && scale == 1)
scale = 0.999999999999999;
if (self->scale == scale)
goto out;
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
double focus_x = 0, focus_y = 0;
if (!event || !gdk_event_get_coords(event, &focus_x, &focus_y)) {
focus_x = 0.5 * allocation.width;
focus_y = 0.5 * allocation.height;
}
double surface_x = focus_x;
double surface_y = focus_y;
widget_to_surface(self, &surface_x, &surface_y);
self->scale = scale;
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_SCALE]);
@@ -1174,6 +1163,31 @@ out:
return set_scale_to_fit(self, false);
}
static gboolean
set_scale(FivView *self, double scale, const GdkEvent *event)
{
// FIXME: Zooming to exactly 1:1 breaks rendering with some images
// when using a native X11 Window. This is a silly workaround.
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
if (window && gdk_window_has_native(window) && scale == 1)
scale = 0.999999999999999;
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
double focus_x = 0, focus_y = 0;
if (!event || !gdk_event_get_coords(event, &focus_x, &focus_y)) {
focus_x = 0.5 * allocation.width;
focus_y = 0.5 * allocation.height;
}
double surface_x = focus_x;
double surface_y = focus_y;
widget_to_surface(self, &surface_x, &surface_y);
return set_scale_with_focus(
self, scale, focus_x, focus_y, surface_x, surface_y);
}
static void
set_scale_to_fit_width(FivView *self)
{
@@ -1418,6 +1432,33 @@ on_drag_end(GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x,
gdk_window_set_cursor(window, NULL);
}
static void
on_zoom_begin(GtkGestureZoom *gesture,
G_GNUC_UNUSED GdkEventSequence *sequence, gpointer user_data)
{
FivView *self = FIV_VIEW(user_data);
self->zoom_gesture_initial_scale = self->scale;
double x = 0, y = 0;
gtk_gesture_get_bounding_box_center(GTK_GESTURE(gesture), &x, &y);
self->zoom_gesture_center[0] = x;
self->zoom_gesture_center[1] = y;
widget_to_surface(self, &x, &y);
self->zoom_gesture_surface[0] = x;
self->zoom_gesture_surface[1] = y;
}
static void
on_zoom_scale_changed(
G_GNUC_UNUSED GtkGestureZoom *gesture, gdouble scale, gpointer user_data)
{
FivView *self = FIV_VIEW(user_data);
set_scale_with_focus(self, self->zoom_gesture_initial_scale * scale,
self->zoom_gesture_center[0], self->zoom_gesture_center[1],
self->zoom_gesture_surface[0], self->zoom_gesture_surface[1]);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
@@ -1438,6 +1479,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., 2);
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)
{
@@ -1782,6 +1944,17 @@ fiv_view_init(FivView *self)
G_CALLBACK(on_drag_update), self);
g_signal_connect(drag, "drag-end",
G_CALLBACK(on_drag_end), self);
GtkGesture *zoom = gtk_gesture_zoom_new(GTK_WIDGET(self));
gtk_event_controller_set_propagation_phase(
GTK_EVENT_CONTROLLER(zoom), GTK_PHASE_BUBBLE);
g_object_set_data_full(
G_OBJECT(self), "fiv-view-zoom-gesture", zoom, g_object_unref);
g_signal_connect(zoom, "begin",
G_CALLBACK(on_zoom_begin), self);
g_signal_connect(zoom, "scale-changed",
G_CALLBACK(on_zoom_scale_changed), self);
}
// --- Public interface --------------------------------------------------------
@@ -2028,6 +2201,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:
+1
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") \
+176 -16
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
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
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
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
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"
+43 -13
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'),
@@ -95,17 +97,22 @@ endif
# XXX: https://github.com/mesonbuild/meson/issues/825
docdir = get_option('datadir') / 'doc' / meson.project_name()
application_ns = 'name.janouch.'
application_url = 'https://janouch.name/p/' + meson.project_name()
application_url = 'https://git.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'
+4 -1
View File
@@ -56,7 +56,8 @@ fetch() {
version=$(curl -# https://exiftool.org/ver.txt)
name=exiftool-$version.tar.gz remotename=Image-ExifTool-$version.tar.gz
status Fetching "$remotename"
[ -f "$name" ] || curl -#o "$name" "https://exiftool.org/$remotename"
[ -f "$name" ] || curl -#Lo "$name" \
"https://sourceforge.net/projects/exiftool/files/$remotename/download"
ln -sf "$name" exiftool.tar.gz
}
@@ -130,6 +131,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