diff --git a/newIDE/app/src/CommandPalette/CommandPalette/AutocompletePicker.js b/newIDE/app/src/CommandPalette/CommandPalette/AutocompletePicker.js index a478f1cceab0..abedec214aa1 100644 --- a/newIDE/app/src/CommandPalette/CommandPalette/AutocompletePicker.js +++ b/newIDE/app/src/CommandPalette/CommandPalette/AutocompletePicker.js @@ -13,7 +13,7 @@ import Chip from '../../UI/Chip'; import TextField from '@material-ui/core/TextField'; import ChevronRightIcon from '../../UI/CustomSvgIcons/ChevronArrowRight'; import Autocomplete from '@material-ui/lab/Autocomplete'; -import filterOptions from './FilterOptions'; +import makeFilterOptions from './FilterOptions'; import { type NamedCommand, type CommandOption, @@ -134,6 +134,10 @@ const AutocompletePicker = ( const [open, setOpen] = React.useState(true); const shortcutMap = useShortcutMap(); const classes = useStyles(); + const filterOptions = React.useMemo( + () => makeFilterOptions(props.i18n), + [props.i18n] + ); // $FlowFixMe[missing-local-annot] const handleClose = (_, reason) => { diff --git a/newIDE/app/src/CommandPalette/CommandPalette/FilterOptions.js b/newIDE/app/src/CommandPalette/CommandPalette/FilterOptions.js index b8ee60fbca74..1716c84ec422 100644 --- a/newIDE/app/src/CommandPalette/CommandPalette/FilterOptions.js +++ b/newIDE/app/src/CommandPalette/CommandPalette/FilterOptions.js @@ -1,28 +1,76 @@ // @flow +import { t } from '@lingui/macro'; +import { type I18n as I18nType } from '@lingui/core'; import { fuzzyOrEmptyFilter } from '../../Utils/FuzzyOrEmptyFilter'; /** - * Filters options both simply and fuzzy-ly, - * prioritizing simple-matched options + * Words that should be treated as synonyms when searching for commands. + * For example, "Edit scene variables" and "Open scene variables" should both match. + * Uses i18n so that synonyms work in all languages. */ -const filterOptions = ( +const getSynonymGroups = (i18n: I18nType): Array> => [ + [i18n._(t`open`), i18n._(t`edit`)], +]; + +/** + * Returns an alternate version of the search text with synonyms swapped, + * or null if no synonym applies. + */ +const getSearchTextWithSynonym = ( + searchText: string, + i18n: I18nType +): string | null => { + const synonymGroups = getSynonymGroups(i18n); + for (const group of synonymGroups) { + for (let i = 0; i < group.length; i++) { + const word = group[i]; + if (searchText.startsWith(word + ' ') || searchText === word) { + // Replace the first occurrence of the synonym with the other synonym. + for (let j = 0; j < group.length; j++) { + if (i !== j) { + return group[j] + searchText.slice(word.length); + } + } + } + } + } + return null; +}; + +/** + * Creates a filter function that filters options both simply and fuzzy-ly, + * prioritizing simple-matched options. + * Accepts an i18n instance so that synonym groups can be translated. + */ +const makeFilterOptions = ( + i18n: I18nType +): (( options: Array, state: { getOptionLabel: T => string, inputValue: string } -): any => { - const searchText = state.inputValue.toLowerCase(); - if (searchText === '') return options; +) => any) => { + return ( + options: Array, + state: { getOptionLabel: T => string, inputValue: string } + ): any => { + const searchText = state.inputValue.toLowerCase(); + if (searchText === '') return options; + + const synonymSearchText = getSearchTextWithSynonym(searchText, i18n); - const directMatches = []; - const fuzzyMatches = []; - options.forEach(option => { - if (option.hit) return directMatches.push(option); - const optionText = state.getOptionLabel(option).toLowerCase(); - if (optionText.includes(searchText)) return directMatches.push(option); - if (fuzzyOrEmptyFilter(searchText, optionText)) - return fuzzyMatches.push(option); - }); + const directMatches = []; + const fuzzyMatches = []; + options.forEach(option => { + if (option.hit) return directMatches.push(option); + const optionText = state.getOptionLabel(option).toLowerCase(); + if (optionText.includes(searchText)) return directMatches.push(option); + if (synonymSearchText && optionText.includes(synonymSearchText)) + return directMatches.push(option); + if (fuzzyOrEmptyFilter(searchText, optionText)) + return fuzzyMatches.push(option); + }); - return [...directMatches, ...fuzzyMatches]; + return [...directMatches, ...fuzzyMatches]; + }; }; -export default filterOptions; +export default makeFilterOptions; diff --git a/newIDE/app/src/CommandPalette/CommandsList.js b/newIDE/app/src/CommandPalette/CommandsList.js index 50ba8a7f16cb..8251d30b38fd 100644 --- a/newIDE/app/src/CommandPalette/CommandsList.js +++ b/newIDE/app/src/CommandPalette/CommandsList.js @@ -48,7 +48,6 @@ export type CommandName = | 'OPEN_SETUP_GRID' | 'EDIT_LAYER_EFFECTS' | 'EDIT_LAYER' - | 'EDIT_NETWORK_PREVIEW' | 'EDIT_OBJECT' | 'EDIT_OBJECT_BEHAVIORS' | 'EDIT_OBJECT_EFFECTS' @@ -68,6 +67,11 @@ export type CommandName = | 'SEARCH_EVENTS' | 'OPEN_EXTENSION_SETTINGS' | 'OPEN_PROFILE' + | 'OPEN_PREFERENCES' + | 'OPEN_ABOUT' + | 'OPEN_LANGUAGE' + | 'OPEN_VERSION_HISTORY' + | 'OPEN_DEBUGGER' | 'OPEN_MEMORY_TRACKER_REGISTRY'; export const commandAreas = { @@ -350,6 +354,30 @@ const commandsList: { [CommandName]: CommandMetadata } = { displayText: t`Open extension settings`, }, + // IDE commands + OPEN_PREFERENCES: { + area: 'IDE', + displayText: t`Open preferences`, + }, + OPEN_ABOUT: { + area: 'IDE', + displayText: t`Open "About GDevelop" (version)`, + }, + OPEN_LANGUAGE: { + area: 'IDE', + displayText: t`Change language`, + }, + + // Project commands + OPEN_VERSION_HISTORY: { + area: 'PROJECT', + displayText: t`Open version history`, + }, + OPEN_DEBUGGER: { + area: 'PROJECT', + displayText: t`Open debugger`, + }, + // Debug commands OPEN_MEMORY_TRACKER_REGISTRY: { area: 'IDE', diff --git a/newIDE/app/src/MainFrame/MainFrameCommands.js b/newIDE/app/src/MainFrame/MainFrameCommands.js index 72cff6558801..efac1ecca022 100644 --- a/newIDE/app/src/MainFrame/MainFrameCommands.js +++ b/newIDE/app/src/MainFrame/MainFrameCommands.js @@ -69,6 +69,11 @@ type CommandHandlers = {| onRestartInGameEditor: (reason: string) => void, onOpenGlobalSearch: () => void, onOpenMemoryTrackerRegistry: () => void, + onOpenPreferences: () => void, + onOpenAbout: () => void, + onOpenLanguage: () => void, + onOpenVersionHistory: () => void, + onOpenDebugger: () => void, |}; const useMainFrameCommands = (handlers: CommandHandlers) => { @@ -174,6 +179,26 @@ const useMainFrameCommands = (handlers: CommandHandlers) => { ), }); + useCommand('OPEN_PREFERENCES', true, { + handler: handlers.onOpenPreferences, + }); + + useCommand('OPEN_ABOUT', true, { + handler: handlers.onOpenAbout, + }); + + useCommand('OPEN_LANGUAGE', true, { + handler: handlers.onOpenLanguage, + }); + + useCommand('OPEN_VERSION_HISTORY', !!handlers.project, { + handler: handlers.onOpenVersionHistory, + }); + + useCommand('OPEN_DEBUGGER', !!handlers.project, { + handler: handlers.onOpenDebugger, + }); + useCommand('OPEN_MEMORY_TRACKER_REGISTRY', true, { handler: handlers.onOpenMemoryTrackerRegistry, }); diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 89fa784be712..d6a9aa8e0aff 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -4815,6 +4815,11 @@ const MainFrame = (props: Props): React.MixedElement => { onRestartInGameEditor, onOpenGlobalSearch: openGlobalSearch, onOpenMemoryTrackerRegistry: () => setMemoryTrackedRegistryDialogOpen(true), + onOpenPreferences: () => openPreferencesDialog(true), + onOpenAbout: () => openAboutDialog(true), + onOpenLanguage: () => openLanguageDialog(true), + onOpenVersionHistory: openVersionHistoryPanel, + onOpenDebugger: openDebugger, }); const resourceManagementProps: ResourceManagementProps = React.useMemo(