Skip to content

Commit 455d77f

Browse files
authored
Add possibility to cancel edition with Piskel/Jfxr/Yarn from the editor (#6319)
1 parent 02093fe commit 455d77f

File tree

5 files changed

+139
-36
lines changed

5 files changed

+139
-36
lines changed

newIDE/app/src/ObjectEditor/Editors/SpriteEditor/index.js

+44-10
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,12 @@ export default function SpriteEditor({
131131
collisionMasksEditorOpen,
132132
setCollisionMasksEditorOpen,
133133
] = React.useState(false);
134+
const abortControllerRef = React.useRef<?AbortController>(null);
134135
const forceUpdate = useForceUpdate();
135136
const spriteConfiguration = gd.asSpriteConfiguration(objectConfiguration);
136137
const windowWidth = useResponsiveWindowWidth();
137138
const isMobileScreen = windowWidth === 'small';
139+
const { showConfirmation } = useAlertDialog();
138140
const hasNoSprites = () => {
139141
for (
140142
let animationIndex = 0;
@@ -546,6 +548,8 @@ export default function SpriteEditor({
546548
animationIndex: number,
547549
directionIndex: number
548550
) => {
551+
abortControllerRef.current = new AbortController();
552+
const { signal } = abortControllerRef.current;
549553
const resourceNames = mapFor(0, direction.getSpritesCount(), i => {
550554
return direction.getSprite(i).getImageName();
551555
});
@@ -575,6 +579,7 @@ export default function SpriteEditor({
575579
isLooping: direction.isLooping(),
576580
existingMetadata: direction.getMetadata(),
577581
},
582+
signal,
578583
}
579584
);
580585

@@ -632,16 +637,21 @@ export default function SpriteEditor({
632637
adaptCollisionMaskIfNeeded();
633638
}
634639
} catch (error) {
640+
if (error.name !== 'UserCancellationError') {
641+
console.error(
642+
'An exception was thrown when launching or reading resources from the external editor:',
643+
error
644+
);
645+
showErrorBox({
646+
message:
647+
'There was an error while using the external editor. Try with another resource and if this persists, please report this as a bug.',
648+
rawError: error,
649+
errorId: 'external-editor-error',
650+
});
651+
}
635652
setExternalEditorOpened(false);
636-
console.error(
637-
'An exception was thrown when launching or reading resources from the external editor:',
638-
error
639-
);
640-
showErrorBox({
641-
message: `There was an error while using the external editor. Try with another resource and if this persists, please report this as a bug.`,
642-
rawError: error,
643-
errorId: 'external-editor-error',
644-
});
653+
} finally {
654+
abortControllerRef.current = null;
645655
}
646656
},
647657
[
@@ -657,6 +667,26 @@ export default function SpriteEditor({
657667
]
658668
);
659669

670+
const cancelEditingWithExternalEditor = React.useCallback(
671+
async () => {
672+
const shouldContinue = await showConfirmation({
673+
title: t`Cancel editing`,
674+
message: t`You will lose any progress made with the external editor. Do you wish to cancel?`,
675+
confirmButtonLabel: t`Cancel edition`,
676+
dismissButtonLabel: t`Continue editing`,
677+
});
678+
if (!shouldContinue) return;
679+
if (abortControllerRef.current) {
680+
abortControllerRef.current.abort();
681+
} else {
682+
console.error(
683+
'Cannot cancel editing with external editor, abort controller is missing.'
684+
);
685+
}
686+
},
687+
[showConfirmation]
688+
);
689+
660690
const createAnimationWith = React.useCallback(
661691
async (i18n: I18nType, externalEditor: ResourceExternalEditor) => {
662692
addAnimation();
@@ -1047,7 +1077,11 @@ export default function SpriteEditor({
10471077
/>
10481078
</Dialog>
10491079
)}
1050-
{externalEditorOpened && <ExternalEditorOpenedDialog />}
1080+
{externalEditorOpened && (
1081+
<ExternalEditorOpenedDialog
1082+
onClose={cancelEditingWithExternalEditor}
1083+
/>
1084+
)}
10511085
</>
10521086
)}
10531087
</I18n>

newIDE/app/src/ResourcesList/BrowserResourceExternalEditors.js

+31-14
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '../Utils/BlobDownloader';
2222
import { showWarningBox } from '../UI/Messages/MessageBox';
2323
import { displayBlackLoadingScreen } from '../Utils/BrowserExternalWindowUtils';
24+
import { UserCancellationError } from '../LoginProvider/Utils';
2425
let nextExternalEditorWindowId = 0;
2526

2627
const externalEditorIndexHtml: { ['piskel' | 'yarn' | 'jfxr']: string } = {
@@ -29,11 +30,20 @@ const externalEditorIndexHtml: { ['piskel' | 'yarn' | 'jfxr']: string } = {
2930
jfxr: 'external/jfxr/jfxr-index.html',
3031
};
3132

32-
const openAndWaitForExternalEditorWindow = async (
33+
const openAndWaitForExternalEditorWindow = async ({
34+
externalEditorWindow,
35+
externalEditorName,
36+
externalEditorInput,
37+
signal,
38+
}: {|
3339
externalEditorWindow: any,
34-
editorName: 'piskel' | 'yarn' | 'jfxr',
35-
externalEditorInput: ExternalEditorInput
36-
): Promise<?ExternalEditorOutput> => {
40+
externalEditorName: 'piskel' | 'yarn' | 'jfxr',
41+
externalEditorInput: ExternalEditorInput,
42+
signal: AbortSignal,
43+
|}): Promise<?ExternalEditorOutput> => {
44+
if (signal.aborted) {
45+
return Promise.reject(new UserCancellationError(''));
46+
}
3747
return new Promise((resolve, reject) => {
3848
let externalEditorLoaded = false;
3949
let externalEditorClosed = false;
@@ -53,7 +63,7 @@ const openAndWaitForExternalEditorWindow = async (
5363

5464
const { id, payload } = event.data;
5565
if (id === `external-editor-ready`) {
56-
console.info(`External editor "${editorName}" ready.`);
66+
console.info(`External editor "${externalEditorName}" ready.`);
5767

5868
// Some browsers like Safari might not trigger the "load" event, but now we can
5969
// be sure the editor is loaded: the proof being that we received this message.
@@ -74,7 +84,9 @@ const openAndWaitForExternalEditorWindow = async (
7484
window.location.origin
7585
);
7686
} else if (id === `save-external-editor-output`) {
77-
console.info(`Received data from external editor "${editorName}."`);
87+
console.info(
88+
`Received data from external editor "${externalEditorName}."`
89+
);
7890
// $FlowFixMe - assuming the typing is good.
7991
externalEditorOutput = payload;
8092
} else if (event.data.id === 'close') {
@@ -90,13 +102,20 @@ const openAndWaitForExternalEditorWindow = async (
90102
return;
91103
}
92104
externalEditorClosed = true;
93-
console.info(`External editor "${editorName}" closed.`);
105+
console.info(`External editor "${externalEditorName}" closed.`);
94106
window.removeEventListener('message', onMessageEvent);
95107
resolve(externalEditorOutput);
96108
};
97109

110+
signal.addEventListener('abort', () => {
111+
reject(new UserCancellationError(''));
112+
if (externalEditorClosed) return;
113+
externalEditorWindow.close();
114+
onExternalEditorWindowClosed();
115+
});
116+
98117
externalEditorWindow.addEventListener('load', () => {
99-
console.info(`External editor "${editorName}" loaded.`);
118+
console.info(`External editor "${externalEditorName}" loaded.`);
100119
externalEditorLoaded = true;
101120

102121
externalEditorWindow.addEventListener('unload', () => {
@@ -110,14 +129,14 @@ const openAndWaitForExternalEditorWindow = async (
110129
// (though not on Safari), then the editor will send a `external-editor-ready` event
111130
// (on all browsers), after which we will then be ready to have it open the resources.
112131
// (see `open-external-editor-input`).
113-
externalEditorWindow.location = externalEditorIndexHtml[editorName];
132+
externalEditorWindow.location = externalEditorIndexHtml[externalEditorName];
114133

115134
// If the editor is not ready after 10 seconds and not closed, force it to be closed.
116135
// Something wrong is going on and we don't want to block the user.
117136
setTimeout(() => {
118137
if (externalEditorLoaded || externalEditorClosed) return;
119138
console.info(
120-
`External editor "${editorName} not loaded after 10 seconds - closing its window."`
139+
`External editor "${externalEditorName} not loaded after 10 seconds - closing its window."`
121140
);
122141

123142
// The external editor is not loaded after 10 seconds, abort.
@@ -232,7 +251,7 @@ const editWithBrowserExternalEditor = async ({
232251
resourceKind: ResourceKind,
233252
options: EditWithExternalEditorOptions,
234253
|}) => {
235-
const { project, resourceNames, resourceManagementProps } = options;
254+
const { project, resourceNames, resourceManagementProps, signal } = options;
236255

237256
// Fetch all edited resources as base64 encoded "data urls" (`data:...`).
238257
const resources = await downloadAndPrepareExternalEditorBase64Resources({
@@ -254,9 +273,7 @@ const editWithBrowserExternalEditor = async ({
254273

255274
sendExternalEditorOpened(externalEditorName);
256275
const externalEditorOutput: ?ExternalEditorOutput = await openAndWaitForExternalEditorWindow(
257-
externalEditorWindow,
258-
externalEditorName,
259-
externalEditorInput
276+
{ externalEditorWindow, externalEditorName, externalEditorInput, signal }
260277
);
261278
if (!externalEditorOutput) return null; // Changes cancelled.
262279

newIDE/app/src/ResourcesList/ResourceExternalEditor.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export type EditWithExternalEditorOptions = {|
8888
i18n: I18nType,
8989
getStorageProvider: () => StorageProvider,
9090
resourceManagementProps: ResourceManagementProps,
91+
signal: AbortSignal,
9192

9293
resourceNames: Array<string>,
9394
extraOptions: {

newIDE/app/src/ResourcesList/ResourceSelector.js

+46-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as React from 'react';
33
import { type I18n as I18nType } from '@lingui/core';
44
import { I18n } from '@lingui/react';
5-
import { Trans } from '@lingui/macro';
5+
import { Trans, t } from '@lingui/macro';
66

77
import SemiControlledAutoComplete, {
88
type DataSource,
@@ -31,6 +31,7 @@ import Edit from '../UI/CustomSvgIcons/Edit';
3131
import Cross from '../UI/CustomSvgIcons/Cross';
3232
import useResourcesChangedWatcher from './UseResourcesChangedWatcher';
3333
import useForceUpdate from '../Utils/UseForceUpdate';
34+
import useAlertDialog from '../UI/Alert/useAlertDialog';
3435

3536
const styles = {
3637
textFieldStyle: { display: 'flex', flex: 1 },
@@ -75,6 +76,8 @@ const ResourceSelector = React.forwardRef<Props, ResourceSelectorInterface>(
7576
const autoCompleteRef = React.useRef<?SemiControlledAutoCompleteInterface>(
7677
null
7778
);
79+
const { showConfirmation } = useAlertDialog();
80+
const abortControllerRef = React.useRef<?AbortController>(null);
7881
const allResourcesNamesRef = React.useRef<Array<string>>([]);
7982
const [notFoundError, setNotFoundError] = React.useState<boolean>(false);
8083
const [resourceName, setResourceName] = React.useState<string>(
@@ -262,6 +265,8 @@ const ResourceSelector = React.forwardRef<Props, ResourceSelectorInterface>(
262265
i18n: I18nType,
263266
resourceExternalEditor: ResourceExternalEditor
264267
) => {
268+
abortControllerRef.current = new AbortController();
269+
const { signal } = abortControllerRef.current;
265270
const resourcesManager = project.getResourcesManager();
266271
const initialResource = resourcesManager.getResource(resourceName);
267272

@@ -282,6 +287,7 @@ const ResourceSelector = React.forwardRef<Props, ResourceSelectorInterface>(
282287
name: resourceName || defaultNewResourceName,
283288
isLooping: false,
284289
},
290+
signal,
285291
});
286292

287293
setExternalEditorOpened(false);
@@ -299,16 +305,21 @@ const ResourceSelector = React.forwardRef<Props, ResourceSelectorInterface>(
299305
triggerResourcesHaveChanged();
300306
forceUpdate();
301307
} catch (error) {
308+
if (error.name !== 'UserCancellationError') {
309+
console.error(
310+
'An exception was thrown when launching or reading resources from the external editor:',
311+
error
312+
);
313+
showErrorBox({
314+
message:
315+
'There was an error while using the external editor. Try with another resource and if this persists, please report this as a bug.',
316+
rawError: error,
317+
errorId: 'external-editor-error',
318+
});
319+
}
302320
setExternalEditorOpened(false);
303-
console.error(
304-
'An exception was thrown when launching or reading resources from the external editor:',
305-
error
306-
);
307-
showErrorBox({
308-
message: `There was an error while using the external editor. Try with another resource and if this persists, please report this as a bug.`,
309-
rawError: error,
310-
errorId: 'external-editor-error',
311-
});
321+
} finally {
322+
abortControllerRef.current = null;
312323
}
313324
},
314325
[
@@ -323,6 +334,26 @@ const ResourceSelector = React.forwardRef<Props, ResourceSelectorInterface>(
323334
]
324335
);
325336

337+
const cancelEditingWithExternalEditor = React.useCallback(
338+
async () => {
339+
const shouldContinue = await showConfirmation({
340+
title: t`Cancel editing`,
341+
message: t`You will lose any progress made with the external editor. Do you wish to cancel?`,
342+
confirmButtonLabel: t`Cancel edition`,
343+
dismissButtonLabel: t`Continue editing`,
344+
});
345+
if (!shouldContinue) return;
346+
if (abortControllerRef.current) {
347+
abortControllerRef.current.abort();
348+
} else {
349+
console.error(
350+
'Cannot cancel editing with external editor, abort controller is missing.'
351+
);
352+
}
353+
},
354+
[showConfirmation]
355+
);
356+
326357
const errorText = notFoundError ? (
327358
<Trans>This resource does not exist in the game</Trans>
328359
) : null;
@@ -414,7 +445,11 @@ const ResourceSelector = React.forwardRef<Props, ResourceSelectorInterface>(
414445
}
415446
/>
416447
) : null}
417-
{externalEditorOpened && <ExternalEditorOpenedDialog />}
448+
{externalEditorOpened && (
449+
<ExternalEditorOpenedDialog
450+
onClose={cancelEditingWithExternalEditor}
451+
/>
452+
)}
418453
</ResponsiveLineStackLayout>
419454
)}
420455
</I18n>

newIDE/app/src/UI/ExternalEditorOpenedDialog.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ import { Trans } from '@lingui/macro';
22
import optionalRequire from '../Utils/OptionalRequire';
33
import Dialog from './Dialog';
44
import Text from './Text';
5+
import FlatButton from './FlatButton';
56

67
const electron = optionalRequire('electron');
78

8-
export const ExternalEditorOpenedDialog = () => {
9+
export const ExternalEditorOpenedDialog = ({
10+
onClose,
11+
}: {|
12+
onClose?: () => Promise<void>,
13+
|}) => {
914
if (!!electron) return null;
1015

1116
return (
@@ -16,6 +21,17 @@ export const ExternalEditorOpenedDialog = () => {
1621
open
1722
maxWidth="sm"
1823
id="external-editor-opened-dialog"
24+
actions={
25+
onClose
26+
? [
27+
<FlatButton
28+
label={<Trans>Cancel</Trans>}
29+
onClick={onClose}
30+
key="close"
31+
/>,
32+
]
33+
: null
34+
}
1935
>
2036
<Text>
2137
<Trans>

0 commit comments

Comments
 (0)