GIMP does as well, and it provides better information. It's not as if anyone is really going to use the spin button features which behave weird with this, and it indicates that decimal numbers may be written at all.
2167 lines
69 KiB
C
2167 lines
69 KiB
C
//
|
||
// 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., 2);
|
||
gtk_spin_button_set_update_policy(
|
||
GTK_SPIN_BUTTON(spin), GTK_UPDATE_IF_VALID);
|
||
gtk_entry_set_activates_default(GTK_ENTRY(spin), TRUE);
|
||
|
||
GtkWidget *zoom_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||
GtkWidget *zoom_label = gtk_label_new_with_mnemonic("_Zoom:");
|
||
gtk_label_set_mnemonic_widget(GTK_LABEL(zoom_label), spin);
|
||
gtk_box_pack_start(GTK_BOX(zoom_box), zoom_label, FALSE, FALSE, 0);
|
||
gtk_box_pack_start(GTK_BOX(zoom_box), spin, TRUE, TRUE, 0);
|
||
gtk_box_pack_start(GTK_BOX(zoom_box), gtk_label_new("%"), FALSE, FALSE, 0);
|
||
|
||
GtkWidget *result_left = gtk_label_new(NULL);
|
||
GtkWidget *result_middle = gtk_label_new("×");
|
||
GtkWidget *result_right = gtk_label_new(NULL);
|
||
gtk_label_set_xalign(GTK_LABEL(result_left), 1.);
|
||
gtk_label_set_xalign(GTK_LABEL(result_right), 0.);
|
||
|
||
GtkSizeGroup *group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
|
||
gtk_size_group_add_widget(group, original_left);
|
||
gtk_size_group_add_widget(group, original_right);
|
||
gtk_size_group_add_widget(group, result_left);
|
||
gtk_size_group_add_widget(group, result_right);
|
||
|
||
GtkWidget *result_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||
gtk_box_pack_start(GTK_BOX(result_box), result_left, TRUE, TRUE, 0);
|
||
gtk_box_pack_start(GTK_BOX(result_box), result_middle, FALSE, FALSE, 0);
|
||
gtk_box_pack_start(GTK_BOX(result_box), result_right, TRUE, TRUE, 0);
|
||
|
||
struct zoom_ask_context data = { result_left, result_right, dimensions };
|
||
g_signal_connect(spin, "changed",
|
||
G_CALLBACK(on_zoom_ask_spin_changed), &data);
|
||
on_zoom_ask_spin_changed(GTK_SPIN_BUTTON(spin), &data);
|
||
|
||
GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
|
||
g_object_set(content, "margin", 12, NULL);
|
||
gtk_box_set_spacing(GTK_BOX(content), 6);
|
||
gtk_container_add(GTK_CONTAINER(content), original_box);
|
||
gtk_container_add(GTK_CONTAINER(content), zoom_box);
|
||
gtk_container_add(GTK_CONTAINER(content), result_box);
|
||
gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
|
||
gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
|
||
gtk_widget_show_all(dialog);
|
||
|
||
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
|
||
double value = gtk_spin_button_get_value(GTK_SPIN_BUTTON(spin));
|
||
if (value > 0)
|
||
set_scale(self, value / 100., NULL);
|
||
}
|
||
gtk_widget_destroy(dialog);
|
||
g_object_unref(group);
|
||
}
|
||
|
||
static void
|
||
copy(FivView *self)
|
||
{
|
||
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]);
|
||
}
|
||
}
|