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