Display the scene editors more conveniently on mobile (#5441)

This commit is contained in:
AlexandreS
2023-06-26 18:07:23 +02:00
committed by GitHub
parent 577c4adb14
commit 007d56e946
31 changed files with 2176 additions and 712 deletions

View File

@@ -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": {

View File

@@ -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<Props, State> {
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<Props, State> {
{gdevelopTheme => (
<div style={styles.container}>
<div
style={{ flex: 1 }}
style={styles.tableContainer}
onKeyDown={this._keyboardShortcuts.onKeyDown}
onKeyUp={this._keyboardShortcuts.onKeyUp}
>
@@ -246,49 +256,64 @@ export default class InstancesList extends Component<Props, State> {
sort={this._sort}
sortBy={sortBy}
sortDirection={sortDirection}
width={width}
width={Math.max(width, minimumWidths.table)}
>
<RVColumn
label={<Trans>Object name</Trans>}
dataKey="name"
width={width * 0.35}
width={Math.max(width * 0.35, minimumWidths.objectName)}
className={'tableColumn'}
/>
<RVColumn
label=""
dataKey="locked"
width={width * 0.05}
width={Math.max(
width * 0.05,
minimumWidths.numberProperty
)}
className={'tableColumn'}
cellRenderer={this._renderLockCell}
/>
<RVColumn
label={<Trans>X</Trans>}
dataKey="x"
width={width * 0.1}
width={Math.max(
width * 0.1,
minimumWidths.numberProperty
)}
className={'tableColumn'}
/>
<RVColumn
label={<Trans>Y</Trans>}
dataKey="y"
width={width * 0.1}
width={Math.max(
width * 0.1,
minimumWidths.numberProperty
)}
className={'tableColumn'}
/>
<RVColumn
label={<Trans>Angle</Trans>}
dataKey="angle"
width={width * 0.1}
width={Math.max(
width * 0.1,
minimumWidths.numberProperty
)}
className={'tableColumn'}
/>
<RVColumn
label={<Trans>Layer</Trans>}
dataKey="layer"
width={width * 0.2}
width={Math.max(width * 0.2, minimumWidths.layerName)}
className={'tableColumn'}
/>
<RVColumn
label={<Trans>Z Order</Trans>}
dataKey="zOrder"
width={width * 0.1}
width={Math.max(
width * 0.1,
minimumWidths.numberProperty
)}
className={'tableColumn'}
/>
</RVTable>

View File

@@ -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 {

View File

@@ -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.

View File

@@ -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<Props> {
// 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<Props> {
'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<Props> {
* 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<Props> {
* 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<Props> {
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<Props> {
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<Props> {
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<Props> {
getRecommendedInitialZoomFactor(Math.max(height, width))
);
this.scrollTo(x, y);
}
};
zoomToFitSelection(instances: Array<gdInitialInstance>) {
zoomToFitSelection = (instances: Array<gdInitialInstance>) => {
if (instances.length === 0) return;
const [firstInstance, ...otherInstances] = instances;
const instanceMeasurer = this.instancesRenderer.getInstanceMeasurer();
@@ -981,9 +984,12 @@ export default class InstancesEditor extends Component<Props> {
);
});
this.fitViewToRectangle(selectedInstancesRectangle, { adaptZoom: true });
}
};
centerViewOnLastInstance(instances: Array<gdInitialInstance>) {
centerViewOnLastInstance = (
instances: Array<gdInitialInstance>,
offset?: ?[number, number]
) => {
if (instances.length === 0) return;
const instanceMeasurer = this.instancesRenderer.getInstanceMeasurer();
@@ -992,7 +998,8 @@ export default class InstancesEditor extends Component<Props> {
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<Props> {
);
};
getViewPosition = () /*: ?ViewPosition */ => {
getViewPosition = (): ?ViewPosition => {
return this.viewPosition;
};

View File

@@ -941,4 +941,4 @@ const areEqual = (prevProps: Props, nextProps: Props): boolean =>
prevProps.project === nextProps.project &&
prevProps.objectsContainer === nextProps.objectsContainer;
export default React.memo<Props>(ObjectsList, areEqual);
export default React.memo<Props, ObjectsListInterface>(ObjectsList, areEqual);

View File

@@ -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<gdInitialInstance>,
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<gdInitialInstance>) => void,
onInstancesSelected: (Array<gdInitialInstance>) => void,
onInstanceDoubleClicked: gdInitialInstance => void,
onInstancesMoved: (Array<gdInitialInstance>) => void,
onInstancesResized: (Array<gdInitialInstance>) => void,
onInstancesRotated: (Array<gdInitialInstance>) => 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<gdInitialInstance>) => void,
centerViewOnLastInstance: (
Array<gdInitialInstance>,
offset?: ?[number, number]
) => void,
getLastCursorSceneCoordinates: () => [number, number],
getLastContextMenuSceneCoordinates: () => [number, number],
getViewPosition: () => ?ViewPosition,
|},
instancesHandlers: {|
getSelectionAABB: () => Rectangle,
addInstances: (
pos: [number, number],
objectNames: Array<string>,
layer: string
) => Array<gdInitialInstance>,
clearHighlightedInstance: () => void,
resetInstanceRenderersFor: (objectName: string) => void,
forceRemountInstancesRenderers: () => void,
addSerializedInstances: ({|
position: [number, number],
copyReferential: [number, number],
serializedInstances: Array<Object>,
preventSnapToGrid?: boolean,
addInstancesInTheForeground?: boolean,
|}) => Array<gdInitialInstance>,
|},
|};

View File

@@ -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,

View File

@@ -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<SelectedTags>([]);
const instancesPropertiesEditorRef = React.useRef<?InstancePropertiesEditorInterface>(
null
);
const layersListRef = React.useRef<?LayersListInterface>(null);
const instancesListRef = React.useRef<?InstancesList>(null);
const editorRef = React.useRef<?InstancesEditor>(null);
const objectsListRef = React.useRef<?ObjectsListInterface>(null);
const editorMosaicRef = React.useRef<?EditorMosaic>(null);
const objectGroupsListRef = React.useRef<?ObjectGroupsList>(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<gdInitialInstance>, multiSelect: boolean) => {
onSelectInstances(instances, multiSelect);
forceUpdateInstancesList();
forceUpdateInstancesPropertiesEditor();
},
[
forceUpdateInstancesList,
forceUpdateInstancesPropertiesEditor,
onSelectInstances,
]
);
const getAllObjectTags = React.useCallback(
(): Array<string> => {
const tagsSet: Set<string> = 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<any> => {
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>
{({ i18n }) => (
<InstancePropertiesEditor
i18n={i18n}
project={project}
layout={layout}
instances={selectedInstances}
editInstanceVariables={props.editInstanceVariables}
onEditObjectByName={props.editObjectByName}
onInstancesModified={forceUpdateInstancesList}
onGetInstanceSize={getInstanceSize}
ref={instancesPropertiesEditorRef}
unsavedChanges={props.unsavedChanges}
historyHandler={props.historyHandler}
/>
)}
</I18n>
),
},
'layers-list': {
type: 'secondary',
title: t`Layers`,
renderEditor: () => (
<LayersList
project={project}
selectedLayer={selectedLayer}
onSelectLayer={props.onSelectLayer}
onEditLayerEffects={props.editLayerEffects}
onEditLayer={props.editLayer}
onRemoveLayer={props.onRemoveLayer}
onRenameLayer={props.onRenameLayer}
onCreateLayer={forceUpdateInstancesPropertiesEditor}
layersContainer={layout}
unsavedChanges={props.unsavedChanges}
ref={layersListRef}
hotReloadPreviewButtonProps={props.hotReloadPreviewButtonProps}
/>
),
},
'instances-list': {
type: 'secondary',
title: t`Instances List`,
renderEditor: () => (
<InstancesList
instances={initialInstances}
selectedInstances={selectedInstances}
onSelectInstances={selectInstances}
ref={instancesListRef}
/>
),
},
'instances-editor': {
type: 'primary',
noTitleBar: true,
renderEditor: () => (
<FullSizeInstancesEditorWithScrollbars
project={project}
layout={layout}
selectedLayer={selectedLayer}
initialInstances={initialInstances}
instancesEditorSettings={props.instancesEditorSettings}
onInstancesEditorSettingsMutated={
props.onInstancesEditorSettingsMutated
}
instancesSelection={props.instancesSelection}
onInstancesAdded={props.onInstancesAdded}
onInstancesSelected={props.onInstancesSelected}
onInstanceDoubleClicked={props.onInstanceDoubleClicked}
onInstancesMoved={props.onInstancesMoved}
onInstancesResized={props.onInstancesResized}
onInstancesRotated={props.onInstancesRotated}
selectedObjectNames={props.selectedObjectNames}
onContextMenu={props.onContextMenu}
isInstanceOf3DObject={props.isInstanceOf3DObject}
instancesEditorShortcutsCallbacks={
props.instancesEditorShortcutsCallbacks
}
wrappedEditorRef={editor => {
editorRef.current = editor;
}}
pauseRendering={!props.isActive}
/>
),
},
'objects-list': {
type: 'secondary',
title: t`Objects`,
toolbarControls: [
<TagsButton
key="tags"
buildMenuTemplate={buildObjectTagsMenuTemplate}
/>,
<CloseButton key="close" />,
],
renderEditor: () => (
<I18n>
{({ i18n }) => (
<ObjectsList
getThumbnail={ObjectsRenderingService.getThumbnail.bind(
ObjectsRenderingService
)}
project={project}
objectsContainer={layout}
layout={layout}
onSelectAllInstancesOfObjectInLayout={
props.onSelectAllInstancesOfObjectInLayout
}
resourceManagementProps={props.resourceManagementProps}
selectedObjectNames={props.selectedObjectNames}
canInstallPrivateAsset={props.canInstallPrivateAsset}
onEditObject={props.onEditObject}
onExportObject={props.onExportObject}
onDeleteObject={(objectWithContext, cb) =>
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}
/>
)}
</I18n>
),
},
'object-groups-list': {
type: 'secondary',
title: t`Object Groups`,
renderEditor: () => (
<I18n>
{({ i18n }) => (
<ObjectGroupsList
ref={objectGroupsListRef}
globalObjectGroups={project.getObjectGroups()}
objectGroups={layout.getObjectGroups()}
onEditGroup={props.onEditObjectGroup}
onDeleteGroup={props.onDeleteObjectGroup}
onRenameGroup={props.onRenameObjectGroup}
canRenameGroup={(newName, global) =>
props.canRenameObjectGroup(newName, global, i18n)
}
beforeSetAsGlobalGroup={groupName =>
props.canObjectOrGroupBeGlobal(i18n, groupName)
}
unsavedChanges={props.unsavedChanges}
/>
)}
</I18n>
),
},
};
return (
<EditorMosaic
editors={editors}
limitToOneSecondaryEditor={windowWidth === 'small'}
initialNodes={
getDefaultEditorMosaicNode('scene-editor') || initialMosaicEditorNodes
}
onOpenedEditorsChanged={props.onOpenedEditorsChanged}
onPersistNodes={node => setDefaultEditorMosaicNode('scene-editor', node)}
ref={editorMosaicRef}
/>
);
});
export default MosaicEditorsDisplay;

View File

@@ -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: <ObjectIcon />,
},
'object-groups-list': {
buttonId: 'toolbar-open-object-groups-panel-button',
icon: <ObjectGroupIcon />,
},
properties: {
buttonId: 'toolbar-open-properties-panel-button',
icon: <EditIcon />,
},
'instances-list': {
buttonId: 'toolbar-open-instances-list-panel-button',
icon: <InstancesListIcon />,
},
'layers-list': {
buttonId: 'toolbar-open-layers-panel-button',
icon: <LayersIcon />,
},
};
const BottomToolbar = (props: Props) => {
return (
<Paper background="medium" square style={styles.container}>
<Toolbar>
<ToolbarGroup>
{Object.keys(editors).map(editorId => {
const { icon, buttonId } = editors[editorId];
const isSelected = props.selectedEditorId === editorId;
return (
<IconButton
color="default"
key={editorId}
disableRipple
disableFocusRipple
id={buttonId}
onClick={() => {
props.onSelectEditor(editorId);
}}
selected={isSelected}
>
{icon}
</IconButton>
);
})}
</ToolbarGroup>
</Toolbar>
</Paper>
);
};
export default BottomToolbar;

View File

@@ -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 (
<div
style={{
...styles.topBarContainer,
backgroundColor: getBackgroundColor(gdevelopTheme, 'light'),
}}
onClick={props.onClick}
onTouchStart={onTouchStart}
onTouchEnd={onTouchEnd}
>
<ColumnStackLayout noMargin>
<div style={styles.topBarHandleContainer}>
<span
style={{
...styles.topBarHandle,
backgroundColor: gdevelopTheme.swipeableDrawer.topBar.pillColor,
}}
/>
</div>
<Column>
<Line
noMargin
justifyContent={props.controls ? 'space-between' : 'flex-start'}
alignItems="center"
>
<Text size="sub-title" noMargin>
{props.title}
</Text>
{props.controls}
</Line>
</Column>
</ColumnStackLayout>
</div>
);
};
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 (
<div style={{ ...styles.container, height, display }}>
<SwipeableDrawerTopBar
title={props.title}
onClick={() => setOpeningState('closed')}
onSwipeUp={() => {
if (openingState === 'halfOpen') setOpeningState('open');
}}
onSwipeDown={() => {
if (openingState === 'halfOpen') setOpeningState('closed');
else if (openingState === 'open') setOpeningState('halfOpen');
}}
controls={props.topBarControls}
/>
<div style={styles.childrenContainer}>{props.children}</div>
</div>
);
};
export default SwipeableDrawer;

View File

@@ -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<MenuItemTemplate>,
setZoomFactor: number => void,
onOpenSettings: () => void,
settingsIcon: React.Node,
canRenameObject: boolean,
onRenameObject: () => void,
|};
const Toolbar = (props: Props) => {
return (
<>
<ToolbarCommands
toggleObjectsList={props.toggleObjectsList}
toggleObjectGroupsList={props.toggleObjectGroupsList}
togglePropertiesPanel={props.toggleProperties}
toggleInstancesList={props.toggleInstancesList}
toggleLayersList={props.toggleLayersList}
undo={props.undo}
canUndo={props.canUndo}
redo={props.redo}
canRedo={props.canRedo}
deleteSelection={props.deleteSelection}
toggleWindowMask={props.toggleWindowMask}
toggleGrid={props.toggleGrid}
setupGrid={props.openSetupGrid}
canDeleteSelection={
props.instancesSelection.getSelectedInstances().length !== 0
}
canRenameObject={props.canRenameObject}
onRenameObject={props.onRenameObject}
/>
<IconButton
size="small"
color="default"
onClick={props.undo}
disabled={!props.canUndo}
tooltip={t`Undo the last changes`}
>
<UndoIcon />
</IconButton>
<IconButton
size="small"
color="default"
onClick={props.redo}
disabled={!props.canRedo}
tooltip={t`Redo the last changes`}
>
<RedoIcon />
</IconButton>
<ElementWithMenu
element={
<IconButton
size="small"
color="default"
tooltip={t`Change editor zoom`}
>
<ZoomInIcon />
</IconButton>
}
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) },
]}
/>
<IconButton
size="small"
color="default"
onClick={props.deleteSelection}
disabled={!props.instancesSelection.getSelectedInstances().length}
tooltip={t`Delete the selected instances from the scene`}
>
<TrashIcon />
</IconButton>
<ToolbarSeparator />
<ToolbarGroup lastChild>
<ElementWithMenu
element={
<IconButton
size="small"
color="default"
tooltip={t`Toggle/edit grid`}
>
<GridIcon />
</IconButton>
}
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(),
},
]}
/>
<ToolbarSeparator />
<IconButton
size="small"
color="default"
onClick={props.onOpenSettings}
tooltip={t`Open settings`}
>
{props.settingsIcon || <EditSceneIcon />}
</IconButton>
</ToolbarGroup>
</>
);
};
export default Toolbar;

View File

@@ -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<?number>(null);
const startYRef = React.useRef<?number>(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;

View File

@@ -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': <Trans>Objects</Trans>,
properties: <Trans>Instance properties</Trans>,
'object-groups-list': <Trans>Objects groups</Trans>,
'instances-list': <Trans>Instances</Trans>,
'layers-list': <Trans>Layers</Trans>,
};
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<SelectedTags>([]);
const { values } = React.useContext(PreferencesContext);
const screenType = useScreenType();
const instancesPropertiesEditorRef = React.useRef<?InstancePropertiesEditorInterface>(
null
);
const layersListRef = React.useRef<?LayersListInterface>(null);
const instancesListRef = React.useRef<?InstancesList>(null);
const editorRef = React.useRef<?InstancesEditor>(null);
const objectsListRef = React.useRef<?ObjectsListInterface>(null);
const objectGroupsListRef = React.useRef<?ObjectGroupsList>(null);
const [selectedEditorId, setSelectedEditorId] = React.useState<?EditorId>(
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<gdInitialInstance>, multiSelect: boolean) => {
onSelectInstances(instances, multiSelect, 'upperCenter');
forceUpdateInstancesList();
forceUpdateInstancesPropertiesEditor();
},
[
forceUpdateInstancesList,
forceUpdateInstancesPropertiesEditor,
onSelectInstances,
]
);
const getAllObjectTags = React.useCallback(
(): Array<string> => {
const tagsSet: Set<string> = 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<any> => {
return buildTagsMenuTemplate({
noTagLabel: i18n._(t`No tags - add a tag to an object first`),
getAllTags: getAllObjectTags,
selectedTags: selectedObjectTags,
onChange: setSelectedObjectTags,
});
},
[selectedObjectTags, getAllObjectTags]
);
return (
<FullSizeMeasurer>
{({ width, height }) => (
<div style={styles.container}>
<InstancesEditor
ref={editorRef}
height={height}
width={width}
project={project}
layout={layout}
selectedLayer={selectedLayer}
screenType={screenType}
initialInstances={initialInstances}
instancesEditorSettings={props.instancesEditorSettings}
onInstancesEditorSettingsMutated={
props.onInstancesEditorSettingsMutated
}
instancesSelection={props.instancesSelection}
onInstancesAdded={props.onInstancesAdded}
onInstancesSelected={props.onInstancesSelected}
onInstanceDoubleClicked={props.onInstanceDoubleClicked}
onInstancesMoved={props.onInstancesMoved}
onInstancesResized={props.onInstancesResized}
onInstancesRotated={props.onInstancesRotated}
selectedObjectNames={props.selectedObjectNames}
onContextMenu={props.onContextMenu}
isInstanceOf3DObject={props.isInstanceOf3DObject}
instancesEditorShortcutsCallbacks={
props.instancesEditorShortcutsCallbacks
}
pauseRendering={!props.isActive}
showObjectInstancesIn3D={values.use3DEditor}
/>
<div style={styles.bottomContainer}>
<SwipeableDrawer
maxHeight={height}
title={
selectedEditorId ? editorTitleById[selectedEditorId] : null
}
openingState={drawerOpeningState}
setOpeningState={setDrawerOpeningState}
topBarControls={
selectedEditorId === 'objects-list'
? [
<TagsButton
key="tags"
size="small"
buildMenuTemplate={buildObjectTagsMenuTemplate}
/>,
]
: null
}
>
{selectedEditorId === 'objects-list' && (
<I18n>
{({ i18n }) => (
<ObjectsList
getThumbnail={ObjectsRenderingService.getThumbnail.bind(
ObjectsRenderingService
)}
project={project}
objectsContainer={layout}
layout={layout}
onSelectAllInstancesOfObjectInLayout={
props.onSelectAllInstancesOfObjectInLayout
}
resourceManagementProps={props.resourceManagementProps}
selectedObjectNames={props.selectedObjectNames}
canInstallPrivateAsset={props.canInstallPrivateAsset}
onEditObject={props.onEditObject}
onExportObject={props.onExportObject}
onDeleteObject={(objectWithContext, cb) =>
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
}
/>
)}
</I18n>
)}
{selectedEditorId === 'properties' && (
<I18n>
{({ i18n }) => (
<InstancePropertiesEditor
i18n={i18n}
project={project}
layout={layout}
instances={selectedInstances}
editInstanceVariables={props.editInstanceVariables}
onEditObjectByName={props.editObjectByName}
onInstancesModified={forceUpdateInstancesList}
onGetInstanceSize={getInstanceSize}
ref={instancesPropertiesEditorRef}
unsavedChanges={props.unsavedChanges}
historyHandler={props.historyHandler}
/>
)}
</I18n>
)}
{selectedEditorId === 'object-groups-list' && (
<I18n>
{({ i18n }) => (
<ObjectGroupsList
ref={objectGroupsListRef}
globalObjectGroups={project.getObjectGroups()}
objectGroups={layout.getObjectGroups()}
onEditGroup={props.onEditObjectGroup}
onDeleteGroup={props.onDeleteObjectGroup}
onRenameGroup={props.onRenameObjectGroup}
canRenameGroup={(newName, global) =>
props.canRenameObjectGroup(newName, global, i18n)
}
beforeSetAsGlobalGroup={groupName =>
props.canObjectOrGroupBeGlobal(i18n, groupName)
}
unsavedChanges={props.unsavedChanges}
/>
)}
</I18n>
)}
{selectedEditorId === 'instances-list' && (
<Paper
background="medium"
square
style={styles.instancesListContainer}
>
<InstancesList
instances={initialInstances}
selectedInstances={selectedInstances}
onSelectInstances={selectInstances}
ref={instancesListRef}
/>
</Paper>
)}
{selectedEditorId === 'layers-list' && (
<LayersList
project={project}
selectedLayer={selectedLayer}
onSelectLayer={props.onSelectLayer}
onEditLayerEffects={props.editLayerEffects}
onEditLayer={props.editLayer}
onRemoveLayer={props.onRemoveLayer}
onRenameLayer={props.onRenameLayer}
onCreateLayer={forceUpdateInstancesPropertiesEditor}
layersContainer={layout}
unsavedChanges={props.unsavedChanges}
ref={layersListRef}
hotReloadPreviewButtonProps={
props.hotReloadPreviewButtonProps
}
/>
)}
</SwipeableDrawer>
<BottomToolbar
selectedEditorId={
drawerOpeningState === 'closed' ? null : selectedEditorId
}
onSelectEditor={halfOpenOrCloseDrawerOnEditor}
/>
</div>
</div>
)}
</FullSizeMeasurer>
);
});
export default SwipeableDrawerEditorsDisplay;

File diff suppressed because it is too large Load Diff

View File

@@ -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<MenuItemTemplate>,
size?: 'small',
|};
export default function TagsButton(props: Props) {
return (
<ElementWithMenu
element={
<IconButton style={styles.container}>
<IconButton
style={
props.size === 'small'
? styles.smallContainer
: styles.mediumContainer
}
>
<Filter htmlColor="inherit" style={styles.icon} />
</IconButton>
}

View File

@@ -57,6 +57,9 @@ type Props = {|
acceleratorString?: string,
'aria-label'?: string,
disableRipple?: boolean,
disableFocusRipple?: boolean,
color?: 'default',
|};

View File

@@ -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()}
>

View File

@@ -22,7 +22,8 @@ export default class ElementWithMenu extends React.Component<Props, State> {
_contextMenu: ?ContextMenuInterface;
_wrappedElement: ?any;
open = () => {
open = (event?: Event) => {
if (event && event.stopPropagation) event.stopPropagation();
const { _contextMenu } = this;
if (!_contextMenu) return;

View File

@@ -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;
}

View File

@@ -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 (
<MuiPaper
id={id}

View File

@@ -63,6 +63,13 @@
}
}
},
"swipeable-drawer": {
"top-bar": {
"pill-color": {
"value": "#0f0f12"
}
}
},
"icon-button": {
"selected": {
"color": {

View File

@@ -469,6 +469,11 @@ export function createGdevelopTheme({
backgroundColor: styles['ThemeSurfaceToolbarBackgroundColor'],
separatorColor: styles['ThemeToolbarSeparatorColor'],
},
swipeableDrawer: {
topBar: {
pillColor: styles['ThemeSwipeableDrawerTopBarPillColor'],
},
},
text: {
color: {
primary: styles['ThemeTextDefaultColor'],

View File

@@ -168,6 +168,13 @@
}
}
},
"swipeable-drawer": {
"top-bar": {
"pill-color": {
"value": "#1D1D26"
}
}
},
"icon-button": {
"selected": {
"color": {

View File

@@ -153,6 +153,13 @@
}
}
},
"swipeable-drawer": {
"top-bar": {
"pill-color": {
"value": "#7F7F85"
}
}
},
"icon-button": {
"selected": {
"color": {

View File

@@ -6,6 +6,13 @@
color: var(--table-text-color);
}
.gd-table .tableHeaderRow {
/* react-virtualized adds a padding right directly on the header row element
* so the !important is needed to override it. It is set to 0 to avoid having
* a gap between the right part of the screen/container and the scroll bar. */
padding-right: 0px !important;
}
.gd-table .tableHeaderRow,
.gd-table .tableEvenRow,
.gd-table .tableSelectedRow,

View File

@@ -59,6 +59,13 @@
}
}
},
"swipeable-drawer": {
"top-bar": {
"pill-color": {
"value": "#1c1f26"
}
}
},
"icon-button": {
"selected": {
"color": {

View File

@@ -71,6 +71,13 @@
}
}
},
"swipeable-drawer": {
"top-bar": {
"pill-color": {
"value": "#14161a"
}
}
},
"icon-button": {
"selected": {
"color": {

View File

@@ -59,6 +59,13 @@
}
}
},
"swipeable-drawer": {
"top-bar": {
"pill-color": {
"value": "#907aa9"
}
}
},
"icon-button": {
"color": {
"value": "#1f1d2e"

View File

@@ -60,6 +60,13 @@
}
}
},
"swipeable-drawer": {
"top-bar": {
"pill-color": {
"value": "#001419"
}
}
},
"icon-button": {
"color": {
"value": "#002B36"

View File

@@ -1,6 +1,5 @@
// @flow
import * as React from 'react';
import GDevelopThemeContext from '../Theme/GDevelopThemeContext';
const styles = {
row: {
@@ -22,15 +21,12 @@ type TreeTableRowProps = {|
|};
export const TreeTableRow = (props: TreeTableRowProps) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<div
id={props.id}
style={{
...styles.row,
alignItems: props.alignItems,
backgroundColor: gdevelopTheme.list.itemsBackgroundColor,
}}
>
{props.children}