1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3  * Author:
4  *   Tavmjong Bah <tavmjong@free.fr>
5  *
6  * Copyright (C) 2018 Tavmong Bah
7  *
8  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
9  */
10 
11 #include <glibmm/i18n.h>
12 #include <glibmm/markup.h>
13 
14 #include "font-selector.h"
15 
16 #include "libnrtype/font-lister.h"
17 #include "libnrtype/font-instance.h"
18 
19 // For updating from selection
20 #include "inkscape.h"
21 #include "desktop.h"
22 #include "object/sp-text.h"
23 
24 namespace Inkscape {
25 namespace UI {
26 namespace Widget {
27 
FontSelector(bool with_size,bool with_variations)28 FontSelector::FontSelector (bool with_size, bool with_variations)
29     : Gtk::Grid ()
30     , family_frame (_("Font family"))
31     , style_frame (C_("Font selector", "Style"))
32     , size_label   (_("Font size"))
33     , size_combobox (true)   // With entry
34     , signal_block (false)
35     , font_size (18)
36 {
37 
38     Inkscape::FontLister* font_lister = Inkscape::FontLister::get_instance();
39 
40     // Font family
41     family_treecolumn.pack_start (family_cell, false);
42     family_treecolumn.set_fixed_width (200);
43     family_treecolumn.add_attribute (family_cell, "text", 0);
44     family_treecolumn.set_cell_data_func (family_cell, &font_lister_cell_data_func);
45 
46     family_treeview.set_row_separator_func (&font_lister_separator_func);
47     family_treeview.set_model (font_lister->get_font_list());
48     family_treeview.set_name ("FontSelector: Family");
49     family_treeview.set_headers_visible (false);
50     family_treeview.append_column (family_treecolumn);
51 
52     family_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
53     family_scroll.add (family_treeview);
54 
55     family_frame.set_hexpand (true);
56     family_frame.set_vexpand (true);
57     family_frame.add (family_scroll);
58 
59     // Style
60     style_treecolumn.pack_start (style_cell, false);
61     style_treecolumn.add_attribute (style_cell, "text", 0);
62     style_treecolumn.set_cell_data_func (style_cell, sigc::mem_fun(*this, &FontSelector::style_cell_data_func));
63     style_treecolumn.set_title ("Face");
64     style_treecolumn.set_resizable (true);
65 
66     style_treeview.set_model (font_lister->get_style_list());
67     style_treeview.set_name ("FontSelectorStyle");
68     style_treeview.append_column ("CSS", font_lister->FontStyleList.cssStyle);
69     style_treeview.append_column (style_treecolumn);
70 
71     style_treeview.get_column(0)->set_resizable (true);
72 
73     style_scroll.set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
74     style_scroll.add (style_treeview);
75 
76     style_frame.set_hexpand (true);
77     style_frame.set_vexpand (true);
78     style_frame.add (style_scroll);
79 
80     // Size
81     size_combobox.set_name ("FontSelectorSize");
82     set_sizes();
83     size_combobox.set_active_text( "18" );
84 
85     // Font Variations
86     font_variations.set_vexpand (true);
87     font_variations_scroll.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
88     font_variations_scroll.add (font_variations);
89 
90     // Grid
91     set_name ("FontSelectorGrid");
92     set_row_spacing(4);
93     set_column_spacing(4);
94     // Add extra columns to the "family frame" to change space distribution
95     // by prioritizing font family over styles
96     const int extra = 4;
97     attach (family_frame,  0, 0, 1 + extra, 2);
98     attach (style_frame,   1 + extra, 0, 2, 1);
99     if (with_size) { // Glyph panel does not use size.
100         attach (size_label,    1 + extra, 1, 1, 1);
101         attach (size_combobox, 2 + extra, 1, 1, 1);
102     }
103     if (with_variations) { // Glyphs panel does not use variations.
104         attach (font_variations_scroll, 0, 2, 3 + extra, 1);
105     }
106 
107     // Add signals
108     family_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_family_changed));
109     style_treeview.get_selection()->signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_style_changed));
110     size_combobox.signal_changed().connect(sigc::mem_fun(*this, &FontSelector::on_size_changed));
111     font_variations.connectChanged(sigc::mem_fun(*this, &FontSelector::on_variations_changed));
112 
113     show_all_children();
114 
115     // Initialize font family lists. (May already be done.) Should be done on document change.
116     font_lister->update_font_list(SP_ACTIVE_DESKTOP->getDocument());
117 }
118 
119 void
set_sizes()120 FontSelector::set_sizes ()
121 {
122     size_combobox.remove_all();
123 
124     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
125     int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
126 
127     int sizes[] = {
128         4, 6, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28,
129         32, 36, 40, 48, 56, 64, 72, 144
130     };
131 
132     // Array must be same length as SPCSSUnit in style-internal.h
133     //                    PX  PT  PC  MM  CM   IN  EM  EX     %
134     double ratios[] = {1,  1,  1, 10,  4, 40, 100, 16,  8, 0.16};
135 
136     for (int i : sizes)
137     {
138         double size = i/ratios[unit];
139         size_combobox.append( Glib::ustring::format(size) );
140     }
141 }
142 
143 void
set_fontsize_tooltip()144 FontSelector::set_fontsize_tooltip()
145 {
146     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
147     int unit = prefs->getInt("/options/font/unitType", SP_CSS_UNIT_PT);
148     Glib::ustring tooltip = Glib::ustring::format(_("Font size"), " (", sp_style_get_css_unit_string(unit), ")");
149     size_combobox.set_tooltip_text (tooltip);
150 }
151 
152 // Update GUI.
153 // We keep a private copy of the style list as the font-family in widget is only temporary
154 // until the "Apply" button is set so the style list can be different from that in
155 // FontLister.
156 void
update_font()157 FontSelector::update_font ()
158 {
159     signal_block = true;
160 
161     Inkscape::FontLister *font_lister = Inkscape::FontLister::get_instance();
162     Gtk::TreePath path;
163     Glib::ustring family = font_lister->get_font_family();
164     Glib::ustring style  = font_lister->get_font_style();
165 
166     // Set font family
167     try {
168         path = font_lister->get_row_for_font (family);
169     } catch (...) {
170         std::cerr << "FontSelector::update_font: Couldn't find row for font-family: "
171                   << family << std::endl;
172         path.clear();
173         path.push_back(0);
174     }
175 
176     Gtk::TreePath currentPath;
177     Gtk::TreeViewColumn *currentColumn;
178     family_treeview.get_cursor(currentPath, currentColumn);
179     if (currentPath.empty() || !font_lister->is_path_for_font(currentPath, family)) {
180         family_treeview.set_cursor (path);
181         family_treeview.scroll_to_row (path);
182     }
183 
184     // Get font-lister style list for selected family
185     Gtk::TreeModel::Row row = *(family_treeview.get_model()->get_iter (path));
186     GList *styles;
187     row.get_value(1, styles);
188 
189     // Copy font-lister style list to private list store, searching for match.
190     Gtk::TreeModel::iterator match;
191     FontLister::FontStyleListClass FontStyleList;
192     Glib::RefPtr<Gtk::ListStore> local_style_list_store = Gtk::ListStore::create(FontStyleList);
193     for ( ; styles; styles = styles->next ) {
194         Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
195         (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
196         (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
197         if (style == ((StyleNames*)styles->data)->CssName) {
198             match = treeModelIter;
199         }
200     }
201 
202     // Attach store to tree view and select row.
203     style_treeview.set_model (local_style_list_store);
204     if (match) {
205         style_treeview.get_selection()->select (match);
206     }
207 
208     Glib::ustring fontspec = font_lister->get_fontspec();
209     update_variations(fontspec);
210 
211     signal_block = false;
212 }
213 
214 void
update_size(double size)215 FontSelector::update_size (double size)
216 {
217     signal_block = true;
218 
219     // Set font size
220     std::stringstream ss;
221     ss << size;
222     size_combobox.get_entry()->set_text( ss.str() );
223     font_size = size; // Store value
224     set_fontsize_tooltip();
225 
226     signal_block = false;
227 }
228 
229 
230 // If use_variations is true (default), we get variation values from variations widget otherwise we
231 // get values from CSS widget (we need to be able to keep the two widgets synchronized both ways).
232 Glib::ustring
get_fontspec(bool use_variations)233 FontSelector::get_fontspec(bool use_variations) {
234 
235     // Build new fontspec from GUI settings
236     Glib::ustring family = "Sans";  // Default...family list may not have been constructed.
237     Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected();
238     if (iter) {
239         (*iter).get_value(0, family);
240     }
241 
242     Glib::ustring style = "Normal";
243     iter = style_treeview.get_selection()->get_selected();
244     if (iter) {
245         (*iter).get_value(0, style);
246     }
247 
248     if (family.empty()) {
249         std::cerr << "FontSelector::get_fontspec: empty family!" << std::endl;
250     }
251 
252     if (style.empty()) {
253         std::cerr << "FontSelector::get_fontspec: empty style!" << std::endl;
254     }
255 
256     Glib::ustring fontspec = family + ", ";
257 
258     if (use_variations) {
259         // Clip any font_variation data in 'style' as we'll replace it.
260         auto pos = style.find('@');
261         if (pos != Glib::ustring::npos) {
262             style.erase (pos, style.length()-1);
263         }
264 
265         Glib::ustring variations = font_variations.get_pango_string();
266 
267         if (variations.empty()) {
268             fontspec += style;
269         } else {
270             fontspec += variations;
271         }
272     } else {
273         fontspec += style;
274     }
275 
276     return fontspec;
277 }
278 
279 void
style_cell_data_func(Gtk::CellRenderer * renderer,Gtk::TreeIter const & iter)280 FontSelector::style_cell_data_func (Gtk::CellRenderer *renderer, Gtk::TreeIter const &iter)
281 {
282     Glib::ustring family = "Sans";  // Default...family list may not have been constructed.
283     Gtk::TreeModel::iterator iter_family = family_treeview.get_selection()->get_selected();
284     if (iter_family) {
285         (*iter_family).get_value(0, family);
286     }
287 
288     Glib::ustring style = "Normal";
289     (*iter).get_value(1, style);
290 
291     Glib::ustring style_escaped  = Glib::Markup::escape_text( style );
292     Glib::ustring font_desc = Glib::Markup::escape_text( family + ", " + style );
293     Glib::ustring markup;
294 
295     markup = "<span font='" + font_desc + "'>" + style_escaped + "</span>";
296 
297     // std::cout << "  markup: " << markup << "  (" << name << ")" << std::endl;
298 
299     renderer->set_property("markup", markup);
300 }
301 
302 
303 // Callbacks
304 
305 // Need to update style list
306 void
on_family_changed()307 FontSelector::on_family_changed() {
308 
309     if (signal_block) return;
310     signal_block = true;
311 
312     Glib::RefPtr<Gtk::TreeModel> model;
313     Gtk::TreeModel::iterator iter = family_treeview.get_selection()->get_selected(model);
314 
315     if (!iter) {
316         // This can happen just after the family list is recreated.
317         signal_block = false;
318         return;
319     }
320 
321     Inkscape::FontLister *fontlister = Inkscape::FontLister::get_instance();
322     fontlister->ensureRowStyles(model, iter);
323 
324     Gtk::TreeModel::Row row = *iter;
325 
326     // Get family name
327     Glib::ustring family;
328     row.get_value(0, family);
329 
330     // Get style list (TO DO: Get rid of GList)
331     GList *styles;
332     row.get_value(1, styles);
333 
334     // Find best style match for selected family with current style (e.g. of selected text).
335     Glib::ustring style = fontlister->get_font_style();
336     Glib::ustring best  = fontlister->get_best_style_match (family, style);
337 
338     // Create are own store of styles for selected font-family (the font-family selected
339     // in the dialog may not be the same as stored in the font-lister class until the
340     // "Apply" button is triggered).
341     Gtk::TreeModel::iterator it_best;
342     FontLister::FontStyleListClass FontStyleList;
343     Glib::RefPtr<Gtk::ListStore>  local_style_list_store = Gtk::ListStore::create(FontStyleList);
344 
345     // Build list and find best match.
346     for ( ; styles; styles = styles->next ) {
347         Gtk::TreeModel::iterator treeModelIter = local_style_list_store->append();
348         (*treeModelIter)[FontStyleList.cssStyle] = ((StyleNames *)styles->data)->CssName;
349         (*treeModelIter)[FontStyleList.displayStyle] = ((StyleNames *)styles->data)->DisplayName;
350         if (best == ((StyleNames*)styles->data)->CssName) {
351             it_best = treeModelIter;
352         }
353     }
354 
355     // Attach store to tree view and select row.
356     style_treeview.set_model (local_style_list_store);
357     if (it_best) {
358         style_treeview.get_selection()->select (it_best);
359     }
360 
361     signal_block = false;
362 
363     // Let world know
364     changed_emit();
365 }
366 
367 void
on_style_changed()368 FontSelector::on_style_changed() {
369     if (signal_block) return;
370 
371     // Update variations widget if new style selected from style widget.
372     signal_block = true;
373     Glib::ustring fontspec = get_fontspec( false );
374     update_variations(fontspec);
375     signal_block = false;
376 
377     // Let world know
378     changed_emit();
379 }
380 
381 void
on_size_changed()382 FontSelector::on_size_changed() {
383 
384     if (signal_block) return;
385 
386     double size;
387     Glib::ustring input = size_combobox.get_active_text();
388     try {
389         size = std::stod (input);
390     }
391     catch (std::invalid_argument) {
392         std::cerr << "FontSelector::on_size_changed: Invalid input: " << input << std::endl;
393         size = -1;
394     }
395 
396     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
397     // Arbitrary: Text and Font preview freezes with huge font sizes.
398     int max_size = prefs->getInt("/dialogs/textandfont/maxFontSize", 10000);
399 
400     if (size <= 0) {
401         return;
402     }
403     if (size > max_size)
404         size = max_size;
405 
406     if (fabs(font_size - size) > 0.001) {
407         font_size = size;
408         // Let world know
409         changed_emit();
410     }
411 }
412 
413 void
on_variations_changed()414 FontSelector::on_variations_changed() {
415 
416     if (signal_block) return;
417 
418     // Let world know
419     changed_emit();
420 }
421 
422 void
changed_emit()423 FontSelector::changed_emit() {
424     signal_block = true;
425     signal_changed.emit (get_fontspec());
426     signal_block = false;
427 }
428 
update_variations(const Glib::ustring & fontspec)429 void FontSelector::update_variations(const Glib::ustring& fontspec) {
430     font_variations.update(fontspec);
431 
432     // Check if there are any variations available; if not, don't expand font_variations_scroll
433     bool hasContent = font_variations.variations_present();
434     font_variations_scroll.set_vexpand(hasContent);
435 }
436 
437 } // namespace Widget
438 } // namespace UI
439 } // namespace Inkscape
440 
441 /*
442   Local Variables:
443   mode:c++
444   c-file-style:"stroustrup"
445   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
446   indent-tabs-mode:nil
447   fill-column:99
448   End:
449 */
450 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
451