Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Core/GDCore/Project/ResourcesContainer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,20 @@ ResourcesContainer::GetResourcePosition(const gd::String &name) const {
return gd::String::npos;
}

Resource &ResourcesContainer::GetResourceAt(std::size_t index) {
if (index < resources.size())
return *resources[index];

return badResource;
}

const Resource &ResourcesContainer::GetResourceAt(std::size_t index) const {
if (index < resources.size())
return *resources[index];

return badResource;
}

void ResourcesContainer::MoveResource(std::size_t oldIndex,
std::size_t newIndex) {
if (oldIndex >= resources.size() || newIndex >= resources.size())
Expand Down
10 changes: 10 additions & 0 deletions Core/GDCore/Project/ResourcesContainer.h
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,16 @@ class GD_CORE_API ResourcesContainer {
*/
std::size_t GetResourcePosition(const gd::String &name) const;

/**
* \brief Return a reference to the resource at the specified position.
*/
Resource &GetResourceAt(std::size_t index);

/**
* \brief Return a reference to the resource at the specified position.
*/
const Resource &GetResourceAt(std::size_t index) const;

/**
* \brief Move a resource up in the list
*/
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/Bindings/Bindings.idl
Original file line number Diff line number Diff line change
Expand Up @@ -1329,12 +1329,14 @@ interface ResourcesContainer {
[Value] VectorString FindFilesNotInResources([Const, Ref] VectorString filesToCheck);
boolean HasResource([Const] DOMString name);
[Const, Ref] Resource GetResource([Const] DOMString name);
[Const, Ref] Resource GetResourceAt(unsigned long index);
[Const, Ref] DOMString GetResourceNameWithOrigin([Const] DOMString originName, [Const] DOMString originIdentifier);
[Const, Ref] DOMString GetResourceNameWithFile([Const] DOMString file);
boolean AddResource([Const, Ref] Resource res);
void RemoveResource([Const] DOMString name);
void RenameResource([Const] DOMString oldName, [Const] DOMString name);
unsigned long GetResourcePosition([Const] DOMString name);
unsigned long Count();
boolean MoveResourceUpInList([Const] DOMString oldName);
boolean MoveResourceDownInList([Const] DOMString oldName);
void MoveResource(unsigned long oldIndex, unsigned long newIndex);
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,12 +1100,14 @@ export class ResourcesContainer extends EmscriptenObject {
findFilesNotInResources(filesToCheck: VectorString): VectorString;
hasResource(name: string): boolean;
getResource(name: string): Resource;
getResourceAt(index: number): Resource;
getResourceNameWithOrigin(originName: string, originIdentifier: string): string;
getResourceNameWithFile(file: string): string;
addResource(res: Resource): boolean;
removeResource(name: string): void;
renameResource(oldName: string, name: string): void;
getResourcePosition(name: string): number;
count(): number;
moveResourceUpInList(oldName: string): boolean;
moveResourceDownInList(oldName: string): boolean;
moveResource(oldIndex: number, newIndex: number): void;
Expand Down
2 changes: 2 additions & 0 deletions GDevelop.js/types/gdresourcescontainer.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ declare class gdResourcesContainer {
findFilesNotInResources(filesToCheck: gdVectorString): gdVectorString;
hasResource(name: string): boolean;
getResource(name: string): gdResource;
getResourceAt(index: number): gdResource;
getResourceNameWithOrigin(originName: string, originIdentifier: string): string;
getResourceNameWithFile(file: string): string;
addResource(res: gdResource): boolean;
removeResource(name: string): void;
renameResource(oldName: string, name: string): void;
getResourcePosition(name: string): number;
count(): number;
moveResourceUpInList(oldName: string): boolean;
moveResourceDownInList(oldName: string): boolean;
moveResource(oldIndex: number, newIndex: number): void;
Expand Down
36 changes: 31 additions & 5 deletions newIDE/app/src/ResourcesEditor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export default class ResourcesEditor extends React.Component<Props, State> {
this.onResourceExternallyChanged.bind(this)
);
}

componentWillUnmount() {
unregisterOnResourceExternallyChangedCallback(
this.resourceExternallyChangedCallbackId
Expand Down Expand Up @@ -126,18 +127,38 @@ export default class ResourcesEditor extends React.Component<Props, State> {
);
if (!answer) return;

const resourcesManager = project.getResourcesManager();
const allNames = resourcesManager.getAllResourceNames().toJSArray();
const currentIndex = allNames.indexOf(resource.getName());

let nextResourceName = null;
if (allNames.length > 1) {
const nextIndex =
currentIndex < allNames.length - 1
? currentIndex + 1
: currentIndex - 1;
nextResourceName = allNames[nextIndex];
}

onDeleteResource(resource, doRemove => {
if (!doRemove || !resource) return;

project.getResourcesManager().removeResource(resource.getName());
resourcesManager.removeResource(resource.getName());

const nextResourceToSelect = nextResourceName
? resourcesManager.getResource(nextResourceName)
: null;

this.setState(
{
selectedResource: null,
selectedResource: nextResourceToSelect,
},
() => {
// Force update of the resources list as otherwise it could render
// resource that was just deleted.
if (this._resourcesList) this._resourcesList.forceUpdateList();
if (this._resourcesList) {
this._resourcesList.forceUpdateList();
if (this._resourcesList.focus) this._resourcesList.focus();
}
if (this._propertiesEditor) this._propertiesEditor.forceUpdate();
this.updateToolbar();
}
);
Expand Down Expand Up @@ -288,6 +309,11 @@ export default class ResourcesEditor extends React.Component<Props, State> {
getResourceActionsSpecificToStorageProvider={
resourcesActionsMenuBuilder
}
onResourceRenamed={() => {
if (this._propertiesEditor) {
this._propertiesEditor.forceUpdate();
}
}}
/>
),
},
Expand Down
102 changes: 93 additions & 9 deletions newIDE/app/src/ResourcesList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -26,6 +27,7 @@ import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
const styles = {
listContainer: {
flex: 1,
outline: 'none',
},
};

Expand Down Expand Up @@ -57,6 +59,7 @@ export const getDefaultResourceThumbnail = (resource: gdResource) => {
export type ResourcesListInterface = {|
forceUpdateList: () => void,
checkMissingPaths: () => void,
focus: () => void,
|};

type Props = {|
Expand All @@ -69,6 +72,7 @@ type Props = {|
newName: string,
cb: (boolean) => void
) => void,
onResourceRenamed?: () => void,
fileMetadata: ?FileMetadata,
onRemoveUnusedResources: ResourceKind => void,
onRemoveAllResourcesWithInvalidPath: () => void,
Expand All @@ -84,6 +88,7 @@ const ResourcesList = React.memo<Props, ResourcesListInterface>(
onSelectResource,
onDeleteResource,
onRenameResource,
onResourceRenamed,
fileMetadata,
onRemoveUnusedResources,
getResourceActionsSpecificToStorageProvider,
Expand All @@ -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) => {
Expand Down Expand Up @@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use filtered list for arrow navigation

When a search filter is active, the list is rendered from filteredList, but arrow-key navigation still picks the next resource via the full manager list (getResourceAt(nextIndex) with an index derived from the full count). In that scenario, pressing Up/Down can select a resource that isn’t in the filtered view, leaving the selection invisible and actions (delete/rename) targeting a hidden resource. Consider computing the next index from the filtered list (or skipping navigation when the filter is non-empty) so keyboard selection stays within the visible list.

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(
Expand Down Expand Up @@ -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: {},
})
);

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.
Expand All @@ -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.
Expand All @@ -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>
Expand Down