Add tutorials in app (#3870)

- Added dismissable tutorials in events sheet, variable editors and object variable tab, add object dialog, sprite animations tab, export game for web tab, add new extension dialog.

- Added a tutorial button.
This commit is contained in:
Fannie Yan
2022-05-10 17:42:45 +02:00
committed by GitHub
parent d873d9747a
commit 59bc76e144
23 changed files with 415 additions and 105 deletions

View File

@@ -1,7 +1,7 @@
// @flow
import * as React from 'react';
import SearchBar from '../../UI/SearchBar';
import { Column } from '../../UI/Grid';
import { Column, Line } from '../../UI/Grid';
import { type ExtensionShortHeader } from '../../Utils/GDevelopServices/Extension';
import { ExtensionStoreContext } from './ExtensionStoreContext';
import { ListSearchResults } from '../../UI/Search/ListSearchResults';
@@ -13,6 +13,7 @@ import {
sendExtensionDetailsOpened,
sendExtensionAddedToProject,
} from '../../Utils/Analytics/EventSender';
import useDismissableTutorialMessage from '../../Hints/useDismissableTutorialMessage';
const styles = {
searchBar: {
@@ -85,6 +86,10 @@ export const ExtensionStore = ({
return extensionMatches ? extensionMatches.matches : [];
};
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
'intro-behaviors-and-functions'
);
return (
<React.Fragment>
<ResponsiveWindowMeasurer>
@@ -98,6 +103,11 @@ export const ExtensionStore = ({
tagsHandler={tagsHandler}
tags={filters && filters.allTags}
/>
{DismissableTutorialMessage && (
<Line>
<Column expand>{DismissableTutorialMessage}</Column>
</Line>
)}
<ListSearchResults
disableAutoTranslate // Search results text highlighting conflicts with dom handling by browser auto-translations features. Disables auto translation to prevent crashes.
onRetry={fetchExtensionsAndFilters}

View File

@@ -11,7 +11,7 @@ import {
type EnumeratedObjectMetadata,
} from '../ObjectsList/EnumerateObjects';
import HelpButton from '../UI/HelpButton';
import { Column } from '../UI/Grid';
import { Column, Line } from '../UI/Grid';
import DismissableInfoBar from '../UI/Messages/DismissableInfoBar';
import { Tabs, Tab } from '../UI/Tabs';
import { AssetStore } from '.';
@@ -33,6 +33,7 @@ import { showErrorBox } from '../UI/Messages/MessageBox';
import { useResourceFetcher } from '../ProjectsStorage/ResourceFetcher';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import ScrollView from '../UI/ScrollView';
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
const ObjectListItem = ({
objectMetadata,
@@ -184,6 +185,10 @@ export default function NewObjectDialog({
]
);
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
'intro-object-types'
);
return (
<Dialog
title={<Trans>Add a new object</Trans>}
@@ -230,6 +235,11 @@ export default function NewObjectDialog({
)}
{currentTab === 'new-object' && (
<ScrollView>
{DismissableTutorialMessage && (
<Line>
<Column expand>{DismissableTutorialMessage}</Column>
</Line>
)}
<List>
{Object.keys(objectsByCategory).map(category => {
const categoryObjectMetadata = objectsByCategory[category];

View File

@@ -128,6 +128,7 @@ const BehaviorsEditor = (props: Props) => {
}
actionLabel={<Trans>Add a behavior</Trans>}
helpPagePath="/behaviors"
tutorialId="intro-behaviors-and-functions"
actionButtonId="add-behavior-button"
onAdd={() => setNewBehaviorDialogOpen(true)}
/>

View File

@@ -32,6 +32,11 @@ import ThemeConsumer from '../../UI/Theme/ThemeConsumer';
import BottomButtons from './BottomButtons';
import { EmptyPlaceholder } from '../../UI/EmptyPlaceholder';
import { CorsAwareImage } from '../../UI/CorsAwareImage';
import { Line } from '../../UI/Grid';
import { type Preferences } from '../../MainFrame/Preferences/PreferencesContext';
import { type Tutorial } from '../../Utils/GDevelopServices/Tutorial';
import TutorialMessage from '../../Hints/TutorialMessage';
import getTutorial from '../../Hints/getTutorial';
const gd: libGDevelop = global.gd;
const getThumbnail = ObjectsRenderingService.getThumbnail.bind(
@@ -227,6 +232,9 @@ type EventsTreeProps = {|
windowWidth: WidthType,
eventsSheetHeight: number,
fontSize?: number,
preferences: Preferences,
tutorials: ?Array<Tutorial>,
|};
// A node displayed by the SortableTree. Almost always represents an
@@ -267,6 +275,26 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
this.onHeightsChanged();
}
componentDidUpdate(prevProps: EventsTreeProps) {
const {
values: { hiddenTutorialHints },
} = this.props.preferences;
const {
values: { hiddenTutorialHints: previousHiddenTutorialHints },
} = prevProps.preferences;
if (
hiddenTutorialHints['intro-event-system'] !==
previousHiddenTutorialHints['intro-event-system']
) {
this.setState({
...this.state,
treeData: this.state.treeData.filter(
data => data.key !== 'eventstree-tutorial-node'
),
});
}
}
/**
* Should be called whenever an event height has changed
*/
@@ -362,6 +390,11 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
};
}
);
const tutorial = getTutorial(
this.props.preferences,
this.props.tutorials,
'intro-event-system'
);
// Add the bottom buttons if we're at the root
const extraNodes = [
@@ -382,6 +415,22 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
children: [],
}
: null,
depth === 0 && eventsList.getEventsCount() !== 0 && tutorial
? {
title: () => (
<Line justifyContent="center">
<TutorialMessage tutorial={tutorial} />
</Line>
),
event: null,
indexInList: eventsList.getEventsCount() + 1,
disabled: false,
depth: 0,
fixedHeight: 150,
children: [],
key: 'eventstree-tutorial-node',
}
: null,
depth === 0 && eventsList.getEventsCount() === 0
? {
title: () => (
@@ -390,6 +439,7 @@ export default class ThemableEventsTree extends Component<EventsTreeProps, *> {
description={<Trans>Events define the rules of a game.</Trans>}
actionLabel={<Trans>Add an event</Trans>}
helpPagePath="/events"
tutorialId="intro-event-system"
onAdd={() =>
this.props.onAddNewEvent(
'BuiltinCommonInstructions::Standard',

View File

@@ -95,6 +95,8 @@ import {
import LeaderboardContext, {
type LeaderboardState,
} from '../Leaderboard/LeaderboardContext';
import { TutorialContext } from '../Tutorial/TutorialContext';
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
const gd: libGDevelop = global.gd;
const zoomLevel = { min: 1, max: 50 };
@@ -129,6 +131,7 @@ type ComponentProps = {|
...Props,
authenticatedUser: AuthenticatedUser,
preferences: Preferences,
tutorials: ?Array<Tutorial>,
leaderboardsManager: ?LeaderboardState,
|};
@@ -1330,6 +1333,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
onChooseResource,
resourceExternalEditors,
onCreateEventsFunction,
tutorials,
} = this.props;
if (!project) return null;
@@ -1414,6 +1418,8 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
: 0
}
fontSize={preferences.values.eventsSheetZoomLevel}
preferences={preferences}
tutorials={tutorials}
/>
{this.state.showSearchPanel && (
<SearchPanel
@@ -1582,12 +1588,14 @@ const EventsSheet = (props, ref) => {
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const preferences = React.useContext(PreferencesContext);
const { tutorials } = React.useContext(TutorialContext);
const leaderboardsManager = React.useContext(LeaderboardContext);
return (
<EventsSheetComponentWithoutHandle
ref={component}
authenticatedUser={authenticatedUser}
preferences={preferences}
tutorials={tutorials}
leaderboardsManager={leaderboardsManager}
{...props}
/>

View File

@@ -14,6 +14,7 @@ import { Tab, Tabs } from '../../UI/Tabs';
import ExportHome from './ExportHome';
import { getGame, type Game } from '../../Utils/GDevelopServices/Game';
import { showWarningBox } from '../../UI/Messages/MessageBox';
import TutorialButton from '../../UI/TutorialButton';
const styles = {
icon: { width: 40, height: 40 },
@@ -174,6 +175,14 @@ const ExportDialog = ({
]}
secondaryActions={[
<HelpButton key="help" helpPagePath={exporter.helpPage} />,
exporter.exportPipeline.name === 'local-html5' ||
exporter.exportPipeline.name === 'browser-html5' ? (
<TutorialButton
key="tutorial"
tutorialId="export-to-itch"
label="How to export to Itch.io"
/>
) : null,
<FlatButton
key="builds"
label={<Trans>See this game builds</Trans>}

View File

@@ -13,6 +13,7 @@ import Poki from '../../UI/CustomSvgIcons/Poki';
import CrazyGames from '../../UI/CustomSvgIcons/CrazyGames';
import NewsGround from '../../UI/CustomSvgIcons/NewsGround';
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage';
const getIconStyle = windowWidth => ({
height: windowWidth === 'small' ? 30 : 48,
@@ -25,6 +26,7 @@ export const ExplanationHeader = () => {
const iconStyle = getIconStyle(windowWidth);
return (
<Column noMargin>
<DismissableTutorialMessage tutorialId="export-to-itch" />
<Line>
<Text>
<Trans>

View File

@@ -1,15 +1,5 @@
// @flow
import { I18n } from '@lingui/react';
import { Trans } from '@lingui/macro';
import * as React from 'react';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import AlertMessage from '../UI/AlertMessage';
import Window from '../Utils/Window';
import RaisedButton from '../UI/RaisedButton';
import YouTubeIcon from '@material-ui/icons/YouTube';
import MenuBookIcon from '@material-ui/icons/MenuBook';
import { TutorialContext } from '../Tutorial/TutorialContext';
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
import useDismissableTutorialMessage from './useDismissableTutorialMessage';
type Props = {|
tutorialId: string,
@@ -18,63 +8,14 @@ type Props = {|
/**
* Show a link to a tutorial that can be permanently hidden. Hidden tutorials
* will be stored in preferences.
* Use useDismissableTutorialMessage if you need to know if the tutorial can't be found
* or was previously hidden before rendering.
*/
const DismissableTutorialMessage = ({ tutorialId }: Props) => {
const preferences = React.useContext(PreferencesContext);
const { values, showTutorialHint } = preferences;
const { tutorials } = React.useContext(TutorialContext);
if (values.hiddenTutorialHints[tutorialId]) return null;
if (!tutorials) return null; // Loading or errored, do not display the tutorial.
const tutorial: ?Tutorial = tutorials.find(
tutorial => tutorial.id === tutorialId
);
if (!tutorial) {
console.warn(`Tutorial ${tutorialId} not found`);
return null;
}
return (
<I18n>
{({ i18n }) => (
<AlertMessage
kind={'info'}
children={tutorial.title}
renderLeftIcon={() => (
<img
alt=""
style={{
maxWidth: 128,
maxHeight: 128,
}}
src={tutorial.thumbnailUrl}
/>
)}
renderRightButton={() => (
<RaisedButton
icon={
tutorial.type === 'video' ? <YouTubeIcon /> : <MenuBookIcon />
}
label={
tutorial.type === 'video' ? (
<Trans>Watch the tutorial</Trans>
) : (
<Trans>Read the tutorial</Trans>
)
}
onClick={() => {
Window.openExternalURL(tutorial.link);
}}
/>
)}
onHide={() => {
showTutorialHint(tutorialId, false);
}}
/>
)}
</I18n>
);
const {
DismissableTutorialMessage: ReturnedDismissableTutorialMessage,
} = useDismissableTutorialMessage(tutorialId);
return ReturnedDismissableTutorialMessage;
};
export default DismissableTutorialMessage;

View File

@@ -0,0 +1,65 @@
// @flow
import { I18n } from '@lingui/react';
import { Trans } from '@lingui/macro';
import * as React from 'react';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import AlertMessage from '../UI/AlertMessage';
import Window from '../Utils/Window';
import RaisedButton from '../UI/RaisedButton';
import YouTubeIcon from '@material-ui/icons/YouTube';
import MenuBookIcon from '@material-ui/icons/MenuBook';
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
type Props = {|
tutorial: Tutorial,
|};
/**
* Show a link to a tutorial that can be permanently hidden. Hidden tutorials
* will be stored in preferences.
*/
const TutorialMessage = ({ tutorial }: Props) => {
const { showTutorialHint } = React.useContext(PreferencesContext);
return (
<I18n>
{({ i18n }) => (
<AlertMessage
kind={'info'}
children={tutorial.title}
renderLeftIcon={() => (
<img
alt=""
style={{
maxWidth: 128,
maxHeight: 128,
}}
src={tutorial.thumbnailUrl}
/>
)}
renderRightButton={() => (
<RaisedButton
icon={
tutorial.type === 'video' ? <YouTubeIcon /> : <MenuBookIcon />
}
label={
tutorial.type === 'video' ? (
<Trans>Watch the tutorial</Trans>
) : (
<Trans>Read the tutorial</Trans>
)
}
onClick={() => {
Window.openExternalURL(tutorial.link);
}}
/>
)}
onHide={() => {
showTutorialHint(tutorial.id, false);
}}
/>
)}
</I18n>
);
};
export default TutorialMessage;

View File

@@ -0,0 +1,28 @@
// @flow
import { type Tutorial } from '../Utils/GDevelopServices/Tutorial';
import { type Preferences } from '../MainFrame/Preferences/PreferencesContext';
/**
* Returns a tutorial if it can be found, otherwise returns null.
*/
const getTutorial = (
preferences: Preferences,
tutorials: ?Array<Tutorial>,
tutorialId: string
) => {
if (!tutorials) return null; // Loading or errored, do not display the tutorial.
const { values } = preferences;
if (values.hiddenTutorialHints[tutorialId]) return null;
const tutorial: ?Tutorial = tutorials.find(
tutorial => tutorial.id === tutorialId
);
if (!tutorial) {
console.warn(`Tutorial with id ${tutorialId} not found`);
return null;
}
return tutorial;
};
export default getTutorial;

View File

@@ -0,0 +1,32 @@
// @flow
import * as React from 'react';
import PreferencesContext from '../MainFrame/Preferences/PreferencesContext';
import { TutorialContext } from '../Tutorial/TutorialContext';
import getTutorial from './getTutorial';
import TutorialMessage from './TutorialMessage';
/**
* Returns the DismissableTutorialMessage component if the tutorial can be found,
* otherwise returns null.
* Useful to use when you need to know if the component is null before rendering,
* to avoid spacing issues when the component is hidden.
*/
const useDismissableTutorialMessage = (tutorialId: string) => {
const preferences = React.useContext(PreferencesContext);
const { tutorials } = React.useContext(TutorialContext);
const tutorial = getTutorial(preferences, tutorials, tutorialId);
const DismissableTutorialMessage = React.useMemo(
() => {
if (!tutorial) return null;
return <TutorialMessage tutorial={tutorial} />;
},
[tutorial]
);
return {
DismissableTutorialMessage,
};
};
export default useDismissableTutorialMessage;

View File

@@ -14,6 +14,8 @@ import ExternalPropertiesDialog, {
import Text from '../../UI/Text';
import { Line } from '../../UI/Grid';
import { sendEventsExtractedAsFunction } from '../../Utils/Analytics/EventSender';
import HelpButton from '../../UI/HelpButton';
import TutorialButton from '../../UI/TutorialButton';
const styles = {
container: {
@@ -176,6 +178,15 @@ export class ExternalEventsEditorContainer extends React.Component<
onClick={this.openExternalPropertiesDialog}
/>
</Line>
<Line justifyContent="flex-start" noMargin>
<TutorialButton
tutorialId="Intermediate-externals"
label="Watch the tutorial"
renderIfNotFound={
<HelpButton helpPagePath="/interface/events-editor/external-events" />
}
/>
</Line>
</PlaceholderMessage>
)}
<ExternalPropertiesDialog

View File

@@ -19,6 +19,8 @@ import ExternalPropertiesDialog, {
import { Line } from '../../UI/Grid';
import Text from '../../UI/Text';
import { prepareInstancesEditorSettings } from '../../InstancesEditor/InstancesEditorSettings';
import TutorialButton from '../../UI/TutorialButton';
import HelpButton from '../../UI/HelpButton';
const styles = {
container: {
@@ -199,6 +201,15 @@ export class ExternalLayoutEditorContainer extends React.Component<
onClick={this.openExternalPropertiesDialog}
/>
</Line>
<Line justifyContent="flex-start" noMargin>
<TutorialButton
tutorialId="Intermediate-externals"
label="Watch the tutorial"
renderIfNotFound={
<HelpButton helpPagePath="/interface/events-editor/external-events" />
}
/>
</Line>
</PlaceholderMessage>
)}
<ExternalPropertiesDialog

View File

@@ -83,8 +83,8 @@ export default function ExternalPropertiesDialog({
>
<Column>
{helpTexts &&
helpTexts.map(helpText => (
<Line>
helpTexts.map((helpText, index) => (
<Line key={index}>
<BackgroundText>{helpText}</BackgroundText>
</Line>
))}

View File

@@ -0,0 +1,22 @@
// @flow
import * as React from 'react';
import { Column, Spacer } from '../../../UI/Grid';
import useDismissableTutorialMessage from '../../../Hints/useDismissableTutorialMessage';
/**
* TODO: Use context directly in SpriteEditor
* when switching SpriteEditor class component to functional component.
*/
const SpacedDismissableTutorialMessage = () => {
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
'intermediate-changing-animations'
);
return DismissableTutorialMessage ? (
<Column>
{DismissableTutorialMessage}
<Spacer />
</Column>
) : null;
};
export default SpacedDismissableTutorialMessage;

View File

@@ -40,6 +40,7 @@ import ScrollView from '../../../UI/ScrollView';
import Checkbox from '../../../UI/Checkbox';
import useForceUpdate from '../../../Utils/UseForceUpdate';
import { EmptyPlaceholder } from '../../../UI/EmptyPlaceholder';
import SpacedDismissableTutorialMessage from './SpacedDismissableTutorialMessage';
const gd: libGDevelop = global.gd;
@@ -317,11 +318,13 @@ class AnimationsListContainer extends React.Component<
description={<Trans>Animations are a sequence of images.</Trans>}
actionLabel={<Trans>Add an animation</Trans>}
helpPagePath="/objects/sprite"
tutorialId="intermediate-changing-animations"
onAdd={this.addAnimation}
/>
</Column>
) : (
<React.Fragment>
<SpacedDismissableTutorialMessage />
<SortableAnimationsList
spriteObject={this.props.spriteObject}
objectName={this.props.objectName}

View File

@@ -25,6 +25,7 @@ import HotReloadPreviewButton, {
import EffectsList from '../EffectsList';
import VariablesList from '../VariablesList/index';
import { sendBehaviorsEditorShown } from '../Utils/Analytics/EventSender';
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
const gd: libGDevelop = global.gd;
export type ObjectEditorTab =
@@ -97,6 +98,10 @@ const InnerDialog = (props: InnerDialogProps) => {
props.onRename(newObjectName);
};
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
'intro-variables'
);
useEffect(
() => {
if (currentTab === 'behaviors') {
@@ -229,20 +234,30 @@ const InnerDialog = (props: InnerDialogProps) => {
/>
)}
{currentTab === 'variables' && (
<VariablesList
variablesContainer={props.object.getVariables()}
emptyPlaceholderTitle={<Trans>Add your first object variable</Trans>}
emptyPlaceholderDescription={
<Trans>
These variables hold additional information on an object.
</Trans>
}
helpPagePath={'/all-features/variables/object-variables'}
onSizeUpdated={
forceUpdate /*Force update to ensure dialog is properly positioned*/
}
onComputeAllVariableNames={props.onComputeAllVariableNames}
/>
<Column expand noMargin>
{props.object.getVariables().count() > 0 &&
DismissableTutorialMessage && (
<Line>
<Column expand>{DismissableTutorialMessage}</Column>
</Line>
)}
<VariablesList
variablesContainer={props.object.getVariables()}
emptyPlaceholderTitle={
<Trans>Add your first object variable</Trans>
}
emptyPlaceholderDescription={
<Trans>
These variables hold additional information on an object.
</Trans>
}
helpPagePath={'/all-features/variables/object-variables'}
onSizeUpdated={
forceUpdate /*Force update to ensure dialog is properly positioned*/
}
onComputeAllVariableNames={props.onComputeAllVariableNames}
/>
</Column>
)}
{currentTab === 'effects' && (
<EffectsList

View File

@@ -9,17 +9,23 @@ import RaisedButton from '../UI/RaisedButton';
import { Column, LargeSpacer } from './Grid';
import HelpButton from '../UI/HelpButton';
import Text from '../UI/Text';
import TutorialButton from './TutorialButton';
type Props = {|
title: React.Node,
description: React.Node,
actionLabel: React.Node,
helpPagePath?: string,
tutorialId?: string,
actionButtonId?: string,
onAdd: () => void,
isLoading?: boolean,
|};
const DefaultHelpButton = ({ helpPagePath }: { helpPagePath?: string }) => (
<HelpButton label={<Trans>Read the doc</Trans>} helpPagePath={helpPagePath} />
);
/**
* A placeholder for when there is no content to display.
* Also take a look at EmptyMessage for a less visible message.
@@ -49,10 +55,17 @@ export const EmptyPlaceholder = (props: Props) => (
icon={props.isLoading ? <CircularProgress size={24} /> : <Add />}
id={props.actionButtonId}
/>
<HelpButton
label={<Trans>Read the doc</Trans>}
helpPagePath={props.helpPagePath}
/>
{props.tutorialId ? (
<TutorialButton
tutorialId={props.tutorialId}
label="Watch tutorial"
renderIfNotFound={
<DefaultHelpButton helpPagePath={props.helpPagePath} />
}
/>
) : (
<DefaultHelpButton helpPagePath={props.helpPagePath} />
)}
</ColumnStackLayout>
</Column>
</Container>

View File

@@ -0,0 +1,43 @@
// @flow
import * as React from 'react';
import FlatButton from '../FlatButton';
import Window from '../../Utils/Window';
import { Trans } from '@lingui/macro';
import { TutorialContext } from '../../Tutorial/TutorialContext';
import { type Tutorial } from '../../Utils/GDevelopServices/Tutorial';
import YouTube from '@material-ui/icons/YouTube';
type PropsType = {|
tutorialId: ?string,
label?: React.Node,
renderIfNotFound?: React.Node,
|};
/**
* The button that can be used in any dialog to open a Youtube tutorial.
*/
const TutorialButton = (props: PropsType) => {
const { tutorials } = React.useContext(TutorialContext);
if (!tutorials || !props.tutorialId) return props.renderIfNotFound || null; // Loading or errored, do not display the tutorial.
const tutorial: ?Tutorial = tutorials.find(
tutorial => tutorial.id === props.tutorialId
);
if (!tutorial) {
console.warn(`Tutorial with id ${props.tutorialId || ''} not found`);
return props.renderIfNotFound || null;
}
return (
<FlatButton
onClick={() => {
if (tutorial.link) {
Window.openExternalURL(tutorial.link);
}
}}
target="_blank"
label={<Trans>{props.label || 'Tutorial'}</Trans>}
icon={<YouTube />}
/>
);
};
export default TutorialButton;

View File

@@ -84,6 +84,9 @@ export const getInstructionTutorialIds = (type: string): Array<string> => {
case 'ChangeAnimation':
case 'ChangeAnimationName':
return ['intermediate-changing-animations'];
case 'PopStartedTouch':
case 'MouseButtonPressed':
return ['intermediate-touchscreen-controls'];
default:
return [];
}

View File

@@ -9,6 +9,8 @@ import useForceUpdate from '../Utils/UseForceUpdate';
import HotReloadPreviewButton, {
type HotReloadPreviewButtonProps,
} from '../HotReload/HotReloadPreviewButton';
import { Column, Line } from '../UI/Grid';
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
type Props = {|
onCancel: () => void,
@@ -42,6 +44,9 @@ const VariablesEditorDialog = ({
serializableObject: variablesContainer,
onCancel,
});
const { DismissableTutorialMessage } = useDismissableTutorialMessage(
'intro-variables'
);
return (
<Dialog
@@ -84,23 +89,30 @@ const VariablesEditorDialog = ({
flexBody
fullHeight
>
<VariablesList
commitVariableValueOnBlur={
// Reduce the number of re-renders by saving the variable value only when the field is blurred.
// We don't do that by default because the VariablesList can be used in a component like
// InstancePropertiesEditor, that can be unmounted at any time, before the text fields get a
// chance to be blurred.
true
}
variablesContainer={variablesContainer}
emptyPlaceholderTitle={emptyPlaceholderTitle}
emptyPlaceholderDescription={emptyPlaceholderDescription}
onSizeUpdated={
forceUpdate /*Force update to ensure dialog is properly positioned*/
}
onComputeAllVariableNames={onComputeAllVariableNames}
helpPagePath={helpPagePath}
/>
<Column expand noMargin>
{variablesContainer.count() > 0 && DismissableTutorialMessage && (
<Line>
<Column expand>{DismissableTutorialMessage}</Column>
</Line>
)}
<VariablesList
commitVariableValueOnBlur={
// Reduce the number of re-renders by saving the variable value only when the field is blurred.
// We don't do that by default because the VariablesList can be used in a component like
// InstancePropertiesEditor, that can be unmounted at any time, before the text fields get a
// chance to be blurred.
true
}
variablesContainer={variablesContainer}
emptyPlaceholderTitle={emptyPlaceholderTitle}
emptyPlaceholderDescription={emptyPlaceholderDescription}
onSizeUpdated={
forceUpdate /*Force update to ensure dialog is properly positioned*/
}
onComputeAllVariableNames={onComputeAllVariableNames}
helpPagePath={helpPagePath}
/>
</Column>
</Dialog>
);
};

View File

@@ -453,6 +453,7 @@ export default class VariablesList extends React.Component<Props, State> {
description={this.props.emptyPlaceholderDescription}
actionLabel="Add a variable"
helpPagePath={this.props.helpPagePath}
tutorialId="intermediate-advanced-variables"
onAdd={this.addVariable}
/>
</Column>

View File

@@ -245,6 +245,7 @@ import {
ExamplesAccordion,
} from '../Profile/ContributionsDetails';
import ListIcon from '../UI/ListIcon';
import { initialPreferences } from '../MainFrame/Preferences/PreferencesContext';
configureActions({
depth: 2,
@@ -268,6 +269,17 @@ const buildFakeMenuTemplate = () => [
click: action('click option 2'),
},
];
const eventsTreeTutorials = [
{
id: 'intro-event-system',
title: 'Event system',
description: 'Description 1',
thumbnailUrl:
'https://raw.githubusercontent.com/4ian/GDevelop/master/Core/docs/images/gdlogo.png',
link: 'https://example.com/tutorial.html',
type: 'video',
},
];
const hotReloadPreviewButtonProps: HotReloadPreviewButtonProps = {
hasPreviewsRunning: false,
@@ -2593,6 +2605,8 @@ storiesOf('EventsTree', module)
screenType={'normal'}
windowWidth={'medium'}
eventsSheetHeight={500}
preferences={initialPreferences}
tutorials={eventsTreeTutorials}
/>
</FixedHeightFlexContainer>
</div>
@@ -2632,6 +2646,8 @@ storiesOf('EventsTree', module)
screenType={'normal'}
windowWidth={'small'}
eventsSheetHeight={500}
preferences={initialPreferences}
tutorials={eventsTreeTutorials}
/>
</FixedHeightFlexContainer>
</div>
@@ -2671,6 +2687,8 @@ storiesOf('EventsTree', module)
screenType={'normal'}
windowWidth={'medium'}
eventsSheetHeight={500}
preferences={initialPreferences}
tutorials={eventsTreeTutorials}
/>
</FixedHeightFlexContainer>
</div>
@@ -2710,6 +2728,8 @@ storiesOf('EventsTree', module)
screenType={'normal'}
windowWidth={'small'}
eventsSheetHeight={500}
preferences={initialPreferences}
tutorials={eventsTreeTutorials}
/>
</FixedHeightFlexContainer>
</div>