[PATCH] Prevent crashes due to accessing X server from multiple threads

Vratislav Podzimek vpodzime at redhat.com
Thu Jul 17 14:20:57 UTC 2014


Gdk and Xklavier both access the X server, so we have to make sure those calls
happen from a single thread (the main loop one).

Make the join_layout_variant function public now that it is used from a
different module.

Signed-off-by: Vratislav Podzimek <vpodzime at redhat.com>
---
 anaconda                                |   2 -
 pyanaconda/keyboard.py                  | 371 +------------------------------
 pyanaconda/ui/gui/spokes/keyboard.py    |   9 +-
 pyanaconda/ui/gui/spokes/welcome.py     |   3 +-
 pyanaconda/ui/gui/xkl_wrapper.py        | 383 ++++++++++++++++++++++++++++++++
 tests/pyanaconda_tests/keyboard_test.py |   6 +-
 6 files changed, 400 insertions(+), 374 deletions(-)
 create mode 100644 pyanaconda/ui/gui/xkl_wrapper.py

diff --git a/anaconda b/anaconda
index e9a9dfa..05320be 100755
--- a/anaconda
+++ b/anaconda
@@ -318,8 +318,6 @@ def doExtraX11Actions(runres):
 
     startSpiceVDAgent()
 
-    keyboard.background_XklWrapper_initialize()
-
 def setupPythonUpdates():
     from distutils.sysconfig import get_python_lib
     import gi.overrides
diff --git a/pyanaconda/keyboard.py b/pyanaconda/keyboard.py
index 74daab8..6966ef8 100644
--- a/pyanaconda/keyboard.py
+++ b/pyanaconda/keyboard.py
@@ -20,31 +20,19 @@
 #
 
 """
-This module include functions and classes for dealing with multiple layouts in
-Anaconda. It wraps the libxklavier functionality to protect Anaconda from
-dealing with its "nice" API that looks like a Lisp-influenced "good old C" and
-also systemd-localed functionality.
-
-It provides a XklWrapper class with several methods that can be used for listing
-and various modifications of keyboard layouts settings and LocaledWrapper class
-with methods for setting, getting and mutually converting X layouts and VConsole
-keymaps.
+This module provides functions for dealing with keyboard layouts/keymaps in
+Anaconda and the LocaledWrapper class with methods for setting, getting and
+mutually converting X layouts and VConsole keymaps.
 
 """
 
 import os
 import re
 import shutil
-import gettext
-import threading
-
-from collections import namedtuple
 
 from pyanaconda import iutil
-from pyanaconda import flags
 from pyanaconda import safe_dbus
-from pyanaconda.constants import DEFAULT_VC_FONT, DEFAULT_KEYBOARD, THREAD_XKL_WRAPPER_INIT
-from pyanaconda.threads import threadMgr, AnacondaThread
+from pyanaconda.constants import DEFAULT_VC_FONT, DEFAULT_KEYBOARD
 
 from gi.repository import GLib
 
@@ -61,12 +49,6 @@ LAYOUT_VARIANT_RE = re.compile(r'^\s*(\w+)\s*' # layout plus
                                r'(?:(?:\(\s*([-\w]+)\s*\))' # variant in parentheses
                                r'|(?:$))\s*') # or nothing
 
-# namedtuple for information about a keyboard layout (its language and description)
-LayoutInfo = namedtuple("LayoutInfo", ["lang", "desc"])
-
-Xkb_ = lambda x: gettext.ldgettext("xkeyboard-config", x)
-iso_ = lambda x: gettext.ldgettext("iso_639", x)
-
 class KeyboardConfigError(Exception):
     """Exception class for keyboard configuration related problems"""
 
@@ -104,7 +86,7 @@ def parse_layout_variant(layout_variant_str):
     # groups may be (layout, None) if no variant was specified
     return (layout, variant or "")
 
-def _join_layout_variant(layout, variant=""):
+def join_layout_variant(layout, variant=""):
     """
     Join layout and variant to form the commonly used 'layout (variant)'
     or 'layout' (if variant is missing) format.
@@ -134,7 +116,7 @@ def normalize_layout_variant(layout_str):
     """
 
     layout, variant = parse_layout_variant(layout_str)
-    return _join_layout_variant(layout, variant)
+    return join_layout_variant(layout, variant)
 
 def populate_missing_items(keyboard):
     """
@@ -335,345 +317,6 @@ def activate_keyboard(keyboard):
         # write out keyboard configuration for the X session
         write_keyboard_config(keyboard, root="/", convert=False)
 
-def background_XklWrapper_initialize():
-    """
-    Create the XklWrapper singleton instance in a separate thread to save time
-    when it's really needed.
-
-    """
-
-    threadMgr.add(AnacondaThread(name=THREAD_XKL_WRAPPER_INIT, target=XklWrapper.get_instance))
-
-class XklWrapperError(KeyboardConfigError):
-    """Exception class for reporting libxklavier-related problems"""
-
-    pass
-
-class XklWrapper(object):
-    """
-    Class wrapping the libxklavier functionality
-
-    Use this class as a singleton class because it provides read-only data
-    and initialization (that takes quite a lot of time) reads always the
-    same data. It doesn't have sense to make multiple instances
-
-    """
-
-    _instance = None
-    _instance_lock = threading.Lock()
-
-    @staticmethod
-    def get_instance():
-        with XklWrapper._instance_lock:
-            if not XklWrapper._instance:
-                XklWrapper._instance = XklWrapper()
-
-        return XklWrapper._instance
-
-    def __init__(self):
-        from gi.repository import GdkX11, Xkl
-
-        self._xkl = Xkl
-
-        #initialize Xkl-related stuff
-        display = GdkX11.x11_get_default_xdisplay()
-        self._engine = Xkl.Engine.get_instance(display)
-
-        self._rec = Xkl.ConfigRec()
-        if not self._rec.get_from_server(self._engine):
-            raise XklWrapperError("Failed to get configuration from server")
-
-        #X is probably initialized to the 'us' layout without any variant and
-        #since we want to add layouts with variants we need the layouts and
-        #variants lists to have the same length. Add "" padding to variants.
-        #See docstring of the add_layout method for details.
-        diff = len(self._rec.layouts) - len(self._rec.variants)
-        if diff > 0 and flags.can_touch_runtime_system("activate layouts"):
-            self._rec.set_variants(self._rec.variants + (diff * [""]))
-            if not self._rec.activate(self._engine):
-                # failed to activate layouts given e.g. by a kickstart (may be
-                # invalid)
-                lay_var_str = ",".join(map(_join_layout_variant,
-                                           self._rec.layouts,
-                                           self._rec.variants))
-                log.error("Failed to activate layouts: '%s', "
-                          "falling back to default %s", lay_var_str, DEFAULT_KEYBOARD)
-                self._rec.set_layouts([DEFAULT_KEYBOARD])
-                self._rec.set_variants([""])
-
-                if not self._rec.activate(self._engine):
-                    # failed to activate even the default layout, something is
-                    # really wrong
-                    raise XklWrapperError("Failed to initialize layouts")
-
-        #needed also for Gkbd.KeyboardDrawingDialog
-        self.configreg = Xkl.ConfigRegistry.get_instance(self._engine)
-        self.configreg.load(False)
-
-        self._layout_infos = dict()
-        self._switch_opt_infos = dict()
-
-        #this might take quite a long time
-        self.configreg.foreach_language(self._get_language_variants, None)
-        self.configreg.foreach_country(self._get_country_variants, None)
-
-        #'grp' means that we want layout (group) switching options
-        self.configreg.foreach_option('grp', self._get_switch_option, None)
-
-    def _get_lang_variant(self, c_reg, item, subitem, lang):
-        if subitem:
-            name = item.get_name() + " (" + subitem.get_name() + ")"
-            description = subitem.get_description()
-        else:
-            name = item.get_name()
-            description = item.get_description()
-
-        #if this layout has already been added for some other language,
-        #do not add it again (would result in duplicates in our lists)
-        if name not in self._layout_infos:
-            self._layout_infos[name] = LayoutInfo(lang, description)
-
-    def _get_country_variant(self, c_reg, item, subitem, country):
-        if subitem:
-            name = item.get_name() + " (" + subitem.get_name() + ")"
-            description = subitem.get_description()
-        else:
-            name = item.get_name()
-            description = item.get_description()
-
-        # if the layout was not added with any language, add it with a country
-        if name not in self._layout_infos:
-            self._layout_infos[name] = LayoutInfo(country, description)
-
-    def _get_language_variants(self, c_reg, item, user_data=None):
-        lang_name, lang_desc = item.get_name(), item.get_description()
-
-        c_reg.foreach_language_variant(lang_name, self._get_lang_variant, lang_desc)
-
-    def _get_country_variants(self, c_reg, item, user_data=None):
-        country_name, country_desc = item.get_name(), item.get_description()
-
-        c_reg.foreach_country_variant(country_name, self._get_country_variant,
-                                      country_desc)
-
-    def _get_switch_option(self, c_reg, item, user_data=None):
-        """Helper function storing layout switching options in foreach cycle"""
-        desc = item.get_description()
-        name = item.get_name()
-
-        self._switch_opt_infos[name] = desc
-
-    def get_current_layout(self):
-        """
-        Get current activated X layout and variant
-
-        :return: current activated X layout and variant (e.g. "cz (qwerty)")
-
-        """
-        # ported from the widgets/src/LayoutIndicator.c code
-
-        self._engine.start_listen(self._xkl.EngineListenModes.TRACK_KEYBOARD_STATE)
-        state = self._engine.get_current_state()
-        cur_group = state.group
-        num_groups = self._engine.get_num_groups()
-
-        # BUG?: if the last layout in the list is activated and removed,
-        #       state.group may be equal to n_groups
-        if cur_group >= num_groups:
-            cur_group = num_groups - 1
-
-        layout = self._rec.layouts[cur_group]
-        try:
-            variant = self._rec.variants[cur_group]
-        except IndexError:
-            # X server may have forgotten to add the "" variant for its default layout
-            variant = ""
-
-        self._engine.stop_listen(self._xkl.EngineListenModes.TRACK_KEYBOARD_STATE)
-
-        return _join_layout_variant(layout, variant)
-
-    def get_available_layouts(self):
-        """A generator yielding layouts (no need to store them as a bunch)"""
-
-        return self._layout_infos.iterkeys()
-
-    def get_switching_options(self):
-        """Method returning list of available layout switching options"""
-
-        return self._switch_opt_infos.iterkeys()
-
-    def get_layout_variant_description(self, layout_variant, with_lang=True, xlated=True):
-        """
-        Get description of the given layout-variant.
-
-        :param layout_variant: layout-variant specification (e.g. 'cz (qwerty)')
-        :type layout_variant: str
-        :param with_lang: whether to include language of the layout-variant (if defined)
-                          in the description or not
-        :type with_lang: bool
-        :param xlated: whethe to return translated or english version of the description
-        :type xlated: bool
-        :return: description of the layout-variant specification (e.g. 'Czech (qwerty)')
-        :rtype: str
-
-        """
-
-        layout_info = self._layout_infos[layout_variant]
-
-        # translate language and upcase its first letter, translate the
-        # layout-variant description
-        if xlated:
-            lang = iutil.upcase_first_letter(iso_(layout_info.lang).decode("utf-8"))
-            description = Xkb_(layout_info.desc).decode("utf-8")
-        else:
-            lang = iutil.upcase_first_letter(layout_info.lang)
-            description = layout_info.desc
-
-        if with_lang and lang and not description.startswith(lang):
-            return "%s (%s)" % (lang, description)
-        else:
-            return description
-
-    def get_switch_opt_description(self, switch_opt):
-        """
-        Get description of the given layout switching option.
-
-        :param switch_opt: switching option name/ID (e.g. 'grp:alt_shift_toggle')
-        :type switch_opt: str
-        :return: description of the layout switching option (e.g. 'Alt + Shift')
-        :rtype: str
-
-        """
-
-        # translate the description of the switching option
-        return Xkb_(self._switch_opt_infos[switch_opt])
-
-    def activate_default_layout(self):
-        """
-        Activates default layout (the first one in the list of configured
-        layouts).
-
-        """
-
-        self._engine.lock_group(0)
-
-    def is_valid_layout(self, layout):
-        """Return if given layout is valid layout or not"""
-
-        return layout in self._layout_infos
-
-    def add_layout(self, layout):
-        """
-        Method that tries to add a given layout to the current X configuration.
-
-        The X layouts configuration is handled by two lists. A list of layouts
-        and a list of variants. Index-matching items in these lists (as if they
-        were zipped) are used for the construction of real layouts (e.g.
-        'cz (qwerty)').
-
-        :param layout: either 'layout' or 'layout (variant)'
-        :raise XklWrapperError: if the given layout is invalid or cannot be added
-
-        """
-
-        try:
-            #we can get 'layout' or 'layout (variant)'
-            (layout, variant) = parse_layout_variant(layout)
-        except InvalidLayoutVariantSpec as ilverr:
-            raise XklWrapperError("Failed to add layout: %s" % ilverr)
-
-        #do not add the same layout-variant combinanion multiple times
-        if (layout, variant) in zip(self._rec.layouts, self._rec.variants):
-            return
-
-        self._rec.set_layouts(self._rec.layouts + [layout])
-        self._rec.set_variants(self._rec.variants + [variant])
-
-        if not self._rec.activate(self._engine):
-            raise XklWrapperError("Failed to add layout '%s (%s)'" % (layout,
-                                                                      variant))
-
-    def remove_layout(self, layout):
-        """
-        Method that tries to remove a given layout from the current X
-        configuration.
-
-        See also the documentation for the add_layout method.
-
-        :param layout: either 'layout' or 'layout (variant)'
-        :raise XklWrapperError: if the given layout cannot be removed
-
-        """
-
-        #we can get 'layout' or 'layout (variant)'
-        (layout, variant) = parse_layout_variant(layout)
-
-        layouts_variants = zip(self._rec.layouts, self._rec.variants)
-
-        if not (layout, variant) in layouts_variants:
-            msg = "'%s (%s)' not in the list of added layouts" % (layout,
-                                                                  variant)
-            raise XklWrapperError(msg)
-
-        idx = layouts_variants.index((layout, variant))
-        new_layouts = self._rec.layouts[:idx] + self._rec.layouts[(idx + 1):]
-        new_variants = self._rec.variants[:idx] + self._rec.variants[(idx + 1):]
-
-        self._rec.set_layouts(new_layouts)
-        self._rec.set_variants(new_variants)
-
-        if not self._rec.activate(self._engine):
-            raise XklWrapperError("Failed to remove layout '%s (%s)'" % (layout,
-                                                                       variant))
-    def replace_layouts(self, layouts_list):
-        """
-        Method that replaces the layouts defined in the current X configuration
-        with the new ones given.
-
-        :param layouts_list: list of layouts defined as either 'layout' or
-                             'layout (variant)'
-        :raise XklWrapperError: if layouts cannot be replaced with the new ones
-
-        """
-
-        new_layouts = list()
-        new_variants = list()
-
-        for layout_variant in layouts_list:
-            (layout, variant) = parse_layout_variant(layout_variant)
-            new_layouts.append(layout)
-            new_variants.append(variant)
-
-        self._rec.set_layouts(new_layouts)
-        self._rec.set_variants(new_variants)
-
-        if not self._rec.activate(self._engine):
-            msg = "Failed to replace layouts with: %s" % ",".join(layouts_list)
-            raise XklWrapperError(msg)
-
-    def set_switching_options(self, options):
-        """
-        Method that sets options for layout switching. It replaces the old
-        options with the new ones.
-
-        :param options: layout switching options to be set
-        :type options: list or generator
-        :raise XklWrapperError: if the old options cannot be replaced with the
-                                new ones
-
-        """
-
-        #preserve old "non-switching options"
-        new_options = [opt for opt in self._rec.options if "grp:" not in opt]
-        new_options += options
-
-        self._rec.set_options(new_options)
-
-        if not self._rec.activate(self._engine):
-            msg = "Failed to set switching options to: %s" % ",".join(options)
-            raise XklWrapperError(msg)
-
 class LocaledWrapperError(KeyboardConfigError):
     """Exception class for reporting Localed-related problems"""
     pass
@@ -746,7 +389,7 @@ class LocaledWrapper(object):
         variants = variants[:len(layouts)]
 
         # map can be used with multiple lists and works like zipWith (Haskell)
-        return map(_join_layout_variant, layouts, variants)
+        return map(join_layout_variant, layouts, variants)
 
     @property
     def options(self):
diff --git a/pyanaconda/ui/gui/spokes/keyboard.py b/pyanaconda/ui/gui/spokes/keyboard.py
index f76fa84..3c4d351 100644
--- a/pyanaconda/ui/gui/spokes/keyboard.py
+++ b/pyanaconda/ui/gui/spokes/keyboard.py
@@ -26,6 +26,7 @@ from pyanaconda.ui.gui import GUIObject
 from pyanaconda.ui.gui.spokes import NormalSpoke
 from pyanaconda.ui.categories.localization import LocalizationCategory
 from pyanaconda.ui.gui.utils import gtk_call_once, escape_markup, gtk_batch_map, timed_action
+from pyanaconda.ui.gui.xkl_wrapper import XklWrapper
 from pyanaconda import keyboard
 from pyanaconda import flags
 from pyanaconda.i18n import _, N_, CN_
@@ -65,7 +66,7 @@ class AddLayoutDialog(GUIObject):
 
     def __init__(self, *args):
         GUIObject.__init__(self, *args)
-        self._xkl_wrapper = keyboard.XklWrapper.get_instance()
+        self._xkl_wrapper = XklWrapper.get_instance()
         self._chosen_layouts = []
 
     def matches_entry(self, model, itr, user_data=None):
@@ -184,7 +185,7 @@ class ConfigureSwitchingDialog(GUIObject):
 
     def __init__(self, *args):
         GUIObject.__init__(self, *args)
-        self._xkl_wrapper = keyboard.XklWrapper.get_instance()
+        self._xkl_wrapper = XklWrapper.get_instance()
 
         self._switchingOptsStore = self.builder.get_object("switchingOptsStore")
 
@@ -273,7 +274,7 @@ class KeyboardSpoke(NormalSpoke):
         NormalSpoke.__init__(self, *args)
         self._remove_last_attempt = False
         self._confirmed = False
-        self._xkl_wrapper = keyboard.XklWrapper.get_instance()
+        self._xkl_wrapper = XklWrapper.get_instance()
         self._add_dialog = None
         self._ready = False
 
@@ -607,7 +608,7 @@ class KeyboardSpoke(NormalSpoke):
             try:
                 self._addLayout(self._store, layout)
                 valid_layouts += layout
-            except keyboard.XklWrapperError:
+            except XklWrapperError:
                 log.error("Failed to add layout '%s'", layout)
 
         if not valid_layouts:
diff --git a/pyanaconda/ui/gui/spokes/welcome.py b/pyanaconda/ui/gui/spokes/welcome.py
index cab1472..5995bbd 100644
--- a/pyanaconda/ui/gui/spokes/welcome.py
+++ b/pyanaconda/ui/gui/spokes/welcome.py
@@ -27,6 +27,7 @@ import langtable
 from pyanaconda.ui.gui.hubs.summary import SummaryHub
 from pyanaconda.ui.gui.spokes import StandaloneSpoke
 from pyanaconda.ui.gui.utils import setup_gtk_direction, escape_markup
+from pyanaconda.ui.gui.xkl_wrapper import XklWrapper
 from pyanaconda.ui.gui.spokes.lib.lang_locale_handler import LangLocaleHandler
 
 from pyanaconda import localization
@@ -55,7 +56,7 @@ class WelcomeLanguageSpoke(LangLocaleHandler, StandaloneSpoke):
     def __init__(self, *args, **kwargs):
         StandaloneSpoke.__init__(self, *args, **kwargs)
         LangLocaleHandler.__init__(self)
-        self._xklwrapper = keyboard.XklWrapper.get_instance()
+        self._xklwrapper = XklWrapper.get_instance()
         self._origStrings = {}
 
     def apply(self):
diff --git a/pyanaconda/ui/gui/xkl_wrapper.py b/pyanaconda/ui/gui/xkl_wrapper.py
new file mode 100644
index 0000000..1f7855b
--- /dev/null
+++ b/pyanaconda/ui/gui/xkl_wrapper.py
@@ -0,0 +1,383 @@
+#
+# Copyright (C) 2012-2014  Red Hat, Inc.
+#
+# This copyrighted material is made available to anyone wishing to use,
+# modify, copy, or redistribute it subject to the terms and conditions of
+# the GNU General Public License v.2, or (at your option) any later version.
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY expressed or implied, including the implied warranties 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, write to the
+# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
+# source code or documentation are not subject to the GNU General Public
+# License and may only be used or replicated with the express permission of
+# Red Hat, Inc.
+#
+# Red Hat Author(s): Vratislav Podzimek <vpodzime at redhat.com>
+#
+
+"""
+This module include functions and classes for dealing with multiple layouts in
+Anaconda. It wraps the libxklavier functionality to protect Anaconda from
+dealing with its "nice" API that looks like a Lisp-influenced "good old C" and
+also systemd-localed functionality.
+
+It provides a XklWrapper class with several methods that can be used for listing
+and various modifications of keyboard layouts settings.
+
+"""
+
+import threading
+import gettext
+from gi.repository import GdkX11, Xkl
+from collections import namedtuple
+
+from pyanaconda import flags
+from pyanaconda import iutil
+from pyanaconda.constants import DEFAULT_KEYBOARD
+from pyanaconda.keyboard import join_layout_variant, parse_layout_variant, KeyboardConfigError, InvalidLayoutVariantSpec
+from pyanaconda.ui.gui.utils import gtk_action_wait
+
+import logging
+log = logging.getLogger("anaconda")
+
+Xkb_ = lambda x: gettext.ldgettext("xkeyboard-config", x)
+iso_ = lambda x: gettext.ldgettext("iso_639", x)
+
+# namedtuple for information about a keyboard layout (its language and description)
+LayoutInfo = namedtuple("LayoutInfo", ["lang", "desc"])
+
+class XklWrapperError(KeyboardConfigError):
+    """Exception class for reporting libxklavier-related problems"""
+
+    pass
+
+class XklWrapper(object):
+    """
+    Class wrapping the libxklavier functionality
+
+    Use this class as a singleton class because it provides read-only data
+    and initialization (that takes quite a lot of time) reads always the
+    same data. It doesn't have sense to make multiple instances
+
+    """
+
+    _instance = None
+    _instance_lock = threading.Lock()
+
+    @staticmethod
+    def get_instance():
+        with XklWrapper._instance_lock:
+            if not XklWrapper._instance:
+                XklWrapper._instance = XklWrapper()
+
+        return XklWrapper._instance
+
+    def __init__(self):
+        #initialize Xkl-related stuff
+        display = GdkX11.x11_get_default_xdisplay()
+        self._engine = Xkl.Engine.get_instance(display)
+
+        self._rec = Xkl.ConfigRec()
+        if not self._rec.get_from_server(self._engine):
+            raise XklWrapperError("Failed to get configuration from server")
+
+        #X is probably initialized to the 'us' layout without any variant and
+        #since we want to add layouts with variants we need the layouts and
+        #variants lists to have the same length. Add "" padding to variants.
+        #See docstring of the add_layout method for details.
+        diff = len(self._rec.layouts) - len(self._rec.variants)
+        if diff > 0 and flags.can_touch_runtime_system("activate layouts"):
+            self._rec.set_variants(self._rec.variants + (diff * [""]))
+            if not self._rec.activate(self._engine):
+                # failed to activate layouts given e.g. by a kickstart (may be
+                # invalid)
+                lay_var_str = ",".join(map(join_layout_variant,
+                                           self._rec.layouts,
+                                           self._rec.variants))
+                log.error("Failed to activate layouts: '%s', "
+                          "falling back to default %s", lay_var_str, DEFAULT_KEYBOARD)
+                self._rec.set_layouts([DEFAULT_KEYBOARD])
+                self._rec.set_variants([""])
+
+                if not self._rec.activate(self._engine):
+                    # failed to activate even the default layout, something is
+                    # really wrong
+                    raise XklWrapperError("Failed to initialize layouts")
+
+        #needed also for Gkbd.KeyboardDrawingDialog
+        self.configreg = Xkl.ConfigRegistry.get_instance(self._engine)
+        self.configreg.load(False)
+
+        self._layout_infos = dict()
+        self._switch_opt_infos = dict()
+
+        #this might take quite a long time
+        self.configreg.foreach_language(self._get_language_variants, None)
+        self.configreg.foreach_country(self._get_country_variants, None)
+
+        #'grp' means that we want layout (group) switching options
+        self.configreg.foreach_option('grp', self._get_switch_option, None)
+
+    def _get_lang_variant(self, c_reg, item, subitem, lang):
+        if subitem:
+            name = item.get_name() + " (" + subitem.get_name() + ")"
+            description = subitem.get_description()
+        else:
+            name = item.get_name()
+            description = item.get_description()
+
+        #if this layout has already been added for some other language,
+        #do not add it again (would result in duplicates in our lists)
+        if name not in self._layout_infos:
+            self._layout_infos[name] = LayoutInfo(lang, description)
+
+    def _get_country_variant(self, c_reg, item, subitem, country):
+        if subitem:
+            name = item.get_name() + " (" + subitem.get_name() + ")"
+            description = subitem.get_description()
+        else:
+            name = item.get_name()
+            description = item.get_description()
+
+        # if the layout was not added with any language, add it with a country
+        if name not in self._layout_infos:
+            self._layout_infos[name] = LayoutInfo(country, description)
+
+    def _get_language_variants(self, c_reg, item, user_data=None):
+        lang_name, lang_desc = item.get_name(), item.get_description()
+
+        c_reg.foreach_language_variant(lang_name, self._get_lang_variant, lang_desc)
+
+    def _get_country_variants(self, c_reg, item, user_data=None):
+        country_name, country_desc = item.get_name(), item.get_description()
+
+        c_reg.foreach_country_variant(country_name, self._get_country_variant,
+                                      country_desc)
+
+    def _get_switch_option(self, c_reg, item, user_data=None):
+        """Helper function storing layout switching options in foreach cycle"""
+        desc = item.get_description()
+        name = item.get_name()
+
+        self._switch_opt_infos[name] = desc
+
+    def get_current_layout(self):
+        """
+        Get current activated X layout and variant
+
+        :return: current activated X layout and variant (e.g. "cz (qwerty)")
+
+        """
+        # ported from the widgets/src/LayoutIndicator.c code
+
+        self._engine.start_listen(Xkl.EngineListenModes.TRACK_KEYBOARD_STATE)
+        state = self._engine.get_current_state()
+        cur_group = state.group
+        num_groups = self._engine.get_num_groups()
+
+        # BUG?: if the last layout in the list is activated and removed,
+        #       state.group may be equal to n_groups
+        if cur_group >= num_groups:
+            cur_group = num_groups - 1
+
+        layout = self._rec.layouts[cur_group]
+        try:
+            variant = self._rec.variants[cur_group]
+        except IndexError:
+            # X server may have forgotten to add the "" variant for its default layout
+            variant = ""
+
+        self._engine.stop_listen(Xkl.EngineListenModes.TRACK_KEYBOARD_STATE)
+
+        return join_layout_variant(layout, variant)
+
+    def get_available_layouts(self):
+        """A generator yielding layouts (no need to store them as a bunch)"""
+
+        return self._layout_infos.iterkeys()
+
+    def get_switching_options(self):
+        """Method returning list of available layout switching options"""
+
+        return self._switch_opt_infos.iterkeys()
+
+    def get_layout_variant_description(self, layout_variant, with_lang=True, xlated=True):
+        """
+        Get description of the given layout-variant.
+
+        :param layout_variant: layout-variant specification (e.g. 'cz (qwerty)')
+        :type layout_variant: str
+        :param with_lang: whether to include language of the layout-variant (if defined)
+                          in the description or not
+        :type with_lang: bool
+        :param xlated: whethe to return translated or english version of the description
+        :type xlated: bool
+        :return: description of the layout-variant specification (e.g. 'Czech (qwerty)')
+        :rtype: str
+
+        """
+
+        layout_info = self._layout_infos[layout_variant]
+
+        # translate language and upcase its first letter, translate the
+        # layout-variant description
+        if xlated:
+            lang = iutil.upcase_first_letter(iso_(layout_info.lang).decode("utf-8"))
+            description = Xkb_(layout_info.desc).decode("utf-8")
+        else:
+            lang = iutil.upcase_first_letter(layout_info.lang)
+            description = layout_info.desc
+
+        if with_lang and lang and not description.startswith(lang):
+            return "%s (%s)" % (lang, description)
+        else:
+            return description
+
+    def get_switch_opt_description(self, switch_opt):
+        """
+        Get description of the given layout switching option.
+
+        :param switch_opt: switching option name/ID (e.g. 'grp:alt_shift_toggle')
+        :type switch_opt: str
+        :return: description of the layout switching option (e.g. 'Alt + Shift')
+        :rtype: str
+
+        """
+
+        # translate the description of the switching option
+        return Xkb_(self._switch_opt_infos[switch_opt])
+
+    @gtk_action_wait
+    def activate_default_layout(self):
+        """
+        Activates default layout (the first one in the list of configured
+        layouts).
+
+        """
+
+        self._engine.lock_group(0)
+
+    def is_valid_layout(self, layout):
+        """Return if given layout is valid layout or not"""
+
+        return layout in self._layout_infos
+
+    @gtk_action_wait
+    def add_layout(self, layout):
+        """
+        Method that tries to add a given layout to the current X configuration.
+
+        The X layouts configuration is handled by two lists. A list of layouts
+        and a list of variants. Index-matching items in these lists (as if they
+        were zipped) are used for the construction of real layouts (e.g.
+        'cz (qwerty)').
+
+        :param layout: either 'layout' or 'layout (variant)'
+        :raise XklWrapperError: if the given layout is invalid or cannot be added
+
+        """
+
+        try:
+            #we can get 'layout' or 'layout (variant)'
+            (layout, variant) = parse_layout_variant(layout)
+        except InvalidLayoutVariantSpec as ilverr:
+            raise XklWrapperError("Failed to add layout: %s" % ilverr)
+
+        #do not add the same layout-variant combinanion multiple times
+        if (layout, variant) in zip(self._rec.layouts, self._rec.variants):
+            return
+
+        self._rec.set_layouts(self._rec.layouts + [layout])
+        self._rec.set_variants(self._rec.variants + [variant])
+
+        if not self._rec.activate(self._engine):
+            raise XklWrapperError("Failed to add layout '%s (%s)'" % (layout,
+                                                                      variant))
+
+    @gtk_action_wait
+    def remove_layout(self, layout):
+        """
+        Method that tries to remove a given layout from the current X
+        configuration.
+
+        See also the documentation for the add_layout method.
+
+        :param layout: either 'layout' or 'layout (variant)'
+        :raise XklWrapperError: if the given layout cannot be removed
+
+        """
+
+        #we can get 'layout' or 'layout (variant)'
+        (layout, variant) = parse_layout_variant(layout)
+
+        layouts_variants = zip(self._rec.layouts, self._rec.variants)
+
+        if not (layout, variant) in layouts_variants:
+            msg = "'%s (%s)' not in the list of added layouts" % (layout,
+                                                                  variant)
+            raise XklWrapperError(msg)
+
+        idx = layouts_variants.index((layout, variant))
+        new_layouts = self._rec.layouts[:idx] + self._rec.layouts[(idx + 1):]
+        new_variants = self._rec.variants[:idx] + self._rec.variants[(idx + 1):]
+
+        self._rec.set_layouts(new_layouts)
+        self._rec.set_variants(new_variants)
+
+        if not self._rec.activate(self._engine):
+            raise XklWrapperError("Failed to remove layout '%s (%s)'" % (layout,
+                                                                       variant))
+
+    @gtk_action_wait
+    def replace_layouts(self, layouts_list):
+        """
+        Method that replaces the layouts defined in the current X configuration
+        with the new ones given.
+
+        :param layouts_list: list of layouts defined as either 'layout' or
+                             'layout (variant)'
+        :raise XklWrapperError: if layouts cannot be replaced with the new ones
+
+        """
+
+        new_layouts = list()
+        new_variants = list()
+
+        for layout_variant in layouts_list:
+            (layout, variant) = parse_layout_variant(layout_variant)
+            new_layouts.append(layout)
+            new_variants.append(variant)
+
+        self._rec.set_layouts(new_layouts)
+        self._rec.set_variants(new_variants)
+
+        if not self._rec.activate(self._engine):
+            msg = "Failed to replace layouts with: %s" % ",".join(layouts_list)
+            raise XklWrapperError(msg)
+
+    @gtk_action_wait
+    def set_switching_options(self, options):
+        """
+        Method that sets options for layout switching. It replaces the old
+        options with the new ones.
+
+        :param options: layout switching options to be set
+        :type options: list or generator
+        :raise XklWrapperError: if the old options cannot be replaced with the
+                                new ones
+
+        """
+
+        #preserve old "non-switching options"
+        new_options = [opt for opt in self._rec.options if "grp:" not in opt]
+        new_options += options
+
+        self._rec.set_options(new_options)
+
+        if not self._rec.activate(self._engine):
+            msg = "Failed to set switching options to: %s" % ",".join(options)
+            raise XklWrapperError(msg)
+
diff --git a/tests/pyanaconda_tests/keyboard_test.py b/tests/pyanaconda_tests/keyboard_test.py
index 8bad3f6..167ba8b 100644
--- a/tests/pyanaconda_tests/keyboard_test.py
+++ b/tests/pyanaconda_tests/keyboard_test.py
@@ -59,11 +59,11 @@ class ParsingAndJoiningTests(unittest.TestCase):
         """Should correctly join keyboard layout and variant to a string spec."""
 
         # both layout and variant specified
-        self.assertEqual(keyboard._join_layout_variant("cz", "qwerty"),
+        self.assertEqual(keyboard.join_layout_variant("cz", "qwerty"),
                          "cz (qwerty)")
 
         # no variant specified
-        self.assertEqual(keyboard._join_layout_variant("cz"), "cz")
+        self.assertEqual(keyboard.join_layout_variant("cz"), "cz")
 
     def layout_variant_parse_join_test(self):
         """Parsing and joining valid layout and variant spec should have no effect."""
@@ -71,7 +71,7 @@ class ParsingAndJoiningTests(unittest.TestCase):
         specs = ("cz", "cz (qwerty)")
         for spec in specs:
             (layout, variant) = keyboard.parse_layout_variant(spec)
-            self.assertEqual(spec, keyboard._join_layout_variant(layout, variant))
+            self.assertEqual(spec, keyboard.join_layout_variant(layout, variant))
 
     def layout_variant_normalize_test(self):
         """Normalizing layout and variant strings should work as expected."""
-- 
1.9.3



More information about the anaconda-patches mailing list