Skip to content

Commit

Permalink
Merge pull request #6159 from KKoukiou/keyboard-layout-dbus-backport-f42
Browse files Browse the repository at this point in the history
Backport PR #6093 to Fedora 42
  • Loading branch information
KKoukiou authored Feb 18, 2025
2 parents 37f3196 + d4258ea commit b263759
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 62 deletions.
4 changes: 2 additions & 2 deletions anaconda.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ Requires: python3-meh >= %{mehver}
%if 0%{?rhel} < 10 || 0%{?fedora}
Requires: libreport-anaconda >= %{libreportanacondaver}
%endif
Requires: python3-iso639
Requires: python3-libselinux
Requires: python3-rpm >= %{rpmver}
Requires: python3-pyparted >= %{pypartedver}
Expand All @@ -111,6 +112,7 @@ Requires: python3-pwquality
Requires: python3-systemd
Requires: python3-productmd
Requires: python3-dasbus >= %{dasbusver}
Requires: python3-xkbregistry
Requires: flatpak-libs
%if %{defined rhel} && %{undefined centos}
Requires: subscription-manager >= %{subscriptionmanagerver}
Expand Down Expand Up @@ -290,9 +292,7 @@ ensure all Anaconda capabilities are supported in the resulting image.
Summary: Graphical user interface for the Anaconda installer
Requires: anaconda-core = %{version}-%{release}
Requires: anaconda-widgets = %{version}-%{release}
Requires: python3-iso639
Requires: python3-meh-gui >= %{mehver}
Requires: python3-xkbregistry
Requires: adwaita-icon-theme
Requires: tecla
Requires: nm-connection-editor
Expand Down
76 changes: 76 additions & 0 deletions pyanaconda/localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"""
Expand Down Expand Up @@ -390,6 +395,77 @@ 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):
"""
Get description of the given layout-variant.
:param layout_variant: layout-variant specification (e.g. 'cz (qwerty)')
:type layout_variant: str
:param layout_infos: Dictionary containing layout metadata
:type layout_infos: dict
: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 = 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.
Expand Down
69 changes: 69 additions & 0 deletions pyanaconda/modules/common/structures/keyboard_layout.py
Original file line number Diff line number Diff line change
@@ -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
55 changes: 54 additions & 1 deletion pyanaconda/modules/localization/localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,20 @@
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,
get_language_id,
get_language_locales,
get_locale_keyboards,
get_native_name,
)
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,
Expand All @@ -50,7 +54,6 @@

log = get_module_logger(__name__)


class LocalizationService(KickstartService):
"""The Localization service."""

Expand Down Expand Up @@ -80,6 +83,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
self._localed_compositor_wrapper = None

Expand Down Expand Up @@ -178,6 +183,54 @@ def get_locale_data(self, locale_id):

return tdata

def get_layout_variant_description(self, layout_variant, with_lang=True, xlated=True):
"""
Return a description of the given layout-variant.
:param layout_variant: Layout-variant identifier (e.g., 'cz (qwerty)')
:param with_lang: Include the language in the description if available
:param xlated: Return a translated version of the description if True
:return: Formatted layout description
"""

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 = get_language_id(lang)

english_name = get_english_name(language_id)

# rxkb_context.layouts lists all XKB layouts, including variants and less common options,
# while langtable.list_keyboards filters for the most relevant layouts per language.
keyboards = self._layout_infos.items()
langtable_keyboards = get_locale_keyboards(language_id)

# Sort the available keyboards by name alphabetically and but put the most common ones
# (langtable) on top
keyboards = sorted(keyboards, key=lambda x: x[0])
keyboards = sorted(
keyboards,
key=lambda x: langtable_keyboards.index(x[0].replace(" ", "")) if x[0].replace(" ", "") in langtable_keyboards else 999
)

layouts = []
for name, info in keyboards:
if any(english_name in langs for langs 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."""
Expand Down
21 changes: 21 additions & 0 deletions pyanaconda/modules/localization/localization_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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."""
Expand Down
Loading

0 comments on commit b263759

Please sign in to comment.