-
Notifications
You must be signed in to change notification settings - Fork 1.2k
QoL Resource tab with shortcuts #8115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ebc49ba
3eadeb3
6509e8c
0baaf86
868dd88
1017c43
f5d3bf6
2a11b5b
8f96e9c
e46fff0
ce42c42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ import * as React from 'react'; | |
| import { AutoSizer } from 'react-virtualized'; | ||
| import Background from '../UI/Background'; | ||
| import SearchBar from '../UI/SearchBar'; | ||
| import KeyboardShortcuts from '../UI/KeyboardShortcuts'; | ||
| import { showWarningBox } from '../UI/Messages/MessageBox'; | ||
| import { filterResourcesList } from './EnumerateResources'; | ||
| import { getResourceFilePathStatus } from './ResourceUtils'; | ||
|
|
@@ -26,6 +27,7 @@ import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList'; | |
| const styles = { | ||
| listContainer: { | ||
| flex: 1, | ||
| outline: 'none', | ||
| }, | ||
| }; | ||
|
|
||
|
|
@@ -57,6 +59,7 @@ export const getDefaultResourceThumbnail = (resource: gdResource) => { | |
| export type ResourcesListInterface = {| | ||
| forceUpdateList: () => void, | ||
| checkMissingPaths: () => void, | ||
| focus: () => void, | ||
| |}; | ||
|
|
||
| type Props = {| | ||
|
|
@@ -69,6 +72,7 @@ type Props = {| | |
| newName: string, | ||
| cb: (boolean) => void | ||
| ) => void, | ||
| onResourceRenamed?: () => void, | ||
| fileMetadata: ?FileMetadata, | ||
| onRemoveUnusedResources: ResourceKind => void, | ||
| onRemoveAllResourcesWithInvalidPath: () => void, | ||
|
|
@@ -84,6 +88,7 @@ const ResourcesList = React.memo<Props, ResourcesListInterface>( | |
| onSelectResource, | ||
| onDeleteResource, | ||
| onRenameResource, | ||
| onResourceRenamed, | ||
| fileMetadata, | ||
| onRemoveUnusedResources, | ||
| getResourceActionsSpecificToStorageProvider, | ||
|
|
@@ -96,6 +101,19 @@ const ResourcesList = React.memo<Props, ResourcesListInterface>( | |
| const [resourcesWithErrors, setResourcesWithErrors] = React.useState({}); | ||
| const [infoBarContent, setInfoBarContent] = React.useState(null); | ||
| const sortableListRef = React.useRef(null); | ||
| const listContainerRef = React.useRef(null); | ||
|
|
||
| const resourcesManager = project.getResourcesManager(); | ||
| const filteredList = React.useMemo( | ||
| () => { | ||
| const allResourcesList = resourcesManager | ||
| .getAllResourceNames() | ||
| .toJSArray() | ||
| .map(resourceName => resourcesManager.getResource(resourceName)); | ||
| return filterResourcesList(allResourcesList, searchText); | ||
| }, | ||
| [resourcesManager, searchText] | ||
| ); | ||
|
|
||
| const deleteResource = React.useCallback( | ||
| (resource: gdResource) => { | ||
|
|
@@ -153,9 +171,51 @@ const ResourcesList = React.memo<Props, ResourcesListInterface>( | |
| resource.setName(newName); | ||
| // Force re-render | ||
| forceUpdateList(); | ||
| if (onResourceRenamed) onResourceRenamed(); | ||
| }); | ||
|
|
||
| // Refocus the list container to allow keyboard shortcuts | ||
| if (listContainerRef.current) listContainerRef.current.focus(); | ||
| }, | ||
| [project, onRenameResource, forceUpdateList, onResourceRenamed] | ||
| ); | ||
|
|
||
| const moveSelection = React.useCallback( | ||
| (delta: number, filteredList: Array<gdResource>) => { | ||
| const resourceCount = filteredList.length; | ||
|
|
||
| if (resourceCount === 0) return; | ||
|
|
||
| let nextIndex = 0; | ||
| if (selectedResource) { | ||
| const currentIndex = filteredList.indexOf(selectedResource); | ||
| if (currentIndex === -1) { | ||
| // Selected resource is not in filtered list, select the first one. | ||
| nextIndex = 0; | ||
| } else { | ||
| nextIndex = Math.max( | ||
| 0, | ||
| Math.min(resourceCount - 1, currentIndex + delta) | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| const nextResource = filteredList[nextIndex]; | ||
| onSelectResource(nextResource); | ||
| }, | ||
|
Comment on lines
201
to
205
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a search filter is active, the list is rendered from Useful? React with 👍 / 👎. |
||
| [selectedResource, onSelectResource] | ||
| ); | ||
|
|
||
| const handleKeyDown = React.useCallback( | ||
| (event: KeyboardEvent) => { | ||
| if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { | ||
| moveSelection(event.key === 'ArrowDown' ? 1 : -1, filteredList); | ||
| event.preventDefault(); | ||
| } else { | ||
| keyboardShortcutsRef.current.onKeyDown(event); | ||
| } | ||
| }, | ||
| [project, onRenameResource, forceUpdateList] | ||
| [moveSelection, filteredList] | ||
| ); | ||
|
|
||
| const moveSelectionTo = React.useCallback( | ||
|
|
@@ -257,9 +317,35 @@ const ResourcesList = React.memo<Props, ResourcesListInterface>( | |
| [project, forceUpdateList] | ||
| ); | ||
|
|
||
| // KeyboardShortcuts callbacks are set dynamically in useEffect below | ||
| // instead of here, because they depend on selectedResource which can change. | ||
| // This ensures the callbacks always use the current selectedResource. | ||
| const keyboardShortcutsRef = React.useRef<KeyboardShortcuts>( | ||
| new KeyboardShortcuts({ | ||
| shortcutCallbacks: {}, | ||
Bouh marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }) | ||
| ); | ||
|
|
||
| React.useEffect( | ||
| () => { | ||
| if (!keyboardShortcutsRef.current) return; | ||
| if (!selectedResource) return; | ||
| keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => { | ||
| deleteResource(selectedResource); | ||
| }); | ||
| keyboardShortcutsRef.current.setShortcutCallback('onRename', () => { | ||
| editName(selectedResource); | ||
| }); | ||
| }, | ||
| [selectedResource, deleteResource, editName] | ||
| ); | ||
|
|
||
| React.useImperativeHandle(ref, () => ({ | ||
| forceUpdateList, | ||
| checkMissingPaths, | ||
| focus: () => { | ||
| if (listContainerRef.current) listContainerRef.current.focus(); | ||
| }, | ||
| })); | ||
|
|
||
| // Check missing paths on mount and when project changes. | ||
|
|
@@ -270,13 +356,6 @@ const ResourcesList = React.memo<Props, ResourcesListInterface>( | |
| [checkMissingPaths] | ||
| ); | ||
|
|
||
| const resourcesManager = project.getResourcesManager(); | ||
| const allResourcesList = resourcesManager | ||
| .getAllResourceNames() | ||
| .toJSArray() | ||
| .map(resourceName => resourcesManager.getResource(resourceName)); | ||
| const filteredList = filterResourcesList(allResourcesList, searchText); | ||
|
|
||
| // Force List component to be mounted again if project | ||
| // has been changed. Avoid accessing to invalid objects that could | ||
| // crash the app. | ||
|
|
@@ -294,7 +373,12 @@ const ResourcesList = React.memo<Props, ResourcesListInterface>( | |
| /> | ||
| </Column> | ||
| </Line> | ||
| <div style={styles.listContainer}> | ||
| <div | ||
| ref={listContainerRef} | ||
| style={styles.listContainer} | ||
| tabIndex={0} | ||
| onKeyDown={handleKeyDown} | ||
| > | ||
| <AutoSizer> | ||
| {({ height, width }) => ( | ||
| <I18n> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.