Skip to content

Commit

Permalink
Improve objects list (#5895)
Browse files Browse the repository at this point in the history
- Remove spell check on inputs
- Unselect item when a parent is closed (and the item becomes not visible)
- Allow to remove folder with all the objects contained in it (recursively)
- Improve selected row color 
- Add hover effects on rows
  • Loading branch information
AlexandreSi authored Nov 15, 2023
1 parent 8e668db commit 4ea6fb7
Show file tree
Hide file tree
Showing 23 changed files with 237 additions and 98 deletions.
5 changes: 5 additions & 0 deletions newIDE/app/scripts/theme-templates/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@
}
},
"list-item": {
"hover": {
"background-color": {
"value": "#2f2f36"
}
},
"group": {
"text": {
"color": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,32 +45,35 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
selectedObjectFolderOrObjectsWithContext: [],
};

_onDeleteObject = (i18n: I18nType) => (
objectWithContext: ObjectWithContext,
_onDeleteObjects = (i18n: I18nType) => (
objectsWithContext: ObjectWithContext[],
done: boolean => void
) => {
const { object } = objectWithContext;
const { project, globalObjectsContainer, eventsBasedObject } = this.props;
const message =
objectsWithContext.length === 1
? t`Do you want to remove all references to this object in groups and events (actions and conditions using the object)?`
: t`Do you want to remove all references to these objects in groups and events (actions and conditions using the objects)?`;

const answer = Window.showYesNoCancelDialog(
i18n._(
t`Do you want to remove all references to this object in groups and events (actions and conditions using the object)?`
)
);
const answer = Window.showYesNoCancelDialog(i18n._(message));

if (answer === 'cancel') return;
const shouldRemoveReferences = answer === 'yes';

gd.WholeProjectRefactorer.objectOrGroupRemovedInEventsBasedObject(
project,
eventsBasedObject,
globalObjectsContainer,
// $FlowFixMe gdObjectsContainer should be a member of gdEventsBasedObject instead of a base class.
eventsBasedObject,
object.getName(),
/* isObjectGroup=*/ false,
shouldRemoveReferences
);
const { project, globalObjectsContainer, eventsBasedObject } = this.props;

objectsWithContext.forEach(objectWithContext => {
const { object } = objectWithContext;
gd.WholeProjectRefactorer.objectOrGroupRemovedInEventsBasedObject(
project,
eventsBasedObject,
globalObjectsContainer,
// $FlowFixMe gdObjectsContainer should be a member of gdEventsBasedObject instead of a base class.
eventsBasedObject,
object.getName(),
/* isObjectGroup=*/ false,
shouldRemoveReferences
);
});
done(true);
};

Expand Down Expand Up @@ -251,7 +254,7 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
onEditObject={this.editObject}
// Don't allow export as there is no assets.
onExportObject={() => {}}
onDeleteObject={this._onDeleteObject(i18n)}
onDeleteObjects={this._onDeleteObjects(i18n)}
getValidatedObjectOrGroupName={
this._getValidatedObjectOrGroupName
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ type Props = {|
|};

const PointRow = ({ pointX, pointY, ...props }: Props) => {
const muiTheme = React.useContext(GDevelopThemeContext);
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<TableRow
style={{
backgroundColor: props.selected
? muiTheme.listItem.selectedBackgroundColor
: muiTheme.list.itemsBackgroundColor,
? gdevelopTheme.listItem.selectedBackgroundColor
: gdevelopTheme.list.itemsBackgroundColor,
}}
onClick={() => props.onClick(props.pointName)}
onPointerEnter={() => props.onPointerEnter(props.pointName)}
Expand All @@ -49,7 +49,7 @@ const PointRow = ({ pointX, pointY, ...props }: Props) => {
margin="none"
inputStyle={
props.selected
? { color: muiTheme.listItem.selectedTextColor }
? { color: gdevelopTheme.listItem.selectedTextColor }
: undefined
}
value={props.pointName}
Expand All @@ -68,7 +68,7 @@ const PointRow = ({ pointX, pointY, ...props }: Props) => {
margin="none"
inputStyle={
props.selected
? { color: muiTheme.listItem.selectedTextColor }
? { color: gdevelopTheme.listItem.selectedTextColor }
: undefined
}
value={roundTo(pointX, POINT_COORDINATE_PRECISION).toString()}
Expand Down Expand Up @@ -98,7 +98,7 @@ const PointRow = ({ pointX, pointY, ...props }: Props) => {
margin="none"
inputStyle={
props.selected
? { color: muiTheme.listItem.selectedTextColor }
? { color: gdevelopTheme.listItem.selectedTextColor }
: undefined
}
value={roundTo(pointY, POINT_COORDINATE_PRECISION).toString()}
Expand Down
27 changes: 26 additions & 1 deletion newIDE/app/src/ObjectsList/EnumerateObjectFolderOrObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,32 @@ const recursivelyEnumerateFoldersInFolder = (
});
};

export const enumerateFoldersInFolder = (folder: gdObjectFolderOrObject) => {
const recursivelyEnumerateObjectsInFolder = (
folder: gdObjectFolderOrObject,
result: gdObject[]
) => {
mapFor(0, folder.getChildrenCount(), i => {
const child = folder.getChildAt(i);
if (!child.isFolder()) {
result.push(child.getObject());
} else {
recursivelyEnumerateObjectsInFolder(child, result);
}
});
};

export const enumerateObjectsInFolder = (
folder: gdObjectFolderOrObject
): gdObject[] => {
if (!folder.isFolder()) return [];
const result = [];
recursivelyEnumerateObjectsInFolder(folder, result);
return result;
};

export const enumerateFoldersInFolder = (
folder: gdObjectFolderOrObject
): {| path: string, folder: gdObjectFolderOrObject |}[] => {
if (!folder.isFolder()) return [];
const result = [];
recursivelyEnumerateFoldersInFolder(folder, '', result);
Expand Down
92 changes: 71 additions & 21 deletions newIDE/app/src/ObjectsList/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { showWarningBox } from '../UI/Messages/MessageBox';
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
import type { ObjectWithContext } from '../ObjectsList/EnumerateObjects';
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
import { CLIPBOARD_KIND } from './ClipboardKind';
import TreeView, { type TreeViewInterface } from '../UI/TreeView';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
Expand All @@ -37,6 +38,7 @@ import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext';
import {
enumerateFoldersInContainer,
enumerateFoldersInFolder,
enumerateObjectsInFolder,
getFoldersAscendanceWithoutRootFolder,
getObjectFolderOrObjectUnifiedName,
type ObjectFolderOrObjectWithContext,
Expand Down Expand Up @@ -195,8 +197,8 @@ type Props = {|
objectsContainer: gdObjectsContainer,
onSelectAllInstancesOfObjectInLayout?: string => void,
resourceManagementProps: ResourceManagementProps,
onDeleteObject: (
objectWithContext: ObjectWithContext,
onDeleteObjects: (
objectWithContext: ObjectWithContext[],
cb: (boolean) => void
) => void,
onRenameObjectFolderOrObjectWithContextFinish: (
Expand Down Expand Up @@ -237,7 +239,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
objectsContainer,
resourceManagementProps,
onSelectAllInstancesOfObjectInLayout,
onDeleteObject,
onDeleteObjects,
onRenameObjectFolderOrObjectWithContextFinish,
selectedObjectFolderOrObjectsWithContext,
canInstallPrivateAsset,
Expand Down Expand Up @@ -441,55 +443,76 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
objectFolderOrObject,
global,
} = objectFolderOrObjectWithContext;

let objectsToDelete: gdObject[];
let folderToDelete: ?gdObjectFolderOrObject = null;
let message: MessageDescriptor;
let title: MessageDescriptor;

if (objectFolderOrObject.isFolder()) {
if (objectFolderOrObject.getChildrenCount() === 0) {
objectsToDelete = enumerateObjectsInFolder(objectFolderOrObject);
if (objectsToDelete.length === 0) {
// Folder is empty or contains only empty folders.
selectObjectFolderOrObjectWithContext(null);
objectFolderOrObject
.getParent()
.removeFolderChild(objectFolderOrObject);
forceUpdateList();
return;
}
return;

folderToDelete = objectFolderOrObject;
if (objectsToDelete.length === 1) {
message = t`Are you sure you want to remove this folder and with it the object ${objectsToDelete[0].getName()}? This can't be undone.`;
title = t`Remove folder and object`;
} else {
message = t`Are you sure you want to remove this folder and all its content (objects ${objectsToDelete
.map(object => object.getName())
.join(', ')})? This can't be undone.`;
title = t`Remove folder and objects`;
}
} else {
objectsToDelete = [objectFolderOrObject.getObject()];
message = t`Are you sure you want to remove this object? This can't be undone.`;
title = t`Remove object`;
}

const answer = await showDeleteConfirmation({
message: t`Are you sure you want to remove this object? This can't be undone.`,
title: t`Remove object`,
});
const answer = await showDeleteConfirmation({ message, title });
if (!answer) return;

const object = objectFolderOrObject.getObject();

const objectWithContext = {
const objectsWithContext = objectsToDelete.map(object => ({
object,
global,
};
}));

// TODO: Change selectedObjectFolderOrObjectWithContext so that it's easy
// to remove an item using keyboard only and to navigate with the arrow
// keys right after deleting it.
selectObjectFolderOrObjectWithContext(null);

// It's important to call onDeleteObject, because the parent might
// It's important to call onDeleteObjects, because the parent might
// have to do some refactoring/clean up work before the object is deleted
// (typically, the SceneEditor will remove instances referring to the object,
// leading to the removal of their renderer - which can keep a reference to
// the object).
onDeleteObject(objectWithContext, doRemove => {
onDeleteObjects(objectsWithContext, doRemove => {
if (!doRemove) return;
const container = global ? project : objectsContainer;
objectsToDelete.forEach(object => {
container.removeObject(object.getName());
});

if (global) {
project.removeObject(object.getName());
} else {
objectsContainer.removeObject(object.getName());
if (folderToDelete) {
folderToDelete.getParent().removeFolderChild(folderToDelete);
forceUpdateList();
}

onObjectModified(false);
});
},
[
objectsContainer,
onDeleteObject,
onDeleteObjects,
onObjectModified,
project,
forceUpdateList,
Expand Down Expand Up @@ -1192,6 +1215,33 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
]
);

/**
* Unselect item if one of the parent is collapsed (folded) so that the item
* does not stay selected and not visible to the user.
*/
const onCollapseItem = React.useCallback(
(item: TreeViewItem) => {
if (
!selectedObjectFolderOrObjectsWithContext ||
selectedObjectFolderOrObjectsWithContext.length !== 1 ||
item.isPlaceholder
)
return;
const { objectFolderOrObject: potentialParent } = item;
const {
objectFolderOrObject: selectedObjectFolderOrObject,
} = selectedObjectFolderOrObjectsWithContext[0];
if (!potentialParent || !selectedObjectFolderOrObject) return;
if (selectedObjectFolderOrObject.isADescendantOf(potentialParent)) {
selectObjectFolderOrObjectWithContext(null);
}
},
[
selectObjectFolderOrObjectWithContext,
selectedObjectFolderOrObjectsWithContext,
]
);

const moveObjectFolderOrObjectToAnotherFolderInSameContainer = React.useCallback(
(
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
Expand Down Expand Up @@ -1263,7 +1313,6 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
{
label: i18n._(t`Delete`),
click: () => deleteObjectFolderOrObjectWithContext(item),
enabled: objectFolderOrObject.getChildrenCount() === 0,
accelerator: 'Backspace',
},
{
Expand Down Expand Up @@ -1527,6 +1576,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>(
getItemHtmlId={getTreeViewItemHtmlId}
getItemDataset={getTreeViewItemData}
onEditItem={editItem}
onCollapseItem={onCollapseItem}
selectedItems={selectedObjectFolderOrObjectsWithContext}
onSelectItems={items => {
if (!items) selectObjectFolderOrObjectWithContext(null);
Expand Down
4 changes: 2 additions & 2 deletions newIDE/app/src/SceneEditor/EditorsDisplay.flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export type SceneEditorsDisplayProps = {|
?ObjectFolderOrObjectWithContext
) => void,
onExportObject: (object: ?gdObject) => void,
onDeleteObject: (
onDeleteObjects: (
i18n: I18nType,
objectWithContext: ObjectWithContext,
objectsWithContext: ObjectWithContext[],
cb: (boolean) => void
) => void,
onAddObjectInstance: (
Expand Down
4 changes: 2 additions & 2 deletions newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,8 +348,8 @@ const MosaicEditorsDisplay = React.forwardRef<
canInstallPrivateAsset={props.canInstallPrivateAsset}
onEditObject={props.onEditObject}
onExportObject={props.onExportObject}
onDeleteObject={(objectWithContext, cb) =>
props.onDeleteObject(i18n, objectWithContext, cb)
onDeleteObjects={(objectWithContext, cb) =>
props.onDeleteObjects(i18n, objectWithContext, cb)
}
getValidatedObjectOrGroupName={(newName, global) =>
props.getValidatedObjectOrGroupName(newName, global, i18n)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
canInstallPrivateAsset={props.canInstallPrivateAsset}
onEditObject={props.onEditObject}
onExportObject={props.onExportObject}
onDeleteObject={(objectWithContext, cb) =>
props.onDeleteObject(i18n, objectWithContext, cb)
onDeleteObjects={(objectWithContext, cb) =>
props.onDeleteObjects(i18n, objectWithContext, cb)
}
getValidatedObjectOrGroupName={(newName, global) =>
props.getValidatedObjectOrGroupName(
Expand Down
Loading

0 comments on commit 4ea6fb7

Please sign in to comment.