Add support for private functions to extensions made in the editor (#2305)

* Actions/conditions/expressions made with events in the editor can be marked "private": they can be used inside the extension (or inside the behavior), but can't be used in the scene events sheet.
* This is useful to share some "internal" logic that you don't want to make available from the events sheet.
This commit is contained in:
Arthur Pacaud
2021-02-15 18:31:38 +01:00
committed by GitHub
parent 5e71f28da5
commit 801d58695c
14 changed files with 123 additions and 33 deletions

View File

@@ -17,6 +17,7 @@ void EventsFunction::SerializeTo(SerializerElement& element) const {
element.SetAttribute("fullName", fullName);
element.SetAttribute("description", description);
element.SetAttribute("sentence", sentence);
element.SetBoolAttribute("private", isPrivate);
events.SerializeTo(element.AddChild("events"));
gd::String functionTypeStr = "Action";
@@ -43,6 +44,7 @@ void EventsFunction::UnserializeFrom(gd::Project& project,
fullName = element.GetStringAttribute("fullName");
description = element.GetStringAttribute("description");
sentence = element.GetStringAttribute("sentence");
isPrivate = element.GetBoolAttribute("private");
events.UnserializeFrom(project, element.GetChild("events"));
gd::String functionTypeStr = element.GetStringAttribute("functionType");

View File

@@ -8,6 +8,7 @@
#define GDCORE_EVENTSFUNCTION_H
#include <vector>
#include "GDCore/Events/EventsList.h"
#include "GDCore/Project/ObjectGroupsContainer.h"
#include "GDCore/String.h"
@@ -116,6 +117,19 @@ class GD_CORE_API EventsFunction {
*/
FunctionType GetFunctionType() const { return functionType; };
/**
* \brief Returns true if the function is private.
*/
bool IsPrivate() { return isPrivate; }
/**
* \brief Sets the privateness of the function.
*/
EventsFunction& SetPrivate(bool _isPrivate) {
isPrivate = _isPrivate;
return *this;
}
/**
* \brief Return the events.
*/
@@ -128,7 +142,7 @@ class GD_CORE_API EventsFunction {
/**
* \brief Return the parameters of the function.
*
*
* \note During code/extension generation, new parameters are added
* to the generated function, like "runtimeScene" and "eventsFunctionContext".
* This should be transparent to the user.
@@ -143,12 +157,14 @@ class GD_CORE_API EventsFunction {
std::vector<gd::ParameterMetadata>& GetParameters() { return parameters; };
/**
* \brief Return a reference to the object groups that can be used in the function.
* \brief Return a reference to the object groups that can be used in the
* function.
*/
ObjectGroupsContainer& GetObjectGroups() { return objectGroups; }
/**
* \brief Return a const reference to the object groups that can be used in the function.
* \brief Return a const reference to the object groups that can be used in
* the function.
*/
const ObjectGroupsContainer& GetObjectGroups() const { return objectGroups; }
@@ -176,6 +192,7 @@ class GD_CORE_API EventsFunction {
FunctionType functionType;
std::vector<gd::ParameterMetadata> parameters;
gd::ObjectGroupsContainer objectGroups;
bool isPrivate = false;
};
} // namespace gd

View File

@@ -1857,6 +1857,8 @@ interface EventsFunction {
[Const, Ref] DOMString GetFullName();
[Ref] EventsFunction SetSentence([Const] DOMString sentence);
[Const, Ref] DOMString GetSentence();
[Ref] EventsFunction SetPrivate(boolean isPrivate);
boolean IsPrivate();
[Ref] EventsFunction SetFunctionType(EventsFunction_FunctionType type);
EventsFunction_FunctionType GetFunctionType();

View File

@@ -14,6 +14,8 @@ declare class gdEventsFunction {
getFullName(): string;
setSentence(sentence: string): gdEventsFunction;
getSentence(): string;
setPrivate(isPrivate: boolean): gdEventsFunction;
isPrivate(): boolean;
setFunctionType(type: EventsFunction_FunctionType): gdEventsFunction;
getFunctionType(): EventsFunction_FunctionType;
getEvents(): gdEventsList;

View File

@@ -222,9 +222,10 @@ const generateFreeFunction = (
// Hide "lifecycle" functions as they are called automatically by
// the game engine.
if (isExtensionLifecycleEventsFunction(eventsFunction.getName())) {
if (isExtensionLifecycleEventsFunction(eventsFunction.getName()))
instructionOrExpression.setHidden();
}
if (eventsFunction.isPrivate()) instructionOrExpression.setPrivate();
const codeNamespace = getFreeFunctionCodeNamespace(
eventsFunction,
@@ -348,6 +349,8 @@ function generateBehavior(
instructionOrExpression.setHidden();
}
if (eventsFunction.isPrivate()) instructionOrExpression.setPrivate();
const codeExtraInformation = instructionOrExpression.getCodeExtraInformation();
codeExtraInformation
.setIncludeFile(includeFile)

View File

@@ -37,7 +37,7 @@ export type EventsFunctionCreationParameters = {|
|};
const getEventsFunctionName = (eventsFunction: gdEventsFunction) =>
eventsFunction.getName();
eventsFunction.getName() + (eventsFunction.isPrivate() ? ' (private)' : '');
type State = {|
renamedEventsFunction: ?gdEventsFunction,
@@ -86,6 +86,11 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
searchText: '',
};
_togglePrivate = (eventsFunction: gdEventsFunction) => {
eventsFunction.setPrivate(!eventsFunction.isPrivate());
this.forceUpdate();
};
_deleteEventsFunction = (
eventsFunction: gdEventsFunction,
{ askForConfirmation }: {| askForConfirmation: boolean |}
@@ -223,6 +228,12 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
click: () => this._editName(eventsFunction),
enabled: this.props.canRename(eventsFunction),
},
{
label: eventsFunction.isPrivate()
? i18n._(t`Make public`)
: i18n._(t`Make private`),
click: () => this._togglePrivate(eventsFunction),
},
{
label: i18n._(t`Remove`),
click: () =>

View File

@@ -16,7 +16,10 @@ import {
filterInstructionsList,
deduplicateInstructionsList,
} from '../../InstructionOrExpression/EnumerateInstructions';
import { type EnumeratedInstructionMetadata } from '../../InstructionOrExpression/EnumeratedInstructionOrExpressionMetadata.js';
import {
type EnumeratedInstructionMetadata,
filterEnumeratedInstructionOrExpressionMetadataByScope,
} from '../../InstructionOrExpression/EnumeratedInstructionOrExpressionMetadata.js';
import { List, type ListItemRefType, ListItem } from '../../UI/List';
import SearchBar, { useShouldAutofocusSearchbar } from '../../UI/SearchBar';
import ThemeConsumer from '../../UI/Theme/ThemeConsumer';
@@ -43,6 +46,7 @@ import {
getObjectOrObjectGroupListItemValue,
getInstructionListItemValue,
} from './SelectorListItems/Keys';
import { type EventsScope } from '../../InstructionOrExpression/EventsScope.flow';
const styles = {
searchBar: {
@@ -63,6 +67,7 @@ type Props = {|
project: gdProject,
globalObjectsContainer: gdObjectsContainer,
objectsContainer: gdObjectsContainer,
scope: EventsScope,
currentTab: TabName,
onChangeTab: TabName => void,
isCondition: boolean,
@@ -88,8 +93,9 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
_selectedItem = React.createRef<ListItemRefType>();
// Free instructions, to be displayed in a tab next to the objects.
freeInstructionsInfo: Array<EnumeratedInstructionMetadata> = enumerateFreeInstructions(
this.props.isCondition
freeInstructionsInfo: Array<EnumeratedInstructionMetadata> = filterEnumeratedInstructionOrExpressionMetadataByScope(
enumerateFreeInstructions(this.props.isCondition),
this.props.scope
);
freeInstructionsInfoTree: InstructionOrExpressionTreeNode = createTree(
this.freeInstructionsInfo
@@ -101,8 +107,9 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
// All the instructions, to be used when searching, so that the search is done
// across all the instructions (including object and behaviors instructions).
allInstructionsInfo: Array<EnumeratedInstructionMetadata> = enumerateAllInstructions(
this.props.isCondition
allInstructionsInfo: Array<EnumeratedInstructionMetadata> = filterEnumeratedInstructionOrExpressionMetadataByScope(
enumerateAllInstructions(this.props.isCondition),
this.props.scope
);
componentDidMount() {

View File

@@ -176,6 +176,7 @@ export default function NewInstructionEditorDialog({
<InstructionOrObjectSelector
style={styles.fullHeightSelector}
project={project}
scope={scope}
currentTab={currentInstructionOrObjectSelectorTab}
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
globalObjectsContainer={globalObjectsContainer}

View File

@@ -142,6 +142,7 @@ export default function NewInstructionEditorMenu({
key="instruction-or-object-selector"
style={styles.fullHeightSelector}
project={project}
scope={scope}
currentTab={currentInstructionOrObjectSelectorTab}
onChangeTab={setCurrentInstructionOrObjectSelectorTab}
globalObjectsContainer={globalObjectsContainer}

View File

@@ -75,6 +75,7 @@ export const enumerateFreeExpressions = (
? extension.getAllStrExpressions()
: extension.getAllExpressions(),
{
extension,
objectMetadata: undefined,
behaviorMetadata: undefined,
}
@@ -94,7 +95,7 @@ export const enumerateObjectExpressions = (
);
const extension = extensionAndObjectMetadata.getExtension();
const objectMetadata = extensionAndObjectMetadata.getMetadata();
const scope = { objectMetadata };
const scope = { extension, objectMetadata };
let objectsExpressions = enumerateExpressionMetadataMap(
'',
@@ -138,7 +139,7 @@ export const enumerateBehaviorExpressions = (
);
const extension = extensionAndBehaviorMetadata.getExtension();
const behaviorMetadata = extensionAndBehaviorMetadata.getMetadata();
const scope = { behaviorMetadata };
const scope = { extension, behaviorMetadata };
return enumerateExpressionMetadataMap(
'',
@@ -173,7 +174,7 @@ export const enumerateAllExpressions = (
type === 'string'
? extension.getAllStrExpressionsForObject(objectType)
: extension.getAllExpressionsForObject(objectType),
{ objectMetadata }
{ extension, objectMetadata }
)
);
});
@@ -188,7 +189,7 @@ export const enumerateAllExpressions = (
type === 'string'
? extension.getAllStrExpressionsForBehavior(behaviorType)
: extension.getAllExpressionsForBehavior(behaviorType),
{ behaviorMetadata }
{ extension, behaviorMetadata }
)
);
});

View File

@@ -231,6 +231,7 @@ export const enumerateAllInstructions = (
prefix,
isCondition ? extension.getAllConditions() : extension.getAllActions(),
{
extension,
objectMetadata: undefined,
behaviorMetadata: undefined,
}
@@ -247,7 +248,7 @@ export const enumerateAllInstructions = (
for (let j = 0; j < allObjectsTypes.size(); ++j) {
const objectType = allObjectsTypes.at(j);
const objectMetadata = extension.getObjectMetadata(objectType);
const scope = { objectMetadata };
const scope = { extension, objectMetadata };
allInstructions = [
...allInstructions,
...enumerateExtensionInstructions(
@@ -271,7 +272,7 @@ export const enumerateAllInstructions = (
for (let j = 0; j < allBehaviorsTypes.size(); ++j) {
const behaviorType = allBehaviorsTypes.at(j);
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
const scope = { behaviorMetadata };
const scope = { extension, behaviorMetadata };
allInstructions = [
...allInstructions,
@@ -377,7 +378,7 @@ export const enumerateObjectAndBehaviorsInstructions = (
//Objects instructions:
if (objectType !== baseObjectType && hasObjectType) {
const objectMetadata = extension.getObjectMetadata(objectType);
const scope = { objectMetadata };
const scope = { extension, objectMetadata };
allInstructions = [
...allInstructions,
@@ -400,7 +401,7 @@ export const enumerateObjectAndBehaviorsInstructions = (
if (hasBaseObjectType) {
const objectMetadata = extension.getObjectMetadata(baseObjectType);
const scope = { objectMetadata };
const scope = { extension, objectMetadata };
allInstructions = [
...allInstructions,
@@ -425,7 +426,7 @@ export const enumerateObjectAndBehaviorsInstructions = (
// eslint-disable-next-line
behaviorTypes.forEach(behaviorType => {
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
const scope = { behaviorMetadata };
const scope = { extension, behaviorMetadata };
allInstructions = [
...enumerateExtensionInstructions(
@@ -479,6 +480,7 @@ export const enumerateFreeInstructions = (
prefix,
isCondition ? extension.getAllConditions() : extension.getAllActions(),
{
extension,
objectMetadata: undefined,
behaviorMetadata: undefined,
}

View File

@@ -3,6 +3,7 @@ import { type EventsScope } from './EventsScope.flow';
const gd: libGDevelop = global.gd;
export type InstructionOrExpressionScope = {|
extension: gdPlatformExtension,
objectMetadata?: ?gdObjectMetadata,
behaviorMetadata?: ?gdBehaviorMetadata,
|};
@@ -29,6 +30,13 @@ export type EnumeratedExpressionMetadata = {|
parameters: Array<gdParameterMetadata>,
|};
// This is copied from gd::WholeProjectRefactorer (see GetBehaviorFullType)
// Could be factored into a single C++ function in gd::PlatformExtension?
const getBehaviorFullType = (extensionName: string, behaviorName: string) => {
const separator = gd.PlatformExtension.getNamespaceSeparator();
return extensionName + separator + behaviorName;
};
// An object representing InstructionMetadata or ExpressionMetadata.
// Allow to use most information without paying the cost to call the
// InstructionMetadata/ExpressionMetadata methods. In theory,
@@ -37,27 +45,31 @@ export type EnumeratedInstructionOrExpressionMetadata =
| EnumeratedInstructionMetadata
| EnumeratedExpressionMetadata;
/**
* Given a list of expression or instructions that were previously enumerated,
* filter the ones that are not usable from the current "scope".
*/
export const filterEnumeratedInstructionOrExpressionMetadataByScope = <
+T: EnumeratedInstructionOrExpressionMetadata
>(
list: Array<T>,
scope: EventsScope
): Array<T> => {
// This is copied from gd::WholeProjectRefactorer (see GetBehaviorFullType)
// Could be factored into a single C++ function in gd::PlatformExtension?
const separator = gd.PlatformExtension.getNamespaceSeparator();
const getBehaviorFullType = (extensionName: string, behaviorName: string) => {
return extensionName + separator + behaviorName;
};
return list.filter(enumeratedInstructionOrExpressionMetadata => {
if (!enumeratedInstructionOrExpressionMetadata.isPrivate) return true;
// The instruction or expression is marked as "private":
// we now compare its scope (where it was declared) and the current scope
// (where we are) to see if we should filter it or not.
const {
behaviorMetadata,
} = enumeratedInstructionOrExpressionMetadata.scope;
type,
scope: { behaviorMetadata, extension },
} = enumeratedInstructionOrExpressionMetadata;
const { eventsBasedBehavior, eventsFunctionsExtension } = scope;
// Show private behavior functions when editing the behavior
if (
behaviorMetadata &&
eventsBasedBehavior &&
@@ -66,9 +78,16 @@ export const filterEnumeratedInstructionOrExpressionMetadataByScope = <
eventsFunctionsExtension.getName(),
eventsBasedBehavior.getName()
) === behaviorMetadata.getName()
) {
)
return true;
// Show private non-behavior functions when editing the extension
if (
!behaviorMetadata &&
eventsFunctionsExtension &&
eventsFunctionsExtension.getName() === extension.getName()
)
return true;
}
return false;
});

View File

@@ -3,8 +3,21 @@ import { type ExpressionAutocompletion } from '../ExpressionAutocompletion';
import { type EnumeratedExpressionMetadata } from '../InstructionOrExpression/EnumeratedInstructionOrExpressionMetadata.js';
import { mapVector } from '../Utils/MapFor';
const makeNewFakeExtension = (gd: libGDevelop) => {
const extension = new gd.PlatformExtension();
extension.setExtensionInformation(
'FakeExtensionForAutocompletionTests',
'FakeExtensionForAutocompletionTests',
'FakeExtensionForAutocompletionTests',
'The extension author',
'MIT'
);
return extension;
};
const makeFakeEnumeratedExpressionMetadata = (
name: string,
extension: gdPlatformExtension,
expressionMetadata: gdExpressionMetadata
): EnumeratedExpressionMetadata => ({
type: name,
@@ -17,7 +30,7 @@ const makeFakeEnumeratedExpressionMetadata = (
expressionMetadata.getParameters(),
parameterMetadata => parameterMetadata
),
scope: {},
scope: { extension },
isPrivate: false,
});
@@ -36,6 +49,8 @@ export const makeFakeExactExpressionAutocompletion = () => {
expressionMetadata.addParameter('expression', 'Some number', '', false);
expressionMetadata.addParameter('string', 'Some string', '', false);
const extension = makeNewFakeExtension(gd);
return [
{
kind: 'Expression',
@@ -44,6 +59,7 @@ export const makeFakeExactExpressionAutocompletion = () => {
isExact: true,
enumeratedExpressionMetadata: makeFakeEnumeratedExpressionMetadata(
'MyFunction',
extension,
expressionMetadata
),
},
@@ -75,6 +91,8 @@ export const makeFakeExpressionAutocompletions = (): Array<ExpressionAutocomplet
'res/actions/replaceScene.png'
);
const extension = makeNewFakeExtension(gd);
return [
{
kind: 'Object',
@@ -113,6 +131,7 @@ export const makeFakeExpressionAutocompletions = (): Array<ExpressionAutocomplet
isExact: false,
enumeratedExpressionMetadata: makeFakeEnumeratedExpressionMetadata(
'MyFunctionWithoutParams',
extension,
expressionWithoutParamsMetadata
),
},
@@ -123,6 +142,7 @@ export const makeFakeExpressionAutocompletions = (): Array<ExpressionAutocomplet
isExact: false,
enumeratedExpressionMetadata: makeFakeEnumeratedExpressionMetadata(
'MyFunction',
extension,
expressionMetadata
),
},

View File

@@ -3036,7 +3036,7 @@ storiesOf('InstructionSelector', module)
storiesOf('InstructionOrObjectSelector', module)
.addDecorator(paperDecorator)
.addDecorator(muiDecorator)
.add('"KeyPressed" condition chosen, ', () => (
.add('"KeyPressed" condition chosen, scope: layout', () => (
<ValueStateHolder
initialValue={'free-instructions'}
render={(value, onChange) => (
@@ -3044,6 +3044,7 @@ storiesOf('InstructionOrObjectSelector', module)
<InstructionOrObjectSelector
style={{ flex: 1, display: 'flex', flexDirection: 'column' }} // TODO
project={testProject.project}
scope={{ layout: testProject.testLayout }}
currentTab={value}
onChangeTab={onChange}
globalObjectsContainer={testProject.project}
@@ -3059,7 +3060,7 @@ storiesOf('InstructionOrObjectSelector', module)
)}
/>
))
.add('"MySpriteObject" object chosen, ', () => (
.add('"MySpriteObject" object chosen, scope: layout', () => (
<ValueStateHolder
initialValue={'objects'}
render={(value, onChange) => (
@@ -3067,6 +3068,7 @@ storiesOf('InstructionOrObjectSelector', module)
<InstructionOrObjectSelector
style={{ flex: 1, display: 'flex', flexDirection: 'column' }} // TODO
project={testProject.project}
scope={{ layout: testProject.testLayout }}
currentTab={value}
onChangeTab={onChange}
globalObjectsContainer={testProject.project}