From 94659550993f454e12e4bcb8cf93a5ff258bc08c Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Sun, 11 Oct 2026 23:01:38 +0200 Subject: Second commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- Makefile | 17 +- TODO | 56 +- common.h | 197 ++++++ dragging.c | 135 +++++ dropping.c | 120 ++++ events.c | 343 +++++++++++ files.c | 119 ++++ filters.c | 173 ++++++ gbild.c | 1602 ++++++++----------------------------------------- image.c | 35 ++ logical-scroll-pane.c | 430 +++++++++++++ logical-scroll-pane.h | 55 ++ opening.c | 173 ++++++ printing.c | 761 +++++++++++++++++++++++ properties.c | 28 + transform.c | 172 ++++++ ui.c | 307 ++++++++++ zoom.c | 82 +++ 18 files changed, 3430 insertions(+), 1375 deletions(-) create mode 100644 common.h create mode 100644 dragging.c create mode 100644 dropping.c create mode 100644 events.c create mode 100644 files.c create mode 100644 filters.c create mode 100644 image.c create mode 100644 logical-scroll-pane.c create mode 100644 logical-scroll-pane.h create mode 100644 opening.c create mode 100644 printing.c create mode 100644 properties.c create mode 100644 transform.c create mode 100644 ui.c create mode 100644 zoom.c diff --git a/Makefile b/Makefile index 6fc5daa..11f1356 100644 --- a/Makefile +++ b/Makefile @@ -4,9 +4,24 @@ CONFIGFILE = config.mk include $(CONFIGFILE) OBJ =\ + dragging.o\ + dropping.o\ + events.o\ + files.o\ + filters.o\ + image.o\ + opening.o\ + printing.o\ + properties.o\ + transform.o\ + ui.o\ + zoom.o\ + logical-scroll-pane.o\ gbild.o -HDR = +HDR =\ + common.h\ + logical-scroll-pane.h all: gbild $(OBJ): $(HDR) diff --git a/TODO b/TODO index 046e02b..829d95e 100644 --- a/TODO +++ b/TODO @@ -1,23 +1,33 @@ -BUG) Dragging the image with Control held down, or using the middle button, shall scroll the display area - -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. - -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) - -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) +Add alignment buttons to the print dialogue. + +Some images may have a physical dimension specified. +This should at least be honoured during print, maybe even on display. + +Optional (opt-in) side-pane ("_Property pane") with image properties (accelerator: F9) + +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 +#include +#include +#include +#if defined(__GNUC__) +# pragma GCC diagnostic pop +#endif + +#if defined(__GNUC__) +# pragma GCC diagnostic ignored "-Wunsuffixed-float-constants" +# pragma GCC diagnostic ignored "-Wsuggest-attribute=const" +# pragma GCC diagnostic ignored "-Wsuggest-attribute=pure" +#endif + +#if defined(__GNUC__) +# define PURE __attribute__((__pure__)) +#else +# define PURE +#endif + +#include "logical-scroll-pane.h" + + +#define FLOAT_EQ(A, B) ((A) >= (B) && (A) <= (B)) + +#define COMMA , + + +enum toggle_group { + GROUP_NONE, + GROUP_ZOOM +}; + +enum zoom_mode { + ZOOM_CUSTOM, + ZOOM_NORMAL, + ZOOM_FIT_WIDTH, + ZOOM_FIT_HEIGHT, + ZOOM_FIT_WINDOW, + ZOOM_BEST_FIT +}; + +enum transformation { + TRANS_NONE, /* 1 */ + TRANS_CLOCKWISE, /* r */ + TRANS_180_DEGREES, /* r² */ + TRANS_ANTICLOCKWISE, /* r³ */ + TRANS_TRANSPOSE, /* t */ + TRANS_FLOP, /* rt */ + TRANS_ANTITRANSPOSE, /* r²t */ + TRANS_FLIP, /* r³t */ + /* + * t² = r⁴ = 1 + * trⁿ = r⁻ⁿt + */ + TRANS_NEGATIVE = 0x08 +}; + +struct file { + char *path; + off_t size; + GdkPixbuf *image; +}; + +typedef double Zoom; /* TODO */ + + +/* dragging.c */ +void set_up_dragging(void); + +/* dropping.c */ +void set_up_dropping(void); + +/* events.c */ +void open_action(GtkAction *action, void *user); +void reload_action(GtkAction *action, void *user); +void print_action(GtkAction *action, void *user); +void properties_action(GtkAction *action, void *user); +void close_action(GtkAction *action, void *user); +void fullscreen_action(GtkToggleAction *action, void *user); +void reset_transformations_action(GtkAction *action, void *user); +void rotate_clockwise_action(GtkAction *action, void *user); +void rotate_180_degrees_action(GtkAction *action, void *user); +void rotate_anticlockwise_action(GtkAction *action, void *user); +void flop_action(GtkAction *action, void *user); +void flip_action(GtkAction *action, void *user); +void transpose_action(GtkAction *action, void *user); +void antitranspose_action(GtkAction *action, void *user); +void invert_action(GtkToggleAction *action, void *user); +void zoom_in_action(GtkAction *action, void *user); +void zoom_out_action(GtkAction *action, void *user); +void normal_size_action(GtkToggleAction *action, void *user); +void width_fit_action(GtkToggleAction *action, void *user); +void height_fit_action(GtkToggleAction *action, void *user); +void window_fit_action(GtkToggleAction *action, void *user); +void best_fit_action(GtkToggleAction *action, void *user); +void next_action(GtkAction *action, void *user); +void previous_action(GtkAction *action, void *user); +void first_action(GtkAction *action, void *user); +void last_action(GtkAction *action, void *user); +int key_pressed(GtkWidget *widget, GdkEventKey *event, void *user); +int scroll_event(GtkWidget *widget, GdkEventScroll *event, void *user); + +/* files.c */ +extern struct file *files; +extern size_t nfiles; +extern size_t current_file; +void free_files(void); +size_t files_from_array(struct file *selected_files, size_t nselected_files); +size_t files_from_list(struct file **files_out, GSList *selected_files); +int set_files_from_argv(int argc, char **argv); +struct file *get_file(void); +int navigate_without_redraw(size_t jump, size_t move); + +/* filters.c */ +void add_image_filters(GtkFileChooser *chooser); +void free_image_filters(void); + +/* image.c */ +extern GdkPixbuf *transformed_image; +GdkPixbuf *get_image(void); +GdkPixbuf *get_original_image(void); + +/* opening.c */ +int load_image(struct file *file, int errors_to_stderr); +GtkFileChooser *construct_chooser(void); + +/* printing.c */ +void open_print(GdkPixbuf *image); + +/* properties.c */ +void open_properties(struct file *file); + +/* transform.c */ +extern enum transformation trans; +GdkPixbuf *apply_transformations(GdkPixbuf *image); +enum transformation reset_transformations_without_redraw(void); +int add_transformation_without_redraw(enum transformation augment); +void transform_point(enum transformation t, double *x, double *y, double *w, double *h); + +/* ui.c */ +extern GtkWidget *window; +extern GtkWidget *display_area; +extern GtkWidget *scroll; +extern GtkToggleAction *fullscreen; +void set_up_ui(void); +void set_actions_sensitive(void); +void clear_toggles(enum toggle_group group, GtkToggleAction *except); +void set_fullscreen_mode(int active); +int activate_fullscreen(void *user); +int deactivate_fullscreen(void *user); +void set_statusbar_message(const char *message); +void free_ui(void); + +/* zoom.c */ +extern Zoom zoom; +extern enum zoom_mode zoom_mode; +int zoom_eq(Zoom a, Zoom b); +double get_zoom(Zoom z); +Zoom make_zoom(double s); +Zoom zoom_in(Zoom z); +Zoom zoom_out(Zoom z); +Zoom calculate_fit(GdkPixbuf *image, int xfit, int yfit, double cap); +Zoom calculate_zoom_mode(GdkPixbuf *image, enum zoom_mode mode); + +/* gbild.c */ +int expose_image(GtkWidget *widget, GdkEventExpose *event, void *user); +void add_images(struct file *new_files, size_t nnew_files); +void set_images_from_array(struct file *selected_files, size_t nselected_files); +void set_images_from_list(GSList *selected_files); +void scroll_by(double horizontally, double vertically); +void set_zoom_around_point(Zoom new_zoom, double x, double y); +void set_zoom_around_centre(Zoom new_zoom); +void set_zoom_and_recentre(Zoom new_zoom); +void reload_redraw(void); +void transform_redraw(enum transformation augment); +void navigate_redraw(void); +int recalculate_zoom_mode(void *user); diff --git a/dragging.c b/dragging.c new file mode 100644 index 0000000..3ac3ff5 --- /dev/null +++ b/dragging.c @@ -0,0 +1,135 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +static char url_list_name[] = "text/uri-list"; +static GtkTargetEntry url_list = {url_list_name, 0u, 0u}; + +static int dragging = 0; +static int drag_source_disabled = 0; +static int drag_x, drag_y; + +static void +set_file_drag_source(GtkWidget *widget) +{ + gtk_drag_source_set(widget, GDK_BUTTON1_MASK, &url_list, 1u, + GDK_ACTION_COPY | GDK_ACTION_MOVE | GDK_ACTION_LINK | GDK_ACTION_ASK); +} + +static int +button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) +{ + (void) user; + if (event->button == 2u || (event->state & GDK_CONTROL_MASK)) { + if (event->button == 1u) { + gtk_drag_source_unset(widget); + drag_source_disabled = 1; + } + dragging = 1; + drag_x = (int)event->x_root; + drag_y = (int)event->y_root; + } + return dragging; +} + +static int +button_released(GtkWidget *widget, GdkEventButton *event, void *user) +{ + (void) event; + (void) user; + dragging = 0; + if (drag_source_disabled) { + set_file_drag_source(widget); + drag_source_disabled = 0; + } + return 0; +} + +static int +pointer_moved(GtkWidget *widget, GdkEventMotion *event, void *user) +{ + int x, y; + (void) event; + (void) user; + if (dragging) { + gdk_display_get_pointer(gtk_widget_get_display(widget), NULL, &x, &y, NULL); + scroll_by(drag_x - x, drag_y - y); + drag_x = x; + drag_y = y; + } + return dragging; +} + +static void +drag_begin(GtkWidget *widget, GdkDragContext *context, void *user) +{ + GdkPixbuf *image, *icon; + intmax_t height, width; + + (void) widget; + (void) user; + + image = get_original_image(); + if (!image) + return; + + width = gdk_pixbuf_get_width(image); + height = gdk_pixbuf_get_height(image); + if (width <= DRAG_ICON_SIZE && height <= DRAG_ICON_SIZE) { + gtk_drag_set_icon_pixbuf(context, image, 0, 0); + return; + } + if (width >= height) { + height = height * DRAG_ICON_SIZE / width; + height = MAX(1, height); + width = DRAG_ICON_SIZE; + } else { + width = width * DRAG_ICON_SIZE / height; + width = MAX(1, width); + height = DRAG_ICON_SIZE; + } + icon = gdk_pixbuf_scale_simple(image, (int)width, (int)height, GDK_INTERP_BILINEAR); + if (!icon) + return; + gtk_drag_set_icon_pixbuf(context, icon, 0, 0); + g_object_unref(icon); +} + +static void +drag_data_get(GtkWidget *widget, GdkDragContext *context, GtkSelectionData *data, + unsigned int info, unsigned int time, void *user) +{ + struct file *file = get_file(); + GError *error = NULL; + char *uri; + + (void) widget; + (void) context; + (void) info; + (void) time; + (void) user; + + if (!file || !file->path) + return; + + uri = g_filename_to_uri(file->path, NULL, &error); + if (!uri) { + weprintf("g_filename_to_uri: %s", error->message); + g_error_free(error); + return; + } + gtk_selection_data_set_uris(data, (char *[]){uri, NULL}); + g_free(uri); +} + +void +set_up_dragging(void) +{ + set_file_drag_source(display_area); + + g_signal_connect(display_area, "button-press-event", G_CALLBACK(&button_pressed), NULL); + g_signal_connect(display_area, "button-release-event", G_CALLBACK(&button_released), NULL); + g_signal_connect(display_area, "motion-notify-event", G_CALLBACK(&pointer_moved), NULL); + + g_signal_connect(display_area, "drag-begin", G_CALLBACK(&drag_begin), NULL); + g_signal_connect(display_area, "drag-data-get", G_CALLBACK(&drag_data_get), NULL); +} diff --git a/dropping.c b/dropping.c new file mode 100644 index 0000000..412f0ab --- /dev/null +++ b/dropping.c @@ -0,0 +1,120 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +static char url_list_name[] = "text/uri-list"; +static GtkTargetEntry url_list = {url_list_name, 0u, 0u}; + +static size_t +get_dropped_files(struct file **files_out, char **uris) +{ + GError *error = NULL; + char **uri, *path; + struct file *dropped_files = NULL; + size_t i = 0u; + + if (!uris) + goto out; + + for (uri = uris; *uri; uri++) + i += 1u; + if (!i) + goto out; + dropped_files = ecalloc(i, sizeof(*dropped_files)); + + for (uri = uris, i = 0u; *uri; uri++) { + error = NULL; /* g_filename_from_uri requires this initialisation */ + path = g_filename_from_uri(*uri, NULL, &error); + if (!path) { + weprintf("g_filename_from_uri: %s", error->message); + g_error_free(error); + continue; + } + dropped_files[i++].path = estrdup(path); + g_free(path); + /* the other fields are set in the next code block */ + } + +out: + *files_out = dropped_files; + return i; +} + +static void +drop_files(GtkWidget *widget, GdkDragContext *context, int x, int y, + GtkSelectionData *data, unsigned int info, unsigned int time, void *user) +{ + char **uris = NULL; + struct file *dropped_files; + size_t n; + int accepted = 1; + GdkModifierType modifiers; + GdkWindow *event_window; + + (void) x; + (void) y; + (void) info; + (void) user; + + if (gtk_drag_get_source_widget(context) == display_area) { + accepted = 0; + goto out; + } + event_window = gtk_widget_get_window(widget); + if (!event_window) + goto out; + if (!gdk_window_get_pointer(event_window, NULL, NULL, &modifiers)) + goto out; + + uris = gtk_selection_data_get_uris(data); + n = get_dropped_files(&dropped_files, uris); + if (!n) + goto out; + + if ((modifiers & GDK_CONTROL_MASK) && nfiles) { + n = files_from_array(dropped_files, n); + add_images(dropped_files, n); + free(dropped_files); + } else { + set_images_from_array(dropped_files, n); + } + +out: + gtk_drag_finish(context, accepted, 0, time); + if (uris) + g_strfreev(uris); +} + +static int +drop_motion(GtkWidget *widget, GdkDragContext *context, int x, int y, unsigned int time, void *user) +{ + GdkWindow *event_window = gtk_widget_get_window(widget); + GdkModifierType modifiers; + GdkDragAction action = 0; + + (void) x; + (void) y; + (void) user; + + if (!gtk_get_current_event_state(&modifiers)) + if (!event_window || !gdk_window_get_pointer(event_window, NULL, NULL, &modifiers)) + goto out; + if (gtk_drag_dest_find_target(widget, context, NULL) == GDK_NONE) + goto out; + + action = (modifiers & GDK_CONTROL_MASK) ? GDK_ACTION_COPY : GDK_ACTION_MOVE; + if (!(gdk_drag_context_get_actions(context) & action)) + action = 0; + +out: + gdk_drag_status(context, action, time); + return 1; +} + +void +set_up_dropping(void) +{ + gtk_drag_dest_set(window, GTK_DEST_DEFAULT_HIGHLIGHT | GTK_DEST_DEFAULT_DROP, + &url_list, 1u, GDK_ACTION_COPY | GDK_ACTION_MOVE); + g_signal_connect(window, "drag-motion", G_CALLBACK(&drop_motion), NULL); + g_signal_connect(window, "drag-data-received", G_CALLBACK(&drop_files), NULL); +} diff --git a/events.c b/events.c new file mode 100644 index 0000000..4c5910e --- /dev/null +++ b/events.c @@ -0,0 +1,343 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +static void +reload_current_file(void) +{ + if (load_image(get_file(), 0)) + reload_redraw(); +} + +static void +reset_transformations(void) +{ + enum transformation augment = reset_transformations_without_redraw(); + if (augment != TRANS_NONE) + transform_redraw(augment); +} + +static void +transform(enum transformation augment) +{ + if (add_transformation_without_redraw(augment)) + transform_redraw(augment); +} + +static void +set_zoom_fit(GtkToggleAction *action, enum zoom_mode mode) +{ + GdkPixbuf *image; + + if (gtk_toggle_action_get_active(action)) { + zoom_mode = mode; + if ((image = get_image())) + set_zoom_and_recentre(calculate_zoom_mode(image, zoom_mode)); + } else if (zoom_mode == mode) { + zoom_mode = ZOOM_CUSTOM; /* deselect zoom */ + } + + clear_toggles(GROUP_ZOOM, action); +} + +static void +navigate(size_t jump, size_t move) +{ + if (navigate_without_redraw(jump, move)) + navigate_redraw(); +} + +void +open_action(GtkAction *action, void *user) +{ + GtkFileChooser *chooser; + (void) action; + (void) user; + chooser = construct_chooser(); + add_image_filters(chooser); + if (gtk_dialog_run(GTK_DIALOG(chooser)) == GTK_RESPONSE_ACCEPT) + set_images_from_list(gtk_file_chooser_get_filenames(chooser)); + gtk_widget_destroy(GTK_WIDGET(chooser)); +} + +void +reload_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + reload_current_file(); +} + +void +print_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + open_print(get_image()); +} + +void +properties_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + open_properties(get_file()); +} + +void +close_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + gtk_main_quit(); +} + +void +fullscreen_action(GtkToggleAction *action, void *user) +{ + int active = gtk_toggle_action_get_active(action); + (void) user; + if (active && current_file >= nfiles) + gtk_toggle_action_set_active(action, 0); + else + set_fullscreen_mode(active); +} + +void +reset_transformations_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + reset_transformations(); +} + +void +rotate_clockwise_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + transform(TRANS_CLOCKWISE); +} + +void +rotate_180_degrees_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + transform(TRANS_180_DEGREES); +} + +void +rotate_anticlockwise_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + transform(TRANS_ANTICLOCKWISE); +} + +void +flop_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + transform(TRANS_FLOP); +} + +void +flip_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + transform(TRANS_FLIP); +} + +void +transpose_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + transform(TRANS_TRANSPOSE); +} + +void +antitranspose_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + transform(TRANS_ANTITRANSPOSE); +} + +void +invert_action(GtkToggleAction *action, void *user) +{ + enum transformation tr; + (void) user; + tr = gtk_toggle_action_get_active(action) ? TRANS_NEGATIVE : TRANS_NONE; + transform(tr ^ (trans & TRANS_NEGATIVE)); +} + +void +zoom_in_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + clear_toggles(GROUP_ZOOM, NULL); + set_zoom_around_centre(zoom_in(zoom)); +} + +void +zoom_out_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + clear_toggles(GROUP_ZOOM, NULL); + set_zoom_around_centre(zoom_out(zoom)); +} + +void +normal_size_action(GtkToggleAction *action, void *user) +{ + (void) user; + set_zoom_fit(action, ZOOM_NORMAL); +} + +void +width_fit_action(GtkToggleAction *action, void *user) +{ + (void) user; + set_zoom_fit(action, ZOOM_FIT_WIDTH); +} + +void +height_fit_action(GtkToggleAction *action, void *user) +{ + (void) user; + set_zoom_fit(action, ZOOM_FIT_HEIGHT); +} + +void +window_fit_action(GtkToggleAction *action, void *user) +{ + (void) user; + set_zoom_fit(action, ZOOM_FIT_WINDOW); +} + +void +best_fit_action(GtkToggleAction *action, void *user) +{ + (void) user; + set_zoom_fit(action, ZOOM_BEST_FIT); +} + +void +next_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + navigate(current_file, +(size_t)1); +} + +void +previous_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + navigate(current_file ? current_file : nfiles, -(size_t)1); +} + +void +first_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + navigate(0u, 0u); +} + +void +last_action(GtkAction *action, void *user) +{ + (void) action; + (void) user; + navigate(nfiles, -(size_t)1); +} + +int +key_pressed(GtkWidget *widget, GdkEventKey *event, void *user) +{ + GtkAdjustment *hadjustment, *vadjustment; + int left, right, up, down, amount; + double hamount, vamount; + + (void) widget; + (void) user; + + if (event->keyval == GDK_Escape) { + deactivate_fullscreen(NULL); + return 1; + } + + if (event->state & GDK_CONTROL_MASK) + return 0; + + amount = (event->state & GDK_MOD1_MASK) ? 1 : SCROLL_AMOUNT; + hamount = (double)amount; + vamount = (double)amount; + if (event->state & GDK_SHIFT_MASK) { + hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll)); + vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll)); + hamount = 0.25 * hadjustment->page_size; + vamount = 0.25 * vadjustment->page_size; + } + + left = event->keyval == GDK_Left; + right = event->keyval == GDK_Right; + up = event->keyval == GDK_Up; + down = event->keyval == GDK_Down; + + if (left | right | up | down) { + scroll_by(right * hamount - left * hamount, + down * vamount - up * vamount); + return 1; + } + + return 0; +} + +int +scroll_event(GtkWidget *widget, GdkEventScroll *event, void *user) +{ + GtkAdjustment *hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll)); + GtkAdjustment *vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll)); + double hamount = hadjustment->step_increment; + double vamount = vadjustment->step_increment; + + int shift = !!(event->state & GDK_SHIFT_MASK); + int ctrl = !!(event->state & GDK_CONTROL_MASK); + int up = event->direction == GDK_SCROLL_UP; + int down = event->direction == GDK_SCROLL_DOWN; + int left = event->direction == GDK_SCROLL_LEFT; + int right = event->direction == GDK_SCROLL_RIGHT; + + int x0, y0; + + (void) widget; + (void) user; + + if (shift) { + left = up, up = 0; + right = down, down = 0; + } + + if (!ctrl) { + /* needed for shift+scroll to work */ + scroll_by(right * hamount - left * hamount, + down * vamount - up * vamount); + } else if (up | down) { + gdk_window_get_origin(display_area->window, &x0, &y0); + clear_toggles(GROUP_ZOOM, NULL); + set_zoom_around_point(up ? zoom_in(zoom) : zoom_out(zoom), event->x_root - x0, event->y_root - y0); + } else { + return 0; + } + + return 1; +} diff --git a/files.c b/files.c new file mode 100644 index 0000000..2db0b17 --- /dev/null +++ b/files.c @@ -0,0 +1,119 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +struct file *files = NULL; +size_t nfiles = 0u; +size_t current_file = 0u; + +void +free_files(void) +{ + while (nfiles) { + --nfiles; + free(files[nfiles].path); + g_clear_object(&files[nfiles].image); + } + free(files); + files = NULL; + current_file = 0u; +} + +size_t +files_from_array(struct file *selected_files, size_t nselected_files) +{ + size_t i, n; + + for (i = 0u, n = 0u; i < nselected_files; i++) { + selected_files[i].image = NULL; + if (load_image(&selected_files[i], 0)) { + if (i != n) + selected_files[n] = selected_files[i]; + n += 1u; + } else { + free(selected_files[i].path); + } + } + + return n; +} + +size_t +files_from_list(struct file **files_out, GSList *selected_files) +{ + struct file *new_files = NULL; + GSList *file; + size_t i; + + for (file = selected_files, i = 0u; file; file = file->next) + i += 1u; + if (!i) + goto out; + new_files = ecalloc(i, sizeof(*files)); + + for (file = selected_files, i = 0u; file; file = file->next) { + new_files[i].path = estrdup(file->data); + new_files[i].image = NULL; + g_free(file->data); + + if (load_image(&new_files[i], 0)) + i += 1u; + else + free(new_files[i].path); + } + g_slist_free(selected_files); + + if (!i) { + free(new_files); + new_files = NULL; + } + +out: + *files_out = new_files; + return i; +} + +int +set_files_from_argv(int argc, char **argv) +{ + int ok = 1; + size_t i; + + nfiles = (size_t)argc; + if (!nfiles) { + files = NULL; + return 1; + } + + files = ecalloc(nfiles, sizeof(*files)); + for (i = 0u; i < nfiles; i++) { + files[i].path = estrdup(argv[i]); + files[i].image = NULL; + if (!load_image(&files[i], 1)) { + free(files[i].path); + ok = 0; + } + } + + return ok; + +} + +struct file * +get_file(void) +{ + if (current_file >= nfiles) + return NULL; + return &files[current_file]; +} + +int +navigate_without_redraw(size_t jump, size_t move) +{ + size_t old_file = current_file; + if (!nfiles) + return 0; + current_file = jump; + current_file += move; + current_file %= nfiles; + return current_file != old_file; +} diff --git a/filters.c b/filters.c new file mode 100644 index 0000000..08bb647 --- /dev/null +++ b/filters.c @@ -0,0 +1,173 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +struct filter { + char *name; + size_t suffixes_start; + size_t suffixes_end; +}; + +static char *suffixes = NULL; +static struct filter *filters = NULL; +static size_t nfilters = 0u; +static struct filter anyfilter = {NULL, 0u, 0u}; + +static int +file_filter(const GtkFileFilterInfo *info, void *user) +{ + struct filter *filter = user; + size_t pos, name_len, suffix_len; + const char *name, *suffix; + size_t backup; + + if (!info->filename) + return 0; + if (!filter || g_file_test(info->filename, G_FILE_TEST_IS_DIR)) + return 1; + + name = info->filename; + name_len = strlen(name); + if (!name_len) + return 0; + + for (pos = filter->suffixes_start; pos != filter->suffixes_end; pos += suffix_len + 1u) { + suffix = &suffixes[pos]; + suffix_len = strlen(suffix); + + backup = (size_t)(name[name_len - 1u] == '~'); + + if (name_len <= 1u + suffix_len + backup) + continue; + if (name[name_len - (1u + suffix_len + backup)] != '.') + continue; + if (memcmp(&name[name_len - (suffix_len + backup)], suffix, suffix_len)) + continue; + + return 1; + } + + return 0; +} + +static int +filtercmp(const void *a_void, const void *b_void) +{ + const struct filter *a = a_void, *b = b_void; + return strcmp(a->name, b->name); +} + +static void +populate_filters(void) +{ + GSList *formats, *item; + GdkPixbufFormat *format; + char **extensions, **extension, *p; + const char *name; + size_t i, len = 0u; + size_t ext_len, need, count; + + formats = gdk_pixbuf_get_formats(); + + for (item = formats; item; item = item->next) { + format = item->data; + if (gdk_pixbuf_format_is_disabled(format)) + continue; + + name = gdk_pixbuf_format_get_description(format); + extensions = gdk_pixbuf_format_get_extensions(format); + + need = 0u; + count = 0u; + for (extension = extensions; *extension; extension++) { + ext_len = strlen(*extension); + if (need > SIZE_MAX - 1u - ext_len) + eprintf("Out of address space"); + need += ext_len + 1u; + count += 1u; + } + if (need > SIZE_MAX - len) + eprintf("Out of address space"); + if (!count) + goto next; + + filters = ereallocarray(filters, nfilters + 1u, sizeof(*filters)); + filters[nfilters].suffixes_start = len; + filters[nfilters].name = emalloc(strlen(name) + sizeof(" ()") + need + (count - 1u)); + + p = stpcpy(stpcpy(filters[nfilters].name, name), " ("); + suffixes = erealloc(suffixes, need + len); + for (extension = extensions;;) { + ext_len = strlen(*extension); + memcpy(&suffixes[len], *extension, ext_len + 1u); + len += ext_len + 1u; + + memcpy(p, *extension, ext_len); + p = &p[ext_len]; + + if (!*++extension) + break; + p = stpcpy(p, ", "); + } + p = stpcpy(p, ")"); + + for (i = 0u; i < nfilters; i++) { + if (!strcmp(filters[nfilters].name, filters[i].name)) { + free(filters[nfilters].name); + len = filters[nfilters].suffixes_start; + goto next; + } + } + + filters[nfilters].suffixes_end = len; + nfilters += 1u; + + next: + g_strfreev(extensions); + } + + qsort(filters, nfilters, sizeof(*filters), &filtercmp); + + anyfilter.suffixes_end = len; + + g_slist_free(formats); +} + +void +add_image_filters(GtkFileChooser *chooser) +{ + static int populated = 0; + + GtkFileFilter *filter; + size_t i; + + if (!populated) { + populate_filters(); + populated = 1; + } + + filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, "All images"); + gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, &anyfilter, NULL); + gtk_file_chooser_add_filter(chooser, filter); + + for (i = 0u; i < nfilters; i++) { + filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, filters[i].name); + gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, &filters[i], NULL); + gtk_file_chooser_add_filter(chooser, filter); + } + + filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, "All files"); + gtk_file_filter_add_custom(filter, GTK_FILE_FILTER_FILENAME, file_filter, NULL, NULL); + gtk_file_chooser_add_filter(chooser, filter); +} + +void +free_image_filters(void) +{ + free(suffixes); + while (nfilters) + free(filters[--nfilters].name); + free(filters); +} diff --git a/gbild.c b/gbild.c index 7ab22ee..ffe51b0 100644 --- a/gbild.c +++ b/gbild.c @@ -1,171 +1,9 @@ /* See LICENSE file for copyright and license details. */ -#if defined(__GNUC__) -# pragma GCC diagnostic push -# pragma GCC diagnostic ignored "-Wdeprecated-declarations" -# pragma GCC diagnostic ignored "-Wstrict-prototypes" -#endif -#include -#include -#include -#include -#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, "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, "P", "Ctrl+P", print_action, D)\ - SEPARATOR(0, D)\ - ACTION(0, 1u, "Proper", "ties", GTK_STOCK_PROPERTIES, "Return", "Alt+Return", properties_action, D)\ - SEPARATOR(0, D)\ - ACTION(0, 0u, "", "Close", GTK_STOCK_CLOSE, "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", "R", "Alt+R", reset_transformations_action, D)\ - ACTION(1, 1u, "Rotate anti", "clockwise", "image-rotate-left", "R", "Shift+R", rotate_anticlockwise_action, D)\ - ACTION(0, 1u, "R", "otate 180°", "image-rotate", "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", "T", "Shift+T", antitranspose_action, D)\ - SEPARATOR(1, D)\ - TOGGLE(1, 1u, "", "Invert colours", "filters-invert", "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", "H", "Ctrl+H", width_fit_action, GROUP_ZOOM, 0, D)\ - TOGGLE(1, 1u, "Fit to ", "height", "zoom-fit-height", "V", "Ctrl+V", height_fit_action, GROUP_ZOOM, 0, D)\ - TOGGLE(1, 1u, "", "Fit to window", "zoom-fit-page", "F", "Ctrl+Shift+F", window_fit_action, GROUP_ZOOM, 0, D)\ - TOGGLE(1, 1u, "", "Best fit", "zoom-fit-best", "F", "Ctrl+F", best_fit_action, GROUP_ZOOM, 1, D)\ - TOPLEVEL(1, "", "Go", D)\ - ACTION(1, 2u, "", "Previous image", GTK_STOCK_GO_BACK, "Left", "Ctrl+Left", previous_action, D)\ - ACTION(1, 2u, "", "Next image", GTK_STOCK_GO_FORWARD, "Right", "Ctrl+Right", next_action, D)\ - SEPARATOR(0, D)\ - ACTION(1, 2u, "", "First image", GTK_STOCK_GOTO_FIRST, "Home", "Ctrl+Home", first_action, D)\ - ACTION(1, 2u, "", "Last image", GTK_STOCK_GOTO_LAST, "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; + + (void) user; - fd = open(file->path, O_RDONLY | O_NOCTTY); - if (fd < 0) - goto fail_errno; + if (suspend_expose) + return 1; - if (fstat(fd, &st)) - goto fail_errno; + cr = gdk_cairo_create(widget->window); + gdk_cairo_set_source_color(cr, &widget->style->bg[GTK_STATE_NORMAL]); - if (!S_ISREG(st.st_mode)) { - error_message = "Not a regular file"; - goto fail; - } + 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); - 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); + 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; - -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); - } + 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; - g_clear_error(&error); - ret = 0; - goto out; -} + cairo_fill(cr); -static void -set_images_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); - } - } - - if (!n) { - free(selected_files); - return; - } + cairo_translate(cr, x, y); + cairo_scale(cr, s, s); + gdk_cairo_set_source_pixbuf(cr, image, 0.0, 0.0); - clear_images(); - files = selected_files; - nfiles = n; - show_image(); - update_status(); - set_actions_sensitive(); +out: + cairo_paint(cr); + cairo_destroy(cr); + return 1; } static void -set_images_from_list(GSList *selected_files) +show_image(int update_zoom) { - struct file *new_files; - GSList *file; - size_t i; + GdkPixbuf *new_image, *original_image, *display_image; + double s; - for (file = selected_files, i = 0u; file; file = file->next) - i += 1u; - if (!i) + 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; - 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); - 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; + new_image = apply_transformations(original_image); + display_image = new_image ? new_image : original_image; - name = info->filename; - name_len = strlen(name); - if (!name_len) - return 0; + if (update_zoom && zoom_mode != ZOOM_CUSTOM && zoom_mode != ZOOM_NORMAL) + zoom = calculate_zoom_mode(display_image, zoom_mode); - for (pos = filter->suffixes_start; pos != filter->suffixes_end; pos += suffix_len + 1u) { - suffix = &suffixes[pos]; - suffix_len = strlen(suffix); + 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))); - 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; + w = gdk_pixbuf_get_width(image); + h = gdk_pixbuf_get_height(image); - clearing_toggles = 1; + 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; -#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__ + 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; - LIST_MENU(X_SKIP, X_SKIP, X_SKIP, X_TOGGLE, ;); + 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; -#undef X_SKIP -#undef X_TOGGLE + 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; - clearing_toggles = 0; -} + suspend_expose++; -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; - - (void) widget; - (void) user; + show_image(0); - if (shift) { - left = up, up = 0; - right = down, down = 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; -} - -static int -button_pressed(GtkWidget *widget, GdkEventButton *event, void *user) -{ - (void) widget; - (void) user; + max = vadjustment->upper - vadjustment->page_size; + y = MAX(vadjustment->lower, adj_y); + y = MIN(y, max); + gtk_adjustment_set_value(vadjustment, y); - 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; - - (void) widget; - (void) user; + GtkAdjustment *hadjustment, *vadjustment; + double x, y, max; - if (event->keyval == GDK_Escape && gtk_toggle_action_get_active(fullscreen)) { - gtk_toggle_action_set_active(fullscreen, 0); - return 1; - } + hadjustment = logical_scroll_pane_get_hadjustment(LOGICAL_SCROLL_PANE(scroll)); + vadjustment = logical_scroll_pane_get_vadjustment(LOGICAL_SCROLL_PANE(scroll)); - if (event->state & GDK_CONTROL_MASK) - return 0; + 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; - amount = (event->state & GDK_MOD1_MASK) ? 1 : SCROLL_AMOUNT; + suspend_expose++; - left = event->keyval == GDK_Left; - right = event->keyval == GDK_Right; - up = event->keyval == GDK_Up; - down = event->keyval == GDK_Down; + show_image(0); - if (left | right | up | down) { - scroll_by(left ? -amount : right ? +amount : 0, - up ? -amount : down ? +amount : 0); - return 1; - } + 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); - return 0; -} + 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); -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); - - if (active) - gtk_window_fullscreen(GTK_WINDOW(window)); - else - gtk_window_unfullscreen(GTK_WINDOW(window)); -} -static void -reset_transformations_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - trans &= ~7u; - show_image(); - /* TODO keep center point */ -} + s = get_zoom(zoom); + width = gdk_pixbuf_get_width(image); + height = gdk_pixbuf_get_height(image); -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 */ -} + 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 -rotate_clockwise_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - transform(TRANS_CLOCKWISE); -} + suspend_expose++; -static void -rotate_180_degrees_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - transform(TRANS_180_DEGREES); -} - -static void -rotate_anticlockwise_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - transform(TRANS_ANTICLOCKWISE); -} - -static void -flop_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - transform(TRANS_FLOP); -} + show_image(0); -static void -flip_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - transform(TRANS_FLIP); -} + 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 -transpose_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - transform(TRANS_TRANSPOSE); -} + 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 -antitranspose_action(GtkAction *action, void *user) -{ - (void) action; - (void) user; - transform(TRANS_ANTITRANSPOSE); + suspend_expose--; + if (!suspend_expose) + gtk_widget_queue_draw(display_area); } -static void -invert_action(GtkToggleAction *action, void *user) +void +navigate_redraw(void) { + suspend_expose++; - enum transformation tr; - (void) user; - tr = gtk_toggle_action_get_active(action) ? TRANS_NEGATIVE : TRANS_NONE; - transform(tr ^ (trans & TRANS_NEGATIVE)); -} + show_image(1); + centre_image(); -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); - - 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_up_ui(); + set_up_dragging(); + set_up_dropping(); + set_actions_sensitive(); gtk_widget_show_all(window); + g_idle_add(&initial_draw, NULL); if (start_fullscreen && nfiles) - g_idle_add(activate_fullscreen, NULL); + g_idle_add(&activate_fullscreen, NULL); gtk_main(); - g_object_unref(actions); if (transformed_image) g_object_unref(transformed_image); -fail: + free_ui(); +out: free_files(); - free(suffixes); - while (nfilters) - free(filters[--nfilters].name); - free(filters); + free_image_filters(); return failed; + +fail: + failed = 1; + goto out; } diff --git a/image.c b/image.c new file mode 100644 index 0000000..ebfb778 --- /dev/null +++ b/image.c @@ -0,0 +1,35 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +GdkPixbuf *transformed_image = NULL; + +static void +forget_transformed_image(void) +{ + if (transformed_image) { + g_object_unref(transformed_image); + transformed_image = NULL; + } +} + +GdkPixbuf * +get_image(void) +{ + struct file *file = get_file(); + if (!file || !file->image) { + forget_transformed_image(); + return NULL; + } + return transformed_image ? transformed_image : file->image; +} + +GdkPixbuf * +get_original_image(void) +{ + struct file *file = get_file(); + if (!file || !file->image) { + forget_transformed_image(); + return NULL; + } + return file->image; +} diff --git a/logical-scroll-pane.c b/logical-scroll-pane.c new file mode 100644 index 0000000..394cfb6 --- /dev/null +++ b/logical-scroll-pane.c @@ -0,0 +1,430 @@ +#include "common.h" + +G_DEFINE_TYPE(LogicalScrollPane, logical_scroll_pane, GTK_TYPE_BIN) + +static void +logical_scroll_pane_queue_child_draw(GtkAdjustment *adjustment, LogicalScrollPane *this) +{ + (void)adjustment; + if (this->child) + gtk_widget_queue_draw(this->child); +} + +static void +logical_scroll_pane_configure_adjustment(GtkAdjustment *adjustment, int content_size, int page_size) +{ + double value, limit; + + page_size = MAX(page_size, 1); + + limit = (double)MAX(content_size - page_size, 0); + value = MAX(0.0, adjustment->value); + value = MIN(value, limit); + + gtk_adjustment_configure(adjustment, value, 0, MAX(content_size, page_size), + page_size * 0.1, page_size * 0.9, page_size); +} + +static void +logical_scroll_pane_destroy(GtkObject *this_) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + if (this->child) gtk_widget_unparent(this->child); + if (this->hscrollbar) gtk_widget_unparent(this->hscrollbar); + if (this->vscrollbar) gtk_widget_unparent(this->vscrollbar); + if (this->hadjustment) g_object_unref(this->hadjustment); + if (this->vadjustment) g_object_unref(this->vadjustment); + + this->child = NULL; + this->hscrollbar = NULL; + this->vscrollbar = NULL; + this->hadjustment = NULL; + this->vadjustment = NULL; + + GTK_OBJECT_CLASS(logical_scroll_pane_parent_class)->destroy(this_); +} + +static void +logical_scroll_pane_size_request(GtkWidget *this_, GtkRequisition *requisition) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + GtkRequisition hreq; + GtkRequisition vreq; + + requisition->width = hreq.width = vreq.width = 0; + requisition->height = hreq.height = vreq.height = 0; + + if (this->hscrollbar && this->hscrollbar_visible) gtk_widget_size_request(this->hscrollbar, &hreq); + if (this->vscrollbar && this->vscrollbar_visible) gtk_widget_size_request(this->vscrollbar, &vreq); + + if (this->vscrollbar_visible) requisition->width += vreq.width; + if (this->hscrollbar_visible) requisition->height += hreq.height; + + requisition->width = MAX(requisition->width, hreq.width); + requisition->height = MAX(requisition->height, vreq.height); +} + +static void +logical_scroll_pane_size_allocate(GtkWidget *this_, GtkAllocation *allocation) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + GtkRequisition hreq; + GtkRequisition vreq; + + GtkAllocation child_allocation; + GtkAllocation hscrollbar_allocation; + GtkAllocation vscrollbar_allocation; + + int content_width; + int content_height; + double x, y, max; + + this_->allocation = *allocation; + + x = this->hadjustment->value + 0.5 * this->hadjustment->page_size; + y = this->vadjustment->value + 0.5 * this->vadjustment->page_size; + + hreq.width = hreq.height = 0; + vreq.width = vreq.height = 0; + + if (this->hscrollbar && this->hscrollbar_visible) gtk_widget_size_request(this->hscrollbar, &hreq); + if (this->vscrollbar && this->vscrollbar_visible) gtk_widget_size_request(this->vscrollbar, &vreq); + + content_width = allocation->width; + if (this->vscrollbar_visible) + content_width -= vreq.width; + content_width = MAX(content_width, 1); + + content_height = allocation->height; + if (this->hscrollbar_visible) + content_height -= hreq.height; + content_height = MAX(content_height, 1); + + logical_scroll_pane_configure_adjustment(this->hadjustment, this->content_width, content_width); + logical_scroll_pane_configure_adjustment(this->vadjustment, this->content_height, content_height); + + max = this->hadjustment->upper - this->hadjustment->page_size; + x -= 0.5 * this->hadjustment->page_size; + x = MIN(MAX(this->hadjustment->lower, x), max); + gtk_adjustment_set_value(this->hadjustment, x); + + max = this->vadjustment->upper - this->vadjustment->page_size; + y -= 0.5 * this->vadjustment->page_size; + y = MIN(MAX(this->vadjustment->lower, y), max); + gtk_adjustment_set_value(this->vadjustment, y); + + if (this->child && GTK_WIDGET_VISIBLE(this->child)) { + child_allocation.x = allocation->x; + child_allocation.y = allocation->y; + child_allocation.width = content_width; + child_allocation.height = content_height; + gtk_widget_size_allocate(this->child, &child_allocation); + } + + if (this->hscrollbar && this->hscrollbar_visible) { + hscrollbar_allocation.x = allocation->x; + hscrollbar_allocation.y = allocation->y + content_height; + hscrollbar_allocation.width = content_width; + hscrollbar_allocation.height = hreq.height; + gtk_widget_size_allocate(this->hscrollbar, &hscrollbar_allocation); + } + + if (this->vscrollbar && this->vscrollbar_visible) { + vscrollbar_allocation.x = allocation->x + content_width; + vscrollbar_allocation.y = allocation->y; + vscrollbar_allocation.width = vreq.width; + vscrollbar_allocation.height = content_height; + gtk_widget_size_allocate(this->vscrollbar, &vscrollbar_allocation); + } +} + +static void +logical_scroll_pane_add(GtkContainer *this_, GtkWidget *widget) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + if (this->child) { + g_warning("LogicalScrollPane already has a child"); + return; + } + logical_scroll_pane_set_child(this, widget); +} + +static void +logical_scroll_pane_remove(GtkContainer *this_, GtkWidget *widget) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + if (widget == this->child) { + this->child = NULL; + GTK_BIN(this)->child = NULL; + } else if (widget == this->hscrollbar) { + this->hscrollbar = NULL; + } else if (widget == this->vscrollbar) { + this->vscrollbar = NULL; + } else { + g_warning("Widget is not a child of LogicalScrollPane"); + return; + } + + gtk_widget_unparent(widget); + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +static void +logical_scroll_pane_forall(GtkContainer *this_, int include_internals, GtkCallback callback, void *callback_data) +{ + LogicalScrollPane *this = LOGICAL_SCROLL_PANE(this_); + + if (this->child) (*callback)(this->child, callback_data); + + if (!include_internals) + return; + + if (this->hscrollbar) (*callback)(this->hscrollbar, callback_data); + if (this->vscrollbar) (*callback)(this->vscrollbar, callback_data); +} + +static void +set_adjustment(LogicalScrollPane *this, GtkAdjustment *adjustment, GtkAdjustment **slot, GtkWidget *scrollbar) +{ + if (!adjustment) + adjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 0.0, 1.0, 1.0, 0.0)); + + if (*slot != adjustment) { + g_object_ref_sink(adjustment); + + if (*slot) { + g_signal_handlers_disconnect_by_data(*slot, this); + g_object_unref(*slot); + } + + *slot = adjustment; + g_signal_connect(*slot, "value-changed", + G_CALLBACK(&logical_scroll_pane_queue_child_draw), this); + } + + if (scrollbar) + gtk_range_set_adjustment(GTK_RANGE(scrollbar), adjustment); +} + +static void +logical_scroll_pane_set_adjustment(LogicalScrollPane *this, int horizontal, GtkAdjustment *adjustment) +{ + if (horizontal) + set_adjustment(this, adjustment, &this->hadjustment, this->hscrollbar); + else + set_adjustment(this, adjustment, &this->vadjustment, this->vscrollbar); +} + +static void +logical_scroll_pane_class_init(LogicalScrollPaneClass *class) +{ + GtkObjectClass *object_class = GTK_OBJECT_CLASS(class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS(class); + + object_class->destroy = &logical_scroll_pane_destroy; + + widget_class->size_request = &logical_scroll_pane_size_request; + widget_class->size_allocate = &logical_scroll_pane_size_allocate; + + container_class->add = &logical_scroll_pane_add; + container_class->remove = &logical_scroll_pane_remove; + container_class->forall = &logical_scroll_pane_forall; +} + +static void +logical_scroll_pane_init(LogicalScrollPane *this) +{ + this->child = NULL; + this->content_width = 1; + this->content_height = 1; + this->hscrollbar_visible = 1; + this->vscrollbar_visible = 1; + + this->hadjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, /* value */ + 0.0, /* lower */ + 0.0, /* upper */ + 1.0, /* step increment */ + 1.0, /* page increment */ + 0.0)); /* page size */ + + this->vadjustment = GTK_ADJUSTMENT(gtk_adjustment_new(0.0, 0.0, 0.0, 1.0, 1.0, 0.0)); + + g_object_ref_sink(this->hadjustment); + g_object_ref_sink(this->vadjustment); + g_signal_connect(this->hadjustment, "value-changed", + G_CALLBACK(&logical_scroll_pane_queue_child_draw), this); + g_signal_connect(this->vadjustment, "value-changed", + G_CALLBACK(&logical_scroll_pane_queue_child_draw), this); + + this->hscrollbar = gtk_hscrollbar_new(this->hadjustment); + this->vscrollbar = gtk_vscrollbar_new(this->vadjustment); + + gtk_widget_set_parent(this->hscrollbar, GTK_WIDGET(this)); + gtk_widget_set_parent(this->vscrollbar, GTK_WIDGET(this)); + + gtk_widget_show(this->hscrollbar); + gtk_widget_show(this->vscrollbar); +} + +GtkWidget * +logical_scroll_pane_new(void) +{ + return GTK_WIDGET(g_object_new(LOGICAL_TYPE_SCROLL_PANE, NULL)); +} + +void +logical_scroll_pane_set_child(LogicalScrollPane *this, GtkWidget *child) +{ + if (child == this->child) + return; + + if (this->child) { + gtk_widget_unparent(this->child); + this->child = NULL; + GTK_BIN(this)->child = NULL; + } + + if (child) { + this->child = child; + GTK_BIN(this)->child = child; + gtk_widget_set_parent(child, GTK_WIDGET(this)); + if (GTK_WIDGET_VISIBLE(this)) + gtk_widget_show(child); + } + + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +GtkWidget * +logical_scroll_pane_get_child(LogicalScrollPane *this) +{ + return this->child; +} + +void +logical_scroll_pane_set_content_size(LogicalScrollPane *this, int width, int height) +{ + this->content_width = MAX(1, width); + this->content_height = MAX(1, height); + logical_scroll_pane_configure_adjustment(this->hadjustment, this->content_width, (int)this->hadjustment->page_size); + logical_scroll_pane_configure_adjustment(this->vadjustment, this->content_height, (int)this->vadjustment->page_size); + gtk_widget_queue_resize(GTK_WIDGET(this)); + if (this->child) + gtk_widget_queue_draw(this->child); +} + +static void +set_scrollbar(LogicalScrollPane *this, GtkWidget *scrollbar, GtkWidget **slot, int *scrollbar_visible) +{ + if (scrollbar == *slot) + return; + + if (scrollbar) { + if (*slot) + gtk_widget_unparent(*slot); + *slot = scrollbar; + gtk_widget_set_parent(scrollbar, GTK_WIDGET(this)); + + if (*scrollbar_visible) + gtk_widget_show(scrollbar); + } else { + if (*slot) { + gtk_widget_unparent(*slot); + *slot = NULL; + } + } + + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +void +logical_scroll_pane_set_hscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar) +{ + set_scrollbar(this, scrollbar, &this->hscrollbar, &this->hscrollbar_visible); +} + +void +logical_scroll_pane_set_vscrollbar(LogicalScrollPane *this, GtkWidget *scrollbar) +{ + set_scrollbar(this, scrollbar, &this->vscrollbar, &this->vscrollbar_visible); +} + +GtkWidget * +logical_scroll_pane_get_hscrollbar(LogicalScrollPane *this) +{ + return this->hscrollbar; +} + +GtkWidget * +logical_scroll_pane_get_vscrollbar(LogicalScrollPane *this) +{ + return this->vscrollbar; +} + +static void +set_scrollbar_visible(LogicalScrollPane *this, int visible, GtkWidget **scrollbar, int *scrollbar_visible) +{ + visible = visible != 0; + + if (*scrollbar_visible == visible) + return; + + *scrollbar_visible = visible; + + if (*scrollbar) + gtk_widget_set_visible(*scrollbar, visible); + + gtk_widget_queue_resize(GTK_WIDGET(this)); +} + +void +logical_scroll_pane_set_hscrollbar_visible(LogicalScrollPane *this, int visible) +{ + set_scrollbar_visible(this, visible, &this->hscrollbar, &this->hscrollbar_visible); +} + +void +logical_scroll_pane_set_vscrollbar_visible(LogicalScrollPane *this, int visible) +{ + set_scrollbar_visible(this, visible, &this->vscrollbar, &this->vscrollbar_visible); +} + +int +logical_scroll_pane_get_hscrollbar_visible(LogicalScrollPane *this) +{ + return this->hscrollbar_visible; +} + +int +logical_scroll_pane_get_vscrollbar_visible(LogicalScrollPane *this) +{ + return this->vscrollbar_visible; +} + +void +logical_scroll_pane_set_hadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment) +{ + logical_scroll_pane_set_adjustment(this, 1, adjustment); +} + +void +logical_scroll_pane_set_vadjustment(LogicalScrollPane *this, GtkAdjustment *adjustment) +{ + logical_scroll_pane_set_adjustment(this, 0, adjustment); +} + +GtkAdjustment * +logical_scroll_pane_get_hadjustment(LogicalScrollPane *this) +{ + return this->hadjustment; +} + +GtkAdjustment * +logical_scroll_pane_get_vadjustment(LogicalScrollPane *this) +{ + return this->vadjustment; +} diff --git a/logical-scroll-pane.h b/logical-scroll-pane.h new file mode 100644 index 0000000..c38ca6a --- /dev/null +++ b/logical-scroll-pane.h @@ -0,0 +1,55 @@ +#include + +#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 + +#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; +} diff --git a/ui.c b/ui.c new file mode 100644 index 0000000..8184d68 --- /dev/null +++ b/ui.c @@ -0,0 +1,307 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" + +#define LIST_MENU(TOPLEVEL, SEPARATOR, ACTION, TOGGLE, D)\ + TOPLEVEL(0, "", "File", D)\ + ACTION(0, 0u, "", "Open", GTK_STOCK_OPEN, "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, "P", "Ctrl+P", print_action, D)\ + SEPARATOR(0, D)\ + ACTION(0, 1u, "Proper", "ties", GTK_STOCK_PROPERTIES, "Return", "Alt+Return", properties_action, D)\ + SEPARATOR(0, D)\ + ACTION(0, 0u, "", "Close", GTK_STOCK_CLOSE, "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", "R", "Alt+R", reset_transformations_action, D)\ + ACTION(1, 1u, "Rotate anti", "clockwise", "image-rotate-left", "R", "Shift+R", rotate_anticlockwise_action, D)\ + ACTION(0, 1u, "R", "otate 180°", "image-rotate", "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", "T", "Shift+T", antitranspose_action, D)\ + SEPARATOR(1, D)\ + TOGGLE(1, 1u, "", "Invert colours", "filters-invert", "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", "H", "Ctrl+H", width_fit_action, GROUP_ZOOM, 0, D)\ + TOGGLE(1, 1u, "Fit to ", "height", "zoom-fit-height", "V", "Ctrl+V", height_fit_action, GROUP_ZOOM, 0, D)\ + TOGGLE(1, 1u, "", "Fit to window", "zoom-fit-page", "F", "Ctrl+Shift+F", window_fit_action, GROUP_ZOOM, 0, D)\ + TOGGLE(1, 1u, "", "Best fit", "zoom-fit-best", "F", "Ctrl+F", best_fit_action, GROUP_ZOOM, 1, D)\ + TOPLEVEL(1, "", "Go", D)\ + ACTION(1, 2u, "", "Previous image", GTK_STOCK_GO_BACK, "Left", "Ctrl+Left", previous_action, D)\ + ACTION(1, 2u, "", "Next image", GTK_STOCK_GO_FORWARD, "Right", "Ctrl+Right", next_action, D)\ + SEPARATOR(0, D)\ + ACTION(1, 2u, "", "First image", GTK_STOCK_GOTO_FIRST, "Home", "Ctrl+Home", first_action, D)\ + ACTION(1, 2u, "", "Last image", GTK_STOCK_GOTO_LAST, "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); +} diff --git a/zoom.c b/zoom.c new file mode 100644 index 0000000..caa9884 --- /dev/null +++ b/zoom.c @@ -0,0 +1,82 @@ +/* See LICENSE file for copyright and license details. */ +#include "common.h" +#include /* 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; +} -- cgit v1.3.1