From 0f82a6bc2e36fe3af630856f83e6aa884828bd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 23 Dec 2025 16:11:03 +0100 Subject: [PATCH 01/30] Add property list editor --- .../public/res/functions/boolean_black.svg | 19 + .../app/public/res/functions/number_black.svg | 8 + .../app/public/res/functions/string_black.svg | 7 + ...sBasedEntityPropertyTreeViewItemContent.js | 298 +++++++ .../PropertyListEditor/index.js | 802 ++++++++++++++++++ .../EventsFunctionsExtensionEditor/index.js | 35 +- newIDE/app/src/UI/ErrorBoundary.js | 1 + 7 files changed, 1166 insertions(+), 4 deletions(-) create mode 100644 newIDE/app/public/res/functions/boolean_black.svg create mode 100644 newIDE/app/public/res/functions/number_black.svg create mode 100644 newIDE/app/public/res/functions/string_black.svg create mode 100644 newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js create mode 100644 newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js diff --git a/newIDE/app/public/res/functions/boolean_black.svg b/newIDE/app/public/res/functions/boolean_black.svg new file mode 100644 index 000000000000..49644782bce9 --- /dev/null +++ b/newIDE/app/public/res/functions/boolean_black.svg @@ -0,0 +1,19 @@ + + + + + diff --git a/newIDE/app/public/res/functions/number_black.svg b/newIDE/app/public/res/functions/number_black.svg new file mode 100644 index 000000000000..106a24fb4f7a --- /dev/null +++ b/newIDE/app/public/res/functions/number_black.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/newIDE/app/public/res/functions/string_black.svg b/newIDE/app/public/res/functions/string_black.svg new file mode 100644 index 000000000000..e4999d431f0c --- /dev/null +++ b/newIDE/app/public/res/functions/string_black.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js new file mode 100644 index 000000000000..91579f4ef6c7 --- /dev/null +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -0,0 +1,298 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; +import { Trans } from '@lingui/macro'; + +import * as React from 'react'; +import newNameGenerator from '../../Utils/NewNameGenerator'; +import Clipboard from '../../Utils/Clipboard'; +import { SafeExtractor } from '../../Utils/SafeExtractor'; +import { + serializeToJSObject, + unserializeFromJSObject, +} from '../../Utils/Serializer'; +import { TreeViewItemContent, type TreeItemProps, scenesRootFolderId } from '.'; +import Tooltip from '@material-ui/core/Tooltip'; +import { type HTMLDataset } from '../../Utils/HTMLDataset'; +import VisibilityOffIcon from '../../UI/CustomSvgIcons/VisibilityOff'; + +const PROPERTIES_CLIPBOARD_KIND = 'Properties'; + +const styles = { + tooltip: { marginRight: 5, verticalAlign: 'bottom' }, +}; + +export type EventsBasedEntityPropertyTreeViewItemProps = {| + ...TreeItemProps, + properties: gdPropertiesContainer, + onOpenProperty: (name: string) => void, + onRenameProperty: (newName: string, oldName: string) => void, + showPropertyOverridingConfirmation: ( + existingPropertyNames: string[] + ) => Promise, + onPropertiesUpdated: () => void, +|}; + +export const getEventsBasedEntityPropertyTreeViewItemId = ( + property: gdNamedPropertyDescriptor +): string => { + // Pointers are used because they stay the same even when the names are + // changed. + return `property-${property.ptr}`; +}; + +export class EventsBasedEntityPropertyTreeViewItemContent + implements TreeViewItemContent { + property: gdNamedPropertyDescriptor; + props: EventsBasedEntityPropertyTreeViewItemProps; + + constructor( + property: gdNamedPropertyDescriptor, + props: EventsBasedEntityPropertyTreeViewItemProps + ) { + this.property = property; + this.props = props; + } + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + return itemContent.getId() === scenesRootFolderId; + } + + getRootId(): string { + return scenesRootFolderId; + } + + getName(): string | React.Node { + return this.property.getName(); + } + + getId(): string { + return getEventsBasedEntityPropertyTreeViewItemId(this.property); + } + + getHtmlId(index: number): ?string { + return `property-item-${index}`; + } + + getDataSet(): ?HTMLDataset { + return { + property: this.property.getName(), + }; + } + + getThumbnail(): ?string { + switch (this.property.getType()) { + case 'Number': + return 'res/functions/number_black.svg'; + case 'Boolean': + return 'res/functions/boolean_black.svg'; + case 'Behavior': + return 'res/functions/behavior_black.svg'; + default: + return 'res/functions/string_black.svg'; + } + } + + onClick(): void { + this.props.onOpenProperty(this.property.getName()); + } + + rename(newName: string): void { + const oldName = this.property.getName(); + if (oldName === newName) { + return; + } + this.props.onRenameProperty(oldName, newName); + } + + edit(): void { + this.props.editName(this.getId()); + } + + buildMenuTemplate(i18n: I18nType, index: number) { + return [ + { + label: i18n._(t`Rename`), + click: () => this.edit(), + accelerator: 'F2', + }, + { + label: i18n._(t`Delete`), + click: () => this.delete(), + accelerator: 'Backspace', + }, + { + type: 'separator', + }, + { + label: i18n._(t`Copy`), + click: () => this.copy(), + accelerator: 'CmdOrCtrl+C', + }, + { + label: i18n._(t`Cut`), + click: () => this.cut(), + accelerator: 'CmdOrCtrl+X', + }, + { + label: i18n._(t`Paste`), + enabled: Clipboard.has(PROPERTIES_CLIPBOARD_KIND), + click: () => this.paste(), + accelerator: 'CmdOrCtrl+V', + }, + { + label: i18n._(t`Duplicate`), + click: () => this._duplicate(), + }, + ]; + } + + renderRightComponent(i18n: I18nType): ?React.Node { + const icons = []; + if (this.property.isHidden()) { + icons.push( + This property won't be visible in the editor.} + > + + + ); + } + return icons.length > 0 ? icons : null; + } + + delete(): void { + this.props.properties.remove(this.property.getName()); + this._onProjectItemModified(); + } + + getIndex(): number { + return this.props.properties.getPosition(this.property); + } + + moveAt(destinationIndex: number): void { + const originIndex = this.getIndex(); + if (destinationIndex !== originIndex) { + this.props.properties.move( + originIndex, + // When moving the item down, it must not be counted. + destinationIndex + (destinationIndex <= originIndex ? 0 : -1) + ); + this._onProjectItemModified(); + } + } + + copy(): void { + Clipboard.set(PROPERTIES_CLIPBOARD_KIND, { + layout: serializeToJSObject(this.property), + name: this.property.getName(), + }); + } + + cut(): void { + this.copy(); + this.delete(); + } + + paste(): void { + this.pasteAsync(); + } + + async pasteAsync(): Promise { + if (!Clipboard.has(PROPERTIES_CLIPBOARD_KIND)) return; + + const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); + const propertyContents = SafeExtractor.extractArray(clipboardContent); + if (!propertyContents) return; + + const newNamedProperties: Array<{ + name: string, + serializedProperty: string, + }> = []; + const existingNamedProperties: Array<{ + name: string, + serializedProperty: string, + }> = []; + propertyContents.forEach(propertyContent => { + const name = SafeExtractor.extractStringProperty(propertyContent, 'name'); + const serializedProperty = SafeExtractor.extractObjectProperty( + propertyContent, + 'serializedProperty' + ); + if (!name || !serializedProperty) { + return; + } + + if (this.props.properties.has(name)) { + existingNamedProperties.push({ name, serializedProperty }); + } else { + newNamedProperties.push({ name, serializedProperty }); + } + }); + + let firstAddedPropertyName: string | null = null; + let index = this.getIndex() + 1; + newNamedProperties.forEach(({ name, serializedProperty }) => { + const property = this.props.properties.insertNew(name, index); + index++; + unserializeFromJSObject(property, serializedProperty); + if (!firstAddedPropertyName) { + firstAddedPropertyName = name; + } + }); + + let shouldOverrideProperties = false; + if (existingNamedProperties.length > 0) { + shouldOverrideProperties = await this.props.showPropertyOverridingConfirmation( + existingNamedProperties.map(namedProperty => namedProperty.name) + ); + + if (shouldOverrideProperties) { + existingNamedProperties.forEach(({ name, serializedProperty }) => { + if (this.props.properties.has(name)) { + const property = this.props.properties.get(name); + unserializeFromJSObject(property, serializedProperty); + } + }); + } + } + + this._onProjectItemModified(); + } + + _duplicate(): void { + const newName = newNameGenerator(this.property.getName(), name => + this.props.properties.has(name) + ); + const newProperty = this.props.properties.insertNew( + newName, + this.getIndex() + 1 + ); + + unserializeFromJSObject(newProperty, serializeToJSObject(this.property)); + newProperty.setName(newName); + + this._onProjectItemModified(); + this.props.editName( + getEventsBasedEntityPropertyTreeViewItemId(newProperty) + ); + } + + _onProjectItemModified() { + if (this.props.unsavedChanges) + this.props.unsavedChanges.triggerUnsavedChanges(); + this.props.forceUpdate(); + this.props.onPropertiesUpdated(); + } + + getRightButton(i18n: I18nType) { + return null; + } +} diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js new file mode 100644 index 000000000000..182dda39817e --- /dev/null +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -0,0 +1,802 @@ +// @flow +import { Trans } from '@lingui/macro'; +import { I18n } from '@lingui/react'; +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; + +import * as React from 'react'; +import SearchBar, { type SearchBarInterface } from '../../UI/SearchBar'; +import newNameGenerator from '../../Utils/NewNameGenerator'; +import UnsavedChangesContext, { + type UnsavedChanges, +} from '../../MainFrame/UnsavedChangesContext'; +import ErrorBoundary from '../../UI/ErrorBoundary'; +import useForceUpdate from '../../Utils/UseForceUpdate'; + +import { AutoSizer } from 'react-virtualized'; +import Background from '../../UI/Background'; +import TreeView, { + type TreeViewInterface, + type MenuButton, +} from '../../UI/TreeView'; +import PreferencesContext, { + type Preferences, +} from '../../MainFrame/Preferences/PreferencesContext'; +import { Column, Line } from '../../UI/Grid'; +import Add from '../../UI/CustomSvgIcons/Add'; +import InAppTutorialContext from '../../InAppTutorial/InAppTutorialContext'; +import { mapFor } from '../../Utils/MapFor'; +import KeyboardShortcuts from '../../UI/KeyboardShortcuts'; +import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; +import { + EventsBasedEntityPropertyTreeViewItemContent, + getEventsBasedEntityPropertyTreeViewItemId, + type EventsBasedEntityPropertyTreeViewItemProps, +} from './EventsBasedEntityPropertyTreeViewItemContent'; +import { type MenuItemTemplate } from '../../UI/Menu/Menu.flow'; +import useAlertDialog from '../../UI/Alert/useAlertDialog'; +import { type ShowConfirmDeleteDialogOptions } from '../../UI/Alert/AlertContext'; +import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; +import { type GDevelopTheme } from '../../UI/Theme'; +import { type HTMLDataset } from '../../Utils/HTMLDataset'; +import EmptyMessage from '../../UI/EmptyMessage'; +import { ColumnStackLayout } from '../../UI/Layout'; +import { useShouldAutofocusInput } from '../../UI/Responsive/ScreenTypeMeasurer'; + +export const getProjectManagerItemId = (identifier: string) => + `project-manager-tab-${identifier}`; + +const gameSettingsRootFolderId = getProjectManagerItemId('game-settings'); +const gamePropertiesItemId = getProjectManagerItemId('game-properties'); +export const scenesRootFolderId = getProjectManagerItemId('scenes'); +export const extensionsRootFolderId = getProjectManagerItemId('extensions'); +export const externalEventsRootFolderId = getProjectManagerItemId( + 'external-events' +); +export const externalLayoutsRootFolderId = getProjectManagerItemId( + 'external-layout' +); + +const scenesEmptyPlaceholderId = 'scenes-placeholder'; + +const styles = { + listContainer: { + flex: 1, + display: 'flex', + flexDirection: 'column', + padding: '0 8px 8px 8px', + }, + autoSizerContainer: { flex: 1 }, + autoSizer: { width: '100%' }, +}; + +const extensionItemReactDndType = 'GD_EXTENSION_ITEM'; + +export interface TreeViewItemContent { + getName(): string | React.Node; + getId(): string; + getHtmlId(index: number): ?string; + getDataSet(): ?HTMLDataset; + getThumbnail(): ?string; + onClick(): void; + buildMenuTemplate(i18n: I18nType, index: number): Array; + getRightButton(i18n: I18nType): ?MenuButton; + renderRightComponent(i18n: I18nType): ?React.Node; + rename(newName: string): void; + edit(): void; + delete(): void; + copy(): void; + paste(): void; + cut(): void; + getIndex(): number; + moveAt(destinationIndex: number): void; + isDescendantOf(itemContent: TreeViewItemContent): boolean; + getRootId(): string; +} + +interface TreeViewItem { + isRoot?: boolean; + isPlaceholder?: boolean; + +content: TreeViewItemContent; + getChildren(i18n: I18nType): ?Array; +} + +export type TreeItemProps = {| + forceUpdate: () => void, + forceUpdateList: () => void, + unsavedChanges?: ?UnsavedChanges, + preferences: Preferences, + gdevelopTheme: GDevelopTheme, + editName: (itemId: string) => void, + scrollToItem: (itemId: string) => void, + showDeleteConfirmation: ( + options: ShowConfirmDeleteDialogOptions + ) => Promise, +|}; + +class LeafTreeViewItem implements TreeViewItem { + content: TreeViewItemContent; + + constructor(content: TreeViewItemContent) { + this.content = content; + } + + getChildren(i18n: I18nType): ?Array { + return null; + } +} + +class PlaceHolderTreeViewItem implements TreeViewItem { + isPlaceholder = true; + content: TreeViewItemContent; + + constructor(id: string, label: string | React.Node) { + this.content = new LabelTreeViewItemContent(id, label); + } + + getChildren(i18n: I18nType): ?Array { + return null; + } +} + +class LabelTreeViewItemContent implements TreeViewItemContent { + id: string; + label: string | React.Node; + dataSet: { [string]: string }; + buildMenuTemplateFunction: ( + i18n: I18nType, + index: number + ) => Array; + rightButton: ?MenuButton; + + constructor( + id: string, + label: string | React.Node, + rightButton?: MenuButton + ) { + this.id = id; + this.label = label; + this.buildMenuTemplateFunction = (i18n: I18nType, index: number) => + rightButton + ? [ + { + id: rightButton.id, + label: rightButton.label, + click: rightButton.click, + }, + ] + : []; + this.rightButton = rightButton; + } + + getName(): string | React.Node { + return this.label; + } + + getId(): string { + return this.id; + } + + getRightButton(i18n: I18nType): ?MenuButton { + return this.rightButton; + } + + getHtmlId(index: number): ?string { + return this.id; + } + + getDataSet(): ?HTMLDataset { + return null; + } + + getThumbnail(): ?string { + return null; + } + + onClick(): void {} + + buildMenuTemplate(i18n: I18nType, index: number) { + return this.buildMenuTemplateFunction(i18n, index); + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return null; + } + + rename(newName: string): void {} + + edit(): void {} + + delete(): void {} + + copy(): void {} + + paste(): void {} + + cut(): void {} + + getIndex(): number { + return 0; + } + + moveAt(destinationIndex: number): void {} + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + return false; + } + + getRootId(): string { + return ''; + } +} + +class ActionTreeViewItemContent implements TreeViewItemContent { + id: string; + label: string | React.Node; + buildMenuTemplateFunction: ( + i18n: I18nType, + index: number + ) => Array; + thumbnail: ?string; + onClickCallback: () => void; + + constructor( + id: string, + label: string | React.Node, + onClickCallback: () => void, + thumbnail?: string + ) { + this.id = id; + this.label = label; + this.onClickCallback = onClickCallback; + this.thumbnail = thumbnail; + this.buildMenuTemplateFunction = (i18n: I18nType, index: number) => []; + } + + getName(): string | React.Node { + return this.label; + } + + getId(): string { + return this.id; + } + + getRightButton(i18n: I18nType): ?MenuButton { + return null; + } + + getEventsFunctionsContainer(): ?gdEventsFunctionsContainer { + return null; + } + + getHtmlId(index: number): ?string { + return this.id; + } + + getDataSet(): ?HTMLDataset { + return null; + } + + getThumbnail(): ?string { + return this.thumbnail; + } + + onClick(): void { + this.onClickCallback(); + } + + buildMenuTemplate(i18n: I18nType, index: number) { + return this.buildMenuTemplateFunction(i18n, index); + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return null; + } + + rename(newName: string): void {} + + edit(): void {} + + delete(): void {} + + copy(): void {} + + paste(): void {} + + cut(): void {} + + getIndex(): number { + return 0; + } + + moveAt(destinationIndex: number): void {} + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + return false; + } + + getRootId(): string { + return ''; + } +} + +const getTreeViewItemName = (item: TreeViewItem) => item.content.getName(); +const getTreeViewItemId = (item: TreeViewItem) => item.content.getId(); +const getTreeViewItemHtmlId = (item: TreeViewItem, index: number) => + item.content.getHtmlId(index); +const getTreeViewItemChildren = (i18n: I18nType) => (item: TreeViewItem) => + item.getChildren(i18n); +const getTreeViewItemThumbnail = (item: TreeViewItem) => + item.content.getThumbnail(); +const getTreeViewItemDataSet = (item: TreeViewItem) => + item.content.getDataSet(); +const buildMenuTemplate = (i18n: I18nType) => ( + item: TreeViewItem, + index: number +) => item.content.buildMenuTemplate(i18n, index); +const renderTreeViewItemRightComponent = (i18n: I18nType) => ( + item: TreeViewItem +) => item.content.renderRightComponent(i18n); +const renameItem = (item: TreeViewItem, newName: string) => { + item.content.rename(newName); +}; +const onClickItem = (item: TreeViewItem) => { + item.content.onClick(); +}; +const editItem = (item: TreeViewItem) => { + item.content.edit(); +}; +const deleteItem = (item: TreeViewItem) => { + item.content.delete(); +}; +const getTreeViewItemRightButton = (i18n: I18nType) => (item: TreeViewItem) => + item.content.getRightButton(i18n); + +export const usePropertyOverridingAlertDialog = () => { + const { showConfirmation } = useAlertDialog(); + return async (existingPropertyNames: Array): Promise => { + return await showConfirmation({ + title: t`Existing properties`, + message: t`These properties already exist:${'\n\n - ' + + existingPropertyNames.join('\n\n - ') + + '\n\n'}Do you want to replace them?`, + confirmButtonLabel: t`Replace`, + dismissButtonLabel: t`Omit`, + }); + }; +}; + +export type PropertyListEditorInterface = {| + forceUpdateList: () => void, + focusSearchBar: () => void, +|}; + +type Props = {| + eventsBasedBehavior: ?gdEventsBasedBehavior, + eventsBasedObject: ?gdEventsBasedObject, + onPropertiesUpdated: () => void, + onRenameProperty: (oldName: string, newName: string) => void, + onOpenProperty: (name: string) => void, +|}; + +const PropertyListEditor = React.forwardRef( + ( + { + eventsBasedBehavior, + eventsBasedObject, + onPropertiesUpdated, + onRenameProperty, + onOpenProperty, + }, + ref + ) => { + const [selectedItems, setSelectedItems] = React.useState< + Array + >([]); + const unsavedChanges = React.useContext(UnsavedChangesContext); + const { triggerUnsavedChanges } = unsavedChanges; + const preferences = React.useContext(PreferencesContext); + const gdevelopTheme = React.useContext(GDevelopThemeContext); + const { currentlyRunningInAppTutorial } = React.useContext( + InAppTutorialContext + ); + const treeViewRef = React.useRef>(null); + const forceUpdate = useForceUpdate(); + const { isMobile } = useResponsiveWindowSize(); + const { showDeleteConfirmation } = useAlertDialog(); + const showPropertyOverridingConfirmation = usePropertyOverridingAlertDialog(); + + const forceUpdateList = React.useCallback( + () => { + forceUpdate(); + if (treeViewRef.current) treeViewRef.current.forceUpdateList(); + }, + [forceUpdate] + ); + + const [searchText, setSearchText] = React.useState(''); + + const scrollToItem = React.useCallback((itemId: string) => { + if (treeViewRef.current) { + treeViewRef.current.scrollToItemFromId(itemId); + } + }, []); + + const searchBarRef = React.useRef(null); + + React.useImperativeHandle(ref, () => ({ + forceUpdateList: () => { + forceUpdate(); + if (treeViewRef.current) treeViewRef.current.forceUpdateList(); + }, + focusSearchBar: () => { + if (searchBarRef.current) searchBarRef.current.focus(); + }, + })); + + const onProjectItemModified = React.useCallback( + () => { + forceUpdate(); + triggerUnsavedChanges(); + }, + [forceUpdate, triggerUnsavedChanges] + ); + + const eventsBasedEntity = eventsBasedBehavior || eventsBasedObject; + const properties = eventsBasedEntity + ? eventsBasedEntity.getPropertyDescriptors() + : null; + + const editName = React.useCallback( + (itemId: string) => { + 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); + } + }, + [isMobile] + ); + + const addNewScene = React.useCallback( + (index: number, i18n: I18nType) => { + if (!properties) return; + + const newName = newNameGenerator(i18n._(t`Property`), name => + properties.has(name) + ); + const property = properties.insertNew(newName, index); + property.setType('Number'); + + onPropertiesUpdated(); + + onProjectItemModified(); + setSearchText(''); + + const sceneItemId = getEventsBasedEntityPropertyTreeViewItemId( + property + ); + if (treeViewRef.current) { + treeViewRef.current.openItems([sceneItemId, scenesRootFolderId]); + } + // Scroll to the new behavior. + // 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(sceneItemId); + }, 100); // A few ms is enough for a new render to be done. + + // We focus it so the user can edit the name directly. + editName(sceneItemId); + }, + [ + properties, + onPropertiesUpdated, + onProjectItemModified, + editName, + scrollToItem, + ] + ); + + const onTreeModified = React.useCallback( + (shouldForceUpdateList: boolean) => { + triggerUnsavedChanges(); + + if (shouldForceUpdateList) forceUpdateList(); + else forceUpdate(); + }, + [forceUpdate, forceUpdateList, triggerUnsavedChanges] + ); + + // Initialize keyboard shortcuts as empty. + // onDelete callback is set outside because it deletes the selected + // item (that is a props). As it is stored in a ref, the keyboard shortcut + // instance does not update with selectedItems changes. + const keyboardShortcutsRef = React.useRef( + new KeyboardShortcuts({ + shortcutCallbacks: {}, + }) + ); + React.useEffect( + () => { + if (keyboardShortcutsRef.current) { + keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => { + if (selectedItems.length > 0) { + deleteItem(selectedItems[0]); + } + }); + keyboardShortcutsRef.current.setShortcutCallback('onRename', () => { + if (selectedItems.length > 0) { + editName(selectedItems[0].content.getId()); + } + }); + keyboardShortcutsRef.current.setShortcutCallback('onCopy', () => { + if (selectedItems.length > 0) { + selectedItems[0].content.copy(); + } + }); + keyboardShortcutsRef.current.setShortcutCallback('onPaste', () => { + if (selectedItems.length > 0) { + selectedItems[0].content.paste(); + } + }); + keyboardShortcutsRef.current.setShortcutCallback('onCut', () => { + if (selectedItems.length > 0) { + selectedItems[0].content.cut(); + } + }); + } + }, + [editName, selectedItems] + ); + + const propertiesTreeViewItemProps = React.useMemo( + () => + properties + ? { + unsavedChanges, + preferences, + gdevelopTheme, + forceUpdate, + forceUpdateList, + showDeleteConfirmation, + showPropertyOverridingConfirmation, + editName, + scrollToItem, + properties, + onOpenProperty, + onPropertiesUpdated, + onRenameProperty, + } + : null, + [ + unsavedChanges, + preferences, + gdevelopTheme, + forceUpdate, + forceUpdateList, + showDeleteConfirmation, + showPropertyOverridingConfirmation, + editName, + scrollToItem, + properties, + onOpenProperty, + onPropertiesUpdated, + onRenameProperty, + ] + ); + + const getTreeViewData = React.useCallback( + (i18n: I18nType): Array => { + return !properties || !propertiesTreeViewItemProps + ? [] + : [ + new LeafTreeViewItem( + new ActionTreeViewItemContent( + gamePropertiesItemId, + i18n._(t`Configuration`), + // TODO Scroll to the configuration + () => {}, + 'res/icons_default/properties_black.svg' + ) + ), + { + isRoot: true, + content: new LabelTreeViewItemContent( + scenesRootFolderId, + i18n._(t`Behavior properties`), + { + icon: , + label: i18n._(t`Add a property`), + click: () => { + // TODO Add after selected scene? + const index = properties.getCount() - 1; + addNewScene(index, i18n); + }, + id: 'add-property', + } + ), + getChildren(i18n: I18nType): ?Array { + if (properties.getCount() === 0) { + return [ + new PlaceHolderTreeViewItem( + scenesEmptyPlaceholderId, + i18n._(t`Start by adding a new property.`) + ), + ]; + } + return mapFor( + 0, + properties.getCount(), + i => + new LeafTreeViewItem( + new EventsBasedEntityPropertyTreeViewItemContent( + properties.getAt(i), + propertiesTreeViewItemProps + ) + ) + ); + }, + }, + ]; + }, + [addNewScene, properties, propertiesTreeViewItemProps] + ); + + const canMoveSelectionTo = React.useCallback( + (destinationItem: TreeViewItem, where: 'before' | 'inside' | 'after') => + selectedItems.every(item => { + return ( + // Project and game settings children `getRootId` return an empty string. + item.content.getRootId().length > 0 && + item.content.getRootId() === destinationItem.content.getRootId() + ); + }), + [selectedItems] + ); + + const moveSelectionTo = React.useCallback( + ( + i18n: I18nType, + destinationItem: TreeViewItem, + where: 'before' | 'inside' | 'after' + ) => { + if (selectedItems.length === 0) { + return; + } + const selectedItem = selectedItems[0]; + selectedItem.content.moveAt( + destinationItem.content.getIndex() + (where === 'after' ? 1 : 0) + ); + onTreeModified(true); + }, + [onTreeModified, selectedItems] + ); + + /** + * 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.length !== 1 || item.isPlaceholder) { + return; + } + if (selectedItems[0].content.isDescendantOf(item.content)) { + setSelectedItems([]); + } + }, + [selectedItems] + ); + + // Force List component to be mounted again if project + // has been changed. Avoid accessing to invalid objects that could + // crash the app. + const listKey = eventsBasedEntity + ? eventsBasedEntity.ptr + : 'no-eventsBasedEntity'; + const initiallyOpenedNodeIds = [ + gameSettingsRootFolderId, + scenesRootFolderId, + extensionsRootFolderId, + externalEventsRootFolderId, + externalLayoutsRootFolderId, + ]; + + return ( + + + + + + {}} + onChange={setSearchText} + placeholder={t`Search in properties`} + /> + + + + {({ i18n }) => ( +
+ + {({ height }) => ( + { + const itemToSelect = items[0]; + if (!itemToSelect) return; + if (itemToSelect.isRoot) return; + setSelectedItems(items); + }} + onClickItem={onClickItem} + onRenameItem={renameItem} + buildMenuTemplate={buildMenuTemplate(i18n)} + getItemRightButton={getTreeViewItemRightButton(i18n)} + renderRightComponent={renderTreeViewItemRightComponent( + i18n + )} + onMoveSelectionToItem={(destinationItem, where) => + moveSelectionTo(i18n, destinationItem, where) + } + canMoveSelectionToItem={canMoveSelectionTo} + reactDndType={extensionItemReactDndType} + initiallyOpenedNodeIds={initiallyOpenedNodeIds} + forceDefaultDraggingPreview + shouldHideMenuIcon={item => !item.content.getRootId()} + /> + )} + +
+ )} +
+
+
+
+ ); + } +); + +const PropertyListEditorWithErrorBoundary = React.forwardRef< + Props, + PropertyListEditorInterface +>((props, ref) => ( + Property list editor} + scope="property-list-editor" + > + + +)); + +export default PropertyListEditorWithErrorBoundary; diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 94b7208ae50e..fe6effad69cc 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -43,6 +43,7 @@ import newNameGenerator from '../Utils/NewNameGenerator'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog'; import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton'; +import PropertyListEditor from './PropertyListEditor'; const gd: libGDevelop = global.gd; @@ -359,7 +360,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< selectedEventsFunction && !selectedEventsFunction.getEvents().getEventsCount() ) { - this._editorNavigator.openEditor('parameters'); + //this._editorNavigator.openEditor('parameters'); } else { this._editorNavigator.openEditor('events-sheet'); } @@ -641,7 +642,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< this.updateToolbar(); if (selectedEventsBasedBehavior) { if (this._editorMosaic) { - this._editorMosaic.collapseEditor('parameters'); + //this._editorMosaic.collapseEditor('parameters'); } if (this._editorNavigator) { this._editorNavigator.openEditor('events-sheet'); @@ -668,7 +669,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< this.updateToolbar(); if (selectedEventsBasedObject) { if (this._editorMosaic) { - this._editorMosaic.collapseEditor('parameters'); + //this._editorMosaic.collapseEditor('parameters'); } if (this._editorNavigator) this._editorNavigator.openEditor('events-sheet'); @@ -1361,7 +1362,9 @@ export default class EventsFunctionsExtensionEditor extends React.Component< const editors = { parameters: { type: 'primary', - title: t`Function Configuration`, + title: selectedEventsFunction + ? t`Function Configuration` + : t`Properties`, toolbarControls: [], renderEditor: () => ( @@ -1420,6 +1423,30 @@ export default class EventsFunctionsExtensionEditor extends React.Component< unsavedChanges={this.props.unsavedChanges} getFunctionGroupNames={this._getFunctionGroupNames} /> + ) : selectedEventsBasedObject || selectedEventsBasedBehavior ? ( + {}} + onRenameProperty={(oldName, newName) => { + if (selectedEventsBasedBehavior) { + this._onBehaviorPropertyRenamed( + selectedEventsBasedBehavior, + oldName, + newName + ); + } else if (selectedEventsBasedObject) { + this._onObjectPropertyRenamed( + selectedEventsBasedObject, + oldName, + newName + ); + } + }} + // TODO Scroll to the property + onOpenProperty={() => {}} + /> ) : ( diff --git a/newIDE/app/src/UI/ErrorBoundary.js b/newIDE/app/src/UI/ErrorBoundary.js index 81316f9383ff..b3d0c3bf1cf1 100644 --- a/newIDE/app/src/UI/ErrorBoundary.js +++ b/newIDE/app/src/UI/ErrorBoundary.js @@ -58,6 +58,7 @@ type ErrorBoundaryScope = | 'debugger' | 'resources' | 'extension-editor' + | 'property-list-editor' | 'extensions-search-dialog' | 'external-events-editor' | 'external-layout-editor' From 43640f5b2ce5e77475b4b0ba0001f4e367eea04e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Tue, 23 Dec 2025 18:47:01 +0100 Subject: [PATCH 02/30] Scroll at selection --- .../EventsBasedBehaviorEditorPanel.js | 192 +- .../EventsBasedBehaviorPropertiesEditor.js | 1796 ++++++++--------- .../src/EventsBasedBehaviorEditor/index.js | 273 ++- .../EventsFunctionsExtensionEditor/index.js | 16 +- 4 files changed, 1085 insertions(+), 1192 deletions(-) diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js index 2f9d50f0b201..6fed5321478e 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js @@ -2,15 +2,18 @@ import { Trans } from '@lingui/macro'; import * as React from 'react'; import EventsBasedBehaviorEditor from './index'; -import { Tabs } from '../UI/Tabs'; -import EventsBasedBehaviorPropertiesEditor from './EventsBasedBehaviorPropertiesEditor'; +import { + EventsBasedBehaviorPropertiesEditor, + type EventsBasedBehaviorPropertiesEditorInterface, +} from './EventsBasedBehaviorPropertiesEditor'; import Background from '../UI/Background'; import { Column, Line } from '../UI/Grid'; import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExtensionEditor'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; - -type TabName = 'configuration' | 'behavior-properties' | 'scene-properties'; +import Text from '../UI/Text'; +import { ColumnStackLayout } from '../UI/Layout'; +import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView'; type Props = {| project: gdProject, @@ -25,92 +28,103 @@ type Props = {| onConfigurationUpdated?: (?ExtensionItemConfigurationAttribute) => void, |}; -export default function EventsBasedBehaviorEditorPanel({ - eventsBasedBehavior, - eventsFunctionsExtension, - project, - projectScopedContainersAccessor, - onRenameProperty, - onRenameSharedProperty, - onPropertyTypeChanged, - unsavedChanges, - onEventsFunctionsAdded, - onConfigurationUpdated, -}: Props) { - const [currentTab, setCurrentTab] = React.useState('configuration'); +export type EventsBasedBehaviorEditorPanelInterface = {| + scrollToProperty: (propertyName: string) => void, +|}; + +export const EventsBasedBehaviorEditorPanel = React.forwardRef< + Props, + EventsBasedBehaviorEditorPanelInterface +>( + ( + { + eventsBasedBehavior, + eventsFunctionsExtension, + project, + projectScopedContainersAccessor, + onRenameProperty, + onRenameSharedProperty, + onPropertyTypeChanged, + unsavedChanges, + onEventsFunctionsAdded, + onConfigurationUpdated, + }: Props, + ref + ) => { + const onPropertiesUpdated = React.useCallback( + () => { + if (unsavedChanges) { + unsavedChanges.triggerUnsavedChanges(); + } + }, + [unsavedChanges] + ); + + const scrollView = React.useRef(null); + const propertiesEditor = React.useRef( + null + ); - const onPropertiesUpdated = React.useCallback( - () => { - if (unsavedChanges) { - unsavedChanges.triggerUnsavedChanges(); + const scrollToProperty = React.useCallback((propertyName: string) => { + if (scrollView.current && propertiesEditor.current) { + scrollView.current.scrollTo( + propertiesEditor.current.getPropertyEditorRef(propertyName) + ); } - }, - [unsavedChanges] - ); + }, []); - return ( - - - - - Configuration, - }, - { - value: 'behavior-properties', - label: Behavior properties, - }, - { - value: 'scene-properties', - label: Scene properties, - }, - ]} + React.useImperativeHandle(ref, () => ({ + scrollToProperty, + })); + + return ( + + + + + Configuration + + + + Behavior properties + + + + Scene properties + + - - - {currentTab === 'configuration' && ( - - )} - {currentTab === 'behavior-properties' && ( - - )} - {currentTab === 'scene-properties' && ( - - )} - - - ); -} + + + + ); + } +); diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js index e93323d3e9b5..db35ec7c54c3 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js @@ -19,7 +19,6 @@ import ChoicesEditor, { type Choice } from '../ChoicesEditor'; import ColorField from '../UI/ColorField'; import BehaviorTypeSelector from '../BehaviorTypeSelector'; import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete'; -import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView'; import ThreeDotsMenu from '../UI/CustomSvgIcons/ThreeDotsMenu'; import { getMeasurementUnitShortLabel } from '../PropertiesEditor/PropertiesMapToSchema'; import Add from '../UI/CustomSvgIcons/Add'; @@ -47,10 +46,6 @@ const gd: libGDevelop = global.gd; const PROPERTIES_CLIPBOARD_KIND = 'Properties'; -const DragSourceAndDropTarget = makeDragSourceAndDropTarget( - 'behavior-properties-list' -); - const styles = { rowContainer: { display: 'flex', @@ -133,987 +128,866 @@ const getChoicesArray = ( })); }; -export default function EventsBasedBehaviorPropertiesEditor({ - project, - projectScopedContainersAccessor, - extension, - eventsBasedBehavior, - properties, - isSceneProperties, - onPropertiesUpdated, - onRenameProperty, - onPropertyTypeChanged, - onEventsFunctionsAdded, - behaviorObjectType, -}: Props) { - const scrollView = React.useRef(null); - const [ - justAddedPropertyName, - setJustAddedPropertyName, - ] = React.useState(null); - const justAddedPropertyElement = React.useRef(null); - - React.useEffect( - () => { - if ( - scrollView.current && - justAddedPropertyElement.current && - justAddedPropertyName - ) { - scrollView.current.scrollTo(justAddedPropertyElement.current); - setJustAddedPropertyName(null); - justAddedPropertyElement.current = null; - } - }, - [justAddedPropertyName] - ); - - const draggedProperty = React.useRef(null); - - const gdevelopTheme = React.useContext(GDevelopThemeContext); - - const showPropertyOverridingConfirmation = usePropertyOverridingAlertDialog(); - - const forceUpdate = useForceUpdate(); - - const [searchText, setSearchText] = React.useState(''); - const [ - searchMatchingPropertyNames, - setSearchMatchingPropertyNames, - ] = React.useState>([]); +export type EventsBasedBehaviorPropertiesEditorInterface = {| + getPropertyEditorRef: (propertyName: string) => React.ElementRef, +|}; - const triggerSearch = React.useCallback( - () => { - const matchingPropertyNames = mapVector( - properties, - (property: gdNamedPropertyDescriptor, i: number) => { - const lowerCaseSearchText = searchText.toLowerCase(); - return property - .getName() - .toLowerCase() - .includes(lowerCaseSearchText) || - property - .getLabel() +export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< + Props, + EventsBasedBehaviorPropertiesEditorInterface +>( + ( + { + project, + projectScopedContainersAccessor, + extension, + eventsBasedBehavior, + properties, + isSceneProperties, + onPropertiesUpdated, + onRenameProperty, + onPropertyTypeChanged, + onEventsFunctionsAdded, + behaviorObjectType, + }: Props, + ref + ) => { + const propertyRefs = React.useRef(new Map>()); + React.useImperativeHandle(ref, () => ({ + getPropertyEditorRef: (propertyName: string) => { + return propertyRefs ? propertyRefs.current.get(propertyName) : null; + }, + })); + + const draggedProperty = React.useRef(null); + + const gdevelopTheme = React.useContext(GDevelopThemeContext); + + const showPropertyOverridingConfirmation = usePropertyOverridingAlertDialog(); + + const forceUpdate = useForceUpdate(); + + const [searchText, setSearchText] = React.useState(''); + const [ + searchMatchingPropertyNames, + setSearchMatchingPropertyNames, + ] = React.useState>([]); + + const triggerSearch = React.useCallback( + () => { + const matchingPropertyNames = mapVector( + properties, + (property: gdNamedPropertyDescriptor, i: number) => { + const lowerCaseSearchText = searchText.toLowerCase(); + return property + .getName() .toLowerCase() .includes(lowerCaseSearchText) || - property - .getGroup() - .toLowerCase() - .includes(lowerCaseSearchText) - ? property.getName() - : null; + property + .getLabel() + .toLowerCase() + .includes(lowerCaseSearchText) || + property + .getGroup() + .toLowerCase() + .includes(lowerCaseSearchText) + ? property.getName() + : null; + } + ).filter(Boolean); + setSearchMatchingPropertyNames(matchingPropertyNames); + }, + [properties, searchText] + ); + + React.useEffect( + () => { + if (searchText) { + triggerSearch(); + } else { + setSearchMatchingPropertyNames([]); } - ).filter(Boolean); - setSearchMatchingPropertyNames(matchingPropertyNames); - }, - [properties, searchText] - ); - - React.useEffect( - () => { - if (searchText) { - triggerSearch(); - } else { - setSearchMatchingPropertyNames([]); - } - }, - [searchText, triggerSearch] - ); - - const addPropertyAt = React.useCallback( - (index: number) => { - const newName = newNameGenerator('Property', name => - properties.has(name) - ); - const property = properties.insertNew(newName, index); - property.setType('Number'); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - setJustAddedPropertyName(newName); - setSearchText(''); - }, - [forceUpdate, onPropertiesUpdated, properties] - ); - - const addProperty = React.useCallback( - () => { - addPropertyAt(properties.getCount()); - }, - [addPropertyAt, properties] - ); - - const removeProperty = React.useCallback( - (name: string) => { - properties.remove(name); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated, properties] - ); - - const copyProperty = React.useCallback( - (property: gdNamedPropertyDescriptor) => { - Clipboard.set(PROPERTIES_CLIPBOARD_KIND, [ - { - name: property.getName(), - serializedProperty: serializeToJSObject(property), - }, - ]); - forceUpdate(); - }, - [forceUpdate] - ); - - const duplicateProperty = React.useCallback( - (property: gdNamedPropertyDescriptor, index: number) => { - const newName = newNameGenerator(property.getName(), name => - properties.has(name) - ); - - const newProperty = properties.insertNew(newName, index); - - unserializeFromJSObject(newProperty, serializeToJSObject(property)); - newProperty.setName(newName); - - forceUpdate(); - }, - [forceUpdate, properties] - ); - - const pasteProperties = React.useCallback( - async propertyInsertionIndex => { - const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); - const propertyContents = SafeExtractor.extractArray(clipboardContent); - if (!propertyContents) return; - - const newNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - const existingNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - propertyContents.forEach(propertyContent => { - const name = SafeExtractor.extractStringProperty( - propertyContent, - 'name' + }, + [searchText, triggerSearch] + ); + + const addPropertyAt = React.useCallback( + (index: number) => { + const newName = newNameGenerator('Property', name => + properties.has(name) ); - const serializedProperty = SafeExtractor.extractObjectProperty( - propertyContent, - 'serializedProperty' + const property = properties.insertNew(newName, index); + property.setType('Number'); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + //setJustAddedPropertyName(newName); + setSearchText(''); + }, + [forceUpdate, onPropertiesUpdated, properties] + ); + + const addProperty = React.useCallback( + () => { + addPropertyAt(properties.getCount()); + }, + [addPropertyAt, properties] + ); + + const removeProperty = React.useCallback( + (name: string) => { + properties.remove(name); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + }, + [forceUpdate, onPropertiesUpdated, properties] + ); + + const copyProperty = React.useCallback( + (property: gdNamedPropertyDescriptor) => { + Clipboard.set(PROPERTIES_CLIPBOARD_KIND, [ + { + name: property.getName(), + serializedProperty: serializeToJSObject(property), + }, + ]); + forceUpdate(); + }, + [forceUpdate] + ); + + const duplicateProperty = React.useCallback( + (property: gdNamedPropertyDescriptor, index: number) => { + const newName = newNameGenerator(property.getName(), name => + properties.has(name) ); - if (!name || !serializedProperty) { - return; - } - if (properties.has(name)) { - existingNamedProperties.push({ name, serializedProperty }); - } else { - newNamedProperties.push({ name, serializedProperty }); - } - }); + const newProperty = properties.insertNew(newName, index); - let firstAddedPropertyName: string | null = null; - let index = propertyInsertionIndex; - newNamedProperties.forEach(({ name, serializedProperty }) => { - const property = properties.insertNew(name, index); - index++; - unserializeFromJSObject(property, serializedProperty); - if (!firstAddedPropertyName) { - firstAddedPropertyName = name; - } - }); + unserializeFromJSObject(newProperty, serializeToJSObject(property)); + newProperty.setName(newName); - let shouldOverrideProperties = false; - if (existingNamedProperties.length > 0) { - shouldOverrideProperties = await showPropertyOverridingConfirmation( - existingNamedProperties.map(namedProperty => namedProperty.name) - ); - - if (shouldOverrideProperties) { - existingNamedProperties.forEach(({ name, serializedProperty }) => { - if (properties.has(name)) { - const property = properties.get(name); - unserializeFromJSObject(property, serializedProperty); - } - }); + forceUpdate(); + }, + [forceUpdate, properties] + ); + + const pasteProperties = React.useCallback( + async propertyInsertionIndex => { + const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); + const propertyContents = SafeExtractor.extractArray(clipboardContent); + if (!propertyContents) return; + + const newNamedProperties: Array<{ + name: string, + serializedProperty: string, + }> = []; + const existingNamedProperties: Array<{ + name: string, + serializedProperty: string, + }> = []; + propertyContents.forEach(propertyContent => { + const name = SafeExtractor.extractStringProperty( + propertyContent, + 'name' + ); + const serializedProperty = SafeExtractor.extractObjectProperty( + propertyContent, + 'serializedProperty' + ); + if (!name || !serializedProperty) { + return; + } + + if (properties.has(name)) { + existingNamedProperties.push({ name, serializedProperty }); + } else { + newNamedProperties.push({ name, serializedProperty }); + } + }); + + let firstAddedPropertyName: string | null = null; + let index = propertyInsertionIndex; + newNamedProperties.forEach(({ name, serializedProperty }) => { + const property = properties.insertNew(name, index); + index++; + unserializeFromJSObject(property, serializedProperty); + if (!firstAddedPropertyName) { + firstAddedPropertyName = name; + } + }); + + let shouldOverrideProperties = false; + if (existingNamedProperties.length > 0) { + shouldOverrideProperties = await showPropertyOverridingConfirmation( + existingNamedProperties.map(namedProperty => namedProperty.name) + ); + + if (shouldOverrideProperties) { + existingNamedProperties.forEach(({ name, serializedProperty }) => { + if (properties.has(name)) { + const property = properties.get(name); + unserializeFromJSObject(property, serializedProperty); + } + }); + } } - } - - setSearchText(''); - forceUpdate(); - if (firstAddedPropertyName) { - setJustAddedPropertyName(firstAddedPropertyName); - } else if (existingNamedProperties.length === 1) { - setJustAddedPropertyName(existingNamedProperties[0].name); - } - if (firstAddedPropertyName || shouldOverrideProperties) { - if (onPropertiesUpdated) onPropertiesUpdated(); - } - }, - [ - forceUpdate, - properties, - showPropertyOverridingConfirmation, - onPropertiesUpdated, - ] - ); - - const pastePropertiesAtTheEnd = React.useCallback( - async () => { - await pasteProperties(properties.getCount()); - }, - [properties, pasteProperties] - ); - - const pastePropertiesBefore = React.useCallback( - async (property: gdNamedPropertyDescriptor) => { - await pasteProperties(properties.getPosition(property)); - }, - [properties, pasteProperties] - ); - - const moveProperty = React.useCallback( - (oldIndex: number, newIndex: number) => { - properties.move(oldIndex, newIndex); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated, properties] - ); - - const movePropertyBefore = React.useCallback( - (targetProperty: gdNamedPropertyDescriptor) => { - const { current } = draggedProperty; - if (!current) return; - - const draggedIndex = properties.getPosition(current); - const targetIndex = properties.getPosition(targetProperty); - - properties.move( - draggedIndex, - targetIndex > draggedIndex ? targetIndex - 1 : targetIndex - ); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [properties, forceUpdate, onPropertiesUpdated] - ); - const setChoices = React.useCallback( - (property: gdNamedPropertyDescriptor) => { - return (choices: Array) => { - property.clearChoices(); - for (const choice of choices) { - property.addChoice(choice.value, choice.label); + setSearchText(''); + forceUpdate(); + if (firstAddedPropertyName) { + //setJustAddedPropertyName(firstAddedPropertyName); + } else if (existingNamedProperties.length === 1) { + //setJustAddedPropertyName(existingNamedProperties[0].name); } - if ( - !getChoicesArray(property).some( - choice => choice.value === property.getValue() - ) - ) { - property.setValue(''); + if (firstAddedPropertyName || shouldOverrideProperties) { + if (onPropertiesUpdated) onPropertiesUpdated(); } + }, + [ + forceUpdate, + properties, + showPropertyOverridingConfirmation, + onPropertiesUpdated, + ] + ); + + const pastePropertiesAtTheEnd = React.useCallback( + async () => { + await pasteProperties(properties.getCount()); + }, + [properties, pasteProperties] + ); + + const pastePropertiesBefore = React.useCallback( + async (property: gdNamedPropertyDescriptor) => { + await pasteProperties(properties.getPosition(property)); + }, + [properties, pasteProperties] + ); + + const moveProperty = React.useCallback( + (oldIndex: number, newIndex: number) => { + properties.move(oldIndex, newIndex); forceUpdate(); - }; - }, - [forceUpdate] - ); - - const getPropertyGroupNames = React.useCallback( - (): Array => { - const groupNames = new Set(); - for (let i = 0; i < properties.size(); i++) { - const property = properties.at(i); - const group = property.getGroup() || ''; - groupNames.add(group); - } - return [...groupNames].sort((a, b) => a.localeCompare(b)); - }, - [properties] - ); - - const setHidden = React.useCallback( - (property: gdNamedPropertyDescriptor, enable: boolean) => { - property.setHidden(enable); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated] - ); - - const setAdvanced = React.useCallback( - (property: gdNamedPropertyDescriptor, enable: boolean) => { - property.setAdvanced(enable); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated] - ); - - const setDeprecated = React.useCallback( - (property: gdNamedPropertyDescriptor, enable: boolean) => { - property.setDeprecated(enable); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated] - ); - - const isClipboardContainingProperties = Clipboard.has( - PROPERTIES_CLIPBOARD_KIND - ); - - return ( - - {({ i18n }) => ( - - {properties.getCount() > 0 ? ( - - - - {mapVector( - properties, - (property: gdNamedPropertyDescriptor, i: number) => { - const propertyRef = - justAddedPropertyName === property.getName() - ? justAddedPropertyElement - : null; - - if ( - searchText && - !searchMatchingPropertyNames.includes( - property.getName() - ) - ) { - return null; - } + onPropertiesUpdated && onPropertiesUpdated(); + }, + [forceUpdate, onPropertiesUpdated, properties] + ); + + const movePropertyBefore = React.useCallback( + (targetProperty: gdNamedPropertyDescriptor) => { + const { current } = draggedProperty; + if (!current) return; + + const draggedIndex = properties.getPosition(current); + const targetIndex = properties.getPosition(targetProperty); + + properties.move( + draggedIndex, + targetIndex > draggedIndex ? targetIndex - 1 : targetIndex + ); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + }, + [properties, forceUpdate, onPropertiesUpdated] + ); + + const setChoices = React.useCallback( + (property: gdNamedPropertyDescriptor) => { + return (choices: Array) => { + property.clearChoices(); + for (const choice of choices) { + property.addChoice(choice.value, choice.label); + } + if ( + !getChoicesArray(property).some( + choice => choice.value === property.getValue() + ) + ) { + property.setValue(''); + } + forceUpdate(); + }; + }, + [forceUpdate] + ); + + const getPropertyGroupNames = React.useCallback( + (): Array => { + const groupNames = new Set(); + for (let i = 0; i < properties.size(); i++) { + const property = properties.at(i); + const group = property.getGroup() || ''; + groupNames.add(group); + } + return [...groupNames].sort((a, b) => a.localeCompare(b)); + }, + [properties] + ); + + const setHidden = React.useCallback( + (property: gdNamedPropertyDescriptor, enable: boolean) => { + property.setHidden(enable); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + }, + [forceUpdate, onPropertiesUpdated] + ); + + const setAdvanced = React.useCallback( + (property: gdNamedPropertyDescriptor, enable: boolean) => { + property.setAdvanced(enable); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + }, + [forceUpdate, onPropertiesUpdated] + ); + + const setDeprecated = React.useCallback( + (property: gdNamedPropertyDescriptor, enable: boolean) => { + property.setDeprecated(enable); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + }, + [forceUpdate, onPropertiesUpdated] + ); + + const isClipboardContainingProperties = Clipboard.has( + PROPERTIES_CLIPBOARD_KIND + ); + + propertyRefs.current.clear(); + + return ( + + {({ i18n }) => ( + + {properties.getCount() > 0 ? ( + + {mapVector( + properties, + (property: gdNamedPropertyDescriptor, i: number) => { + if ( + searchText && + !searchMatchingPropertyNames.includes(property.getName()) + ) { + return null; + } - return ( - { - draggedProperty.current = property; - return {}; + return ( + +
{ + propertyRefs.current.set(property.getName(), ref); }} - canDrag={() => true} - canDrop={() => true} - drop={() => { - movePropertyBefore(property); + style={{ + ...styles.rowContent, + backgroundColor: + gdevelopTheme.list.itemsBackgroundColor, }} > - {({ - connectDragSource, - connectDropTarget, - isOver, - canDrop, - }) => - connectDropTarget( -
+ + + { + if (newName === property.getName()) return; + + const projectScopedContainers = projectScopedContainersAccessor.get(); + const validatedNewName = getValidatedPropertyName( + properties, + projectScopedContainers, + newName + ); + onRenameProperty( + property.getName(), + validatedNewName + ); + property.setName(validatedNewName); + + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + fullWidth + /> + + + + { + if (value === 'Hidden') { + setHidden(property, true); + setDeprecated(property, false); + setAdvanced(property, false); + } else if (value === 'Deprecated') { + setHidden(property, false); + setDeprecated(property, true); + setAdvanced(property, false); + } else if (value === 'Advanced') { + setHidden(property, false); + setDeprecated(property, false); + setAdvanced(property, true); + } else if (value === 'Visible') { + setHidden(property, false); + setDeprecated(property, false); + setAdvanced(property, false); + } + }} + fullWidth > - {isOver && } -
+ + + + + + + + + + } + buildMenuTemplate={(i18n: I18nType) => [ + { + label: i18n._( + t`Generate expression and action` + ), + click: () => { + gd.PropertyFunctionGenerator.generateBehaviorGetterAndSetter( + project, + extension, + eventsBasedBehavior, + property, + !!isSceneProperties + ); + onEventsFunctionsAdded(); + }, + enabled: gd.PropertyFunctionGenerator.canGenerateGetterAndSetter( + eventsBasedBehavior, + property + ), + }, + ...renderQuickCustomizationMenuItems({ + i18n, + visibility: property.getQuickCustomizationVisibility(), + onChangeVisibility: visibility => { + property.setQuickCustomizationVisibility( + visibility + ); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + }, + }), + ]} + /> + +
+ + + + Type} + value={property.getType()} + onChange={(e, i, value: string) => { + property.setType(value); + if (value === 'Behavior') { + property.setHidden(false); + } + if (value === 'Resource') { + setExtraInfoString(property, 'json'); + } + forceUpdate(); + onPropertyTypeChanged(property.getName()); + onPropertiesUpdated && onPropertiesUpdated(); + }} + fullWidth + > + + + + + + + + + + {!isSceneProperties && ( + + )} + + {property.getType() === 'Number' && ( + Measurement unit + } + value={property + .getMeasurementUnit() + .getName()} + onChange={(e, i, value: string) => { + property.setMeasurementUnit( + gd.MeasurementUnit.getDefaultMeasurementUnitByName( + value + ) + ); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); }} + fullWidth > - {connectDragSource( - - - - - - )} - - - { - if (newName === property.getName()) - return; - - const projectScopedContainers = projectScopedContainersAccessor.get(); - const validatedNewName = getValidatedPropertyName( - properties, - projectScopedContainers, - newName - ); - onRenameProperty( - property.getName(), - validatedNewName - ); - property.setName(validatedNewName); - - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - /> - - - { - if (value === 'Hidden') { - setHidden(property, true); - setDeprecated(property, false); - setAdvanced(property, false); - } else if (value === 'Deprecated') { - setHidden(property, false); - setDeprecated(property, true); - setAdvanced(property, false); - } else if (value === 'Advanced') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, true); - } else if (value === 'Visible') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, false); - } - }} - fullWidth - > + {mapFor( + 0, + gd.MeasurementUnit.getDefaultMeasurementUnitsCount(), + i => { + const measurementUnit = gd.MeasurementUnit.getDefaultMeasurementUnitAtIndex( + i + ); + const unitShortLabel = getMeasurementUnitShortLabel( + measurementUnit + ); + const label = + measurementUnit.getLabel() + + (unitShortLabel.length > 0 + ? ' — ' + unitShortLabel + : ''); + return ( - - - - - - - - - + ); } - buildMenuTemplate={(i18n: I18nType) => [ - { - label: i18n._(t`Add a property below`), - click: () => addPropertyAt(i + 1), - }, - { - label: i18n._(t`Delete`), - click: () => - removeProperty(property.getName()), - }, - { - label: i18n._(t`Copy`), - click: () => copyProperty(property), - }, - { - label: i18n._(t`Paste`), - click: () => - pastePropertiesBefore(property), - enabled: isClipboardContainingProperties, - }, - { - label: i18n._(t`Duplicate`), - click: () => - duplicateProperty(property, i + 1), - }, - { type: 'separator' }, - { - label: i18n._(t`Move up`), - click: () => moveProperty(i, i - 1), - enabled: i - 1 >= 0, - }, - { - label: i18n._(t`Move down`), - click: () => moveProperty(i, i + 1), - enabled: i + 1 < properties.getCount(), - }, - { - label: i18n._( - t`Generate expression and action` - ), - click: () => { - gd.PropertyFunctionGenerator.generateBehaviorGetterAndSetter( - project, - extension, - eventsBasedBehavior, - property, - !!isSceneProperties - ); - onEventsFunctionsAdded(); - }, - enabled: gd.PropertyFunctionGenerator.canGenerateGetterAndSetter( - eventsBasedBehavior, - property - ), - }, - ...renderQuickCustomizationMenuItems({ - i18n, - visibility: property.getQuickCustomizationVisibility(), - onChangeVisibility: visibility => { - property.setQuickCustomizationVisibility( - visibility - ); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }, - }), - ]} + )} + + )} + {(property.getType() === 'String' || + property.getType() === 'Number' || + property.getType() === 'ObjectAnimationName' || + property.getType() === 'KeyboardKey' || + property.getType() === 'MultilineString') && ( + Default value + } + hintText={ + property.getType() === 'Number' + ? '123' + : 'ABC' + } + value={property.getValue()} + onChange={newValue => { + property.setValue(newValue); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + multiline={ + property.getType() === 'MultilineString' + } + fullWidth + /> + )} + {property.getType() === 'Boolean' && ( + Default value + } + value={ + property.getValue() === 'true' + ? 'true' + : 'false' + } + onChange={(e, i, value) => { + property.setValue(value); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + fullWidth + > + - -
- - - - Type} - value={property.getType()} - onChange={(e, i, value: string) => { - property.setType(value); - if (value === 'Behavior') { - property.setHidden(false); - } - if (value === 'Resource') { - setExtraInfoString( - property, - 'json' - ); - } - forceUpdate(); - onPropertyTypeChanged( - property.getName() - ); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - - - - - - - - - - {!isSceneProperties && ( - - )} - - {property.getType() === 'Number' && ( - Measurement unit - } - value={property - .getMeasurementUnit() - .getName()} - onChange={(e, i, value: string) => { - property.setMeasurementUnit( - gd.MeasurementUnit.getDefaultMeasurementUnitByName( - value - ) - ); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - {mapFor( - 0, - gd.MeasurementUnit.getDefaultMeasurementUnitsCount(), - i => { - const measurementUnit = gd.MeasurementUnit.getDefaultMeasurementUnitAtIndex( - i - ); - const unitShortLabel = getMeasurementUnitShortLabel( - measurementUnit - ); - const label = - measurementUnit.getLabel() + - (unitShortLabel.length > 0 - ? ' — ' + unitShortLabel - : ''); - return ( - - ); - } - )} - - )} - {(property.getType() === 'String' || - property.getType() === 'Number' || - property.getType() === - 'ObjectAnimationName' || - property.getType() === 'KeyboardKey' || - property.getType() === - 'MultilineString') && ( - Default value - } - hintText={ - property.getType() === 'Number' - ? '123' - : 'ABC' - } - value={property.getValue()} - onChange={newValue => { - property.setValue(newValue); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - multiline={ - property.getType() === - 'MultilineString' - } - fullWidth - /> - )} - {property.getType() === 'Boolean' && ( - Default value - } - value={ - property.getValue() === 'true' - ? 'true' - : 'false' - } - onChange={(e, i, value) => { - property.setValue(value); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - - - - )} - {property.getType() === 'Behavior' && ( - { - // Change the type of the required behavior. - const extraInfo = property.getExtraInfo(); - if (extraInfo.size() === 0) { - extraInfo.push_back(newValue); - } else { - extraInfo.set(0, newValue); - } - const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata( - project.getCurrentPlatform(), - newValue - ); - const projectScopedContainers = projectScopedContainersAccessor.get(); - const validatedNewName = getValidatedPropertyName( - properties, - projectScopedContainers, - behaviorMetadata.getDefaultName() - ); - property.setName(validatedNewName); - property.setLabel( - behaviorMetadata.getFullName() - ); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - disabled={false} - /> - )} - {property.getType() === 'Color' && ( - Default value - } - disableAlpha - fullWidth - color={property.getValue()} - onChange={color => { - property.setValue(color); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - /> - )} - {property.getType() === 'Resource' && ( - 0 - ? property.getExtraInfo().at(0) - : '' - } - onChange={(e, i, value) => { - setExtraInfoString(property, value); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - /> - )} - {property.getType() === 'Choice' && ( - Default value - } - value={property.getValue()} - onChange={(e, i, value) => { - property.setValue(value); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - {getChoicesArray(property).map( - (choice, index) => ( - - ) - )} - - )} - - {property.getType() === 'Choice' && ( - - )} - - Short label - } - translatableHintText={t`Make the purpose of the property easy to understand`} - floatingLabelFixed - value={property.getLabel()} - onChange={text => { - property.setLabel(text); - forceUpdate(); - }} - fullWidth - /> - Group name + + + )} + {property.getType() === 'Behavior' && ( + { + // Change the type of the required behavior. + const extraInfo = property.getExtraInfo(); + if (extraInfo.size() === 0) { + extraInfo.push_back(newValue); + } else { + extraInfo.set(0, newValue); + } + const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata( + project.getCurrentPlatform(), + newValue + ); + const projectScopedContainers = projectScopedContainersAccessor.get(); + const validatedNewName = getValidatedPropertyName( + properties, + projectScopedContainers, + behaviorMetadata.getDefaultName() + ); + property.setName(validatedNewName); + property.setLabel( + behaviorMetadata.getFullName() + ); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + disabled={false} + /> + )} + {property.getType() === 'Color' && ( + Default value + } + disableAlpha + fullWidth + color={property.getValue()} + onChange={color => { + property.setValue(color); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + /> + )} + {property.getType() === 'Resource' && ( + 0 + ? property.getExtraInfo().at(0) + : '' + } + onChange={(e, i, value) => { + setExtraInfoString(property, value); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + fullWidth + /> + )} + {property.getType() === 'Choice' && ( + Default value + } + value={property.getValue()} + onChange={(e, i, value) => { + property.setValue(value); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + fullWidth + > + {getChoicesArray(property).map( + (choice, index) => ( + { - property.setGroup(text); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - dataSource={getPropertyGroupNames().map( - name => ({ - text: name, - value: name, - }) - )} - openOnFocus={true} /> - - Description - } - translatableHintText={t`Optionally, explain the purpose of the property in more details`} - floatingLabelFixed - value={property.getDescription()} - onChange={text => { - property.setDescription(text); - forceUpdate(); - }} - fullWidth - /> - - -
- ) - } -
- ); - } - )} -
-
- - - - } - label={Paste} - onClick={() => { - pastePropertiesAtTheEnd(); - }} - disabled={!isClipboardContainingProperties} - /> - {}} - onChange={text => setSearchText(text)} - placeholder={t`Search properties`} - /> - - - Add a property} - onClick={addProperty} - icon={} - /> - - + ) + )} + + )} + + {property.getType() === 'Choice' && ( + + )} + + Short label} + translatableHintText={t`Make the purpose of the property easy to understand`} + floatingLabelFixed + value={property.getLabel()} + onChange={text => { + property.setLabel(text); + forceUpdate(); + }} + fullWidth + /> + Group name} + hintText={t`Leave it empty to use the default group`} + fullWidth + value={property.getGroup()} + onChange={text => { + property.setGroup(text); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + }} + dataSource={getPropertyGroupNames().map( + name => ({ + text: name, + value: name, + }) + )} + openOnFocus={true} + /> + + Description} + translatableHintText={t`Optionally, explain the purpose of the property in more details`} + floatingLabelFixed + value={property.getDescription()} + onChange={text => { + property.setDescription(text); + forceUpdate(); + }} + fullWidth + /> + + +
+ ); + } + )}
- - ) : ( - - Add your first property} - description={ - Properties store data inside behaviors. - } - actionLabel={Add a property} - helpPagePath={'/behaviors/events-based-behaviors'} - helpPageAnchor={'add-and-use-properties-in-a-behavior'} - onAction={addProperty} - secondaryActionIcon={} - secondaryActionLabel={ - isClipboardContainingProperties ? Paste : null - } - onSecondaryAction={() => { - pastePropertiesAtTheEnd(); - }} - /> - - )} - - )} -
- ); -} + ) : ( + + Add your first property} + description={ + Properties store data inside behaviors. + } + actionLabel={Add a property} + helpPagePath={'/behaviors/events-based-behaviors'} + helpPageAnchor={'add-and-use-properties-in-a-behavior'} + onAction={addProperty} + secondaryActionIcon={} + secondaryActionLabel={ + isClipboardContainingProperties ? ( + Paste + ) : null + } + onSecondaryAction={() => { + pastePropertiesAtTheEnd(); + }} + /> + + )} + + )} +
+ ); + } +); diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/index.js b/newIDE/app/src/EventsBasedBehaviorEditor/index.js index b0afe49c5e4d..b4ae8fd4ed00 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/index.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/index.js @@ -19,7 +19,6 @@ import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExte import SelectField from '../UI/SelectField'; import SelectOption from '../UI/SelectOption'; import Window from '../Utils/Window'; -import ScrollView from '../UI/ScrollView'; const gd: libGDevelop = global.gd; @@ -68,154 +67,150 @@ export default function EventsBasedBehaviorEditor({ return ( {({ i18n }) => ( - - - + + + + This is the configuration of your behavior. Make sure to choose a + proper internal name as it's hard to change it later. Enter a + description explaining what the behavior is doing to the object. + + + Internal Name} + value={eventsBasedBehavior.getName()} + disabled + fullWidth + /> + Name displayed in editor} + value={eventsBasedBehavior.getFullName()} + onChange={text => { + eventsBasedBehavior.setFullName(text); + onChange(); + }} + fullWidth + /> + Description} + helperMarkdownText={i18n._( + t`Explain what the behavior is doing to the object. Start with a verb when possible.` + )} + value={eventsBasedBehavior.getDescription()} + onChange={text => { + eventsBasedBehavior.setDescription(text); + onChange(); + }} + multiline + fullWidth + rows={3} + /> + Object on which this behavior can be used + } + project={project} + value={eventsBasedBehavior.getObjectType()} + onChange={(objectType: string) => { + eventsBasedBehavior.setObjectType(objectType); + onChange(); + }} + allowedObjectTypes={ + allObjectTypes.length === 0 + ? undefined /* Allow anything as the behavior is not used */ + : allObjectTypes.length === 1 + ? [ + '', + allObjectTypes[0], + ] /* Allow only the type of the objects using the behavior */ + : [ + '', + ] /* More than one type of object are using the behavior. Only "any object" can be used on this behavior */ + } + /> + {allObjectTypes.length > 1 && ( + - This is the configuration of your behavior. Make sure to choose - a proper internal name as it's hard to change it later. Enter a - description explaining what the behavior is doing to the object. + This behavior is being used by multiple types of objects. Thus, + you can't restrict its usage to any particular object type. All + the object types using this behavior are listed here: + {allObjectTypes.join(', ')} - - Internal Name} - value={eventsBasedBehavior.getName()} - disabled - fullWidth - /> - Name displayed in editor} - value={eventsBasedBehavior.getFullName()} - onChange={text => { - eventsBasedBehavior.setFullName(text); - onChange(); - }} - fullWidth - /> - Description} - helperMarkdownText={i18n._( - t`Explain what the behavior is doing to the object. Start with a verb when possible.` - )} - value={eventsBasedBehavior.getDescription()} - onChange={text => { - eventsBasedBehavior.setDescription(text); - onChange(); - }} - multiline - fullWidth - rows={3} - /> - + )} + {isDev && ( + Object on which this behavior can be used + Visibility in quick customization dialog } - project={project} - value={eventsBasedBehavior.getObjectType()} - onChange={(objectType: string) => { - eventsBasedBehavior.setObjectType(objectType); + value={eventsBasedBehavior.getQuickCustomizationVisibility()} + onChange={(e, i, valueString: string) => { + // $FlowFixMe + const value: QuickCustomization_Visibility = valueString; + eventsBasedBehavior.setQuickCustomizationVisibility(value); onChange(); }} - allowedObjectTypes={ - allObjectTypes.length === 0 - ? undefined /* Allow anything as the behavior is not used */ - : allObjectTypes.length === 1 - ? [ - '', - allObjectTypes[0], - ] /* Allow only the type of the objects using the behavior */ - : [ - '', - ] /* More than one type of object are using the behavior. Only "any object" can be used on this behavior */ - } - /> - {allObjectTypes.length > 1 && ( - + fullWidth + > + + + + + )} + Private} + checked={eventsBasedBehavior.isPrivate()} + onCheck={(e, checked) => { + eventsBasedBehavior.setPrivate(checked); + if (onConfigurationUpdated) onConfigurationUpdated('isPrivate'); + onChange(); + }} + tooltipOrHelperText={ + eventsBasedBehavior.isPrivate() ? ( - This behavior is being used by multiple types of objects. - Thus, you can't restrict its usage to any particular object - type. All the object types using this behavior are listed - here: - {allObjectTypes.join(', ')} + This behavior won't be visible in the scene and events + editors. - - )} - {isDev && ( - Visibility in quick customization dialog - } - value={eventsBasedBehavior.getQuickCustomizationVisibility()} - onChange={(e, i, valueString: string) => { - // $FlowFixMe - const value: QuickCustomization_Visibility = valueString; - eventsBasedBehavior.setQuickCustomizationVisibility(value); - onChange(); - }} - fullWidth - > - - - - - )} - Private} - checked={eventsBasedBehavior.isPrivate()} - onCheck={(e, checked) => { - eventsBasedBehavior.setPrivate(checked); - if (onConfigurationUpdated) onConfigurationUpdated('isPrivate'); - onChange(); - }} - tooltipOrHelperText={ - eventsBasedBehavior.isPrivate() ? ( - - This behavior won't be visible in the scene and events - editors. - - ) : ( - - This behavior will be visible in the scene and events - editors. - - ) - } - /> - {eventsBasedBehavior - .getEventsFunctions() - .getEventsFunctionsCount() === 0 && ( - + ) : ( - Once you're done, start adding some functions to the behavior. - Then, test the behavior by adding it to an object in a scene. + This behavior will be visible in the scene and events editors. - - )} - - - - - + ) + } + /> + {eventsBasedBehavior + .getEventsFunctions() + .getEventsFunctionsCount() === 0 && ( + + + Once you're done, start adding some functions to the behavior. + Then, test the behavior by adding it to an object in a scene. + + + )} + + + + )} ); diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index fe6effad69cc..68618b98d5d3 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -19,7 +19,10 @@ import { type EventsFunctionCreationParameters } from '../EventsFunctionsList/Ev import { type EventsBasedObjectCreationParameters } from '../EventsFunctionsList/EventsBasedObjectTreeViewItemContent'; import Background from '../UI/Background'; import OptionsEditorDialog from './OptionsEditorDialog'; -import EventsBasedBehaviorEditorPanel from '../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel'; +import { + EventsBasedBehaviorEditorPanel, + type EventsBasedBehaviorEditorPanelInterface, +} from '../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel'; import EventsBasedObjectEditorPanel from '../EventsBasedObjectEditor/EventsBasedObjectEditorPanel'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import BehaviorMethodSelectorDialog from './BehaviorMethodSelectorDialog'; @@ -152,6 +155,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< }; editor: ?EventsSheetInterface; eventsFunctionList: ?EventsFunctionsListInterface; + eventsBasedBehaviorEditorPanel: ?EventsBasedBehaviorEditorPanelInterface; _editorMosaic: ?EditorMosaicInterface; _editorNavigator: ?EditorNavigatorInterface; // Create an empty "context" of objects. @@ -1444,8 +1448,13 @@ export default class EventsFunctionsExtensionEditor extends React.Component< ); } }} - // TODO Scroll to the property - onOpenProperty={() => {}} + onOpenProperty={propertyName => { + if (this.eventsBasedBehaviorEditorPanel) { + this.eventsBasedBehaviorEditorPanel.scrollToProperty( + propertyName + ); + } + }} /> ) : ( @@ -1516,6 +1525,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< ) : selectedEventsBasedBehavior && this._projectScopedContainersAccessor ? ( (this.eventsBasedBehaviorEditorPanel = ref)} project={project} projectScopedContainersAccessor={ this._projectScopedContainersAccessor From 733f958421eb898a58c1cc1dcea9e9c4d9c6989f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Wed, 24 Dec 2025 13:18:38 +0100 Subject: [PATCH 03/30] Follow selection both ways --- newIDE/app/src/BehaviorTypeSelector/index.js | 4 +- .../EventsBasedBehaviorEditorPanel.js | 22 ++++-- .../EventsBasedBehaviorPropertiesEditor.js | 45 +++++++++++- .../ResourceTypeSelectField.js | 3 + .../PropertyListEditor/index.js | 71 +++++++++++++------ .../EventsFunctionsExtensionEditor/index.js | 23 +++++- newIDE/app/src/UI/SelectField.js | 2 + .../app/src/UI/SemiControlledAutoComplete.js | 2 + 8 files changed, 138 insertions(+), 34 deletions(-) diff --git a/newIDE/app/src/BehaviorTypeSelector/index.js b/newIDE/app/src/BehaviorTypeSelector/index.js index 6750fd6b3687..e32f69d62b8e 100644 --- a/newIDE/app/src/BehaviorTypeSelector/index.js +++ b/newIDE/app/src/BehaviorTypeSelector/index.js @@ -13,6 +13,7 @@ type Props = {| objectType: string, value: string, onChange: string => void, + onFocus?: (event: SyntheticFocusEvent) => void, disabled?: boolean, eventsFunctionsExtension: gdEventsFunctionsExtension | null, |}; @@ -33,7 +34,7 @@ export default class BehaviorTypeSelector extends React.Component< }; render() { - const { disabled, objectType, value, onChange } = this.props; + const { disabled, objectType, value, onChange, onFocus } = this.props; const { behaviorMetadata } = this.state; // If the behavior type is not in the list, we'll still @@ -48,6 +49,7 @@ export default class BehaviorTypeSelector extends React.Component< onChange={(e, i, value: string) => { onChange(value); }} + onFocus={onFocus} disabled={disabled} fullWidth > diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js index 6fed5321478e..ca4b3cd1e283 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js @@ -7,7 +7,6 @@ import { type EventsBasedBehaviorPropertiesEditorInterface, } from './EventsBasedBehaviorPropertiesEditor'; import Background from '../UI/Background'; -import { Column, Line } from '../UI/Grid'; import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExtensionEditor'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; @@ -23,12 +22,15 @@ type Props = {| onRenameProperty: (oldName: string, newName: string) => void, onRenameSharedProperty: (oldName: string, newName: string) => void, onPropertyTypeChanged: (propertyName: string) => void, + onFocusProperty: (propertyName: string) => void, + onPropertiesUpdated: () => void, onEventsFunctionsAdded: () => void, unsavedChanges?: ?UnsavedChanges, onConfigurationUpdated?: (?ExtensionItemConfigurationAttribute) => void, |}; export type EventsBasedBehaviorEditorPanelInterface = {| + forceUpdateProperties: () => void, scrollToProperty: (propertyName: string) => void, |}; @@ -48,16 +50,19 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< unsavedChanges, onEventsFunctionsAdded, onConfigurationUpdated, + onPropertiesUpdated, + onFocusProperty, }: Props, ref ) => { - const onPropertiesUpdated = React.useCallback( + const _onPropertiesUpdated = React.useCallback( () => { if (unsavedChanges) { unsavedChanges.triggerUnsavedChanges(); } + onPropertiesUpdated(); }, - [unsavedChanges] + [onPropertiesUpdated, unsavedChanges] ); const scrollView = React.useRef(null); @@ -74,6 +79,11 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< }, []); React.useImperativeHandle(ref, () => ({ + forceUpdateProperties: () => { + if (propertiesEditor.current) { + propertiesEditor.current.forceUpdate(); + } + }, scrollToProperty, })); @@ -103,7 +113,8 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< properties={eventsBasedBehavior.getPropertyDescriptors()} onRenameProperty={onRenameProperty} behaviorObjectType={eventsBasedBehavior.getObjectType()} - onPropertiesUpdated={onPropertiesUpdated} + onPropertiesUpdated={_onPropertiesUpdated} + onFocusProperty={onFocusProperty} onPropertyTypeChanged={onPropertyTypeChanged} onEventsFunctionsAdded={onEventsFunctionsAdded} /> @@ -118,7 +129,8 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< eventsBasedBehavior={eventsBasedBehavior} properties={eventsBasedBehavior.getSharedPropertyDescriptors()} onRenameProperty={onRenameSharedProperty} - onPropertiesUpdated={onPropertiesUpdated} + onPropertiesUpdated={_onPropertiesUpdated} + onFocusProperty={onFocusProperty} onPropertyTypeChanged={onPropertyTypeChanged} onEventsFunctionsAdded={onEventsFunctionsAdded} /> diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js index db35ec7c54c3..cfc2dcedeceb 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js @@ -91,7 +91,8 @@ type Props = {| eventsBasedBehavior: gdEventsBasedBehavior, properties: gdPropertiesContainer, isSceneProperties?: boolean, - onPropertiesUpdated?: () => void, + onPropertiesUpdated: () => void, + onFocusProperty: (propertyName: string) => void, onRenameProperty: (oldName: string, newName: string) => void, onPropertyTypeChanged: (propertyName: string) => void, onEventsFunctionsAdded: () => void, @@ -129,6 +130,7 @@ const getChoicesArray = ( }; export type EventsBasedBehaviorPropertiesEditorInterface = {| + forceUpdate: () => void, getPropertyEditorRef: (propertyName: string) => React.ElementRef, |}; @@ -145,6 +147,7 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< properties, isSceneProperties, onPropertiesUpdated, + onFocusProperty, onRenameProperty, onPropertyTypeChanged, onEventsFunctionsAdded, @@ -152,8 +155,10 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< }: Props, ref ) => { + const forceUpdate = useForceUpdate(); const propertyRefs = React.useRef(new Map>()); React.useImperativeHandle(ref, () => ({ + forceUpdate, getPropertyEditorRef: (propertyName: string) => { return propertyRefs ? propertyRefs.current.get(propertyName) : null; }, @@ -165,8 +170,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< const showPropertyOverridingConfirmation = usePropertyOverridingAlertDialog(); - const forceUpdate = useForceUpdate(); - const [searchText, setSearchText] = React.useState(''); const [ searchMatchingPropertyNames, @@ -513,6 +516,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< onPropertiesUpdated && onPropertiesUpdated(); }} + onFocus={() => + onFocusProperty(property.getName()) + } fullWidth /> @@ -556,6 +562,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< setAdvanced(property, false); } }} + onFocus={() => + onFocusProperty(property.getName()) + } fullWidth > + onFocusProperty(property.getName()) + } fullWidth > + onFocusProperty(property.getName()) + } fullWidth > {mapFor( @@ -766,6 +781,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< onPropertiesUpdated && onPropertiesUpdated(); }} + onFocus={() => + onFocusProperty(property.getName()) + } multiline={ property.getType() === 'MultilineString' } @@ -788,6 +806,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< onPropertiesUpdated && onPropertiesUpdated(); }} + onFocus={() => + onFocusProperty(property.getName()) + } fullWidth > + onFocusProperty(property.getName()) + } disabled={false} /> )} @@ -870,6 +894,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< onPropertiesUpdated && onPropertiesUpdated(); }} + onFocus={() => + onFocusProperty(property.getName()) + } fullWidth /> )} @@ -885,6 +912,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< onPropertiesUpdated && onPropertiesUpdated(); }} + onFocus={() => + onFocusProperty(property.getName()) + } fullWidth > {getChoicesArray(property).map( @@ -922,6 +952,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< property.setLabel(text); forceUpdate(); }} + onFocus={() => + onFocusProperty(property.getName()) + } fullWidth /> + onFocusProperty(property.getName()) + } dataSource={getPropertyGroupNames().map( name => ({ text: name, @@ -953,6 +989,9 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< property.setDescription(text); forceUpdate(); }} + onFocus={() => + onFocusProperty(property.getName()) + } fullWidth /> diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField.js index 35a73ffec174..b6c4de72c4ce 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField.js @@ -14,12 +14,14 @@ type Props = {| index: number, text: string ) => void, + onFocus?: (event: SyntheticFocusEvent) => void, fullWidth?: boolean, |}; export default function ResourceTypeSelectField({ value, onChange, + onFocus, fullWidth, }: Props) { return ( @@ -29,6 +31,7 @@ export default function ResourceTypeSelectField({ floatingLabelText={Resource type} value={value} onChange={onChange} + onFocus={onFocus} fullWidth={fullWidth} > {allResourceKindsAndMetadata.map(({ kind, displayName }) => ( diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 182dda39817e..8b3d57300267 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -369,6 +369,7 @@ export const usePropertyOverridingAlertDialog = () => { export type PropertyListEditorInterface = {| forceUpdateList: () => void, focusSearchBar: () => void, + setSelectedProperty: (propertyName: string) => void, |}; type Props = {| @@ -424,16 +425,6 @@ const PropertyListEditor = React.forwardRef( const searchBarRef = React.useRef(null); - React.useImperativeHandle(ref, () => ({ - forceUpdateList: () => { - forceUpdate(); - if (treeViewRef.current) treeViewRef.current.forceUpdateList(); - }, - focusSearchBar: () => { - if (searchBarRef.current) searchBarRef.current.focus(); - }, - })); - const onProjectItemModified = React.useCallback( () => { forceUpdate(); @@ -591,6 +582,21 @@ const PropertyListEditor = React.forwardRef( ] ); + const createPropertyItem = React.useCallback( + (property: gdNamedPropertyDescriptor) => { + if (!propertiesTreeViewItemProps) { + return null; + } + return new LeafTreeViewItem( + new EventsBasedEntityPropertyTreeViewItemContent( + property, + propertiesTreeViewItemProps + ) + ); + }, + [propertiesTreeViewItemProps] + ); + const getTreeViewData = React.useCallback( (i18n: I18nType): Array => { return !properties || !propertiesTreeViewItemProps @@ -630,24 +636,45 @@ const PropertyListEditor = React.forwardRef( ), ]; } - return mapFor( - 0, - properties.getCount(), - i => - new LeafTreeViewItem( - new EventsBasedEntityPropertyTreeViewItemContent( - properties.getAt(i), - propertiesTreeViewItemProps - ) - ) - ); + return mapFor(0, properties.getCount(), i => + createPropertyItem(properties.getAt(i)) + ).filter(Boolean); }, }, ]; }, - [addNewScene, properties, propertiesTreeViewItemProps] + [addNewScene, createPropertyItem, properties, propertiesTreeViewItemProps] ); + React.useImperativeHandle(ref, () => ({ + forceUpdateList: () => { + forceUpdate(); + if (treeViewRef.current) treeViewRef.current.forceUpdateList(); + }, + focusSearchBar: () => { + if (searchBarRef.current) searchBarRef.current.focus(); + }, + setSelectedProperty: (propertyName: string) => { + if (!properties || !properties.has(propertyName)) { + return; + } + const property = properties.get(propertyName); + const propertyItemId = getEventsBasedEntityPropertyTreeViewItemId( + property + ); + setSelectedItems(selectedItems => { + if ( + selectedItems.length === 1 && + selectedItems[0].content.getId() === propertyItemId + ) { + return selectedItems; + } + return [createPropertyItem(property)].filter(Boolean); + }); + scrollToItem(propertyItemId); + }, + })); + const canMoveSelectionTo = React.useCallback( (destinationItem: TreeViewItem, where: 'before' | 'inside' | 'after') => selectedItems.every(item => { diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 68618b98d5d3..38066e7cb21a 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -46,7 +46,9 @@ import newNameGenerator from '../Utils/NewNameGenerator'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; import GlobalAndSceneVariablesDialog from '../VariablesList/GlobalAndSceneVariablesDialog'; import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton'; -import PropertyListEditor from './PropertyListEditor'; +import PropertyListEditor, { + type PropertyListEditorInterface, +} from './PropertyListEditor'; const gd: libGDevelop = global.gd; @@ -156,6 +158,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< editor: ?EventsSheetInterface; eventsFunctionList: ?EventsFunctionsListInterface; eventsBasedBehaviorEditorPanel: ?EventsBasedBehaviorEditorPanelInterface; + propertyListEditor: ?PropertyListEditorInterface; _editorMosaic: ?EditorMosaicInterface; _editorNavigator: ?EditorNavigatorInterface; // Create an empty "context" of objects. @@ -1429,10 +1432,14 @@ export default class EventsFunctionsExtensionEditor extends React.Component< /> ) : selectedEventsBasedObject || selectedEventsBasedBehavior ? ( (this.propertyListEditor = ref)} eventsBasedBehavior={selectedEventsBasedBehavior} eventsBasedObject={selectedEventsBasedObject} - // TODO Force update the other view - onPropertiesUpdated={() => {}} + onPropertiesUpdated={() => { + if (this.eventsBasedBehaviorEditorPanel) { + this.eventsBasedBehaviorEditorPanel.forceUpdateProperties(); + } + }} onRenameProperty={(oldName, newName) => { if (selectedEventsBasedBehavior) { this._onBehaviorPropertyRenamed( @@ -1555,6 +1562,16 @@ export default class EventsFunctionsExtensionEditor extends React.Component< propertyName ); }} + onPropertiesUpdated={() => { + if (this.propertyListEditor) { + this.propertyListEditor.forceUpdateList(); + } + }} + onFocusProperty={propertyName => { + if (this.propertyListEditor) { + this.propertyListEditor.setSelectedProperty(propertyName); + } + }} onEventsFunctionsAdded={() => { if (this.eventsFunctionList) { this.eventsFunctionList.forceUpdateList(); diff --git a/newIDE/app/src/UI/SelectField.js b/newIDE/app/src/UI/SelectField.js index 0959a5e93309..482035c5a4dc 100644 --- a/newIDE/app/src/UI/SelectField.js +++ b/newIDE/app/src/UI/SelectField.js @@ -43,6 +43,7 @@ type Props = {| children: React.Node, disabled?: boolean, stopPropagationOnClick?: boolean, + onFocus?: (event: SyntheticFocusEvent) => void, id?: ?string, style?: { @@ -128,6 +129,7 @@ const SelectField = React.forwardRef( } : undefined } + onFocus={props.onFocus} InputProps={{ style: props.inputStyle, disableUnderline: !!props.disableUnderline, diff --git a/newIDE/app/src/UI/SemiControlledAutoComplete.js b/newIDE/app/src/UI/SemiControlledAutoComplete.js index a2c3a12a0fc2..c7ee2625058d 100644 --- a/newIDE/app/src/UI/SemiControlledAutoComplete.js +++ b/newIDE/app/src/UI/SemiControlledAutoComplete.js @@ -55,6 +55,7 @@ type Props = {| id?: ?string, onBlur?: (event: SyntheticFocusEvent) => void, onClick?: (event: SyntheticPointerEvent) => void, + onFocus?: (event: SyntheticFocusEvent) => void, commitOnInputChange?: boolean, onRequestClose?: () => void, onApply?: () => void, @@ -314,6 +315,7 @@ export default React.forwardRef( setInputValue(null); setIsMenuOpen(false); }} + onFocus={props.onFocus} open={isMenuOpen} style={{ ...props.style, From 9ccd4a9c9a28ef86b216abffda1f6d3497adcda8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Wed, 24 Dec 2025 15:01:45 +0100 Subject: [PATCH 04/30] Move the context menu in the tree --- .../EventsBasedBehaviorPropertiesEditor.js | 299 ++++-------------- ...sBasedEntityPropertyTreeViewItemContent.js | 56 ++++ .../PropertyListEditor/index.js | 31 +- .../EventsFunctionsExtensionEditor/index.js | 7 + 4 files changed, 156 insertions(+), 237 deletions(-) diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js index cfc2dcedeceb..b53e4753fc66 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js @@ -38,7 +38,6 @@ import ResponsiveFlatButton from '../UI/ResponsiveFlatButton'; import { EmptyPlaceholder } from '../UI/EmptyPlaceholder'; import useAlertDialog from '../UI/Alert/useAlertDialog'; import SearchBar from '../UI/SearchBar'; -import { renderQuickCustomizationMenuItems } from '../QuickCustomization/QuickCustomizationMenuItems'; import ResourceTypeSelectField from '../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; @@ -164,56 +163,10 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< }, })); - const draggedProperty = React.useRef(null); - const gdevelopTheme = React.useContext(GDevelopThemeContext); const showPropertyOverridingConfirmation = usePropertyOverridingAlertDialog(); - const [searchText, setSearchText] = React.useState(''); - const [ - searchMatchingPropertyNames, - setSearchMatchingPropertyNames, - ] = React.useState>([]); - - const triggerSearch = React.useCallback( - () => { - const matchingPropertyNames = mapVector( - properties, - (property: gdNamedPropertyDescriptor, i: number) => { - const lowerCaseSearchText = searchText.toLowerCase(); - return property - .getName() - .toLowerCase() - .includes(lowerCaseSearchText) || - property - .getLabel() - .toLowerCase() - .includes(lowerCaseSearchText) || - property - .getGroup() - .toLowerCase() - .includes(lowerCaseSearchText) - ? property.getName() - : null; - } - ).filter(Boolean); - setSearchMatchingPropertyNames(matchingPropertyNames); - }, - [properties, searchText] - ); - - React.useEffect( - () => { - if (searchText) { - triggerSearch(); - } else { - setSearchMatchingPropertyNames([]); - } - }, - [searchText, triggerSearch] - ); - const addPropertyAt = React.useCallback( (index: number) => { const newName = newNameGenerator('Property', name => @@ -224,7 +177,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< forceUpdate(); onPropertiesUpdated && onPropertiesUpdated(); //setJustAddedPropertyName(newName); - setSearchText(''); }, [forceUpdate, onPropertiesUpdated, properties] ); @@ -236,44 +188,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< [addPropertyAt, properties] ); - const removeProperty = React.useCallback( - (name: string) => { - properties.remove(name); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated, properties] - ); - - const copyProperty = React.useCallback( - (property: gdNamedPropertyDescriptor) => { - Clipboard.set(PROPERTIES_CLIPBOARD_KIND, [ - { - name: property.getName(), - serializedProperty: serializeToJSObject(property), - }, - ]); - forceUpdate(); - }, - [forceUpdate] - ); - - const duplicateProperty = React.useCallback( - (property: gdNamedPropertyDescriptor, index: number) => { - const newName = newNameGenerator(property.getName(), name => - properties.has(name) - ); - - const newProperty = properties.insertNew(newName, index); - - unserializeFromJSObject(newProperty, serializeToJSObject(property)); - newProperty.setName(newName); - - forceUpdate(); - }, - [forceUpdate, properties] - ); - const pasteProperties = React.useCallback( async propertyInsertionIndex => { const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); @@ -335,7 +249,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< } } - setSearchText(''); forceUpdate(); if (firstAddedPropertyName) { //setJustAddedPropertyName(firstAddedPropertyName); @@ -368,33 +281,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< [properties, pasteProperties] ); - const moveProperty = React.useCallback( - (oldIndex: number, newIndex: number) => { - properties.move(oldIndex, newIndex); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated, properties] - ); - - const movePropertyBefore = React.useCallback( - (targetProperty: gdNamedPropertyDescriptor) => { - const { current } = draggedProperty; - if (!current) return; - - const draggedIndex = properties.getPosition(current); - const targetIndex = properties.getPosition(targetProperty); - - properties.move( - draggedIndex, - targetIndex > draggedIndex ? targetIndex - 1 : targetIndex - ); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [properties, forceUpdate, onPropertiesUpdated] - ); - const setChoices = React.useCallback( (property: gdNamedPropertyDescriptor) => { return (choices: Array) => { @@ -470,13 +356,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< {mapVector( properties, (property: gdNamedPropertyDescriptor, i: number) => { - if ( - searchText && - !searchMatchingPropertyNames.includes(property.getName()) - ) { - return null; - } - return (
- - + + - - - { - if (value === 'Hidden') { - setHidden(property, true); - setDeprecated(property, false); - setAdvanced(property, false); - } else if (value === 'Deprecated') { - setHidden(property, false); - setDeprecated(property, true); - setAdvanced(property, false); - } else if (value === 'Advanced') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, true); - } else if (value === 'Visible') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, false); - } - }} - onFocus={() => - onFocusProperty(property.getName()) - } - fullWidth + - - - - - - - - - - - } - buildMenuTemplate={(i18n: I18nType) => [ - { - label: i18n._( - t`Generate expression and action` - ), - click: () => { - gd.PropertyFunctionGenerator.generateBehaviorGetterAndSetter( - project, - extension, - eventsBasedBehavior, - property, - !!isSceneProperties - ); - onEventsFunctionsAdded(); - }, - enabled: gd.PropertyFunctionGenerator.canGenerateGetterAndSetter( - eventsBasedBehavior, - property - ), - }, - ...renderQuickCustomizationMenuItems({ - i18n, - visibility: property.getQuickCustomizationVisibility(), - onChangeVisibility: visibility => { - property.setQuickCustomizationVisibility( - visibility - ); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - }), - ]} - /> - + { + if (value === 'Hidden') { + setHidden(property, true); + setDeprecated(property, false); + setAdvanced(property, false); + } else if (value === 'Deprecated') { + setHidden(property, false); + setDeprecated(property, true); + setAdvanced(property, false); + } else if (value === 'Advanced') { + setHidden(property, false); + setDeprecated(property, false); + setAdvanced(property, true); + } else if (value === 'Visible') { + setHidden(property, false); + setDeprecated(property, false); + setAdvanced(property, false); + } + }} + onFocus={() => + onFocusProperty(property.getName()) + } + fullWidth + > + + + + + + + +
diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 91579f4ef6c7..d8d58b0395db 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -15,6 +15,9 @@ import { TreeViewItemContent, type TreeItemProps, scenesRootFolderId } from '.'; import Tooltip from '@material-ui/core/Tooltip'; import { type HTMLDataset } from '../../Utils/HTMLDataset'; import VisibilityOffIcon from '../../UI/CustomSvgIcons/VisibilityOff'; +import { renderQuickCustomizationMenuItems } from '../../QuickCustomization/QuickCustomizationMenuItems'; + +const gd: libGDevelop = global.gd; const PROPERTIES_CLIPBOARD_KIND = 'Properties'; @@ -24,13 +27,20 @@ const styles = { export type EventsBasedEntityPropertyTreeViewItemProps = {| ...TreeItemProps, + project: gdProject, + extension: gdEventsFunctionsExtension, + eventsBasedEntity: gdAbstractEventsBasedEntity, + eventsBasedBehavior: ?gdEventsBasedBehavior, + eventsBasedObject: ?gdEventsBasedObject, properties: gdPropertiesContainer, + isSceneProperties: boolean, onOpenProperty: (name: string) => void, onRenameProperty: (newName: string, oldName: string) => void, showPropertyOverridingConfirmation: ( existingPropertyNames: string[] ) => Promise, onPropertiesUpdated: () => void, + onEventsFunctionsAdded: () => void, |}; export const getEventsBasedEntityPropertyTreeViewItemId = ( @@ -144,6 +154,52 @@ export class EventsBasedEntityPropertyTreeViewItemContent label: i18n._(t`Duplicate`), click: () => this._duplicate(), }, + { + type: 'separator', + }, + { + label: i18n._(t`Generate expression and action`), + click: () => { + const { + project, + extension, + eventsBasedBehavior, + eventsBasedObject, + isSceneProperties, + onEventsFunctionsAdded, + } = this.props; + if (eventsBasedBehavior) { + gd.PropertyFunctionGenerator.generateBehaviorGetterAndSetter( + project, + extension, + eventsBasedBehavior, + this.property, + isSceneProperties + ); + } else if (eventsBasedObject) { + gd.PropertyFunctionGenerator.generateObjectGetterAndSetter( + project, + extension, + eventsBasedObject, + this.property + ); + } + onEventsFunctionsAdded(); + }, + enabled: gd.PropertyFunctionGenerator.canGenerateGetterAndSetter( + this.props.eventsBasedEntity, + this.property + ), + }, + ...renderQuickCustomizationMenuItems({ + i18n, + visibility: this.property.getQuickCustomizationVisibility(), + onChangeVisibility: visibility => { + this.property.setQuickCustomizationVisibility(visibility); + this.props.forceUpdate(); + this.props.onPropertiesUpdated(); + }, + }), ]; } diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 8b3d57300267..b04d86da9789 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -373,21 +373,27 @@ export type PropertyListEditorInterface = {| |}; type Props = {| + project: gdProject, + extension: gdEventsFunctionsExtension, eventsBasedBehavior: ?gdEventsBasedBehavior, eventsBasedObject: ?gdEventsBasedObject, onPropertiesUpdated: () => void, onRenameProperty: (oldName: string, newName: string) => void, onOpenProperty: (name: string) => void, + onEventsFunctionsAdded: () => void, |}; const PropertyListEditor = React.forwardRef( ( { + project, + extension, eventsBasedBehavior, eventsBasedObject, onPropertiesUpdated, onRenameProperty, onOpenProperty, + onEventsFunctionsAdded, }, ref ) => { @@ -453,7 +459,7 @@ const PropertyListEditor = React.forwardRef( [isMobile] ); - const addNewScene = React.useCallback( + const addProperty = React.useCallback( (index: number, i18n: I18nType) => { if (!properties) return; @@ -548,7 +554,7 @@ const PropertyListEditor = React.forwardRef( const propertiesTreeViewItemProps = React.useMemo( () => - properties + properties && eventsBasedEntity ? { unsavedChanges, preferences, @@ -559,13 +565,22 @@ const PropertyListEditor = React.forwardRef( showPropertyOverridingConfirmation, editName, scrollToItem, + project, + extension, + eventsBasedEntity, + eventsBasedBehavior, + eventsBasedObject, properties, + isSceneProperties: false, onOpenProperty, onPropertiesUpdated, onRenameProperty, + onEventsFunctionsAdded, } : null, [ + properties, + eventsBasedEntity, unsavedChanges, preferences, gdevelopTheme, @@ -575,10 +590,14 @@ const PropertyListEditor = React.forwardRef( showPropertyOverridingConfirmation, editName, scrollToItem, - properties, + project, + extension, + eventsBasedBehavior, + eventsBasedObject, onOpenProperty, onPropertiesUpdated, onRenameProperty, + onEventsFunctionsAdded, ] ); @@ -620,9 +639,7 @@ const PropertyListEditor = React.forwardRef( icon: , label: i18n._(t`Add a property`), click: () => { - // TODO Add after selected scene? - const index = properties.getCount() - 1; - addNewScene(index, i18n); + addProperty(0, i18n); }, id: 'add-property', } @@ -643,7 +660,7 @@ const PropertyListEditor = React.forwardRef( }, ]; }, - [addNewScene, createPropertyItem, properties, propertiesTreeViewItemProps] + [addProperty, createPropertyItem, properties, propertiesTreeViewItemProps] ); React.useImperativeHandle(ref, () => ({ diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 38066e7cb21a..3eadc8ef9ded 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -1433,6 +1433,8 @@ export default class EventsFunctionsExtensionEditor extends React.Component< ) : selectedEventsBasedObject || selectedEventsBasedBehavior ? ( (this.propertyListEditor = ref)} + project={project} + extension={eventsFunctionsExtension} eventsBasedBehavior={selectedEventsBasedBehavior} eventsBasedObject={selectedEventsBasedObject} onPropertiesUpdated={() => { @@ -1462,6 +1464,11 @@ export default class EventsFunctionsExtensionEditor extends React.Component< ); } }} + onEventsFunctionsAdded={() => { + if (this.eventsFunctionList) { + this.eventsFunctionList.forceUpdateList(); + } + }} /> ) : ( From 53be9a14ca65aa449dcb8a708ee95c56f7d93444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Wed, 24 Dec 2025 21:26:35 +0100 Subject: [PATCH 05/30] Merge the properties editor for behavior and object --- .../EventsBasedBehaviorEditorPanel.js | 114 ++++++++++++------ .../EventsBasedBehaviorPropertiesEditor.js | 49 +++++--- .../EventsFunctionsExtensionEditor/index.js | 26 +++- 3 files changed, 130 insertions(+), 59 deletions(-) diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js index ca4b3cd1e283..3e84444385c6 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js @@ -13,12 +13,14 @@ import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/Even import Text from '../UI/Text'; import { ColumnStackLayout } from '../UI/Layout'; import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView'; +import EventsBasedObjectEditor from '../EventsBasedObjectEditor'; type Props = {| project: gdProject, projectScopedContainersAccessor: ProjectScopedContainersAccessor, eventsFunctionsExtension: gdEventsFunctionsExtension, - eventsBasedBehavior: gdEventsBasedBehavior, + eventsBasedBehavior?: ?gdEventsBasedBehavior, + eventsBasedObject?: ?gdEventsBasedObject, onRenameProperty: (oldName: string, newName: string) => void, onRenameSharedProperty: (oldName: string, newName: string) => void, onPropertyTypeChanged: (propertyName: string) => void, @@ -27,6 +29,10 @@ type Props = {| onEventsFunctionsAdded: () => void, unsavedChanges?: ?UnsavedChanges, onConfigurationUpdated?: (?ExtensionItemConfigurationAttribute) => void, + onOpenCustomObjectEditor: () => void, + onEventsBasedObjectChildrenEdited: ( + eventsBasedObject: gdEventsBasedObject + ) => void, |}; export type EventsBasedBehaviorEditorPanelInterface = {| @@ -41,6 +47,7 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< ( { eventsBasedBehavior, + eventsBasedObject, eventsFunctionsExtension, project, projectScopedContainersAccessor, @@ -52,6 +59,8 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< onConfigurationUpdated, onPropertiesUpdated, onFocusProperty, + onOpenCustomObjectEditor, + onEventsBasedObjectChildrenEdited, }: Props, ref ) => { @@ -87,6 +96,8 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< scrollToProperty, })); + const eventsBasedEntity = eventsBasedBehavior || eventsBasedObject; + return ( @@ -94,46 +105,73 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< Configuration - + {eventsBasedBehavior ? ( + + ) : eventsBasedObject ? ( + + ) : null} Behavior properties - - - Scene properties - - + {eventsBasedEntity && ( + + )} + {eventsBasedBehavior && ( + + Scene properties + + )} + {eventsBasedBehavior && ( + + )} diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js index b53e4753fc66..653a890275a1 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js +++ b/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js @@ -19,13 +19,9 @@ import ChoicesEditor, { type Choice } from '../ChoicesEditor'; import ColorField from '../UI/ColorField'; import BehaviorTypeSelector from '../BehaviorTypeSelector'; import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete'; -import ThreeDotsMenu from '../UI/CustomSvgIcons/ThreeDotsMenu'; import { getMeasurementUnitShortLabel } from '../PropertiesEditor/PropertiesMapToSchema'; import Add from '../UI/CustomSvgIcons/Add'; -import { DragHandleIcon } from '../UI/DragHandle'; import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; -import DropIndicator from '../UI/SortableVirtualizedItemList/DropIndicator'; -import { makeDragSourceAndDropTarget } from '../UI/DragAndDrop/DragSourceAndDropTarget'; import useForceUpdate from '../Utils/UseForceUpdate'; import Clipboard from '../Utils/Clipboard'; import { SafeExtractor } from '../Utils/SafeExtractor'; @@ -37,7 +33,6 @@ import PasteIcon from '../UI/CustomSvgIcons/Clipboard'; import ResponsiveFlatButton from '../UI/ResponsiveFlatButton'; import { EmptyPlaceholder } from '../UI/EmptyPlaceholder'; import useAlertDialog from '../UI/Alert/useAlertDialog'; -import SearchBar from '../UI/SearchBar'; import ResourceTypeSelectField from '../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; @@ -87,7 +82,8 @@ type Props = {| project: gdProject, projectScopedContainersAccessor: ProjectScopedContainersAccessor, extension: gdEventsFunctionsExtension, - eventsBasedBehavior: gdEventsBasedBehavior, + eventsBasedBehavior?: ?gdEventsBasedBehavior, + eventsBasedObject?: ?gdEventsBasedObject, properties: gdPropertiesContainer, isSceneProperties?: boolean, onPropertiesUpdated: () => void, @@ -95,7 +91,7 @@ type Props = {| onRenameProperty: (oldName: string, newName: string) => void, onPropertyTypeChanged: (propertyName: string) => void, onEventsFunctionsAdded: () => void, - behaviorObjectType?: string, + behaviorObjectType: string, |}; // Those names are used internally by GDevelop. @@ -143,6 +139,7 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< projectScopedContainersAccessor, extension, eventsBasedBehavior, + eventsBasedObject, properties, isSceneProperties, onPropertiesUpdated, @@ -518,16 +515,26 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< value="Color" label={t`Color (text)`} /> - - + {eventsBasedObject && ( + + )} + {eventsBasedBehavior && !isSceneProperties && ( + + )} + {eventsBasedBehavior && !isSceneProperties && ( + + )} - {!isSceneProperties && ( + {eventsBasedBehavior && !isSceneProperties && ( Add your first property} description={ - Properties store data inside behaviors. + eventsBasedObject ? ( + Properties store data inside objects. + ) : ( + Properties store data inside behaviors. + ) } actionLabel={Add a property} helpPagePath={'/behaviors/events-based-behaviors'} diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 3eadc8ef9ded..7fafcbb30cb3 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -23,7 +23,6 @@ import { EventsBasedBehaviorEditorPanel, type EventsBasedBehaviorEditorPanelInterface, } from '../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel'; -import EventsBasedObjectEditorPanel from '../EventsBasedObjectEditor/EventsBasedObjectEditorPanel'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import BehaviorMethodSelectorDialog from './BehaviorMethodSelectorDialog'; import ObjectMethodSelectorDialog from './ObjectMethodSelectorDialog'; @@ -158,6 +157,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< editor: ?EventsSheetInterface; eventsFunctionList: ?EventsFunctionsListInterface; eventsBasedBehaviorEditorPanel: ?EventsBasedBehaviorEditorPanelInterface; + eventsBasedObjectEditorPanel: ?EventsBasedBehaviorEditorPanelInterface; propertyListEditor: ?PropertyListEditorInterface; _editorMosaic: ?EditorMosaicInterface; _editorNavigator: ?EditorNavigatorInterface; @@ -1441,6 +1441,9 @@ export default class EventsFunctionsExtensionEditor extends React.Component< if (this.eventsBasedBehaviorEditorPanel) { this.eventsBasedBehaviorEditorPanel.forceUpdateProperties(); } + if (this.eventsBasedObjectEditorPanel) { + this.eventsBasedObjectEditorPanel.forceUpdateProperties(); + } }} onRenameProperty={(oldName, newName) => { if (selectedEventsBasedBehavior) { @@ -1463,6 +1466,11 @@ export default class EventsFunctionsExtensionEditor extends React.Component< propertyName ); } + if (this.eventsBasedObjectEditorPanel) { + this.eventsBasedObjectEditorPanel.scrollToProperty( + propertyName + ); + } }} onEventsFunctionsAdded={() => { if (this.eventsFunctionList) { @@ -1585,10 +1593,13 @@ export default class EventsFunctionsExtensionEditor extends React.Component< } }} onConfigurationUpdated={this._onConfigurationUpdated} + onOpenCustomObjectEditor={() => {}} + onEventsBasedObjectChildrenEdited={() => {}} /> ) : selectedEventsBasedObject && this._projectScopedContainersAccessor ? ( - (this.eventsBasedObjectEditorPanel = ref)} project={project} projectScopedContainersAccessor={ this._projectScopedContainersAccessor @@ -1603,6 +1614,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< newName ) } + onRenameSharedProperty={() => {}} onPropertyTypeChanged={propertyName => { gd.WholeProjectRefactorer.changeEventsBasedObjectPropertyType( project, @@ -1611,6 +1623,16 @@ export default class EventsFunctionsExtensionEditor extends React.Component< propertyName ); }} + onPropertiesUpdated={() => { + if (this.propertyListEditor) { + this.propertyListEditor.forceUpdateList(); + } + }} + onFocusProperty={propertyName => { + if (this.propertyListEditor) { + this.propertyListEditor.setSelectedProperty(propertyName); + } + }} onEventsFunctionsAdded={() => { if (this.eventsFunctionList) { this.eventsFunctionList.forceUpdateList(); From 9874093d3a8ea228c747641440485328fdbdba3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Wed, 24 Dec 2025 21:33:56 +0100 Subject: [PATCH 06/30] Rename files --- .../EventsBasedBehaviorEditor.js} | 0 ...sBasedBehaviorOrObjectPropertiesEditor.js} | 0 .../EventsBasedObjectEditor.js} | 0 .../index.js} | 6 +- .../EventsBasedObjectEditorPanel.js | 99 -- .../EventsBasedObjectPropertiesEditor.js | 1043 ----------------- .../EventsFunctionsExtensionEditor/index.js | 2 +- 7 files changed, 4 insertions(+), 1146 deletions(-) rename newIDE/app/src/{EventsBasedBehaviorEditor/index.js => EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js} (100%) rename newIDE/app/src/{EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js => EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js} (100%) rename newIDE/app/src/{EventsBasedObjectEditor/index.js => EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js} (100%) rename newIDE/app/src/{EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js => EventsBasedBehaviorOrObjectEditor/index.js} (97%) delete mode 100644 newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js delete mode 100644 newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectPropertiesEditor.js diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/index.js b/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js similarity index 100% rename from newIDE/app/src/EventsBasedBehaviorEditor/index.js rename to newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js b/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js similarity index 100% rename from newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorPropertiesEditor.js rename to newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js diff --git a/newIDE/app/src/EventsBasedObjectEditor/index.js b/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js similarity index 100% rename from newIDE/app/src/EventsBasedObjectEditor/index.js rename to newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js diff --git a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js b/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/index.js similarity index 97% rename from newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js rename to newIDE/app/src/EventsBasedBehaviorOrObjectEditor/index.js index 3e84444385c6..5d456e3bd174 100644 --- a/newIDE/app/src/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.js +++ b/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/index.js @@ -1,11 +1,11 @@ // @flow import { Trans } from '@lingui/macro'; import * as React from 'react'; -import EventsBasedBehaviorEditor from './index'; +import EventsBasedBehaviorEditor from './EventsBasedBehaviorEditor'; import { EventsBasedBehaviorPropertiesEditor, type EventsBasedBehaviorPropertiesEditorInterface, -} from './EventsBasedBehaviorPropertiesEditor'; +} from './EventsBasedBehaviorOrObjectPropertiesEditor'; import Background from '../UI/Background'; import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExtensionEditor'; @@ -13,7 +13,7 @@ import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/Even import Text from '../UI/Text'; import { ColumnStackLayout } from '../UI/Layout'; import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView'; -import EventsBasedObjectEditor from '../EventsBasedObjectEditor'; +import EventsBasedObjectEditor from './EventsBasedObjectEditor'; type Props = {| project: gdProject, diff --git a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js b/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js deleted file mode 100644 index ff5a032a0ee3..000000000000 --- a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js +++ /dev/null @@ -1,99 +0,0 @@ -// @flow -import { Trans } from '@lingui/macro'; -import * as React from 'react'; -import EventsBasedObjectEditor from './index'; -import { Tabs } from '../UI/Tabs'; -import EventsBasedObjectPropertiesEditor from './EventsBasedObjectPropertiesEditor'; -import Background from '../UI/Background'; -import { Column, Line } from '../UI/Grid'; -import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; -import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; - -type TabName = 'configuration' | 'properties' | 'children'; - -type Props = {| - project: gdProject, - projectScopedContainersAccessor: ProjectScopedContainersAccessor, - eventsFunctionsExtension: gdEventsFunctionsExtension, - eventsBasedObject: gdEventsBasedObject, - onRenameProperty: (oldName: string, newName: string) => void, - onPropertyTypeChanged: (propertyName: string) => void, - onEventsFunctionsAdded: () => void, - onOpenCustomObjectEditor: () => void, - unsavedChanges?: ?UnsavedChanges, - onEventsBasedObjectChildrenEdited: ( - eventsBasedObject: gdEventsBasedObject - ) => void, -|}; - -export default function EventsBasedObjectEditorPanel({ - project, - projectScopedContainersAccessor, - eventsFunctionsExtension, - eventsBasedObject, - onRenameProperty, - onPropertyTypeChanged, - onEventsFunctionsAdded, - onOpenCustomObjectEditor, - unsavedChanges, - onEventsBasedObjectChildrenEdited, -}: Props) { - const [currentTab, setCurrentTab] = React.useState('configuration'); - - const onPropertiesUpdated = React.useCallback( - () => { - if (unsavedChanges) { - unsavedChanges.triggerUnsavedChanges(); - } - }, - [unsavedChanges] - ); - - return ( - - - - - Configuration, - }, - { - value: 'properties', - label: Properties, - }, - ]} - /> - - - {currentTab === 'configuration' && ( - - )} - {currentTab === 'properties' && ( - - )} - - - ); -} diff --git a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectPropertiesEditor.js b/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectPropertiesEditor.js deleted file mode 100644 index 425cea97ca9d..000000000000 --- a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectPropertiesEditor.js +++ /dev/null @@ -1,1043 +0,0 @@ -// @flow -import { Trans } from '@lingui/macro'; -import { t } from '@lingui/macro'; -import { I18n } from '@lingui/react'; -import { type I18n as I18nType } from '@lingui/core'; -import * as React from 'react'; -import { Column, Line, Spacer } from '../UI/Grid'; -import { LineStackLayout } from '../UI/Layout'; -import SelectField from '../UI/SelectField'; -import SelectOption from '../UI/SelectOption'; -import { mapFor, mapVector } from '../Utils/MapFor'; -import RaisedButton from '../UI/RaisedButton'; -import IconButton from '../UI/IconButton'; -import ElementWithMenu from '../UI/Menu/ElementWithMenu'; -import SemiControlledTextField from '../UI/SemiControlledTextField'; -import newNameGenerator from '../Utils/NewNameGenerator'; -import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout'; -import ChoicesEditor, { type Choice } from '../ChoicesEditor'; -import ColorField from '../UI/ColorField'; -import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete'; -import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView'; -import ThreeDotsMenu from '../UI/CustomSvgIcons/ThreeDotsMenu'; -import { getMeasurementUnitShortLabel } from '../PropertiesEditor/PropertiesMapToSchema'; -import Add from '../UI/CustomSvgIcons/Add'; -import { DragHandleIcon } from '../UI/DragHandle'; -import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; -import DropIndicator from '../UI/SortableVirtualizedItemList/DropIndicator'; -import { makeDragSourceAndDropTarget } from '../UI/DragAndDrop/DragSourceAndDropTarget'; -import useForceUpdate from '../Utils/UseForceUpdate'; -import Clipboard from '../Utils/Clipboard'; -import { SafeExtractor } from '../Utils/SafeExtractor'; -import { - serializeToJSObject, - unserializeFromJSObject, -} from '../Utils/Serializer'; -import PasteIcon from '../UI/CustomSvgIcons/Clipboard'; -import ResponsiveFlatButton from '../UI/ResponsiveFlatButton'; -import { EmptyPlaceholder } from '../UI/EmptyPlaceholder'; -import useAlertDialog from '../UI/Alert/useAlertDialog'; -import SearchBar from '../UI/SearchBar'; -import ResourceTypeSelectField from '../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; -import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; - -const gd: libGDevelop = global.gd; - -const PROPERTIES_CLIPBOARD_KIND = 'Properties'; - -const DragSourceAndDropTarget = makeDragSourceAndDropTarget( - 'behavior-properties-list' -); - -const styles = { - rowContainer: { - display: 'flex', - flexDirection: 'column', - marginTop: 5, - }, - rowContent: { - display: 'flex', - flex: 1, - alignItems: 'center', - padding: '8px 0px', - }, -}; - -export const usePropertyOverridingAlertDialog = () => { - const { showConfirmation } = useAlertDialog(); - return async (existingPropertyNames: Array): Promise => { - return await showConfirmation({ - title: t`Existing properties`, - message: t`These properties already exist:${'\n\n - ' + - existingPropertyNames.join('\n\n - ') + - '\n\n'}Do you want to replace them?`, - confirmButtonLabel: t`Replace`, - dismissButtonLabel: t`Omit`, - }); - }; -}; - -const setExtraInfoString = ( - property: gdNamedPropertyDescriptor, - value: string -) => { - const vectorString = new gd.VectorString(); - vectorString.push_back(value); - property.setExtraInfo(vectorString); - vectorString.delete(); -}; - -type Props = {| - project: gdProject, - projectScopedContainersAccessor: ProjectScopedContainersAccessor, - extension: gdEventsFunctionsExtension, - eventsBasedObject: gdEventsBasedObject, - onPropertiesUpdated?: () => void, - onRenameProperty: (oldName: string, newName: string) => void, - onPropertyTypeChanged: (propertyName: string) => void, - onEventsFunctionsAdded: () => void, -|}; - -// Those names are used internally by GDevelop. -const PROTECTED_PROPERTY_NAMES = ['name', 'type']; - -const getValidatedPropertyName = ( - properties: gdPropertiesContainer, - projectScopedContainers: gdProjectScopedContainers, - newName: string -): string => { - const variablesContainersList = projectScopedContainers.getVariablesContainersList(); - const objectsContainersList = projectScopedContainers.getObjectsContainersList(); - const safeAndUniqueNewName = newNameGenerator( - gd.Project.getSafeName(newName), - tentativeNewName => - properties.has(tentativeNewName) || - variablesContainersList.has(tentativeNewName) || - objectsContainersList.hasObjectNamed(tentativeNewName) || - PROTECTED_PROPERTY_NAMES.includes(tentativeNewName) - ); - return safeAndUniqueNewName; -}; - -const getChoicesArray = ( - property: gdNamedPropertyDescriptor -): Array => { - return mapVector(property.getChoices(), choice => ({ - value: choice.getValue(), - label: choice.getLabel(), - })); -}; - -export default function EventsBasedObjectPropertiesEditor({ - project, - projectScopedContainersAccessor, - extension, - eventsBasedObject, - onPropertiesUpdated, - onRenameProperty, - onPropertyTypeChanged, - onEventsFunctionsAdded, -}: Props) { - const scrollView = React.useRef(null); - const [ - justAddedPropertyName, - setJustAddedPropertyName, - ] = React.useState(null); - const justAddedPropertyElement = React.useRef(null); - - React.useEffect( - () => { - if ( - scrollView.current && - justAddedPropertyElement.current && - justAddedPropertyName - ) { - scrollView.current.scrollTo(justAddedPropertyElement.current); - setJustAddedPropertyName(null); - justAddedPropertyElement.current = null; - } - }, - [justAddedPropertyName] - ); - - const draggedProperty = React.useRef(null); - - const gdevelopTheme = React.useContext(GDevelopThemeContext); - - const showPropertyOverridingConfirmation = usePropertyOverridingAlertDialog(); - - const forceUpdate = useForceUpdate(); - - const [searchText, setSearchText] = React.useState(''); - const [ - searchMatchingPropertyNames, - setSearchMatchingPropertyNames, - ] = React.useState>([]); - - const triggerSearch = React.useCallback( - () => { - const properties = eventsBasedObject.getPropertyDescriptors(); - const matchingPropertyNames = mapVector( - properties, - (property: gdNamedPropertyDescriptor, i: number) => { - const lowerCaseSearchText = searchText.toLowerCase(); - return property - .getName() - .toLowerCase() - .includes(lowerCaseSearchText) || - property - .getLabel() - .toLowerCase() - .includes(lowerCaseSearchText) || - property - .getGroup() - .toLowerCase() - .includes(lowerCaseSearchText) - ? property.getName() - : null; - } - ).filter(Boolean); - setSearchMatchingPropertyNames(matchingPropertyNames); - }, - [eventsBasedObject, searchText] - ); - - React.useEffect( - () => { - if (searchText) { - triggerSearch(); - } else { - setSearchMatchingPropertyNames([]); - } - }, - [searchText, triggerSearch] - ); - - const addPropertyAt = React.useCallback( - (index: number) => { - const properties = eventsBasedObject.getPropertyDescriptors(); - - const newName = newNameGenerator('Property', name => - properties.has(name) - ); - const property = properties.insertNew(newName, index); - property.setType('Number'); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - setJustAddedPropertyName(newName); - setSearchText(''); - }, - [eventsBasedObject, forceUpdate, onPropertiesUpdated] - ); - - const addProperty = React.useCallback( - () => { - const properties = eventsBasedObject.getPropertyDescriptors(); - addPropertyAt(properties.getCount()); - }, - [addPropertyAt, eventsBasedObject] - ); - - const removeProperty = React.useCallback( - (name: string) => { - const properties = eventsBasedObject.getPropertyDescriptors(); - - properties.remove(name); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [eventsBasedObject, forceUpdate, onPropertiesUpdated] - ); - - const copyProperty = React.useCallback( - (property: gdNamedPropertyDescriptor) => { - Clipboard.set(PROPERTIES_CLIPBOARD_KIND, [ - { - name: property.getName(), - serializedProperty: serializeToJSObject(property), - }, - ]); - forceUpdate(); - }, - [forceUpdate] - ); - - const duplicateProperty = React.useCallback( - (property: gdNamedPropertyDescriptor, index: number) => { - const properties = eventsBasedObject.getPropertyDescriptors(); - const newName = newNameGenerator(property.getName(), name => - properties.has(name) - ); - - const newProperty = properties.insertNew(newName, index); - - unserializeFromJSObject(newProperty, serializeToJSObject(property)); - newProperty.setName(newName); - - forceUpdate(); - }, - [forceUpdate, eventsBasedObject] - ); - - const pasteProperties = React.useCallback( - async propertyInsertionIndex => { - const properties = eventsBasedObject.getPropertyDescriptors(); - - const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); - const propertyContents = SafeExtractor.extractArray(clipboardContent); - if (!propertyContents) return; - - const newNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - const existingNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - propertyContents.forEach(propertyContent => { - const name = SafeExtractor.extractStringProperty( - propertyContent, - 'name' - ); - const serializedProperty = SafeExtractor.extractObjectProperty( - propertyContent, - 'serializedProperty' - ); - if (!name || !serializedProperty) { - return; - } - - if (properties.has(name)) { - existingNamedProperties.push({ name, serializedProperty }); - } else { - newNamedProperties.push({ name, serializedProperty }); - } - }); - - let firstAddedPropertyName: string | null = null; - let index = propertyInsertionIndex; - newNamedProperties.forEach(({ name, serializedProperty }) => { - const property = properties.insertNew(name, index); - index++; - unserializeFromJSObject(property, serializedProperty); - if (!firstAddedPropertyName) { - firstAddedPropertyName = name; - } - }); - - let shouldOverrideProperties = false; - if (existingNamedProperties.length > 0) { - shouldOverrideProperties = await showPropertyOverridingConfirmation( - existingNamedProperties.map(namedProperty => namedProperty.name) - ); - - if (shouldOverrideProperties) { - existingNamedProperties.forEach(({ name, serializedProperty }) => { - if (properties.has(name)) { - const property = properties.get(name); - unserializeFromJSObject(property, serializedProperty); - } - }); - } - } - - setSearchText(''); - forceUpdate(); - if (firstAddedPropertyName) { - setJustAddedPropertyName(firstAddedPropertyName); - } else if (existingNamedProperties.length === 1) { - setJustAddedPropertyName(existingNamedProperties[0].name); - } - if (firstAddedPropertyName || shouldOverrideProperties) { - if (onPropertiesUpdated) onPropertiesUpdated(); - } - }, - [ - eventsBasedObject, - forceUpdate, - showPropertyOverridingConfirmation, - onPropertiesUpdated, - ] - ); - - const pastePropertiesAtTheEnd = React.useCallback( - async () => { - const properties = eventsBasedObject.getPropertyDescriptors(); - await pasteProperties(properties.getCount()); - }, - [eventsBasedObject, pasteProperties] - ); - - const pastePropertiesBefore = React.useCallback( - async (property: gdNamedPropertyDescriptor) => { - const properties = eventsBasedObject.getPropertyDescriptors(); - await pasteProperties(properties.getPosition(property)); - }, - [eventsBasedObject, pasteProperties] - ); - - const moveProperty = React.useCallback( - (oldIndex: number, newIndex: number) => { - const properties = eventsBasedObject.getPropertyDescriptors(); - - properties.move(oldIndex, newIndex); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [eventsBasedObject, forceUpdate, onPropertiesUpdated] - ); - - const movePropertyBefore = React.useCallback( - (targetProperty: gdNamedPropertyDescriptor) => { - const { current } = draggedProperty; - if (!current) return; - - const properties = eventsBasedObject.getPropertyDescriptors(); - - const draggedIndex = properties.getPosition(current); - const targetIndex = properties.getPosition(targetProperty); - - properties.move( - draggedIndex, - targetIndex > draggedIndex ? targetIndex - 1 : targetIndex - ); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [eventsBasedObject, forceUpdate, onPropertiesUpdated] - ); - - const setChoices = React.useCallback( - (property: gdNamedPropertyDescriptor) => { - return (choices: Array) => { - property.clearChoices(); - for (const choice of choices) { - property.addChoice(choice.value, choice.label); - } - if ( - !getChoicesArray(property).some( - choice => choice.value === property.getValue() - ) - ) { - property.setValue(''); - } - forceUpdate(); - }; - }, - [forceUpdate] - ); - - const getPropertyGroupNames = React.useCallback( - (): Array => { - const properties = eventsBasedObject.getPropertyDescriptors(); - - const groupNames = new Set(); - for (let i = 0; i < properties.size(); i++) { - const property = properties.at(i); - const group = property.getGroup() || ''; - groupNames.add(group); - } - return [...groupNames].sort((a, b) => a.localeCompare(b)); - }, - [eventsBasedObject] - ); - - const setHidden = React.useCallback( - (property: gdNamedPropertyDescriptor, enable: boolean) => { - property.setHidden(enable); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated] - ); - - const setAdvanced = React.useCallback( - (property: gdNamedPropertyDescriptor, enable: boolean) => { - property.setAdvanced(enable); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated] - ); - - const setDeprecated = React.useCallback( - (property: gdNamedPropertyDescriptor, enable: boolean) => { - property.setDeprecated(enable); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }, - [forceUpdate, onPropertiesUpdated] - ); - - const isClipboardContainingProperties = Clipboard.has( - PROPERTIES_CLIPBOARD_KIND - ); - - const properties = eventsBasedObject.getPropertyDescriptors(); - - return ( - - {({ i18n }) => ( - - {properties.getCount() > 0 ? ( - - - - {mapVector( - properties, - (property: gdNamedPropertyDescriptor, i: number) => { - const propertyRef = - justAddedPropertyName === property.getName() - ? justAddedPropertyElement - : null; - - if ( - searchText && - !searchMatchingPropertyNames.includes( - property.getName() - ) - ) { - return null; - } - - return ( - { - draggedProperty.current = property; - return {}; - }} - canDrag={() => true} - canDrop={() => true} - drop={() => { - movePropertyBefore(property); - }} - > - {({ - connectDragSource, - connectDropTarget, - isOver, - canDrop, - }) => - connectDropTarget( -
- {isOver && } -
- {connectDragSource( - - - - - - )} - - - { - if (newName === property.getName()) - return; - - const projectScopedContainers = projectScopedContainersAccessor.get(); - const validatedNewName = getValidatedPropertyName( - properties, - projectScopedContainers, - newName - ); - onRenameProperty( - property.getName(), - validatedNewName - ); - property.setName(validatedNewName); - - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - /> - - - { - if (value === 'Hidden') { - setHidden(property, true); - setDeprecated(property, false); - setAdvanced(property, false); - } else if (value === 'Deprecated') { - setHidden(property, false); - setDeprecated(property, true); - setAdvanced(property, false); - } else if (value === 'Advanced') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, true); - } else if (value === 'Visible') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, false); - } - }} - fullWidth - > - - - - - - - - - - - } - buildMenuTemplate={(i18n: I18nType) => [ - { - label: i18n._(t`Add a property below`), - click: () => addPropertyAt(i + 1), - }, - { - label: i18n._(t`Delete`), - click: () => - removeProperty(property.getName()), - }, - { - label: i18n._(t`Copy`), - click: () => copyProperty(property), - }, - { - label: i18n._(t`Paste`), - click: () => - pastePropertiesBefore(property), - enabled: isClipboardContainingProperties, - }, - { - label: i18n._(t`Duplicate`), - click: () => - duplicateProperty(property, i + 1), - }, - { type: 'separator' }, - { - label: i18n._(t`Move up`), - click: () => moveProperty(i, i - 1), - enabled: i - 1 >= 0, - }, - { - label: i18n._(t`Move down`), - click: () => moveProperty(i, i + 1), - enabled: i + 1 < properties.getCount(), - }, - { - label: i18n._( - t`Generate expression and action` - ), - click: () => { - gd.PropertyFunctionGenerator.generateObjectGetterAndSetter( - project, - extension, - eventsBasedObject, - property - ); - onEventsFunctionsAdded(); - }, - enabled: gd.PropertyFunctionGenerator.canGenerateGetterAndSetter( - eventsBasedObject, - property - ), - }, - ]} - /> - -
- - - - Type} - value={property.getType()} - onChange={(e, i, value: string) => { - property.setType(value); - if (value === 'Resource') { - setExtraInfoString( - property, - 'json' - ); - } - forceUpdate(); - onPropertyTypeChanged( - property.getName() - ); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - - - - - - - - - - {property.getType() === 'Number' && ( - Measurement unit - } - value={property - .getMeasurementUnit() - .getName()} - onChange={(e, i, value: string) => { - property.setMeasurementUnit( - gd.MeasurementUnit.getDefaultMeasurementUnitByName( - value - ) - ); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - {mapFor( - 0, - gd.MeasurementUnit.getDefaultMeasurementUnitsCount(), - i => { - const measurementUnit = gd.MeasurementUnit.getDefaultMeasurementUnitAtIndex( - i - ); - const unitShortLabel = getMeasurementUnitShortLabel( - measurementUnit - ); - const label = - measurementUnit.getLabel() + - (unitShortLabel.length > 0 - ? ' — ' + unitShortLabel - : ''); - return ( - - ); - } - )} - - )} - {(property.getType() === 'String' || - property.getType() === 'Number' || - property.getType() === - 'MultilineString') && ( - Default value - } - hintText={ - property.getType() === 'Number' - ? '123' - : 'ABC' - } - value={property.getValue()} - onChange={newValue => { - property.setValue(newValue); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - multiline={ - property.getType() === - 'MultilineString' - } - fullWidth - /> - )} - {property.getType() === 'Boolean' && ( - Default value - } - value={ - property.getValue() === 'true' - ? 'true' - : 'false' - } - onChange={(e, i, value) => { - property.setValue(value); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - - - - )} - {property.getType() === 'Color' && ( - Color - } - disableAlpha - fullWidth - color={property.getValue()} - onChange={color => { - property.setValue(color); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - /> - )} - {property.getType() === 'Choice' && ( - Default value - } - value={property.getValue()} - onChange={(e, i, value) => { - property.setValue(value); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - > - {getChoicesArray(property).map( - (choice, index) => ( - - ) - )} - - )} - {property.getType() === 'Resource' && ( - 0 - ? property.getExtraInfo().at(0) - : '' - } - onChange={(e, i, value) => { - setExtraInfoString(property, value); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - fullWidth - /> - )} - - {property.getType() === 'Choice' && ( - - )} - - Short label - } - translatableHintText={t`Make the purpose of the property easy to understand`} - floatingLabelFixed - value={property.getLabel()} - onChange={text => { - property.setLabel(text); - forceUpdate(); - }} - fullWidth - /> - Group name - } - hintText={t`Leave it empty to use the default group`} - fullWidth - value={property.getGroup()} - onChange={text => { - property.setGroup(text); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - dataSource={getPropertyGroupNames().map( - name => ({ - text: name, - value: name, - }) - )} - openOnFocus={true} - /> - - Description - } - translatableHintText={t`Optionally, explain the purpose of the property in more details`} - floatingLabelFixed - value={property.getDescription()} - onChange={text => { - property.setDescription(text); - forceUpdate(); - }} - fullWidth - /> - - -
- ) - } -
- ); - } - )} -
-
- - - - } - label={Paste} - onClick={() => { - pastePropertiesAtTheEnd(); - }} - disabled={!isClipboardContainingProperties} - /> - {}} - onChange={text => setSearchText(text)} - placeholder={t`Search properties`} - /> - - - Add a property} - onClick={addProperty} - icon={} - /> - - - -
- ) : ( - - Add your first property} - description={ - Properties store data inside objects. - } - actionLabel={Add a property} - helpPagePath={'/behaviors/events-based-behaviors'} - helpPageAnchor={'add-and-use-properties-in-a-behavior'} - onAction={addProperty} - secondaryActionIcon={} - secondaryActionLabel={ - isClipboardContainingProperties ? Paste : null - } - onSecondaryAction={() => { - pastePropertiesAtTheEnd(); - }} - /> - - )} -
- )} -
- ); -} diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 7fafcbb30cb3..970324496d7e 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -22,7 +22,7 @@ import OptionsEditorDialog from './OptionsEditorDialog'; import { EventsBasedBehaviorEditorPanel, type EventsBasedBehaviorEditorPanelInterface, -} from '../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel'; +} from '../EventsBasedBehaviorOrObjectEditor'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import BehaviorMethodSelectorDialog from './BehaviorMethodSelectorDialog'; import ObjectMethodSelectorDialog from './ObjectMethodSelectorDialog'; From 193a603fe102f5c33e85a475198d351dffadfd46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 25 Dec 2025 13:44:46 +0100 Subject: [PATCH 07/30] Move files --- .../EventsBasedBehaviorEditor.js | 30 +++++----- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 56 +++++++++---------- .../EventsBasedObjectEditor.js | 26 ++++----- .../index.js | 14 ++--- .../EventsFunctionsExtensionEditor/index.js | 2 +- 5 files changed, 64 insertions(+), 64 deletions(-) rename newIDE/app/src/{ => EventsFunctionsExtensionEditor}/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js (88%) rename newIDE/app/src/{ => EventsFunctionsExtensionEditor}/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js (95%) rename newIDE/app/src/{ => EventsFunctionsExtensionEditor}/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js (89%) rename newIDE/app/src/{ => EventsFunctionsExtensionEditor}/EventsBasedBehaviorOrObjectEditor/index.js (92%) diff --git a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js similarity index 88% rename from newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js rename to newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js index b4ae8fd4ed00..1ba3fde3ebd0 100644 --- a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor.js @@ -4,21 +4,21 @@ import { t } from '@lingui/macro'; import { I18n } from '@lingui/react'; import * as React from 'react'; -import TextField from '../UI/TextField'; -import SemiControlledTextField from '../UI/SemiControlledTextField'; -import ObjectTypeSelector from '../ObjectTypeSelector'; -import DismissableAlertMessage from '../UI/DismissableAlertMessage'; -import AlertMessage from '../UI/AlertMessage'; -import { ColumnStackLayout } from '../UI/Layout'; -import useForceUpdate from '../Utils/UseForceUpdate'; -import HelpButton from '../UI/HelpButton'; -import { Line } from '../UI/Grid'; -import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; -import Checkbox from '../UI/Checkbox'; -import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExtensionEditor'; -import SelectField from '../UI/SelectField'; -import SelectOption from '../UI/SelectOption'; -import Window from '../Utils/Window'; +import TextField from '../../UI/TextField'; +import SemiControlledTextField from '../../UI/SemiControlledTextField'; +import ObjectTypeSelector from '../../ObjectTypeSelector'; +import DismissableAlertMessage from '../../UI/DismissableAlertMessage'; +import AlertMessage from '../../UI/AlertMessage'; +import { ColumnStackLayout } from '../../UI/Layout'; +import useForceUpdate from '../../Utils/UseForceUpdate'; +import HelpButton from '../../UI/HelpButton'; +import { Line } from '../../UI/Grid'; +import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext'; +import Checkbox from '../../UI/Checkbox'; +import { type ExtensionItemConfigurationAttribute } from '../../EventsFunctionsExtensionEditor'; +import SelectField from '../../UI/SelectField'; +import SelectOption from '../../UI/SelectOption'; +import Window from '../../Utils/Window'; const gd: libGDevelop = global.gd; diff --git a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js similarity index 95% rename from newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js rename to newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index 653a890275a1..b530f498e925 100644 --- a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -4,37 +4,37 @@ import { t } from '@lingui/macro'; import { I18n } from '@lingui/react'; import { type I18n as I18nType } from '@lingui/core'; import * as React from 'react'; -import { Column, Line, Spacer } from '../UI/Grid'; -import { LineStackLayout } from '../UI/Layout'; -import SelectField from '../UI/SelectField'; -import SelectOption from '../UI/SelectOption'; -import { mapFor, mapVector } from '../Utils/MapFor'; -import RaisedButton from '../UI/RaisedButton'; -import IconButton from '../UI/IconButton'; -import ElementWithMenu from '../UI/Menu/ElementWithMenu'; -import SemiControlledTextField from '../UI/SemiControlledTextField'; -import newNameGenerator from '../Utils/NewNameGenerator'; -import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout'; -import ChoicesEditor, { type Choice } from '../ChoicesEditor'; -import ColorField from '../UI/ColorField'; -import BehaviorTypeSelector from '../BehaviorTypeSelector'; -import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete'; -import { getMeasurementUnitShortLabel } from '../PropertiesEditor/PropertiesMapToSchema'; -import Add from '../UI/CustomSvgIcons/Add'; -import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; -import useForceUpdate from '../Utils/UseForceUpdate'; -import Clipboard from '../Utils/Clipboard'; -import { SafeExtractor } from '../Utils/SafeExtractor'; +import { Column, Line, Spacer } from '../../UI/Grid'; +import { LineStackLayout } from '../../UI/Layout'; +import SelectField from '../../UI/SelectField'; +import SelectOption from '../../UI/SelectOption'; +import { mapFor, mapVector } from '../../Utils/MapFor'; +import RaisedButton from '../../UI/RaisedButton'; +import IconButton from '../../UI/IconButton'; +import ElementWithMenu from '../../UI/Menu/ElementWithMenu'; +import SemiControlledTextField from '../../UI/SemiControlledTextField'; +import newNameGenerator from '../../Utils/NewNameGenerator'; +import { ResponsiveLineStackLayout, ColumnStackLayout } from '../../UI/Layout'; +import ChoicesEditor, { type Choice } from '../../ChoicesEditor'; +import ColorField from '../../UI/ColorField'; +import BehaviorTypeSelector from '../../BehaviorTypeSelector'; +import SemiControlledAutoComplete from '../../UI/SemiControlledAutoComplete'; +import { getMeasurementUnitShortLabel } from '../../PropertiesEditor/PropertiesMapToSchema'; +import Add from '../../UI/CustomSvgIcons/Add'; +import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; +import useForceUpdate from '../../Utils/UseForceUpdate'; +import Clipboard from '../../Utils/Clipboard'; +import { SafeExtractor } from '../../Utils/SafeExtractor'; import { serializeToJSObject, unserializeFromJSObject, -} from '../Utils/Serializer'; -import PasteIcon from '../UI/CustomSvgIcons/Clipboard'; -import ResponsiveFlatButton from '../UI/ResponsiveFlatButton'; -import { EmptyPlaceholder } from '../UI/EmptyPlaceholder'; -import useAlertDialog from '../UI/Alert/useAlertDialog'; -import ResourceTypeSelectField from '../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; -import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; +} from '../../Utils/Serializer'; +import PasteIcon from '../../UI/CustomSvgIcons/Clipboard'; +import ResponsiveFlatButton from '../../UI/ResponsiveFlatButton'; +import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder'; +import useAlertDialog from '../../UI/Alert/useAlertDialog'; +import ResourceTypeSelectField from '../../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; +import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; const gd: libGDevelop = global.gd; diff --git a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js similarity index 89% rename from newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js rename to newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js index a52bd1f217cf..bc886c365ebc 100644 --- a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor.js @@ -3,19 +3,19 @@ import { Trans } from '@lingui/macro'; import { t } from '@lingui/macro'; import * as React from 'react'; -import TextField from '../UI/TextField'; -import SemiControlledTextField from '../UI/SemiControlledTextField'; -import DismissableAlertMessage from '../UI/DismissableAlertMessage'; -import AlertMessage from '../UI/AlertMessage'; -import { ColumnStackLayout } from '../UI/Layout'; -import useForceUpdate from '../Utils/UseForceUpdate'; -import Checkbox from '../UI/Checkbox'; -import HelpButton from '../UI/HelpButton'; -import { Line } from '../UI/Grid'; -import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; -import RaisedButton from '../UI/RaisedButton'; -import Window from '../Utils/Window'; -import ScrollView from '../UI/ScrollView'; +import TextField from '../../UI/TextField'; +import SemiControlledTextField from '../../UI/SemiControlledTextField'; +import DismissableAlertMessage from '../../UI/DismissableAlertMessage'; +import AlertMessage from '../../UI/AlertMessage'; +import { ColumnStackLayout } from '../../UI/Layout'; +import useForceUpdate from '../../Utils/UseForceUpdate'; +import Checkbox from '../../UI/Checkbox'; +import HelpButton from '../../UI/HelpButton'; +import { Line } from '../../UI/Grid'; +import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext'; +import RaisedButton from '../../UI/RaisedButton'; +import Window from '../../Utils/Window'; +import ScrollView from '../../UI/ScrollView'; const gd: libGDevelop = global.gd; diff --git a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js similarity index 92% rename from newIDE/app/src/EventsBasedBehaviorOrObjectEditor/index.js rename to newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js index 5d456e3bd174..2e7e004badce 100644 --- a/newIDE/app/src/EventsBasedBehaviorOrObjectEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js @@ -6,13 +6,13 @@ import { EventsBasedBehaviorPropertiesEditor, type EventsBasedBehaviorPropertiesEditorInterface, } from './EventsBasedBehaviorOrObjectPropertiesEditor'; -import Background from '../UI/Background'; -import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; -import { type ExtensionItemConfigurationAttribute } from '../EventsFunctionsExtensionEditor'; -import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; -import Text from '../UI/Text'; -import { ColumnStackLayout } from '../UI/Layout'; -import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView'; +import Background from '../../UI/Background'; +import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext'; +import { type ExtensionItemConfigurationAttribute } from '../../EventsFunctionsExtensionEditor'; +import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; +import Text from '../../UI/Text'; +import { ColumnStackLayout } from '../../UI/Layout'; +import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView'; import EventsBasedObjectEditor from './EventsBasedObjectEditor'; type Props = {| diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 970324496d7e..930628c16bca 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -22,7 +22,7 @@ import OptionsEditorDialog from './OptionsEditorDialog'; import { EventsBasedBehaviorEditorPanel, type EventsBasedBehaviorEditorPanelInterface, -} from '../EventsBasedBehaviorOrObjectEditor'; +} from './EventsBasedBehaviorOrObjectEditor'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import BehaviorMethodSelectorDialog from './BehaviorMethodSelectorDialog'; import ObjectMethodSelectorDialog from './ObjectMethodSelectorDialog'; From a5008a7202ccf31d9c44528f6c789239a0ac8668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 25 Dec 2025 14:23:24 +0100 Subject: [PATCH 08/30] Fix property renaming --- ...sBasedEntityPropertyTreeViewItemContent.js | 34 +++++++++++++++++++ .../PropertyListEditor/index.js | 5 +++ .../EventsFunctionsExtensionEditor/index.js | 7 +++- 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index d8d58b0395db..33ef810157d5 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -16,6 +16,7 @@ import Tooltip from '@material-ui/core/Tooltip'; import { type HTMLDataset } from '../../Utils/HTMLDataset'; import VisibilityOffIcon from '../../UI/CustomSvgIcons/VisibilityOff'; import { renderQuickCustomizationMenuItems } from '../../QuickCustomization/QuickCustomizationMenuItems'; +import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; const gd: libGDevelop = global.gd; @@ -25,9 +26,31 @@ const styles = { tooltip: { marginRight: 5, verticalAlign: 'bottom' }, }; +// Those names are used internally by GDevelop. +const PROTECTED_PROPERTY_NAMES = ['name', 'type']; + +const getValidatedPropertyName = ( + properties: gdPropertiesContainer, + projectScopedContainers: gdProjectScopedContainers, + newName: string +): string => { + const variablesContainersList = projectScopedContainers.getVariablesContainersList(); + const objectsContainersList = projectScopedContainers.getObjectsContainersList(); + const safeAndUniqueNewName = newNameGenerator( + gd.Project.getSafeName(newName), + tentativeNewName => + properties.has(tentativeNewName) || + variablesContainersList.has(tentativeNewName) || + objectsContainersList.hasObjectNamed(tentativeNewName) || + PROTECTED_PROPERTY_NAMES.includes(tentativeNewName) + ); + return safeAndUniqueNewName; +}; + export type EventsBasedEntityPropertyTreeViewItemProps = {| ...TreeItemProps, project: gdProject, + projectScopedContainersAccessor: ProjectScopedContainersAccessor, extension: gdEventsFunctionsExtension, eventsBasedEntity: gdAbstractEventsBasedEntity, eventsBasedBehavior: ?gdEventsBasedBehavior, @@ -113,6 +136,17 @@ export class EventsBasedEntityPropertyTreeViewItemContent return; } this.props.onRenameProperty(oldName, newName); + + const projectScopedContainers = this.props.projectScopedContainersAccessor.get(); + const validatedNewName = getValidatedPropertyName( + this.props.properties, + projectScopedContainers, + newName + ); + this.props.onRenameProperty(oldName, validatedNewName); + this.property.setName(validatedNewName); + + this._onProjectItemModified(); } edit(): void { diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index b04d86da9789..54b3e782b8d5 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -42,6 +42,7 @@ import { type HTMLDataset } from '../../Utils/HTMLDataset'; import EmptyMessage from '../../UI/EmptyMessage'; import { ColumnStackLayout } from '../../UI/Layout'; import { useShouldAutofocusInput } from '../../UI/Responsive/ScreenTypeMeasurer'; +import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; export const getProjectManagerItemId = (identifier: string) => `project-manager-tab-${identifier}`; @@ -374,6 +375,7 @@ export type PropertyListEditorInterface = {| type Props = {| project: gdProject, + projectScopedContainersAccessor: ProjectScopedContainersAccessor, extension: gdEventsFunctionsExtension, eventsBasedBehavior: ?gdEventsBasedBehavior, eventsBasedObject: ?gdEventsBasedObject, @@ -387,6 +389,7 @@ const PropertyListEditor = React.forwardRef( ( { project, + projectScopedContainersAccessor, extension, eventsBasedBehavior, eventsBasedObject, @@ -566,6 +569,7 @@ const PropertyListEditor = React.forwardRef( editName, scrollToItem, project, + projectScopedContainersAccessor, extension, eventsBasedEntity, eventsBasedBehavior, @@ -591,6 +595,7 @@ const PropertyListEditor = React.forwardRef( editName, scrollToItem, project, + projectScopedContainersAccessor, extension, eventsBasedBehavior, eventsBasedObject, diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 930628c16bca..e211ddd08f20 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -1430,10 +1430,15 @@ export default class EventsFunctionsExtensionEditor extends React.Component< unsavedChanges={this.props.unsavedChanges} getFunctionGroupNames={this._getFunctionGroupNames} /> - ) : selectedEventsBasedObject || selectedEventsBasedBehavior ? ( + ) : (selectedEventsBasedObject || + selectedEventsBasedBehavior) && + this._projectScopedContainersAccessor ? ( (this.propertyListEditor = ref)} project={project} + projectScopedContainersAccessor={ + this._projectScopedContainersAccessor + } extension={eventsFunctionsExtension} eventsBasedBehavior={selectedEventsBasedBehavior} eventsBasedObject={selectedEventsBasedObject} From c977b041be71c7db14cb961de2db8983c1564226 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 25 Dec 2025 19:42:32 +0100 Subject: [PATCH 09/30] Fix stories --- .../EventsBasedBehaviorOrObjectEditor/index.js | 15 ++++++++------- .../src/EventsFunctionsExtensionEditor/index.js | 12 ++++++------ .../EventsBasedBehaviorEditor.stories.js | 2 +- .../EventsBasedBehaviorEditorPanel.stories.js | 16 ++++++++++++---- .../EventsBasedObjectEditor.stories.js | 2 +- .../EventsBasedObjectEditorPanel.stories.js | 9 ++++++--- 6 files changed, 34 insertions(+), 22 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js index 2e7e004badce..7e1b740610e9 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js @@ -35,14 +35,14 @@ type Props = {| ) => void, |}; -export type EventsBasedBehaviorEditorPanelInterface = {| +export type EventsBasedBehaviorOrObjectEditorInterface = {| forceUpdateProperties: () => void, scrollToProperty: (propertyName: string) => void, |}; -export const EventsBasedBehaviorEditorPanel = React.forwardRef< +export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< Props, - EventsBasedBehaviorEditorPanelInterface + EventsBasedBehaviorOrObjectEditorInterface >( ( { @@ -102,9 +102,6 @@ export const EventsBasedBehaviorEditorPanel = React.forwardRef< - - Configuration - {eventsBasedBehavior ? ( ) : null} - Behavior properties + {eventsBasedObject ? ( + Object properties + ) : ( + Behavior properties + )} {eventsBasedEntity && ( ) : selectedEventsBasedBehavior && this._projectScopedContainersAccessor ? ( - (this.eventsBasedBehaviorEditorPanel = ref)} project={project} projectScopedContainersAccessor={ @@ -1603,7 +1603,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< /> ) : selectedEventsBasedObject && this._projectScopedContainersAccessor ? ( - (this.eventsBasedObjectEditorPanel = ref)} project={project} projectScopedContainersAccessor={ diff --git a/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditor.stories.js b/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditor.stories.js index 789672f86616..0a3ef06ac1f8 100644 --- a/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditor.stories.js +++ b/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditor.stories.js @@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions'; import { testProject } from '../../GDevelopJsInitializerDecorator'; import paperDecorator from '../../PaperDecorator'; -import EventsBasedBehaviorEditor from '../../../EventsBasedBehaviorEditor/'; +import EventsBasedBehaviorEditor from '../../../EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorEditor'; export default { title: 'EventsBasedBehaviorEditor/index', diff --git a/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js b/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js index 414be7964d68..e4c94a7e9049 100644 --- a/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js +++ b/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js @@ -6,17 +6,17 @@ import { action } from '@storybook/addon-actions'; // Keep first as it creates the `global.gd` object: import { testProject } from '../../GDevelopJsInitializerDecorator'; -import EventsBasedBehaviorEditorPanel from '../../../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel'; +import { EventsBasedBehaviorOrObjectEditor } from '../../../EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor'; import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider'; export default { title: 'EventsBasedBehaviorEditor/EventsBasedBehaviorEditorDialog', - component: EventsBasedBehaviorEditorPanel, + component: EventsBasedBehaviorOrObjectEditor, }; export const Default = () => ( - ( onPropertyTypeChanged={action('onPropertyTypeChanged')} onRenameSharedProperty={action('shared property rename')} onEventsFunctionsAdded={action('functions added')} + onFocusProperty={action('onFocusProperty')} + onPropertiesUpdated={action('onPropertiesUpdated')} + onEventsBasedObjectChildrenEdited={action('onEventsBasedObjectChildrenEdited')} + onOpenCustomObjectEditor={action('onOpenCustomObjectEditor')} /> ); export const WithoutFunction = () => ( - ( onPropertyTypeChanged={action('onPropertyTypeChanged')} onRenameSharedProperty={action('shared property rename')} onEventsFunctionsAdded={action('functions added')} + onFocusProperty={action('onFocusProperty')} + onPropertiesUpdated={action('onPropertiesUpdated')} + onEventsBasedObjectChildrenEdited={action('onEventsBasedObjectChildrenEdited')} + onOpenCustomObjectEditor={action('onOpenCustomObjectEditor')} /> ); diff --git a/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditor.stories.js b/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditor.stories.js index 69bd53e6cb69..9b0ad1cc424d 100644 --- a/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditor.stories.js +++ b/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditor.stories.js @@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions'; import { testProject } from '../../GDevelopJsInitializerDecorator'; import paperDecorator from '../../PaperDecorator'; -import EventsBasedObjectEditor from '../../../EventsBasedObjectEditor'; +import EventsBasedObjectEditor from '../../../EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedObjectEditor'; export default { title: 'EventsBasedObjectEditor/index', diff --git a/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.stories.js b/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.stories.js index 7963125b4d8d..507af69d776c 100644 --- a/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.stories.js +++ b/newIDE/app/src/stories/componentStories/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.stories.js @@ -6,17 +6,17 @@ import { action } from '@storybook/addon-actions'; // Keep first as it creates the `global.gd` object: import { testProject } from '../../GDevelopJsInitializerDecorator'; -import EventsBasedObjectEditorPanel from '../../../EventsBasedObjectEditor/EventsBasedObjectEditorPanel'; +import { EventsBasedBehaviorOrObjectEditor } from '../../../EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor'; import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider'; export default { title: 'EventsBasedObjectEditor/EventsBasedObjectEditorDialog', - component: EventsBasedObjectEditorPanel, + component: EventsBasedBehaviorOrObjectEditor, }; export const Default = () => ( - ( onEventsBasedObjectChildrenEdited={action( 'onEventsBasedObjectChildrenEdited' )} + onFocusProperty={action('onFocusProperty')} + onPropertiesUpdated={action('onPropertiesUpdated')} + onRenameSharedProperty={action('onRenameSharedProperty')} /> ); From 63d2f557a3b135fc06eb0bd2b38f0ad8f498748d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 25 Dec 2025 22:56:15 +0100 Subject: [PATCH 10/30] Handle shared properties --- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 10 +- .../index.js | 51 ++++-- ...sBasedEntityPropertyTreeViewItemContent.js | 48 +++-- .../PropertyListEditor/index.js | 164 +++++++++++++----- .../EventsFunctionsExtensionEditor/index.js | 61 ++++--- .../EventsBasedBehaviorEditorPanel.stories.js | 8 +- 6 files changed, 241 insertions(+), 101 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index b530f498e925..8eb056a68870 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -85,7 +85,7 @@ type Props = {| eventsBasedBehavior?: ?gdEventsBasedBehavior, eventsBasedObject?: ?gdEventsBasedObject, properties: gdPropertiesContainer, - isSceneProperties?: boolean, + isSharedProperties?: boolean, onPropertiesUpdated: () => void, onFocusProperty: (propertyName: string) => void, onRenameProperty: (oldName: string, newName: string) => void, @@ -141,7 +141,7 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< eventsBasedBehavior, eventsBasedObject, properties, - isSceneProperties, + isSharedProperties, onPropertiesUpdated, onFocusProperty, onRenameProperty, @@ -521,14 +521,14 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< label={t`Leaderboard (text)`} /> )} - {eventsBasedBehavior && !isSceneProperties && ( + {eventsBasedBehavior && !isSharedProperties && ( )} - {eventsBasedBehavior && !isSceneProperties && ( + {eventsBasedBehavior && !isSharedProperties && ( - {eventsBasedBehavior && !isSceneProperties && ( + {eventsBasedBehavior && !isSharedProperties && ( void, onRenameSharedProperty: (oldName: string, newName: string) => void, onPropertyTypeChanged: (propertyName: string) => void, - onFocusProperty: (propertyName: string) => void, + onFocusProperty: (propertyName: string, isSharedProperties: boolean) => void, onPropertiesUpdated: () => void, onEventsFunctionsAdded: () => void, unsavedChanges?: ?UnsavedChanges, @@ -37,7 +37,8 @@ type Props = {| export type EventsBasedBehaviorOrObjectEditorInterface = {| forceUpdateProperties: () => void, - scrollToProperty: (propertyName: string) => void, + scrollToConfiguration: () => void, + scrollToProperty: (propertyName: string, isSharedProperties: boolean) => void, |}; export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< @@ -78,14 +79,9 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< const propertiesEditor = React.useRef( null ); - - const scrollToProperty = React.useCallback((propertyName: string) => { - if (scrollView.current && propertiesEditor.current) { - scrollView.current.scrollTo( - propertiesEditor.current.getPropertyEditorRef(propertyName) - ); - } - }, []); + const scenePropertiesEditor = React.useRef( + null + ); React.useImperativeHandle(ref, () => ({ forceUpdateProperties: () => { @@ -93,7 +89,29 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< propertiesEditor.current.forceUpdate(); } }, - scrollToProperty, + scrollToConfiguration: () => { + if (scrollView.current) { + scrollView.current.scrollToPosition(0); + } + }, + scrollToProperty: (propertyName: string, isSharedProperties: boolean) => { + if (!scrollView.current) { + return; + } + if (isSharedProperties) { + if (scenePropertiesEditor.current) { + scrollView.current.scrollTo( + scenePropertiesEditor.current.getPropertyEditorRef(propertyName) + ); + } + } else { + if (propertiesEditor.current) { + scrollView.current.scrollTo( + propertiesEditor.current.getPropertyEditorRef(propertyName) + ); + } + } + }, })); const eventsBasedEntity = eventsBasedBehavior || eventsBasedObject; @@ -143,7 +161,9 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< } onRenameProperty={onRenameProperty} onPropertiesUpdated={_onPropertiesUpdated} - onFocusProperty={onFocusProperty} + onFocusProperty={propertyName => + onFocusProperty(propertyName, false) + } onPropertyTypeChanged={onPropertyTypeChanged} onEventsFunctionsAdded={onEventsFunctionsAdded} /> @@ -155,7 +175,8 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< )} {eventsBasedBehavior && ( + onFocusProperty(propertyName, true) + } onPropertyTypeChanged={onPropertyTypeChanged} onEventsFunctionsAdded={onEventsFunctionsAdded} /> diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 33ef810157d5..01195d8ebcb2 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -11,7 +11,12 @@ import { serializeToJSObject, unserializeFromJSObject, } from '../../Utils/Serializer'; -import { TreeViewItemContent, type TreeItemProps, scenesRootFolderId } from '.'; +import { + TreeViewItemContent, + type TreeItemProps, + propertiesRootFolderId, + sharedPropertiesRootFolderId, +} from '.'; import Tooltip from '@material-ui/core/Tooltip'; import { type HTMLDataset } from '../../Utils/HTMLDataset'; import VisibilityOffIcon from '../../UI/CustomSvgIcons/VisibilityOff'; @@ -56,8 +61,8 @@ export type EventsBasedEntityPropertyTreeViewItemProps = {| eventsBasedBehavior: ?gdEventsBasedBehavior, eventsBasedObject: ?gdEventsBasedObject, properties: gdPropertiesContainer, - isSceneProperties: boolean, - onOpenProperty: (name: string) => void, + isSharedProperties: boolean, + onOpenProperty: (name: string, isSharedProperties: boolean) => void, onRenameProperty: (newName: string, oldName: string) => void, showPropertyOverridingConfirmation: ( existingPropertyNames: string[] @@ -67,11 +72,14 @@ export type EventsBasedEntityPropertyTreeViewItemProps = {| |}; export const getEventsBasedEntityPropertyTreeViewItemId = ( - property: gdNamedPropertyDescriptor + property: gdNamedPropertyDescriptor, + isSharedProperties: boolean ): string => { // Pointers are used because they stay the same even when the names are // changed. - return `property-${property.ptr}`; + return `${isSharedProperties ? 'shared-property' : 'property'}-${ + property.ptr + }`; }; export class EventsBasedEntityPropertyTreeViewItemContent @@ -88,11 +96,13 @@ export class EventsBasedEntityPropertyTreeViewItemContent } isDescendantOf(itemContent: TreeViewItemContent): boolean { - return itemContent.getId() === scenesRootFolderId; + return itemContent.getId() === this.getRootId(); } getRootId(): string { - return scenesRootFolderId; + return this.props.isSharedProperties + ? sharedPropertiesRootFolderId + : propertiesRootFolderId; } getName(): string | React.Node { @@ -100,16 +110,22 @@ export class EventsBasedEntityPropertyTreeViewItemContent } getId(): string { - return getEventsBasedEntityPropertyTreeViewItemId(this.property); + return getEventsBasedEntityPropertyTreeViewItemId( + this.property, + this.props.isSharedProperties + ); } getHtmlId(index: number): ?string { - return `property-item-${index}`; + return `${ + this.props.isSharedProperties ? 'shared-property' : 'property' + }-item-${index}`; } getDataSet(): ?HTMLDataset { return { property: this.property.getName(), + isSharedProperties: this.props.isSharedProperties ? 'true' : 'false', }; } @@ -127,7 +143,10 @@ export class EventsBasedEntityPropertyTreeViewItemContent } onClick(): void { - this.props.onOpenProperty(this.property.getName()); + this.props.onOpenProperty( + this.property.getName(), + this.props.isSharedProperties + ); } rename(newName: string): void { @@ -199,7 +218,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent extension, eventsBasedBehavior, eventsBasedObject, - isSceneProperties, + isSharedProperties, onEventsFunctionsAdded, } = this.props; if (eventsBasedBehavior) { @@ -208,7 +227,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent extension, eventsBasedBehavior, this.property, - isSceneProperties + isSharedProperties ); } else if (eventsBasedObject) { gd.PropertyFunctionGenerator.generateObjectGetterAndSetter( @@ -371,7 +390,10 @@ export class EventsBasedEntityPropertyTreeViewItemContent this._onProjectItemModified(); this.props.editName( - getEventsBasedEntityPropertyTreeViewItemId(newProperty) + getEventsBasedEntityPropertyTreeViewItemId( + newProperty, + this.props.isSharedProperties + ) ); } diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 54b3e782b8d5..430617587e2d 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -47,18 +47,16 @@ import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/E export const getProjectManagerItemId = (identifier: string) => `project-manager-tab-${identifier}`; -const gameSettingsRootFolderId = getProjectManagerItemId('game-settings'); -const gamePropertiesItemId = getProjectManagerItemId('game-properties'); -export const scenesRootFolderId = getProjectManagerItemId('scenes'); -export const extensionsRootFolderId = getProjectManagerItemId('extensions'); -export const externalEventsRootFolderId = getProjectManagerItemId( - 'external-events' +const configurationItemId = getProjectManagerItemId( + 'events-based-entity-configuration' ); -export const externalLayoutsRootFolderId = getProjectManagerItemId( - 'external-layout' +export const propertiesRootFolderId = getProjectManagerItemId('properties'); +export const sharedPropertiesRootFolderId = getProjectManagerItemId( + 'properties' ); -const scenesEmptyPlaceholderId = 'scenes-placeholder'; +const propertiesEmptyPlaceholderId = 'properties-placeholder'; +const sharedPropertiesEmptyPlaceholderId = 'shared-properties-placeholder'; const styles = { listContainer: { @@ -370,7 +368,10 @@ export const usePropertyOverridingAlertDialog = () => { export type PropertyListEditorInterface = {| forceUpdateList: () => void, focusSearchBar: () => void, - setSelectedProperty: (propertyName: string) => void, + setSelectedProperty: ( + propertyName: string, + isSharedProperties: boolean + ) => void, |}; type Props = {| @@ -381,7 +382,8 @@ type Props = {| eventsBasedObject: ?gdEventsBasedObject, onPropertiesUpdated: () => void, onRenameProperty: (oldName: string, newName: string) => void, - onOpenProperty: (name: string) => void, + onOpenConfiguration: () => void, + onOpenProperty: (name: string, isSharedProperties: boolean) => void, onEventsFunctionsAdded: () => void, |}; @@ -395,6 +397,7 @@ const PropertyListEditor = React.forwardRef( eventsBasedObject, onPropertiesUpdated, onRenameProperty, + onOpenConfiguration, onOpenProperty, onEventsFunctionsAdded, }, @@ -446,6 +449,9 @@ const PropertyListEditor = React.forwardRef( const properties = eventsBasedEntity ? eventsBasedEntity.getPropertyDescriptors() : null; + const sharedProperties = eventsBasedBehavior + ? eventsBasedBehavior.getSharedPropertyDescriptors() + : null; const editName = React.useCallback( (itemId: string) => { @@ -463,7 +469,12 @@ const PropertyListEditor = React.forwardRef( ); const addProperty = React.useCallback( - (index: number, i18n: I18nType) => { + ( + properties: gdPropertiesContainer, + isSharedProperties: boolean, + index: number, + i18n: I18nType + ) => { if (!properties) return; const newName = newNameGenerator(i18n._(t`Property`), name => @@ -477,29 +488,36 @@ const PropertyListEditor = React.forwardRef( onProjectItemModified(); setSearchText(''); - const sceneItemId = getEventsBasedEntityPropertyTreeViewItemId( - property + const propertyItemId = getEventsBasedEntityPropertyTreeViewItemId( + property, + isSharedProperties ); if (treeViewRef.current) { - treeViewRef.current.openItems([sceneItemId, scenesRootFolderId]); + treeViewRef.current.openItems([ + propertyItemId, + isSharedProperties + ? sharedPropertiesRootFolderId + : propertiesRootFolderId, + ]); } // Scroll to the new behavior. // 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(sceneItemId); + scrollToItem(propertyItemId); + onOpenProperty(newName, isSharedProperties); }, 100); // A few ms is enough for a new render to be done. // We focus it so the user can edit the name directly. - editName(sceneItemId); + editName(propertyItemId); }, [ - properties, onPropertiesUpdated, onProjectItemModified, editName, scrollToItem, + onOpenProperty, ] ); @@ -575,7 +593,7 @@ const PropertyListEditor = React.forwardRef( eventsBasedBehavior, eventsBasedObject, properties, - isSceneProperties: false, + isSharedProperties: false, onOpenProperty, onPropertiesUpdated, onRenameProperty, @@ -606,19 +624,34 @@ const PropertyListEditor = React.forwardRef( ] ); + const sharedPropertiesTreeViewItemProps = React.useMemo( + () => + sharedProperties && propertiesTreeViewItemProps + ? { + ...propertiesTreeViewItemProps, + properties: sharedProperties, + isSharedProperties: true, + } + : null, + [propertiesTreeViewItemProps, sharedProperties] + ); + const createPropertyItem = React.useCallback( - (property: gdNamedPropertyDescriptor) => { - if (!propertiesTreeViewItemProps) { + (property: gdNamedPropertyDescriptor, isSharedProperties: boolean) => { + const treeViewItemProps = isSharedProperties + ? sharedPropertiesTreeViewItemProps + : propertiesTreeViewItemProps; + if (!treeViewItemProps) { return null; } return new LeafTreeViewItem( new EventsBasedEntityPropertyTreeViewItemContent( property, - propertiesTreeViewItemProps + treeViewItemProps ) ); }, - [propertiesTreeViewItemProps] + [propertiesTreeViewItemProps, sharedPropertiesTreeViewItemProps] ); const getTreeViewData = React.useCallback( @@ -628,23 +661,24 @@ const PropertyListEditor = React.forwardRef( : [ new LeafTreeViewItem( new ActionTreeViewItemContent( - gamePropertiesItemId, + configurationItemId, i18n._(t`Configuration`), - // TODO Scroll to the configuration - () => {}, + onOpenConfiguration, 'res/icons_default/properties_black.svg' ) ), { isRoot: true, content: new LabelTreeViewItemContent( - scenesRootFolderId, - i18n._(t`Behavior properties`), + propertiesRootFolderId, + eventsBasedObject + ? i18n._(t`Object properties`) + : i18n._(t`Behavior properties`), { icon: , label: i18n._(t`Add a property`), click: () => { - addProperty(0, i18n); + addProperty(properties, false, 0, i18n); }, id: 'add-property', } @@ -653,19 +687,57 @@ const PropertyListEditor = React.forwardRef( if (properties.getCount() === 0) { return [ new PlaceHolderTreeViewItem( - scenesEmptyPlaceholderId, + propertiesEmptyPlaceholderId, i18n._(t`Start by adding a new property.`) ), ]; } return mapFor(0, properties.getCount(), i => - createPropertyItem(properties.getAt(i)) + createPropertyItem(properties.getAt(i), false) ).filter(Boolean); }, }, - ]; + sharedProperties + ? { + isRoot: true, + content: new LabelTreeViewItemContent( + sharedPropertiesRootFolderId, + i18n._(t`Scene properties`), + { + icon: , + label: i18n._(t`Add a property`), + click: () => { + addProperty(sharedProperties, true, 0, i18n); + }, + id: 'add-shared-property', + } + ), + getChildren(i18n: I18nType): ?Array { + if (sharedProperties.getCount() === 0) { + return [ + new PlaceHolderTreeViewItem( + sharedPropertiesEmptyPlaceholderId, + i18n._(t`Start by adding a new property.`) + ), + ]; + } + return mapFor(0, sharedProperties.getCount(), i => + createPropertyItem(sharedProperties.getAt(i), true) + ).filter(Boolean); + }, + } + : null, + ].filter(Boolean); }, - [addProperty, createPropertyItem, properties, propertiesTreeViewItemProps] + [ + addProperty, + createPropertyItem, + eventsBasedObject, + onOpenConfiguration, + properties, + propertiesTreeViewItemProps, + sharedProperties, + ] ); React.useImperativeHandle(ref, () => ({ @@ -676,13 +748,20 @@ const PropertyListEditor = React.forwardRef( focusSearchBar: () => { if (searchBarRef.current) searchBarRef.current.focus(); }, - setSelectedProperty: (propertyName: string) => { - if (!properties || !properties.has(propertyName)) { + setSelectedProperty: ( + propertyName: string, + isSharedProperties: boolean + ) => { + const propertiesContainer = isSharedProperties + ? sharedProperties + : properties; + if (!propertiesContainer || !propertiesContainer.has(propertyName)) { return; } - const property = properties.get(propertyName); + const property = propertiesContainer.get(propertyName); const propertyItemId = getEventsBasedEntityPropertyTreeViewItemId( - property + property, + isSharedProperties ); setSelectedItems(selectedItems => { if ( @@ -691,7 +770,9 @@ const PropertyListEditor = React.forwardRef( ) { return selectedItems; } - return [createPropertyItem(property)].filter(Boolean); + return [createPropertyItem(property, isSharedProperties)].filter( + Boolean + ); }); scrollToItem(propertyItemId); }, @@ -750,11 +831,8 @@ const PropertyListEditor = React.forwardRef( ? eventsBasedEntity.ptr : 'no-eventsBasedEntity'; const initiallyOpenedNodeIds = [ - gameSettingsRootFolderId, - scenesRootFolderId, - extensionsRootFolderId, - externalEventsRootFolderId, - externalLayoutsRootFolderId, + propertiesRootFolderId, + sharedPropertiesRootFolderId, ]; return ( diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 0ae09f4d7e54..379b1c1ab4e2 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -156,8 +156,8 @@ export default class EventsFunctionsExtensionEditor extends React.Component< }; editor: ?EventsSheetInterface; eventsFunctionList: ?EventsFunctionsListInterface; - eventsBasedBehaviorEditorPanel: ?EventsBasedBehaviorOrObjectEditorInterface; - eventsBasedObjectEditorPanel: ?EventsBasedBehaviorOrObjectEditorInterface; + eventsBasedBehaviorEditor: ?EventsBasedBehaviorOrObjectEditorInterface; + eventsBasedObjectEditor: ?EventsBasedBehaviorOrObjectEditorInterface; propertyListEditor: ?PropertyListEditorInterface; _editorMosaic: ?EditorMosaicInterface; _editorNavigator: ?EditorNavigatorInterface; @@ -1442,14 +1442,6 @@ export default class EventsFunctionsExtensionEditor extends React.Component< extension={eventsFunctionsExtension} eventsBasedBehavior={selectedEventsBasedBehavior} eventsBasedObject={selectedEventsBasedObject} - onPropertiesUpdated={() => { - if (this.eventsBasedBehaviorEditorPanel) { - this.eventsBasedBehaviorEditorPanel.forceUpdateProperties(); - } - if (this.eventsBasedObjectEditorPanel) { - this.eventsBasedObjectEditorPanel.forceUpdateProperties(); - } - }} onRenameProperty={(oldName, newName) => { if (selectedEventsBasedBehavior) { this._onBehaviorPropertyRenamed( @@ -1465,15 +1457,30 @@ export default class EventsFunctionsExtensionEditor extends React.Component< ); } }} - onOpenProperty={propertyName => { - if (this.eventsBasedBehaviorEditorPanel) { - this.eventsBasedBehaviorEditorPanel.scrollToProperty( - propertyName - ); + onPropertiesUpdated={() => { + const eventsBasedEntityEditor = + this.eventsBasedBehaviorEditor || + this.eventsBasedObjectEditor; + if (eventsBasedEntityEditor) { + eventsBasedEntityEditor.forceUpdateProperties(); + } + }} + onOpenConfiguration={propertyName => { + const eventsBasedEntityEditor = + this.eventsBasedBehaviorEditor || + this.eventsBasedObjectEditor; + if (eventsBasedEntityEditor) { + eventsBasedEntityEditor.scrollToConfiguration(); } - if (this.eventsBasedObjectEditorPanel) { - this.eventsBasedObjectEditorPanel.scrollToProperty( - propertyName + }} + onOpenProperty={(propertyName, isSharedProperties) => { + const eventsBasedEntityEditor = + this.eventsBasedBehaviorEditor || + this.eventsBasedObjectEditor; + if (eventsBasedEntityEditor) { + eventsBasedEntityEditor.scrollToProperty( + propertyName, + isSharedProperties ); } }} @@ -1552,7 +1559,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< ) : selectedEventsBasedBehavior && this._projectScopedContainersAccessor ? ( (this.eventsBasedBehaviorEditorPanel = ref)} + ref={ref => (this.eventsBasedBehaviorEditor = ref)} project={project} projectScopedContainersAccessor={ this._projectScopedContainersAccessor @@ -1587,9 +1594,12 @@ export default class EventsFunctionsExtensionEditor extends React.Component< this.propertyListEditor.forceUpdateList(); } }} - onFocusProperty={propertyName => { + onFocusProperty={(propertyName, isSharedProperties) => { if (this.propertyListEditor) { - this.propertyListEditor.setSelectedProperty(propertyName); + this.propertyListEditor.setSelectedProperty( + propertyName, + isSharedProperties + ); } }} onEventsFunctionsAdded={() => { @@ -1604,7 +1614,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< ) : selectedEventsBasedObject && this._projectScopedContainersAccessor ? ( (this.eventsBasedObjectEditorPanel = ref)} + ref={ref => (this.eventsBasedObjectEditor = ref)} project={project} projectScopedContainersAccessor={ this._projectScopedContainersAccessor @@ -1633,9 +1643,12 @@ export default class EventsFunctionsExtensionEditor extends React.Component< this.propertyListEditor.forceUpdateList(); } }} - onFocusProperty={propertyName => { + onFocusProperty={(propertyName, isSharedProperties) => { if (this.propertyListEditor) { - this.propertyListEditor.setSelectedProperty(propertyName); + this.propertyListEditor.setSelectedProperty( + propertyName, + isSharedProperties + ); } }} onEventsFunctionsAdded={() => { diff --git a/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js b/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js index e4c94a7e9049..da25ac6a79b5 100644 --- a/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js +++ b/newIDE/app/src/stories/componentStories/EventsBasedBehaviorEditor/EventsBasedBehaviorEditorPanel.stories.js @@ -29,7 +29,9 @@ export const Default = () => ( onEventsFunctionsAdded={action('functions added')} onFocusProperty={action('onFocusProperty')} onPropertiesUpdated={action('onPropertiesUpdated')} - onEventsBasedObjectChildrenEdited={action('onEventsBasedObjectChildrenEdited')} + onEventsBasedObjectChildrenEdited={action( + 'onEventsBasedObjectChildrenEdited' + )} onOpenCustomObjectEditor={action('onOpenCustomObjectEditor')} /> @@ -50,7 +52,9 @@ export const WithoutFunction = () => ( onEventsFunctionsAdded={action('functions added')} onFocusProperty={action('onFocusProperty')} onPropertiesUpdated={action('onPropertiesUpdated')} - onEventsBasedObjectChildrenEdited={action('onEventsBasedObjectChildrenEdited')} + onEventsBasedObjectChildrenEdited={action( + 'onEventsBasedObjectChildrenEdited' + )} onOpenCustomObjectEditor={action('onOpenCustomObjectEditor')} /> From 153ab36018a26b558d826a1228f30e0d5332e246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 27 Dec 2025 20:43:40 +0100 Subject: [PATCH 11/30] Fix shared property creation not updating --- .../EventsBasedBehaviorOrObjectPropertiesEditor.js | 5 ++--- .../EventsBasedBehaviorOrObjectEditor/index.js | 3 +++ .../PropertyListEditor/index.js | 13 +++---------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index 8eb056a68870..a05d257b28e5 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -155,9 +155,8 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< const propertyRefs = React.useRef(new Map>()); React.useImperativeHandle(ref, () => ({ forceUpdate, - getPropertyEditorRef: (propertyName: string) => { - return propertyRefs ? propertyRefs.current.get(propertyName) : null; - }, + getPropertyEditorRef: (propertyName: string) => + propertyRefs ? propertyRefs.current.get(propertyName) : null, })); const gdevelopTheme = React.useContext(GDevelopThemeContext); diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js index 60b175831a51..51ec6bf5862e 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js @@ -88,6 +88,9 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< if (propertiesEditor.current) { propertiesEditor.current.forceUpdate(); } + if (scenePropertiesEditor.current) { + scenePropertiesEditor.current.forceUpdate(); + } }, scrollToConfiguration: () => { if (scrollView.current) { diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 430617587e2d..7994313d6999 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -44,16 +44,9 @@ import { ColumnStackLayout } from '../../UI/Layout'; import { useShouldAutofocusInput } from '../../UI/Responsive/ScreenTypeMeasurer'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; -export const getProjectManagerItemId = (identifier: string) => - `project-manager-tab-${identifier}`; - -const configurationItemId = getProjectManagerItemId( - 'events-based-entity-configuration' -); -export const propertiesRootFolderId = getProjectManagerItemId('properties'); -export const sharedPropertiesRootFolderId = getProjectManagerItemId( - 'properties' -); +const configurationItemId = 'events-based-entity-configuration'; +export const propertiesRootFolderId = 'properties'; +export const sharedPropertiesRootFolderId = 'shared-properties'; const propertiesEmptyPlaceholderId = 'properties-placeholder'; const sharedPropertiesEmptyPlaceholderId = 'shared-properties-placeholder'; From 00ecdafb1c4926cbfcc1cdefd811385d5a176d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 27 Dec 2025 21:35:14 +0100 Subject: [PATCH 12/30] Make it works on mobile --- ...sBasedEntityPropertyTreeViewItemContent.js | 2 +- .../PropertyListEditor/index.js | 20 ++++++++- .../EventsFunctionsExtensionEditor/index.js | 41 +++++++++++++++++-- .../src/UI/EditorMosaic/EditorNavigator.js | 8 ++-- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 01195d8ebcb2..0b0e9a413298 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -124,7 +124,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent getDataSet(): ?HTMLDataset { return { - property: this.property.getName(), + propertyName: this.property.getName(), isSharedProperties: this.props.isSharedProperties ? 'true' : 'false', }; } diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 7994313d6999..7307cfa0a6a2 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -365,6 +365,10 @@ export type PropertyListEditorInterface = {| propertyName: string, isSharedProperties: boolean ) => void, + getSelectedProperty: () => {| + propertyName: string, + isSharedProperties: boolean, + |} | null, |}; type Props = {| @@ -493,7 +497,7 @@ const PropertyListEditor = React.forwardRef( : propertiesRootFolderId, ]); } - // Scroll to the new behavior. + // Scroll to the new property. // 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. @@ -769,6 +773,20 @@ const PropertyListEditor = React.forwardRef( }); scrollToItem(propertyItemId); }, + getSelectedProperty: () => { + const selectedItem = selectedItems[0]; + if (!selectedItem) { + return null; + } + const dataset = selectedItem.content.getDataSet(); + if (!dataset || !dataset.propertyName || !dataset.isSharedProperties) { + return null; + } + return { + propertyName: dataset.propertyName, + isSharedProperties: dataset.isSharedProperties === 'true', + }; + }, })); const canMoveSelectionTo = React.useCallback( diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 379b1c1ab4e2..42030c6342cc 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -1747,7 +1747,11 @@ export default class EventsFunctionsExtensionEditor extends React.Component< transitions={{ 'events-sheet': { nextIcon: , - nextLabel: Parameters, + nextLabel: selectedEventsFunction ? ( + Parameters + ) : ( + Property list + ), nextEditor: 'parameters', previousEditor: () => { this._selectEventsFunction(null, null, null); @@ -1756,8 +1760,39 @@ export default class EventsFunctionsExtensionEditor extends React.Component< }, parameters: { nextIcon: , - nextLabel: Validate these parameters, - nextEditor: 'events-sheet', + nextLabel: selectedEventsFunction ? ( + Validate these parameters + ) : null, + nextEditor: selectedEventsFunction ? 'events-sheet' : null, + previousEditor: selectedEventsFunction + ? null + : () => { + if (this.propertyListEditor) { + const selection = this.propertyListEditor.getSelectedProperty(); + if (selection) { + const { + propertyName, + isSharedProperties, + } = selection; + // Scroll to the selected property. + // 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(() => { + const eventsBasedEntityEditor = + this.eventsBasedBehaviorEditor || + this.eventsBasedObjectEditor; + if (eventsBasedEntityEditor) { + eventsBasedEntityEditor.scrollToProperty( + propertyName, + isSharedProperties + ); + } + }, 100); // A few ms is enough for a new render to be done. + } + } + return 'events-sheet'; + }, }, }} onEditorChanged={ diff --git a/newIDE/app/src/UI/EditorMosaic/EditorNavigator.js b/newIDE/app/src/UI/EditorMosaic/EditorNavigator.js index 4177657e6493..166a68a4b4ed 100644 --- a/newIDE/app/src/UI/EditorMosaic/EditorNavigator.js +++ b/newIDE/app/src/UI/EditorMosaic/EditorNavigator.js @@ -27,10 +27,10 @@ type Props = {| }, transitions: { [string]: {| - nextEditor?: string | (() => string), - nextLabel?: React.Node, - nextIcon?: React.Node, - previousEditor?: string | (() => string), + nextEditor?: string | (() => string) | null, + nextLabel?: React.Node | null, + nextIcon?: React.Node | null, + previousEditor?: string | (() => string) | null, |}, }, onEditorChanged: (editorName: string) => void, From 1895ed5357b30280c369363ca1c3d2cc9850de58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 28 Dec 2025 12:04:07 +0100 Subject: [PATCH 13/30] Keep the "Add property" button on mobile --- .../index.js | 82 +++++++++++++++---- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js index 51ec6bf5862e..b4cd4ab4ca85 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/index.js @@ -12,8 +12,14 @@ import { type ExtensionItemConfigurationAttribute } from '../../EventsFunctionsE import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; import Text from '../../UI/Text'; import { ColumnStackLayout } from '../../UI/Layout'; +import { Column, Line } from '../../UI/Grid'; import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView'; import EventsBasedObjectEditor from './EventsBasedObjectEditor'; +import RaisedButton from '../../UI/RaisedButton'; +import AddIcon from '../../UI/CustomSvgIcons/Add'; +import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; +import useForceUpdate from '../../Utils/UseForceUpdate'; +import newNameGenerator from '../../Utils/NewNameGenerator'; type Props = {| project: gdProject, @@ -65,6 +71,8 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< }: Props, ref ) => { + const forceUpdate = useForceUpdate(); + const _onPropertiesUpdated = React.useCallback( () => { if (unsavedChanges) { @@ -83,21 +91,8 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< null ); - React.useImperativeHandle(ref, () => ({ - forceUpdateProperties: () => { - if (propertiesEditor.current) { - propertiesEditor.current.forceUpdate(); - } - if (scenePropertiesEditor.current) { - scenePropertiesEditor.current.forceUpdate(); - } - }, - scrollToConfiguration: () => { - if (scrollView.current) { - scrollView.current.scrollToPosition(0); - } - }, - scrollToProperty: (propertyName: string, isSharedProperties: boolean) => { + const scrollToProperty = React.useCallback( + (propertyName: string, isSharedProperties: boolean) => { if (!scrollView.current) { return; } @@ -115,10 +110,55 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< } } }, + [] + ); + + React.useImperativeHandle(ref, () => ({ + forceUpdateProperties: () => { + if (propertiesEditor.current) { + propertiesEditor.current.forceUpdate(); + } + if (scenePropertiesEditor.current) { + scenePropertiesEditor.current.forceUpdate(); + } + }, + scrollToConfiguration: () => { + if (scrollView.current) { + scrollView.current.scrollToPosition(0); + } + }, + scrollToProperty, })); const eventsBasedEntity = eventsBasedBehavior || eventsBasedObject; + const addProperty = React.useCallback( + () => { + if (!eventsBasedEntity) { + return; + } + const properties = eventsBasedEntity.getPropertyDescriptors(); + const newName = newNameGenerator('Property', name => + properties.has(name) + ); + const property = properties.insertNew(newName, properties.getCount()); + property.setType('Number'); + forceUpdate(); + onPropertiesUpdated && onPropertiesUpdated(); + + // Scroll to the selected property. + // 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(() => { + scrollToProperty(newName, false); + }, 100); // A few ms is enough for a new render to be done. + }, + [eventsBasedEntity, forceUpdate, onPropertiesUpdated, scrollToProperty] + ); + + const { windowSize } = useResponsiveWindowSize(); + return ( @@ -201,6 +241,18 @@ export const EventsBasedBehaviorOrObjectEditor = React.forwardRef< )} + {windowSize === 'small' && ( + + + Add a property} + onClick={addProperty} + icon={} + /> + + + )} ); } From 5cd43191f6a7b49002c2636645296ecdc8c83b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Mon, 29 Dec 2025 13:34:30 +0100 Subject: [PATCH 14/30] Fix copy paste --- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 19 ++----------------- ...sBasedEntityPropertyTreeViewItemContent.js | 10 ++++++---- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index a05d257b28e5..c5274fc385df 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -2,16 +2,11 @@ import { Trans } from '@lingui/macro'; import { t } from '@lingui/macro'; import { I18n } from '@lingui/react'; -import { type I18n as I18nType } from '@lingui/core'; import * as React from 'react'; -import { Column, Line, Spacer } from '../../UI/Grid'; -import { LineStackLayout } from '../../UI/Layout'; +import { Column, Line } from '../../UI/Grid'; import SelectField from '../../UI/SelectField'; import SelectOption from '../../UI/SelectOption'; import { mapFor, mapVector } from '../../Utils/MapFor'; -import RaisedButton from '../../UI/RaisedButton'; -import IconButton from '../../UI/IconButton'; -import ElementWithMenu from '../../UI/Menu/ElementWithMenu'; import SemiControlledTextField from '../../UI/SemiControlledTextField'; import newNameGenerator from '../../Utils/NewNameGenerator'; import { ResponsiveLineStackLayout, ColumnStackLayout } from '../../UI/Layout'; @@ -20,17 +15,14 @@ import ColorField from '../../UI/ColorField'; import BehaviorTypeSelector from '../../BehaviorTypeSelector'; import SemiControlledAutoComplete from '../../UI/SemiControlledAutoComplete'; import { getMeasurementUnitShortLabel } from '../../PropertiesEditor/PropertiesMapToSchema'; -import Add from '../../UI/CustomSvgIcons/Add'; import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; import useForceUpdate from '../../Utils/UseForceUpdate'; import Clipboard from '../../Utils/Clipboard'; import { SafeExtractor } from '../../Utils/SafeExtractor'; import { - serializeToJSObject, unserializeFromJSObject, } from '../../Utils/Serializer'; import PasteIcon from '../../UI/CustomSvgIcons/Clipboard'; -import ResponsiveFlatButton from '../../UI/ResponsiveFlatButton'; import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder'; import useAlertDialog from '../../UI/Alert/useAlertDialog'; import ResourceTypeSelectField from '../../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; @@ -270,13 +262,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< [properties, pasteProperties] ); - const pastePropertiesBefore = React.useCallback( - async (property: gdNamedPropertyDescriptor) => { - await pasteProperties(properties.getPosition(property)); - }, - [properties, pasteProperties] - ); - const setChoices = React.useCallback( (property: gdNamedPropertyDescriptor) => { return (choices: Array) => { @@ -353,7 +338,7 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< properties, (property: gdNamedPropertyDescriptor, i: number) => { return ( - +
{ propertyRefs.current.set(property.getName(), ref); diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 0b0e9a413298..518950d95e80 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -299,10 +299,12 @@ export class EventsBasedEntityPropertyTreeViewItemContent } copy(): void { - Clipboard.set(PROPERTIES_CLIPBOARD_KIND, { - layout: serializeToJSObject(this.property), - name: this.property.getName(), - }); + Clipboard.set(PROPERTIES_CLIPBOARD_KIND, [ + { + name: this.property.getName(), + serializedProperty: serializeToJSObject(this.property), + }, + ]); } cut(): void { From 1ad8cec845e29a3544595ab7e7c9fa825a7178d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Mon, 29 Dec 2025 14:01:40 +0100 Subject: [PATCH 15/30] Factorize paste function --- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 92 +----------- ...sBasedEntityPropertyTreeViewItemContent.js | 132 ++++++++++-------- 2 files changed, 81 insertions(+), 143 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index c5274fc385df..4f50cf2fff33 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -18,15 +18,12 @@ import { getMeasurementUnitShortLabel } from '../../PropertiesEditor/PropertiesM import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; import useForceUpdate from '../../Utils/UseForceUpdate'; import Clipboard from '../../Utils/Clipboard'; -import { SafeExtractor } from '../../Utils/SafeExtractor'; -import { - unserializeFromJSObject, -} from '../../Utils/Serializer'; import PasteIcon from '../../UI/CustomSvgIcons/Clipboard'; import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder'; import useAlertDialog from '../../UI/Alert/useAlertDialog'; import ResourceTypeSelectField from '../../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; +import { pasteProperties } from '../PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent'; const gd: libGDevelop = global.gd; @@ -176,90 +173,15 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< [addPropertyAt, properties] ); - const pasteProperties = React.useCallback( - async propertyInsertionIndex => { - const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); - const propertyContents = SafeExtractor.extractArray(clipboardContent); - if (!propertyContents) return; - - const newNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - const existingNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - propertyContents.forEach(propertyContent => { - const name = SafeExtractor.extractStringProperty( - propertyContent, - 'name' - ); - const serializedProperty = SafeExtractor.extractObjectProperty( - propertyContent, - 'serializedProperty' - ); - if (!name || !serializedProperty) { - return; - } - - if (properties.has(name)) { - existingNamedProperties.push({ name, serializedProperty }); - } else { - newNamedProperties.push({ name, serializedProperty }); - } - }); - - let firstAddedPropertyName: string | null = null; - let index = propertyInsertionIndex; - newNamedProperties.forEach(({ name, serializedProperty }) => { - const property = properties.insertNew(name, index); - index++; - unserializeFromJSObject(property, serializedProperty); - if (!firstAddedPropertyName) { - firstAddedPropertyName = name; - } - }); - - let shouldOverrideProperties = false; - if (existingNamedProperties.length > 0) { - shouldOverrideProperties = await showPropertyOverridingConfirmation( - existingNamedProperties.map(namedProperty => namedProperty.name) - ); - - if (shouldOverrideProperties) { - existingNamedProperties.forEach(({ name, serializedProperty }) => { - if (properties.has(name)) { - const property = properties.get(name); - unserializeFromJSObject(property, serializedProperty); - } - }); - } - } - - forceUpdate(); - if (firstAddedPropertyName) { - //setJustAddedPropertyName(firstAddedPropertyName); - } else if (existingNamedProperties.length === 1) { - //setJustAddedPropertyName(existingNamedProperties[0].name); - } - if (firstAddedPropertyName || shouldOverrideProperties) { - if (onPropertiesUpdated) onPropertiesUpdated(); - } - }, - [ - forceUpdate, - properties, - showPropertyOverridingConfirmation, - onPropertiesUpdated, - ] - ); - const pastePropertiesAtTheEnd = React.useCallback( async () => { - await pasteProperties(properties.getCount()); + await pasteProperties( + properties, + properties.getCount(), + showPropertyOverridingConfirmation + ); }, - [properties, pasteProperties] + [properties, showPropertyOverridingConfirmation] ); const setChoices = React.useCallback( diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 518950d95e80..eea4a2c419cd 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -52,6 +52,73 @@ const getValidatedPropertyName = ( return safeAndUniqueNewName; }; +export const pasteProperties = async ( + properties: gdPropertiesContainer, + insertionIndex: number, + showPropertyOverridingConfirmation: ( + existingPropertyNames: string[] + ) => Promise +): Promise => { + if (!Clipboard.has(PROPERTIES_CLIPBOARD_KIND)) return false; + + const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); + const propertyContents = SafeExtractor.extractArray(clipboardContent); + if (!propertyContents) return false; + + const newNamedProperties: Array<{ + name: string, + serializedProperty: string, + }> = []; + const existingNamedProperties: Array<{ + name: string, + serializedProperty: string, + }> = []; + propertyContents.forEach(propertyContent => { + const name = SafeExtractor.extractStringProperty(propertyContent, 'name'); + const serializedProperty = SafeExtractor.extractObjectProperty( + propertyContent, + 'serializedProperty' + ); + if (!name || !serializedProperty) { + return false; + } + + if (properties.has(name)) { + existingNamedProperties.push({ name, serializedProperty }); + } else { + newNamedProperties.push({ name, serializedProperty }); + } + }); + + let firstAddedPropertyName: string | null = null; + let index = insertionIndex; + newNamedProperties.forEach(({ name, serializedProperty }) => { + const property = properties.insertNew(name, index); + index++; + unserializeFromJSObject(property, serializedProperty); + if (!firstAddedPropertyName) { + firstAddedPropertyName = name; + } + }); + + let shouldOverrideProperties = false; + if (existingNamedProperties.length > 0) { + shouldOverrideProperties = await showPropertyOverridingConfirmation( + existingNamedProperties.map(namedProperty => namedProperty.name) + ); + + if (shouldOverrideProperties) { + existingNamedProperties.forEach(({ name, serializedProperty }) => { + if (properties.has(name)) { + const property = properties.get(name); + unserializeFromJSObject(property, serializedProperty); + } + }); + } + } + return true; +}; + export type EventsBasedEntityPropertyTreeViewItemProps = {| ...TreeItemProps, project: gdProject, @@ -317,65 +384,14 @@ export class EventsBasedEntityPropertyTreeViewItemContent } async pasteAsync(): Promise { - if (!Clipboard.has(PROPERTIES_CLIPBOARD_KIND)) return; - - const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); - const propertyContents = SafeExtractor.extractArray(clipboardContent); - if (!propertyContents) return; - - const newNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - const existingNamedProperties: Array<{ - name: string, - serializedProperty: string, - }> = []; - propertyContents.forEach(propertyContent => { - const name = SafeExtractor.extractStringProperty(propertyContent, 'name'); - const serializedProperty = SafeExtractor.extractObjectProperty( - propertyContent, - 'serializedProperty' - ); - if (!name || !serializedProperty) { - return; - } - - if (this.props.properties.has(name)) { - existingNamedProperties.push({ name, serializedProperty }); - } else { - newNamedProperties.push({ name, serializedProperty }); - } - }); - - let firstAddedPropertyName: string | null = null; - let index = this.getIndex() + 1; - newNamedProperties.forEach(({ name, serializedProperty }) => { - const property = this.props.properties.insertNew(name, index); - index++; - unserializeFromJSObject(property, serializedProperty); - if (!firstAddedPropertyName) { - firstAddedPropertyName = name; - } - }); - - let shouldOverrideProperties = false; - if (existingNamedProperties.length > 0) { - shouldOverrideProperties = await this.props.showPropertyOverridingConfirmation( - existingNamedProperties.map(namedProperty => namedProperty.name) - ); - - if (shouldOverrideProperties) { - existingNamedProperties.forEach(({ name, serializedProperty }) => { - if (this.props.properties.has(name)) { - const property = this.props.properties.get(name); - unserializeFromJSObject(property, serializedProperty); - } - }); - } + const hasPasteAnyProperty = await pasteProperties( + this.props.properties, + this.getIndex() + 1, + this.props.showPropertyOverridingConfirmation + ); + if (hasPasteAnyProperty) { + this._onProjectItemModified(); } - - this._onProjectItemModified(); } _duplicate(): void { From 9e24bf403f018800e5b424009fd109b0b9ad84bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Mon, 29 Dec 2025 17:58:00 +0100 Subject: [PATCH 16/30] Remove unused code. --- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 23 ++++--------------- ...sBasedEntityPropertyTreeViewItemContent.js | 2 +- .../PropertyListEditor/index.js | 2 -- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index 4f50cf2fff33..78a7c8f0229d 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -20,15 +20,16 @@ import useForceUpdate from '../../Utils/UseForceUpdate'; import Clipboard from '../../Utils/Clipboard'; import PasteIcon from '../../UI/CustomSvgIcons/Clipboard'; import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder'; -import useAlertDialog from '../../UI/Alert/useAlertDialog'; import ResourceTypeSelectField from '../../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/ResourceTypeSelectField'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; -import { pasteProperties } from '../PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent'; +import { + pasteProperties, + PROPERTIES_CLIPBOARD_KIND, +} from '../PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent'; +import { usePropertyOverridingAlertDialog } from '../PropertyListEditor'; const gd: libGDevelop = global.gd; -const PROPERTIES_CLIPBOARD_KIND = 'Properties'; - const styles = { rowContainer: { display: 'flex', @@ -43,20 +44,6 @@ const styles = { }, }; -export const usePropertyOverridingAlertDialog = () => { - const { showConfirmation } = useAlertDialog(); - return async (existingPropertyNames: Array): Promise => { - return await showConfirmation({ - title: t`Existing properties`, - message: t`These properties already exist:${'\n\n - ' + - existingPropertyNames.join('\n\n - ') + - '\n\n'}Do you want to replace them?`, - confirmButtonLabel: t`Replace`, - dismissButtonLabel: t`Omit`, - }); - }; -}; - const setExtraInfoString = ( property: gdNamedPropertyDescriptor, value: string diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index eea4a2c419cd..2f51657b6058 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -25,7 +25,7 @@ import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/E const gd: libGDevelop = global.gd; -const PROPERTIES_CLIPBOARD_KIND = 'Properties'; +export const PROPERTIES_CLIPBOARD_KIND = 'Properties'; const styles = { tooltip: { marginRight: 5, verticalAlign: 'bottom' }, diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 7307cfa0a6a2..3f01c6010759 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -39,9 +39,7 @@ import { type ShowConfirmDeleteDialogOptions } from '../../UI/Alert/AlertContext import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; import { type GDevelopTheme } from '../../UI/Theme'; import { type HTMLDataset } from '../../Utils/HTMLDataset'; -import EmptyMessage from '../../UI/EmptyMessage'; import { ColumnStackLayout } from '../../UI/Layout'; -import { useShouldAutofocusInput } from '../../UI/Responsive/ScreenTypeMeasurer'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; const configurationItemId = 'events-based-entity-configuration'; From e104dab01e3f98b8f554f6a2478e1c0c2906604f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Wed, 31 Dec 2025 22:50:36 +0100 Subject: [PATCH 17/30] Add property folders in Core --- .../Project/AbstractEventsBasedEntity.cpp | 7 + Core/GDCore/Project/EventsBasedBehavior.cpp | 13 +- Core/GDCore/Project/ObjectFolderOrObject.h | 6 +- Core/GDCore/Project/PropertiesContainer.cpp | 89 ++++++ Core/GDCore/Project/PropertiesContainer.h | 52 +++- .../Project/PropertyFolderOrProperty.cpp | 276 ++++++++++++++++++ .../GDCore/Project/PropertyFolderOrProperty.h | 224 ++++++++++++++ GDevelop.js/Bindings/Bindings.idl | 35 +++ GDevelop.js/Bindings/Wrapper.cpp | 2 + GDevelop.js/types.d.ts | 31 ++ GDevelop.js/types/gdpropertiescontainer.js | 4 + .../types/gdpropertyfolderorproperty.js | 24 ++ .../types/gdvectorpropertyfolderorproperty.js | 7 + GDevelop.js/types/libgdevelop.js | 2 + 14 files changed, 754 insertions(+), 18 deletions(-) create mode 100644 Core/GDCore/Project/PropertiesContainer.cpp create mode 100644 Core/GDCore/Project/PropertyFolderOrProperty.cpp create mode 100644 Core/GDCore/Project/PropertyFolderOrProperty.h create mode 100644 GDevelop.js/types/gdpropertyfolderorproperty.js create mode 100644 GDevelop.js/types/gdvectorpropertyfolderorproperty.js diff --git a/Core/GDCore/Project/AbstractEventsBasedEntity.cpp b/Core/GDCore/Project/AbstractEventsBasedEntity.cpp index 6fdbc3acd1c6..ff7973c9d7e7 100644 --- a/Core/GDCore/Project/AbstractEventsBasedEntity.cpp +++ b/Core/GDCore/Project/AbstractEventsBasedEntity.cpp @@ -32,6 +32,8 @@ void AbstractEventsBasedEntity::SerializeTo(SerializerElement& element) const { eventsFunctionsContainer.SerializeEventsFunctionsTo(eventsFunctionsElement); propertyDescriptors.SerializeElementsTo( "propertyDescriptor", element.AddChild("propertyDescriptors")); + propertyDescriptors.SerializeFoldersTo( + element.AddChild("propertyFolderStructure")); } void AbstractEventsBasedEntity::UnserializeFrom( @@ -47,6 +49,11 @@ void AbstractEventsBasedEntity::UnserializeFrom( project, eventsFunctionsElement); propertyDescriptors.UnserializeElementsFrom( "propertyDescriptor", element.GetChild("propertyDescriptors")); + if (element.HasChild("propertiesFolderStructure")) { + propertyDescriptors.UnserializeFoldersFrom( + project, element.GetChild("propertiesFolderStructure", 0)); + } + propertyDescriptors.AddMissingPropertiesInRootFolder(); } } // namespace gd diff --git a/Core/GDCore/Project/EventsBasedBehavior.cpp b/Core/GDCore/Project/EventsBasedBehavior.cpp index 3e93da9dbae6..ccd184009bc3 100644 --- a/Core/GDCore/Project/EventsBasedBehavior.cpp +++ b/Core/GDCore/Project/EventsBasedBehavior.cpp @@ -21,8 +21,12 @@ EventsBasedBehavior::EventsBasedBehavior() void EventsBasedBehavior::SerializeTo(SerializerElement& element) const { AbstractEventsBasedEntity::SerializeTo(element); element.SetAttribute("objectType", objectType); - sharedPropertyDescriptors.SerializeElementsTo( - "propertyDescriptor", element.AddChild("sharedPropertyDescriptors")); + if (!sharedPropertyDescriptors.empty()) { + sharedPropertyDescriptors.SerializeElementsTo( + "propertyDescriptor", element.AddChild("sharedPropertyDescriptors")); + sharedPropertyDescriptors.SerializeFoldersTo( + element.AddChild("sharedPropertyFolderStructure")); + } if (quickCustomizationVisibility != QuickCustomization::Visibility::Default) { element.SetStringAttribute( "quickCustomizationVisibility", @@ -38,6 +42,11 @@ void EventsBasedBehavior::UnserializeFrom(gd::Project& project, objectType = element.GetStringAttribute("objectType"); sharedPropertyDescriptors.UnserializeElementsFrom( "propertyDescriptor", element.GetChild("sharedPropertyDescriptors")); + if (element.HasChild("sharedPropertiesFolderStructure")) { + sharedPropertyDescriptors.UnserializeFoldersFrom( + project, element.GetChild("sharedPropertiesFolderStructure", 0)); + } + sharedPropertyDescriptors.AddMissingPropertiesInRootFolder(); if (element.HasChild("quickCustomizationVisibility")) { quickCustomizationVisibility = element.GetStringAttribute("quickCustomizationVisibility") == "visible" diff --git a/Core/GDCore/Project/ObjectFolderOrObject.h b/Core/GDCore/Project/ObjectFolderOrObject.h index 7e4023279f38..f536c688360f 100644 --- a/Core/GDCore/Project/ObjectFolderOrObject.h +++ b/Core/GDCore/Project/ObjectFolderOrObject.h @@ -3,8 +3,8 @@ * Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights * reserved. This project is released under the MIT License. */ -#ifndef GDCORE_OBJECTFOLDEROROBJECT_H -#define GDCORE_OBJECTFOLDEROROBJECT_H +#pragma once + #include #include @@ -210,5 +210,3 @@ class GD_CORE_API ObjectFolderOrObject { }; } // namespace gd - -#endif // GDCORE_OBJECTFOLDEROROBJECT_H diff --git a/Core/GDCore/Project/PropertiesContainer.cpp b/Core/GDCore/Project/PropertiesContainer.cpp new file mode 100644 index 000000000000..07b064f854d8 --- /dev/null +++ b/Core/GDCore/Project/PropertiesContainer.cpp @@ -0,0 +1,89 @@ +/* + * GDevelop Core + * Copyright 2008-2025 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "GDCore/Project/PropertiesContainer.h" +#include "GDCore/Project/NamedPropertyDescriptor.h" + +namespace gd { + +PropertiesContainer::PropertiesContainer( + EventsFunctionsContainer::FunctionOwner owner) + : SerializableWithNameList(), owner(owner) { + rootFolder = gd::make_unique("__ROOT"); +} + +PropertiesContainer::PropertiesContainer(const PropertiesContainer &other) + : SerializableWithNameList(other), + owner(other.owner) { + // The properties folders are not copied. + // It's not an issue because the UI uses the serialization for duplication. + rootFolder = gd::make_unique("__ROOT"); +} + +PropertiesContainer & +PropertiesContainer::operator=(const PropertiesContainer &other) { + if (this != &other) { + SerializableWithNameList::operator=(other); + owner = other.owner; + // The properties folders are not copied. + // It's not an issue because the UI uses the serialization for duplication. + rootFolder = gd::make_unique("__ROOT"); + } + return *this; +} + +gd::NamedPropertyDescriptor &PropertiesContainer::InsertNewPropertyInFolder( + const gd::String &name, + gd::PropertyFolderOrProperty &propertyFolderOrProperty, + std::size_t position) { + gd::NamedPropertyDescriptor &newlyCreatedProperty = + InsertNew(name, GetCount()); + propertyFolderOrProperty.InsertProperty(&newlyCreatedProperty, position); + return newlyCreatedProperty; +} + +std::vector +PropertiesContainer::GetAllPropertyFolderOrProperty() const { + std::vector results; + + std::function + addChildrenOfFolder = [&](const PropertyFolderOrProperty &folder) { + for (size_t i = 0; i < folder.GetChildrenCount(); ++i) { + const auto &child = folder.GetChildAt(i); + results.push_back(&child); + + if (child.IsFolder()) { + addChildrenOfFolder(child); + } + } + }; + + addChildrenOfFolder(*rootFolder); + + return results; +} + +void PropertiesContainer::AddMissingPropertiesInRootFolder() { + for (std::size_t i = 0; i < GetCount(); ++i) { + auto &property = Get(i); + if (!rootFolder->HasPropertyNamed(property.GetName())) { + const gd::String &group = property.GetGroup(); + auto &folder = !group.empty() ? rootFolder->GetOrCreateChildFolder(group) + : *rootFolder; + folder.InsertProperty(&property); + } + } +} + +void PropertiesContainer::SerializeFoldersTo(SerializerElement &element) const { + rootFolder->SerializeTo(element); +} + +void PropertiesContainer::UnserializeFoldersFrom( + gd::Project &project, const SerializerElement &element) { + rootFolder->UnserializeFrom(project, element, *this); +} + +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/PropertiesContainer.h b/Core/GDCore/Project/PropertiesContainer.h index 8876255f34e0..abc717489a3e 100644 --- a/Core/GDCore/Project/PropertiesContainer.h +++ b/Core/GDCore/Project/PropertiesContainer.h @@ -2,6 +2,7 @@ #include "EventsFunctionsContainer.h" #include "GDCore/Tools/SerializableWithNameList.h" #include "NamedPropertyDescriptor.h" +#include "GDCore/Project/PropertyFolderOrProperty.h" namespace gd { @@ -16,20 +17,11 @@ namespace gd { class PropertiesContainer : public SerializableWithNameList { public: - PropertiesContainer(EventsFunctionsContainer::FunctionOwner owner) - : SerializableWithNameList(), owner(owner) {} + PropertiesContainer(EventsFunctionsContainer::FunctionOwner owner); - PropertiesContainer(const PropertiesContainer& other) - : SerializableWithNameList(other), - owner(other.owner) {} + PropertiesContainer(const PropertiesContainer& other); - PropertiesContainer& operator=(const PropertiesContainer& other) { - if (this != &other) { - SerializableWithNameList::operator=(other); - owner = other.owner; - } - return *this; - } + PropertiesContainer& operator=(const PropertiesContainer& other); void ForEachPropertyMatchingSearch( const gd::String& search, @@ -43,8 +35,44 @@ class PropertiesContainer EventsFunctionsContainer::FunctionOwner GetOwner() const { return owner; } + /** + * \brief Add a new empty property called \a name in the + * given folder at the specified position.
+ * + * \return A reference to the property in the list. + */ + gd::NamedPropertyDescriptor& InsertNewPropertyInFolder( + const gd::String& name, + gd::PropertyFolderOrProperty& propertyFolderOrProperty, + std::size_t position); + + /** + * Returns a vector containing all object and folders in this container. + * Only use this for checking if you hold a valid `PropertyFolderOrProperty` - + * don't use this for rendering or anything else. + */ + std::vector GetAllPropertyFolderOrProperty() const; + + gd::PropertyFolderOrProperty& GetRootFolder() { + return *rootFolder; + } + + void AddMissingPropertiesInRootFolder(); + + /** + * \brief Serialize folder structure. + */ + void SerializeFoldersTo(SerializerElement& element) const; + + /** + * \brief Unserialize folder structure. + */ + void UnserializeFoldersFrom(gd::Project& project, + const SerializerElement& element); + private: EventsFunctionsContainer::FunctionOwner owner; + std::unique_ptr rootFolder; }; } // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.cpp b/Core/GDCore/Project/PropertyFolderOrProperty.cpp new file mode 100644 index 000000000000..aec0458bd33e --- /dev/null +++ b/Core/GDCore/Project/PropertyFolderOrProperty.cpp @@ -0,0 +1,276 @@ +/* + * GDevelop Core + * Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "GDCore/Project/PropertyFolderOrProperty.h" + +#include + +#include "GDCore/Project/NamedPropertyDescriptor.h" +#include "GDCore/Project/PropertiesContainer.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/Tools/Log.h" + +using namespace std; + +namespace gd { + +PropertyFolderOrProperty PropertyFolderOrProperty::badPropertyFolderOrProperty; + +PropertyFolderOrProperty::PropertyFolderOrProperty() + : folderName("__NULL"), + property(nullptr) {} +PropertyFolderOrProperty::PropertyFolderOrProperty(gd::String folderName_, + PropertyFolderOrProperty* parent_) + : folderName(folderName_), + parent(parent_), + property(nullptr) {} +PropertyFolderOrProperty::PropertyFolderOrProperty(gd::NamedPropertyDescriptor* property_, + PropertyFolderOrProperty* parent_) + : property(property_), + parent(parent_) {} +PropertyFolderOrProperty::~PropertyFolderOrProperty() {} + +bool PropertyFolderOrProperty::HasPropertyNamed(const gd::String& name) { + if (IsFolder()) { + return std::any_of( + children.begin(), + children.end(), + [&name]( + std::unique_ptr& propertyFolderOrProperty) { + return propertyFolderOrProperty->HasPropertyNamed(name); + }); + } + if (!property) return false; + return property->GetName() == name; +} + +PropertyFolderOrProperty& PropertyFolderOrProperty::GetOrCreateChildFolder(const gd::String& name) { + if (!IsFolder()) { + LogError("Try to create of a folder '" + name + "' inside a property"); + return gd::PropertyFolderOrProperty::badPropertyFolderOrProperty; + } + for (auto &&child : children) { + if (child->IsFolder() && child->folderName == name) { + return *child; + } + } + return InsertNewFolder(name, GetChildrenCount()); +} + +PropertyFolderOrProperty& PropertyFolderOrProperty::GetPropertyNamed( + const gd::String& name) { + if (property && property->GetName() == name) { + return *this; + } + if (IsFolder()) { + for (std::size_t j = 0; j < children.size(); j++) { + PropertyFolderOrProperty& foundInChild = children[j]->GetPropertyNamed(name); + if (&(foundInChild) != &badPropertyFolderOrProperty) { + return foundInChild; + } + } + } + return badPropertyFolderOrProperty; +} + +void PropertyFolderOrProperty::SetFolderName(const gd::String& name) { + if (!IsFolder()) return; + folderName = name; +} + +PropertyFolderOrProperty& PropertyFolderOrProperty::GetChildAt(std::size_t index) { + if (index >= children.size()) return badPropertyFolderOrProperty; + return *children[index]; +} +const PropertyFolderOrProperty& PropertyFolderOrProperty::GetChildAt( + std::size_t index) const { + if (index >= children.size()) return badPropertyFolderOrProperty; + return *children[index]; +} +PropertyFolderOrProperty& PropertyFolderOrProperty::GetPropertyChild( + const gd::String& name) { + for (std::size_t j = 0; j < children.size(); j++) { + if (!children[j]->IsFolder()) { + if (children[j]->GetProperty().GetName() == name) return *children[j]; + }; + } + return badPropertyFolderOrProperty; +} + +void PropertyFolderOrProperty::InsertProperty(gd::NamedPropertyDescriptor* insertedProperty, + std::size_t position) { + auto propertyFolderOrProperty = + gd::make_unique(insertedProperty, this); + if (position < children.size()) { + children.insert(children.begin() + position, + std::move(propertyFolderOrProperty)); + } else { + children.push_back(std::move(propertyFolderOrProperty)); + } +} + +std::size_t PropertyFolderOrProperty::GetChildPosition( + const PropertyFolderOrProperty& child) const { + for (std::size_t j = 0; j < children.size(); j++) { + if (children[j].get() == &child) return j; + } + return gd::String::npos; +} + +PropertyFolderOrProperty& PropertyFolderOrProperty::InsertNewFolder( + const gd::String& newFolderName, std::size_t position) { + auto newFolderPtr = + gd::make_unique(newFolderName, this); + gd::PropertyFolderOrProperty& newFolder = *(*(children.insert( + position < children.size() ? children.begin() + position : children.end(), + std::move(newFolderPtr)))); + return newFolder; +}; + +void PropertyFolderOrProperty::RemoveRecursivelyPropertyNamed( + const gd::String& name) { + if (IsFolder()) { + children.erase( + std::remove_if(children.begin(), + children.end(), + [&name](std::unique_ptr& + propertyFolderOrProperty) { + return !propertyFolderOrProperty->IsFolder() && + propertyFolderOrProperty->GetProperty().GetName() == + name; + }), + children.end()); + for (auto& it : children) { + it->RemoveRecursivelyPropertyNamed(name); + } + } +}; + +void PropertyFolderOrProperty::Clear() { + if (IsFolder()) { + for (auto& it : children) { + it->Clear(); + } + children.clear(); + } +}; + +bool PropertyFolderOrProperty::IsADescendantOf( + const PropertyFolderOrProperty& otherPropertyFolderOrProperty) { + if (parent == nullptr) return false; + if (&(*parent) == &otherPropertyFolderOrProperty) return true; + return parent->IsADescendantOf(otherPropertyFolderOrProperty); +} + +void PropertyFolderOrProperty::MoveChild(std::size_t oldIndex, + std::size_t newIndex) { + if (!IsFolder()) return; + if (oldIndex >= children.size() || newIndex >= children.size()) return; + + std::unique_ptr propertyFolderOrProperty = + std::move(children[oldIndex]); + children.erase(children.begin() + oldIndex); + children.insert(children.begin() + newIndex, std::move(propertyFolderOrProperty)); +} + +void PropertyFolderOrProperty::RemoveFolderChild( + const PropertyFolderOrProperty& childToRemove) { + if (!IsFolder() || !childToRemove.IsFolder() || + childToRemove.GetChildrenCount() > 0) { + return; + } + std::vector>::iterator it = find_if( + children.begin(), + children.end(), + [&childToRemove](std::unique_ptr& child) { + return child.get() == &childToRemove; + }); + if (it == children.end()) return; + + children.erase(it); +} + +void PropertyFolderOrProperty::MovePropertyFolderOrPropertyToAnotherFolder( + gd::PropertyFolderOrProperty& propertyFolderOrProperty, + gd::PropertyFolderOrProperty& newParentFolder, + std::size_t newPosition) { + if (!newParentFolder.IsFolder()) return; + if (newParentFolder.IsADescendantOf(propertyFolderOrProperty)) return; + + std::vector>::iterator it = + find_if(children.begin(), + children.end(), + [&propertyFolderOrProperty](std::unique_ptr& + childPropertyFolderOrProperty) { + return childPropertyFolderOrProperty.get() == &propertyFolderOrProperty; + }); + if (it == children.end()) return; + + std::unique_ptr propertyFolderOrPropertyPtr = + std::move(*it); + children.erase(it); + + propertyFolderOrPropertyPtr->parent = &newParentFolder; + newParentFolder.children.insert( + newPosition < newParentFolder.children.size() + ? newParentFolder.children.begin() + newPosition + : newParentFolder.children.end(), + std::move(propertyFolderOrPropertyPtr)); +} + +void PropertyFolderOrProperty::SerializeTo(SerializerElement& element) const { + if (IsFolder()) { + element.SetAttribute("folderName", GetFolderName()); + if (children.size() > 0) { + SerializerElement& childrenElement = element.AddChild("children"); + childrenElement.ConsiderAsArrayOf("propertyFolderOrProperty"); + for (std::size_t j = 0; j < children.size(); j++) { + children[j]->SerializeTo( + childrenElement.AddChild("propertyFolderOrProperty")); + } + } + } else { + element.SetAttribute("propertyName", GetProperty().GetName()); + } +} + +void PropertyFolderOrProperty::UnserializeFrom( + gd::Project& project, + const SerializerElement& element, + gd::PropertiesContainer& propertiesContainer) { + children.clear(); + gd::String potentialFolderName = element.GetStringAttribute("folderName", ""); + + if (!potentialFolderName.empty()) { + property = nullptr; + folderName = potentialFolderName; + + if (element.HasChild("children")) { + const SerializerElement& childrenElements = + element.GetChild("children", 0); + childrenElements.ConsiderAsArrayOf("propertyFolderOrProperty"); + for (std::size_t i = 0; i < childrenElements.GetChildrenCount(); ++i) { + std::unique_ptr childPropertyFolderOrProperty = + make_unique(); + childPropertyFolderOrProperty->UnserializeFrom( + project, childrenElements.GetChild(i), propertiesContainer); + childPropertyFolderOrProperty->parent = this; + children.push_back(std::move(childPropertyFolderOrProperty)); + } + } + } else { + folderName = ""; + gd::String propertyName = element.GetStringAttribute("propertyName"); + if (propertiesContainer.Has(propertyName)) { + property = &propertiesContainer.Get(propertyName); + } else { + gd::LogError("Property with name " + propertyName + + " not found in properties container."); + property = nullptr; + } + } +}; + +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.h b/Core/GDCore/Project/PropertyFolderOrProperty.h new file mode 100644 index 000000000000..e0e41bd3c63d --- /dev/null +++ b/Core/GDCore/Project/PropertyFolderOrProperty.h @@ -0,0 +1,224 @@ +/* + * GDevelop Core + * Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once + +#include +#include + +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/String.h" +#include "GDCore/Project/QuickCustomization.h" + +namespace gd { +class Project; +class NamedPropertyDescriptor; +class SerializerElement; +class PropertiesContainer; +} // namespace gd + +namespace gd { + +/** + * \brief Class representing a folder structure in order to organize properties + * in folders (to be used with a PropertiesContainer.) + * + * \see gd::PropertiesContainer + */ +class GD_CORE_API PropertyFolderOrProperty { + public: + /** + * \brief Default constructor creating an empty instance. Useful for the null + * property pattern. + */ + PropertyFolderOrProperty(); + + virtual ~PropertyFolderOrProperty(); + + /** + * \brief Constructor for creating an instance representing a folder. + */ + PropertyFolderOrProperty(gd::String folderName_, + PropertyFolderOrProperty* parent_ = nullptr); + + /** + * \brief Constructor for creating an instance representing a property. + */ + PropertyFolderOrProperty(gd::NamedPropertyDescriptor* property_, + PropertyFolderOrProperty* parent_ = nullptr); + + /** + * \brief Returns the property behind the instance. + */ + gd::NamedPropertyDescriptor& GetProperty() const { return *property; } + + /** + * \brief Returns true if the instance represents a folder. + */ + bool IsFolder() const { return !folderName.empty(); } + + /** + * \brief Returns the name of the folder. + */ + const gd::String& GetFolderName() const { return folderName; } + + /** + * \brief Set the folder name. Does nothing if called on an instance not + * representing a folder. + */ + void SetFolderName(const gd::String& name); + + /** + * \brief Returns true if the instance represents the property with the given + * name or if any of the children does (recursive search). + */ + bool HasPropertyNamed(const gd::String& name); + + /** + * \brief Returns the child instance holding the property with the given name + * (recursive search). + */ + PropertyFolderOrProperty& GetPropertyNamed(const gd::String& name); + + /** + * \brief Returns the number of children. Returns 0 if the instance represents + * a property. + */ + std::size_t GetChildrenCount() const { + if (IsFolder()) return children.size(); + return 0; + } + + /** + * \brief Returns the child PropertyFolderOrProperty at the given index. + */ + PropertyFolderOrProperty& GetChildAt(std::size_t index); + + /** + * \brief Returns the child PropertyFolderOrProperty at the given index. + */ + const PropertyFolderOrProperty& GetChildAt(std::size_t index) const; + + /** + * \brief Returns the child PropertyFolderOrProperty that represents the property + * with the given name. To use only if sure that the instance holds the property + * in its direct children (no recursive search). + */ + PropertyFolderOrProperty& GetPropertyChild(const gd::String& name); + + /** + * \brief Returns the first direct child that represents a folder + * with the given name or create one. + */ + PropertyFolderOrProperty& GetOrCreateChildFolder(const gd::String& name); + + /** + * \brief Returns the parent of the instance. If the instance has no parent + * (root folder), the null property is returned. + */ + PropertyFolderOrProperty& GetParent() { + if (parent == nullptr) { + return badPropertyFolderOrProperty; + } + return *parent; + }; + + /** + * \brief Returns true if the instance is a root folder (that's to say it + * has no parent). + */ + bool IsRootFolder() { return !property && !parent; } + + /** + * \brief Moves a child from a position to a new one. + */ + void MoveChild(std::size_t oldIndex, std::size_t newIndex); + + /** + * \brief Removes the given child from the instance's children. If the given + * child contains children of its own, does nothing. + */ + void RemoveFolderChild(const PropertyFolderOrProperty& childToRemove); + + /** + * \brief Removes the child representing the property with the given name from + * the instance children and recursively does it for every folder children. + */ + void RemoveRecursivelyPropertyNamed(const gd::String& name); + + /** + * \brief Clears all children + */ + void Clear(); + + /** + * \brief Inserts an instance representing the given property at the given + * position. + */ + void InsertProperty(gd::NamedPropertyDescriptor* insertedProperty, + std::size_t position = (size_t)-1); + + /** + * \brief Inserts an instance representing a folder with the given name at the + * given position. + */ + PropertyFolderOrProperty& InsertNewFolder(const gd::String& newFolderName, + std::size_t position); + + /** + * \brief Returns true if the instance is a descendant of the given instance + * of PropertyFolderOrProperty. + */ + bool IsADescendantOf(const PropertyFolderOrProperty& otherPropertyFolderOrProperty); + + /** + * \brief Returns the position of the given instance of PropertyFolderOrProperty + * in the instance's children. + */ + std::size_t GetChildPosition(const PropertyFolderOrProperty& child) const; + + /** + * \brief Moves the given child PropertyFolderOrProperty to the given folder at + * the given position. + */ + void MovePropertyFolderOrPropertyToAnotherFolder( + gd::PropertyFolderOrProperty& propertyFolderOrProperty, + gd::PropertyFolderOrProperty& newParentFolder, + std::size_t newPosition); + + /** \name Saving and loading + * Members functions related to saving and loading the properties of the class. + */ + ///@{ + /** + * \brief Serialize the PropertyFolderOrProperty instance. + */ + void SerializeTo(SerializerElement& element) const; + + /** + * \brief Unserialize the PropertyFolderOrProperty instance. + */ + void UnserializeFrom(gd::Project& project, + const SerializerElement& element, + PropertiesContainer& propertiesContainer); + ///@} + + private: + static gd::PropertyFolderOrProperty badPropertyFolderOrProperty; + + gd::PropertyFolderOrProperty* + parent = nullptr; // nullptr if root folder, points to the parent folder otherwise. + + // Representing a property: + gd::NamedPropertyDescriptor* property; // nullptr if folderName is set. + + // or representing a folder: + gd::String folderName; // Empty if property is set. + + std::vector> + children; // Folder children. +}; + +} // namespace gd diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 1dda800ed39e..0b4fec069c1d 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -59,6 +59,11 @@ interface VectorObjectFolderOrObject { [Const] ObjectFolderOrObject at(unsigned long index); }; +interface VectorPropertyFolderOrProperty { + unsigned long size(); + [Const] PropertyFolderOrProperty at(unsigned long index); +}; + interface VectorScreenshot { unsigned long size(); [Const, Ref] Screenshot at(unsigned long index); @@ -3373,6 +3378,28 @@ interface EventsBasedObjectsList { [Ref] EventsBasedObject at(unsigned long index); }; +interface PropertyFolderOrProperty { + void PropertyFolderOrProperty(); + boolean IsFolder(); + boolean IsRootFolder(); + [Ref] NamedPropertyDescriptor GetProperty(); + [Const, Ref] DOMString GetFolderName(); + void SetFolderName([Const] DOMString name); + boolean HasPropertyNamed([Const] DOMString name); + [Ref] PropertyFolderOrProperty GetPropertyNamed([Const] DOMString name); + unsigned long GetChildrenCount(); + [Ref] PropertyFolderOrProperty GetChildAt(unsigned long pos); + [Ref] PropertyFolderOrProperty GetPropertyChild([Const] DOMString name); + [Ref] PropertyFolderOrProperty GetOrCreateChildFolder([Const] DOMString name); + unsigned long GetChildPosition([Const, Ref] PropertyFolderOrProperty child); + [Ref] PropertyFolderOrProperty GetParent(); + [Ref] PropertyFolderOrProperty InsertNewFolder([Const] DOMString name, unsigned long newPosition); + void MovePropertyFolderOrPropertyToAnotherFolder([Ref] PropertyFolderOrProperty propertyFolderOrProperty, [Ref] PropertyFolderOrProperty newParentFolder, unsigned long newPosition); + void MoveChild(unsigned long oldIndex, unsigned long newIndex); + void RemoveFolderChild([Const, Ref] PropertyFolderOrProperty childToRemove); + boolean IsADescendantOf([Const, Ref] PropertyFolderOrProperty otherPropertyFolderOrProperty); +}; + interface PropertiesContainer { void PropertiesContainer(EventsFunctionsContainer_FunctionOwner owner); @@ -3388,6 +3415,14 @@ interface PropertiesContainer { unsigned long size(); [Ref] NamedPropertyDescriptor at(unsigned long index); + + [Ref] NamedPropertyDescriptor InsertNewPropertyInFolder( + [Const] DOMString name, + [Ref] PropertyFolderOrProperty folder, + unsigned long pos); + [Ref] PropertyFolderOrProperty GetRootFolder(); + [Value] VectorPropertyFolderOrProperty GetAllPropertyFolderOrProperty(); + void AddMissingPropertiesInRootFolder(); }; interface EventsFunctionsExtension { diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp index 63176d39e2b7..c5f29ed42c10 100644 --- a/GDevelop.js/Bindings/Wrapper.cpp +++ b/GDevelop.js/Bindings/Wrapper.cpp @@ -91,6 +91,7 @@ #include #include #include +#include #include #include #include @@ -471,6 +472,7 @@ typedef std::shared_ptr SharedPtrSerializerElement; typedef std::vector VectorUnfilledRequiredBehaviorPropertyProblem; typedef std::vector VectorObjectFolderOrObject; +typedef std::vector VectorPropertyFolderOrProperty; typedef std::vector VectorScreenshot; typedef QuickCustomization::Visibility QuickCustomization_Visibility; diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index dcb868473aa6..6ef8df9da784 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -155,6 +155,11 @@ export class VectorObjectFolderOrObject extends EmscriptenObject { at(index: number): ObjectFolderOrObject; } +export class VectorPropertyFolderOrProperty extends EmscriptenObject { + size(): number; + at(index: number): PropertyFolderOrProperty; +} + export class VectorScreenshot extends EmscriptenObject { size(): number; at(index: number): Screenshot; @@ -2450,6 +2455,28 @@ export class EventsBasedObjectsList extends EmscriptenObject { at(index: number): EventsBasedObject; } +export class PropertyFolderOrProperty extends EmscriptenObject { + constructor(); + isFolder(): boolean; + isRootFolder(): boolean; + getProperty(): NamedPropertyDescriptor; + getFolderName(): string; + setFolderName(name: string): void; + hasPropertyNamed(name: string): boolean; + getPropertyNamed(name: string): PropertyFolderOrProperty; + getChildrenCount(): number; + getChildAt(pos: number): PropertyFolderOrProperty; + getPropertyChild(name: string): PropertyFolderOrProperty; + getOrCreateChildFolder(name: string): PropertyFolderOrProperty; + getChildPosition(child: PropertyFolderOrProperty): number; + getParent(): PropertyFolderOrProperty; + insertNewFolder(name: string, newPosition: number): PropertyFolderOrProperty; + movePropertyFolderOrPropertyToAnotherFolder(propertyFolderOrProperty: PropertyFolderOrProperty, newParentFolder: PropertyFolderOrProperty, newPosition: number): void; + moveChild(oldIndex: number, newIndex: number): void; + removeFolderChild(childToRemove: PropertyFolderOrProperty): void; + isADescendantOf(otherPropertyFolderOrProperty: PropertyFolderOrProperty): boolean; +} + export class PropertiesContainer extends EmscriptenObject { constructor(owner: EventsFunctionsContainer_FunctionOwner); insertNew(name: string, pos: number): NamedPropertyDescriptor; @@ -2463,6 +2490,10 @@ export class PropertiesContainer extends EmscriptenObject { getPosition(item: NamedPropertyDescriptor): number; size(): number; at(index: number): NamedPropertyDescriptor; + insertNewPropertyInFolder(name: string, folder: PropertyFolderOrProperty, pos: number): NamedPropertyDescriptor; + getRootFolder(): PropertyFolderOrProperty; + getAllPropertyFolderOrProperty(): VectorPropertyFolderOrProperty; + addMissingPropertiesInRootFolder(): void; } export class EventsFunctionsExtension extends EmscriptenObject { diff --git a/GDevelop.js/types/gdpropertiescontainer.js b/GDevelop.js/types/gdpropertiescontainer.js index 5b2a0e0239fc..fb63510fba29 100644 --- a/GDevelop.js/types/gdpropertiescontainer.js +++ b/GDevelop.js/types/gdpropertiescontainer.js @@ -12,6 +12,10 @@ declare class gdPropertiesContainer { getPosition(item: gdNamedPropertyDescriptor): number; size(): number; at(index: number): gdNamedPropertyDescriptor; + insertNewPropertyInFolder(name: string, folder: gdPropertyFolderOrProperty, pos: number): gdNamedPropertyDescriptor; + getRootFolder(): gdPropertyFolderOrProperty; + getAllPropertyFolderOrProperty(): gdVectorPropertyFolderOrProperty; + addMissingPropertiesInRootFolder(): void; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/gdpropertyfolderorproperty.js b/GDevelop.js/types/gdpropertyfolderorproperty.js new file mode 100644 index 000000000000..24a754813ddb --- /dev/null +++ b/GDevelop.js/types/gdpropertyfolderorproperty.js @@ -0,0 +1,24 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdPropertyFolderOrProperty { + constructor(): void; + isFolder(): boolean; + isRootFolder(): boolean; + getProperty(): gdNamedPropertyDescriptor; + getFolderName(): string; + setFolderName(name: string): void; + hasPropertyNamed(name: string): boolean; + getPropertyNamed(name: string): gdPropertyFolderOrProperty; + getChildrenCount(): number; + getChildAt(pos: number): gdPropertyFolderOrProperty; + getPropertyChild(name: string): gdPropertyFolderOrProperty; + getOrCreateChildFolder(name: string): gdPropertyFolderOrProperty; + getChildPosition(child: gdPropertyFolderOrProperty): number; + getParent(): gdPropertyFolderOrProperty; + insertNewFolder(name: string, newPosition: number): gdPropertyFolderOrProperty; + movePropertyFolderOrPropertyToAnotherFolder(propertyFolderOrProperty: gdPropertyFolderOrProperty, newParentFolder: gdPropertyFolderOrProperty, newPosition: number): void; + moveChild(oldIndex: number, newIndex: number): void; + removeFolderChild(childToRemove: gdPropertyFolderOrProperty): void; + isADescendantOf(otherPropertyFolderOrProperty: gdPropertyFolderOrProperty): boolean; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdvectorpropertyfolderorproperty.js b/GDevelop.js/types/gdvectorpropertyfolderorproperty.js new file mode 100644 index 000000000000..78aa994990fc --- /dev/null +++ b/GDevelop.js/types/gdvectorpropertyfolderorproperty.js @@ -0,0 +1,7 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdVectorPropertyFolderOrProperty { + size(): number; + at(index: number): gdPropertyFolderOrProperty; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/libgdevelop.js b/GDevelop.js/types/libgdevelop.js index eab9afa3582f..f21211acb534 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -52,6 +52,7 @@ declare class libGDevelop { VectorInt: Class; VectorVariable: Class; VectorObjectFolderOrObject: Class; + VectorPropertyFolderOrProperty: Class; VectorScreenshot: Class; MapStringString: Class; MapStringBoolean: Class; @@ -241,6 +242,7 @@ declare class libGDevelop { EventsBasedObjectVariant: Class; EventsBasedObjectVariantsContainer: Class; EventsBasedObjectsList: Class; + PropertyFolderOrProperty: Class; PropertiesContainer: Class; EventsFunctionsExtension: Class; AbstractFileSystem: Class; From 69df4779728f18090a7bdb561511ac1513dde24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 2 Jan 2026 16:04:14 +0100 Subject: [PATCH 18/30] Update the tree when inserting and removing properties --- Core/GDCore/Project/PropertiesContainer.cpp | 88 +++++++- Core/GDCore/Project/PropertiesContainer.h | 65 ++++-- .../Project/PropertyFolderOrProperty.cpp | 211 ++++++++++-------- .../GDCore/Project/PropertyFolderOrProperty.h | 2 + 4 files changed, 246 insertions(+), 120 deletions(-) diff --git a/Core/GDCore/Project/PropertiesContainer.cpp b/Core/GDCore/Project/PropertiesContainer.cpp index 07b064f854d8..eee4b0756dc3 100644 --- a/Core/GDCore/Project/PropertiesContainer.cpp +++ b/Core/GDCore/Project/PropertiesContainer.cpp @@ -10,13 +10,12 @@ namespace gd { PropertiesContainer::PropertiesContainer( EventsFunctionsContainer::FunctionOwner owner) - : SerializableWithNameList(), owner(owner) { + : properties(), owner(owner) { rootFolder = gd::make_unique("__ROOT"); } PropertiesContainer::PropertiesContainer(const PropertiesContainer &other) - : SerializableWithNameList(other), - owner(other.owner) { + : properties(other.properties), owner(other.owner) { // The properties folders are not copied. // It's not an issue because the UI uses the serialization for duplication. rootFolder = gd::make_unique("__ROOT"); @@ -25,7 +24,7 @@ PropertiesContainer::PropertiesContainer(const PropertiesContainer &other) PropertiesContainer & PropertiesContainer::operator=(const PropertiesContainer &other) { if (this != &other) { - SerializableWithNameList::operator=(other); + properties = other.properties; owner = other.owner; // The properties folders are not copied. // It's not an issue because the UI uses the serialization for duplication. @@ -34,12 +33,77 @@ PropertiesContainer::operator=(const PropertiesContainer &other) { return *this; } +NamedPropertyDescriptor & +PropertiesContainer::Insert(const NamedPropertyDescriptor &property, + size_t position) { + auto &newProperty = properties.Insert(property, position); + rootFolder->InsertProperty(&newProperty); + return newProperty; +} + +NamedPropertyDescriptor &PropertiesContainer::InsertNew(const gd::String &name, + size_t position) { + + auto &newlyCreatedProperty = properties.InsertNew(name, position); + rootFolder->InsertProperty(&newlyCreatedProperty); + return newlyCreatedProperty; +} + +bool PropertiesContainer::Has(const gd::String &name) const { + return properties.Has(name); +} + +NamedPropertyDescriptor &PropertiesContainer::Get(const gd::String &name) { + return properties.Get(name); +} + +const NamedPropertyDescriptor & +PropertiesContainer::Get(const gd::String &name) const { + return properties.Get(name); +} + +NamedPropertyDescriptor &PropertiesContainer::Get(size_t index) { + return properties.Get(index); +} + +const NamedPropertyDescriptor &PropertiesContainer::Get(size_t index) const { + return properties.Get(index); +} + +void PropertiesContainer::Remove(const gd::String &name) { + properties.Remove(name); + rootFolder->RemoveRecursivelyPropertyNamed(name); +} + +void PropertiesContainer::Move(std::size_t oldIndex, std::size_t newIndex) { + properties.Move(oldIndex, newIndex); +} + +bool PropertiesContainer::IsEmpty() const { return properties.IsEmpty(); }; + +size_t PropertiesContainer::GetCount() const { return properties.GetCount(); } + +std::size_t +PropertiesContainer::GetPosition(const NamedPropertyDescriptor &element) const { + return properties.GetPosition(element); +} + +const std::vector> & +PropertiesContainer::GetInternalVector() const { + return properties.GetInternalVector(); +}; + +std::vector> & +PropertiesContainer::GetInternalVector() { + return properties.GetInternalVector(); +}; + gd::NamedPropertyDescriptor &PropertiesContainer::InsertNewPropertyInFolder( const gd::String &name, gd::PropertyFolderOrProperty &propertyFolderOrProperty, std::size_t position) { gd::NamedPropertyDescriptor &newlyCreatedProperty = - InsertNew(name, GetCount()); + properties.InsertNew(name, properties.GetCount()); propertyFolderOrProperty.InsertProperty(&newlyCreatedProperty, position); return newlyCreatedProperty; } @@ -66,8 +130,8 @@ PropertiesContainer::GetAllPropertyFolderOrProperty() const { } void PropertiesContainer::AddMissingPropertiesInRootFolder() { - for (std::size_t i = 0; i < GetCount(); ++i) { - auto &property = Get(i); + for (std::size_t i = 0; i < properties.GetCount(); ++i) { + auto &property = properties.Get(i); if (!rootFolder->HasPropertyNamed(property.GetName())) { const gd::String &group = property.GetGroup(); auto &folder = !group.empty() ? rootFolder->GetOrCreateChildFolder(group) @@ -77,6 +141,16 @@ void PropertiesContainer::AddMissingPropertiesInRootFolder() { } } +void PropertiesContainer::SerializeElementsTo( + const gd::String &elementName, SerializerElement &element) const { + properties.SerializeElementsTo(elementName, element); +} + +void PropertiesContainer::UnserializeElementsFrom( + const gd::String &elementName, const SerializerElement &element) { + properties.UnserializeElementsFrom(elementName, element); +} + void PropertiesContainer::SerializeFoldersTo(SerializerElement &element) const { rootFolder->SerializeTo(element); } diff --git a/Core/GDCore/Project/PropertiesContainer.h b/Core/GDCore/Project/PropertiesContainer.h index abc717489a3e..d917a3f21880 100644 --- a/Core/GDCore/Project/PropertiesContainer.h +++ b/Core/GDCore/Project/PropertiesContainer.h @@ -1,8 +1,8 @@ #pragma once #include "EventsFunctionsContainer.h" +#include "GDCore/Project/PropertyFolderOrProperty.h" #include "GDCore/Tools/SerializableWithNameList.h" #include "NamedPropertyDescriptor.h" -#include "GDCore/Project/PropertyFolderOrProperty.h" namespace gd { @@ -14,20 +14,43 @@ namespace gd { * * \ingroup PlatformDefinition */ -class PropertiesContainer - : public SerializableWithNameList { - public: +class PropertiesContainer { +public: PropertiesContainer(EventsFunctionsContainer::FunctionOwner owner); - PropertiesContainer(const PropertiesContainer& other); + PropertiesContainer(const PropertiesContainer &other); - PropertiesContainer& operator=(const PropertiesContainer& other); + PropertiesContainer &operator=(const PropertiesContainer &other); + + NamedPropertyDescriptor &Insert(const NamedPropertyDescriptor &element, + size_t position = (size_t)-1); + NamedPropertyDescriptor &InsertNew(const gd::String &name, + size_t position = (size_t)-1); + bool Has(const gd::String &name) const; + NamedPropertyDescriptor &Get(const gd::String &name); + const NamedPropertyDescriptor &Get(const gd::String &name) const; + NamedPropertyDescriptor &Get(size_t index); + const NamedPropertyDescriptor &Get(size_t index) const; + void Remove(const gd::String &name); + void Move(std::size_t oldIndex, std::size_t newIndex); + size_t GetCount() const; + std::size_t GetPosition(const NamedPropertyDescriptor &element) const; + bool IsEmpty() const; + size_t size() const { return GetCount(); } + NamedPropertyDescriptor &at(size_t index) { return Get(index); }; + bool empty() const { return IsEmpty(); } + const std::vector>& GetInternalVector() const; + std::vector>& GetInternalVector(); + void SerializeElementsTo(const gd::String& elementName, + SerializerElement& element) const; + void UnserializeElementsFrom(const gd::String& elementName, + const SerializerElement& element); void ForEachPropertyMatchingSearch( - const gd::String& search, - std::function fn) + const gd::String &search, + std::function fn) const { - for (const auto& property : elements) { + for (const auto &property : properties.GetInternalVector()) { if (property->GetName().FindCaseInsensitive(search) != gd::String::npos) fn(*property); } @@ -41,9 +64,9 @@ class PropertiesContainer * * \return A reference to the property in the list. */ - gd::NamedPropertyDescriptor& InsertNewPropertyInFolder( - const gd::String& name, - gd::PropertyFolderOrProperty& propertyFolderOrProperty, + gd::NamedPropertyDescriptor &InsertNewPropertyInFolder( + const gd::String &name, + gd::PropertyFolderOrProperty &propertyFolderOrProperty, std::size_t position); /** @@ -51,28 +74,28 @@ class PropertiesContainer * Only use this for checking if you hold a valid `PropertyFolderOrProperty` - * don't use this for rendering or anything else. */ - std::vector GetAllPropertyFolderOrProperty() const; + std::vector + GetAllPropertyFolderOrProperty() const; - gd::PropertyFolderOrProperty& GetRootFolder() { - return *rootFolder; - } + gd::PropertyFolderOrProperty &GetRootFolder() { return *rootFolder; } void AddMissingPropertiesInRootFolder(); /** * \brief Serialize folder structure. */ - void SerializeFoldersTo(SerializerElement& element) const; + void SerializeFoldersTo(SerializerElement &element) const; /** * \brief Unserialize folder structure. */ - void UnserializeFoldersFrom(gd::Project& project, - const SerializerElement& element); + void UnserializeFoldersFrom(gd::Project &project, + const SerializerElement &element); - private: +private: EventsFunctionsContainer::FunctionOwner owner; + SerializableWithNameList properties; std::unique_ptr rootFolder; }; -} // namespace gd \ No newline at end of file +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.cpp b/Core/GDCore/Project/PropertyFolderOrProperty.cpp index aec0458bd33e..1edac5f1dee0 100644 --- a/Core/GDCore/Project/PropertyFolderOrProperty.cpp +++ b/Core/GDCore/Project/PropertyFolderOrProperty.cpp @@ -19,34 +19,31 @@ namespace gd { PropertyFolderOrProperty PropertyFolderOrProperty::badPropertyFolderOrProperty; PropertyFolderOrProperty::PropertyFolderOrProperty() - : folderName("__NULL"), - property(nullptr) {} -PropertyFolderOrProperty::PropertyFolderOrProperty(gd::String folderName_, - PropertyFolderOrProperty* parent_) - : folderName(folderName_), - parent(parent_), - property(nullptr) {} -PropertyFolderOrProperty::PropertyFolderOrProperty(gd::NamedPropertyDescriptor* property_, - PropertyFolderOrProperty* parent_) - : property(property_), - parent(parent_) {} + : folderName("__NULL"), property(nullptr) {} +PropertyFolderOrProperty::PropertyFolderOrProperty( + gd::String folderName_, PropertyFolderOrProperty *parent_) + : folderName(folderName_), parent(parent_), property(nullptr) {} +PropertyFolderOrProperty::PropertyFolderOrProperty( + gd::NamedPropertyDescriptor *property_, PropertyFolderOrProperty *parent_) + : property(property_), parent(parent_) {} PropertyFolderOrProperty::~PropertyFolderOrProperty() {} -bool PropertyFolderOrProperty::HasPropertyNamed(const gd::String& name) { +bool PropertyFolderOrProperty::HasPropertyNamed(const gd::String &name) { if (IsFolder()) { - return std::any_of( - children.begin(), - children.end(), - [&name]( - std::unique_ptr& propertyFolderOrProperty) { - return propertyFolderOrProperty->HasPropertyNamed(name); - }); + return std::any_of(children.begin(), children.end(), + [&name](std::unique_ptr + &propertyFolderOrProperty) { + return propertyFolderOrProperty->HasPropertyNamed( + name); + }); } - if (!property) return false; + if (!property) + return false; return property->GetName() == name; } -PropertyFolderOrProperty& PropertyFolderOrProperty::GetOrCreateChildFolder(const gd::String& name) { +PropertyFolderOrProperty & +PropertyFolderOrProperty::GetOrCreateChildFolder(const gd::String &name) { if (!IsFolder()) { LogError("Try to create of a folder '" + name + "' inside a property"); return gd::PropertyFolderOrProperty::badPropertyFolderOrProperty; @@ -59,14 +56,15 @@ PropertyFolderOrProperty& PropertyFolderOrProperty::GetOrCreateChildFolder(const return InsertNewFolder(name, GetChildrenCount()); } -PropertyFolderOrProperty& PropertyFolderOrProperty::GetPropertyNamed( - const gd::String& name) { +PropertyFolderOrProperty & +PropertyFolderOrProperty::GetPropertyNamed(const gd::String &name) { if (property && property->GetName() == name) { return *this; } if (IsFolder()) { for (std::size_t j = 0; j < children.size(); j++) { - PropertyFolderOrProperty& foundInChild = children[j]->GetPropertyNamed(name); + PropertyFolderOrProperty &foundInChild = + children[j]->GetPropertyNamed(name); if (&(foundInChild) != &badPropertyFolderOrProperty) { return foundInChild; } @@ -75,32 +73,51 @@ PropertyFolderOrProperty& PropertyFolderOrProperty::GetPropertyNamed( return badPropertyFolderOrProperty; } -void PropertyFolderOrProperty::SetFolderName(const gd::String& name) { - if (!IsFolder()) return; +void PropertyFolderOrProperty::SetFolderName(const gd::String &name) { + if (!IsFolder()) + return; folderName = name; + if (parent && !parent->parent) { + SetGroupNameOfAllProperties(name); + } +} + +void PropertyFolderOrProperty::SetGroupNameOfAllProperties( + const gd::String &groupName) { + if (IsFolder()) { + for (auto &&child : children) { + child->SetGroupNameOfAllProperties(groupName); + } + } else { + property->SetGroup(groupName); + } } -PropertyFolderOrProperty& PropertyFolderOrProperty::GetChildAt(std::size_t index) { - if (index >= children.size()) return badPropertyFolderOrProperty; +PropertyFolderOrProperty & +PropertyFolderOrProperty::GetChildAt(std::size_t index) { + if (index >= children.size()) + return badPropertyFolderOrProperty; return *children[index]; } -const PropertyFolderOrProperty& PropertyFolderOrProperty::GetChildAt( - std::size_t index) const { - if (index >= children.size()) return badPropertyFolderOrProperty; +const PropertyFolderOrProperty & +PropertyFolderOrProperty::GetChildAt(std::size_t index) const { + if (index >= children.size()) + return badPropertyFolderOrProperty; return *children[index]; } -PropertyFolderOrProperty& PropertyFolderOrProperty::GetPropertyChild( - const gd::String& name) { +PropertyFolderOrProperty & +PropertyFolderOrProperty::GetPropertyChild(const gd::String &name) { for (std::size_t j = 0; j < children.size(); j++) { if (!children[j]->IsFolder()) { - if (children[j]->GetProperty().GetName() == name) return *children[j]; + if (children[j]->GetProperty().GetName() == name) + return *children[j]; }; } return badPropertyFolderOrProperty; } -void PropertyFolderOrProperty::InsertProperty(gd::NamedPropertyDescriptor* insertedProperty, - std::size_t position) { +void PropertyFolderOrProperty::InsertProperty( + gd::NamedPropertyDescriptor *insertedProperty, std::size_t position) { auto propertyFolderOrProperty = gd::make_unique(insertedProperty, this); if (position < children.size()) { @@ -112,37 +129,38 @@ void PropertyFolderOrProperty::InsertProperty(gd::NamedPropertyDescriptor* inser } std::size_t PropertyFolderOrProperty::GetChildPosition( - const PropertyFolderOrProperty& child) const { + const PropertyFolderOrProperty &child) const { for (std::size_t j = 0; j < children.size(); j++) { - if (children[j].get() == &child) return j; + if (children[j].get() == &child) + return j; } return gd::String::npos; } -PropertyFolderOrProperty& PropertyFolderOrProperty::InsertNewFolder( - const gd::String& newFolderName, std::size_t position) { +PropertyFolderOrProperty & +PropertyFolderOrProperty::InsertNewFolder(const gd::String &newFolderName, + std::size_t position) { auto newFolderPtr = gd::make_unique(newFolderName, this); - gd::PropertyFolderOrProperty& newFolder = *(*(children.insert( + gd::PropertyFolderOrProperty &newFolder = *(*(children.insert( position < children.size() ? children.begin() + position : children.end(), std::move(newFolderPtr)))); return newFolder; }; void PropertyFolderOrProperty::RemoveRecursivelyPropertyNamed( - const gd::String& name) { + const gd::String &name) { if (IsFolder()) { children.erase( - std::remove_if(children.begin(), - children.end(), - [&name](std::unique_ptr& - propertyFolderOrProperty) { - return !propertyFolderOrProperty->IsFolder() && - propertyFolderOrProperty->GetProperty().GetName() == - name; - }), + std::remove_if( + children.begin(), children.end(), + [&name](std::unique_ptr + &propertyFolderOrProperty) { + return !propertyFolderOrProperty->IsFolder() && + propertyFolderOrProperty->GetProperty().GetName() == name; + }), children.end()); - for (auto& it : children) { + for (auto &it : children) { it->RemoveRecursivelyPropertyNamed(name); } } @@ -150,7 +168,7 @@ void PropertyFolderOrProperty::RemoveRecursivelyPropertyNamed( void PropertyFolderOrProperty::Clear() { if (IsFolder()) { - for (auto& it : children) { + for (auto &it : children) { it->Clear(); } children.clear(); @@ -158,73 +176,82 @@ void PropertyFolderOrProperty::Clear() { }; bool PropertyFolderOrProperty::IsADescendantOf( - const PropertyFolderOrProperty& otherPropertyFolderOrProperty) { - if (parent == nullptr) return false; - if (&(*parent) == &otherPropertyFolderOrProperty) return true; + const PropertyFolderOrProperty &otherPropertyFolderOrProperty) { + if (parent == nullptr) + return false; + if (&(*parent) == &otherPropertyFolderOrProperty) + return true; return parent->IsADescendantOf(otherPropertyFolderOrProperty); } void PropertyFolderOrProperty::MoveChild(std::size_t oldIndex, - std::size_t newIndex) { - if (!IsFolder()) return; - if (oldIndex >= children.size() || newIndex >= children.size()) return; + std::size_t newIndex) { + if (!IsFolder()) + return; + if (oldIndex >= children.size() || newIndex >= children.size()) + return; std::unique_ptr propertyFolderOrProperty = std::move(children[oldIndex]); children.erase(children.begin() + oldIndex); - children.insert(children.begin() + newIndex, std::move(propertyFolderOrProperty)); + children.insert(children.begin() + newIndex, + std::move(propertyFolderOrProperty)); } void PropertyFolderOrProperty::RemoveFolderChild( - const PropertyFolderOrProperty& childToRemove) { + const PropertyFolderOrProperty &childToRemove) { if (!IsFolder() || !childToRemove.IsFolder() || childToRemove.GetChildrenCount() > 0) { return; } - std::vector>::iterator it = find_if( - children.begin(), - children.end(), - [&childToRemove](std::unique_ptr& child) { - return child.get() == &childToRemove; - }); - if (it == children.end()) return; + std::vector>::iterator it = + find_if(children.begin(), children.end(), + [&childToRemove]( + std::unique_ptr &child) { + return child.get() == &childToRemove; + }); + if (it == children.end()) + return; children.erase(it); } void PropertyFolderOrProperty::MovePropertyFolderOrPropertyToAnotherFolder( - gd::PropertyFolderOrProperty& propertyFolderOrProperty, - gd::PropertyFolderOrProperty& newParentFolder, - std::size_t newPosition) { - if (!newParentFolder.IsFolder()) return; - if (newParentFolder.IsADescendantOf(propertyFolderOrProperty)) return; + gd::PropertyFolderOrProperty &propertyFolderOrProperty, + gd::PropertyFolderOrProperty &newParentFolder, std::size_t newPosition) { + if (!newParentFolder.IsFolder()) + return; + if (newParentFolder.IsADescendantOf(propertyFolderOrProperty)) + return; std::vector>::iterator it = - find_if(children.begin(), - children.end(), - [&propertyFolderOrProperty](std::unique_ptr& - childPropertyFolderOrProperty) { - return childPropertyFolderOrProperty.get() == &propertyFolderOrProperty; + find_if(children.begin(), children.end(), + [&propertyFolderOrProperty]( + std::unique_ptr + &childPropertyFolderOrProperty) { + return childPropertyFolderOrProperty.get() == + &propertyFolderOrProperty; }); - if (it == children.end()) return; + if (it == children.end()) + return; std::unique_ptr propertyFolderOrPropertyPtr = std::move(*it); children.erase(it); propertyFolderOrPropertyPtr->parent = &newParentFolder; - newParentFolder.children.insert( - newPosition < newParentFolder.children.size() - ? newParentFolder.children.begin() + newPosition - : newParentFolder.children.end(), - std::move(propertyFolderOrPropertyPtr)); + newParentFolder.children.insert(newPosition < newParentFolder.children.size() + ? newParentFolder.children.begin() + + newPosition + : newParentFolder.children.end(), + std::move(propertyFolderOrPropertyPtr)); } -void PropertyFolderOrProperty::SerializeTo(SerializerElement& element) const { +void PropertyFolderOrProperty::SerializeTo(SerializerElement &element) const { if (IsFolder()) { element.SetAttribute("folderName", GetFolderName()); if (children.size() > 0) { - SerializerElement& childrenElement = element.AddChild("children"); + SerializerElement &childrenElement = element.AddChild("children"); childrenElement.ConsiderAsArrayOf("propertyFolderOrProperty"); for (std::size_t j = 0; j < children.size(); j++) { children[j]->SerializeTo( @@ -237,9 +264,8 @@ void PropertyFolderOrProperty::SerializeTo(SerializerElement& element) const { } void PropertyFolderOrProperty::UnserializeFrom( - gd::Project& project, - const SerializerElement& element, - gd::PropertiesContainer& propertiesContainer) { + gd::Project &project, const SerializerElement &element, + gd::PropertiesContainer &propertiesContainer) { children.clear(); gd::String potentialFolderName = element.GetStringAttribute("folderName", ""); @@ -248,12 +274,13 @@ void PropertyFolderOrProperty::UnserializeFrom( folderName = potentialFolderName; if (element.HasChild("children")) { - const SerializerElement& childrenElements = + const SerializerElement &childrenElements = element.GetChild("children", 0); childrenElements.ConsiderAsArrayOf("propertyFolderOrProperty"); for (std::size_t i = 0; i < childrenElements.GetChildrenCount(); ++i) { - std::unique_ptr childPropertyFolderOrProperty = - make_unique(); + std::unique_ptr + childPropertyFolderOrProperty = + make_unique(); childPropertyFolderOrProperty->UnserializeFrom( project, childrenElements.GetChild(i), propertiesContainer); childPropertyFolderOrProperty->parent = this; @@ -273,4 +300,4 @@ void PropertyFolderOrProperty::UnserializeFrom( } }; -} // namespace gd \ No newline at end of file +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.h b/Core/GDCore/Project/PropertyFolderOrProperty.h index e0e41bd3c63d..0454e7dd056a 100644 --- a/Core/GDCore/Project/PropertyFolderOrProperty.h +++ b/Core/GDCore/Project/PropertyFolderOrProperty.h @@ -206,6 +206,8 @@ class GD_CORE_API PropertyFolderOrProperty { ///@} private: + void SetGroupNameOfAllProperties(const gd::String& groupName); + static gd::PropertyFolderOrProperty badPropertyFolderOrProperty; gd::PropertyFolderOrProperty* From 163e99b33e8c25b7621040878c913c7c7d20befd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Thu, 1 Jan 2026 22:52:45 +0100 Subject: [PATCH 19/30] Handle property folders in the tree --- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 1 + .../EnumeratePropertyFolderOrProperty.js | 93 +++ ...EntityPropertyFolderTreeViewItemContent.js | 351 ++++++++++ ...sBasedEntityPropertyTreeViewItemContent.js | 82 ++- .../PropertyListEditor/index.js | 644 +++++++++++++++--- 5 files changed, 1035 insertions(+), 136 deletions(-) create mode 100644 newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EnumeratePropertyFolderOrProperty.js create mode 100644 newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index 78a7c8f0229d..e764bdd84cad 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -164,6 +164,7 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< async () => { await pasteProperties( properties, + properties.getRootFolder(), properties.getCount(), showPropertyOverridingConfirmation ); diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EnumeratePropertyFolderOrProperty.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EnumeratePropertyFolderOrProperty.js new file mode 100644 index 000000000000..c6c7f4925df8 --- /dev/null +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EnumeratePropertyFolderOrProperty.js @@ -0,0 +1,93 @@ +// @flow + +import { mapFor } from '../../Utils/MapFor'; + +export const getPropertyFolderOrPropertyUnifiedName = ( + propertyFolderOrProperty: gdPropertyFolderOrProperty +) => + propertyFolderOrProperty.isFolder() + ? propertyFolderOrProperty.getFolderName() + : propertyFolderOrProperty.getProperty().getName(); + +const recursivelyEnumerateFoldersInFolder = ( + folder: gdPropertyFolderOrProperty, + prefix: string, + result: {| path: string, folder: gdPropertyFolderOrProperty |}[] +) => { + mapFor(0, folder.getChildrenCount(), i => { + const child = folder.getChildAt(i); + if (child.isFolder()) { + const newPrefix = prefix + ? prefix + ' > ' + child.getFolderName() + : child.getFolderName(); + result.push({ + path: newPrefix, + folder: child, + }); + recursivelyEnumerateFoldersInFolder(child, newPrefix, result); + } + }); +}; + +const recursivelyEnumeratePropertiesInFolder = ( + folder: gdPropertyFolderOrProperty, + result: gdNamedPropertyDescriptor[] +) => { + mapFor(0, folder.getChildrenCount(), i => { + const child = folder.getChildAt(i); + if (!child.isFolder()) { + result.push(child.getProperty()); + } else { + recursivelyEnumeratePropertiesInFolder(child, result); + } + }); +}; + +export const enumeratePropertiesInFolder = ( + folder: gdPropertyFolderOrProperty +): gdNamedPropertyDescriptor[] => { + if (!folder.isFolder()) return []; + const result = []; + recursivelyEnumeratePropertiesInFolder(folder, result); + return result; +}; + +export const enumerateFoldersInFolder = ( + folder: gdPropertyFolderOrProperty +): {| path: string, folder: gdPropertyFolderOrProperty |}[] => { + if (!folder.isFolder()) return []; + const result = []; + recursivelyEnumerateFoldersInFolder(folder, '', result); + return result; +}; + +export const enumerateFoldersInContainer = ( + container: gdPropertiesContainer +): {| path: string, folder: gdPropertyFolderOrProperty |}[] => { + const rootFolder = container.getRootFolder(); + const result = []; + recursivelyEnumerateFoldersInFolder(rootFolder, '', result); + return result; +}; + +export const getPropertiesInFolder = ( + propertyFolderOrProperty: gdPropertyFolderOrProperty +): gdNamedPropertyDescriptor[] => { + if (!propertyFolderOrProperty.isFolder()) return []; + return mapFor(0, propertyFolderOrProperty.getChildrenCount(), i => { + const child = propertyFolderOrProperty.getChildAt(i); + if (child.isFolder()) { + return null; + } + return child.getProperty(); + }).filter(Boolean); +}; + +export const getFoldersAscendanceWithoutRootFolder = ( + propertyFolderOrProperty: gdPropertyFolderOrProperty +): gdPropertyFolderOrProperty[] => { + if (propertyFolderOrProperty.isRootFolder()) return []; + const parent = propertyFolderOrProperty.getParent(); + if (parent.isRootFolder()) return []; + return [parent, ...getFoldersAscendanceWithoutRootFolder(parent)]; +}; diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js new file mode 100644 index 000000000000..149b128e9016 --- /dev/null +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js @@ -0,0 +1,351 @@ +// @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'; +import { + TreeViewItemContent, + type TreeItemProps, + propertiesRootFolderId, + sharedPropertiesRootFolderId, +} from '.'; +import { + enumerateFoldersInContainer, + enumerateFoldersInFolder, + enumeratePropertiesInFolder, +} from './EnumeratePropertyFolderOrProperty'; +import { + pasteProperties, + PROPERTIES_CLIPBOARD_KIND, +} from './EventsBasedEntityPropertyTreeViewItemContent'; +import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow'; +import { type HTMLDataset } from '../../Utils/HTMLDataset'; + +const gd: libGDevelop = global.gd; + +export const expandAllSubfolders = ( + propertyFolder: gdPropertyFolderOrProperty, + expandFolders: ( + propertyFolderOrPropertyList: Array + ) => void +) => { + const subFolders = enumerateFoldersInFolder(propertyFolder).map( + folderAndPath => folderAndPath.folder + ); + expandFolders([propertyFolder, ...subFolders].map(folder => folder)); +}; + +export type EventsBasedEntityPropertyFolderTreeViewItemProps = {| + ...TreeItemProps, + project: gdProject, + properties: gdPropertiesContainer, + isSharedProperties: boolean, + editName: (itemId: string) => void, + onPropertiesUpdated: () => void, + showPropertyOverridingConfirmation: ( + existingPropertyNames: string[] + ) => Promise, + expandFolders: ( + propertyFolderOrPropertyList: Array + ) => void, + addFolder: ( + items: Array, + isSharedProperties: boolean + ) => void, + addProperty: ( + properties: gdPropertiesContainer, + isSharedProperties: boolean, + parentFolder: gdPropertyFolderOrProperty, + index: number + ) => void, + onMovedPropertyFolderOrPropertyToAnotherFolderInSameContainer: ( + propertyFolderOrProperty: gdPropertyFolderOrProperty, + isSharedProperties: boolean + ) => void, + showDeleteConfirmation: (options: any) => Promise, + setSelectedPropertyFolderOrProperty: ( + propertyFolderOrProperty: gdPropertyFolderOrProperty | null, + isSharedProperties: boolean + ) => void, +|}; + +export const getEventsBasedEntityPropertyFolderTreeViewItemId = ( + propertyFolder: gdPropertyFolderOrProperty +): 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 `property-folder-${propertyFolder.ptr}`; +}; + +export class EventsBasedEntityPropertyFolderTreeViewItemContent + implements TreeViewItemContent { + propertyFolder: gdPropertyFolderOrProperty; + props: EventsBasedEntityPropertyFolderTreeViewItemProps; + + constructor( + propertyFolder: gdPropertyFolderOrProperty, + props: EventsBasedEntityPropertyFolderTreeViewItemProps + ) { + this.propertyFolder = propertyFolder; + this.props = props; + } + + getPropertyFolderOrProperty(): gdPropertyFolderOrProperty | null { + return this.propertyFolder; + } + + isDescendantOf(treeViewItemContent: TreeViewItemContent): boolean { + const propertyFolderOrProperty = treeViewItemContent.getPropertyFolderOrProperty(); + return ( + !!propertyFolderOrProperty && + this.propertyFolder.isADescendantOf(propertyFolderOrProperty) + ); + } + + isSibling(treeViewItemContent: TreeViewItemContent): boolean { + const propertyFolderOrProperty = treeViewItemContent.getPropertyFolderOrProperty(); + return ( + !!propertyFolderOrProperty && + this.propertyFolder.getParent() === propertyFolderOrProperty.getParent() + ); + } + + getRootId(): string { + return this.props.isSharedProperties + ? sharedPropertiesRootFolderId + : propertiesRootFolderId; + } + + getIndex(): number { + return this.propertyFolder + .getParent() + .getChildPosition(this.propertyFolder); + } + + getName(): string | React.Node { + return this.propertyFolder.getFolderName(); + } + + getId(): string { + return getEventsBasedEntityPropertyFolderTreeViewItemId( + this.propertyFolder + ); + } + + getHtmlId(index: number): ?string { + return null; + } + + getDataSet(): ?HTMLDataset { + return { + folderName: this.propertyFolder.getFolderName(), + isSharedProperties: this.props.isSharedProperties ? 'true' : 'false', + }; + } + + getThumbnail(): ?string { + return 'FOLDER'; + } + + onClick(): void {} + + rename(newName: string): void { + if (this.getName() === newName) { + return; + } + this.propertyFolder.setFolderName(newName); + this.props.onPropertiesUpdated(); + } + + edit(): void {} + + _getPasteLabel(i18n: I18nType) { + let translation = t`Paste`; + if (Clipboard.has(PROPERTIES_CLIPBOARD_KIND)) { + const clipboardContent = Clipboard.get(PROPERTIES_CLIPBOARD_KIND); + const clipboardObjectName = + SafeExtractor.extractStringProperty(clipboardContent, 'name') || ''; + translation = t`Paste ${clipboardObjectName} inside folder`; + } + return i18n._(translation); + } + + buildMenuTemplate(i18n: I18nType, index: number) { + const { + properties, + isSharedProperties, + expandFolders, + addFolder, + addProperty, + onMovedPropertyFolderOrPropertyToAnotherFolderInSameContainer, + } = this.props; + + const folderAndPathsInContainer = enumerateFoldersInContainer(properties); + folderAndPathsInContainer.unshift({ + path: i18n._(t`Root folder`), + folder: properties.getRootFolder(), + }); + + const filteredFolderAndPathsInContainer = folderAndPathsInContainer.filter( + folderAndPath => + !folderAndPath.folder.isADescendantOf(this.propertyFolder) && + folderAndPath.folder !== this.propertyFolder + ); + return [ + { + label: this._getPasteLabel(i18n), + enabled: Clipboard.has(PROPERTIES_CLIPBOARD_KIND), + click: () => this.paste(), + }, + { + label: i18n._(t`Rename`), + click: () => this.props.editName(this.getId()), + accelerator: 'F2', + }, + { + label: i18n._(t`Delete`), + click: () => this.delete(), + accelerator: 'Backspace', + }, + { + label: i18n._('Move to folder'), + submenu: [ + ...filteredFolderAndPathsInContainer.map(({ folder, path }) => ({ + label: path, + enabled: folder !== this.propertyFolder.getParent(), + click: () => { + if (folder === this.propertyFolder.getParent()) return; + this.propertyFolder + .getParent() + .movePropertyFolderOrPropertyToAnotherFolder( + this.propertyFolder, + folder, + 0 + ); + onMovedPropertyFolderOrPropertyToAnotherFolderInSameContainer( + folder, + this.props.isSharedProperties + ); + }, + })), + + { type: 'separator' }, + { + label: i18n._(t`Create new folder...`), + click: () => + addFolder([this.propertyFolder.getParent()], isSharedProperties), + }, + ], + }, + { type: 'separator' }, + { + label: i18n._(t`Add a new property`), + click: () => + addProperty( + this.props.properties, + this.props.isSharedProperties, + this.propertyFolder, + 0 + ), + }, + { + label: i18n._(t`Add a new folder`), + click: () => addFolder([this.propertyFolder], isSharedProperties), + }, + { type: 'separator' }, + { + label: i18n._(t`Expand all sub folders`), + click: () => expandAllSubfolders(this.propertyFolder, expandFolders), + }, + ]; + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return null; + } + + delete(): void { + this._delete(); + } + + async _delete(): Promise { + const { + properties, + forceUpdateList, + showDeleteConfirmation, + setSelectedPropertyFolderOrProperty, + } = this.props; + + const propertiesToDelete = enumeratePropertiesInFolder(this.propertyFolder); + if (propertiesToDelete.length === 0) { + // Folder is empty or contains only empty folders. + setSelectedPropertyFolderOrProperty(null, false); + this.propertyFolder.getParent().removeFolderChild(this.propertyFolder); + forceUpdateList(); + return; + } + + let message: MessageDescriptor; + let title: MessageDescriptor; + if (propertiesToDelete.length === 1) { + message = t`Are you sure you want to remove this folder and with it the property ${propertiesToDelete[0].getName()}? This can't be undone.`; + title = t`Remove folder and property`; + } else { + message = t`Are you sure you want to remove this folder and all its content (properties ${propertiesToDelete + .map(property => property.getName()) + .join(', ')})? This can't be undone.`; + title = t`Remove folder and properties`; + } + + const answer = await showDeleteConfirmation({ message, title }); + if (!answer) return; + + // TODO: Change selectedPropertyFolderOrPropertyWithContext so that it's easy + // to remove an item using keyboard only and to navigate with the arrow + // keys right after deleting it. + setSelectedPropertyFolderOrProperty(null, false); + + for (const propertyToDelete of propertiesToDelete) { + properties.remove(propertyToDelete.getName()); + } + this.propertyFolder.getParent().removeFolderChild(this.propertyFolder); + this._onProjectItemModified(); + } + + copy(): void {} + + cut(): void {} + + paste(): void { + this.pasteAsync(); + } + + async pasteAsync(): Promise { + const hasPasteAnyProperty = await pasteProperties( + this.props.properties, + this.propertyFolder, + this.getIndex() + 1, + this.props.showPropertyOverridingConfirmation + ); + if (hasPasteAnyProperty) { + this._onProjectItemModified(); + this.props.expandFolders([this.propertyFolder]); + } + } + + duplicate(): void {} + + _onProjectItemModified() { + if (this.props.unsavedChanges) + this.props.unsavedChanges.triggerUnsavedChanges(); + this.props.forceUpdate(); + this.props.onPropertiesUpdated(); + } + + getRightButton(i18n: I18nType) { + return null; + } +} diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 2f51657b6058..9faa2741f1ab 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -54,6 +54,7 @@ const getValidatedPropertyName = ( export const pasteProperties = async ( properties: gdPropertiesContainer, + parentFolder: gdPropertyFolderOrProperty, insertionIndex: number, showPropertyOverridingConfirmation: ( existingPropertyNames: string[] @@ -151,19 +152,39 @@ export const getEventsBasedEntityPropertyTreeViewItemId = ( export class EventsBasedEntityPropertyTreeViewItemContent implements TreeViewItemContent { - property: gdNamedPropertyDescriptor; + property: gdPropertyFolderOrProperty; props: EventsBasedEntityPropertyTreeViewItemProps; constructor( - property: gdNamedPropertyDescriptor, + property: gdPropertyFolderOrProperty, props: EventsBasedEntityPropertyTreeViewItemProps ) { this.property = property; this.props = props; } - isDescendantOf(itemContent: TreeViewItemContent): boolean { - return itemContent.getId() === this.getRootId(); + getPropertyFolderOrProperty(): gdPropertyFolderOrProperty | null { + return this.property; + } + + isDescendantOf(treeViewItemContent: TreeViewItemContent): boolean { + const propertyFolderOrProperty = treeViewItemContent.getPropertyFolderOrProperty(); + return ( + !!propertyFolderOrProperty && + this.property.isADescendantOf(propertyFolderOrProperty) + ); + } + + isSibling(treeViewItemContent: TreeViewItemContent): boolean { + const propertyFolderOrProperty = treeViewItemContent.getPropertyFolderOrProperty(); + return ( + !!propertyFolderOrProperty && + this.property.getParent() === propertyFolderOrProperty.getParent() + ); + } + + getIndex(): number { + return this.property.getParent().getChildPosition(this.property); } getRootId(): string { @@ -173,12 +194,12 @@ export class EventsBasedEntityPropertyTreeViewItemContent } getName(): string | React.Node { - return this.property.getName(); + return this.property.getProperty().getName(); } getId(): string { return getEventsBasedEntityPropertyTreeViewItemId( - this.property, + this.property.getProperty(), this.props.isSharedProperties ); } @@ -191,13 +212,13 @@ export class EventsBasedEntityPropertyTreeViewItemContent getDataSet(): ?HTMLDataset { return { - propertyName: this.property.getName(), + propertyName: this.property.getProperty().getName(), isSharedProperties: this.props.isSharedProperties ? 'true' : 'false', }; } getThumbnail(): ?string { - switch (this.property.getType()) { + switch (this.property.getProperty().getType()) { case 'Number': return 'res/functions/number_black.svg'; case 'Boolean': @@ -211,13 +232,13 @@ export class EventsBasedEntityPropertyTreeViewItemContent onClick(): void { this.props.onOpenProperty( - this.property.getName(), + this.property.getProperty().getName(), this.props.isSharedProperties ); } rename(newName: string): void { - const oldName = this.property.getName(); + const oldName = this.property.getProperty().getName(); if (oldName === newName) { return; } @@ -230,7 +251,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent newName ); this.props.onRenameProperty(oldName, validatedNewName); - this.property.setName(validatedNewName); + this.property.getProperty().setName(validatedNewName); this._onProjectItemModified(); } @@ -240,6 +261,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent } buildMenuTemplate(i18n: I18nType, index: number) { + const property = this.property.getProperty(); return [ { label: i18n._(t`Rename`), @@ -293,7 +315,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent project, extension, eventsBasedBehavior, - this.property, + property, isSharedProperties ); } else if (eventsBasedObject) { @@ -301,21 +323,21 @@ export class EventsBasedEntityPropertyTreeViewItemContent project, extension, eventsBasedObject, - this.property + property ); } onEventsFunctionsAdded(); }, enabled: gd.PropertyFunctionGenerator.canGenerateGetterAndSetter( this.props.eventsBasedEntity, - this.property + property ), }, ...renderQuickCustomizationMenuItems({ i18n, - visibility: this.property.getQuickCustomizationVisibility(), + visibility: property.getQuickCustomizationVisibility(), onChangeVisibility: visibility => { - this.property.setQuickCustomizationVisibility(visibility); + property.setQuickCustomizationVisibility(visibility); this.props.forceUpdate(); this.props.onPropertiesUpdated(); }, @@ -325,7 +347,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent renderRightComponent(i18n: I18nType): ?React.Node { const icons = []; - if (this.property.isHidden()) { + if (this.property.getProperty().isHidden()) { icons.push( { const hasPasteAnyProperty = await pasteProperties( this.props.properties, + this.property.getParent(), this.getIndex() + 1, this.props.showPropertyOverridingConfirmation ); @@ -395,8 +402,9 @@ export class EventsBasedEntityPropertyTreeViewItemContent } _duplicate(): void { - const newName = newNameGenerator(this.property.getName(), name => - this.props.properties.has(name) + const newName = newNameGenerator( + this.property.getProperty().getName(), + name => this.props.properties.has(name) ); const newProperty = this.props.properties.insertNew( newName, diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 3f01c6010759..82233b091595 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -33,6 +33,12 @@ import { getEventsBasedEntityPropertyTreeViewItemId, type EventsBasedEntityPropertyTreeViewItemProps, } from './EventsBasedEntityPropertyTreeViewItemContent'; +import { + EventsBasedEntityPropertyFolderTreeViewItemContent, + getEventsBasedEntityPropertyFolderTreeViewItemId, + expandAllSubfolders, + type EventsBasedEntityPropertyFolderTreeViewItemProps, +} from './EventsBasedEntityPropertyFolderTreeViewItemContent'; import { type MenuItemTemplate } from '../../UI/Menu/Menu.flow'; import useAlertDialog from '../../UI/Alert/useAlertDialog'; import { type ShowConfirmDeleteDialogOptions } from '../../UI/Alert/AlertContext'; @@ -41,6 +47,7 @@ import { type GDevelopTheme } from '../../UI/Theme'; import { type HTMLDataset } from '../../Utils/HTMLDataset'; import { ColumnStackLayout } from '../../UI/Layout'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; +import { getFoldersAscendanceWithoutRootFolder } from './EnumeratePropertyFolderOrProperty'; const configurationItemId = 'events-based-entity-configuration'; export const propertiesRootFolderId = 'properties'; @@ -62,6 +69,18 @@ const styles = { const extensionItemReactDndType = 'GD_EXTENSION_ITEM'; +export const getTreeViewItemIdFromPropertyFolderOrProperty = ( + propertyFolderOrObject: gdPropertyFolderOrProperty, + isSharedProperties: boolean +): string => { + return propertyFolderOrObject.isFolder() + ? getEventsBasedEntityPropertyFolderTreeViewItemId(propertyFolderOrObject) + : getEventsBasedEntityPropertyTreeViewItemId( + propertyFolderOrObject.getProperty(), + isSharedProperties + ); +}; + export interface TreeViewItemContent { getName(): string | React.Node; getId(): string; @@ -79,9 +98,10 @@ export interface TreeViewItemContent { paste(): void; cut(): void; getIndex(): number; - moveAt(destinationIndex: number): void; isDescendantOf(itemContent: TreeViewItemContent): boolean; + isSibling(itemContent: TreeViewItemContent): boolean; getRootId(): string; + getPropertyFolderOrProperty(): gdPropertyFolderOrProperty | null; } interface TreeViewItem { @@ -129,6 +149,83 @@ class PlaceHolderTreeViewItem implements TreeViewItem { } } +const createTreeViewItem = ({ + propertyFolderOrProperty, + propertyFolderTreeViewItemProps, + propertyTreeViewItemProps, +}: {| + propertyFolderOrProperty: gdPropertyFolderOrProperty, + propertyFolderTreeViewItemProps: EventsBasedEntityPropertyFolderTreeViewItemProps, + propertyTreeViewItemProps: EventsBasedEntityPropertyTreeViewItemProps, +|}): TreeViewItem => { + if (propertyFolderOrProperty.isFolder()) { + return new PropertyFolderTreeViewItem({ + propertyFolderOrProperty: propertyFolderOrProperty, + isRoot: false, + propertyFolderTreeViewItemProps, + propertyTreeViewItemProps, + content: new EventsBasedEntityPropertyFolderTreeViewItemContent( + propertyFolderOrProperty, + propertyFolderTreeViewItemProps + ), + }); + } else { + return new LeafTreeViewItem( + new EventsBasedEntityPropertyTreeViewItemContent( + propertyFolderOrProperty, + propertyTreeViewItemProps + ) + ); + } +}; + +class PropertyFolderTreeViewItem implements TreeViewItem { + isRoot: boolean; + isPlaceholder = false; + content: TreeViewItemContent; + propertyFolderOrProperty: gdPropertyFolderOrProperty; + placeholder: ?PlaceHolderTreeViewItem; + propertyFolderTreeViewItemProps: EventsBasedEntityPropertyFolderTreeViewItemProps; + propertyTreeViewItemProps: EventsBasedEntityPropertyTreeViewItemProps; + + constructor({ + propertyFolderOrProperty, + isRoot, + content, + placeholder, + propertyFolderTreeViewItemProps, + propertyTreeViewItemProps, + }: {| + propertyFolderOrProperty: gdPropertyFolderOrProperty, + isRoot: boolean, + content: TreeViewItemContent, + placeholder?: PlaceHolderTreeViewItem, + propertyFolderTreeViewItemProps: EventsBasedEntityPropertyFolderTreeViewItemProps, + propertyTreeViewItemProps: EventsBasedEntityPropertyTreeViewItemProps, + |}) { + this.isRoot = isRoot; + this.content = content; + this.propertyFolderOrProperty = propertyFolderOrProperty; + this.placeholder = placeholder; + this.propertyFolderTreeViewItemProps = propertyFolderTreeViewItemProps; + this.propertyTreeViewItemProps = propertyTreeViewItemProps; + } + + getChildren(i18n: I18nType): ?Array { + if (this.propertyFolderOrProperty.getChildrenCount() === 0) { + return this.placeholder ? [this.placeholder] : []; + } + return mapFor(0, this.propertyFolderOrProperty.getChildrenCount(), i => { + const child = this.propertyFolderOrProperty.getChildAt(i); + return createTreeViewItem({ + propertyFolderOrProperty: child, + propertyFolderTreeViewItemProps: this.propertyFolderTreeViewItemProps, + propertyTreeViewItemProps: this.propertyTreeViewItemProps, + }); + }); + } +} + class LabelTreeViewItemContent implements TreeViewItemContent { id: string; label: string | React.Node; @@ -142,20 +239,23 @@ class LabelTreeViewItemContent implements TreeViewItemContent { constructor( id: string, label: string | React.Node, - rightButton?: MenuButton + rightButton?: MenuButton, + buildMenuTemplateFunction?: () => Array ) { this.id = id; this.label = label; this.buildMenuTemplateFunction = (i18n: I18nType, index: number) => - rightButton - ? [ - { + [ + rightButton + ? { id: rightButton.id, - label: rightButton.label, + label: i18n._(rightButton.label), click: rightButton.click, - }, - ] - : []; + enabled: rightButton.enabled, + } + : null, + ...(buildMenuTemplateFunction ? buildMenuTemplateFunction() : []), + ].filter(Boolean); this.rightButton = rightButton; } @@ -209,15 +309,21 @@ class LabelTreeViewItemContent implements TreeViewItemContent { return 0; } - moveAt(destinationIndex: number): void {} - isDescendantOf(itemContent: TreeViewItemContent): boolean { return false; } + isSibling(treeViewItemContent: TreeViewItemContent): boolean { + return false; + } + getRootId(): string { return ''; } + + getPropertyFolderOrProperty(): gdPropertyFolderOrProperty | null { + return null; + } } class ActionTreeViewItemContent implements TreeViewItemContent { @@ -299,15 +405,21 @@ class ActionTreeViewItemContent implements TreeViewItemContent { return 0; } - moveAt(destinationIndex: number): void {} - isDescendantOf(itemContent: TreeViewItemContent): boolean { return false; } + isSibling(treeViewItemContent: TreeViewItemContent): boolean { + return false; + } + getRootId(): string { return ''; } + + getPropertyFolderOrProperty(): gdPropertyFolderOrProperty | null { + return null; + } } const getTreeViewItemName = (item: TreeViewItem) => item.content.getName(); @@ -401,6 +513,14 @@ const PropertyListEditor = React.forwardRef( const [selectedItems, setSelectedItems] = React.useState< Array >([]); + + const setSelectedPropertyFolderOrProperty = React.useRef< + ( + propertyFolderOrProperty: gdPropertyFolderOrProperty | null, + isSharedProperties: boolean + ) => void + >((propertyFolderOrProperty, isSharedProperties) => {}); + const unsavedChanges = React.useContext(UnsavedChangesContext); const { triggerUnsavedChanges } = unsavedChanges; const preferences = React.useContext(PreferencesContext); @@ -467,15 +587,19 @@ const PropertyListEditor = React.forwardRef( ( properties: gdPropertiesContainer, isSharedProperties: boolean, - index: number, - i18n: I18nType + parentFolder: gdPropertyFolderOrProperty, + index: number ) => { if (!properties) return; - const newName = newNameGenerator(i18n._(t`Property`), name => + const newName = newNameGenerator('Property', name => properties.has(name) ); - const property = properties.insertNew(newName, index); + const property = properties.insertNewPropertyInFolder( + newName, + parentFolder, + index + ); property.setType('Number'); onPropertiesUpdated(); @@ -568,6 +692,143 @@ const PropertyListEditor = React.forwardRef( [editName, selectedItems] ); + const getClosestVisibleParentId = ( + propertyFolderOrProperty: gdPropertyFolderOrProperty, + isSharedProperties: boolean + ): ?string => { + const treeView = treeViewRef.current; + if (!treeView) return null; + const topToBottomAscendanceId = getFoldersAscendanceWithoutRootFolder( + propertyFolderOrProperty + ) + .reverse() + .map(parent => + getEventsBasedEntityPropertyFolderTreeViewItemId( + propertyFolderOrProperty + ) + ); + const topToBottomAscendanceOpenness = treeView.areItemsOpenFromId( + topToBottomAscendanceId + ); + const firstClosedFolderIndex = topToBottomAscendanceOpenness.indexOf( + false + ); + if (firstClosedFolderIndex === -1) { + // If all parents are open, return the propertyFolderOrProperty given as input. + return getTreeViewItemIdFromPropertyFolderOrProperty( + propertyFolderOrProperty, + isSharedProperties + ); + } + // $FlowFixMe - We are confident this TreeView item is in fact a PropertyFolderOrPropertyWithContext + return topToBottomAscendanceId[firstClosedFolderIndex]; + }; + + const addFolder = React.useCallback( + ( + items: Array, + isSharedProperties: boolean + ) => { + let newPropertyFolderOrProperty; + if (items.length === 1) { + const selectedPropertyFolderOrProperty = items[0]; + if (selectedPropertyFolderOrProperty.isFolder()) { + const newFolder = selectedPropertyFolderOrProperty.insertNewFolder( + 'NewFolder', + 0 + ); + newPropertyFolderOrProperty = newFolder; + if (treeViewRef.current) { + treeViewRef.current.openItems([ + getEventsBasedEntityPropertyFolderTreeViewItemId(items[0]), + ]); + } + } else { + const parentFolder = selectedPropertyFolderOrProperty.getParent(); + const newFolder = parentFolder.insertNewFolder( + 'NewFolder', + parentFolder.getChildPosition(selectedPropertyFolderOrProperty) + + 1 + ); + newPropertyFolderOrProperty = newFolder; + } + } else { + const rootFolder = isSharedProperties + ? sharedProperties && sharedProperties.getRootFolder() + : properties && properties.getRootFolder(); + if (!rootFolder) { + return; + } + const newFolder = rootFolder.insertNewFolder('NewFolder', 0); + newPropertyFolderOrProperty = newFolder; + } + setSelectedPropertyFolderOrProperty.current( + newPropertyFolderOrProperty, + isSharedProperties + ); + const itemsToOpen = getFoldersAscendanceWithoutRootFolder( + newPropertyFolderOrProperty + ).map(folder => + getEventsBasedEntityPropertyFolderTreeViewItemId(folder) + ); + itemsToOpen.push( + isSharedProperties + ? sharedPropertiesRootFolderId + : propertiesRootFolderId + ); + if (treeViewRef.current) treeViewRef.current.openItems(itemsToOpen); + + editName( + getEventsBasedEntityPropertyFolderTreeViewItemId( + newPropertyFolderOrProperty + ) + ); + forceUpdateList(); + }, + [ + setSelectedPropertyFolderOrProperty, + editName, + forceUpdateList, + sharedProperties, + properties, + ] + ); + + const onMovedPropertyFolderOrPropertyToAnotherFolderInSameContainer = React.useCallback( + ( + propertyFolderOrProperty: gdPropertyFolderOrProperty, + isSharedProperties: boolean + ) => { + const treeView = treeViewRef.current; + if (treeView) { + const closestVisibleParentId = getClosestVisibleParentId( + propertyFolderOrProperty, + isSharedProperties + ); + if (closestVisibleParentId) { + treeView.animateItemFromId(closestVisibleParentId); + } + } + onTreeModified(true); + }, + [onTreeModified] + ); + + const expandFolders = React.useCallback( + (propertyFolderOrPropertyList: Array) => { + if (treeViewRef.current) { + treeViewRef.current.openItems( + propertyFolderOrPropertyList.map(propertyFolderOrProperty => + getEventsBasedEntityPropertyFolderTreeViewItemId( + propertyFolderOrProperty + ) + ) + ); + } + }, + [] + ); + const propertiesTreeViewItemProps = React.useMemo( () => properties && eventsBasedEntity @@ -631,27 +892,74 @@ const PropertyListEditor = React.forwardRef( [propertiesTreeViewItemProps, sharedProperties] ); - const createPropertyItem = React.useCallback( - (property: gdNamedPropertyDescriptor, isSharedProperties: boolean) => { - const treeViewItemProps = isSharedProperties - ? sharedPropertiesTreeViewItemProps - : propertiesTreeViewItemProps; - if (!treeViewItemProps) { - return null; - } - return new LeafTreeViewItem( - new EventsBasedEntityPropertyTreeViewItemContent( - property, - treeViewItemProps - ) - ); - }, - [propertiesTreeViewItemProps, sharedPropertiesTreeViewItemProps] + const propertyFolderTreeViewItemProps = React.useMemo( + () => + properties + ? { + unsavedChanges, + preferences, + gdevelopTheme, + forceUpdate, + forceUpdateList, + showDeleteConfirmation, + showPropertyOverridingConfirmation, + editName, + scrollToItem, + project, + properties, + isSharedProperties: false, + onPropertiesUpdated, + expandFolders, + addFolder, + addProperty, + onMovedPropertyFolderOrPropertyToAnotherFolderInSameContainer, + setSelectedPropertyFolderOrProperty: ( + propertyFolderOrProperty, + isSharedProperties + ) => + setSelectedPropertyFolderOrProperty.current( + propertyFolderOrProperty, + isSharedProperties + ), + } + : null, + [ + properties, + unsavedChanges, + preferences, + gdevelopTheme, + forceUpdate, + forceUpdateList, + showDeleteConfirmation, + showPropertyOverridingConfirmation, + editName, + scrollToItem, + project, + onPropertiesUpdated, + expandFolders, + addFolder, + addProperty, + onMovedPropertyFolderOrPropertyToAnotherFolderInSameContainer, + ] + ); + + const sharedPropertyFolderTreeViewItemProps = React.useMemo( + () => + sharedProperties && propertyFolderTreeViewItemProps + ? { + ...propertyFolderTreeViewItemProps, + properties: sharedProperties, + isSharedProperties: true, + } + : null, + [propertyFolderTreeViewItemProps, sharedProperties] ); const getTreeViewData = React.useCallback( (i18n: I18nType): Array => { - return !properties || !propertiesTreeViewItemProps + return !properties || + !propertiesTreeViewItemProps || + !propertyFolderTreeViewItemProps ? [] : [ new LeafTreeViewItem( @@ -662,7 +970,8 @@ const PropertyListEditor = React.forwardRef( 'res/icons_default/properties_black.svg' ) ), - { + new PropertyFolderTreeViewItem({ + propertyFolderOrProperty: properties.getRootFolder(), isRoot: true, content: new LabelTreeViewItemContent( propertiesRootFolderId, @@ -673,27 +982,44 @@ const PropertyListEditor = React.forwardRef( icon: , label: i18n._(t`Add a property`), click: () => { - addProperty(properties, false, 0, i18n); + addProperty( + properties, + false, + properties.getRootFolder(), + 0 + ); }, id: 'add-property', - } + }, + () => [ + { + label: i18n._(t`Add a folder`), + click: () => + addFolder([properties.getRootFolder()], false), + }, + { type: 'separator' }, + { + label: i18n._(t`Expand all sub folders`), + click: () => + expandAllSubfolders( + properties.getRootFolder(), + expandFolders + ), + }, + ] + ), + placeholder: new PlaceHolderTreeViewItem( + propertiesEmptyPlaceholderId, + i18n._(t`Start by adding a new property.`) ), - getChildren(i18n: I18nType): ?Array { - if (properties.getCount() === 0) { - return [ - new PlaceHolderTreeViewItem( - propertiesEmptyPlaceholderId, - i18n._(t`Start by adding a new property.`) - ), - ]; - } - return mapFor(0, properties.getCount(), i => - createPropertyItem(properties.getAt(i), false) - ).filter(Boolean); - }, - }, - sharedProperties - ? { + propertyTreeViewItemProps: propertiesTreeViewItemProps, + propertyFolderTreeViewItemProps, + }), + sharedProperties && + sharedPropertiesTreeViewItemProps && + sharedPropertyFolderTreeViewItemProps + ? new PropertyFolderTreeViewItem({ + propertyFolderOrProperty: sharedProperties.getRootFolder(), isRoot: true, content: new LabelTreeViewItemContent( sharedPropertiesRootFolderId, @@ -702,51 +1028,106 @@ const PropertyListEditor = React.forwardRef( icon: , label: i18n._(t`Add a property`), click: () => { - addProperty(sharedProperties, true, 0, i18n); + addProperty( + sharedProperties, + true, + sharedProperties.getRootFolder(), + 0 + ); }, id: 'add-shared-property', - } + }, + () => [ + { + label: i18n._(t`Add a folder`), + click: () => + addFolder([sharedProperties.getRootFolder()], true), + }, + { type: 'separator' }, + { + label: i18n._(t`Expand all sub folders`), + click: () => + expandAllSubfolders( + sharedProperties.getRootFolder(), + expandFolders + ), + }, + ] ), - getChildren(i18n: I18nType): ?Array { - if (sharedProperties.getCount() === 0) { - return [ - new PlaceHolderTreeViewItem( - sharedPropertiesEmptyPlaceholderId, - i18n._(t`Start by adding a new property.`) - ), - ]; - } - return mapFor(0, sharedProperties.getCount(), i => - createPropertyItem(sharedProperties.getAt(i), true) - ).filter(Boolean); - }, - } + placeholder: new PlaceHolderTreeViewItem( + sharedPropertiesEmptyPlaceholderId, + i18n._(t`Start by adding a new property.`) + ), + propertyTreeViewItemProps: sharedPropertiesTreeViewItemProps, + propertyFolderTreeViewItemProps: sharedPropertyFolderTreeViewItemProps, + }) : null, ].filter(Boolean); }, [ + addFolder, addProperty, - createPropertyItem, eventsBasedObject, + expandFolders, onOpenConfiguration, properties, propertiesTreeViewItemProps, + propertyFolderTreeViewItemProps, sharedProperties, + sharedPropertiesTreeViewItemProps, + sharedPropertyFolderTreeViewItemProps, ] ); - React.useImperativeHandle(ref, () => ({ - forceUpdateList: () => { - forceUpdate(); - if (treeViewRef.current) treeViewRef.current.forceUpdateList(); - }, - focusSearchBar: () => { - if (searchBarRef.current) searchBarRef.current.focus(); + // Avoid a circular dependency with propertiesTreeViewItemProps + React.useEffect( + () => { + setSelectedPropertyFolderOrProperty.current = ( + propertyFolderOrProperty: gdPropertyFolderOrProperty | null, + isSharedProperties: boolean + ) => { + if (!propertyFolderOrProperty) { + setSelectedItems([]); + return; + } + const propertyItemId = getTreeViewItemIdFromPropertyFolderOrProperty( + propertyFolderOrProperty, + isSharedProperties + ); + setSelectedItems(selectedItems => { + if ( + selectedItems.length === 1 && + selectedItems[0].content.getId() === propertyItemId + ) { + return selectedItems; + } + const treeViewItemProps = isSharedProperties + ? sharedPropertiesTreeViewItemProps + : propertiesTreeViewItemProps; + if (!treeViewItemProps || !propertyFolderTreeViewItemProps) { + return []; + } + return [ + createTreeViewItem({ + propertyFolderOrProperty, + propertyFolderTreeViewItemProps, + propertyTreeViewItemProps: treeViewItemProps, + }), + ].filter(Boolean); + }); + scrollToItem(propertyItemId); + }; }, - setSelectedProperty: ( - propertyName: string, - isSharedProperties: boolean - ) => { + [ + propertiesTreeViewItemProps, + propertyFolderTreeViewItemProps, + scrollToItem, + sharedPropertiesTreeViewItemProps, + ] + ); + + const setSelectedProperty = React.useCallback( + (propertyName: string, isSharedProperties: boolean) => { const propertiesContainer = isSharedProperties ? sharedProperties : properties; @@ -754,23 +1135,25 @@ const PropertyListEditor = React.forwardRef( return; } const property = propertiesContainer.get(propertyName); - const propertyItemId = getEventsBasedEntityPropertyTreeViewItemId( - property, + setSelectedPropertyFolderOrProperty.current( + propertiesContainer + .getRootFolder() + .getPropertyNamed(property.getName()), isSharedProperties ); - setSelectedItems(selectedItems => { - if ( - selectedItems.length === 1 && - selectedItems[0].content.getId() === propertyItemId - ) { - return selectedItems; - } - return [createPropertyItem(property, isSharedProperties)].filter( - Boolean - ); - }); - scrollToItem(propertyItemId); }, + [properties, sharedProperties] + ); + + React.useImperativeHandle(ref, () => ({ + forceUpdateList: () => { + forceUpdate(); + if (treeViewRef.current) treeViewRef.current.forceUpdateList(); + }, + focusSearchBar: () => { + if (searchBarRef.current) searchBarRef.current.focus(); + }, + setSelectedProperty, getSelectedProperty: () => { const selectedItem = selectedItems[0]; if (!selectedItem) { @@ -805,13 +1188,76 @@ const PropertyListEditor = React.forwardRef( destinationItem: TreeViewItem, where: 'before' | 'inside' | 'after' ) => { - if (selectedItems.length === 0) { + if (destinationItem.isRoot || selectedItems.length !== 1) { return; } const selectedItem = selectedItems[0]; - selectedItem.content.moveAt( - destinationItem.content.getIndex() + (where === 'after' ? 1 : 0) - ); + const selectedPropertyFolderOrProperty = selectedItem.content.getPropertyFolderOrProperty(); + + if ( + !selectedPropertyFolderOrProperty || + destinationItem.content.getId() === selectedItem.content.getId() + ) { + return; + } + + if (destinationItem.isPlaceholder) { + return; + } + + const destinationPropertyFolderOrProperty = destinationItem.content.getPropertyFolderOrProperty(); + if (!destinationPropertyFolderOrProperty) { + return; + } + if ( + selectedItem.content.getRootId() !== + destinationItem.content.getRootId() + ) { + return; + } + // At this point, the move is done from within the same container. + let parent; + if ( + where === 'inside' && + destinationPropertyFolderOrProperty.isFolder() + ) { + parent = destinationPropertyFolderOrProperty; + } else { + parent = destinationPropertyFolderOrProperty.getParent(); + } + const selectedPropertyFolderOrPropertyParent = selectedPropertyFolderOrProperty.getParent(); + if (parent === selectedPropertyFolderOrPropertyParent) { + const fromIndex = selectedItem.content.getIndex(); + let toIndex = destinationItem.content.getIndex(); + if (toIndex > fromIndex) toIndex -= 1; + if (where === 'after') toIndex += 1; + selectedPropertyFolderOrPropertyParent.moveChild(fromIndex, toIndex); + } else { + if (destinationItem.content.isDescendantOf(selectedItem.content)) { + return; + } + const position = + where === 'inside' + ? 0 + : destinationItem.content.getIndex() + + (where === 'after' ? 1 : 0); + selectedPropertyFolderOrPropertyParent.movePropertyFolderOrPropertyToAnotherFolder( + selectedPropertyFolderOrProperty, + parent, + position + ); + const treeView = treeViewRef.current; + if (treeView) { + const closestVisibleParentId = getClosestVisibleParentId( + parent, + destinationItem.content.getRootId() === + sharedPropertiesRootFolderId + ); + if (closestVisibleParentId) { + treeView.animateItemFromId(closestVisibleParentId); + } + } + } onTreeModified(true); }, [onTreeModified, selectedItems] From 5b7842c3531fa1364934dc58530054d0d12b3d5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 3 Jan 2026 14:11:15 +0100 Subject: [PATCH 20/30] Use the same order as the tree in the editor --- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 881 +++++++++--------- 1 file changed, 452 insertions(+), 429 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index e764bdd84cad..376ee9431589 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -27,6 +27,7 @@ import { PROPERTIES_CLIPBOARD_KIND, } from '../PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent'; import { usePropertyOverridingAlertDialog } from '../PropertyListEditor'; +import Text from '../../UI/Text'; const gd: libGDevelop = global.gd; @@ -245,91 +246,155 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< {properties.getCount() > 0 ? ( {mapVector( - properties, - (property: gdNamedPropertyDescriptor, i: number) => { - return ( - -
{ - propertyRefs.current.set(property.getName(), ref); - }} - style={{ - ...styles.rowContent, - backgroundColor: - gdevelopTheme.list.itemsBackgroundColor, - }} + properties.getAllPropertyFolderOrProperty(), + ( + propertyFolderOrProperty: gdPropertyFolderOrProperty, + i: number + ) => { + if (propertyFolderOrProperty.isFolder()) { + return ( + - - - - { - if (newName === property.getName()) return; + {propertyFolderOrProperty.getFolderName()} + + ); + } else { + const property = propertyFolderOrProperty.getProperty(); + return ( + +
{ + propertyRefs.current.set(property.getName(), ref); + }} + style={{ + ...styles.rowContent, + backgroundColor: + gdevelopTheme.list.itemsBackgroundColor, + }} + > + + + + { + if (newName === property.getName()) + return; - const projectScopedContainers = projectScopedContainersAccessor.get(); - const validatedNewName = getValidatedPropertyName( - properties, - projectScopedContainers, - newName - ); - onRenameProperty( - property.getName(), - validatedNewName - ); - property.setName(validatedNewName); + const projectScopedContainers = projectScopedContainersAccessor.get(); + const validatedNewName = getValidatedPropertyName( + properties, + projectScopedContainers, + newName + ); + onRenameProperty( + property.getName(), + validatedNewName + ); + property.setName(validatedNewName); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - fullWidth - /> - - + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + onFocus={() => + onFocusProperty(property.getName()) + } + fullWidth + /> + + + { + if (value === 'Hidden') { + setHidden(property, true); + setDeprecated(property, false); + setAdvanced(property, false); + } else if (value === 'Deprecated') { + setHidden(property, false); + setDeprecated(property, true); + setAdvanced(property, false); + } else if (value === 'Advanced') { + setHidden(property, false); + setDeprecated(property, false); + setAdvanced(property, true); + } else if (value === 'Visible') { + setHidden(property, false); + setDeprecated(property, false); + setAdvanced(property, false); + } + }} + onFocus={() => + onFocusProperty(property.getName()) + } + fullWidth + > + + + + + + + + +
+ + + Type} + value={property.getType()} onChange={(e, i, value: string) => { - if (value === 'Hidden') { - setHidden(property, true); - setDeprecated(property, false); - setAdvanced(property, false); - } else if (value === 'Deprecated') { - setHidden(property, false); - setDeprecated(property, true); - setAdvanced(property, false); - } else if (value === 'Advanced') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, true); - } else if (value === 'Visible') { - setHidden(property, false); - setDeprecated(property, false); - setAdvanced(property, false); + property.setType(value); + if (value === 'Behavior') { + property.setHidden(false); + } + if (value === 'Resource') { + setExtraInfoString(property, 'json'); } + forceUpdate(); + onPropertyTypeChanged(property.getName()); + onPropertiesUpdated && + onPropertiesUpdated(); }} onFocus={() => onFocusProperty(property.getName()) @@ -337,317 +402,329 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< fullWidth > - - - - -
- - - - Type} - value={property.getType()} - onChange={(e, i, value: string) => { - property.setType(value); - if (value === 'Behavior') { - property.setHidden(false); - } - if (value === 'Resource') { - setExtraInfoString(property, 'json'); - } - forceUpdate(); - onPropertyTypeChanged(property.getName()); - onPropertiesUpdated && onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - fullWidth - > - - - - - - {eventsBasedObject && ( - )} - {eventsBasedBehavior && !isSharedProperties && ( + {eventsBasedObject && ( + + )} + {eventsBasedBehavior && + !isSharedProperties && ( + + )} + {eventsBasedBehavior && + !isSharedProperties && ( + + )} - )} - {eventsBasedBehavior && !isSharedProperties && ( + {eventsBasedBehavior && + !isSharedProperties && ( + + )} + + {property.getType() === 'Number' && ( + Measurement unit + } + value={property + .getMeasurementUnit() + .getName()} + onChange={(e, i, value: string) => { + property.setMeasurementUnit( + gd.MeasurementUnit.getDefaultMeasurementUnitByName( + value + ) + ); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + onFocus={() => + onFocusProperty(property.getName()) + } + fullWidth + > + {mapFor( + 0, + gd.MeasurementUnit.getDefaultMeasurementUnitsCount(), + i => { + const measurementUnit = gd.MeasurementUnit.getDefaultMeasurementUnitAtIndex( + i + ); + const unitShortLabel = getMeasurementUnitShortLabel( + measurementUnit + ); + const label = + measurementUnit.getLabel() + + (unitShortLabel.length > 0 + ? ' — ' + unitShortLabel + : ''); + return ( + + ); + } + )} + )} - - - {eventsBasedBehavior && !isSharedProperties && ( - Default value + } + hintText={ + property.getType() === 'Number' + ? '123' + : 'ABC' + } + value={property.getValue()} + onChange={newValue => { + property.setValue(newValue); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + onFocus={() => + onFocusProperty(property.getName()) + } + multiline={ + property.getType() === 'MultilineString' + } + fullWidth /> )} - - {property.getType() === 'Number' && ( - Measurement unit - } - value={property - .getMeasurementUnit() - .getName()} - onChange={(e, i, value: string) => { - property.setMeasurementUnit( - gd.MeasurementUnit.getDefaultMeasurementUnitByName( - value - ) - ); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - fullWidth - > - {mapFor( - 0, - gd.MeasurementUnit.getDefaultMeasurementUnitsCount(), - i => { - const measurementUnit = gd.MeasurementUnit.getDefaultMeasurementUnitAtIndex( - i + {property.getType() === 'Boolean' && ( + Default value + } + value={ + property.getValue() === 'true' + ? 'true' + : 'false' + } + onChange={(e, i, value) => { + property.setValue(value); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + onFocus={() => + onFocusProperty(property.getName()) + } + fullWidth + > + + + + )} + {property.getType() === 'Behavior' && ( + { + // Change the type of the required behavior. + const extraInfo = property.getExtraInfo(); + if (extraInfo.size() === 0) { + extraInfo.push_back(newValue); + } else { + extraInfo.set(0, newValue); + } + const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata( + project.getCurrentPlatform(), + newValue ); - const unitShortLabel = getMeasurementUnitShortLabel( - measurementUnit + const projectScopedContainers = projectScopedContainersAccessor.get(); + const validatedNewName = getValidatedPropertyName( + properties, + projectScopedContainers, + behaviorMetadata.getDefaultName() ); - const label = - measurementUnit.getLabel() + - (unitShortLabel.length > 0 - ? ' — ' + unitShortLabel - : ''); - return ( - + property.setName(validatedNewName); + property.setLabel( + behaviorMetadata.getFullName() ); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + onFocus={() => + onFocusProperty(property.getName()) } - )} - - )} - {(property.getType() === 'String' || - property.getType() === 'Number' || - property.getType() === 'ObjectAnimationName' || - property.getType() === 'KeyboardKey' || - property.getType() === 'MultilineString') && ( - Default value - } - hintText={ - property.getType() === 'Number' - ? '123' - : 'ABC' - } - value={property.getValue()} - onChange={newValue => { - property.setValue(newValue); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - multiline={ - property.getType() === 'MultilineString' - } - fullWidth - /> - )} - {property.getType() === 'Boolean' && ( - Default value - } - value={ - property.getValue() === 'true' - ? 'true' - : 'false' - } - onChange={(e, i, value) => { - property.setValue(value); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - fullWidth - > - - Default value + } + disableAlpha + fullWidth + color={property.getValue()} + onChange={color => { + property.setValue(color); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} /> - - )} - {property.getType() === 'Behavior' && ( - { - // Change the type of the required behavior. - const extraInfo = property.getExtraInfo(); - if (extraInfo.size() === 0) { - extraInfo.push_back(newValue); - } else { - extraInfo.set(0, newValue); + )} + {property.getType() === 'Resource' && ( + 0 + ? property.getExtraInfo().at(0) + : '' } - const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata( - project.getCurrentPlatform(), - newValue - ); - const projectScopedContainers = projectScopedContainersAccessor.get(); - const validatedNewName = getValidatedPropertyName( - properties, - projectScopedContainers, - behaviorMetadata.getDefaultName() - ); - property.setName(validatedNewName); - property.setLabel( - behaviorMetadata.getFullName() - ); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - disabled={false} - /> - )} - {property.getType() === 'Color' && ( - Default value - } - disableAlpha - fullWidth - color={property.getValue()} - onChange={color => { - property.setValue(color); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} + onChange={(e, i, value) => { + setExtraInfoString(property, value); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + onFocus={() => + onFocusProperty(property.getName()) + } + fullWidth + /> + )} + {property.getType() === 'Choice' && ( + Default value + } + value={property.getValue()} + onChange={(e, i, value) => { + property.setValue(value); + forceUpdate(); + onPropertiesUpdated && + onPropertiesUpdated(); + }} + onFocus={() => + onFocusProperty(property.getName()) + } + fullWidth + > + {getChoicesArray(property).map( + (choice, index) => ( + + ) + )} + + )} + + {property.getType() === 'Choice' && ( + )} - {property.getType() === 'Resource' && ( - 0 - ? property.getExtraInfo().at(0) - : '' - } - onChange={(e, i, value) => { - setExtraInfoString(property, value); + + Short label} + translatableHintText={t`Make the purpose of the property easy to understand`} + floatingLabelFixed + value={property.getLabel()} + onChange={text => { + property.setLabel(text); forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); }} onFocus={() => onFocusProperty(property.getName()) } fullWidth /> - )} - {property.getType() === 'Choice' && ( - Default value - } - value={property.getValue()} - onChange={(e, i, value) => { - property.setValue(value); + Group name} + hintText={t`Leave it empty to use the default group`} + fullWidth + value={property.getGroup()} + onChange={text => { + property.setGroup(text); forceUpdate(); onPropertiesUpdated && onPropertiesUpdated(); @@ -655,41 +732,23 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< onFocus={() => onFocusProperty(property.getName()) } - fullWidth - > - {getChoicesArray(property).map( - (choice, index) => ( - - ) + dataSource={getPropertyGroupNames().map( + name => ({ + text: name, + value: name, + }) )} - - )} - - {property.getType() === 'Choice' && ( - - )} - + openOnFocus={true} + /> + Short label} - translatableHintText={t`Make the purpose of the property easy to understand`} + floatingLabelText={Description} + translatableHintText={t`Optionally, explain the purpose of the property in more details`} floatingLabelFixed - value={property.getLabel()} + value={property.getDescription()} onChange={text => { - property.setLabel(text); + property.setDescription(text); forceUpdate(); }} onFocus={() => @@ -697,47 +756,11 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< } fullWidth /> - Group name} - hintText={t`Leave it empty to use the default group`} - fullWidth - value={property.getGroup()} - onChange={text => { - property.setGroup(text); - forceUpdate(); - onPropertiesUpdated && onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - dataSource={getPropertyGroupNames().map( - name => ({ - text: name, - value: name, - }) - )} - openOnFocus={true} - /> - - Description} - translatableHintText={t`Optionally, explain the purpose of the property in more details`} - floatingLabelFixed - value={property.getDescription()} - onChange={text => { - property.setDescription(text); - forceUpdate(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - fullWidth - /> - - -
- ); + + + + ); + } } )}
From ca260baec493a5be6bb21ebd83b51d78029e70ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 00:01:49 +0100 Subject: [PATCH 21/30] Fix updating the groups --- .../GDCore/Project/PropertyFolderOrProperty.cpp | 17 +++++++++++++++++ Core/GDCore/Project/PropertyFolderOrProperty.h | 2 ++ .../PropertyListEditor/index.js | 3 ++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.cpp b/Core/GDCore/Project/PropertyFolderOrProperty.cpp index 1edac5f1dee0..aa4470a05e1d 100644 --- a/Core/GDCore/Project/PropertyFolderOrProperty.cpp +++ b/Core/GDCore/Project/PropertyFolderOrProperty.cpp @@ -17,6 +17,7 @@ using namespace std; namespace gd { PropertyFolderOrProperty PropertyFolderOrProperty::badPropertyFolderOrProperty; +gd::String PropertyFolderOrProperty::emptyGroupName; PropertyFolderOrProperty::PropertyFolderOrProperty() : folderName("__NULL"), property(nullptr) {} @@ -93,6 +94,19 @@ void PropertyFolderOrProperty::SetGroupNameOfAllProperties( } } +const gd::String &PropertyFolderOrProperty::GetGroupName() { + auto *groupFolder = this; + auto *rootFolder = parent; + if (!rootFolder) { + return gd::PropertyFolderOrProperty::emptyGroupName; + } + while (rootFolder->parent) { + groupFolder = rootFolder; + rootFolder = rootFolder->parent; + } + return groupFolder->GetFolderName(); +} + PropertyFolderOrProperty & PropertyFolderOrProperty::GetChildAt(std::size_t index) { if (index >= children.size()) @@ -120,6 +134,7 @@ void PropertyFolderOrProperty::InsertProperty( gd::NamedPropertyDescriptor *insertedProperty, std::size_t position) { auto propertyFolderOrProperty = gd::make_unique(insertedProperty, this); + propertyFolderOrProperty->GetProperty().SetGroup(GetGroupName()); if (position < children.size()) { children.insert(children.begin() + position, std::move(propertyFolderOrProperty)); @@ -240,6 +255,8 @@ void PropertyFolderOrProperty::MovePropertyFolderOrPropertyToAnotherFolder( children.erase(it); propertyFolderOrPropertyPtr->parent = &newParentFolder; + propertyFolderOrPropertyPtr->SetGroupNameOfAllProperties( + newParentFolder.GetGroupName()); newParentFolder.children.insert(newPosition < newParentFolder.children.size() ? newParentFolder.children.begin() + newPosition diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.h b/Core/GDCore/Project/PropertyFolderOrProperty.h index 0454e7dd056a..f4df97f7b6c9 100644 --- a/Core/GDCore/Project/PropertyFolderOrProperty.h +++ b/Core/GDCore/Project/PropertyFolderOrProperty.h @@ -207,8 +207,10 @@ class GD_CORE_API PropertyFolderOrProperty { private: void SetGroupNameOfAllProperties(const gd::String& groupName); + const gd::String &GetGroupName(); static gd::PropertyFolderOrProperty badPropertyFolderOrProperty; + static gd::String emptyGroupName; gd::PropertyFolderOrProperty* parent = nullptr; // nullptr if root folder, points to the parent folder otherwise. diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 82233b091595..c2b94dbb029d 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -643,11 +643,12 @@ const PropertyListEditor = React.forwardRef( const onTreeModified = React.useCallback( (shouldForceUpdateList: boolean) => { triggerUnsavedChanges(); + onPropertiesUpdated(); if (shouldForceUpdateList) forceUpdateList(); else forceUpdate(); }, - [forceUpdate, forceUpdateList, triggerUnsavedChanges] + [forceUpdate, forceUpdateList, onPropertiesUpdated, triggerUnsavedChanges] ); // Initialize keyboard shortcuts as empty. From c98583ae8e16e612efb4f55cb9abaf85133c4024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 00:10:37 +0100 Subject: [PATCH 22/30] Fix copy paste --- .../EventsBasedEntityPropertyTreeViewItemContent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 9faa2741f1ab..f2d99b619d25 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -375,7 +375,7 @@ export class EventsBasedEntityPropertyTreeViewItemContent Clipboard.set(PROPERTIES_CLIPBOARD_KIND, [ { name: this.property.getProperty().getName(), - serializedProperty: serializeToJSObject(this.property), + serializedProperty: serializeToJSObject(this.property.getProperty()), }, ]); } From 26210d46a13abcb065263745749463a861b3960b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 18:08:26 +0100 Subject: [PATCH 23/30] Expand all property folders by default --- .../PropertyListEditor/index.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index c2b94dbb029d..6a2f3724ecf9 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -47,7 +47,10 @@ import { type GDevelopTheme } from '../../UI/Theme'; import { type HTMLDataset } from '../../Utils/HTMLDataset'; import { ColumnStackLayout } from '../../UI/Layout'; import { ProjectScopedContainersAccessor } from '../../InstructionOrExpression/EventsScope'; -import { getFoldersAscendanceWithoutRootFolder } from './EnumeratePropertyFolderOrProperty'; +import { + getFoldersAscendanceWithoutRootFolder, + enumerateFoldersInContainer, +} from './EnumeratePropertyFolderOrProperty'; const configurationItemId = 'events-based-entity-configuration'; export const propertiesRootFolderId = 'properties'; @@ -1289,6 +1292,16 @@ const PropertyListEditor = React.forwardRef( const initiallyOpenedNodeIds = [ propertiesRootFolderId, sharedPropertiesRootFolderId, + ...(properties + ? enumerateFoldersInContainer(properties).map(({ folder }) => + getEventsBasedEntityPropertyFolderTreeViewItemId(folder) + ) + : []), + ...(sharedProperties + ? enumerateFoldersInContainer(sharedProperties).map(({ folder }) => + getEventsBasedEntityPropertyFolderTreeViewItemId(folder) + ) + : []), ]; return ( From 5fea6268d639a29b071bf894caaf079b44e52ab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 18:54:17 +0100 Subject: [PATCH 24/30] Ensure group names are in sync with the tree when unserializing --- Core/GDCore/Project/AbstractEventsBasedEntity.cpp | 2 ++ Core/GDCore/Project/EventsBasedBehavior.cpp | 2 ++ Core/GDCore/Project/PropertiesContainer.cpp | 1 + Core/GDCore/Project/PropertyDescriptor.cpp | 13 ++++++++++--- Core/GDCore/Project/PropertyFolderOrProperty.cpp | 12 ++++++++++++ Core/GDCore/Project/PropertyFolderOrProperty.h | 2 ++ 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Core/GDCore/Project/AbstractEventsBasedEntity.cpp b/Core/GDCore/Project/AbstractEventsBasedEntity.cpp index ff7973c9d7e7..99553377d4ea 100644 --- a/Core/GDCore/Project/AbstractEventsBasedEntity.cpp +++ b/Core/GDCore/Project/AbstractEventsBasedEntity.cpp @@ -53,7 +53,9 @@ void AbstractEventsBasedEntity::UnserializeFrom( propertyDescriptors.UnserializeFoldersFrom( project, element.GetChild("propertiesFolderStructure", 0)); } + // Compatibility with GD <= 5.6.251 propertyDescriptors.AddMissingPropertiesInRootFolder(); + // end of compatibility code } } // namespace gd diff --git a/Core/GDCore/Project/EventsBasedBehavior.cpp b/Core/GDCore/Project/EventsBasedBehavior.cpp index ccd184009bc3..75c0b0ceea63 100644 --- a/Core/GDCore/Project/EventsBasedBehavior.cpp +++ b/Core/GDCore/Project/EventsBasedBehavior.cpp @@ -46,7 +46,9 @@ void EventsBasedBehavior::UnserializeFrom(gd::Project& project, sharedPropertyDescriptors.UnserializeFoldersFrom( project, element.GetChild("sharedPropertiesFolderStructure", 0)); } + // Compatibility with GD <= 5.6.251 sharedPropertyDescriptors.AddMissingPropertiesInRootFolder(); + // end of compatibility code if (element.HasChild("quickCustomizationVisibility")) { quickCustomizationVisibility = element.GetStringAttribute("quickCustomizationVisibility") == "visible" diff --git a/Core/GDCore/Project/PropertiesContainer.cpp b/Core/GDCore/Project/PropertiesContainer.cpp index eee4b0756dc3..ac9af79f2144 100644 --- a/Core/GDCore/Project/PropertiesContainer.cpp +++ b/Core/GDCore/Project/PropertiesContainer.cpp @@ -158,6 +158,7 @@ void PropertiesContainer::SerializeFoldersTo(SerializerElement &element) const { void PropertiesContainer::UnserializeFoldersFrom( gd::Project &project, const SerializerElement &element) { rootFolder->UnserializeFrom(project, element, *this); + rootFolder->UpdateGroupNameOfAllProperties(); } } // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/PropertyDescriptor.cpp b/Core/GDCore/Project/PropertyDescriptor.cpp index 1eaff4e62c6e..ba836da8da9a 100644 --- a/Core/GDCore/Project/PropertyDescriptor.cpp +++ b/Core/GDCore/Project/PropertyDescriptor.cpp @@ -23,8 +23,13 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const { element.AddChild("label").SetStringValue(label); if (!description.empty()) element.AddChild("description").SetStringValue(description); - if (!group.empty()) element.AddChild("group").SetStringValue(group); - + // Compatibility with GD <= 5.6.251 + // The group is now persisted in the `propertiesFolderStructure` node. + // TODO Stop persisting the group name after a few releases when users are + // unlikely to go back to 5.6.251 to avoid redundancy. + if (!group.empty()) + element.AddChild("group").SetStringValue(group); + // end of compatibility code if (!extraInformation.empty()) { SerializerElement& extraInformationElement = element.AddChild("extraInformation"); @@ -83,9 +88,11 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) { description = element.HasChild("description") ? element.GetChild("description").GetStringValue() : ""; + // Compatibility with GD <= 5.6.251 + // The group is now persisted in the `propertiesFolderStructure` node. group = element.HasChild("group") ? element.GetChild("group").GetStringValue() : ""; - + // end of compatibility code extraInformation.clear(); if (element.HasChild("extraInformation")) { const SerializerElement& extraInformationElement = diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.cpp b/Core/GDCore/Project/PropertyFolderOrProperty.cpp index aa4470a05e1d..ef35ca7ade80 100644 --- a/Core/GDCore/Project/PropertyFolderOrProperty.cpp +++ b/Core/GDCore/Project/PropertyFolderOrProperty.cpp @@ -107,6 +107,18 @@ const gd::String &PropertyFolderOrProperty::GetGroupName() { return groupFolder->GetFolderName(); } +void PropertyFolderOrProperty::UpdateGroupNameOfAllProperties() { + if (parent) { + SetGroupNameOfAllProperties(GetGroupName()); + } + else { + // This is a root folder, the group is not the same for all children. + for (auto &&child : children) { + child->SetGroupNameOfAllProperties(child->GetGroupName()); + } + } +} + PropertyFolderOrProperty & PropertyFolderOrProperty::GetChildAt(std::size_t index) { if (index >= children.size()) diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.h b/Core/GDCore/Project/PropertyFolderOrProperty.h index f4df97f7b6c9..ebd81074195c 100644 --- a/Core/GDCore/Project/PropertyFolderOrProperty.h +++ b/Core/GDCore/Project/PropertyFolderOrProperty.h @@ -205,6 +205,8 @@ class GD_CORE_API PropertyFolderOrProperty { PropertiesContainer& propertiesContainer); ///@} + void UpdateGroupNameOfAllProperties(); + private: void SetGroupNameOfAllProperties(const gd::String& groupName); const gd::String &GetGroupName(); From 6fa170b69b7845dbf2b4a9ed37d76c08615bcc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 20:58:00 +0100 Subject: [PATCH 25/30] Fix property removing and pasted property group --- Core/GDCore/Project/PropertiesContainer.cpp | 2 +- .../Project/PropertyFolderOrProperty.cpp | 2 +- ...sBasedEntityPropertyTreeViewItemContent.js | 31 +++++++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Core/GDCore/Project/PropertiesContainer.cpp b/Core/GDCore/Project/PropertiesContainer.cpp index ac9af79f2144..9bba9dc51242 100644 --- a/Core/GDCore/Project/PropertiesContainer.cpp +++ b/Core/GDCore/Project/PropertiesContainer.cpp @@ -71,8 +71,8 @@ const NamedPropertyDescriptor &PropertiesContainer::Get(size_t index) const { } void PropertiesContainer::Remove(const gd::String &name) { - properties.Remove(name); rootFolder->RemoveRecursivelyPropertyNamed(name); + properties.Remove(name); } void PropertiesContainer::Move(std::size_t oldIndex, std::size_t newIndex) { diff --git a/Core/GDCore/Project/PropertyFolderOrProperty.cpp b/Core/GDCore/Project/PropertyFolderOrProperty.cpp index ef35ca7ade80..f0ffe77af521 100644 --- a/Core/GDCore/Project/PropertyFolderOrProperty.cpp +++ b/Core/GDCore/Project/PropertyFolderOrProperty.cpp @@ -144,9 +144,9 @@ PropertyFolderOrProperty::GetPropertyChild(const gd::String &name) { void PropertyFolderOrProperty::InsertProperty( gd::NamedPropertyDescriptor *insertedProperty, std::size_t position) { + insertedProperty->SetGroup(GetGroupName()); auto propertyFolderOrProperty = gd::make_unique(insertedProperty, this); - propertyFolderOrProperty->GetProperty().SetGroup(GetGroupName()); if (position < children.size()) { children.insert(children.begin() + position, std::move(propertyFolderOrProperty)); diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index f2d99b619d25..9083e74dabf0 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -94,9 +94,15 @@ export const pasteProperties = async ( let firstAddedPropertyName: string | null = null; let index = insertionIndex; newNamedProperties.forEach(({ name, serializedProperty }) => { - const property = properties.insertNew(name, index); + const property = properties.insertNewPropertyInFolder( + name, + parentFolder, + index + ); index++; + const groupName = property.getGroup(); unserializeFromJSObject(property, serializedProperty); + property.setGroup(groupName); if (!firstAddedPropertyName) { firstAddedPropertyName = name; } @@ -113,6 +119,21 @@ export const pasteProperties = async ( if (properties.has(name)) { const property = properties.get(name); unserializeFromJSObject(property, serializedProperty); + // Move the property in the current folder + const rootFolder = properties.getRootFolder(); + if (rootFolder.hasPropertyNamed(name)) { + const originalPropertyFolderOrProperty = rootFolder.getPropertyNamed( + name + ); + originalPropertyFolderOrProperty + .getParent() + .movePropertyFolderOrPropertyToAnotherFolder( + originalPropertyFolderOrProperty, + parentFolder, + index + ); + index++; + } } }); } @@ -406,12 +427,16 @@ export class EventsBasedEntityPropertyTreeViewItemContent this.property.getProperty().getName(), name => this.props.properties.has(name) ); - const newProperty = this.props.properties.insertNew( + const newProperty = this.props.properties.insertNewPropertyInFolder( newName, + this.property.getParent(), this.getIndex() + 1 ); - unserializeFromJSObject(newProperty, serializeToJSObject(this.property)); + unserializeFromJSObject( + newProperty, + serializeToJSObject(this.property.getProperty()) + ); newProperty.setName(newName); this._onProjectItemModified(); From 1ed94ffb13cd86d69dcad6168b658020c8e0a29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 21:13:49 +0100 Subject: [PATCH 26/30] Add an icon for resource properties --- .../app/public/res/functions/resource_black.svg | 15 +++++++++++++++ ...ventsBasedEntityPropertyTreeViewItemContent.js | 2 ++ 2 files changed, 17 insertions(+) create mode 100644 newIDE/app/public/res/functions/resource_black.svg diff --git a/newIDE/app/public/res/functions/resource_black.svg b/newIDE/app/public/res/functions/resource_black.svg new file mode 100644 index 000000000000..86a7abd2cfac --- /dev/null +++ b/newIDE/app/public/res/functions/resource_black.svg @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js index 9083e74dabf0..a03bb4fd1dce 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyTreeViewItemContent.js @@ -246,6 +246,8 @@ export class EventsBasedEntityPropertyTreeViewItemContent return 'res/functions/boolean_black.svg'; case 'Behavior': return 'res/functions/behavior_black.svg'; + case 'Resource': + return 'res/functions/resource_black.svg'; default: return 'res/functions/string_black.svg'; } From cdc616f6ecfce6438c85f7bde59ab5d016bd51a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 21:24:37 +0100 Subject: [PATCH 27/30] Typo --- .../EventsBasedEntityPropertyFolderTreeViewItemContent.js | 2 +- .../PropertyListEditor/index.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js index 149b128e9016..f96be13d2b0b 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/EventsBasedEntityPropertyFolderTreeViewItemContent.js @@ -294,7 +294,7 @@ export class EventsBasedEntityPropertyFolderTreeViewItemContent message = t`Are you sure you want to remove this folder and with it the property ${propertiesToDelete[0].getName()}? This can't be undone.`; title = t`Remove folder and property`; } else { - message = t`Are you sure you want to remove this folder and all its content (properties ${propertiesToDelete + message = t`Are you sure you want to remove this folder and all its properties (${propertiesToDelete .map(property => property.getName()) .join(', ')})? This can't be undone.`; title = t`Remove folder and properties`; diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js index 6a2f3724ecf9..9d0a0c025afc 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/PropertyListEditor/index.js @@ -738,7 +738,7 @@ const PropertyListEditor = React.forwardRef( const selectedPropertyFolderOrProperty = items[0]; if (selectedPropertyFolderOrProperty.isFolder()) { const newFolder = selectedPropertyFolderOrProperty.insertNewFolder( - 'NewFolder', + 'New folder', 0 ); newPropertyFolderOrProperty = newFolder; @@ -750,7 +750,7 @@ const PropertyListEditor = React.forwardRef( } else { const parentFolder = selectedPropertyFolderOrProperty.getParent(); const newFolder = parentFolder.insertNewFolder( - 'NewFolder', + 'New folder', parentFolder.getChildPosition(selectedPropertyFolderOrProperty) + 1 ); @@ -763,7 +763,7 @@ const PropertyListEditor = React.forwardRef( if (!rootFolder) { return; } - const newFolder = rootFolder.insertNewFolder('NewFolder', 0); + const newFolder = rootFolder.insertNewFolder('New folder', 0); newPropertyFolderOrProperty = newFolder; } setSelectedPropertyFolderOrProperty.current( From 6869586637e56b9044a324aaee7a098d7763c1bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Jan 2026 21:27:37 +0100 Subject: [PATCH 28/30] Remove the field for property group that is now useless --- ...tsBasedBehaviorOrObjectPropertiesEditor.js | 22 ------------------- 1 file changed, 22 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js index 376ee9431589..38ec2e551197 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/EventsBasedBehaviorOrObjectEditor/EventsBasedBehaviorOrObjectPropertiesEditor.js @@ -718,28 +718,6 @@ export const EventsBasedBehaviorPropertiesEditor = React.forwardRef< } fullWidth /> - Group name} - hintText={t`Leave it empty to use the default group`} - fullWidth - value={property.getGroup()} - onChange={text => { - property.setGroup(text); - forceUpdate(); - onPropertiesUpdated && - onPropertiesUpdated(); - }} - onFocus={() => - onFocusProperty(property.getName()) - } - dataSource={getPropertyGroupNames().map( - name => ({ - text: name, - value: name, - }) - )} - openOnFocus={true} - /> Date: Fri, 9 Jan 2026 15:37:44 +0100 Subject: [PATCH 29/30] Remove commented code. --- .../app/src/EventsFunctionsExtensionEditor/index.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 42030c6342cc..2f83748893b7 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -358,16 +358,13 @@ export default class EventsFunctionsExtensionEditor extends React.Component< () => { this.updateToolbar(); - if (this._editorMosaic) { - this._editorMosaic.uncollapseEditor('parameters', 25); - } if (this._editorNavigator) { // Open the parameters of the function if it's a new, empty function. if ( selectedEventsFunction && !selectedEventsFunction.getEvents().getEventsCount() ) { - //this._editorNavigator.openEditor('parameters'); + this._editorNavigator.openEditor('parameters'); } else { this._editorNavigator.openEditor('events-sheet'); } @@ -648,9 +645,6 @@ export default class EventsFunctionsExtensionEditor extends React.Component< () => { this.updateToolbar(); if (selectedEventsBasedBehavior) { - if (this._editorMosaic) { - //this._editorMosaic.collapseEditor('parameters'); - } if (this._editorNavigator) { this._editorNavigator.openEditor('events-sheet'); } @@ -675,9 +669,6 @@ export default class EventsFunctionsExtensionEditor extends React.Component< () => { this.updateToolbar(); if (selectedEventsBasedObject) { - if (this._editorMosaic) { - //this._editorMosaic.collapseEditor('parameters'); - } if (this._editorNavigator) this._editorNavigator.openEditor('events-sheet'); } From 0239fb4572bdfe46c6c6bb9c97317e0f9763b2a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 9 Jan 2026 15:45:49 +0100 Subject: [PATCH 30/30] Fix attribute name --- Core/GDCore/Project/AbstractEventsBasedEntity.cpp | 2 +- Core/GDCore/Project/EventsBasedBehavior.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/GDCore/Project/AbstractEventsBasedEntity.cpp b/Core/GDCore/Project/AbstractEventsBasedEntity.cpp index 99553377d4ea..9247d2bbd39a 100644 --- a/Core/GDCore/Project/AbstractEventsBasedEntity.cpp +++ b/Core/GDCore/Project/AbstractEventsBasedEntity.cpp @@ -33,7 +33,7 @@ void AbstractEventsBasedEntity::SerializeTo(SerializerElement& element) const { propertyDescriptors.SerializeElementsTo( "propertyDescriptor", element.AddChild("propertyDescriptors")); propertyDescriptors.SerializeFoldersTo( - element.AddChild("propertyFolderStructure")); + element.AddChild("propertiesFolderStructure")); } void AbstractEventsBasedEntity::UnserializeFrom( diff --git a/Core/GDCore/Project/EventsBasedBehavior.cpp b/Core/GDCore/Project/EventsBasedBehavior.cpp index 75c0b0ceea63..57fe5a23cdea 100644 --- a/Core/GDCore/Project/EventsBasedBehavior.cpp +++ b/Core/GDCore/Project/EventsBasedBehavior.cpp @@ -25,7 +25,7 @@ void EventsBasedBehavior::SerializeTo(SerializerElement& element) const { sharedPropertyDescriptors.SerializeElementsTo( "propertyDescriptor", element.AddChild("sharedPropertyDescriptors")); sharedPropertyDescriptors.SerializeFoldersTo( - element.AddChild("sharedPropertyFolderStructure")); + element.AddChild("sharedPropertiesFolderStructure")); } if (quickCustomizationVisibility != QuickCustomization::Visibility::Default) { element.SetStringAttribute(