Remember collapsed state of object behaviors configuration panels (#5641)

This commit is contained in:
AlexandreS
2023-09-07 14:30:23 +02:00
committed by GitHub
parent 128657c876
commit 18211d197a
7 changed files with 250 additions and 175 deletions

View File

@@ -31,10 +31,10 @@ namespace gd {
*/
class GD_CORE_API BehaviorConfigurationContainer {
public:
BehaviorConfigurationContainer(){};
BehaviorConfigurationContainer(const gd::String& name_, const gd::String& type_)
: name(name_), type(type_){};
BehaviorConfigurationContainer() : folded(false){};
BehaviorConfigurationContainer(const gd::String& name_,
const gd::String& type_)
: name(name_), type(type_), folded(false){};
virtual ~BehaviorConfigurationContainer();
virtual BehaviorConfigurationContainer* Clone() const { return new BehaviorConfigurationContainer(*this); }
@@ -61,7 +61,7 @@ class GD_CORE_API BehaviorConfigurationContainer {
/**
* \brief Called when the IDE wants to know about the custom properties of the
* behavior.
*
*
* \return a std::map with properties names as key.
* \see gd::PropertyDescriptor
*/
@@ -104,6 +104,17 @@ class GD_CORE_API BehaviorConfigurationContainer {
const gd::SerializerElement& GetContent() const { return content; };
gd::SerializerElement& GetContent() { return content; };
/**
* \brief Set if the behavior configuration panel should be folded in the UI.
*/
void SetFolded(bool fold = true) { folded = fold; }
/**
* \brief True if the behavior configuration panel should be folded in the UI.
*/
bool IsFolded() const { return folded; }
protected:
/**
* \brief Called when the IDE wants to know about the custom properties of the
@@ -148,6 +159,7 @@ protected:
///< in the form "ExtensionName::BehaviorTypeName"
gd::SerializerElement content; // Storage for the behavior properties
bool folded;
};
} // namespace gd

View File

@@ -573,6 +573,9 @@ interface Behavior {
void SerializeTo([Ref] SerializerElement element);
void UnserializeFrom([Const, Ref] SerializerElement element);
boolean IsFolded();
void SetFolded(boolean folded);
boolean IsDefaultBehavior();
};

View File

@@ -10,6 +10,8 @@ declare class gdBehavior {
initializeContent(): void;
serializeTo(element: gdSerializerElement): void;
unserializeFrom(element: gdSerializerElement): void;
isFolded(): boolean;
setFolded(folded: boolean): void;
isDefaultBehavior(): boolean;
delete(): void;
ptr: number;

View File

@@ -62,6 +62,210 @@ export const useBehaviorOverridingAlertDialog = () => {
};
};
type BehaviorConfigurationEditorInterface = {||};
type BehaviorConfigurationEditorProps = {|
project: gdProject,
object: gdObject,
behavior: gdBehavior,
resourceManagementProps: ResourceManagementProps,
onBehaviorsUpdated: () => void,
onChangeBehaviorName: (behavior: gdBehavior, newName: string) => void,
onRemoveBehavior: (behaviorName: string) => void,
copyBehavior: (behaviorName: string) => void,
canPasteBehaviors: boolean,
pasteBehaviors: () => Promise<void>,
openExtension: (behaviorType: string) => void,
|};
const BehaviorConfigurationEditor = React.forwardRef<
BehaviorConfigurationEditorProps,
BehaviorConfigurationEditorInterface
>(
(
{
project,
object,
behavior,
resourceManagementProps,
onBehaviorsUpdated,
onChangeBehaviorName,
onRemoveBehavior,
copyBehavior,
canPasteBehaviors,
pasteBehaviors,
openExtension,
},
ref
) => {
const { values } = React.useContext(PreferencesContext);
const forceUpdate = useForceUpdate();
const behaviorName = behavior.getName();
const behaviorTypeName = behavior.getTypeName();
if (behavior.isDefaultBehavior()) {
return null;
}
const expanded = !behavior.isFolded();
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
gd.JsPlatform.get(),
behaviorTypeName
);
if (gd.MetadataProvider.isBadBehaviorMetadata(behaviorMetadata)) {
return (
<Accordion
defaultExpanded
id={`behavior-parameters-${behaviorName}`}
ref={ref}
>
<AccordionHeader
actions={[
<IconButton
key="delete"
onClick={ev => {
ev.stopPropagation();
onRemoveBehavior(behaviorName);
}}
>
<Trash />
</IconButton>,
]}
>
<MiniToolbarText firstChild>
<Trans>Unknown behavior</Trans>{' '}
</MiniToolbarText>
<Column noMargin expand>
<TextField margin="none" value={behaviorName} disabled />
</Column>
</AccordionHeader>
<AccordionBody>
<EmptyMessage>
<Trans>
This behavior is unknown. It might be a behavior that was
defined in an extension and that was later removed. You should
delete it.
</Trans>
</EmptyMessage>
</AccordionBody>
</Accordion>
);
}
const BehaviorComponent = BehaviorsEditorService.getEditor(
behaviorTypeName
);
const tutorialIds = getBehaviorTutorialIds(behaviorTypeName);
const enabledTutorialIds = tutorialIds.filter(
tutorialId => !values.hiddenTutorialHints[tutorialId]
);
const iconUrl = behaviorMetadata.getIconFilename();
return (
<Accordion
expanded={expanded}
onChange={(_, newExpanded) => {
behavior.setFolded(!newExpanded);
forceUpdate();
}}
id={`behavior-parameters-${behaviorName}`}
ref={ref}
>
<AccordionHeader
actions={[
<HelpIcon
key="help"
size="small"
helpPagePath={behaviorMetadata.getHelpPath()}
/>,
<ElementWithMenu
key="menu"
element={
<IconButton size="small">
<ThreeDotsMenu />
</IconButton>
}
buildMenuTemplate={(i18n: I18nType) => [
{
label: i18n._(t`Delete`),
click: () => onRemoveBehavior(behaviorName),
},
{
label: i18n._(t`Copy`),
click: () => copyBehavior(behaviorName),
},
{
label: i18n._(t`Paste`),
click: pasteBehaviors,
enabled: canPasteBehaviors,
},
...(project.hasEventsBasedBehavior(behaviorTypeName)
? [
{ type: 'separator' },
{
label: i18n._(t`Edit this behavior`),
click: () => openExtension(behaviorTypeName),
},
]
: []),
]}
/>,
]}
>
{iconUrl ? (
<IconContainer
src={iconUrl}
alt={behaviorMetadata.getFullName()}
size={20}
/>
) : null}
<Column expand>
<TextField
value={behaviorName}
translatableHintText={t`Behavior name`}
margin="none"
fullWidth
disabled
onChange={(e, text) => onChangeBehaviorName(behavior, text)}
id={`behavior-${behaviorName}-name-text-field`}
/>
</Column>
</AccordionHeader>
<AccordionBody>
<Column
expand
noMargin
// Avoid Physics2 behavior overflow on small screens
noOverflowParent
>
{enabledTutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>
</Line>
) : null}
<Line>
<BehaviorComponent
behavior={behavior}
project={project}
object={object}
resourceManagementProps={resourceManagementProps}
onBehaviorUpdated={onBehaviorsUpdated}
/>
</Line>
</Column>
</AccordionBody>
</Accordion>
);
}
);
type Props = {|
project: gdProject,
eventsFunctionsExtension?: gdEventsFunctionsExtension,
@@ -82,8 +286,8 @@ const BehaviorsEditor = (props: Props) => {
justAddedBehaviorName,
setJustAddedBehaviorName,
] = React.useState<?string>(null);
const justAddedBehaviorAccordionElement = React.useRef(
(null: ?React$Component<any, any>)
const justAddedBehaviorAccordionElement = React.useRef<?BehaviorConfigurationEditorInterface>(
null
);
React.useEffect(
@@ -125,8 +329,6 @@ const BehaviorsEditor = (props: Props) => {
.filter(behavior => !behavior.isDefaultBehavior());
const forceUpdate = useForceUpdate();
const { values } = React.useContext(PreferencesContext);
const addBehavior = React.useCallback(
(type: string, defaultName: string) => {
const wasBehaviorAdded = addBehaviorToObject(
@@ -412,68 +614,6 @@ const BehaviorsEditor = (props: Props) => {
<ScrollView ref={scrollView}>
{allVisibleBehaviors.map((behavior, index) => {
const behaviorName = behavior.getName();
const behaviorTypeName = behavior.getTypeName();
if (behavior.isDefaultBehavior()) {
return null;
}
const behaviorMetadata = gd.MetadataProvider.getBehaviorMetadata(
gd.JsPlatform.get(),
behaviorTypeName
);
if (gd.MetadataProvider.isBadBehaviorMetadata(behaviorMetadata)) {
return (
<Accordion
key={behaviorName}
defaultExpanded
id={`behavior-parameters-${behaviorName}`}
>
<AccordionHeader
actions={[
<IconButton
key="delete"
onClick={ev => {
ev.stopPropagation();
onRemoveBehavior(behaviorName);
}}
>
<Trash />
</IconButton>,
]}
>
<MiniToolbarText firstChild>
<Trans>Unknown behavior</Trans>{' '}
</MiniToolbarText>
<Column noMargin expand>
<TextField
margin="none"
value={behaviorName}
disabled
/>
</Column>
</AccordionHeader>
<AccordionBody>
<EmptyMessage>
<Trans>
This behavior is unknown. It might be a behavior that
was defined in an extension and that was later
removed. You should delete it.
</Trans>
</EmptyMessage>
</AccordionBody>
</Accordion>
);
}
const BehaviorComponent = BehaviorsEditorService.getEditor(
behaviorTypeName
);
const tutorialIds = getBehaviorTutorialIds(behaviorTypeName);
const enabledTutorialIds = tutorialIds.filter(
tutorialId => !values.hiddenTutorialHints[tutorialId]
);
const iconUrl = behaviorMetadata.getIconFilename();
const ref =
justAddedBehaviorName === behaviorName
@@ -481,107 +621,21 @@ const BehaviorsEditor = (props: Props) => {
: null;
return (
<Accordion
key={behaviorName}
defaultExpanded
id={`behavior-parameters-${behaviorName}`}
<BehaviorConfigurationEditor
ref={ref}
>
<AccordionHeader
actions={[
<HelpIcon
key="help"
size="small"
helpPagePath={behaviorMetadata.getHelpPath()}
/>,
<ElementWithMenu
key="menu"
element={
<IconButton size="small">
<ThreeDotsMenu />
</IconButton>
}
buildMenuTemplate={(i18n: I18nType) => [
{
label: i18n._(t`Delete`),
click: () => onRemoveBehavior(behaviorName),
},
{
label: i18n._(t`Copy`),
click: () => copyBehavior(behaviorName),
},
{
label: i18n._(t`Paste`),
click: pasteBehaviors,
enabled: isClipboardContainingBehaviors,
},
...(project.hasEventsBasedBehavior(behaviorTypeName)
? [
{ type: 'separator' },
{
label: i18n._(t`Edit this behavior`),
click: () => openExtension(behaviorTypeName),
},
]
: []),
]}
/>,
]}
>
{iconUrl ? (
<IconContainer
src={iconUrl}
alt={behaviorMetadata.getFullName()}
size={20}
/>
) : null}
<Column expand>
<TextField
value={behaviorName}
translatableHintText={t`Behavior name`}
margin="none"
fullWidth
disabled
onChange={(e, text) =>
onChangeBehaviorName(behavior, text)
}
id={`behavior-${behaviorName}-name-text-field`}
/>
</Column>
</AccordionHeader>
<AccordionBody>
<Column
expand
noMargin
// Avoid Physics2 behavior overflow on small screens
noOverflowParent
>
{enabledTutorialIds.length ? (
<Line>
<ColumnStackLayout expand>
{tutorialIds.map(tutorialId => (
<DismissableTutorialMessage
key={tutorialId}
tutorialId={tutorialId}
/>
))}
</ColumnStackLayout>
</Line>
) : null}
<Line>
<BehaviorComponent
behavior={behavior}
project={project}
object={object}
resourceManagementProps={
props.resourceManagementProps
}
onBehaviorUpdated={onBehaviorsUpdated}
/>
</Line>
</Column>
</AccordionBody>
</Accordion>
key={behaviorName}
project={project}
object={object}
behavior={behavior}
copyBehavior={copyBehavior}
onRemoveBehavior={onRemoveBehavior}
onBehaviorsUpdated={onBehaviorsUpdated}
onChangeBehaviorName={onChangeBehaviorName}
openExtension={openExtension}
canPasteBehaviors={isClipboardContainingBehaviors}
pasteBehaviors={pasteBehaviors}
resourceManagementProps={props.resourceManagementProps}
/>
);
})}
</ScrollView>

View File

@@ -42,6 +42,9 @@ const styles = {
},
};
// Those names are used internally by GDevelop.
const PROTECTED_PROPERTY_NAMES = ['name', 'type'];
const getValidatedPropertyName = (
i18n: I18nType,
properties: gdNamedPropertyDescriptorsList,
@@ -52,8 +55,7 @@ const getValidatedPropertyName = (
tentativeNewName => {
if (
properties.has(tentativeNewName) ||
// The name of a property cannot be "name" or "type", as they are used by GDevelop internally.
(tentativeNewName === 'name' || tentativeNewName === 'type')
PROTECTED_PROPERTY_NAMES.includes(tentativeNewName)
) {
return true;
}

View File

@@ -122,7 +122,7 @@ type AccordionProps = {|
// Use accordion in controlled mode
expanded?: boolean,
onChange?: (open: boolean) => void,
onChange?: (event: any, open: boolean) => void,
id?: string,
|};

View File

@@ -24,7 +24,9 @@ type Props = {|
export type ScrollViewInterface = {|
getScrollPosition: () => number,
scrollTo: (target: ?React$Component<any, any>) => void,
scrollTo: (
target: ?React$Component<any, any> | ?React.ElementRef<any>
) => void,
scrollToPosition: (number: number) => void,
scrollToBottom: () => void,
|};