/*
* 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}
>
);
};