mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Allow extensions to define labels for properties with string selectors (#7825)
This commit is contained in:
@@ -248,12 +248,13 @@ gd::String PropertyFunctionGenerator::GetStringifiedExtraInfo(
|
||||
gd::String arrayString;
|
||||
arrayString += "[";
|
||||
bool isFirst = true;
|
||||
for (const gd::String &choice : property.GetExtraInfo()) {
|
||||
for (const auto &choice : property.GetChoices()) {
|
||||
if (!isFirst) {
|
||||
arrayString += ",";
|
||||
}
|
||||
isFirst = false;
|
||||
arrayString += "\"" + choice + "\"";
|
||||
// TODO Handle labels (and search "choice label")
|
||||
arrayString += "\"" + choice.GetValue() + "\"";
|
||||
}
|
||||
arrayString += "]";
|
||||
return arrayString;
|
||||
|
@@ -34,6 +34,20 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
|
||||
}
|
||||
}
|
||||
|
||||
if (!choices.empty()
|
||||
// Compatibility with GD <= 5.5.239
|
||||
|| !extraInformation.empty()
|
||||
// end of compatibility code
|
||||
) {
|
||||
SerializerElement &choicesElement = element.AddChild("choices");
|
||||
choicesElement.ConsiderAsArrayOf("choice");
|
||||
for (const auto &choice : choices) {
|
||||
auto &choiceElement = choicesElement.AddChild("Choice");
|
||||
choiceElement.SetStringAttribute("value", choice.GetValue());
|
||||
choiceElement.SetStringAttribute("label", choice.GetLabel());
|
||||
}
|
||||
}
|
||||
|
||||
if (hidden) {
|
||||
element.AddChild("hidden").SetBoolValue(hidden);
|
||||
}
|
||||
@@ -80,6 +94,26 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
|
||||
extraInformationElement.GetChild(i).GetStringValue());
|
||||
}
|
||||
|
||||
if (element.HasChild("choices")) {
|
||||
choices.clear();
|
||||
const SerializerElement &choicesElement = element.GetChild("choices");
|
||||
choicesElement.ConsiderAsArrayOf("choice");
|
||||
for (std::size_t i = 0; i < choicesElement.GetChildrenCount(); ++i) {
|
||||
auto &choiceElement = choicesElement.GetChild(i);
|
||||
AddChoice(choiceElement.GetStringAttribute("value"),
|
||||
choiceElement.GetStringAttribute("label"));
|
||||
}
|
||||
}
|
||||
// Compatibility with GD <= 5.5.239
|
||||
else if (type == "Choice") {
|
||||
choices.clear();
|
||||
for (auto &choiceValue : extraInformation) {
|
||||
AddChoice(choiceValue, choiceValue);
|
||||
}
|
||||
extraInformation.clear();
|
||||
}
|
||||
// end of compatibility code
|
||||
|
||||
hidden = element.HasChild("hidden")
|
||||
? element.GetChild("hidden").GetBoolValue()
|
||||
: false;
|
||||
|
@@ -116,6 +116,11 @@ class GD_CORE_API PropertyDescriptor {
|
||||
return *this;
|
||||
}
|
||||
|
||||
PropertyDescriptor& ClearChoices() {
|
||||
choices.clear();
|
||||
return *this;
|
||||
}
|
||||
|
||||
PropertyDescriptor& AddChoice(const gd::String& value,
|
||||
const gd::String& label) {
|
||||
choices.push_back(PropertyDescriptorChoice(value, label));
|
||||
|
@@ -139,8 +139,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
.SetLabel("Dot shape")
|
||||
.SetDescription("The shape is used for collision.")
|
||||
.SetGroup("Movement");
|
||||
property.GetExtraInfo().push_back("Dot shape");
|
||||
property.GetExtraInfo().push_back("Bounding disk");
|
||||
property.AddChoice("DotShape", "Dot shape");
|
||||
property.AddChoice("BoundingDisk", "Bounding disk");
|
||||
|
||||
gd::PropertyFunctionGenerator::GenerateBehaviorGetterAndSetter(
|
||||
project, extension, behavior, property, false);
|
||||
@@ -157,7 +157,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
gd::EventsFunction::ExpressionAndCondition);
|
||||
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
|
||||
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
|
||||
"[\"Dot shape\",\"Bounding disk\"]");
|
||||
"[\"DotShape\",\"BoundingDisk\"]");
|
||||
}
|
||||
|
||||
SECTION("Can generate functions for a boolean property in a behavior") {
|
||||
@@ -386,8 +386,8 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
.SetLabel("Dot shape")
|
||||
.SetDescription("The shape is used for collision.")
|
||||
.SetGroup("Movement");
|
||||
property.GetExtraInfo().push_back("Dot shape");
|
||||
property.GetExtraInfo().push_back("Bounding disk");
|
||||
property.AddChoice("DotShape", "Dot shape");
|
||||
property.AddChoice("BoundingDisk", "Bounding disk");
|
||||
|
||||
gd::PropertyFunctionGenerator::GenerateObjectGetterAndSetter(
|
||||
project, extension, object, property);
|
||||
@@ -404,7 +404,7 @@ TEST_CASE("PropertyFunctionGenerator", "[common]") {
|
||||
gd::EventsFunction::ExpressionAndCondition);
|
||||
REQUIRE(getter.GetExpressionType().GetName() == "stringWithSelector");
|
||||
REQUIRE(getter.GetExpressionType().GetExtraInfo() ==
|
||||
"[\"Dot shape\",\"Bounding disk\"]");
|
||||
"[\"DotShape\",\"BoundingDisk\"]");
|
||||
}
|
||||
|
||||
SECTION("Can generate functions for a boolean property in an object") {
|
||||
|
@@ -1174,6 +1174,7 @@ interface PropertyDescriptor {
|
||||
[Const, Ref] DOMString GetDescription();
|
||||
[Ref] PropertyDescriptor SetGroup([Const] DOMString label);
|
||||
[Const, Ref] DOMString GetGroup();
|
||||
[Ref] PropertyDescriptor ClearChoices();
|
||||
[Ref] PropertyDescriptor AddChoice([Const] DOMString value, [Const] DOMString label);
|
||||
[Const, Ref] VectorPropertyDescriptorChoice GetChoices();
|
||||
[Ref] PropertyDescriptor AddExtraInfo([Const] DOMString type);
|
||||
|
1
GDevelop.js/types.d.ts
vendored
1
GDevelop.js/types.d.ts
vendored
@@ -971,6 +971,7 @@ export class PropertyDescriptor extends EmscriptenObject {
|
||||
getDescription(): string;
|
||||
setGroup(label: string): PropertyDescriptor;
|
||||
getGroup(): string;
|
||||
clearChoices(): PropertyDescriptor;
|
||||
addChoice(value: string, label: string): PropertyDescriptor;
|
||||
getChoices(): VectorPropertyDescriptorChoice;
|
||||
addExtraInfo(type: string): PropertyDescriptor;
|
||||
|
@@ -11,6 +11,7 @@ declare class gdPropertyDescriptor {
|
||||
getDescription(): string;
|
||||
setGroup(label: string): gdPropertyDescriptor;
|
||||
getGroup(): string;
|
||||
clearChoices(): gdPropertyDescriptor;
|
||||
addChoice(value: string, label: string): gdPropertyDescriptor;
|
||||
getChoices(): gdVectorPropertyDescriptorChoice;
|
||||
addExtraInfo(type: string): gdPropertyDescriptor;
|
||||
|
102
newIDE/app/src/ChoicesEditor/index.js
Normal file
102
newIDE/app/src/ChoicesEditor/index.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import {
|
||||
ResponsiveLineStackLayout,
|
||||
LineStackLayout,
|
||||
ColumnStackLayout,
|
||||
} from '../UI/Layout';
|
||||
import { Line } from '../UI/Grid';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Add from '../UI/CustomSvgIcons/Add';
|
||||
import Trash from '../UI/CustomSvgIcons/Trash';
|
||||
|
||||
export type Choice = {|
|
||||
value: string,
|
||||
label: string,
|
||||
|};
|
||||
|
||||
type ChoicesEditorProps = {|
|
||||
choices: Array<Choice>,
|
||||
setChoices: (Array<Choice>) => void,
|
||||
disabled?: boolean,
|
||||
hideLabels?: boolean,
|
||||
|};
|
||||
|
||||
const ChoicesEditor = ({
|
||||
choices,
|
||||
setChoices,
|
||||
disabled,
|
||||
hideLabels,
|
||||
}: ChoicesEditorProps) => {
|
||||
const updateChoices = () => setChoices(choices);
|
||||
|
||||
return (
|
||||
<ResponsiveLineStackLayout>
|
||||
<ColumnStackLayout justifyContent="flex-end" expand noMargin>
|
||||
{choices.map((item, index) => (
|
||||
<LineStackLayout
|
||||
key={index}
|
||||
justifyContent="flex-end"
|
||||
expand
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
<SemiControlledTextField
|
||||
disabled={disabled}
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Value</Trans>}
|
||||
value={item.value}
|
||||
onChange={text => {
|
||||
choices[index].value = text;
|
||||
updateChoices();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
{!hideLabels && (
|
||||
<SemiControlledTextField
|
||||
disabled={disabled}
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Label displayed in editor</Trans>}
|
||||
value={item.label}
|
||||
onChange={text => {
|
||||
choices[index].label = text;
|
||||
updateChoices();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
tooltip={t`Delete option`}
|
||||
onClick={() => {
|
||||
choices.splice(index, 1);
|
||||
updateChoices();
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</LineStackLayout>
|
||||
))}
|
||||
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
disabled={disabled}
|
||||
primary
|
||||
onClick={() => {
|
||||
choices.push({ value: 'New Option', label: '' });
|
||||
updateChoices();
|
||||
}}
|
||||
label={<Trans>Add a new option</Trans>}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</Line>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChoicesEditor;
|
@@ -15,7 +15,7 @@ import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
import StringArrayEditor from '../StringArrayEditor';
|
||||
import ChoicesEditor, { type Choice } from '../ChoicesEditor';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import BehaviorTypeSelector from '../BehaviorTypeSelector';
|
||||
import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete';
|
||||
@@ -124,9 +124,13 @@ const getValidatedPropertyName = (
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
const extraInfoVector = property.getExtraInfo();
|
||||
return extraInfoVector.toJSArray();
|
||||
const getChoicesArray = (
|
||||
property: gdNamedPropertyDescriptor
|
||||
): Array<Choice> => {
|
||||
return mapVector(property.getChoices(), choice => ({
|
||||
value: choice.getValue(),
|
||||
label: choice.getLabel(),
|
||||
}));
|
||||
};
|
||||
|
||||
export default function EventsBasedBehaviorPropertiesEditor({
|
||||
@@ -397,17 +401,18 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
[properties, forceUpdate, onPropertiesUpdated]
|
||||
);
|
||||
|
||||
const setChoiceExtraInfo = React.useCallback(
|
||||
const setChoices = React.useCallback(
|
||||
(property: gdNamedPropertyDescriptor) => {
|
||||
return (newExtraInfo: Array<string>) => {
|
||||
const defaultValueIndex = getExtraInfoArray(property).indexOf(
|
||||
property.getValue()
|
||||
return (choices: Array<Choice>) => {
|
||||
property.clearChoices();
|
||||
for (const choice of choices) {
|
||||
property.addChoice(choice.value, choice.label);
|
||||
}
|
||||
property.setValue(
|
||||
getChoicesArray(property).includes(property.getValue())
|
||||
? property.getValue()
|
||||
: ''
|
||||
);
|
||||
const vectorString = new gd.VectorString();
|
||||
newExtraInfo.forEach(item => vectorString.push_back(item));
|
||||
property.setExtraInfo(vectorString);
|
||||
vectorString.delete();
|
||||
property.setValue(newExtraInfo[defaultValueIndex] || '');
|
||||
forceUpdate();
|
||||
};
|
||||
},
|
||||
@@ -954,12 +959,12 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
{getExtraInfoArray(property).map(
|
||||
{getChoicesArray(property).map(
|
||||
(choice, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={choice}
|
||||
label={choice}
|
||||
value={choice.value}
|
||||
label={choice.value}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -967,11 +972,9 @@ export default function EventsBasedBehaviorPropertiesEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{property.getType() === 'Choice' && (
|
||||
<StringArrayEditor
|
||||
extraInfo={getExtraInfoArray(property)}
|
||||
setExtraInfo={setChoiceExtraInfo(
|
||||
property
|
||||
)}
|
||||
<ChoicesEditor
|
||||
choices={getChoicesArray(property)}
|
||||
setChoices={setChoices(property)}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
|
@@ -15,7 +15,7 @@ import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
import StringArrayEditor from '../StringArrayEditor';
|
||||
import ChoicesEditor, { type Choice } from '../ChoicesEditor';
|
||||
import ColorField from '../UI/ColorField';
|
||||
import SemiControlledAutoComplete from '../UI/SemiControlledAutoComplete';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
@@ -119,9 +119,13 @@ const getValidatedPropertyName = (
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
const extraInfoVector = property.getExtraInfo();
|
||||
return extraInfoVector.toJSArray();
|
||||
const getChoicesArray = (
|
||||
property: gdNamedPropertyDescriptor
|
||||
): Array<Choice> => {
|
||||
return mapVector(property.getChoices(), choice => ({
|
||||
value: choice.getValue(),
|
||||
label: choice.getLabel(),
|
||||
}));
|
||||
};
|
||||
|
||||
export default function EventsBasedObjectPropertiesEditor({
|
||||
@@ -404,17 +408,18 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
[eventsBasedObject, forceUpdate, onPropertiesUpdated]
|
||||
);
|
||||
|
||||
const setChoiceExtraInfo = React.useCallback(
|
||||
const setChoices = React.useCallback(
|
||||
(property: gdNamedPropertyDescriptor) => {
|
||||
return (newExtraInfo: Array<string>) => {
|
||||
const defaultValueIndex = getExtraInfoArray(property).indexOf(
|
||||
property.getValue()
|
||||
return (choices: Array<Choice>) => {
|
||||
property.clearChoices();
|
||||
for (const choice of choices) {
|
||||
property.addChoice(choice.value, choice.label);
|
||||
}
|
||||
property.setValue(
|
||||
getChoicesArray(property).includes(property.getValue())
|
||||
? property.getValue()
|
||||
: ''
|
||||
);
|
||||
const vectorString = new gd.VectorString();
|
||||
newExtraInfo.forEach(item => vectorString.push_back(item));
|
||||
property.setExtraInfo(vectorString);
|
||||
vectorString.delete();
|
||||
property.setValue(newExtraInfo[defaultValueIndex] || '');
|
||||
forceUpdate();
|
||||
};
|
||||
},
|
||||
@@ -877,12 +882,12 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
}}
|
||||
fullWidth
|
||||
>
|
||||
{getExtraInfoArray(property).map(
|
||||
{getChoicesArray(property).map(
|
||||
(choice, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={choice}
|
||||
label={choice}
|
||||
value={choice.value}
|
||||
label={choice.label}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
@@ -906,11 +911,9 @@ export default function EventsBasedObjectPropertiesEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{property.getType() === 'Choice' && (
|
||||
<StringArrayEditor
|
||||
extraInfo={getExtraInfoArray(property)}
|
||||
setExtraInfo={setChoiceExtraInfo(
|
||||
property
|
||||
)}
|
||||
<ChoicesEditor
|
||||
choices={getChoicesArray(property)}
|
||||
setChoices={setChoices(property)}
|
||||
/>
|
||||
)}
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
|
@@ -9,7 +9,7 @@ import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import ObjectTypeSelector from '../../ObjectTypeSelector';
|
||||
import BehaviorTypeSelector from '../../BehaviorTypeSelector';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
import StringArrayEditor from '../../StringArrayEditor';
|
||||
import ChoicesEditor, { type Choice } from '../../ChoicesEditor';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
import ResourceTypeSelectField from './ResourceTypeSelectField';
|
||||
|
||||
@@ -295,11 +295,18 @@ export default function ValueTypeEditor({
|
||||
)}
|
||||
</ResponsiveLineStackLayout>
|
||||
{valueTypeMetadata.getName() === 'stringWithSelector' && (
|
||||
<StringArrayEditor
|
||||
<ChoicesEditor
|
||||
disabled={disabled}
|
||||
extraInfo={getExtraInfoArray(valueTypeMetadata)}
|
||||
setExtraInfo={(newExtraInfo: Array<string>) => {
|
||||
valueTypeMetadata.setExtraInfo(JSON.stringify(newExtraInfo));
|
||||
choices={getExtraInfoArray(valueTypeMetadata).map(value => ({
|
||||
value,
|
||||
label: '',
|
||||
}))}
|
||||
hideLabels
|
||||
setChoices={(choices: Array<Choice>) => {
|
||||
// TODO Handle labels (and search "choice label")
|
||||
valueTypeMetadata.setExtraInfo(
|
||||
JSON.stringify(choices.map(choice => choice.value))
|
||||
);
|
||||
forceUpdate();
|
||||
onTypeUpdated();
|
||||
}}
|
||||
|
@@ -1,77 +0,0 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import { ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import { IconButton } from '@material-ui/core';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Add from '../UI/CustomSvgIcons/Add';
|
||||
import Trash from '../UI/CustomSvgIcons/Trash';
|
||||
|
||||
type StringArrayEditorProps = {|
|
||||
extraInfo: Array<string>,
|
||||
setExtraInfo: (Array<string>) => void,
|
||||
disabled?: boolean,
|
||||
|};
|
||||
|
||||
const StringArrayEditor = ({
|
||||
extraInfo,
|
||||
setExtraInfo,
|
||||
disabled,
|
||||
}: StringArrayEditorProps) => {
|
||||
const updateExtraInfo = () => setExtraInfo(extraInfo);
|
||||
|
||||
return (
|
||||
<ResponsiveLineStackLayout>
|
||||
<Column justifyContent="flex-end" expand noMargin>
|
||||
{extraInfo.map((item, index) => (
|
||||
<Line
|
||||
key={index}
|
||||
justifyContent="flex-end"
|
||||
expand
|
||||
alignItems="center"
|
||||
noMargin
|
||||
>
|
||||
<SemiControlledTextField
|
||||
disabled={disabled}
|
||||
commitOnBlur
|
||||
value={item}
|
||||
onChange={text => {
|
||||
extraInfo[index] = text;
|
||||
updateExtraInfo();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
tooltip={t`Delete option`}
|
||||
onClick={() => {
|
||||
extraInfo.splice(index, 1);
|
||||
updateExtraInfo();
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</IconButton>
|
||||
</Line>
|
||||
))}
|
||||
|
||||
<Line justifyContent="flex-end" expand>
|
||||
<RaisedButton
|
||||
disabled={disabled}
|
||||
primary
|
||||
onClick={() => {
|
||||
extraInfo.push('New Option');
|
||||
updateExtraInfo();
|
||||
}}
|
||||
label={<Trans>Add a new option</Trans>}
|
||||
icon={<Add />}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default StringArrayEditor;
|
Reference in New Issue
Block a user