From 007d56e9460b8ef4e00a7527a5cdaf8ee8bb75c8 Mon Sep 17 00:00:00 2001 From: AlexandreS <32449369+AlexandreSi@users.noreply.github.com> Date: Mon, 26 Jun 2023 18:07:23 +0200 Subject: [PATCH] Display the scene editors more conveniently on mobile (#5441) --- newIDE/app/scripts/theme-templates/theme.json | 61 + .../InstancesEditor/InstancesList/index.js | 49 +- .../src/InstancesEditor/SelectedInstances.js | 4 +- .../app/src/InstancesEditor/ViewPosition.js | 8 + newIDE/app/src/InstancesEditor/index.js | 59 +- newIDE/app/src/ObjectsList/index.js | 2 +- .../src/SceneEditor/EditorsDisplay.flow.js | 161 +++ .../{ => MosaicEditorsDisplay}/Toolbar.js | 36 +- .../SceneEditor/MosaicEditorsDisplay/index.js | 422 ++++++ .../BottomToolbar.js | 74 ++ .../SwipeableDrawer.js | 131 ++ .../SwipeableDrawerEditorsDisplay/Toolbar.js | 163 +++ .../UseSwipeGesture.js | 50 + .../SwipeableDrawerEditorsDisplay/index.js | 413 ++++++ newIDE/app/src/SceneEditor/index.js | 1132 +++++++---------- newIDE/app/src/UI/EditorMosaic/TagsButton.js | 16 +- newIDE/app/src/UI/IconButton.js | 3 + newIDE/app/src/UI/Menu/ContextMenu.js | 12 +- newIDE/app/src/UI/Menu/ElementWithMenu.js | 3 +- .../UI/Menu/MaterialUIMenuImplementation.js | 6 +- newIDE/app/src/UI/Paper.js | 18 +- .../app/src/UI/Theme/BlueDarkTheme/theme.json | 7 + newIDE/app/src/UI/Theme/CreateTheme.js | 5 + .../src/UI/Theme/DefaultDarkTheme/theme.json | 7 + .../src/UI/Theme/DefaultLightTheme/theme.json | 7 + newIDE/app/src/UI/Theme/Global/Table.css | 7 + newIDE/app/src/UI/Theme/NordTheme/theme.json | 7 + .../app/src/UI/Theme/OneDarkTheme/theme.json | 7 + .../app/src/UI/Theme/RosePineTheme/theme.json | 7 + .../UI/Theme/SolarizedDarkTheme/theme.json | 7 + newIDE/app/src/UI/TreeTable/index.js | 4 - 31 files changed, 2176 insertions(+), 712 deletions(-) create mode 100644 newIDE/app/src/SceneEditor/EditorsDisplay.flow.js rename newIDE/app/src/SceneEditor/{ => MosaicEditorsDisplay}/Toolbar.js (87%) create mode 100644 newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js create mode 100644 newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/BottomToolbar.js create mode 100644 newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/SwipeableDrawer.js create mode 100644 newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/Toolbar.js create mode 100644 newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/UseSwipeGesture.js create mode 100644 newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js diff --git a/newIDE/app/scripts/theme-templates/theme.json b/newIDE/app/scripts/theme-templates/theme.json index c5e52fc94..67977f03a 100644 --- a/newIDE/app/scripts/theme-templates/theme.json +++ b/newIDE/app/scripts/theme-templates/theme.json @@ -48,6 +48,67 @@ } } }, + "swipeable-drawer": { + "top-bar": { + "pill-color": { + "value": "#1D1D26" + } + } + }, + "icon-button": { + "selected": { + "color": { + "value": "#1D1D26", + "comment": "Palette/Grey/100" + }, + "background-color": { + "value": "#C9B6FC", + "comment:": "Palette/Purple/20" + } + } + }, + "link": { + "color": { + "value": "#DDD1FF", + "comment": "Palette/Purple/10" + }, + "hover-color": { + "value": "#C9B6FC", + "comment": "Palette/Purple/20" + } + }, + "switch": { + "default": { + "thumb-color": { + "value": "#EBEBED", + "comment": "Palette/Grey/10" + }, + "track-color": { + "value": "#606066", + "comment": "Palette/Grey/60" + } + }, + "toggled": { + "thumb-color": { + "value": "#DDD1FF", + "comment": "Palette/Purple/10" + }, + "track-color": { + "value": "#7046EC", + "comment": "Palette/Purple/40" + } + }, + "disabled": { + "thumb-color": { + "value": "#494952", + "comment": "Palette/Grey/70" + }, + "track-color": { + "value": "#32323B", + "comment": "Palette/Grey/80" + } + } + }, "search-bar": { "default": { "background-color": { diff --git a/newIDE/app/src/InstancesEditor/InstancesList/index.js b/newIDE/app/src/InstancesEditor/InstancesList/index.js index b080539b6..158db4fa6 100644 --- a/newIDE/app/src/InstancesEditor/InstancesList/index.js +++ b/newIDE/app/src/InstancesEditor/InstancesList/index.js @@ -14,8 +14,17 @@ import SearchBar, { type SearchBarInterface } from '../../UI/SearchBar'; import RemoveCircle from '../../UI/CustomSvgIcons/RemoveCircle'; import Lock from '../../UI/CustomSvgIcons/Lock'; import LockOpen from '../../UI/CustomSvgIcons/LockOpen'; +import { toFixedWithoutTrailingZeros } from '../../Utils/Mathematics'; const gd = global.gd; +const minimumWidths = { + table: 400, + objectName: 80, + icon: 40, + numberProperty: 40, + layerName: 50, +}; + type State = {| searchText: string, sortBy: string, @@ -46,6 +55,7 @@ const styles = { flexDirection: 'column', alignItems: 'stretch', }, + tableContainer: { flex: 1, overflowX: 'auto', overflowY: 'hidden' }, }; const compareStrings = (x: string, y: string, direction: number): number => { @@ -89,9 +99,9 @@ export default class InstancesList extends Component { instance, name, locked: instance.isLocked(), - x: instance.getX().toFixed(2), - y: instance.getY().toFixed(2), - angle: instance.getAngle().toFixed(2), + x: toFixedWithoutTrailingZeros(instance.getX(), 2), + y: toFixedWithoutTrailingZeros(instance.getY(), 2), + angle: toFixedWithoutTrailingZeros(instance.getAngle(), 2), layer: instance.getLayer(), zOrder: instance.getZOrder(), }); @@ -225,7 +235,7 @@ export default class InstancesList extends Component { {gdevelopTheme => (
@@ -246,49 +256,64 @@ export default class InstancesList extends Component { sort={this._sort} sortBy={sortBy} sortDirection={sortDirection} - width={width} + width={Math.max(width, minimumWidths.table)} > Object name} dataKey="name" - width={width * 0.35} + width={Math.max(width * 0.35, minimumWidths.objectName)} className={'tableColumn'} /> X} dataKey="x" - width={width * 0.1} + width={Math.max( + width * 0.1, + minimumWidths.numberProperty + )} className={'tableColumn'} /> Y} dataKey="y" - width={width * 0.1} + width={Math.max( + width * 0.1, + minimumWidths.numberProperty + )} className={'tableColumn'} /> Angle} dataKey="angle" - width={width * 0.1} + width={Math.max( + width * 0.1, + minimumWidths.numberProperty + )} className={'tableColumn'} /> Layer} dataKey="layer" - width={width * 0.2} + width={Math.max(width * 0.2, minimumWidths.layerName)} className={'tableColumn'} /> Z Order} dataKey="zOrder" - width={width * 0.1} + width={Math.max( + width * 0.1, + minimumWidths.numberProperty + )} className={'tableColumn'} /> diff --git a/newIDE/app/src/InstancesEditor/SelectedInstances.js b/newIDE/app/src/InstancesEditor/SelectedInstances.js index 9d06543a9..2d0db890c 100644 --- a/newIDE/app/src/InstancesEditor/SelectedInstances.js +++ b/newIDE/app/src/InstancesEditor/SelectedInstances.js @@ -190,7 +190,7 @@ export default class SelectedInstances { ); } - getSelectionAABB(): Rectangle { + getSelectionAABB = (): Rectangle => { const selectionAABB = new Rectangle(); const selection = this.instancesSelection.getSelectedInstances(); let instanceRect = new Rectangle(); @@ -208,7 +208,7 @@ export default class SelectedInstances { } } return selectionAABB; - } + }; render() { const { diff --git a/newIDE/app/src/InstancesEditor/ViewPosition.js b/newIDE/app/src/InstancesEditor/ViewPosition.js index 1b39bc4bf..9931ef758 100644 --- a/newIDE/app/src/InstancesEditor/ViewPosition.js +++ b/newIDE/app/src/InstancesEditor/ViewPosition.js @@ -83,6 +83,14 @@ export default class ViewPosition { return [x + this.viewX, y + this.viewY]; }; + /** + * Convert a length from canvas referential to scene referential. + */ + toSceneScale = (a: number): number => + this.instancesEditorSettings.zoomFactor === 0 + ? a + : a / Math.abs(this.instancesEditorSettings.zoomFactor); + /** * Convert a point from the "world" coordinates (for example, an object position) to the * canvas coordinates. diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js index ae4dcc5a9..b18380b54 100644 --- a/newIDE/app/src/InstancesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/index.js @@ -96,9 +96,9 @@ type Props = {| ...InstancesEditorPropsWithoutSizeAndScroll, width: number, height: number, - onViewPositionChanged: ViewPosition => void, - onMouseMove: MouseEvent => void, - onMouseLeave: MouseEvent => void, + onViewPositionChanged?: ViewPosition => void, + onMouseMove?: MouseEvent => void, + onMouseLeave?: MouseEvent => void, screenType: ScreenType, showObjectInstancesIn3D: boolean, |}; @@ -161,7 +161,7 @@ export default class InstancesEditor extends Component { // project can be used here for initializing stuff, but don't keep references to it. // Instead, create editors in _mountEditorComponents (as they will be destroyed/recreated // if the project changes). - const { project } = this.props; + const { project, onMouseMove, onMouseLeave } = this.props; this.keyboardShortcuts = new KeyboardShortcuts({ shortcutCallbacks: { @@ -257,12 +257,14 @@ export default class InstancesEditor extends Component { 'mouseup', this.keyboardShortcuts.onMouseUp ); - this.pixiRenderer.view.addEventListener('mousemove', event => { - this.props.onMouseMove(event); - }); - this.pixiRenderer.view.addEventListener('mouseout', event => { - this.props.onMouseLeave(event); - }); + if (onMouseMove) + this.pixiRenderer.view.addEventListener('mousemove', event => { + onMouseMove(event); + }); + if (onMouseLeave) + this.pixiRenderer.view.addEventListener('mouseout', event => { + onMouseLeave(event); + }); this.pixiRenderer.view.addEventListener('focusout', event => { if (this.keyboardShortcuts) { this.keyboardShortcuts.resetModifiers(); @@ -368,9 +370,9 @@ export default class InstancesEditor extends Component { * this when the initial instances were recreated to ensure that there * is not mismatch between renderers and the instances that were updated. */ - forceRemount() { + forceRemount = () => { this._mountEditorComponents(this.props); - } + }; _mountEditorComponents(props: Props) { //Remove and delete any existing editor component @@ -558,14 +560,14 @@ export default class InstancesEditor extends Component { * See also ResourcesLoader and PixiResourcesLoader. * @param {string} objectName The name of the object for which instance must be re-rendered. */ - resetInstanceRenderersFor(objectName: string) { + resetInstanceRenderersFor = (objectName: string) => { if (this.instancesRenderer) this.instancesRenderer.resetInstanceRenderersFor(objectName); - } + }; - zoomBy(value: number) { + zoomBy = (value: number) => { this.setZoomFactor(this.getZoomFactor() * value); - } + }; /** * Zoom and scroll so that the cursor stays on the same position scene-wise. @@ -642,7 +644,8 @@ export default class InstancesEditor extends Component { if ( !this.keyboardShortcuts.shouldMultiSelect() && - !this.keyboardShortcuts.shouldMoveView() + !this.keyboardShortcuts.shouldMoveView() && + this.props.instancesSelection.hasSelectedInstances() ) { this.props.instancesSelection.clearSelection(); this.props.onInstancesSelected([]); @@ -925,7 +928,7 @@ export default class InstancesEditor extends Component { return this.canvasArea.getBoundingClientRect(); } - zoomToFitContent() { + zoomToFitContent = () => { const { initialInstances } = this.props; if (initialInstances.getInstancesCount() === 0) return; @@ -954,9 +957,9 @@ export default class InstancesEditor extends Component { initialInstances.iterateOverInstances(getInstanceRectangle); getInstanceRectangle.delete(); if (contentAABB) this.fitViewToRectangle(contentAABB, { adaptZoom: true }); - } + }; - zoomToInitialPosition() { + zoomToInitialPosition = () => { const width = this.props.project.getGameResolutionWidth(); const height = this.props.project.getGameResolutionHeight(); const x = width / 2; @@ -965,9 +968,9 @@ export default class InstancesEditor extends Component { getRecommendedInitialZoomFactor(Math.max(height, width)) ); this.scrollTo(x, y); - } + }; - zoomToFitSelection(instances: Array) { + zoomToFitSelection = (instances: Array) => { if (instances.length === 0) return; const [firstInstance, ...otherInstances] = instances; const instanceMeasurer = this.instancesRenderer.getInstanceMeasurer(); @@ -981,9 +984,12 @@ export default class InstancesEditor extends Component { ); }); this.fitViewToRectangle(selectedInstancesRectangle, { adaptZoom: true }); - } + }; - centerViewOnLastInstance(instances: Array) { + centerViewOnLastInstance = ( + instances: Array, + offset?: ?[number, number] + ) => { if (instances.length === 0) return; const instanceMeasurer = this.instancesRenderer.getInstanceMeasurer(); @@ -992,7 +998,8 @@ export default class InstancesEditor extends Component { new Rectangle() ); this.fitViewToRectangle(lastInstanceRectangle, { adaptZoom: false }); - } + if (offset) this.scrollBy(offset[0], offset[1]); + }; getLastContextMenuSceneCoordinates = () => { return this.viewPosition.toSceneCoordinates( @@ -1008,7 +1015,7 @@ export default class InstancesEditor extends Component { ); }; - getViewPosition = () /*: ?ViewPosition */ => { + getViewPosition = (): ?ViewPosition => { return this.viewPosition; }; diff --git a/newIDE/app/src/ObjectsList/index.js b/newIDE/app/src/ObjectsList/index.js index a443c44fb..0dabec7c5 100644 --- a/newIDE/app/src/ObjectsList/index.js +++ b/newIDE/app/src/ObjectsList/index.js @@ -941,4 +941,4 @@ const areEqual = (prevProps: Props, nextProps: Props): boolean => prevProps.project === nextProps.project && prevProps.objectsContainer === nextProps.objectsContainer; -export default React.memo(ObjectsList, areEqual); +export default React.memo(ObjectsList, areEqual); diff --git a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js new file mode 100644 index 000000000..5430b1ef5 --- /dev/null +++ b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js @@ -0,0 +1,161 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; + +import { + type GroupWithContext, + type ObjectWithContext, +} from '../ObjectsList/EnumerateObjects'; +import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; +import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton'; +import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; +import { type InstancesEditorSettings } from '../InstancesEditor/InstancesEditorSettings'; +import InstancesSelection from '../InstancesEditor/InstancesSelection'; +import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog'; +import { type HistoryHandler } from '../VariablesList/VariablesList'; +import { type InstancesEditorShortcutsCallbacks } from '../InstancesEditor'; +import { type EditorId } from '.'; +import Rectangle from '../Utils/Rectangle'; +import ViewPosition from '../InstancesEditor/ViewPosition'; + +export type SceneEditorsDisplayProps = {| + project: gdProject, + layout: gdLayout, + initialInstances: gdInitialInstancesContainer, + instancesSelection: InstancesSelection, + selectedLayer: string, + onSelectInstances: ( + instances: Array, + multiSelect: boolean, + targetPosition?: 'center' | 'upperCenter' + ) => void, + editInstanceVariables: (instance: ?gdInitialInstance) => void, + editObjectByName: (objectName: string, initialTab?: ObjectEditorTab) => void, + onEditObject: gdObject => void, + selectedObjectNames: string[], + renamedObjectWithContext: ?ObjectWithContext, + onSelectLayer: (layerName: string) => void, + editLayerEffects: (layer: ?gdLayer) => void, + editLayer: (layer: ?gdLayer) => void, + onRemoveLayer: (layerName: string, done: (boolean) => void) => void, + onRenameLayer: ( + oldName: string, + newName: string, + done: (boolean) => void + ) => void, + onObjectCreated: gdObject => void, + onObjectSelected: (?ObjectWithContext) => void, + onExportObject: (object: ?gdObject) => void, + onDeleteObject: ( + i18n: I18nType, + objectWithContext: ObjectWithContext, + cb: (boolean) => void + ) => void, + onAddObjectInstance: ( + objectName: string, + targetPosition?: 'center' | 'upperCenter' + ) => void, + onRenameObjectStart: (?ObjectWithContext) => void, + onRenameObjectFinish: ( + objectWithContext: ObjectWithContext, + newName: string, + done: (boolean) => void + ) => void, + onEditObjectGroup: (?gdObjectGroup) => void, + onDeleteObjectGroup: ( + groupWithContext: GroupWithContext, + done: (boolean) => void + ) => void, + onRenameObjectGroup: ( + groupWithContext: GroupWithContext, + newName: string, + done: (boolean) => void + ) => void, + canRenameObjectGroup: ( + newName: string, + global: boolean, + i18n: I18nType + ) => boolean, + canObjectOrGroupBeGlobal: ( + i18n: I18nType, + objectOrGroupName: string + ) => boolean, + canObjectOrGroupUseNewName: ( + newName: string, + global: boolean, + i18n: I18nType + ) => boolean, + + updateBehaviorsSharedData: () => void, + onInstancesAdded: (Array) => void, + onInstancesSelected: (Array) => void, + onInstanceDoubleClicked: gdInitialInstance => void, + onInstancesMoved: (Array) => void, + onInstancesResized: (Array) => void, + onInstancesRotated: (Array) => void, + isInstanceOf3DObject: gdInitialInstance => boolean, + onSelectAllInstancesOfObjectInLayout: string => void, + + canInstallPrivateAsset: () => boolean, + + instancesEditorSettings: InstancesEditorSettings, + onInstancesEditorSettingsMutated: InstancesEditorSettings => void, + + historyHandler: HistoryHandler, + unsavedChanges?: ?UnsavedChanges, + hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, + onContextMenu: ( + x: number, + y: number, + ignoreSelectedObjectsForContextMenu?: boolean + ) => void, + + resourceManagementProps: ResourceManagementProps, + isActive: boolean, + instancesEditorShortcutsCallbacks: InstancesEditorShortcutsCallbacks, + + onOpenedEditorsChanged: () => void, +|}; + +export type SceneEditorsDisplayInterface = {| + getName: () => 'mosaic' | 'swipeableDrawer', + forceUpdateInstancesList: () => void, + forceUpdateInstancesPropertiesEditor: () => void, + forceUpdateObjectsList: () => void, + forceUpdateObjectGroupsList: () => void, + forceUpdateLayersList: () => void, + openNewObjectDialog: () => void, + toggleEditorView: (editorId: EditorId) => void, + isEditorVisible: (editorId: EditorId) => boolean, + viewControls: {| + zoomBy: (factor: number) => void, + setZoomFactor: (factor: number) => void, + zoomToInitialPosition: () => void, + zoomToFitContent: () => void, + zoomToFitSelection: (Array) => void, + centerViewOnLastInstance: ( + Array, + offset?: ?[number, number] + ) => void, + getLastCursorSceneCoordinates: () => [number, number], + getLastContextMenuSceneCoordinates: () => [number, number], + getViewPosition: () => ?ViewPosition, + |}, + instancesHandlers: {| + getSelectionAABB: () => Rectangle, + addInstances: ( + pos: [number, number], + objectNames: Array, + layer: string + ) => Array, + clearHighlightedInstance: () => void, + resetInstanceRenderersFor: (objectName: string) => void, + forceRemountInstancesRenderers: () => void, + addSerializedInstances: ({| + position: [number, number], + copyReferential: [number, number], + serializedInstances: Array, + preventSnapToGrid?: boolean, + addInstancesInTheForeground?: boolean, + |}) => Array, + |}, +|}; diff --git a/newIDE/app/src/SceneEditor/Toolbar.js b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/Toolbar.js similarity index 87% rename from newIDE/app/src/SceneEditor/Toolbar.js rename to newIDE/app/src/SceneEditor/MosaicEditorsDisplay/Toolbar.js index 855f0374c..95867a33c 100644 --- a/newIDE/app/src/SceneEditor/Toolbar.js +++ b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/Toolbar.js @@ -2,24 +2,24 @@ import { t } from '@lingui/macro'; import { type I18n as I18nType } from '@lingui/core'; import * as React from 'react'; -import { ToolbarGroup } from '../UI/Toolbar'; -import ToolbarSeparator from '../UI/ToolbarSeparator'; -import IconButton from '../UI/IconButton'; -import ElementWithMenu from '../UI/Menu/ElementWithMenu'; -import ToolbarCommands from './ToolbarCommands'; -import InstancesSelection from '../InstancesEditor/InstancesSelection'; -import { type MenuItemTemplate } from '../UI/Menu/Menu.flow'; -import ObjectIcon from '../UI/CustomSvgIcons/Object'; -import ObjectGroupIcon from '../UI/CustomSvgIcons/ObjectGroup'; -import EditIcon from '../UI/CustomSvgIcons/Edit'; -import InstancesListIcon from '../UI/CustomSvgIcons/InstancesList'; -import LayersIcon from '../UI/CustomSvgIcons/Layers'; -import UndoIcon from '../UI/CustomSvgIcons/Undo'; -import RedoIcon from '../UI/CustomSvgIcons/Redo'; -import TrashIcon from '../UI/CustomSvgIcons/Trash'; -import GridIcon from '../UI/CustomSvgIcons/Grid'; -import ZoomInIcon from '../UI/CustomSvgIcons/ZoomIn'; -import EditSceneIcon from '../UI/CustomSvgIcons/EditScene'; +import { ToolbarGroup } from '../../UI/Toolbar'; +import ToolbarSeparator from '../../UI/ToolbarSeparator'; +import IconButton from '../../UI/IconButton'; +import ElementWithMenu from '../../UI/Menu/ElementWithMenu'; +import ToolbarCommands from '../ToolbarCommands'; +import InstancesSelection from '../../InstancesEditor/InstancesSelection'; +import { type MenuItemTemplate } from '../../UI/Menu/Menu.flow'; +import ObjectIcon from '../../UI/CustomSvgIcons/Object'; +import ObjectGroupIcon from '../../UI/CustomSvgIcons/ObjectGroup'; +import EditIcon from '../../UI/CustomSvgIcons/Edit'; +import InstancesListIcon from '../../UI/CustomSvgIcons/InstancesList'; +import LayersIcon from '../../UI/CustomSvgIcons/Layers'; +import UndoIcon from '../../UI/CustomSvgIcons/Undo'; +import RedoIcon from '../../UI/CustomSvgIcons/Redo'; +import TrashIcon from '../../UI/CustomSvgIcons/Trash'; +import GridIcon from '../../UI/CustomSvgIcons/Grid'; +import ZoomInIcon from '../../UI/CustomSvgIcons/ZoomIn'; +import EditSceneIcon from '../../UI/CustomSvgIcons/EditScene'; type Props = {| toggleObjectsList: () => void, diff --git a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js new file mode 100644 index 000000000..06b2e5e18 --- /dev/null +++ b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js @@ -0,0 +1,422 @@ +// @flow + +import * as React from 'react'; +import { t } from '@lingui/macro'; +import { I18n } from '@lingui/react'; +import { type I18n as I18nType } from '@lingui/core'; + +import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer'; +import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext'; +import EditorMosaic from '../../UI/EditorMosaic'; +import InstancesEditor from '../../InstancesEditor'; +import InstancePropertiesEditor, { + type InstancePropertiesEditorInterface, +} from '../../InstancesEditor/InstancePropertiesEditor'; +import LayersList, { type LayersListInterface } from '../../LayersList'; +import FullSizeInstancesEditorWithScrollbars from '../../InstancesEditor/FullSizeInstancesEditorWithScrollbars'; +import TagsButton from '../../UI/EditorMosaic/TagsButton'; +import CloseButton from '../../UI/EditorMosaic/CloseButton'; +import ObjectsList, { type ObjectsListInterface } from '../../ObjectsList'; +import ObjectGroupsList from '../../ObjectGroupsList'; +import InstancesList from '../../InstancesEditor/InstancesList'; +import ObjectsRenderingService from '../../ObjectsRendering/ObjectsRenderingService'; + +import { + getTagsFromString, + buildTagsMenuTemplate, + type SelectedTags, +} from '../../Utils/TagsHelper'; +import { enumerateObjects } from '../../ObjectsList/EnumerateObjects'; +import Rectangle from '../../Utils/Rectangle'; +import { type EditorId } from '..'; +import { + type SceneEditorsDisplayProps, + type SceneEditorsDisplayInterface, +} from '../EditorsDisplay.flow'; + +const initialMosaicEditorNodes = { + direction: 'row', + first: 'properties', + splitPercentage: 23, + second: { + direction: 'row', + first: 'instances-editor', + second: 'objects-list', + splitPercentage: 77, + }, +}; + +const noop = () => {}; + +const defaultPanelConfigByEditor = { + 'objects-list': { + position: 'end', + splitPercentage: 75, + direction: 'column', + }, + properties: { + position: 'start', + splitPercentage: 25, + direction: 'column', + }, + 'object-groups-list': { + position: 'end', + splitPercentage: 75, + direction: 'column', + }, + 'instances-list': { + position: 'end', + splitPercentage: 75, + direction: 'row', + }, + 'layers-list': { + position: 'end', + splitPercentage: 75, + direction: 'row', + }, +}; + +// Forward ref to allow Scene editor to force update some editors +const MosaicEditorsDisplay = React.forwardRef< + SceneEditorsDisplayProps, + SceneEditorsDisplayInterface +>((props, ref) => { + const { + project, + layout, + initialInstances, + selectedLayer, + onSelectInstances, + } = props; + const windowWidth = useResponsiveWindowWidth(); + const { + getDefaultEditorMosaicNode, + setDefaultEditorMosaicNode, + } = React.useContext(PreferencesContext); + const selectedInstances = props.instancesSelection.getSelectedInstances(); + const [ + selectedObjectTags, + setSelectedObjectTags, + ] = React.useState([]); + + const instancesPropertiesEditorRef = React.useRef( + null + ); + const layersListRef = React.useRef(null); + const instancesListRef = React.useRef(null); + const editorRef = React.useRef(null); + const objectsListRef = React.useRef(null); + const editorMosaicRef = React.useRef(null); + const objectGroupsListRef = React.useRef(null); + + const forceUpdateInstancesPropertiesEditor = React.useCallback(() => { + if (instancesPropertiesEditorRef.current) + instancesPropertiesEditorRef.current.forceUpdate(); + }, []); + const forceUpdateInstancesList = React.useCallback(() => { + if (instancesListRef.current) instancesListRef.current.forceUpdate(); + }, []); + const forceUpdateObjectsList = React.useCallback(() => { + if (objectsListRef.current) objectsListRef.current.forceUpdateList(); + }, []); + const forceUpdateObjectGroupsList = React.useCallback(() => { + if (objectGroupsListRef.current) objectGroupsListRef.current.forceUpdate(); + }, []); + const forceUpdateLayersList = React.useCallback(() => { + if (layersListRef.current) layersListRef.current.forceUpdate(); + }, []); + const getInstanceSize = React.useCallback((instance: gdInitialInstance) => { + if (!editorRef.current) return [0, 0, 0]; + + return editorRef.current.getInstanceSize(instance); + }, []); + const openNewObjectDialog = React.useCallback(() => { + if (!objectsListRef.current) return; + + objectsListRef.current.openNewObjectDialog(); + }, []); + const toggleEditorView = React.useCallback((editorId: EditorId) => { + if (!editorMosaicRef.current) return; + const config = defaultPanelConfigByEditor[editorId]; + editorMosaicRef.current.toggleEditor( + editorId, + config.position, + config.splitPercentage, + config.direction + ); + }, []); + const isEditorVisible = React.useCallback((editorId: EditorId) => { + if (!editorMosaicRef.current) return false; + return editorMosaicRef.current.getOpenedEditorNames().includes(editorId); + }, []); + + React.useImperativeHandle(ref, () => { + const { current: editor } = editorRef; + return { + getName: () => 'mosaic', + forceUpdateInstancesList, + forceUpdateInstancesPropertiesEditor, + forceUpdateObjectsList, + forceUpdateObjectGroupsList, + forceUpdateLayersList, + openNewObjectDialog, + toggleEditorView, + isEditorVisible, + viewControls: { + zoomBy: editor ? editor.zoomBy : noop, + setZoomFactor: editor ? editor.setZoomFactor : noop, + zoomToInitialPosition: editor ? editor.zoomToInitialPosition : noop, + zoomToFitContent: editor ? editor.zoomToFitContent : noop, + zoomToFitSelection: editor ? editor.zoomToFitSelection : noop, + centerViewOnLastInstance: editor + ? editor.centerViewOnLastInstance + : noop, + getLastCursorSceneCoordinates: editor + ? editor.getLastCursorSceneCoordinates + : () => [0, 0], + getLastContextMenuSceneCoordinates: editor + ? editor.getLastContextMenuSceneCoordinates + : () => [0, 0], + getViewPosition: editor ? editor.getViewPosition : noop, + }, + instancesHandlers: { + getSelectionAABB: editor + ? editor.selectedInstances.getSelectionAABB + : () => new Rectangle(), + addInstances: editor ? editor.addInstances : () => [], + clearHighlightedInstance: editor + ? editor.clearHighlightedInstance + : noop, + resetInstanceRenderersFor: editor + ? editor.resetInstanceRenderersFor + : noop, + forceRemountInstancesRenderers: editor ? editor.forceRemount : noop, + addSerializedInstances: editor + ? editor.addSerializedInstances + : () => [], + }, + }; + }); + + const selectInstances = React.useCallback( + (instances: Array, multiSelect: boolean) => { + onSelectInstances(instances, multiSelect); + forceUpdateInstancesList(); + forceUpdateInstancesPropertiesEditor(); + }, + [ + forceUpdateInstancesList, + forceUpdateInstancesPropertiesEditor, + onSelectInstances, + ] + ); + + const getAllObjectTags = React.useCallback( + (): Array => { + const tagsSet: Set = new Set(); + enumerateObjects(project, layout).allObjectsList.forEach(({ object }) => { + getTagsFromString(object.getTags()).forEach(tag => tagsSet.add(tag)); + }); + + return Array.from(tagsSet); + }, + [project, layout] + ); + + const buildObjectTagsMenuTemplate = React.useCallback( + (i18n: I18nType): Array => { + return buildTagsMenuTemplate({ + noTagLabel: i18n._(t`No tags - add a tag to an object first`), + getAllTags: getAllObjectTags, + selectedTags: selectedObjectTags, + onChange: setSelectedObjectTags, + }); + }, + [selectedObjectTags, getAllObjectTags] + ); + + const editors = { + properties: { + type: 'secondary', + title: t`Properties`, + renderEditor: () => ( + + {({ i18n }) => ( + + )} + + ), + }, + 'layers-list': { + type: 'secondary', + title: t`Layers`, + renderEditor: () => ( + + ), + }, + 'instances-list': { + type: 'secondary', + title: t`Instances List`, + renderEditor: () => ( + + ), + }, + 'instances-editor': { + type: 'primary', + noTitleBar: true, + renderEditor: () => ( + { + editorRef.current = editor; + }} + pauseRendering={!props.isActive} + /> + ), + }, + 'objects-list': { + type: 'secondary', + title: t`Objects`, + toolbarControls: [ + , + , + ], + renderEditor: () => ( + + {({ i18n }) => ( + + props.onDeleteObject(i18n, objectWithContext, cb) + } + canRenameObject={(newName, global) => + props.canObjectOrGroupUseNewName(newName, global, i18n) + } + onObjectCreated={props.onObjectCreated} + onObjectSelected={props.onObjectSelected} + renamedObjectWithContext={props.renamedObjectWithContext} + onRenameObjectStart={props.onRenameObjectStart} + onRenameObjectFinish={props.onRenameObjectFinish} + onAddObjectInstance={props.onAddObjectInstance} + onObjectPasted={props.updateBehaviorsSharedData} + selectedObjectTags={selectedObjectTags} + beforeSetAsGlobalObject={objectName => + props.canObjectOrGroupBeGlobal(i18n, objectName) + } + onChangeSelectedObjectTags={setSelectedObjectTags} + getAllObjectTags={getAllObjectTags} + ref={objectsListRef} + unsavedChanges={props.unsavedChanges} + hotReloadPreviewButtonProps={props.hotReloadPreviewButtonProps} + /> + )} + + ), + }, + 'object-groups-list': { + type: 'secondary', + title: t`Object Groups`, + renderEditor: () => ( + + {({ i18n }) => ( + + props.canRenameObjectGroup(newName, global, i18n) + } + beforeSetAsGlobalGroup={groupName => + props.canObjectOrGroupBeGlobal(i18n, groupName) + } + unsavedChanges={props.unsavedChanges} + /> + )} + + ), + }, + }; + return ( + setDefaultEditorMosaicNode('scene-editor', node)} + ref={editorMosaicRef} + /> + ); +}); + +export default MosaicEditorsDisplay; diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/BottomToolbar.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/BottomToolbar.js new file mode 100644 index 000000000..16729b30b --- /dev/null +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/BottomToolbar.js @@ -0,0 +1,74 @@ +// @flow + +import * as React from 'react'; +import { Toolbar, ToolbarGroup } from '../../UI/Toolbar'; +import ObjectIcon from '../../UI/CustomSvgIcons/Object'; +import ObjectGroupIcon from '../../UI/CustomSvgIcons/ObjectGroup'; +import EditIcon from '../../UI/CustomSvgIcons/Edit'; +import InstancesListIcon from '../../UI/CustomSvgIcons/InstancesList'; +import LayersIcon from '../../UI/CustomSvgIcons/Layers'; +import IconButton from '../../UI/IconButton'; +import { type EditorId } from '..'; +import Paper from '../../UI/Paper'; + +const styles = { container: { padding: 4, paddingBottom: 8 } }; + +type Props = {| + selectedEditorId: ?EditorId, + onSelectEditor: EditorId => void, +|}; + +const editors = { + 'objects-list': { + buttonId: 'toolbar-open-objects-panel-button', + icon: , + }, + 'object-groups-list': { + buttonId: 'toolbar-open-object-groups-panel-button', + icon: , + }, + properties: { + buttonId: 'toolbar-open-properties-panel-button', + icon: , + }, + 'instances-list': { + buttonId: 'toolbar-open-instances-list-panel-button', + icon: , + }, + 'layers-list': { + buttonId: 'toolbar-open-layers-panel-button', + icon: , + }, +}; + +const BottomToolbar = (props: Props) => { + return ( + + + + {Object.keys(editors).map(editorId => { + const { icon, buttonId } = editors[editorId]; + const isSelected = props.selectedEditorId === editorId; + return ( + { + props.onSelectEditor(editorId); + }} + selected={isSelected} + > + {icon} + + ); + })} + + + + ); +}; + +export default BottomToolbar; diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/SwipeableDrawer.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/SwipeableDrawer.js new file mode 100644 index 000000000..452c0e93c --- /dev/null +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/SwipeableDrawer.js @@ -0,0 +1,131 @@ +// @flow + +import * as React from 'react'; +import Text from '../../UI/Text'; +import { Column, Line } from '../../UI/Grid'; +import { getBackgroundColor } from '../../UI/Paper'; +import { ColumnStackLayout } from '../../UI/Layout'; +import GDevelopThemeContext from '../../UI/Theme/GDevelopThemeContext'; +import useSwipeGesture from './UseSwipeGesture'; + +const topMargin = 52; // This is equal to the height of the bottom bar. + +const styles = { + container: { + flexDirection: 'column', + overflow: 'hidden', + }, + childrenContainer: { + display: 'flex', + flex: 1, + overflowY: 'auto', + overflowX: 'hidden', + }, + topBarContainer: { + borderRadius: '8px 8px 0 0', + padding: 4, + }, + topBarHandleContainer: { + height: 4, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + }, + topBarHandle: { + height: 4, + width: '40%', + borderRadius: 2, + }, +}; + +type SwipeableDrawerTopBarProps = {| + title: React.Node, + onClick: () => void, + onSwipeUp: () => void, + onSwipeDown: () => void, + controls: ?React.Node, +|}; + +const SwipeableDrawerTopBar = (props: SwipeableDrawerTopBarProps) => { + const gdevelopTheme = React.useContext(GDevelopThemeContext); + const { onTouchStart, onTouchEnd } = useSwipeGesture({ + onSwipeUp: props.onSwipeUp, + onSwipeDown: props.onSwipeDown, + }); + + return ( +
+ +
+ +
+ + + + {props.title} + + {props.controls} + + +
+
+ ); +}; + +type DrawerOpeningState = 'closed' | 'halfOpen' | 'open'; + +type Props = {| + maxHeight: number, + children: React.Node, + title: React.Node, + openingState: DrawerOpeningState, + setOpeningState: DrawerOpeningState => void, + topBarControls: ?React.Node, +|}; + +const SwipeableDrawer = (props: Props) => { + const { openingState, setOpeningState } = props; + const height = + openingState === 'closed' + ? 0 + : openingState === 'halfOpen' + ? props.maxHeight * 0.42 // Empirical value that leaves space in both editor and canvas. + : props.maxHeight - topMargin; + const display = openingState === 'closed' ? 'none' : 'flex'; + return ( +
+ setOpeningState('closed')} + onSwipeUp={() => { + if (openingState === 'halfOpen') setOpeningState('open'); + }} + onSwipeDown={() => { + if (openingState === 'halfOpen') setOpeningState('closed'); + else if (openingState === 'open') setOpeningState('halfOpen'); + }} + controls={props.topBarControls} + /> +
{props.children}
+
+ ); +}; + +export default SwipeableDrawer; diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/Toolbar.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/Toolbar.js new file mode 100644 index 000000000..b5e3df0e1 --- /dev/null +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/Toolbar.js @@ -0,0 +1,163 @@ +// @flow +import { t } from '@lingui/macro'; +import { type I18n as I18nType } from '@lingui/core'; +import * as React from 'react'; +import { ToolbarGroup } from '../../UI/Toolbar'; +import ToolbarSeparator from '../../UI/ToolbarSeparator'; +import IconButton from '../../UI/IconButton'; +import ElementWithMenu from '../../UI/Menu/ElementWithMenu'; +import ToolbarCommands from '../ToolbarCommands'; +import InstancesSelection from '../../InstancesEditor/InstancesSelection'; +import { type MenuItemTemplate } from '../../UI/Menu/Menu.flow'; +import UndoIcon from '../../UI/CustomSvgIcons/Undo'; +import RedoIcon from '../../UI/CustomSvgIcons/Redo'; +import TrashIcon from '../../UI/CustomSvgIcons/Trash'; +import GridIcon from '../../UI/CustomSvgIcons/Grid'; +import ZoomInIcon from '../../UI/CustomSvgIcons/ZoomIn'; +import EditSceneIcon from '../../UI/CustomSvgIcons/EditScene'; + +type Props = {| + toggleObjectsList: () => void, + toggleObjectGroupsList: () => void, + toggleProperties: () => void, + toggleInstancesList: () => void, + toggleLayersList: () => void, + undo: () => void, + canUndo: boolean, + redo: () => void, + canRedo: boolean, + deleteSelection: () => void, + instancesSelection: InstancesSelection, + isWindowMaskShown: () => boolean, + toggleWindowMask: () => void, + isGridShown: () => boolean, + toggleGrid: () => void, + openSetupGrid: () => void, + getContextMenuZoomItems: I18nType => Array, + setZoomFactor: number => void, + onOpenSettings: () => void, + settingsIcon: React.Node, + canRenameObject: boolean, + onRenameObject: () => void, +|}; + +const Toolbar = (props: Props) => { + return ( + <> + + + + + + + + + + + } + buildMenuTemplate={(i18n: I18nType) => [ + ...props.getContextMenuZoomItems(i18n), + { type: 'separator' }, + { label: '5%', click: () => props.setZoomFactor(0.05) }, + { label: '10%', click: () => props.setZoomFactor(0.1) }, + { label: '25%', click: () => props.setZoomFactor(0.25) }, + { label: '50%', click: () => props.setZoomFactor(0.5) }, + { label: '100%', click: () => props.setZoomFactor(1.0) }, + { label: '150%', click: () => props.setZoomFactor(1.5) }, + { label: '200%', click: () => props.setZoomFactor(2.0) }, + { label: '400%', click: () => props.setZoomFactor(4.0) }, + ]} + /> + + + + + + + + + } + buildMenuTemplate={(i18n: I18nType) => [ + { + type: 'checkbox', + label: i18n._(t`Show Mask`), + checked: props.isWindowMaskShown(), + click: () => props.toggleWindowMask(), + }, + { + type: 'checkbox', + label: i18n._(t`Show grid`), + checked: props.isGridShown(), + click: () => props.toggleGrid(), + }, + { type: 'separator' }, + { + label: i18n._(t`Setup grid`), + click: () => props.openSetupGrid(), + }, + ]} + /> + + + {props.settingsIcon || } + + + + ); +}; + +export default Toolbar; diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/UseSwipeGesture.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/UseSwipeGesture.js new file mode 100644 index 000000000..4ac61e418 --- /dev/null +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/UseSwipeGesture.js @@ -0,0 +1,50 @@ +// @flow + +import * as React from 'react'; + +type Props = {| + onSwipeUp: () => void, + onSwipeDown: () => void, +|}; + +const minMovement = 30; // px +const minSpeed = 200; // px/s + +const useSwipeGesture = (props: Props) => { + const startTimeRef = React.useRef(null); + const startYRef = React.useRef(null); + + const onTouchStart = React.useCallback((event: TouchEvent) => { + startTimeRef.current = Date.now(); + startYRef.current = event.touches[0].clientY; + }, []); + + const onTouchEnd = React.useCallback( + (event: TouchEvent) => { + if (!startYRef.current || !startTimeRef.current) return; + const { current: startY } = startYRef; + const { current: startTime } = startTimeRef; + + const deltaY = event.changedTouches[0].clientY - startY; + const deltaTimeInSeconds = (Date.now() - startTime) / 1000; + if ( + Math.abs(deltaY) > minMovement && + Math.abs(deltaY) / deltaTimeInSeconds > minSpeed + ) { + if (deltaY < 0) props.onSwipeUp(); + else props.onSwipeDown(); + } + + startTimeRef.current = null; + startYRef.current = null; + }, + [props] + ); + + return { + onTouchStart, + onTouchEnd, + }; +}; + +export default useSwipeGesture; diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js new file mode 100644 index 000000000..9fda0afcf --- /dev/null +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js @@ -0,0 +1,413 @@ +// @flow + +import * as React from 'react'; +import { Trans, t } from '@lingui/macro'; +import { I18n } from '@lingui/react'; +import { type I18n as I18nType } from '@lingui/core'; + +import InstancesEditor from '../../InstancesEditor'; +import InstancePropertiesEditor, { + type InstancePropertiesEditorInterface, +} from '../../InstancesEditor/InstancePropertiesEditor'; +import LayersList, { type LayersListInterface } from '../../LayersList'; +import TagsButton from '../../UI/EditorMosaic/TagsButton'; +import ObjectsList, { type ObjectsListInterface } from '../../ObjectsList'; +import ObjectGroupsList from '../../ObjectGroupsList'; +import InstancesList from '../../InstancesEditor/InstancesList'; +import ObjectsRenderingService from '../../ObjectsRendering/ObjectsRenderingService'; + +import { + getTagsFromString, + buildTagsMenuTemplate, + type SelectedTags, +} from '../../Utils/TagsHelper'; +import { enumerateObjects } from '../../ObjectsList/EnumerateObjects'; +import Rectangle from '../../Utils/Rectangle'; +import SwipeableDrawer from './SwipeableDrawer'; +import BottomToolbar from './BottomToolbar'; +import { FullSizeMeasurer } from '../../UI/FullSizeMeasurer'; +import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext'; +import { useScreenType } from '../../UI/Reponsive/ScreenTypeMeasurer'; +import Paper from '../../UI/Paper'; +import { type EditorId } from '..'; +import { + type SceneEditorsDisplayInterface, + type SceneEditorsDisplayProps, +} from '../EditorsDisplay.flow'; + +const editorTitleById = { + 'objects-list': Objects, + properties: Instance properties, + 'object-groups-list': Objects groups, + 'instances-list': Instances, + 'layers-list': Layers, +}; + +const noop = () => {}; + +const styles = { + container: { width: '100%' }, + bottomContainer: { position: 'absolute', bottom: 0, width: '100%' }, + instancesListContainer: { display: 'flex', flex: 1 }, +}; + +// Forward ref to allow Scene editor to force update some editors +const SwipeableDrawerEditorsDisplay = React.forwardRef< + SceneEditorsDisplayProps, + SceneEditorsDisplayInterface +>((props, ref) => { + const { + project, + layout, + initialInstances, + selectedLayer, + onSelectInstances, + } = props; + const selectedInstances = props.instancesSelection.getSelectedInstances(); + const [ + selectedObjectTags, + setSelectedObjectTags, + ] = React.useState([]); + const { values } = React.useContext(PreferencesContext); + const screenType = useScreenType(); + + const instancesPropertiesEditorRef = React.useRef( + null + ); + const layersListRef = React.useRef(null); + const instancesListRef = React.useRef(null); + const editorRef = React.useRef(null); + const objectsListRef = React.useRef(null); + const objectGroupsListRef = React.useRef(null); + + const [selectedEditorId, setSelectedEditorId] = React.useState( + null + ); + + const [drawerOpeningState, setDrawerOpeningState] = React.useState< + 'closed' | 'halfOpen' | 'open' + >('closed'); + + const halfOpenOrCloseDrawerOnEditor = React.useCallback( + (editorId: ?EditorId) => { + if (selectedEditorId === editorId) { + if (drawerOpeningState === 'closed') { + setDrawerOpeningState('halfOpen'); + } else { + setDrawerOpeningState('closed'); + } + } else { + setSelectedEditorId(editorId || null); + if (drawerOpeningState === 'closed') setDrawerOpeningState('halfOpen'); + } + }, + [selectedEditorId, drawerOpeningState] + ); + + const forceUpdateInstancesPropertiesEditor = React.useCallback(() => { + if (instancesPropertiesEditorRef.current) + instancesPropertiesEditorRef.current.forceUpdate(); + }, []); + const forceUpdateInstancesList = React.useCallback(() => { + if (instancesListRef.current) instancesListRef.current.forceUpdate(); + }, []); + const forceUpdateObjectsList = React.useCallback(() => { + if (objectsListRef.current) objectsListRef.current.forceUpdateList(); + }, []); + const forceUpdateObjectGroupsList = React.useCallback(() => { + if (objectGroupsListRef.current) objectGroupsListRef.current.forceUpdate(); + }, []); + const forceUpdateLayersList = React.useCallback(() => { + if (layersListRef.current) layersListRef.current.forceUpdate(); + }, []); + const getInstanceSize = React.useCallback((instance: gdInitialInstance) => { + if (!editorRef.current) return [0, 0, 0]; + + return editorRef.current.getInstanceSize(instance); + }, []); + const openNewObjectDialog = React.useCallback(() => { + if (!objectsListRef.current) return; + + objectsListRef.current.openNewObjectDialog(); + }, []); + const isEditorVisible = React.useCallback( + (editorId: EditorId) => { + return editorId === selectedEditorId && drawerOpeningState !== 'closed'; + }, + [selectedEditorId, drawerOpeningState] + ); + + React.useImperativeHandle(ref, () => { + const { current: editor } = editorRef; + + return { + getName: () => 'swipeableDrawer', + forceUpdateInstancesList, + forceUpdateInstancesPropertiesEditor, + forceUpdateObjectsList, + forceUpdateObjectGroupsList, + forceUpdateLayersList, + openNewObjectDialog, + toggleEditorView: halfOpenOrCloseDrawerOnEditor, + isEditorVisible, + viewControls: { + zoomBy: editor ? editor.zoomBy : noop, + setZoomFactor: editor ? editor.setZoomFactor : noop, + zoomToInitialPosition: editor ? editor.zoomToInitialPosition : noop, + zoomToFitContent: editor ? editor.zoomToFitContent : noop, + zoomToFitSelection: editor ? editor.zoomToFitSelection : noop, + centerViewOnLastInstance: editor + ? editor.centerViewOnLastInstance + : noop, + getLastCursorSceneCoordinates: editor + ? editor.getLastCursorSceneCoordinates + : () => [0, 0], + getLastContextMenuSceneCoordinates: editor + ? editor.getLastContextMenuSceneCoordinates + : () => [0, 0], + getViewPosition: editor ? editor.getViewPosition : noop, + }, + instancesHandlers: { + getSelectionAABB: editor + ? editor.selectedInstances.getSelectionAABB + : () => new Rectangle(), + addInstances: editor ? editor.addInstances : () => [], + clearHighlightedInstance: editor + ? editor.clearHighlightedInstance + : noop, + resetInstanceRenderersFor: editor + ? editor.resetInstanceRenderersFor + : noop, + forceRemountInstancesRenderers: editor ? editor.forceRemount : noop, + addSerializedInstances: editor + ? editor.addSerializedInstances + : () => [], + }, + }; + }); + + const selectInstances = React.useCallback( + (instances: Array, multiSelect: boolean) => { + onSelectInstances(instances, multiSelect, 'upperCenter'); + forceUpdateInstancesList(); + forceUpdateInstancesPropertiesEditor(); + }, + [ + forceUpdateInstancesList, + forceUpdateInstancesPropertiesEditor, + onSelectInstances, + ] + ); + + const getAllObjectTags = React.useCallback( + (): Array => { + const tagsSet: Set = new Set(); + enumerateObjects(project, layout).allObjectsList.forEach(({ object }) => { + getTagsFromString(object.getTags()).forEach(tag => tagsSet.add(tag)); + }); + + return Array.from(tagsSet); + }, + [project, layout] + ); + + const buildObjectTagsMenuTemplate = React.useCallback( + (i18n: I18nType): Array => { + return buildTagsMenuTemplate({ + noTagLabel: i18n._(t`No tags - add a tag to an object first`), + getAllTags: getAllObjectTags, + selectedTags: selectedObjectTags, + onChange: setSelectedObjectTags, + }); + }, + [selectedObjectTags, getAllObjectTags] + ); + + return ( + + {({ width, height }) => ( +
+ +
+ , + ] + : null + } + > + {selectedEditorId === 'objects-list' && ( + + {({ i18n }) => ( + + props.onDeleteObject(i18n, objectWithContext, cb) + } + canRenameObject={(newName, global) => + props.canObjectOrGroupUseNewName(newName, global, i18n) + } + onObjectCreated={props.onObjectCreated} + onObjectSelected={props.onObjectSelected} + renamedObjectWithContext={props.renamedObjectWithContext} + onRenameObjectStart={props.onRenameObjectStart} + onRenameObjectFinish={props.onRenameObjectFinish} + onAddObjectInstance={objectName => + props.onAddObjectInstance(objectName, 'upperCenter') + } + onObjectPasted={props.updateBehaviorsSharedData} + selectedObjectTags={selectedObjectTags} + beforeSetAsGlobalObject={objectName => + props.canObjectOrGroupBeGlobal(i18n, objectName) + } + onChangeSelectedObjectTags={setSelectedObjectTags} + getAllObjectTags={getAllObjectTags} + ref={objectsListRef} + unsavedChanges={props.unsavedChanges} + hotReloadPreviewButtonProps={ + props.hotReloadPreviewButtonProps + } + /> + )} + + )} + {selectedEditorId === 'properties' && ( + + {({ i18n }) => ( + + )} + + )} + {selectedEditorId === 'object-groups-list' && ( + + {({ i18n }) => ( + + props.canRenameObjectGroup(newName, global, i18n) + } + beforeSetAsGlobalGroup={groupName => + props.canObjectOrGroupBeGlobal(i18n, groupName) + } + unsavedChanges={props.unsavedChanges} + /> + )} + + )} + {selectedEditorId === 'instances-list' && ( + + + + )} + {selectedEditorId === 'layers-list' && ( + + )} + + +
+
+ )} +
+ ); +}); + +export default SwipeableDrawerEditorsDisplay; diff --git a/newIDE/app/src/SceneEditor/index.js b/newIDE/app/src/SceneEditor/index.js index d1902079d..ed1106251 100644 --- a/newIDE/app/src/SceneEditor/index.js +++ b/newIDE/app/src/SceneEditor/index.js @@ -6,15 +6,6 @@ import { t } from '@lingui/macro'; import * as React from 'react'; import uniq from 'lodash/uniq'; -import ObjectsList, { type ObjectsListInterface } from '../ObjectsList'; -import ObjectGroupsList from '../ObjectGroupsList'; -import ObjectsRenderingService from '../ObjectsRendering/ObjectsRenderingService'; -import InstancesEditor from '../InstancesEditor'; -import InstancePropertiesEditor, { - type InstancePropertiesEditorInterface, -} from '../InstancesEditor/InstancePropertiesEditor'; -import InstancesList from '../InstancesEditor/InstancesList'; -import LayersList, { type LayersListInterface } from '../LayersList'; import LayerRemoveDialog from '../LayersList/LayerRemoveDialog'; import LayerEditorDialog from '../LayersList/LayerEditorDialog'; import VariablesEditorDialog from '../VariablesList/VariablesEditorDialog'; @@ -25,12 +16,12 @@ import InstancesSelection from '../InstancesEditor/InstancesSelection'; import SetupGridDialog from './SetupGridDialog'; import ScenePropertiesDialog from './ScenePropertiesDialog'; import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog'; -import Toolbar from './Toolbar'; +import MosaicEditorsDisplayToolbar from './MosaicEditorsDisplay/Toolbar'; +import SwipeableDrawerEditorsDisplayToolbar from './SwipeableDrawerEditorsDisplay/Toolbar'; import { serializeToJSObject } from '../Utils/Serializer'; import Clipboard, { SafeExtractor } from '../Utils/Clipboard'; import Window from '../Utils/Window'; -import FullSizeInstancesEditorWithScrollbars from '../InstancesEditor/FullSizeInstancesEditorWithScrollbars'; -import EditorMosaic from '../UI/EditorMosaic'; +import { ResponsiveWindowMeasurer } from '../UI/Reponsive/ResponsiveWindowMeasurer'; import DismissableInfoBar from '../UI/Messages/DismissableInfoBar'; import ContextMenu, { type ContextMenuInterface } from '../UI/Menu/ContextMenu'; import { showWarningBox } from '../UI/Messages/MessageBox'; @@ -55,18 +46,10 @@ import { type GroupWithContext, enumerateObjects, } from '../ObjectsList/EnumerateObjects'; -import TagsButton from '../UI/EditorMosaic/TagsButton'; -import CloseButton from '../UI/EditorMosaic/CloseButton'; import InfoBar from '../UI/Messages/InfoBar'; -import { - type SelectedTags, - buildTagsMenuTemplate, - getTagsFromString, -} from '../Utils/TagsHelper'; -import { ResponsiveWindowMeasurer } from '../UI/Reponsive/ResponsiveWindowMeasurer'; +import { type SelectedTags } from '../Utils/TagsHelper'; import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext'; import SceneVariablesDialog from './SceneVariablesDialog'; -import PreferencesContext from '../MainFrame/Preferences/PreferencesContext'; import { onObjectAdded, onInstanceAdded } from '../Hints/ObjectsAdditionalWork'; import { type InfoBarDetails } from '../Hints/ObjectsAdditionalWork'; import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton'; @@ -76,12 +59,22 @@ import { getInstancesInLayoutForObject } from '../Utils/Layout'; import { zoomInFactor, zoomOutFactor } from '../Utils/ZoomUtils'; import debounce from 'lodash/debounce'; import { mapFor } from '../Utils/MapFor'; +import MosaicEditorsDisplay from './MosaicEditorsDisplay'; +import SwipeableDrawerEditorsDisplay from './SwipeableDrawerEditorsDisplay'; +import { type SceneEditorsDisplayInterface } from './EditorsDisplay.flow'; const gd: libGDevelop = global.gd; const BASE_LAYER_NAME = ''; const INSTANCES_CLIPBOARD_KIND = 'Instances'; +export type EditorId = + | 'objects-list' + | 'properties' + | 'object-groups-list' + | 'instances-list' + | 'layers-list'; + const styles = { container: { display: 'flex', @@ -91,25 +84,6 @@ const styles = { }, }; -const initialMosaicEditorNodes = { - direction: 'row', - first: 'properties', - splitPercentage: 23, - second: { - direction: 'row', - first: 'instances-editor', - second: 'objects-list', - splitPercentage: 77, - }, -}; - -const getInitialMosaicEditorNodesSmallWindow = () => ({ - direction: Window.getOrientation() === 'portrait' ? 'column' : 'row', - first: 'instances-editor', - second: 'objects-list', - splitPercentage: 70, -}); - type Props = {| initialInstances: gdInitialInstancesContainer, getInitialInstancesEditorSettings: () => InstancesEditorSettings, @@ -149,7 +123,7 @@ type State = {| editedGroup: ?gdObjectGroup, - instancesEditorSettings: Object, + instancesEditorSettings: InstancesEditorSettings, history: HistoryState, layoutVariablesDialogOpen: boolean, @@ -175,14 +149,8 @@ export default class SceneEditor extends React.Component { }; instancesSelection: InstancesSelection; - editor: ?InstancesEditor; contextMenu: ?ContextMenuInterface; - editorMosaic: ?EditorMosaic; - _objectGroupsList: ?ObjectGroupsList; - _objectsList: ?ObjectsListInterface; - _layersList: ?LayersListInterface; - _propertiesEditor: ?InstancePropertiesEditorInterface; - _instancesList: ?InstancesList; + editorDisplay: ?SceneEditorsDisplayInterface; constructor(props: Props) { super(props); @@ -237,44 +205,75 @@ export default class SceneEditor extends React.Component { } updateToolbar = () => { - const openedEditorNames = this.editorMosaic - ? this.editorMosaic.getOpenedEditorNames() - : []; - this.props.setToolbar( - - !!this.state.instancesEditorSettings.windowMask - } - toggleGrid={this.toggleGrid} - isGridShown={() => !!this.state.instancesEditorSettings.grid} - openSetupGrid={this.openSetupGrid} - setZoomFactor={this.setZoomFactor} - getContextMenuZoomItems={this.getContextMenuZoomItems} - canUndo={canUndo(this.state.history)} - canRedo={canRedo(this.state.history)} - undo={this.undo} - redo={this.redo} - onOpenSettings={this.openSceneProperties} - settingsIcon={} - canRenameObject={this.state.selectedObjectsWithContext.length === 1} - onRenameObject={this._startRenamingSelectedObject} - /> - ); + const { editorDisplay } = this; + if (!editorDisplay) return; + + if (editorDisplay.getName() === 'mosaic') { + this.props.setToolbar( + + !!this.state.instancesEditorSettings.windowMask + } + toggleGrid={this.toggleGrid} + isGridShown={() => !!this.state.instancesEditorSettings.grid} + openSetupGrid={this.openSetupGrid} + setZoomFactor={this.setZoomFactor} + getContextMenuZoomItems={this.getContextMenuZoomItems} + canUndo={canUndo(this.state.history)} + canRedo={canRedo(this.state.history)} + undo={this.undo} + redo={this.redo} + onOpenSettings={this.openSceneProperties} + settingsIcon={} + canRenameObject={this.state.selectedObjectsWithContext.length === 1} + onRenameObject={this._startRenamingSelectedObject} + /> + ); + } else { + this.props.setToolbar( + + !!this.state.instancesEditorSettings.windowMask + } + toggleGrid={this.toggleGrid} + isGridShown={() => !!this.state.instancesEditorSettings.grid} + openSetupGrid={this.openSetupGrid} + setZoomFactor={this.setZoomFactor} + getContextMenuZoomItems={this.getContextMenuZoomItems} + canUndo={canUndo(this.state.history)} + canRedo={canRedo(this.state.history)} + undo={this.undo} + redo={this.redo} + onOpenSettings={this.openSceneProperties} + settingsIcon={} + canRenameObject={this.state.selectedObjectsWithContext.length === 1} + onRenameObject={this._startRenamingSelectedObject} + /> + ); + } }; // To be updated, see https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops. @@ -292,28 +291,28 @@ export default class SceneEditor extends React.Component { } toggleObjectsList = () => { - if (!this.editorMosaic) return; - this.editorMosaic.toggleEditor('objects-list', 'end', 75, 'column'); + if (!this.editorDisplay) return; + this.editorDisplay.toggleEditorView('objects-list'); }; toggleProperties = () => { - if (!this.editorMosaic) return; - this.editorMosaic.toggleEditor('properties', 'start', 25, 'column'); + if (!this.editorDisplay) return; + this.editorDisplay.toggleEditorView('properties'); }; toggleObjectGroupsList = () => { - if (!this.editorMosaic) return; - this.editorMosaic.toggleEditor('object-groups-list', 'end', 75, 'column'); + if (!this.editorDisplay) return; + this.editorDisplay.toggleEditorView('object-groups-list'); }; toggleInstancesList = () => { - if (!this.editorMosaic) return; - this.editorMosaic.toggleEditor('instances-list', 'end', 75, 'row'); + if (!this.editorDisplay) return; + this.editorDisplay.toggleEditorView('instances-list'); }; toggleLayersList = () => { - if (!this.editorMosaic) return; - this.editorMosaic.toggleEditor('layers-list', 'end', 75, 'row'); + if (!this.editorDisplay) return; + this.editorDisplay.toggleEditorView('layers-list'); }; toggleWindowMask = () => { @@ -388,15 +387,9 @@ export default class SceneEditor extends React.Component { }; openObjectExporterDialog = (object: ?gdObject) => { - if (object) { - this.setState({ - exportedObject: object, - }); - } else { - this.setState({ - exportedObject: null, - }); - } + this.setState({ + exportedObject: object, + }); }; editObjectByName = (objectName: string, initialTab?: ObjectEditorTab) => { @@ -448,7 +441,8 @@ export default class SceneEditor extends React.Component { () => { // /!\ Force the instances editor to destroy and mount again the // renderers to avoid keeping any references to existing instances - if (this.editor) this.editor.forceRemount(); + if (this.editorDisplay) + this.editorDisplay.instancesHandlers.forceRemountInstancesRenderers(); this.updateToolbar(); } ); @@ -463,7 +457,8 @@ export default class SceneEditor extends React.Component { () => { // /!\ Force the instances editor to destroy and mount again the // renderers to avoid keeping any references to existing instances - if (this.editor) this.editor.forceRemount(); + if (this.editorDisplay) + this.editorDisplay.instancesHandlers.forceRemountInstancesRenderers(); this.updateToolbar(); } ); @@ -488,32 +483,44 @@ export default class SceneEditor extends React.Component { }; _createNewObjectAndInstanceUnderCursor = () => { - if (!this.editor) { + if (!this.editorDisplay) { return; } // Remember where to create the instance, when the object will be created this.setState({ - newObjectInstanceSceneCoordinates: this.editor.getLastCursorSceneCoordinates(), + newObjectInstanceSceneCoordinates: this.editorDisplay.viewControls.getLastCursorSceneCoordinates(), }); - if (this._objectsList) this._objectsList.openNewObjectDialog(); + if (this.editorDisplay) this.editorDisplay.openNewObjectDialog(); else this.toggleObjectsList(); }; - addInstanceAtTheCenter = (objectName: string) => { - const { editor } = this; - if (editor) - this._addInstance( - [editor.grid.viewPosition.viewX, editor.grid.viewPosition.viewY], - objectName + addInstanceOnTheScene = ( + objectName: string, + targetPosition: 'center' | 'upperCenter' = 'center' + ) => { + if (!this.editorDisplay) { + return; + } + const viewPosition = this.editorDisplay.viewControls.getViewPosition(); + let position = [0, 0]; + if (viewPosition) { + position = viewPosition.toSceneCoordinates( + viewPosition.getWidth() / 2, + viewPosition.getHeight() / + // If the target position is the upper center, the Y position is at the first + // quarter of the screen. Otherwise, it's at the half of the screen. + (targetPosition === 'upperCenter' ? 4 : 2) ); + } + this._addInstance(position, objectName); }; _addInstance = (pos: [number, number], objectName: string) => { - if (!objectName || !this.editor) return; + if (!objectName || !this.editorDisplay) return; - const instances = this.editor.addInstances( + const instances = this.editorDisplay.instancesHandlers.addInstances( pos, [objectName], this.state.selectedLayer @@ -634,7 +641,8 @@ export default class SceneEditor extends React.Component { _onSelectInstances = ( instances: Array, - multiSelect: boolean + multiSelect: boolean, + targetPosition?: 'center' | 'upperCenter' ) => { this.instancesSelection.selectInstances({ instances, @@ -642,10 +650,16 @@ export default class SceneEditor extends React.Component { layersLocks: null, ignoreSeal: true, }); + if (this.editorDisplay) { + let offset = null; + const { viewControls } = this.editorDisplay; + const viewPosition = viewControls.getViewPosition(); + if (viewPosition && targetPosition === 'upperCenter') { + offset = [0, viewPosition.toSceneScale(viewPosition.getHeight() / 4)]; + } - if (this.editor) this.editor.centerViewOnLastInstance(instances); - this.forceUpdateInstancesList(); - this.forceUpdatePropertiesEditor(); + viewControls.centerViewOnLastInstance(instances, offset); + } this.updateToolbar(); }; @@ -715,7 +729,8 @@ export default class SceneEditor extends React.Component { done(doRemove); // /!\ Force the instances editor to destroy and mount again the // renderers to avoid keeping any references to existing instances - if (this.editor) this.editor.forceRemount(); + if (this.editorDisplay) + this.editorDisplay.instancesHandlers.forceRemountInstancesRenderers(); this.forceUpdateLayersList(); @@ -758,11 +773,8 @@ export default class SceneEditor extends React.Component { this.forceUpdatePropertiesEditor(); }; - _onCreateLayer = () => { - this.forceUpdatePropertiesEditor(); - }; - - _onDeleteObject = (i18n: I18nType) => ( + _onDeleteObject = ( + i18n: I18nType, objectWithContext: ObjectWithContext, done: boolean => void ) => { @@ -1068,7 +1080,8 @@ export default class SceneEditor extends React.Component { }); this.instancesSelection.clearSelection(); - if (this.editor) this.editor.clearHighlightedInstance(); + if (this.editorDisplay) + this.editorDisplay.instancesHandlers.clearHighlightedInstance(); this.setState( { @@ -1087,16 +1100,18 @@ export default class SceneEditor extends React.Component { }; zoomToInitialPosition = () => { - if (this.editor) this.editor.zoomToInitialPosition(); + if (this.editorDisplay) + this.editorDisplay.viewControls.zoomToInitialPosition(); }; zoomToFitContent = () => { - if (this.editor) this.editor.zoomToFitContent(); + if (this.editorDisplay) this.editorDisplay.viewControls.zoomToFitContent(); }; zoomToFitSelection = () => { const selectedInstances = this.instancesSelection.getSelectedInstances(); - if (this.editor) this.editor.zoomToFitSelection(selectedInstances); + if (this.editorDisplay) + this.editorDisplay.viewControls.zoomToFitSelection(selectedInstances); }; getContextMenuZoomItems = (i18n: I18nType) => { @@ -1131,15 +1146,18 @@ export default class SceneEditor extends React.Component { }; setZoomFactor = (zoomFactor: number) => { - if (this.editor) this.editor.setZoomFactor(zoomFactor); + if (this.editorDisplay) + this.editorDisplay.viewControls.setZoomFactor(zoomFactor); }; zoomIn = () => { - if (this.editor) this.editor.zoomBy(zoomInFactor); + if (this.editorDisplay) + this.editorDisplay.viewControls.zoomBy(zoomInFactor); }; zoomOut = () => { - if (this.editor) this.editor.zoomBy(zoomOutFactor); + if (this.editorDisplay) + this.editorDisplay.viewControls.zoomBy(zoomOutFactor); }; _onContextMenu = ( @@ -1285,13 +1303,13 @@ export default class SceneEditor extends React.Component { let x = 0; let y = 0; - if (this.editor) { - const selectionAABB = this.editor.selectedInstances.getSelectionAABB(); + if (this.editorDisplay) { + const selectionAABB = this.editorDisplay.instancesHandlers.getSelectionAABB(); x = selectionAABB.centerX(); y = selectionAABB.centerY(); } - if (this.editor) { + if (this.editorDisplay) { Clipboard.set(INSTANCES_CLIPBOARD_KIND, { x, y, @@ -1309,18 +1327,20 @@ export default class SceneEditor extends React.Component { duplicateSelection = ({ useLastCursorPosition, }: CopyCutPasteOptions = {}) => { - const { editor } = this; - if (!editor) return; + const { editorDisplay } = this; + if (!editorDisplay) return; const serializedSelection = this.instancesSelection .getSelectedInstances() .map(instance => serializeToJSObject(instance)); - const newInstances = editor.addSerializedInstances({ - position: [0, 0], - copyReferential: [-2 * MOVEMENT_BIG_DELTA, -2 * MOVEMENT_BIG_DELTA], - serializedInstances: serializedSelection, - preventSnapToGrid: true, - }); + const newInstances = editorDisplay.instancesHandlers.addSerializedInstances( + { + position: [0, 0], + copyReferential: [-2 * MOVEMENT_BIG_DELTA, -2 * MOVEMENT_BIG_DELTA], + serializedInstances: serializedSelection, + preventSnapToGrid: true, + } + ); this._onInstancesAdded(newInstances); this.instancesSelection.clearSelection(); this.instancesSelection.selectInstances({ @@ -1332,12 +1352,12 @@ export default class SceneEditor extends React.Component { }; paste = ({ useLastCursorPosition }: CopyCutPasteOptions = {}) => { - const { editor } = this; - if (!editor) return; + const { editorDisplay } = this; + if (!editorDisplay) return; const position = useLastCursorPosition - ? editor.getLastCursorSceneCoordinates() - : editor.getLastContextMenuSceneCoordinates(); + ? editorDisplay.viewControls.getLastCursorSceneCoordinates() + : editorDisplay.viewControls.getLastContextMenuSceneCoordinates(); const clipboardContent = Clipboard.get(INSTANCES_CLIPBOARD_KIND); const instancesContent = SafeExtractor.extractArrayProperty( @@ -1352,15 +1372,19 @@ export default class SceneEditor extends React.Component { 'pasteInTheForeground' ) || false; if (x === null || y === null || instancesContent === null) return; + const viewPosition = editorDisplay.viewControls.getViewPosition(); + if (!viewPosition) return; - const newInstances = editor.addSerializedInstances({ - position: editor.viewPosition.containsPoint(position[0], position[1]) - ? position - : [editor.viewPosition.viewX, editor.viewPosition.viewY], - copyReferential: [x, y], - serializedInstances: instancesContent, - addInstancesInTheForeground: pasteInTheForeground, - }); + const newInstances = editorDisplay.instancesHandlers.addSerializedInstances( + { + position: viewPosition.containsPoint(position[0], position[1]) + ? position + : [viewPosition.getViewX(), viewPosition.getViewY()], + copyReferential: [x, y], + serializedInstances: instancesContent, + addInstancesInTheForeground: pasteInTheForeground, + } + ); this._onInstancesAdded(newInstances); this.instancesSelection.clearSelection(); @@ -1391,11 +1415,11 @@ export default class SceneEditor extends React.Component { }; forceUpdateObjectsList = () => { - if (this._objectsList) this._objectsList.forceUpdateList(); + if (this.editorDisplay) this.editorDisplay.forceUpdateObjectsList(); }; forceUpdateObjectGroupsList = () => { - if (this._objectGroupsList) this._objectGroupsList.forceUpdate(); + if (this.editorDisplay) this.editorDisplay.forceUpdateObjectGroupsList(); }; forceUpdateLayersList = () => { @@ -1403,15 +1427,16 @@ export default class SceneEditor extends React.Component { if (!this.props.layout.hasLayerNamed(this.state.selectedLayer)) { this.setState({ selectedLayer: BASE_LAYER_NAME }); } - if (this._layersList) this._layersList.forceUpdate(); + if (this.editorDisplay) this.editorDisplay.forceUpdateLayersList(); }; forceUpdateInstancesList = () => { - if (this._instancesList) this._instancesList.forceUpdate(); + if (this.editorDisplay) this.editorDisplay.forceUpdateInstancesList(); }; forceUpdatePropertiesEditor = () => { - if (this._propertiesEditor) this._propertiesEditor.forceUpdate(); + if (this.editorDisplay) + this.editorDisplay.forceUpdateInstancesPropertiesEditor(); }; reloadResourcesFor = (object: gdObject) => { @@ -1430,44 +1455,14 @@ export default class SceneEditor extends React.Component { objectResourceNames, () => {}, () => { - if (this.editor) - this.editor.resetInstanceRenderersFor(object.getName()); + if (this.editorDisplay) + this.editorDisplay.instancesHandlers.resetInstanceRenderersFor( + object.getName() + ); } ); }; - _getAllObjectTags = (): Array => { - const { project, layout } = this.props; - - const tagsSet: Set = new Set(); - enumerateObjects(project, layout).allObjectsList.forEach(({ object }) => { - getTagsFromString(object.getTags()).forEach(tag => tagsSet.add(tag)); - }); - - return Array.from(tagsSet); - }; - - _buildObjectTagsMenuTemplate = (i18n: I18nType): Array => { - const { selectedObjectTags } = this.state; - - return buildTagsMenuTemplate({ - noTagLabel: i18n._(t`No tags - add a tag to an object first`), - getAllTags: this._getAllObjectTags, - selectedTags: selectedObjectTags, - onChange: selectedObjectTags => { - this.setState({ selectedObjectTags }); - }, - }); - }; - - _getInstanceSize = ( - initialInstance: gdInitialInstance - ): [number, number, number] => { - if (!this.editor) return [0, 0, 0]; - - return this.editor.getInstanceSize(initialInstance); - }; - render() { const { project, @@ -1477,7 +1472,6 @@ export default class SceneEditor extends React.Component { isActive, } = this.props; const { editedObjectWithContext } = this.state; - const selectedInstances = this.instancesSelection.getSelectedInstances(); const variablesEditedAssociatedObjectName = this.state .variablesEditedInstance ? this.state.variablesEditedInstance.getObjectName() @@ -1501,28 +1495,67 @@ export default class SceneEditor extends React.Component { ) : null; - const editors = { - properties: { - type: 'secondary', - title: t`Properties`, - renderEditor: () => ( - - {({ i18n }) => ( - + {windowWidth => { + const EditorsDisplay = + windowWidth === 'small' + ? SwipeableDrawerEditorsDisplay + : MosaicEditorsDisplay; + return ( +
+ { + this.editObject(object, 'variables'); + }} + onOpenSceneProperties={this.openSceneProperties} + onOpenSceneVariables={this.editLayoutVariables} + onEditObjectGroup={this.editGroup} + onEditLayerEffects={this.editLayerEffects} + onEditLayer={this.editLayer} + /> + (this.editorDisplay = ref)} + project={project} + layout={layout} + initialInstances={initialInstances} + instancesSelection={this.instancesSelection} + onSelectInstances={this._onSelectInstances} + onAddObjectInstance={this.addInstanceOnTheScene} + selectedLayer={this.state.selectedLayer} + editLayer={this.editLayer} + editLayerEffects={this.editLayerEffects} editInstanceVariables={this.editInstanceVariables} - onEditObjectByName={this.editObjectByName} - onInstancesModified={instances => - this.forceUpdateInstancesList() + editObjectByName={this.editObjectByName} + selectedObjectNames={selectedObjectNames} + renamedObjectWithContext={this.state.renamedObjectWithContext} + onRenameLayer={this._onRenameLayer} + onRemoveLayer={this._onRemoveLayer} + onSelectLayer={(layer: string) => + this.setState({ selectedLayer: layer }) } - onGetInstanceSize={this._getInstanceSize} - ref={propertiesEditor => - (this._propertiesEditor = propertiesEditor) - } - unsavedChanges={this.props.unsavedChanges} + onExportObject={this.openObjectExporterDialog} + onDeleteObject={this._onDeleteObject} + canObjectOrGroupUseNewName={this._canObjectOrGroupUseNewName} + onEditObjectGroup={this.editGroup} + onDeleteObjectGroup={this._onDeleteGroup} + onRenameObjectGroup={this._onRenameGroup} + canRenameObjectGroup={this._canObjectOrGroupUseNewName} + canObjectOrGroupBeGlobal={this.canObjectOrGroupBeGlobal} + updateBehaviorsSharedData={this.updateBehaviorsSharedData} + onEditObject={this.props.onEditObject || this.editObject} + onRenameObjectStart={this._onRenameObjectStart} + onRenameObjectFinish={this._onRenameObjectFinish} + onObjectCreated={this._onObjectCreated} + onObjectSelected={this._onObjectSelected} + canInstallPrivateAsset={this.props.canInstallPrivateAsset} historyHandler={{ undo: this.undo, redo: this.redo, @@ -1536,448 +1569,273 @@ export default class SceneEditor extends React.Component { ), }), }} - /> - )} - - ), - }, - 'layers-list': { - type: 'secondary', - title: t`Layers`, - renderEditor: () => ( - - this.setState({ selectedLayer: layer }) - } - onEditLayerEffects={this.editLayerEffects} - onEditLayer={this.editLayer} - onRemoveLayer={this._onRemoveLayer} - onRenameLayer={this._onRenameLayer} - onCreateLayer={this._onCreateLayer} - layersContainer={layout} - unsavedChanges={this.props.unsavedChanges} - ref={layersList => (this._layersList = layersList)} - hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps} - /> - ), - }, - 'instances-list': { - type: 'secondary', - title: t`Instances List`, - renderEditor: () => ( - (this._instancesList = instancesList)} - /> - ), - }, - 'instances-editor': { - type: 'primary', - noTitleBar: true, - renderEditor: () => ( - this.copySelection({ useLastCursorPosition: true }), - onCut: () => this.cutSelection({ useLastCursorPosition: true }), - onPaste: () => this.paste({ useLastCursorPosition: true }), - onDuplicate: () => - this.duplicateSelection({ useLastCursorPosition: true }), - onDelete: this.deleteSelection, - onUndo: this.undo, - onRedo: this.redo, - onZoomOut: this.zoomOut, - onZoomIn: this.zoomIn, - onShift1: this.zoomToFitSelection, - onShift2: this.zoomToInitialPosition, - onShift3: this.zoomToFitContent, - }} - wrappedEditorRef={editor => { - this.editor = editor; - }} - pauseRendering={!isActive} - /> - ), - }, - 'objects-list': { - type: 'secondary', - title: t`Objects`, - toolbarControls: [ - - {({ i18n }) => ( - - this._buildObjectTagsMenuTemplate(i18n) - } - /> - )} - , - , - ], - renderEditor: () => ( - - {({ i18n }) => ( - + this.copySelection({ useLastCursorPosition: true }), + onCut: () => + this.cutSelection({ useLastCursorPosition: true }), + onPaste: () => this.paste({ useLastCursorPosition: true }), + onDuplicate: () => + this.duplicateSelection({ useLastCursorPosition: true }), + onDelete: this.deleteSelection, + onUndo: this.undo, + onRedo: this.redo, + onZoomOut: this.zoomOut, + onZoomIn: this.zoomIn, + onShift1: this.zoomToFitSelection, + onShift2: this.zoomToInitialPosition, + onShift3: this.zoomToFitContent, + }} + onInstancesAdded={this._onInstancesAdded} + onInstancesSelected={this._onInstancesSelected} + onInstanceDoubleClicked={this._onInstanceDoubleClicked} + onInstancesMoved={this._onInstancesMoved} + onInstancesResized={this._onInstancesResized} + onInstancesRotated={this._onInstancesRotated} + isInstanceOf3DObject={this.isInstanceOf3DObject} onSelectAllInstancesOfObjectInLayout={ this.onSelectAllInstancesOfObjectInLayout } + instancesEditorSettings={this.state.instancesEditorSettings} + onInstancesEditorSettingsMutated={ + this._onInstancesEditorSettingsMutated + } + onContextMenu={this._onContextMenu} resourceManagementProps={this.props.resourceManagementProps} - selectedObjectNames={selectedObjectNames} - canInstallPrivateAsset={this.props.canInstallPrivateAsset} - onEditObject={this.props.onEditObject || this.editObject} - onExportObject={this.openObjectExporterDialog} - onDeleteObject={this._onDeleteObject(i18n)} - canRenameObject={(newName, global) => - this._canObjectOrGroupUseNewName(newName, global, i18n) - } - onObjectCreated={this._onObjectCreated} - onObjectSelected={this._onObjectSelected} - renamedObjectWithContext={this.state.renamedObjectWithContext} - onRenameObjectStart={this._onRenameObjectStart} - onRenameObjectFinish={this._onRenameObjectFinish} - onAddObjectInstance={this.addInstanceAtTheCenter} - onObjectPasted={() => this.updateBehaviorsSharedData()} - selectedObjectTags={this.state.selectedObjectTags} - beforeSetAsGlobalObject={objectName => - this.canObjectOrGroupBeGlobal(i18n, objectName) - } - onChangeSelectedObjectTags={selectedObjectTags => - this.setState({ - selectedObjectTags, - }) - } - getAllObjectTags={this._getAllObjectTags} - ref={ - // $FlowFixMe Make this component functional. - objectsList => (this._objectsList = objectsList) - } - unsavedChanges={this.props.unsavedChanges} hotReloadPreviewButtonProps={ this.props.hotReloadPreviewButtonProps } + isActive={isActive} + onOpenedEditorsChanged={this.updateToolbar} /> - )} - - ), - }, - 'object-groups-list': { - type: 'secondary', - title: t`Object Groups`, - renderEditor: () => ( - - {({ i18n }) => ( - - (this._objectGroupsList = objectGroupsList) - } - globalObjectGroups={project.getObjectGroups()} - objectGroups={layout.getObjectGroups()} - onEditGroup={this.editGroup} - onDeleteGroup={this._onDeleteGroup} - onRenameGroup={this._onRenameGroup} - canRenameGroup={(newName, global) => - this._canObjectOrGroupUseNewName(newName, global, i18n) - } - beforeSetAsGlobalGroup={groupName => - this.canObjectOrGroupBeGlobal(i18n, groupName) - } - unsavedChanges={this.props.unsavedChanges} - /> - )} - - ), - }, - }; + + {({ i18n }) => ( + + {editedObjectWithContext && ( + { + const { editedObjectWithContext } = this.state; + if (!editedObjectWithContext) return []; - return ( -
- { - this.editObject(object, 'variables'); - }} - onOpenSceneProperties={this.openSceneProperties} - onOpenSceneVariables={this.editLayoutVariables} - onEditObjectGroup={this.editGroup} - onEditLayerEffects={this.editLayerEffects} - onEditLayer={this.editLayer} - /> - - {windowWidth => ( - - {({ getDefaultEditorMosaicNode, setDefaultEditorMosaicNode }) => ( - - setDefaultEditorMosaicNode( - windowWidth === 'small' - ? 'scene-editor-small' - : 'scene-editor', - node - ) - } - ref={editorMosaic => (this.editorMosaic = editorMosaic)} + return EventsRootVariablesFinder.findAllObjectVariables( + project.getCurrentPlatform(), + project, + layout, + editedObjectWithContext.object + ); + }} + onCancel={() => { + if (editedObjectWithContext) { + this.reloadResourcesFor( + editedObjectWithContext.object + ); + } + this.editObject(null); + }} + canRenameObject={newName => + this._canObjectOrGroupUseNewName( + newName, + editedObjectWithContext.global, + i18n + ) + } + onRename={newName => { + this._onRenameEditedObject(newName); + }} + onApply={() => { + if (editedObjectWithContext) { + this.reloadResourcesFor( + editedObjectWithContext.object + ); + } + this.editObject(null); + this.updateBehaviorsSharedData(); + this.forceUpdateObjectsList(); + + if (this.props.unsavedChanges) + this.props.unsavedChanges.triggerUnsavedChanges(); + }} + hotReloadPreviewButtonProps={ + this.props.hotReloadPreviewButtonProps + } + onUpdateBehaviorsSharedData={() => + this.updateBehaviorsSharedData() + } + openBehaviorEvents={this.props.openBehaviorEvents} + /> + )} + + )} + + {this.state.exportedObject && ( + { + this.openObjectExporterDialog(null); + }} /> )} - - )} - - - {({ i18n }) => ( - - {editedObjectWithContext && ( - { - const { editedObjectWithContext } = this.state; - if (!editedObjectWithContext) return []; - - return EventsRootVariablesFinder.findAllObjectVariables( - project.getCurrentPlatform(), - project, - layout, - editedObjectWithContext.object - ); - }} - onCancel={() => { - if (editedObjectWithContext) { - this.reloadResourcesFor(editedObjectWithContext.object); - } - this.editObject(null); - }} - canRenameObject={newName => - this._canObjectOrGroupUseNewName( - newName, - editedObjectWithContext.global, - i18n - ) + group={this.state.editedGroup} + objectsContainer={layout} + globalObjectsContainer={project} + onCancel={() => this.editGroup(null)} + onApply={() => this.editGroup(null)} + /> + )} + {this.state.setupGridOpen && ( + { - this._onRenameEditedObject(newName); - }} - onApply={() => { - if (editedObjectWithContext) { - this.reloadResourcesFor(editedObjectWithContext.object); + onCancel={() => this.openSetupGrid(false)} + onApply={() => this.openSetupGrid(false)} + /> + )} + {!!this.state.variablesEditedInstance && + !!variablesEditedAssociatedObject && ( + this.editInstanceVariables(null)} + onApply={() => this.editInstanceVariables(null)} + emptyPlaceholderTitle={ + Add your first instance variable } - this.editObject(null); - this.updateBehaviorsSharedData(); - this.forceUpdateObjectsList(); - - if (this.props.unsavedChanges) - this.props.unsavedChanges.triggerUnsavedChanges(); - }} + emptyPlaceholderDescription={ + + Instance variables overwrite the default values of the + variables of the object. + + } + helpPagePath={'/all-features/variables/instance-variables'} + title={Instance Variables} + onEditObjectVariables={ + variablesEditedAssociatedObject + ? () => { + this.editObject( + variablesEditedAssociatedObject, + 'variables' + ); + this.editInstanceVariables(null); + } + : undefined + } + hotReloadPreviewButtonProps={ + this.props.hotReloadPreviewButtonProps + } + onComputeAllVariableNames={() => { + const { variablesEditedInstance } = this.state; + if (!variablesEditedInstance) { + return []; + } + const variablesEditedObject = getObjectByName( + project, + layout, + variablesEditedInstance.getObjectName() + ); + return variablesEditedObject + ? EventsRootVariablesFinder.findAllObjectVariables( + project.getCurrentPlatform(), + project, + layout, + variablesEditedObject + ) + : []; + }} + /> + )} + {!!this.state.layerRemoved && + this.state.onCloseLayerRemoveDialog && ( + + )} + {!!this.state.editedLayer && ( + + this.setState({ + editedLayer: null, + }) + } hotReloadPreviewButtonProps={ this.props.hotReloadPreviewButtonProps } - onUpdateBehaviorsSharedData={() => - this.updateBehaviorsSharedData() - } - openBehaviorEvents={this.props.openBehaviorEvents} /> )} - - )} - - {this.state.exportedObject && ( - { - this.openObjectExporterDialog(null); - }} - /> - )} - {!!this.state.editedGroup && ( - this.editGroup(null)} - onApply={() => this.editGroup(null)} - /> - )} - {this.state.setupGridOpen && ( - this.openSetupGrid(false)} - onApply={() => this.openSetupGrid(false)} - /> - )} - {!!this.state.variablesEditedInstance && - !!variablesEditedAssociatedObject && ( - this.editInstanceVariables(null)} - onApply={() => this.editInstanceVariables(null)} - emptyPlaceholderTitle={ - Add your first instance variable - } - emptyPlaceholderDescription={ - - Instance variables overwrite the default values of the - variables of the object. - - } - helpPagePath={'/all-features/variables/instance-variables'} - title={Instance Variables} - onEditObjectVariables={ - variablesEditedAssociatedObject - ? () => { - this.editObject( - variablesEditedAssociatedObject, - 'variables' - ); - this.editInstanceVariables(null); - } - : undefined - } - hotReloadPreviewButtonProps={ - this.props.hotReloadPreviewButtonProps - } - onComputeAllVariableNames={() => { - const { variablesEditedInstance } = this.state; - if (!variablesEditedInstance) { - return []; - } - const variablesEditedObject = getObjectByName( - project, - layout, - variablesEditedInstance.getObjectName() - ); - return variablesEditedObject - ? EventsRootVariablesFinder.findAllObjectVariables( - project.getCurrentPlatform(), - project, - layout, - variablesEditedObject - ) - : []; - }} - /> - )} - {!!this.state.layerRemoved && this.state.onCloseLayerRemoveDialog && ( - - )} - {!!this.state.editedLayer && ( - - this.setState({ - editedLayer: null, - }) - } - hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps} - /> - )} - {this.state.scenePropertiesDialogOpen && ( - this.openSceneProperties(false)} - onApply={() => this.openSceneProperties(false)} - onEditVariables={() => this.editLayoutVariables(true)} - onOpenMoreSettings={this.props.onOpenMoreSettings} - resourceManagementProps={this.props.resourceManagementProps} - /> - )} - {!!this.state.layoutVariablesDialogOpen && ( - this.editLayoutVariables(false)} - onClose={() => this.editLayoutVariables(false)} - hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps} - /> - )} - - {({ i18n }) => ( - - this.openSceneProperties(false)} + onApply={() => this.openSceneProperties(false)} + onEditVariables={() => this.editLayoutVariables(true)} + onOpenMoreSettings={this.props.onOpenMoreSettings} + resourceManagementProps={this.props.resourceManagementProps} + /> + )} + {!!this.state.layoutVariablesDialogOpen && ( + this.editLayoutVariables(false)} + onClose={() => this.editLayoutVariables(false)} + hotReloadPreviewButtonProps={ + this.props.hotReloadPreviewButtonProps + } + /> + )} + + {({ i18n }) => ( + + + (this.contextMenu = contextMenu)} + buildMenuTemplate={(i18n, buildOptions) => + this.buildContextMenu(i18n, layout, buildOptions) + } + /> + )} + + this.onInstanceAddedOnInvisibleLayer(null)} /> - (this.contextMenu = contextMenu)} - buildMenuTemplate={(i18n, buildOptions) => - this.buildContextMenu(i18n, layout, buildOptions) - } - /> - - )} - - this.onInstanceAddedOnInvisibleLayer(null)} - /> -
+
+ ); + }} + ); } } diff --git a/newIDE/app/src/UI/EditorMosaic/TagsButton.js b/newIDE/app/src/UI/EditorMosaic/TagsButton.js index fcad972b3..d2fdbd81d 100644 --- a/newIDE/app/src/UI/EditorMosaic/TagsButton.js +++ b/newIDE/app/src/UI/EditorMosaic/TagsButton.js @@ -7,11 +7,16 @@ import { type MenuItemTemplate } from '../Menu/Menu.flow'; import Filter from '../CustomSvgIcons/Filter'; const styles = { - container: { + mediumContainer: { padding: 0, width: 32, height: 32, }, + smallContainer: { + padding: 0, + width: 16, + height: 16, + }, icon: { width: 16, height: 16, @@ -20,13 +25,20 @@ const styles = { type Props = {| buildMenuTemplate: (i18n: I18nType) => Array, + size?: 'small', |}; export default function TagsButton(props: Props) { return ( + } diff --git a/newIDE/app/src/UI/IconButton.js b/newIDE/app/src/UI/IconButton.js index 74bf0f173..8628b5829 100644 --- a/newIDE/app/src/UI/IconButton.js +++ b/newIDE/app/src/UI/IconButton.js @@ -57,6 +57,9 @@ type Props = {| acceleratorString?: string, 'aria-label'?: string, + disableRipple?: boolean, + disableFocusRipple?: boolean, + color?: 'default', |}; diff --git a/newIDE/app/src/UI/Menu/ContextMenu.js b/newIDE/app/src/UI/Menu/ContextMenu.js index d8faf7ae6..8ef702cfb 100644 --- a/newIDE/app/src/UI/Menu/ContextMenu.js +++ b/newIDE/app/src/UI/Menu/ContextMenu.js @@ -55,7 +55,17 @@ const MaterialUIContextMenu = React.forwardRef< top: anchorPosition[1], }} anchorReference={'anchorPosition'} - onClose={() => setOpenMenu(false)} + onClose={(event, reason) => { + if (reason === 'backdropClick') { + // Prevent any side effect of a backdrop click that should only + // close the context menu. + // When used in the ElementWithMenu component, there are cases where + // the event propagates to the element on which the menu is set up and + // then the event bubbles up, triggering click events on its way up. + event.stopPropagation(); + } + setOpenMenu(false); + }} TransitionComponent={Fade} {...menuImplementation.getMenuProps()} > diff --git a/newIDE/app/src/UI/Menu/ElementWithMenu.js b/newIDE/app/src/UI/Menu/ElementWithMenu.js index ef9b98527..0852d15c9 100644 --- a/newIDE/app/src/UI/Menu/ElementWithMenu.js +++ b/newIDE/app/src/UI/Menu/ElementWithMenu.js @@ -22,7 +22,8 @@ export default class ElementWithMenu extends React.Component { _contextMenu: ?ContextMenuInterface; _wrappedElement: ?any; - open = () => { + open = (event?: Event) => { + if (event && event.stopPropagation) event.stopPropagation(); const { _contextMenu } = this; if (!_contextMenu) return; diff --git a/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js b/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js index 89d9af759..4f6f3821e 100644 --- a/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js +++ b/newIDE/app/src/UI/Menu/MaterialUIMenuImplementation.js @@ -224,7 +224,8 @@ export default class MaterialUIMenuImplementation // $FlowFixMe - existence should be inferred by Flow. item.enabled === false } - onClick={async () => { + onClick={async e => { + e.stopPropagation(); if (item.enabled === false) { return; } @@ -269,7 +270,8 @@ export default class MaterialUIMenuImplementation dense key={'item' + item.label} disabled={item.enabled === false} - onClick={() => { + onClick={e => { + e.stopPropagation(); if (item.enabled === false) { return; } diff --git a/newIDE/app/src/UI/Paper.js b/newIDE/app/src/UI/Paper.js index 08148ee1b..1bd97d9c7 100644 --- a/newIDE/app/src/UI/Paper.js +++ b/newIDE/app/src/UI/Paper.js @@ -2,6 +2,7 @@ import * as React from 'react'; import MuiPaper from '@material-ui/core/Paper'; import GDevelopThemeContext from './Theme/GDevelopThemeContext'; +import { type GDevelopTheme } from './Theme'; type Props = {| id?: string, @@ -16,6 +17,16 @@ type Props = {| square?: boolean, |}; +export const getBackgroundColor = ( + gdevelopTheme: GDevelopTheme, + backgroundColor: 'light' | 'medium' | 'dark' +) => + backgroundColor === 'dark' + ? gdevelopTheme.paper.backgroundColor.dark + : backgroundColor === 'medium' + ? gdevelopTheme.paper.backgroundColor.medium + : gdevelopTheme.paper.backgroundColor.light; + const Paper = ({ id, children, @@ -26,12 +37,7 @@ const Paper = ({ square, }: Props) => { const gdevelopTheme = React.useContext(GDevelopThemeContext); - const backgroundColor = - background === 'dark' - ? gdevelopTheme.paper.backgroundColor.dark - : background === 'medium' - ? gdevelopTheme.paper.backgroundColor.medium - : gdevelopTheme.paper.backgroundColor.light; + const backgroundColor = getBackgroundColor(gdevelopTheme, background); return ( { - const gdevelopTheme = React.useContext(GDevelopThemeContext); - return (
{props.children}