Skip to content

Commit ef45836

Browse files
committed
Merge branch 'main' of github.com:wpilibsuite/systemcore-blocks-interface into pr_mrc_port
2 parents 28bbe5b + f002bce commit ef45836

File tree

9 files changed

+451
-56
lines changed

9 files changed

+451
-56
lines changed

src/App.tsx

Lines changed: 247 additions & 51 deletions
Large diffs are not rendered by default.

src/blocks/mrc_class_method_def.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { MUTATOR_BLOCK_NAME, PARAM_CONTAINER_BLOCK_NAME, MethodMutatorArgBlock }
3838
export const BLOCK_NAME = 'mrc_class_method_def';
3939

4040
export const FIELD_METHOD_NAME = 'NAME';
41+
export const RETURN_VALUE = 'RETURN';
4142

4243
type Parameter = {
4344
name: string,
@@ -54,6 +55,7 @@ interface ClassMethodDefMixin extends ClassMethodDefMixinType {
5455
mrcParameters: Parameter[],
5556
mrcPythonMethodName: string,
5657
mrcFuncName: string | null,
58+
mrcUpdateReturnInput(): void,
5759
}
5860
type ClassMethodDefMixinType = typeof CLASS_METHOD_DEF;
5961

@@ -179,6 +181,7 @@ const CLASS_METHOD_DEF = {
179181
(this as Blockly.BlockSvg).setMutator(null);
180182
}
181183
this.mrcUpdateParams();
184+
this.mrcUpdateReturnInput();
182185
},
183186
compose: function (this: ClassMethodDefBlock, containerBlock: any) {
184187
// Parameter list.
@@ -250,6 +253,21 @@ const CLASS_METHOD_DEF = {
250253
}
251254
}
252255
},
256+
mrcUpdateReturnInput: function (this: ClassMethodDefBlock) {
257+
// Remove existing return input if it exists
258+
if (this.getInput(RETURN_VALUE)) {
259+
this.removeInput(RETURN_VALUE);
260+
}
261+
262+
// Add return input if return type is not 'None'
263+
if (this.mrcReturnType && this.mrcReturnType !== 'None') {
264+
this.appendValueInput(RETURN_VALUE)
265+
.setAlign(Blockly.inputs.Align.RIGHT)
266+
.appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
267+
// Move the return input to be after the statement input
268+
this.moveInputBefore('STACK', RETURN_VALUE);
269+
}
270+
},
253271
removeParameterFields: function (input: Blockly.Input) {
254272
const fieldsToRemove = input.fieldRow
255273
.filter(field => field.name?.startsWith('PARAM_'))
@@ -502,6 +520,23 @@ export function createCustomMethodBlock(): toolboxItems.Block {
502520
return new toolboxItems.Block(BLOCK_NAME, extraState, fields, null);
503521
}
504522

523+
export function createCustomMethodBlockWithReturn(): toolboxItems.Block {
524+
const extraState: ClassMethodDefExtraState = {
525+
canChangeSignature: true,
526+
canBeCalledWithinClass: true,
527+
canBeCalledOutsideClass: true,
528+
returnType: 'Any',
529+
params: [],
530+
};
531+
const fields: {[key: string]: any} = {};
532+
fields[FIELD_METHOD_NAME] = 'my_method_with_return';
533+
const inputs: {[key: string]: any} = {};
534+
inputs[RETURN_VALUE] = {
535+
'type': 'input_value',
536+
};
537+
return new toolboxItems.Block(BLOCK_NAME, extraState, fields, inputs);
538+
}
539+
505540
export function getBaseClassBlocks(
506541
baseClassName: string): toolboxItems.Block[] {
507542
const blocks: toolboxItems.Block[] = [];

src/i18n/locales/en/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"BLOCKS": "Blocks",
4444
"CODE": "Code",
4545
"COPY": "Copy",
46+
"COLLAPSE": "Collapse",
47+
"EXPAND": "Expand",
4648
"FAILED_TO_RENAME_PROJECT": "Failed to rename project",
4749
"FAILED_TO_COPY_PROJECT": "Failed to copy project",
4850
"FAILED_TO_CREATE_PROJECT": "Failed to create a new project.",

src/i18n/locales/es/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
"BLOCKS": "Bloques",
4141
"CODE": "Código",
4242
"COPY": "Copiar",
43+
"COLLAPSE": "Colapsar",
44+
"EXPAND": "Expandir",
4345
"FAILED_TO_RENAME_PROJECT": "Error al renombrar proyecto",
4446
"FAILED_TO_COPY_PROJECT": "Error al copiar proyecto",
4547
"FAILED_TO_CREATE_PROJECT": "Error al crear un nuevo proyecto.",

src/i18n/locales/he/translation.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
"BLOCKS": "בלוקים",
4444
"CODE": "קוד",
4545
"COPY": "העתק",
46+
"COLLAPSE": "כווץ",
47+
"EXPAND": "הרחב",
4648
"FAILED_TO_RENAME_PROJECT": "נכשל בשינוי שם הפרויקט",
4749
"FAILED_TO_COPY_PROJECT": "נכשל בהעתקת הפרויקט",
4850
"FAILED_TO_CREATE_PROJECT": "נכשל ביצירת פרויקט חדש.",

src/reactComponents/CodeDisplay.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import * as Antd from 'antd';
2222
import * as React from 'react';
2323
import { CopyOutlined as CopyIcon } from '@ant-design/icons';
2424
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
25+
import SiderCollapseTrigger from './SiderCollapseTrigger';
2526
import { dracula, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism';
2627

2728
import type { MessageInstance } from 'antd/es/message/interface';
@@ -36,6 +37,8 @@ interface CodeDisplayProps {
3637
theme: string;
3738
messageApi: MessageInstance;
3839
setAlertErrorMessage: StringFunction;
40+
isCollapsed?: boolean;
41+
onToggleCollapse?: () => void;
3942
}
4043

4144
/** Success message for copy operation. */
@@ -110,10 +113,26 @@ export default function CodeDisplay(props: CodeDisplayProps): React.JSX.Element
110113
</SyntaxHighlighter>
111114
);
112115

116+
/** Renders the collapse/expand trigger at the bottom center of the panel. */
117+
const renderCollapseTrigger = (): React.JSX.Element | null => {
118+
if (!props.onToggleCollapse) return null;
119+
120+
return (
121+
<SiderCollapseTrigger
122+
collapsed={props.isCollapsed || false}
123+
onToggle={props.onToggleCollapse}
124+
isRightPanel={true}
125+
/>
126+
);
127+
};
128+
113129
return (
114-
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
115-
{renderHeader()}
116-
{renderCodeBlock()}
117-
</Antd.Flex>
130+
<div style={{ height: '100%', position: 'relative' }}>
131+
<Antd.Flex vertical gap="small" style={{ height: '100%' }}>
132+
{renderHeader()}
133+
{renderCodeBlock()}
134+
</Antd.Flex>
135+
{renderCollapseTrigger()}
136+
</div>
118137
);
119138
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Porpoiseful LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
/**
19+
* @author [email protected] (Alan Smith)
20+
*/
21+
import * as React from 'react';
22+
import * as Antd from 'antd';
23+
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
24+
import { useTranslation } from 'react-i18next';
25+
26+
/** Props for the SiderCollapseTrigger component. */
27+
interface SiderCollapseTriggerProps {
28+
collapsed: boolean;
29+
onToggle: () => void;
30+
isRightPanel?: boolean;
31+
}
32+
33+
/**
34+
* Custom collapse trigger for Sider that matches the right panel's appearance.
35+
*/
36+
export default function SiderCollapseTrigger(props: SiderCollapseTriggerProps): React.JSX.Element {
37+
const { token } = Antd.theme.useToken();
38+
const { t } = useTranslation();
39+
const [isHovered, setIsHovered] = React.useState(false);
40+
41+
return (
42+
<div
43+
style={{
44+
position: 'absolute',
45+
bottom: 0,
46+
left: '50%',
47+
transform: 'translateX(-50%)',
48+
backgroundColor: isHovered ? token.colorBgTextHover : token.colorBgContainer,
49+
border: `1px solid ${token.colorBorder}`,
50+
borderBottom: 'none',
51+
borderRadius: '6px 6px 0 0',
52+
padding: '2px 6px',
53+
cursor: 'pointer',
54+
display: 'flex',
55+
alignItems: 'center',
56+
justifyContent: 'center',
57+
minWidth: '24px',
58+
height: '22px',
59+
color: isHovered ? token.colorText : token.colorTextSecondary,
60+
transition: 'all 0.2s',
61+
zIndex: 1,
62+
}}
63+
onClick={props.onToggle}
64+
onMouseEnter={() => setIsHovered(true)}
65+
onMouseLeave={() => setIsHovered(false)}
66+
>
67+
<Antd.Tooltip title={props.collapsed ? t("EXPAND") : t("COLLAPSE")}>
68+
{props.isRightPanel ? (
69+
// Right panel: reversed arrows
70+
props.collapsed ?
71+
<LeftOutlined style={{ fontSize: '12px', color: 'inherit' }} /> :
72+
<RightOutlined style={{ fontSize: '12px', color: 'inherit' }} />
73+
) : (
74+
// Left panel: normal arrows
75+
props.collapsed ?
76+
<RightOutlined style={{ fontSize: '12px', color: 'inherit' }} /> :
77+
<LeftOutlined style={{ fontSize: '12px', color: 'inherit' }} />
78+
)}
79+
</Antd.Tooltip>
80+
</div>
81+
);
82+
}

src/reactComponents/UserSettingsProvider.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ const USER_THEME_KEY = 'userTheme';
3131
const DEFAULT_LANGUAGE = 'en';
3232
const DEFAULT_THEME = 'dark';
3333

34+
/** Helper function to generate project-specific storage key for open tabs. */
35+
const getUserOptionsKey = (projectName: string): string => `user_options_${projectName}`;
36+
3437
/** User settings interface. */
3538
export interface UserSettings {
3639
language: string;
@@ -42,6 +45,8 @@ export interface UserSettingsContextType {
4245
settings: UserSettings;
4346
updateLanguage: (language: string) => Promise<void>;
4447
updateTheme: (theme: string) => Promise<void>;
48+
updateOpenTabs: (projectName: string, tabPaths: string[]) => Promise<void>;
49+
getOpenTabs: (projectName: string) => Promise<string[]>;
4550
isLoading: boolean;
4651
error: string | null;
4752
storage: Storage | null;
@@ -134,10 +139,52 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({
134139
}
135140
};
136141

142+
/** Update open tabs for a specific project. */
143+
const updateOpenTabs = async (projectName: string, tabPaths: string[]): Promise<void> => {
144+
try {
145+
setError(null);
146+
147+
if (storage) {
148+
const storageKey = getUserOptionsKey(projectName);
149+
await storage.saveEntry(storageKey, JSON.stringify(tabPaths));
150+
} else {
151+
console.warn('No storage available, cannot save open tabs');
152+
}
153+
} catch (err) {
154+
setError(`Failed to save open tabs: ${err}`);
155+
console.error('Error saving open tabs:', err);
156+
throw err;
157+
}
158+
};
159+
160+
/** Get open tabs for a specific project. */
161+
const getOpenTabs = async (projectName: string): Promise<string[]> => {
162+
try {
163+
if (!storage) {
164+
return [];
165+
}
166+
167+
const storageKey = getUserOptionsKey(projectName);
168+
const tabsJson = await storage.fetchEntry(storageKey, JSON.stringify([]));
169+
170+
try {
171+
return JSON.parse(tabsJson);
172+
} catch (error) {
173+
console.warn(`Failed to parse open tabs for project ${projectName}, using default:`, error);
174+
return [];
175+
}
176+
} catch (err) {
177+
console.error(`Error loading open tabs for project ${projectName}:`, err);
178+
return [];
179+
}
180+
};
181+
137182
const contextValue: UserSettingsContextType = {
138183
settings,
139184
updateLanguage,
140185
updateTheme,
186+
updateOpenTabs,
187+
getOpenTabs,
141188
isLoading,
142189
error,
143190
storage: storage || null,
@@ -149,3 +196,12 @@ export const UserSettingsProvider: React.FC<UserSettingsProviderProps> = ({
149196
</UserSettingsContext.Provider>
150197
);
151198
};
199+
200+
/** Custom hook to use user settings context. */
201+
export const useUserSettings = (): UserSettingsContextType => {
202+
const context = React.useContext(UserSettingsContext);
203+
if (!context) {
204+
throw new Error('useUserSettings must be used within a UserSettingsProvider');
205+
}
206+
return context;
207+
};

src/toolbox/methods_category.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import * as storageModule from '../storage/module';
2626
import { MRC_CATEGORY_STYLE_METHODS } from '../themes/styles';
2727
import { CLASS_NAME_ROBOT_BASE, CLASS_NAME_OPMODE, CLASS_NAME_MECHANISM } from '../blocks/utils/python';
2828
import { addInstanceWithinBlocks } from '../blocks/mrc_call_python_function';
29-
import { createCustomMethodBlock, getBaseClassBlocks, FIELD_METHOD_NAME } from '../blocks/mrc_class_method_def';
29+
import { createCustomMethodBlock, getBaseClassBlocks, FIELD_METHOD_NAME, createCustomMethodBlockWithReturn } from '../blocks/mrc_class_method_def';
3030
import { Editor } from '../editor/editor';
3131

3232

@@ -113,6 +113,7 @@ class MethodsCategory {
113113
text: 'Custom Methods',
114114
},
115115
createCustomMethodBlock(),
116+
createCustomMethodBlockWithReturn()
116117
);
117118

118119
// Get blocks for calling methods defined in the current workspace.

0 commit comments

Comments
 (0)