Files
fiv/fiv-view.c
T
p c55500f51a Support dragging the view
It would also be possible to handle this through press/motion/release
event handlers, though GtkGestureDrag is more convenient for hacking in
support for dragging to widgets not supporting GtkScrollable (yet).

There may be some undesired interactions lurking, besides the jarring
movements when dragging native GdkWindows (these are a pain).
2022-07-15 14:00:31 +02:00

1364 lines
43 KiB
C

//
// fiv-view.c: image viewing widget
//
// Copyright (c) 2021 - 2022, 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.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include "config.h"
#include "fiv-io.h"
#include "fiv-view.h"
#include <math.h>
#include <stdbool.h>
#include <gtk/gtk.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h>
#endif // GDK_WINDOWING_X11
#ifdef GDK_WINDOWING_QUARTZ
#include <gdk/gdkquartz.h>
#endif // GDK_WINDOWING_QUARTZ
GType
fiv_view_command_get_type(void)
{
static gsize guard;
if (g_once_init_enter(&guard)) {
#define XX(constant, name) {constant, #constant, name},
static const GEnumValue values[] = {FIV_VIEW_COMMANDS(XX) {}};
#undef XX
GType type = g_enum_register_static(
g_intern_static_string("FivViewCommand"), values);
g_once_init_leave(&guard, type);
}
return guard;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
struct _FivView {
GtkWidget parent_instance;
gchar *messages; ///< Image load information
gchar *uri; ///< Path to the current image (if any)
cairo_surface_t *image; ///< The loaded image (sequence)
cairo_surface_t *page; ///< Current page within image, weak
cairo_surface_t *page_scaled; ///< Current page within image, scaled
cairo_surface_t *frame; ///< Current frame within page, weak
FivIoOrientation orientation; ///< Current page orientation
bool enable_cms : 1; ///< Smooth scaling toggle
bool filter : 1; ///< Smooth scaling toggle
bool checkerboard : 1; ///< Show checkerboard background
bool enhance : 1; ///< Try to enhance picture data
bool scale_to_fit : 1; ///< Image no larger than the allocation
double scale; ///< Scaling factor
cairo_surface_t *enhance_swap; ///< Quick swap in/out
FivIoProfile screen_cms_profile; ///< Target colour profile for widget
int remaining_loops; ///< Greater than zero if limited
gint64 frame_time; ///< Current frame's start, µs precision
gulong frame_update_connection; ///< GdkFrameClock::update
};
G_DEFINE_TYPE(FivView, fiv_view, GTK_TYPE_WIDGET)
typedef struct _Dimensions {
double width, height;
} Dimensions;
static FivIoOrientation view_left[9] = {
[FivIoOrientationUnknown] = FivIoOrientationUnknown,
[FivIoOrientation0] = FivIoOrientation270,
[FivIoOrientationMirror0] = FivIoOrientationMirror270,
[FivIoOrientation180] = FivIoOrientation90,
[FivIoOrientationMirror180] = FivIoOrientationMirror90,
[FivIoOrientationMirror270] = FivIoOrientationMirror180,
[FivIoOrientation90] = FivIoOrientation0,
[FivIoOrientationMirror90] = FivIoOrientationMirror0,
[FivIoOrientation270] = FivIoOrientation180,
};
static FivIoOrientation view_mirror[9] = {
[FivIoOrientationUnknown] = FivIoOrientationUnknown,
[FivIoOrientation0] = FivIoOrientationMirror0,
[FivIoOrientationMirror0] = FivIoOrientation0,
[FivIoOrientation180] = FivIoOrientationMirror180,
[FivIoOrientationMirror180] = FivIoOrientation180,
[FivIoOrientationMirror270] = FivIoOrientation270,
[FivIoOrientation90] = FivIoOrientationMirror90,
[FivIoOrientationMirror90] = FivIoOrientation90,
[FivIoOrientation270] = FivIoOrientationMirror270,
};
static FivIoOrientation view_right[9] = {
[FivIoOrientationUnknown] = FivIoOrientationUnknown,
[FivIoOrientation0] = FivIoOrientation90,
[FivIoOrientationMirror0] = FivIoOrientationMirror90,
[FivIoOrientation180] = FivIoOrientation270,
[FivIoOrientationMirror180] = FivIoOrientationMirror270,
[FivIoOrientationMirror270] = FivIoOrientationMirror0,
[FivIoOrientation90] = FivIoOrientation180,
[FivIoOrientationMirror90] = FivIoOrientationMirror180,
[FivIoOrientation270] = FivIoOrientation0,
};
enum {
PROP_MESSAGES = 1,
PROP_SCALE,
PROP_SCALE_TO_FIT,
PROP_ENABLE_CMS,
PROP_FILTER,
PROP_CHECKERBOARD,
PROP_ENHANCE,
PROP_PLAYING,
PROP_HAS_IMAGE,
PROP_CAN_ANIMATE,
PROP_HAS_PREVIOUS_PAGE,
PROP_HAS_NEXT_PAGE,
N_PROPERTIES
};
static GParamSpec *view_properties[N_PROPERTIES];
enum {
COMMAND,
LAST_SIGNAL
};
// Globals are, sadly, the canonical way of storing signal numbers.
static guint view_signals[LAST_SIGNAL];
static void
fiv_view_finalize(GObject *gobject)
{
FivView *self = FIV_VIEW(gobject);
g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free);
g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
g_clear_pointer(&self->image, cairo_surface_destroy);
g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
g_free(self->uri);
g_free(self->messages);
G_OBJECT_CLASS(fiv_view_parent_class)->finalize(gobject);
}
static void
fiv_view_get_property(
GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
{
FivView *self = FIV_VIEW(object);
switch (property_id) {
case PROP_MESSAGES:
g_value_set_string(value, self->messages);
break;
case PROP_SCALE:
g_value_set_double(value, self->scale);
break;
case PROP_SCALE_TO_FIT:
g_value_set_boolean(value, self->scale_to_fit);
break;
case PROP_ENABLE_CMS:
g_value_set_boolean(value, self->enable_cms);
break;
case PROP_FILTER:
g_value_set_boolean(value, self->filter);
break;
case PROP_CHECKERBOARD:
g_value_set_boolean(value, self->checkerboard);
break;
case PROP_ENHANCE:
g_value_set_boolean(value, self->enhance);
break;
case PROP_PLAYING:
g_value_set_boolean(value, !!self->frame_update_connection);
break;
case PROP_HAS_IMAGE:
g_value_set_boolean(value, !!self->image);
break;
case PROP_CAN_ANIMATE:
g_value_set_boolean(value, self->page &&
cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next));
break;
case PROP_HAS_PREVIOUS_PAGE:
g_value_set_boolean(value, self->image && self->page != self->image);
break;
case PROP_HAS_NEXT_PAGE:
g_value_set_boolean(value, self->page &&
cairo_surface_get_user_data(self->page, &fiv_io_key_page_next));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static void
fiv_view_set_property(
GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
{
FivView *self = FIV_VIEW(object);
switch (property_id) {
case PROP_SCALE_TO_FIT:
if (self->scale_to_fit != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT);
break;
case PROP_ENABLE_CMS:
if (self->enable_cms != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_CMS);
break;
case PROP_FILTER:
if (self->filter != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_FILTER);
break;
case PROP_CHECKERBOARD:
if (self->checkerboard != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD);
break;
case PROP_ENHANCE:
if (self->enhance != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_ENHANCE);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
static Dimensions
get_surface_dimensions(FivView *self)
{
if (!self->image)
return (Dimensions) {};
Dimensions dimensions = {};
fiv_io_orientation_dimensions(
self->page, self->orientation, &dimensions.width, &dimensions.height);
return dimensions;
}
static void
get_display_dimensions(FivView *self, int *width, int *height)
{
Dimensions surface_dimensions = get_surface_dimensions(self);
*width = ceil(surface_dimensions.width * self->scale);
*height = ceil(surface_dimensions.height * self->scale);
}
static void
fiv_view_get_preferred_height(GtkWidget *widget, gint *minimum, gint *natural)
{
FivView *self = FIV_VIEW(widget);
if (self->scale_to_fit) {
*minimum = 1;
*natural = MAX(*minimum, ceil(get_surface_dimensions(self).height));
} else {
int dw, dh;
get_display_dimensions(self, &dw, &dh);
*minimum = *natural = dh;
}
}
static void
fiv_view_get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural)
{
FivView *self = FIV_VIEW(widget);
if (self->scale_to_fit) {
*minimum = 1;
*natural = MAX(*minimum, ceil(get_surface_dimensions(self).width));
} else {
int dw, dh;
get_display_dimensions(self, &dw, &dh);
*minimum = *natural = dw;
}
}
static void
prescale_page(FivView *self)
{
FivIoRenderClosure *closure = NULL;
if (!self->image || !(closure =
cairo_surface_get_user_data(self->page, &fiv_io_key_render)))
return;
// TODO(p): Restart the animation. No vector formats currently animate.
g_return_if_fail(!self->frame_update_connection);
// If it fails, the previous frame pointer may become invalid.
g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
self->frame = self->page_scaled = closure->render(closure, self->scale);
if (!self->page_scaled)
self->frame = self->page;
}
static void
fiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
{
GTK_WIDGET_CLASS(fiv_view_parent_class)->size_allocate(widget, allocation);
FivView *self = FIV_VIEW(widget);
if (!self->image || !self->scale_to_fit)
return;
Dimensions surface_dimensions = get_surface_dimensions(self);
double scale = 1;
if (ceil(surface_dimensions.width * scale) > allocation->width)
scale = allocation->width / surface_dimensions.width;
if (ceil(surface_dimensions.height * scale) > allocation->height)
scale = allocation->height / surface_dimensions.height;
if (self->scale != scale) {
self->scale = scale;
g_object_notify_by_pspec(G_OBJECT(widget), view_properties[PROP_SCALE]);
prescale_page(self);
}
}
// https://www.freedesktop.org/wiki/OpenIcc/ICC_Profiles_in_X_Specification_0.4
// has disappeared, but you can use the wayback machine.
//
// Note that Wayland does not have any appropriate protocol, as of writing:
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/14
static void
reload_screen_cms_profile(FivView *self, GdkWindow *window)
{
g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free);
GdkDisplay *display = gdk_window_get_display(window);
GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
int num = -1;
for (int i = gdk_display_get_n_monitors(display); num < 0 && i--; )
if (gdk_display_get_monitor(display, i) == monitor)
num = i;
if (num < 0)
goto out;
char atom[32] = "";
g_snprintf(atom, sizeof atom, "_ICC_PROFILE%c%d", num ? '_' : '\0', num);
// Sadly, there is no nice GTK+/GDK mechanism to watch this for changes.
int format = 0, length = 0;
GdkAtom type = GDK_NONE;
guchar *data = NULL;
GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(window));
if (gdk_property_get(root, gdk_atom_intern(atom, FALSE), GDK_NONE, 0,
8 << 20 /* MiB */, FALSE, &type, &format, &length, &data)) {
if (format == 8 && length > 0)
self->screen_cms_profile = fiv_io_profile_new(data, length);
g_free(data);
}
out:
if (!self->screen_cms_profile)
self->screen_cms_profile = fiv_io_profile_new_sRGB();
}
static void
fiv_view_realize(GtkWidget *widget)
{
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
GdkWindowAttr attributes = {
.window_type = GDK_WINDOW_CHILD,
.x = allocation.x,
.y = allocation.y,
.width = allocation.width,
.height = allocation.height,
// Input-only would presumably also work (as in GtkPathBar, e.g.),
// but it merely seems to involve more work.
.wclass = GDK_INPUT_OUTPUT,
// Assuming here that we can't ask for a higher-precision Visual
// than what we get automatically.
.visual = gtk_widget_get_visual(widget),
// Pointer motion/release enables attaching GtkGestureDrag to
// the parent GtkScrolledWindow, having it work with the mouse.
.event_mask = gtk_widget_get_events(widget) | GDK_KEY_PRESS_MASK |
GDK_SCROLL_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
GDK_POINTER_MOTION_MASK,
};
// We need this window to receive input events at all.
GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
// Without the following call, or the rendering mode set to "recording",
// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
// creates backing stores using cairo_content_t constants.
//
// It completely breaks the Quartz backend, so limit it to X11.
#ifdef GDK_WINDOWING_X11
// FIXME: This causes some flicker while scrolling, because it disables
// double buffering, see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
//
// If GTK+'s OpenGL integration fails to deliver, we need to use the window
// directly, sidestepping the toolkit entirely.
if (GDK_IS_X11_WINDOW(window))
gdk_window_ensure_native(window);
#endif // GDK_WINDOWING_X11
gtk_widget_register_window(widget, window);
gtk_widget_set_window(widget, window);
gtk_widget_set_realized(widget, TRUE);
reload_screen_cms_profile(FIV_VIEW(widget), window);
}
static gboolean
fiv_view_draw(GtkWidget *widget, cairo_t *cr)
{
// Placed here due to our using a native GdkWindow on X11,
// which makes the widget have no double buffering or default background.
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
GtkStyleContext *style = gtk_widget_get_style_context(widget);
gtk_render_background(style, cr, 0, 0, allocation.width, allocation.height);
FivView *self = FIV_VIEW(widget);
if (!self->image ||
!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
return TRUE;
int w, h;
get_display_dimensions(self, &w, &h);
double x = 0;
double y = 0;
if (w < allocation.width)
x = round((allocation.width - w) / 2.);
if (h < allocation.height)
y = round((allocation.height - h) / 2.);
Dimensions surface_dimensions = {};
cairo_matrix_t matrix = fiv_io_orientation_apply(
self->page_scaled ? self->page_scaled : self->page, self->orientation,
&surface_dimensions.width, &surface_dimensions.height);
cairo_translate(cr, x, y);
if (self->checkerboard) {
gtk_style_context_save(style);
gtk_style_context_add_class(style, "checkerboard");
gtk_render_background(style, cr, 0, 0, w, h);
gtk_style_context_restore(style);
}
// Then all frames are pre-scaled.
if (self->page_scaled) {
cairo_set_source_surface(cr, self->frame, 0, 0);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr);
return TRUE;
}
// FIXME: Recording surfaces do not work well with CAIRO_SURFACE_TYPE_XLIB,
// we always get a shitty pixmap, where transparency contains junk.
if (cairo_surface_get_type(self->frame) == CAIRO_SURFACE_TYPE_RECORDING) {
cairo_surface_t *image =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
cairo_t *tcr = cairo_create(image);
cairo_scale(tcr, self->scale, self->scale);
cairo_set_source_surface(tcr, self->frame, 0, 0);
cairo_pattern_set_matrix(cairo_get_source(tcr), &matrix);
cairo_paint(tcr);
cairo_destroy(tcr);
cairo_set_source_surface(cr, image, 0, 0);
cairo_paint(cr);
cairo_surface_destroy(image);
return TRUE;
}
// XXX: The rounding together with padding may result in up to
// a pixel's worth of made-up picture data.
cairo_rectangle(cr, 0, 0, w, h);
cairo_clip(cr);
cairo_scale(cr, self->scale, self->scale);
cairo_set_source_surface(cr, self->frame, 0, 0);
cairo_pattern_t *pattern = cairo_get_source(cr);
cairo_pattern_set_matrix(pattern, &matrix);
cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD);
// TODO(p): Prescale it ourselves to an off-screen bitmap, gamma-correctly.
if (self->filter)
cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD);
else
cairo_pattern_set_filter(pattern, CAIRO_FILTER_NEAREST);
#ifdef GDK_WINDOWING_QUARTZ
// Not supported there. Acts a bit like repeating, but weirdly offset.
if (GDK_IS_QUARTZ_WINDOW(gtk_widget_get_window(widget)))
cairo_pattern_set_extend(pattern, CAIRO_EXTEND_NONE);
#endif // GDK_WINDOWING_QUARTZ
cairo_paint(cr);
return TRUE;
}
static gboolean
fiv_view_button_press_event(GtkWidget *widget, GdkEventButton *event)
{
GTK_WIDGET_CLASS(fiv_view_parent_class)->button_press_event(widget, event);
if (event->button == GDK_BUTTON_PRIMARY &&
gtk_widget_get_focus_on_click(widget))
gtk_widget_grab_focus(widget);
// TODO(p): Use for left button scroll drag, which may rather be a gesture.
return FALSE;
}
#define SCALE_STEP 1.25
static gboolean
set_scale_to_fit(FivView *self, bool scale_to_fit)
{
if (self->scale_to_fit != scale_to_fit) {
self->scale_to_fit = scale_to_fit;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_SCALE_TO_FIT]);
gtk_widget_queue_resize(GTK_WIDGET(self));
}
return TRUE;
}
static gboolean
set_scale(FivView *self, double scale)
{
if (self->scale != scale) {
self->scale = scale;
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_SCALE]);
prescale_page(self);
gtk_widget_queue_resize(GTK_WIDGET(self));
}
return set_scale_to_fit(self, false);
}
static void
set_scale_to_fit_width(FivView *self)
{
double w = get_surface_dimensions(self).width;
int allocated = gtk_widget_get_allocated_width(
gtk_widget_get_parent(GTK_WIDGET(self)));
if (ceil(w * self->scale) > allocated)
set_scale(self, allocated / w);
}
static void
set_scale_to_fit_height(FivView *self)
{
double h = get_surface_dimensions(self).height;
int allocated = gtk_widget_get_allocated_height(
gtk_widget_get_parent(GTK_WIDGET(self)));
if (ceil(h * self->scale) > allocated)
set_scale(self, allocated / h);
}
static gboolean
fiv_view_scroll_event(GtkWidget *widget, GdkEventScroll *event)
{
FivView *self = FIV_VIEW(widget);
if (!self->image)
return FALSE;
if (event->state & gtk_accelerator_get_default_mod_mask())
return FALSE;
switch (event->direction) {
case GDK_SCROLL_UP:
return set_scale(self, self->scale * SCALE_STEP);
case GDK_SCROLL_DOWN:
return set_scale(self, self->scale / SCALE_STEP);
default:
// For some reason, native GdkWindows may also get GDK_SCROLL_SMOOTH.
// Left/right are good to steal from GtkScrolledWindow for consistency.
return TRUE;
}
}
static void
stop_animating(FivView *self)
{
GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self));
if (!clock || !self->frame_update_connection)
return;
g_signal_handler_disconnect(clock, self->frame_update_connection);
gdk_frame_clock_end_updating(clock);
self->frame_time = 0;
self->frame_update_connection = 0;
self->remaining_loops = 0;
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
}
static gboolean
advance_frame(FivView *self)
{
cairo_surface_t *next =
cairo_surface_get_user_data(self->frame, &fiv_io_key_frame_next);
if (next) {
self->frame = next;
} else {
if (self->remaining_loops && !--self->remaining_loops)
return FALSE;
self->frame = self->page;
}
return TRUE;
}
static gboolean
advance_animation(FivView *self, GdkFrameClock *clock)
{
gint64 now = gdk_frame_clock_get_frame_time(clock);
while (true) {
// TODO(p): See if infinite frames can actually happen, and how.
intptr_t duration = (intptr_t) cairo_surface_get_user_data(
self->frame, &fiv_io_key_frame_duration);
if (duration < 0)
return FALSE;
// Do not busy loop. GIF timings are given in hundredths of a second.
// Note that browsers seem to do [< 10] => 100:
// https://bugs.webkit.org/show_bug.cgi?id=36082
if (duration == 0)
duration = gdk_frame_timings_get_refresh_interval(
gdk_frame_clock_get_current_timings(clock)) / 1000;
if (duration == 0)
duration = 1;
gint64 then = self->frame_time + duration * 1000;
if (then > now)
return TRUE;
if (!advance_frame(self))
return FALSE;
self->frame_time = then;
gtk_widget_queue_draw(GTK_WIDGET(self));
}
}
static void
on_frame_clock_update(GdkFrameClock *clock, gpointer user_data)
{
FivView *self = FIV_VIEW(user_data);
if (!advance_animation(self, clock))
stop_animating(self);
}
static void
start_animating(FivView *self)
{
stop_animating(self);
GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self));
if (!clock || !self->image ||
!cairo_surface_get_user_data(self->page, &fiv_io_key_frame_next))
return;
self->frame_time = gdk_frame_clock_get_frame_time(clock);
self->frame_update_connection = g_signal_connect(
clock, "update", G_CALLBACK(on_frame_clock_update), self);
self->remaining_loops =
(uintptr_t) cairo_surface_get_user_data(self->page, &fiv_io_key_loops);
gdk_frame_clock_begin_updating(clock);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
}
static void
switch_page(FivView *self, cairo_surface_t *page)
{
g_clear_pointer(&self->page_scaled, cairo_surface_destroy);
self->frame = self->page = page;
prescale_page(self);
if (!self->page ||
(self->orientation = (uintptr_t) cairo_surface_get_user_data(
self->page, &fiv_io_key_orientation)) == FivIoOrientationUnknown)
self->orientation = FivIoOrientation0;
start_animating(self);
gtk_widget_queue_resize(GTK_WIDGET(self));
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_CAN_ANIMATE]);
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_HAS_PREVIOUS_PAGE]);
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_HAS_NEXT_PAGE]);
}
static void
fiv_view_map(GtkWidget *widget)
{
GTK_WIDGET_CLASS(fiv_view_parent_class)->map(widget);
// Loading before mapping will fail to obtain a GdkFrameClock.
start_animating(FIV_VIEW(widget));
}
void
fiv_view_unmap(GtkWidget *widget)
{
stop_animating(FIV_VIEW(widget));
GTK_WIDGET_CLASS(fiv_view_parent_class)->unmap(widget);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
show_error_dialog(GtkWindow *parent, GError *error)
{
GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
g_error_free(error);
}
static GtkWindow *
get_toplevel(GtkWidget *widget)
{
if (GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget))))
return GTK_WINDOW(widget);
return NULL;
}
static void
on_draw_page(G_GNUC_UNUSED GtkPrintOperation *operation,
GtkPrintContext *context, G_GNUC_UNUSED int page_nr, FivView *self)
{
// Any DPI will be wrong, unless we import that information from the image.
double scale = 1 / 96.;
Dimensions surface_dimensions = {};
cairo_matrix_t matrix =
fiv_io_orientation_apply(self->page, self->orientation,
&surface_dimensions.width, &surface_dimensions.height);
double w = surface_dimensions.width * scale;
double h = surface_dimensions.height * scale;
// Scale down to fit the print area, taking care to not divide by zero.
double areaw = gtk_print_context_get_width(context);
double areah = gtk_print_context_get_height(context);
scale *= fmin((areaw < w) ? areaw / w : 1, (areah < h) ? areah / h : 1);
cairo_t *cr = gtk_print_context_get_cairo_context(context);
cairo_scale(cr, scale, scale);
cairo_set_source_surface(cr, self->frame, 0, 0);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr);
}
static void
print(FivView *self)
{
GtkPrintOperation *print = gtk_print_operation_new();
gtk_print_operation_set_n_pages(print, 1);
gtk_print_operation_set_embed_page_setup(print, TRUE);
gtk_print_operation_set_unit(print, GTK_UNIT_INCH);
gtk_print_operation_set_job_name(print, "Image");
g_signal_connect(print, "draw-page", G_CALLBACK(on_draw_page), self);
static GtkPrintSettings *settings = NULL;
if (settings != NULL)
gtk_print_operation_set_print_settings(print, settings);
GError *error = NULL;
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
GtkPrintOperationResult res = gtk_print_operation_run(
print, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, window, &error);
if (res == GTK_PRINT_OPERATION_RESULT_APPLY) {
if (settings != NULL)
g_object_unref(settings);
settings = g_object_ref(gtk_print_operation_get_print_settings(print));
}
if (error)
show_error_dialog(window, error);
g_object_unref(print);
}
static gboolean
save_as(FivView *self, cairo_surface_t *frame)
{
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
FivIoProfile target = NULL;
if (self->enable_cms && (target = self->screen_cms_profile)) {
GtkWidget *dialog = gtk_message_dialog_new(window, GTK_DIALOG_MODAL,
GTK_MESSAGE_WARNING, GTK_BUTTONS_CLOSE, "%s",
"Color management overrides attached color profiles.");
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
}
GtkWidget *dialog = gtk_file_chooser_dialog_new(
frame ? "Save frame as" : "Save page as",
window, GTK_FILE_CHOOSER_ACTION_SAVE,
"_Cancel", GTK_RESPONSE_CANCEL, "_Save", GTK_RESPONSE_ACCEPT, NULL);
GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
GFile *file = g_file_new_for_uri(self->uri);
const gchar *path = g_file_peek_path(file);
// TODO(p): Use g_file_info_get_display_name().
gchar *basename = g_filename_display_basename(path ? path : self->uri);
// Note that GTK+'s save dialog is too stupid to automatically change
// the extension when user changes the filter. Presumably,
// gtk_file_chooser_set_extra_widget() can be used to circumvent this.
gchar *name =
g_strdup_printf(frame ? "%s.frame.webp" : "%s.webp", basename);
gtk_file_chooser_set_current_name(chooser, name);
g_free(name);
if (path) {
gchar *dirname = g_path_get_dirname(path);
gtk_file_chooser_set_current_folder(chooser, dirname);
g_free(dirname);
}
g_free(basename);
g_object_unref(file);
// This is the best general format: supports lossless encoding, animations,
// alpha channel, and Exif and ICC profile metadata.
// PNG is another viable option, but sPNG can't do APNG, Wuffs can't save,
// and libpng is a pain in the arse.
GtkFileFilter *webp_filter = gtk_file_filter_new();
gtk_file_filter_add_mime_type(webp_filter, "image/webp");
gtk_file_filter_add_pattern(webp_filter, "*.webp");
gtk_file_filter_set_name(webp_filter, "Lossless WebP (*.webp)");
gtk_file_chooser_add_filter(chooser, webp_filter);
// The format is supported by Exiv2 and ExifTool.
// This is mostly a developer tool.
GtkFileFilter *exv_filter = gtk_file_filter_new();
gtk_file_filter_add_mime_type(exv_filter, "image/x-exv");
gtk_file_filter_add_pattern(exv_filter, "*.exv");
gtk_file_filter_set_name(exv_filter, "Exiv2 metadata (*.exv)");
gtk_file_chooser_add_filter(chooser, exv_filter);
GError *error = NULL;
switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
gchar *path;
case GTK_RESPONSE_ACCEPT:
path = gtk_file_chooser_get_filename(chooser);
if (!(gtk_file_chooser_get_filter(chooser) == webp_filter
? fiv_io_save(self->page, frame, target, path, &error)
: fiv_io_save_metadata(self->page, path, &error)))
show_error_dialog(window, error);
g_free(path);
// Fall-through.
default:
gtk_widget_destroy(dialog);
// Fall-through.
case GTK_RESPONSE_NONE:
return TRUE;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static GtkWidget *
info_start_group(GtkWidget *vbox, const char *group)
{
GtkWidget *label = gtk_label_new(group);
gtk_widget_set_hexpand(label, TRUE);
gtk_widget_set_halign(label, GTK_ALIGN_FILL);
PangoAttrList *attrs = pango_attr_list_new();
pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
gtk_label_set_attributes(GTK_LABEL(label), attrs);
pango_attr_list_unref(attrs);
GtkWidget *grid = gtk_grid_new();
GtkWidget *expander = gtk_expander_new(NULL);
gtk_expander_set_label_widget(GTK_EXPANDER(expander), label);
gtk_expander_set_expanded(GTK_EXPANDER(expander), TRUE);
gtk_container_add(GTK_CONTAINER(expander), grid);
gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
gtk_box_pack_start(GTK_BOX(vbox), expander, FALSE, FALSE, 0);
return grid;
}
static GtkWidget *
info_parse(char *tsv)
{
GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
const char *last_group = NULL;
GtkWidget *grid = NULL;
int line = 1, row = 0;
for (char *nl; (nl = strchr(tsv, '\n')); line++, tsv = ++nl) {
*nl = 0;
if (nl > tsv && nl[-1] == '\r')
nl[-1] = 0;
char *group = tsv, *tag = strchr(group, '\t');
if (!tag) {
g_warning("ExifTool parse error on line %d", line);
continue;
}
*tag++ = 0;
for (char *p = group; *p; p++)
if (*p == '_')
*p = ' ';
char *value = strchr(tag, '\t');
if (!value) {
g_warning("ExifTool parse error on line %d", line);
continue;
}
*value++ = 0;
if (!last_group || strcmp(last_group, group)) {
grid = info_start_group(vbox, (last_group = group));
row = 0;
}
GtkWidget *a = gtk_label_new(tag);
gtk_size_group_add_widget(sg, a);
gtk_label_set_selectable(GTK_LABEL(a), TRUE);
gtk_label_set_xalign(GTK_LABEL(a), 0.);
gtk_grid_attach(GTK_GRID(grid), a, 0, row, 1, 1);
GtkWidget *b = gtk_label_new(value);
gtk_label_set_selectable(GTK_LABEL(b), TRUE);
gtk_label_set_xalign(GTK_LABEL(b), 0.);
gtk_label_set_line_wrap(GTK_LABEL(b), TRUE);
gtk_widget_set_hexpand(b, TRUE);
gtk_grid_attach(GTK_GRID(grid), b, 1, row, 1, 1);
row++;
}
g_object_unref(sg);
return vbox;
}
static void
info(FivView *self)
{
// TODO(p): Add a fallback to internal capabilities.
// The simplest is to specify the filename and the resolution.
GtkWindow *window = get_toplevel(GTK_WIDGET(self));
int flags = G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE;
GError *error = NULL;
gchar *path = g_filename_from_uri(self->uri, NULL, &error);
if (!path) {
show_error_dialog(window, error);
return;
}
GSubprocess *subprocess = g_subprocess_new(flags, &error, "exiftool",
"-tab", "-groupNames", "-duplicates", "-extractEmbedded", "--binary",
"-quiet", "--", path, NULL);
g_free(path);
if (error) {
show_error_dialog(window, error);
return;
}
gchar *out = NULL, *err = NULL;
if (!g_subprocess_communicate_utf8(
subprocess, NULL, NULL, &out, &err, &error)) {
show_error_dialog(window, error);
return;
}
GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
"use-header-bar", TRUE,
"title", "Information",
"transient-for", window,
"destroy-with-parent", TRUE, NULL);
GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
if (*err) {
GtkWidget *info = gtk_info_bar_new();
GtkInfoBar *info_bar = GTK_INFO_BAR(info);
gtk_info_bar_set_message_type(info_bar, GTK_MESSAGE_WARNING);
GtkWidget *info_area = gtk_info_bar_get_content_area(info_bar);
GtkWidget *label = gtk_label_new(g_strstrip(err));
gtk_container_add(GTK_CONTAINER(info_area), label);
gtk_container_add(GTK_CONTAINER(content_area), info);
}
GtkWidget *scroller = gtk_scrolled_window_new(NULL, NULL);
GtkScrolledWindow *sw = GTK_SCROLLED_WINDOW(scroller);
gtk_scrolled_window_set_max_content_width(sw, 600);
gtk_scrolled_window_set_max_content_height(sw, 800);
gtk_scrolled_window_set_propagate_natural_width(sw, TRUE);
gtk_scrolled_window_set_propagate_natural_height(sw, TRUE);
GtkWidget *info = info_parse(out);
gtk_widget_set_hexpand(info, TRUE);
gtk_widget_set_vexpand(info, TRUE);
gtk_style_context_add_class(
gtk_widget_get_style_context(GTK_WIDGET(info)), "fiv-information");
gtk_container_add(GTK_CONTAINER(scroller), info);
gtk_container_add(GTK_CONTAINER(content_area), scroller);
g_free(out);
g_free(err);
g_object_unref(subprocess);
gtk_widget_show_all(dialog);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static gboolean
fiv_view_key_press_event(GtkWidget *widget, GdkEventKey *event)
{
FivView *self = FIV_VIEW(widget);
// So far, our commands cannot accept arguments, so these few are hardcoded.
if (self->image &&
!(event->state & gtk_accelerator_get_default_mod_mask()) &&
event->keyval >= GDK_KEY_1 && event->keyval <= GDK_KEY_9)
return set_scale(self, event->keyval - GDK_KEY_0);
return GTK_WIDGET_CLASS(fiv_view_parent_class)
->key_press_event(widget, event);
}
static void
bind(GtkBindingSet *bs, guint keyval, GdkModifierType modifiers,
FivViewCommand command)
{
gtk_binding_entry_add_signal(
bs, keyval, modifiers, "command", 1, FIV_TYPE_VIEW_COMMAND, command);
}
static void
fiv_view_class_init(FivViewClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS(klass);
object_class->finalize = fiv_view_finalize;
object_class->get_property = fiv_view_get_property;
object_class->set_property = fiv_view_set_property;
view_properties[PROP_MESSAGES] = g_param_spec_string(
"messages", "Messages", "Informative messages from the last image load",
NULL, G_PARAM_READABLE);
view_properties[PROP_SCALE] = g_param_spec_double(
"scale", "Scale", "Zoom level",
0, G_MAXDOUBLE, 1.0, G_PARAM_READABLE);
view_properties[PROP_SCALE_TO_FIT] = g_param_spec_boolean(
"scale-to-fit", "Scale to fit", "Scale images down to fit the window",
TRUE, G_PARAM_READWRITE);
view_properties[PROP_ENABLE_CMS] = g_param_spec_boolean(
"enable-cms", "Enable CMS", "Enable color management",
TRUE, G_PARAM_READWRITE);
view_properties[PROP_FILTER] = g_param_spec_boolean(
"filter", "Use filtering", "Scale images smoothly",
TRUE, G_PARAM_READWRITE);
view_properties[PROP_CHECKERBOARD] = g_param_spec_boolean(
"checkerboard", "Show checkerboard", "Highlight transparent background",
FALSE, G_PARAM_READWRITE);
view_properties[PROP_ENHANCE] = g_param_spec_boolean(
"enhance", "Enhance JPEG", "Enhance low-quality JPEG",
FALSE, G_PARAM_READWRITE);
view_properties[PROP_PLAYING] = g_param_spec_boolean(
"playing", "Playing animation", "An animation is running",
FALSE, G_PARAM_READABLE);
view_properties[PROP_HAS_IMAGE] = g_param_spec_boolean(
"has-image", "Has an image", "An image is loaded",
FALSE, G_PARAM_READABLE);
view_properties[PROP_CAN_ANIMATE] = g_param_spec_boolean(
"can-animate", "Can animate", "An animation is loaded",
FALSE, G_PARAM_READABLE);
view_properties[PROP_HAS_PREVIOUS_PAGE] = g_param_spec_boolean(
"has-previous-page", "Has a previous page", "Preceding pages exist",
FALSE, G_PARAM_READABLE);
view_properties[PROP_HAS_NEXT_PAGE] = g_param_spec_boolean(
"has-next-page", "Has a next page", "Following pages exist",
FALSE, G_PARAM_READABLE);
g_object_class_install_properties(
object_class, N_PROPERTIES, view_properties);
view_signals[COMMAND] =
g_signal_new_class_handler("command", G_TYPE_FROM_CLASS(klass),
G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_CALLBACK(fiv_view_command),
NULL, NULL, NULL, G_TYPE_NONE, 1, FIV_TYPE_VIEW_COMMAND);
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
widget_class->get_preferred_height = fiv_view_get_preferred_height;
widget_class->get_preferred_width = fiv_view_get_preferred_width;
widget_class->size_allocate = fiv_view_size_allocate;
widget_class->map = fiv_view_map;
widget_class->unmap = fiv_view_unmap;
widget_class->realize = fiv_view_realize;
widget_class->draw = fiv_view_draw;
widget_class->button_press_event = fiv_view_button_press_event;
widget_class->scroll_event = fiv_view_scroll_event;
widget_class->key_press_event = fiv_view_key_press_event;
GtkBindingSet *bs = gtk_binding_set_by_class(klass);
// First, the standard, intuitive bindings.
bind(bs, GDK_KEY_0, GDK_CONTROL_MASK, FIV_VIEW_COMMAND_ZOOM_1);
bind(bs, GDK_KEY_plus, GDK_CONTROL_MASK, FIV_VIEW_COMMAND_ZOOM_IN);
bind(bs, GDK_KEY_minus, GDK_CONTROL_MASK, FIV_VIEW_COMMAND_ZOOM_OUT);
bind(bs, GDK_KEY_p, GDK_CONTROL_MASK, FIV_VIEW_COMMAND_PRINT);
bind(bs, GDK_KEY_r, GDK_CONTROL_MASK, FIV_VIEW_COMMAND_RELOAD);
bind(bs, GDK_KEY_s, GDK_CONTROL_MASK, FIV_VIEW_COMMAND_SAVE_PAGE);
bind(bs, GDK_KEY_s, GDK_MOD1_MASK, FIV_VIEW_COMMAND_SAVE_FRAME);
bind(bs, GDK_KEY_Return, GDK_MOD1_MASK, FIV_VIEW_COMMAND_INFO);
// The scale-to-fit binding is from gThumb, which has more such modes.
bind(bs, GDK_KEY_F5, 0, FIV_VIEW_COMMAND_RELOAD);
bind(bs, GDK_KEY_r, 0, FIV_VIEW_COMMAND_RELOAD);
bind(bs, GDK_KEY_plus, 0, FIV_VIEW_COMMAND_ZOOM_IN);
bind(bs, GDK_KEY_minus, 0, FIV_VIEW_COMMAND_ZOOM_OUT);
bind(bs, GDK_KEY_w, 0, FIV_VIEW_COMMAND_FIT_WIDTH);
bind(bs, GDK_KEY_h, 0, FIV_VIEW_COMMAND_FIT_HEIGHT);
bind(bs, GDK_KEY_x, 0, FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT);
bind(bs, GDK_KEY_c, 0, FIV_VIEW_COMMAND_TOGGLE_CMS);
bind(bs, GDK_KEY_i, 0, FIV_VIEW_COMMAND_TOGGLE_FILTER);
bind(bs, GDK_KEY_t, 0, FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD);
bind(bs, GDK_KEY_e, 0, FIV_VIEW_COMMAND_TOGGLE_ENHANCE);
bind(bs, GDK_KEY_less, 0, FIV_VIEW_COMMAND_ROTATE_LEFT);
bind(bs, GDK_KEY_equal, 0, FIV_VIEW_COMMAND_MIRROR);
bind(bs, GDK_KEY_greater, 0, FIV_VIEW_COMMAND_ROTATE_RIGHT);
bind(bs, GDK_KEY_bracketleft, 0, FIV_VIEW_COMMAND_PAGE_PREVIOUS);
bind(bs, GDK_KEY_bracketright, 0, FIV_VIEW_COMMAND_PAGE_NEXT);
bind(bs, GDK_KEY_braceleft, 0, FIV_VIEW_COMMAND_FRAME_PREVIOUS);
bind(bs, GDK_KEY_braceright, 0, FIV_VIEW_COMMAND_FRAME_NEXT);
bind(bs, GDK_KEY_space, 0, FIV_VIEW_COMMAND_TOGGLE_PLAYBACK);
// TODO(p): Later override "screen_changed", recreate Pango layouts there,
// if we get to have any, or otherwise reflect DPI changes.
gtk_widget_class_set_css_name(widget_class, "fiv-view");
}
static void
fiv_view_init(FivView *self)
{
gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
self->enable_cms = true;
self->filter = true;
self->checkerboard = false;
self->scale = 1.0;
}
// --- Public interface --------------------------------------------------------
static cairo_surface_t *
open_without_swapping_in(FivView *self, const char *uri)
{
FivIoOpenContext ctx = {
.uri = uri,
.screen_profile = self->enable_cms ? self->screen_cms_profile : NULL,
.screen_dpi = 96, // TODO(p): Try to retrieve it from the screen.
.enhance = self->enhance,
.warnings = g_ptr_array_new_with_free_func(g_free),
};
GError *error = NULL;
cairo_surface_t *surface = fiv_io_open(&ctx, &error);
if (error) {
g_ptr_array_add(ctx.warnings, g_strdup(error->message));
g_error_free(error);
}
g_clear_pointer(&self->messages, g_free);
if (ctx.warnings->len) {
g_ptr_array_add(ctx.warnings, NULL);
self->messages = g_strjoinv("\n", (gchar **) ctx.warnings->pdata);
}
g_ptr_array_free(ctx.warnings, TRUE);
return surface;
}
// TODO(p): Progressive picture loading, or at least async/cancellable.
gboolean
fiv_view_set_uri(FivView *self, const char *uri)
{
// This is extremely expensive, and only works sometimes.
g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
if (self->enhance) {
self->enhance = FALSE;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_ENHANCE]);
}
cairo_surface_t *surface = open_without_swapping_in(self, uri);
g_clear_pointer(&self->image, cairo_surface_destroy);
self->frame = self->page = NULL;
self->image = surface;
switch_page(self, self->image);
set_scale_to_fit(self, true);
g_free(self->uri);
self->uri = g_strdup(uri);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_HAS_IMAGE]);
return surface != NULL;
}
static void
page_step(FivView *self, int step)
{
cairo_user_data_key_t *key =
step < 0 ? &fiv_io_key_page_previous : &fiv_io_key_page_next;
cairo_surface_t *page = cairo_surface_get_user_data(self->page, key);
if (page)
switch_page(self, page);
}
static void
frame_step(FivView *self, int step)
{
stop_animating(self);
cairo_user_data_key_t *key =
step < 0 ? &fiv_io_key_frame_previous : &fiv_io_key_frame_next;
if (!step || !(self->frame = cairo_surface_get_user_data(self->frame, key)))
self->frame = self->page;
gtk_widget_queue_draw(GTK_WIDGET(self));
}
static gboolean
reload(FivView *self)
{
cairo_surface_t *surface = open_without_swapping_in(self, self->uri);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]);
if (!surface)
return FALSE;
g_clear_pointer(&self->image, cairo_surface_destroy);
g_clear_pointer(&self->enhance_swap, cairo_surface_destroy);
switch_page(self, (self->image = surface));
return TRUE;
}
static void
swap_enhanced_image(FivView *self)
{
cairo_surface_t *saved = self->image;
self->image = self->page = self->frame = NULL;
if (self->enhance_swap) {
switch_page(self, (self->image = self->enhance_swap));
self->enhance_swap = saved;
} else if (reload(self)) {
self->enhance_swap = saved;
} else {
switch_page(self, (self->image = saved));
}
}
void
fiv_view_command(FivView *self, FivViewCommand command)
{
g_return_if_fail(FIV_IS_VIEW(self));
GtkWidget *widget = GTK_WIDGET(self);
if (!self->image)
return;
switch (command) {
break; case FIV_VIEW_COMMAND_RELOAD:
reload(self);
break; case FIV_VIEW_COMMAND_ROTATE_LEFT:
self->orientation = view_left[self->orientation];
gtk_widget_queue_resize(widget);
break; case FIV_VIEW_COMMAND_MIRROR:
self->orientation = view_mirror[self->orientation];
gtk_widget_queue_resize(widget);
break; case FIV_VIEW_COMMAND_ROTATE_RIGHT:
self->orientation = view_right[self->orientation];
gtk_widget_queue_resize(widget);
break; case FIV_VIEW_COMMAND_PAGE_FIRST:
switch_page(self, self->image);
break; case FIV_VIEW_COMMAND_PAGE_PREVIOUS:
page_step(self, -1);
break; case FIV_VIEW_COMMAND_PAGE_NEXT:
page_step(self, +1);
break; case FIV_VIEW_COMMAND_PAGE_LAST:
for (cairo_surface_t *s = self->page;
(s = cairo_surface_get_user_data(s, &fiv_io_key_page_next)); )
self->page = s;
switch_page(self, self->page);
break; case FIV_VIEW_COMMAND_FRAME_FIRST:
frame_step(self, 0);
break; case FIV_VIEW_COMMAND_FRAME_PREVIOUS:
frame_step(self, -1);
break; case FIV_VIEW_COMMAND_FRAME_NEXT:
frame_step(self, +1);
break; case FIV_VIEW_COMMAND_TOGGLE_PLAYBACK:
self->frame_update_connection
? stop_animating(self)
: start_animating(self);
break; case FIV_VIEW_COMMAND_TOGGLE_CMS:
self->enable_cms = !self->enable_cms;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_ENABLE_CMS]);
reload(self);
break; case FIV_VIEW_COMMAND_TOGGLE_FILTER:
self->filter = !self->filter;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_FILTER]);
gtk_widget_queue_draw(widget);
break; case FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD:
self->checkerboard = !self->checkerboard;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_CHECKERBOARD]);
gtk_widget_queue_draw(widget);
break; case FIV_VIEW_COMMAND_TOGGLE_ENHANCE:
self->enhance = !self->enhance;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_ENHANCE]);
swap_enhanced_image(self);
break; case FIV_VIEW_COMMAND_PRINT:
print(self);
break; case FIV_VIEW_COMMAND_SAVE_PAGE:
save_as(self, NULL);
break; case FIV_VIEW_COMMAND_SAVE_FRAME:
save_as(self, self->frame);
break; case FIV_VIEW_COMMAND_INFO:
info(self);
break; case FIV_VIEW_COMMAND_ZOOM_IN:
set_scale(self, self->scale * SCALE_STEP);
break; case FIV_VIEW_COMMAND_ZOOM_OUT:
set_scale(self, self->scale / SCALE_STEP);
break; case FIV_VIEW_COMMAND_ZOOM_1:
set_scale(self, 1.0);
break; case FIV_VIEW_COMMAND_FIT_WIDTH:
set_scale_to_fit_width(self);
break; case FIV_VIEW_COMMAND_FIT_HEIGHT:
set_scale_to_fit_height(self);
break; case FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT:
set_scale_to_fit(self, !self->scale_to_fit);
}
}