aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile17
-rw-r--r--TODO50
-rw-r--r--common.h197
-rw-r--r--dragging.c135
-rw-r--r--dropping.c120
-rw-r--r--events.c343
-rw-r--r--files.c119
-rw-r--r--filters.c173
-rw-r--r--gbild.c1600
-rw-r--r--image.c35
-rw-r--r--logical-scroll-pane.c430
-rw-r--r--logical-scroll-pane.h55
-rw-r--r--opening.c173
-rw-r--r--printing.c761
-rw-r--r--properties.c28
-rw-r--r--transform.c172
-rw-r--r--ui.c307
-rw-r--r--zoom.c82
18 files changed, 3426 insertions, 1371 deletions
diff --git a/Makefile b/Makefile
index 6fc5daa..11f1356 100644
--- a/Makefile
+++ b/Makefile
@@ -4,9 +4,24 @@ CONFIGFILE = config.mk
include $(CONFIGFILE)
OBJ =\
+ dragging.o\
+ dropping.o\
+ events.o\
+ files.o\
+ filters.o\
+ image.o\
+ opening.o\
+ printing.o\
+ properties.o\
+ transform.o\
+ ui.o\
+ zoom.o\
+ logical-scroll-pane.o\
gbild.o
-HDR =
+HDR =\
+ common.h\
+ logical-scroll-pane.h
all: gbild
$(OBJ): $(HDR)
diff --git a/TODO b/TODO
index 046e02b..829d95e 100644
--- a/TODO
+++ b/TODO
@@ -1,23 +1,33 @@
-BUG) Dragging the image with Control held down, or using the middle button, shall scroll the display area
+Add alignment buttons to the print dialogue.
-BUG) It shall be possible to drag and drop (started without any modifier) the image
- (the file itself is being dragged, not a copy of the image) to another application.
+Some images may have a physical dimension specified.
+This should at least be honoured during print, maybe even on display.
-LATER) Optional (opt-in) side-pane ("_Property pane") with image properties (accelerator: F9)
-LATER) Optional (opt-in) bottom-pane ("Image _collection") with image thumbnails (accelerator: Control+F9)
- The bottom-pane shall support:
- * drag and drop to reorder
- * drag to another application to drop the file by path there
- * drop form another application to add the image to the collection
- * right-clicking on a thumbnail and "_Remove from collection" (icon: edit-delete)
+Optional (opt-in) side-pane ("_Property pane") with image properties (accelerator: F9)
-LATER) Automatically go to next picture
-LATER) What file types shall be supported
-LATER) Animation support
-LATER) Preferences for transparency
-LATER) Preferences for changling GDK_INTERP_BILINEAR
-LATER) Fix "GFileInfo created without standard::is-hidden" error message
-LATER) Fix "GFileInfo created without standard::is-backup" error message
-LATER) Add support for farbfeld
-LATER) Add support for lss16
-LATER) Add support for pam (portable anymap)
+Optional (opt-in) bottom-pane ("Image _collection") with image thumbnails (accelerator: Control+F9)
+ The bottom-pane shall support:
+ * drag and drop to reorder
+ * drag to another application to drop the file by path there
+ * drop form another application to add the image to the collection
+ * right-clicking on a thumbnail and "_Remove from collection" (icon: edit-delete)
+
+Automatically go to next picture
+
+Animation support
+
+Preferences for transparency
+
+Preferences for changling GDK_INTERP_BILINEAR
+
+Fix "GFileInfo created without standard::is-hidden" error message
+
+Fix "GFileInfo created without standard::is-backup" error message
+
+Add support for farbfeld
+
+Add support for lss16
+
+Add support for pam (portable anymap)
+
+Colour space support
diff --git a/common.h b/common.h
new file mode 100644
index 0000000..aaf4b09
--- /dev/null
+++ b/common.h
@@ -0,0 +1,197 @@
+/* See LICENSE file for copyright and license details. */
+#ifndef SCROLL_AMOUNT
+# define SCROLL_AMOUNT 30
+#endif
+#ifndef DRAG_ICON_SIZE
+# define DRAG_ICON_SIZE 128
+#endif
+#ifndef PREVIEW_WIDTH
+# define PREVIEW_WIDTH 192
+#endif
+#ifndef PREVIEW_HEIGHT
+# define PREVIEW_HEIGHT 256
+#endif
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
+# pragma GCC diagnostic ignored "-Wstrict-prototypes"
+#endif
+#include <gdk/gdkkeysyms.h>
+#include <gtk/gtk.h>
+#include <libsimple.h>
+#include <libsimple-arg.h>
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic ignored "-Wunsuffixed-float-constants"
+# pragma GCC diagnostic ignored "-Wsuggest-attribute=const"
+# pragma GCC diagnostic ignored "-Wsuggest-attribute=pure"
+#endif
+
+#if defined(__GNUC__)
+# define PURE __attribute__((__pure__))
+#else
+# define PURE
+#endif
+
+#include "logical-scroll-pane.h"
+
+
+#define FLOAT_EQ(A, B) ((A) >= (B) && (A) <= (B))
+
+#define COMMA ,
+
+
+enum toggle_group {
+ GROUP_NONE,
+ GROUP_ZOOM
+};
+
+enum zoom_mode {
+ ZOOM_CUSTOM,
+ ZOOM_NORMAL,
+ ZOOM_FIT_WIDTH,
+ ZOOM_FIT_HEIGHT,
+ ZOOM_FIT_WINDOW,
+ ZOOM_BEST_FIT
+};
+
+enum transformation {
+ TRANS_NONE, /* 1 */
+ TRANS_CLOCKWISE, /* r */
+ TRANS_180_DEGREES, /* r² */
+ TRANS_ANTICLOCKWISE, /* r³ */
+ TRANS_TRANSPOSE, /* t */
+ TRANS_FLOP, /* rt */
+ TRANS_ANTITRANSPOSE, /* r²t */
+ TRANS_FLIP, /* r³t */
+ /*
+ * t² = r⁴ = 1
+ * trⁿ = r⁻ⁿt
+ */
+ TRANS_NEGATIVE = 0x08
+};
+
+struct file {
+ char *path;
+ off_t size;
+ GdkPixbuf *image;
+};
+
+typedef double Zoom; /* TODO */
+
+
+/* dragging.c */
+void set_up_dragging(void);
+
+/* dropping.c */
+void set_up_dropping(void);
+
+/* events.c */
+void open_action(GtkAction *action, void *user);
+void reload_action(GtkAction *action, void *user);
+void print_action(GtkAction *action, void *user);
+void properties_action(GtkAction *action, void *user);
+void close_action(GtkAction *action, void *user);
+void fullscreen_action(GtkToggleAction *action, void *user);
+void reset_transformations_action(GtkAction *action, void *user);
+void rotate_clockwise_action(GtkAction *action, void *user);
+void rotate_180_degrees_action(GtkAction *action, void *user);
+void rotate_anticlockwise_action(GtkAction *action, void *user);
+void flop_action(GtkAction *action, void *user);
+void flip_action(GtkAction *action, void *user);
+void transpose_action(GtkAction *action, void *user);
+void antitranspose_action(GtkAction *action, void *user);
+void invert_action(GtkToggleAction *action, void *user);
+void zoom_in_action(GtkAction *action, void *user);
+void zoom_out_action(GtkAction *action, void *user);
+void normal_size_action(GtkToggleAction *action, void *user);
+void width_fit_action(GtkToggleAction *action, void *user);
+void height_fit_action(GtkToggleAction *action, void *user);
+void window_fit_action(GtkToggleAction *action, void *user);
+void best_fit_action(GtkToggleAction *action, void *user);
+void next_action(GtkAction *action, void *user);
+void previous_action(GtkAction *action, void *user);
+void first_action(GtkAction *action, void *user);
+void last_action(GtkAction *action, void *user);
+int key_pressed(GtkWidget *widget, GdkEventKey *event, void *user);
+int scroll_event(GtkWidget *widget, GdkEventScroll *event, void *user);
+
+/* files.c */
+extern struct file *files;
+extern size_t nfiles;
+extern size_t current_file;
+void free_files(void);
+size_t files_from_array(struct file *selected_files, size_t nselected_files);
+size_t files_from_list(struct file **files_out, GSList *selected_files);
+int set_files_from_argv(int argc, char **argv);
+struct file *get_file(void);
+int navigate_without_redraw(size_t jump, size_t move);
+
+/* filters.c */
+void add_image_filters(GtkFileChooser *chooser);
+void free_image_filters(void);
+
+/* image.c */
+extern GdkPixbuf *transformed_image;
+GdkPixbuf *get_image(void);
+GdkPixbuf *get_original_image(void);
+
+/* opening.c */
+int load_image(struct file *file, int errors_to_stderr);
+GtkFileChooser *construct_chooser(void);
+
+/* printing.c */
+void open_print(GdkPixbuf *image);
+
+/* properties.c */
+void open_properties(struct file *file);
+
+/* transform.c */
+extern enum transformation trans;
+GdkPixbuf *apply_transformations(GdkPixbuf *image);
+enum transformation reset_transformations_without_redraw(void);
+int add_transformation_without_redraw(enum transformation augment);
+void transform_point(enum transformation t, double *x, double *y, double *w, double *h);
+
+/* ui.c */
+extern GtkWidget *window;
+extern GtkWidget *display_area;
+extern GtkWidget *scroll;
+extern GtkToggleAction *fullscreen;
+void set_up_ui(void);
+void set_actions_sensitive(void);
+void clear_toggles(enum toggle_group group, GtkToggleAction *except);
+void set_fullscreen_mode(int active);
+int activate_fullscreen(void *user);
+int deactivate_fullscreen(void *user);
+void set_statusbar_message(const char *message);
+void free_ui(void);
+
+/* zoom.c */
+extern Zoom zoom;
+extern enum zoom_mode zoom_mode;
+int zoom_eq(Zoom a, Zoom b);
+double get_zoom(Zoom z);
+Zoom make_zoom(double s);
+Zoom zoom_in(Zoom z);
+Zoom zoom_out(Zoom z);
+Zoom calculate_fit(GdkPixbuf *image, int xfit, int yfit, double cap);
+Zoom calculate_zoom_mode(GdkPixbuf *image, enum zoom_mode mode);
+
+/* gbild.c */
+int expose_image(GtkWidget *widget, GdkEventExpose *event, void *user);
+void add_images(struct file *new_files, size_t nnew_files);
+void set_images_from_array(struct file *selected_files, size_t nselected_files);
+void set_images_from_list(GSList *selected_files);
+void scroll_by(double horizontally, double vertically);
+void set_zoom_around_point(Zoom new_zoom, double x, double y);
+void set_zoom_around_centre(Zoom new_zoom);
+void set_zoom_and_recentre(Zoom new_zoom);
+void reload_redraw(void);
+void transform_redraw(enum transformation augment);
+void navigate_redraw(void);
+int recalculate_zoom_mode(void *user);
diff --git a/dragging.c b/dragging.c
new file mode 100644
index 0000000..3ac3ff5
--- /dev/null
+++ b/dragging.c
@@ -0,0 +1,135 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+static char url_list_name[] = "text/uri-list";
+static GtkTargetEntry url_list = {url_list_name, 0u, 0u};
+
+static int dragging = 0;
+static int drag_source_disabled = 0;
+static int drag_x, drag_y;
+
+static void
+set_file_drag_source(GtkWidget *widget)
+{
+ gtk_drag_source_set(widget, GDK_BUTTON1_MASK, &url_list, 1u,
+ GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK);
+}
+
+static int
+button_pressed(GtkWidget *widget, GdkEventButton *event, void *user)
+{
+ (void) user;
+ if (event->button == 2u || (event->state & GDK_CONTROL_MASK)) {
+ if (event->button == 1u) {
+ gtk_drag_source_unset(widget);
+ drag_source_disabled = 1;
+ }
+ dragging = 1;
+ drag_x = (int)event->x_root;
+ drag_y = (int)event->y_root;
+ }
+ return dragging;
+}
+
+static int
+button_released(GtkWidget *widget, GdkEventButton *event, void *user)
+{
+ (void) event;
+ (void) user;
+ dragging = 0;
+ if (drag_source_disabled) {
+ set_file_drag_source(widget);
+ drag_source_disabled = 0;
+ }
+ return 0;
+}
+
+static int
+pointer_moved(GtkWidget *widget, GdkEventMotion *event, void *user)
+{
+ int x, y;
+ (void) event;
+ (void) user;
+ if (dragging) {
+ gdk_display_get_pointer(gtk_widget_get_display(widget), NULL, &x, &y, NULL);
+ scroll_by(drag_x - x, drag_y - y);
+ drag_x = x;
+ drag_y = y;
+ }
+ return dragging;
+}
+
+static void
+drag_begin(GtkWidget *widget, GdkDragContext *context, void *user)
+{
+ GdkPixbuf *image, *icon;
+ intmax_t height, width;
+
+ (void) widget;
+ (void) user;
+
+ image = get_original_image();
+ if (!image)
+ return;
+
+ width = gdk_pixbuf_get_width(image);
+ height = gdk_pixbuf_get_height(image);
+ if (width <= DRAG_ICON_SIZE && height <= DRAG_ICON_SIZE) {
+ gtk_drag_set_icon_pixbuf(context, image, 0, 0);
+ return;
+ }
+ if (width >= height) {
+ height = height * DRAG_ICON_SIZE / width;
+ height = MAX(1, height);
+ width = DRAG_ICON_SIZE;
+ } else {
+ width = width * DRAG_ICON_SIZE / height;
+ width = MAX(1, width);
+ height = DRAG_ICON_SIZE;
+ }
+ icon = gdk_pixbuf_scale_simple(image, (int)width, (int)height, GDK_INTERP_BILINEAR);
+ if (!icon)
+ return;
+ gtk_drag_set_icon_pixbuf(context, icon, 0, 0);
+ g_object_unref(icon);
+}
+
+static void
+drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data,
+ unsigned int info, unsigned int time, void *user)
+{
+ struct file *file = get_file();
+ GError *error = NULL;
+ char *uri;
+
+ (void) widget;
+ (void) context;
+ (void) info;
+ (void) time;
+ (void) user;
+
+ if (!file || !file->path)
+ return;
+
+ uri = g_filename_to_uri(file->path, NULL, &error);
+ if (!uri) {
+ weprintf("g_filename_to_uri: %s", error->message);
+ g_error_free(error);
+ return;
+ }
+ gtk_selection_data_set_uris(data, (char *[]){uri, NULL});
+ g_free(uri);
+}
+
+void
+set_up_dragging(void)
+{
+ set_file_drag_source(display_area);
+
+ g_signal_connect(display_area, "button-press-event", G_CALLBACK(&button_pressed), NULL);
+ g_signal_connect(display_area, "button-release-event", G_CALLBACK(&button_released), NULL);
+ g_signal_connect(display_area, "motion-notify-event", G_CALLBACK(&pointer_moved), NULL);
+
+ g_signal_connect(display_area, "drag-begin", G_CALLBACK(&drag_begin), NULL);
+ g_signal_connect(display_area, "drag-data-get", G_CALLBACK(&drag_data_get), NULL);
+}
diff --git a/dropping.c b/dropping.c
new file mode 100644
index 0000000..412f0ab
--- /dev/null
+++ b/dropping.c
@@ -0,0 +1,120 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+static char url_list_name[] = "text/uri-list";
+static GtkTargetEntry url_list = {url_list_name, 0u, 0u};
+
+static size_t
+get_dropped_files(struct file **files_out, char **uris)
+{
+ GError *error = NULL;
+ char **uri, *path;
+ struct file *dropped_files = NULL;
+ size_t i = 0u;
+
+ if (!uris)
+ goto out;
+
+ for (uri = uris; *uri; uri++)
+ i += 1u;
+ if (!i)
+ goto out;
+ dropped_files = ecalloc(i, sizeof(*dropped_files));
+
+ for (uri = uris, i = 0u; *uri; uri++) {
+ error = NULL; /* g_filename_from_uri requires this initialisation */
+ path = g_filename_from_uri(*uri, NULL, &error);
+ if (!path) {
+ weprintf("g_filename_from_uri: %s", error->message);
+ g_error_free(error);
+ continue;
+ }
+ dropped_files[i++].path = estrdup(path);
+ g_free(path);
+ /* the other fields are set in the next code block */
+ }
+
+out:
+ *files_out = dropped_files;
+ return i;
+}
+
+static void
+drop_files(GtkWidget *widget, GdkDragContext *context, int x, int y,
+ GtkSelectionData *data, unsigned int info, unsigned int time, void *user)
+{
+ char **uris = NULL;
+ struct file *dropped_files;
+ size_t n;
+ int accepted = 1;
+ GdkModifierType modifiers;
+ GdkWindow *event_window;
+
+ (void) x;
+ (void) y;
+ (void) info;
+ (void) user;
+
+ if (gtk_drag_get_source_widget(context) == display_area) {
+ accepted = 0;
+ goto out;
+ }
+ event_window = gtk_widget_get_window(widget);
+ if (!event_window)
+ goto out;
+ if (!gdk_window_get_pointer(event_window, NULL, NULL, &modifiers))
+ goto out;
+
+ uris = gtk_selection_data_get_uris(data);
+ n = get_dropped_files(&dropped_files, uris);
+ if (!n)
+ goto out;
+
+ if ((modifiers & GDK_CONTROL_MASK) && nfiles) {
+ n = files_from_array(dropped_files, n);
+ add_images(dropped_files, n);
+ free(dropped_files);
+ } else {
+ set_images_from_array(dropped_files, n);
+ }
+
+out:
+ gtk_drag_finish(context, accepted, 0, time);
+ if (uris)
+ g_strfreev(uris);
+}
+
+static int
+drop_motion(GtkWidget *widget, GdkDragContext *context, int x, int y, unsigned int time, void *user)
+{
+ GdkWindow *event_window = gtk_widget_get_window(widget);
+ GdkModifierType modifiers;
+ GdkDragAction action = 0;
+
+ (void) x;
+ (void) y;
+ (void) user;
+
+ if (!gtk_get_current_event_state(&modifiers))
+ if (!event_window || !gdk_window_get_pointer(event_window, NULL, NULL, &modifiers))
+ goto out;
+ if (gtk_drag_dest_find_target(widget, context, NULL) == GDK_NONE)
+ goto out;
+
+ action = (modifiers & GDK_CONTROL_MASK) ? GDK_ACTION_COPY : GDK_ACTION_MOVE;
+ if (!(gdk_drag_context_get_actions(context) & action))
+ action = 0;
+
+out:
+ gdk_drag_status(context, action, time);
+ return 1;
+}
+
+void
+set_up_dropping(void)
+{
+ gtk_drag_dest_set(window, GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP,
+ &url_list, 1u, GDK_ACTION_COPY | GDK_ACTION_MOVE);
+ g_signal_connect(window, "drag-motion", G_CALLBACK(&drop_motion), NULL);
+ g_signal_connect(window, "drag-data-received", G_CALLBACK(&drop_files), NULL);
+}
diff --git a/events.c b/events.c
new file mode 100644
index 0000000..4c5910e
--- /dev/null
+++ b/events.c
@@ -0,0 +1,343 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+static void
+reload_current_file(void)
+{
+ if (load_image(get_file(), 0))
+ reload_redraw();
+}
+
+static void
+reset_transformations(void)
+{
+ enum transformation augment = reset_transformations_without_redraw();
+ if (augment != TRANS_NONE)
+ transform_redraw(augment);
+}
+
+static void
+transform(enum transformation augment)
+{
+ if (add_transformation_without_redraw(augment))
+ transform_redraw(augment);
+}
+
+static void
+set_zoom_fit(GtkToggleAction *action, enum zoom_mode mode)
+{
+ GdkPixbuf *image;
+
+ if (gtk_toggle_action_get_active(action)) {
+ zoom_mode = mode;
+ if ((image = get_image()))
+ set_zoom_and_recentre(calculate_zoom_mode(image, zoom_mode));
+ } else if (zoom_mode == mode) {
+ zoom_mode = ZOOM_CUSTOM; /* deselect zoom */
+ }
+
+ clear_toggles(GROUP_ZOOM, action);
+}
+
+static void
+navigate(size_t jump, size_t move)
+{
+ if (navigate_without_redraw(jump, move))
+ navigate_redraw();
+}
+
+void
+open_action(GtkAction *action, void *user)
+{
+ GtkFileChooser *chooser;
+ (void) action;
+ (void) user;
+ chooser = construct_chooser();
+ add_image_filters(chooser);
+ if (gtk_dialog_run(GTK_DIALOG(chooser)) == GTK_RESPONSE_ACCEPT)
+ set_images_from_list(gtk_file_chooser_get_filenames(chooser));
+ gtk_widget_destroy(GTK_WIDGET(chooser));
+}
+
+void
+reload_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ reload_current_file();
+}
+
+void
+print_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ open_print(get_image());
+}
+
+void
+properties_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ open_properties(get_file());
+}
+
+void
+close_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ gtk_main_quit();
+}
+
+void
+fullscreen_action(GtkToggleAction *action, void *user)
+{
+ int active = gtk_toggle_action_get_active(action);
+ (void) user;
+ if (active && current_file >= nfiles)
+ gtk_toggle_action_set_active(action, 0);
+ else
+ set_fullscreen_mode(active);
+}
+
+void
+reset_transformations_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ reset_transformations();
+}
+
+void
+rotate_clockwise_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ transform(TRANS_CLOCKWISE);
+}
+
+void
+rotate_180_degrees_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ transform(TRANS_180_DEGREES);
+}
+
+void
+rotate_anticlockwise_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ transform(TRANS_ANTICLOCKWISE);
+}
+
+void
+flop_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ transform(TRANS_FLOP);
+}
+
+void
+flip_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ transform(TRANS_FLIP);
+}
+
+void
+transpose_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ transform(TRANS_TRANSPOSE);
+}
+
+void
+antitranspose_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ transform(TRANS_ANTITRANSPOSE);
+}
+
+void
+invert_action(GtkToggleAction *action, void *user)
+{
+ enum transformation tr;
+ (void) user;
+ tr = gtk_toggle_action_get_active(action) ? TRANS_NEGATIVE : TRANS_NONE;
+ transform(tr ^ (trans & TRANS_NEGATIVE));
+}
+
+void
+zoom_in_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ clear_toggles(GROUP_ZOOM, NULL);
+ set_zoom_around_centre(zoom_in(zoom));
+}
+
+void
+zoom_out_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ clear_toggles(GROUP_ZOOM, NULL);
+ set_zoom_around_centre(zoom_out(zoom));
+}
+
+void
+normal_size_action(GtkToggleAction *action, void *user)
+{
+ (void) user;
+ set_zoom_fit(action, ZOOM_NORMAL);
+}
+
+void
+width_fit_action(GtkToggleAction *action, void *user)
+{
+ (void) user;
+ set_zoom_fit(action, ZOOM_FIT_WIDTH);
+}
+
+void
+height_fit_action(GtkToggleAction *action, void *user)
+{
+ (void) user;
+ set_zoom_fit(action, ZOOM_FIT_HEIGHT);
+}
+
+void
+window_fit_action(GtkToggleAction *action, void *user)
+{
+ (void) user;
+ set_zoom_fit(action, ZOOM_FIT_WINDOW);
+}
+
+void
+best_fit_action(GtkToggleAction *action, void *user)
+{
+ (void) user;
+ set_zoom_fit(action, ZOOM_BEST_FIT);
+}
+
+void
+next_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ navigate(current_file, +(size_t)1);
+}
+
+void
+previous_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ navigate(current_file ? current_file : nfiles, -(size_t)1);
+}
+
+void
+first_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ navigate(0u, 0u);
+}
+
+void
+last_action(GtkAction *action, void *user)
+{
+ (void) action;
+ (void) user;
+ navigate(nfiles, -(size_t)1);
+}
+
+int
+key_pressed(GtkWidget *widget, GdkEventKey *event, void *user)
+{
+ GtkAdjustment *hadjustment, *vadjustment;
+ int left, right, up, down, amount;
+ double hamount, vamount;
+
+ (void) widget;
+ (void) user;
+
+ if (event->keyval == GDK_Escape) {
+ deactivate_fullscreen(NULL);
+ return 1;
+ }
+
+ if (event->state & GDK_CONTROL_MASK)
+ return 0;
+
+ amount = (event->state & GDK_MOD1_MASK) ? 1 : SCROLL_AMOUNT;
+ hamount = (double)amount;
+ vamount = (double)amount;
+ if (event->state & GDK_SHIFT_MASK) {
+ hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
+ hamount = 0.25 * hadjustment->page_size;
+ vamount = 0.25 * vadjustment->page_size;
+ }
+
+ left = event->keyval == GDK_Left;
+ right = event->keyval == GDK_Right;
+ up = event->keyval == GDK_Up;
+ down = event->keyval == GDK_Down;
+
+ if (left | right | up | down) {
+ scroll_by(right * hamount - left * hamount,
+ down * vamount - up * vamount);
+ return 1;
+ }
+
+ return 0;
+}
+
+int
+scroll_event(GtkWidget *widget, GdkEventScroll *event, void *user)
+{
+ GtkAdjustment *hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ GtkAdjustment *vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
+ double hamount = hadjustment->step_increment;
+ double vamount = vadjustment->step_increment;
+
+ int shift = !!(event->state & GDK_SHIFT_MASK);
+ int ctrl = !!(event->state & GDK_CONTROL_MASK);
+ int up = event->direction == GDK_SCROLL_UP;
+ int down = event->direction == GDK_SCROLL_DOWN;
+ int left = event->direction == GDK_SCROLL_LEFT;
+ int right = event->direction == GDK_SCROLL_RIGHT;
+
+ int x0, y0;
+
+ (void) widget;
+ (void) user;
+
+ if (shift) {
+ left = up, up = 0;
+ right = down, down = 0;
+ }
+
+ if (!ctrl) {
+ /* needed for shift+scroll to work */
+ scroll_by(right * hamount - left * hamount,
+ down * vamount - up * vamount);
+ } else if (up | down) {
+ gdk_window_get_origin(display_area->window, &x0, &y0);
+ clear_toggles(GROUP_ZOOM, NULL);
+ set_zoom_around_point(up ? zoom_in(zoom) : zoom_out(zoom), event->x_root - x0, event->y_root - y0);
+ } else {
+ return 0;
+ }
+
+ return 1;
+}
diff --git a/files.c b/files.c
new file mode 100644
index 0000000..2db0b17
--- /dev/null
+++ b/files.c
@@ -0,0 +1,119 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+struct file *files = NULL;
+size_t nfiles = 0u;
+size_t current_file = 0u;
+
+void
+free_files(void)
+{
+ while (nfiles) {
+ --nfiles;
+ free(files[nfiles].path);
+ g_clear_object(&files[nfiles].image);
+ }
+ free(files);
+ files = NULL;
+ current_file = 0u;
+}
+
+size_t
+files_from_array(struct file *selected_files, size_t nselected_files)
+{
+ size_t i, n;
+
+ for (i = 0u, n = 0u; i < nselected_files; i++) {
+ selected_files[i].image = NULL;
+ if (load_image(&selected_files[i], 0)) {
+ if (i != n)
+ selected_files[n] = selected_files[i];
+ n += 1u;
+ } else {
+ free(selected_files[i].path);
+ }
+ }
+
+ return n;
+}
+
+size_t
+files_from_list(struct file **files_out, GSList *selected_files)
+{
+ struct file *new_files = NULL;
+ GSList *file;
+ size_t i;
+
+ for (file = selected_files, i = 0u; file; file = file->next)
+ i += 1u;
+ if (!i)
+ goto out;
+ new_files = ecalloc(i, sizeof(*files));
+
+ for (file = selected_files, i = 0u; file; file = file->next) {
+ new_files[i].path = estrdup(file->data);
+ new_files[i].image = NULL;
+ g_free(file->data);
+
+ if (load_image(&new_files[i], 0))
+ i += 1u;
+ else
+ free(new_files[i].path);
+ }
+ g_slist_free(selected_files);
+
+ if (!i) {
+ free(new_files);
+ new_files = NULL;
+ }
+
+out:
+ *files_out = new_files;
+ return i;
+}
+
+int
+set_files_from_argv(int argc, char **argv)
+{
+ int ok = 1;
+ size_t i;
+
+ nfiles = (size_t)argc;
+ if (!nfiles) {
+ files = NULL;
+ return 1;
+ }
+
+ files = ecalloc(nfiles, sizeof(*files));
+ for (i = 0u; i < nfiles; i++) {
+ files[i].path = estrdup(argv[i]);
+ files[i].image = NULL;
+ if (!load_image(&files[i], 1)) {
+ free(files[i].path);
+ ok = 0;
+ }
+ }
+
+ return ok;
+
+}
+
+struct file *
+get_file(void)
+{
+ if (current_file >= nfiles)
+ return NULL;
+ return &files[current_file];
+}
+
+int
+navigate_without_redraw(size_t jump, size_t move)
+{
+ size_t old_file = current_file;
+ if (!nfiles)
+ return 0;
+ current_file = jump;
+ current_file += move;
+ current_file %= nfiles;
+ return current_file != old_file;
+}
diff --git a/filters.c b/filters.c
new file mode 100644
index 0000000..08bb647
--- /dev/null
+++ b/filters.c
@@ -0,0 +1,173 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+struct filter {
+ char *name;
+ size_t suffixes_start;
+ size_t suffixes_end;
+};
+
+static char *suffixes = NULL;
+static struct filter *filters = NULL;
+static size_t nfilters = 0u;
+static struct filter anyfilter = {NULL, 0u, 0u};
+
+static int
+file_filter(const GtkFileFilterInfo *info, void *user)
+{
+ struct filter *filter = user;
+ size_t pos, name_len, suffix_len;
+ const char *name, *suffix;
+ size_t backup;
+
+ if (!info->filename)
+ return 0;
+ if (!filter || g_file_test(info->filename, G_FILE_TEST_IS_DIR))
+ return 1;
+
+ name = info->filename;
+ name_len = strlen(name);
+ if (!name_len)
+ return 0;
+
+ for (pos = filter->suffixes_start; pos != filter->suffixes_end; pos += suffix_len + 1u) {
+ suffix = &suffixes[pos];
+ suffix_len = strlen(suffix);
+
+ backup = (size_t)(name[name_len - 1u] == '~');
+
+ if (name_len <= 1u + suffix_len + backup)
+ continue;
+ if (name[name_len - (1u + suffix_len + backup)] != '.')
+ continue;
+ if (memcmp(&name[name_len - (suffix_len + backup)], suffix, suffix_len))
+ continue;
+
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+filtercmp(const void *a_void, const void *b_void)
+{
+ const struct filter *a = a_void, *b = b_void;
+ return strcmp(a->name, b->name);
+}
+
+static void
+populate_filters(void)
+{
+ GSList *formats, *item;
+ GdkPixbufFormat *format;
+ char **extensions, **extension, *p;
+ const char *name;
+ size_t i, len = 0u;
+ size_t ext_len, need, count;
+
+ formats = gdk_pixbuf_get_formats();
+
+ for (item = formats; item; item = item->next) {
+ format = item->data;
+ if (gdk_pixbuf_format_is_disabled(format))
+ continue;
+
+ name = gdk_pixbuf_format_get_description(format);
+ extensions = gdk_pixbuf_format_get_extensions(format);
+
+ need = 0u;
+ count = 0u;
+ for (extension = extensions; *extension; extension++) {
+ ext_len = strlen(*extension);
+ if (need > SIZE_MAX - 1u - ext_len)
+ eprintf("Out of address space");
+ need += ext_len + 1u;
+ count += 1u;
+ }
+ if (need > SIZE_MAX - len)
+ eprintf("Out of address space");
+ if (!count)
+ goto next;
+
+ filters = ereallocarray(filters, nfilters + 1u, sizeof(*filters));
+ filters[nfilters].suffixes_start = len;
+ filters[nfilters].name = emalloc(strlen(name) + sizeof(" ()") + need + (count - 1u));
+
+ p = stpcpy(stpcpy(filters[nfilters].name, name), " (");
+ suffixes = erealloc(suffixes, need + len);
+ for (extension = extensions;;) {
+ ext_len = strlen(*extension);
+ memcpy(&suffixes[len], *extension, ext_len + 1u);
+ len += ext_len + 1u;
+
+ memcpy(p, *extension, ext_len);
+ p = &p[ext_len];
+
+ if (!*++extension)
+ break;
+ p = stpcpy(p, ", ");
+ }
+ p = stpcpy(p, ")");
+
+ for (i = 0u; i < nfilters; i++) {
+ if (!strcmp(filters[nfilters].name, filters[i].name)) {
+ free(filters[nfilters].name);
+ len = filters[nfilters].suffixes_start;
+ goto next;
+ }
+ }
+
+ filters[nfilters].suffixes_end = len;
+ nfilters += 1u;
+
+ next:
+ g_strfreev(extensions);
+ }
+
+ qsort(filters, nfilters, sizeof(*filters), &filtercmp);
+
+ anyfilter.suffixes_end = len;
+
+ g_slist_free(formats);
+}
+
+void
+add_image_filters(GtkFileChooser *chooser)
+{
+ static int populated = 0;
+
+ GtkFileFilter *filter;
+ size_t i;
+
+ if (!populated) {
+ populate_filters();
+ populated = 1;
+ }
+
+ filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, "All images");
+ gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, &anyfilter, NULL);
+ gtk_file_chooser_add_filter(chooser, filter);
+
+ for (i = 0u; i < nfilters; i++) {
+ filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, filters[i].name);
+ gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, &filters[i], NULL);
+ gtk_file_chooser_add_filter(chooser, filter);
+ }
+
+ filter = gtk_file_filter_new();
+ gtk_file_filter_set_name(filter, "All files");
+ gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, NULL, NULL);
+ gtk_file_chooser_add_filter(chooser, filter);
+}
+
+void
+free_image_filters(void)
+{
+ free(suffixes);
+ while (nfilters)
+ free(filters[--nfilters].name);
+ free(filters);
+}
diff --git a/gbild.c b/gbild.c
index 7ab22ee..ffe51b0 100644
--- a/gbild.c
+++ b/gbild.c
@@ -1,171 +1,9 @@
/* See LICENSE file for copyright and license details. */
-#if defined(__GNUC__)
-# pragma GCC diagnostic push
-# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
-# pragma GCC diagnostic ignored "-Wstrict-prototypes"
-#endif
-#include <gdk/gdkkeysyms.h>
-#include <gtk/gtk.h>
-#include <libsimple.h>
-#include <libsimple-arg.h>
-#if defined(__GNUC__)
-# pragma GCC diagnostic pop
-#endif
-
-#if defined(__GNUC__)
-# pragma GCC diagnostic ignored "-Wunsuffixed-float-constants"
-#endif
+#include "common.h"
USAGE("[-f] [file] ...");
-#define SCROLL_AMOUNT 30
-
-#define FLOAT_EQ(A, B) ((A) >= (B) && (A) <= (B))
-
-#define COMMA ,
-
-#define LIST_MENU(TOPLEVEL, SEPARATOR, ACTION, TOGGLE, D)\
- TOPLEVEL(0, "", "File", D)\
- ACTION(0, 0u, "", "Open", GTK_STOCK_OPEN, "<control>0", "Ctrl+0", open_action, D)\
- ACTION(0, 1u, "", "Reload", GTK_STOCK_REFRESH, "F5", "F5", reload_action, D)\
- SEPARATOR(0, D)\
- ACTION(0, 1u, "", "Print", GTK_STOCK_PRINT, "<control>P", "Ctrl+P", print_action, D)\
- SEPARATOR(0, D)\
- ACTION(0, 1u, "Proper", "ties", GTK_STOCK_PROPERTIES, "<alt>Return", "Alt+Return", properties_action, D)\
- SEPARATOR(0, D)\
- ACTION(0, 0u, "", "Close", GTK_STOCK_CLOSE, "<control>W", "Ctrl+W", close_action, D)\
- TOPLEVEL(0, "", "View", D)\
- TOGGLE(1, 1u, "F", "ullscreen", GTK_STOCK_FULLSCREEN, "F11", "F11", fullscreen_action, GROUP_NONE, 0, D)\
- SEPARATOR(1, D)\
- ACTION(0, 1u, "Re", "set transformations", "document-revert", "<alt>R", "Alt+R", reset_transformations_action, D)\
- ACTION(1, 1u, "Rotate anti", "clockwise", "image-rotate-left", "<shift>R", "Shift+R", rotate_anticlockwise_action, D)\
- ACTION(0, 1u, "R", "otate 180°", "image-rotate", "<control>space", "Ctrl+Space", rotate_180_degrees_action, D)\
- ACTION(1, 1u, "", "Rotate clockwise", "image-rotate-right", "R", "R", rotate_clockwise_action, D)\
- ACTION(1, 1u, "", "Mirror horizontally", "object-flip-horizontal", "H", "H", flop_action, D)\
- ACTION(1, 1u, "Mirror ", "vertically", "object-flip-vertical", "V", "V", flip_action, D)\
- ACTION(1, 1u, "", "Transpose", "object-transpose", "T", "T", transpose_action, D)\
- ACTION(1, 1u, "", "Antitranspose", "object-transpose-mirrored", "<shift>T", "Shift+T", antitranspose_action, D)\
- SEPARATOR(1, D)\
- TOGGLE(1, 1u, "", "Invert colours", "filters-invert", "<control>I", "Ctrl+I", invert_action, GROUP_NONE, 0, D)\
- SEPARATOR(1, D)\
- /* TODO these are not properly honoured */\
- ACTION(1, 1u, "", "Zoom in", "zoom-in", "plus", "+", zoom_in_action, D)\
- ACTION(1, 1u, "Zoom ", "out", "zoom-out", "minus", "-", zoom_out_action, D)\
- TOGGLE(1, 1u, "", "Normal size", "zoom-original", "0", "0", normal_size_action, GROUP_ZOOM, 0, D)\
- TOGGLE(1, 1u, "Fit to ", "width", "zoom-fit-width", "<control>H", "Ctrl+H", width_fit_action, GROUP_ZOOM, 0, D)\
- TOGGLE(1, 1u, "Fit to ", "height", "zoom-fit-height", "<control>V", "Ctrl+V", height_fit_action, GROUP_ZOOM, 0, D)\
- TOGGLE(1, 1u, "", "Fit to window", "zoom-fit-page", "<control><shift>F", "Ctrl+Shift+F", window_fit_action, GROUP_ZOOM, 0, D)\
- TOGGLE(1, 1u, "", "Best fit", "zoom-fit-best", "<control>F", "Ctrl+F", best_fit_action, GROUP_ZOOM, 1, D)\
- TOPLEVEL(1, "", "Go", D)\
- ACTION(1, 2u, "", "Previous image", GTK_STOCK_GO_BACK, "<control>Left", "Ctrl+Left", previous_action, D)\
- ACTION(1, 2u, "", "Next image", GTK_STOCK_GO_FORWARD, "<control>Right", "Ctrl+Right", next_action, D)\
- SEPARATOR(0, D)\
- ACTION(1, 2u, "", "First image", GTK_STOCK_GOTO_FIRST, "<control>Home", "Ctrl+Home", first_action, D)\
- ACTION(1, 2u, "", "Last image", GTK_STOCK_GOTO_LAST, "<control>End", "Ctrl+End", last_action,)
-
-enum toggle_group {
- GROUP_NONE,
- GROUP_ZOOM
-};
-
-enum transformation {
- TRANS_NONE, /* 1 */
- TRANS_CLOCKWISE, /* r */
- TRANS_180_DEGREES, /* r² */
- TRANS_ANTICLOCKWISE, /* r³ */
- TRANS_TRANSPOSE, /* t */
- TRANS_FLOP, /* rt */
- TRANS_ANTITRANSPOSE, /* r²t */
- TRANS_FLIP, /* r³t */
- /*
- * t² = r⁴ = 1
- * trⁿ = r⁻ⁿt
- */
- TRANS_NEGATIVE = 0x08
-};
-
-struct file {
- char *path;
- off_t size;
- GdkPixbuf *image;
-};
-
-struct filter {
- char *name;
- size_t suffixes_start;
- size_t suffixes_end;
-};
-
-static struct file *files = NULL;
-static size_t nfiles = 0u;
-static size_t current_file = 0u;
-static enum transformation trans = TRANS_NONE;
-static GdkPixbuf *transformed_image = NULL;
-
-static double zoom = 1.0;
-static int clearing_toggles = 0;
-static int dragging = 0;
-static int drag_x, drag_y;
-
-static GtkWidget *window;
-static GtkWidget *menubar;
-static GtkWidget *statusbar;
-static GtkWidget *toolbar;
-static GtkWidget *display_area;
-static GtkWidget *scroll;
-static GtkToggleAction *fullscreen;
-static GtkActionGroup *actions;
-
-static const char *home_directory;
-static char *suffixes = NULL;
-static struct filter *filters = NULL;
-static size_t nfilters = 0u;
-static struct filter anyfilter = {NULL, 0u, 0u};
-static char image_target_name[] = "text/uri-list";
-static GtkTargetEntry image_target = {image_target_name, 0u, 0u};
-
-static double
-zoom_in(double z)
-{
- return z * 1.25;
-}
-
-static double
-zoom_out(double z)
-{
- return z * 0.8;
-}
-
-static GtkAction *
-get_action(const char *name)
-{
- return gtk_action_group_get_action(actions, name);
-}
-
-static void
-set_actions_sensitive(void)
-{
-#if defined(__GNUC__)
-# pragma GCC diagnostic push
-# pragma GCC diagnostic ignored "-Wtype-limits"
-#endif
-
-#define X_SKIP(...)
-#define X_ACTION(T, N, A, B, ICON, ACCEL, ATEXT, F, ...)\
- if (N) gtk_action_set_sensitive(get_action(A B), nfiles >= N) __VA_ARGS__
-#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ATEXT, F, GROUP, ACTIVE, ...)\
- if (N) gtk_action_set_sensitive(get_action(A B), nfiles >= N) __VA_ARGS__
-
- LIST_MENU(X_SKIP, X_SKIP, X_ACTION, X_TOGGLE, ;);
-
-#undef X_SKIP
-#undef X_ACTION
-#undef X_TOGGLE
-
-#if defined(__GNUC__)
-# pragma GCC diagnostic pop
-#endif
-}
+static int suspend_expose = 0;
static size_t
format_statusbar_message(char *buffer, size_t size, struct file *file)
@@ -194,11 +32,11 @@ format_statusbar_message(char *buffer, size_t size, struct file *file)
done:
r = snprintf(buffer, size,
- "Image %zu of %zu | %ju%s %c | %i × %i pixels", /* TODO LATER add zoom */
+ "Image %zu of %zu | %ju%s %c | %i × %i pixels | %g%% scale",
current_file + 1u, nfiles,
fsize, fsize_decimals, units[unit],
- gdk_pixbuf_get_width(file->image),
- gdk_pixbuf_get_height(file->image));
+ gdk_pixbuf_get_width(file->image), gdk_pixbuf_get_height(file->image),
+ 100.0 * get_zoom(zoom));
if (r < 0)
eprintf("snprintf:");
@@ -208,1245 +46,372 @@ done:
static void
update_status(void)
{
- char *message;
+ struct file *file = get_file();
+ char *message = NULL;
size_t len;
-
- gtk_statusbar_pop(GTK_STATUSBAR(statusbar), 0u);
-
- if (current_file >= nfiles || !files[current_file].image)
- return;
-
- len = format_statusbar_message(NULL, 0u, &files[current_file]);
- message = malloc(++len);
- format_statusbar_message(message, len, &files[current_file]);
-
- gtk_statusbar_push(GTK_STATUSBAR(statusbar), 0u, message);
-
- free(message);
-}
-
-static void
-show_image(void)
-{
- GdkPixbuf *new_image, *current_image;
- int channels, height, rowstride, width, x, y;
- uint8_t *pixels;
-
- if (current_file >= nfiles || !files[current_file].image) {
- if (transformed_image)
- g_object_unref(transformed_image);
- gtk_image_clear(GTK_IMAGE(display_area));
- return;
+ if (file) {
+ len = format_statusbar_message(NULL, 0u, file);
+ message = malloc(++len);
+ format_statusbar_message(message, len, file);
}
-
- current_image = files[current_file].image;
-
- switch (trans & 7u) {
- case TRANS_NONE:
- new_image = NULL;
- goto transformed;
- case TRANS_CLOCKWISE:
- new_image = gdk_pixbuf_rotate_simple(current_image, GDK_PIXBUF_ROTATE_CLOCKWISE);
- break;
- case TRANS_180_DEGREES:
- new_image = gdk_pixbuf_rotate_simple(current_image, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
- break;
- case TRANS_ANTICLOCKWISE:
- new_image = gdk_pixbuf_rotate_simple(current_image, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
- break;
- case TRANS_FLIP:
- case TRANS_TRANSPOSE:
- new_image = gdk_pixbuf_flip(current_image, 0);
- break;
- case TRANS_FLOP:
- case TRANS_ANTITRANSPOSE:
- new_image = gdk_pixbuf_flip(current_image, 1);
- break;
- }
- if (!new_image)
- exit(1);
-
- switch (trans & 7u) {
- case TRANS_TRANSPOSE:
- case TRANS_ANTITRANSPOSE:
- current_image = new_image;
- new_image = gdk_pixbuf_rotate_simple(current_image, GDK_PIXBUF_ROTATE_CLOCKWISE);
- if (!new_image)
- exit(1);
- g_object_unref(current_image);
- break;
- default:
- break;
- }
-
-transformed:
- if (trans & TRANS_NEGATIVE) {
- if (!new_image) {
- new_image = gdk_pixbuf_copy(current_image);
- if (!new_image)
- exit(1);
- }
-
- pixels = gdk_pixbuf_get_pixels(new_image);
- width = gdk_pixbuf_get_width(new_image);
- height = gdk_pixbuf_get_height(new_image);
- rowstride = gdk_pixbuf_get_rowstride(new_image);
- channels = gdk_pixbuf_get_n_channels(new_image);
-
- for (y = 0; y < height; y++) {
- for (x = 0; x < width * channels; x += channels) {
- /* TODO LATER perceptual inversion rather than sRGB */
- pixels[x + 0] = 255 - pixels[x + 0];
- pixels[x + 1] = 255 - pixels[x + 1];
- pixels[x + 2] = 255 - pixels[x + 2];
- }
- pixels = &pixels[rowstride];
- }
- }
-
- current_image = new_image ? new_image : files[current_file].image;
-
- if (FLOAT_EQ(zoom, 1.0)) {
- gtk_image_set_from_pixbuf(GTK_IMAGE(display_area), current_image);
- } else {
- /* TODO do not create an up scaled image, draw it dynamically upscaled (just the exposed region) */
- current_image = gdk_pixbuf_scale_simple(current_image,
- (int)(zoom * gdk_pixbuf_get_width(current_image)),
- (int)(zoom * gdk_pixbuf_get_height(current_image)),
- GDK_INTERP_BILINEAR);
- gtk_image_set_from_pixbuf(GTK_IMAGE(display_area), current_image);
- g_object_unref(current_image);
- }
-
- if (transformed_image)
- g_object_unref(transformed_image);
- transformed_image = new_image;
-}
-
-static void
-free_files(void)
-{
- while (nfiles) {
- --nfiles;
- free(files[nfiles].path);
- g_clear_object(&files[nfiles].image);
- }
- free(files);
- files = NULL;
- current_file = 0u;
-}
-
-static void
-clear_images(void)
-{
- gtk_image_clear(GTK_IMAGE(display_area));
- gtk_statusbar_pop(GTK_STATUSBAR(statusbar), 0u);
- g_clear_object(&transformed_image);
- free_files();
+ set_statusbar_message(message);
+ free(message);
}
-static int
-load_image(struct file *file, int errors_to_stderr)
+int
+expose_image(GtkWidget *widget, GdkEventExpose *event, void *user)
{
- const char *error_message;
- GError *error = NULL;
- GtkWidget *dialog;
GdkPixbuf *image;
- GdkPixbufLoader *loader = NULL;
- struct stat st;
- unsigned char buffer[8 << 10];
- ssize_t r;
- int fd, ret = 1;
+ cairo_t *cr;
+ double height, width, x, y, s;
- fd = open(file->path, O_RDONLY | O_NOCTTY);
- if (fd < 0)
- goto fail_errno;
+ (void) user;
- if (fstat(fd, &st))
- goto fail_errno;
+ if (suspend_expose)
+ return 1;
- if (!S_ISREG(st.st_mode)) {
- error_message = "Not a regular file";
- goto fail;
- }
+ cr = gdk_cairo_create(widget->window);
+ gdk_cairo_set_source_color(cr, &widget->style->bg[GTK_STATE_NORMAL]);
- loader = gdk_pixbuf_loader_new();
- if (!loader)
- goto fail_errno;
- for (;;) {
- r = read(fd, buffer, sizeof(buffer));
- if (r <= 0) {
- if (!r)
- break;
- if (errno == EINTR)
- continue;
- goto fail_errno;
- }
- if (!gdk_pixbuf_loader_write(loader, buffer, (size_t)r, &error))
- goto fail_gerror;
- }
- if (!gdk_pixbuf_loader_close(loader, &error)) {
- fail_gerror:
- error_message = error->message;
- goto fail;
- }
- image = gdk_pixbuf_loader_get_pixbuf(loader);
+ cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
+ cairo_rectangle(cr, event->area.x, event->area.y, event->area.width, event->area.height);
+
+ image = get_image();
if (!image) {
- error_message = "Unsupported image format";
- goto fail;
+ cairo_fill(cr);
+ goto out;
}
- g_object_ref(image);
-
- g_clear_object(&file->image);
- file->size = st.st_size;
- file->image = image;
-
-out:
- g_clear_object(&loader);
- if (fd >= 0)
- close(fd);
- return ret;
+ s = get_zoom(zoom);
+ width = s * gdk_pixbuf_get_width(image);
+ height = s * gdk_pixbuf_get_height(image);
+ if (width < widget->allocation.width)
+ x = 0.5 * ((double)widget->allocation.width - width);
+ else
+ x = -logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll))->value;
+ if (height < widget->allocation.height)
+ y = 0.5 * ((double)widget->allocation.height - height);
+ else
+ y = -logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll))->value;
-fail_errno:
- error_message = strerror(errno);
+ cairo_fill(cr);
-fail:
- if (errors_to_stderr) {
- weprintf("Cannot open %s: %s", file->path, error_message);
- } else {
- dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
- GTK_BUTTONS_CLOSE, "Cannot open %s", file->path);
- gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error_message);
- gtk_dialog_run(GTK_DIALOG(dialog));
- gtk_widget_destroy(dialog);
- }
+ cairo_translate(cr, x, y);
+ cairo_scale(cr, s, s);
+ gdk_cairo_set_source_pixbuf(cr, image, 0.0, 0.0);
- g_clear_error(&error);
- ret = 0;
- goto out;
+out:
+ cairo_paint(cr);
+ cairo_destroy(cr);
+ return 1;
}
static void
-set_images_from_array(struct file *selected_files, size_t nselected_files)
+show_image(int update_zoom)
{
- size_t i, n;
+ GdkPixbuf *new_image, *original_image, *display_image;
+ double s;
- for (i = 0u, n = 0u; i < nselected_files; i++) {
- selected_files[i].image = NULL;
- if (load_image(&selected_files[i], 0)) {
- if (i != n)
- selected_files[n] = selected_files[i];
- n += 1u;
- } else {
- free(selected_files[i].path);
- }
- }
-
- if (!n) {
- free(selected_files);
+ original_image = get_original_image();
+ if (!original_image) {
+ logical_scroll_pane_set_content_size(LOGICAL_SCROLL_PANE(scroll), 1, 1);
+ gtk_widget_queue_draw(display_area);
return;
}
- clear_images();
- files = selected_files;
- nfiles = n;
- show_image();
- update_status();
- set_actions_sensitive();
-}
+ new_image = apply_transformations(original_image);
+ display_image = new_image ? new_image : original_image;
-static void
-set_images_from_list(GSList *selected_files)
-{
- struct file *new_files;
- GSList *file;
- size_t i;
+ if (update_zoom && zoom_mode != ZOOM_CUSTOM && zoom_mode != ZOOM_NORMAL)
+ zoom = calculate_zoom_mode(display_image, zoom_mode);
- for (file = selected_files, i = 0u; file; file = file->next)
- i += 1u;
- if (!i)
- return;
- new_files = ecalloc(i, sizeof(*files));
+ s = get_zoom(zoom);
+ logical_scroll_pane_set_content_size(LOGICAL_SCROLL_PANE(scroll),
+ (int)(s * gdk_pixbuf_get_width(display_image)),
+ (int)(s * gdk_pixbuf_get_height(display_image)));
- for (file = selected_files, i = 0u; file; file = file->next) {
- new_files[i].path = estrdup(file->data);
- new_files[i].image = NULL;
- g_free(file->data);
-
- if (load_image(&new_files[i], 0))
- i += 1u;
- else
- free(new_files[i].path);
- }
- g_slist_free(selected_files);
-
- if (!i) {
- free(new_files);
- return;
- }
-
- clear_images();
- files = new_files;
- nfiles = i;
- show_image();
- update_status();
- set_actions_sensitive();
-}
-
-static int
-file_filter(const GtkFileFilterInfo *info, void *user)
-{
- struct filter *filter = user;
- size_t pos, name_len, suffix_len;
- const char *name, *suffix;
- size_t backup;
-
- if (!info->filename)
- return 0;
- if (!filter || g_file_test(info->filename, G_FILE_TEST_IS_DIR))
- return 1;
-
- name = info->filename;
- name_len = strlen(name);
- if (!name_len)
- return 0;
-
- for (pos = filter->suffixes_start; pos != filter->suffixes_end; pos += suffix_len + 1u) {
- suffix = &suffixes[pos];
- suffix_len = strlen(suffix);
-
- backup = (size_t)(name[name_len - 1u] == '~');
-
- if (name_len <= 1u + suffix_len + backup)
- continue;
- if (name[name_len - (1u + suffix_len + backup)] != '.')
- continue;
- if (memcmp(&name[name_len - (suffix_len + backup)], suffix, suffix_len))
- continue;
-
- return 1;
- }
-
- return 0;
-}
-
-static int
-filtercmp(const void *a_void, const void *b_void)
-{
- const struct filter *a = a_void, *b = b_void;
- return strcmp(a->name, b->name);
-}
-
-static void
-populate_filters(void)
-{
- GSList *formats, *item;
- GdkPixbufFormat *format;
- char **extensions, **extension, *p;
- const char *name;
- size_t i, len = 0u;
- size_t ext_len, need, count;
-
- formats = gdk_pixbuf_get_formats();
-
- for (item = formats; item; item = item->next) {
- format = item->data;
- if (gdk_pixbuf_format_is_disabled(format))
- continue;
-
- name = gdk_pixbuf_format_get_description(format);
- extensions = gdk_pixbuf_format_get_extensions(format);
-
- need = 0u;
- count = 0u;
- for (extension = extensions; *extension; extension++) {
- ext_len = strlen(*extension);
- if (need > SIZE_MAX - 1u - ext_len)
- eprintf("Out of address space");
- need += ext_len + 1u;
- count += 1u;
- }
- if (need > SIZE_MAX - len)
- eprintf("Out of address space");
- if (!count)
- goto next;
-
- filters = ereallocarray(filters, nfilters + 1u, sizeof(*filters));
- filters[nfilters].suffixes_start = len;
- filters[nfilters].name = emalloc(strlen(name) + sizeof(" ()") + need + (count - 1u));
-
- p = stpcpy(stpcpy(filters[nfilters].name, name), " (");
- suffixes = erealloc(suffixes, need + len);
- for (extension = extensions;;) {
- ext_len = strlen(*extension);
- memcpy(&suffixes[len], *extension, ext_len + 1u);
- len += ext_len + 1u;
-
- memcpy(p, *extension, ext_len);
- p = &p[ext_len];
-
- if (!*++extension)
- break;
- p = stpcpy(p, ", ");
- }
- p = stpcpy(p, ")");
-
- for (i = 0u; i < nfilters; i++) {
- if (!strcmp(filters[nfilters].name, filters[i].name)) {
- free(filters[nfilters].name);
- len = filters[nfilters].suffixes_start;
- goto next;
- }
- }
-
- filters[nfilters].suffixes_end = len;
- nfilters += 1u;
-
- next:
- g_strfreev(extensions);
- }
-
- qsort(filters, nfilters, sizeof(*filters), &filtercmp);
-
- anyfilter.suffixes_end = len;
-
- g_slist_free(formats);
-}
-
-static void
-add_image_filters(GtkFileChooser *chooser)
-{
- GtkFileFilter *filter;
- size_t i;
-
- filter = gtk_file_filter_new();
- gtk_file_filter_set_name(filter, "All images");
- gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, &anyfilter, NULL);
- gtk_file_chooser_add_filter(chooser, filter);
-
- for (i = 0u; i < nfilters; i++) {
- filter = gtk_file_filter_new();
- gtk_file_filter_set_name(filter, filters[i].name);
- gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, &filters[i], NULL);
- gtk_file_chooser_add_filter(chooser, filter);
- }
-
- filter = gtk_file_filter_new();
- gtk_file_filter_set_name(filter, "All files");
- gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, NULL, NULL);
- gtk_file_chooser_add_filter(chooser, filter);
-}
-
-static void
-preview_image(GtkFileChooser *chooser, GtkWidget *preview_widget)
-{
- char *filename;
- GdkPixbuf *preview;
-
- filename = gtk_file_chooser_get_preview_filename(chooser);
- preview = filename ? gdk_pixbuf_new_from_file_at_scale(filename, 192, 192, 1, NULL) : NULL;
- gtk_image_set_from_pixbuf(GTK_IMAGE(g_object_get_data(G_OBJECT(preview_widget), "image")), preview);
- if (preview)
- g_object_unref(preview);
- g_free(filename);
+ if (transformed_image)
+ g_object_unref(transformed_image);
+ transformed_image = new_image;
}
static void
-open_action(GtkAction *action, void *user)
+set_images(struct file *new_files, size_t nnew_files)
{
- /* TODO The preview widget should have a fixed size (although it may be subject
- * to manual resize by the user). When there is no preview the preview widge
- * shall just say "Preview" (centered on both dimensions) */
-
- /* TODO The preview widget shall not have a filename header */
-
- GtkWidget *chooser_widget, *image, *preview;
- GtkFileChooser *chooser;
-
- (void) action;
- (void) user;
-
- chooser_widget = gtk_file_chooser_dialog_new("Open",
- GTK_WINDOW(window),
- GTK_FILE_CHOOSER_ACTION_OPEN,
- GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
- GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
- NULL);
- chooser = GTK_FILE_CHOOSER(chooser_widget);
-
- g_object_set(gtk_settings_get_default(), "gtk-recent-files-enabled", 0, NULL);
- gtk_file_chooser_set_select_multiple(chooser, 1);
- gtk_file_chooser_set_current_folder(chooser, home_directory);
- gtk_file_chooser_set_local_only(chooser, 1);
- gtk_file_chooser_set_do_overwrite_confirmation(chooser, 0);
-
- preview = gtk_vbox_new(0, 6);
- image = gtk_image_new();
- gtk_box_pack_start(GTK_BOX(preview), image, 0, 0, 0u);
- gtk_box_pack_start(GTK_BOX(preview), gtk_label_new("Preview"), 0, 0, 0u);
- g_object_set_data(G_OBJECT(preview), "image", image);
- gtk_file_chooser_set_preview_widget(chooser, preview);
- gtk_file_chooser_set_preview_widget_active(chooser, 1);
- g_signal_connect(chooser, "update-preview", G_CALLBACK(preview_image), preview);
-
- add_image_filters(chooser);
- if (gtk_dialog_run(GTK_DIALOG(chooser)) == GTK_RESPONSE_ACCEPT)
- set_images_from_list(gtk_file_chooser_get_filenames(chooser));
- gtk_widget_destroy(GTK_WIDGET(chooser));
+ g_clear_object(&transformed_image);
+ free_files();
+ files = new_files;
+ nfiles = nnew_files;
+ navigate_redraw();
+ set_actions_sensitive();
}
-static void
-reload_action(GtkAction *action, void *user)
+void
+add_images(struct file *new_files, size_t nnew_files)
{
- (void) action;
- (void) user;
- if (current_file >= nfiles)
- return;
- if (load_image(&files[current_file], 0)) {
- show_image();
+ files = ereallocarray(files, nfiles + nnew_files, sizeof(*files));
+ memcpy(&files[nfiles], new_files, nnew_files * sizeof(*files));
+ nfiles += nnew_files;
+ if (nfiles == nnew_files)
+ navigate_redraw();
+ else
update_status();
- }
-}
-
-static void
-close_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- gtk_main_quit();
-}
-
-static void
-begin_print(GtkPrintOperation *operation, GtkPrintContext *context, void *user)
-{
- (void) context;
- (void) user;
- gtk_print_operation_set_n_pages(operation, 1);
-}
-
-static void
-draw_page(GtkPrintOperation *operation, GtkPrintContext *context, int page, void *user)
-{
- GdkPixbuf *image = user;
- double height, width, x, y;
- double scale_x, scale_y, scale;
- cairo_t *cr;
-
- (void) operation;
- (void) page;
-
- width = gtk_print_context_get_width(context);
- height = gtk_print_context_get_height(context);
- scale_x = width / gdk_pixbuf_get_width(image);
- scale_y = height / gdk_pixbuf_get_height(image);
- scale = MIN(scale_x, scale_y);
- x = 0.5 * (width - scale * gdk_pixbuf_get_width(image));
- y = 0.5 * (height - scale * gdk_pixbuf_get_height(image));
-
- cr = gtk_print_context_get_cairo_context(context);
- cairo_translate(cr, x, y);
- cairo_scale(cr, scale, scale);
- gdk_cairo_set_source_pixbuf(cr, image, 0.0f, 0.0f);
- cairo_paint(cr);
+ set_actions_sensitive();
}
-static void
-print_action(GtkAction *action, void *user)
+void
+set_images_from_array(struct file *selected_files, size_t nselected_files)
{
- /* TODO the print dialogue needs a positioning tab that lets the user scale and pan */
-
- GdkPixbuf *image;
- GtkPrintOperation *operation;
- GtkPageSetup *setup;
- GtkPageOrientation orientation = GTK_PAGE_ORIENTATION_PORTRAIT;
-
- (void) action;
- (void) user;
-
- if (current_file >= nfiles || !files[current_file].image)
- return;
-
- image = transformed_image ? transformed_image : files[current_file].image;
- g_object_ref(image);
- operation = gtk_print_operation_new();
-
- if (gdk_pixbuf_get_width(image) > gdk_pixbuf_get_height(image))
- orientation = GTK_PAGE_ORIENTATION_LANDSCAPE;
- setup = gtk_page_setup_new();
- gtk_page_setup_set_orientation(setup, orientation);
- gtk_print_operation_set_default_page_setup(operation, setup);
- g_object_unref(setup);
-
- gtk_print_operation_set_embed_page_setup(operation, 1);
- g_signal_connect(operation, "begin-print", G_CALLBACK(begin_print), image);
- g_signal_connect(operation, "draw-page", G_CALLBACK(draw_page), image);
- gtk_print_operation_run(operation, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW(window), NULL);
- g_object_unref(operation);
- g_object_unref(image);
+ size_t n = files_from_array(selected_files, nselected_files);
+ if (n)
+ set_images(selected_files, n);
+ else
+ free(selected_files);
}
-static void
-properties_action(GtkAction *action, void *user)
+void
+set_images_from_list(GSList *selected_files)
{
- /* TODO LATER What shall "Properties" display? */
-
- GtkWidget *dialog;
- struct file *file;
- const char *name;
-
- (void) action;
- (void) user;
-
- if (current_file >= nfiles || !files[current_file].image)
- return;
-
- file = &files[current_file];
- name = strrchr(file->path, '/');
- name = name ? &name[1u] : file->path;
-
- dialog = gtk_message_dialog_new(GTK_WINDOW(window), 0, GTK_MESSAGE_INFO,
- GTK_BUTTONS_CLOSE, "%s", name);
- gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
- "%i × %i pixels",
- gdk_pixbuf_get_width(file->image),
- gdk_pixbuf_get_height(file->image));
- g_signal_connect_swapped(dialog, "response", G_CALLBACK(gtk_widget_destroy), dialog);
- gtk_widget_show(dialog);
+ struct file *new_files;
+ size_t n = files_from_list(&new_files, selected_files);
+ if (n)
+ set_images(new_files, n);
}
-static void
-scroll_by(int horizontally, double vertically)
+void
+scroll_by(double horizontally, double vertically)
{
GtkAdjustment *adjustment;
double min, val, max;
- adjustment = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
+ adjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
min = adjustment->lower;
- val = adjustment->value + (double)horizontally;
+ val = adjustment->value + horizontally;
max = adjustment->upper - adjustment->page_size;
val = MIN(MAX(min, val), max);
gtk_adjustment_set_value(adjustment, val);
- adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
+ adjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
min = adjustment->lower;
- val = adjustment->value + (double)vertically;
+ val = adjustment->value + vertically;
max = adjustment->upper - adjustment->page_size;
val = MIN(MAX(min, val), max);
gtk_adjustment_set_value(adjustment, val);
}
-static void
-set_zoom(double new_zoom, double x, double y)
+void
+set_zoom_around_point(Zoom new_zoom, double x, double y)
{
GtkAdjustment *hadjustment, *vadjustment;
- double old_zoom;
+ double old_s, new_s, old_x0, old_y0, new_x0, new_y0;
+ double iw, ih, vw, vh, hx, hy, adj_x, adj_y, max;
+ int w, h;
+ GdkPixbuf *image;
- if (current_file >= nfiles || !files[current_file].image)
- return;
- if (FLOAT_EQ(new_zoom, zoom))
+ if (zoom_eq(new_zoom, zoom))
return;
+ image = get_image();
+ if (!image)
+ goto out;
- hadjustment = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
- vadjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
- old_zoom = zoom;
+ old_s = get_zoom(zoom);
+ new_s = get_zoom(new_zoom);
zoom = new_zoom;
- show_image();
- gtk_adjustment_set_value(hadjustment, (hadjustment->value + x) * zoom / old_zoom - x);
- gtk_adjustment_set_value(vadjustment, (vadjustment->value + y) * zoom / old_zoom - y);
- update_status();
-}
-
-static void
-clear_toggles(enum toggle_group group, GtkToggleAction *except)
-{
- GtkToggleAction *action;
- clearing_toggles = 1;
+ w = gdk_pixbuf_get_width(image);
+ h = gdk_pixbuf_get_height(image);
-#define X_SKIP(...)
-#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ATEXT, F, GROUP, ACTIVE, ...)\
- do {\
- if (GROUP == group) {\
- action = GTK_TOGGLE_ACTION(get_action(A B));\
- if (action != except)\
- gtk_toggle_action_set_active(action, 0);\
- }\
- } while (0) __VA_ARGS__
+ hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
+ vw = hadjustment->page_size;
+ vh = vadjustment->page_size;
- LIST_MENU(X_SKIP, X_SKIP, X_SKIP, X_TOGGLE, ;);
+ iw = old_s * (double)w;
+ ih = old_s * (double)h;
+ old_x0 = vw > iw ? 0.5 * (vw - iw) : 0.0;
+ old_y0 = vh > ih ? 0.5 * (vh - ih) : 0.0;
-#undef X_SKIP
-#undef X_TOGGLE
-
- clearing_toggles = 0;
-}
+ iw = new_s * (double)w;
+ ih = new_s * (double)h;
+ new_x0 = vw > iw ? 0.5 * (vw - iw) : 0.0;
+ new_y0 = vh > ih ? 0.5 * (vh - ih) : 0.0;
-static int
-scroll_event(GtkWidget *widget, GdkEventScroll *event, void *user)
-{
- int shift = !!(event->state & GDK_SHIFT_MASK);
- int ctrl = !!(event->state & GDK_CONTROL_MASK);
- int up = event->direction == GDK_SCROLL_UP;
- int down = event->direction == GDK_SCROLL_DOWN;
- int left = event->direction == GDK_SCROLL_LEFT;
- int right = event->direction == GDK_SCROLL_RIGHT;
+ hx = (x - old_x0 + hadjustment->value) / old_s;
+ hy = (y - old_y0 + vadjustment->value) / old_s;
+ adj_x = new_x0 + hx * new_s - x;
+ adj_y = new_y0 + hy * new_s - y;
- (void) widget;
- (void) user;
+ suspend_expose++;
- if (shift) {
- left = up, up = 0;
- right = down, down = 0;
- }
+ show_image(0);
- if (!ctrl) {
- scroll_by(left ? -SCROLL_AMOUNT : right ? +SCROLL_AMOUNT : 0,
- up ? -SCROLL_AMOUNT : down ? +SCROLL_AMOUNT : 0);
- } else if (up | down) {
- clear_toggles(GROUP_ZOOM, NULL);
- set_zoom(up ? zoom_in(zoom) : zoom_out(zoom), event->x, event->y);
- } else {
- return 0;
- }
+ max = hadjustment->upper - hadjustment->page_size;
+ x = MAX(hadjustment->lower, adj_x);
+ x = MIN(x, max);
+ gtk_adjustment_set_value(hadjustment, x);
- return 1;
-}
+ max = vadjustment->upper - vadjustment->page_size;
+ y = MAX(vadjustment->lower, adj_y);
+ y = MIN(y, max);
+ gtk_adjustment_set_value(vadjustment, y);
-static int
-button_pressed(GtkWidget *widget, GdkEventButton *event, void *user)
-{
- (void) widget;
- (void) user;
-
- if (event->button != 2u && !(event->state & GDK_CONTROL_MASK))
- return 0;
-
- dragging = 1;
- drag_x = (int)event->x;
- drag_y = (int)event->y;
- return 1;
-}
-
-static int
-button_released(GtkWidget *widget, GdkEventButton *event, void *user)
-{
- (void) widget;
- (void) event;
- (void) user;
- dragging = 0;
- return 0;
+ suspend_expose--;
+ if (!suspend_expose)
+ gtk_widget_queue_draw(display_area);
+out:
+ update_status();
}
-static int
-pointer_moved(GtkWidget *widget, GdkEventMotion *event, void *user)
+void
+set_zoom_around_centre(Zoom new_zoom)
{
- double x, y;
-
- (void) widget;
- (void) user;
-
- if (!dragging)
- return 0;
+ GtkAdjustment *hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ GtkAdjustment *vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
- gdk_event_get_coords((GdkEvent *)event, &x, &y);
- scroll_by(drag_x - (int)x, drag_y - (int)y);
- drag_x = (int)x;
- drag_y = (int)y;
-
- return 1;
+ set_zoom_around_point(new_zoom, 0.5 * hadjustment->page_size, 0.5 * vadjustment->page_size);
}
static void
-zoom_in_action(GtkAction *action, void *user)
+centre_image(void)
{
- (void) action;
- (void) user;
- clear_toggles(GROUP_ZOOM, NULL);
- set_zoom(zoom_in(zoom), 0.0, 0.0);
- /* TODO keep center point */
-}
+ GtkAdjustment *hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ GtkAdjustment *vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
-static void
-zoom_out_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- clear_toggles(GROUP_ZOOM, NULL);
- set_zoom(zoom_out(zoom), 0.0, 0.0);
- /* TODO keep center point */
+ gtk_adjustment_set_value(hadjustment, 0.5 * (hadjustment->upper - hadjustment->page_size));
+ gtk_adjustment_set_value(vadjustment, 0.5 * (vadjustment->upper - vadjustment->page_size));
}
-static void
-set_zoom_fit(GtkToggleAction *action, int xfit, int yfit, int cap)
+void
+set_zoom_and_recentre(Zoom new_zoom)
{
- GtkAdjustment *adjustment;
- GdkPixbuf *image;
- double s = 1.0, y;
- int w, h;
-
- if (!gtk_toggle_action_get_active(action) || current_file >= nfiles || !files[current_file].image) {
- if (!clearing_toggles)
- clear_toggles(GROUP_ZOOM, action);
+ if (zoom_eq(new_zoom, zoom))
return;
- }
- image = transformed_image ? transformed_image : files[current_file].image;
+ suspend_expose++;
- if (xfit) {
- adjustment = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(scroll));
- w = gdk_pixbuf_get_width(image);
- s = adjustment->page_size / (double)w;
- }
-
- if (yfit) {
- adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroll));
- h = gdk_pixbuf_get_height(image);
- y = adjustment->page_size / (double)h;
- s = xfit ? MIN(s, y) : y;
- }
-
- if (cap && s > 1.0)
- s = 1.0;
-
- clear_toggles(GROUP_ZOOM, action);
- set_zoom(s, 0.0, 0.0);
- /* TODO center image */
-}
-
-static void
-normal_size_action(GtkToggleAction *action, void *user)
-{
- (void) user;
- set_zoom_fit(action, 0, 0, 0);
-}
-
-static void
-width_fit_action(GtkToggleAction *action, void *user)
-{
- (void) user;
- set_zoom_fit(action, 1, 0, 0);
-}
-
-static void
-height_fit_action(GtkToggleAction *action, void *user)
-{
- (void) user;
- set_zoom_fit(action, 0, 1, 0);
-}
-
-static void
-window_fit_action(GtkToggleAction *action, void *user)
-{
- (void) user;
- set_zoom_fit(action, 1, 1, 0);
-}
+ zoom = new_zoom;
+ show_image(0);
+ centre_image();
-static void
-best_fit_action(GtkToggleAction *action, void *user)
-{
- (void) user;
- set_zoom_fit(action, 1, 1, 1);
+ suspend_expose--;
+ if (!suspend_expose)
+ gtk_widget_queue_draw(display_area);
+ update_status();
}
-static int
-key_pressed(GtkWidget *widget, GdkEventKey *event, void *user)
+void
+reload_redraw(void)
{
- int left, right, up, down, amount;
+ GtkAdjustment *hadjustment, *vadjustment;
+ double x, y, max;
- (void) widget;
- (void) user;
+ hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
- if (event->keyval == GDK_Escape && gtk_toggle_action_get_active(fullscreen)) {
- gtk_toggle_action_set_active(fullscreen, 0);
- return 1;
- }
+ x = hadjustment->value + 0.5 * hadjustment->page_size;
+ x = hadjustment->upper > hadjustment->page_size ? x / hadjustment->upper : 0.5;
+ y = vadjustment->value + 0.5 * vadjustment->page_size;
+ y = vadjustment->upper > vadjustment->page_size ? y / vadjustment->upper : 0.5;
- if (event->state & GDK_CONTROL_MASK)
- return 0;
+ suspend_expose++;
- amount = (event->state & GDK_MOD1_MASK) ? 1 : SCROLL_AMOUNT;
+ show_image(0);
- left = event->keyval == GDK_Left;
- right = event->keyval == GDK_Right;
- up = event->keyval == GDK_Up;
- down = event->keyval == GDK_Down;
+ max = hadjustment->upper - hadjustment->page_size;
+ x = x * hadjustment->upper - 0.5 * hadjustment->page_size;
+ x = MIN(MAX(hadjustment->lower, x), max);
+ gtk_adjustment_set_value(hadjustment, x);
- if (left | right | up | down) {
- scroll_by(left ? -amount : right ? +amount : 0,
- up ? -amount : down ? +amount : 0);
- return 1;
- }
+ max = vadjustment->upper - vadjustment->page_size;
+ y = y * vadjustment->upper - 0.5 * vadjustment->page_size;
+ y = MIN(MAX(vadjustment->lower, y), max);
+ gtk_adjustment_set_value(vadjustment, y);
- return 0;
-}
-
-static int
-window_state_changed(GtkWidget *widget, GdkEventWindowState *event, void *user)
-{
- (void) widget;
- (void) user;
- gtk_toggle_action_set_active(fullscreen, !!(event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN));
- return 0;
+ suspend_expose--;
+ if (!suspend_expose)
+ gtk_widget_queue_draw(display_area);
+ update_status();
}
-static void
-fullscreen_action(GtkToggleAction *action, void *user)
+void
+transform_redraw(enum transformation augment)
{
- int active = gtk_toggle_action_get_active(action);
-
- (void) user;
+ GdkPixbuf *image;
+ GtkAdjustment *hadjustment, *vadjustment;
+ double s, x, y, width, height, max;
- if (active && current_file >= nfiles) {
- gtk_toggle_action_set_active(action, 0);
+ hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
+ image = get_image();
+ if (!image)
return;
- }
- gtk_widget_set_visible(menubar, !active);
- gtk_widget_set_visible(toolbar, !active);
- gtk_widget_set_visible(statusbar, !active);
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
- active ? GTK_POLICY_NEVER : GTK_POLICY_ALWAYS,
- active ? GTK_POLICY_NEVER : GTK_POLICY_ALWAYS);
+ s = get_zoom(zoom);
+ width = gdk_pixbuf_get_width(image);
+ height = gdk_pixbuf_get_height(image);
- if (active)
- gtk_window_fullscreen(GTK_WINDOW(window));
- else
- gtk_window_unfullscreen(GTK_WINDOW(window));
-}
+ x = hadjustment->value + 0.5 * hadjustment->page_size;
+ x = s * width > hadjustment->page_size ? x / s : 0.5 * width;
+ y = vadjustment->value + 0.5 * vadjustment->page_size;
+ y = s * height > vadjustment->page_size ? y / s : 0.5 * height;
+ transform_point(augment, &x, &y, &width, &height);
-static void
-reset_transformations_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- trans &= ~7u;
- show_image();
- /* TODO keep center point */
-}
+ suspend_expose++;
-static void
-transform(enum transformation augment)
-{
- if (augment & TRANS_TRANSPOSE)
- trans = ((augment - trans) & 3u) | ((augment ^ trans) & ~3u);
- else
- trans = ((augment + trans) & 3u) | ((augment ^ trans) & ~3u);
- show_image();
- /* TODO keep center point */
-}
+ show_image(0);
-static void
-rotate_clockwise_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- transform(TRANS_CLOCKWISE);
-}
+ max = hadjustment->upper - hadjustment->page_size;
+ x = s * x - 0.5 * hadjustment->page_size;
+ x = MIN(MAX(hadjustment->lower, x), max);
+ gtk_adjustment_set_value(hadjustment, x);
-static void
-rotate_180_degrees_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- transform(TRANS_180_DEGREES);
-}
+ max = vadjustment->upper - vadjustment->page_size;
+ y = s * y - 0.5 * vadjustment->page_size;
+ y = MIN(MAX(vadjustment->lower, y), max);
+ gtk_adjustment_set_value(vadjustment, y);
-static void
-rotate_anticlockwise_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- transform(TRANS_ANTICLOCKWISE);
+ suspend_expose--;
+ if (!suspend_expose)
+ gtk_widget_queue_draw(display_area);
}
-static void
-flop_action(GtkAction *action, void *user)
+void
+navigate_redraw(void)
{
- (void) action;
- (void) user;
- transform(TRANS_FLOP);
-}
+ suspend_expose++;
-static void
-flip_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- transform(TRANS_FLIP);
-}
-
-static void
-transpose_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- transform(TRANS_TRANSPOSE);
-}
-
-static void
-antitranspose_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- transform(TRANS_ANTITRANSPOSE);
-}
-
-static void
-invert_action(GtkToggleAction *action, void *user)
-{
+ show_image(1);
+ centre_image();
- enum transformation tr;
- (void) user;
- tr = gtk_toggle_action_get_active(action) ? TRANS_NEGATIVE : TRANS_NONE;
- transform(tr ^ (trans & TRANS_NEGATIVE));
-}
-
-static void
-navigate(size_t jump, size_t move)
-{
- if (!nfiles)
- return;
- current_file = jump;
- current_file += move;
- current_file %= nfiles;
- show_image();
+ suspend_expose--;
+ if (!suspend_expose)
+ gtk_widget_queue_draw(display_area);
update_status();
}
-static void
-next_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- navigate(current_file, +(size_t)1);
-}
-
-static void
-previous_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- navigate(current_file ? current_file : nfiles, -(size_t)1);
-}
-
-static void
-first_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- navigate(0u, 0u);
-}
-
-static void
-last_action(GtkAction *action, void *user)
-{
- (void) action;
- (void) user;
- navigate(nfiles, -(size_t)1);
-}
-
-static void
-drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data,
- unsigned int info, unsigned int time, void *user)
-{
- GError *error = NULL;
- char *uri;
-
- (void) widget;
- (void) context;
- (void) info;
- (void) time;
- (void) user;
-
- if (current_file >= nfiles || !files[current_file].path)
- return;
-
- uri = g_filename_to_uri(files[current_file].path, NULL, &error);
- if (!uri) {
- weprintf("g_filename_to_uri: %s", error->message);
- g_error_free(error);
- return;
- }
- gtk_selection_data_set_uris(data, (char *[]){uri, NULL});
- g_free(uri);
-}
-
-static void
-drop_files(GtkWidget *widget, GdkDragContext *context, int x, int y,
- GtkSelectionData *data, unsigned int info, unsigned int time, void *user)
+int
+recalculate_zoom_mode(void *user)
{
- GError *error = NULL;
- char **uris, **uri, *path;
- struct file *dropped_files;
- size_t i, j;
- GdkModifierType modifiers;
- GdkWindow *event_window;
-
- (void) widget;
- (void) x;
- (void) y;
- (void) info;
+ GdkPixbuf *image = get_image();
(void) user;
-
- uris = gtk_selection_data_get_uris(data);
- if (!uris) {
- gtk_drag_finish(context, 0, 0, time);
- return;
- }
-
- for (uri = uris, i = 0u; *uri; uri++)
- i += 1u;
- if (!i)
- goto out;
- dropped_files = ecalloc(i, sizeof(*dropped_files));
-
- for (uri = uris, i = 0u; *uri; uri++) {
- error = NULL; /* g_filename_from_uri requires this initialisation */
- path = g_filename_from_uri(*uri, NULL, &error);
- if (!path) {
- weprintf("g_filename_from_uri: %s", error->message);
- g_error_free(error);
- continue;
- }
- dropped_files[i++].path = estrdup(path);
- g_free(path);
- /* the other fields are set by set_images_from_array */
- }
-
- event_window = gtk_widget_get_window(widget);
- if (!event_window)
- goto out;
- if (!gdk_window_get_pointer(event_window, NULL, NULL, &modifiers))
- goto out;
- if ((modifiers & GDK_CONTROL_MASK) && nfiles) {
- files = ereallocarray(files, nfiles + i, sizeof(*files));
- for (j = 0u; j < i; j++) {
- if (load_image(&dropped_files[j], 0))
- files[nfiles++] = dropped_files[j];
- else
- free(dropped_files[j].path);
- }
- free(dropped_files);
- update_status();
- } else {
- set_images_from_array(dropped_files, i);
- }
-
-out:
- gtk_drag_finish(context, 1, 0, time);
- g_strfreev(uris);
-}
-
-static GtkWidget *
-append_toplevel_menu(int add_separator_to_toolbar, const char *name)
-{
- GtkWidget *item, *menu;
-
- if (add_separator_to_toolbar)
- gtk_toolbar_insert(GTK_TOOLBAR(toolbar), gtk_separator_tool_item_new(), -1);
-
- item = gtk_action_create_menu_item(get_action(name));
- menu = gtk_menu_new();
- gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), menu);
- gtk_menu_shell_append(GTK_MENU_SHELL(menubar), item);
- return gtk_menu_item_get_submenu(GTK_MENU_ITEM(item));
-}
-
-static void
-append_separator(int add_to_toolbar, GtkWidget *menu)
-{
- if (add_to_toolbar)
- gtk_toolbar_insert(GTK_TOOLBAR(toolbar), gtk_separator_tool_item_new(), -1);
- if (menu)
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
-}
-
-static void
-append_action(int add_to_toolbar, GtkWidget *menu, const char *name, const char *tooltip)
-{
- GtkWidget *item;
- if (add_to_toolbar) {
- item = gtk_action_create_tool_item(get_action(name));
- gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(item), -1);
- if (tooltip)
- gtk_tool_item_set_tooltip_text(GTK_TOOL_ITEM(item), tooltip);
- }
- if (menu) {
- item = gtk_action_create_menu_item(get_action(name));
- gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
- }
-}
-
-static void
-create_menu_and_toolbar(void)
-{
- GtkWidget *menu;
-
- menubar = gtk_menu_bar_new();
- toolbar = gtk_toolbar_new();
-
-#define X_TOPLEVEL(T, A, B, D) menu = append_toplevel_menu(T, A B) D
-#define X_SEPARTOR(T, D) append_separator(T, menu) D
-#define X_ACTION(T, N, A, B, ICON, ACCEL, ACCEL_TEXT, F, D) append_action(T, menu, A B, A B " (" ACCEL_TEXT ")") D
-#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ACCEL_TEXT, F, G, ACTIVE, D) append_action(T, menu, A B, A B " (" ACCEL_TEXT ")") D
-
- LIST_MENU(X_TOPLEVEL, X_SEPARTOR, X_ACTION, X_TOGGLE, ;);
-
-#undef X_TOPLEVEL
-#undef X_SEPARTOR
-#undef X_ACTION
-#undef X_TOGGLE
-}
-
-static void
-connect_accelerators(GtkAccelGroup *accelerators)
-{
- GList *item, *list;
-
- list = gtk_action_group_list_actions(actions);
- for (item = list; item; item = item->next) {
- gtk_action_set_accel_group(item->data, accelerators);
- gtk_action_connect_accelerator(item->data);
- }
- g_list_free(list);
+ if (image && zoom_mode != ZOOM_CUSTOM && zoom_mode != ZOOM_NORMAL)
+ set_zoom_and_recentre(calculate_zoom_mode(image, zoom_mode));
+ return 0;
}
static int
-activate_fullscreen(void *user)
+initial_draw(void *user)
{
(void) user;
- gtk_toggle_action_set_active(fullscreen, 1);
+ navigate_redraw();
return 0;
}
-#define X_SKIP(...)
-#define X_TOPLEVEL(T, A, B, ...) {A B, NULL, A "_" B, NULL, NULL, NULL} __VA_ARGS__
-#define X_ACTION(T, N, A, B, ICON, ACCEL, ATEXT, F, ...) {A B, ICON, A "_" B, ACCEL, NULL, G_CALLBACK(F)} __VA_ARGS__
-#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ATEXT, F, G, ACTIVE, ...) {A B, ICON, A "_" B, ACCEL, NULL, G_CALLBACK(F), ACTIVE} __VA_ARGS__
-
-static GtkActionEntry entries[] = {
- LIST_MENU(X_TOPLEVEL, X_SKIP, X_ACTION, X_SKIP, COMMA)
-};
-
-static GtkToggleActionEntry toggle_entries[] = {
- LIST_MENU(X_SKIP, X_SKIP, X_SKIP, X_TOGGLE, COMMA)
-};
-
-#undef X_SKIP
-#undef X_TOPLEVEL
-#undef X_ACTION
-#undef X_TOGGLE
-
int
main(int argc, char *argv[])
{
- struct passwd *pwd;
- GtkAccelGroup *accelerators;
- GtkWidget *box;
- size_t i;
- int failed = 0;
int start_fullscreen = 0;
+ int failed = 0;
ARGBEGIN {
case 'f':
@@ -1456,97 +421,32 @@ main(int argc, char *argv[])
usage();
} ARGEND;
- nfiles = (size_t)argc;
- if (nfiles) {
- files = ecalloc(nfiles, sizeof(*files));
- for (i = 0u; i < nfiles; i++) {
- files[i].path = estrdup(argv[i]);
- files[i].image = NULL;
- if (!load_image(&files[i], 1)) {
- free(files[i].path);
- failed = 1;
- }
- }
- }
- if (failed)
+ if (!set_files_from_argv(argc, argv))
goto fail;
- home_directory = getenv("HOME");
- if (!home_directory || !*home_directory) {
- pwd = getpwuid(getuid());
- if (pwd && pwd->pw_dir && *pwd->pw_dir)
- home_directory = pwd->pw_dir;
- else
- home_directory = ".";
- }
-
gtk_init(NULL, NULL);
- populate_filters();
-
- window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
- gtk_window_set_title(GTK_WINDOW(window), "gbild");
- gtk_window_set_icon_name(GTK_WINDOW(window), "image-viewer");
- gtk_window_set_default_size(GTK_WINDOW(window), 900, 700);
- gtk_window_set_wmclass(GTK_WINDOW(window), "gbild", "gbild");
-
- g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
- g_signal_connect(window, "key-press-event", G_CALLBACK(key_pressed), NULL);
- g_signal_connect(window, "window-state-event", G_CALLBACK(window_state_changed), NULL);
-
- actions = gtk_action_group_new("gbild");
- gtk_action_group_add_actions(actions, entries, ELEMSOF(entries), NULL);
- gtk_action_group_add_toggle_actions(actions, toggle_entries, ELEMSOF(toggle_entries), NULL);
- fullscreen = GTK_TOGGLE_ACTION(gtk_action_group_get_action(actions, "Fullscreen"));
- accelerators = gtk_accel_group_new();
- gtk_window_add_accel_group(GTK_WINDOW(window), accelerators);
- connect_accelerators(accelerators);
- create_menu_and_toolbar();
-
- box = gtk_vbox_new(0, 0);
- gtk_container_add(GTK_CONTAINER(window), box);
- gtk_box_pack_start(GTK_BOX(box), menubar, 0, 0, 0u);
- gtk_box_pack_start(GTK_BOX(box), toolbar, 0, 0, 0u);
- display_area = gtk_image_new();
- scroll = gtk_scrolled_window_new(NULL, NULL);
- gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_ALWAYS, GTK_POLICY_ALWAYS);
- gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scroll), display_area);
- gtk_widget_add_events(scroll, GDK_SCROLL_MASK);
- g_signal_connect(scroll, "scroll-event", G_CALLBACK(scroll_event), NULL);
- gtk_box_pack_start(GTK_BOX(box), scroll, 1, 1, 0u);
- statusbar = gtk_statusbar_new();
- gtk_box_pack_end(GTK_BOX(box), statusbar, 0, 0, 0u);
-
- gtk_widget_add_events(display_area, GDK_BUTTON_PRESS_MASK |
- GDK_BUTTON_RELEASE_MASK |
- GDK_POINTER_MOTION_MASK);
- g_signal_connect(display_area, "button-press-event", G_CALLBACK(button_pressed), NULL);
- g_signal_connect(display_area, "button-release-event", G_CALLBACK(button_released), NULL);
- g_signal_connect(display_area, "motion-notify-event", G_CALLBACK(pointer_moved), NULL);
-
- gtk_drag_source_set(display_area, GDK_BUTTON1_MASK, &image_target, 1u, GDK_ACTION_COPY);
- g_signal_connect(display_area, "drag-data-get", G_CALLBACK(drag_data_get), NULL);
+ set_up_ui();
+ set_up_dragging();
+ set_up_dropping();
- gtk_drag_dest_set(window, GTK_DEST_DEFAULT_ALL, &image_target, 1u, GDK_ACTION_COPY);
- g_signal_connect(window, "drag-data-received", G_CALLBACK(drop_files), NULL);
-
- show_image();
- update_status();
set_actions_sensitive();
gtk_widget_show_all(window);
+ g_idle_add(&initial_draw, NULL);
if (start_fullscreen && nfiles)
- g_idle_add(activate_fullscreen, NULL);
+ g_idle_add(&activate_fullscreen, NULL);
gtk_main();
- g_object_unref(actions);
if (transformed_image)
g_object_unref(transformed_image);
-fail:
+ free_ui();
+out:
free_files();
- free(suffixes);
- while (nfilters)
- free(filters[--nfilters].name);
- free(filters);
+ free_image_filters();
return failed;
+
+fail:
+ failed = 1;
+ goto out;
}
diff --git a/image.c b/image.c
new file mode 100644
index 0000000..ebfb778
--- /dev/null
+++ b/image.c
@@ -0,0 +1,35 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+GdkPixbuf *transformed_image = NULL;
+
+static void
+forget_transformed_image(void)
+{
+ if (transformed_image) {
+ g_object_unref(transformed_image);
+ transformed_image = NULL;
+ }
+}
+
+GdkPixbuf *
+get_image(void)
+{
+ struct file *file = get_file();
+ if (!file || !file->image) {
+ forget_transformed_image();
+ return NULL;
+ }
+ return transformed_image ? transformed_image : file->image;
+}
+
+GdkPixbuf *
+get_original_image(void)
+{
+ struct file *file = get_file();
+ if (!file || !file->image) {
+ forget_transformed_image();
+ return NULL;
+ }
+ return file->image;
+}
diff --git a/logical-scroll-pane.c b/logical-scroll-pane.c
new file mode 100644
index 0000000..394cfb6
--- /dev/null
+++ b/logical-scroll-pane.c
@@ -0,0 +1,430 @@
+#include "common.h"
+
+G_DEFINE_TYPE(LogicalScrollPane, logical_scroll_pane, GTK_TYPE_BIN)
+
+static void
+logical_scroll_pane_queue_child_draw(GtkAdjustment *adjustment, LogicalScrollPane *this)
+{
+ (void)adjustment;
+ if (this->child)
+ gtk_widget_queue_draw(this->child);
+}
+
+static void
+logical_scroll_pane_configure_adjustment(GtkAdjustment *adjustment, int content_size, int page_size)
+{
+ double value, limit;
+
+ page_size = MAX(page_size, 1);
+
+ limit = (double)MAX(content_size - page_size, 0);
+ value = MAX(0.0, adjustment->value);
+ value = MIN(value, limit);
+
+ gtk_adjustment_configure(adjustment, value, 0, MAX(content_size, page_size),
+ page_size * 0.1, page_size * 0.9, page_size);
+}
+
+static void
+logical_scroll_pane_destroy(GtkObject *this_)
+{
+ LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_);
+
+ if (this->child) gtk_widget_unparent(this->child);
+ if (this->hscrollbar) gtk_widget_unparent(this->hscrollbar);
+ if (this->vscrollbar) gtk_widget_unparent(this->vscrollbar);
+ if (this->hadjustment) g_object_unref(this->hadjustment);
+ if (this->vadjustment) g_object_unref(this->vadjustment);
+
+ this->child = NULL;
+ this->hscrollbar = NULL;
+ this->vscrollbar = NULL;
+ this->hadjustment = NULL;
+ this->vadjustment = NULL;
+
+ GTK_OBJECT_CLASS(logical_scroll_pane_parent_class)->destroy(this_);
+}
+
+static void
+logical_scroll_pane_size_request(GtkWidget *this_, GtkRequisition *requisition)
+{
+ LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_);
+ GtkRequisition hreq;
+ GtkRequisition vreq;
+
+ requisition->width = hreq.width = vreq.width = 0;
+ requisition->height = hreq.height = vreq.height = 0;
+
+ if (this->hscrollbar && this->hscrollbar_visible) gtk_widget_size_request(this->hscrollbar, &hreq);
+ if (this->vscrollbar && this->vscrollbar_visible) gtk_widget_size_request(this->vscrollbar, &vreq);
+
+ if (this->vscrollbar_visible) requisition->width += vreq.width;
+ if (this->hscrollbar_visible) requisition->height += hreq.height;
+
+ requisition->width = MAX(requisition->width, hreq.width);
+ requisition->height = MAX(requisition->height, vreq.height);
+}
+
+static void
+logical_scroll_pane_size_allocate(GtkWidget *this_, GtkAllocation *allocation)
+{
+ LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_);
+
+ GtkRequisition hreq;
+ GtkRequisition vreq;
+
+ GtkAllocation child_allocation;
+ GtkAllocation hscrollbar_allocation;
+ GtkAllocation vscrollbar_allocation;
+
+ int content_width;
+ int content_height;
+ double x, y, max;
+
+ this_->allocation = *allocation;
+
+ x = this->hadjustment->value + 0.5 * this->hadjustment->page_size;
+ y = this->vadjustment->value + 0.5 * this->vadjustment->page_size;
+
+ hreq.width = hreq.height = 0;
+ vreq.width = vreq.height = 0;
+
+ if (this->hscrollbar && this->hscrollbar_visible) gtk_widget_size_request(this->hscrollbar, &hreq);
+ if (this->vscrollbar && this->vscrollbar_visible) gtk_widget_size_request(this->vscrollbar, &vreq);
+
+ content_width = allocation->width;
+ if (this->vscrollbar_visible)
+ content_width -= vreq.width;
+ content_width = MAX(content_width, 1);
+
+ content_height = allocation->height;
+ if (this->hscrollbar_visible)
+ content_height -= hreq.height;
+ content_height = MAX(content_height, 1);
+
+ logical_scroll_pane_configure_adjustment(this->hadjustment, this->content_width, content_width);
+ logical_scroll_pane_configure_adjustment(this->vadjustment, this->content_height, content_height);
+
+ max = this->hadjustment->upper - this->hadjustment->page_size;
+ x -= 0.5 * this->hadjustment->page_size;
+ x = MIN(MAX(this->hadjustment->lower, x), max);
+ gtk_adjustment_set_value(this->hadjustment, x);
+
+ max = this->vadjustment->upper - this->vadjustment->page_size;
+ y -= 0.5 * this->vadjustment->page_size;
+ y = MIN(MAX(this->vadjustment->lower, y), max);
+ gtk_adjustment_set_value(this->vadjustment, y);
+
+ if (this->child && GTK_WIDGET_VISIBLE(this->child)) {
+ child_allocation.x = allocation->x;
+ child_allocation.y = allocation->y;
+ child_allocation.width = content_width;
+ child_allocation.height = content_height;
+ gtk_widget_size_allocate(this->child, &child_allocation);
+ }
+
+ if (this->hscrollbar && this->hscrollbar_visible) {
+ hscrollbar_allocation.x = allocation->x;
+ hscrollbar_allocation.y = allocation->y + content_height;
+ hscrollbar_allocation.width = content_width;
+ hscrollbar_allocation.height = hreq.height;
+ gtk_widget_size_allocate(this->hscrollbar, &hscrollbar_allocation);
+ }
+
+ if (this->vscrollbar && this->vscrollbar_visible) {
+ vscrollbar_allocation.x = allocation->x + content_width;
+ vscrollbar_allocation.y = allocation->y;
+ vscrollbar_allocation.width = vreq.width;
+ vscrollbar_allocation.height = content_height;
+ gtk_widget_size_allocate(this->vscrollbar, &vscrollbar_allocation);
+ }
+}
+
+static void
+logical_scroll_pane_add(GtkContainer *this_, GtkWidget *widget)
+{
+ LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_);
+ if (this->child) {
+ g_warning("LogicalScrollPane already has a child");
+ return;
+ }
+ logical_scroll_pane_set_child(this, widget);
+}
+
+static void
+logical_scroll_pane_remove(GtkContainer *this_, GtkWidget *widget)
+{
+ LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_);
+
+ if (widget == this->child) {
+ this->child = NULL;
+ GTK_BIN(this)->child = NULL;
+ } else if (widget == this->hscrollbar) {
+ this->hscrollbar = NULL;
+ } else if (widget == this->vscrollbar) {
+ this->vscrollbar = NULL;
+ } else {
+ g_warning("Widget is not a child of LogicalScrollPane");
+ return;
+ }
+
+ gtk_widget_unparent(widget);
+ gtk_widget_queue_resize(GTK_WIDGET(this));
+}
+
+static void
+logical_scroll_pane_forall(GtkContainer *this_, int include_internals, GtkCallback callback, void *callback_data)
+{
+ LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_);
+
+ if (this->child) (*callback)(this->child, callback_data);
+
+ if (!include_internals)
+ return;
+
+ if (this->hscrollbar) (*callback)(this->hscrollbar, callback_data);
+ if (this->vscrollbar) (*callback)(this->vscrollbar, callback_data);
+}
+
+static void
+set_adjustment(LogicalScrollPane *this, GtkAdjustment *adjustment, GtkAdjustment **slot, GtkWidget *scrollbar)
+{
+ if (!adjustment)
+ adjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 0.0, 1.0, 1.0, 0.0));
+
+ if (*slot != adjustment) {
+ g_object_ref_sink(adjustment);
+
+ if (*slot) {
+ g_signal_handlers_disconnect_by_data(*slot, this);
+ g_object_unref(*slot);
+ }
+
+ *slot = adjustment;
+ g_signal_connect(*slot, "value-changed",
+ G_CALLBACK(&logical_scroll_pane_queue_child_draw), this);
+ }
+
+ if (scrollbar)
+ gtk_range_set_adjustment(GTK_RANGE(scrollbar), adjustment);
+}
+
+static void
+logical_scroll_pane_set_adjustment(LogicalScrollPane *this, int horizontal, GtkAdjustment *adjustment)
+{
+ if (horizontal)
+ set_adjustment(this, adjustment, &this->hadjustment, this->hscrollbar);
+ else
+ set_adjustment(this, adjustment, &this->vadjustment, this->vscrollbar);
+}
+
+static void
+logical_scroll_pane_class_init(LogicalScrollPaneClass *class)
+{
+ GtkObjectClass *object_class = GTK_OBJECT_CLASS(class);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS(class);
+
+ object_class->destroy = &logical_scroll_pane_destroy;
+
+ widget_class->size_request = &logical_scroll_pane_size_request;
+ widget_class->size_allocate = &logical_scroll_pane_size_allocate;
+
+ container_class->add = &logical_scroll_pane_add;
+ container_class->remove = &logical_scroll_pane_remove;
+ container_class->forall = &logical_scroll_pane_forall;
+}
+
+static void
+logical_scroll_pane_init(LogicalScrollPane *this)
+{
+ this->child = NULL;
+ this->content_width = 1;
+ this->content_height = 1;
+ this->hscrollbar_visible = 1;
+ this->vscrollbar_visible = 1;
+
+ this->hadjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, /* value */
+ 0.0, /* lower */
+ 0.0, /* upper */
+ 1.0, /* step increment */
+ 1.0, /* page increment */
+ 0.0)); /* page size */
+
+ this->vadjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 0.0, 1.0, 1.0, 0.0));
+
+ g_object_ref_sink(this->hadjustment);
+ g_object_ref_sink(this->vadjustment);
+ g_signal_connect(this->hadjustment, "value-changed",
+ G_CALLBACK(&logical_scroll_pane_queue_child_draw), this);
+ g_signal_connect(this->vadjustment, "value-changed",
+ G_CALLBACK(&logical_scroll_pane_queue_child_draw), this);
+
+ this->hscrollbar = gtk_hscrollbar_new(this->hadjustment);
+ this->vscrollbar = gtk_vscrollbar_new(this->vadjustment);
+
+ gtk_widget_set_parent(this->hscrollbar, GTK_WIDGET(this));
+ gtk_widget_set_parent(this->vscrollbar, GTK_WIDGET(this));
+
+ gtk_widget_show(this->hscrollbar);
+ gtk_widget_show(this->vscrollbar);
+}
+
+GtkWidget *
+logical_scroll_pane_new(void)
+{
+ return GTK_WIDGET(g_object_new(LOGICAL_TYPE_SCROLL_PANE, NULL));
+}
+
+void
+logical_scroll_pane_set_child(LogicalScrollPane *this, GtkWidget *child)
+{
+ if (child == this->child)
+ return;
+
+ if (this->child) {
+ gtk_widget_unparent(this->child);
+ this->child = NULL;
+ GTK_BIN(this)->child = NULL;
+ }
+
+ if (child) {
+ this->child = child;
+ GTK_BIN(this)->child = child;
+ gtk_widget_set_parent(child, GTK_WIDGET(this));
+ if (GTK_WIDGET_VISIBLE(this))
+ gtk_widget_show(child);
+ }
+
+ gtk_widget_queue_resize(GTK_WIDGET(this));
+}
+
+GtkWidget *
+logical_scroll_pane_get_child(LogicalScrollPane *this)
+{
+ return this->child;
+}
+
+void
+logical_scroll_pane_set_content_size(LogicalScrollPane *this, int width, int height)
+{
+ this->content_width = MAX(1, width);
+ this->content_height = MAX(1, height);
+ logical_scroll_pane_configure_adjustment(this->hadjustment, this->content_width, (int)this->hadjustment->page_size);
+ logical_scroll_pane_configure_adjustment(this->vadjustment, this->content_height, (int)this->vadjustment->page_size);
+ gtk_widget_queue_resize(GTK_WIDGET(this));
+ if (this->child)
+ gtk_widget_queue_draw(this->child);
+}
+
+static void
+set_scrollbar(LogicalScrollPane *this, GtkWidget *scrollbar, GtkWidget **slot, int *scrollbar_visible)
+{
+ if (scrollbar == *slot)
+ return;
+
+ if (scrollbar) {
+ if (*slot)
+ gtk_widget_unparent(*slot);
+ *slot = scrollbar;
+ gtk_widget_set_parent(scrollbar, GTK_WIDGET(this));
+
+ if (*scrollbar_visible)
+ gtk_widget_show(scrollbar);
+ } else {
+ if (*slot) {
+ gtk_widget_unparent(*slot);
+ *slot = NULL;
+ }
+ }
+
+ gtk_widget_queue_resize(GTK_WIDGET(this));
+}
+
+void
+logical_scroll_pane_set_hscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar)
+{
+ set_scrollbar(this, scrollbar, &this->hscrollbar, &this->hscrollbar_visible);
+}
+
+void
+logical_scroll_pane_set_vscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar)
+{
+ set_scrollbar(this, scrollbar, &this->vscrollbar, &this->vscrollbar_visible);
+}
+
+GtkWidget *
+logical_scroll_pane_get_hscrollbar(LogicalScrollPane *this)
+{
+ return this->hscrollbar;
+}
+
+GtkWidget *
+logical_scroll_pane_get_vscrollbar(LogicalScrollPane *this)
+{
+ return this->vscrollbar;
+}
+
+static void
+set_scrollbar_visible(LogicalScrollPane *this, int visible, GtkWidget **scrollbar, int *scrollbar_visible)
+{
+ visible = visible != 0;
+
+ if (*scrollbar_visible == visible)
+ return;
+
+ *scrollbar_visible = visible;
+
+ if (*scrollbar)
+ gtk_widget_set_visible(*scrollbar, visible);
+
+ gtk_widget_queue_resize(GTK_WIDGET(this));
+}
+
+void
+logical_scroll_pane_set_hscrollbar_visible(LogicalScrollPane *this, int visible)
+{
+ set_scrollbar_visible(this, visible, &this->hscrollbar, &this->hscrollbar_visible);
+}
+
+void
+logical_scroll_pane_set_vscrollbar_visible(LogicalScrollPane *this, int visible)
+{
+ set_scrollbar_visible(this, visible, &this->vscrollbar, &this->vscrollbar_visible);
+}
+
+int
+logical_scroll_pane_get_hscrollbar_visible(LogicalScrollPane *this)
+{
+ return this->hscrollbar_visible;
+}
+
+int
+logical_scroll_pane_get_vscrollbar_visible(LogicalScrollPane *this)
+{
+ return this->vscrollbar_visible;
+}
+
+void
+logical_scroll_pane_set_hadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment)
+{
+ logical_scroll_pane_set_adjustment(this, 1, adjustment);
+}
+
+void
+logical_scroll_pane_set_vadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment)
+{
+ logical_scroll_pane_set_adjustment(this, 0, adjustment);
+}
+
+GtkAdjustment *
+logical_scroll_pane_get_hadjustment(LogicalScrollPane *this)
+{
+ return this->hadjustment;
+}
+
+GtkAdjustment *
+logical_scroll_pane_get_vadjustment(LogicalScrollPane *this)
+{
+ return this->vadjustment;
+}
diff --git a/logical-scroll-pane.h b/logical-scroll-pane.h
new file mode 100644
index 0000000..c38ca6a
--- /dev/null
+++ b/logical-scroll-pane.h
@@ -0,0 +1,55 @@
+#include <gtk/gtk.h>
+
+#define LOGICAL_TYPE_SCROLL_PANE (logical_scroll_pane_get_type())
+#define LOGICAL_SCROLL_PANE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), LOGICAL_TYPE_SCROLL_PANE, LogicalScrollPane))
+#define LOGICAL_IS_SCROLL_PANE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), LOGICAL_TYPE_SCROLL_PANE))
+
+typedef struct _LogicalScrollPane LogicalScrollPane;
+typedef struct _LogicalScrollPaneClass LogicalScrollPaneClass;
+
+struct _LogicalScrollPane {
+ GtkBin parent_instance;
+
+ GtkWidget *child;
+ GtkWidget *hscrollbar;
+ GtkWidget *vscrollbar;
+
+ GtkAdjustment *hadjustment;
+ GtkAdjustment *vadjustment;
+
+ int content_width;
+ int content_height;
+ int hscrollbar_visible;
+ int vscrollbar_visible;
+};
+
+struct _LogicalScrollPaneClass {
+ GtkBinClass parent_class;
+};
+
+PURE GType logical_scroll_pane_get_type(void);
+
+GtkWidget *logical_scroll_pane_new(void);
+
+void logical_scroll_pane_set_child(LogicalScrollPane *this, GtkWidget *child);
+PURE GtkWidget *logical_scroll_pane_get_child(LogicalScrollPane *this);
+
+void logical_scroll_pane_set_content_size(LogicalScrollPane *this, int width, int height);
+
+void logical_scroll_pane_set_hscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar);
+void logical_scroll_pane_set_vscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar);
+
+PURE GtkWidget *logical_scroll_pane_get_hscrollbar(LogicalScrollPane *this);
+PURE GtkWidget *logical_scroll_pane_get_vscrollbar(LogicalScrollPane *this);
+
+void logical_scroll_pane_set_hscrollbar_visible(LogicalScrollPane *this, int visible);
+void logical_scroll_pane_set_vscrollbar_visible(LogicalScrollPane *this, int visible);
+
+PURE int logical_scroll_pane_get_hscrollbar_visible(LogicalScrollPane *this);
+PURE int logical_scroll_pane_get_vscrollbar_visible(LogicalScrollPane *this);
+
+void logical_scroll_pane_set_hadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment);
+void logical_scroll_pane_set_vadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment);
+
+PURE GtkAdjustment *logical_scroll_pane_get_hadjustment(LogicalScrollPane *this);
+PURE GtkAdjustment *logical_scroll_pane_get_vadjustment(LogicalScrollPane *this);
diff --git a/opening.c b/opening.c
new file mode 100644
index 0000000..3e98740
--- /dev/null
+++ b/opening.c
@@ -0,0 +1,173 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+int
+load_image(struct file *file, int errors_to_stderr)
+{
+ const char *error_message;
+ GError *error = NULL;
+ GtkWidget *dialog;
+ GdkPixbuf *image;
+ GdkPixbufLoader *loader = NULL;
+ struct stat st;
+ unsigned char buffer[8 << 10];
+ ssize_t r;
+ int fd, ret = 1;
+
+ if (!file)
+ return 0;
+
+ fd = open(file->path, O_RDONLY | O_NOCTTY);
+ if (fd < 0)
+ goto fail_errno;
+
+ if (fstat(fd, &st))
+ goto fail_errno;
+
+ if (!S_ISREG(st.st_mode)) {
+ error_message = "Not a regular file";
+ goto fail;
+ }
+
+ loader = gdk_pixbuf_loader_new();
+ if (!loader)
+ goto fail_errno;
+ for (;;) {
+ r = read(fd, buffer, sizeof(buffer));
+ if (r <= 0) {
+ if (!r)
+ break;
+ if (errno == EINTR)
+ continue;
+ goto fail_errno;
+ }
+ if (!gdk_pixbuf_loader_write(loader, buffer, (size_t)r, &error))
+ goto fail_gerror;
+ }
+ if (!gdk_pixbuf_loader_close(loader, &error)) {
+ fail_gerror:
+ error_message = error->message;
+ goto fail;
+ }
+ image = gdk_pixbuf_loader_get_pixbuf(loader);
+ if (!image) {
+ error_message = "Unsupported image format";
+ goto fail;
+ }
+ g_object_ref(image);
+
+ g_clear_object(&file->image);
+
+ file->size = st.st_size;
+ file->image = image;
+
+out:
+ g_clear_object(&loader);
+ if (fd >= 0)
+ close(fd);
+ return ret;
+
+fail_errno:
+ error_message = strerror(errno);
+
+fail:
+ if (errors_to_stderr) {
+ weprintf("Cannot open %s: %s", file->path, error_message);
+ } else {
+ dialog = gtk_message_dialog_new(GTK_WINDOW(window), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR,
+ GTK_BUTTONS_CLOSE, "Cannot open %s", file->path);
+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), "%s", error_message);
+ gtk_dialog_run(GTK_DIALOG(dialog));
+ gtk_widget_destroy(dialog);
+ }
+
+ g_clear_error(&error);
+ ret = 0;
+ goto out;
+}
+
+static void
+preview_image(GtkFileChooser *chooser, GtkWidget *preview_widget)
+{
+ char *filename;
+ GdkPixbuf *preview;
+ GtkWidget *image, *label;
+
+ filename = gtk_file_chooser_get_preview_filename(chooser);
+ preview = !filename ? NULL :
+ gdk_pixbuf_new_from_file_at_scale(filename, PREVIEW_WIDTH, PREVIEW_HEIGHT, 1, NULL);
+ image = g_object_get_data(G_OBJECT(preview_widget), "image");
+ label = g_object_get_data(G_OBJECT(preview_widget), "label");
+ gtk_image_set_from_pixbuf(GTK_IMAGE(image), preview);
+ if (preview) {
+ gtk_widget_show(image);
+ gtk_widget_hide(label);
+ } else {
+ gtk_widget_hide(image);
+ gtk_widget_show(label);
+ }
+ gtk_file_chooser_set_preview_widget_active(chooser, 1);
+ if (preview)
+ g_object_unref(preview);
+ g_free(filename);
+}
+
+static const char *
+get_home_directory(void)
+{
+ static const char *home_directory = NULL;
+
+ struct passwd *pwd;
+
+ if (home_directory)
+ return home_directory;
+
+ home_directory = getenv("HOME");
+ if (!home_directory || !*home_directory) {
+ pwd = getpwuid(getuid());
+ if (pwd && pwd->pw_dir && *pwd->pw_dir)
+ home_directory = pwd->pw_dir;
+ else
+ home_directory = ".";
+ }
+
+ return home_directory;
+}
+
+GtkFileChooser *
+construct_chooser(void)
+{
+ GtkWidget *widget, *image, *label, *preview;
+ GtkFileChooser *chooser;
+
+ widget = gtk_file_chooser_dialog_new("Open",
+ GTK_WINDOW(window),
+ GTK_FILE_CHOOSER_ACTION_OPEN,
+ GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
+ GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
+ NULL);
+ chooser = GTK_FILE_CHOOSER(widget);
+
+ g_object_set(gtk_settings_get_default(), "gtk-recent-files-enabled", 0, NULL);
+ gtk_file_chooser_set_select_multiple(chooser, 1);
+ gtk_file_chooser_set_current_folder(chooser, get_home_directory());
+ gtk_file_chooser_set_local_only(chooser, 1);
+ gtk_file_chooser_set_do_overwrite_confirmation(chooser, 0);
+
+ preview = gtk_vbox_new(0, 6);
+ image = gtk_image_new();
+ label = gtk_label_new("Preview");
+ gtk_widget_set_size_request(preview, PREVIEW_WIDTH, PREVIEW_HEIGHT);
+ gtk_widget_set_size_request(image, PREVIEW_WIDTH, PREVIEW_HEIGHT);
+ gtk_box_pack_start(GTK_BOX(preview), image, 0, 0, 0u);
+ gtk_box_pack_start(GTK_BOX(preview), label, 0, 0, 0u);
+ g_object_set_data(G_OBJECT(preview), "image", image);
+ g_object_set_data(G_OBJECT(preview), "label", label);
+ gtk_file_chooser_set_preview_widget(chooser, preview);
+ gtk_file_chooser_set_use_preview_label(chooser, 0);
+ gtk_file_chooser_set_preview_widget_active(chooser, 1);
+ gtk_widget_hide(image);
+ g_signal_connect(chooser, "update-preview", G_CALLBACK(&preview_image), preview);
+
+ return chooser;
+}
diff --git a/printing.c b/printing.c
new file mode 100644
index 0000000..e3b5431
--- /dev/null
+++ b/printing.c
@@ -0,0 +1,761 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+#include <math.h>
+
+#define LIST_UNITS(X, ...)\
+ X(UNIT_MILLIMETRES, "Millimetres", 72.0 / 25.4) __VA_ARGS__\
+ X(UNIT_CENTIMETRES, "Centimetres", 72.0 / 2.54) __VA_ARGS__\
+ X(UNIT_INCHES, "Inches", 72.0) __VA_ARGS__\
+ X(UNIT_MODERN_POINTS, "Modern points (1/72 inch)", 1.0) __VA_ARGS__\
+ X(UNIT_TRADITIONAL_POINTS, "Traditional points (1/72.27 inch)", 72.0 / 72.27) __VA_ARGS__\
+ X(UNIT_PERCENT, "Percent of printable area", 0.01)
+
+#define LIST_HORIZONTAL_FIELDS(X, ...)\
+ X(FIELD_WIDTH, "Width") __VA_ARGS__\
+ X(FIELD_LEFT, "Left") __VA_ARGS__\
+ X(FIELD_RIGHT, "Right")
+
+#define LIST_VERTICAL_FIELDS(X, ...)\
+ X(FIELD_HEIGHT, "Height") __VA_ARGS__\
+ X(FIELD_TOP, "Top") __VA_ARGS__\
+ X(FIELD_BOTTOM, "Bottom")
+
+#define LIST_FIELDS(X, ...)\
+ LIST_HORIZONTAL_FIELDS(X, __VA_ARGS__) __VA_ARGS__\
+ LIST_VERTICAL_FIELDS(X, __VA_ARGS__)
+
+enum unit {
+#define X(E, N, S) E
+ LIST_UNITS(X, COMMA)
+#undef X
+};
+
+enum field {
+#define X(E, N) E
+ LIST_FIELDS(X, COMMA)
+#undef X
+};
+
+#define X_COUNT(...) 1u
+#define NFIELDS (LIST_FIELDS(X_COUNT, +))
+
+struct print_data {
+ GdkPixbuf *image;
+ double width;
+ double height;
+ double left;
+ double top;
+};
+
+struct positioning_widget {
+ struct print_data *data;
+ GtkWidget *unit;
+ GtkWidget *entry[NFIELDS];
+ GtkWidget *preview;
+ double page_width;
+ double page_height;
+ double drag_start_x;
+ double drag_start_y;
+ double drag_start_left;
+ double drag_start_top;
+ int updating;
+ int dragging;
+};
+
+#define X(E, T) "_"T":"
+static const char *const field_labels[] = { LIST_FIELDS(X, COMMA) };
+#undef X
+
+static int
+horizontal(enum field field)
+{
+#define X(E, T) if (field == E) return 1
+ LIST_HORIZONTAL_FIELDS(X, ;);
+#undef X
+ return 0;
+}
+
+static double
+unit_in_points(struct positioning_widget *widget, enum field field)
+{
+ enum unit unit = gtk_combo_box_get_active(GTK_COMBO_BOX(widget->unit));
+ double ret;
+ switch (unit) {
+#define X(E, N, S) case E: ret = (S); break
+ LIST_UNITS(X, ;);
+#undef X
+ }
+ if (unit == UNIT_PERCENT)
+ ret *= horizontal(field) ? widget->page_width : widget->page_height;
+ return ret;
+}
+
+static double
+points_to_unit(struct positioning_widget *widget, enum field field, double value)
+{
+ return value / unit_in_points(widget, field);
+}
+
+static double
+unit_to_points(struct positioning_widget *widget, enum field field, double value)
+{
+ return value * unit_in_points(widget, field);
+}
+
+static void
+get_printable_size(GtkPageSetup *setup, double *width, double *height)
+{
+ *width = gtk_page_setup_get_paper_width(setup, GTK_UNIT_POINTS) -
+ gtk_page_setup_get_left_margin(setup, GTK_UNIT_POINTS) -
+ gtk_page_setup_get_right_margin(setup, GTK_UNIT_POINTS);
+
+ *height = gtk_page_setup_get_paper_height(setup, GTK_UNIT_POINTS) -
+ gtk_page_setup_get_top_margin(setup, GTK_UNIT_POINTS) -
+ gtk_page_setup_get_bottom_margin(setup, GTK_UNIT_POINTS);
+
+ *width = MAX(*width, 0.1);
+ *height = MAX(*height, 0.1);
+}
+
+static double
+get_preview_scale(GtkWidget *preview, struct positioning_widget *widget)
+{
+ int width = MAX(preview->allocation.width - 16, 1);
+ int height = MAX(preview->allocation.height - 16, 1);
+ double x = (double)width / widget->page_width;
+ double y = (double)height / widget->page_height;
+ return MIN(x, y);
+}
+
+static double
+get_field_value(struct positioning_widget *widget, enum field field)
+{
+ struct print_data *data = widget->data;
+ switch (field) {
+ case FIELD_WIDTH: return data->width;
+ case FIELD_HEIGHT: return data->height;
+ case FIELD_TOP: return data->top;
+ case FIELD_LEFT: return data->left;
+ case FIELD_RIGHT: return widget->page_width - data->left - data->width;
+ case FIELD_BOTTOM: return widget->page_height - data->top - data->height;
+ }
+ abort();
+}
+
+static int
+set_field_value(struct positioning_widget *widget, enum field field, double value)
+{
+ struct print_data *data = widget->data;
+ int image_width = gdk_pixbuf_get_width(data->image);
+ int image_height = gdk_pixbuf_get_height(data->image);
+ double aspect, pos, size, page, pos_opp, size_opp, page_opp;
+ double new_pos, new_size, new_pos_opp, new_size_opp;
+ double *posp, *pos_oppp, *sizep, *size_oppp, limit;
+
+ switch (field) {
+ case FIELD_WIDTH:
+ case FIELD_LEFT:
+ case FIELD_RIGHT:
+ posp = &data->left;
+ sizep = &data->width;
+ page = widget->page_width;
+ pos_oppp = &data->top;
+ size_oppp = &data->height;
+ page_opp = widget->page_height;
+ aspect = (double)image_height / (double)image_width;
+ break;
+
+ case FIELD_HEIGHT:
+ case FIELD_TOP:
+ case FIELD_BOTTOM:
+ posp = &data->top;
+ sizep = &data->height;
+ page = widget->page_height;
+ pos_oppp = &data->left;
+ size_oppp = &data->width;
+ page_opp = widget->page_width;
+ aspect = (double)image_width / (double)image_height;
+ break;
+ }
+
+ pos = *posp;
+ size = *sizep;
+ pos_opp = *pos_oppp;
+ size_opp = *size_oppp;
+
+ switch (field) {
+ case FIELD_WIDTH:
+ case FIELD_HEIGHT:
+ limit = page_opp / aspect;
+ limit = MIN(page, limit);
+ new_size = MIN(MAX(0.0, value), limit);
+ new_size_opp = new_size * aspect;
+ new_pos = pos + 0.5 * (size - new_size);
+ new_pos = MIN(MAX(0.0, new_pos), page - new_size);
+ new_pos_opp = pos_opp + 0.5 * (size_opp - new_size_opp);
+ new_pos_opp = MIN(MAX(0.0, new_pos_opp), page_opp - new_size_opp);
+ break;
+
+ case FIELD_LEFT:
+ case FIELD_TOP:
+ value = MAX(value, 0.0);
+ new_pos = value;
+ new_size = pos + size - new_pos;
+ new_size_opp = new_size * aspect;
+ if (new_size < 0.0 || new_size_opp > page_opp) {
+ new_size = size;
+ new_size_opp = size_opp;
+ new_pos = MIN(MAX(0.0, value), page - new_size);
+ new_pos_opp = pos_opp;
+ } else {
+ new_pos_opp = pos_opp + 0.5 * (size_opp - new_size_opp);
+ new_pos_opp = MIN(MAX(0.0, new_pos_opp), page_opp - new_size_opp);
+ }
+ break;
+
+ case FIELD_RIGHT:
+ case FIELD_BOTTOM:
+ value = MAX(value, 0.0);
+ new_pos = pos;
+ new_size = page - new_pos - value;
+ new_size_opp = new_size * aspect;
+ if (new_size < 0.0 || new_size_opp > page_opp) {
+ new_size = size;
+ new_size_opp = size_opp;
+ limit = page - new_size;
+ new_pos = limit - value;
+ new_pos = MIN(MAX(0.0, new_pos), limit);
+ new_pos_opp = pos_opp;
+ } else {
+ new_pos_opp = pos_opp + 0.5 * (size_opp - new_size_opp);
+ new_pos_opp = MIN(MAX(0.0, new_pos_opp), page_opp - new_size_opp);
+ }
+ break;
+ }
+
+ *posp = new_pos;
+ *sizep = new_size;
+ *pos_oppp = new_pos_opp;
+ *size_oppp = new_size_opp;
+ return 1;
+}
+
+static enum field
+get_positioning_field(struct positioning_widget *widget, GtkWidget *entry)
+{
+ enum field field = 0u;
+ while (field < NFIELDS && widget->entry[field] != entry)
+ field++;
+ return field;
+}
+
+static int
+positioning_preview_expose(GtkWidget *preview, GdkEventExpose *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ struct print_data *data = widget->data;
+ double scale = get_preview_scale(preview, widget);
+ double w = scale * widget->page_width;
+ double h = scale * widget->page_height;
+ double image_scale;
+ double page_x = preview->allocation.width - 8.0 - w;
+ double page_y = 8.0;
+ int image_width;
+ cairo_t *cr;
+
+ cr = gdk_cairo_create(preview->window);
+
+ cairo_rectangle(cr, event->area.x, event->area.y, event->area.width, event->area.height);
+ cairo_clip(cr);
+ gdk_cairo_set_source_color(cr, &preview->style->bg[GTK_STATE_NORMAL]);
+ cairo_paint(cr);
+
+ cairo_rectangle(cr, page_x, page_y, w, h);
+ cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
+ cairo_fill(cr);
+
+ cairo_save(cr);
+
+ cairo_rectangle(cr, page_x, page_y, w, h);
+ cairo_clip(cr);
+ image_width = gdk_pixbuf_get_width(data->image);
+ image_scale = scale * data->width / (double)image_width;
+ cairo_translate(cr, page_x + scale * data->left, page_y + scale * data->top);
+ cairo_scale(cr, image_scale, image_scale);
+ gdk_cairo_set_source_pixbuf(cr, data->image, 0.0, 0.0);
+ cairo_paint(cr);
+
+ cairo_restore(cr);
+
+ cairo_rectangle(cr, page_x, page_y, w, h);
+ cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
+ cairo_set_line_width(cr, 1.0);
+ cairo_stroke(cr);
+ cairo_destroy(cr);
+ return 0;
+}
+
+static int
+positioning_preview_button_pressed(GtkWidget *preview, GdkEventButton *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ struct print_data *data = widget->data;
+ double scale = get_preview_scale(preview, widget);
+ double page_x = preview->allocation.width - 8.0 - scale * widget->page_width;
+ double page_y = 8.0;
+ double image_x = page_x + scale * data->left;
+ double image_y = page_y + scale * data->top;
+
+ if (event->button != 1 ||
+ image_x > event->x || event->x > image_x + scale * data->width ||
+ image_y > event->y || event->y > image_y + scale * data->height)
+ return 0;
+
+ widget->drag_start_x = event->x;
+ widget->drag_start_y = event->y;
+ widget->drag_start_left = data->left;
+ widget->drag_start_top = data->top;
+ widget->dragging = 1;
+ return 1;
+}
+
+static void
+update_positioning_widget(struct positioning_widget *widget, enum field active_field)
+{
+ struct print_data *data = widget->data;
+ double x, y, value, scale = 1.0;
+ enum field field;
+ char *text;
+ int len, r;
+
+ if (data->width > 0.0 && data->height > 0.0) {
+ x = widget->page_width / data->width;
+ y = widget->page_height / data->height;
+ scale = MIN(x, y);
+ if (scale < 1.0) {
+ data->width *= scale;
+ data->height *= scale;
+ }
+ }
+
+ x = widget->page_width - data->width;
+ y = widget->page_height - data->height;
+ data->left = MIN(MAX(0.0, data->left), x);
+ data->top = MIN(MAX(0.0, data->top), y);
+
+ widget->updating = 1;
+
+ for (field = 0u; field < NFIELDS; field++) {
+ if (field == active_field)
+ continue;
+
+ value = points_to_unit(widget, field, get_field_value(widget, field));
+
+ len = snprintf(NULL, 0u, "%.2f", value);
+ if (len < 0) eprintf("snprintf:");
+
+ text = malloc((size_t)len + 1u);
+ if (!text) abort();
+
+ r = sprintf(text, "%.2f", value);
+ if (r < 0) eprintf("snprintf:");
+ if (r > len) abort();
+
+ gtk_entry_set_text(GTK_ENTRY(widget->entry[field]), text);
+ free(text);
+ }
+
+ widget->updating = 0;
+ if (widget->preview)
+ gtk_widget_queue_draw(widget->preview);
+}
+
+static int
+positioning_preview_motion(GtkWidget *preview, GdkEventMotion *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ struct print_data *data = widget->data;
+ double scale;
+ double left;
+ double top;
+
+ if (!widget->dragging)
+ return 0;
+
+ scale = get_preview_scale(preview, widget);
+ left = widget->drag_start_left + (event->x - widget->drag_start_x) / scale;
+ left = MIN(MAX(0.0, left), widget->page_width - data->width);
+ top = widget->drag_start_top + (event->y - widget->drag_start_y) / scale;
+ top = MIN(MAX(0.0, top), widget->page_height - data->height);
+ data->left = left;
+ data->top = top;
+ update_positioning_widget(widget, NFIELDS);
+ return 1;
+}
+
+static int
+positioning_preview_button_released(GtkWidget *preview, GdkEventButton *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ (void) preview;
+ if (event->button != 1 || !widget->dragging)
+ return 0;
+ widget->dragging = 0;
+ return 1;
+}
+
+static int
+positioning_preview_scrolled(GtkWidget *preview, GdkEventScroll *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ struct print_data *data = widget->data;
+ int image_width, image_height;
+ double aspect, amount, scale, page_x;
+ double pointer_x, pointer_y;
+ double maximum, width, height;
+
+ if (!(event->state & GDK_CONTROL_MASK))
+ return 0;
+
+ switch (event->direction) {
+ case GDK_SCROLL_UP: amount = +0.02; break;
+ case GDK_SCROLL_DOWN: amount = -0.02; break;
+ case GDK_SCROLL_LEFT:
+ case GDK_SCROLL_RIGHT:
+ default:
+ return 0;
+ }
+ amount *= widget->page_width;
+
+ image_width = gdk_pixbuf_get_width(data->image);
+ image_height = gdk_pixbuf_get_height(data->image);
+ aspect = (double)image_width / (double)image_height;
+
+ maximum = MIN(widget->page_width, widget->page_height * aspect);
+ width = CLAMP(data->width + amount, 0.0, maximum);
+ height = width / aspect;
+
+ scale = get_preview_scale(preview, widget);
+ page_x = preview->allocation.width - 8.0 - scale * widget->page_width;
+ pointer_x = (event->x - page_x) / scale;
+ pointer_y = (event->y - 8.0) / scale;
+ if (data->width > 0.0) {
+ data->left = pointer_x - (pointer_x - data->left) * width / data->width;
+ data->top = pointer_y - (pointer_y - data->top) * height / data->height;
+ }
+ data->width = width;
+ data->height = height;
+ update_positioning_widget(widget, NFIELDS);
+ return 1;
+}
+
+static int
+get_entry_value(GtkEntry *entry, double *value)
+{
+ const char *text = gtk_entry_get_text(entry);
+ char *end;
+ double parsed = strtod(text, &end);
+ while (isspace(*(unsigned char *)end))
+ end++;
+ if (end == text || *end || !isfinite(parsed))
+ return 0;
+ *value = parsed;
+ return 1;
+}
+
+static void
+adjust_positioning_field(struct positioning_widget *widget, enum field field, double amount)
+{
+ double value;
+
+ if (field == NFIELDS)
+ return;
+
+ value = get_field_value(widget, field);
+ value = points_to_unit(widget, field, value);
+ value += amount;
+ value = unit_to_points(widget, field, value);
+
+ if (set_field_value(widget, field, value))
+ update_positioning_widget(widget, NFIELDS);
+}
+
+static int
+positioning_entry_key_pressed(GtkWidget *entry, GdkEventKey *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ enum field field = get_positioning_field(widget, entry);
+ double amount;
+ switch (event->keyval) {
+ case GDK_Up: amount = +1.0; break;
+ case GDK_Down: amount = -1.0; break;
+ case GDK_Page_Up: amount = +10.0; break;
+ case GDK_Page_Down: amount = -10.0; break;
+ default:
+ return 0;
+ }
+ adjust_positioning_field(widget, field, amount);
+ return 1;
+}
+
+static int
+positioning_entry_scrolled(GtkWidget *entry, GdkEventScroll *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ enum field field = get_positioning_field(widget, entry);
+ if (event->direction == GDK_SCROLL_UP)
+ adjust_positioning_field(widget, field, +1.0);
+ else if (event->direction == GDK_SCROLL_DOWN)
+ adjust_positioning_field(widget, field, -1.0);
+ else
+ return 0;
+ return 1;
+}
+
+static void
+positioning_entry_inserted(GtkEditable *editable, const char *text, int length, int *position, void *user)
+{
+ (void) position;
+ (void) user;
+ if (memchr(text, '-', (size_t)length))
+ g_signal_stop_emission_by_name(editable, "insert-text");
+}
+
+static void
+positioning_entry_changed(GtkEditable *editable, void *user)
+{
+ struct positioning_widget *widget = user;
+ enum field field;
+ double value;
+
+ if (widget->updating)
+ return;
+
+ for (field = 0; field < NFIELDS; field++)
+ if (GTK_EDITABLE(widget->entry[field]) == editable)
+ goto have_field;
+ return;
+
+have_field:
+ if (get_entry_value(GTK_ENTRY(widget->entry[field]), &value)) {
+ value = unit_to_points(widget, field, value);
+ if (!set_field_value(widget, field, value))
+ field = NFIELDS;
+ update_positioning_widget(widget, field);
+ }
+}
+
+static void
+positioning_unit_changed(GtkComboBox *combo, void *user)
+{
+ struct positioning_widget *widget = user;
+ (void) combo;
+ if (!widget->updating)
+ update_positioning_widget(widget, NFIELDS);
+}
+
+static void
+begin_print(GtkPrintOperation *operation, GtkPrintContext *context, void *user)
+{
+ (void) context;
+ (void) user;
+ gtk_print_operation_set_n_pages(operation, 1);
+}
+
+static int
+positioning_entry_focus_out(GtkWidget *entry, GdkEventFocus *event, void *user)
+{
+ struct positioning_widget *widget = user;
+ (void) entry;
+ (void) event;
+ if (!widget->updating)
+ update_positioning_widget(widget, NFIELDS);
+ return 0;
+}
+
+static GtkWidget *
+create_positioning_widget(GtkPrintOperation *operation, void *user)
+{
+#define X(E, N, S) N
+ static const char *const unit_names[] = {LIST_UNITS(X, COMMA)};
+#undef X
+
+#define X(E, T) E
+ static const enum field field_layout[2][3] = {
+ {LIST_HORIZONTAL_FIELDS(X, COMMA)},
+ {LIST_VERTICAL_FIELDS(X, COMMA)}
+ };
+#undef X
+
+ struct print_data *data = user;
+ struct positioning_widget *positioning;
+ GtkPageSetup *setup;
+ GtkWidget *box, *grid, *label;
+ enum field field;
+ size_t unit;
+ unsigned int row, column;
+
+ positioning = g_new0(struct positioning_widget, 1);
+ positioning->data = data;
+ setup = gtk_print_operation_get_default_page_setup(operation);
+ get_printable_size(setup, &positioning->page_width, &positioning->page_height);
+
+ box = gtk_hbox_new(0, 12);
+ grid = gtk_table_new(4, 4, 0);
+ gtk_container_set_border_width(GTK_CONTAINER(grid), 8);
+ gtk_table_set_row_spacings(GTK_TABLE(grid), 6);
+ gtk_table_set_col_spacings(GTK_TABLE(grid), 6);
+ label = gtk_label_new_with_mnemonic("_Units:");
+ gtk_misc_set_alignment(GTK_MISC(label), 1.0f, 0.5f);
+ gtk_table_attach(GTK_TABLE(grid), label, 0, 1, 0, 1, GTK_FILL, 0, 4u, 4u);
+ positioning->unit = gtk_combo_box_new_text();
+ for (unit = 0; unit < ELEMSOF(unit_names); unit++)
+ gtk_combo_box_append_text(GTK_COMBO_BOX(positioning->unit), unit_names[unit]);
+ gtk_combo_box_set_active(GTK_COMBO_BOX(positioning->unit), UNIT_MILLIMETRES);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(label), positioning->unit);
+ gtk_table_attach(GTK_TABLE(grid), positioning->unit, 1, 4, 0, 1, GTK_EXPAND | GTK_FILL, 0, 4u, 4u);
+
+ for (field = 0; field < NFIELDS; field++) {
+ positioning->entry[field] = gtk_entry_new();
+ gtk_entry_set_width_chars(GTK_ENTRY(positioning->entry[field]), 12);
+ gtk_widget_add_events(positioning->entry[field], GDK_SCROLL_MASK);
+ g_signal_connect(positioning->entry[field], "changed",
+ G_CALLBACK(&positioning_entry_changed), positioning);
+ g_signal_connect(positioning->entry[field], "focus-out-event",
+ G_CALLBACK(&positioning_entry_focus_out), positioning);
+ g_signal_connect(positioning->entry[field], "insert-text",
+ G_CALLBACK(&positioning_entry_inserted), positioning);
+ g_signal_connect(positioning->entry[field], "key-press-event",
+ G_CALLBACK(&positioning_entry_key_pressed), positioning);
+ g_signal_connect(positioning->entry[field], "scroll-event",
+ G_CALLBACK(&positioning_entry_scrolled), positioning);
+ }
+ for (row = 0; row < 3; row++) {
+ for (column = 0; column < 2; column++) {
+ field = field_layout[column][row];
+ label = gtk_label_new_with_mnemonic(field_labels[field]);
+ gtk_misc_set_alignment(GTK_MISC(label), 1.0f, 0.5f);
+ gtk_label_set_mnemonic_widget(GTK_LABEL(label), positioning->entry[field]);
+ gtk_table_attach(GTK_TABLE(grid), label, 2 * column, 2 * column + 1,
+ row + 1, row + 2, GTK_FILL, 0, 4u, 4u);
+ gtk_table_attach(GTK_TABLE(grid), positioning->entry[field],
+ 2 * column + 1, 2 * column + 2, row + 1, row + 2,
+ GTK_FILL, 0, 4u, 4u);
+ }
+ }
+ positioning->preview = gtk_drawing_area_new();
+ gtk_widget_set_size_request(positioning->preview, 200, 200);
+ gtk_widget_add_events(positioning->preview,
+ GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK);
+ g_signal_connect(positioning->preview, "expose-event",
+ G_CALLBACK(&positioning_preview_expose), positioning);
+ g_signal_connect(positioning->preview, "button-press-event",
+ G_CALLBACK(&positioning_preview_button_pressed), positioning);
+ g_signal_connect(positioning->preview, "motion-notify-event",
+ G_CALLBACK(&positioning_preview_motion), positioning);
+ g_signal_connect(positioning->preview, "scroll-event",
+ G_CALLBACK(&positioning_preview_scrolled), positioning);
+ g_signal_connect(positioning->preview, "button-release-event",
+ G_CALLBACK(&positioning_preview_button_released), positioning);
+ gtk_box_pack_start(GTK_BOX(box), grid, 0, 0, 0u);
+ gtk_box_pack_start(GTK_BOX(box), positioning->preview, 1, 1, 0u);
+ g_signal_connect(positioning->unit, "changed", G_CALLBACK(&positioning_unit_changed), positioning);
+ g_object_set_data_full(G_OBJECT(box), "positioning", positioning, g_free);
+ update_positioning_widget(positioning, NFIELDS);
+ gtk_widget_show_all(box);
+ return box;
+}
+
+static void
+apply_print_positioning(GtkPrintOperation *operation, GtkWidget *widget, void *user)
+{
+ struct positioning_widget *positioning = g_object_get_data(G_OBJECT(widget), "positioning");
+ (void) operation;
+ (void) user;
+ if (positioning)
+ update_positioning_widget(positioning, NFIELDS);
+}
+
+static void
+update_print_positioning(GtkPrintOperation *operation, GtkWidget *widget, GtkPageSetup *setup,
+ GtkPrintSettings *settings, void *user)
+{
+ struct positioning_widget *positioning = g_object_get_data(G_OBJECT(widget), "positioning");
+ (void) operation;
+ (void) settings;
+ (void) user;
+ if (positioning) {
+ get_printable_size(setup, &positioning->page_width, &positioning->page_height);
+ update_positioning_widget(positioning, NFIELDS);
+ }
+}
+
+static void
+draw_page(GtkPrintOperation *operation, GtkPrintContext *context, int page, void *user)
+{
+ struct print_data *data = user;
+ GdkPixbuf *image = data->image;
+ double scale;
+ cairo_t *cr;
+
+ (void) operation;
+ (void) page;
+
+ scale = data->width / gdk_pixbuf_get_width(image);
+ cr = gtk_print_context_get_cairo_context(context);
+ cairo_translate(cr, data->left, data->top);
+ cairo_scale(cr, scale, scale);
+ gdk_cairo_set_source_pixbuf(cr, image, 0.0, 0.0);
+ cairo_paint(cr);
+}
+
+void
+open_print(GdkPixbuf *image)
+{
+ struct print_data data;
+ GtkPrintOperation *operation;
+ GtkPageSetup *setup;
+ GtkPageOrientation orientation = GTK_PAGE_ORIENTATION_PORTRAIT;
+ double page_width, page_height, s, x, y;
+ int width, height;
+
+ if (!image)
+ return;
+
+ data.image = image;
+ g_object_ref(data.image);
+
+ width = gdk_pixbuf_get_width(data.image);
+ height = gdk_pixbuf_get_height(data.image);
+
+ if (width > height)
+ orientation = GTK_PAGE_ORIENTATION_LANDSCAPE;
+
+ setup = gtk_page_setup_new();
+ gtk_page_setup_set_orientation(setup, orientation);
+ get_printable_size(setup, &page_width, &page_height);
+
+ x = page_width / (double)width;
+ y = page_height / (double)height;
+ s = MIN(x, y);
+ data.width = s * width;
+ data.height = s * height;
+ data.left = 0.5 * (page_width - data.width);
+ data.top = 0.5 * (page_height - data.height);
+
+ operation = gtk_print_operation_new();
+ gtk_print_operation_set_default_page_setup(operation, setup);
+ g_object_unref(setup);
+ gtk_print_operation_set_embed_page_setup(operation, 1);
+ gtk_print_operation_set_custom_tab_label(operation, "Positioning");
+
+ g_signal_connect(operation, "create-custom-widget", G_CALLBACK(&create_positioning_widget), &data);
+ g_signal_connect(operation, "custom-widget-apply", G_CALLBACK(&apply_print_positioning), &data);
+ g_signal_connect(operation, "update-custom-widget", G_CALLBACK(&update_print_positioning), &data);
+ g_signal_connect(operation, "begin-print", G_CALLBACK(&begin_print), &data);
+ g_signal_connect(operation, "draw-page", G_CALLBACK(&draw_page), &data);
+
+ gtk_print_operation_run(operation, GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW(window), NULL);
+ g_object_unref(operation);
+ g_object_unref(data.image);
+}
diff --git a/properties.c b/properties.c
new file mode 100644
index 0000000..b9ebce7
--- /dev/null
+++ b/properties.c
@@ -0,0 +1,28 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+void
+open_properties(struct file *file)
+{
+ /* TODO What shall "Properties" display? */
+
+ const char *name;
+ GtkWidget *dialog;
+
+ if (!file || !file->path || !file->image)
+ return;
+
+ name = strrchr(file->path, '/');
+ name = name ? &name[1u] : file->path;
+
+ dialog = gtk_message_dialog_new(GTK_WINDOW(window), 0, GTK_MESSAGE_INFO,
+ GTK_BUTTONS_CLOSE, "%s", name);
+
+ gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
+ "%i × %i pixels",
+ gdk_pixbuf_get_width(file->image),
+ gdk_pixbuf_get_height(file->image));
+
+ g_signal_connect_swapped(dialog, "response", G_CALLBACK(&gtk_widget_destroy), dialog);
+ gtk_widget_show(dialog);
+}
diff --git a/transform.c b/transform.c
new file mode 100644
index 0000000..b9461d7
--- /dev/null
+++ b/transform.c
@@ -0,0 +1,172 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+enum transformation trans = TRANS_NONE;
+
+static GdkPixbuf *
+make_negative(GdkPixbuf *image)
+{
+ GdkPixbuf *new_image;
+ uint8_t *pixels;
+ int width, height, rowstride, channels, x, y;
+
+ new_image = gdk_pixbuf_copy(image);
+ if (!new_image)
+ exit(1);
+
+ pixels = gdk_pixbuf_get_pixels(new_image);
+ width = gdk_pixbuf_get_width(new_image);
+ height = gdk_pixbuf_get_height(new_image);
+ rowstride = gdk_pixbuf_get_rowstride(new_image);
+ channels = gdk_pixbuf_get_n_channels(new_image);
+
+ for (y = 0; y < height; y++) {
+ for (x = 0; x < width * channels; x += channels) {
+ /* TODO
+ * use libcolour to convert to CIE LChuv
+ * (or CIE LChab which isn't supported yet),
+ * then invert the lightness and the hue
+ * and then convert back to sRGB; however
+ * we must account for the fact that some
+ * pixels may be out of gamut
+ *
+ * TODO
+ * this may be a bit slow, so we to use
+ * opencl if available and otherwise use
+ * libcolour_convert_en_masse as much as
+ * possible and cache the negative for
+ * each file
+ *
+ * TODO
+ * it shall be a configuration in Prefernces
+ * what model is used for colour inversion
+ */
+ pixels[x + 0] = 255 - pixels[x + 0];
+ pixels[x + 1] = 255 - pixels[x + 1];
+ pixels[x + 2] = 255 - pixels[x + 2];
+ }
+ pixels = &pixels[rowstride];
+ }
+
+ return new_image;
+}
+
+static GdkPixbuf *
+apply_rotation_and_reflection(GdkPixbuf *image)
+{
+ GdkPixbuf *new_image, *newer_image;
+
+ switch (trans & 7u) {
+ case TRANS_NONE:
+ return NULL;
+ case TRANS_CLOCKWISE:
+ new_image = gdk_pixbuf_rotate_simple(image, GDK_PIXBUF_ROTATE_CLOCKWISE);
+ break;
+ case TRANS_180_DEGREES:
+ new_image = gdk_pixbuf_rotate_simple(image, GDK_PIXBUF_ROTATE_UPSIDEDOWN);
+ break;
+ case TRANS_ANTICLOCKWISE:
+ new_image = gdk_pixbuf_rotate_simple(image, GDK_PIXBUF_ROTATE_COUNTERCLOCKWISE);
+ break;
+ case TRANS_FLIP:
+ case TRANS_TRANSPOSE:
+ new_image = gdk_pixbuf_flip(image, 0);
+ break;
+ case TRANS_FLOP:
+ case TRANS_ANTITRANSPOSE:
+ new_image = gdk_pixbuf_flip(image, 1);
+ break;
+ }
+ if (!new_image)
+ exit(1);
+
+ switch (trans & 7u) {
+ case TRANS_TRANSPOSE:
+ case TRANS_ANTITRANSPOSE:
+ newer_image = gdk_pixbuf_rotate_simple(new_image, GDK_PIXBUF_ROTATE_CLOCKWISE);
+ if (!newer_image)
+ exit(1);
+ g_object_unref(new_image);
+ return newer_image;
+ default:
+ return new_image;
+ }
+}
+
+GdkPixbuf *
+apply_transformations(GdkPixbuf *image)
+{
+ GdkPixbuf *negative_image, *new_image;
+ negative_image = (trans & TRANS_NEGATIVE) ? make_negative(image) : NULL;
+ new_image = apply_rotation_and_reflection(negative_image ? negative_image : image);
+ if (!new_image)
+ new_image = negative_image ? negative_image : image;
+ else if (negative_image)
+ g_object_unref(negative_image);
+ return new_image == image ? NULL : new_image;
+}
+
+static enum transformation
+inverse_transformation(enum transformation t)
+{
+ return ((4u - t) & 3u) | (t & ~3u);
+}
+
+enum transformation
+reset_transformations_without_redraw(void)
+{
+ enum transformation augment = inverse_transformation(trans & 7u);
+ trans &= ~7u;
+ return augment;
+}
+
+int
+add_transformation_without_redraw(enum transformation augment)
+{
+ enum transformation old = trans;
+ if (augment & TRANS_TRANSPOSE)
+ trans = ((augment - trans) & 3u) | ((augment ^ trans) & ~3u);
+ else
+ trans = ((augment + trans) & 3u) | ((augment ^ trans) & ~3u);
+ return trans != old;
+}
+
+void
+transform_point(enum transformation t, double *x, double *y, double *w, double *h)
+{
+ double x1 = *x, y1 = *y, x2 = *w, y2 = *h;
+
+ switch (t & 7u) {
+ case TRANS_NONE:
+ return;
+ case TRANS_CLOCKWISE:
+ *x = y2 - y1;
+ *y = x1;
+ break;
+ case TRANS_180_DEGREES:
+ *x = x2 - x1;
+ *y = y2 - y1;
+ return;
+ case TRANS_ANTICLOCKWISE:
+ *x = y1;
+ *y = x2 - x1;
+ break;
+ case TRANS_TRANSPOSE:
+ *x = y1;
+ *y = x1;
+ break;
+ case TRANS_FLIP:
+ *y = y2 - y1;
+ return;
+ case TRANS_ANTITRANSPOSE:
+ *x = y2 - y1;
+ *y = x2 - x1;
+ break;
+ case TRANS_FLOP:
+ *x = x2 - x1;
+ return;
+ }
+
+ *w = y2;
+ *h = x2;
+}
diff --git a/ui.c b/ui.c
new file mode 100644
index 0000000..8184d68
--- /dev/null
+++ b/ui.c
@@ -0,0 +1,307 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+
+#define LIST_MENU(TOPLEVEL, SEPARATOR, ACTION, TOGGLE, D)\
+ TOPLEVEL(0, "", "File", D)\
+ ACTION(0, 0u, "", "Open", GTK_STOCK_OPEN, "<control>0", "Ctrl+0", open_action, D)\
+ ACTION(0, 1u, "", "Reload", GTK_STOCK_REFRESH, "F5", "F5", reload_action, D)\
+ SEPARATOR(0, D)\
+ ACTION(0, 1u, "", "Print", GTK_STOCK_PRINT, "<control>P", "Ctrl+P", print_action, D)\
+ SEPARATOR(0, D)\
+ ACTION(0, 1u, "Proper", "ties", GTK_STOCK_PROPERTIES, "<alt>Return", "Alt+Return", properties_action, D)\
+ SEPARATOR(0, D)\
+ ACTION(0, 0u, "", "Close", GTK_STOCK_CLOSE, "<control>W", "Ctrl+W", close_action, D)\
+ TOPLEVEL(0, "", "View", D)\
+ TOGGLE(1, 1u, "F", "ullscreen", GTK_STOCK_FULLSCREEN, "F11", "F11", fullscreen_action, GROUP_NONE, 0, D)\
+ SEPARATOR(1, D)\
+ ACTION(0, 1u, "Re", "set transformations", "document-revert", "<alt>R", "Alt+R", reset_transformations_action, D)\
+ ACTION(1, 1u, "Rotate anti", "clockwise", "image-rotate-left", "<shift>R", "Shift+R", rotate_anticlockwise_action, D)\
+ ACTION(0, 1u, "R", "otate 180°", "image-rotate", "<control>space", "Ctrl+Space", rotate_180_degrees_action, D)\
+ ACTION(1, 1u, "", "Rotate clockwise", "image-rotate-right", "R", "R", rotate_clockwise_action, D)\
+ ACTION(1, 1u, "", "Mirror horizontally", "object-flip-horizontal", "H", "H", flop_action, D)\
+ ACTION(1, 1u, "Mirror ", "vertically", "object-flip-vertical", "V", "V", flip_action, D)\
+ ACTION(1, 1u, "", "Transpose", "object-transpose", "T", "T", transpose_action, D)\
+ ACTION(1, 1u, "", "Antitranspose", "object-transpose-mirrored", "<shift>T", "Shift+T", antitranspose_action, D)\
+ SEPARATOR(1, D)\
+ TOGGLE(1, 1u, "", "Invert colours", "filters-invert", "<control>I", "Ctrl+I", invert_action, GROUP_NONE, 0, D)\
+ SEPARATOR(1, D)\
+ ACTION(1, 1u, "", "Zoom in", "zoom-in", "plus", "+", zoom_in_action, D)\
+ ACTION(1, 1u, "Zoom ", "out", "zoom-out", "minus", "-", zoom_out_action, D)\
+ TOGGLE(1, 1u, "", "Normal size", "zoom-original", "0", "0", normal_size_action, GROUP_ZOOM, 0, D)\
+ TOGGLE(1, 1u, "Fit to ", "width", "zoom-fit-width", "<control>H", "Ctrl+H", width_fit_action, GROUP_ZOOM, 0, D)\
+ TOGGLE(1, 1u, "Fit to ", "height", "zoom-fit-height", "<control>V", "Ctrl+V", height_fit_action, GROUP_ZOOM, 0, D)\
+ TOGGLE(1, 1u, "", "Fit to window", "zoom-fit-page", "<control><shift>F", "Ctrl+Shift+F", window_fit_action, GROUP_ZOOM, 0, D)\
+ TOGGLE(1, 1u, "", "Best fit", "zoom-fit-best", "<control>F", "Ctrl+F", best_fit_action, GROUP_ZOOM, 1, D)\
+ TOPLEVEL(1, "", "Go", D)\
+ ACTION(1, 2u, "", "Previous image", GTK_STOCK_GO_BACK, "<control>Left", "Ctrl+Left", previous_action, D)\
+ ACTION(1, 2u, "", "Next image", GTK_STOCK_GO_FORWARD, "<control>Right", "Ctrl+Right", next_action, D)\
+ SEPARATOR(0, D)\
+ ACTION(1, 2u, "", "First image", GTK_STOCK_GOTO_FIRST, "<control>Home", "Ctrl+Home", first_action, D)\
+ ACTION(1, 2u, "", "Last image", GTK_STOCK_GOTO_LAST, "<control>End", "Ctrl+End", last_action,)
+
+GtkWidget *window;
+GtkWidget *display_area;
+GtkWidget *scroll;
+GtkToggleAction *fullscreen;
+
+static GtkWidget *statusbar;
+static GtkWidget *menubar;
+static GtkWidget *toolbar;
+static GtkActionGroup *actions;
+
+#define X_SKIP(...)
+#define X_TOPLEVEL(T, A, B, ...) {A B, NULL, A "_" B, NULL, NULL, NULL} __VA_ARGS__
+#define X_ACTION(T, N, A, B, ICON, ACCEL, ATEXT, F, ...) {A B, ICON, A "_" B, ACCEL, NULL, G_CALLBACK(&F)} __VA_ARGS__
+#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ATEXT, F, G, ACTIVE, ...) {A B, ICON, A "_" B, ACCEL, NULL, G_CALLBACK(&F), ACTIVE} __VA_ARGS__
+
+static GtkActionEntry entries[] = {
+ LIST_MENU(X_TOPLEVEL, X_SKIP, X_ACTION, X_SKIP, COMMA)
+};
+
+static GtkToggleActionEntry toggle_entries[] = {
+ LIST_MENU(X_SKIP, X_SKIP, X_SKIP, X_TOGGLE, COMMA)
+};
+
+#undef X_SKIP
+#undef X_TOPLEVEL
+#undef X_ACTION
+#undef X_TOGGLE
+
+static GtkAction *
+get_action(const char *name)
+{
+ return gtk_action_group_get_action(actions, name);
+}
+
+static GtkWidget *
+append_toplevel_menu(int add_separator_to_toolbar, const char *name)
+{
+ GtkWidget *item, *menu;
+
+ if (add_separator_to_toolbar)
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), gtk_separator_tool_item_new(), -1);
+
+ item = gtk_action_create_menu_item(get_action(name));
+ menu = gtk_menu_new();
+ gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), menu);
+ gtk_menu_shell_append(GTK_MENU_SHELL(menubar), item);
+ return gtk_menu_item_get_submenu(GTK_MENU_ITEM(item));
+}
+
+static void
+append_separator(int add_to_toolbar, GtkWidget *menu)
+{
+ if (add_to_toolbar)
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), gtk_separator_tool_item_new(), -1);
+ if (menu)
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
+}
+
+static void
+append_action(int add_to_toolbar, GtkWidget *menu, const char *name, const char *tooltip)
+{
+ GtkWidget *item;
+ if (add_to_toolbar) {
+ item = gtk_action_create_tool_item(get_action(name));
+ gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(item), -1);
+ if (tooltip)
+ gtk_tool_item_set_tooltip_text(GTK_TOOL_ITEM(item), tooltip);
+ }
+ if (menu) {
+ item = gtk_action_create_menu_item(get_action(name));
+ gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
+ }
+}
+
+static void
+create_menu_and_toolbar(void)
+{
+ GtkWidget *menu;
+
+ menubar = gtk_menu_bar_new();
+ toolbar = gtk_toolbar_new();
+
+#define X_TOPLEVEL(T, A, B, D) menu = append_toplevel_menu(T, A B) D
+#define X_SEPARTOR(T, D) append_separator(T, menu) D
+#define X_ACTION(T, N, A, B, ICON, ACCEL, ACCEL_TEXT, F, D) append_action(T, menu, A B, A B " (" ACCEL_TEXT ")") D
+#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ACCEL_TEXT, F, G, ACTIVE, D) append_action(T, menu, A B, A B " (" ACCEL_TEXT ")") D
+
+ LIST_MENU(X_TOPLEVEL, X_SEPARTOR, X_ACTION, X_TOGGLE, ;);
+
+#undef X_TOPLEVEL
+#undef X_SEPARTOR
+#undef X_ACTION
+#undef X_TOGGLE
+}
+
+static void
+connect_accelerators(GtkAccelGroup *accelerators)
+{
+ GList *item, *list;
+
+ list = gtk_action_group_list_actions(actions);
+ for (item = list; item; item = item->next) {
+ gtk_action_set_accel_group(item->data, accelerators);
+ gtk_action_connect_accelerator(item->data);
+ }
+ g_list_free(list);
+}
+
+static int
+window_state_changed(GtkWidget *widget, GdkEventWindowState *event, void *user)
+{
+ (void) widget;
+ (void) user;
+ gtk_toggle_action_set_active(fullscreen, !!(event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN));
+ if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN)
+ g_idle_add(&recalculate_zoom_mode, NULL);
+ return 0;
+}
+
+void
+set_up_ui(void)
+{
+ GtkAccelGroup *accelerators;
+ GtkWidget *box;
+
+ window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(window), "gbild");
+ gtk_window_set_icon_name(GTK_WINDOW(window), "image-viewer");
+ gtk_window_set_default_size(GTK_WINDOW(window), 900, 700);
+ gtk_window_set_wmclass(GTK_WINDOW(window), "gbild", "gbild");
+
+ g_signal_connect(window, "destroy", G_CALLBACK(&gtk_main_quit), NULL);
+ g_signal_connect(window, "key-press-event", G_CALLBACK(&key_pressed), NULL);
+ g_signal_connect(window, "window-state-event", G_CALLBACK(&window_state_changed), NULL);
+
+ actions = gtk_action_group_new("gbild");
+ gtk_action_group_add_actions(actions, entries, ELEMSOF(entries), NULL);
+ gtk_action_group_add_toggle_actions(actions, toggle_entries, ELEMSOF(toggle_entries), NULL);
+ fullscreen = GTK_TOGGLE_ACTION(gtk_action_group_get_action(actions, "Fullscreen"));
+ accelerators = gtk_accel_group_new();
+ gtk_window_add_accel_group(GTK_WINDOW(window), accelerators);
+ connect_accelerators(accelerators);
+ create_menu_and_toolbar();
+
+ box = gtk_vbox_new(0, 0);
+ gtk_container_add(GTK_CONTAINER(window), box);
+ gtk_box_pack_start(GTK_BOX(box), menubar, 0, 0, 0u);
+ gtk_box_pack_start(GTK_BOX(box), toolbar, 0, 0, 0u);
+ display_area = gtk_drawing_area_new();
+ gtk_widget_set_app_paintable(display_area, 1);
+ gtk_widget_set_double_buffered(display_area, 0);
+ scroll = logical_scroll_pane_new();
+ logical_scroll_pane_set_child(LOGICAL_SCROLL_PANE(scroll), display_area);
+ gtk_widget_add_events(scroll, GDK_SCROLL_MASK);
+ g_signal_connect(scroll, "scroll-event", G_CALLBACK(&scroll_event), NULL);
+ gtk_box_pack_start(GTK_BOX(box), scroll, 1, 1, 0u);
+ statusbar = gtk_statusbar_new();
+ gtk_box_pack_end(GTK_BOX(box), statusbar, 0, 0, 0u);
+
+ gtk_widget_add_events(display_area, GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_POINTER_MOTION_MASK);
+ g_signal_connect(display_area, "expose-event", G_CALLBACK(&expose_image), NULL);
+}
+
+void
+set_actions_sensitive(void)
+{
+#if defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wtype-limits"
+#endif
+
+#define X_SKIP(...)
+#define X_ACTION(T, N, A, B, ICON, ACCEL, ATEXT, F, ...)\
+ if (N) gtk_action_set_sensitive(get_action(A B), nfiles >= N) __VA_ARGS__
+#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ATEXT, F, GROUP, ACTIVE, ...)\
+ if (N) gtk_action_set_sensitive(get_action(A B), nfiles >= N) __VA_ARGS__
+
+ LIST_MENU(X_SKIP, X_SKIP, X_ACTION, X_TOGGLE, ;);
+
+#undef X_SKIP
+#undef X_ACTION
+#undef X_TOGGLE
+
+#if defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+}
+
+void
+clear_toggles(enum toggle_group group, GtkToggleAction *except)
+{
+ static int clearing_toggles = 0;
+
+ GtkToggleAction *action;
+
+ if (clearing_toggles)
+ return;
+
+ clearing_toggles = 1;
+
+#define X_SKIP(...)
+#define X_TOGGLE(T, N, A, B, ICON, ACCEL, ATEXT, F, GROUP, ACTIVE, ...)\
+ do {\
+ if (GROUP == group) {\
+ action = GTK_TOGGLE_ACTION(get_action(A B));\
+ if (action != except)\
+ gtk_toggle_action_set_active(action, 0);\
+ }\
+ } while (0) __VA_ARGS__
+
+ LIST_MENU(X_SKIP, X_SKIP, X_SKIP, X_TOGGLE, ;);
+
+#undef X_SKIP
+#undef X_TOGGLE
+
+ clearing_toggles = 0;
+}
+
+void
+set_fullscreen_mode(int active)
+{
+ gtk_widget_set_visible(menubar, !active);
+ gtk_widget_set_visible(toolbar, !active);
+ gtk_widget_set_visible(statusbar, !active);
+ logical_scroll_pane_set_hscrollbar_visible(LOGICAL_SCROLL_PANE(scroll), !active);
+ logical_scroll_pane_set_vscrollbar_visible(LOGICAL_SCROLL_PANE(scroll), !active);
+
+ if (active)
+ gtk_window_fullscreen(GTK_WINDOW(window));
+ else
+ gtk_window_unfullscreen(GTK_WINDOW(window));
+}
+
+int
+activate_fullscreen(void *user)
+{
+ (void) user;
+ if (!gtk_toggle_action_get_active(fullscreen))
+ gtk_toggle_action_set_active(fullscreen, 1);
+ return 0;
+}
+
+int
+deactivate_fullscreen(void *user)
+{
+ (void) user;
+ if (gtk_toggle_action_get_active(fullscreen))
+ gtk_toggle_action_set_active(fullscreen, 0);
+ return 0;
+}
+
+void
+set_statusbar_message(const char *message)
+{
+ gtk_statusbar_pop(GTK_STATUSBAR(statusbar), 0u);
+ if (message)
+ gtk_statusbar_push(GTK_STATUSBAR(statusbar), 0u, message);
+}
+
+void
+free_ui(void)
+{
+ g_object_unref(actions);
+}
diff --git a/zoom.c b/zoom.c
new file mode 100644
index 0000000..caa9884
--- /dev/null
+++ b/zoom.c
@@ -0,0 +1,82 @@
+/* See LICENSE file for copyright and license details. */
+#include "common.h"
+#include <math.h> /* just for HUGE_VAL, no -lm needed */
+
+Zoom zoom = {1.0};
+enum zoom_mode zoom_mode = ZOOM_BEST_FIT;
+
+int
+zoom_eq(Zoom a, Zoom b)
+{
+ return FLOAT_EQ(a, b);
+}
+
+double
+get_zoom(Zoom z)
+{
+ return z;
+}
+
+Zoom
+make_zoom(double s)
+{
+ return s;
+}
+
+Zoom
+zoom_in(Zoom z)
+{
+ return get_zoom(z) * 1.25;
+}
+
+Zoom
+zoom_out(Zoom z)
+{
+ return get_zoom(z) * (1.0 / 1.25);
+}
+
+Zoom
+calculate_fit(GdkPixbuf *image, int xfit, int yfit, double cap)
+{
+ GtkAdjustment *adjustment;
+ double s = 1.0, y;
+ int w, h;
+
+ if (xfit) {
+ adjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll));
+ w = gdk_pixbuf_get_width(image);
+ s = adjustment->page_size / (double)w;
+ }
+
+ if (yfit) {
+ adjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll));
+ h = gdk_pixbuf_get_height(image);
+ y = adjustment->page_size / (double)h;
+ s = xfit ? MIN(s, y) : y;
+ }
+
+ if (s > cap)
+ s = cap;
+
+ return make_zoom(s);
+}
+
+Zoom
+calculate_zoom_mode(GdkPixbuf *image, enum zoom_mode mode)
+{
+ switch (mode) {
+ case ZOOM_CUSTOM:
+ break;
+ case ZOOM_NORMAL:
+ return calculate_fit(image, 0, 0, 1.0);
+ case ZOOM_FIT_WIDTH:
+ return calculate_fit(image, 1, 0, HUGE_VAL);
+ case ZOOM_FIT_HEIGHT:
+ return calculate_fit(image, 0, 1, HUGE_VAL);
+ case ZOOM_FIT_WINDOW:
+ return calculate_fit(image, 1, 1, HUGE_VAL);
+ case ZOOM_BEST_FIT:
+ return calculate_fit(image, 1, 1, 1.0);
+ }
+ return zoom;
+}