Skip to content
Merged
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
File renamed without changes
43 changes: 19 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ import * as Antd from 'antd';
import '@ant-design/v5-patch-for-react-19';

import * as Blockly from 'blockly/core';
import {pythonGenerator} from 'blockly/python';
import { pythonGenerator } from 'blockly/python';

import Header from './reactComponents/Header';
import * as Menu from './reactComponents/Menu';
import CodeDisplay from './reactComponents/CodeDisplay';
import BlocklyComponent, {BlocklyComponentType} from './reactComponents/BlocklyComponent';
import BlocklyComponent, { BlocklyComponentType } from './reactComponents/BlocklyComponent';
import ToolboxSettingsModal from './reactComponents/ToolboxSettings';
import * as Tabs from './reactComponents/Tabs';
import {TabType } from './types/TabType';
import { TabType } from './types/TabType';

import {createGeneratorContext, GeneratorContext} from './editor/generator_context';
import { createGeneratorContext, GeneratorContext } from './editor/generator_context';
import * as editor from './editor/editor';
import {extendedPythonGenerator} from './editor/extended_python_generator';
import { extendedPythonGenerator } from './editor/extended_python_generator';

import * as commonStorage from './storage/common_storage';
import * as clientSideStorage from './storage/client_side_storage';
Expand All @@ -46,6 +46,7 @@ import { initialize as initializePythonBlocks } from './blocks/utils/python';
import * as ChangeFramework from './blocks/utils/change_framework'
import { mutatorOpenListener } from './blocks/mrc_param_container'
import { TOOLBOX_UPDATE_EVENT } from './blocks/mrc_mechanism_component_holder';
import { antdThemeFromString } from './reactComponents/ThemeModal';

/** Storage key for shown toolbox categories. */
const SHOWN_TOOLBOX_CATEGORIES_KEY = 'shownPythonToolboxCategories';
Expand Down Expand Up @@ -98,6 +99,7 @@ const App: React.FC = (): React.JSX.Element => {
const [triggerPythonRegeneration, setTriggerPythonRegeneration] = React.useState(0);
const [leftCollapsed, setLeftCollapsed] = React.useState(false);
const [rightCollapsed, setRightCollapsed] = React.useState(false);
const [theme, setTheme] = React.useState('dark');


const blocksEditor = React.useRef<editor.Editor | null>(null);
Expand Down Expand Up @@ -264,7 +266,7 @@ const App: React.FC = (): React.JSX.Element => {
// Add event listener for toolbox updates
React.useEffect(() => {
window.addEventListener(TOOLBOX_UPDATE_EVENT, handleToolboxUpdateRequest);

return () => {
window.removeEventListener(TOOLBOX_UPDATE_EVENT, handleToolboxUpdateRequest);
};
Expand Down Expand Up @@ -312,8 +314,8 @@ const App: React.FC = (): React.JSX.Element => {
if (currentModule && blocklyComponent.current && generatorContext.current) {
const blocklyWorkspace = blocklyComponent.current.getBlocklyWorkspace();
setGeneratedCode(extendedPythonGenerator.mrcWorkspaceToCode(
blocklyWorkspace,
generatorContext.current
blocklyWorkspace,
generatorContext.current
));
} else {
setGeneratedCode('');
Expand All @@ -336,26 +338,14 @@ const App: React.FC = (): React.JSX.Element => {
}
}, [project]);

const {Sider,Content} = Antd.Layout;
const { Sider, Content } = Antd.Layout;

return (
<Antd.ConfigProvider
theme={{
algorithm: Antd.theme.darkAlgorithm,
components: {
Layout: {
headerBg: '#000000', // This is only for dark mode
siderBg: '#000000', // This is only for dark mode
triggerBg: '#000000', // This is only for dark mode
},
Menu:{
darkItemBg: '#000000', // This is only for dark mode
}
},
}}
theme={antdThemeFromString(theme)}
>
{contextHolder}
<Antd.Layout style={{height: FULL_VIEWPORT_HEIGHT}}>
<Antd.Layout style={{ height: FULL_VIEWPORT_HEIGHT }}>
<Header
alertErrorMessage={alertErrorMessage}
setAlertErrorMessage={setAlertErrorMessage}
Expand All @@ -379,6 +369,7 @@ const App: React.FC = (): React.JSX.Element => {
project={project}
setProject={setProject}
openWPIToolboxSettings={() => setToolboxSettingsModalIsOpen(true)}
setTheme={setTheme}
/>
</Sider>
<Antd.Layout>
Expand All @@ -395,7 +386,10 @@ const App: React.FC = (): React.JSX.Element => {
/>
<Antd.Layout>
<Content>
<BlocklyComponent ref={blocklyComponent} />
<BlocklyComponent
theme={theme}
ref={blocklyComponent}
/>
</Content>
<Sider
collapsible
Expand All @@ -409,6 +403,7 @@ const App: React.FC = (): React.JSX.Element => {
generatedCode={generatedCode}
messageApi={messageApi}
setAlertErrorMessage={setAlertErrorMessage}
theme={theme}
/>
</Sider>
</Antd.Layout>
Expand Down
33 changes: 29 additions & 4 deletions src/reactComponents/BlocklyComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
import * as React from 'react';
import * as Blockly from 'blockly/core';
import * as locale from 'blockly/msg/en';
import * as MrcTheme from '../themes/mrc_theme_dark';
import * as MrcDarkTheme from '../themes/mrc_theme_dark';
import * as MrcLightTheme from '../themes/mrc_theme_light';
import {pluginInfo as HardwareConnectionsPluginInfo} from '../blocks/utils/connection_checker';

import 'blockly/blocks'; // Includes standard blocks like controls_if, logic_compare, etc.
Expand All @@ -31,6 +32,11 @@ export interface BlocklyComponentType {
getBlocklyWorkspace: () => Blockly.WorkspaceSvg;
}

/** Interface for props passed to the BlocklyComponent. */
export interface BlocklyComponentProps {
theme: string;
}

/** Grid spacing for the Blockly workspace. */
const GRID_SPACING = 20;

Expand Down Expand Up @@ -68,14 +74,25 @@ const WORKSPACE_STYLE: React.CSSProperties = {
* React component that renders a Blockly workspace with proper initialization,
* cleanup, and resize handling.
*/
const BlocklyComponent = React.forwardRef<BlocklyComponentType | null>(
(_, ref): React.JSX.Element => {
const BlocklyComponent = React.forwardRef<BlocklyComponentType | null, BlocklyComponentProps>(
({ theme }, ref): React.JSX.Element => {
const blocklyDiv = React.useRef<HTMLDivElement | null>(null);
const workspaceRef = React.useRef<Blockly.WorkspaceSvg | null>(null);

const getBlocklyTheme = (): Blockly.Theme => {
if (theme === 'dark' || theme === 'compact-dark') {
return MrcDarkTheme.theme;
}
if (theme === 'light' || theme === 'compact') {
return MrcLightTheme.theme;
}
// Default to light theme if unknown
return MrcLightTheme.theme;
};

/** Creates the Blockly workspace configuration object. */
const createWorkspaceConfig = (): Blockly.BlocklyOptions => ({
theme: MrcTheme.theme,
theme: getBlocklyTheme(),
horizontalLayout: false, // Forces vertical layout for the workspace
// Start with an empty (but not null) toolbox. It will be replaced later.
toolbox: {
Expand Down Expand Up @@ -168,6 +185,14 @@ const BlocklyComponent = React.forwardRef<BlocklyComponentType | null>(
return cleanupWorkspace;
}, []);

// Update theme when theme prop changes
React.useEffect(() => {
if (workspaceRef.current) {
const newTheme = getBlocklyTheme();
workspaceRef.current.setTheme(newTheme);
}
}, [theme]);

// Handle workspace resize
React.useEffect(() => {
return setupResizeObserver();
Expand Down
59 changes: 33 additions & 26 deletions src/reactComponents/CodeDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,37 +20,29 @@
*/
import * as Antd from 'antd';
import * as React from 'react';
import {CopyOutlined as CopyIcon} from '@ant-design/icons';
import {Prism as SyntaxHighlighter} from 'react-syntax-highlighter';
import {dracula} from 'react-syntax-highlighter/dist/esm/styles/prism';
import { CopyOutlined as CopyIcon } from '@ant-design/icons';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';

import type {MessageInstance} from 'antd/es/message/interface';
import type { MessageInstance } from 'antd/es/message/interface';

/** Function type for setting string values. */
type StringFunction = (input: string) => void;

/** Props for the CodeDisplay component. */
interface CodeDisplayProps {
generatedCode: string;
theme: string;
messageApi: MessageInstance;
setAlertErrorMessage: StringFunction;
}

/** Background color for the syntax highlighter. */
const SYNTAX_HIGHLIGHTER_BACKGROUND = '#333';

/** Full dimensions style for the syntax highlighter. */
const FULL_SIZE_STYLE = {
width: '100%',
height: '100%',
};

/** Header text color. */
const HEADER_TEXT_COLOR = '#fff';

/** Button text color. */
const BUTTON_TEXT_COLOR = 'white';

/** Success message for copy operation. */
const COPY_SUCCESS_MESSAGE = 'Copy completed successfully.';

Expand All @@ -62,37 +54,52 @@ const COPY_ERROR_MESSAGE_PREFIX = 'Could not copy code: ';
* Shows generated Python code in a dark theme with a copy button.
*/
export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element {
const syntaxHighligherFromTheme = (theme: string) => {
switch (theme) {
case 'dark':
case 'compact-dark':
return dracula;
case 'light':
case 'compact':
return oneLight;
default:
return dracula; // Default to dracula if theme is unknown
}
}

const { token } = Antd.theme.useToken();
const syntaxStyle = syntaxHighligherFromTheme(props.theme);

/** Handles copying the generated code to clipboard. */
const handleCopyCode = (): void => {
navigator.clipboard.writeText(props.generatedCode).then(
() => {
props.messageApi.open({
type: 'success',
content: COPY_SUCCESS_MESSAGE,
});
},
(err) => {
props.setAlertErrorMessage(COPY_ERROR_MESSAGE_PREFIX + err);
}
() => {
props.messageApi.open({
type: 'success',
content: COPY_SUCCESS_MESSAGE,
});
},
(err) => {
props.setAlertErrorMessage(COPY_ERROR_MESSAGE_PREFIX + err);
}
);
};

/** Creates the custom style object for the syntax highlighter. */
const getSyntaxHighlighterStyle = (): React.CSSProperties => ({
backgroundColor: SYNTAX_HIGHLIGHTER_BACKGROUND,
backgroundColor: token.colorBgContainer,
...FULL_SIZE_STYLE,
});

/** Renders the header section with title and copy button. */
const renderHeader = (): React.JSX.Element => (
<Antd.Space>
<h3 style={{color: HEADER_TEXT_COLOR, margin: 0}}>Code</h3>
<Antd.Typography.Title level={3}>Code</Antd.Typography.Title>
<Antd.Tooltip title="Copy">
<Antd.Button
icon={<CopyIcon />}
size="small"
onClick={handleCopyCode}
style={{color: BUTTON_TEXT_COLOR}}
/>
</Antd.Tooltip>
</Antd.Space>
Expand All @@ -102,7 +109,7 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element
const renderCodeBlock = (): React.JSX.Element => (
<SyntaxHighlighter
language="python"
style={dracula}
style={syntaxStyle}
customStyle={getSyntaxHighlighterStyle()}
>
{props.generatedCode}
Expand Down
10 changes: 6 additions & 4 deletions src/reactComponents/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const TEXT_PADDING_LEFT = 20;
* and any error messages.
*/
export default function Header(props: HeaderProps): React.JSX.Element {
const { token } = Antd.theme.useToken();

const isDarkTheme = token.colorBgLayout === '#000000';

/** Handles clearing the error message. */
const handleClearError = (): void => {
props.setAlertErrorMessage('');
Expand Down Expand Up @@ -76,17 +80,16 @@ export default function Header(props: HeaderProps): React.JSX.Element {
};

return (
<Antd.Flex vertical style={{background: '#000'}}>
<Antd.Flex vertical>
<Antd.Flex style={{alignItems: 'center'}}>
<img
height={LOGO_HEIGHT}
style={{objectFit: 'contain'}}
src="/FIRST_HorzRGB_reverse.png"
src={isDarkTheme ? "/FIRST_HorzRGB_reverse.png" : "/FIRST_HorzRGB.png"}
alt="FIRST Logo"
/>
<Antd.Typography
style={{
color: 'white',
paddingLeft: TEXT_PADDING_LEFT,
fontSize: TEXT_FONT_SIZE,
fontWeight: TITLE_FONT_WEIGHT,
Expand All @@ -96,7 +99,6 @@ export default function Header(props: HeaderProps): React.JSX.Element {
</Antd.Typography>
<Antd.Typography
style={{
color: 'white',
paddingLeft: TEXT_PADDING_LEFT,
fontSize: TEXT_FONT_SIZE,
fontWeight: 'normal',
Expand Down
Loading