diff --git a/CMakeLists.txt b/CMakeLists.txt index 5aa1b195..34fed8fe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,32 @@ set(VERSION "0.11.0") set(VERSION_INFO "Release") set(LANGUAGE_SUPPORT_DIRECTORY ${CMAKE_INSTALL_PREFIX}/share/locale) +if (NOT ISO_CODE_639_XML) + find_path(ISOCODES_DIRECTORY NAMES iso_639.xml PATHS ${CMAKE_INSTALL_PREFIX} /usr/share/xml/iso-codes) + if (ISOCODES_DIRECTORY) + set(ISO_CODE_639_XML ${ISOCODES_DIRECTORY}/iso_639.xml) + else () + message(WARNING "File iso_639.xml not found. Please specify it manually using cmake -DISO_CODE_639_XML=/path/to/iso_639.xml") + endif () +else () + if (NOT EXISTS ${ISO_CODE_639_XML}) + message(WARNING "The path to iso_639.xml specified in ISO_CODE_639_XML is not valid.") + endif () +endif () + +if (NOT ISO_CODE_3166_XML) + find_path(ISOCODES_DIRECTORY NAMES iso_3166.xml PATHS ${CMAKE_INSTALL_PREFIX} /usr/share/xml/iso-codes) + if (ISOCODES_DIRECTORY) + set(ISO_CODE_3166_XML ${ISOCODES_DIRECTORY}/iso_3166.xml) + else () + message(WARNING "File iso_3166.xml not found. Please specify it manually using cmake -DISO_CODE_3166_XML=/path/to/iso_3166.xml") + endif () +else () + if (NOT EXISTS ${ISO_CODE_3166_XML}) + message(WARNING "The path to iso_3166.xml specified in ISO_CODE_3166_XML is not valid.") + endif () +endif () + # Packaging filenamesnames. set(ARCHIVE_BASE_NAME ${CMAKE_PROJECT_NAME}-${VERSION}) set(ARCHIVE_FULL_NAME ${ARCHIVE_BASE_NAME}.tar.xz) @@ -58,6 +84,8 @@ find_package(PkgConfig) pkg_check_modules(LIBUNITY QUIET unity>=5.12.0) pkg_check_modules(LIBMESSAGINGMENU QUIET messaging-menu>=12.10.2) +pkg_check_modules(ENCHANT QUIET enchant) + pkg_check_modules(SQLITE311 QUIET sqlite3>=3.11.0) pkg_check_modules(SQLITE312 QUIET sqlite3>=3.12.0) if (SQLITE311_FOUND AND NOT SQLITE312_FOUND) diff --git a/bindings/vapi/enchant.vapi b/bindings/vapi/enchant.vapi new file mode 100644 index 00000000..c4a030f7 --- /dev/null +++ b/bindings/vapi/enchant.vapi @@ -0,0 +1,34 @@ +[CCode (cheader_filename = "enchant.h")] +namespace Enchant { + public delegate void BrokerDescribeFn (string provider_name, string provider_desc, string provider_dll_file); + public delegate void DictDescribeFn (string lang_tag, string provider_name, string provider_desc, string provider_file); + + [Compact] + [CCode (free_function = "enchant_broker_free")] + public class Broker { + [CCode (cname = "enchant_broker_init")] + public Broker (); + + public unowned Dict request_dict (string tag); + public unowned Dict request_pwl_dict (string pwl); + public void free_dict (Dict dict); + public int dict_exists (string tag); + public void set_ordering (string tag, string ordering); + public void describe (BrokerDescribeFn fn); + public void list_dicts (DictDescribeFn fn); + public unowned string get_error (); + } + + [Compact] + public class Dict { + public int check (string word, long len = -1); + public unowned string[] suggest (string word, long len = -1); + public void free_string_list ([CCode (array_length = false)] string[] string_list); + public void add_to_session (string word, long len = -1); + public int is_in_session (string word, long len = -1); + public void store_replacement ( string mis, long mis_len, string cor, long cor_len); + public void add_to_pwl ( string word, long len = -1); + public void describe (DictDescribeFn fn); + public unowned string get_error (); + } +} diff --git a/desktop/org.yorba.geary.gschema.xml b/desktop/org.yorba.geary.gschema.xml index 2c435b3d..46d55985 100644 --- a/desktop/org.yorba.geary.gschema.xml +++ b/desktop/org.yorba.geary.gschema.xml @@ -73,6 +73,18 @@ enable inline spell checking True to spell check while typing. + + + [] + Languages that shall be used in the spell checker + List of the languages to use in the spell checker + + + + [] + Languages that are displayed in the spell checker popover. + List of languages that are always displayed in the popover of the spell checker. + true enable notification sounds diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d0e494c5..ad1b34fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -355,6 +355,7 @@ client/composer/contact-entry-completion.vala client/composer/contact-list-store.vala client/composer/email-entry.vala client/composer/scrollable-overlay.vala +client/composer/spell-check-popover.vala client/composer/webview-edit-fixer.vala client/conversation-list/conversation-list-cell-renderer.vala @@ -495,6 +496,7 @@ pkg_check_modules(DEPS REQUIRED gcr-3>=3.10.1 gobject-introspection-1.0 webkitgtk-3.0>=2.4.0 + enchant>=1.6 ${EXTRA_CLIENT_PKG_CONFIG} ) @@ -505,7 +507,7 @@ set(ENGINE_PACKAGES # webkitgtk-3.0 is listed as a custom VAPI (below) to ensure it's treated as a dependency and # built before compilation set(CLIENT_PACKAGES - gtk+-3.0 libsecret-1 libsoup-2.4 libnotify libcanberra gcr-3 ${EXTRA_CLIENT_PACKAGES} + gtk+-3.0 libsecret-1 libsoup-2.4 libnotify libcanberra gcr-3 enchant ${EXTRA_CLIENT_PACKAGES} ) set(CONSOLE_PACKAGES @@ -523,6 +525,8 @@ set(CFLAGS -D_GSETTINGS_DIR=\"${CMAKE_BINARY_DIR}/gsettings\" -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLANGUAGE_SUPPORT_DIRECTORY=\"${LANGUAGE_SUPPORT_DIRECTORY}\" + -DISO_CODE_639_XML=\"${ISO_CODE_639_XML}\" + -DISO_CODE_3166_XML=\"${ISO_CODE_3166_XML}\" -DGCR_API_SUBJECT_TO_CHANGE -g ) diff --git a/src/client/application/geary-config.vala b/src/client/application/geary-config.vala index e79fe268..faa49434 100644 --- a/src/client/application/geary-config.vala +++ b/src/client/application/geary-config.vala @@ -23,6 +23,8 @@ public class Configuration { public const string STARTUP_NOTIFICATIONS_KEY = "startup-notifications"; public const string ASK_OPEN_ATTACHMENT_KEY = "ask-open-attachment"; public const string COMPOSE_AS_HTML_KEY = "compose-as-html"; + public const string SPELL_CHECK_VISIBLE_LANGUAGES = "spell-check-visible-languages"; + public const string SPELL_CHECK_LANGUAGES = "spell-check-languages"; public Settings settings { get; private set; } public Settings gnome_interface; @@ -77,6 +79,24 @@ public class Configuration { get { return settings.get_boolean(SPELL_CHECK_KEY); } } + public string[] spell_check_languages { + owned get { + return settings.get_strv(SPELL_CHECK_LANGUAGES); + } + set { settings.set_strv(SPELL_CHECK_LANGUAGES, value); } + } + + public string[] spell_check_visible_languages { + owned get { + string[] langs = settings.get_strv(SPELL_CHECK_VISIBLE_LANGUAGES); + if (langs.length == 0) { + langs = International.get_user_preferred_languages(); + } + return langs; + } + set { settings.set_strv(SPELL_CHECK_VISIBLE_LANGUAGES, value); } + } + public bool play_sounds { get { return settings.get_boolean(PLAY_SOUNDS_KEY); } } diff --git a/src/client/composer/composer-toolbar.vala b/src/client/composer/composer-toolbar.vala index 8e36c774..f080e0d4 100644 --- a/src/client/composer/composer-toolbar.vala +++ b/src/client/composer/composer-toolbar.vala @@ -7,6 +7,8 @@ public class ComposerToolbar : PillToolbar { public string label_text { get; set; } + + public Gtk.Button select_dictionary_button; public ComposerToolbar(Gtk.ActionGroup toolbar_action_group, Gtk.Menu menu) { base(toolbar_action_group); @@ -36,6 +38,12 @@ public class ComposerToolbar : PillToolbar { insert.add(create_toolbar_button(null, ComposerWidget.ACTION_REMOVE_FORMAT)); add_start(create_pill_buttons(insert)); + // Select dictionary + insert.clear(); + select_dictionary_button = create_toolbar_button(null, ComposerWidget.ACTION_SELECT_DICTIONARY); + insert.add(select_dictionary_button); + add_start(create_pill_buttons(insert)); + // Menu. insert.clear(); insert.add(create_menu_button(null, menu, ComposerWidget.ACTION_MENU)); diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala index 2308e6b4..510bf9b8 100644 --- a/src/client/composer/composer-widget.vala +++ b/src/client/composer/composer-widget.vala @@ -68,6 +68,7 @@ public class ComposerWidget : Gtk.EventBox { public const string ACTION_SEND = "send"; public const string ACTION_ADD_ATTACHMENT = "add attachment"; public const string ACTION_ADD_ORIGINAL_ATTACHMENTS = "add original attachments"; + public const string ACTION_SELECT_DICTIONARY = "select dictionary"; private const string DRAFT_SAVED_TEXT = _("Saved"); private const string DRAFT_SAVING_TEXT = _("Saving"); @@ -250,7 +251,9 @@ public class ComposerWidget : Gtk.EventBox { private Gtk.MenuItem html_item2; private Gtk.MenuItem extended_item; + private ComposerToolbar composer_toolbar; private Gtk.ActionGroup actions; + private SpellCheckPopover? spell_check_popover = null; private string? hover_url = null; private bool action_flag = false; private bool is_attachment_overlay_visible = false; @@ -405,7 +408,7 @@ public class ComposerWidget : Gtk.EventBox { actions.get_action(ACTION_OUTDENT).icon_name = "format-indent-less-symbolic"; } - ComposerToolbar composer_toolbar = new ComposerToolbar(actions, menu); + composer_toolbar = new ComposerToolbar(actions, menu); Gtk.Alignment toolbar_area = (Gtk.Alignment) builder.get_object("toolbar area"); toolbar_area.add(composer_toolbar); bind_property("toolbar-text", composer_toolbar, "label-text", BindingFlags.SYNC_CREATE); @@ -447,6 +450,7 @@ public class ComposerWidget : Gtk.EventBox { actions.get_action(ACTION_SEND).activate.connect(on_send); actions.get_action(ACTION_ADD_ATTACHMENT).activate.connect(on_add_attachment_button_clicked); actions.get_action(ACTION_ADD_ORIGINAL_ATTACHMENTS).activate.connect(on_pending_attachments_button_clicked); + actions.get_action(ACTION_SELECT_DICTIONARY).activate.connect(on_select_dictionary_clicked); ui = new Gtk.UIManager(); ui.insert_action_group(actions, 0); @@ -578,6 +582,8 @@ public class ComposerWidget : Gtk.EventBox { WebKit.WebSettings s = editor.settings; s.enable_spell_checking = GearyApplication.instance.config.spell_check; + s.spell_checking_languages = string.joinv(",", + GearyApplication.instance.config.spell_check_languages); s.auto_load_images = false; s.enable_scripts = false; s.enable_java_applet = false; @@ -800,8 +806,10 @@ public class ComposerWidget : Gtk.EventBox { set_focus(); // Focus in the GTK widget hierarchy - // Ensure the editor is in correct mode re HTML + // Ensure the editor is in correct mode re HTML and that the spell checker + // is visible only when needed on_compose_as_html(); + on_spell_check_changed(); Util.DOM.bind_event(editor,"a", "click", (Callback) on_link_clicked, this); update_actions(); @@ -2056,6 +2064,7 @@ public class ComposerWidget : Gtk.EventBox { private void on_spell_check_changed() { editor.settings.enable_spell_checking = GearyApplication.instance.config.spell_check; + actions.get_action(ACTION_SELECT_DICTIONARY).visible = editor.settings.enable_spell_checking; } // This overrides the keypress handling for the *widget*; the WebView editor's keypress overrides @@ -2165,7 +2174,18 @@ public class ComposerWidget : Gtk.EventBox { return false; } - + + private void on_select_dictionary_clicked() { + if (spell_check_popover == null) { + spell_check_popover = new SpellCheckPopover(composer_toolbar.select_dictionary_button); + spell_check_popover.selection_changed.connect((active_langs) => { + editor.settings.spell_checking_languages = string.joinv(",", active_langs); + GearyApplication.instance.config.spell_check_languages = active_langs; + }); + } + spell_check_popover.toggle(); + } + private bool on_editor_key_press(Gdk.EventKey event) { // widget's keypress override doesn't receive non-modifier keys when the editor processes // them, regardless if true or false is called; this deals with that issue (specifically diff --git a/src/client/composer/spell-check-popover.vala b/src/client/composer/spell-check-popover.vala new file mode 100644 index 00000000..c289ef13 --- /dev/null +++ b/src/client/composer/spell-check-popover.vala @@ -0,0 +1,309 @@ +/* Copyright 2016 Software Freedom Conservancy Inc. + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +public class SpellCheckPopover { + + /** + * This signal is emitted then the selection of rows changes. + * + * @param active_langs The new set of active dictionaries after the + * selection has changed. + */ + public signal void selection_changed(string[] active_langs); + + private Gtk.Popover? popover = null; + private GLib.GenericSet selected_rows; + private bool is_expanded = false; + private Gtk.ListBox langs_list; + private Gtk.SearchEntry search_box; + private Gtk.ScrolledWindow view; + private Gtk.Box content; + + private enum SpellCheckStatus { + INACTIVE, + ACTIVE + } + + private class SpellCheckLangRow : Gtk.ListBoxRow { + + /** + * This signal is emitted then the user activates the row. + * + * @param lang_code The language code associated to this row (such as en_US). + * @param status true if the associated dictionary should be enabled, false if it should be + * disabled. + */ + public signal void toggled (string lang_code, bool status); + + /** + * @brief Signal when the visibility has changed. + */ + public signal void visibility_changed (); + + private string lang_code; + private string lang_name; + private string country_name; + private bool is_lang_visible; + private Gtk.Image active_image; + private Gtk.Button remove_button; + private SpellCheckStatus lang_active = SpellCheckStatus.INACTIVE; + + public SpellCheckLangRow (string lang_code) { + this.lang_code = lang_code; + Gtk.Box box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 6); + + lang_name = International.language_name_from_locale(lang_code); + country_name = International.country_name_from_locale(lang_code); + + string label_text = lang_name; + if (country_name != null) + label_text += " (" + country_name + ")"; + Gtk.Label label = new Gtk.Label(label_text); + label.set_xalign(0.0f); + label.set_size_request(-1, 24); + + box.pack_start(label, false, false); + + Gtk.IconSize sz = Gtk.IconSize.SMALL_TOOLBAR; + active_image = new Gtk.Image.from_icon_name("object-select-symbolic", sz); + remove_button = new Gtk.Button(); + remove_button.set_relief(Gtk.ReliefStyle.NONE); + box.pack_start(active_image, false, false, 6); + box.pack_start(remove_button, true, true); + remove_button.halign = Gtk.Align.END; // Make the button stay at the right end of the screen + + remove_button.clicked.connect(on_remove_clicked); + + is_lang_visible = false; + foreach (string visible_lang in GearyApplication.instance.config.spell_check_visible_languages) { + if (visible_lang == lang_code) + is_lang_visible = true; + } + + foreach (string active_lang in GearyApplication.instance.config.spell_check_languages) { + if (active_lang == lang_code) + lang_active = SpellCheckStatus.ACTIVE; + } + + update_images(); + add(box); + } + + public bool is_lang_active() { + return lang_active == SpellCheckStatus.ACTIVE; + } + + private void update_images() { + Gtk.IconSize sz = Gtk.IconSize.SMALL_TOOLBAR; + + switch (lang_active) { + case SpellCheckStatus.ACTIVE: + active_image.set_from_icon_name("object-select-symbolic", sz); + break; + case SpellCheckStatus.INACTIVE: + active_image.clear(); + break; + } + + if (is_lang_visible) { + remove_button.set_image(new Gtk.Image.from_icon_name("list-remove-symbolic", sz)); + } + else { + remove_button.set_image(new Gtk.Image.from_icon_name("list-add-symbolic", sz)); + } + } + + private void on_remove_clicked() { + is_lang_visible = ! is_lang_visible; + + update_images(); + + if (!is_lang_visible && lang_active == SpellCheckStatus.ACTIVE) + set_lang_active(SpellCheckStatus.INACTIVE); + + if (is_lang_visible) { + string[] visible_langs = GearyApplication.instance.config.spell_check_visible_languages; + visible_langs += lang_code; + GearyApplication.instance.config.spell_check_visible_languages = visible_langs; + } + else { + string[] visible_langs = {}; + foreach (string lang in GearyApplication.instance.config.spell_check_visible_languages) { + if (lang != lang_code) + visible_langs += lang; + } + GearyApplication.instance.config.spell_check_visible_languages = visible_langs; + } + + visibility_changed(); + } + + public bool match_filter(string filter) { + string filter_down = filter.down(); + return ((lang_name != null ? filter_down in lang_name.down() : false) || + (country_name != null ? filter_down in country_name.down() : false)); + } + + private void set_lang_active(SpellCheckStatus active) { + lang_active = active; + + switch (active) { + case SpellCheckStatus.ACTIVE: + // If the lang is not visible make it visible now + if (!is_lang_visible) { + string[] visible_langs = GearyApplication.instance.config.spell_check_visible_languages; + visible_langs += lang_code; + GearyApplication.instance.config.spell_check_visible_languages = visible_langs; + is_lang_visible = true; + } + break; + case SpellCheckStatus.INACTIVE: + break; + } + + update_images(); + this.toggled(lang_code, active == SpellCheckStatus.ACTIVE); + } + + public void handle_activation(SpellCheckPopover spell_check_popover) { + // Make sure that we do not enable the language when the user is just + // trying to remove it from the list. + if (!visible) + return; + + switch (lang_active) { + case SpellCheckStatus.ACTIVE: + set_lang_active(SpellCheckStatus.INACTIVE); + break; + case SpellCheckStatus.INACTIVE: + set_lang_active(SpellCheckStatus.ACTIVE); + break; + } + } + + public bool is_row_visible(bool is_expanded) { + return is_lang_visible || is_expanded; + } + } + + public SpellCheckPopover(Gtk.Widget button) { + popover = new Gtk.Popover(button); + selected_rows = new GLib.GenericSet(GLib.str_hash, GLib.str_equal); + setup_popover(); + } + + private bool filter_function (Gtk.ListBoxRow row) { + string text = search_box.get_text(); + SpellCheckLangRow r = row as SpellCheckLangRow; + return (r.is_row_visible(is_expanded) && r.match_filter(text)); + } + + private void setup_popover() { + // We populate the popover with the list of languages that the user wants to see + string[] languages = International.get_available_dictionaries(); + + content = new Gtk.Box(Gtk.Orientation.VERTICAL, 6); + search_box = new Gtk.SearchEntry(); + search_box.set_placeholder_text(_("Search for more languages")); + search_box.changed.connect(on_search_box_changed); + search_box.grab_focus.connect(on_search_box_grab_focus); + content.pack_start(search_box, false, true); + + view = new Gtk.ScrolledWindow(null, null); + view.set_shadow_type(Gtk.ShadowType.IN); + view.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); + + langs_list = new Gtk.ListBox(); + langs_list.set_selection_mode(Gtk.SelectionMode.NONE); + foreach (string lang in languages) { + SpellCheckLangRow row = new SpellCheckLangRow(lang); + langs_list.add(row); + + if (row.is_lang_active()) + selected_rows.add(lang); + + row.toggled.connect(this.on_row_toggled); + row.visibility_changed.connect(this.on_visibility_changed); + } + langs_list.row_activated.connect(on_row_activated); + view.add(langs_list); + + content.pack_start(view, true, true); + + langs_list.set_filter_func(this.filter_function); + + view.set_size_request(350, 300); + popover.add(content); + + // Make sure that the search box does not get the focus first. We want it to have it only + // if the user wants to perform an extended search. + content.set_focus_child(view); + content.set_margin_start(6); + content.set_margin_end(6); + content.set_margin_top(6); + content.set_margin_bottom(6); + } + + private void on_row_activated(Gtk.ListBoxRow row) { + SpellCheckLangRow r = row as SpellCheckLangRow; + r.handle_activation(this); + // Make sure that we update the visible languages based on the + // possibly updated is_lang_visible_properties. + langs_list.invalidate_filter(); + } + + private void on_search_box_changed() { + langs_list.invalidate_filter(); + } + + private void on_search_box_grab_focus() { + set_expanded(true); + } + + private void set_expanded(bool expanded) { + is_expanded = expanded; + langs_list.invalidate_filter(); + } + + /* + * Toggle the visibility of the popover, and return the final status. + * + * @return true if the Popover is visible after the call, false otherwise. + */ + public bool toggle() { + if (popover.get_visible()) { + popover.hide(); + } + else { + // Make sure that when the box is shown the list is not expanded anymore. + search_box.set_text(""); + content.set_focus_child(view); + is_expanded = false; + langs_list.invalidate_filter(); + + popover.show_all(); + } + + return popover.get_visible(); + } + + private void on_row_toggled(string lang_code, bool active) { + if (active) + selected_rows.add(lang_code); + else + selected_rows.remove(lang_code); + + // Signal that the selection has changed + string[] active_langs = {}; + selected_rows.foreach((lang) => active_langs += lang); + this.selection_changed(active_langs); + } + + private void on_visibility_changed() { + langs_list.invalidate_filter(); + } + +} diff --git a/src/client/util/util-international.vala b/src/client/util/util-international.vala index d0d90055..47f3ae48 100644 --- a/src/client/util/util-international.vala +++ b/src/client/util/util-international.vala @@ -5,23 +5,216 @@ */ extern const string LANGUAGE_SUPPORT_DIRECTORY; +extern const string ISO_CODE_639_XML; +extern const string ISO_CODE_3166_XML; public const string TRANSLATABLE = "translatable"; namespace International { -public const string SYSTEM_LOCALE = ""; + private GLib.HashTable language_names = null; + private GLib.HashTable country_names = null; -void init(string package_name, string program_path, string locale = SYSTEM_LOCALE) { - Intl.setlocale(LocaleCategory.ALL, locale); - Intl.bindtextdomain(package_name, get_langpack_dir_path(program_path)); - Intl.bind_textdomain_codeset(package_name, "UTF-8"); - Intl.textdomain(package_name); -} + public const string SYSTEM_LOCALE = ""; -// TODO: Geary should be able to use langpacks from the build directory -private string get_langpack_dir_path(string program_path) { - return LANGUAGE_SUPPORT_DIRECTORY; -} + void init(string package_name, string program_path, string locale = SYSTEM_LOCALE) { + Intl.setlocale(LocaleCategory.ALL, locale); + Intl.bindtextdomain(package_name, get_langpack_dir_path(program_path)); + Intl.bind_textdomain_codeset(package_name, "UTF-8"); + Intl.textdomain(package_name); + } + + // TODO: Geary should be able to use langpacks from the build directory + private string get_langpack_dir_path(string program_path) { + return LANGUAGE_SUPPORT_DIRECTORY; + } + + public string[] get_available_dictionaries() { + string[] dictionaries = {}; + + Enchant.Broker broker = new Enchant.Broker(); + broker.list_dicts((lang_tag, provider_name, provider_desc, provider_file) => { + dictionaries += lang_tag; + }); + + // Whenever regional variants of the dictionaries are available use them + // in place of the generic ones, e.g., discard en if en_US, en_GB, ... + // are installed on the system. + GLib.GenericSet regional_dictionaries = + new GLib.GenericSet(GLib.str_hash, GLib.str_equal); + foreach (string dic in dictionaries) { + if ("_" in dic) { + int underscore = dic.index_of_char('_'); + regional_dictionaries.add(dic.substring(0, underscore)); + } + } + + GLib.List filtered_dictionaries = new GLib.List(); + foreach (string dic in dictionaries) { + if ("_" in dic || ! regional_dictionaries.contains(dic)) + filtered_dictionaries.append(dic); + } + + filtered_dictionaries.sort((dic_a, dic_b) => (dic_a < dic_b) ? -1 : 1); + + dictionaries = {}; + foreach (string dic in filtered_dictionaries) { + dictionaries += dic; + } + + return dictionaries; + } + + public string[] get_available_locales() { + string[] locales = {}; + + try { + string? output = null; + GLib.Subprocess p = new GLib.Subprocess.newv({ "locale", "-a" }, + GLib.SubprocessFlags.STDOUT_PIPE); + p.communicate_utf8(null, null, out output, null); + + foreach (string l in output.split("\n")) { + locales += l; + } + } catch (GLib.Error e) { + return locales; + } + + return locales; + } + + /* + * Strip the information about the encoding from the locale. + * + * That is, en_US.UTF-8 is mapped to en_US, while en_GB remains + * unchanged. + */ + public string strip_encoding(string locale) { + int dot = locale.index_of_char('.'); + return locale.substring(0, dot); + } + + public string[] get_user_preferred_languages() { + GLib.GenericSet dicts = new GLib.GenericSet(GLib.str_hash, GLib.str_equal); + foreach (string dict in get_available_dictionaries()) { + dicts.add(dict); + } + + GLib.GenericSet locales = new GLib.GenericSet(GLib.str_hash, GLib.str_equal); + foreach (string locale in get_available_locales()) { + locales.add(strip_encoding(locale)); + } + + string[] output = {}; + unowned string[] language_names = GLib.Intl.get_language_names(); + foreach (string lang in language_names) { + // Check if we have the associated locale and the dictionary installed before actually + // considering this language. + if (lang != "C" && dicts.contains(lang) && locales.contains(lang)) { + output += lang; + } + } + return output; + } + + public string? language_name_from_locale (string locale) { + if (language_names == null) { + language_names = new HashTable(GLib.str_hash, GLib.str_equal); + + unowned Xml.Doc doc = Xml.Parser.parse_file(ISO_CODE_639_XML); + if (doc == null) { + return null; + } + else { + unowned Xml.Node root = doc.get_root_element(); + for (unowned Xml.Node entry = root.children; entry != null; entry = entry.next) { + if (entry.type == Xml.ElementType.ELEMENT_NODE) { + string? iso_639_1 = null; + string? language_name = null; + + for (unowned Xml.Attr a = entry.properties; a != null; a = a.next) { + switch (a.name) { + case "iso_639_1_code": + iso_639_1 = a.children->content; + break; + case "name": + language_name = a.children->content; + break; + default: + break; + } + + if (language_name != null) { + if (iso_639_1 != null) { + language_names.insert(iso_639_1, language_name); + } + } + } + } + } + } + } + + // Look for the name of language matching only the part before the _ + int pos = -1; + if ("_" in locale) { + pos = locale.index_of_char('_'); + } + + // Return a translated version of the language. + string language_name = GLib.dgettext("iso_639", language_names.get(locale.substring(0, pos))); + + return language_name; + } + + public string? country_name_from_locale(string locale) { + if (country_names == null) { + country_names = new HashTable(GLib.str_hash, GLib.str_equal); + + unowned Xml.Doc doc = Xml.Parser.parse_file(ISO_CODE_3166_XML); + + if (doc == null) { + return null; + } + else { + unowned Xml.Node root = doc.get_root_element(); + for (unowned Xml.Node entry = root.children; entry != null; entry = entry.next) { + if (entry.type == Xml.ElementType.ELEMENT_NODE) { + string? iso_3166 = null; + string? country_name = null; + + for (unowned Xml.Attr a = entry.properties; a != null; a = a.next) { + switch (a.name) { + case "alpha_2_code": + iso_3166 = a.children->content; + break; + case "name": + country_name = a.children->content; + break; + default: + break; + } + + if (country_name != null) { + if (iso_3166 != null) { + country_names.insert(iso_3166, country_name); + } + } + } + } + } + } + } + + // Look for the name of language matching only the part before the _ + int pos = -1; + if ("_" in locale) { + pos = locale.index_of_char('_'); + } + + string country_name = GLib.dgettext("iso_3166", country_names.get(locale.substring(pos+1))); + + return country_name; + } } - diff --git a/ui/composer.glade b/ui/composer.glade index 0c952526..87ed4f65 100644 --- a/ui/composer.glade +++ b/ui/composer.glade @@ -238,6 +238,13 @@ edit-copy-symbolic + + + Select spell checking language + Spelling language + accessories-dictionary-symbolic + + True