diff --git a/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js b/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js index cd72ee40d37a..412f6ed24c8c 100644 --- a/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js +++ b/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js @@ -1,7 +1,6 @@ // @flow import { type I18n as I18nType } from '@lingui/core'; import { t } from '@lingui/macro'; - import * as React from 'react'; import Clipboard from '../Utils/Clipboard'; import { SafeExtractor } from '../Utils/SafeExtractor'; @@ -24,6 +23,70 @@ import { exceptionallyGuardAgainstDeadObject } from '../Utils/IsNullPtr'; const gd: libGDevelop = global.gd; +// Builds a stable path key like "Root/Enemies/Boss" by walking up the folder tree. +// This key survives page reloads, unlike objectFolder.ptr which is a memory address. +const getFolderStableKey = ( + objectFolder: gdObjectFolderOrObject, + isGlobal: boolean +): string => { + const parts: Array = []; + let current = objectFolder; + try { + while (current && !current.isRootFolder()) { + parts.unshift(current.getFolderName()); + current = current.getParent(); + } + } catch (e) { + // fallback: if walking fails, use the folder name only + parts.unshift(objectFolder.getFolderName()); + } + const scope = isGlobal ? 'global' : 'scene'; + return scope + '/' + parts.join('/'); +}; + +export const folderColors = { + get(objectFolder: gdObjectFolderOrObject, isGlobal: boolean): string | null { + try { + const key = getFolderStableKey(objectFolder, isGlobal); + const saved = localStorage.getItem('gdevelop_custom_folder_colors'); + const colors: { [string]: string } = saved ? JSON.parse(saved) : {}; + return colors[key] || null; + } catch (e) { + return null; + } + }, + set(objectFolder: gdObjectFolderOrObject, isGlobal: boolean, color: string) { + try { + const key = getFolderStableKey(objectFolder, isGlobal); + const saved = localStorage.getItem('gdevelop_custom_folder_colors'); + const colors: { [string]: string } = saved ? JSON.parse(saved) : {}; + colors[key] = color; + localStorage.setItem( + 'gdevelop_custom_folder_colors', + JSON.stringify(colors) + ); + } catch (e) { + console.error('Error saving folder color:', e); + } + }, + remove(objectFolder: gdObjectFolderOrObject, isGlobal: boolean) { + try { + const key = getFolderStableKey(objectFolder, isGlobal); + const saved = localStorage.getItem('gdevelop_custom_folder_colors'); + if (saved) { + const colors: { [string]: string } = JSON.parse(saved); + delete colors[key]; + localStorage.setItem( + 'gdevelop_custom_folder_colors', + JSON.stringify(colors) + ); + } + } catch (e) { + console.error('Error removing folder color:', e); + } + }, +}; + export const expandAllSubfolders = ( objectFolder: gdObjectFolderOrObject, isGlobal: boolean, @@ -81,14 +144,15 @@ export type ObjectFolderTreeViewItemProps = {| forceUpdateList: () => void, forceUpdate: () => void, isListLocked: boolean, + openColorPicker: ( + objectFolder: gdObjectFolderOrObject, + isGlobal: boolean + ) => void, |}; export const getObjectFolderTreeViewItemId = ( objectFolder: gdObjectFolderOrObject ): string => { - // Use the ptr as id since two folders can have the same name. - // If using folder name, this would need for methods when renaming - // the folder to keep it open. return `object-folder-${objectFolder.ptr}`; }; @@ -139,14 +203,18 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { return false; } - getName(): string | React.Node { + _getFolderName(): string { + if (!exceptionallyGuardAgainstDeadObject(this.objectFolder)) return ''; + return this.objectFolder.getFolderName(); + } + + getName(): string { if (!exceptionallyGuardAgainstDeadObject(this.objectFolder)) return ''; return this.objectFolder.getFolderName(); } getId(): string { - // getObjectFolderTreeViewItemId only uses .ptr, so it's safe even if dead. - return getObjectFolderTreeViewItemId(this.objectFolder); + return getObjectFolderTreeViewItemId(this.objectFolder) || ''; } getHtmlId(index: number): ?string { @@ -155,13 +223,18 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { getDataSet(): ?HTMLDataset { if (!exceptionallyGuardAgainstDeadObject(this.objectFolder)) return null; + const color = folderColors.get(this.objectFolder, this._isGlobal); return { folderName: this.objectFolder.getFolderName(), global: this._isGlobal.toString(), + ...(color ? { folderColor: color } : {}), }; } - getThumbnail(): ?string { + const color = folderColors.get(this.objectFolder, this._isGlobal); + if (color) { + return 'NO_ICON'; + } return 'FOLDER'; } @@ -169,16 +242,21 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { rename(newName: string): void { const safeNewName = newName.replaceAll('/', '-'); - if (this.getName() === safeNewName) { + if (this._getFolderName() === safeNewName) { return; } - + // Save the current color before renaming (the key will change with the new name) + const currentColor = folderColors.get(this.objectFolder, this._isGlobal); this.props.onRenameObjectFolderOrObjectWithContextFinish( { objectFolderOrObject: this.objectFolder, global: this._isGlobal }, safeNewName, doRename => { if (!doRename) return; - + // After rename, the folder path key has changed: remove old key and save with new key + if (currentColor) { + folderColors.remove(this.objectFolder, this._isGlobal); + folderColors.set(this.objectFolder, this._isGlobal, currentColor); + } this.props.onObjectModified(false); } ); @@ -186,7 +264,11 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { edit(): void {} - _getPasteLabel( + _openColorPicker(): void { + this.props.openColorPicker(this.objectFolder, this._isGlobal); + } + + getPasteLabel( i18n: I18nType, { isGlobalObject, @@ -220,9 +302,11 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { const container = this._isGlobal ? globalObjectsContainer : objectsContainer; + if (!container) { return []; } + const folderAndPathsInContainer = enumerateFoldersInContainer(container); folderAndPathsInContainer.unshift({ path: i18n._(t`Root folder`), @@ -234,9 +318,10 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { !folderAndPath.folder.isADescendantOf(this.objectFolder) && folderAndPath.folder !== this.objectFolder ); + return [ { - label: this._getPasteLabel(i18n, { + label: this.getPasteLabel(i18n, { isGlobalObject: this._isGlobal, isFolder: true, }), @@ -249,6 +334,11 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { accelerator: 'F2', enabled: !isListLocked, }, + { + label: i18n._(t`Change folder color`), + click: () => this._openColorPicker(), + enabled: !isListLocked, + }, { label: i18n._(t`Delete`), click: () => this.delete(), @@ -281,7 +371,6 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { }); }, })), - { type: 'separator' }, { label: i18n._(t`Create new folder...`), @@ -350,9 +439,10 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { } = this.props; const objectsToDelete = enumerateObjectsInFolder(this.objectFolder); + if (objectsToDelete.length === 0) { - // Folder is empty or contains only empty folders. selectObjectFolderOrObjectWithContext(null); + folderColors.remove(this.objectFolder, this._isGlobal); this.objectFolder.getParent().removeFolderChild(this.objectFolder); forceUpdateList(); return; @@ -360,6 +450,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { let message: MessageDescriptor; let title: MessageDescriptor; + 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`; @@ -378,17 +469,9 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { global: this._isGlobal, })); - // 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); const folderToDelete = this.objectFolder; - // 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). onDeleteObjects(objectsWithContext, doRemove => { if (!doRemove) return; const container = this._isGlobal @@ -400,6 +483,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { }); } + folderColors.remove(folderToDelete, this._isGlobal); folderToDelete.getParent().removeFolderChild(folderToDelete); forceUpdateList(); @@ -408,7 +492,6 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { } copy(): void {} - cut(): void {} paste(): void { @@ -427,6 +510,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { clipboardContent, 'type' ); + if (!objectName || !objectType || !serializedObject) return; const { @@ -465,6 +549,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { onObjectModified(false); if (onObjectPasted) onObjectPasted(newObjectWithContext.object); + expandFolders([ { objectFolderOrObject: this.objectFolder, global: this._isGlobal }, ]); diff --git a/newIDE/app/src/ObjectsList/index.js b/newIDE/app/src/ObjectsList/index.js index 6f22aa5b438e..3b7eff5b358f 100644 --- a/newIDE/app/src/ObjectsList/index.js +++ b/newIDE/app/src/ObjectsList/index.js @@ -49,6 +49,7 @@ import { ObjectFolderTreeViewItemContent, getObjectFolderTreeViewItemId, expandAllSubfolders, + folderColors, type ObjectFolderTreeViewItemProps, } from './ObjectFolderTreeViewItemContent'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; @@ -78,6 +79,102 @@ const styles = { }, autoSizerContainer: { flex: 1 }, autoSizer: { width: '100%' }, + colorPickerCard: { + position: 'fixed', + zIndex: 1000, + background: '#1e1e1e', + borderRadius: 12, + border: '0.5px solid rgba(255,255,255,0.1)', + width: 300, + overflow: 'hidden', + boxShadow: '0 8px 32px rgba(0,0,0,0.6)', + userSelect: 'none', + }, + colorPickerHeader: { + padding: '14px 16px 12px', + borderBottom: '0.5px solid rgba(255,255,255,0.08)', + display: 'flex', + alignItems: 'center', + gap: 10, + cursor: 'grab', + }, + colorPickerBody: { + padding: 16, + }, + colorPickerGrid: { + display: 'grid', + gridTemplateColumns: 'repeat(7, 1fr)', + gap: 6, + marginBottom: 14, + }, + colorPickerHexRow: { + display: 'flex', + alignItems: 'center', + gap: 8, + marginBottom: 14, + background: 'rgba(255,255,255,0.05)', + border: '0.5px solid rgba(255,255,255,0.1)', + borderRadius: 8, + padding: '7px 10px', + }, + colorPickerHexDot: { + width: 22, + height: 22, + borderRadius: 4, + flexShrink: 0, + }, + colorPickerHexText: { + fontSize: 13, + color: 'rgba(255,255,255,0.45)', + fontFamily: 'monospace', + }, + colorPickerCustomBtn: { + marginLeft: 'auto', + display: 'flex', + alignItems: 'center', + gap: 5, + padding: '4px 10px', + borderRadius: 6, + border: '0.5px solid rgba(255,255,255,0.2)', + background: 'rgba(255,255,255,0.07)', + color: 'rgba(255,255,255,0.6)', + fontSize: 12, + cursor: 'pointer', + }, + colorPickerActions: { + display: 'flex', + gap: 8, + }, + colorPickerBtnConfirm: { + flex: 1, + padding: '8px', + borderRadius: 8, + border: 'none', + background: '#6d28d9', + color: 'white', + fontSize: 13, + fontWeight: 500, + cursor: 'pointer', + }, + colorPickerBtnReset: { + padding: '8px 14px', + borderRadius: 8, + border: '0.5px solid rgba(255,255,255,0.15)', + background: 'transparent', + color: 'rgba(255,255,255,0.5)', + fontSize: 20, + cursor: 'pointer', + lineHeight: 1, + }, + colorPickerBtnCancel: { + padding: '8px 14px', + borderRadius: 8, + border: '0.5px solid rgba(255,255,255,0.15)', + background: 'transparent', + color: 'rgba(255,255,255,0.5)', + fontSize: 13, + cursor: 'pointer', + }, }; export const getLabelsForObjectsAndGroupsLists = ( @@ -98,7 +195,7 @@ export const getLabelsForObjectsAndGroupsLists = ( } else if (scope.eventsBasedObject) { return { localScopeObjectsTitle: t`Object's children`, - higherScopeObjectsTitle: null, // Global objects not accessible from custom object. + higherScopeObjectsTitle: null, localScopeGroupsTitle: t`Object's groups`, higherScopeGroupsTitle: null, }; @@ -448,22 +545,7 @@ type Props = {| eventsFunctionsExtension: gdEventsFunctionsExtension | null, eventsBasedObject: gdEventsBasedObject | null, initialInstances?: gdInitialInstancesContainer, - /** The objects retrieved from ProjectScopedContainers must never be kept in a - * state as they may be temporary copies. - * It also contains "fake" objects like "Object" for the parent of custom objects. - * It's useful to check if an object name is taken, but not to edit ObjectsContainer. - * Also see `ProjectScopedContainers::MakeNewProjectScopedContainersForEventsBasedObject`. - * Search for "ProjectScopedContainers wrongly containing temporary objects containers or objects" - * in the codebase. - */ projectScopedContainersAccessor: ProjectScopedContainersAccessor, - - // These 2 containers always contains the "real" objects. - // TODO: they should be replaced by projectScopedContainersAccessor, but we can't use this - // as `ProjectScopedContainers` may return temporary objects that can't be edited or have references - // to them kept. - // Search for "ProjectScopedContainers wrongly containing temporary objects containers or objects" - // in the codebase. globalObjectsContainer: gdObjectsContainer | null, objectsContainer: gdObjectsContainer, @@ -522,6 +604,205 @@ type Props = {| isListLocked: boolean, |}; +// ─── Componente ColorPickerDialog ──────────────────────────────────────────── + +const PRESET_COLORS = [ + '#ef4444', + '#f97316', + '#eab308', + '#22c55e', + '#3b82f6', + '#a855f7', + '#ec4899', + '#14b8a6', + '#6366f1', + '#f43f5e', + '#84cc16', + '#06b6d4', + '#f59e0b', + '#78716c', +]; + +type ColorPickerDialogProps = {| + objectFolder: gdObjectFolderOrObject, + isGlobal: boolean, + onClose: () => void, + onForceUpdate: () => void, +|}; + +const ColorPickerDialog = ({ + objectFolder, + isGlobal, + onClose, + onForceUpdate, +}: ColorPickerDialogProps) => { + const currentColor = folderColors.get(objectFolder, isGlobal) || '#a855f7'; + const [selectedColor, setSelectedColor] = React.useState(currentColor); + const nativeInputRef = React.useRef(null); + + const [pos, setPos] = React.useState({ + x: window.innerWidth / 2 - 150, + y: window.innerHeight / 2 - 160, + }); + const dragging = React.useRef(false); + const dragOffset = React.useRef({ x: 0, y: 0 }); + + const onMouseDown = (e: MouseEvent) => { + dragging.current = true; + dragOffset.current = { x: e.clientX - pos.x, y: e.clientY - pos.y }; + e.preventDefault(); + }; + + React.useEffect(() => { + const onMouseMove = (e: MouseEvent) => { + if (!dragging.current) return; + setPos({ + x: e.clientX - dragOffset.current.x, + y: e.clientY - dragOffset.current.y, + }); + }; + const onMouseUp = () => { + dragging.current = false; + }; + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseUp); + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseUp); + }; + }, []); + + const handleConfirm = () => { + folderColors.set(objectFolder, isGlobal, selectedColor); + onForceUpdate(); + onClose(); + }; + + const handleReset = () => { + // Reset to default grey instead of removing, so the SVG folder icon is kept + folderColors.set(objectFolder, isGlobal, '#6b7280'); + onForceUpdate(); + onClose(); + }; + + const handleCustomPick = () => { + if (nativeInputRef.current) nativeInputRef.current.click(); + }; + + const folderName = objectFolder.getFolderName(); + + return ( +
e.stopPropagation()} + > +
+ + + + + + Folder color + + + {folderName} + +
+ +
+
+ {PRESET_COLORS.map(color => ( +
setSelectedColor(color)} + /> + ))} +
+ +
+
+ {selectedColor} + + setSelectedColor(e.target.value)} + style={{ + position: 'absolute', + opacity: 0, + width: 0, + height: 0, + pointerEvents: 'none', + }} + /> +
+ +
+ + + +
+
+
+ ); +}; +// ───────────────────────────────────────────────────────────────────────────── + const ObjectsList = React.forwardRef( ( { @@ -585,6 +866,13 @@ const ObjectsList = React.forwardRef( from: ObjectFolderOrObjectWithContext | null, } | null>(null); + // ── STATO COLOR PICKER ────────────────────────────────────────────────── + const [colorPickerOpen, setColorPickerOpen] = React.useState<{ + objectFolder: gdObjectFolderOrObject, + isGlobal: boolean, + } | null>(null); + // ──────────────────────────────────────────────────────────────────────── + React.useImperativeHandle(ref, () => ({ forceUpdateList: () => { forceUpdate(); @@ -603,10 +891,6 @@ const ObjectsList = React.forwardRef( setObjectAssetSwappingDialogOpen, ] = React.useState<{ objectWithContext: ObjectWithContext } | null>(null); - // Initialize keyboard shortcuts as empty. - // onDelete, onDuplicate and onRename callbacks are set in an effect because it applies - // to the selected item (that is a props). As it is stored in a ref, the keyboard shortcut - // instance does not update with selectedObjectFolderOrObjectsWithContext changes. const keyboardShortcutsRef = React.useRef( new KeyboardShortcuts({ shortcutCallbacks: {}, @@ -645,7 +929,6 @@ const ObjectsList = React.forwardRef( if ( newObjectDialogOpen && newObjectDialogOpen.from && - // If a scene objectFolderOrObject is selected, insert new object next to or inside it. !newObjectDialogOpen.from.global ) { const selectedItem = newObjectDialogOpen.from.objectFolderOrObject; @@ -689,16 +972,11 @@ const ObjectsList = React.forwardRef( if (treeViewRef.current) treeViewRef.current.openItems([sceneObjectsRootFolderId]); - // Scroll to the new object. - // Ideally, we'd wait for the list to be updated to scroll, but - // to simplify the code, we just wait a few ms for a new render - // to be done. setTimeout(() => { scrollToItem(getObjectTreeViewItemId(object)); - }, 100); // A few ms is enough for a new render to be done. + }, 100); setNewObjectDialogOpen(null); - // TODO Should it be called later? // $FlowFixMe[constant-condition] if (onEditObject) { onEditObject(object); @@ -729,9 +1007,6 @@ const ObjectsList = React.forwardRef( onObjectCreated(objects, isTheFirstOfItsTypeInProject); - // Here, the last object in the array might not be the last object - // in the tree view, given the fact that assets are added in parallel - // See (AssetPackInstallDialog.onInstallAssets). const lastObject = objects[objects.length - 1]; if (newObjectDialogOpen && newObjectDialogOpen.from) { @@ -750,13 +1025,9 @@ const ObjectsList = React.forwardRef( treeViewRef.current.openItems([sceneObjectsRootFolderId]); } } - // Scroll to the new object. - // Ideally, we'd wait for the list to be updated to scroll, but - // to simplify the code, we just wait a few ms for a new render - // to be done. setTimeout(() => { scrollToItem(getObjectTreeViewItemId(lastObject)); - }, 100); // A few ms is enough for a new render to be done. + }, 100); }, [onObjectCreated, scrollToItem, newObjectDialogOpen] ); @@ -775,6 +1046,15 @@ const ObjectsList = React.forwardRef( [] ); + // ── CALLBACK OPEN COLOR PICKER ────────────────────────────────────────── + const openColorPicker = React.useCallback( + (objectFolder: gdObjectFolderOrObject, isGlobal: boolean) => { + setColorPickerOpen({ objectFolder, isGlobal }); + }, + [] + ); + // ──────────────────────────────────────────────────────────────────────── + const onObjectModified = React.useCallback( (shouldForceUpdateList: boolean) => { if (unsavedChanges) unsavedChanges.triggerUnsavedChanges(); @@ -799,8 +1079,6 @@ const ObjectsList = React.forwardRef( const treeView = treeViewRef.current; if (treeView) { if (isMobile) { - // Position item at top of the screen to make sure it will be visible - // once the keyboard is open. treeView.scrollToItemFromId(itemId, 'start'); } treeView.renameItemFromId(itemId); @@ -827,10 +1105,9 @@ const ObjectsList = React.forwardRef( false ); if (firstClosedFolderIndex === -1) { - // If all parents are open, return the objectFolderOrObject given as input. return getTreeViewItemIdFromObjectFolderOrObject(objectFolderOrObject); } - // $FlowFixMe[incompatible-type] - We are confident this TreeView item is in fact a ObjectFolderOrObjectWithContext + // $FlowFixMe[incompatible-type] return topToBottomAscendanceId[firstClosedFolderIndex]; }; @@ -882,9 +1159,6 @@ const ObjectsList = React.forwardRef( ); if (!answer) return; - // It's safe to call moveObjectFolderOrObjectToAnotherContainerInFolder because - // it does not invalidate the references to the object in memory - so other editors - // like InstancesRenderer can continue to work. objectsContainer.moveObjectFolderOrObjectToAnotherContainerInFolder( objectFolderOrObject, globalObjectsContainer, @@ -905,13 +1179,9 @@ const ObjectsList = React.forwardRef( newObjectFolderOrObjectWithContext ); - // Scroll to the moved object. - // Ideally, we'd wait for the list to be updated to scroll, but - // to simplify the code, we just wait a few ms for a new render - // to be done. setTimeout(() => { scrollToItem(getObjectTreeViewItemId(object)); - }, 100); // A few ms is enough for a new render to be done. + }, 100); }, [ project, @@ -942,6 +1212,8 @@ const ObjectsList = React.forwardRef( objectFolderOrObject: newFolder, global, }; + // Assign default grey color to new folder + folderColors.set(newFolder, global, '#6b7280'); if (treeViewRef.current) { treeViewRef.current.openItems([ getObjectFolderTreeViewItemId(items[0].objectFolderOrObject), @@ -957,6 +1229,8 @@ const ObjectsList = React.forwardRef( objectFolderOrObject: newFolder, global, }; + // Assign default grey color to new folder + folderColors.set(newFolder, global, '#6b7280'); } } else { const rootFolder = objectsContainer.getRootFolder(); @@ -965,6 +1239,8 @@ const ObjectsList = React.forwardRef( objectFolderOrObject: newFolder, global: false, }; + // Assign default grey color to new folder + folderColors.set(newFolder, false, '#6b7280'); } selectObjectFolderOrObjectWithContext( newObjectFolderOrObjectWithContext @@ -1109,6 +1385,7 @@ const ObjectsList = React.forwardRef( forceUpdateList, forceUpdate, isListLocked, + openColorPicker, // <-- AGGIUNTO }), [ project, @@ -1129,6 +1406,7 @@ const ObjectsList = React.forwardRef( forceUpdateList, forceUpdate, isListLocked, + openColorPicker, // <-- AGGIUNTO ] ); @@ -1345,14 +1623,11 @@ const ObjectsList = React.forwardRef( selectedItems.length === 1 && !selectedItems[0].content.isGlobal() ) { - // In that case, the user is drag n dropping a scene object on the - // empty placeholder of the global objects section. const objectFolderOrObject = selectedItems[0].content.getObjectFolderOrObject(); return !!objectFolderOrObject && !objectFolderOrObject.isFolder(); } return false; } - // Check if at least one element in the selection can be moved. if ( selectedItems.every( selectedItem => @@ -1446,7 +1721,6 @@ const ObjectsList = React.forwardRef( return; } - // At this point, the move is done from within the same container. if ( selectedItem.content.isGlobal() === destinationItem.content.isGlobal() ) { @@ -1499,10 +1773,6 @@ const ObjectsList = React.forwardRef( [onObjectModified, selectedItems, setAsGlobalObject] ); - /** - * 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 (!selectedItems || selectedItems.length !== 1) return; @@ -1515,9 +1785,6 @@ const ObjectsList = React.forwardRef( [selectObjectFolderOrObjectWithContext, selectedItems] ); - // Force List component to be mounted again if project or objectsContainer - // has been changed. Avoid accessing to invalid objects that could - // crash the app. const listKey = project.ptr + ';' + objectsContainer.ptr; const initiallyOpenedNodeIds = [ globalObjectsRootFolder && globalObjectsRootFolder.getChildrenCount() > 0 @@ -1587,11 +1854,7 @@ const ObjectsList = React.forwardRef( ( )}
+ + {/* Dialog nuovi oggetti */} {newObjectDialogOpen && ( setNewObjectDialogOpen(null)} @@ -1659,6 +1924,8 @@ const ObjectsList = React.forwardRef( onExtensionInstalled={onExtensionInstalled} /> )} + + {/* Dialog swap asset */} {objectAssetSwappingDialogOpen && ( { @@ -1679,18 +1946,23 @@ const ObjectsList = React.forwardRef( onExtensionInstalled={onExtensionInstalled} /> )} + + {/* ── COLOR PICKER DIALOG ── */} + {colorPickerOpen && ( + setColorPickerOpen(null)} + onForceUpdate={forceUpdateList} + /> + )} + {/* ───────────────────────── */} ); } ); const arePropsEqual = (prevProps: Props, nextProps: Props): boolean => - // The component is costly to render, so avoid any re-rendering as much - // as possible. - // We make the assumption that no changes to objects list is made outside - // from the component. - // If a change is made, the component won't notice it: you have to manually - // call forceUpdate. prevProps.selectedObjectFolderOrObjectsWithContext === nextProps.selectedObjectFolderOrObjectsWithContext && prevProps.project === nextProps.project && diff --git a/newIDE/app/src/UI/TreeView/TreeViewRow.js b/newIDE/app/src/UI/TreeView/TreeViewRow.js index 9be47a172b6a..04ad8f3e0bbe 100644 --- a/newIDE/app/src/UI/TreeView/TreeViewRow.js +++ b/newIDE/app/src/UI/TreeView/TreeViewRow.js @@ -404,7 +404,25 @@ const TreeViewRow = (props: Props) => { /> )} - {node.thumbnailSrc && node.thumbnailSrc !== 'FOLDER' ? ( + {node.thumbnailSrc === 'NO_ICON' ? ( + node.dataset && node.dataset.folderColor ? ( + + + + + ) : null + ) : node.thumbnailSrc && node.thumbnailSrc !== 'FOLDER' ? (