diff --git a/src/actions/localization-actions.js b/src/actions/localization-actions.js index ab2cbc655..d6846ff6a 100644 --- a/src/actions/localization-actions.js +++ b/src/actions/localization-actions.js @@ -21,6 +21,7 @@ import { getLanguageData, getLanguages, getLocaleData, + getLocaleKeyboardLayouts, getLocales, } from "../apis/localization.js"; @@ -69,3 +70,14 @@ export const getCommonLocalesAction = () => { }); }; }; + +export const getKeyboardLayoutsAction = ({ language }) => { + return async (dispatch) => { + const keyboardLayouts = await getLocaleKeyboardLayouts({ locale: language }); + + dispatch({ + payload: { keyboardLayouts }, + type: "GET_KEYBOARD_LAYOUTS" + }); + }; +}; diff --git a/src/apis/localization.js b/src/apis/localization.js index abb1a40a1..e88c141a6 100644 --- a/src/apis/localization.js +++ b/src/apis/localization.js @@ -17,7 +17,7 @@ import cockpit from "cockpit"; -import { getLanguageAction, getLanguagesAction } from "../actions/localization-actions.js"; +import { getKeyboardLayoutsAction, getLanguageAction, getLanguagesAction } from "../actions/localization-actions.js"; import { debug } from "../helpers/log.js"; import { _callClient, _getProperty, _setProperty } from "./helpers.js"; @@ -62,18 +62,22 @@ export class LocalizationClient { } async initData () { + const language = await getLanguage(); await this.dispatch(getLanguageAction()); await this.dispatch(getLanguagesAction()); + await this.dispatch(getKeyboardLayoutsAction({ language })); } startEventMonitor () { this.client.subscribe( { }, - (path, iface, signal, args) => { + async (path, iface, signal, args) => { switch (signal) { case "PropertiesChanged": if (args[0] === INTERFACE_NAME && Object.hasOwn(args[1], "Language")) { - this.dispatch(getLanguageAction()); + await this.dispatch(getLanguageAction()); + const language = await getLanguage(); + await this.dispatch(getKeyboardLayoutsAction({ language })); } else { debug(`Unhandled signal on ${path}: ${iface}.${signal}`, JSON.stringify(args)); } @@ -139,3 +143,19 @@ export const getLocaleData = ({ locale }) => { export const setLanguage = ({ lang }) => { return setProperty("Language", cockpit.variant("s", lang)); }; + +/** + * @param {string} layout Keyboard layout id + */ +export const setKeyboardLayout = ({ layout }) => { + return setProperty("SetCompositorSelectedLayout", cockpit.variant("s", layout)); +}; + +/** + * @param {string} lang Locale id + * + * @returns {Promise} Resolves a list of locale keyboards + */ +export const getLocaleKeyboardLayouts = ({ locale }) => { + return callClient("GetLocaleKeyboardLayouts", [locale]); +}; diff --git a/src/components/localization/InstallationLanguage.jsx b/src/components/localization/InstallationLanguage.jsx index 8e95a0ad2..b935b8b5c 100644 --- a/src/components/localization/InstallationLanguage.jsx +++ b/src/components/localization/InstallationLanguage.jsx @@ -17,7 +17,7 @@ import cockpit from "cockpit"; -import React, { useContext, useEffect } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { Button, Form, @@ -36,6 +36,7 @@ import { SearchIcon, TimesIcon } from "@patternfly/react-icons"; import { setLocale } from "../../apis/boss.js"; import { + setKeyboardLayout, setLanguage, } from "../../apis/localization.js"; @@ -47,6 +48,8 @@ import { import { LanguageContext } from "../../contexts/Common.jsx"; +import { KeyboardSelector } from "./Keyboard.jsx"; + import "./InstallationLanguage.scss"; const _ = cockpit.gettext; @@ -295,7 +298,13 @@ class LanguageSelector extends React.Component { } const InstallationLanguage = ({ setIsFormValid, setStepNotification }) => { - const { commonLocales, language, languages } = useContext(LanguageContext); + const { commonLocales, keyboardLayouts, language, languages } = useContext(LanguageContext); + const [keyboard, setKeyboard] = useState(""); + + const handleSetKeyboard = (value) => { + setKeyboard(value); + setKeyboardLayout(value); + }; useEffect(() => { setIsFormValid(language !== ""); @@ -303,13 +312,9 @@ const InstallationLanguage = ({ setIsFormValid, setStepNotification }) => { return ( <> - - {_("Choose a language")} - -
- + {_("Choose Language and Keyboard")} + + { reRenderApp={setLanguage} /> + + + + ); diff --git a/src/components/localization/InstallationLanguage.scss b/src/components/localization/InstallationLanguage.scss index 2b0fdd17e..af446b8c1 100644 --- a/src/components/localization/InstallationLanguage.scss +++ b/src/components/localization/InstallationLanguage.scss @@ -1,13 +1,34 @@ -.anaconda-screen-language-menu.pf-v5-c-menu.pf-m-scrollable { +.anaconda-screen-selectors-container { + display: grid; + grid-template-columns: 150px 1fr; + row-gap: var(--pf-v5-global--spacer--lg); + column-gap: var(--pf-v5-global--spacer--md); +} + +.anaconda-screen-selectors-container .pf-v5-c-form__group { + display: contents; +} + +.anaconda-screen-selectors-container label { + grid-column: 1; + font-weight: 600; + text-align: left; +} + +.anaconda-screen-selectors-container .pf-v5-c-form__group-control { + grid-column: 2; max-width: 400px; +} + +.anaconda-screen-language-menu.pf-v5-c-menu.pf-m-scrollable { // heading: 84, footer: 44, content (about from header): 158, necessary padding underneath: 8px, + // keyboardSelector: 60 // 50px magic number - --pf-v5-c-menu__content--MaxHeight: calc(100vh - 84px - 44px - 158px - 8px - 50px); + --pf-v5-c-menu__content--MaxHeight: calc(100vh - 84px - 44px - 158px - 8px - 60px - 50px); } .anaconda-screen-language-search { margin-bottom: var(--pf-v5-global--spacer--sm); - max-width: 400px; } .anaconda-screen-language-menu { diff --git a/src/components/localization/Keyboard.jsx b/src/components/localization/Keyboard.jsx new file mode 100644 index 000000000..cbd48595d --- /dev/null +++ b/src/components/localization/Keyboard.jsx @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 Red Hat, Inc. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with This program; If not, see . + */ + +import cockpit from "cockpit"; + +import React, { useEffect } from "react"; +import { FormSelect, FormSelectOption } from "@patternfly/react-core"; + +const _ = cockpit.gettext; + +export const KeyboardSelector = ({ idPrefix, keyboards, selectedKeyboard, setKeyboard }) => { + useEffect(() => { + // Ensure the selected keyboard is valid or reset to the default layout + if (keyboards.length > 0) { + const selectedLayoutId = selectedKeyboard?.split(":")[0]; + const isKeyboardValid = keyboards.some( + ({ "layout-id": layoutId }) => layoutId?.v === selectedLayoutId + ); + + // Reset to default if current selection is invalid + if (!isKeyboardValid) { + setKeyboard(keyboards[0]["layout-id"]?.v); // Default layout without variant + } + } + }, [keyboards, selectedKeyboard, setKeyboard]); + + const handleChange = (event) => { + const { value } = event.target; + setKeyboard(value); + }; + + const selectedValue = + selectedKeyboard || (keyboards.length > 0 ? keyboards[0]["layout-id"]?.v : ""); + + return ( + + {keyboards.map(({ description, "layout-id": layoutId }) => ( + + ))} + + ); +}; diff --git a/src/reducer.js b/src/reducer.js index e218e5019..fd35368ec 100644 --- a/src/reducer.js +++ b/src/reducer.js @@ -45,6 +45,7 @@ export const storageInitialState = { /* Initial state for the localization store substate */ export const localizationInitialState = { commonLocales: [], + keyboardLayouts: [], language: "", languages: {} }; @@ -173,6 +174,8 @@ export const localizationReducer = (state = localizationInitialState, action) => return { ...state, commonLocales: action.payload.commonLocales }; } else if (action.type === "GET_LANGUAGE") { return { ...state, language: action.payload.language }; + } else if (action.type === "GET_KEYBOARD_LAYOUTS") { + return { ...state, keyboardLayouts: action.payload.keyboardLayouts }; } else { return state; }