[shotwell/wip/phako/nicer-screen: 1/2] split-up CheckerboardLayout.vala



commit 037f2cfc33b1e95bb6fe770fd70c39804b6e27a3
Author: Jens Georg <mail jensge org>
Date:   Mon Jan 28 16:00:37 2019 +0100

    split-up CheckerboardLayout.vala

 src/CheckerboardItem.vala     | 734 +++++++++++++++++++++++++++++++++++++
 src/CheckerboardItemText.vala |  98 +++++
 src/CheckerboardLayout.vala   | 818 ------------------------------------------
 src/meson.build               |   2 +
 4 files changed, 834 insertions(+), 818 deletions(-)
---
diff --git a/src/CheckerboardItem.vala b/src/CheckerboardItem.vala
new file mode 100644
index 00000000..c423cad4
--- /dev/null
+++ b/src/CheckerboardItem.vala
@@ -0,0 +1,734 @@
+/* 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 CheckerboardItem : ThumbnailView {
+    // Collection properties CheckerboardItem understands
+    // SHOW_TITLES (bool)
+    public const string PROP_SHOW_TITLES = "show-titles";
+    // SHOW_COMMENTS (bool)
+    public const string PROP_SHOW_COMMENTS = "show-comments";
+    // SHOW_SUBTITLES (bool)
+    public const string PROP_SHOW_SUBTITLES = "show-subtitles";
+    
+    public const int FRAME_WIDTH = 8;
+    public const int LABEL_PADDING = 4;
+    public const int BORDER_WIDTH = 1;
+
+    public const int SHADOW_RADIUS = 4;
+    public const float SHADOW_INITIAL_ALPHA = 0.5f;
+    
+    public const int TRINKET_SCALE = 12;
+    public const int TRINKET_PADDING = 1;
+    
+    public const int BRIGHTEN_SHIFT = 0x18;
+    
+    public Dimensions requisition = Dimensions();
+    public Gdk.Rectangle allocation = Gdk.Rectangle();
+    
+    private bool exposure = false;
+    private CheckerboardItemText? title = null;
+    private bool title_visible = true;
+    private CheckerboardItemText? comment = null;
+    private bool comment_visible = true;
+    private CheckerboardItemText? subtitle = null;
+    private bool subtitle_visible = false;
+    private bool is_cursor = false;
+    private Pango.Alignment tag_alignment = Pango.Alignment.LEFT;
+    private Gee.List<Tag>? user_visible_tag_list = null;
+    private Gee.Collection<Tag> tags;
+    private Gdk.Pixbuf pixbuf = null;
+    private Gdk.Pixbuf display_pixbuf = null;
+    private Gdk.Pixbuf brightened = null;
+    private Dimensions pixbuf_dim = Dimensions();
+    private int col = -1;
+    private int row = -1;
+    private int horizontal_trinket_offset = 0;
+    
+    public CheckerboardItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title, string? 
comment,
+        bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
+        base(source);
+        
+        pixbuf_dim = initial_pixbuf_dim;
+        this.title = new CheckerboardItemText(title, alignment, marked_up);
+        // on the checkboard page we display the comment in 
+        // one line, i.e., replacing all newlines with spaces.
+        // that means that the display will contain "..." if the comment
+        // is too long.
+        // warning: changes here have to be done in set_comment, too!
+        if (comment != null)
+            this.comment = new CheckerboardItemText(comment.replace("\n", " "), alignment,
+                marked_up);
+        
+        // Don't calculate size here, wait for the item to be assigned to a ViewCollection
+        // (notify_membership_changed) and calculate when the collection's property settings
+        // are known
+    }
+
+    public bool has_tags { get; private set; }
+
+    public override string get_name() {
+        return (title != null) ? title.get_text() : base.get_name();
+    }
+    
+    public string get_title() {
+        return (title != null) ? title.get_text() : "";
+    }
+    
+    public string get_comment() {
+        return (comment != null) ? comment.get_text() : "";
+    }
+    
+    public void set_title(string text, bool marked_up = false,
+        Pango.Alignment alignment = Pango.Alignment.LEFT) {
+        if (title != null && title.is_set_to(text, marked_up, alignment))
+            return;
+        
+        title = new CheckerboardItemText(text, alignment, marked_up);
+        
+        if (title_visible) {
+            recalc_size("set_title");
+            notify_view_altered();
+        }
+    }
+
+    public void translate_coordinates(ref int x, ref int y) {
+        x -= allocation.x + FRAME_WIDTH;
+        y -= allocation.y + FRAME_WIDTH;
+    }
+    
+    public void clear_title() {
+        if (title == null)
+            return;
+        
+        title = null;
+        
+        if (title_visible) {
+            recalc_size("clear_title");
+            notify_view_altered();
+        }
+    }
+    
+    private void set_title_visible(bool visible) {
+        if (title_visible == visible)
+            return;
+        
+        title_visible = visible;
+        
+        recalc_size("set_title_visible");
+        notify_view_altered();
+    }
+    
+    public void set_comment(string text, bool marked_up = false,
+        Pango.Alignment alignment = Pango.Alignment.LEFT) {
+        if (comment != null && comment.is_set_to(text, marked_up, alignment))
+            return;
+        
+        comment = new CheckerboardItemText(text.replace("\n", " "), alignment, marked_up);
+        
+        if (comment_visible) {
+            recalc_size("set_comment");
+            notify_view_altered();
+        }
+    }
+    
+    public void clear_comment() {
+        if (comment == null)
+            return;
+        
+        comment = null;
+        
+        if (comment_visible) {
+            recalc_size("clear_comment");
+            notify_view_altered();
+        }
+    }
+    
+    private void set_comment_visible(bool visible) {
+        if (comment_visible == visible)
+            return;
+        
+        comment_visible = visible;
+        
+        recalc_size("set_comment_visible");
+        notify_view_altered();
+    }
+
+    public void set_tags(Gee.Collection<Tag>? tags,
+            Pango.Alignment alignment = Pango.Alignment.LEFT) {
+        has_tags = (tags != null && tags.size > 0);
+        tag_alignment = alignment;
+        string text;
+        if (has_tags) {
+            this.tags = tags;
+            user_visible_tag_list = Tag.make_user_visible_tag_list(tags);
+            text = Tag.make_tag_markup_string(user_visible_tag_list);
+        } else {
+            text = "<small>.</small>";
+        }
+
+        if (subtitle != null && subtitle.is_set_to(text, true, alignment))
+            return;
+        subtitle = new CheckerboardItemText(text, alignment, true);
+
+        if (subtitle_visible) {
+            recalc_size("set_subtitle");
+            notify_view_altered();
+        }
+    }
+
+    public void clear_tags() {
+        clear_subtitle();
+        has_tags = false;
+        user_visible_tag_list = null;
+    }
+
+    public void highlight_user_visible_tag(int index)
+            requires (user_visible_tag_list != null) {
+        string text = Tag.make_tag_markup_string(user_visible_tag_list, index);
+        subtitle = new CheckerboardItemText(text, tag_alignment, true);
+
+        if (subtitle_visible)
+            notify_view_altered();
+    }
+
+    public Tag get_user_visible_tag(int index)
+            requires (index >= 0 && index < user_visible_tag_list.size) {
+        return user_visible_tag_list.get(index);
+    }
+
+    public Pango.Layout? get_tag_list_layout() {
+        return has_tags ? subtitle.get_pango_layout() : null;
+    }
+
+    public Gdk.Rectangle get_subtitle_allocation() {
+        return subtitle.allocation;
+    }
+
+    public string get_subtitle() {
+        return (subtitle != null) ? subtitle.get_text() : "";
+    }
+    
+    public void set_subtitle(string text, bool marked_up = false, 
+        Pango.Alignment alignment = Pango.Alignment.LEFT) {
+        if (subtitle != null && subtitle.is_set_to(text, marked_up, alignment))
+            return;
+        
+        subtitle = new CheckerboardItemText(text, alignment, marked_up);
+        
+        if (subtitle_visible) {
+            recalc_size("set_subtitle");
+            notify_view_altered();
+        }
+    }
+    
+    public void clear_subtitle() {
+        if (subtitle == null)
+            return;
+        
+        subtitle = null;
+        
+        if (subtitle_visible) {
+            recalc_size("clear_subtitle");
+            notify_view_altered();
+        }
+    }
+    
+    private void set_subtitle_visible(bool visible) {
+        if (subtitle_visible == visible)
+            return;
+        
+        subtitle_visible = visible;
+        
+        recalc_size("set_subtitle_visible");
+        notify_view_altered();
+    }
+
+    public void set_is_cursor(bool is_cursor) {
+        this.is_cursor = is_cursor;
+    }
+
+    public bool get_is_cursor() {
+        return is_cursor;
+    }
+    
+    public virtual void handle_mouse_motion(int x, int y, int height, int width) {
+
+    }
+
+    public virtual void handle_mouse_leave() {
+        unbrighten();
+    }
+
+    public virtual void handle_mouse_enter() {
+        brighten();
+    }
+
+    protected override void notify_membership_changed(DataCollection? collection) {
+        bool title_visible = (bool) get_collection_property(PROP_SHOW_TITLES, true);
+        bool comment_visible = (bool) get_collection_property(PROP_SHOW_COMMENTS, true);
+        bool subtitle_visible = (bool) get_collection_property(PROP_SHOW_SUBTITLES, false);
+        
+        bool altered = false;
+        if (this.title_visible != title_visible) {
+            this.title_visible = title_visible;
+            altered = true;
+        }
+        
+        if (this.comment_visible != comment_visible) {
+            this.comment_visible = comment_visible;
+            altered = true;
+        }
+        
+        if (this.subtitle_visible != subtitle_visible) {
+            this.subtitle_visible = subtitle_visible;
+            altered = true;
+        }
+        
+        if (altered || !requisition.has_area()) {
+            recalc_size("notify_membership_changed");
+            notify_view_altered();
+        }
+        
+        base.notify_membership_changed(collection);
+    }
+    
+    protected override void notify_collection_property_set(string name, Value? old, Value val) {
+        switch (name) {
+            case PROP_SHOW_TITLES:
+                set_title_visible((bool) val);
+            break;
+            
+            case PROP_SHOW_COMMENTS:
+                set_comment_visible((bool) val);
+            break;
+            
+            case PROP_SHOW_SUBTITLES:
+                set_subtitle_visible((bool) val);
+            break;
+        }
+        
+        base.notify_collection_property_set(name, old, val);
+    }
+    
+    // The alignment point is the coordinate on the y-axis (relative to the top of the
+    // CheckerboardItem) which this item should be aligned to.  This allows for
+    // bottom-alignment along the bottom edge of the thumbnail.
+    public int get_alignment_point() {
+        return FRAME_WIDTH + BORDER_WIDTH + pixbuf_dim.height;
+    }
+    
+    public virtual void exposed() {
+        exposure = true;
+    }
+    
+    public virtual void unexposed() {
+        exposure = false;
+        
+        if (title != null)
+            title.clear_pango_layout();
+        
+        if (comment != null)
+            comment.clear_pango_layout();
+        
+        if (subtitle != null)
+            subtitle.clear_pango_layout();
+    }
+    
+    public virtual bool is_exposed() {
+        return exposure;
+    }
+
+    public bool has_image() {
+        return pixbuf != null;
+    }
+    
+    public Gdk.Pixbuf? get_image() {
+        return pixbuf;
+    }
+    
+    public void set_image(Gdk.Pixbuf pixbuf) {
+        this.pixbuf = pixbuf;
+        display_pixbuf = pixbuf;
+        pixbuf_dim = Dimensions.for_pixbuf(pixbuf);
+        
+        recalc_size("set_image");
+        notify_view_altered();
+    }
+    
+    public void clear_image(Dimensions dim) {
+        bool had_image = pixbuf != null;
+        
+        pixbuf = null;
+        display_pixbuf = null;
+        pixbuf_dim = dim;
+        
+        recalc_size("clear_image");
+        
+        if (had_image)
+            notify_view_altered();
+    }
+    
+    public static int get_max_width(int scale) {
+        // width is frame width (two sides) + frame padding (two sides) + width of pixbuf (text
+        // never wider)
+        return (FRAME_WIDTH * 2) + scale;
+    }
+    
+    private void recalc_size(string reason) {
+        Dimensions old_requisition = requisition;
+        
+        // only add in the text heights if they're displayed
+        int title_height = (title != null && title_visible)
+            ? title.get_height() + LABEL_PADDING : 0;
+        int comment_height = (comment != null && comment_visible)
+            ? comment.get_height() + LABEL_PADDING : 0;
+        int subtitle_height = (subtitle != null && subtitle_visible)
+            ? subtitle.get_height() + LABEL_PADDING : 0;
+        
+        // width is frame width (two sides) + frame padding (two sides) + width of pixbuf
+        // (text never wider)
+        requisition.width = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2) + pixbuf_dim.width;
+        
+        // height is frame width (two sides) + frame padding (two sides) + height of pixbuf
+        // + height of text + label padding (between pixbuf and text)
+        requisition.height = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2)
+            + pixbuf_dim.height + title_height + comment_height + subtitle_height;
+        
+#if TRACE_REFLOW_ITEMS
+        debug("recalc_size %s: %s title_height=%d comment_height=%d subtitle_height=%d requisition=%s", 
+            get_source().get_name(), reason, title_height, comment_height, subtitle_height,
+            requisition.to_string());
+#endif
+        
+        if (!requisition.approx_equals(old_requisition)) {
+#if TRACE_REFLOW_ITEMS
+            debug("recalc_size %s: %s notifying geometry altered", get_source().get_name(), reason);
+#endif
+            notify_geometry_altered();
+        }
+    }
+    
+    protected static Dimensions get_border_dimensions(Dimensions object_dim, int border_width) {
+        Dimensions dimensions = Dimensions();
+        dimensions.width = object_dim.width + (border_width * 2);
+        dimensions.height = object_dim.height + (border_width * 2);
+        return dimensions;
+    }
+    
+    protected static Gdk.Point get_border_origin(Gdk.Point object_origin, int border_width) {
+        Gdk.Point origin = Gdk.Point();
+        origin.x = object_origin.x - border_width;
+        origin.y = object_origin.y - border_width;
+        return origin;
+    }
+
+    protected virtual void paint_shadow(Cairo.Context ctx, Dimensions dimensions, Gdk.Point origin, 
+        int radius, float initial_alpha) { 
+        double rgb_all = 0.0;
+        
+        // top right corner
+        paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + radius, rgb_all, radius, 
+            initial_alpha, -0.5 * Math.PI, 0);
+        // bottom right corner
+        paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + dimensions.height, rgb_all, 
+            radius, initial_alpha, 0, 0.5 * Math.PI);
+        // bottom left corner
+        paint_shadow_in_corner(ctx, origin.x + radius, origin.y + dimensions.height, rgb_all, radius, 
+            initial_alpha, 0.5 * Math.PI, Math.PI);
+
+        // left right 
+        Cairo.Pattern lr = new Cairo.Pattern.linear(0, origin.y + dimensions.height, 
+            0, origin.y + dimensions.height + radius);
+        lr.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
+        lr.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
+        ctx.set_source(lr);
+        ctx.rectangle(origin.x + radius, origin.y + dimensions.height, dimensions.width - radius, radius);
+        ctx.fill();
+
+        // top down
+        Cairo.Pattern td = new Cairo.Pattern.linear(origin.x + dimensions.width, 
+            0, origin.x + dimensions.width + radius, 0);
+        td.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
+        td.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
+        ctx.set_source(td);
+        ctx.rectangle(origin.x + dimensions.width, origin.y + radius, 
+            radius, dimensions.height - radius);
+        ctx.fill();
+    }
+
+    protected void paint_shadow_in_corner(Cairo.Context ctx, int x, int y, 
+       double rgb_all, float radius, float initial_alpha, double arc1, double arc2) {
+        Cairo.Pattern p = new Cairo.Pattern.radial(x, y, 0, x, y, radius);
+        p.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
+        p.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0);
+        ctx.set_source(p);
+        ctx.move_to(x, y);
+        ctx.arc(x, y, radius, arc1, arc2);
+        ctx.close_path();
+        ctx.fill(); 
+    }
+
+    protected virtual void paint_border(Cairo.Context ctx, Dimensions object_dimensions,
+        Gdk.Point object_origin, int border_width) {
+        if (border_width == 1) {
+            ctx.rectangle(object_origin.x - border_width, object_origin.y - border_width,
+                object_dimensions.width + (border_width * 2),
+                object_dimensions.height + (border_width * 2));
+            ctx.fill();
+        } else {
+            Dimensions dimensions = get_border_dimensions(object_dimensions, border_width);
+            Gdk.Point origin = get_border_origin(object_origin, border_width);
+            
+            // amount of rounding needed on corners varies by size of object
+            double scale = int.max(object_dimensions.width, object_dimensions.height);
+            draw_rounded_corners_filled(ctx, dimensions, origin, 0.25 * scale);
+        }
+    }
+
+    protected virtual void paint_image(Cairo.Context ctx, Gdk.Pixbuf pixbuf, Gdk.Point origin) {
+        paint_pixmap_with_background(ctx, pixbuf, origin.x, origin.y);
+    }
+
+    private int get_selection_border_width(int scale) {
+        return ((scale <= ((Thumbnail.MIN_SCALE + Thumbnail.MAX_SCALE) / 3)) ? 5 : 4)
+            + BORDER_WIDTH;
+    }
+    
+    protected virtual Gdk.Pixbuf? get_top_left_trinket(int scale) {
+        return null;
+    }
+    
+    protected virtual Gdk.Pixbuf? get_top_right_trinket(int scale) {
+        return null;
+    }
+    
+    protected virtual Gdk.Pixbuf? get_bottom_left_trinket(int scale) {
+        return null;
+    }
+    
+    protected virtual Gdk.Pixbuf? get_bottom_right_trinket(int scale) {
+        return null;
+    }
+    
+    public void paint(Gtk.StyleContext style_context, Cairo.Context ctx, Gdk.RGBA bg_color, Gdk.RGBA 
selected_color,
+        Gdk.RGBA? border_color, Gdk.RGBA? focus_color) {
+        ctx.save();
+        ctx.translate(allocation.x + FRAME_WIDTH,
+                      allocation.y + FRAME_WIDTH);
+        // calc the top-left point of the pixbuf
+        Gdk.Point pixbuf_origin = Gdk.Point();
+        pixbuf_origin.x = BORDER_WIDTH;
+        pixbuf_origin.y = BORDER_WIDTH;
+        
+        ctx.set_line_width(FRAME_WIDTH);
+        ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
+            selected_color.alpha);
+
+        // draw shadow
+        if (border_color != null) {
+            ctx.save();
+            Dimensions shadow_dim = Dimensions();
+            shadow_dim.width = pixbuf_dim.width + BORDER_WIDTH;
+            shadow_dim.height = pixbuf_dim.height + BORDER_WIDTH;
+            paint_shadow(ctx, shadow_dim, pixbuf_origin, SHADOW_RADIUS, SHADOW_INITIAL_ALPHA);
+            ctx.restore();
+        }
+        
+        // draw a border for the cursor with the selection width and normal border color
+        if (is_cursor) {
+            ctx.save();
+            ctx.set_source_rgba(focus_color.red, focus_color.green, focus_color.blue,
+                    focus_color.alpha);
+            paint_border(ctx, pixbuf_dim, pixbuf_origin,
+                get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
+            ctx.restore();
+        }
+        
+        // draw selection border
+        if (is_selected()) {
+            // border thickness depends on the size of the thumbnail
+            ctx.save();
+            paint_border(ctx, pixbuf_dim, pixbuf_origin,
+                get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
+            ctx.restore();
+        }
+        
+        if (display_pixbuf != null) {
+            ctx.save();
+            ctx.set_source_rgba(bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha);
+            paint_image(ctx, display_pixbuf, pixbuf_origin);
+            ctx.restore();
+        }
+        
+        // title and subtitles are LABEL_PADDING below bottom of pixbuf
+        int text_y = pixbuf_dim.height + FRAME_WIDTH + LABEL_PADDING;
+        if (title != null && title_visible) {
+            // get the layout sized so its width is no more than the pixbuf's
+            // resize the text width to be no more than the pixbuf's
+            title.allocation.x = 0;
+            title.allocation.y = text_y;
+            title.allocation.width = pixbuf_dim.width;
+            title.allocation.height = title.get_height();
+            style_context.render_layout(ctx, title.allocation.x, title.allocation.y,
+                    title.get_pango_layout(pixbuf_dim.width));
+
+            text_y += title.get_height() + LABEL_PADDING;
+        }
+
+        if (comment != null && comment_visible) {
+            comment.allocation.x = 0;
+            comment.allocation.y = text_y;
+            comment.allocation.width = pixbuf_dim.width;
+            comment.allocation.height = comment.get_height();
+            style_context.render_layout(ctx, comment.allocation.x, comment.allocation.y,
+                    comment.get_pango_layout(pixbuf_dim.width));
+
+            text_y += comment.get_height() + LABEL_PADDING;
+        }
+
+        if (subtitle != null && subtitle_visible) {
+            subtitle.allocation.x = 0;
+            subtitle.allocation.y = text_y;
+            subtitle.allocation.width = pixbuf_dim.width;
+            subtitle.allocation.height = subtitle.get_height();
+
+            style_context.render_layout(ctx, subtitle.allocation.x, subtitle.allocation.y,
+                    subtitle.get_pango_layout(pixbuf_dim.width));
+
+            // increment text_y if more text lines follow
+        }
+        
+        ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
+            selected_color.alpha);
+        
+        // draw trinkets last
+        Gdk.Pixbuf? trinket = get_bottom_left_trinket(TRINKET_SCALE);
+        if (trinket != null) {
+            int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
+            int y = pixbuf_origin.y + pixbuf_dim.height - trinket.get_height() -
+                TRINKET_PADDING;
+            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+            ctx.fill();
+        }
+        
+        trinket = get_top_left_trinket(TRINKET_SCALE);
+        if (trinket != null) {
+            int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
+            int y = pixbuf_origin.y + TRINKET_PADDING;
+            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+            ctx.fill();
+        }
+        
+        trinket = get_top_right_trinket(TRINKET_SCALE);
+        if (trinket != null) {
+            int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width - 
+                get_horizontal_trinket_offset() - TRINKET_PADDING;
+            int y = pixbuf_origin.y + TRINKET_PADDING;
+            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+            ctx.fill();
+        }
+        
+        trinket = get_bottom_right_trinket(TRINKET_SCALE);
+        if (trinket != null) {
+            int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width - 
+                get_horizontal_trinket_offset() - TRINKET_PADDING;
+            int y = pixbuf_origin.y + pixbuf_dim.height - trinket.height - 
+                TRINKET_PADDING;
+            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
+            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
+            ctx.fill();
+        }
+        ctx.restore();
+    }
+    
+    protected void set_horizontal_trinket_offset(int horizontal_trinket_offset) {
+        assert(horizontal_trinket_offset >= 0);
+        this.horizontal_trinket_offset = horizontal_trinket_offset;
+    }
+    
+    protected int get_horizontal_trinket_offset() {
+        return horizontal_trinket_offset;
+    }
+
+    public void set_grid_coordinates(int col, int row) {
+        this.col = col;
+        this.row = row;
+    }
+    
+    public int get_column() {
+        return col;
+    }
+    
+    public int get_row() {
+        return row;
+    }
+    
+    public void brighten() {
+        // "should" implies "can" and "didn't already"
+        if (brightened != null || pixbuf == null)
+            return;
+        
+        // create a new lightened pixbuf to display
+        brightened = pixbuf.copy();
+        shift_colors(brightened, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, 0);
+        
+        display_pixbuf = brightened;
+        
+        notify_view_altered();
+    }
+    
+
+    public void unbrighten() {
+        // "should", "can", "didn't already"
+        if (brightened == null || pixbuf == null)
+            return;
+        
+        brightened = null;
+
+        // return to the normal image
+        display_pixbuf = pixbuf;
+        
+        notify_view_altered();
+    }
+    
+    public override void visibility_changed(bool visible) {
+        // if going from visible to hidden, unbrighten
+        if (!visible)
+            unbrighten();
+        
+        base.visibility_changed(visible);
+    }
+    
+    private bool query_tooltip_on_text(CheckerboardItemText text, Gtk.Tooltip tooltip) {
+        if (!text.get_pango_layout().is_ellipsized())
+            return false;
+        
+        if (text.is_marked_up())
+            tooltip.set_markup(text.get_text());
+        else
+            tooltip.set_text(text.get_text());
+        
+        return true;
+    }
+    
+    public bool query_tooltip(int x, int y, Gtk.Tooltip tooltip) {
+        if (title != null && title_visible && coord_in_rectangle(x, y, title.allocation))
+            return query_tooltip_on_text(title, tooltip);
+        
+        if (comment != null && comment_visible && coord_in_rectangle(x, y, comment.allocation))
+            return query_tooltip_on_text(comment, tooltip);
+        
+        if (subtitle != null && subtitle_visible && coord_in_rectangle(x, y, subtitle.allocation))
+            return query_tooltip_on_text(subtitle, tooltip);
+        
+        return false;
+    }
+}
+
+
diff --git a/src/CheckerboardItemText.vala b/src/CheckerboardItemText.vala
new file mode 100644
index 00000000..89249385
--- /dev/null
+++ b/src/CheckerboardItemText.vala
@@ -0,0 +1,98 @@
+/* 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.
+ */
+
+private class CheckerboardItemText {
+    private static int one_line_height = 0;
+    
+    private string text;
+    private bool marked_up;
+    private Pango.Alignment alignment;
+    private Pango.Layout layout = null;
+    private bool single_line = true;
+    private int height = 0;
+    
+    public Gdk.Rectangle allocation = Gdk.Rectangle();
+    
+    public CheckerboardItemText(string text, Pango.Alignment alignment = Pango.Alignment.LEFT,
+        bool marked_up = false) {
+        this.text = text;
+        this.marked_up = marked_up;
+        this.alignment = alignment;
+        
+        single_line = is_single_line();
+    }
+    
+    private bool is_single_line() {
+        return !String.contains_char(text, '\n');
+    }
+    
+    public bool is_marked_up() {
+        return marked_up;
+    }
+    
+    public bool is_set_to(string text, bool marked_up, Pango.Alignment alignment) {
+        return (this.marked_up == marked_up && this.alignment == alignment && this.text == text);
+    }
+    
+    public string get_text() {
+        return text;
+    }
+    
+    public int get_height() {
+        if (height == 0)
+            update_height();
+        
+        return height;
+    }
+    
+    public Pango.Layout get_pango_layout(int max_width = 0) {
+        if (layout == null)
+            create_pango();
+        
+        if (max_width > 0)
+            layout.set_width(max_width * Pango.SCALE);
+        
+        return layout;
+    }
+    
+    public void clear_pango_layout() {
+        layout = null;
+    }
+    
+    private void update_height() {
+        if (one_line_height != 0 && single_line)
+            height = one_line_height;
+        else
+            create_pango();
+    }
+    
+    private void create_pango() {
+        // create layout for this string and ellipsize so it never extends past its laid-down width
+        layout = AppWindow.get_instance().create_pango_layout(null);
+        if (!marked_up)
+            layout.set_text(text, -1);
+        else
+            layout.set_markup(text, -1);
+        
+        layout.set_ellipsize(Pango.EllipsizeMode.END);
+        layout.set_alignment(alignment);
+        
+        // getting pixel size is expensive, and we only need the height, so use cached values
+        // whenever possible
+        if (one_line_height != 0 && single_line) {
+            height = one_line_height;
+        } else {
+            int width;
+            layout.get_pixel_size(out width, out height);
+            
+            // cache first one-line height discovered
+            if (one_line_height == 0 && single_line)
+                one_line_height = height;
+        }
+    }
+}
+
+
diff --git a/src/CheckerboardLayout.vala b/src/CheckerboardLayout.vala
index bf6c9151..20f0d14b 100644
--- a/src/CheckerboardLayout.vala
+++ b/src/CheckerboardLayout.vala
@@ -4,824 +4,6 @@
  * See the COPYING file in this distribution.
  */
 
-private class CheckerboardItemText {
-    private static int one_line_height = 0;
-    
-    private string text;
-    private bool marked_up;
-    private Pango.Alignment alignment;
-    private Pango.Layout layout = null;
-    private bool single_line = true;
-    private int height = 0;
-    
-    public Gdk.Rectangle allocation = Gdk.Rectangle();
-    
-    public CheckerboardItemText(string text, Pango.Alignment alignment = Pango.Alignment.LEFT,
-        bool marked_up = false) {
-        this.text = text;
-        this.marked_up = marked_up;
-        this.alignment = alignment;
-        
-        single_line = is_single_line();
-    }
-    
-    private bool is_single_line() {
-        return !String.contains_char(text, '\n');
-    }
-    
-    public bool is_marked_up() {
-        return marked_up;
-    }
-    
-    public bool is_set_to(string text, bool marked_up, Pango.Alignment alignment) {
-        return (this.marked_up == marked_up && this.alignment == alignment && this.text == text);
-    }
-    
-    public string get_text() {
-        return text;
-    }
-    
-    public int get_height() {
-        if (height == 0)
-            update_height();
-        
-        return height;
-    }
-    
-    public Pango.Layout get_pango_layout(int max_width = 0) {
-        if (layout == null)
-            create_pango();
-        
-        if (max_width > 0)
-            layout.set_width(max_width * Pango.SCALE);
-        
-        return layout;
-    }
-    
-    public void clear_pango_layout() {
-        layout = null;
-    }
-    
-    private void update_height() {
-        if (one_line_height != 0 && single_line)
-            height = one_line_height;
-        else
-            create_pango();
-    }
-    
-    private void create_pango() {
-        // create layout for this string and ellipsize so it never extends past its laid-down width
-        layout = AppWindow.get_instance().create_pango_layout(null);
-        if (!marked_up)
-            layout.set_text(text, -1);
-        else
-            layout.set_markup(text, -1);
-        
-        layout.set_ellipsize(Pango.EllipsizeMode.END);
-        layout.set_alignment(alignment);
-        
-        // getting pixel size is expensive, and we only need the height, so use cached values
-        // whenever possible
-        if (one_line_height != 0 && single_line) {
-            height = one_line_height;
-        } else {
-            int width;
-            layout.get_pixel_size(out width, out height);
-            
-            // cache first one-line height discovered
-            if (one_line_height == 0 && single_line)
-                one_line_height = height;
-        }
-    }
-}
-
-public abstract class CheckerboardItem : ThumbnailView {
-    // Collection properties CheckerboardItem understands
-    // SHOW_TITLES (bool)
-    public const string PROP_SHOW_TITLES = "show-titles";
-    // SHOW_COMMENTS (bool)
-    public const string PROP_SHOW_COMMENTS = "show-comments";
-    // SHOW_SUBTITLES (bool)
-    public const string PROP_SHOW_SUBTITLES = "show-subtitles";
-    
-    public const int FRAME_WIDTH = 8;
-    public const int LABEL_PADDING = 4;
-    public const int BORDER_WIDTH = 1;
-
-    public const int SHADOW_RADIUS = 4;
-    public const float SHADOW_INITIAL_ALPHA = 0.5f;
-    
-    public const int TRINKET_SCALE = 12;
-    public const int TRINKET_PADDING = 1;
-    
-    public const int BRIGHTEN_SHIFT = 0x18;
-    
-    public Dimensions requisition = Dimensions();
-    public Gdk.Rectangle allocation = Gdk.Rectangle();
-    
-    private bool exposure = false;
-    private CheckerboardItemText? title = null;
-    private bool title_visible = true;
-    private CheckerboardItemText? comment = null;
-    private bool comment_visible = true;
-    private CheckerboardItemText? subtitle = null;
-    private bool subtitle_visible = false;
-    private bool is_cursor = false;
-    private Pango.Alignment tag_alignment = Pango.Alignment.LEFT;
-    private Gee.List<Tag>? user_visible_tag_list = null;
-    private Gee.Collection<Tag> tags;
-    private Gdk.Pixbuf pixbuf = null;
-    private Gdk.Pixbuf display_pixbuf = null;
-    private Gdk.Pixbuf brightened = null;
-    private Dimensions pixbuf_dim = Dimensions();
-    private int col = -1;
-    private int row = -1;
-    private int horizontal_trinket_offset = 0;
-    
-    public CheckerboardItem(ThumbnailSource source, Dimensions initial_pixbuf_dim, string title, string? 
comment,
-        bool marked_up = false, Pango.Alignment alignment = Pango.Alignment.LEFT) {
-        base(source);
-        
-        pixbuf_dim = initial_pixbuf_dim;
-        this.title = new CheckerboardItemText(title, alignment, marked_up);
-        // on the checkboard page we display the comment in 
-        // one line, i.e., replacing all newlines with spaces.
-        // that means that the display will contain "..." if the comment
-        // is too long.
-        // warning: changes here have to be done in set_comment, too!
-        if (comment != null)
-            this.comment = new CheckerboardItemText(comment.replace("\n", " "), alignment,
-                marked_up);
-        
-        // Don't calculate size here, wait for the item to be assigned to a ViewCollection
-        // (notify_membership_changed) and calculate when the collection's property settings
-        // are known
-    }
-
-    public bool has_tags { get; private set; }
-
-    public override string get_name() {
-        return (title != null) ? title.get_text() : base.get_name();
-    }
-    
-    public string get_title() {
-        return (title != null) ? title.get_text() : "";
-    }
-    
-    public string get_comment() {
-        return (comment != null) ? comment.get_text() : "";
-    }
-    
-    public void set_title(string text, bool marked_up = false,
-        Pango.Alignment alignment = Pango.Alignment.LEFT) {
-        if (title != null && title.is_set_to(text, marked_up, alignment))
-            return;
-        
-        title = new CheckerboardItemText(text, alignment, marked_up);
-        
-        if (title_visible) {
-            recalc_size("set_title");
-            notify_view_altered();
-        }
-    }
-
-    public void translate_coordinates(ref int x, ref int y) {
-        x -= allocation.x + FRAME_WIDTH;
-        y -= allocation.y + FRAME_WIDTH;
-    }
-    
-    public void clear_title() {
-        if (title == null)
-            return;
-        
-        title = null;
-        
-        if (title_visible) {
-            recalc_size("clear_title");
-            notify_view_altered();
-        }
-    }
-    
-    private void set_title_visible(bool visible) {
-        if (title_visible == visible)
-            return;
-        
-        title_visible = visible;
-        
-        recalc_size("set_title_visible");
-        notify_view_altered();
-    }
-    
-    public void set_comment(string text, bool marked_up = false,
-        Pango.Alignment alignment = Pango.Alignment.LEFT) {
-        if (comment != null && comment.is_set_to(text, marked_up, alignment))
-            return;
-        
-        comment = new CheckerboardItemText(text.replace("\n", " "), alignment, marked_up);
-        
-        if (comment_visible) {
-            recalc_size("set_comment");
-            notify_view_altered();
-        }
-    }
-    
-    public void clear_comment() {
-        if (comment == null)
-            return;
-        
-        comment = null;
-        
-        if (comment_visible) {
-            recalc_size("clear_comment");
-            notify_view_altered();
-        }
-    }
-    
-    private void set_comment_visible(bool visible) {
-        if (comment_visible == visible)
-            return;
-        
-        comment_visible = visible;
-        
-        recalc_size("set_comment_visible");
-        notify_view_altered();
-    }
-
-    public void set_tags(Gee.Collection<Tag>? tags,
-            Pango.Alignment alignment = Pango.Alignment.LEFT) {
-        has_tags = (tags != null && tags.size > 0);
-        tag_alignment = alignment;
-        string text;
-        if (has_tags) {
-            this.tags = tags;
-            user_visible_tag_list = Tag.make_user_visible_tag_list(tags);
-            text = Tag.make_tag_markup_string(user_visible_tag_list);
-        } else {
-            text = "<small>.</small>";
-        }
-
-        if (subtitle != null && subtitle.is_set_to(text, true, alignment))
-            return;
-        subtitle = new CheckerboardItemText(text, alignment, true);
-
-        if (subtitle_visible) {
-            recalc_size("set_subtitle");
-            notify_view_altered();
-        }
-    }
-
-    public void clear_tags() {
-        clear_subtitle();
-        has_tags = false;
-        user_visible_tag_list = null;
-    }
-
-    public void highlight_user_visible_tag(int index)
-            requires (user_visible_tag_list != null) {
-        string text = Tag.make_tag_markup_string(user_visible_tag_list, index);
-        subtitle = new CheckerboardItemText(text, tag_alignment, true);
-
-        if (subtitle_visible)
-            notify_view_altered();
-    }
-
-    public Tag get_user_visible_tag(int index)
-            requires (index >= 0 && index < user_visible_tag_list.size) {
-        return user_visible_tag_list.get(index);
-    }
-
-    public Pango.Layout? get_tag_list_layout() {
-        return has_tags ? subtitle.get_pango_layout() : null;
-    }
-
-    public Gdk.Rectangle get_subtitle_allocation() {
-        return subtitle.allocation;
-    }
-
-    public string get_subtitle() {
-        return (subtitle != null) ? subtitle.get_text() : "";
-    }
-    
-    public void set_subtitle(string text, bool marked_up = false, 
-        Pango.Alignment alignment = Pango.Alignment.LEFT) {
-        if (subtitle != null && subtitle.is_set_to(text, marked_up, alignment))
-            return;
-        
-        subtitle = new CheckerboardItemText(text, alignment, marked_up);
-        
-        if (subtitle_visible) {
-            recalc_size("set_subtitle");
-            notify_view_altered();
-        }
-    }
-    
-    public void clear_subtitle() {
-        if (subtitle == null)
-            return;
-        
-        subtitle = null;
-        
-        if (subtitle_visible) {
-            recalc_size("clear_subtitle");
-            notify_view_altered();
-        }
-    }
-    
-    private void set_subtitle_visible(bool visible) {
-        if (subtitle_visible == visible)
-            return;
-        
-        subtitle_visible = visible;
-        
-        recalc_size("set_subtitle_visible");
-        notify_view_altered();
-    }
-
-    public void set_is_cursor(bool is_cursor) {
-        this.is_cursor = is_cursor;
-    }
-
-    public bool get_is_cursor() {
-        return is_cursor;
-    }
-    
-    public virtual void handle_mouse_motion(int x, int y, int height, int width) {
-
-    }
-
-    public virtual void handle_mouse_leave() {
-        unbrighten();
-    }
-
-    public virtual void handle_mouse_enter() {
-        brighten();
-    }
-
-    protected override void notify_membership_changed(DataCollection? collection) {
-        bool title_visible = (bool) get_collection_property(PROP_SHOW_TITLES, true);
-        bool comment_visible = (bool) get_collection_property(PROP_SHOW_COMMENTS, true);
-        bool subtitle_visible = (bool) get_collection_property(PROP_SHOW_SUBTITLES, false);
-        
-        bool altered = false;
-        if (this.title_visible != title_visible) {
-            this.title_visible = title_visible;
-            altered = true;
-        }
-        
-        if (this.comment_visible != comment_visible) {
-            this.comment_visible = comment_visible;
-            altered = true;
-        }
-        
-        if (this.subtitle_visible != subtitle_visible) {
-            this.subtitle_visible = subtitle_visible;
-            altered = true;
-        }
-        
-        if (altered || !requisition.has_area()) {
-            recalc_size("notify_membership_changed");
-            notify_view_altered();
-        }
-        
-        base.notify_membership_changed(collection);
-    }
-    
-    protected override void notify_collection_property_set(string name, Value? old, Value val) {
-        switch (name) {
-            case PROP_SHOW_TITLES:
-                set_title_visible((bool) val);
-            break;
-            
-            case PROP_SHOW_COMMENTS:
-                set_comment_visible((bool) val);
-            break;
-            
-            case PROP_SHOW_SUBTITLES:
-                set_subtitle_visible((bool) val);
-            break;
-        }
-        
-        base.notify_collection_property_set(name, old, val);
-    }
-    
-    // The alignment point is the coordinate on the y-axis (relative to the top of the
-    // CheckerboardItem) which this item should be aligned to.  This allows for
-    // bottom-alignment along the bottom edge of the thumbnail.
-    public int get_alignment_point() {
-        return FRAME_WIDTH + BORDER_WIDTH + pixbuf_dim.height;
-    }
-    
-    public virtual void exposed() {
-        exposure = true;
-    }
-    
-    public virtual void unexposed() {
-        exposure = false;
-        
-        if (title != null)
-            title.clear_pango_layout();
-        
-        if (comment != null)
-            comment.clear_pango_layout();
-        
-        if (subtitle != null)
-            subtitle.clear_pango_layout();
-    }
-    
-    public virtual bool is_exposed() {
-        return exposure;
-    }
-
-    public bool has_image() {
-        return pixbuf != null;
-    }
-    
-    public Gdk.Pixbuf? get_image() {
-        return pixbuf;
-    }
-    
-    public void set_image(Gdk.Pixbuf pixbuf) {
-        this.pixbuf = pixbuf;
-        display_pixbuf = pixbuf;
-        pixbuf_dim = Dimensions.for_pixbuf(pixbuf);
-        
-        recalc_size("set_image");
-        notify_view_altered();
-    }
-    
-    public void clear_image(Dimensions dim) {
-        bool had_image = pixbuf != null;
-        
-        pixbuf = null;
-        display_pixbuf = null;
-        pixbuf_dim = dim;
-        
-        recalc_size("clear_image");
-        
-        if (had_image)
-            notify_view_altered();
-    }
-    
-    public static int get_max_width(int scale) {
-        // width is frame width (two sides) + frame padding (two sides) + width of pixbuf (text
-        // never wider)
-        return (FRAME_WIDTH * 2) + scale;
-    }
-    
-    private void recalc_size(string reason) {
-        Dimensions old_requisition = requisition;
-        
-        // only add in the text heights if they're displayed
-        int title_height = (title != null && title_visible)
-            ? title.get_height() + LABEL_PADDING : 0;
-        int comment_height = (comment != null && comment_visible)
-            ? comment.get_height() + LABEL_PADDING : 0;
-        int subtitle_height = (subtitle != null && subtitle_visible)
-            ? subtitle.get_height() + LABEL_PADDING : 0;
-        
-        // width is frame width (two sides) + frame padding (two sides) + width of pixbuf
-        // (text never wider)
-        requisition.width = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2) + pixbuf_dim.width;
-        
-        // height is frame width (two sides) + frame padding (two sides) + height of pixbuf
-        // + height of text + label padding (between pixbuf and text)
-        requisition.height = (FRAME_WIDTH * 2) + (BORDER_WIDTH * 2)
-            + pixbuf_dim.height + title_height + comment_height + subtitle_height;
-        
-#if TRACE_REFLOW_ITEMS
-        debug("recalc_size %s: %s title_height=%d comment_height=%d subtitle_height=%d requisition=%s", 
-            get_source().get_name(), reason, title_height, comment_height, subtitle_height,
-            requisition.to_string());
-#endif
-        
-        if (!requisition.approx_equals(old_requisition)) {
-#if TRACE_REFLOW_ITEMS
-            debug("recalc_size %s: %s notifying geometry altered", get_source().get_name(), reason);
-#endif
-            notify_geometry_altered();
-        }
-    }
-    
-    protected static Dimensions get_border_dimensions(Dimensions object_dim, int border_width) {
-        Dimensions dimensions = Dimensions();
-        dimensions.width = object_dim.width + (border_width * 2);
-        dimensions.height = object_dim.height + (border_width * 2);
-        return dimensions;
-    }
-    
-    protected static Gdk.Point get_border_origin(Gdk.Point object_origin, int border_width) {
-        Gdk.Point origin = Gdk.Point();
-        origin.x = object_origin.x - border_width;
-        origin.y = object_origin.y - border_width;
-        return origin;
-    }
-
-    protected virtual void paint_shadow(Cairo.Context ctx, Dimensions dimensions, Gdk.Point origin, 
-        int radius, float initial_alpha) { 
-        double rgb_all = 0.0;
-        
-        // top right corner
-        paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + radius, rgb_all, radius, 
-            initial_alpha, -0.5 * Math.PI, 0);
-        // bottom right corner
-        paint_shadow_in_corner(ctx, origin.x + dimensions.width, origin.y + dimensions.height, rgb_all, 
-            radius, initial_alpha, 0, 0.5 * Math.PI);
-        // bottom left corner
-        paint_shadow_in_corner(ctx, origin.x + radius, origin.y + dimensions.height, rgb_all, radius, 
-            initial_alpha, 0.5 * Math.PI, Math.PI);
-
-        // left right 
-        Cairo.Pattern lr = new Cairo.Pattern.linear(0, origin.y + dimensions.height, 
-            0, origin.y + dimensions.height + radius);
-        lr.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
-        lr.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
-        ctx.set_source(lr);
-        ctx.rectangle(origin.x + radius, origin.y + dimensions.height, dimensions.width - radius, radius);
-        ctx.fill();
-
-        // top down
-        Cairo.Pattern td = new Cairo.Pattern.linear(origin.x + dimensions.width, 
-            0, origin.x + dimensions.width + radius, 0);
-        td.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
-        td.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0.0);
-        ctx.set_source(td);
-        ctx.rectangle(origin.x + dimensions.width, origin.y + radius, 
-            radius, dimensions.height - radius);
-        ctx.fill();
-    }
-
-    protected void paint_shadow_in_corner(Cairo.Context ctx, int x, int y, 
-       double rgb_all, float radius, float initial_alpha, double arc1, double arc2) {
-        Cairo.Pattern p = new Cairo.Pattern.radial(x, y, 0, x, y, radius);
-        p.add_color_stop_rgba(0.0, rgb_all, rgb_all, rgb_all, initial_alpha);
-        p.add_color_stop_rgba(1.0, rgb_all, rgb_all, rgb_all, 0);
-        ctx.set_source(p);
-        ctx.move_to(x, y);
-        ctx.arc(x, y, radius, arc1, arc2);
-        ctx.close_path();
-        ctx.fill(); 
-    }
-
-    protected virtual void paint_border(Cairo.Context ctx, Dimensions object_dimensions,
-        Gdk.Point object_origin, int border_width) {
-        if (border_width == 1) {
-            ctx.rectangle(object_origin.x - border_width, object_origin.y - border_width,
-                object_dimensions.width + (border_width * 2),
-                object_dimensions.height + (border_width * 2));
-            ctx.fill();
-        } else {
-            Dimensions dimensions = get_border_dimensions(object_dimensions, border_width);
-            Gdk.Point origin = get_border_origin(object_origin, border_width);
-            
-            // amount of rounding needed on corners varies by size of object
-            double scale = int.max(object_dimensions.width, object_dimensions.height);
-            draw_rounded_corners_filled(ctx, dimensions, origin, 0.25 * scale);
-        }
-    }
-
-    protected virtual void paint_image(Cairo.Context ctx, Gdk.Pixbuf pixbuf, Gdk.Point origin) {
-        paint_pixmap_with_background(ctx, pixbuf, origin.x, origin.y);
-    }
-
-    private int get_selection_border_width(int scale) {
-        return ((scale <= ((Thumbnail.MIN_SCALE + Thumbnail.MAX_SCALE) / 3)) ? 5 : 4)
-            + BORDER_WIDTH;
-    }
-    
-    protected virtual Gdk.Pixbuf? get_top_left_trinket(int scale) {
-        return null;
-    }
-    
-    protected virtual Gdk.Pixbuf? get_top_right_trinket(int scale) {
-        return null;
-    }
-    
-    protected virtual Gdk.Pixbuf? get_bottom_left_trinket(int scale) {
-        return null;
-    }
-    
-    protected virtual Gdk.Pixbuf? get_bottom_right_trinket(int scale) {
-        return null;
-    }
-    
-    public void paint(Gtk.StyleContext style_context, Cairo.Context ctx, Gdk.RGBA bg_color, Gdk.RGBA 
selected_color,
-        Gdk.RGBA? border_color, Gdk.RGBA? focus_color) {
-        ctx.save();
-        ctx.translate(allocation.x + FRAME_WIDTH,
-                      allocation.y + FRAME_WIDTH);
-        // calc the top-left point of the pixbuf
-        Gdk.Point pixbuf_origin = Gdk.Point();
-        pixbuf_origin.x = BORDER_WIDTH;
-        pixbuf_origin.y = BORDER_WIDTH;
-        
-        ctx.set_line_width(FRAME_WIDTH);
-        ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
-            selected_color.alpha);
-
-        // draw shadow
-        if (border_color != null) {
-            ctx.save();
-            Dimensions shadow_dim = Dimensions();
-            shadow_dim.width = pixbuf_dim.width + BORDER_WIDTH;
-            shadow_dim.height = pixbuf_dim.height + BORDER_WIDTH;
-            paint_shadow(ctx, shadow_dim, pixbuf_origin, SHADOW_RADIUS, SHADOW_INITIAL_ALPHA);
-            ctx.restore();
-        }
-        
-        // draw a border for the cursor with the selection width and normal border color
-        if (is_cursor) {
-            ctx.save();
-            ctx.set_source_rgba(focus_color.red, focus_color.green, focus_color.blue,
-                    focus_color.alpha);
-            paint_border(ctx, pixbuf_dim, pixbuf_origin,
-                get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
-            ctx.restore();
-        }
-        
-        // draw selection border
-        if (is_selected()) {
-            // border thickness depends on the size of the thumbnail
-            ctx.save();
-            paint_border(ctx, pixbuf_dim, pixbuf_origin,
-                get_selection_border_width(int.max(pixbuf_dim.width, pixbuf_dim.height)));
-            ctx.restore();
-        }
-        
-        if (display_pixbuf != null) {
-            ctx.save();
-            ctx.set_source_rgba(bg_color.red, bg_color.green, bg_color.blue, bg_color.alpha);
-            paint_image(ctx, display_pixbuf, pixbuf_origin);
-            ctx.restore();
-        }
-        
-        // title and subtitles are LABEL_PADDING below bottom of pixbuf
-        int text_y = pixbuf_dim.height + FRAME_WIDTH + LABEL_PADDING;
-        if (title != null && title_visible) {
-            // get the layout sized so its width is no more than the pixbuf's
-            // resize the text width to be no more than the pixbuf's
-            title.allocation.x = 0;
-            title.allocation.y = text_y;
-            title.allocation.width = pixbuf_dim.width;
-            title.allocation.height = title.get_height();
-            style_context.render_layout(ctx, title.allocation.x, title.allocation.y,
-                    title.get_pango_layout(pixbuf_dim.width));
-
-            text_y += title.get_height() + LABEL_PADDING;
-        }
-
-        if (comment != null && comment_visible) {
-            comment.allocation.x = 0;
-            comment.allocation.y = text_y;
-            comment.allocation.width = pixbuf_dim.width;
-            comment.allocation.height = comment.get_height();
-            style_context.render_layout(ctx, comment.allocation.x, comment.allocation.y,
-                    comment.get_pango_layout(pixbuf_dim.width));
-
-            text_y += comment.get_height() + LABEL_PADDING;
-        }
-
-        if (subtitle != null && subtitle_visible) {
-            subtitle.allocation.x = 0;
-            subtitle.allocation.y = text_y;
-            subtitle.allocation.width = pixbuf_dim.width;
-            subtitle.allocation.height = subtitle.get_height();
-
-            style_context.render_layout(ctx, subtitle.allocation.x, subtitle.allocation.y,
-                    subtitle.get_pango_layout(pixbuf_dim.width));
-
-            // increment text_y if more text lines follow
-        }
-        
-        ctx.set_source_rgba(selected_color.red, selected_color.green, selected_color.blue,
-            selected_color.alpha);
-        
-        // draw trinkets last
-        Gdk.Pixbuf? trinket = get_bottom_left_trinket(TRINKET_SCALE);
-        if (trinket != null) {
-            int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
-            int y = pixbuf_origin.y + pixbuf_dim.height - trinket.get_height() -
-                TRINKET_PADDING;
-            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
-            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
-            ctx.fill();
-        }
-        
-        trinket = get_top_left_trinket(TRINKET_SCALE);
-        if (trinket != null) {
-            int x = pixbuf_origin.x + TRINKET_PADDING + get_horizontal_trinket_offset();
-            int y = pixbuf_origin.y + TRINKET_PADDING;
-            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
-            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
-            ctx.fill();
-        }
-        
-        trinket = get_top_right_trinket(TRINKET_SCALE);
-        if (trinket != null) {
-            int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width - 
-                get_horizontal_trinket_offset() - TRINKET_PADDING;
-            int y = pixbuf_origin.y + TRINKET_PADDING;
-            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
-            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
-            ctx.fill();
-        }
-        
-        trinket = get_bottom_right_trinket(TRINKET_SCALE);
-        if (trinket != null) {
-            int x = pixbuf_origin.x + pixbuf_dim.width - trinket.width - 
-                get_horizontal_trinket_offset() - TRINKET_PADDING;
-            int y = pixbuf_origin.y + pixbuf_dim.height - trinket.height - 
-                TRINKET_PADDING;
-            Gdk.cairo_set_source_pixbuf(ctx, trinket, x, y);
-            ctx.rectangle(x, y, trinket.get_width(), trinket.get_height());
-            ctx.fill();
-        }
-        ctx.restore();
-    }
-    
-    protected void set_horizontal_trinket_offset(int horizontal_trinket_offset) {
-        assert(horizontal_trinket_offset >= 0);
-        this.horizontal_trinket_offset = horizontal_trinket_offset;
-    }
-    
-    protected int get_horizontal_trinket_offset() {
-        return horizontal_trinket_offset;
-    }
-
-    public void set_grid_coordinates(int col, int row) {
-        this.col = col;
-        this.row = row;
-    }
-    
-    public int get_column() {
-        return col;
-    }
-    
-    public int get_row() {
-        return row;
-    }
-    
-    public void brighten() {
-        // "should" implies "can" and "didn't already"
-        if (brightened != null || pixbuf == null)
-            return;
-        
-        // create a new lightened pixbuf to display
-        brightened = pixbuf.copy();
-        shift_colors(brightened, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, BRIGHTEN_SHIFT, 0);
-        
-        display_pixbuf = brightened;
-        
-        notify_view_altered();
-    }
-    
-
-    public void unbrighten() {
-        // "should", "can", "didn't already"
-        if (brightened == null || pixbuf == null)
-            return;
-        
-        brightened = null;
-
-        // return to the normal image
-        display_pixbuf = pixbuf;
-        
-        notify_view_altered();
-    }
-    
-    public override void visibility_changed(bool visible) {
-        // if going from visible to hidden, unbrighten
-        if (!visible)
-            unbrighten();
-        
-        base.visibility_changed(visible);
-    }
-    
-    private bool query_tooltip_on_text(CheckerboardItemText text, Gtk.Tooltip tooltip) {
-        if (!text.get_pango_layout().is_ellipsized())
-            return false;
-        
-        if (text.is_marked_up())
-            tooltip.set_markup(text.get_text());
-        else
-            tooltip.set_text(text.get_text());
-        
-        return true;
-    }
-    
-    public bool query_tooltip(int x, int y, Gtk.Tooltip tooltip) {
-        if (title != null && title_visible && coord_in_rectangle(x, y, title.allocation))
-            return query_tooltip_on_text(title, tooltip);
-        
-        if (comment != null && comment_visible && coord_in_rectangle(x, y, comment.allocation))
-            return query_tooltip_on_text(comment, tooltip);
-        
-        if (subtitle != null && subtitle_visible && coord_in_rectangle(x, y, subtitle.allocation))
-            return query_tooltip_on_text(subtitle, tooltip);
-        
-        return false;
-    }
-}
-
 public class CheckerboardLayout : Gtk.DrawingArea {
     public const int TOP_PADDING = 16;
     public const int BOTTOM_PADDING = 16;
diff --git a/src/meson.build b/src/meson.build
index 65197196..abea7b21 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -166,6 +166,8 @@ executable('shotwell',
             'NaturalCollate.vala',
             'Thumbnail.vala',
             'ThumbnailCache.vala',
+            'CheckerboardItem.vala',
+            'CheckerboardItemText.vala',
             'CheckerboardLayout.vala',
             'PhotoPage.vala',
             'Page.vala',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]