File: AsyncImage.vala

package info (click to toggle)
granite 6.2.0-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,748 kB
  • sloc: python: 10; makefile: 8
file content (380 lines) | stat: -rw-r--r-- 16,496 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
/*
 * Copyright 2017–2019 elementary, Inc. (https://elementary.io)
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

/**
 * AsyncImage is a {@link Gtk.Image} that provides a way to load
 * icons and images asynchronously without blocking the main GTK thread.
 *
 * AsyncImage can be used to improve your GTK interface's performance that
 * has a lot of images to load and populate e.g: the applications menu and
 * an icon chooser.
 *
 * Primarily the {@link Gtk.Image} loads it's surface synchronously and blocks the main GTK thread
 * which can cause significant slow downs and lagging. The AsyncImage is a wrapper for the {@link Gtk.Image}
 * and provides with two main methods: {@link Granite.AsyncImage.set_from_gicon_async} and {@link Granite.AsyncImage.set_from_file_async}.
 *
 * AsyncImage internally operates only on {@link Gdk.Pixbuf} and {@link Cairo.Surface}'s which means that you cannot read valid properties
 * from the main {@link Gtk.Image} like {@link Gtk.Image.icon_name}, {@link Gtk.Image.gicon} or {@link Gtk.Image.file}.
 * The only property which will be set is the final surface: {@link Gtk.Image.surface}.
 *
 * Even though AsyncImage sets only the {@link Gtk.Image.surface}, it automatically detects changes to the underlying {@link Gtk.Widget.scale_factor}
 * and reloads the icon to a new scale factor when it changes. If you request to set an {@link GLib.ThemedIcon} and the icon or GTK theme changes
 * the AsyncImage will also reload it to display the new icon with applied changes.
 *
 * The {@link Granite.AsyncImage.gicon_async} and {@link Granite.AsyncImage.size_async} are properties which reflect
 * the current icon and it's size which will or is currently displayed. Note that those two properties will return
 * meaningful results //''only''// when you call {@link Granite.AsyncImage.set_from_gicon_async} and it's wrappers.
 *
 * AsyncImage has also its own cache for already loaded icons. If you attempt to load the same icon at the same size
 * AsyncImage will look it up and if it's available, will set it immediately.
 *
 * If you want to detect when the image was actually loaded into the {@link Cairo.Surface} you can connect to
 * the {@link GLib.Object.notify} signal for {@link Gtk.Image.surface}.
 */
public class Granite.AsyncImage : Gtk.Image {
    private class CacheEntry {
        public string icon;
        public Cairo.Surface? surface;
        public int size;
        public int scale_factor;

        public CacheEntry (string icon, Cairo.Surface? surface, int size, int scale_factor) {
            this.icon = icon;
            this.surface = surface;
            this.size = size;
            this.scale_factor = scale_factor;
        }
    }

    private static Gee.ArrayList<CacheEntry> cache;

    /**
     * If the image should be loaded when the image is rendered.
     *
     * For more details see {@link Granite.AsyncImage.AsyncImage}.
     */
    public bool load_on_realize { construct; private get; }

    /**
     * If the widget should act as a placeholder when the image is not yet loaded.
     *
     * For more details see {@link Granite.AsyncImage.AsyncImage}.
     */
    public bool auto_size_request { construct; private get; }

    /**
     * The icon that will be or is currently displayed in the image.
     *
     * Note that this property is by default and will be ``null`` if you didn't call the {@link Granite.AsyncImage.set_from_gicon_async} or it's wrappers.
     */
    public Icon? gicon_async { get; private set; default = null; }

    /**
     * The size in pixels of the displayed {@link Granite.AsyncImage.gicon_async}.
     *
     * Note that this property is by default and will be ``-1`` if you didn't call the {@link Granite.AsyncImage.set_from_gicon_async} or it's wrappers.
     */
    public int size_async { get; private set; default = -1; }

    private int current_scale_factor = 1;

    /**
     * Creates a new {@link Granite.AsyncImage} that displays
     * a requested icon or file to display asynchronously.
     *
     * The ``load_on_realize`` boolean parameter specifies if the requested image should load when
     * it's about to render and show. This is useful when you don't want to have the image data
     * loaded into memory immediately after calling {@link Granite.AsyncImage.set_from_gicon_async}.
     * Internally this parameter causes the {@link Granite.AsyncImage} to connect to the {@link Gtk.Widget.realize} signal.
     *
     * ``auto_size_request`` boolean parameter specifies if AsyncImage should allocate initial
     * space when loading the image. This is useful when the image is not yet loaded and the widget
     * should act as a placeholder until the image is loaded. Calling any of the ``set_from`` methods will
     * call the {@link Gtk.Widget.set_size_request} with the passed ``size`` or ``width`` and ``height`` if you called {@link Granite.AsyncImage.set_from_file_async}.
     * When image is loaded and shown the size request is then reset to the original values.
     *
     * @param load_on_realize if ``true`` the image will be loaded when it's rendered, false to load the image immediately
     * @param auto_size_request if the widget should act as a placeholder when the image is not yet loaded
     */
    public AsyncImage (bool load_on_realize = true, bool auto_size_request = true) {
        Object (load_on_realize: load_on_realize, auto_size_request: auto_size_request);
    }

    /**
     * Creates a new {@link Granite.AsyncImage} with the supplied
     * ``icon`` and ``size``. See {@link Granite.AsyncImage.AsyncImage} for more details.
     *
     * This is equivalent to calling {@link Granite.AsyncImage.AsyncImage} and {@link Granite.AsyncImage.set_from_gicon_async}.
     *
     * @param icon the {@link GLib.Icon} to display in the image
     * @param size the size of the icon, ``-1`` to load the default size
     * @param load_on_realize if ``true`` the image will be loaded when it's rendered, false to load the image immediately
     * @param auto_size_request if the widget should act as a placeholder when the image is not yet loaded
     */
    public AsyncImage.from_gicon_async (
        Icon icon,
        int size,
        bool load_on_realize = true,
        bool auto_size_request = true
    ) {
        Object (load_on_realize: load_on_realize, auto_size_request: auto_size_request);
        set_from_gicon_async.begin (icon, size);
    }

    /**
     * Creates a new {@link Granite.AsyncImage} with the supplied
     * ``icon_name`` and {@link Gtk.IconSize}. See {@link Granite.AsyncImage.AsyncImage} for more details.
     *
     * This is equivalent to calling {@link Granite.AsyncImage.AsyncImage} and {@link Granite.AsyncImage.set_from_icon_name_async}.
     *
     * @param icon_name the icon name to display in the image
     * @param icon_size the {@link Gtk.IconSize} as the size for the image
     * @param load_on_realize if ``true`` the image will be loaded when it's rendered, false to load the image immediately
     * @param auto_size_request if the widget should act as a placeholder when the image is not yet loaded
     */
    public AsyncImage.from_icon_name_async (
        string icon_name,
        Gtk.IconSize icon_size,
        bool load_on_realize = true,
        bool auto_size_request = true
    ) {
        Object (load_on_realize: load_on_realize, auto_size_request: auto_size_request);
        set_from_icon_name_async.begin (icon_name, icon_size);
    }

    static construct {
        cache = new Gee.ArrayList<CacheEntry> ();
    }

    construct {
        if (load_on_realize) {
            realize.connect (() => update.begin ());
        }

        style_updated.connect (() => {
            if (get_realized ()) {
                update.begin (true);
            }
        });

        direction_changed.connect (() => update.begin (true));

        notify["scale-factor"].connect (() => {
            if (get_scale_factor () != current_scale_factor) {
                update.begin ();
            }
        });
    }

    /**
     * Sets the image to display an {@link GLib.Icon} with a specified size asynchronously.
     *
     * This method sets the {@link Granite.AsyncImage.gicon_async} and {@link Granite.AsyncImage.size_async} properties
     * and depending on the {@link Granite.AsyncImage.load_on_realize} setting, loads it when the image realizes or
     * loads it immediately.
     *
     * Use {@link GLib.ThemedIcon} or {@link Granite.AsyncImage.set_from_icon_name_async} to load the image
     * from an icon name.
     *
     * If the ``icon`` is a {@link GLib.FileIcon} then the image will be loaded using  the {@link Granite.AsyncImage.set_from_file_async}
     * method with the supplied size for both ``width`` and ``height`` with preserving the aspect ratio of the image.
     *
     * If the {@link Granite.AsyncImage.load_on_realize} is ``true``, the error will never be thrown in this method since
     * the loading will happen internally in the AsyncImage when the {@link Gtk.Widget.realize} signal is invoked.
     * In this case, a warning will be printed with relevant information about a fauilure.
     *
     * @param icon the {@link GLib.Icon} to display in the image
     * @param size the size of the icon, ``0`` will clear the {@link Gtk.Image.pixbuf}, ``-1`` to load the default size
     * @param cancellable the cancellable to stop loading the icon
     *
     * @throws GLib.Error when the the icon was not found or failed to load
     */
    public async void set_from_gicon_async (Icon icon, int size, Cancellable? cancellable = null) throws Error {
        gicon_async = icon;
        size_async = size;

        if (auto_size_request) {
            set_size_request (size, size);
        }

        if (!load_on_realize) {
            try {
                yield set_from_gicon_async_internal (gicon_async, size_async, cancellable, false);
            } catch (Error e) {
                throw e;
            }
        }
    }

    /**
     * A wrapper for {@link Granite.AsyncImage.set_from_gicon_async} to display an icon name.
     *
     * This is a convenience method for setting an icon name with a desired {@link Gtk.IconSize}. Note that you'll not be
     * able to change the icon size afterwards with {@link Gtk.Image.pixel_size} or {@link Gtk.Image.icon_size}. You will
     * have to call one of the {@link Granite.AsyncImage} set_from_ methods to change it's size.
     *
     * See {@link Granite.AsyncImage.set_from_gicon_async} for more details.
     *
     * @param icon_name the icon name to display in the image
     * @param icon_size the {@link Gtk.IconSize} as the size for the image
     * @param cancellable the cancellable to stop loading the icon
     *
     * @throws GLib.Error when the the icon was not found or failed to load
     */
    public async void set_from_icon_name_async (
        string icon_name,
        Gtk.IconSize icon_size,
        Cancellable? cancellable = null
    ) throws Error {
        int width, height;
        if (!Gtk.icon_size_lookup (icon_size, out width, out height)) {
            warning ("Invalid icon size %d", icon_size);
            return;
        }

        try {
            yield set_from_gicon_async (new ThemedIcon (icon_name), int.min (width, height), cancellable);
        } catch (Error e) {
            throw e;
        }
    }

    /**
     * Sets the image to display a {@link GLib.File} with requested width and height.
     *
     * ''Note that this method is not a wrapper to the main'' {@link Granite.AsyncImage.set_from_gicon_async} ''method''. Internally, it only creates
     * a {@link Gdk.Pixbuf} with an {@link GLib.InputStream}, loads it asynchronously and sets the {@link Gtk.Image}'s surface to the result.
     *
     * This method will reset the {@link Granite.AsyncImage.gicon_async} and {@link Granite.AsyncImage.size_async} properties to their
     * default values and will not make the {@link Granite.AsyncImage} update the image when the scale factor or icon theme changes.
     *
     * For the time that the image is loaded, the size request of the AsyncImage will be set to ``width`` and ``height`` if ``auto_size_request`` is set to ``true``
     *
     * @param file the {@link GLib.File} to display in the image
     * @param width the width of the final image, ``-1`` to not constrain the width
     * @param height the height of the final image, ``-1`` to not constrain the height
     * @param preserve_aspect_ratio ``true`` to preserve the image's aspect ratio
     * @param cancellable the cancellable to stop loading the image
     *
     * @throws GLib.Error when the the file was not found or failed to load
     */
    public async void set_from_file_async (
        File file,
        int width,
        int height,
        bool preserve_aspect_ratio,
        Cancellable? cancellable = null
    ) throws Error {
        gicon_async = null;
        size_async = -1;

        if (auto_size_request) {
            set_size_request (width, height);
        }

        try {
            var stream = yield file.read_async ();
            var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async (
                stream,
                width * current_scale_factor,
                height * current_scale_factor,
                preserve_aspect_ratio,
                cancellable
            );
            surface = Gdk.cairo_surface_create_from_pixbuf (pixbuf, current_scale_factor, null);
            reset_size_request ();
        } catch (Error e) {
            reset_size_request ();
            throw e;
        }
    }

    private async void set_from_gicon_async_internal (
        Icon icon,
        int size,
        Cancellable? cancellable = null,
        bool bypass_cache
    ) throws Error {
        current_scale_factor = get_scale_factor ();

        if (size == 0) {
            clear ();
            return;
        } else if (size != -1 && !bypass_cache) {
            string target_icon = icon.to_string ();
            foreach (var entry in cache) {
                if (
                    entry.icon == target_icon &&
                    entry.size == size &&
                    entry.scale_factor == current_scale_factor
                ) {
                    surface = entry.surface;
                    reset_size_request ();
                    return;
                }
            }
        }

        if (icon is FileIcon) {
            try {
                yield set_from_file_async (((FileIcon)icon).file, size, size, true);
            } catch (Error e) {
                throw e;
            }

            return;
        }

        var style_context = get_style_context ();
        var theme = Gtk.IconTheme.get_for_screen (style_context.get_screen ());

        var flags = Gtk.IconLookupFlags.FORCE_SIZE | Gtk.IconLookupFlags.USE_BUILTIN;
        if (Gtk.StateFlags.DIR_RTL in style_context.get_state ()) {
            flags |= Gtk.IconLookupFlags.DIR_RTL;
        } else {
            flags |= Gtk.IconLookupFlags.DIR_LTR;
        }

        var info = theme.lookup_by_gicon_for_scale (icon, size, current_scale_factor, flags);
        if (info == null) {
            reset_size_request ();
            throw new IOError.NOT_FOUND ("Failed to lookup icon \"%s\" at size %i".printf (icon.to_string (), size));
        }

        try {
            Gdk.Pixbuf pixbuf;
            if (info.is_symbolic ()) {
                pixbuf = yield info.load_symbolic_for_context_async (style_context, cancellable);
            } else {
                pixbuf = yield info.load_icon_async ();
            }

            surface = Gdk.cairo_surface_create_from_pixbuf (pixbuf, current_scale_factor, null);
            reset_size_request ();

            var entry = new CacheEntry (icon.to_string (), surface, size, current_scale_factor);
            cache.add (entry);
        } catch (Error e) {
            reset_size_request ();
            throw e;
        }
    }

    private async void update (bool bypass_cache = false) {
        if (gicon_async != null && (gicon_async is ThemedIcon || gicon_async is FileIcon)) {
            try {
                yield set_from_gicon_async_internal (gicon_async, size_async, null, bypass_cache);
            } catch (Error e) {
                warning (e.message);
            }
        }
    }

    private void reset_size_request () {
        if (auto_size_request) {
            set_size_request (-1, -1);
        }
    }
}