diff options
Diffstat (limited to 'subprojects/frontend/src/editor')
-rw-r--r-- | subprojects/frontend/src/editor/AnalysisErrorNotification.tsx | 74 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/AnimatedButton.tsx | 11 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/DiagnosticValue.ts | 1 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorButtons.tsx | 6 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorErrors.tsx | 93 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorPane.tsx | 4 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorStore.ts | 132 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorTheme.ts | 15 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/GenerateButton.tsx | 68 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/GeneratedModelStore.ts | 50 |
10 files changed, 410 insertions, 44 deletions
diff --git a/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx new file mode 100644 index 00000000..591a3600 --- /dev/null +++ b/subprojects/frontend/src/editor/AnalysisErrorNotification.tsx | |||
@@ -0,0 +1,74 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { reaction } from 'mobx'; | ||
8 | import { type SnackbarKey, useSnackbar } from 'notistack'; | ||
9 | import { useEffect, useState } from 'react'; | ||
10 | |||
11 | import type EditorStore from './EditorStore'; | ||
12 | |||
13 | function MessageObserver({ | ||
14 | editorStore, | ||
15 | }: { | ||
16 | editorStore: EditorStore; | ||
17 | }): React.ReactNode { | ||
18 | const [message, setMessage] = useState( | ||
19 | editorStore.delayedErrors.semanticsError ?? '', | ||
20 | ); | ||
21 | // Instead of making this component an `observer`, | ||
22 | // we only update the message is one is present to make sure that the | ||
23 | // disappear animation has a chance to complete. | ||
24 | useEffect( | ||
25 | () => | ||
26 | reaction( | ||
27 | () => editorStore.delayedErrors.semanticsError, | ||
28 | (newMessage) => { | ||
29 | if (newMessage !== undefined) { | ||
30 | setMessage(newMessage); | ||
31 | } | ||
32 | }, | ||
33 | { fireImmediately: false }, | ||
34 | ), | ||
35 | [editorStore], | ||
36 | ); | ||
37 | return message; | ||
38 | } | ||
39 | |||
40 | export default function AnalysisErrorNotification({ | ||
41 | editorStore, | ||
42 | }: { | ||
43 | editorStore: EditorStore; | ||
44 | }): null { | ||
45 | const { enqueueSnackbar, closeSnackbar } = useSnackbar(); | ||
46 | useEffect(() => { | ||
47 | let key: SnackbarKey | undefined; | ||
48 | const disposer = reaction( | ||
49 | () => editorStore.delayedErrors.semanticsError !== undefined, | ||
50 | (hasError) => { | ||
51 | if (hasError) { | ||
52 | if (key === undefined) { | ||
53 | key = enqueueSnackbar({ | ||
54 | message: <MessageObserver editorStore={editorStore} />, | ||
55 | variant: 'error', | ||
56 | persist: true, | ||
57 | }); | ||
58 | } | ||
59 | } else if (key !== undefined) { | ||
60 | closeSnackbar(key); | ||
61 | key = undefined; | ||
62 | } | ||
63 | }, | ||
64 | { fireImmediately: true }, | ||
65 | ); | ||
66 | return () => { | ||
67 | disposer(); | ||
68 | if (key !== undefined) { | ||
69 | closeSnackbar(key); | ||
70 | } | ||
71 | }; | ||
72 | }, [editorStore, enqueueSnackbar, closeSnackbar]); | ||
73 | return null; | ||
74 | } | ||
diff --git a/subprojects/frontend/src/editor/AnimatedButton.tsx b/subprojects/frontend/src/editor/AnimatedButton.tsx index dbbda618..606aabea 100644 --- a/subprojects/frontend/src/editor/AnimatedButton.tsx +++ b/subprojects/frontend/src/editor/AnimatedButton.tsx | |||
@@ -45,10 +45,10 @@ export default function AnimatedButton({ | |||
45 | children, | 45 | children, |
46 | }: { | 46 | }: { |
47 | 'aria-label'?: string; | 47 | 'aria-label'?: string; |
48 | onClick?: () => void; | 48 | onClick?: React.MouseEventHandler<HTMLElement>; |
49 | color: 'error' | 'warning' | 'primary' | 'inherit'; | 49 | color: 'error' | 'warning' | 'primary' | 'inherit'; |
50 | disabled?: boolean; | 50 | disabled?: boolean; |
51 | startIcon: JSX.Element; | 51 | startIcon?: JSX.Element; |
52 | sx?: SxProps<Theme> | undefined; | 52 | sx?: SxProps<Theme> | undefined; |
53 | children?: ReactNode; | 53 | children?: ReactNode; |
54 | }): JSX.Element { | 54 | }): JSX.Element { |
@@ -79,7 +79,11 @@ export default function AnimatedButton({ | |||
79 | className="rounded shaded" | 79 | className="rounded shaded" |
80 | disabled={disabled ?? false} | 80 | disabled={disabled ?? false} |
81 | startIcon={startIcon} | 81 | startIcon={startIcon} |
82 | width={width === undefined ? 'auto' : `calc(${width} + 50px)`} | 82 | width={ |
83 | width === undefined | ||
84 | ? 'auto' | ||
85 | : `calc(${width} + ${startIcon === undefined ? 28 : 50}px)` | ||
86 | } | ||
83 | > | 87 | > |
84 | <Box | 88 | <Box |
85 | display="flex" | 89 | display="flex" |
@@ -100,6 +104,7 @@ AnimatedButton.defaultProps = { | |||
100 | 'aria-label': undefined, | 104 | 'aria-label': undefined, |
101 | onClick: undefined, | 105 | onClick: undefined, |
102 | disabled: false, | 106 | disabled: false, |
107 | startIcon: undefined, | ||
103 | sx: undefined, | 108 | sx: undefined, |
104 | children: undefined, | 109 | children: undefined, |
105 | }; | 110 | }; |
diff --git a/subprojects/frontend/src/editor/DiagnosticValue.ts b/subprojects/frontend/src/editor/DiagnosticValue.ts index 20478262..410a46b7 100644 --- a/subprojects/frontend/src/editor/DiagnosticValue.ts +++ b/subprojects/frontend/src/editor/DiagnosticValue.ts | |||
@@ -14,6 +14,7 @@ export default class DiagnosticValue extends RangeValue { | |||
14 | error: new DiagnosticValue('error'), | 14 | error: new DiagnosticValue('error'), |
15 | warning: new DiagnosticValue('warning'), | 15 | warning: new DiagnosticValue('warning'), |
16 | info: new DiagnosticValue('info'), | 16 | info: new DiagnosticValue('info'), |
17 | hint: new DiagnosticValue('hint'), | ||
17 | }; | 18 | }; |
18 | 19 | ||
19 | private constructor(public readonly severity: Severity) { | 20 | private constructor(public readonly severity: Severity) { |
diff --git a/subprojects/frontend/src/editor/EditorButtons.tsx b/subprojects/frontend/src/editor/EditorButtons.tsx index 9b187e5c..ca51f975 100644 --- a/subprojects/frontend/src/editor/EditorButtons.tsx +++ b/subprojects/frontend/src/editor/EditorButtons.tsx | |||
@@ -5,8 +5,8 @@ | |||
5 | */ | 5 | */ |
6 | 6 | ||
7 | import type { Diagnostic } from '@codemirror/lint'; | 7 | import type { Diagnostic } from '@codemirror/lint'; |
8 | import CancelIcon from '@mui/icons-material/Cancel'; | ||
8 | import CheckIcon from '@mui/icons-material/Check'; | 9 | import CheckIcon from '@mui/icons-material/Check'; |
9 | import ErrorIcon from '@mui/icons-material/Error'; | ||
10 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; | 10 | import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered'; |
11 | import FormatPaint from '@mui/icons-material/FormatPaint'; | 11 | import FormatPaint from '@mui/icons-material/FormatPaint'; |
12 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; | 12 | import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; |
@@ -28,7 +28,7 @@ import type EditorStore from './EditorStore'; | |||
28 | function getLintIcon(severity: Diagnostic['severity'] | undefined) { | 28 | function getLintIcon(severity: Diagnostic['severity'] | undefined) { |
29 | switch (severity) { | 29 | switch (severity) { |
30 | case 'error': | 30 | case 'error': |
31 | return <ErrorIcon fontSize="small" />; | 31 | return <CancelIcon fontSize="small" />; |
32 | case 'warning': | 32 | case 'warning': |
33 | return <WarningIcon fontSize="small" />; | 33 | return <WarningIcon fontSize="small" />; |
34 | case 'info': | 34 | case 'info': |
@@ -95,7 +95,7 @@ export default observer(function EditorButtons({ | |||
95 | })} | 95 | })} |
96 | value="show-lint-panel" | 96 | value="show-lint-panel" |
97 | > | 97 | > |
98 | {getLintIcon(editorStore?.highestDiagnosticLevel)} | 98 | {getLintIcon(editorStore?.delayedErrors?.highestDiagnosticLevel)} |
99 | </ToggleButton> | 99 | </ToggleButton> |
100 | </ToggleButtonGroup> | 100 | </ToggleButtonGroup> |
101 | <IconButton | 101 | <IconButton |
diff --git a/subprojects/frontend/src/editor/EditorErrors.tsx b/subprojects/frontend/src/editor/EditorErrors.tsx new file mode 100644 index 00000000..40becf7e --- /dev/null +++ b/subprojects/frontend/src/editor/EditorErrors.tsx | |||
@@ -0,0 +1,93 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { Diagnostic } from '@codemirror/lint'; | ||
8 | import { type IReactionDisposer, makeAutoObservable, reaction } from 'mobx'; | ||
9 | |||
10 | import type EditorStore from './EditorStore'; | ||
11 | |||
12 | const HYSTERESIS_TIME_MS = 250; | ||
13 | |||
14 | export interface State { | ||
15 | analyzing: boolean; | ||
16 | errorCount: number; | ||
17 | warningCount: number; | ||
18 | infoCount: number; | ||
19 | semanticsError: string | undefined; | ||
20 | } | ||
21 | |||
22 | export default class EditorErrors implements State { | ||
23 | private readonly disposer: IReactionDisposer; | ||
24 | |||
25 | private timer: number | undefined; | ||
26 | |||
27 | analyzing = false; | ||
28 | |||
29 | errorCount = 0; | ||
30 | |||
31 | warningCount = 0; | ||
32 | |||
33 | infoCount = 0; | ||
34 | |||
35 | semanticsError: string | undefined; | ||
36 | |||
37 | constructor(private readonly store: EditorStore) { | ||
38 | this.updateImmediately(this.getNextState()); | ||
39 | makeAutoObservable<EditorErrors, 'disposer' | 'timer'>(this, { | ||
40 | disposer: false, | ||
41 | timer: false, | ||
42 | }); | ||
43 | this.disposer = reaction( | ||
44 | () => this.getNextState(), | ||
45 | (nextState) => { | ||
46 | if (this.timer !== undefined) { | ||
47 | clearTimeout(this.timer); | ||
48 | this.timer = undefined; | ||
49 | } | ||
50 | if (nextState.analyzing) { | ||
51 | this.timer = setTimeout( | ||
52 | () => this.updateImmediately(nextState), | ||
53 | HYSTERESIS_TIME_MS, | ||
54 | ); | ||
55 | } else { | ||
56 | this.updateImmediately(nextState); | ||
57 | } | ||
58 | }, | ||
59 | { fireImmediately: true }, | ||
60 | ); | ||
61 | } | ||
62 | |||
63 | get highestDiagnosticLevel(): Diagnostic['severity'] | undefined { | ||
64 | if (this.errorCount > 0) { | ||
65 | return 'error'; | ||
66 | } | ||
67 | if (this.warningCount > 0) { | ||
68 | return 'warning'; | ||
69 | } | ||
70 | if (this.infoCount > 0) { | ||
71 | return 'info'; | ||
72 | } | ||
73 | return undefined; | ||
74 | } | ||
75 | |||
76 | private getNextState(): State { | ||
77 | return { | ||
78 | analyzing: this.store.analyzing, | ||
79 | errorCount: this.store.errorCount, | ||
80 | warningCount: this.store.warningCount, | ||
81 | infoCount: this.store.infoCount, | ||
82 | semanticsError: this.store.semanticsError, | ||
83 | }; | ||
84 | } | ||
85 | |||
86 | private updateImmediately(nextState: State) { | ||
87 | Object.assign(this, nextState); | ||
88 | } | ||
89 | |||
90 | dispose() { | ||
91 | this.disposer(); | ||
92 | } | ||
93 | } | ||
diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx index 87f408fe..1125a0ec 100644 --- a/subprojects/frontend/src/editor/EditorPane.tsx +++ b/subprojects/frontend/src/editor/EditorPane.tsx | |||
@@ -13,6 +13,7 @@ import { useState } from 'react'; | |||
13 | 13 | ||
14 | import { useRootStore } from '../RootStoreProvider'; | 14 | import { useRootStore } from '../RootStoreProvider'; |
15 | 15 | ||
16 | import AnalysisErrorNotification from './AnalysisErrorNotification'; | ||
16 | import ConnectionStatusNotification from './ConnectionStatusNotification'; | 17 | import ConnectionStatusNotification from './ConnectionStatusNotification'; |
17 | import EditorArea from './EditorArea'; | 18 | import EditorArea from './EditorArea'; |
18 | import EditorButtons from './EditorButtons'; | 19 | import EditorButtons from './EditorButtons'; |
@@ -39,7 +40,7 @@ export default observer(function EditorPane(): JSX.Element { | |||
39 | const { editorStore } = useRootStore(); | 40 | const { editorStore } = useRootStore(); |
40 | 41 | ||
41 | return ( | 42 | return ( |
42 | <Stack direction="column" flexGrow={1} flexShrink={1} overflow="auto"> | 43 | <Stack direction="column" height="100%" overflow="auto"> |
43 | <Toolbar variant="dense"> | 44 | <Toolbar variant="dense"> |
44 | <EditorButtons editorStore={editorStore} /> | 45 | <EditorButtons editorStore={editorStore} /> |
45 | </Toolbar> | 46 | </Toolbar> |
@@ -48,6 +49,7 @@ export default observer(function EditorPane(): JSX.Element { | |||
48 | <EditorLoading /> | 49 | <EditorLoading /> |
49 | ) : ( | 50 | ) : ( |
50 | <> | 51 | <> |
52 | <AnalysisErrorNotification editorStore={editorStore} /> | ||
51 | <ConnectionStatusNotification editorStore={editorStore} /> | 53 | <ConnectionStatusNotification editorStore={editorStore} /> |
52 | <SearchPanelPortal editorStore={editorStore} /> | 54 | <SearchPanelPortal editorStore={editorStore} /> |
53 | <EditorArea editorStore={editorStore} /> | 55 | <EditorArea editorStore={editorStore} /> |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index b98f085e..9508858d 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -26,9 +26,13 @@ import { makeAutoObservable, observable, runInAction } from 'mobx'; | |||
26 | import { nanoid } from 'nanoid'; | 26 | import { nanoid } from 'nanoid'; |
27 | 27 | ||
28 | import type PWAStore from '../PWAStore'; | 28 | import type PWAStore from '../PWAStore'; |
29 | import GraphStore from '../graph/GraphStore'; | ||
29 | import getLogger from '../utils/getLogger'; | 30 | import getLogger from '../utils/getLogger'; |
30 | import type XtextClient from '../xtext/XtextClient'; | 31 | import type XtextClient from '../xtext/XtextClient'; |
32 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | ||
31 | 33 | ||
34 | import EditorErrors from './EditorErrors'; | ||
35 | import GeneratedModelStore from './GeneratedModelStore'; | ||
32 | import LintPanelStore from './LintPanelStore'; | 36 | import LintPanelStore from './LintPanelStore'; |
33 | import SearchPanelStore from './SearchPanelStore'; | 37 | import SearchPanelStore from './SearchPanelStore'; |
34 | import createEditorState from './createEditorState'; | 38 | import createEditorState from './createEditorState'; |
@@ -54,13 +58,26 @@ export default class EditorStore { | |||
54 | 58 | ||
55 | readonly lintPanel: LintPanelStore; | 59 | readonly lintPanel: LintPanelStore; |
56 | 60 | ||
61 | readonly delayedErrors: EditorErrors; | ||
62 | |||
57 | showLineNumbers = false; | 63 | showLineNumbers = false; |
58 | 64 | ||
59 | disposed = false; | 65 | disposed = false; |
60 | 66 | ||
67 | analyzing = false; | ||
68 | |||
69 | semanticsError: string | undefined; | ||
70 | |||
71 | graph: GraphStore; | ||
72 | |||
73 | generatedModels = new Map<string, GeneratedModelStore>(); | ||
74 | |||
75 | selectedGeneratedModel: string | undefined; | ||
76 | |||
61 | constructor(initialValue: string, pwaStore: PWAStore) { | 77 | constructor(initialValue: string, pwaStore: PWAStore) { |
62 | this.id = nanoid(); | 78 | this.id = nanoid(); |
63 | this.state = createEditorState(initialValue, this); | 79 | this.state = createEditorState(initialValue, this); |
80 | this.delayedErrors = new EditorErrors(this); | ||
64 | this.searchPanel = new SearchPanelStore(this); | 81 | this.searchPanel = new SearchPanelStore(this); |
65 | this.lintPanel = new LintPanelStore(this); | 82 | this.lintPanel = new LintPanelStore(this); |
66 | (async () => { | 83 | (async () => { |
@@ -75,6 +92,7 @@ export default class EditorStore { | |||
75 | })().catch((error) => { | 92 | })().catch((error) => { |
76 | log.error('Failed to load XtextClient', error); | 93 | log.error('Failed to load XtextClient', error); |
77 | }); | 94 | }); |
95 | this.graph = new GraphStore(); | ||
78 | makeAutoObservable<EditorStore, 'client'>(this, { | 96 | makeAutoObservable<EditorStore, 'client'>(this, { |
79 | id: false, | 97 | id: false, |
80 | state: observable.ref, | 98 | state: observable.ref, |
@@ -213,19 +231,6 @@ export default class EditorStore { | |||
213 | this.doCommand(nextDiagnostic); | 231 | this.doCommand(nextDiagnostic); |
214 | } | 232 | } |
215 | 233 | ||
216 | get highestDiagnosticLevel(): Diagnostic['severity'] | undefined { | ||
217 | if (this.errorCount > 0) { | ||
218 | return 'error'; | ||
219 | } | ||
220 | if (this.warningCount > 0) { | ||
221 | return 'warning'; | ||
222 | } | ||
223 | if (this.infoCount > 0) { | ||
224 | return 'info'; | ||
225 | } | ||
226 | return undefined; | ||
227 | } | ||
228 | |||
229 | updateSemanticHighlighting(ranges: IHighlightRange[]): void { | 234 | updateSemanticHighlighting(ranges: IHighlightRange[]): void { |
230 | this.dispatch(setSemanticHighlighting(ranges)); | 235 | this.dispatch(setSemanticHighlighting(ranges)); |
231 | } | 236 | } |
@@ -282,8 +287,109 @@ export default class EditorStore { | |||
282 | return true; | 287 | return true; |
283 | } | 288 | } |
284 | 289 | ||
290 | analysisStarted() { | ||
291 | this.analyzing = true; | ||
292 | } | ||
293 | |||
294 | analysisCompleted(semanticAnalysisSkipped = false) { | ||
295 | this.analyzing = false; | ||
296 | if (semanticAnalysisSkipped) { | ||
297 | this.semanticsError = undefined; | ||
298 | } | ||
299 | } | ||
300 | |||
301 | setSemanticsError(semanticsError: string) { | ||
302 | this.semanticsError = semanticsError; | ||
303 | } | ||
304 | |||
305 | setSemantics(semantics: SemanticsSuccessResult) { | ||
306 | this.semanticsError = undefined; | ||
307 | this.graph.setSemantics(semantics); | ||
308 | } | ||
309 | |||
285 | dispose(): void { | 310 | dispose(): void { |
286 | this.client?.dispose(); | 311 | this.client?.dispose(); |
312 | this.delayedErrors.dispose(); | ||
287 | this.disposed = true; | 313 | this.disposed = true; |
288 | } | 314 | } |
315 | |||
316 | startModelGeneration(randomSeed?: number): void { | ||
317 | this.client | ||
318 | ?.startModelGeneration(randomSeed) | ||
319 | ?.catch((error) => log.error('Could not start model generation', error)); | ||
320 | } | ||
321 | |||
322 | addGeneratedModel(uuid: string, randomSeed: number): void { | ||
323 | this.generatedModels.set(uuid, new GeneratedModelStore(randomSeed)); | ||
324 | this.selectGeneratedModel(uuid); | ||
325 | } | ||
326 | |||
327 | cancelModelGeneration(): void { | ||
328 | this.client | ||
329 | ?.cancelModelGeneration() | ||
330 | ?.catch((error) => log.error('Could not start model generation', error)); | ||
331 | } | ||
332 | |||
333 | selectGeneratedModel(uuid: string | undefined): void { | ||
334 | if (uuid === undefined) { | ||
335 | this.selectedGeneratedModel = uuid; | ||
336 | return; | ||
337 | } | ||
338 | if (this.generatedModels.has(uuid)) { | ||
339 | this.selectedGeneratedModel = uuid; | ||
340 | return; | ||
341 | } | ||
342 | this.selectedGeneratedModel = undefined; | ||
343 | } | ||
344 | |||
345 | deleteGeneratedModel(uuid: string | undefined): void { | ||
346 | if (uuid === undefined) { | ||
347 | return; | ||
348 | } | ||
349 | if (this.selectedGeneratedModel === uuid) { | ||
350 | let previous: string | undefined; | ||
351 | let found: string | undefined; | ||
352 | this.generatedModels.forEach((_value, key) => { | ||
353 | if (key === uuid) { | ||
354 | found = previous; | ||
355 | } | ||
356 | previous = key; | ||
357 | }); | ||
358 | this.selectGeneratedModel(found); | ||
359 | } | ||
360 | const generatedModel = this.generatedModels.get(uuid); | ||
361 | if (generatedModel !== undefined && generatedModel.running) { | ||
362 | this.cancelModelGeneration(); | ||
363 | } | ||
364 | this.generatedModels.delete(uuid); | ||
365 | } | ||
366 | |||
367 | modelGenerationCancelled(): void { | ||
368 | this.generatedModels.forEach((value) => | ||
369 | value.setError('Model generation cancelled'), | ||
370 | ); | ||
371 | } | ||
372 | |||
373 | setGeneratedModelMessage(uuid: string, message: string): void { | ||
374 | this.generatedModels.get(uuid)?.setMessage(message); | ||
375 | } | ||
376 | |||
377 | setGeneratedModelError(uuid: string, message: string): void { | ||
378 | this.generatedModels.get(uuid)?.setError(message); | ||
379 | } | ||
380 | |||
381 | setGeneratedModelSemantics( | ||
382 | uuid: string, | ||
383 | semantics: SemanticsSuccessResult, | ||
384 | ): void { | ||
385 | this.generatedModels.get(uuid)?.setSemantics(semantics); | ||
386 | } | ||
387 | |||
388 | get generating(): boolean { | ||
389 | let generating = false; | ||
390 | this.generatedModels.forEach((value) => { | ||
391 | generating = generating || value.running; | ||
392 | }); | ||
393 | return generating; | ||
394 | } | ||
289 | } | 395 | } |
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts index e057ce18..055b62e2 100644 --- a/subprojects/frontend/src/editor/EditorTheme.ts +++ b/subprojects/frontend/src/editor/EditorTheme.ts | |||
@@ -4,15 +4,13 @@ | |||
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | 6 | ||
7 | import errorSVG from '@material-icons/svg/svg/error/baseline.svg?raw'; | 7 | import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; |
8 | import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; | 8 | import expandMoreSVG from '@material-icons/svg/svg/expand_more/baseline.svg?raw'; |
9 | import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; | 9 | import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw'; |
10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; | 10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; |
11 | import { alpha, styled, type CSSObject } from '@mui/material/styles'; | 11 | import { alpha, styled, type CSSObject } from '@mui/material/styles'; |
12 | 12 | ||
13 | function svgURL(svg: string): string { | 13 | import svgURL from '../utils/svgURL'; |
14 | return `url('data:image/svg+xml;utf8,${svg}')`; | ||
15 | } | ||
16 | 14 | ||
17 | export default styled('div', { | 15 | export default styled('div', { |
18 | name: 'EditorTheme', | 16 | name: 'EditorTheme', |
@@ -56,15 +54,16 @@ export default styled('div', { | |||
56 | '.cm-activeLineGutter': { | 54 | '.cm-activeLineGutter': { |
57 | background: 'transparent', | 55 | background: 'transparent', |
58 | }, | 56 | }, |
59 | '.cm-cursor, .cm-cursor-primary': { | 57 | '.cm-cursor, .cm-dropCursor, .cm-cursor-primary': { |
60 | borderLeft: `2px solid ${theme.palette.info.main}`, | 58 | borderLeft: `2px solid ${theme.palette.info.main}`, |
59 | marginLeft: -1, | ||
61 | }, | 60 | }, |
62 | '.cm-selectionBackground': { | 61 | '.cm-selectionBackground': { |
63 | background: theme.palette.highlight.selection, | 62 | background: theme.palette.highlight.selection, |
64 | }, | 63 | }, |
65 | '.cm-focused': { | 64 | '.cm-focused': { |
66 | outline: 'none', | 65 | outline: 'none', |
67 | '.cm-selectionBackground': { | 66 | '& > .cm-scroller > .cm-selectionLayer .cm-selectionBackground': { |
68 | background: theme.palette.highlight.selection, | 67 | background: theme.palette.highlight.selection, |
69 | }, | 68 | }, |
70 | }, | 69 | }, |
@@ -106,7 +105,7 @@ export default styled('div', { | |||
106 | color: theme.palette.text.primary, | 105 | color: theme.palette.text.primary, |
107 | }, | 106 | }, |
108 | }, | 107 | }, |
109 | '.tok-problem-abstract, .tok-problem-new': { | 108 | '.tok-problem-abstract': { |
110 | fontStyle: 'italic', | 109 | fontStyle: 'italic', |
111 | }, | 110 | }, |
112 | '.tok-problem-containment': { | 111 | '.tok-problem-containment': { |
@@ -331,7 +330,7 @@ export default styled('div', { | |||
331 | '.cm-lintRange-active': { | 330 | '.cm-lintRange-active': { |
332 | background: theme.palette.highlight.activeLintRange, | 331 | background: theme.palette.highlight.activeLintRange, |
333 | }, | 332 | }, |
334 | ...lintSeverityStyle('error', errorSVG, 120), | 333 | ...lintSeverityStyle('error', cancelSVG, 120), |
335 | ...lintSeverityStyle('warning', warningSVG, 110), | 334 | ...lintSeverityStyle('warning', warningSVG, 110), |
336 | ...lintSeverityStyle('info', infoSVG, 100), | 335 | ...lintSeverityStyle('info', infoSVG, 100), |
337 | }; | 336 | }; |
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 3837ef8e..b6b1655a 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx | |||
@@ -4,10 +4,9 @@ | |||
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | 6 | ||
7 | import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined'; | 7 | import CancelIcon from '@mui/icons-material/Cancel'; |
8 | import CloseIcon from '@mui/icons-material/Close'; | ||
8 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | 9 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; |
9 | import Button from '@mui/material/Button'; | ||
10 | import type { SxProps, Theme } from '@mui/material/styles'; | ||
11 | import { observer } from 'mobx-react-lite'; | 10 | import { observer } from 'mobx-react-lite'; |
12 | 11 | ||
13 | import AnimatedButton from './AnimatedButton'; | 12 | import AnimatedButton from './AnimatedButton'; |
@@ -18,26 +17,59 @@ const GENERATE_LABEL = 'Generate'; | |||
18 | const GenerateButton = observer(function GenerateButton({ | 17 | const GenerateButton = observer(function GenerateButton({ |
19 | editorStore, | 18 | editorStore, |
20 | hideWarnings, | 19 | hideWarnings, |
21 | sx, | ||
22 | }: { | 20 | }: { |
23 | editorStore: EditorStore | undefined; | 21 | editorStore: EditorStore | undefined; |
24 | hideWarnings?: boolean | undefined; | 22 | hideWarnings?: boolean | undefined; |
25 | sx?: SxProps<Theme> | undefined; | ||
26 | }): JSX.Element { | 23 | }): JSX.Element { |
27 | if (editorStore === undefined) { | 24 | if (editorStore === undefined) { |
28 | return ( | 25 | return ( |
29 | <Button | 26 | <AnimatedButton color="inherit" disabled> |
27 | Loading… | ||
28 | </AnimatedButton> | ||
29 | ); | ||
30 | } | ||
31 | |||
32 | const { | ||
33 | delayedErrors: { analyzing, errorCount, warningCount, semanticsError }, | ||
34 | generating, | ||
35 | } = editorStore; | ||
36 | |||
37 | if (analyzing) { | ||
38 | return ( | ||
39 | <AnimatedButton color="inherit" disabled> | ||
40 | Analyzing… | ||
41 | </AnimatedButton> | ||
42 | ); | ||
43 | } | ||
44 | |||
45 | if (generating) { | ||
46 | return ( | ||
47 | <AnimatedButton | ||
30 | color="inherit" | 48 | color="inherit" |
31 | className="rounded shaded" | 49 | onClick={() => editorStore.cancelModelGeneration()} |
32 | disabled | 50 | startIcon={<CloseIcon />} |
33 | {...(sx === undefined ? {} : { sx })} | ||
34 | > | 51 | > |
35 | Loading… | 52 | Cancel |
36 | </Button> | 53 | </AnimatedButton> |
37 | ); | 54 | ); |
38 | } | 55 | } |
39 | 56 | ||
40 | const { errorCount, warningCount } = editorStore; | 57 | if (semanticsError !== undefined && editorStore.opened) { |
58 | return ( | ||
59 | <AnimatedButton | ||
60 | color="error" | ||
61 | disabled | ||
62 | startIcon={<CancelIcon />} | ||
63 | sx={(theme) => ({ | ||
64 | '&.Mui-disabled': { | ||
65 | color: `${theme.palette.error.main} !important`, | ||
66 | }, | ||
67 | })} | ||
68 | > | ||
69 | Analysis error | ||
70 | </AnimatedButton> | ||
71 | ); | ||
72 | } | ||
41 | 73 | ||
42 | const diagnostics: string[] = []; | 74 | const diagnostics: string[] = []; |
43 | if (errorCount > 0) { | 75 | if (errorCount > 0) { |
@@ -54,8 +86,7 @@ const GenerateButton = observer(function GenerateButton({ | |||
54 | aria-label={`Select next diagnostic out of ${summary}`} | 86 | aria-label={`Select next diagnostic out of ${summary}`} |
55 | onClick={() => editorStore.nextDiagnostic()} | 87 | onClick={() => editorStore.nextDiagnostic()} |
56 | color="error" | 88 | color="error" |
57 | startIcon={<DangerousOutlinedIcon />} | 89 | startIcon={<CancelIcon />} |
58 | {...(sx === undefined ? {} : { sx })} | ||
59 | > | 90 | > |
60 | {summary} | 91 | {summary} |
61 | </AnimatedButton> | 92 | </AnimatedButton> |
@@ -67,7 +98,13 @@ const GenerateButton = observer(function GenerateButton({ | |||
67 | disabled={!editorStore.opened} | 98 | disabled={!editorStore.opened} |
68 | color={warningCount > 0 ? 'warning' : 'primary'} | 99 | color={warningCount > 0 ? 'warning' : 'primary'} |
69 | startIcon={<PlayArrowIcon />} | 100 | startIcon={<PlayArrowIcon />} |
70 | {...(sx === undefined ? {} : { sx })} | 101 | onClick={(event) => { |
102 | if (event.shiftKey) { | ||
103 | editorStore.startModelGeneration(1); | ||
104 | } else { | ||
105 | editorStore.startModelGeneration(); | ||
106 | } | ||
107 | }} | ||
71 | > | 108 | > |
72 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} | 109 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} |
73 | </AnimatedButton> | 110 | </AnimatedButton> |
@@ -76,7 +113,6 @@ const GenerateButton = observer(function GenerateButton({ | |||
76 | 113 | ||
77 | GenerateButton.defaultProps = { | 114 | GenerateButton.defaultProps = { |
78 | hideWarnings: false, | 115 | hideWarnings: false, |
79 | sx: undefined, | ||
80 | }; | 116 | }; |
81 | 117 | ||
82 | export default GenerateButton; | 118 | export default GenerateButton; |
diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts new file mode 100644 index 00000000..5088d603 --- /dev/null +++ b/subprojects/frontend/src/editor/GeneratedModelStore.ts | |||
@@ -0,0 +1,50 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { makeAutoObservable } from 'mobx'; | ||
8 | |||
9 | import GraphStore from '../graph/GraphStore'; | ||
10 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | ||
11 | |||
12 | export default class GeneratedModelStore { | ||
13 | title: string; | ||
14 | |||
15 | message = 'Waiting for server'; | ||
16 | |||
17 | error = false; | ||
18 | |||
19 | graph: GraphStore | undefined; | ||
20 | |||
21 | constructor(randomSeed: number) { | ||
22 | const time = new Date().toLocaleTimeString(undefined, { hour12: false }); | ||
23 | this.title = `Generated at ${time} (${randomSeed})`; | ||
24 | makeAutoObservable(this); | ||
25 | } | ||
26 | |||
27 | get running(): boolean { | ||
28 | return !this.error && this.graph === undefined; | ||
29 | } | ||
30 | |||
31 | setMessage(message: string): void { | ||
32 | if (this.running) { | ||
33 | this.message = message; | ||
34 | } | ||
35 | } | ||
36 | |||
37 | setError(message: string): void { | ||
38 | if (this.running) { | ||
39 | this.error = true; | ||
40 | this.message = message; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | setSemantics(semantics: SemanticsSuccessResult): void { | ||
45 | if (this.running) { | ||
46 | this.graph = new GraphStore(); | ||
47 | this.graph.setSemantics(semantics); | ||
48 | } | ||
49 | } | ||
50 | } | ||