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