diff options
Diffstat (limited to 'src/SinglePhotoPage.vala')
-rw-r--r-- | src/SinglePhotoPage.vala | 537 |
1 files changed, 537 insertions, 0 deletions
diff --git a/src/SinglePhotoPage.vala b/src/SinglePhotoPage.vala new file mode 100644 index 0000000..754a649 --- /dev/null +++ b/src/SinglePhotoPage.vala @@ -0,0 +1,537 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU LGPL (version 2.1 or later). + * See the COPYING file in this distribution. + */ + +public abstract class SinglePhotoPage : Page { + public const Gdk.InterpType FAST_INTERP = Gdk.InterpType.NEAREST; + public const Gdk.InterpType QUALITY_INTERP = Gdk.InterpType.BILINEAR; + public const int KEY_REPEAT_INTERVAL_MSEC = 200; + + public enum UpdateReason { + NEW_PIXBUF, + QUALITY_IMPROVEMENT, + RESIZED_CANVAS + } + + protected Gtk.DrawingArea canvas = new Gtk.DrawingArea(); + protected Gtk.Viewport viewport = new Gtk.Viewport(null, null); + + private bool scale_up_to_viewport; + private TransitionClock transition_clock; + private int transition_duration_msec = 0; + private Cairo.Surface pixmap = null; + private Cairo.Context pixmap_ctx = null; + private Cairo.Context text_ctx = null; + private Dimensions pixmap_dim = Dimensions(); + private Gdk.Pixbuf unscaled = null; + private Dimensions max_dim = Dimensions(); + private Gdk.Pixbuf scaled = null; + private Gdk.Pixbuf old_scaled = null; // previous scaled image + private Gdk.Rectangle scaled_pos = Gdk.Rectangle(); + private ZoomState static_zoom_state; + private bool zoom_high_quality = true; + private ZoomState saved_zoom_state; + private bool has_saved_zoom_state = false; + private uint32 last_nav_key = 0; + + protected SinglePhotoPage(string page_name, bool scale_up_to_viewport) { + base(page_name); + this.wheel_factor = 0.9999; + + this.scale_up_to_viewport = scale_up_to_viewport; + + transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock(); + + // With the current code automatically resizing the image to the viewport, scrollbars + // should never be shown, but this may change if/when zooming is supported + set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); + + set_border_width(0); + set_shadow_type(Gtk.ShadowType.NONE); + + viewport.set_shadow_type(Gtk.ShadowType.NONE); + viewport.set_border_width(0); + viewport.add(canvas); + + add(viewport); + + canvas.add_events(Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.STRUCTURE_MASK + | Gdk.EventMask.SUBSTRUCTURE_MASK); + + viewport.size_allocate.connect(on_viewport_resize); + canvas.draw.connect(on_canvas_exposed); + + set_event_source(canvas); + Config.Facade.get_instance().colors_changed.connect(on_colors_changed); + } + + ~SinglePhotoPage() { + Config.Facade.get_instance().colors_changed.disconnect(on_colors_changed); + } + + public bool is_transition_in_progress() { + return transition_clock.is_in_progress(); + } + + public void cancel_transition() { + if (transition_clock.is_in_progress()) + transition_clock.cancel(); + } + + public void set_transition(string effect_id, int duration_msec) { + cancel_transition(); + + transition_clock = TransitionEffectsManager.get_instance().create_transition_clock(effect_id); + if (transition_clock == null) + transition_clock = TransitionEffectsManager.get_instance().create_null_transition_clock(); + + transition_duration_msec = duration_msec; + } + + // This method includes a call to pixmap_ctx.paint(). + private void render_zoomed_to_pixmap(ZoomState zoom_state) { + assert(is_zoom_supported()); + + Gdk.Rectangle view_rect = zoom_state.get_viewing_rectangle_wrt_content(); + + Gdk.Pixbuf zoomed; + if (get_zoom_buffer() != null) { + zoomed = (zoom_high_quality) ? get_zoom_buffer().get_zoomed_image(zoom_state) : + get_zoom_buffer().get_zoom_preview_image(zoom_state); + } else { + Gdk.Rectangle view_rect_proj = zoom_state.get_viewing_rectangle_projection(unscaled); + + Gdk.Pixbuf proj_subpixbuf = new Gdk.Pixbuf.subpixbuf(unscaled, view_rect_proj.x, + view_rect_proj.y, view_rect_proj.width, view_rect_proj.height); + + zoomed = proj_subpixbuf.scale_simple(view_rect.width, view_rect.height, + Gdk.InterpType.BILINEAR); + } + + if (zoomed == null) { + return; + } + + int draw_x = (pixmap_dim.width - view_rect.width) / 2; + draw_x = draw_x.clamp(0, int.MAX); + + int draw_y = (pixmap_dim.height - view_rect.height) / 2; + draw_y = draw_y.clamp(0, int.MAX); + paint_pixmap_with_background(pixmap_ctx, zoomed, draw_x, draw_y); + } + + protected void on_interactive_zoom(ZoomState interactive_zoom_state) { + assert(is_zoom_supported()); + + set_source_color_from_string(pixmap_ctx, "#000"); + pixmap_ctx.paint(); + + bool old_quality_setting = zoom_high_quality; + zoom_high_quality = false; + render_zoomed_to_pixmap(interactive_zoom_state); + zoom_high_quality = old_quality_setting; + + canvas.queue_draw(); + } + + protected void on_interactive_pan(ZoomState interactive_zoom_state) { + assert(is_zoom_supported()); + + set_source_color_from_string(pixmap_ctx, "#000"); + pixmap_ctx.paint(); + + bool old_quality_setting = zoom_high_quality; + zoom_high_quality = true; + render_zoomed_to_pixmap(interactive_zoom_state); + zoom_high_quality = old_quality_setting; + + canvas.queue_draw(); + } + + protected virtual bool is_zoom_supported() { + return false; + } + + protected virtual void cancel_zoom() { + if (pixmap != null) { + set_source_color_from_string(pixmap_ctx, "#000"); + pixmap_ctx.paint(); + } + } + + protected virtual void save_zoom_state() { + saved_zoom_state = static_zoom_state; + has_saved_zoom_state = true; + } + + protected virtual void restore_zoom_state() { + if (!has_saved_zoom_state) + return; + + static_zoom_state = saved_zoom_state; + repaint(); + has_saved_zoom_state = false; + } + + protected virtual ZoomBuffer? get_zoom_buffer() { + return null; + } + + protected ZoomState get_saved_zoom_state() { + return saved_zoom_state; + } + + protected void set_zoom_state(ZoomState zoom_state) { + assert(is_zoom_supported()); + + static_zoom_state = zoom_state; + } + + protected ZoomState get_zoom_state() { + assert(is_zoom_supported()); + + return static_zoom_state; + } + + public override void switched_to() { + base.switched_to(); + + if (unscaled != null) + repaint(); + } + + public override void set_container(Gtk.Window container) { + base.set_container(container); + + // scrollbar policy in fullscreen mode needs to be auto/auto, else the pixbuf will shift + // off the screen + if (container is FullscreenWindow) + set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC); + } + + // max_dim represents the maximum size of the original pixbuf (i.e. pixbuf may be scaled and + // the caller capable of producing larger ones depending on the viewport size). max_dim + // is used when scale_up_to_viewport is set to true. Pass a Dimensions with no area if + // max_dim should be ignored (i.e. scale_up_to_viewport is false). + public void set_pixbuf(Gdk.Pixbuf unscaled, Dimensions max_dim, Direction? direction = null) { + static_zoom_state = ZoomState(max_dim, pixmap_dim, + static_zoom_state.get_interpolation_factor(), + static_zoom_state.get_viewport_center()); + + cancel_transition(); + + this.unscaled = unscaled; + this.max_dim = max_dim; + this.old_scaled = scaled; + scaled = null; + + // need to make sure this has happened + canvas.realize(); + + repaint(direction); + } + + public void blank_display() { + unscaled = null; + max_dim = Dimensions(); + scaled = null; + pixmap = null; + + // this has to have happened + canvas.realize(); + + // force a redraw + invalidate_all(); + } + + public Cairo.Surface? get_surface() { + return pixmap; + } + + public Dimensions get_surface_dim() { + return pixmap_dim; + } + + public Cairo.Context get_cairo_context() { + return pixmap_ctx; + } + + public void paint_text(Pango.Layout pango_layout, int x, int y) { + text_ctx.move_to(x, y); + Pango.cairo_show_layout(text_ctx, pango_layout); + } + + public Scaling get_canvas_scaling() { + return (get_container() is FullscreenWindow) ? Scaling.for_screen(AppWindow.get_instance(), scale_up_to_viewport) + : Scaling.for_widget(viewport, scale_up_to_viewport); + } + + public Gdk.Pixbuf? get_unscaled_pixbuf() { + return unscaled; + } + + public Gdk.Pixbuf? get_scaled_pixbuf() { + return scaled; + } + + // Returns a rectangle describing the pixbuf in relation to the canvas + public Gdk.Rectangle get_scaled_pixbuf_position() { + return scaled_pos; + } + + public bool is_inside_pixbuf(int x, int y) { + return coord_in_rectangle((int)Math.lround(x * Application.get_scale()), + (int)Math.lround(y * Application.get_scale()), scaled_pos); + } + + public void invalidate(Gdk.Rectangle rect) { + if (canvas.get_window() != null) + canvas.get_window().invalidate_rect(rect, false); + } + + public void invalidate_all() { + if (canvas.get_window() != null) + canvas.get_window().invalidate_rect(null, false); + } + + private void on_viewport_resize() { + // do fast repaints while resizing + internal_repaint(true, null); + } + + protected override void on_resize_finished(Gdk.Rectangle rect) { + base.on_resize_finished(rect); + + // when the resize is completed, do a high-quality repaint + repaint(); + } + + private bool on_canvas_exposed(Cairo.Context exposed_ctx) { + // draw pixmap onto canvas unless it's not been instantiated, in which case draw black + // (so either old image or contents of another page is not left on screen) + if (pixmap != null) { + pixmap.set_device_scale(Application.get_scale(), Application.get_scale()); + exposed_ctx.set_source_surface(pixmap, 0, 0); + } + else + set_source_color_from_string(exposed_ctx, "#000"); + + exposed_ctx.rectangle(0, 0, get_allocated_width(), get_allocated_height()); + exposed_ctx.paint(); + + if (pixmap != null) { + pixmap.set_device_scale(1.0, 1.0); + } + + return true; + } + + protected virtual void new_surface(Cairo.Context ctx, Dimensions ctx_dim) { + } + + protected virtual void updated_pixbuf(Gdk.Pixbuf pixbuf, UpdateReason reason, Dimensions old_dim) { + } + + protected virtual void paint(Cairo.Context ctx, Dimensions ctx_dim) { + if (is_zoom_supported() && (!static_zoom_state.is_default())) { + set_source_color_from_string(ctx, "#000"); + ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height); + ctx.fill(); + + render_zoomed_to_pixmap(static_zoom_state); + } else if (!transition_clock.paint(ctx, ctx_dim.width, ctx_dim.height)) { + // transition is not running, so paint the full image on a black background + set_source_color_from_string(ctx, "#000"); + + ctx.rectangle(0, 0, pixmap_dim.width, pixmap_dim.height); + ctx.fill(); + + //scaled.save("src%010d.png".printf(buffer_counter), "png"); + paint_pixmap_with_background(ctx, scaled, scaled_pos.x, scaled_pos.y); + //pixmap.write_to_png("%010d.png".printf(buffer_counter++)); + } + } + + private void repaint_pixmap() { + if (pixmap_ctx == null) + return; + + paint(pixmap_ctx, pixmap_dim); + invalidate_all(); + } + + public void repaint(Direction? direction = null) { + internal_repaint(false, direction); + } + + private void internal_repaint(bool fast, Direction? direction) { + // if not in view, assume a full repaint needed in future but do nothing more + if (!is_in_view()) { + pixmap = null; + scaled = null; + + return; + } + + // no image or window, no painting + if (unscaled == null || canvas.get_window() == null) + return; + + Gtk.Allocation allocation; + viewport.get_allocation(out allocation); + + int width = allocation.width; + int height = allocation.height; + + if (width <= 0 || height <= 0) + return; + + bool new_pixbuf = (scaled == null); + + // save if reporting an image being rescaled + Dimensions old_scaled_dim = Dimensions.for_rectangle(scaled_pos); + + Gdk.Rectangle old_scaled_pos = scaled_pos; + + // attempt to reuse pixmap + if (pixmap_dim.width != width || pixmap_dim.height != height) + pixmap = null; + + // if necessary, create a pixmap as large as the entire viewport + bool new_pixmap = false; + if (pixmap == null) { + init_pixmap((int)Math.lround(width * Application.get_scale()), (int)Math.lround(height * Application.get_scale())); + new_pixmap = true; + } + + if (new_pixbuf || new_pixmap) { + Dimensions unscaled_dim = Dimensions.for_pixbuf(unscaled); + + // determine scaled size of pixbuf ... if a max dimensions is set and not scaling up, + // respect it + Dimensions scaled_dim = Dimensions(); + if (!scale_up_to_viewport && max_dim.has_area() && max_dim.width < width && max_dim.height < height) + scaled_dim = max_dim; + else + scaled_dim = unscaled_dim.get_scaled_proportional(pixmap_dim); + + // center pixbuf on the canvas + scaled_pos.x = (int)Math.lround(((width * Application.get_scale()) - scaled_dim.width) / 2.0); + scaled_pos.y = (int)Math.lround(((height * Application.get_scale()) - scaled_dim.height) / 2.0); + scaled_pos.width = scaled_dim.width; + scaled_pos.height = scaled_dim.height; + } + + Gdk.InterpType interp = (fast) ? FAST_INTERP : QUALITY_INTERP; + + // rescale if canvas rescaled or better quality is requested + if (scaled == null) { + scaled = resize_pixbuf(unscaled, Dimensions.for_rectangle(scaled_pos), interp); + + UpdateReason reason = UpdateReason.RESIZED_CANVAS; + if (new_pixbuf) + reason = UpdateReason.NEW_PIXBUF; + else if (!new_pixmap && interp == QUALITY_INTERP) + reason = UpdateReason.QUALITY_IMPROVEMENT; + + static_zoom_state = ZoomState(max_dim, pixmap_dim, + static_zoom_state.get_interpolation_factor(), + static_zoom_state.get_viewport_center()); + + updated_pixbuf(scaled, reason, old_scaled_dim); + } + + zoom_high_quality = !fast; + + if (direction != null && !transition_clock.is_in_progress()) { + Spit.Transitions.Visuals visuals = new Spit.Transitions.Visuals(old_scaled, + old_scaled_pos, scaled, scaled_pos, parse_color("#000")); + + transition_clock.start(visuals, direction.to_transition_direction(), transition_duration_msec, + repaint_pixmap); + } + + if (!transition_clock.is_in_progress()) + repaint_pixmap(); + } + + private void init_pixmap(int width, int height) { + assert(unscaled != null); + assert(canvas.get_window() != null); + + // Cairo backing surface (manual double-buffering) + pixmap = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height); + pixmap_dim = Dimensions(width, height); + + // Cairo context for drawing on the pixmap + pixmap_ctx = new Cairo.Context(pixmap); + + // need a new pixbuf to fit this scale + scaled = null; + + // Cairo context for drawing text on the pixmap + text_ctx = new Cairo.Context(pixmap); + set_source_color_from_string(text_ctx, "#fff"); + + + // no need to resize canvas, viewport does that automatically + + new_surface(pixmap_ctx, pixmap_dim); + } + + protected override bool on_context_keypress() { + return popup_context_menu(get_page_context_menu()); + } + + protected virtual void on_previous_photo() { + } + + protected virtual void on_next_photo() { + } + + public override bool key_press_event(Gdk.EventKey event) { + // if the user holds the arrow keys down, we will receive a steady stream of key press + // events for an operation that isn't designed for a rapid succession of output ... + // we staunch the supply of new photos to under a quarter second (#533) + bool nav_ok = (event.time - last_nav_key) > KEY_REPEAT_INTERVAL_MSEC; + + bool handled = true; + switch (Gdk.keyval_name(event.keyval)) { + case "Left": + case "KP_Left": + case "BackSpace": + if (nav_ok) { + on_previous_photo(); + last_nav_key = event.time; + } + break; + + case "Right": + case "KP_Right": + case "space": + if (nav_ok) { + on_next_photo(); + last_nav_key = event.time; + } + break; + + default: + handled = false; + break; + } + + if (handled) + return true; + + return (base.key_press_event != null) ? base.key_press_event(event) : true; + } + + private void on_colors_changed() { + invalidate_transparent_background(); + repaint(); + } +} + + |