import Setting, { AppType, SettingItemSubType } from '@joplin/lib/models/Setting'; import { themeStyle } from '@joplin/lib/theme'; import * as React from 'react'; import { useCallback, useId } from 'react'; import control_PluginsStates from './plugins/PluginsStates'; import bridge from '../../../services/bridge'; import { _ } from '@joplin/lib/locale'; import Button, { ButtonLevel, ButtonSize } from '../../Button/Button'; import FontSearch from './FontSearch'; import * as pathUtils from '@joplin/lib/path-utils'; import SettingLabel from './SettingLabel'; import SettingDescription from './SettingDescription'; const settingKeyToControl: Record = { 'plugins.states': control_PluginsStates, }; export interface UpdateSettingValueEvent { key: string; value: unknown; } interface Props { themeId: number; settingKey: string; value: unknown; fonts: string[]; onUpdateSettingValue: (event: UpdateSettingValueEvent)=> void; onSettingButtonClick: (key: string)=> void; } const SettingComponent: React.FC = props => { const theme = themeStyle(props.themeId); const output: React.ReactNode = null; const updateSettingValue = useCallback((key: string, value: unknown) => { props.onUpdateSettingValue({ key, value }); }, [props.onUpdateSettingValue]); const rowStyle = { marginBottom: theme.mainPadding * 1.5, }; const controlStyle = { display: 'inline-block', color: theme.color, fontFamily: theme.fontFamily, backgroundColor: theme.backgroundColor, }; const textInputBaseStyle: React.CSSProperties = { ...controlStyle, fontFamily: theme.fontFamily, border: '1px solid', padding: '4px 6px', boxSizing: 'border-box', borderColor: theme.borderColor4, borderRadius: 3, paddingLeft: 6, paddingRight: 6, paddingTop: 4, paddingBottom: 4, }; const key = props.settingKey; const md = Setting.settingMetadata(key); const descriptionText = Setting.keyDescription(key, AppType.Desktop); const inputId = useId(); const descriptionId = useId(); const descriptionComp = ; if (key in settingKeyToControl) { const CustomSettingComponent = settingKeyToControl[key]; const label = md.label ? : null; return (
{label} { updateSettingValue(key, event.value); }} />
); } else if (md.isEnum) { const value = props.value as string; const items = []; const settingOptions = md.options(); const array = Setting.enumOptionsToValueLabels(settingOptions, md.optionsOrder ? md.optionsOrder() : [], { valueKey: 'key', labelKey: 'label', }); for (let i = 0; i < array.length; i++) { const e = array[i]; items.push( , ); } return (
{descriptionComp}
); } else if (md.type === Setting.TYPE_BOOL) { const value = props.value as boolean; const checkboxSize = theme.fontSize * 1.1666666666666; return (
updateSettingValue(key, event.target.checked)} style={{ marginLeft: 0, width: checkboxSize, height: checkboxSize }} // Prefer aria-details to aria-describedby for checkbox inputs -- // on MacOS, VoiceOver reads "checked"/"unchecked" only after reading the // potentially-lengthy description. For other input types, the input value // is read first. aria-details={descriptionId} />
{descriptionComp}
); } else if (md.type === Setting.TYPE_STRING) { const value = props.value as string; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied const inputStyle: any = { ...textInputBaseStyle, width: '50%', minWidth: '20em' }; const inputType = md.secure === true ? 'password' : 'text'; if (md.subType === 'file_path_and_args' || md.subType === 'file_path' || md.subType === 'directory_path') { inputStyle.marginBottom = theme.mainPadding / 2; const splitCmd = (cmdString: string) => { // Normally not necessary but certain plugins found a way to // set the set the value to "undefined", leading to a crash. // This is now fixed at the model level but to be sure we // check here too, to handle any already existing data. // https://github.com/laurent22/joplin/issues/7621 if (!cmdString) cmdString = ''; const path = pathUtils.extractExecutablePath(cmdString); const args = cmdString.substr(path.length + 1); return [pathUtils.unquotePath(path), args]; }; const joinCmd = (cmdArray: string[]) => { if (!cmdArray[0] && !cmdArray[1]) return ''; let cmdString = pathUtils.quotePath(cmdArray[0]); if (!cmdString) cmdString = '""'; if (cmdArray[1]) cmdString += ` ${cmdArray[1]}`; return cmdString; }; const onPathChange: React.ChangeEventHandler = event => { if (md.subType === 'file_path_and_args') { const cmd = splitCmd(value); cmd[0] = event.target.value; updateSettingValue(key, joinCmd(cmd)); } else { updateSettingValue(key, event.target.value); } }; const onArgsChange: React.ChangeEventHandler = event => { const cmd = splitCmd(value); cmd[1] = event.target.value; updateSettingValue(key, joinCmd(cmd)); }; const browseButtonClick = async () => { if (md.subType === 'directory_path') { const paths = await bridge().showOpenDialog({ properties: ['openDirectory'], }); if (!paths || !paths.length) return; updateSettingValue(key, paths[0]); } else { const paths = await bridge().showOpenDialog(); if (!paths || !paths.length) return; if (md.subType === 'file_path') { updateSettingValue(key, paths[0]); } else { const cmd = splitCmd(value); cmd[0] = paths[0]; updateSettingValue(key, joinCmd(cmd)); } } }; const cmd = splitCmd(value); const path = md.subType === 'file_path_and_args' ? cmd[0] : value; const argInputId = `setting_path_arg_${key}`; const argComp = md.subType !== 'file_path_and_args' ? null : (
{descriptionComp}
); const pathDescriptionId = `setting_path_label_${key}`; return (
{_('Path:')}
{descriptionComp}
{argComp}
); } else { const onTextChange: React.ChangeEventHandler = event => { updateSettingValue(key, event.target.value); }; return (
{ md.subType === SettingItemSubType.FontFamily || md.subType === SettingItemSubType.MonospaceFontFamily ? updateSettingValue(key, fontFamily)} subtype={md.subType} inputId={inputId} /> : }
{descriptionComp}
); } } else if (md.type === Setting.TYPE_INT) { const value = props.value as number; const onNumChange: React.ChangeEventHandler = (event) => { updateSettingValue(key, event.target.value); }; const label = [md.label()]; if (md.unitLabel) label.push(`(${md.unitLabel(md.value)})`); return (
{descriptionComp}
); } else if (md.type === Setting.TYPE_BUTTON) { const labelComp = md.hideLabel ? null : ( ); return (
{labelComp}
); } else { console.warn(`Type not implemented: ${key}`); } return output; }; export default SettingComponent;