From 412803442b1b56808c52ce104dbbf7d468678746 Mon Sep 17 00:00:00 2001 From: Jiri Konecny Date: Wed, 28 Aug 2024 17:24:24 +0200 Subject: [PATCH] Switch keyboard management to Localed Because of the switch to Wayland Anaconda has to change compositor keyboard manager because libxklavier doesn't work on Wayland. To fix that we migrated to Gnome Kiosk DBus API in RHEL-10, however, this solution can't be used outside of Gnome Kiosk. For that reason, we are switching to Localed which we set as the default to enable Anaconda to control keyboard switching. --- .../modules/common/constants/services.py | 5 - .../localization/gk_keyboard_manager.py | 135 --------------- .../modules/localization/localization.py | 32 ++-- .../localization/gk_keyboard_manager_test.py | 156 ------------------ .../localization/test_module_localization.py | 40 ++--- 5 files changed, 33 insertions(+), 335 deletions(-) delete mode 100644 pyanaconda/modules/localization/gk_keyboard_manager.py delete mode 100644 tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py diff --git a/pyanaconda/modules/common/constants/services.py b/pyanaconda/modules/common/constants/services.py index 88b3e6f41d20..72ed9b4890a5 100644 --- a/pyanaconda/modules/common/constants/services.py +++ b/pyanaconda/modules/common/constants/services.py @@ -110,11 +110,6 @@ # Session services. -GK_INPUT_SOURCES = DBusServiceIdentifier( - namespace=("org", "gnome", "Kiosk"), - message_bus=SessionBus -) - MUTTER_DISPLAY_CONFIG = DBusServiceIdentifier( namespace=("org", "gnome", "Mutter", "DisplayConfig"), message_bus=SessionBus diff --git a/pyanaconda/modules/localization/gk_keyboard_manager.py b/pyanaconda/modules/localization/gk_keyboard_manager.py deleted file mode 100644 index 8a68e59990f8..000000000000 --- a/pyanaconda/modules/localization/gk_keyboard_manager.py +++ /dev/null @@ -1,135 +0,0 @@ -# -# Copyright (C) 2024 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 pyanaconda.core.signal import Signal -from pyanaconda.keyboard import join_layout_variant, parse_layout_variant, KeyboardConfigError -from pyanaconda.modules.common.constants.services import GK_INPUT_SOURCES - - -class GkKeyboardManager(object): - """Class wrapping GNOME Kiosk's input sources API.""" - - def __init__(self): - self.compositor_selected_layout_changed = Signal() - self.compositor_layouts_changed = Signal() - - object_path = GK_INPUT_SOURCES.object_path + '/InputSources/Manager' - self._proxy = GK_INPUT_SOURCES.get_proxy(object_path=object_path) - self._proxy.PropertiesChanged.connect(self._on_properties_changed) - - def _on_properties_changed(self, interface, changed_props, invalid_props): - for prop in changed_props: - if prop == 'SelectedInputSource': - layout_path = changed_props[prop] - layout_variant = self._path_to_layout(layout_path.get_string()) - self.compositor_selected_layout_changed.emit(layout_variant) - if prop == 'InputSources': - layout_paths = changed_props[prop] - layout_variants = map(self._path_to_layout, list(layout_paths)) - self.compositor_layouts_changed.emit(list(layout_variants)) - - def _path_to_layout(self, layout_path): - """Transforms a layout path as returned by GNOME Kiosk to "layout (variant)". - - :param layout_path: D-Bus path to the layout. - (e.g. "/org/gnome/Kiosk/InputSources/xkb_cz_2b_mon_5f_todo_5f_galik") - :type layout_path: str - :return: The layout with format "layout (variant)" (e.g. "cn (mon_todo_galik)") - :rtype: str - - :raise KeyboardConfigError: if layouts with invalid backend type is found - """ - layout_proxy = GK_INPUT_SOURCES.get_proxy(object_path=layout_path) - - if layout_proxy.BackendType != 'xkb': - raise KeyboardConfigError('Failed to get configuration from compositor') - - if '+' in layout_proxy.BackendId: - layout, variant = layout_proxy.BackendId.split('+') - return join_layout_variant(layout, variant) - else: - return layout_proxy.BackendId - - def _layout_to_xkb(self, layout_variant): - """Transforms a "layout (variant)" to a "('xkb', 'layout+variant')". - - :param layout_variant: The layout with format "layout (variant)" (e.g. "cz (qwerty)") - :type layout_variant: str - :return: The layout with format "('xkb', 'layout+variant')" (e.g. "('xkb', 'cz+qwerty')") - :rtype: str - """ - layout, variant = parse_layout_variant(layout_variant) - if variant: - return ('xkb', '{0}+{1}'.format(layout, variant)) - else: - return ('xkb', layout) - - def get_compositor_selected_layout(self): - """Get the activated keyboard layout. - - :return: Current keyboard layout (e.g. "cz (qwerty)") - :rtype: str - """ - layout_path = self._proxy.SelectedInputSource - if not layout_path or layout_path == '/': - return '' - - return self._path_to_layout(layout_path) - - def set_compositor_selected_layout(self, layout_variant): - """Set the activated keyboard layout. - - :param layout_variant: The layout to set, with format "layout (variant)" - (e.g. "cz (qwerty)") - :type layout_variant: str - :return: If the keyboard layout was activated - :rtype: bool - """ - layout_paths = self._proxy.InputSources - for layout_path in layout_paths: - if self._path_to_layout(layout_path) == layout_variant: - self._proxy.SelectInputSource(layout_path) - return True - - return False - - def select_next_compositor_layout(self): - """Set the next available layout as active.""" - self._proxy.SelectNextInputSource() - - def get_compositor_layouts(self): - """Get all available keyboard layouts. - - :return: A list of keyboard layouts (e.g. ["cz (qwerty)", cn (mon_todo_galik)]) - :rtype: list of strings - """ - layout_paths = self._proxy.InputSources - layout_variants = map(self._path_to_layout, list(layout_paths)) - return list(layout_variants) - - def set_compositor_layouts(self, layout_variants, options): - """Set the available keyboard layouts. - - :param layout_variants: A list of keyboard layouts (e.g. ["cz (qwerty)", - cn (mon_todo_galik)]) - :type layout_variants: list of strings - :param options: A list of switching options - :type options: list of strings - """ - xkb_layouts = list(map(self._layout_to_xkb, layout_variants)) - self._proxy.SetInputSources(xkb_layouts, options) diff --git a/pyanaconda/modules/localization/localization.py b/pyanaconda/modules/localization/localization.py index 452481e56acc..6d8e44951d1c 100644 --- a/pyanaconda/modules/localization/localization.py +++ b/pyanaconda/modules/localization/localization.py @@ -35,7 +35,6 @@ from pyanaconda.modules.localization.runtime import GetMissingKeyboardConfigurationTask, \ ApplyKeyboardTask, AssignGenericKeyboardSettingTask from pyanaconda.modules.localization.localed import LocaledWrapper -from pyanaconda.modules.localization.gk_keyboard_manager import GkKeyboardManager from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) @@ -71,7 +70,6 @@ def __init__(self): self.compositor_layouts_changed = Signal() self._localed_wrapper = None - self._compositor_keyboard_manager = None def publish(self): """Publish the module.""" @@ -249,6 +247,13 @@ def set_keyboard_seen(self, keyboard_seen): def localed_wrapper(self): if not self._localed_wrapper: self._localed_wrapper = LocaledWrapper() + + self._localed_wrapper.compositor_selected_layout_changed.connect( + self.compositor_selected_layout_changed.emit + ) + self._localed_wrapper.compositor_layouts_changed.connect( + self.compositor_layouts_changed.emit + ) return self._localed_wrapper def install_with_tasks(self): @@ -321,30 +326,17 @@ def set_from_generic_keyboard_setting(self, keyboard): result = task.run() self._update_settings_from_task(result) - @property - def compositor_keyboard_manager(self): - if not self._compositor_keyboard_manager: - self._compositor_keyboard_manager = GkKeyboardManager() - self._compositor_keyboard_manager.compositor_selected_layout_changed.connect( - lambda layout: self.compositor_selected_layout_changed.emit(layout) - ) - self._compositor_keyboard_manager.compositor_layouts_changed.connect( - lambda layouts: self.compositor_layouts_changed.emit(layouts) - ) - - return self._compositor_keyboard_manager - def get_compositor_selected_layout(self): - return self.compositor_keyboard_manager.get_compositor_selected_layout() + return self.localed_wrapper.current_layout_variant def set_compositor_selected_layout(self, layout_variant): - return self.compositor_keyboard_manager.set_compositor_selected_layout(layout_variant) + return self.localed_wrapper.set_current_layout(layout_variant) def select_next_compositor_layout(self): - return self.compositor_keyboard_manager.select_next_compositor_layout() + return self.localed_wrapper.select_next_layout() def get_compositor_layouts(self): - return self.compositor_keyboard_manager.get_compositor_layouts() + return self.localed_wrapper.layouts_variants def set_compositor_layouts(self, layout_variants, options): - self.compositor_keyboard_manager.set_compositor_layouts(layout_variants, options) + self.localed_wrapper.set_layouts(layout_variants, options) diff --git a/tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py b/tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py deleted file mode 100644 index 021b08ab9b98..000000000000 --- a/tests/unit_tests/pyanaconda_tests/modules/localization/gk_keyboard_manager_test.py +++ /dev/null @@ -1,156 +0,0 @@ -# -# Copyright (C) 2024 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. -# -import unittest -import pytest - -from unittest.mock import patch, Mock - -from pyanaconda.modules.localization.gk_keyboard_manager import GkKeyboardManager -from pyanaconda.keyboard import KeyboardConfigError - - -LAYOUT_PROXY_MOCKS = { - "/org/gnome/Kiosk/InputSources/xkb_fr": - Mock(BackendType="xkb", BackendId="fr"), - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik": - Mock(BackendType="xkb", BackendId="cn+mon_todo_galik"), - "/org/gnome/Kiosk/InputSources/non-xkb_fr": - Mock(BackendType="non-xkb", BackendId="fr"), - "/org/gnome/Kiosk/InputSources/Manager": - Mock(), -} - -MockedGKIS = Mock() -MockedGKIS.get_proxy = lambda object_path: LAYOUT_PROXY_MOCKS[object_path] -MockedGKIS.object_path = "/org/gnome/Kiosk" - - -@patch("pyanaconda.modules.localization.gk_keyboard_manager.GK_INPUT_SOURCES", new=MockedGKIS) -class GkKeyboardManagerTestCase(unittest.TestCase): - """Test the Gnome Kiosk keyboard manager.""" - - def test_properties_changed(self): - """Test _on_properties_changed callback""" - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - callback1_mock = Mock() - callback2_mock = Mock() - mocked_manager.compositor_selected_layout_changed.connect(callback1_mock) - mocked_manager.compositor_layouts_changed.connect(callback2_mock) - - object_path_mock = Mock() - object_path_mock.get_string.return_value = "/org/gnome/Kiosk/InputSources/xkb_fr" - mocked_manager._on_properties_changed( - "org.gnome.Kiosk.InputSources", - {"SelectedInputSource": object_path_mock}, - {}, - ) - callback1_mock.assert_called_once_with("fr") - callback2_mock.assert_not_called() - - mocked_manager._on_properties_changed( - "org.gnome.Kiosk.InputSources", - {"InputSources": ["/org/gnome/Kiosk/InputSources/xkb_fr"]}, - [], - ) - callback1_mock.assert_called_once_with("fr") - callback2_mock.assert_called_once_with(["fr"]) - - def test_get_compositor_selected_layout(self): - """Test the get_compositor_selected_layout method""" - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - - mocked_manager._proxy.SelectedInputSource = "/" - assert mocked_manager.get_compositor_selected_layout() == "" - - mocked_manager._proxy.SelectedInputSource = None - assert mocked_manager.get_compositor_selected_layout() == "" - - layout_path = "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik" - mocked_manager._proxy.SelectedInputSource = layout_path - assert mocked_manager.get_compositor_selected_layout() == "cn (mon_todo_galik)" - - def test_set_compositor_selected_layout(self): - """Test the set_compositor_selected_layout method""" - - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - assert mocked_manager.set_compositor_selected_layout("cn (mon_todo_galik)") is True - mocked_manager._proxy.SelectInputSource.assert_called_with( - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik" - ) - - # non-xkb type raises exception - # (even in case there is xkb-type data for the layout) - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/non-xkb_fr", - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - with pytest.raises(KeyboardConfigError): - mocked_manager.set_compositor_selected_layout("fr") - - # Source not found - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_fr" - ] - assert mocked_manager.set_compositor_selected_layout("cn (mon_todo_galik)") is False - - def test_select_next_compositor_layout(self): - """Test the select_next_compositor_layout method""" - mocked_manager = GkKeyboardManager() - mocked_manager.select_next_compositor_layout() - mocked_manager._proxy.SelectNextInputSource.assert_called_once() - - def test_get_compositor_layouts(self): - """Test the get_compositor_layouts method""" - - mocked_manager = GkKeyboardManager() - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/xkb_cn_2b_mon_5f_todo_5f_galik", - "/org/gnome/Kiosk/InputSources/xkb_fr", - ] - assert mocked_manager.get_compositor_layouts() == ["cn (mon_todo_galik)", "fr"] - - mocked_manager._proxy.InputSources = [ - "/org/gnome/Kiosk/InputSources/non-xkb_fr", - "/org/gnome/Kiosk/InputSources/xkb_fr", - ] - with pytest.raises(KeyboardConfigError): - mocked_manager.get_compositor_layouts() - - def test_set_compositor_layouts(self): - """Test the set_compositor_layouts method""" - mocked_manager = GkKeyboardManager() - mocked_manager.set_compositor_layouts( - ["cz (qwerty)", "fi", "us (euro)", "fr"], - ["grp:alt_shift_toggle", "grp:ctrl_alt_toggle"], - ) - mocked_manager._proxy.SetInputSources.assert_called_with( - [("xkb", "cz+qwerty"), ("xkb", "fi"), ("xkb", "us+euro"), ("xkb", "fr")], - ["grp:alt_shift_toggle", "grp:ctrl_alt_toggle"], - ) diff --git a/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py b/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py index 0ed2b6d2b3dc..ca4867a153f3 100644 --- a/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py +++ b/tests/unit_tests/pyanaconda_tests/modules/localization/test_module_localization.py @@ -393,32 +393,34 @@ def test_keyboard_kickstart4(self): """ self._test_kickstart(ks_in, ks_out) - @patch("pyanaconda.modules.localization.localization.GkKeyboardManager") - def test_compositor_layouts_api(self, gk_manager_cls): - manager_class_mock = Mock() - manager_class_mock.compositor_selected_layout_changed = Signal() - manager_class_mock.compositor_layouts_changed = Signal() - gk_manager_cls.return_value = manager_class_mock + @patch("pyanaconda.modules.localization.localization.LocaledWrapper") + def test_compositor_layouts_api(self, mocked_localed_wrapper): + localed_class_mock = Mock() + localed_class_mock.compositor_selected_layout_changed = Signal() + localed_class_mock.compositor_layouts_changed = Signal() + mocked_localed_wrapper.return_value = localed_class_mock - self.localization_module._compositor_keyboard_manager = None - manager_mock = self.localization_module.compositor_keyboard_manager + self.localization_module._localed_wrapper = None + manager_mock = self.localization_module.localed_wrapper + + manager_mock.current_layout_variant = "cz" + assert self.localization_interface.GetCompositorSelectedLayout() == "cz" - self.localization_interface.GetCompositorSelectedLayout() - # pylint: disable=no-member - manager_mock.get_compositor_selected_layout.assert_called_once() self.localization_interface.SetCompositorSelectedLayout("cz (qwerty)") # pylint: disable=no-member - manager_mock.set_compositor_selected_layout.assert_called_once_with("cz (qwerty)") + manager_mock.set_current_layout.assert_called_once_with("cz (qwerty)") + self.localization_interface.SelectNextCompositorLayout() # pylint: disable=no-member - manager_mock.select_next_compositor_layout.assert_called_once() - self.localization_interface.GetCompositorLayouts() - # pylint: disable=no-member - manager_mock.get_compositor_layouts.assert_called_once() + manager_mock.select_next_layout.assert_called_once() + + manager_mock.layouts_variants = ["us", "es"] + assert self.localization_interface.GetCompositorLayouts() == ["us","es"] + self.localization_interface.SetCompositorLayouts(["cz (qwerty)", "cn (mon_todo_galik)"], ["option"]) # pylint: disable=no-member - manager_mock.set_compositor_layouts.assert_called_once_with( + manager_mock.set_layouts.assert_called_once_with( ["cz (qwerty)", "cn (mon_todo_galik)"], ["option"] ) @@ -427,13 +429,13 @@ def test_compositor_layouts_api(self, gk_manager_cls): callback_mock = Mock() # pylint: disable=no-member self.localization_interface.CompositorSelectedLayoutChanged.connect(callback_mock) - manager_class_mock.compositor_selected_layout_changed.emit("cz (qwerty)") + localed_class_mock.compositor_selected_layout_changed.emit("cz (qwerty)") callback_mock.assert_called_once_with("cz (qwerty)") callback_mock = Mock() # pylint: disable=no-member self.localization_interface.CompositorLayoutsChanged.connect(callback_mock) - manager_class_mock.compositor_layouts_changed.emit(["cz (qwerty)", "cn (mon_todo_galik)"]) + localed_class_mock.compositor_layouts_changed.emit(["cz (qwerty)", "cn (mon_todo_galik)"]) callback_mock.assert_called_once_with(["cz (qwerty)", "cn (mon_todo_galik)"]) class LocalizationModuleTestCase(unittest.TestCase):