diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f790d52b7..63331fb75 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,7 +15,7 @@ jobs: - name: Install Dependencies run: | apt update - apt install -y meson libgranite-dev libswitchboard-2.0-dev libxml2-dev libgnomekbd-dev libxklavier-dev valac + apt install -y meson libgranite-dev libswitchboard-2.0-dev libxml2-dev libgnomekbd-dev libibus-1.0-dev libxklavier-dev valac - name: Build env: DESTDIR: out diff --git a/README.md b/README.md index ee705f745..4d5f224f7 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ You'll need the following dependencies: * libgnomekbd-dev * libgranite-dev * libgtk-3-dev +* libibus-1.0-dev * libxklavier-dev * libxml2-dev * meson diff --git a/po/POTFILES b/po/POTFILES index de5aa888f..12e86f617 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -1,5 +1,12 @@ src/Plug.vala +src/InputMethod/AddEnginesList.vala +src/InputMethod/Utils.vala src/Dialogs/ConflictDialog.vala +src/Dialogs/InstallEngineDialog.vala +src/Dialogs/ProgressDialog.vala +src/InputMethod/Installer/aptd-client.vala +src/InputMethod/Installer/InstallList.vala +src/InputMethod/Installer/UbuntuInstaller.vala src/Layout/AdvancedSettingsGrid.vala src/Layout/AdvancedSettingsPanel.vala src/Layout/Handler.vala @@ -11,8 +18,12 @@ src/Shortcuts/Settings.vala src/Shortcuts/Shortcut.vala src/Views/AbstractPage.vala src/Views/Behavior.vala +src/Views/InputMethod.vala src/Views/Layout.vala src/Views/Shortcuts.vala +src/Widgets/InputMethod/AddEnginesPopover.vala +src/Widgets/InputMethod/EnginesRow.vala +src/Widgets/InputMethod/LanguagesRow.vala src/Widgets/Layout/AddLayoutPopover.vala src/Widgets/Layout/Display.vala src/Widgets/Shortcuts/CustomTree.vala diff --git a/src/Dialogs/InstallEngineDialog.vala b/src/Dialogs/InstallEngineDialog.vala new file mode 100644 index 000000000..ffba3a82b --- /dev/null +++ b/src/Dialogs/InstallEngineDialog.vala @@ -0,0 +1,140 @@ +/* +* Copyright 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public class Pantheon.Keyboard.InputMethodPage.InstallEngineDialog : Granite.MessageDialog { + private InstallList? engines_filter; + + public InstallEngineDialog (Gtk.Window parent) { + Object ( + primary_text: _("Choose an engine to install"), + secondary_text: _("Select an engine from the list to install and use."), + image_icon: new ThemedIcon ("extension"), + transient_for: parent, + buttons: Gtk.ButtonsType.CANCEL + ); + } + + construct { + var languages_list = new Gtk.ListBox () { + activate_on_single_click = true, + expand = true, + selection_mode = Gtk.SelectionMode.NONE + }; + + foreach (var language in InstallList.get_all ()) { + var lang = new LanguagesRow (language); + languages_list.add (lang); + } + + var back_button = new Gtk.Button.with_label (_("Languages")) { + halign = Gtk.Align.START, + margin = 6 + }; + back_button.get_style_context ().add_class (Granite.STYLE_CLASS_BACK_BUTTON); + + var language_title = new Gtk.Label (""); + + var language_header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6); + language_header.pack_start (back_button); + language_header.set_center_widget (language_title); + + var listbox = new Gtk.ListBox () { + expand = true + }; + listbox.set_filter_func (filter_function); + listbox.set_sort_func (sort_function); + + foreach (var language in InstallList.get_all ()) { + foreach (var engine in language.get_components ()) { + listbox.add (new EnginesRow (engine)); + } + } + + var scrolled = new Gtk.ScrolledWindow (null, null); + scrolled.add (listbox); + + var engine_list_grid = new Gtk.Grid () { + orientation = Gtk.Orientation.VERTICAL + }; + engine_list_grid.get_style_context ().add_class (Gtk.STYLE_CLASS_VIEW); + engine_list_grid.add (language_header); + engine_list_grid.add (new Gtk.Separator (Gtk.Orientation.HORIZONTAL)); + engine_list_grid.add (scrolled); + + var stack = new Gtk.Stack () { + height_request = 200, + width_request = 300, + transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT + }; + stack.add (languages_list); + stack.add (engine_list_grid); + + var frame = new Gtk.Frame (null); + frame.add (stack); + + custom_bin.add (frame); + custom_bin.show_all (); + + var install_button = add_button (_("Install"), Gtk.ResponseType.OK); + install_button.sensitive = false; + install_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + languages_list.row_activated.connect ((row) => { + stack.visible_child = engine_list_grid; + language_title.label = ((LanguagesRow) row).language.get_name (); + engines_filter = ((LanguagesRow) row).language; + listbox.invalidate_filter (); + var adjustment = scrolled.get_vadjustment (); + adjustment.set_value (adjustment.lower); + }); + + back_button.clicked.connect (() => { + stack.visible_child = languages_list; + install_button.sensitive = false; + }); + + listbox.selected_rows_changed.connect (() => { + foreach (var engines_row in listbox.get_children ()) { + ((EnginesRow) engines_row).selected = false; + } + + ((EnginesRow) listbox.get_selected_row ()).selected = true; + install_button.sensitive = true; + }); + + response.connect ((response_id) => { + if (response_id == Gtk.ResponseType.OK) { + string engine_to_install = ((EnginesRow) listbox.get_selected_row ()).engine_name; + UbuntuInstaller.get_default ().install (engine_to_install); + } + }); + } + + [CCode (instance_pos = -1)] + private bool filter_function (Gtk.ListBoxRow row) { + if (InstallList.get_language_from_engine_name (((EnginesRow) row).engine_name) == engines_filter) { + return true; + } + + return false; + } + + [CCode (instance_pos = -1)] + private int sort_function (Gtk.ListBoxRow row1, Gtk.ListBoxRow row2) { + return ((EnginesRow) row1).engine_name.collate (((EnginesRow) row1).engine_name); + } +} diff --git a/src/Dialogs/ProgressDialog.vala b/src/Dialogs/ProgressDialog.vala new file mode 100644 index 000000000..f110acaf1 --- /dev/null +++ b/src/Dialogs/ProgressDialog.vala @@ -0,0 +1,82 @@ +/* +* Copyright 2011-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it +* and/or modify it under the terms of the GNU Lesser General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along +* with this program. If not, see http://www.gnu.org/licenses/. +*/ + +public class Pantheon.Keyboard.InputMethodPage.ProgressDialog : Gtk.Dialog { + public int progress { + set { + if (value >= 100) { + destroy (); + } + + progress_bar.fraction = value / 100.0; + } + } + + private Gtk.ProgressBar progress_bar; + + construct { + var image = new Gtk.Image.from_icon_name ("preferences-desktop-locale", Gtk.IconSize.DIALOG) { + valign = Gtk.Align.START + }; + + var primary_label = new Gtk.Label (null) { + max_width_chars = 50, + wrap = true, + xalign = 0 + }; + primary_label.get_style_context ().add_class (Granite.STYLE_CLASS_PRIMARY_LABEL); + + unowned UbuntuInstaller installer = UbuntuInstaller.get_default (); + switch (installer.transaction_mode) { + case UbuntuInstaller.TransactionMode.INSTALL: + primary_label.label = _("Installing %s").printf (installer.engine_to_address); + break; + case UbuntuInstaller.TransactionMode.REMOVE: + primary_label.label = _("Removing %s").printf (installer.engine_to_address); + break; + } + + progress_bar = new Gtk.ProgressBar () { + hexpand = true, + valign = Gtk.Align.START, + width_request = 300 + }; + + var cancel_button = (Gtk.Button) add_button (_("Cancel"), 0); + + installer.bind_property ("install-cancellable", cancel_button, "sensitive"); + + var grid = new Gtk.Grid () { + column_spacing = 12, + margin = 6, + row_spacing = 6 + }; + grid.attach (image, 0, 0, 1, 2); + grid.attach (primary_label, 1, 0); + grid.attach (progress_bar, 1, 1); + grid.show_all (); + + border_width = 6; + deletable = false; + get_content_area ().add (grid); + + cancel_button.clicked.connect (() => { + installer.cancel_install (); + destroy (); + }); + } +} diff --git a/src/InputMethod/AddEnginesList.vala b/src/InputMethod/AddEnginesList.vala new file mode 100644 index 000000000..c46503207 --- /dev/null +++ b/src/InputMethod/AddEnginesList.vala @@ -0,0 +1,38 @@ +/* +* 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public class Pantheon.Keyboard.InputMethodPage.AddEnginesList : Object { + /* + * Stores strings used to add/remove engines in the code and won't be shown in the UI. + * It consists from "", + * e.g. "mozc-jp" or "libpinyin" + */ + public string engine_id { get; private set; } + + /* + * Stores strings used to show in the UI. + * It consists from " - ", + * e.g. "Japanese - Mozc" or "Chinese - Intelligent Pinyin" + */ + public string engine_full_name { get; private set; } + + public AddEnginesList (IBus.EngineDesc engine) { + engine_id = engine.name; + engine_full_name = "%s - %s".printf (IBus.get_language_name (engine.language), + Utils.gettext_engine_longname (engine)); + } +} diff --git a/src/InputMethod/Installer/InstallList.vala b/src/InputMethod/Installer/InstallList.vala new file mode 100644 index 000000000..275c30205 --- /dev/null +++ b/src/InputMethod/Installer/InstallList.vala @@ -0,0 +1,73 @@ +/* +* 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public enum Pantheon.Keyboard.InputMethodPage.InstallList { + JA, + KO, + ZH; + + public string get_name () { + switch (this) { + case JA: + return _("Japanese"); + case KO: + return _("Korean"); + case ZH: + return _("Chinese"); + default: + assert_not_reached (); + } + } + + public string[] get_components () { + switch (this) { + case JA: + return { "ibus-anthy", "ibus-mozc", "ibus-skk" }; + case KO: + return { "ibus-hangul" }; + case ZH: + return { "ibus-cangjie", "ibus-chewing", "ibus-pinyin" }; + default: + assert_not_reached (); + } + } + + public static InstallList get_language_from_engine_name (string engine_name) { + switch (engine_name) { + case "ibus-anthy": + return JA; + case "ibus-mozc": + return JA; + case "ibus-skk": + return JA; + case "ibus-hangul": + return KO; + case "ibus-cangjie": + return ZH; + case "ibus-chewing": + return ZH; + case "ibus-pinyin": + return ZH; + default: + assert_not_reached (); + } + } + + public static InstallList[] get_all () { + return { JA, KO, ZH }; + } +} diff --git a/src/InputMethod/Installer/UbuntuInstaller.vala b/src/InputMethod/Installer/UbuntuInstaller.vala new file mode 100644 index 000000000..b65aa1fa7 --- /dev/null +++ b/src/InputMethod/Installer/UbuntuInstaller.vala @@ -0,0 +1,142 @@ +/* +* Copyright 2011-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it +* and/or modify it under the terms of the GNU Lesser General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +* Public License for more details. +* +* You should have received a copy of the GNU General Public License along +* with this program. If not, see http://www.gnu.org/licenses/. +*/ + +public class Pantheon.Keyboard.InputMethodPage.UbuntuInstaller : Object { + private AptdProxy aptd; + private AptdTransactionProxy proxy; + + public bool install_cancellable { get; private set; } + public TransactionMode transaction_mode { get; private set; } + public string engine_to_address { get; private set; } + + public signal void install_finished (string langcode); + public signal void install_failed (); + public signal void remove_finished (string langcode); + public signal void progress_changed (int progress); + + public enum TransactionMode { + INSTALL, + REMOVE, + INSTALL_MISSING, + } + + Gee.HashMap transactions; + + private static GLib.Once instance; + public static unowned UbuntuInstaller get_default () { + return instance.once (() => { + return new UbuntuInstaller (); + }); + } + + private UbuntuInstaller () {} + + construct { + transactions = new Gee.HashMap (); + aptd = new AptdProxy (); + + try { + aptd.connect_to_aptd (); + } catch (Error e) { + warning ("Could not connect to APT daemon"); + } + } + + public void install (string engine_name) { + transaction_mode = TransactionMode.INSTALL; + engine_to_address = engine_name; + string[] packages = {}; + packages += engine_to_address; + + foreach (var packet in packages) { + message ("Packet: %s", packet); + } + + aptd.install_packages.begin (packages, (obj, res) => { + try { + var transaction_id = aptd.install_packages.end (res); + transactions.@set (transaction_id, "i-" + engine_name); + run_transaction (transaction_id); + } catch (Error e) { + warning ("Could not queue downloads: %s", e.message); + } + }); + } + + public void cancel_install () { + if (install_cancellable) { + warning ("cancel_install"); + try { + proxy.cancel (); + } catch (Error e) { + warning ("cannot cancel installation:%s", e.message); + } + } + } + + private void run_transaction (string transaction_id) { + proxy = new AptdTransactionProxy (); + proxy.finished.connect (() => { + on_apt_finshed (transaction_id, true); + }); + + proxy.property_changed.connect ((prop, val) => { + if (prop == "Progress") { + progress_changed ((int) val.get_int32 ()); + } + + if (prop == "Cancellable") { + install_cancellable = val.get_boolean (); + } + }); + + try { + proxy.connect_to_aptd (transaction_id); + proxy.simulate (); + + proxy.run (); + } catch (Error e) { + on_apt_finshed (transaction_id, false); + warning ("Could no run transaction: %s", e.message); + } + } + + private void on_apt_finshed (string id, bool success) { + if (!success) { + install_failed (); + transactions.unset (id); + return; + } + + if (!transactions.has_key (id)) { //transaction already removed + return; + } + + var action = transactions.get (id); + var lang = action[2:action.length]; + + message ("ID %s -> %s", id, success ? "success" : "failed"); + + if (action[0:1] == "i") { // install + install_finished (lang); + } else { + remove_finished (lang); + } + + transactions.unset (id); + } +} diff --git a/src/InputMethod/Installer/aptd-client.vala b/src/InputMethod/Installer/aptd-client.vala new file mode 100644 index 000000000..ee5c3f5fe --- /dev/null +++ b/src/InputMethod/Installer/aptd-client.vala @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2012 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * Authored by Pawel Stolowski + */ + +namespace Pantheon.Keyboard.InputMethodPage { + private const string APTD_DBUS_NAME = "org.debian.apt"; + private const string APTD_DBUS_PATH = "/org/debian/apt"; + + /** + * Expose a subset of org.debian.apt interfaces -- only what's needed by applications lens. + */ + [DBus (name = "org.debian.apt")] + public interface AptdService : GLib.Object { + public abstract async string install_packages (string[] packages) throws GLib.Error; + public abstract async string remove_packages (string[] packages) throws GLib.Error; + public abstract async void quit () throws GLib.Error; + } + + [DBus (name = "org.debian.apt.transaction")] + public interface AptdTransactionService : GLib.Object { + public abstract void run () throws GLib.Error; + public abstract void simulate () throws GLib.Error; + public abstract void cancel () throws GLib.Error; + public signal void finished (string exit_state); + public signal void property_changed (string property, Variant val); + } + + public class AptdProxy : GLib.Object { + private AptdService _aptd_service; + + public void connect_to_aptd () throws GLib.Error { + _aptd_service = Bus.get_proxy_sync (BusType.SYSTEM, APTD_DBUS_NAME, APTD_DBUS_PATH); + } + + public async string install_packages (string[] packages) throws GLib.Error { + string res = yield _aptd_service.install_packages (packages); + return res; + } + + public async string remove_packages (string[] packages) throws GLib.Error { + string res = yield _aptd_service.remove_packages (packages); + return res; + } + + public async void quit () throws GLib.Error { + yield _aptd_service.quit (); + } + } + + public class AptdTransactionProxy : GLib.Object { + public signal void finished (string transaction_id); + public signal void property_changed (string property, Variant variant); + + private AptdTransactionService _aptd_service; + + public void connect_to_aptd (string transaction_id) throws GLib.Error { + _aptd_service = Bus.get_proxy_sync (BusType.SYSTEM, APTD_DBUS_NAME, transaction_id); + _aptd_service.finished.connect ((exit_state) => { + debug ("aptd transaction finished: %s\n", exit_state); + finished (transaction_id); + }); + _aptd_service.property_changed.connect ((prop, variant) => { + property_changed (prop, variant); + }); + } + + public void simulate () throws GLib.Error { + _aptd_service.simulate (); + } + + public void run () throws GLib.Error { + _aptd_service.run (); + } + + public void cancel () throws GLib.Error { + _aptd_service.cancel (); + } + } +} diff --git a/src/InputMethod/Utils.vala b/src/InputMethod/Utils.vala new file mode 100644 index 000000000..20691936f --- /dev/null +++ b/src/InputMethod/Utils.vala @@ -0,0 +1,46 @@ +/* +* 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public class Pantheon.Keyboard.InputMethodPage.Utils : Object { + private static string[] _active_engines; + // Stores currently activated engines + public static string[] active_engines { + get { + _active_engines = Pantheon.Keyboard.Plug.ibus_general_settings.get_strv ("preload-engines"); + return _active_engines; + } + set { + Pantheon.Keyboard.Plug.ibus_general_settings.set_strv ("preload-engines", value); + Pantheon.Keyboard.Plug.ibus_general_settings.set_strv ("engines-order", value); + } + } + + // From https://github.com/ibus/ibus/blob/master/ui/gtk2/i18n.py#L47-L54 + public static string gettext_engine_longname (IBus.EngineDesc engine) { + string name = engine.name; + if (name.has_prefix ("xkb:")) { + return dgettext ("xkeyboard-config", engine.longname); + } + + string textdomain = engine.textdomain; + if (textdomain == "") { + return engine.longname; + } + + return dgettext (textdomain, engine.longname); + } +} diff --git a/src/Plug.vala b/src/Plug.vala index 3f0da4213..a0719a951 100644 --- a/src/Plug.vala +++ b/src/Plug.vala @@ -18,6 +18,8 @@ */ public class Pantheon.Keyboard.Plug : Switchboard.Plug { + public static GLib.Settings ibus_general_settings; + private Gtk.Grid grid; private Gtk.Stack stack; @@ -26,6 +28,7 @@ public class Pantheon.Keyboard.Plug : Switchboard.Plug { settings.set ("input/keyboard", "Layout"); settings.set ("input/keyboard/layout", "Layout"); settings.set ("input/keyboard/behavior", "Behavior"); + settings.set ("input/keyboard/inputmethod", "Input Method"); settings.set ("input/keyboard/shortcuts", "Shortcuts"); Object (category: Category.HARDWARE, code_name: "io.elementary.switchboard.keyboard", @@ -35,11 +38,16 @@ public class Pantheon.Keyboard.Plug : Switchboard.Plug { supported_settings: settings); } + static construct { + ibus_general_settings = new GLib.Settings ("org.freedesktop.ibus.general"); + } + public override Gtk.Widget get_widget () { if (grid == null) { stack = new Gtk.Stack (); stack.margin = 12; stack.add_titled (new Keyboard.LayoutPage.Page (), "layout", _("Layout")); + stack.add_titled (new Keyboard.InputMethodPage.Page (), "inputmethod", _("Input Method")); stack.add_titled (new Keyboard.Shortcuts.Page (), "shortcuts", _("Shortcuts")); stack.add_titled (new Keyboard.Behaviour.Page (), "behavior", _("Behavior")); @@ -74,6 +82,9 @@ public class Pantheon.Keyboard.Plug : Switchboard.Plug { case "Behavior": stack.visible_child_name = "behavior"; break; + case "Input Method": + stack.visible_child_name = "inputmethod"; + break; case "Layout": stack.visible_child_name = "layout"; break; @@ -88,6 +99,10 @@ public class Pantheon.Keyboard.Plug : Switchboard.Plug { search_results.set ("%s → %s → %s".printf (display_name, _("Layout"), _("Compose Key")), "Layout"); search_results.set ("%s → %s → %s".printf (display_name, _("Layout"), _("⌘ key behavior")), "Layout"); search_results.set ("%s → %s → %s".printf (display_name, _("Layout"), _("Caps Lock behavior")), "Layout"); + search_results.set ("%s → %s".printf (display_name, _("Input Method")), "Input Method"); + search_results.set ("%s → %s → %s".printf (display_name, _("Input Method"), _("Switch engines")), "Input Method"); + search_results.set ("%s → %s → %s".printf (display_name, _("Input Method"), _("Show candidate window")), "Input Method"); + search_results.set ("%s → %s → %s".printf (display_name, _("Input Method"), _("Embed preedit text in application window")), "Input Method"); search_results.set ("%s → %s".printf (display_name, _("Shortcuts")), "Shortcuts"); search_results.set ("%s → %s".printf (display_name, _("Behavior")), "Behavior"); search_results.set ("%s → %s → %s".printf (display_name, _("Behavior"), _("Repeat Keys")), "Behavior"); @@ -98,6 +113,7 @@ public class Pantheon.Keyboard.Plug : Switchboard.Plug { public Switchboard.Plug get_plug (Module module) { debug ("Activating Keyboard plug"); + IBus.init (); var plug = new Pantheon.Keyboard.Plug (); return plug; } diff --git a/src/Views/InputMethod.vala b/src/Views/InputMethod.vala new file mode 100644 index 000000000..709081953 --- /dev/null +++ b/src/Views/InputMethod.vala @@ -0,0 +1,349 @@ +/* +* 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public class Pantheon.Keyboard.InputMethodPage.Page : Pantheon.Keyboard.AbstractPage { + private IBus.Bus bus; + private GLib.Settings ibus_panel_settings; + // Stores all installed engines +#if IBUS_1_5_19 + private List engines; +#else + private List engines; +#endif + + private Granite.Widgets.AlertView spawn_failed_alert; + private Gtk.ListBox listbox; + private Gtk.MenuButton remove_button; + private AddEnginesPopover add_engines_popover; + private Gtk.Stack stack; + + construct { + bus = new IBus.Bus (); + ibus_panel_settings = new GLib.Settings ("org.freedesktop.ibus.panel"); + + // no_daemon_runnning view shown if IBus Daemon is not running + var no_daemon_runnning_alert = new Granite.Widgets.AlertView ( + _("IBus Daemon is not running"), + _("You need to run IBus Daemon to enable or configure input method engines."), + "dialog-information" + ) { + halign = Gtk.Align.CENTER, + valign = Gtk.Align.CENTER + }; + no_daemon_runnning_alert.get_style_context ().remove_class (Gtk.STYLE_CLASS_VIEW); + no_daemon_runnning_alert.show_action (_("Start IBus Daemon")); + no_daemon_runnning_alert.action_activated.connect (() => { + spawn_ibus_daemon (); + }); + + // spawn_failed view shown if IBus Daemon is not running + spawn_failed_alert = new Granite.Widgets.AlertView ( + _("Failed to start IBus Daemon"), + "", + "dialog-error" + ) { + halign = Gtk.Align.CENTER, + valign = Gtk.Align.CENTER + }; + spawn_failed_alert.get_style_context ().remove_class (Gtk.STYLE_CLASS_VIEW); + + // normal view shown if IBus Daemon is already running + listbox = new Gtk.ListBox (); + + var scroll = new Gtk.ScrolledWindow (null, null) { + hscrollbar_policy = Gtk.PolicyType.NEVER, + expand = true + }; + scroll.add (listbox); + + add_engines_popover = new AddEnginesPopover (); + + var add_button = new Gtk.MenuButton () { + image = new Gtk.Image.from_icon_name ("list-add-symbolic", Gtk.IconSize.BUTTON), + popover = add_engines_popover, + tooltip_text = _("Add…") + }; + + remove_button = new Gtk.MenuButton () { + image = new Gtk.Image.from_icon_name ("list-remove-symbolic", Gtk.IconSize.BUTTON), + tooltip_text = _("Remove") + }; + + var actionbar = new Gtk.ActionBar (); + actionbar.get_style_context ().add_class (Gtk.STYLE_CLASS_INLINE_TOOLBAR); + actionbar.add (add_button); + actionbar.add (remove_button); + + var left_grid = new Gtk.Grid (); + left_grid.attach (scroll, 0, 0); + left_grid.attach (actionbar, 0, 1); + + var display = new Gtk.Frame (null); + display.add (left_grid); + + var keyboard_shortcut_label = new Gtk.Label (_("Switch engines:")) { + halign = Gtk.Align.END + }; + + var keyboard_shortcut_combobox = new Gtk.ComboBoxText () { + halign = Gtk.Align.START + }; + keyboard_shortcut_combobox.append ("alt-space", Granite.accel_to_string ("space")); + keyboard_shortcut_combobox.append ("ctl-space", Granite.accel_to_string ("space")); + keyboard_shortcut_combobox.append ("shift-space", Granite.accel_to_string ("space")); + keyboard_shortcut_combobox.active_id = get_keyboard_shortcut (); + + var show_ibus_panel_label = new Gtk.Label (_("Show candidate window:")) { + halign = Gtk.Align.END + }; + + var show_ibus_panel_combobox = new Gtk.ComboBoxText () { + halign = Gtk.Align.START + }; + show_ibus_panel_combobox.append ("none", _("Do not show")); + show_ibus_panel_combobox.append ("auto-hide", _("Auto hide")); + show_ibus_panel_combobox.append ("always-show", _("Always show")); + + var embed_preedit_text_label = new Gtk.Label (_("Embed preedit text in application window:")) { + halign = Gtk.Align.END + }; + + var embed_preedit_text_switch = new Gtk.Switch () { + halign = Gtk.Align.START + }; + + var entry_test = new Gtk.Entry () { + hexpand = true, + placeholder_text = (_("Type to test your settings")) + }; + + var ibus_button = new Gtk.Button.with_label (_("Advanced Settings…")); + + var action_area = new Gtk.Grid () { + column_spacing = 12, + valign = Gtk.Align.END, + vexpand = true + }; + action_area.add (entry_test); + action_area.add (ibus_button); + + var right_grid = new Gtk.Grid () { + column_spacing = 12, + halign = Gtk.Align.CENTER, + hexpand = true, + margin = 12, + row_spacing = 12 + }; + right_grid.attach (keyboard_shortcut_label, 0, 0); + right_grid.attach (keyboard_shortcut_combobox, 1, 0); + right_grid.attach (show_ibus_panel_label, 0, 1); + right_grid.attach (show_ibus_panel_combobox, 1, 1); + right_grid.attach (embed_preedit_text_label, 0, 2); + right_grid.attach (embed_preedit_text_switch, 1, 2); + + var main_grid = new Gtk.Grid () { + column_spacing = 12, + row_spacing = 12 + }; + main_grid.attach (display, 0, 0, 1, 2); + main_grid.attach (right_grid, 1, 0); + main_grid.attach (action_area, 1, 1); + + stack = new Gtk.Stack (); + stack.add_named (no_daemon_runnning_alert, "no_daemon_runnning_view"); + stack.add_named (spawn_failed_alert, "spawn_failed_view"); + stack.add_named (main_grid, "main_view"); + stack.show_all (); + + add (stack); + + set_visible_view (); + + add_button.clicked.connect (() => { + add_engines_popover.show_all (); + }); + + add_engines_popover.add_engine.connect ((engine) => { + string[] new_engine_list = Utils.active_engines; + new_engine_list += engine; + Utils.active_engines = new_engine_list; + + update_engines_list (); + add_engines_popover.popdown (); + }); + + remove_button.clicked.connect (() => { + int index = listbox.get_selected_row ().get_index (); + + // Convert to GLib.Array once, because Vala does not support "-=" operator + Array removed_lists = new Array (); + foreach (var active_engine in Utils.active_engines) { + removed_lists.append_val (active_engine); + } + + // Remove applicable engine from the list + removed_lists.remove_index (index); + + /* + * Substitute the contents of removed_lists through another string array, + * because array concatenation is not supported for public array variables and parameters + */ + string[] new_engines; + for (int i = 0; i < removed_lists.length; i++) { + new_engines += removed_lists.index (i); + } + + Utils.active_engines = new_engines; + update_engines_list (); + }); + + keyboard_shortcut_combobox.changed.connect (() => { + set_keyboard_shortcut (keyboard_shortcut_combobox.active_id); + }); + + ibus_button.clicked.connect (() => { + try { + var appinfo = GLib.AppInfo.create_from_commandline ("ibus-setup", null, GLib.AppInfoCreateFlags.NONE); + appinfo.launch (null, null); + } catch (Error e) { + critical ("Could not open ibus setup: %s", e.message); + } + }); + + ibus_panel_settings.bind ("show", show_ibus_panel_combobox, "active", SettingsBindFlags.DEFAULT); + Pantheon.Keyboard.Plug.ibus_general_settings.bind ("embed-preedit-text", embed_preedit_text_switch, "active", SettingsBindFlags.DEFAULT); + } + + private string get_keyboard_shortcut () { + // TODO: Support getting multiple shortcut keys like ibus-setup does + string[] keyboard_shortcuts = Pantheon.Keyboard.Plug.ibus_general_settings.get_child ("hotkey").get_strv ("triggers"); + + string keyboard_shortcut = ""; + foreach (var ks in keyboard_shortcuts) { + switch (ks) { + case "space": + keyboard_shortcut = "alt-space"; + break; + case "space": + keyboard_shortcut = "shift-space"; + break; + case "space": + keyboard_shortcut = "ctl-space"; + break; + default: + break; + } + } + + return keyboard_shortcut; + } + + private void set_keyboard_shortcut (string combobox_id) { + // TODO: Support setting multiple shortcut keys like ibus-setup does + string[] keyboard_shortcuts = {}; + + switch (combobox_id) { + case "alt-space": + keyboard_shortcuts += "space"; + break; + case "shift-space": + keyboard_shortcuts += "space"; + break; + default: + keyboard_shortcuts += "space"; + break; + } + + Pantheon.Keyboard.Plug.ibus_general_settings.get_child ("hotkey").set_strv ("triggers", keyboard_shortcuts); + } + + private void update_engines_list () { + engines = bus.list_engines (); + + // Stores names of currently activated engines + string[] engine_full_names = {}; + + listbox.get_children ().foreach ((listbox_child) => { + listbox_child.destroy (); + }); + + // Add the language and the name of activated engines + foreach (var active_engine in Utils.active_engines) { + foreach (var engine in engines) { + if (engine.name == active_engine) { + engine_full_names += "%s - %s".printf (IBus.get_language_name (engine.language), + Utils.gettext_engine_longname (engine)); + } + } + } + + foreach (var engine_full_name in engine_full_names) { + var label = new Gtk.Label (engine_full_name) { + halign = Gtk.Align.START, + margin = 6 + }; + + var listboxrow = new Gtk.ListBoxRow (); + listboxrow.add (label); + + listbox.add (listboxrow); + } + + listbox.show_all (); + listbox.select_row (listbox.get_row_at_index (0)); + + // Update the sensitivity of buttons depends on whether there are active engines + remove_button.sensitive = listbox.get_row_at_index (0) != null; + } + + private void spawn_ibus_daemon () { + bool is_spawn_succeeded = false; + try { + is_spawn_succeeded = Process.spawn_sync ("/", { "ibus-daemon", "-drx" }, Environ.get (), SpawnFlags.SEARCH_PATH, null); + } catch (GLib.SpawnError e) { + warning (e.message); + set_visible_view (e.message); + return; + } + + uint timeout_start_daemon = Timeout.add (500, () => { + set_visible_view (); + return Gdk.EVENT_PROPAGATE; + }); + timeout_start_daemon = 0; + } + + private void set_visible_view (string error_message = "") { + if (error_message != "") { + stack.visible_child_name = "spawn_failed_view"; + spawn_failed_alert.description = error_message; + } else if (bus.is_connected ()) { + stack.visible_child_name = "main_view"; + update_engines_list (); + add_engines_popover.update_engines_list (); + } else { + stack.visible_child_name = "no_daemon_runnning_view"; + } + } + + public override void reset () { + set_keyboard_shortcut ("ctrl-space"); + ibus_panel_settings.reset ("show"); + ibus_panel_settings.reset ("show-icon-on-systray"); + Pantheon.Keyboard.Plug.ibus_general_settings.reset ("embed-preedit-text"); + } +} diff --git a/src/Views/Layout.vala b/src/Views/Layout.vala index 21fd3f15b..579a2bb5f 100644 --- a/src/Views/Layout.vala +++ b/src/Views/Layout.vala @@ -123,18 +123,10 @@ namespace Pantheon.Keyboard.LayoutPage { advanced_settings = new AdvancedSettings (panels); var entry_test = new Gtk.Entry (); - entry_test.hexpand = true; + entry_test.valign = Gtk.Align.END; + entry_test.expand = true; entry_test.placeholder_text = (_("Type to test your layout")); - var ibus_button = new Gtk.Button.with_label (_("Input Method Settings…")); - - var action_area = new Gtk.Grid (); - action_area.column_spacing = 12; - action_area.valign = Gtk.Align.END; - action_area.vexpand = true; - action_area.add (entry_test); - action_area.add (ibus_button); - attach (display, 0, 0, 1, 9); attach (switch_layout_label, 1, 0, 1, 1); attach (switch_layout_combo, 2, 0, 1, 1); @@ -176,7 +168,7 @@ namespace Pantheon.Keyboard.LayoutPage { attach (num_lock_indicator_switch, 2, 7); } - attach (action_area, 1, 8, 2); + attach (entry_test, 1, 8, 2); // Cannot be just called from the constructor because the stack switcher // shows every child after the constructor has been called @@ -188,15 +180,6 @@ namespace Pantheon.Keyboard.LayoutPage { show_panel_for_active_layout (); }); - ibus_button.clicked.connect (() => { - try { - var appinfo = GLib.AppInfo.create_from_commandline ("ibus-setup", null, GLib.AppInfoCreateFlags.NONE); - appinfo.launch (null, null); - } catch (Error e) { - critical ("Could not open ibus setup: %s", e.message); - } - }); - var gala_behavior_settings = new GLib.Settings ("org.pantheon.desktop.gala.behavior"); var overlay_string = gala_behavior_settings.get_string ("overlay-action"); diff --git a/src/Widgets/InputMethod/AddEnginesPopover.vala b/src/Widgets/InputMethod/AddEnginesPopover.vala new file mode 100644 index 000000000..46e005d18 --- /dev/null +++ b/src/Widgets/InputMethod/AddEnginesPopover.vala @@ -0,0 +1,155 @@ +/* +* 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public class Pantheon.Keyboard.InputMethodPage.AddEnginesPopover : Gtk.Popover { + public signal void add_engine (string new_engine); + +#if IBUS_1_5_19 + private List engines; +#else + private List engines; +#endif + + private Gtk.SearchEntry search_entry; + private GLib.ListStore liststore; + private Gtk.ListBox listbox; + + construct { + search_entry = new Gtk.SearchEntry () { + margin = 12 + }; + + ///TRANSLATORS: This text appears in a search entry and tell users to type some search word + ///to look for a input method engine they want to add. + ///It does not mean search engines in web browsers. + search_entry.placeholder_text = _("Search engine"); + + liststore = new GLib.ListStore (Type.OBJECT); + + listbox = new Gtk.ListBox (); + + var scrolled = new Gtk.ScrolledWindow (null, null) { + expand = true, + height_request = 300, + width_request = 500 + }; + scrolled.add (listbox); + + var install_button = new Gtk.Button.with_label (_("Install Unlisted Engines…")); + + var cancel_button = new Gtk.Button.with_label (_("Cancel")); + + var add_button = new Gtk.Button.with_label (_("Add Engine")); + add_button.get_style_context ().add_class (Gtk.STYLE_CLASS_SUGGESTED_ACTION); + + var button_box = new Gtk.ButtonBox (Gtk.Orientation.HORIZONTAL) { + layout_style = Gtk.ButtonBoxStyle.END, + margin = 12, + spacing = 6 + }; + button_box.add (install_button); + button_box.add (cancel_button); + button_box.add (add_button); + button_box.set_child_secondary (install_button, true); + + var grid = new Gtk.Grid (); + grid.attach (search_entry, 0, 0); + grid.attach (scrolled, 0, 1); + grid.attach (new Gtk.Separator (Gtk.Orientation.HORIZONTAL), 0, 2); + grid.attach (button_box, 0, 3); + + add (grid); + + listbox.button_press_event.connect ((event) => { + if (event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS) { + trigger_add_engine (); + return false; + } + + return false; + }); + + listbox.set_filter_func ((list_box_row) => { + var item = (AddEnginesList) liststore.get_item (list_box_row.get_index ()); + return search_entry.text.down () in item.engine_full_name.down (); + }); + + search_entry.search_changed.connect (() => { + listbox.invalidate_filter (); + }); + + install_button.clicked.connect (() => { + popdown (); + + var install_dialog = new InstallEngineDialog ((Gtk.Window) get_toplevel ()); + install_dialog.run (); + install_dialog.destroy (); + }); + + cancel_button.clicked.connect (() => { + popdown (); + }); + + add_button.clicked.connect (() => { + trigger_add_engine (); + }); + } + + private void trigger_add_engine () { + int index = listbox.get_selected_row ().get_index (); + + // If the engine trying to add is already active, do not add it + foreach (var active_engine in Utils.active_engines) { + if (active_engine == (((AddEnginesList) liststore.get_item (index)).engine_id)) { + popdown (); + return; + } + } + + add_engine (((AddEnginesList) liststore.get_item (index)).engine_id); + } + + public void update_engines_list () { + engines = new IBus.Bus ().list_engines (); + liststore.remove_all (); + + foreach (var engine in engines) { + liststore.append (new AddEnginesList (engine)); + } + + liststore.sort ((a, b) => { + return ((AddEnginesList) a).engine_full_name.collate (((AddEnginesList) b).engine_full_name); + }); + + for (int i = 0; i < liststore.get_n_items (); i++) { + var label = new Gtk.Label (((AddEnginesList) liststore.get_item (i)).engine_full_name) { + halign = Gtk.Align.START, + margin = 6, + margin_end = 12, + margin_start = 12 + }; + + var listboxrow = new Gtk.ListBoxRow (); + listboxrow.add (label); + + listbox.add (listboxrow); + } + + listbox.select_row (listbox.get_row_at_index (0)); + search_entry.grab_focus (); + } +} diff --git a/src/Widgets/InputMethod/EnginesRow.vala b/src/Widgets/InputMethod/EnginesRow.vala new file mode 100644 index 000000000..e336af4fe --- /dev/null +++ b/src/Widgets/InputMethod/EnginesRow.vala @@ -0,0 +1,54 @@ +/* +* 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public class Pantheon.Keyboard.InputMethodPage.EnginesRow : Gtk.ListBoxRow { + public bool selected { get; set; } + public string engine_name { get; construct; } + + public EnginesRow (string engine_name) { + Object ( + engine_name: engine_name + ); + } + + construct { + var label = new Gtk.Label (engine_name) { + halign = Gtk.Align.START, + hexpand = true + }; + + var selection_icon = new Gtk.Image.from_icon_name ("object-select-symbolic", Gtk.IconSize.MENU) { + no_show_all = true, + visible = false + }; + + var grid = new Gtk.Grid () { + column_spacing = 6, + margin = 3, + margin_start = 6, + margin_end = 6 + }; + grid.add (label); + grid.add (selection_icon); + + add (grid); + + notify["selected"].connect (() => { + selection_icon.visible = selected; + }); + } +} diff --git a/src/Widgets/InputMethod/LanguagesRow.vala b/src/Widgets/InputMethod/LanguagesRow.vala new file mode 100644 index 000000000..dc064ae5c --- /dev/null +++ b/src/Widgets/InputMethod/LanguagesRow.vala @@ -0,0 +1,43 @@ +/* +* 2019-2020 elementary, Inc. (https://elementary.io) +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +*/ + +public class Pantheon.Keyboard.InputMethodPage.LanguagesRow : Gtk.ListBoxRow { + public InstallList language { get; construct; } + + public LanguagesRow (InstallList language) { + Object (language: language); + } + + construct { + var label = new Gtk.Label (language.get_name ()) { + halign = Gtk.Align.START, + hexpand = true + }; + + var caret = new Gtk.Image.from_icon_name ("pan-end-symbolic", Gtk.IconSize.MENU); + + var grid = new Gtk.Grid () { + margin = 3, + margin_start = 6, + margin_end = 6 + }; + grid.add (label); + grid.add (caret); + + add (grid); + } +} diff --git a/src/meson.build b/src/meson.build index ccd3538aa..b370153cf 100644 --- a/src/meson.build +++ b/src/meson.build @@ -16,8 +16,12 @@ plug_files = files( 'Widgets/Shortcuts/CustomTree.vala', 'Widgets/Layout/Display.vala', 'Widgets/Layout/AddLayoutPopover.vala', + 'Widgets/InputMethod/LanguagesRow.vala', + 'Widgets/InputMethod/EnginesRow.vala', + 'Widgets/InputMethod/AddEnginesPopover.vala', 'Views/Shortcuts.vala', 'Views/Layout.vala', + 'Views/InputMethod.vala', 'Views/Behavior.vala', 'Views/AbstractPage.vala', 'Shortcuts/Shortcut.vala', @@ -29,7 +33,14 @@ plug_files = files( 'Layout/Handler.vala', 'Layout/AdvancedSettingsPanel.vala', 'Layout/AdvancedSettingsGrid.vala', - 'Dialogs/ConflictDialog.vala' + 'InputMethod/Utils.vala', + 'InputMethod/AddEnginesList.vala', + 'InputMethod/Installer/UbuntuInstaller.vala', + 'InputMethod/Installer/InstallList.vala', + 'InputMethod/Installer/aptd-client.vala', + 'Dialogs/ProgressDialog.vala', + 'Dialogs/InstallEngineDialog.vala', + 'Dialogs/ConflictDialog.vala', ) switchboard_dep = dependency('switchboard-2.0') @@ -37,6 +48,11 @@ switchboard_plugsdir = switchboard_dep.get_pkgconfig_variable('plugsdir', define gnome_keyboard_ui_dep = meson.get_compiler('c').find_library('gnomekbdui') +ibus_dep = dependency('ibus-1.0') +if(ibus_dep.version().version_compare('>=1.5.19')) + add_project_arguments(['--define', 'IBUS_1_5_19'], language: 'vala') +endif + shared_module( meson.project_name(), plug_files, @@ -50,6 +66,7 @@ shared_module( dependency('libxml-2.0'), dependency('libgnomekbd'), gnome_keyboard_ui_dep, + ibus_dep, switchboard_dep ], install: true,