8000 Add undo/redo buttons (#491) · joernalraun/python-editor-next@071d0fc · GitHub
[go: up one dir, main page]

Skip to content

Commit 071d0fc

Browse files
Add undo/redo buttons (microbit-foundation#491)
They operate on the text editor only. Closes microbit-foundation#488.
1 parent 08d8c30 commit 071d0fc

File tree

12 files changed

+250
-41
lines changed

12 files changed

+250
-41
lines changed

lang/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,10 @@
403403
"defaultMessage": "Read more",
404404
"description": "Action text to expand a collapsed section"
405405
},
406+
"redo": {
407+
"defaultMessage": "Redo",
408+
"description": "Aria label for the redo button"
409+
},
406410
"reference-description": {
407411
"defaultMessage": "Reference documentation for micro:bit MicroPython",
408412
"description": "Description at the top of the Reference tab"
@@ -535,6 +539,10 @@
535539
"defaultMessage": "View {name} documentation",
536540
"description": "Aria label for the toolkit topic right arrow"
537541
},
542+
"undo": {
543+
"defaultMessage": "Undo",
544+
"description": "Aria label for the undo button"
545+
},
538546
"unexpected-error-description": {
539547
"defaultMessage": "Please try again or <link>raise a support request</link>",
540548
"description": "Progress dialog text telling user that code is being flashed"

lang/fr.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,10 @@
402402
"defaultMessage": "Lire plus",
403403
"description": "Action text to expand a collapsed section"
404404
},
405+
"redo": {
406+
"defaultMessage": "Redo",
407+
"description": "Aria label for the redo button"
408+
},
405409
"reference-description": {
406410
"defaultMessage": "Documentation de référence pour MicroPython de micro:bit",
407411
"description": "Description at the top of the Reference tab"
@@ -534,6 +538,10 @@
534538
"defaultMessage": "Afficher la documentation {name}",
535539
"description": "Aria label for the toolkit topic right arrow"
536540
},
541+
"undo": {
542+
"defaultMessage": "Undo",
543+
"description": "Aria label for the undo button"
544+
},
537545
"unexpected-error-description": {
538546
"defaultMessage&quo F438 t;: "Veuillez réessayer ou <link>soumettre une demande d'assistance</link>",
539547
"description": "Progress dialog text telling user that code is being flashed"

lang/lol.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,10 @@
402402
"defaultMessage": "Реад море",
403403
"description": "Action text to expand a collapsed section"
404404
},
405+
"redo": {
406+
"defaultMessage": "Redo",
407+
"description": "Aria label for the redo button"
408+
},
405409
"reference-description": {
406410
"defaultMessage": "Референсе досюментатіон фор місро:біт МісроПутхон",
407411
"description": "Description at the top of the Reference tab"
@@ -534,6 +538,10 @@
534538
"defaultMessage": "Віев {name} досюментатіон",
535539
"description": "Aria label for the toolkit topic right arrow"
536540
},
541+
"undo": {
542+
"defaultMessage": "Undo",
543+
"description": "Aria label for the undo button"
544+
},
537545
"unexpected-error-description": {
538546
"defaultMessage": "Плеасе тру аджаін ор <link>раісе а сюппорт реквюест</link>",
539547
"description": "Progress dialog text telling user that code is being flashed"

src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { deployment, useDeployment } from "./deployment";
1313
import { MicrobitWebUSBConnection } from "./device/device";
1414
import { DeviceContextProvider } from "./device/device-hooks";
1515
import { MockDeviceConnection } from "./device/mock";
16+
import SearchProvider from "./documentation/search/search-hooks";
17+
import ToolkitProvider from "./documentation/toolkit-hooks";
1618
import { ActiveEditorProvider } from "./editor/active-editor-hooks";
1719
import { FileSystem } from "./fs/fs";
1820
import { FileSystemProvider } from "./fs/fs-hooks";
@@ -34,8 +36,6 @@ import {
3436
import BeforeUnloadDirtyCheck from "./workbench/BeforeUnloadDirtyCheck";
3537
import { SelectionProvider } from "./workbench/use-selection";
3638
import Workbench from "./workbench/Workbench";
37-
import ToolkitProvider from "./documentation/toolkit-hooks";
38-
import SearchProvider from "./documentation/search/search-hooks";
3939

4040
const isMockDeviceMode = () =>
4141
// We use a cookie set from the e2e tests. Avoids having separate test and live builds.

src/editor/EditorArea.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { WorkbenchSelection } from "../workbench/use-selection";
1111
import ActiveFileInfo from "./ActiveFileInfo";
1212
import EditorContainer from "./EditorContainer";
1313
import ZoomControls from "../editor/ZoomControls";
14+
import UndoRedoControls from "./UndoRedoControls";
1415

1516
interface EditorAreaProps extends BoxProps {
1617
selection: WorkbenchSelection;
@@ -63,6 +64,13 @@ const EditorArea = ({
6364
borderColor="gray.200"
6465
/>
6566
<Box position="relative" flex="1 1 auto" height={0}>
67+
<UndoRedoControls
68+
display={["none", "none", "none", "flex"]}
69+
zIndex="1"
70+
top={6}
71+
right={10}
72+
position="absolute"
73+
/>
6674
<EditorContainer selection={selection} />
6775
</Box>
6876
</Flex>

src/editor/UndoRedoControls.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* (c) 2021, Micro:bit Educational Foundation and contributors
3+
*
4+
* SPDX-License-Identifier: MIT
5+
*/
6+
import { ButtonGroup, IconButton } from "@chakra-ui/react";
7+
import { RiArrowGoForwardLine, RiArrowGoBackLine } from "react-icons/ri";
8+
import { useIntl } from "react-intl";
9+
import {
10+
useActiveEditorActions,
11+
useActiveEditorInfo,
12+
} from "../editor/active-editor-hooks";
13+
14+
const UndoRedoControls = ({ ...props }) => {
15+
const intl = useIntl();
16+
const actions = useActiveEditorActions();
17+
const editorInfo = useActiveEditorInfo();
18+
19+
return (
20+
<ButtonGroup
21+
{...props}
22+
isAttached
23+
colorScheme="gray"
24+
variant="zoom"
25+
transform="rotate(90deg)"
26+
transformOrigin="bottom"
27+
>
28+
<IconButton
29+
isRound
30+
icon={<RiArrowGoBackLine style={{ transform: "rotate(-90deg)" }} />}
31+
aria-label={intl.formatMessage({ id: "undo" })}
32+
onClick={actions?.undo}
33+
disabled={editorInfo.undo ? false : true}
34+
/>
35+
<IconButton
36+
isRound
37+
borderLeft="1px"
38+
borderLeftColor="gray.10"
39+
icon={<RiArrowGoForwardLine style={{ transform: "rotate(-90deg)" }} />}
40+
aria-label={intl.formatMessage({ id: "redo" })}
41+
onClick={actions?.redo}
42+
disabled={editorInfo.redo ? false : true}
43+
/>
44+
</ButtonGroup>
45+
);
46+
};
47+
48+
export default UndoRedoControls;

src/editor/ZoomControls.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
ThemeTypings,
1111
} from "@chakra-ui/react";
1212
import { useCallback } from "react";
13-
import { RiAddLine, RiSubtractLine } from "react-icons/ri";
13+
import { RiZoomInLine, RiZoomOutLine } from "react-icons/ri";
1414
import { useIntl } from "react-intl";
1515
import {
1616
fontSizeStep,
@@ -42,22 +42,20 @@ const ZoomControls = ({ size, ...props }: ZoomControlsProps) => {
4242
}, [setSettings, settings]);
4343
const intl = useIntl();
4444
return (
45-
<ButtonGroup {...props} colorScheme="blackAlpha" variant="ghost">
45+
<ButtonGroup {...props} isAttached colorScheme="gray" variant="zoom">
4646
<IconButton
4747
size={size}
4848
isRound
49-
color="#838383"
50-
fontSize="xl"
51-
icon={<RiSubtractLine />}
49+
icon={<RiZoomOutLine />}
5250
aria-label={intl.formatMessage({ id: "zoom-out-action" })}
5351
onClick={handleZoomOut}
5452
/>
5553
<IconButton
54+
borderLeft="1px"
55+
borderLeftColor="gray.10"
5656
size={size}
5757
isRound
58-
color="#838383"
59-
fontSize="xl"
60-
icon={<RiAddLine />}
58+
icon={<RiZoomInLine />}
6159
aria-label={intl.formatMessage({ id: "zoom-in-action" })}
6260
onClick={handleZoomIn}
6361
/>

src/editor/active-editor-hooks.tsx

Lines changed: 95 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,13 @@ import React, {
1313
useContext,
1414
useState,
1515
} from "react";
16+
import { undo, redo } from "@codemirror/history";
1617
import { calculateChanges } from "./codemirror/edits";
1718

18-
type ActiveEditorState = [
19-
ActiveEditorActions | undefined,
20-
Dispatch<SetStateAction<ActiveEditorActions | undefined>>
21-
];
22-
23-
const ActiveEditorContext = React.createContext<ActiveEditorState | undefined>(
24-
undefined
25-
);
26-
27-
export const useActiveEditor = (): ActiveEditorState => {
28-
const context = useContext(ActiveEditorContext);
29-
if (!context) {
30-
throw new Error();
31-
}
32-
return context;
33-
};
34-
35-
export const useActiveEditorActions = (): ActiveEditorActions | undefined => {
36-
return useActiveEditor()?.[0];
37-
};
38-
3919
/**
40-
* Actions that operate on the active editor.
20+
* Actions that operate on a CM editor.
4121
*/
42-
export class ActiveEditorActions {
22+
export class EditorActions {
4323
constructor(private view: EditorView) {}
4424

4525
/**
@@ -51,19 +31,107 @@ export class ActiveEditorActions {
5131
this.view.dispatch(calculateChanges(this.view.state, code, "example"));
5232
this.view.focus();
5333
};
34+
undo = (): void => {
35+
undo(this.view);
36+
this.view.focus();
37+
};
38+
redo = (): void => {
39+
redo(this.view);
40+
this.view.focus();
41+
};
5442
}
5543

44+
type UseActiveEditorReturn = [
45+
EditorActions | undefined,
46+
Dispatch<SetStateAction<EditorActions | undefined>>
47+
];
48+
49+
const ActiveEditorActionsContext = React.createContext<
50+
UseActiveEditorReturn | undefined
51+
>(undefined);
52+
53+
export interface EditorInfo {
54+
/**
55+
* The size of the undo stack.
56+
*/
57+
undo: number;
58+
/**
59+
* The size of the redo stack.
60+
*/
61+
redo: number;
62+
}
63+
64+
export const defaultEditorInfo: EditorInfo = {
65+
undo: 0,
66+
redo: 0,
67+
};
68+
69+
type UseActiveEditorInfoReturn = [
70+
EditorInfo,
71+
Dispatch<SetStateAction<EditorInfo>>
72+
];
73+
74+
const ActiveEditorStateContext = React.createContext<
75+
UseActiveEditorInfoReturn | undefined
76+
>(undefined);
77+
78+
const ActiveEditorStateProvider = ({ children }: { children: ReactNode }) => {
79+
const value = useState<EditorInfo>(defaultEditorInfo);
80+
return (
81+
<ActiveEditorStateContext.Provider value={value}>
82+
{children}
83+
</ActiveEditorStateContext.Provider>
84+
);
85+
};
5686
interface ActiveEditorProviderProps {
5787
children: ReactNode;
5888
}
5989

6090
export const ActiveEditorProvider = ({
6191
children,
6292
}: ActiveEditorProviderProps) => {
63-
const value = useState<ActiveEditorActions>();
93+
const actions = useState<EditorActions>();
6494
return (
65-
<ActiveEditorContext.Provider value={value}>
66-
{children}
67-
</ActiveEditorContext.Provider>
95+
<ActiveEditorActionsContext.Provider value={actions}>
96+
<ActiveEditorStateProvider>{children}</ActiveEditorStateProvider>
97+
</ActiveEditorActionsContext.Provider>
6898
);
6999
};
100+
101+
/**
102+
* Used to update the active editor actions from CM.
103+
* Prefer {@link useActiveEditorActions} or {@link useActiveEditorInfoState}.
104+
*/
105+
export const useActiveEditorActionsState = (): UseActiveEditorReturn => {
106+
const value = useContext(ActiveEditorActionsContext);
107+
if (!value) {
108+
throw new Error("Missing provider");
109+
}
110+
return value;
111+
};
112+
113+
/**
114+
* Used to update the editor info from CM.
115+
* Prefer {@link useActiveEditorActions} or {@link useActiveEditorInfoState}.
116+
*/
117+
export const useActiveEditorInfoState = (): UseActiveEditorInfoReturn => {
118+
const context = useContext(ActiveEditorStateContext);
119+
if (!context) {
120+
throw new Error("Missing provider!");
121+
}
122+
return context;
123+
};
124+
125+
/**
126+
* Information on the active editor.
127+
*/
128+
export const useActiveEditorInfo = (): EditorInfo => {
129+
return useActiveEditorInfoState()[0];
130+
};
131+
132+
/**
133+
* Actions that affect the current editor.
134+
*/
135+
export const useActiveEditorActions = (): EditorActions | undefined => {
136+
return useActiveEditorActionsState()?.[0];
137+
};

0 commit comments

Comments
 (0)
0