1/* Copyright 2016 Software Freedom Conservancy Inc. 2 * 3 * This software is licensed under the GNU Lesser General Public License 4 * (version 2.1 or later). See the COPYING file in this distribution. 5 */ 6 7public class SpellCheckPopover { 8 9 /** 10 * This signal is emitted then the selection of rows changes. 11 * 12 * @param active_langs The new set of active dictionaries after the 13 * selection has changed. 14 */ 15 public signal void selection_changed(string[] active_langs); 16 17 private Gtk.Popover? popover = null; 18 private GLib.GenericSet<string> selected_rows; 19 private bool is_expanded = false; 20 private Gtk.ListBox langs_list; 21 private Gtk.SearchEntry search_box; 22 private Gtk.ScrolledWindow view; 23 private Gtk.Box content; 24 private Application.Configuration config; 25 26 private enum SpellCheckStatus { 27 INACTIVE, 28 ACTIVE 29 } 30 31 private class SpellCheckLangRow : Gtk.ListBoxRow { 32 33 public string lang_code { get; private set; } 34 35 private string lang_name; 36 private string country_name; 37 private bool is_lang_visible; 38 private Gtk.Image active_image; 39 private Gtk.Button visibility_button; 40 private SpellCheckStatus lang_active = SpellCheckStatus.INACTIVE; 41 42 /** 43 * Emitted when the language has been enabled or disabled. 44 */ 45 public signal void enabled_changed(bool is_enabled); 46 47 /** 48 * @brief Signal when the visibility has changed. 49 */ 50 public signal void visibility_changed(bool is_visible); 51 52 53 public SpellCheckLangRow(string lang_code, 54 bool is_active, 55 bool is_visible) { 56 this.lang_code = lang_code; 57 this.lang_active = is_active 58 ? SpellCheckStatus.ACTIVE 59 : SpellCheckStatus.INACTIVE; 60 this.is_lang_visible = is_active || is_visible; 61 62 Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); 63 box.margin = 6; 64 box.margin_start = 12; 65 66 lang_name = Util.I18n.language_name_from_locale(lang_code); 67 country_name = Util.I18n.country_name_from_locale(lang_code); 68 69 string label_text = lang_name; 70 Gtk.Label label = new Gtk.Label(label_text); 71 label.tooltip_text = label_text; 72 label.halign = Gtk.Align.START; 73 label.ellipsize = END; 74 label.xalign = 0; 75 76 if (country_name != null) { 77 Gtk.Box label_box = new Gtk.Box(VERTICAL, 3); 78 Gtk.Label country_label = new Gtk.Label(country_name); 79 country_label.tooltip_text = country_name; 80 country_label.halign = Gtk.Align.START; 81 country_label.ellipsize = END; 82 country_label.xalign = 0; 83 country_label.get_style_context().add_class("dim-label"); 84 85 label_box.add(label); 86 label_box.add(country_label); 87 box.pack_start(label_box, false, false); 88 } else { 89 box.pack_start(label, false, false); 90 } 91 92 Gtk.IconSize sz = Gtk.IconSize.SMALL_TOOLBAR; 93 active_image = new Gtk.Image.from_icon_name("object-select-symbolic", sz); 94 this.visibility_button = new Gtk.Button(); 95 this.visibility_button.set_relief(Gtk.ReliefStyle.NONE); 96 box.pack_start(active_image, false, false, 6); 97 box.pack_start(this.visibility_button, true, true); 98 this.visibility_button.halign = Gtk.Align.END; // Make the button stay at the right end of the screen 99 this.visibility_button.valign = CENTER; 100 101 this.visibility_button.clicked.connect(on_visibility_clicked); 102 103 update_images(); 104 add(box); 105 } 106 107 public bool is_lang_active() { 108 return lang_active == SpellCheckStatus.ACTIVE; 109 } 110 111 private void update_images() { 112 Gtk.IconSize sz = Gtk.IconSize.SMALL_TOOLBAR; 113 114 switch (lang_active) { 115 case SpellCheckStatus.ACTIVE: 116 active_image.set_from_icon_name("object-select-symbolic", sz); 117 break; 118 case SpellCheckStatus.INACTIVE: 119 active_image.clear(); 120 break; 121 } 122 123 if (is_lang_visible) { 124 this.visibility_button.set_image(new Gtk.Image.from_icon_name("list-remove-symbolic", sz)); 125 this.visibility_button.set_tooltip_text(_("Remove this language from the preferred list")); 126 } 127 else { 128 this.visibility_button.set_image(new Gtk.Image.from_icon_name("list-add-symbolic", sz)); 129 this.visibility_button.set_tooltip_text(_("Add this language to the preferred list")); 130 } 131 } 132 133 public bool match_filter(string filter) { 134 string filter_down = filter.down(); 135 return ((lang_name != null ? filter_down in lang_name.down() : false) || 136 (country_name != null ? filter_down in country_name.down() : false)); 137 } 138 139 private void set_lang_active(SpellCheckStatus active) { 140 this.lang_active = active; 141 142 switch (active) { 143 case SpellCheckStatus.ACTIVE: 144 // If the lang is not visible make it visible now 145 if (!this.is_lang_visible) { 146 set_lang_visible(true); 147 } 148 break; 149 case SpellCheckStatus.INACTIVE: 150 break; 151 } 152 153 update_images(); 154 this.enabled_changed(active == SpellCheckStatus.ACTIVE); 155 } 156 157 private void set_lang_visible(bool is_visible) { 158 this.is_lang_visible = is_visible; 159 160 update_images(); 161 if (!this.is_lang_visible && 162 this.lang_active == SpellCheckStatus.ACTIVE) { 163 set_lang_active(SpellCheckStatus.INACTIVE); 164 } 165 166 visibility_changed(is_visible); 167 } 168 169 public void handle_activation(SpellCheckPopover spell_check_popover) { 170 // Make sure that we do not enable the language when the user is just 171 // trying to remove it from the list. 172 if (!visible) 173 return; 174 175 switch (lang_active) { 176 case SpellCheckStatus.ACTIVE: 177 set_lang_active(SpellCheckStatus.INACTIVE); 178 break; 179 case SpellCheckStatus.INACTIVE: 180 set_lang_active(SpellCheckStatus.ACTIVE); 181 break; 182 } 183 } 184 185 public bool is_row_visible(bool is_expanded) { 186 return is_lang_visible || is_expanded; 187 } 188 189 private void on_visibility_clicked() { 190 set_lang_visible(!this.is_lang_visible); 191 } 192 193 } 194 195 public SpellCheckPopover(Gtk.MenuButton button, Application.Configuration config) { 196 this.popover = new Gtk.Popover(button); 197 button.popover = this.popover; 198 this.config = config; 199 this.selected_rows = new GLib.GenericSet<string>(GLib.str_hash, GLib.str_equal); 200 setup_popover(); 201 } 202 203 private void header_function(Gtk.ListBoxRow row, Gtk.ListBoxRow? before) { 204 if (before != null) { 205 if (row.get_header() == null) { 206 row.set_header(new Gtk.Separator(HORIZONTAL)); 207 } 208 } 209 } 210 211 private bool filter_function (Gtk.ListBoxRow row) { 212 string text = search_box.get_text(); 213 SpellCheckLangRow r = row as SpellCheckLangRow; 214 return (r.is_row_visible(is_expanded) && r.match_filter(text)); 215 } 216 217 private void setup_popover() { 218 // We populate the popover with the list of languages that the user wants to see 219 string[] languages = Util.I18n.get_available_dictionaries(); 220 string[] enabled_langs = this.config.get_spell_check_languages(); 221 string[] visible_langs = this.config.get_spell_check_visible_languages(); 222 223 content = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); 224 search_box = new Gtk.SearchEntry(); 225 search_box.set_placeholder_text(_("Search for more languages")); 226 search_box.changed.connect(on_search_box_changed); 227 search_box.grab_focus.connect(on_search_box_grab_focus); 228 content.pack_start(search_box, false, true); 229 230 view = new Gtk.ScrolledWindow(null, null); 231 view.set_shadow_type(Gtk.ShadowType.IN); 232 view.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); 233 234 langs_list = new Gtk.ListBox(); 235 langs_list.set_selection_mode(Gtk.SelectionMode.NONE); 236 foreach (string lang in languages) { 237 SpellCheckLangRow row = new SpellCheckLangRow( 238 lang, 239 lang in enabled_langs, 240 lang in visible_langs 241 ); 242 langs_list.add(row); 243 244 if (row.is_lang_active()) 245 selected_rows.add(lang); 246 247 row.enabled_changed.connect(this.on_row_enabled_changed); 248 row.visibility_changed.connect(this.on_row_visibility_changed); 249 } 250 langs_list.row_activated.connect(on_row_activated); 251 view.add(langs_list); 252 253 content.pack_start(view, true, true); 254 255 langs_list.set_filter_func(this.filter_function); 256 langs_list.set_header_func(this.header_function); 257 258 popover.add(content); 259 260 // Make sure that the search box does not get the focus first. We want it to have it only 261 // if the user wants to perform an extended search. 262 content.set_focus_child(view); 263 content.set_margin_start(6); 264 content.set_margin_end(6); 265 content.set_margin_top(6); 266 content.set_margin_bottom(6); 267 268 popover.show.connect(this.on_shown); 269 popover.set_size_request(360, 350); 270 } 271 272 private void on_row_activated(Gtk.ListBoxRow row) { 273 SpellCheckLangRow r = row as SpellCheckLangRow; 274 r.handle_activation(this); 275 // Make sure that we update the visible languages based on the 276 // possibly updated is_lang_visible_properties. 277 langs_list.invalidate_filter(); 278 } 279 280 private void on_search_box_changed() { 281 langs_list.invalidate_filter(); 282 } 283 284 private void on_search_box_grab_focus() { 285 set_expanded(true); 286 } 287 288 private void set_expanded(bool expanded) { 289 is_expanded = expanded; 290 langs_list.invalidate_filter(); 291 } 292 293 private void on_shown() { 294 search_box.set_text(""); 295 content.set_focus_child(view); 296 is_expanded = false; 297 langs_list.invalidate_filter(); 298 299 popover.show_all(); 300 } 301 302 private void on_row_enabled_changed(SpellCheckLangRow row, 303 bool is_active) { 304 string lang = row.lang_code; 305 if (is_active) { 306 selected_rows.add(lang); 307 } else { 308 selected_rows.remove(lang); 309 } 310 311 // Signal that the selection has changed 312 string[] active_langs = {}; 313 selected_rows.foreach((lang) => active_langs += lang); 314 this.selection_changed(active_langs); 315 } 316 317 private void on_row_visibility_changed(SpellCheckLangRow row, 318 bool is_visible) { 319 langs_list.invalidate_filter(); 320 321 string[] visible_langs = this.config.get_spell_check_visible_languages(); 322 string lang = row.lang_code; 323 if (is_visible) { 324 if (!(lang in visible_langs)) { 325 visible_langs += lang; 326 } 327 } else { 328 string[] new_langs = {}; 329 foreach (string lang_code in visible_langs) { 330 if (lang != lang_code) { 331 new_langs += lang_code; 332 } 333 } 334 visible_langs = new_langs; 335 } 336 this.config.set_spell_check_visible_languages(visible_langs); 337 } 338 339} 340