/* 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(); } }