1/*
2 * Copyright 2011-2017 Corentin Noël <corentin@elementary.io>
3 * SPDX-License-Identifier: LGPL-3.0-or-later
4 */
5
6/**
7 * An horizontal bar showing the remaining amount of space.
8 *
9 * {{../doc/images/StorageBar.png}}
10 *
11 * ''Example''<<BR>>
12 * {{{
13 * public class StorageView : Gtk.Grid {
14 *     construct {
15 *         var file_root = GLib.File.new_for_path ("/");
16 *
17 *         try {
18 *             var info = file_root.query_filesystem_info (GLib.FileAttribute.FILESYSTEM_SIZE, null);
19 *
20 *             var size = info.get_attribute_uint64 (GLib.FileAttribute.FILESYSTEM_SIZE);
21 *
22 *             var storage = new Granite.Widgets.StorageBar.with_total_usage (size, size/2);
23 *             storage.update_block_size (Granite.Widgets.StorageBar.ItemDescription.AUDIO, size/40);
24 *             storage.update_block_size (Granite.Widgets.StorageBar.ItemDescription.VIDEO, size/30);
25 *             storage.update_block_size (Granite.Widgets.StorageBar.ItemDescription.APP, size/20);
26 *             storage.update_block_size (Granite.Widgets.StorageBar.ItemDescription.PHOTO, size/10);
27 *             storage.update_block_size (Granite.Widgets.StorageBar.ItemDescription.FILES, size/5);
28 *
29 *             add (storage);
30 *         } catch (Error e) {
31 *             critical (e.message);
32 *         }
33 *     }
34 * }
35 * }}}
36 */
37public class Granite.Widgets.StorageBar : Gtk.Box {
38    public enum ItemDescription {
39        OTHER,
40        AUDIO,
41        VIDEO,
42        PHOTO,
43        APP,
44        FILES = OTHER;
45
46        public static string? get_class (ItemDescription description) {
47            switch (description) {
48                case ItemDescription.FILES:
49                    return "files";
50                case ItemDescription.AUDIO:
51                    return "audio";
52                case ItemDescription.VIDEO:
53                    return "video";
54                case ItemDescription.PHOTO:
55                    return "photo";
56                case ItemDescription.APP:
57                    return "app";
58                default:
59                    return null;
60            }
61        }
62
63        public static string get_name (ItemDescription description) {
64            switch (description) {
65                case ItemDescription.AUDIO:
66                    return _("Audio");
67                case ItemDescription.VIDEO:
68                    /// TRANSLATORS: Refers to videos the mime type. Not Videos the app.
69                    return _("Videos");
70                case ItemDescription.PHOTO:
71                    /// TRANSLATORS: Refers to photos the mime type. Not Photos the app.
72                    return _("Photos");
73                case ItemDescription.APP:
74                    return _("Apps");
75                case ItemDescription.FILES:
76                    /// TRANSLATORS: Refers to files the mime type. Not Files the app.
77                    return _("Files");
78                default:
79                    return _("Other");
80            }
81        }
82    }
83
84    private uint64 _storage = 0;
85    public uint64 storage {
86        get {
87            return _storage;
88        }
89
90        set {
91            _storage = value;
92            update_size_description ();
93        }
94    }
95
96    private uint64 _total_usage = 0;
97
98    public uint64 total_usage {
99        get {
100            return _total_usage;
101        }
102
103        set {
104            _total_usage = uint64.min (value, storage);
105            update_size_description ();
106        }
107    }
108
109    public int inner_margin_sides {
110        get {
111            return fillblock_box.margin_start;
112        }
113        set {
114            fillblock_box.margin_end = fillblock_box.margin_start = value;
115        }
116    }
117
118    private Gtk.Label description_label;
119    private GLib.HashTable<int, FillBlock> blocks;
120    private int index = 0;
121    private Gtk.Box fillblock_box;
122    private Gtk.Box legend_box;
123    private FillBlock free_space;
124    private FillBlock used_space;
125
126    /**
127     * Creates a new StorageBar widget with the given amount of space.
128     *
129     * @param storage the total amount of space.
130     */
131    public StorageBar (uint64 storage) {
132        Object (storage: storage);
133    }
134
135    /**
136     * Creates a new StorageBar widget with the given amount of space.an a larger total usage block
137     *
138     * @param storage the total amount of space.
139     * @param usage the amount of space used.
140     */
141    public StorageBar.with_total_usage (uint64 storage, uint64 total_usage) {
142        Object (storage: storage, total_usage: total_usage);
143    }
144
145    static construct {
146        Granite.init ();
147    }
148
149    construct {
150        orientation = Gtk.Orientation.VERTICAL;
151        description_label = new Gtk.Label (null);
152        description_label.hexpand = true;
153        description_label.margin_top = 6;
154        get_style_context ().add_class (Granite.STYLE_CLASS_STORAGEBAR);
155        blocks = new GLib.HashTable<int, FillBlock> (null, null);
156        fillblock_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
157        fillblock_box.get_style_context ().add_class (Gtk.STYLE_CLASS_TROUGH);
158        fillblock_box.hexpand = true;
159        inner_margin_sides = 12;
160        legend_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12);
161        legend_box.expand = true;
162        var legend_center_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
163        legend_center_box.set_center_widget (legend_box);
164        var legend_scrolled = new Gtk.ScrolledWindow (null, null);
165        legend_scrolled.vscrollbar_policy = Gtk.PolicyType.NEVER;
166        legend_scrolled.hexpand = true;
167        legend_scrolled.add (legend_center_box);
168        var grid = new Gtk.Grid ();
169        grid.attach (legend_scrolled, 0, 0, 1, 1);
170        grid.attach (fillblock_box, 0, 1, 1, 1);
171        grid.attach (description_label, 0, 2, 1, 1);
172        set_center_widget (grid);
173
174        fillblock_box.size_allocate.connect ((allocation) => {
175            // lost_size is here because we use truncation so that it is possible for a full device to have a filed bar.
176            double lost_size = 0;
177            int current_x = allocation.x;
178            for (int i = 0; i < blocks.length; i++) {
179                weak FillBlock block = blocks.get (i);
180                if (block == null || block.visible == false)
181                    continue;
182
183                var new_allocation = Gtk.Allocation ();
184                new_allocation.x = current_x;
185                new_allocation.y = allocation.y;
186                double width = (((double)allocation.width) * (double) block.size / (double) storage) + lost_size;
187                lost_size -= GLib.Math.trunc (lost_size);
188                new_allocation.width = (int) GLib.Math.trunc (width);
189                new_allocation.height = allocation.height;
190                block.size_allocate_with_baseline (new_allocation, block.get_allocated_baseline ());
191
192                lost_size = width - new_allocation.width;
193                current_x += new_allocation.width;
194            }
195        });
196
197        create_default_blocks ();
198    }
199
200    private void create_default_blocks () {
201        var seq = new Sequence<ItemDescription> ();
202        seq.append (ItemDescription.FILES);
203        seq.append (ItemDescription.AUDIO);
204        seq.append (ItemDescription.VIDEO);
205        seq.append (ItemDescription.PHOTO);
206        seq.append (ItemDescription.APP);
207        seq.sort ((a, b) => {
208            if (a == ItemDescription.FILES)
209                return 1;
210            if (b == ItemDescription.FILES)
211                return -1;
212
213            return ItemDescription.get_name (a).collate (ItemDescription.get_name (b));
214        });
215
216        seq.foreach ((description) => {
217            var fill_block = new FillBlock (description, 0);
218            fillblock_box.add (fill_block);
219            legend_box.add (fill_block.legend_item);
220            blocks.set (index, fill_block);
221            index++;
222        });
223
224        free_space = new FillBlock (ItemDescription.FILES, storage);
225        used_space = new FillBlock (ItemDescription.FILES, total_usage);
226        free_space.get_style_context ().add_class ("empty-block");
227        free_space.get_style_context ().remove_class ("files");
228        used_space.get_style_context ().remove_class ("files");
229        blocks.set (index++, used_space);
230        blocks.set (index++, free_space);
231        fillblock_box.add (used_space);
232        fillblock_box.add (free_space);
233
234        update_size_description ();
235    }
236
237    private void update_size_description () {
238        uint64 user_size = 0;
239        foreach (weak FillBlock block in blocks.get_values ()) {
240            if (block.visible == false || block == free_space || block == used_space)
241                continue;
242            user_size += block.size;
243        }
244
245        uint64 free;
246        if (user_size > total_usage) {
247            free = storage - user_size;
248            used_space.size = 0;
249        } else {
250            free = storage - total_usage;
251            used_space.size = total_usage - user_size;
252        }
253
254        free_space.size = free;
255        description_label.label = _("%s free out of %s").printf (GLib.format_size (free), GLib.format_size (storage));
256    }
257
258    /**
259     * Update the specified block with a given amount of space.
260     *
261     * @param description the category to update.
262     * @param size the size of the category or 0 to hide.
263     */
264    public void update_block_size (ItemDescription description, uint64 size) {
265        foreach (weak FillBlock block in blocks.get_values ()) {
266            if (block.description == description) {
267                block.size = size;
268                update_size_description ();
269                return;
270            }
271        }
272    }
273
274    internal class FillBlock : FillRound {
275        private uint64 _size = 0;
276        public uint64 size {
277            get {
278                return _size;
279            }
280            set {
281                _size = value;
282                if (_size == 0) {
283                    no_show_all = true;
284                    visible = false;
285                    legend_item.no_show_all = true;
286                    legend_item.visible = false;
287                } else {
288                    no_show_all = false;
289                    visible = true;
290                    legend_item.no_show_all = false;
291                    legend_item.visible = true;
292                    size_label.label = GLib.format_size (_size);
293                    queue_resize ();
294                }
295            }
296        }
297
298        public ItemDescription description { public get; construct set; }
299        public Gtk.Grid legend_item { public get; private set; }
300        private Gtk.Label name_label;
301        private Gtk.Label size_label;
302        private FillRound legend_fill;
303
304        internal FillBlock (ItemDescription description, uint64 size) {
305            Object (size: size, description: description);
306            var clas = ItemDescription.get_class (description);
307            if (clas != null) {
308                get_style_context ().add_class (clas);
309                legend_fill.get_style_context ().add_class (clas);
310            }
311
312            name_label.label = "<b>%s</b>".printf (GLib.Markup.escape_text (ItemDescription.get_name (description)));
313        }
314
315        construct {
316            show_all ();
317            legend_item = new Gtk.Grid ();
318            legend_item.column_spacing = 6;
319            name_label = new Gtk.Label (null);
320            name_label.halign = Gtk.Align.START;
321            name_label.use_markup = true;
322            size_label = new Gtk.Label (null);
323            size_label.halign = Gtk.Align.START;
324            legend_fill = new FillRound ();
325            legend_fill.get_style_context ().add_class ("legend");
326            var legend_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
327            legend_box.set_center_widget (legend_fill);
328            legend_item.attach (legend_box, 0, 0, 1, 2);
329            legend_item.attach (name_label, 1, 0, 1, 1);
330            legend_item.attach (size_label, 1, 1, 1, 1);
331        }
332    }
333
334    internal class FillRound : Gtk.Widget {
335        internal FillRound () {
336
337        }
338
339        construct {
340            set_has_window (false);
341            var style_context = get_style_context ();
342            style_context.add_class ("fill-block");
343            expand = true;
344        }
345
346        public override bool draw (Cairo.Context cr) {
347            var width = get_allocated_width ();
348            var height = get_allocated_height ();
349            var context = get_style_context ();
350            context.render_background (cr, 0, 0, width, height);
351            context.render_frame (cr, 0, 0, width, height);
352            return true;
353        }
354
355        public override void get_preferred_width (out int minimum_width, out int natural_width) {
356            base.get_preferred_width (out minimum_width, out natural_width);
357            var context = get_style_context ();
358            var padding = context.get_padding (get_state_flags ());
359            minimum_width = int.max (padding.left + padding.right, minimum_width);
360            minimum_width = int.max (1, minimum_width);
361            natural_width = int.max (minimum_width, natural_width);
362        }
363
364        public override void get_preferred_height (out int minimum_height, out int natural_height) {
365            base.get_preferred_height (out minimum_height, out natural_height);
366            var context = get_style_context ();
367            var padding = context.get_padding (get_state_flags ());
368            minimum_height = int.max (padding.top + padding.bottom, minimum_height);
369            minimum_height = int.max (1, minimum_height);
370            natural_height = int.max (minimum_height, natural_height);
371        }
372    }
373}
374