diff options
Diffstat (limited to 'src/Thumbnail.vala')
-rw-r--r-- | src/Thumbnail.vala | 400 |
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; + } +} |