Thumbnail.vala 13 KB
Newer Older
1
/* Copyright 2016 Software Freedom Conservancy Inc.
2 3
 *
 * This software is licensed under the GNU LGPL (version 2.1 or later).
4
 * See the COPYING file in this distribution.
5
 */
Jim Nelson's avatar
Jim Nelson committed
6

7
public class Thumbnail : MediaSourceItem {
8 9 10 11 12
    // 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";
13 14
    // SHOW_RATINGS (bool)
    public const string PROP_SHOW_RATINGS = "show-ratings";
15
    
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
    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();
        }
    }
31
    
32 33
    public const Gdk.InterpType LOW_QUALITY_INTERP = Gdk.InterpType.NEAREST;
    public const Gdk.InterpType HIGH_QUALITY_INTERP = Gdk.InterpType.BILINEAR;
34
    
35
    private const int HQ_IMPROVEMENT_MSEC = 100;
36
    
37
    private MediaSource media;
38
    private int scale;
39
    private Dimensions original_dim;
40
    private Dimensions dim;
41
    private Gdk.Pixbuf unscaled_pixbuf = null;
42
    private Cancellable cancellable = null;
43
    private bool hq_scheduled = false;
44
    private bool hq_reschedule = false;
45 46 47
    // this is cached locally because there are situations where the constant calls to is_exposed()
    // was showing up in sysprof
    private bool exposure = false;
Jim Nelson's avatar
Jim Nelson committed
48
    
49
    public Thumbnail(MediaSource media, int scale = DEFAULT_SCALE) {
50 51 52
        base (media, media.get_dimensions().get_scaled(scale, true), media.get_name(),
            media.get_comment());

53
        this.media = media;
54 55
        this.scale = scale;
        
56 57
        Tag.global.container_contents_altered.connect(on_tag_contents_altered);
        Tag.global.items_altered.connect(on_tags_altered);
58
        
59
        assert((media is LibraryPhoto) || (media is Video));
Jens Georg's avatar
Jens Georg committed
60

61
        original_dim = media.get_dimensions();
62
        dim = original_dim.get_scaled(scale, true);
63 64 65
        
        // initialize title and tags text line so they're properly accounted for when the display
        // size is calculated
66
        update_title(true);
67
        update_comment(true);
68
        update_tags(true);
69
    }
70

71
    ~Thumbnail() {
72 73
        if (cancellable != null)
            cancellable.cancel();
74 75 76

        Tag.global.container_contents_altered.disconnect(on_tag_contents_altered);
        Tag.global.items_altered.disconnect(on_tags_altered);
77 78
    }
    
79
    private void update_tags(bool init = false) {
80
        Gee.Collection<Tag>? tags = Tag.global.fetch_sorted_for_source(media);
81
        if (tags == null || tags.size == 0)
Andreas Brauchli's avatar
Andreas Brauchli committed
82
            clear_tags();
83
        else
Andreas Brauchli's avatar
Andreas Brauchli committed
84
            set_tags(tags);
85 86
    }
    
Jim Nelson's avatar
Jim Nelson committed
87
    private void on_tag_contents_altered(ContainerSource container, Gee.Collection<DataSource>? added,
88
        bool relinking, Gee.Collection<DataSource>? removed, bool unlinking) {
89 90 91
        if (!exposure)
            return;
        
92 93
        bool tag_added = (added != null) ? added.contains(media) : false;
        bool tag_removed = (removed != null) ? removed.contains(media) : false;
94
        
95
        // if media source we're monitoring is added or removed to any tag, update tag list
96
        if (tag_added || tag_removed)
97 98 99
            update_tags();
    }
    
100 101
    private void on_tags_altered(Gee.Map<DataObject, Alteration> altered) {
        if (!exposure)
102
            return;
103

104 105 106
        foreach (DataObject object in altered.keys) {
            Tag tag = (Tag) object;
            
107
            if (tag.contains(media)) {
108 109 110 111 112
                update_tags();
                
                break;
            }
        }
113 114
    }
    
115
    private void update_title(bool init = false) {
116
        string title = media.get_name();
117 118
        if (is_string_empty(title))
            clear_title();
119
        else if (!init)
120 121 122
            set_title(title);
    }
    
123 124 125 126 127 128 129 130
    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);
    }

131 132
    protected override void notify_altered(Alteration alteration) {
        if (exposure && alteration.has_detail("metadata", "name"))
133
            update_title();
134 135
        if (exposure && alteration.has_detail("metadata", "comment"))
            update_comment();
136 137
        
        base.notify_altered(alteration);
138 139
    }
    
140 141
    public MediaSource get_media_source() {
        return media;
142 143
    }
    
144 145 146
    //
    // Comparators
    //
147 148

    public static int64 photo_id_ascending_comparator(void *a, void *b) {
149
        return ((Thumbnail *) a)->media.get_instance_id() - ((Thumbnail *) b)->media.get_instance_id();
150 151 152 153 154
    }

    public static int64 photo_id_descending_comparator(void *a, void *b) {
        return photo_id_ascending_comparator(b, a);
    }
155 156
    
    public static int64 title_ascending_comparator(void *a, void *b) {
157
        int64 result = strcmp(((Thumbnail *) a)->get_natural_collation_key(), ((Thumbnail *) b)->get_natural_collation_key());
158
        return (result != 0) ? result : photo_id_ascending_comparator(a, b);
159 160 161
    }
    
    public static int64 title_descending_comparator(void *a, void *b) {
162
        int64 result = title_ascending_comparator(b, a);
163 164 165 166 167 168
        
        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");
169 170 171
    }
    
    public static int64 exposure_time_ascending_comparator(void *a, void *b) {
172 173 174
        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);
175
        
176
        return (result != 0) ? result : filename_ascending_comparator(a, b);
177 178 179
    }
    
    public static int64 exposure_time_desending_comparator(void *a, void *b) {
180
        int64 result = exposure_time_ascending_comparator(b, a);
181
        
182
        return (result != 0) ? result : filename_descending_comparator(a, b);
183
    }
184 185 186 187 188
    
    public static bool exposure_time_comparator_predicate(DataObject object, Alteration alteration) {
        return alteration.has_detail("metadata", "exposure-time");
    }
    
189 190 191 192
    public static bool filename_comparator_predicate(DataObject object, Alteration alteration) {
        return alteration.has_detail("metadata", "filename");
    }

193 194 195
    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();
Jens Georg's avatar
Jens Georg committed
196 197

        int64 result = strcmp(path_a.collate_key_for_filename(), path_b.collate_key_for_filename());
198 199
        return (result != 0) ? result : photo_id_ascending_comparator(a, b);
    }
Jens Georg's avatar
Jens Georg committed
200

201 202 203 204 205 206
    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);
    }
    
207
    public static int64 rating_ascending_comparator(void *a, void *b) {
208
        int64 result = ((Thumbnail *) a)->media.get_rating() - ((Thumbnail *) b)->media.get_rating();
209 210
        
        return (result != 0) ? result : photo_id_ascending_comparator(a, b);
211 212 213 214
    }

    public static int64 rating_descending_comparator(void *a, void *b) {
        int64 result = rating_ascending_comparator(b, a);
215 216 217 218 219 220
        
        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");
221
    }
222
    
223
    protected override void thumbnail_altered() {
224
        original_dim = media.get_dimensions();
225 226
        dim = original_dim.get_scaled(scale, true);
        
227
        if (exposure)
Valentín Barros's avatar
Valentín Barros committed
228
            delayed_high_quality_fetch();
229 230
        else
            paint_empty();
231
        
232 233 234
        base.thumbnail_altered();
    }
    
235 236 237 238 239
    protected override void notify_collection_property_set(string name, Value? old, Value val) {
        switch (name) {
            case PROP_SIZE:
                resize((int) val);
            break;
240
            
241 242 243
            case PROP_SHOW_RATINGS:
                notify_view_altered();
            break;
244 245 246 247 248 249
        }
        
        base.notify_collection_property_set(name, old, val);
    }
    
    private void resize(int new_scale) {
250 251
        assert(new_scale >= MIN_SCALE);
        assert(new_scale <= MAX_SCALE);
252
        
253
        if (scale == new_scale)
254
            return;
255 256
        
        scale = new_scale;
257 258 259
        dim = original_dim.get_scaled(scale, true);
        
        cancel_async_fetch();
260
        
261
        if (exposure) {
262 263 264 265 266 267 268 269 270 271 272 273 274
            // 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));
            
275
            delayed_high_quality_fetch();
276
        } else {
277
            clear_image(dim);
278
        }
279 280
    }
    
281 282 283
    private void paint_empty() {
        cancel_async_fetch();
        clear_image(dim);
284
        unscaled_pixbuf = null;
285 286
    }
    
287
    private void schedule_low_quality_fetch() {
288 289
        cancel_async_fetch();
        cancellable = new Cancellable();
290 291
        
        ThumbnailCache.fetch_async_scaled(media, ThumbnailCache.Size.SMALLEST, 
292 293 294
            dim, LOW_QUALITY_INTERP, on_low_quality_fetched, cancellable);
    }
    
295 296 297 298
    private void delayed_high_quality_fetch() {
        if (hq_scheduled) {
            hq_reschedule = true;
            
299
            return;
300
        }
301
        
302
        Timeout.add_full(Priority.DEFAULT, HQ_IMPROVEMENT_MSEC, on_schedule_high_quality);
303
        hq_scheduled = true;
304 305
    }
    
306
    private bool on_schedule_high_quality() {
307 308 309 310 311 312 313
        if (hq_reschedule) {
            hq_reschedule = false;
            
            return true;
        }
        
        cancel_async_fetch();
314 315
        cancellable = new Cancellable();
        
316
        if (exposure) {
317
            ThumbnailCache.fetch_async_scaled(media, scale, dim,
318
                HIGH_QUALITY_INTERP, on_high_quality_fetched, cancellable);
319
        }
320 321 322 323
        
        hq_scheduled = false;
        
        return false;
324 325 326
    }
    
    private void cancel_async_fetch() {
327 328 329
        // cancel outstanding I/O
        if (cancellable != null)
            cancellable.cancel();
330 331
    }
    
332 333
    private void on_low_quality_fetched(Gdk.Pixbuf? pixbuf, Gdk.Pixbuf? unscaled, Dimensions dim,
        Gdk.InterpType interp, Error? err) {
334
        if (err != null)
335
            critical("Unable to fetch low-quality thumbnail for %s (scale: %d): %s", to_string(), scale,
336
                err.message);
337
        
338 339
        if (pixbuf != null)
            set_image(pixbuf);
340
        
341 342 343
        if (unscaled != null)
            unscaled_pixbuf = unscaled;
        
344
        delayed_high_quality_fetch();
345 346
    }
    
347 348
    private void on_high_quality_fetched(Gdk.Pixbuf? pixbuf, Gdk.Pixbuf? unscaled, Dimensions dim,
        Gdk.InterpType interp, Error? err) {
349
        if (err != null)
350
            critical("Unable to fetch high-quality thumbnail for %s (scale: %d): %s", to_string(), scale, 
351
                err.message);
352
        
353 354
        if (pixbuf != null)
            set_image(pixbuf);
355 356 357
        
        if (unscaled != null)
            unscaled_pixbuf = unscaled;
358 359
    }
    
360
    public override void exposed() {
361 362
        exposure = true;
        
363
        if (!has_image())
364
            schedule_low_quality_fetch();
365
        
366
        update_title();
367
        update_comment();
368 369
        update_tags();
        
370
        base.exposed();
371 372
    }
    
373
    public override void unexposed() {
374 375
        exposure = false;
        
376
        paint_empty();
377
        
378
        base.unexposed();
379
    }
Jens Georg's avatar
Jens Georg committed
380

Jim Nelson's avatar
Jim Nelson committed
381 382
    protected override Gdk.Pixbuf? get_top_right_trinket(int scale) {
        Flaggable? flaggable = media as Flaggable;
383
        
Jens Georg's avatar
Jens Georg committed
384 385 386 387 388
        if (!(flaggable != null && flaggable.is_flagged()))
            return null;

        return Resources.get_flagged_trinket(scale);

389
    }
390
    
Jim Nelson's avatar
Jim Nelson committed
391 392 393 394 395 396
    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;
397
    }
Jens Georg's avatar
Jens Georg committed
398 399 400 401

    protected override Gdk.Pixbuf? get_top_left_trinket(int scale) {
        return (media is Video) ? Resources.get_video_trinket (scale) : null;
    }
Jim Nelson's avatar
Jim Nelson committed
402
}