diff options
| author | Mattias Andrée <m@maandree.se> | 2026-10-11 23:01:38 +0200 |
|---|---|---|
| committer | Mattias Andrée <m@maandree.se> | 2026-10-11 23:29:28 +0200 |
| commit | 94659550993f454e12e4bcb8cf93a5ff258bc08c (patch) | |
| tree | f8e1cfe9fbcdec6f09b6cc18c761684dc34505d2 | |
| parent | First commit (diff) | |
| download | gbild-94659550993f454e12e4bcb8cf93a5ff258bc08c.tar.gz gbild-94659550993f454e12e4bcb8cf93a5ff258bc08c.tar.bz2 gbild-94659550993f454e12e4bcb8cf93a5ff258bc08c.tar.xz | |
Second commit
Signed-off-by: Mattias Andrée <m@maandree.se>
| -rw-r--r-- | Makefile | 17 | ||||
| -rw-r--r-- | TODO | 50 | ||||
| -rw-r--r-- | common.h | 197 | ||||
| -rw-r--r-- | dragging.c | 135 | ||||
| -rw-r--r-- | dropping.c | 120 | ||||
| -rw-r--r-- | events.c | 343 | ||||
| -rw-r--r-- | files.c | 119 | ||||
| -rw-r--r-- | filters.c | 173 | ||||
| -rw-r--r-- | gbild.c | 1600 | ||||
| -rw-r--r-- | image.c | 35 | ||||
| -rw-r--r-- | logical-scroll-pane.c | 430 | ||||
| -rw-r--r-- | logical-scroll-pane.h | 55 | ||||
| -rw-r--r-- | opening.c | 173 | ||||
| -rw-r--r-- | printing.c | 761 | ||||
| -rw-r--r-- | properties.c | 28 | ||||
| -rw-r--r-- | transform.c | 172 | ||||
| -rw-r--r-- | ui.c | 307 | ||||
| -rw-r--r-- | zoom.c | 82 |
18 files changed, 3426 insertions, 1371 deletions
@@ -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) @@ -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; +} @@ -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); +} @@ -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; } @@ -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(>k_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; +} @@ -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(>k_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); +} @@ -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; +} |
