Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/workbench/CollapsableButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
Button,
HTMLChakraProps,
IconButton,
ThemingProps,
} from "@chakra-ui/react";
import React from "react";

export interface CollapsableButtonProps
extends HTMLChakraProps<"button">,
ThemingProps<"Button"> {
mode: "icon" | "button";
text: string;
icon: React.ReactElement;
/**
* Width used only in button mode.
*/
buttonWidth?: number | string;
}

/**
* Button that can be a regular or icon button.
*
* We'd like to do this at a lower-level so we can animate a transition.
*/
const CollapsableButton = ({
mode,
text,
icon,
buttonWidth,
...props
}: CollapsableButtonProps) => {
return mode === "icon" ? (
<IconButton icon={icon} aria-label={text} {...props} />
) : (
<Button leftIcon={icon} minWidth={buttonWidth} {...props}>
{text}
</Button>
);
};

export default CollapsableButton;
85 changes: 23 additions & 62 deletions src/workbench/DeviceConnection.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { Button, HStack, Switch, Text, VStack } from "@chakra-ui/react";
import React, { useCallback, useState } from "react";
import { RiFlashlightFill } from "react-icons/ri";
import { useConnectionStatus, useDevice } from "../device/device-hooks";
import { ConnectionMode, ConnectionStatus, WebUSBError } from "../device";
import { useFileSystem } from "../fs/fs-hooks";
import DownloadButton from "./DownloadButton";
import { HStack, Switch, Text } from "@chakra-ui/react";
import React, { useCallback } from "react";
import Separate from "../common/Separate";
import useActionFeedback, {
ActionFeedback,
} from "../common/use-action-feedback";
import { BoardId } from "../device/board-id";
import Separate from "../common/Separate";
import { ConnectionMode, ConnectionStatus, WebUSBError } from "../device";
import { useConnectionStatus, useDevice } from "../device/device-hooks";
import { useFileSystem } from "../fs/fs-hooks";
import DownloadButton from "./DownloadButton";
import FlashButton from "./FlashButton";

class HexGenerationError extends Error {}

Expand All @@ -23,7 +22,6 @@ const DeviceConnection = () => {
const connectionStatus = useConnectionStatus();
const connected = connectionStatus === ConnectionStatus.CONNECTED;
const supported = connectionStatus !== ConnectionStatus.NOT_SUPPORTED;
const [progress, setProgress] = useState<undefined | number>(undefined);
const actionFeedback = useActionFeedback();
const device = useDevice();
const fs = useFileSystem();
Expand All @@ -38,67 +36,30 @@ const DeviceConnection = () => {
}
}
}, [device, connected]);

const handleFlash = useCallback(async () => {
const dataSource = async (boardId: BoardId) => {
try {
return await fs.toHexForFlash(boardId);
} catch (e) {
throw new HexGenerationError(e.message);
}
};

try {
await device.flash(dataSource, { partial: true, progress: setProgress });
} catch (e) {
if (e instanceof HexGenerationError) {
actionFeedback.expectedError({
title: "Failed to build the hex file",
description: e.message,
});
} else {
handleWebUSBError(actionFeedback, e);
}
}
}, [fs, device, actionFeedback]);

const buttonWidth = "10rem";
return (
<VStack
backgroundColor="var(--sidebar)"
padding={5}
spacing={2}
align="flex-start"
>
<HStack>
{supported ? (
<HStack as="label" spacing={3}>
<Switch
size="lg"
isChecked={connected}
onChange={handleToggleConnected}
/>
<HStack as="label" spacing={3} width="14rem">
<Switch isChecked={connected} onChange={handleToggleConnected} />
<Text as="span" size="lg" fontWeight="semibold">
{connected ? "micro:bit connected" : "micro:bit disconnected"}
</Text>
</HStack>
) : null}

<HStack justifyContent="space-between" width="100%">
{connected && (
<Button
leftIcon={<RiFlashlightFill />}
size="lg"
width="100%"
disabled={!fs || !connected || typeof progress !== "undefined"}
onClick={handleFlash}
>
{typeof progress === "undefined"
? "Flash"
: `Flashing… (${(progress * 100).toFixed(0)}%)`}
</Button>
<HStack>
{supported && (
<FlashButton
mode={connected ? "button" : "icon"}
buttonWidth={buttonWidth}
/>
)}
<DownloadButton size="lg" width="100%" />
<DownloadButton
mode={connected ? "icon" : "button"}
buttonWidth={buttonWidth}
/>
</HStack>
</VStack>
</HStack>
);
};

Expand Down
23 changes: 18 additions & 5 deletions src/workbench/DownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import {
BoxProps,
Button,
ButtonProps,
Icon,
IconButton,
} from "@chakra-ui/react";
import React, { useCallback } from "react";
import { RiDownload2Line } from "react-icons/ri";
import useActionFeedback from "../common/use-action-feedback";
import { useFileSystem } from "../fs/fs-hooks";
import CollapsableButton, { CollapsableButtonProps } from "./CollapsableButton";

interface DownloadButtonProps
extends Omit<CollapsableButtonProps, "onClick" | "text" | "icon"> {}

/**
* Download HEX button.
Expand All @@ -12,7 +22,7 @@ import { useFileSystem } from "../fs/fs-hooks";
*
* Otherwise it's a more minor action.
*/
const DownloadButton = (props: ButtonProps) => {
const DownloadButton = (props: DownloadButtonProps) => {
const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const handleDownload = useCallback(async () => {
Expand All @@ -33,9 +43,12 @@ const DownloadButton = (props: ButtonProps) => {
}, []);

return (
<Button leftIcon={<RiDownload2Line />} onClick={handleDownload} {...props}>
Download
</Button>
<CollapsableButton
{...props}
icon={<RiDownload2Line />}
onClick={handleDownload}
text="Download"
/>
);
};

Expand Down
3 changes: 2 additions & 1 deletion src/workbench/Files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useFileSystem, useFileSystemState } from "../fs/fs-hooks";
import { saveAs } from "file-saver";
import useActionFeedback from "../common/use-action-feedback";
import ProjectNameEditable from "./ProjectNameEditable";
import OpenButton from "./OpenButton";

interface FilesProps {
onSelectedFileChanged: (name: string) => void;
Expand All @@ -28,14 +29,14 @@ const Files = ({ onSelectedFileChanged }: FilesProps) => {
}
return (
<VStack alignItems="stretch" padding={2} spacing={5}>
<ProjectNameEditable />
<List>
{fs.files.map((f) => (
<ListItem key={f.name}>
<FileRow value={f} onClick={() => onSelectedFileChanged(f.name)} />
</ListItem>
))}
</List>
<OpenButton variant="outline" text="Open a project" />
</VStack>
);
};
Expand Down
80 changes: 80 additions & 0 deletions src/workbench/FlashButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useCallback, useState } from "react";
import { RiFlashlightFill } from "react-icons/ri";
import Separate from "../common/Separate";
import useActionFeedback, {
ActionFeedback,
} from "../common/use-action-feedback";
import { ConnectionStatus, WebUSBError } from "../device";
import { BoardId } from "../device/board-id";
import { useConnectionStatus, useDevice } from "../device/device-hooks";
import { useFileSystem } from "../fs/fs-hooks";
import CollapsableButton, { CollapsableButtonProps } from "./CollapsableButton";

class HexGenerationError extends Error {}

/**
* Flash button.
*/
const FlashButton = (
props: Omit<CollapsableButtonProps, "onClick" | "text" | "icon">
) => {
const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const device = useDevice();
const status = useConnectionStatus();
const connected = status === ConnectionStatus.CONNECTED;
const [progress, setProgress] = useState<number | undefined>();

const handleFlash = useCallback(async () => {
const dataSource = async (boardId: BoardId) => {
try {
return await fs.toHexForFlash(boardId);
} catch (e) {
throw new HexGenerationError(e.message);
}
};

try {
await device.flash(dataSource, { partial: true, progress: setProgress });
} catch (e) {
if (e instanceof HexGenerationError) {
actionFeedback.expectedError({
title: "Failed to build the hex file",
description: e.message,
});
} else {
handleWebUSBError(actionFeedback, e);
}
}
}, [fs, device, actionFeedback]);
const text =
typeof progress === "undefined"
? "Flash"
: `Flashing… (${(progress * 100).toFixed(0)}%)`;
return (
<CollapsableButton
{...props}
disabled={!fs || !connected || typeof progress !== "undefined"}
icon={<RiFlashlightFill />}
onClick={handleFlash}
text={text}
/>
);
};

const handleWebUSBError = (actionFeedback: ActionFeedback, e: any) => {
if (e instanceof WebUSBError) {
actionFeedback.expectedError({
title: e.title,
description: (
<Separate separator={(k) => <br key={k} />}>
{[e.message, e.description].filter(Boolean)}
</Separate>
),
});
} else {
actionFeedback.unexpectedError(e);
}
};

export default FlashButton;
3 changes: 2 additions & 1 deletion src/workbench/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Flex, HStack, VStack } from "@chakra-ui/react";
import React from "react";
import DeviceConnection from "./DeviceConnection";
import HelpMenu from "./HelpMenu";
import Logo from "./Logo";
import OpenButton from "./OpenButton";
Expand Down Expand Up @@ -27,7 +28,7 @@ const Header = () => {
</HStack>
</HStack>
<HStack spacing={3} as="nav">
<OpenButton size={size}>Open</OpenButton>
<DeviceConnection />
<HelpMenu size={size} />
</HStack>
</Flex>
Expand Down
1 change: 0 additions & 1 deletion src/workbench/LeftPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ const LeftPanelContents = ({ panes }: LeftPanelConentsProps) => {
))}
</TabPanels>
</Tabs>
<DeviceConnection />
</Flex>
);
};
Expand Down
8 changes: 6 additions & 2 deletions src/workbench/OpenButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { RiFolderOpenLine } from "react-icons/ri";
import useActionFeedback from "../common/use-action-feedback";
import { useFileSystem } from "../fs/fs-hooks";

interface OpenButtonProps extends ButtonProps {
text?: string;
}

/**
* Open HEX button, with an associated input field.
*/
const OpenButton = (props: ButtonProps) => {
const OpenButton = ({ text = "Open", ...props }: OpenButtonProps) => {
const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const ref = useRef<HTMLInputElement>(null);
Expand Down Expand Up @@ -51,7 +55,7 @@ const OpenButton = (props: ButtonProps) => {
onClick={handleChooseFile}
{...props}
>
Open
{text}
</Button>
</>
);
Expand Down
32 changes: 19 additions & 13 deletions src/workbench/Workbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,28 @@ const Workbench = () => {
</LeftResizable>
<Fill>
<Fill>
<EditorContainer
key={filename}
filename={filename}
onSelectedFileChanged={setFilename}
/>
<Fill>
<EditorContainer
key={filename}
filename={filename}
onSelectedFileChanged={setFilename}
/>
</Fill>
<RightResizable
size="30%"
style={{ borderLeft: "4px solid whitesmoke" }}
>
<Simulator />
</RightResizable>
</Fill>
<BottomResizable size="20%">
<Placeholder bgColor="blackAlpha.900" color="white" text="Serial" />
<BottomResizable size="25%">
<Placeholder
bgColor="blackAlpha.900"
color="white"
text="Serial here, showing errors from your micro:bit when you run the code"
/>
</BottomResizable>
</Fill>
<RightResizable
size="20%"
style={{ borderLeft: "4px solid whitesmoke" }}
>
<Simulator />
</RightResizable>
</Fill>
</ViewPort>
);
Expand Down