Skip to content

Commit

Permalink
Creating a dbus interface to get local keyboard layouts
Browse files Browse the repository at this point in the history
Adding dbus interface for loading locale keyboards

Remove duplicate function and migrate to localization one
  • Loading branch information
adamkankovsky committed Jan 28, 2025
1 parent 7ebe8c5 commit fb322bc
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 71 deletions.
74 changes: 74 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,75 @@ 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(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]
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
31 changes: 29 additions & 2 deletions pyanaconda/modules/localization/localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,28 @@
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
import gettext

import langtable

from pyanaconda.anaconda_loggers import get_module_logger
from pyanaconda.core.configuration.anaconda import conf
from pyanaconda.core.dbus import DBus
from pyanaconda.core.signal import Signal
from pyanaconda.core.string import upcase_first_letter
from pyanaconda.localization import (
_build_layout_infos,
get_available_translations,
get_common_languages,
get_english_name,
get_language_id,
get_language_locales,
get_native_name,
get_native_name, get_layout_variant_description,
)
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 +55,6 @@

log = get_module_logger(__name__)


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

Expand Down Expand Up @@ -80,6 +84,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):
Expand Down Expand Up @@ -177,6 +183,27 @@ def get_locale_data(self, locale_id):

return tdata

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 = 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
11 changes: 6 additions & 5 deletions pyanaconda/ui/gui/spokes/keyboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from pyanaconda.core.string import have_word_match, strip_accents
from pyanaconda.core.threads import thread_manager
from pyanaconda.core.util import startProgram
from pyanaconda.localization import get_layout_variant_description
from pyanaconda.modules.common.constants.services import LOCALIZATION
from pyanaconda.modules.common.util import is_module_available
from pyanaconda.ui.categories.localization import LocalizationCategory
Expand Down Expand Up @@ -62,7 +63,7 @@ def _show_layout(column, renderer, model, itr, wrapper):
if not value:
return ""

return wrapper.get_layout_variant_description(value)
return get_layout_variant_description(value)


def _show_description(column, renderer, model, itr, wrapper):
Expand Down Expand Up @@ -94,8 +95,8 @@ def matches_entry(self, model, itr, user_data=None):
# nothing matches a separator
return False

eng_value = self._xkl_wrapper.get_layout_variant_description(value, xlated=False)
xlated_value = self._xkl_wrapper.get_layout_variant_description(value)
eng_value = get_layout_variant_description(value, xlated=False)
xlated_value = get_layout_variant_description(value)
translit_value = strip_accents(xlated_value).lower()
entry_text = entry_text.lower()

Expand All @@ -114,7 +115,7 @@ def _sort_layout(self, layout):
"""
return locale_mod.strxfrm(
self._xkl_wrapper.get_layout_variant_description(layout)
get_layout_variant_description(layout)
)

def refresh(self):
Expand Down Expand Up @@ -382,7 +383,7 @@ def completed(self):
@property
def status(self):
# We don't need to check that self._store is empty, because that isn't allowed.
descriptions = (self._xkl_wrapper.get_layout_variant_description(row[0])
descriptions = (get_layout_variant_description(row[0])
for row in self._store)
return ", ".join(descriptions)

Expand Down
Loading

0 comments on commit fb322bc

Please sign in to comment.