/* * Copyright (C) 2024 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ import cockpit from "cockpit"; import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js"; import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js"; import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content/index.js"; import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js"; import { Label } from "@patternfly/react-core/dist/esm/components/Label/index.js"; import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List/index.js"; import { Modal, ModalBody, ModalFooter, ModalHeader, ModalVariant } from "@patternfly/react-core/dist/esm/components/Modal/index.js"; import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js"; import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid/index.js"; import ArrowDownIcon from "@patternfly/react-icons/dist/esm/icons/arrow-down-icon"; import ArrowUpIcon from "@patternfly/react-icons/dist/esm/icons/arrow-up-icon"; import StarIcon from "@patternfly/react-icons/dist/esm/icons/star-icon"; import TrashIcon from "@patternfly/react-icons/dist/esm/icons/trash-icon"; import { setCompositorLayouts, setXKeyboardDefaults, setXLayouts, } from "../../apis/localization.js"; import { getKeyboardConfigurationAction, } from "../../actions/localization-actions.js"; import { getLocaleById } from "../../helpers/localization.js"; import { LanguageContext, PageContext } from "../../contexts/Common.jsx"; import { MenuSearch } from "../common/MenuSearch.jsx"; import "./Keyboard.scss"; const _ = cockpit.gettext; const SCREEN_ID = "anaconda-screen-language"; const SelectedKeyboards = ({ xlayouts }) => ( {!xlayouts?.length && _("No keyboard layouts selected")} {xlayouts?.length === 1 && xlayouts[0]} {xlayouts?.length > 1 && ( {xlayouts[0]} {xlayouts.slice(1).map((layout, index) => ( , {layout} ))} )} ); const buildMenuItem = (keyboard) => { const { description, "is-common": isCommon, "layout-id": layoutId, } = keyboard; const id = ( SCREEN_ID + "-keyboard-" + (isCommon.v ? "option-common-" : "option-alpha-") + layoutId?.v.replace(/[\s()]/g, "_") ); return { id, item: keyboard, itemId: layoutId?.v, itemText: description.v, itemType: "menu-item", key: layoutId?.v, onSearch: search => { const searchLower = search.toLowerCase(); return ( description.v.toLowerCase().includes(searchLower) || layoutId?.v.toLowerCase().includes(searchLower) ); }, }; }; const buildMenuGroup = (keyboards, showCommon) => ({ id: SCREEN_ID + "-keyboard-group-" + (showCommon ? "common" : "other") + "-keyboards", itemChildren: keyboards .filter(keyboard => keyboard["is-common"].v === showCommon) .map(keyboard => buildMenuItem(keyboard)), itemLabel: showCommon ? _("Suggested keyboards") : _("Other keyboards"), itemLabelHeadingLevel: "h3", itemType: "menu-group", }); const moveIndex = (arr, from, to) => { if (to < 0 || to >= arr.length) return arr; const newArray = arr.slice(); const [movedItem] = newArray.splice(from, 1); newArray.splice(to, 0, movedItem); return newArray; }; const uniqPush = (arr, id) => (arr.includes(id) ? arr : [...arr, id]); const LayoutActionButtons = ({ index, modalId, setSelected, totalItems }) => { const cssClasses = { actions: "change-system-keyboard-layout-modal-actions" }; const makeCurrent = (currentIndex) => setSelected(selected => moveIndex(selected, currentIndex, 0)); const moveUp = (currentIndex) => setSelected(selected => moveIndex(selected, currentIndex, currentIndex - 1)); const moveDown = (currentIndex) => setSelected(selected => moveIndex(selected, currentIndex, currentIndex + 1)); const removeAt = (currentIndex) => setSelected(selected => selected.filter((_, idx) => idx !== currentIndex)); return (
); }; const KeyboardDialog = ({ currentLayouts = [], onClose, onSaved }) => { const modalId = SCREEN_ID + "-change-system-keyboard-layout-modal"; const cssClasses = { listRow: "change-system-keyboard-layout-modal-list-row" }; const [alert, setAlert] = useState(); const [selected, setSelected] = useState(currentLayouts); const { keyboardLayouts } = useContext(LanguageContext); const currentLocale = getLocaleById(keyboardLayouts, selected?.[0]); const currentSupportsAscii = currentLocale?.["supports-ascii"]?.v === true; const options = useMemo( () => [buildMenuGroup(keyboardLayouts, true), buildMenuGroup(keyboardLayouts, false)], [keyboardLayouts] ); useEffect(() => { setAlert(); }, [selected]); const addLayout = (_e, item) => { const id = typeof item === "string" ? item : item?.itemId || item?.key; if (id) { setSelected(selected => uniqPush(selected, id)); } }; const saveAll = async () => { try { onSaved?.(selected); onClose(); } catch (e) { setAlert(e?.message || _("Failed to save layouts")); } }; return ( {_("Find layout")}
{_("Selected (top = current)")}
{selected.length === 0 ? ( {_("No layouts selected yet")} ) : ( {selected.map((id, idx) => (
))}
)} {selected.length > 0 && !currentSupportsAscii && ( )} {alert && }
); }; export const KeyboardGnome = ({ dispatch, onError, onSuccess }) => { const { plannedXlayouts } = useContext(LanguageContext); useEffect(() => { const onFocus = () => { dispatch(getKeyboardConfigurationAction({ onError, onSuccess })); }; window.addEventListener("focus", onFocus); return () => window.removeEventListener("focus", onFocus); }, [dispatch, onError, onSuccess]); return ( <> ); }; const KeyboardNonGnome = () => { const modalId = SCREEN_ID + "-change-system-keyboard-layout-modal"; const [keyboardAlert, setKeyboardAlert] = useState(); const [open, setOpen] = useState(false); const { language, plannedXlayouts, xlayouts } = useContext(LanguageContext); const langRef = useRef(language); useEffect(() => { // On initial load, if no X layouts, set sensible defaults if (xlayouts?.length > 0) { return; } setXKeyboardDefaults(); }, [xlayouts]); useEffect(() => { // When language changes, choose sensible defaults for X keyboard layouts if (langRef.current === language) { return; } langRef.current = language; setXKeyboardDefaults(); }, [language]); const handleSaved = async (selectedLayouts) => { try { await setCompositorLayouts({ layouts: selectedLayouts }); await setXLayouts({ layouts: selectedLayouts }); } catch (ex) { setKeyboardAlert(ex?.message); } }; const selectedKeyboards = ; return ( <> {selectedKeyboards} {keyboardAlert && } {open && setOpen(false)} onSaved={handleSaved} currentLayouts={plannedXlayouts} />} ); }; export const Keyboard = ({ dispatch, isGnome, setIsKeyboardValid }) => { const { plannedVconsole, plannedXlayouts } = useContext(LanguageContext); const { setStepNotification } = useContext(PageContext) ?? {}; const [keyboardConfigError, setKeyboardConfigError] = useState(); useEffect(() => { if (!keyboardConfigError) { setStepNotification?.(null); return; } setStepNotification?.({ message: keyboardConfigError, step: "anaconda-screen-language", }); }, [keyboardConfigError, setStepNotification]); // Fetch keyboard configuration when component mounts useEffect(() => { dispatch(getKeyboardConfigurationAction({ onError: setKeyboardConfigError, onSuccess: () => { setKeyboardConfigError(); } })); }, [dispatch]); useEffect(() => { setIsKeyboardValid( (plannedVconsole ?? "") !== "" && (plannedXlayouts?.length > 0) && !keyboardConfigError ); }, [ isGnome, keyboardConfigError, plannedVconsole, plannedXlayouts, setIsKeyboardValid ]); if (!plannedXlayouts?.length && !keyboardConfigError) { return null; } const keyboardAlert = ( ); const keyboard = ( isGnome ? ( { setKeyboardConfigError(); }} /> ) : ); return ( <> {keyboard} {plannedVconsole === "" && keyboardAlert} ); };