Files
fiv/fiv-view.c
Přemysl Eric Janouch eb65d8582f
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
Bump copyright years
2025-11-20 12:50:21 +01:00

2167 lines
69 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// fiv-view.c: image viewing widget
//
// 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.
//
// 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 "fiv-context-menu.h"
#include <math.h>
#include <stdbool.h>
#include <epoxy/gl.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
#ifdef GDK_WINDOWING_WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <gdk/gdkwin32.h>
#endif // GDK_WINDOWING_WIN32
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;
GtkAdjustment *hadjustment; ///< GtkScrollable boilerplate
GtkAdjustment *vadjustment; ///< GtkScrollable boilerplate
GtkScrollablePolicy hscroll_policy; ///< GtkScrollable boilerplate
GtkScrollablePolicy vscroll_policy; ///< GtkScrollable boilerplate
gchar *messages; ///< Image load information
gchar *uri; ///< Path to the current image (if any)
FivIoImage *image; ///< The loaded image (sequence)
FivIoImage *page; ///< Current page within image, weak
FivIoImage *page_scaled; ///< Current page within image, scaled
FivIoImage *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
bool fixate : 1; ///< Keep zoom and position
double scale; ///< Scaling factor
double drag_start[2]; ///< Adjustment values for drag origin
FivIoImage *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
GdkGLContext *gl_context; ///< OpenGL context
bool gl_initialized; ///< Objects have been created
GLuint gl_program; ///< Linked render program
};
G_DEFINE_TYPE_EXTENDED(FivView, fiv_view, GTK_TYPE_WIDGET, 0,
G_IMPLEMENT_INTERFACE(GTK_TYPE_SCROLLABLE, NULL))
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] = FivIoOrientation90,
[FivIoOrientation90] = FivIoOrientationMirror270,
[FivIoOrientationMirror90] = FivIoOrientation270,
[FivIoOrientation270] = FivIoOrientationMirror90,
};
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_FIXATE,
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,
// These are overriden, we do not register them.
PROP_HADJUSTMENT,
PROP_VADJUSTMENT,
PROP_HSCROLL_POLICY,
PROP_VSCROLL_POLICY,
};
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];
// --- OpenGL ------------------------------------------------------------------
// While GTK+ 3 technically still supports legacy desktop OpenGL 2.0[1],
// we will pick the 3.3 core profile, which is fairly old by now.
// It doesn't seem to make any sense to go below 3.2.
//
// [1] https://stackoverflow.com/a/37923507/76313
//
// OpenGL ES
//
// Currently, we do not support OpenGL ES at all--it needs its own shaders
// (if only because of different #version statements), and also further analysis
// as to what is our minimum version requirement. While GTK+ 3 can again go
// down as low as OpenGL ES 2.0, this might be too much of a hassle to support.
//
// ES can be forced via GDK_GL=gles, if gdk_gl_context_set_required_version()
// doesn't stand in the way.
//
// Let's not forget that this is a desktop image viewer first and foremost.
static const char *
gl_error_string(GLenum err)
{
switch (err) {
case GL_NO_ERROR:
return "no error";
case GL_CONTEXT_LOST:
return "context lost";
case GL_INVALID_ENUM:
return "invalid enum";
case GL_INVALID_VALUE:
return "invalid value";
case GL_INVALID_OPERATION:
return "invalid operation";
case GL_INVALID_FRAMEBUFFER_OPERATION:
return "invalid framebuffer operation";
case GL_OUT_OF_MEMORY:
return "out of memory";
case GL_STACK_UNDERFLOW:
return "stack underflow";
case GL_STACK_OVERFLOW:
return "stack overflow";
default:
return NULL;
}
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static const char *gl_vertex =
"#version 330\n"
"layout(location = 0) in vec4 position;\n"
"out vec2 coordinates;\n"
"void main() {\n"
"\tcoordinates = position.zw;\n"
"\tgl_Position = vec4(position.xy, 0., 1.);\n"
"}\n";
static const char *gl_fragment =
"#version 330\n"
"in vec2 coordinates;\n"
"layout(location = 0) out vec4 color;\n"
"uniform sampler2D picture;\n"
"uniform bool checkerboard;\n"
"\n"
"vec3 checker() {\n"
"\tvec2 xy = gl_FragCoord.xy / 20.;\n"
"\tif (checkerboard && (int(floor(xy.x) + floor(xy.y)) & 1) == 0)\n"
"\t\treturn vec3(0.98);\n"
"\telse\n"
"\t\treturn vec3(1.00);\n"
"}\n"
"\n"
"void main() {\n"
"\tvec3 c = checker();\n"
"\tvec4 t = texture(picture, coordinates);\n"
"\t// Premultiplied blending with a solid background.\n"
"\t// XXX: This is only correct for linear components.\n"
"\tcolor = vec4(c * (1. - t.a) + t.rgb, 1.);\n"
"}\n";
static GLuint
gl_make_shader(int type, const char *glsl)
{
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &glsl, NULL);
glCompileShader(shader);
GLint status = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (!status) {
GLint len = 0;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &len);
GLchar *buffer = g_malloc0(len + 1);
glGetShaderInfoLog(shader, len, NULL, buffer);
g_warning("GL shader compilation failed: %s", buffer);
g_free(buffer);
glDeleteShader(shader);
return 0;
}
return shader;
}
static GLuint
gl_make_program(void)
{
GLuint vertex = gl_make_shader(GL_VERTEX_SHADER, gl_vertex);
GLuint fragment = gl_make_shader(GL_FRAGMENT_SHADER, gl_fragment);
if (!vertex || !fragment) {
glDeleteShader(vertex);
glDeleteShader(fragment);
return 0;
}
GLuint program = glCreateProgram();
glAttachShader(program, vertex);
glAttachShader(program, fragment);
glLinkProgram(program);
glDeleteShader(vertex);
glDeleteShader(fragment);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (!status) {
GLint len = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &len);
GLchar *buffer = g_malloc0(len + 1);
glGetProgramInfoLog(program, len, NULL, buffer);
g_warning("GL program linking failed: %s", buffer);
g_free(buffer);
glDeleteProgram(program);
return 0;
}
return program;
}
// -----------------------------------------------------------------------------
static void
on_adjustment_value_changed(
G_GNUC_UNUSED GtkAdjustment *adjustment, gpointer user_data)
{
gtk_widget_queue_draw(GTK_WIDGET(user_data));
}
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
update_adjustments(FivView *self)
{
int dw = 0, dh = 0;
get_display_dimensions(self, &dw, &dh);
GtkAllocation alloc;
gtk_widget_get_allocation(GTK_WIDGET(self), &alloc);
if (self->hadjustment) {
gtk_adjustment_configure(self->hadjustment,
gtk_adjustment_get_value(self->hadjustment),
0, MAX(dw, alloc.width),
alloc.width * 0.1, alloc.width * 0.9, alloc.width);
}
if (self->vadjustment) {
gtk_adjustment_configure(self->vadjustment,
gtk_adjustment_get_value(self->vadjustment),
0, MAX(dh, alloc.height),
alloc.height * 0.1, alloc.height * 0.9, alloc.height);
}
}
static gboolean
replace_adjustment(
FivView *self, GtkAdjustment **adjustment, GtkAdjustment *replacement)
{
if (*adjustment == replacement)
return FALSE;
if (*adjustment) {
g_signal_handlers_disconnect_by_func(
*adjustment, on_adjustment_value_changed, self);
g_clear_object(adjustment);
}
if (replacement) {
*adjustment = g_object_ref(replacement);
g_signal_connect(*adjustment, "value-changed",
G_CALLBACK(on_adjustment_value_changed), self);
update_adjustments(self);
}
return TRUE;
}
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, fiv_io_image_unref);
g_clear_pointer(&self->image, fiv_io_image_unref);
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
g_free(self->uri);
g_free(self->messages);
replace_adjustment(self, &self->hadjustment, NULL);
replace_adjustment(self, &self->vadjustment, NULL);
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_FIXATE:
g_value_set_boolean(value, self->fixate);
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 && self->page->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 && self->page->page_next);
break;
case PROP_HADJUSTMENT:
g_value_set_object(value, self->hadjustment);
break;
case PROP_VADJUSTMENT:
g_value_set_object(value, self->vadjustment);
break;
case PROP_HSCROLL_POLICY:
g_value_set_enum(value, self->hscroll_policy);
break;
case PROP_VSCROLL_POLICY:
g_value_set_enum(value, self->vscroll_policy);
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_FIXATE:
if (self->fixate != g_value_get_boolean(value))
fiv_view_command(self, FIV_VIEW_COMMAND_TOGGLE_FIXATE);
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;
case PROP_HADJUSTMENT:
if (replace_adjustment(
self, &self->hadjustment, g_value_get_object(value)))
g_object_notify_by_pspec(object, pspec);
break;
case PROP_VADJUSTMENT:
if (replace_adjustment(
self, &self->vadjustment, g_value_get_object(value)))
g_object_notify_by_pspec(object, pspec);
break;
case PROP_HSCROLL_POLICY:
if ((gint) self->hscroll_policy != g_value_get_enum(value)) {
self->hscroll_policy = g_value_get_enum(value);
gtk_widget_queue_resize(GTK_WIDGET(self));
g_object_notify_by_pspec(object, pspec);
}
break;
case PROP_VSCROLL_POLICY:
if ((gint) self->vscroll_policy != g_value_get_enum(value)) {
self->vscroll_policy = g_value_get_enum(value);
gtk_widget_queue_resize(GTK_WIDGET(self));
g_object_notify_by_pspec(object, pspec);
}
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
}
}
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 = self->page->render))
return;
// TODO(p): Restart the animation. No vector formats currently animate.
g_return_if_fail(!self->frame_update_connection);
// Optimization, taking into account the workaround in set_scale().
if (!self->page_scaled &&
(self->scale == 1 || self->scale == 0.999999999999999))
return;
// If it fails, the previous frame pointer may become invalid.
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
self->frame = self->page_scaled = closure->render(closure,
self->enable_cms ? fiv_io_cmm_get_default() : NULL,
self->enable_cms ? self->screen_cms_profile : NULL, self->scale);
if (!self->page_scaled)
self->frame = self->page;
}
static void
set_source_image(FivView *self, cairo_t *cr)
{
cairo_surface_t *surface = fiv_io_image_to_surface_noref(self->frame);
cairo_set_source_surface(cr, surface, 0, 0);
cairo_surface_destroy(surface);
}
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)
goto out;
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);
}
out:
update_adjustments(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 FivIoProfile *
monitor_cms_profile(GdkWindow *root, int num)
{
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;
FivIoProfile *result = NULL;
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)
result = fiv_io_cmm_get_profile(
fiv_io_cmm_get_default(), data, length);
g_free(data);
}
return result;
}
static void
reload_screen_cms_profile(FivView *self, GdkWindow *window)
{
g_clear_pointer(&self->screen_cms_profile, fiv_io_profile_free);
#ifdef GDK_WINDOWING_WIN32
if (GDK_IS_WIN32_WINDOW(window)) {
HWND hwnd = GDK_WINDOW_HWND(window);
HDC hdc = GetDC(hwnd);
if (hdc) {
DWORD len = 0;
(void) GetICMProfile(hdc, &len, NULL);
gchar *path = g_new(gchar, len);
if (GetICMProfile(hdc, &len, path)) {
gchar *data = NULL;
gsize length = 0;
if (g_file_get_contents(path, &data, &length, NULL))
self->screen_cms_profile = fiv_io_cmm_get_profile(
fiv_io_cmm_get_default(), data, length);
g_free(data);
}
g_free(path);
ReleaseDC(hwnd, hdc);
}
goto out;
}
#endif // GDK_WINDOWING_WIN32
GdkDisplay *display = gdk_window_get_display(window);
GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window);
GdkWindow *root = gdk_screen_get_root_window(gdk_window_get_screen(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;
// Cater to xiccd limitations (agalakhov/xiccd#33).
if (!(self->screen_cms_profile = monitor_cms_profile(root, num)) && num)
self->screen_cms_profile = monitor_cms_profile(root, 0);
out:
if (!self->screen_cms_profile)
self->screen_cms_profile =
fiv_io_cmm_get_profile_sRGB(fiv_io_cmm_get_default());
}
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 GtkGestureDrag.
.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);
GSettings *settings = g_settings_new(PROJECT_NS PROJECT_NAME);
gboolean opengl = g_settings_get_boolean(settings, "opengl");
// 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
// Note that this disables double buffering, and sometimes causes artefacts,
// see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
//
// GTK+'s OpenGL integration is terrible, so we may need to use
// the X11 subwindow directly, sidestepping the toolkit entirely.
if (GDK_IS_X11_WINDOW(window) &&
g_settings_get_boolean(settings, "native-view-window"))
gdk_window_ensure_native(window);
#endif // GDK_WINDOWING_X11
g_object_unref(settings);
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);
FivView *self = FIV_VIEW(widget);
g_clear_object(&self->gl_context);
if (!opengl)
return;
GError *error = NULL;
GdkGLContext *gl_context = gdk_window_create_gl_context(window, &error);
if (!gl_context) {
g_warning("GL: %s", error->message);
g_error_free(error);
return;
}
gdk_gl_context_set_use_es(gl_context, FALSE);
gdk_gl_context_set_required_version(gl_context, 3, 3);
gdk_gl_context_set_debug_enabled(gl_context, TRUE);
if (!gdk_gl_context_realize(gl_context, &error)) {
g_warning("GL: %s", error->message);
g_error_free(error);
g_object_unref(gl_context);
return;
}
self->gl_context = gl_context;
}
static void GLAPIENTRY
gl_on_message(G_GNUC_UNUSED GLenum source, GLenum type, G_GNUC_UNUSED GLuint id,
G_GNUC_UNUSED GLenum severity, G_GNUC_UNUSED GLsizei length,
const GLchar *message, G_GNUC_UNUSED const void *user_data)
{
if (type == GL_DEBUG_TYPE_ERROR)
g_warning("GL: error: %s", message);
else
g_debug("GL: %s", message);
}
static void
fiv_view_unrealize(GtkWidget *widget)
{
FivView *self = FIV_VIEW(widget);
if (self->gl_context) {
if (self->gl_initialized) {
gdk_gl_context_make_current(self->gl_context);
glDeleteProgram(self->gl_program);
}
if (self->gl_context == gdk_gl_context_get_current())
gdk_gl_context_clear_current();
g_clear_object(&self->gl_context);
}
GTK_WIDGET_CLASS(fiv_view_parent_class)->unrealize(widget);
}
static bool
gl_draw(FivView *self, cairo_t *cr)
{
gdk_gl_context_make_current(self->gl_context);
if (!self->gl_initialized) {
GLuint program = gl_make_program();
if (!program)
return false;
glDisable(GL_SCISSOR_TEST);
glDisable(GL_STENCIL_TEST);
glDisable(GL_DEPTH_TEST);
glDisable(GL_CULL_FACE);
glDisable(GL_BLEND);
if (epoxy_has_gl_extension("GL_ARB_debug_output")) {
glEnable(GL_DEBUG_OUTPUT);
glDebugMessageCallback(gl_on_message, NULL);
}
self->gl_program = program;
self->gl_initialized = true;
}
// This limit is always less than that of Cairo/pixman,
// and we'd have to figure out tiling.
GLint max = 0;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max);
if (max < (GLint) self->frame->width ||
max < (GLint) self->frame->height) {
g_warning("OpenGL max. texture size is too small");
return false;
}
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
int dw = 0, dh = 0, dx = 0, dy = 0;
get_display_dimensions(self, &dw, &dh);
int clipw = dw, cliph = dh;
double x1 = 0., y1 = 0., x2 = 1., y2 = 1.;
if (self->hadjustment)
x1 = floor(gtk_adjustment_get_value(self->hadjustment)) / dw;
if (self->vadjustment)
y1 = floor(gtk_adjustment_get_value(self->vadjustment)) / dh;
if (dw <= allocation.width) {
dx = round((allocation.width - dw) / 2.);
} else {
x2 = x1 + (double) allocation.width / dw;
clipw = allocation.width;
}
if (dh <= allocation.height) {
dy = round((allocation.height - dh) / 2.);
} else {
y2 = y1 + (double) allocation.height / dh;
cliph = allocation.height;
}
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self));
clipw *= scale;
cliph *= scale;
enum { SRC, DEST };
GLuint textures[2] = {};
glGenTextures(2, textures);
// https://stackoverflow.com/questions/25157306 0..1
// GL_TEXTURE_RECTANGLE seems kind-of useful
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
if (self->filter) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
} else {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
}
// GL_UNPACK_ALIGNMENT is initially 4, which is fine for these.
// Texture swizzling is OpenGL 3.3.
if (self->frame->format == CAIRO_FORMAT_ARGB32) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
} else if (self->frame->format == CAIRO_FORMAT_RGB24) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self->frame->data);
} else if (self->frame->format == CAIRO_FORMAT_RGB30) {
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_SWIZZLE_A, GL_ONE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
self->frame->width, self->frame->height,
0, GL_BGRA, GL_UNSIGNED_INT_2_10_10_10_REV, self->frame->data);
} else {
g_warning("GL: unsupported bitmap format");
}
// GtkGLArea creates textures like this.
glBindTexture(GL_TEXTURE_2D, textures[DEST]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, clipw, cliph, 0, GL_BGRA,
GL_UNSIGNED_BYTE, NULL);
glViewport(0, 0, clipw, cliph);
GLuint vao = 0;
glGenVertexArrays(1, &vao);
GLuint frame_buffer = 0;
glGenFramebuffers(1, &frame_buffer);
glBindFramebuffer(GL_FRAMEBUFFER, frame_buffer);
glFramebufferTexture2D(
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, textures[DEST], 0);
glClearColor(0., 0., 0., 1.);
glClear(GL_COLOR_BUFFER_BIT);
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (status != GL_FRAMEBUFFER_COMPLETE)
g_warning("GL framebuffer status: %u", status);
glUseProgram(self->gl_program);
GLint position_location = glGetAttribLocation(
self->gl_program, "position");
GLint picture_location = glGetUniformLocation(
self->gl_program, "picture");
GLint checkerboard_location = glGetUniformLocation(
self->gl_program, "checkerboard");
glUniform1i(picture_location, 0);
glUniform1i(checkerboard_location, self->checkerboard);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[SRC]);
// Note that the Y axis is flipped in the table.
double vertices[][4] = {
{-1., -1., x1, y2},
{+1., -1., x2, y2},
{+1., +1., x2, y1},
{-1., +1., x1, y1},
};
cairo_matrix_t matrix = fiv_io_orientation_matrix(self->orientation, 1, 1);
cairo_matrix_transform_point(&matrix, &vertices[0][2], &vertices[0][3]);
cairo_matrix_transform_point(&matrix, &vertices[1][2], &vertices[1][3]);
cairo_matrix_transform_point(&matrix, &vertices[2][2], &vertices[2][3]);
cairo_matrix_transform_point(&matrix, &vertices[3][2], &vertices[3][3]);
GLuint vertex_buffer = 0;
glGenBuffers(1, &vertex_buffer);
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
glBindVertexArray(vao);
glVertexAttribPointer(position_location,
G_N_ELEMENTS(vertices[0]), GL_DOUBLE, GL_FALSE, sizeof vertices[0], 0);
glEnableVertexAttribArray(position_location);
glDrawArrays(GL_TRIANGLE_FAN, 0, G_N_ELEMENTS(vertices));
glDisableVertexAttribArray(position_location);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glUseProgram(0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// XXX: Native GdkWindows send this to the software fallback path.
// XXX: This only reliably alpha blends when using the software fallback,
// such as with a native window, because 7237f5d in GTK+ 3 is a regression.
// (Introduced in 3.24.39, reverted in 3.24.42.)
//
// We had to resort to rendering the checkerboard pattern in the shader.
// Unfortunately, it is hard to retrieve the theme colours from CSS.
GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(self));
cairo_translate(cr, dx, dy);
gdk_cairo_draw_from_gl(
cr, window, textures[DEST], GL_TEXTURE, scale, 0, 0, clipw, cliph);
gdk_gl_context_make_current(self->gl_context);
glDeleteBuffers(1, &vertex_buffer);
glDeleteTextures(2, textures);
glDeleteVertexArrays(1, &vao);
glDeleteFramebuffers(1, &frame_buffer);
// TODO(p): Possibly use this clue as a hint to use Cairo rendering.
GLenum err = 0;
while ((err = glGetError()) != GL_NO_ERROR) {
const char *string = gl_error_string(err);
if (string)
g_warning("GL: error: %s", string);
else
g_warning("GL: error: %u", err);
}
gdk_gl_context_clear_current();
return true;
}
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;
if (self->gl_context && gl_draw(self, cr))
return TRUE;
int dw = 0, dh = 0;
get_display_dimensions(self, &dw, &dh);
double x = 0;
double y = 0;
if (self->hadjustment)
x = -floor(gtk_adjustment_get_value(self->hadjustment));
if (self->vadjustment)
y = -floor(gtk_adjustment_get_value(self->vadjustment));
if (dw < allocation.width)
x = round((allocation.width - dw) / 2.);
if (dh < allocation.height)
y = round((allocation.height - dh) / 2.);
// XXX: This naming is confusing, because it isn't actually for the surface,
// but rather for our possibly rotated rendition of it.
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, dw, dh);
gtk_style_context_restore(style);
}
// Then all frames are pre-scaled.
if (self->page_scaled) {
set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr);
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, dw, dh);
cairo_clip(cr);
cairo_scale(cr, self->scale, self->scale);
set_source_image(self, cr);
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)
{
if (GTK_WIDGET_CLASS(fiv_view_parent_class)
->button_press_event(widget, event))
return GDK_EVENT_STOP;
if (event->button == GDK_BUTTON_PRIMARY &&
gtk_widget_get_focus_on_click(widget))
gtk_widget_grab_focus(widget);
return GDK_EVENT_PROPAGATE;
}
#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) {
if ((self->scale_to_fit = scale_to_fit)) {
self->fixate = false;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_FIXATE]);
}
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_SCALE_TO_FIT]);
gtk_widget_queue_resize(GTK_WIDGET(self));
}
return TRUE;
}
static void
widget_to_surface(FivView *self, double *x, double *y)
{
int dw, dh;
get_display_dimensions(self, &dw, &dh);
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
// Unneeded, thus unimplemented: this means zero adjustment values.
if (!self->hadjustment || !self->vadjustment)
return;
*x = (*x + (dw < allocation.width
? -round((allocation.width - dw) / 2.)
: +floor(gtk_adjustment_get_value(self->hadjustment))))
/ self->scale;
*y = (*y + (dh < allocation.height
? -round((allocation.height - dh) / 2.)
: +floor(gtk_adjustment_get_value(self->vadjustment))))
/ self->scale;
}
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;
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]);
prescale_page(self);
// Similar to set_orientation().
if (self->hadjustment && self->vadjustment) {
Dimensions surface_dimensions = get_surface_dimensions(self);
update_adjustments(self);
if (surface_dimensions.width * self->scale > allocation.width)
gtk_adjustment_set_value(
self->hadjustment, surface_x * self->scale - focus_x);
if (surface_dimensions.height * self->scale > allocation.height)
gtk_adjustment_set_value(
self->vadjustment, surface_y * self->scale - focus_y);
}
gtk_widget_queue_resize(GTK_WIDGET(self));
out:
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(self));
if (ceil(w * self->scale) > allocated)
set_scale(self, allocated / w, NULL);
}
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(self));
if (ceil(h * self->scale) > allocated)
set_scale(self, allocated / h, NULL);
}
static gboolean
fiv_view_scroll_event(GtkWidget *widget, GdkEventScroll *event)
{
FivView *self = FIV_VIEW(widget);
if (!self->image)
return GDK_EVENT_PROPAGATE;
if (event->state & gtk_accelerator_get_default_mod_mask())
return GDK_EVENT_PROPAGATE;
switch (event->direction) {
case GDK_SCROLL_UP:
return set_scale(self, self->scale * SCALE_STEP, (GdkEvent *) event);
case GDK_SCROLL_DOWN:
return set_scale(self, self->scale / SCALE_STEP, (GdkEvent *) event);
default:
// For some reason, native GdkWindows may also get GDK_SCROLL_SMOOTH.
// Left/right are good to steal from GtkScrolledWindow for consistency.
return GDK_EVENT_STOP;
}
}
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;
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
}
static gboolean
advance_frame(FivView *self)
{
FivIoImage *next = self->frame->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.
int64_t duration = self->frame->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 || !self->page->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);
// Only restart looping the animation if it has stopped at the end.
if (!self->remaining_loops) {
self->remaining_loops = self->page->loops;
if (self->remaining_loops && !self->frame->frame_next) {
self->frame = self->page;
gtk_widget_queue_draw(GTK_WIDGET(self));
}
}
gdk_frame_clock_begin_updating(clock);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_PLAYING]);
}
static void
switch_page(FivView *self, FivIoImage *page)
{
g_clear_pointer(&self->page_scaled, fiv_io_image_unref);
self->frame = self->page = page;
// XXX: When self->scale_to_fit is in effect,
// this uses an old value that may no longer be appropriate,
// resulting in wasted effort.
prescale_page(self);
if (!self->page ||
(self->orientation = self->page->orientation) ==
FivIoOrientationUnknown)
self->orientation = FivIoOrientation0;
self->remaining_loops = 0;
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
on_drag_begin(GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x,
G_GNUC_UNUSED gdouble start_y, gpointer user_data)
{
GtkGesture *gesture = GTK_GESTURE(drag);
switch (
gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture))) {
case GDK_BUTTON_PRIMARY:
case GDK_BUTTON_MIDDLE:
break;
default:
gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
GdkModifierType state = 0;
GdkEventSequence *sequence = gtk_gesture_get_last_updated_sequence(gesture);
gdk_event_get_state(gtk_gesture_get_last_event(gesture, sequence), &state);
if (state & gtk_accelerator_get_default_mod_mask()) {
gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
return;
}
// Since we set this up as a pointer-only gesture, there is only the NULL
// sequence, so gtk_gesture_set_sequence_state() is completely unneeded.
gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_CLAIMED);
GdkWindow *window = gtk_widget_get_window(
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(drag)));
GdkCursor *cursor =
gdk_cursor_new_from_name(gdk_window_get_display(window), "grabbing");
gdk_window_set_cursor(window, cursor);
g_object_unref(cursor);
FivView *self = FIV_VIEW(user_data);
self->drag_start[0] = self->hadjustment ?
gtk_adjustment_get_value(self->hadjustment) : 0;
self->drag_start[1] = self->vadjustment ?
gtk_adjustment_get_value(self->vadjustment) : 0;
}
static void
on_drag_update(G_GNUC_UNUSED GtkGestureDrag *drag, gdouble offset_x,
gdouble offset_y, gpointer user_data)
{
FivView *self = FIV_VIEW(user_data);
if (self->hadjustment) {
gtk_adjustment_set_value(
self->hadjustment, self->drag_start[0] - offset_x);
}
if (self->vadjustment) {
gtk_adjustment_set_value(
self->vadjustment, self->drag_start[1] - offset_y);
}
}
static void
on_drag_end(GtkGestureDrag *drag, G_GNUC_UNUSED gdouble start_x,
G_GNUC_UNUSED gdouble start_y, G_GNUC_UNUSED gpointer user_data)
{
GdkWindow *window = gtk_widget_get_window(
gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(drag)));
gdk_window_set_cursor(window, NULL);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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;
}
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)
{
double fractional_width = 0, fractional_height = 0;
cairo_matrix_t matrix = fiv_io_orientation_apply(
self->frame, self->orientation, &fractional_width, &fractional_height);
int w = ceil(fractional_width), h = ceil(fractional_height);
// XXX: SVG is rendered pre-scaled.
cairo_surface_t *transformed =
cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
cairo_t *cr = cairo_create(transformed);
set_source_image(self, cr);
cairo_pattern_set_matrix(cairo_get_source(cr), &matrix);
cairo_paint(cr);
cairo_destroy(cr);
// TODO(p): Use 16-bit PNGs for >8-bit Cairo surfaces: PNG-encode them
// ourselves and fall back to gtk_selection_data_set_pixbuf().
GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(transformed, 0, 0, w, h);
cairo_surface_destroy(transformed);
gtk_clipboard_set_image(
gtk_clipboard_get_for_display(
gtk_widget_get_display(GTK_WIDGET(self)), GDK_SELECTION_CLIPBOARD),
pixbuf);
g_object_unref(pixbuf);
}
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 = {};
// XXX: Perhaps use self->frame, even though their sizes should match.
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);
set_source_image(self, cr);
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, FivIoImage *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);
GFileInfo *info =
g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
G_FILE_QUERY_INFO_NONE, NULL, NULL);
// 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.
const char *basename = info ? g_file_info_get_display_name(info) : "image";
gchar *name = g_strconcat(basename, frame ? "-frame.webp" : ".webp", NULL);
gtk_file_chooser_set_current_name(chooser, name);
g_free(name);
if (g_file_peek_path(file)) {
GFile *parent = g_file_get_parent(file);
(void) gtk_file_chooser_set_current_folder_file(chooser, parent, NULL);
g_object_unref(parent);
}
g_object_unref(file);
g_clear_object(&info);
// 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 void
info(FivView *self)
{
fiv_context_menu_information(get_toplevel(GTK_WIDGET(self)), self->uri);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
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, NULL);
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_FIXATE] = g_param_spec_boolean(
"fixate", "Fixate", "Keep zoom and position",
FALSE, 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);
g_object_class_override_property(
object_class, PROP_HADJUSTMENT, "hadjustment");
g_object_class_override_property(
object_class, PROP_VADJUSTMENT, "vadjustment");
g_object_class_override_property(
object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
g_object_class_override_property(
object_class, PROP_VSCROLL_POLICY, "vscroll-policy");
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->unrealize = fiv_view_unrealize;
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;
// _gtk_get_primary_accel_mod() is private.
GdkModifierType primary = GDK_CONTROL_MASK;
gtk_accelerator_parse_with_keycode("<Primary>", NULL, NULL, &primary);
GtkBindingSet *bs = gtk_binding_set_by_class(klass);
// First, the standard, intuitive bindings.
bind(bs, GDK_KEY_0, primary, FIV_VIEW_COMMAND_ZOOM_1);
bind(bs, GDK_KEY_plus, primary, FIV_VIEW_COMMAND_ZOOM_IN);
bind(bs, GDK_KEY_minus, primary, FIV_VIEW_COMMAND_ZOOM_OUT);
bind(bs, GDK_KEY_c, primary, FIV_VIEW_COMMAND_COPY);
bind(bs, GDK_KEY_p, primary, FIV_VIEW_COMMAND_PRINT);
bind(bs, GDK_KEY_r, primary, FIV_VIEW_COMMAND_RELOAD);
bind(bs, GDK_KEY_s, primary, 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_k, 0, FIV_VIEW_COMMAND_TOGGLE_FIXATE);
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;
GtkGesture *drag = gtk_gesture_drag_new(GTK_WIDGET(self));
gtk_event_controller_set_propagation_phase(
GTK_EVENT_CONTROLLER(drag), GTK_PHASE_BUBBLE);
g_object_set_data_full(
G_OBJECT(self), "fiv-view-drag-gesture", drag, g_object_unref);
// GtkScrolledWindow's internal GtkGestureDrag is set to only look for
// touch events (and its "event_controllers" are perfectly private,
// so we can't change this), hopefully this is mutually exclusive with that.
// Though note that the GdkWindow doesn't register for touch events now.
gtk_gesture_single_set_exclusive(GTK_GESTURE_SINGLE(drag), TRUE);
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), 0);
g_signal_connect(drag, "drag-begin",
G_CALLBACK(on_drag_begin), self);
g_signal_connect(drag, "drag-update",
G_CALLBACK(on_drag_update), self);
g_signal_connect(drag, "drag-end",
G_CALLBACK(on_drag_end), self);
}
// --- Public interface --------------------------------------------------------
static FivIoImage *
open_without_swapping_in(FivView *self, const char *uri)
{
FivIoOpenContext ctx = {
.uri = uri,
.cmm = self->enable_cms ? fiv_io_cmm_get_default() : NULL,
.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;
FivIoImage *image = 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 image;
}
// 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, fiv_io_image_unref);
if (self->enhance) {
self->enhance = FALSE;
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_ENHANCE]);
}
FivIoImage *image = open_without_swapping_in(self, uri);
g_clear_pointer(&self->image, fiv_io_image_unref);
self->frame = self->page = NULL;
self->image = image;
switch_page(self, self->image);
// Otherwise, adjustment values and zoom are retained implicitly.
if (!self->fixate)
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 image != NULL;
}
static void
page_step(FivView *self, int step)
{
FivIoImage *page = step < 0
? self->page->page_previous
: self->page->page_next;
if (page)
switch_page(self, page);
}
static void
frame_step(FivView *self, int step)
{
stop_animating(self);
if (step > 0) {
// Decrease the loop counter as if running on a timer.
(void) advance_frame(self);
} else if (!step || !(self->frame = self->frame->frame_previous)) {
self->frame = self->page;
self->remaining_loops = 0;
}
gtk_widget_queue_draw(GTK_WIDGET(self));
}
static gboolean
reload(FivView *self)
{
FivIoImage *image = open_without_swapping_in(self, self->uri);
g_object_notify_by_pspec(G_OBJECT(self), view_properties[PROP_MESSAGES]);
if (!image)
return FALSE;
g_clear_pointer(&self->image, fiv_io_image_unref);
g_clear_pointer(&self->enhance_swap, fiv_io_image_unref);
switch_page(self, (self->image = image));
return TRUE;
}
static void
swap_enhanced_image(FivView *self)
{
FivIoImage *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));
}
}
static void
transformed_to_real(FivView *self, double *x, double *y)
{
double sw = 0, sh = 0;
cairo_matrix_t matrix =
fiv_io_orientation_apply(self->page, self->orientation, &sw, &sh);
cairo_matrix_transform_point(&matrix, x, y);
}
static void
set_orientation(FivView *self, FivIoOrientation orientation)
{
GtkAllocation allocation;
gtk_widget_get_allocation(GTK_WIDGET(self), &allocation);
// In the future, rotating gestures can pick another centre point.
double focus_x = 0.5 * allocation.width;
double focus_y = 0.5 * allocation.height;
double surface_x = focus_x;
double surface_y = focus_y;
widget_to_surface(self, &surface_x, &surface_y);
transformed_to_real(self, &surface_x, &surface_y);
self->orientation = orientation;
// Similar to set_scale().
Dimensions surface_dimensions = {};
cairo_matrix_t matrix =
fiv_io_orientation_apply(self->page, self->orientation,
&surface_dimensions.width, &surface_dimensions.height);
if (self->hadjustment && self->vadjustment &&
cairo_matrix_invert(&matrix) == CAIRO_STATUS_SUCCESS) {
cairo_matrix_transform_point(&matrix, &surface_x, &surface_y);
update_adjustments(self);
if (surface_dimensions.width * self->scale > allocation.width)
gtk_adjustment_set_value(
self->hadjustment, surface_x * self->scale - focus_x);
if (surface_dimensions.height * self->scale > allocation.height)
gtk_adjustment_set_value(
self->vadjustment, surface_y * self->scale - focus_y);
}
gtk_widget_queue_resize(GTK_WIDGET(self));
}
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:
set_orientation(self, view_left[self->orientation]);
break; case FIV_VIEW_COMMAND_MIRROR:
set_orientation(self, view_mirror[self->orientation]);
break; case FIV_VIEW_COMMAND_ROTATE_RIGHT:
set_orientation(self, view_right[self->orientation]);
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 (FivIoImage *I = self->page; (I = I->page_next); )
self->page = I;
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_COPY:
copy(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, NULL);
break; case FIV_VIEW_COMMAND_ZOOM_OUT:
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:
set_scale_to_fit_height(self);
break; case FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT:
set_scale_to_fit(self, !self->scale_to_fit);
break; case FIV_VIEW_COMMAND_TOGGLE_FIXATE:
if ((self->fixate = !self->fixate))
set_scale_to_fit(self, false);
g_object_notify_by_pspec(
G_OBJECT(self), view_properties[PROP_FIXATE]);
}
}