From 02cfc0208208a610732c936047433594e5985f06 Mon Sep 17 00:00:00 2001 From: Adam Kankovsky Date: Tue, 14 Jan 2025 10:28:32 +0100 Subject: [PATCH] Creating a dbus interface to get local keyboard layouts Adding dbus interface for loading locale keyboards Remove duplicate function and migrate to localization one --- pyanaconda/localization.py | 61 ++++++++++++++++ .../common/structures/keyboard_layout.py | 69 +++++++++++++++++++ .../modules/localization/localization.py | 46 ++++++++++++- .../localization/localization_interface.py | 21 ++++++ pyanaconda/ui/gui/xkl_wrapper.py | 51 +------------- 5 files changed, 199 insertions(+), 49 deletions(-) create mode 100644 pyanaconda/modules/common/structures/keyboard_layout.py diff --git a/pyanaconda/localization.py b/pyanaconda/localization.py index 3e44ebb462a..1f47ae142fc 100644 --- a/pyanaconda/localization.py +++ b/pyanaconda/localization.py @@ -25,7 +25,9 @@ import re from collections import namedtuple +import iso639 import langtable +from xkbregistry import rxkb from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core import constants @@ -36,7 +38,10 @@ log = get_module_logger(__name__) SCRIPTS_SUPPORTED_BY_CONSOLE = {'Latn', 'Cyrl', 'Grek'} +LayoutInfo = namedtuple("LayoutInfo", ["langs", "desc"]) +Xkb_ = lambda x: gettext.translation("xkeyboard-config", fallback=True).gettext(x) +iso_ = lambda x: gettext.translation("iso_639", fallback=True).gettext(x) class LocalizationConfigError(Exception): """Exception class for localization configuration related problems""" @@ -390,6 +395,62 @@ def get_territory_locales(territory): return langtable.list_locales(territoryId=territory) +def _build_layout_infos(): + """Build localized information for keyboard layouts. + + :param rxkb_context: RXKB context (e.g., rxkb.Context()) + :return: Dictionary with layouts and their descriptions + """ + rxkb_context = rxkb.Context() + layout_infos = {} + + for layout in rxkb_context.layouts.values(): + name = layout.name + if layout.variant: + name += f" ({layout.variant})" + + langs = [] + for lang in layout.iso639_codes: + if iso639.find(iso639_2=lang): + langs.append(iso639.to_name(lang)) + + if name not in layout_infos: + layout_infos[name] = LayoutInfo(langs, layout.description) + else: + layout_infos[name].langs.extend(langs) + + return layout_infos + + +def _get_layout_variant_description(layout_variant, layout_infos, with_lang, xlated): + + layout_info = layout_infos[layout_variant] + lang = "" + # translate language and upcase its first letter, translate the + # layout-variant description + if xlated: + if len(layout_info.langs) == 1: + lang = iso_(layout_info.langs[0]) + description = Xkb_(layout_info.desc) + else: + if len(layout_info.langs) == 1: + lang = upcase_first_letter(layout_info.langs[0]) + description = layout_info.desc + + if with_lang and lang: + # ISO language/country names can be things like + # "Occitan (post 1500); Provencal", or + # "Iran, Islamic Republic of", or "Greek, Modern (1453-)" + # or "Catalan; Valencian": let's handle that gracefully + # let's also ignore case, e.g. in French all translated + # language names are lower-case for some reason + checklang = lang.split()[0].strip(",;").lower() + if checklang not in description.lower(): + return "%s (%s)" % (lang, description) + + return description + + def get_locale_keyboards(locale): """Function returning preferred keyboard layouts for the given locale. diff --git a/pyanaconda/modules/common/structures/keyboard_layout.py b/pyanaconda/modules/common/structures/keyboard_layout.py new file mode 100644 index 00000000000..c232e68d2b3 --- /dev/null +++ b/pyanaconda/modules/common/structures/keyboard_layout.py @@ -0,0 +1,69 @@ +# +# DBus structure for keyboard layout in localization module. +# +# Copyright (C) 2025 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. +# +from dasbus.structure import DBusData +from dasbus.typing import List, Str # Pylint: disable=wildcard-import + +__all__ = ["KeyboardLayout"] + + +class KeyboardLayout(DBusData): + """Structure representing a keyboard layout.""" + + def __init__(self): + self._layout_id = "" + self._description = "" + self._langs = [] + + @property + def layout_id(self) -> Str: + """Return the keyboard layout ID.""" + return self._layout_id + + @layout_id.setter + def layout_id(self, value: Str): + self._layout_id = value + + @property + def description(self) -> Str: + """Return the description of the layout.""" + return self._description + + @description.setter + def description(self, value: Str): + self._description = value + + @property + def langs(self) -> List[Str]: + """Return the list of associated languages.""" + return self._langs + + @langs.setter + def langs(self, value: List[Str]): + self._langs = value + + def __eq__(self, other): + """Ensure KeyboardLayout objects are correctly compared.""" + if isinstance(other, KeyboardLayout): + return ( + self.layout_id == other.layout_id + and self.description == other.description + and self.langs == other.langs + ) + return False diff --git a/pyanaconda/modules/localization/localization.py b/pyanaconda/modules/localization/localization.py index 627f39dc2f8..62ee0246323 100644 --- a/pyanaconda/modules/localization/localization.py +++ b/pyanaconda/modules/localization/localization.py @@ -24,6 +24,8 @@ from pyanaconda.core.dbus import DBus from pyanaconda.core.signal import Signal from pyanaconda.localization import ( + _build_layout_infos, + _get_layout_variant_description, get_available_translations, get_common_languages, get_english_name, @@ -34,6 +36,7 @@ from pyanaconda.modules.common.base import KickstartService from pyanaconda.modules.common.constants.services import LOCALIZATION from pyanaconda.modules.common.containers import TaskContainer +from pyanaconda.modules.common.structures.keyboard_layout import KeyboardLayout from pyanaconda.modules.common.structures.language import LanguageData, LocaleData from pyanaconda.modules.localization.installation import ( KeyboardInstallationTask, @@ -50,7 +53,6 @@ log = get_module_logger(__name__) - class LocalizationService(KickstartService): """The Localization service.""" @@ -80,6 +82,8 @@ def __init__(self): self.compositor_selected_layout_changed = Signal() self.compositor_layouts_changed = Signal() + self._layout_infos = _build_layout_infos() + self._localed_wrapper = None def publish(self): @@ -177,6 +181,46 @@ def get_locale_data(self, locale_id): return tdata + 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 + + """ + + return _get_layout_variant_description(layout_variant, self._layout_infos, with_lang, xlated) + + + def get_locale_keyboard_layouts(self, lang): + """Get localized keyboard layouts for a given locale. + + :param lang: locale string (e.g., "cs_CZ.UTF-8") + :return: list of dictionaries with keyboard layout information + """ + language_id = lang.split("_")[0].lower() + english_name = get_english_name(language_id) + + layouts = [] + for name, info in self._layout_infos.items(): + if english_name in info.langs: + if name: + layout = KeyboardLayout() + layout.layout_id = name + layout.description = self.get_layout_variant_description(name, with_lang=True, xlated=True) + layout.langs = info.langs + layouts.append(layout) + + return layouts + @property def language(self): """Return the language.""" diff --git a/pyanaconda/modules/localization/localization_interface.py b/pyanaconda/modules/localization/localization_interface.py index 0b46702583c..8ac51f05434 100644 --- a/pyanaconda/modules/localization/localization_interface.py +++ b/pyanaconda/modules/localization/localization_interface.py @@ -25,6 +25,7 @@ from pyanaconda.modules.common.base import KickstartModuleInterface from pyanaconda.modules.common.constants.services import LOCALIZATION from pyanaconda.modules.common.containers import TaskContainer +from pyanaconda.modules.common.structures.keyboard_layout import KeyboardLayout from pyanaconda.modules.common.structures.language import LanguageData, LocaleData @@ -91,6 +92,26 @@ def GetLocaleData(self, locale_id: Str) -> Structure: locale_data = self.implementation.get_locale_data(locale_id) return LocaleData.to_structure(locale_data) + def GetLocaleKeyboardLayouts(self, lang: Str) -> List[Structure]: + """Get keyboard layouts for the specified language. + + Returns a list of keyboard layouts available for the given language. + Each layout is represented as a `KeyboardLayout` structure. + + Example output: + [ + KeyboardLayout(layout_id="us", description="English (US)", langs=["English"]), + KeyboardLayout(layout_id="cz", description="Czech", langs=["Czech"]), + KeyboardLayout(layout_id="cz (qwerty)", description="Czech (QWERTY)", langs=["Czech"]) + ] + + :param lang: Language code string (e.g., "en_US.UTF-8") + :return: List of `KeyboardLayout` structures + """ + return KeyboardLayout.to_structure_list( + self.implementation.get_locale_keyboard_layouts(lang) + ) + @property def Language(self) -> Str: """The language the system will use.""" diff --git a/pyanaconda/ui/gui/xkl_wrapper.py b/pyanaconda/ui/gui/xkl_wrapper.py index c2261667759..b18018550bb 100644 --- a/pyanaconda/ui/gui/xkl_wrapper.py +++ b/pyanaconda/ui/gui/xkl_wrapper.py @@ -18,22 +18,16 @@ import gettext import threading -from collections import namedtuple -import iso639 from xkbregistry import rxkb from pyanaconda import localization from pyanaconda.core.async_utils import async_action_wait -from pyanaconda.core.string import upcase_first_letter from pyanaconda.keyboard import normalize_layout_variant +from pyanaconda.localization import _build_layout_infos, _get_layout_variant_description from pyanaconda.modules.common.constants.services import LOCALIZATION Xkb_ = lambda x: gettext.translation("xkeyboard-config", fallback=True).gettext(x) -iso_ = lambda x: gettext.translation("iso_639", fallback=True).gettext(x) - -# namedtuple for information about a keyboard layout (its language and description) -LayoutInfo = namedtuple("LayoutInfo", ["langs", "desc"]) class XklWrapper: """ @@ -62,26 +56,11 @@ def __init__(self): self._rxkb = rxkb.Context() self._layout_infos = {} - self._build_layout_infos() + self._layout_infos = _build_layout_infos() self._switch_opt_infos = {} self._build_switch_opt_infos() - def _build_layout_infos(self): - for layout in self._rxkb.layouts.values(): - name = layout.name - if layout.variant: - name += ' (' + layout.variant + ')' - - langs = [] - for lang in layout.iso639_codes: - if iso639.find(iso639_2=lang): - langs.append(iso639.to_name(lang)) - - if name not in self._layout_infos: - self._layout_infos[name] = LayoutInfo(langs, layout.description) - else: - self._layout_infos[name].langs.extend(langs) def _build_switch_opt_infos(self): for group in self._rxkb.option_groups: @@ -147,31 +126,7 @@ def get_layout_variant_description(self, layout_variant, with_lang=True, xlated= """ - layout_info = self._layout_infos[layout_variant] - lang = "" - # translate language and upcase its first letter, translate the - # layout-variant description - if xlated: - if len(layout_info.langs) == 1: - lang = iso_(layout_info.langs[0]) - description = Xkb_(layout_info.desc) - else: - if len(layout_info.langs) == 1: - lang = upcase_first_letter(layout_info.langs[0]) - description = layout_info.desc - - if with_lang and lang: - # ISO language/country names can be things like - # "Occitan (post 1500); Provencal", or - # "Iran, Islamic Republic of", or "Greek, Modern (1453-)" - # or "Catalan; Valencian": let's handle that gracefully - # let's also ignore case, e.g. in French all translated - # language names are lower-case for some reason - checklang = lang.split()[0].strip(",;").lower() - if checklang not in description.lower(): - return "%s (%s)" % (lang, description) - - return description + return _get_layout_variant_description(layout_variant, self._layout_infos, with_lang, xlated) def get_switch_opt_description(self, switch_opt): """