1/* Browse.vala 2 * 3 * Copyright (C) 2009 - 2021 Jerry Casiano 4 * 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. 17 * 18 * If not, see <http://www.gnu.org/licenses/gpl-3.0.txt>. 19*/ 20 21namespace FontManager { 22 23 public enum BrowseMode { 24 LIST, 25 GRID, 26 N_MODES; 27 } 28 29 [GtkTemplate (ui = "/org/gnome/FontManager/ui/font-manager-font-preview-tile.ui")] 30 public class FontPreviewTile : Gtk.Grid { 31 32 [GtkChild] public unowned Gtk.Label family { get; } 33 [GtkChild] public unowned Gtk.Label count { get; } 34 [GtkChild] public unowned Gtk.Label preview { get; } 35 36 } 37 38 public class GridListModel : Object, ListModel { 39 40 public GenericArray <Object>? items { get; private set; } 41 42 construct { 43 items = new GenericArray <Object> (); 44 } 45 46 public Type get_item_type () { 47 return typeof(Family); 48 } 49 50 public uint get_n_items () { 51 return items.length; 52 } 53 54 public Object? get_item (uint position) { 55 return items[position]; 56 } 57 58 public void clear () { 59 uint start = 0; 60 uint end = get_n_items(); 61 items.remove_range(start, end); 62 items_changed(start, end, 0); 63 return; 64 } 65 66 public void add_item (Object item) { 67 return_if_fail(item is Family); 68 uint position = get_n_items(); 69 items.add(item); 70 items_changed(position, 0, 1); 71 return; 72 } 73 74 public void remove_item (uint position) { 75 items.remove_index(position); 76 items_changed(position, 1, 0); 77 return; 78 } 79 80 } 81 82 [GtkTemplate (ui = "/org/gnome/FontManager/ui/font-manager-browse-view.ui")] 83 public class Browse : Gtk.Box { 84 85 public signal void mode_selected (BrowseMode mode); 86 87 public double preview_size { get; set; } 88 public GLib.HashTable <string, string>? samples { get; set; default = null; } 89 public Gtk.Adjustment adjustment { get; set; } 90 91 public Gtk.TreeModel? model { get; set; } 92 93 public BrowseMode mode { 94 get { 95 return grid_is_visible ? BrowseMode.GRID : BrowseMode.LIST; 96 } 97 set { 98 list_view.set_active(value == BrowseMode.LIST); 99 grid_view.set_active(value == BrowseMode.GRID); 100 } 101 } 102 103 [GtkChild] public unowned Gtk.TreeView treeview { get; } 104 [GtkChild] public unowned PreviewEntry entry { get; } 105 106 [GtkChild] unowned FontScale fontscale; 107 [GtkChild] unowned Gtk.Stack browse_stack; 108 [GtkChild] unowned Gtk.FlowBox flowbox; 109 [GtkChild] unowned Gtk.Label page_count; 110 [GtkChild] unowned Gtk.Button prev_page; 111 [GtkChild] unowned Gtk.Button next_page; 112 [GtkChild] unowned Gtk.RadioButton list_view; 113 [GtkChild] unowned Gtk.RadioButton grid_view; 114 [GtkChild] unowned Gtk.Box page_controls; 115 [GtkChild] unowned Gtk.Entry selected_page; 116 117 double n_pages = 0.0; 118 double current_page = 0.0; 119 double MAX_TILES = 25.0; 120 bool grid_is_visible = false; 121 GridListModel flowbox_model; 122 123 public override void constructed () { 124 flowbox_model = new GridListModel(); 125 flowbox.bind_model(flowbox_model, (Gtk.FlowBoxCreateWidgetFunc) preview_tile_from_item); 126 var renderer = new CellRendererTitle(); 127 treeview.insert_column_with_data_func(0, "", renderer, cell_data_func); 128 treeview.get_selection().set_mode(Gtk.SelectionMode.NONE); 129 bind_property("model", treeview, "model", BindingFlags.DEFAULT | BindingFlags.SYNC_CREATE); 130 bind_property("preview-size", fontscale, "value", BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE); 131 fontscale.bind_property("adjustment", this, "adjustment", BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE); 132 adjustment.value_changed.connect(() => { 133 treeview.get_column(0).queue_resize(); 134 update_grid(); 135 }); 136 notify["model"].connect(() => { 137 expand_all(); 138 n_pages = 1.0; 139 current_page = 1.0; 140 update_grid(); 141 if (model != null) 142 n_pages = Math.ceil(model.iter_n_children(null) / MAX_TILES); 143 update_page_controls(); 144 }); 145 base.constructed(); 146 return; 147 } 148 149 public void restore_state (GLib.Settings settings) { 150 preview_size = settings.get_double("browse-font-size"); 151 entry.text = settings.get_string("browse-preview-text"); 152 mode = (BrowseMode) settings.get_enum("browse-mode"); 153 treeview.get_column(0).queue_resize(); 154 mode_selected.connect((m) => { settings.set_enum("browse-mode", (int) m); }); 155 settings.bind("browse-font-size", this, "preview-size", SettingsBindFlags.DEFAULT); 156 settings.bind("browse-preview-text", entry, "text", SettingsBindFlags.DEFAULT); 157 return; 158 } 159 160 [GtkCallback] 161 void on_entry_changed () { 162 treeview.get_column(0).queue_resize(); 163 update_grid(); 164 return; 165 } 166 167 [GtkCallback] 168 void on_grid_map () { 169 grid_is_visible = true; 170 update_grid(); 171 treeview.get_column(0).queue_resize(); 172 return; 173 } 174 175 [GtkCallback] 176 void on_grid_unmap () { 177 grid_is_visible = false; 178 update_grid(); 179 treeview.get_column(0).queue_resize(); 180 return; 181 } 182 183 [GtkCallback] 184 void on_mode_button_click (Gtk.Button button) { 185 browse_stack.set_visible_child_name(button.name); 186 page_controls.set_visible(button.name == "grid"); 187 mode_selected(button.name == "grid" ? BrowseMode.GRID : BrowseMode.LIST); 188 return; 189 } 190 191 [GtkCallback] 192 void on_prev_page_clicked (Gtk.Button button) { 193 current_page--; 194 update_grid(); 195 return; 196 } 197 198 [GtkCallback] 199 void on_next_page_clicked (Gtk.Button button) { 200 current_page++; 201 update_grid(); 202 return; 203 } 204 205 [GtkCallback] 206 void on_selected_page_changed (Gtk.Editable entry) { 207 double selected = double.parse(((Gtk.Entry) entry).get_text()); 208 current_page = selected.clamp(1.0, n_pages); 209 update_grid(); 210 return; 211 } 212 213 [GtkCallback] 214 bool on_selected_page_focus_in_event (Gtk.Widget entry, 215 Gdk.EventFocus unused) { 216 selected_page.set_text(""); 217 return false; 218 } 219 220 [GtkCallback] 221 bool on_selected_page_focus_out_event (Gtk.Widget entry, 222 Gdk.EventFocus unused) { 223 selected_page.set_text("%.f".printf(current_page)); 224 return false; 225 } 226 227 void update_page_controls () { 228 selected_page.set_width_chars(n_pages < 100 ? 2 : 3); 229 page_count.set_text("/ %.f".printf(n_pages)); 230 selected_page.set_text("%.f".printf(current_page)); 231 prev_page.set_sensitive(current_page > 1); 232 next_page.set_sensitive(n_pages > 1 && current_page < n_pages); 233 return; 234 } 235 236 string get_preview_label_markup (Family family) { 237 uint n_variations = family.variations.get_length(); 238 var result = new StringBuilder(); 239 for (uint i = 0; i < n_variations; i++) { 240 if (i > 0) 241 result.append("\n"); 242 Font variation = new Font(); 243 variation.source_object = family.variations.get_object_element(i); 244 string markup = "<span fallback = \"false\" font = \"%s %i\">%s</span>"; 245 string preview_text = variation.style != null ? 246 variation.style : 247 variation.description; 248 if (entry.text_length > 0) 249 preview_text = entry.text; 250 else if (samples != null && samples.contains(variation.description)) 251 preview_text = samples.lookup(variation.description); 252 result.append(markup.printf(variation.description, 253 (int) preview_size, 254 Markup.escape_text(preview_text))); 255 } 256 return result.str; 257 } 258 259 [CCode (instance_pos = -1)] 260 Gtk.Widget preview_tile_from_item (Object _item) { 261 var item = (Family) _item; 262 var tile = new FontPreviewTile(); 263 string markup = "<span size=\"%i\"><b>%s</b></span>"; 264 int title_size = (int) get_desc_size() * Pango.SCALE; 265 string title = markup.printf(title_size, Markup.escape_text(item.family)); 266 tile.family.set_markup(title); 267 tile.preview.set_markup(get_preview_label_markup(item)); 268 tile.count.set_text(item.variations.get_length().to_string()); 269 tile.show(); 270 return tile; 271 } 272 273 void update_grid () { 274 flowbox_model.clear(); 275 if (!grid_is_visible) 276 return; 277 if (model != null) { 278 double end = (current_page * MAX_TILES); 279 int start = (current_page == 1) ? 0 : (int) (end - MAX_TILES); 280 double current = start; 281 Gtk.TreeIter iter; 282 bool valid = model.iter_nth_child(out iter, null, start); 283 while (valid && current < end) { 284 Value val; 285 model.get_value(iter, FontModelColumn.OBJECT, out val); 286 flowbox_model.add_item(val.get_object()); 287 valid = model.iter_next(ref iter); 288 current++; 289 val.unset(); 290 } 291 update_page_controls(); 292 } 293 return; 294 } 295 296 void expand_all () { 297 treeview.expand_all(); 298 /* Workaround first row height bug? */ 299 treeview.get_column(0).queue_resize(); 300 return; 301 } 302 303 void cell_data_func (Gtk.TreeViewColumn layout, 304 Gtk.CellRenderer cell, 305 Gtk.TreeModel model, 306 Gtk.TreeIter treeiter) { 307 if (grid_is_visible) 308 return; 309 Value val; 310 model.get_value(treeiter, FontModelColumn.OBJECT, out val); 311 Object obj = val.get_object(); 312 string font_desc; 313 bool active = true; 314 Pango.AttrList attrs = new Pango.AttrList(); 315 attrs.insert(Pango.attr_fallback_new(false)); 316 cell.set_property("attributes", attrs); 317 Pango.FontDescription default_desc = get_font(treeview); 318 default_desc.set_size((int) ((get_desc_size()) * Pango.SCALE)); 319 cell.set_property("font-desc" , default_desc); 320 Reject? reject = get_default_application().reject; 321 if (obj is Family) { 322 font_desc = ((Family) obj).family; 323 if (reject != null) 324 active = !(((Family) obj).family in reject); 325 cell.set_property("title" , font_desc); 326 cell.set_padding(24, 8); 327 ((CellRendererPill) cell).render_background = true; 328 } else { 329 ((CellRendererPill) cell).render_background = false; 330 font_desc = ((Font) obj).description; 331 if (reject != null) 332 active = !(((Font) obj).family in reject); 333 Pango.FontDescription desc = Pango.FontDescription.from_string(font_desc); 334 desc.set_size((int) (preview_size * Pango.SCALE)); 335 cell.set_property("font-desc" , desc); 336 cell.set_padding(32, 10); 337 if (entry.text_length > 0) 338 cell.set_property("text", entry.text); 339 else if (samples != null && samples.contains(font_desc)) 340 cell.set_property("text", samples.lookup(font_desc)); 341 else 342 cell.set_property("text", font_desc); 343 } 344 cell.set_property("sensitive" , active); 345 cell.set_property("strikethrough", !active); 346 val.unset(); 347 return; 348 } 349 350 double get_desc_size () { 351 double desc_size = preview_size; 352 if (desc_size <= 10) 353 return desc_size; 354 else if (desc_size <= 20) 355 return desc_size / 1.25; 356 else if (desc_size <= 30) 357 return desc_size / 1.5; 358 else if (desc_size <= 50) 359 return desc_size / 1.75; 360 else 361 return desc_size / 2; 362 } 363 364 } 365 366} 367