summaryrefslogtreecommitdiff
path: root/src/Thumbnail.vala
diff options
context:
space:
mode:
Diffstat (limited to 'src/Thumbnail.vala')
-rw-r--r--src/Thumbnail.vala400
1 files changed, 400 insertions, 0 deletions
diff --git a/src/Thumbnail.vala b/src/Thumbnail.vala
new file mode 100644
index 0000000..c33d43b
--- /dev/null
+++ b/src/Thumbnail.vala
@@ -0,0 +1,400 @@
+/* Copyright 2009-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+public class Thumbnail : MediaSourceItem {
+ // Collection properties Thumbnail responds to
+ // SHOW_TAGS (bool)
+ public const string PROP_SHOW_TAGS = CheckerboardItem.PROP_SHOW_SUBTITLES;
+ // SIZE (int, scale)
+ public const string PROP_SIZE = "thumbnail-size";
+ // SHOW_RATINGS (bool)
+ public const string PROP_SHOW_RATINGS = "show-ratings";
+
+ public static int MIN_SCALE {
+ get {
+ return 72;
+ }
+ }
+ public static int MAX_SCALE {
+ get {
+ return ThumbnailCache.Size.LARGEST.get_scale();
+ }
+ }
+ public static int DEFAULT_SCALE {
+ get {
+ return ThumbnailCache.Size.MEDIUM.get_scale();
+ }
+ }
+
+ public const Gdk.InterpType LOW_QUALITY_INTERP = Gdk.InterpType.NEAREST;
+ public const Gdk.InterpType HIGH_QUALITY_INTERP = Gdk.InterpType.BILINEAR;
+
+ private const int HQ_IMPROVEMENT_MSEC = 100;
+
+ private MediaSource media;
+ private int scale;
+ private Dimensions original_dim;
+ private Dimensions dim;
+ private Gdk.Pixbuf unscaled_pixbuf = null;
+ private Cancellable cancellable = null;
+ private bool hq_scheduled = false;
+ private bool hq_reschedule = false;
+ // this is cached locally because there are situations where the constant calls to is_exposed()
+ // was showing up in sysprof
+ private bool exposure = false;
+
+ public Thumbnail(MediaSource media, int scale = DEFAULT_SCALE) {
+ base (media, media.get_dimensions().get_scaled(scale, true), media.get_name(),
+ media.get_comment());
+
+ this.media = media;
+ this.scale = scale;
+
+ Tag.global.container_contents_altered.connect(on_tag_contents_altered);
+ Tag.global.items_altered.connect(on_tags_altered);
+
+ assert((media is LibraryPhoto) || (media is Video));
+ set_enable_sprockets(media is Video);
+
+ original_dim = media.get_dimensions();
+ dim = original_dim.get_scaled(scale, true);
+
+ // initialize title and tags text line so they're properly accounted for when the display
+ // size is calculated
+ update_title(true);
+ update_comment(true);
+ update_tags(true);
+ }
+
+ ~Thumbnail() {
+ if (cancellable != null)
+ cancellable.cancel();
+
+ Tag.global.container_contents_altered.disconnect(on_tag_contents_altered);
+ Tag.global.items_altered.disconnect(on_tags_altered);
+ }
+
+ private void update_tags(bool init = false) {
+ Gee.Collection<Tag>? tags = Tag.global.fetch_sorted_for_source(media);
+ if (tags == null || tags.size == 0)
+ clear_subtitle();
+ else if (!init)
+ set_subtitle(Tag.make_tag_string(tags, "<small>", ", ", "</small>", true), true);
+ else
+ set_subtitle("<small>.</small>", true);
+ }
+
+ private void on_tag_contents_altered(ContainerSource container, Gee.Collection<DataSource>? added,
+ bool relinking, Gee.Collection<DataSource>? removed, bool unlinking) {
+ if (!exposure)
+ return;
+
+ bool tag_added = (added != null) ? added.contains(media) : false;
+ bool tag_removed = (removed != null) ? removed.contains(media) : false;
+
+ // if media source we're monitoring is added or removed to any tag, update tag list
+ if (tag_added || tag_removed)
+ update_tags();
+ }
+
+ private void on_tags_altered(Gee.Map<DataObject, Alteration> altered) {
+ if (!exposure)
+ return;
+
+ foreach (DataObject object in altered.keys) {
+ Tag tag = (Tag) object;
+
+ if (tag.contains(media)) {
+ update_tags();
+
+ break;
+ }
+ }
+ }
+
+ private void update_title(bool init = false) {
+ string title = media.get_name();
+ if (is_string_empty(title))
+ clear_title();
+ else if (!init)
+ set_title(title);
+ else
+ set_title("");
+ }
+
+ private void update_comment(bool init = false) {
+ string comment = media.get_comment();
+ if (is_string_empty(comment))
+ clear_comment();
+ else if (!init)
+ set_comment(comment);
+ else
+ set_comment("");
+ }
+
+ protected override void notify_altered(Alteration alteration) {
+ if (exposure && alteration.has_detail("metadata", "name"))
+ update_title();
+ if (exposure && alteration.has_detail("metadata", "comment"))
+ update_comment();
+
+ base.notify_altered(alteration);
+ }
+
+ public MediaSource get_media_source() {
+ return media;
+ }
+
+ //
+ // Comparators
+ //
+
+ public static int64 photo_id_ascending_comparator(void *a, void *b) {
+ return ((Thumbnail *) a)->media.get_instance_id() - ((Thumbnail *) b)->media.get_instance_id();
+ }
+
+ public static int64 photo_id_descending_comparator(void *a, void *b) {
+ return photo_id_ascending_comparator(b, a);
+ }
+
+ public static int64 title_ascending_comparator(void *a, void *b) {
+ int64 result = strcmp(((Thumbnail *) a)->media.get_name(), ((Thumbnail *) b)->media.get_name());
+
+ return (result != 0) ? result : photo_id_ascending_comparator(a, b);
+ }
+
+ public static int64 title_descending_comparator(void *a, void *b) {
+ int64 result = title_ascending_comparator(b, a);
+
+ return (result != 0) ? result : photo_id_descending_comparator(a, b);
+ }
+
+ public static bool title_comparator_predicate(DataObject object, Alteration alteration) {
+ return alteration.has_detail("metadata", "title");
+ }
+
+ public static int64 exposure_time_ascending_comparator(void *a, void *b) {
+ int64 time_a = (int64) (((Thumbnail *) a)->media.get_exposure_time());
+ int64 time_b = (int64) (((Thumbnail *) b)->media.get_exposure_time());
+ int64 result = (time_a - time_b);
+
+ return (result != 0) ? result : filename_ascending_comparator(a, b);
+ }
+
+ public static int64 exposure_time_desending_comparator(void *a, void *b) {
+ int64 result = exposure_time_ascending_comparator(b, a);
+
+ return (result != 0) ? result : filename_descending_comparator(a, b);
+ }
+
+ public static bool exposure_time_comparator_predicate(DataObject object, Alteration alteration) {
+ return alteration.has_detail("metadata", "exposure-time");
+ }
+
+ public static int64 filename_ascending_comparator(void *a, void *b) {
+ string path_a = ((Thumbnail *) a)->media.get_file().get_basename().down();
+ string path_b = ((Thumbnail *) b)->media.get_file().get_basename().down();
+
+ int64 result = strcmp(g_utf8_collate_key_for_filename(path_a),
+ g_utf8_collate_key_for_filename(path_b));
+ return (result != 0) ? result : photo_id_ascending_comparator(a, b);
+ }
+
+ public static int64 filename_descending_comparator(void *a, void *b) {
+ int64 result = filename_ascending_comparator(b, a);
+
+ return (result != 0) ? result : photo_id_descending_comparator(a, b);
+ }
+
+ public static int64 rating_ascending_comparator(void *a, void *b) {
+ int64 result = ((Thumbnail *) a)->media.get_rating() - ((Thumbnail *) b)->media.get_rating();
+
+ return (result != 0) ? result : photo_id_ascending_comparator(a, b);
+ }
+
+ public static int64 rating_descending_comparator(void *a, void *b) {
+ int64 result = rating_ascending_comparator(b, a);
+
+ return (result != 0) ? result : photo_id_descending_comparator(a, b);
+ }
+
+ public static bool rating_comparator_predicate(DataObject object, Alteration alteration) {
+ return alteration.has_detail("metadata", "rating");
+ }
+
+ protected override void thumbnail_altered() {
+ original_dim = media.get_dimensions();
+ dim = original_dim.get_scaled(scale, true);
+
+ if (exposure)
+ delayed_high_quality_fetch();
+ else
+ paint_empty();
+
+ base.thumbnail_altered();
+ }
+
+ protected override void notify_collection_property_set(string name, Value? old, Value val) {
+ switch (name) {
+ case PROP_SIZE:
+ resize((int) val);
+ break;
+
+ case PROP_SHOW_RATINGS:
+ notify_view_altered();
+ break;
+ }
+
+ base.notify_collection_property_set(name, old, val);
+ }
+
+ private void resize(int new_scale) {
+ assert(new_scale >= MIN_SCALE);
+ assert(new_scale <= MAX_SCALE);
+
+ if (scale == new_scale)
+ return;
+
+ scale = new_scale;
+ dim = original_dim.get_scaled(scale, true);
+
+ cancel_async_fetch();
+
+ if (exposure) {
+ // attempt to use an unscaled pixbuf (which is always larger or equal to the current
+ // size, and will most likely be larger than the new size -- and if not, a new one will
+ // be on its way), then use the current pixbuf if available (which may have to be
+ // scaled up, which is ugly)
+ Gdk.Pixbuf? resizable = null;
+ if (unscaled_pixbuf != null)
+ resizable = unscaled_pixbuf;
+ else if (has_image())
+ resizable = get_image();
+
+ if (resizable != null)
+ set_image(resize_pixbuf(resizable, dim, LOW_QUALITY_INTERP));
+
+ delayed_high_quality_fetch();
+ } else {
+ clear_image(dim);
+ }
+ }
+
+ private void paint_empty() {
+ cancel_async_fetch();
+ clear_image(dim);
+ unscaled_pixbuf = null;
+ }
+
+ private void schedule_low_quality_fetch() {
+ cancel_async_fetch();
+ cancellable = new Cancellable();
+
+ ThumbnailCache.fetch_async_scaled(media, ThumbnailCache.Size.SMALLEST,
+ dim, LOW_QUALITY_INTERP, on_low_quality_fetched, cancellable);
+ }
+
+ private void delayed_high_quality_fetch() {
+ if (hq_scheduled) {
+ hq_reschedule = true;
+
+ return;
+ }
+
+ Timeout.add_full(Priority.DEFAULT, HQ_IMPROVEMENT_MSEC, on_schedule_high_quality);
+ hq_scheduled = true;
+ }
+
+ private bool on_schedule_high_quality() {
+ if (hq_reschedule) {
+ hq_reschedule = false;
+
+ return true;
+ }
+
+ cancel_async_fetch();
+ cancellable = new Cancellable();
+
+ if (exposure) {
+ ThumbnailCache.fetch_async_scaled(media, scale, dim,
+ HIGH_QUALITY_INTERP, on_high_quality_fetched, cancellable);
+ }
+
+ hq_scheduled = false;
+
+ return false;
+ }
+
+ private void cancel_async_fetch() {
+ // cancel outstanding I/O
+ if (cancellable != null)
+ cancellable.cancel();
+ }
+
+ private void on_low_quality_fetched(Gdk.Pixbuf? pixbuf, Gdk.Pixbuf? unscaled, Dimensions dim,
+ Gdk.InterpType interp, Error? err) {
+ if (err != null)
+ critical("Unable to fetch low-quality thumbnail for %s (scale: %d): %s", to_string(), scale,
+ err.message);
+
+ if (pixbuf != null)
+ set_image(pixbuf);
+
+ if (unscaled != null)
+ unscaled_pixbuf = unscaled;
+
+ delayed_high_quality_fetch();
+ }
+
+ private void on_high_quality_fetched(Gdk.Pixbuf? pixbuf, Gdk.Pixbuf? unscaled, Dimensions dim,
+ Gdk.InterpType interp, Error? err) {
+ if (err != null)
+ critical("Unable to fetch high-quality thumbnail for %s (scale: %d): %s", to_string(), scale,
+ err.message);
+
+ if (pixbuf != null)
+ set_image(pixbuf);
+
+ if (unscaled != null)
+ unscaled_pixbuf = unscaled;
+ }
+
+ public override void exposed() {
+ exposure = true;
+
+ if (!has_image())
+ schedule_low_quality_fetch();
+
+ update_title();
+ update_comment();
+ update_tags();
+
+ base.exposed();
+ }
+
+ public override void unexposed() {
+ exposure = false;
+
+ paint_empty();
+
+ base.unexposed();
+ }
+
+ protected override Gdk.Pixbuf? get_top_right_trinket(int scale) {
+ Flaggable? flaggable = media as Flaggable;
+
+ return (flaggable != null && flaggable.is_flagged())
+ ? Resources.get_icon(Resources.ICON_FLAGGED_TRINKET) : null;
+ }
+
+ protected override Gdk.Pixbuf? get_bottom_left_trinket(int scale) {
+ Rating rating = media.get_rating();
+ bool show_ratings = (bool) get_collection_property(PROP_SHOW_RATINGS, false);
+
+ return (rating != Rating.UNRATED && show_ratings)
+ ? Resources.get_rating_trinket(rating, scale) : null;
+ }
+}