Compare commits

...

53 Commits

Author SHA1 Message Date
AlexandreSi
ca3944a2fc Fix typing 2024-04-10 17:55:13 +02:00
AlexandreSi
15f0210572 Move applyRatio util to Rendered3DInstance class as static method 2024-04-10 17:38:19 +02:00
AlexandreSi
4a76115684 Remove rounding operations 2024-04-10 17:25:35 +02:00
AlexandreSi
8bd7f69c30 Display number value as string in CompactTextField 2024-04-10 17:10:22 +02:00
Alex
9652b1e292 Update test 2024-04-10 17:10:15 +02:00
AlexandreSi
ebec6e3b9e Prettier 2024-04-10 16:08:16 +02:00
AlexandreSi
b40199d669 Prettier 2024-04-10 16:08:16 +02:00
AlexandreSi
3ef4257953 Reduce intermediary values to prevent precision issues 2024-04-10 16:08:16 +02:00
AlexandreSi
a67bb9c9bc Fix react key warning 2024-04-10 16:08:16 +02:00
AlexandreSi
2e71b55f50 Add comment 2024-04-10 16:08:16 +02:00
AlexandreSi
57d9c6c4d6 Change edit button icon according to 2D/3D instance 2024-04-10 16:08:16 +02:00
AlexandreSi
6de95f0b27 Add theme attributes to other themes 2024-04-10 16:08:15 +02:00
AlexandreSi
c0176c7de4 Fix stories 2024-04-10 16:08:15 +02:00
AlexandreSi
787b584bcf Add section in them for text fields 2024-04-10 16:08:15 +02:00
AlexandreSi
c33814f63f Improve value computing keeping ratio to avoid precision issues 2024-04-10 16:08:15 +02:00
AlexandreSi
0ea05689ea Prettier 2024-04-10 16:08:15 +02:00
AlexandreSi
f8a46fd9c1 Use keepRatio flag to update width/height/depth accordingly 2024-04-10 16:08:15 +02:00
AlexandreSi
8de642c3d3 Add new attribute on instance to define if dimensions should keep the same ratio 2024-04-10 16:08:15 +02:00
AlexandreSi
a9d4e19823 Use compact text fields end adornement to reset hasCustomSize flag 2024-04-10 16:08:15 +02:00
AlexandreSi
7259d58894 Add possibility to add end adornment to compact text field 2024-04-10 16:08:15 +02:00
AlexandreSi
41f502e841 Fix instance properties panel title 2024-04-10 16:08:15 +02:00
AlexandreSi
b54aa51847 Add separator above section title in compact properties editor 2024-04-10 16:08:15 +02:00
AlexandreSi
979fe73c19 Improve performance of icon control 2024-04-10 16:08:15 +02:00
AlexandreSi
94f12b6c2b Change compact textfields semicontrol strategy 2024-04-10 16:08:15 +02:00
AlexandreSi
e04d3d2ff0 Add todo 2024-04-10 16:08:15 +02:00
AlexandreSi
a215445199 Use compact instance variables in editor 2024-04-10 16:08:15 +02:00
AlexandreSi
a5bc734982 Make in app tutorial element highlighter work well with compact fields 2024-04-10 16:08:15 +02:00
AlexandreSi
865282ccd4 Add util component to easily add highlighter to story and add it to compact fields stories 2024-04-10 16:08:15 +02:00
AlexandreSi
13e5c2c4cd Fix flow 2024-04-10 16:08:15 +02:00
AlexandreSi
ef614a15e9 Add boolean icon property with vertical bar 2024-04-10 16:08:15 +02:00
AlexandreSi
92ffcc8773 Fix shrinking issues 2024-04-10 16:08:15 +02:00
AlexandreSi
9aef64e410 Add possibility to prevent wrapping 2024-04-10 16:08:15 +02:00
AlexandreSi
b1dd950c9c Display animation field in a section 2024-04-10 16:08:15 +02:00
AlexandreSi
de1272c12f Extract instance properties schema making logic in dedicated file 2024-04-10 16:08:15 +02:00
AlexandreSi
13a0d266b9 Improve layout 2024-04-10 16:08:15 +02:00
AlexandreSi
c5d643a61c Add new booleanIcon field 2024-04-10 16:08:15 +02:00
AlexandreSi
da2364fd76 Initialize Compact instance properties editor 2024-04-10 16:08:15 +02:00
AlexandreSi
a89fd28c10 Few fixes 2024-04-10 16:08:15 +02:00
AlexandreSi
26534d617f Add story for 3D instance properties editor 2024-04-10 16:08:15 +02:00
AlexandreSi
0c3d9b1e98 Fix Instance properties editor story 2024-04-10 16:08:15 +02:00
AlexandreSi
05ee5046d7 Add possibilty to display error in compact semi controlled textfield 2024-04-10 16:08:15 +02:00
AlexandreSi
34c86457db Handle click on label to focus input 2024-04-10 16:08:15 +02:00
AlexandreSi
35de1fe1b5 WIP Add semi controlled text field on top of compact text field 2024-04-10 16:08:15 +02:00
AlexandreSi
62a4595b51 Add styling for dropdown on Windows 2024-04-10 16:08:15 +02:00
AlexandreSi
bab4cb0d23 Improve compact text field for numbers 2024-04-10 16:08:15 +02:00
AlexandreSi
e872d90eac Use left icon as number value control 2024-04-10 16:08:15 +02:00
AlexandreSi
d995ed96e2 Add tooltip on left icon 2024-04-10 16:08:15 +02:00
AlexandreSi
30d588dde4 Add possibility to specify left icon for compact select field 2024-04-10 16:08:15 +02:00
AlexandreSi
73af8de8e6 Add possibility to specify left icon for compact text field 2024-04-10 16:08:14 +02:00
AlexandreSi
ac2e589a76 Add hover effect on errored texts 2024-04-10 16:08:14 +02:00
AlexandreSi
3dc7331b2f Use hidden option 2024-04-10 16:08:14 +02:00
AlexandreSi
499b1988c3 Initialize select component 2024-04-10 16:08:14 +02:00
AlexandreSi
f2cba25435 Initialize component 2024-04-10 16:08:14 +02:00
55 changed files with 4320 additions and 42 deletions

View File

@@ -15,7 +15,7 @@
namespace gd {
gd::String* InitialInstance::badStringProperyValue = NULL;
gd::String* InitialInstance::badStringPropertyValue = NULL;
InitialInstance::InitialInstance()
: objectName(""),
@@ -34,6 +34,7 @@ InitialInstance::InitialInstance()
depth(0),
locked(false),
sealed(false),
keepRatio(true),
persistentUuid(UUID::MakeUuid4()) {}
void InitialInstance::UnserializeFrom(const SerializerElement& element) {
@@ -58,6 +59,7 @@ void InitialInstance::UnserializeFrom(const SerializerElement& element) {
SetLayer(element.GetStringAttribute("layer"));
SetLocked(element.GetBoolAttribute("locked", false));
SetSealed(element.GetBoolAttribute("sealed", false));
SetShouldKeepRatio(element.GetBoolAttribute("keepRatio", false));
persistentUuid = element.GetStringAttribute("persistentUuid");
if (persistentUuid.empty()) ResetPersistentUuid();
@@ -120,6 +122,7 @@ void InitialInstance::SerializeTo(SerializerElement& element) const {
if (HasCustomDepth()) element.SetAttribute("depth", GetCustomDepth());
if (IsLocked()) element.SetAttribute("locked", IsLocked());
if (IsSealed()) element.SetAttribute("sealed", IsSealed());
if (ShouldKeepRatio()) element.SetAttribute("keepRatio", ShouldKeepRatio());
if (persistentUuid.empty()) persistentUuid = UUID::MakeUuid4();
element.SetStringAttribute("persistentUuid", persistentUuid);
@@ -188,10 +191,10 @@ double InitialInstance::GetRawDoubleProperty(const gd::String& name) const {
const gd::String& InitialInstance::GetRawStringProperty(
const gd::String& name) const {
if (!badStringProperyValue) badStringProperyValue = new gd::String("");
if (!badStringPropertyValue) badStringPropertyValue = new gd::String("");
const auto& it = stringProperties.find(name);
return it != stringProperties.end() ? it->second : *badStringProperyValue;
return it != stringProperties.end() ? it->second : *badStringPropertyValue;
}
void InitialInstance::SetRawDoubleProperty(const gd::String& name,

View File

@@ -206,6 +206,17 @@ class GD_CORE_API InitialInstance {
*/
void SetSealed(bool enable = true) { sealed = enable; }
/**
* \brief Return true if the dimensions (width, height and depth) should keep
* the same ratio.
*/
bool ShouldKeepRatio() const { return keepRatio; };
/**
* \brief Define if instance's dimensions should keep the same ratio.
*/
void SetShouldKeepRatio(bool enable = true) { keepRatio = enable; }
///@}
/** \name Variable management
@@ -340,11 +351,13 @@ class GD_CORE_API InitialInstance {
gd::VariablesContainer initialVariables; ///< Instance specific variables
bool locked; ///< True if the instance is locked
bool sealed; ///< True if the instance is sealed
bool keepRatio; ///< True if the instance's dimensions
/// should keep the same ratio.
mutable gd::String persistentUuid; ///< A persistent random version 4 UUID,
///< useful for hot reloading.
/// useful for hot reloading.
static gd::String*
badStringProperyValue; ///< Empty string returned by GetRawStringProperty
badStringPropertyValue; ///< Empty string returned by GetRawStringProperty
};
} // namespace gd

View File

@@ -3134,14 +3134,51 @@ module.exports = {
modelDepth < epsilon
? Number.POSITIVE_INFINITY
: originalDepth / modelDepth;
let scaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
if (!Number.isFinite(scaleRatio)) {
scaleRatio = 1;
}
const minScaleRatio = Math.min(widthRatio, heightRatio, depthRatio);
if (!Number.isFinite(minScaleRatio)) {
this._defaultWidth = modelWidth;
this._defaultHeight = modelHeight;
this._defaultDepth = modelDepth;
} else {
if (widthRatio === minScaleRatio) {
this._defaultWidth = originalWidth;
this._defaultHeight = Rendered3DInstance.applyRatio({
oldReferenceValue: modelWidth,
newReferenceValue: originalWidth,
valueToApplyTo: modelHeight,
});
this._defaultDepth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelWidth,
newReferenceValue: originalWidth,
valueToApplyTo: modelDepth,
});
} else if (heightRatio === minScaleRatio) {
this._defaultWidth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelHeight,
newReferenceValue: originalHeight,
valueToApplyTo: modelWidth,
});
this._defaultWidth = scaleRatio * modelWidth;
this._defaultHeight = scaleRatio * modelHeight;
this._defaultDepth = scaleRatio * modelDepth;
this._defaultHeight = originalHeight;
this._defaultDepth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelHeight,
newReferenceValue: originalHeight,
valueToApplyTo: modelDepth,
});
} else {
this._defaultWidth = Rendered3DInstance.applyRatio({
oldReferenceValue: modelDepth,
newReferenceValue: originalDepth,
valueToApplyTo: modelWidth,
});
this._defaultHeight = Rendered3DInstance.applyRatio({
oldReferenceValue: modelDepth,
newReferenceValue: originalDepth,
valueToApplyTo: modelHeight,
});
this._defaultDepth = originalDepth;
}
}
}
}

View File

@@ -111,6 +111,19 @@ class Rendered3DInstance {
*/
static toRad(angleInDegrees: number): number;
/**
* Applies ratio to value without intermediary value to avoid precision issues.
*/
static applyRatio({
oldReferenceValue,
newReferenceValue,
valueToApplyTo,
}: {
oldReferenceValue: number;
newReferenceValue: number;
valueToApplyTo: number;
}): number;
/**
* Called when the scene editor is rendered.
*/

View File

@@ -1175,6 +1175,8 @@ interface InitialInstance {
void SetLocked(boolean lock);
boolean IsSealed();
void SetSealed(boolean seal);
boolean ShouldKeepRatio();
void SetShouldKeepRatio(boolean keepRatio);
long GetZOrder();
void SetZOrder(long zOrder);
[Const, Ref] DOMString GetLayer();
@@ -1855,7 +1857,7 @@ interface BehaviorMetadata {
[Ref] Behavior Get();
BehaviorsSharedData GetSharedDataInstance();
[Value] MapStringPropertyDescriptor GetProperties();
[Value] MapStringPropertyDescriptor GetSharedProperties();
};

View File

@@ -1025,6 +1025,8 @@ export class InitialInstance extends EmscriptenObject {
setLocked(lock: boolean): void;
isSealed(): boolean;
setSealed(seal: boolean): void;
shouldKeepRatio(): boolean;
setShouldKeepRatio(keepRatio: boolean): void;
getZOrder(): number;
setZOrder(zOrder: number): void;
getLayer(): string;

View File

@@ -19,6 +19,8 @@ declare class gdInitialInstance {
setLocked(lock: boolean): void;
isSealed(): boolean;
setSealed(seal: boolean): void;
shouldKeepRatio(): boolean;
setShouldKeepRatio(keepRatio: boolean): void;
getZOrder(): number;
setZOrder(zOrder: number): void;
getLayer(): string;

View File

@@ -356,6 +356,55 @@
"badge-color": {
"value": "#6BAFFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FFC2B4",
"comment": "Palette/Red/20"
},
"border-color": {
"value": "#DDD1FF",
"comment": "Palette/Purple/10"
},
"caret-color": {
"value": "#C9B6FC",
"comment": "Palette/Purple/20"
}
},
"disabled": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"placeholder": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"default": {
"background-color": {
"value": "#32323B",
"comment": "Palette/Grey/80"
},
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
}
},
"hover": {
"border-color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(221, 209, 255, 0.16)"
}
}
}
},
"input": {

View File

@@ -0,0 +1,440 @@
// @flow
import * as React from 'react';
import { mapFor } from '../Utils/MapFor';
import { type Schema, type Instance } from '.';
import { type ResourceKind } from '../ResourcesList/ResourceSource';
import { type Field } from '.';
import MeasurementUnitDocumentation from '../PropertiesEditor/MeasurementUnitDocumentation';
const createField = (
name: string,
property: gdPropertyDescriptor,
getProperties: (instance: Instance) => any,
onUpdateProperty: (
instance: Instance,
propertyName: string,
newValue: string
) => void,
object: ?gdObject
): ?Field => {
const propertyDescription = property.getDescription();
const getLabel = (instance: Instance) => {
const propertyName = getProperties(instance)
.get(name)
.getLabel();
if (propertyName) return propertyName;
return (
name.charAt(0).toUpperCase() +
name
.slice(1)
.split(/(?=[A-Z])/)
.join(' ')
);
};
const getDescription = () => propertyDescription;
const getEndAdornment = (instance: Instance) => {
const property = getProperties(instance).get(name);
const measurementUnit = property.getMeasurementUnit();
return {
label: getMeasurementUnitShortLabel(measurementUnit),
tooltipContent: (
<MeasurementUnitDocumentation
label={measurementUnit.getLabel()}
description={measurementUnit.getDescription()}
elementsWithWords={measurementUnit.getElementsWithWords()}
/>
),
};
};
const valueType = property.getType().toLowerCase();
if (valueType === 'number') {
return {
name,
valueType,
getValue: (instance: Instance): number => {
return (
parseFloat(
getProperties(instance)
.get(name)
.getValue()
) || 0
); // Consider a missing value as 0 to avoid propagating NaN.
},
setValue: (instance: Instance, newValue: number) => {
onUpdateProperty(instance, name, '' + newValue);
},
getLabel,
getDescription,
getEndAdornment,
};
} else if (valueType === 'string' || valueType === '') {
return {
name,
valueType: 'string',
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)
.getValue();
},
setValue: (instance: Instance, newValue: string) => {
onUpdateProperty(instance, name, newValue);
},
getLabel,
getDescription,
};
} else if (valueType === 'boolean') {
return {
name,
valueType,
getValue: (instance: Instance): boolean => {
return (
getProperties(instance)
.get(name)
.getValue() === 'true'
);
},
setValue: (instance: Instance, newValue: boolean) => {
onUpdateProperty(instance, name, newValue ? '1' : '0');
},
getLabel,
getDescription,
};
} else if (valueType === 'choice') {
// Choice is a "string" (with a selector for the user in the UI)
const choices = property
.getExtraInfo()
.toJSArray()
.map(value => ({ value, label: value }));
return {
name,
valueType: 'string',
getChoices: () => choices,
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)
.getValue();
},
setValue: (instance: Instance, newValue: string) => {
onUpdateProperty(instance, name, newValue);
},
getLabel,
getDescription,
};
} else if (valueType === 'behavior') {
const behaviorType =
property.getExtraInfo().size() > 0 ? property.getExtraInfo().at(0) : '';
return {
name,
valueType: 'string',
getChoices: () => {
return !object || behaviorType === ''
? []
: object
.getAllBehaviorNames()
.toJSArray()
.map(name =>
object.getBehavior(name).getTypeName() === behaviorType
? name
: null
)
.filter(Boolean)
.map(value => ({ value, label: value }));
},
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)
.getValue();
},
setValue: (instance: Instance, newValue: string) => {
onUpdateProperty(instance, name, newValue);
},
getLabel,
getDescription,
};
} else if (valueType === 'resource') {
// Resource is a "string" (with a selector in the UI)
const extraInfos = property.getExtraInfo().toJSArray();
// $FlowFixMe - assume the passed resource kind is always valid.
const kind: ResourceKind = extraInfos[0] || '';
// $FlowFixMe - assume the passed resource kind is always valid.
const fallbackKind: ResourceKind = extraInfos[1] || '';
return {
name,
valueType: 'resource',
resourceKind: kind,
fallbackResourceKind: fallbackKind,
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)
.getValue();
},
setValue: (instance: Instance, newValue: string) => {
onUpdateProperty(instance, name, newValue);
},
getLabel,
getDescription,
};
} else if (valueType === 'color') {
return {
name,
valueType: 'color',
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)
.getValue();
},
setValue: (instance: Instance, newValue: string) => {
onUpdateProperty(instance, name, newValue);
},
getLabel,
getDescription,
};
} else if (valueType === 'textarea') {
return {
name,
valueType: 'textarea',
getValue: (instance: Instance): string => {
return getProperties(instance)
.get(name)
.getValue();
},
setValue: (instance: Instance, newValue: string) => {
onUpdateProperty(instance, name, newValue);
},
getLabel,
getDescription,
};
} else {
console.error(
`A property with type=${valueType} could not be mapped to a field. Ensure that this type is correct and understood by the IDE.`
);
return null;
}
};
const propertyKeywordCouples: Array<Array<string>> = [
['X', 'Y', 'Z'],
['Width', 'Height', 'Depth'],
['Top', 'Bottom'],
['Left', 'Right'],
['Front', 'Back'],
['Up', 'Down'],
['Min', 'Max'],
['Low', 'High'],
['Color', 'Opacity'],
['Horizontal', 'Vertical'],
['Acceleration', 'Deceleration'],
['Duration', 'Easing'],
['EffectName', 'EffectProperty'],
['Gravity', 'MaxFallingSpeed'],
['JumpSpeed', 'JumpSustainTime'],
['XGrabTolerance', 'YGrabOffset'],
['MaxSpeed', 'SlopeMaxAngle'],
];
const uncapitalize = str => {
if (!str) return str;
return str[0].toLowerCase() + str.substr(1);
};
/**
* Return true when the property exists and should be displayed.
*
* @param properties The properties
* @param name The property name
* @param visibility `true` when only deprecated properties must be displayed
* and `false` when only not deprecated ones must be displayed
*/
const isPropertyVisible = (
properties: gdMapStringPropertyDescriptor,
name: string,
visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated'
): boolean => {
if (!properties.has(name)) {
return false;
}
const property = properties.get(name);
if (property.isHidden()) {
return false;
}
if (visibility === 'All') {
return true;
}
if (visibility === 'Deprecated') {
return property.isDeprecated();
}
if (visibility === 'Advanced') {
return property.isAdvanced();
}
if (visibility === 'Basic') {
return !property.isAdvanced() && !property.isDeprecated();
}
return true;
};
/**
* Transform a MapStringPropertyDescriptor to a schema that can be used
* in CompactPropertiesEditor.
* This method is similar to PropertiesMapToSchema.propertiesMapToSchema but returns
* fields compatible with CompactPropertiesEditor.
*
* @param properties The properties to use
* @param getProperties The function called to read again the properties
* @param onUpdateProperty The function called to update a property of an object
*/
const propertiesMapToSchema = (
properties: gdMapStringPropertyDescriptor,
getProperties: (instance: Instance) => any,
onUpdateProperty: (
instance: Instance,
propertyName: string,
newValue: string
) => void,
object: ?gdObject,
visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated' = 'All'
): Schema => {
const propertyNames = properties.keys();
// Aggregate field by groups to be able to build field groups with a title.
const fieldsByGroups = new Map<string, Array<Field>>();
const alreadyHandledProperties = new Set<string>();
mapFor(0, propertyNames.size(), i => {
const name = propertyNames.at(i);
const property = properties.get(name);
if (!isPropertyVisible(properties, name, visibility)) {
return null;
}
if (alreadyHandledProperties.has(name)) return null;
const groupName = property.getGroup() || '';
let fields = fieldsByGroups.get(groupName);
if (!fields) {
fields = [];
fieldsByGroups.set(groupName, fields);
}
// Search a property couple that can be put in a row.
let field: ?Field = null;
for (const propertyKeywords of propertyKeywordCouples) {
const rowPropertyNames: string[] = [];
for (let index = 0; index < propertyKeywords.length; index++) {
const keyword = propertyKeywords[index];
if (name.includes(keyword)) {
const rowAllPropertyNames = propertyKeywords.map(otherKeyword =>
name.replace(keyword, otherKeyword)
);
for (const rowPropertyName of rowAllPropertyNames) {
if (isPropertyVisible(properties, rowPropertyName, visibility)) {
rowPropertyNames.push(rowPropertyName);
}
}
}
const uncapitalizeKeyword = uncapitalize(keyword);
if (name.startsWith(uncapitalizeKeyword)) {
const rowAllPropertyNames = propertyKeywords.map(otherKeyword =>
name.replace(uncapitalizeKeyword, uncapitalize(otherKeyword))
);
for (const rowPropertyName of rowAllPropertyNames) {
if (isPropertyVisible(properties, rowPropertyName, visibility)) {
rowPropertyNames.push(rowPropertyName);
}
}
}
}
if (rowPropertyNames.length > 1) {
const rowProperties = rowPropertyNames.map(name =>
properties.get(name)
);
if (
rowProperties.every(
property => property.getGroup() === rowProperties[0].getGroup()
)
) {
const rowFields: Field[] = [];
for (
let index = 0;
index < rowProperties.length && index < rowPropertyNames.length;
index++
) {
const rowProperty = rowProperties[index];
const rowPropertyName = rowPropertyNames[index];
const field = createField(
rowPropertyName,
rowProperty,
getProperties,
onUpdateProperty,
object
);
if (field) {
rowFields.push(field);
}
}
if (rowFields.length === rowProperties.length) {
field = {
name: rowPropertyNames.join('-'),
type: 'row',
children: rowFields,
};
rowPropertyNames.forEach(propertyName => {
alreadyHandledProperties.add(propertyName);
});
}
}
}
}
if (!field) {
field = createField(
name,
property,
getProperties,
onUpdateProperty,
object
);
}
if (field) {
fields.push(field);
}
});
if (fieldsByGroups.size === 0) {
return [];
}
const defaultGroupField = fieldsByGroups.get('');
if (fieldsByGroups.size === 1 && defaultGroupField) {
// Avoid to create a blank title
return defaultGroupField;
}
// Create a group for the default one too because it would look weird with the indentation.
const groupNames = [...fieldsByGroups.keys()].sort((a, b) =>
a.localeCompare(b)
);
return groupNames.map(groupName => ({
name: groupName,
type: 'column',
title: groupName,
// The group actually always exists here.
children: fieldsByGroups.get(groupName) || [],
}));
};
const exponents = ['⁰', '¹', '²', '³', '⁴', '⁵'];
export const getMeasurementUnitShortLabel = (
measurementUnit: gdMeasurementUnit
): string => {
return mapFor(0, measurementUnit.getElementsCount(), i => {
const baseUnit = measurementUnit.getElementBaseUnit(i);
const power = measurementUnit.getElementPower(i);
const absPower = Math.abs(power);
const showPower = power < 0 || (absPower > 1 && absPower < 6);
return (
baseUnit.getSymbol() +
(power < 0 ? '⁻' : '') +
(showPower ? exponents[absPower] : '')
);
}).join(' · ');
};
export default propertiesMapToSchema;

View File

@@ -0,0 +1,791 @@
// @flow
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import ResourceSelectorWithThumbnail from '../ResourcesList/ResourceSelectorWithThumbnail';
import SelectOption from '../UI/SelectOption';
import { type MenuItemTemplate } from '../UI/Menu/Menu.flow';
import {
type ResourceKind,
type ResourceManagementProps,
} from '../ResourcesList/ResourceSource';
import {
ResponsiveLineStackLayout,
ColumnStackLayout,
LineStackLayout,
} from '../UI/Layout';
import CompactSelectField from '../UI/CompactSelectField';
import CompactSemiControlledTextField from '../UI/CompactSemiControlledTextField';
import CompactSemiControlledNumberField from '../UI/CompactSemiControlledNumberField';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import { Line, marginsSize } from '../UI/Grid';
import Text from '../UI/Text';
import useForceUpdate from '../Utils/UseForceUpdate';
import Edit from '../UI/CustomSvgIcons/Edit';
import IconButton from '../UI/IconButton';
import FlatButton from '../UI/FlatButton';
import VerticallyCenterWithBar from '../UI/VerticallyCenterWithBar';
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
import { textEllipsisStyle } from '../UI/TextEllipsis';
// An "instance" here is the objects for which properties are shown
export type Instance = Object; // This could be improved using generics.
export type Instances = Array<Instance>;
// "Value" fields are fields displayed in the properties.
export type ValueFieldCommonProperties = {|
name: string,
getLabel?: Instance => string,
getDescription?: Instance => string,
getExtraDescription?: Instance => string,
disabled?: boolean | ((instances: Array<gdInitialInstance>) => boolean),
onEditButtonBuildMenuTemplate?: (i18n: I18nType) => Array<MenuItemTemplate>,
onEditButtonClick?: () => void,
|};
// "Primitive" value fields are "simple" fields.
export type PrimitiveValueField =
| {|
valueType: 'number',
getValue: Instance => number,
setValue: (instance: Instance, newValue: number) => void,
// TODO: support this attribute.
getEndAdornment?: Instance => {|
label: string,
tooltipContent: React.Node,
|},
getEndAdornmentIcon?: () => React.Node,
onClickEndAdornment?: Instance => void,
renderLeftIcon?: (className?: string) => React.Node,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'string',
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
getChoices?: ?() => Array<{|
value: string,
label: string,
labelIsUserDefined?: boolean,
|}>,
getEndAdornmentIcon?: () => React.Node,
onClickEndAdornment?: Instance => void,
renderLeftIcon?: (className?: string) => React.Node,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'boolean',
getValue: Instance => boolean,
setValue: (instance: Instance, newValue: boolean) => void,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'enumIcon',
renderIcon: (value: any) => React.Node,
getValue: Instance => any,
isHighlighted: (value: any) => boolean,
setValue: (instance: Instance, newValue: any) => void,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'color',
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
...ValueFieldCommonProperties,
|}
| {|
valueType: 'textarea',
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
...ValueFieldCommonProperties,
|};
// "Resource" fields are showing a resource selector.
type ResourceField = {|
valueType: 'resource',
resourceKind: ResourceKind,
fallbackResourceKind?: ResourceKind,
getValue: Instance => string,
setValue: (instance: Instance, newValue: string) => void,
renderLeftIcon?: (className?: string) => React.Node,
...ValueFieldCommonProperties,
|};
type Title = {|
name: string,
renderLeftIcon: (className?: string) => React.Node,
getValue?: Instance => string,
nonFieldType: 'title',
defaultValue?: string,
|};
type SectionTitle = {|
name: string,
nonFieldType: 'sectionTitle',
getValue: typeof undefined,
|};
type VerticalCenterWithBar = {|
name: string,
nonFieldType: 'verticalCenterWithBar',
child: PrimitiveValueField,
|};
type ActionButton = {|
label: string,
disabled: 'onValuesDifferent',
getValue: Instance => string,
nonFieldType: 'button',
getIcon?: ({| fontSize: string |}) => React.Node,
onClick: (instance: Instance) => void,
|};
// A value field is a primitive or a resource.
export type ValueField = PrimitiveValueField | ResourceField;
// A field can be a primitive, a resource or a list of fields
export type Field =
| PrimitiveValueField
| ResourceField
| SectionTitle
| Title
| ActionButton
| VerticalCenterWithBar
| {|
name: string,
type: 'row' | 'column',
preventWrap?: boolean,
title?: ?string,
children: Array<Field>,
|};
// The schema is the tree of all fields.
export type Schema = Array<Field>;
type Props = {|
onInstancesModified?: Instances => void,
instances: Instances,
schema: Schema,
mode?: 'column' | 'row',
preventWrap?: boolean,
// If set, render the "extra" description content from fields
// (see getExtraDescription).
renderExtraDescriptionText?: (extraDescription: string) => string,
unsavedChanges?: ?UnsavedChanges,
// Optional context:
project?: ?gdProject,
resourceManagementProps?: ?ResourceManagementProps,
|};
const styles = {
columnContainer: {
display: 'flex',
flexDirection: 'column',
},
fieldContainer: {
flex: 1,
display: 'flex',
flexDirection: 'row',
alignItems: 'baseline',
},
field: {
flex: 1,
width: 'auto',
},
subHeader: {
paddingLeft: 0,
},
container: { flex: 1, minWidth: 0 },
separator: {
marginRight: -marginsSize,
marginLeft: -marginsSize,
marginTop: marginsSize,
borderTop: '1px solid black',
},
};
export const Separator = () => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<div
style={{
...styles.separator,
borderColor: gdevelopTheme.listItem.separatorColor,
}}
/>
);
};
const getDisabled = ({
instances,
field,
}: {|
instances: Instances,
field: ValueField,
|}): boolean => {
return typeof field.disabled === 'boolean'
? field.disabled
: typeof field.disabled === 'function'
? field.disabled(instances)
: false;
};
/**
* Get the value for the given field across all instances.
* If one of the instances doesn't share the same value, returns the default value.
* If there is no instances, returns the default value.
* If the field does not have a `getValue` method, returns `null`.
*/
const getFieldValue = ({
instances,
field,
defaultValue,
}: {|
instances: Instances,
field: ValueField | ActionButton | SectionTitle | Title,
defaultValue?: any,
|}): any => {
if (!instances[0]) {
console.log(
'getFieldValue was called with an empty list of instances (or containing undefined). This is a bug that should be fixed'
);
return defaultValue;
}
const { getValue } = field;
if (!getValue) return null;
let value = getValue(instances[0]);
for (var i = 1; i < instances.length; ++i) {
if (value !== getValue(instances[i])) {
if (typeof defaultValue !== 'undefined') value = defaultValue;
break;
}
}
return value;
};
const getFieldLabel = ({
instances,
field,
}: {|
instances: Instances,
field: ValueField,
|}): any => {
if (!instances[0]) {
console.log(
'CompactPropertiesEditor._getFieldLabel was called with an empty list of instances (or containing undefined). This is a bug that should be fixed'
);
return field.name;
}
if (field.getLabel) return field.getLabel(instances[0]);
return field.name;
};
const CompactPropertiesEditor = ({
onInstancesModified,
instances,
schema,
mode,
renderExtraDescriptionText,
unsavedChanges,
project,
resourceManagementProps,
preventWrap,
}: Props) => {
const forceUpdate = useForceUpdate();
const _onInstancesModified = React.useCallback(
(instances: Instances) => {
// This properties editor is dealing with fields that are
// responsible to update their state (see field.setValue).
if (unsavedChanges) unsavedChanges.triggerUnsavedChanges();
if (onInstancesModified) onInstancesModified(instances);
forceUpdate();
},
[unsavedChanges, onInstancesModified, forceUpdate]
);
const getFieldDescription = React.useCallback(
(field: ValueField): ?string => {
if (!instances[0]) {
console.log(
'CompactPropertiesEditor._getFieldDescription was called with an empty list of instances (or containing undefined). This is a bug that should be fixed'
);
return undefined;
}
const descriptions: Array<string> = [];
if (field.getDescription)
descriptions.push(field.getDescription(instances[0]));
if (renderExtraDescriptionText && field.getExtraDescription)
descriptions.push(
renderExtraDescriptionText(field.getExtraDescription(instances[0]))
);
return descriptions.join('\n') || undefined;
},
[instances, renderExtraDescriptionText]
);
const renderInputField = React.useCallback(
(field: ValueField) => {
if (field.name === 'PLEASE_ALSO_SHOW_EDIT_BUTTON_THANKS') return null; // This special property was used in GDevelop 4 IDE to ask for a Edit button to be shown, ignore it.
if (field.valueType === 'boolean') {
return 'TODO';
// const { setValue } = field;
// const description = getFieldDescription(field);
// return (
// <InlineCheckbox
// label={
// !description ? (
// getFieldLabel({ instances, field })
// ) : (
// <React.Fragment>
// <Line noMargin>{getFieldLabel({ instances, field })}</Line>
// <FormHelperText style={{ display: 'inline' }}>
// <MarkdownText source={description} />
// </FormHelperText>
// </React.Fragment>
// )
// }
// key={field.name}
// id={field.name}
// checked={getFieldValue({ instances, field })}
// onCheck={(event, newValue) => {
// instances.forEach(i => setValue(i, !!newValue));
// _onInstancesModified(instances);
// }}
// disabled={getDisabled({ instances, field })}
// />
// );
} else if (field.valueType === 'number') {
const { setValue, onClickEndAdornment } = field;
// TODO: Support end adornment
// const endAdornment = getEndAdornment && getEndAdornment(instances[0]);
return (
<CompactSemiControlledNumberField
value={getFieldValue({ instances, field })}
key={field.name}
id={field.name}
// floatingLabelText={getFieldLabel({ instances, field })}
// helperMarkdownText={getFieldDescription(field)}
onChange={newValue => {
// If the value is not a number, the user is probably still typing, adding a dot or a comma.
// So don't update the value, it will be reverted if they leave the field.
if (isNaN(newValue)) return;
instances.forEach(i => setValue(i, newValue));
_onInstancesModified(instances);
}}
disabled={getDisabled({ instances, field })}
renderLeftIcon={field.renderLeftIcon}
leftIconTooltip={getFieldLabel({ instances, field })}
renderEndAdornmentOnHover={field.getEndAdornmentIcon}
onClickEndAdornment={() => {
if (!onClickEndAdornment) return;
instances.forEach(i => onClickEndAdornment(i));
_onInstancesModified(instances);
}}
useLeftIconAsNumberControl
// endAdornment={
// endAdornment && (
// <Tooltip title={endAdornment.tooltipContent}>
// <InputAdornment position="end">
// {endAdornment.label}
// </InputAdornment>
// </Tooltip>
// )
// }
/>
);
} else if (field.valueType === 'color') {
return 'TODO';
// const { setValue } = field;
// return (
// <Column key={field.name} expand noMargin>
// <ColorField
// id={field.name}
// floatingLabelText={getFieldLabel({ instances, field })}
// helperMarkdownText={getFieldDescription(field)}
// disableAlpha
// fullWidth
// color={getFieldValue({ instances, field })}
// onChange={color => {
// const rgbString =
// color.length === 0 ? '' : rgbOrHexToRGBString(color);
// instances.forEach(i => setValue(i, rgbString));
// _onInstancesModified(instances);
// }}
// />
// </Column>
// );
} else if (field.valueType === 'enumIcon') {
const value = getFieldValue({ instances, field });
return (
<IconButton
key={field.name}
id={field.name}
size="small"
tooltip={getFieldLabel({ instances, field })}
selected={field.isHighlighted(value)}
onClick={event => {
instances.forEach(i => field.setValue(i, !value));
_onInstancesModified(instances);
}}
>
{field.renderIcon(value)}
</IconButton>
);
} else if (field.valueType === 'textarea') {
return 'TODO';
// const { setValue } = field;
// return (
// <SemiControlledTextField
// key={field.name}
// id={field.name}
// onChange={text => {
// instances.forEach(i => setValue(i, text || ''));
// _onInstancesModified(instances);
// }}
// value={getFieldValue({ instances, field })}
// floatingLabelText={getFieldLabel({ instances, field })}
// floatingLabelFixed
// helperMarkdownText={getFieldDescription(field)}
// multiline
// style={styles.field}
// />
// );
} else if (field.valueType === 'resource') {
return 'TODO';
} else {
const {
// TODO: Still support onEditButtonClick & onEditButtonBuildMenuTemplate ?
// onEditButtonBuildMenuTemplate,
// onEditButtonClick,
setValue,
onClickEndAdornment,
} = field;
return (
<CompactSemiControlledTextField
value={getFieldValue({
instances,
field,
defaultValue: '(Multiple values)',
})}
id={field.name}
// floatingLabelText={getFieldLabel({ instances, field })}
// helperMarkdownText={getFieldDescription(field)}
onChange={newValue => {
instances.forEach(i => setValue(i, newValue || ''));
_onInstancesModified(instances);
}}
disabled={getDisabled({ instances, field })}
renderLeftIcon={field.renderLeftIcon}
leftIconTooltip={getFieldLabel({ instances, field })}
renderEndAdornmentOnHover={field.getEndAdornmentIcon || undefined}
onClickEndAdornment={() => {
if (!onClickEndAdornment) return;
instances.forEach(i => onClickEndAdornment(i));
_onInstancesModified(instances);
}}
/>
);
}
},
[instances, _onInstancesModified]
);
const renderSelectField = React.useCallback(
(field: ValueField) => {
if (!field.getChoices || !field.getValue) return;
const children = field
.getChoices()
.map(({ value, label, labelIsUserDefined }) => (
<SelectOption
key={value}
value={value}
label={label}
shouldNotTranslate={labelIsUserDefined}
/>
));
if (field.valueType === 'number') {
const { setValue } = field;
return (
<CompactSelectField
value={getFieldValue({ instances, field })}
key={field.name}
id={field.name}
// floatingLabelText={getFieldLabel({ instances, field })}
// helperMarkdownText={getFieldDescription(field)}
onChange={(newValue: string) => {
instances.forEach(i => setValue(i, parseFloat(newValue) || 0));
_onInstancesModified(instances);
}}
disabled={field.disabled}
>
{children}
</CompactSelectField>
);
} else if (field.valueType === 'string') {
const { setValue } = field;
return (
<CompactSelectField
value={getFieldValue({
instances,
field,
defaultValue: '(Multiple values)',
})}
key={field.name}
id={field.name}
// floatingLabelText={getFieldLabel({ instances, field })}
// helperMarkdownText={getFieldDescription(field)}
onChange={(newValue: string) => {
instances.forEach(i => setValue(i, newValue || ''));
_onInstancesModified(instances);
}}
disabled={getDisabled({ instances, field })}
renderLeftIcon={field.renderLeftIcon}
leftIconTooltip={getFieldLabel({ instances, field })}
>
{children}
</CompactSelectField>
);
}
},
[instances, _onInstancesModified]
);
const renderButton = React.useCallback(
(field: ActionButton) => {
let disabled = false;
if (field.disabled === 'onValuesDifferent') {
const DIFFERENT_VALUES = 'DIFFERENT_VALUES';
disabled =
getFieldValue({
instances,
field,
defaultValue: DIFFERENT_VALUES,
}) === DIFFERENT_VALUES;
}
return (
<FlatButton
key={`button-${field.label}`}
fullWidth
primary
leftIcon={
field.getIcon ? (
field.getIcon({ fontSize: 'small' })
) : (
<Edit fontSize="small" />
)
}
disabled={disabled}
label={field.label}
onClick={() => {
field.onClick(instances[0]);
}}
/>
);
},
[instances]
);
const renderResourceField = (field: ResourceField) => {
if (!project || !resourceManagementProps) {
console.error(
'You tried to display a resource field in a PropertiesEditor that does not support display resources. If you need to display resources, pass additional props (project, resourceManagementProps).'
);
return null;
}
const { setValue } = field;
return (
<ResourceSelectorWithThumbnail
key={field.name}
project={project}
resourceManagementProps={resourceManagementProps}
resourceKind={field.resourceKind}
fallbackResourceKind={field.fallbackResourceKind}
resourceName={getFieldValue({
instances,
field,
defaultValue: '(Multiple values)', //TODO
})}
onChange={newValue => {
instances.forEach(i => setValue(i, newValue));
_onInstancesModified(instances);
}}
floatingLabelText={getFieldLabel({ instances, field })}
helperMarkdownText={getFieldDescription(field)}
/>
);
};
const renderVerticalCenterWithBar = (field: Field) =>
field.child && field.child.getValue ? (
<VerticallyCenterWithBar key={field.name}>
{renderInputField(field.child)}
</VerticallyCenterWithBar>
) : (
'TODO'
);
const renderContainer =
mode === 'row'
? (fields: React.Node) =>
preventWrap ? (
<LineStackLayout noMargin alignItems="center" expand>
{fields}
</LineStackLayout>
) : (
<ResponsiveLineStackLayout noMargin alignItems="center" expand>
{fields}
</ResponsiveLineStackLayout>
)
: (fields: React.Node) => (
<ColumnStackLayout noMargin expand>
{fields}
</ColumnStackLayout>
);
const renderTitle = React.useCallback(
(field: Title) => {
const { getValue, renderLeftIcon } = field;
let additionalText = null;
if (getValue) {
let selectedInstancesValue = getFieldValue({
instances,
field,
defaultValue: field.defaultValue || 'Multiple Values',
});
if (!!selectedInstancesValue) additionalText = selectedInstancesValue;
}
if (!!additionalText) {
return (
<LineStackLayout
alignItems="center"
key={`section-title-${field.name}`}
expand
>
{renderLeftIcon()}
<Text displayInlineAsSpan noMargin>
{field.name}
</Text>
<Text allowSelection displayInlineAsSpan size="body2" noMargin>
-
</Text>
<Text
allowSelection
displayInlineAsSpan
size="body2"
noMargin
style={textEllipsisStyle}
>
{additionalText}
</Text>
</LineStackLayout>
);
}
return (
<LineStackLayout key={`title-${field.name}`}>
{renderLeftIcon()}
<Text displayInlineAsSpan size="sub-title" noMargin>
{field.name}
</Text>
</LineStackLayout>
);
},
[instances]
);
const renderSectionTitle = React.useCallback((field: SectionTitle) => {
return [
<Line key={`section-title-${field.name}`}>
<Text displayInlineAsSpan size="sub-title" noMargin>
{field.name}
</Text>
</Line>,
];
}, []);
return renderContainer(
schema.map(field => {
if (!!field.nonFieldType) {
if (field.nonFieldType === 'title') {
return renderTitle(field);
} else if (field.nonFieldType === 'sectionTitle') {
return renderSectionTitle(field);
} else if (field.nonFieldType === 'button') {
return renderButton(field);
} else if (field.nonFieldType === 'verticalCenterWithBar') {
return renderVerticalCenterWithBar(field);
}
return null;
} else if (field.children) {
if (field.type === 'row') {
const contentView = (
<React.Fragment key={field.name}>
<CompactPropertiesEditor
project={project}
resourceManagementProps={resourceManagementProps}
schema={field.children}
instances={instances}
mode="row"
unsavedChanges={unsavedChanges}
onInstancesModified={onInstancesModified}
preventWrap={field.preventWrap}
/>
</React.Fragment>
);
if (field.title) {
return [
<Separator key={field.name + '-separator'} />,
<Text key={field.name + '-title'} size="sub-title" noMargin>
{field.title}
</Text>,
contentView,
];
}
return contentView;
}
return (
<div key={field.name} style={styles.container}>
<React.Fragment key={field.name}>
<CompactPropertiesEditor
project={project}
resourceManagementProps={resourceManagementProps}
schema={field.children}
instances={instances}
mode="column"
unsavedChanges={unsavedChanges}
onInstancesModified={onInstancesModified}
preventWrap={field.preventWrap}
/>
</React.Fragment>
</div>
);
} else if (field.valueType === 'resource') {
return renderResourceField(field);
} else {
if (field.getChoices && field.getValue) return renderSelectField(field);
if (field.getValue) return renderInputField(field);
}
return null;
})
);
};
export default CompactPropertiesEditor;

View File

@@ -0,0 +1,569 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import { t } from '@lingui/macro';
import { type Schema } from '../../CompactPropertiesEditor';
import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
import enumerateLayers from '../../LayersList/EnumerateLayers';
import Angle from '../../UI/CustomSvgIcons/Angle';
import Layers from '../../UI/CustomSvgIcons/Layers';
import LetterX from '../../UI/CustomSvgIcons/LetterX';
import LetterY from '../../UI/CustomSvgIcons/LetterY';
import LetterH from '../../UI/CustomSvgIcons/LetterH';
import LetterW from '../../UI/CustomSvgIcons/LetterW';
import Depth from '../../UI/CustomSvgIcons/Depth';
import LetterZ from '../../UI/CustomSvgIcons/LetterZ';
import Instance from '../../UI/CustomSvgIcons/Instance';
import Link from '../../UI/CustomSvgIcons/Link';
import Unlink from '../../UI/CustomSvgIcons/Unlink';
import RemoveCircle from '../../UI/CustomSvgIcons/RemoveCircle';
import Lock from '../../UI/CustomSvgIcons/Lock';
import LockOpen from '../../UI/CustomSvgIcons/LockOpen';
import Restore from '../../UI/CustomSvgIcons/Restore';
import Object3d from '../../UI/CustomSvgIcons/Object3d';
import Object2d from '../../UI/CustomSvgIcons/Object2d';
/**
* Applies ratio to value without intermediary value to avoid precision issues.
*/
const applyRatio = ({
oldReferenceValue,
newReferenceValue,
valueToApplyTo,
}: {|
oldReferenceValue: number,
newReferenceValue: number,
valueToApplyTo: number,
|}) => {
return (newReferenceValue / oldReferenceValue) * valueToApplyTo;
};
const getEditObjectButton = ({
i18n,
onEditObjectByName,
is3DInstance,
}: {|
i18n: I18nType,
onEditObjectByName: (name: string) => void,
is3DInstance: boolean,
|}) => ({
label: i18n._(t`Edit object`),
disabled: 'onValuesDifferent',
nonFieldType: 'button',
getIcon: is3DInstance
? props => <Object3d {...props} />
: props => <Object2d {...props} />,
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
onClick: (instance: gdInitialInstance) =>
onEditObjectByName(instance.getObjectName()),
});
const getRotationXAndRotationYFields = ({ i18n }: {| i18n: I18nType |}) => [
{
name: 'RotationX',
getLabel: () => i18n._(t`Rotation (X)`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getRotationX(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setRotationX(newValue),
renderLeftIcon: className => <LetterX className={className} />,
},
{
name: 'RotationY',
getLabel: () => i18n._(t`Rotation (Y)`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getRotationY(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setRotationY(newValue),
renderLeftIcon: className => <LetterY className={className} />,
},
];
const getRotationZField = ({
i18n,
label,
Icon,
}: {|
i18n: I18nType,
label: MessageDescriptor,
Icon: React.ComponentType<any>,
|}) => ({
name: 'Angle',
getLabel: () => i18n._(label),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getAngle(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setAngle(newValue),
renderLeftIcon: className => <Icon className={className} />,
});
const getXAndYFields = ({ i18n }: {| i18n: I18nType |}): Schema => [
{
name: 'X',
getLabel: () => i18n._(t`X`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getX(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setX(newValue),
renderLeftIcon: className => <LetterX className={className} />,
},
{
name: 'Y',
getLabel: () => i18n._(t`Y`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getY(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setY(newValue),
renderLeftIcon: className => <LetterY className={className} />,
},
];
const getZField = ({ i18n }: {| i18n: I18nType |}) => ({
name: 'Z',
getLabel: () => i18n._(t`Z`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getZ(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setZ(newValue),
renderLeftIcon: className => <LetterZ className={className} />,
});
const getLayerField = ({
i18n,
layout,
}: {|
i18n: I18nType,
layout: gdLayout,
|}) => ({
name: 'Layer',
getLabel: () => i18n._(t`Layer`),
valueType: 'string',
getChoices: () => enumerateLayers(layout),
getValue: (instance: gdInitialInstance) => instance.getLayer(),
setValue: (instance: gdInitialInstance, newValue: string) =>
instance.setLayer(newValue),
renderLeftIcon: className => <Layers className={className} />,
});
const getZOrderField = ({ i18n }: {| i18n: I18nType |}) => ({
name: 'Z Order',
getLabel: () => i18n._(t`Z Order`),
valueType: 'number',
getValue: (instance: gdInitialInstance) => instance.getZOrder(),
setValue: (instance: gdInitialInstance, newValue: number) =>
instance.setZOrder(newValue),
renderLeftIcon: className => <LetterZ className={className} />,
});
const getTitleRow = ({ i18n }: {| i18n: I18nType |}) => ({
name: 'Title',
type: 'row',
preventWrap: true,
children: [
{
name: i18n._(t`Instance`),
renderLeftIcon: className => (
<Instance className={className} fontSize="small" />
),
getValue: (instance: gdInitialInstance) => instance.getObjectName(),
nonFieldType: 'title',
defaultValue: i18n._(t`Different objects`),
},
{
name: 'Lock instance',
getLabel: (instance: gdInitialInstance) =>
instance.isSealed()
? i18n._(t`Free instance`)
: instance.isLocked()
? i18n._(t`Prevent selection in the editor`)
: i18n._(t`Lock position/angle in the editor`),
valueType: 'enumIcon',
renderIcon: value =>
value === 'sealed' ? (
<RemoveCircle fontSize="small" />
) : value === 'locked' ? (
<Lock fontSize="small" />
) : (
<LockOpen fontSize="small" />
),
isHighlighted: value => value === 'locked' || value === 'sealed',
getValue: (instance: gdInitialInstance) =>
instance.isSealed()
? 'sealed'
: instance.isLocked()
? 'locked'
: 'free',
setValue: (instance: gdInitialInstance, newValue: boolean) => {
if (instance.isSealed()) {
instance.setSealed(false);
instance.setLocked(false);
return;
}
if (instance.isLocked()) {
instance.setSealed(true);
return;
}
instance.setLocked(true);
},
},
],
});
const getWidthField = ({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}: {|
i18n: I18nType,
getInstanceWidth: gdInitialInstance => number,
getInstanceHeight: gdInitialInstance => number,
getInstanceDepth: gdInitialInstance => number,
forceUpdate: () => void,
|}) => ({
name: 'Width',
getLabel: () => i18n._(t`Width`),
valueType: 'number',
getValue: getInstanceWidth,
setValue: (instance: gdInitialInstance, newValue: number) => {
const shouldKeepRatio = instance.shouldKeepRatio();
const newWidth = Math.max(newValue, 0);
if (shouldKeepRatio) {
const initialWidth = getInstanceWidth(instance) || 1;
instance.setCustomWidth(newWidth);
instance.setCustomHeight(
applyRatio({
oldReferenceValue: initialWidth,
newReferenceValue: newWidth,
valueToApplyTo: getInstanceHeight(instance),
})
);
instance.setCustomDepth(
applyRatio({
oldReferenceValue: initialWidth,
newReferenceValue: newWidth,
valueToApplyTo: getInstanceDepth(instance),
})
);
} else {
instance.setCustomWidth(newWidth);
instance.setCustomHeight(getInstanceHeight(instance));
instance.setCustomDepth(getInstanceDepth(instance));
}
// This must be done after reading the size.
instance.setHasCustomSize(true);
instance.setHasCustomDepth(true);
forceUpdate();
},
renderLeftIcon: className => <LetterW className={className} />,
getEndAdornmentIcon: className => <Restore className={className} />,
onClickEndAdornment: (instance: gdInitialInstance) => {
instance.setHasCustomSize(false);
instance.setHasCustomDepth(false);
forceUpdate();
},
});
const getHeightField = ({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}: {|
i18n: I18nType,
getInstanceWidth: gdInitialInstance => number,
getInstanceHeight: gdInitialInstance => number,
getInstanceDepth: gdInitialInstance => number,
forceUpdate: () => void,
|}) => ({
name: 'Height',
getLabel: () => i18n._(t`Height`),
valueType: 'number',
getValue: getInstanceHeight,
setValue: (instance: gdInitialInstance, newValue: number) => {
const shouldKeepRatio = instance.shouldKeepRatio();
const newHeight = Math.max(newValue, 0);
if (shouldKeepRatio) {
const initialHeight = getInstanceHeight(instance) || 1;
instance.setCustomWidth(
applyRatio({
oldReferenceValue: initialHeight,
newReferenceValue: newHeight,
valueToApplyTo: getInstanceWidth(instance),
})
);
instance.setCustomHeight(newHeight);
instance.setCustomDepth(
applyRatio({
oldReferenceValue: initialHeight,
newReferenceValue: newHeight,
valueToApplyTo: getInstanceDepth(instance),
})
);
} else {
instance.setCustomWidth(getInstanceWidth(instance));
instance.setCustomHeight(newHeight);
instance.setCustomDepth(getInstanceDepth(instance));
}
// This must be done after reading the size.
instance.setHasCustomSize(true);
instance.setHasCustomDepth(true);
forceUpdate();
},
renderLeftIcon: className => <LetterH className={className} />,
getEndAdornmentIcon: className => <Restore className={className} />,
onClickEndAdornment: (instance: gdInitialInstance) => {
instance.setHasCustomSize(false);
instance.setHasCustomDepth(false);
forceUpdate();
},
});
const getDepthField = ({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}: {|
i18n: I18nType,
getInstanceWidth: gdInitialInstance => number,
getInstanceHeight: gdInitialInstance => number,
getInstanceDepth: gdInitialInstance => number,
forceUpdate: () => void,
|}) => ({
name: 'Depth',
getLabel: () => i18n._(t`Depth`),
valueType: 'number',
getValue: getInstanceDepth,
setValue: (instance: gdInitialInstance, newValue: number) => {
const shouldKeepRatio = instance.shouldKeepRatio();
const newDepth = Math.max(newValue, 0);
if (shouldKeepRatio) {
const initialDepth = getInstanceDepth(instance) || 1;
instance.setCustomWidth(
applyRatio({
oldReferenceValue: initialDepth,
newReferenceValue: newDepth,
valueToApplyTo: getInstanceWidth(instance),
})
);
instance.setCustomHeight(
applyRatio({
oldReferenceValue: initialDepth,
newReferenceValue: newDepth,
valueToApplyTo: getInstanceHeight(instance),
})
);
instance.setCustomDepth(newDepth);
} else {
instance.setCustomWidth(getInstanceWidth(instance));
instance.setCustomHeight(getInstanceHeight(instance));
instance.setCustomDepth(newDepth);
}
// This must be done after reading the size.
instance.setHasCustomSize(true);
instance.setHasCustomDepth(true);
forceUpdate();
},
renderLeftIcon: className => <Depth className={className} />,
getEndAdornmentIcon: className => <Restore className={className} />,
onClickEndAdornment: (instance: gdInitialInstance) => {
instance.setHasCustomSize(false);
instance.setHasCustomDepth(false);
forceUpdate();
},
});
const getCustomSizeField = ({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}: {|
i18n: I18nType,
getInstanceWidth: gdInitialInstance => number,
getInstanceHeight: gdInitialInstance => number,
getInstanceDepth: gdInitialInstance => number,
forceUpdate: () => void,
|}) => ({
name: 'Keep ratio',
getLabel: () => i18n._(t`Keep ratio`),
valueType: 'enumIcon',
isHighlighted: value => value,
renderIcon: value =>
value ? <Link fontSize="small" /> : <Unlink fontSize="small" />,
getValue: (instance: gdInitialInstance) => instance.shouldKeepRatio(),
setValue: (instance: gdInitialInstance, newValue: boolean) =>
instance.setShouldKeepRatio(newValue),
});
export const makeSchema = ({
is3DInstance,
i18n,
forceUpdate,
onEditObjectByName,
onGetInstanceSize,
layout,
}: {|
is3DInstance: boolean,
i18n: I18nType,
forceUpdate: () => void,
onEditObjectByName: (name: string) => void,
onGetInstanceSize: gdInitialInstance => [number, number, number],
layout: gdLayout,
|}): Schema => {
const getInstanceWidth = (instance: gdInitialInstance) =>
instance.hasCustomSize()
? instance.getCustomWidth()
: onGetInstanceSize(instance)[0];
const getInstanceHeight = (instance: gdInitialInstance) =>
instance.hasCustomSize()
? instance.getCustomHeight()
: onGetInstanceSize(instance)[1];
const getInstanceDepth = (instance: gdInitialInstance) =>
instance.hasCustomDepth()
? instance.getCustomDepth()
: onGetInstanceSize(instance)[2];
if (is3DInstance) {
return [
getTitleRow({ i18n }),
getEditObjectButton({ i18n, onEditObjectByName, is3DInstance }),
{
name: 'Position',
type: 'row',
preventWrap: true,
children: [...getXAndYFields({ i18n }), getZField({ i18n })],
},
{
name: 'Size',
type: 'row',
preventWrap: true,
children: [
{
name: 'Custom size',
type: 'column',
children: [
getWidthField({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}),
getHeightField({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}),
getDepthField({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}),
],
},
{
name: 'verticalCenterCustomSize',
nonFieldType: 'verticalCenterWithBar',
child: getCustomSizeField({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}),
},
],
},
getLayerField({ i18n, layout }),
{
name: 'Rotation',
type: 'row',
title: i18n._(t`Rotation`),
preventWrap: true,
children: [
...getRotationXAndRotationYFields({ i18n }),
getRotationZField({ i18n, label: t`Z`, Icon: LetterZ }),
],
},
];
}
return [
getTitleRow({ i18n }),
getEditObjectButton({ i18n, onEditObjectByName, is3DInstance }),
{
name: 'Position',
type: 'row',
preventWrap: true,
children: getXAndYFields({ i18n }),
},
getZOrderField({ i18n }),
{
name: 'custom-size-row',
type: 'row',
preventWrap: true,
children: [
getWidthField({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}),
{
name: 'height-and-custom-size',
type: 'row',
preventWrap: true,
children: [
getHeightField({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}),
getCustomSizeField({
i18n,
getInstanceWidth,
getInstanceHeight,
getInstanceDepth,
forceUpdate,
}),
],
},
],
},
getRotationZField({ i18n, label: t`Angle`, Icon: Angle }),
getLayerField({ i18n, layout }),
];
};
export const reorderInstanceSchemaForCustomProperties = (
schema: Schema,
i18n: I18nType
): Schema => {
const newSchema = [...schema];
const animationFieldIndex = newSchema.findIndex(
field => field.name && field.name === 'animation'
);
if (animationFieldIndex === -1) return newSchema;
const [animationField] = newSchema.splice(animationFieldIndex, 1);
newSchema.unshift({
name: 'Animation',
type: 'row',
title: i18n._(t`Animation`),
children: [animationField],
});
return newSchema;
};

View File

@@ -0,0 +1,217 @@
// @flow
import { Trans } from '@lingui/macro';
import { type I18n as I18nType } from '@lingui/core';
import * as React from 'react';
import Background from '../../UI/Background';
import EmptyMessage from '../../UI/EmptyMessage';
import CompactPropertiesEditor, {
Separator,
} from '../../CompactPropertiesEditor';
import propertiesMapToSchema from '../../CompactPropertiesEditor/PropertiesMapToCompactSchema';
import { type Schema } from '../../CompactPropertiesEditor';
import getObjectByName from '../../Utils/GetObjectByName';
import IconButton from '../../UI/IconButton';
import { Line, Column } from '../../UI/Grid';
import Text from '../../UI/Text';
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
import ScrollView from '../../UI/ScrollView';
import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder';
import VariablesList, {
type HistoryHandler,
} from '../../VariablesList/VariablesList';
import ShareExternal from '../../UI/CustomSvgIcons/ShareExternal';
import useForceUpdate from '../../Utils/UseForceUpdate';
import ErrorBoundary from '../../UI/ErrorBoundary';
import {
makeSchema,
reorderInstanceSchemaForCustomProperties,
} from './CompactPropertiesSchema';
const gd: libGDevelop = global.gd;
type Props = {|
project: gdProject,
layout: gdLayout,
instances: Array<gdInitialInstance>,
onEditObjectByName: string => void,
onInstancesModified?: (Array<gdInitialInstance>) => void,
onGetInstanceSize: gdInitialInstance => [number, number, number],
editInstanceVariables: gdInitialInstance => void,
unsavedChanges?: ?UnsavedChanges,
i18n: I18nType,
historyHandler?: HistoryHandler,
|};
export type CompactInstancePropertiesEditorInterface = {|
forceUpdate: () => void,
|};
const CompactInstancePropertiesEditor = ({
instances,
i18n,
project,
layout,
unsavedChanges,
historyHandler,
onEditObjectByName,
onGetInstanceSize,
editInstanceVariables,
onInstancesModified,
}: Props) => {
const forceUpdate = useForceUpdate();
const schemaFor2D: Schema = React.useMemo(
() =>
makeSchema({
i18n,
is3DInstance: false,
onGetInstanceSize,
onEditObjectByName,
layout,
forceUpdate,
}),
[i18n, onGetInstanceSize, onEditObjectByName, layout, forceUpdate]
);
const schemaFor3D: Schema = React.useMemo(
() =>
makeSchema({
i18n,
is3DInstance: true,
onGetInstanceSize,
onEditObjectByName,
layout,
forceUpdate,
}),
[i18n, onGetInstanceSize, onEditObjectByName, layout, forceUpdate]
);
// TODO: multiple instances support.
const instance = instances[0];
const { object, instanceSchema } = React.useMemo<{|
object?: gdObject,
instanceSchema?: Schema,
|}>(
() => {
if (!instance) return { object: undefined, instanceSchema: undefined };
const associatedObjectName = instance.getObjectName();
const object = getObjectByName(project, layout, associatedObjectName);
const properties = instance.getCustomProperties(project, layout);
if (!object) return { object: undefined, instanceSchema: undefined };
const is3DInstance = gd.MetadataProvider.getObjectMetadata(
project.getCurrentPlatform(),
object.getType()
).isRenderedIn3D();
const instanceSchemaForCustomProperties = propertiesMapToSchema(
properties,
(instance: gdInitialInstance) =>
instance.getCustomProperties(project, layout),
(instance: gdInitialInstance, name, value) =>
instance.updateCustomProperty(name, value, project, layout)
);
const reorderedInstanceSchemaForCustomProperties = reorderInstanceSchemaForCustomProperties(
instanceSchemaForCustomProperties,
i18n
);
return {
object,
instanceSchema: is3DInstance
? schemaFor3D.concat(reorderedInstanceSchemaForCustomProperties)
: schemaFor2D.concat(reorderedInstanceSchemaForCustomProperties),
};
},
[project, layout, instance, schemaFor2D, schemaFor3D, i18n]
);
if (!object || !instance || !instanceSchema) return null;
return (
<ErrorBoundary
componentTitle={<Trans>Instance properties</Trans>}
scope="scene-editor-instance-properties"
>
<ScrollView
autoHideScrollbar
key={instances
.map((instance: gdInitialInstance) => '' + instance.ptr)
.join(';')}
>
<Column expand noMargin id="instance-properties-editor">
<Column>
<CompactPropertiesEditor
unsavedChanges={unsavedChanges}
schema={instanceSchema}
instances={instances}
onInstancesModified={onInstancesModified}
/>
<Separator />
<Line alignItems="center" justifyContent="space-between">
<Text size="sub-title" noMargin>
<Trans>Instance Variables</Trans>
</Text>
<IconButton
size="small"
onClick={() => {
editInstanceVariables(instance);
}}
>
<ShareExternal fontSize="small" />
</IconButton>
</Line>
</Column>
{object ? (
<VariablesList
directlyStoreValueChangesWhileEditing
inheritedVariablesContainer={object.getVariables()}
variablesContainer={instance.getVariables()}
size="small"
onComputeAllVariableNames={() =>
object
? EventsRootVariablesFinder.findAllObjectVariables(
project.getCurrentPlatform(),
project,
layout,
object
)
: []
}
historyHandler={historyHandler}
/>
) : null}
</Column>
</ScrollView>
</ErrorBoundary>
);
};
const CompactInstancePropertiesEditorContainer = React.forwardRef<
Props,
CompactInstancePropertiesEditorInterface
>((props, ref) => {
const forceUpdate = useForceUpdate();
React.useImperativeHandle(ref, () => ({
forceUpdate,
}));
return (
<Background minWidth={0}>
{!props.instances || !props.instances.length ? (
<EmptyMessage>
<Trans>
Click on an instance in the scene to display its properties
</Trans>
</EmptyMessage>
) : (
<CompactInstancePropertiesEditor {...props} />
)}
</Background>
);
});
export default CompactInstancePropertiesEditorContainer;

View File

@@ -17,9 +17,9 @@ describe('EnumerateObjects', () => {
allObjectsList,
} = enumerateObjects(project, testLayout);
expect(containerObjectsList).toHaveLength(20);
expect(containerObjectsList).toHaveLength(21);
expect(projectObjectsList).toHaveLength(2);
expect(allObjectsList).toHaveLength(22);
expect(allObjectsList).toHaveLength(23);
});
it('can enumerate objects with a filter on object type', () => {

View File

@@ -44,10 +44,25 @@ export default class Rendered3DInstance {
/**
* Convert an angle from degrees to radians.
*/
static toRad(angleInDegrees: number) {
static toRad(angleInDegrees: number): number {
return (angleInDegrees / 180) * Math.PI;
}
/**
* Applies ratio to value without intermediary value to avoid precision issues.
*/
static applyRatio({
oldReferenceValue,
newReferenceValue,
valueToApplyTo,
}: {|
oldReferenceValue: number,
newReferenceValue: number,
valueToApplyTo: number,
|}): number {
return (newReferenceValue / oldReferenceValue) * valueToApplyTo;
}
/**
* Called when the scene editor is rendered.
*/

View File

@@ -10,9 +10,6 @@ import EditorMosaic, {
type EditorMosaicInterface,
} 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 CloseButton from '../../UI/EditorMosaic/CloseButton';
@@ -31,6 +28,9 @@ import {
type SceneEditorsDisplayProps,
type SceneEditorsDisplayInterface,
} from '../EditorsDisplay.flow';
import CompactInstancePropertiesEditorContainer, {
type CompactInstancePropertiesEditorInterface,
} from '../../InstancesEditor/CompactInstancePropertiesEditor';
const initialMosaicEditorNodes = {
direction: 'row',
@@ -93,7 +93,7 @@ const MosaicEditorsDisplay = React.forwardRef<
} = React.useContext(PreferencesContext);
const selectedInstances = props.instancesSelection.getSelectedInstances();
const instancesPropertiesEditorRef = React.useRef<?InstancePropertiesEditorInterface>(
const instancesPropertiesEditorRef = React.useRef<?CompactInstancePropertiesEditorInterface>(
null
);
const layersListRef = React.useRef<?LayersListInterface>(null);
@@ -242,7 +242,7 @@ const MosaicEditorsDisplay = React.forwardRef<
renderEditor: () => (
<I18n>
{({ i18n }) => (
<InstancePropertiesEditor
<CompactInstancePropertiesEditorContainer
i18n={i18n}
project={project}
layout={layout}

View File

@@ -5,9 +5,6 @@ import { Trans } from '@lingui/macro';
import { I18n } from '@lingui/react';
import InstancesEditor from '../../InstancesEditor';
import InstancePropertiesEditor, {
type InstancePropertiesEditorInterface,
} from '../../InstancesEditor/InstancePropertiesEditor';
import LayersList, { type LayersListInterface } from '../../LayersList';
import ObjectsList, { type ObjectsListInterface } from '../../ObjectsList';
import ObjectGroupsList, {
@@ -31,6 +28,9 @@ import {
type SceneEditorsDisplayProps,
} from '../EditorsDisplay.flow';
import ErrorBoundary from '../../UI/ErrorBoundary';
import CompactInstancePropertiesEditorContainer, {
type CompactInstancePropertiesEditorInterface,
} from '../../InstancesEditor/CompactInstancePropertiesEditor';
export const swipeableDrawerContainerId = 'swipeable-drawer-container';
@@ -66,7 +66,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
const { values } = React.useContext(PreferencesContext);
const screenType = useScreenType();
const instancesPropertiesEditorRef = React.useRef<?InstancePropertiesEditorInterface>(
const instancesPropertiesEditorRef = React.useRef<?CompactInstancePropertiesEditorInterface>(
null
);
const layersListRef = React.useRef<?LayersListInterface>(null);
@@ -328,7 +328,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
{selectedEditorId === 'properties' && (
<I18n>
{({ i18n }) => (
<InstancePropertiesEditor
<CompactInstancePropertiesEditorContainer
i18n={i18n}
project={project}
layout={layout}

View File

@@ -15,6 +15,7 @@ const styles = {
type Props = {|
children: React.Node,
maxWidth?: boolean,
minWidth?: number,
width?: number | string,
/** Sometimes required on Safari */
noFullHeight?: boolean,
@@ -34,6 +35,7 @@ const Background = (props: Props) => (
width: props.width ? props.width : undefined,
flex: props.noExpand ? undefined : 1,
...(props.maxWidth ? styles.maxWidth : undefined),
minWidth: props.minWidth,
}}
background="dark"
>

View File

@@ -0,0 +1,139 @@
.container {
display: flex;
align-items: center;
}
.compactSelectField {
border-radius: 4px;
color: var(--theme-text-default-color);
background-color: var(--theme-text-field-default-background-color);
transition: box-shadow 0.1s;
position: relative;
display: flex;
flex: 1;
}
/* Style dropdown options on Windows */
.compactSelectField select option {
color: var(--theme-text-default-color);
background-color: var(--theme-text-field-default-background-color);
}
.container.disabled .compactSelectField {
color: var(--theme-text-field-disabled-color);
}
.container.errored:not(.disabled) .compactSelectField {
border: none;
outline: 1px solid var(--theme-text-field-default-error);
}
.container.errored:not(.disabled):hover .compactSelectField {
outline: 1px solid var(--theme-text-field-active-error);
}
.container.errored:not(.disabled):focus-within .compactSelectField {
outline: 1px solid var(--theme-text-field-active-error);
}
.compactSelectField::before {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
content: '';
border-radius: 4px;
pointer-events: none;
}
.container:not(.disabled):not(.errored):hover .compactSelectField::before {
border-bottom: 1px solid var(--theme-text-field-hover-border-color);
}
.container:not(.disabled):not(.errored):focus-within
.compactSelectField::before {
border-bottom: 1px solid var(--theme-text-field-active-border-color);
}
.compactSelectField select {
appearance: none;
outline: none;
border: none;
padding: 2px 8px;
background-image: none;
background-color: transparent;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
font-size: 14px;
line-height: 20px;
font-family: var(--gdevelop-modern-font-family);
color: inherit;
flex: 1;
border-radius: inherit; /* Needed for InAppTutorialElementHighlighter to adapt its border-radius to the input container */
}
.compactSelectField select::placeholder {
color: var(--theme-text-field-placeholder-color);
}
.arrowContainer {
pointer-events: none;
position: absolute;
right: 4px;
top: 4px;
width: 16px;
height: 16px;
display: flex;
border-radius: 4px;
transition: background-color 0.1s linear;
}
.arrow {
flex: 1;
mask-size: 18px;
mask-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 18 18' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='currentColor' fill-rule='evenodd' clip-rule='evenodd' d='M5.49303 6.80023C5.69538 6.61233 6.01175 6.62405 6.19965 6.8264L7.99992 8.76515L9.80019 6.8264C9.98809 6.62405 10.3045 6.61233 10.5068 6.80023C10.7092 6.98813 10.7209 7.3045 10.533 7.50685L8.36632 9.84019C8.27171 9.94207 8.13896 9.99996 7.99992 9.99996C7.86089 9.99996 7.72813 9.94207 7.63352 9.84019L5.46686 7.50685C5.27896 7.3045 5.29067 6.98813 5.49303 6.80023Z'/%3E%3C/svg%3E");
background-color: var(--theme-text-secondary-color);
transition: background-color 0.1s linear;
}
.container.disabled .arrow {
background-color: var(--theme-text-disabled-color);
}
.container:not(.disabled) .arrowContainer {
background-color: none;
}
.container:not(.disabled):hover .arrowContainer {
background-color: var(--theme-text-field-end-adornment-icon-background-color);
}
.container:not(.disabled):focus-within .arrowContainer {
background-color: var(--theme-text-field-end-adornment-icon-background-color);
}
.container:not(.disabled):hover .arrow {
background-color: var(--theme-text-default-color);
}
.container:not(.disabled):focus-within .arrow {
background-color: var(--theme-text-default-color);
}
.leftIconContainer {
display: flex;
margin-right: 4px;
}
/* svg tag is needed to be first priority compared to Material UI Custom SVG icon classes*/
svg.leftIcon {
font-size: 18px;
color: var(--theme-text-field-placeholder-color);
transition: color 0.1s linear;
}
.container.disabled .leftIcon {
color: var(--theme-text-field-disabled-color);
}
.container:not(.disabled):hover .leftIcon {
color: var(--theme-text-default-color);
}
.container:not(.disabled):focus-within .leftIcon {
color: var(--theme-text-default-color);
}

View File

@@ -0,0 +1,83 @@
// @flow
import * as React from 'react';
import Tooltip from '@material-ui/core/Tooltip';
import classNames from 'classnames';
import classes from './CompactSelectField.module.css';
import { tooltipEnterDelay } from '../Tooltip';
type Props = {|
onChange: string => void,
value: string,
id?: string,
disabled?: boolean,
errored?: boolean,
placeholder?: string,
children: React.Node,
renderLeftIcon?: (className: string) => React.Node,
leftIconTooltip?: React.Node,
|};
const CompactSelectField = ({
onChange,
value,
id,
disabled,
errored,
placeholder,
children,
renderLeftIcon,
leftIconTooltip,
}: Props) => {
return (
<div
className={classNames({
[classes.container]: true,
[classes.disabled]: disabled,
[classes.errored]: errored,
})}
>
{renderLeftIcon && (
<Tooltip
title={leftIconTooltip}
enterDelay={tooltipEnterDelay}
placement="bottom"
PopperProps={{
modifiers: {
offset: {
enabled: true,
/**
* It does not seem possible to get the tooltip closer to the anchor
* when positioned on top. So it is positioned on bottom with a negative offset.
*/
offset: '0,-20',
},
},
}}
>
<div className={classes.leftIconContainer}>
{renderLeftIcon(classes.leftIcon)}
</div>
</Tooltip>
)}
<div
className={classNames({
[classes.compactSelectField]: true,
})}
>
<select
id={id}
disabled={disabled}
value={value}
onChange={e => onChange(e.currentTarget.value)}
>
{children}
</select>
<div className={classNames({ [classes.arrowContainer]: true })}>
<span className={classNames({ [classes.arrow]: true })} />
</div>
</div>
</div>
);
};
export default CompactSelectField;

View File

@@ -0,0 +1,14 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0px;
}
.container .error {
font-size: 14px;
line-height: 20px;
font-family: var(--gdevelop-modern-font-family);
color: var(--theme-error-color);
padding: 2px 0px;
}

View File

@@ -0,0 +1,73 @@
// @flow
import * as React from 'react';
import CompactTextField from '../CompactTextField';
import classes from './CompactSemiControlledNumberField.module.css';
type Props = {|
id?: string,
value: number,
onChange: number => void,
commitOnBlur?: boolean,
disabled?: boolean,
errored?: boolean,
placeholder?: string,
renderLeftIcon?: (className: string) => React.Node,
leftIconTooltip?: React.Node,
useLeftIconAsNumberControl?: boolean,
renderEndAdornmentOnHover?: (className: string) => React.Node,
onClickEndAdornment?: () => void,
errorText?: React.Node,
|};
const CompactSemiControlledNumberField = ({
value,
onChange,
errorText,
commitOnBlur,
...otherProps
}: Props) => {
const [focused, setFocused] = React.useState<boolean>(false);
const [temporaryValue, setTemporaryValue] = React.useState<?number>(null);
return (
<div className={classes.container}>
<CompactTextField
type="number"
value={focused ? temporaryValue : value}
onChange={(valueAsString, reason) => {
const newValue = parseFloat(valueAsString);
const isNewValueValid = !Number.isNaN(newValue);
if (isNewValueValid) {
setTemporaryValue(newValue);
if (reason === 'keyInput') {
if (!commitOnBlur) onChange(newValue);
} else {
onChange(newValue);
}
} else {
setTemporaryValue(null);
}
}}
onFocus={event => {
setFocused(true);
setTemporaryValue(value);
}}
onBlur={event => {
const newValue = parseFloat(event.currentTarget.value);
const isNewValueValid = !Number.isNaN(newValue);
if (isNewValueValid) {
onChange(newValue);
}
setFocused(false);
setTemporaryValue(null);
}}
{...otherProps}
/>
{errorText && <div className={classes.error}>{errorText}</div>}
</div>
);
};
export default CompactSemiControlledNumberField;

View File

@@ -0,0 +1,14 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0px;
}
.container .error {
font-size: 14px;
line-height: 20px;
font-family: var(--gdevelop-modern-font-family);
color: var(--theme-error-color);
padding: 2px 0px;
}

View File

@@ -0,0 +1,58 @@
// @flow
import * as React from 'react';
import CompactTextField from '../CompactTextField';
import classes from './CompactSemiControlledTextField.module.css';
type Props = {|
id?: string,
value: string,
onChange: string => void,
commitOnBlur?: boolean,
disabled?: boolean,
errored?: boolean,
placeholder?: string,
renderLeftIcon?: (className: string) => React.Node,
leftIconTooltip?: React.Node,
renderEndAdornmentOnHover?: (className: string) => React.Node,
onClickEndAdornment?: () => void,
errorText?: React.Node,
|};
const CompactSemiControlledTextField = ({
value,
onChange,
errorText,
commitOnBlur,
...otherProps
}: Props) => {
const [focused, setFocused] = React.useState<boolean>(false);
const [text, setText] = React.useState<string>('');
return (
<div className={classes.container}>
<CompactTextField
type="text"
value={focused ? text : value}
onFocus={event => {
setFocused(true);
setText(value);
}}
onChange={newValue => {
setText(newValue);
if (!commitOnBlur) onChange(newValue);
}}
onBlur={event => {
onChange(event.currentTarget.value);
setFocused(false);
setText('');
}}
{...otherProps}
/>
{errorText && <div className={classes.error}>{errorText}</div>}
</div>
);
};
export default CompactSemiControlledTextField;

View File

@@ -0,0 +1,159 @@
.container {
display: flex;
align-items: center;
}
.compactTextField {
border-radius: 4px;
color: var(--theme-text-default-color);
background-color: var(--theme-text-field-default-background-color);
transition: box-shadow 0.1s;
position: relative;
display: flex;
flex: 1;
min-width: 0px;
}
.container.disabled .compactTextField {
color: var(--theme-text-field-disabled-color);
}
.container.errored:not(.disabled) .compactTextField {
border: none;
outline: 1px solid var(--theme-text-field-default-error);
}
.container.errored:not(.disabled):hover .compactTextField {
outline: 1px solid var(--theme-text-field-active-error);
}
.container.errored:not(.disabled):focus-within .compactTextField {
outline: 1px solid var(--theme-text-field-active-error);
}
.compactTextField::before {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
content: '';
border-radius: 4px;
pointer-events: none;
}
.container:not(.disabled):not(.errored):hover .compactTextField::before {
border-bottom: 1px solid var(--theme-text-field-hover-border-color);
}
.container:not(.disabled):not(.errored):focus-within .compactTextField::before {
border-bottom: 1px solid var(--theme-text-field-active-border-color);
}
.compactTextField input {
outline: none;
border: none;
padding: 2px 8px;
background-image: none;
background-color: transparent;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
font-size: 14px;
line-height: 20px;
font-family: var(--gdevelop-modern-font-family);
color: inherit;
flex: 1;
caret-color: var(--theme-text-field-active-caret-color);
min-width: 0px;
border-radius: inherit; /* Needed for InAppTutorialElementHighlighter to adapt its border-radius to the input container */
}
.compactTextField.withEndAdornment input {
padding-right: 20px;
}
.compactTextField input::placeholder {
color: var(--theme-text-field-placeholder-color);
}
.leftIconContainer {
display: flex;
margin-right: 4px;
}
.label {
display: flex;
cursor: inherit;
}
.container:not(.disabled) .leftIconContainer.control {
cursor: ew-resize;
}
/* svg tag is needed to be first priority compared to Material UI Custom SVG icon classes*/
svg.leftIcon {
font-size: 20px;
color: var(--theme-text-field-placeholder-color);
transition: color 0.1s linear;
}
.container.disabled .leftIcon {
color: var(--theme-text-field-disabled-color);
}
.container:not(.disabled):hover .leftIcon {
color: var(--theme-text-default-color);
}
.container:not(.disabled):focus-within .leftIcon {
color: var(--theme-text-default-color);
}
.endAdornmentButton {
outline: 0;
border: 0;
position: absolute;
right: 3px;
top: 3px;
width: 17px;
height: 17px;
display: none;
padding: 1px;
border-radius: 4px;
background-color: unset;
}
/* svg tag is needed to be first priority compared to Material UI Custom SVG icon classes*/
svg.endAdornmentIcon {
font-size: 15px;
color: var(--theme-text-field-placeholder-color);
transition: color 0.1s linear;
}
.container:not(.disabled):hover .endAdornmentButton,
.container:not(.disabled):focus-within .endAdornmentButton {
display: flex;
}
.container:not(.disabled):hover .endAdornmentButton:hover,
.container:not(.disabled):focus-within .endAdornmentButton:hover,
.container:not(.disabled):hover .endAdornmentButton:focus,
.container:not(.disabled):focus-within .endAdornmentButton:focus {
background-color: var(--theme-text-field-end-adornment-icon-background-color);
}
.container:not(.disabled):hover .endAdornmentButton:hover .endAdornmentIcon,
.container:not(.disabled):focus-within
.endAdornmentButton:hover
.endAdornmentIcon,
.container:not(.disabled):hover .endAdornmentButton:focus .endAdornmentIcon,
.container:not(.disabled):focus-within
.endAdornmentButton:focus
.endAdornmentIcon {
color: var(--theme-text-default-color);
}
/* Chrome, Safari, Edge, Opera */
.compactTextField input::-webkit-outer-spin-button,
.compactTextField input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.compactTextField input[type='number'] {
-moz-appearance: textfield;
appearance: textfield;
}

View File

@@ -0,0 +1,66 @@
// @flow
import * as React from 'react';
type Props = {|
onChange: number => void,
onGetInitialValue: () => number,
|};
const useClickDragAsControl = ({ onChange, onGetInitialValue }: Props) => {
const clickDragOrigin = React.useRef<?{|
initialValue: number,
accumulatedValue: number,
previouslyChangedValue: ?number,
|}>(null);
const start = React.useCallback(
(e: MouseEvent) => {
const target = e.currentTarget;
if (target instanceof Element) {
target.requestPointerLock();
clickDragOrigin.current = {
initialValue: onGetInitialValue(),
accumulatedValue: 0,
previouslyChangedValue: null,
};
}
},
[onGetInitialValue]
);
const onMove = React.useCallback(
(e: MouseEvent) => {
if (!clickDragOrigin.current) return;
const { initialValue, previouslyChangedValue } = clickDragOrigin.current;
clickDragOrigin.current.accumulatedValue +=
e.movementX /
// Sensitivity setting
2;
const newValue =
Math.round(clickDragOrigin.current.accumulatedValue) + initialValue;
if (newValue !== previouslyChangedValue) {
if (!clickDragOrigin.current) return;
clickDragOrigin.current.previouslyChangedValue = newValue;
onChange(newValue);
}
},
[onChange]
);
const clear = React.useCallback(() => {
if (clickDragOrigin.current) {
document.exitPointerLock();
clickDragOrigin.current = null;
}
}, []);
return {
onMouseDown: start,
onMouseMove: onMove,
onMouseUp: clear,
};
};
export default useClickDragAsControl;

View File

@@ -0,0 +1,163 @@
// @flow
import * as React from 'react';
import Tooltip from '@material-ui/core/Tooltip';
import classNames from 'classnames';
import classes from './CompactTextField.module.css';
import { tooltipEnterDelay } from '../Tooltip';
import useClickDragAsControl from './UseClickDragAsControl';
import { makeTimestampedId } from '../../Utils/TimestampedId';
import { toFixedWithoutTrailingZeros } from '../../Utils/Mathematics';
type ValueProps =
| {|
type?: 'text',
value: string,
onChange: (newValue: string, reason: 'keyInput') => void,
|}
| {|
type: 'number',
value: ?number, // null value corresponds to an empty input.
onChange: (newValue: number, reason: 'keyInput' | 'iconControl') => void,
|};
type OtherProps = {|
onBlur?: ({
currentTarget: {
value: string,
},
}) => void,
onFocus?: ({
currentTarget: {
value: string,
},
preventDefault: () => void,
}) => void,
|};
export type CompactTextFieldProps = {|
...ValueProps,
...OtherProps,
id?: string,
disabled?: boolean,
errored?: boolean,
placeholder?: string,
renderLeftIcon?: (className: string) => React.Node,
leftIconTooltip?: React.Node,
useLeftIconAsNumberControl?: boolean,
renderEndAdornmentOnHover?: (className: string) => React.Node,
onClickEndAdornment?: () => void,
|};
const CompactTextField = ({
type,
value,
onChange,
id,
disabled,
errored,
placeholder,
renderLeftIcon,
leftIconTooltip,
useLeftIconAsNumberControl,
renderEndAdornmentOnHover,
onClickEndAdornment,
onBlur,
onFocus,
}: CompactTextFieldProps) => {
const idToUse = React.useRef<string>(id || makeTimestampedId());
const controlProps = useClickDragAsControl({
// $FlowExpectedError - Click drag controls should not be used if value type is not number.
onChange: value => onChange(value, 'iconControl'),
// $FlowExpectedError
onGetInitialValue: () => value,
});
const onBlurInput = React.useCallback(
event => {
if (onBlur) onBlur(event);
},
[onBlur]
);
const onFocusInput = React.useCallback(
event => {
if (onFocus) onFocus(event);
},
[onFocus]
);
return (
<div
className={classNames({
[classes.container]: true,
[classes.disabled]: disabled,
[classes.errored]: errored,
})}
>
{renderLeftIcon && (
<Tooltip
title={leftIconTooltip}
enterDelay={tooltipEnterDelay}
placement="bottom"
PopperProps={{
modifiers: {
offset: {
enabled: true,
/**
* It does not seem possible to get the tooltip closer to the anchor
* when positioned on top. So it is positioned on bottom with a negative offset.
*/
offset: '0,-20',
},
},
}}
>
<div
className={classNames({
[classes.leftIconContainer]: true,
[classes.control]: !!useLeftIconAsNumberControl,
})}
{...(useLeftIconAsNumberControl ? controlProps : {})}
>
<label htmlFor={idToUse.current} className={classes.label}>
{renderLeftIcon(classes.leftIcon)}
</label>
</div>
</Tooltip>
)}
<div
className={classNames({
[classes.compactTextField]: true,
[classes.withEndAdornment]: !!renderEndAdornmentOnHover,
})}
>
<input
id={idToUse.current}
type={type || 'text'}
disabled={disabled}
value={
value === null
? ''
: typeof value === 'number'
? toFixedWithoutTrailingZeros(value, 2)
: value
}
onChange={e => onChange(e.currentTarget.value, 'keyInput')}
placeholder={placeholder}
onBlur={onBlurInput}
onFocus={onFocusInput}
/>
{renderEndAdornmentOnHover && (
<button
onClick={onClickEndAdornment}
className={classes.endAdornmentButton}
>
{renderEndAdornmentOnHover(classes.endAdornmentIcon)}
</button>
)}
</div>
</div>
);
};
export default CompactTextField;

View File

@@ -0,0 +1,13 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M3.8335 2.66666C3.8335 2.39051 3.60964 2.16666 3.3335 2.16666C3.05735 2.16666 2.8335 2.39051 2.8335 2.66666V12C2.8335 12.2761 3.05735 12.5 3.3335 12.5H12.6668C12.943 12.5 13.1668 12.2761 13.1668 12C13.1668 11.7238 12.943 11.5 12.6668 11.5H8.46948C8.32936 10.3516 7.72143 9.26415 6.89538 8.4381C6.06933 7.61205 4.98186 7.00412 3.8335 6.86401V2.66666ZM6.18828 9.14521C5.51647 8.4734 4.67669 8.00907 3.8335 7.87393V11.5H7.45955C7.32441 10.6568 6.86008 9.81701 6.18828 9.14521Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,62 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M1.3335 13.3333H14.6668H1.3335Z" fill="currentColor" />
<path
d="M1.3335 13.3333H14.6668"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3.3335 2.66666H12.6668"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 10.6733L2.00667 10.6659"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14.0067 10.6733L14 10.6659"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2.6665 8.00667L2.67317 7.99927"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M13.3397 8.00667L13.333 7.99927"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3.3335 5.33999L3.34016 5.33258"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12.6732 5.33999L12.6665 5.33258"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M8 4.66666V11.3333M8 4.66666L7 5.66666M8 4.66666L9 5.66666M8 11.3333L6 9.33332M8 11.3333L10 9.33332"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,13 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M8.00016 3.93256L3.93273 8L8.00016 12.0674L12.0676 8L8.00016 3.93256ZM7.46279 3.05572C7.60537 2.91331 7.79865 2.83333 8.00016 2.83333C8.20168 2.83333 8.39495 2.91331 8.53753 3.05572L12.9442 7.46241C13.0866 7.60499 13.1668 7.79848 13.1668 8C13.1668 8.20151 13.0868 8.39478 12.9444 8.53736L8.53775 12.9441C8.39517 13.0865 8.20168 13.1667 8.00016 13.1667C7.79865 13.1667 7.60537 13.0867 7.46279 12.9443L3.05611 8.53759C2.9137 8.395 2.8335 8.20151 2.8335 8C2.8335 7.79848 2.91348 7.60521 3.05589 7.46263L7.46279 3.05572Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="17" height="20" viewBox="0 0 16 20" fill="none">
<path
d="M10.29 15V10.478H6.23V15H4.9V5.354H6.23V9.386H10.29V5.354H11.62V15H10.29Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="17" height="20" viewBox="0 0 17 20" fill="none">
<path
d="M13.214 5.354L11.254 15H9.546L7.768 6.922L5.976 15H4.31L2.35 5.354H3.652L5.206 13.838L7.068 5.354H8.482L10.386 13.838L11.996 5.354H13.214Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="12" height="20" viewBox="0 0 12 20" fill="none">
<path
d="M6.55 9.792L9.49 15H7.978L5.752 10.73L3.498 15H2.07L4.968 9.862L2.322 5.354H3.834L5.78 8.98L7.74 5.354H9.168L6.55 9.792Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="17" height="20" viewBox="0 0 17 20" fill="none">
<path
d="M12.13 5.354L9.022 11.29V15H7.678V11.304L4.57 5.354H6.04L8.392 10.128L10.744 5.354H12.13Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="20" viewBox="0 0 16 20" fill="none">
<path
d="M10.678 5.354V6.432L5.904 13.866H10.678L10.524 15H4.42V13.936L9.236 6.474H4.924V5.354H10.678Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,25 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.6537 5.19282C4.64208 3.3546 6.12275 1.85505 7.96096 1.84338C9.7992 1.83171 11.2988 3.31241 11.3104 5.15065L11.3179 6.32914C11.3196 6.60528 11.0972 6.83055 10.8211 6.8323C10.5449 6.83405 10.3197 6.61161 10.3179 6.33548L10.3104 5.15699C10.3023 3.87101 9.25325 2.83519 7.96732 2.84336C6.68136 2.85153 5.64556 3.90054 5.65368 5.18649C5.65368 5.18649 5.65368 5.18649 5.65368 5.18649L5.66115 6.36498C5.6629 6.64112 5.44046 6.86639 5.16432 6.86814C4.88819 6.86989 4.66292 6.64745 4.66117 6.37132L4.6537 5.19282Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.17889 9.16792C5.45503 9.16617 5.6803 9.38861 5.68205 9.66474L5.68952 10.8432C5.68952 10.8432 5.68952 10.8432 5.68952 10.8432C5.69769 12.1292 6.74671 13.165 8.03266 13.1569C9.3186 13.1487 10.3544 12.0997 10.3463 10.8137L10.3388 9.63524C10.3371 9.35911 10.5595 9.13383 10.8356 9.13208C11.1118 9.13034 11.337 9.35277 11.3388 9.62891L11.3463 10.8074C11.3579 12.6456 9.87721 14.1452 8.03898 14.1568C6.20076 14.1685 4.70121 12.6878 4.68954 10.8496L4.68207 9.67108C4.68032 9.39494 4.90276 9.16967 5.17889 9.16792Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M7.98337 5.37884C8.25951 5.37709 8.48478 5.59953 8.48653 5.87567L8.51341 10.1182C8.51516 10.3944 8.29272 10.6196 8.01659 10.6214C7.74045 10.6231 7.51518 10.4007 7.51343 10.1246L7.48655 5.882C7.4848 5.60586 7.70724 5.38059 7.98337 5.37884Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,11 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M2.5 8C2.5 4.96499 4.97482 2.5 8.03379 2.5C10.1843 2.5 12.0485 3.71928 12.9644 5.5H10.8667C10.5905 5.5 10.3667 5.72386 10.3667 6C10.3667 6.27614 10.5905 6.5 10.8667 6.5H13.8C13.9045 6.5 14.0097 6.48361 14.1103 6.45026C14.2109 6.41694 14.3084 6.36614 14.3952 6.29671C14.4821 6.22717 14.5585 6.13862 14.6136 6.03235C14.6689 5.92544 14.7 5.8053 14.7 5.68V2.86667C14.7 2.59052 14.4761 2.36667 14.2 2.36667C13.9239 2.36667 13.7 2.59052 13.7 2.86667V4.76132C12.5718 2.81196 10.4562 1.5 8.03379 1.5C4.42784 1.5 1.5 4.40741 1.5 8C1.5 11.5926 4.42784 14.5 8.03379 14.5C11.3074 14.5 14.0208 12.1042 14.4944 8.97482C14.5357 8.70178 14.3478 8.44695 14.0748 8.40563C13.8018 8.36431 13.5469 8.55215 13.5056 8.82518C13.1055 11.469 10.8096 13.5 8.03379 13.5C4.97482 13.5 2.5 11.035 2.5 8Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -0,0 +1,19 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.65394 5.19279C4.64233 3.35457 6.12299 1.85502 7.96121 1.84335C9.79944 1.83168 11.299 3.31238 11.3107 5.15062L11.3181 6.32911C11.3199 6.60525 11.0974 6.83052 10.8213 6.83227C10.5452 6.83402 10.3199 6.61158 10.3182 6.33544L10.3107 5.15696C10.3025 3.87098 9.2535 2.83516 7.96756 2.84333C6.68161 2.8515 5.6458 3.90051 5.65392 5.18646L5.66139 6.36495C5.66314 6.64109 5.4407 6.86636 5.16457 6.86811C4.88843 6.86986 4.66316 6.64742 4.66141 6.37128L4.65394 5.19279Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.17914 9.16789C5.45528 9.16614 5.68055 9.38858 5.6823 9.66471L5.68976 10.8432C5.69793 12.1291 6.74695 13.165 8.0329 13.1568C9.31884 13.1487 10.3547 12.0997 10.3465 10.8137L10.3391 9.63521C10.3373 9.35908 10.5597 9.1338 10.8359 9.13205C11.112 9.1303 11.3373 9.35274 11.339 9.62888L11.3465 10.8074C11.3581 12.6456 9.87746 14.1452 8.03922 14.1568C6.20101 14.1684 4.70146 12.6878 4.68978 10.8495L4.68232 9.67105C4.68057 9.39491 4.903 9.16964 5.17914 9.16789Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -31,6 +31,10 @@ export const Line = (props: {|
// all the height (if set to flex: 1) and to *not* grow
// larger than the parent.
minHeight: props.useFullHeight ? '0' : undefined,
// Setting min-width to 0 prevents the line to keep a minimum width
// in some rare cases (CompactInstancePropertiesEditor when in the
// mosaic editor for instance).
minWidth: 0,
}}
>
{props.children}

View File

@@ -439,6 +439,52 @@
"badge-color": {
"value": "#6BAFFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FFC2B4",
"comment": "Palette/Red/20"
},
"border-color": {
"value": "#99c4f8"
},
"caret-color": {
"value": "#7fb6f6"
}
},
"disabled": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"placeholder": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"default": {
"background-color": {
"value": "rgba(255, 255, 255, 0.09)"
},
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
}
},
"hover": {
"border-color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(221, 209, 255, 0.16)"
}
}
}
},
"input": {

View File

@@ -512,6 +512,55 @@
"badge-color": {
"value": "#6BAFFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FFC2B4",
"comment": "Palette/Red/20"
},
"border-color": {
"value": "#DDD1FF",
"comment": "Palette/Purple/10"
},
"caret-color": {
"value": "#C9B6FC",
"comment": "Palette/Purple/20"
}
},
"disabled": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"placeholder": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"default": {
"background-color": {
"value": "#32323B",
"comment": "Palette/Grey/80"
},
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
}
},
"hover": {
"border-color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(221, 209, 255, 0.16)"
}
}
}
},
"input": {

View File

@@ -500,6 +500,55 @@
"badge-color": {
"value": "#2B8CFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
},
"border-color": {
"value": "#7046EC",
"comment": "Palette/Purple/40"
},
"caret-color": {
"value": "#9979F1",
"comment": "Palette/Purple/30"
}
},
"disabled": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"placeholder": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"default": {
"background-color": {
"value": "#D9D9DE",
"comment": "Palette/Grey/20"
},
"error": {
"value": "#FF5E3B",
"comment": "Palette/Red/50"
}
},
"hover": {
"border-color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(79, 40, 205, 0.16)"
}
}
}
},
"input": {

View File

@@ -430,6 +430,52 @@
"badge-color": {
"value": "#6BAFFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FFC2B4",
"comment": "Palette/Red/20"
},
"border-color": {
"value": "#D8DEE9"
},
"caret-color": {
"value": "#7e9abc"
}
},
"disabled": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"placeholder": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"default": {
"background-color": {
"value": "rgba(255, 255, 255, 0.09)"
},
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
}
},
"hover": {
"border-color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(221, 209, 255, 0.16)"
}
}
}
},
"input": {

View File

@@ -445,6 +445,52 @@
"badge-color": {
"value": "#6BAFFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FFC2B4",
"comment": "Palette/Red/20"
},
"border-color": {
"value": "#D6DEEC"
},
"caret-color": {
"value": "#a9d3f6"
}
},
"disabled": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"placeholder": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"default": {
"background-color": {
"value": "rgba(255, 255, 255, 0.09)"
},
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
}
},
"hover": {
"border-color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(221, 209, 255, 0.16)"
}
}
}
},
"input": {

View File

@@ -431,6 +431,52 @@
"badge-color": {
"value": "#6BAFFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FFC2B4",
"comment": "Palette/Red/20"
},
"border-color": {
"value": "#6e6a86"
},
"caret-color": {
"value": "#decff1"
}
},
"disabled": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"placeholder": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"default": {
"background-color": {
"value": "rgba(255, 255, 255, 0.09)"
},
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
}
},
"hover": {
"border-color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(221, 209, 255, 0.16)"
}
}
}
},
"input": {

View File

@@ -432,6 +432,52 @@
"badge-color": {
"value": "#6BAFFF"
}
},
"text-field": {
"active": {
"error": {
"value": "#FFC2B4",
"comment": "Palette/Red/20"
},
"border-color": {
"value": "#444444"
},
"caret-color": {
"value": "#9ac9db"
}
},
"disabled": {
"color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"placeholder": {
"color": {
"value": "#A6A6AB",
"comment": "Palette/Grey/40"
}
},
"default": {
"background-color": {
"value": "rgba(255, 255, 255, 0.09)"
},
"error": {
"value": "#FF8569",
"comment": "Palette/Red/40"
}
},
"hover": {
"border-color": {
"value": "#7F7F85",
"comment": "Palette/Grey/50"
}
},
"endAdornmentIcon": {
"background-color": {
"value": "rgba(221, 209, 255, 0.16)"
}
}
}
},
"input": {

View File

@@ -0,0 +1,48 @@
// @flow
import * as React from 'react';
import GDevelopThemeContext from './Theme/GDevelopThemeContext';
const styles = {
container: {
display: 'flex',
alignSelf: 'stretch',
flexDirection: 'column',
justifyContent: 'stretch',
alignItems: 'center',
},
verticalBar: {
flex: 1,
},
childrenContainer: {
flex: 0,
},
};
type Props = {|
children: React.Node,
|};
const VerticallyCenterWithBar = (props: Props) => {
const gdevelopTheme = React.useContext(GDevelopThemeContext);
return (
<div style={styles.container}>
<div
style={{
...styles.verticalBar,
borderLeft: `1px solid ${gdevelopTheme.palette.secondary}`,
}}
/>
<div style={styles.childrenContainer}>{props.children}</div>
<div
style={{
...styles.verticalBar,
borderLeft: `1px solid ${gdevelopTheme.palette.secondary}`,
}}
/>
</div>
);
};
export default VerticallyCenterWithBar;

View File

@@ -351,4 +351,48 @@ export const makeTestExtensions = (gd: libGDevelop) => {
platform.addNewExtension(extension);
extension.delete(); // Release the extension as it was copied inside gd.JsPlatform
}
{
const extension = new gd.PlatformExtension();
extension
.setExtensionInformation(
'FakeScene3D',
'Fake 3D',
'Fake support for 3D in GDevelop.',
'',
'MIT'
)
.setCategory('General');
extension
.addInstructionOrExpressionGroupMetadata('3D')
.setIcon('res/conditions/3d_box.svg');
const Cube3DObject = new gd.ObjectJsImplementation();
// $FlowExpectedError
Cube3DObject.getProperties = function(objectContent) {
const objectProperties = new gd.MapStringPropertyDescriptor();
return objectProperties;
};
// $FlowExpectedError
Cube3DObject.getInitialInstanceProperties = function(
content,
instance,
project,
layout
) {
const instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
extension
.addObject(
'Cube3DObject',
'3D Box',
'A box with images for each face',
'JsPlatform/Extensions/3d_box.svg',
Cube3DObject
)
.setCategoryFullName('General')
.markAsRenderedIn3D();
platform.addNewExtension(extension);
extension.delete(); // Release the extension as it was copied inside gd.JsPlatform
}
};

View File

@@ -10,6 +10,7 @@ export type TestProject = {|
panelSpriteObject: gdObject,
spriteObjectConfiguration: gdSpriteObject,
emptySpriteObjectConfiguration: gdSpriteObject,
cube3dObject: gdObject,
customObject: gdObject,
spriteObject: gdObject,
emptySpriteObject: gdObject,
@@ -21,6 +22,7 @@ export type TestProject = {|
group2: gdObjectGroup,
group4WithLongsNames: gdObjectGroup,
testLayoutInstance1: gdInitialInstance,
testLayoutInstance2: gdInitialInstance,
testInstruction: gdInstruction,
testExternalEvents1: gdExternalEvents,
testExternalEvents2: gdExternalEvents,
@@ -213,6 +215,12 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => {
'MyTiledSpriteObject',
0
);
const cube3dObject = testLayout.insertNewObject(
project,
'FakeScene3D::Cube3DObject',
'CubeObject',
0
);
const panelSpriteObject = testLayout.insertNewObject(
project,
'PanelSpriteObject::PanelSprite',
@@ -386,6 +394,14 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => {
testLayoutInstance1.setX(10);
testLayoutInstance1.setY(15);
const testLayoutInstance2 = testLayout
.getInitialInstances()
.insertNewInitialInstance();
testLayoutInstance2.setX(120);
testLayoutInstance2.setY(-15);
testLayoutInstance2.setZ(32);
testLayoutInstance2.setObjectName(cube3dObject.getName());
const testSpriteObjectInstance = testLayout
.getInitialInstances()
.insertNewInitialInstance();
@@ -880,6 +896,7 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => {
tiledSpriteObjectConfiguration: tiledSpriteObject.getConfiguration(),
panelSpriteObject,
customObject,
cube3dObject,
spriteObject,
spriteObjectConfiguration,
emptySpriteObject,
@@ -892,6 +909,7 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => {
group2,
group4WithLongsNames,
testLayoutInstance1,
testLayoutInstance2,
testInstruction,
testExternalEvents1,
testExternalEvents2,

View File

@@ -0,0 +1,75 @@
// @flow
import * as React from 'react';
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
import Toggle from '../UI/Toggle';
import InAppTutorialElementHighlighter from '../InAppTutorial/InAppTutorialElementHighlighter';
import CompactSelectField from '../UI/CompactSelectField';
import { Column } from '../UI/Grid';
type Props = {|
elements: {| label: string, id: string |}[],
children: React.Node,
|};
const ElementHighlighterProvider = (props: Props) => {
const [
shouldHighlightField,
setShouldHighlightField,
] = React.useState<boolean>(false);
const [
elementToHighlightId,
setElementToHighlightId,
] = React.useState<?string>(props.elements[0] ? props.elements[0].id : null);
const [elementToHighlight, setElementToHighlight] = React.useState<any>(null);
React.useEffect(
() => {
if (!shouldHighlightField) {
setElementToHighlight(null);
return;
}
const element = elementToHighlightId
? document.getElementById(elementToHighlightId)
: null;
setElementToHighlight(element);
},
[elementToHighlightId, shouldHighlightField]
);
return (
<>
<ColumnStackLayout noMargin>
{props.children}
<ResponsiveLineStackLayout noMargin expand noColumnMargin>
<Column expand noMargin justifyContent="center">
<Toggle
label="Highlight field"
labelPosition="right"
toggled={shouldHighlightField}
onToggle={(e, active) => setShouldHighlightField(active)}
/>
</Column>
<Column expand noMargin justifyContent="center">
<CompactSelectField
value={elementToHighlightId || ''}
onChange={setElementToHighlightId}
>
{props.elements.map(element => (
<option
label={element.label}
value={element.id}
key={element.id}
/>
))}
</CompactSelectField>
</Column>
</ResponsiveLineStackLayout>
</ColumnStackLayout>
{elementToHighlight && (
<InAppTutorialElementHighlighter element={elementToHighlight} />
)}
</>
);
};
export default ElementHighlighterProvider;

View File

@@ -0,0 +1,59 @@
// @flow
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { I18n } from '@lingui/react';
// Keep first as it creates the `global.gd` object:
import { testProject } from '../../GDevelopJsInitializerDecorator';
import paperDecorator from '../../PaperDecorator';
import CompactInstancePropertiesEditor from '../../../InstancesEditor/CompactInstancePropertiesEditor';
import SerializedObjectDisplay from '../../SerializedObjectDisplay';
import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider';
export default {
title: 'LayoutEditor/CompactInstancePropertiesEditor',
component: CompactInstancePropertiesEditor,
decorators: [paperDecorator],
};
export const Instance2d = () => (
<DragAndDropContextProvider>
<I18n>
{({ i18n }) => (
<SerializedObjectDisplay object={testProject.testLayout}>
<CompactInstancePropertiesEditor
i18n={i18n}
project={testProject.project}
layout={testProject.testLayout}
instances={[testProject.testSpriteObjectInstance]}
editInstanceVariables={action('edit instance variables')}
onGetInstanceSize={() => [100, 101, 102]}
onEditObjectByName={action('edit object')}
/>
</SerializedObjectDisplay>
)}
</I18n>
</DragAndDropContextProvider>
);
export const Instance3d = () => (
<DragAndDropContextProvider>
<I18n>
{({ i18n }) => (
<SerializedObjectDisplay object={testProject.testLayout}>
<CompactInstancePropertiesEditor
i18n={i18n}
project={testProject.project}
layout={testProject.testLayout}
instances={[testProject.testLayoutInstance2]}
editInstanceVariables={action('edit instance variables')}
onGetInstanceSize={() => [100, 101, 102]}
onEditObjectByName={action('edit object')}
/>
</SerializedObjectDisplay>
)}
</I18n>
</DragAndDropContextProvider>
);

View File

@@ -10,6 +10,7 @@ import { testProject } from '../../GDevelopJsInitializerDecorator';
import paperDecorator from '../../PaperDecorator';
import InstancePropertiesEditor from '../../../InstancesEditor/InstancePropertiesEditor';
import SerializedObjectDisplay from '../../SerializedObjectDisplay';
import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider';
export default {
title: 'LayoutEditor/InstancePropertiesEditor',
@@ -17,20 +18,42 @@ export default {
decorators: [paperDecorator],
};
export const Default = () => (
<I18n>
{({ i18n }) => (
<SerializedObjectDisplay object={testProject.testLayout}>
<InstancePropertiesEditor
i18n={i18n}
project={testProject.project}
layout={testProject.testLayout}
instances={[testProject.testLayoutInstance1]}
editInstanceVariables={action('edit instance variables')}
onGetInstanceSize={() => [100, 101, 102]}
onEditObjectByName={action('edit object')}
/>
</SerializedObjectDisplay>
)}
</I18n>
export const Instance2d = () => (
<DragAndDropContextProvider>
<I18n>
{({ i18n }) => (
<SerializedObjectDisplay object={testProject.testLayout}>
<InstancePropertiesEditor
i18n={i18n}
project={testProject.project}
layout={testProject.testLayout}
instances={[testProject.testSpriteObjectInstance]}
editInstanceVariables={action('edit instance variables')}
onGetInstanceSize={() => [100, 101, 102]}
onEditObjectByName={action('edit object')}
/>
</SerializedObjectDisplay>
)}
</I18n>
</DragAndDropContextProvider>
);
export const Instance3d = () => (
<DragAndDropContextProvider>
<I18n>
{({ i18n }) => (
<SerializedObjectDisplay object={testProject.testLayout}>
<InstancePropertiesEditor
i18n={i18n}
project={testProject.project}
layout={testProject.testLayout}
instances={[testProject.testLayoutInstance2]}
editInstanceVariables={action('edit instance variables')}
onGetInstanceSize={() => [100, 101, 102]}
onEditObjectByName={action('edit object')}
/>
</SerializedObjectDisplay>
)}
</I18n>
</DragAndDropContextProvider>
);

View File

@@ -0,0 +1,99 @@
// @flow
import * as React from 'react';
import muiDecorator from '../../ThemeDecorator';
import paperDecorator from '../../PaperDecorator';
import CompactSelectField from '../../../UI/CompactSelectField';
import { ColumnStackLayout } from '../../../UI/Layout';
import Layers from '../../../UI/CustomSvgIcons/Layers';
import ElementHighlighterProvider from '../../ElementHighlighterProvider';
export default {
title: 'UI Building Blocks/CompactSelectField',
component: CompactSelectField,
decorators: [paperDecorator, muiDecorator],
};
const options = [
<option>First option</option>,
<option>Segundo</option>,
<option>Troisième option</option>,
];
export const Default = () => {
const [value, setValue] = React.useState<string>('');
const [value1, setValue1] = React.useState<string>('');
const [value2, setValue2] = React.useState<string>('');
const [value3, setValue3] = React.useState<string>('');
const [value4, setValue4] = React.useState<string>('');
const [value5, setValue5] = React.useState<string>('');
return (
<ElementHighlighterProvider
elements={[
{ label: 'With icon', id: 'with-icon' },
{ label: 'Without icon', id: 'without-icon' },
]}
>
<ColumnStackLayout expand>
<CompactSelectField value={value} onChange={setValue} id="without-icon">
{options}
</CompactSelectField>
<CompactSelectField value={value1} onChange={setValue1} errored>
{options}
</CompactSelectField>
<CompactSelectField value={value2} onChange={setValue2}>
{[
<option style={{ display: 'none' }}>Select an option</option>,
...options,
]}
</CompactSelectField>
<CompactSelectField
disabled
value={'disabled field'}
onChange={() => {}}
>
{options}
</CompactSelectField>
<CompactSelectField
value={value3}
onChange={setValue3}
renderLeftIcon={className => <Layers className={className} />}
leftIconTooltip={'Layer'}
id="with-icon"
>
{options}
</CompactSelectField>
<CompactSelectField
value={value4}
onChange={setValue4}
errored
renderLeftIcon={className => <Layers className={className} />}
leftIconTooltip={'Layer'}
>
{options}
</CompactSelectField>
<CompactSelectField
value={value5}
onChange={setValue5}
renderLeftIcon={className => <Layers className={className} />}
leftIconTooltip={'Layer'}
>
{[
<option style={{ display: 'none' }}>Select an option</option>,
...options,
]}
</CompactSelectField>
<CompactSelectField
disabled
value={'disabled field'}
onChange={() => {}}
renderLeftIcon={className => <Layers className={className} />}
leftIconTooltip={'Layer'}
>
{options}
</CompactSelectField>
</ColumnStackLayout>
</ElementHighlighterProvider>
);
};

View File

@@ -0,0 +1,160 @@
// @flow
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import muiDecorator from '../../ThemeDecorator';
import paperDecorator from '../../PaperDecorator';
import CompactSemiControlledNumberField from '../../../UI/CompactSemiControlledNumberField';
import { ColumnStackLayout } from '../../../UI/Layout';
import Angle from '../../../UI/CustomSvgIcons/Angle';
import { Column } from '../../../UI/Grid';
import ElementHighlighterProvider from '../../ElementHighlighterProvider';
import Text from '../../../UI/Text';
import Restore from '../../../UI/CustomSvgIcons/Restore';
export default {
title: 'UI Building Blocks/CompactSemiControlledNumberField',
component: CompactSemiControlledNumberField,
decorators: [paperDecorator, muiDecorator],
};
export const Default = () => {
const [value, setValue] = React.useState<number>(45);
const [value1, setValue1] = React.useState<number>(1);
const [value2, setValue2] = React.useState<number>(25);
const [value3, setValue3] = React.useState<number>(-12);
const [value4, setValue4] = React.useState<number>(566560);
const [value5, setValue5] = React.useState<number>(334);
const [value6, setValue6] = React.useState<number>(334);
return (
<ElementHighlighterProvider
elements={[
{ label: 'With icon', id: 'with-icon' },
{ label: 'Without icon', id: 'without-icon' },
{ label: 'With end adornment', id: 'with-end-adornment' },
]}
>
<ColumnStackLayout expand useLargeSpacer>
<Column noMargin>
<CompactSemiControlledNumberField
commitOnBlur
value={value}
onChange={setValue}
id="without-icon"
/>
<div>Commits on blur: state value is {value}</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
commitOnBlur
value={value1}
onChange={setValue1}
errored
errorText={'This value cannot be used'}
/>
<div>Commits on blur: state value is {value1}</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
commitOnBlur
value={value2}
onChange={setValue2}
placeholder="With placeholder"
/>
<div>Commits on blur: state value is {value2}</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
disabled
value={666}
onChange={() => {}}
/>
<div>Disabled field</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
id="with-icon"
value={value3}
onChange={setValue3}
renderLeftIcon={className => <Angle className={className} />}
useLeftIconAsNumberControl
leftIconTooltip={'Angle'}
/>
<div>State value is {value3}</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
value={value4}
onChange={setValue4}
errored
errorText={'An error occurred.'}
renderLeftIcon={className => <Angle className={className} />}
useLeftIconAsNumberControl
leftIconTooltip={'Angle'}
/>
<div>State value is {value4}</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
commitOnBlur
value={value5}
onChange={setValue5}
placeholder="With placeholder"
renderLeftIcon={className => <Angle className={className} />}
useLeftIconAsNumberControl
leftIconTooltip={'Angle'}
/>
<div>Commits on blur: state value is {value5}</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
disabled
value={777}
onChange={() => {}}
renderLeftIcon={className => <Angle className={className} />}
useLeftIconAsNumberControl
leftIconTooltip={'Angle'}
/>
<div>Disabled field</div>
</Column>
<Text>With end adornment</Text>
<Column noMargin>
<CompactSemiControlledNumberField
value={value6}
onChange={setValue6}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
useLeftIconAsNumberControl
id="with-end-adornment"
renderEndAdornmentOnHover={className => (
<Restore className={className} />
)}
onClickEndAdornment={action('onClickEndAdornment')}
/>
<div>State value is {value6}</div>
</Column>
<Column noMargin>
<CompactSemiControlledNumberField
disabled
value={45.1}
onChange={() => {}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle disabled'}
useLeftIconAsNumberControl
renderEndAdornmentOnHover={className => (
<Restore className={className} />
)}
onClickEndAdornment={action('onClickEndAdornment')}
/>
<div>Disabled field</div>
</Column>
</ColumnStackLayout>
</ElementHighlighterProvider>
);
};

View File

@@ -0,0 +1,155 @@
// @flow
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import muiDecorator from '../../ThemeDecorator';
import paperDecorator from '../../PaperDecorator';
import CompactSemiControlledTextField from '../../../UI/CompactSemiControlledTextField';
import { ColumnStackLayout } from '../../../UI/Layout';
import Angle from '../../../UI/CustomSvgIcons/Angle';
import { Column } from '../../../UI/Grid';
import ElementHighlighterProvider from '../../ElementHighlighterProvider';
import Text from '../../../UI/Text';
import Restore from '../../../UI/CustomSvgIcons/Restore';
export default {
title: 'UI Building Blocks/CompactSemiControlledTextField',
component: CompactSemiControlledTextField,
decorators: [paperDecorator, muiDecorator],
};
export const Default = () => {
const [value, setValue] = React.useState<string>('');
const [value1, setValue1] = React.useState<string>('');
const [value2, setValue2] = React.useState<string>('');
const [value3, setValue3] = React.useState<string>('');
const [value4, setValue4] = React.useState<string>('');
const [value5, setValue5] = React.useState<string>('');
const [value6, setValue6] = React.useState<string>('');
return (
<ElementHighlighterProvider
elements={[
{ label: 'With icon', id: 'with-icon' },
{ label: 'Without icon', id: 'without-icon' },
{ label: 'With end adornment', id: 'with-end-adornment' },
]}
>
<ColumnStackLayout expand useLargeSpacer>
<Column noMargin>
<CompactSemiControlledTextField
commitOnBlur
value={value}
onChange={setValue}
id="without-icon"
/>
<div>State value is {value}</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
commitOnBlur
value={value1}
onChange={setValue1}
errored
errorText={'This value cannot be used'}
/>
<div>State value is {value1}</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
commitOnBlur
value={value2}
onChange={setValue2}
placeholder="With placeholder"
/>
<div>State value is {value2}</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
disabled
value={'disabled field'}
onChange={() => {}}
/>
<div>Disabled field</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
id="with-icon"
commitOnBlur
value={value3}
onChange={setValue3}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
/>
<div>State value is {value3}</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
commitOnBlur
value={value4}
onChange={setValue4}
errored
errorText={'An error occurred.'}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
/>
<div>State value is {value4}</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
commitOnBlur
value={value5}
onChange={setValue5}
placeholder="With placeholder"
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
/>
<div>State value is {value5}</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
disabled
value={'disabled field'}
onChange={() => {}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
/>
<div>Disabled field</div>
</Column>
<Text>With end adornment</Text>
<Column noMargin>
<CompactSemiControlledTextField
value={value6}
onChange={setValue6}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
id="with-end-adornment"
renderEndAdornmentOnHover={className => (
<Restore className={className} />
)}
onClickEndAdornment={action('onClickEndAdornment')}
/>
<div>State value is {value6}</div>
</Column>
<Column noMargin>
<CompactSemiControlledTextField
disabled
value={'Disabled field'}
onChange={valueAsString => {}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle disabled'}
renderEndAdornmentOnHover={className => (
<Restore className={className} />
)}
onClickEndAdornment={action('onClickEndAdornment')}
/>
</Column>
</ColumnStackLayout>
</ElementHighlighterProvider>
);
};

View File

@@ -0,0 +1,143 @@
// @flow
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import muiDecorator from '../../ThemeDecorator';
import paperDecorator from '../../PaperDecorator';
import CompactTextField from '../../../UI/CompactTextField';
import { ColumnStackLayout } from '../../../UI/Layout';
import Angle from '../../../UI/CustomSvgIcons/Angle';
import Text from '../../../UI/Text';
import ElementHighlighterProvider from '../../ElementHighlighterProvider';
import Restore from '../../../UI/CustomSvgIcons/Restore';
export default {
title: 'UI Building Blocks/CompactTextField',
component: CompactTextField,
decorators: [paperDecorator, muiDecorator],
};
export const Default = () => {
const [value, setValue] = React.useState<string>('');
const [value1, setValue1] = React.useState<string>('');
const [value2, setValue2] = React.useState<string>('');
const [value3, setValue3] = React.useState<string>('');
const [value4, setValue4] = React.useState<string>('');
const [value5, setValue5] = React.useState<string>('');
const [value8, setValue8] = React.useState<string>('');
const [value6, setValue6] = React.useState<number>(0);
const [value7, setValue7] = React.useState<number>(0);
return (
<ElementHighlighterProvider
elements={[
{ label: 'Text with icon', id: 'with-icon' },
{ label: 'text without icon', id: 'without-icon' },
{ label: 'Number with icon', id: 'number-with-icon' },
{ label: 'Text with end adornment', id: 'text-with-end-adornment' },
]}
>
<ColumnStackLayout expand>
<CompactTextField value={value} onChange={setValue} id="without-icon" />
<CompactTextField value={value1} onChange={setValue1} errored />
<CompactTextField
value={value2}
onChange={setValue2}
placeholder="With placeholder"
/>
<CompactTextField
disabled
value={'disabled field'}
onChange={() => {}}
/>
<CompactTextField
value={value3}
onChange={setValue3}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
id="with-icon"
/>
<CompactTextField
value={value4}
onChange={setValue4}
errored
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
/>
<CompactTextField
value={value5}
onChange={setValue5}
placeholder="With placeholder"
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
/>
<CompactTextField
disabled
value={'disabled field'}
onChange={() => {}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
/>
<Text>Numbers</Text>
<CompactTextField
type="number"
value={value6}
onChange={valueAsString => {
if (!valueAsString) setValue6(valueAsString);
else setValue6(parseFloat(valueAsString) || 0);
}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
useLeftIconAsNumberControl
id="number-with-icon"
/>
<CompactTextField
type="number"
value={value7}
onChange={valueAsString => {
if (!valueAsString) setValue7(valueAsString);
else setValue7(parseFloat(valueAsString) || 0);
}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
placeholder="80"
useLeftIconAsNumberControl
/>
<CompactTextField
type="number"
disabled
value={45}
onChange={valueAsString => {}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle disabled'}
useLeftIconAsNumberControl
/>
<Text>With end adornment</Text>
<CompactTextField
value={value8}
onChange={setValue8}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle'}
useLeftIconAsNumberControl
id="text-with-end-adornment"
renderEndAdornmentOnHover={className => (
<Restore className={className} />
)}
onClickEndAdornment={action('onClickEndAdornment')}
/>
<CompactTextField
disabled
value={'Disabled field'}
onChange={valueAsString => {}}
renderLeftIcon={className => <Angle className={className} />}
leftIconTooltip={'Angle disabled'}
useLeftIconAsNumberControl
renderEndAdornmentOnHover={className => (
<Restore className={className} />
)}
onClickEndAdornment={action('onClickEndAdornment')}
/>
</ColumnStackLayout>
</ElementHighlighterProvider>
);
};