diff --git a/src/ui/widgets/EmbeddedDisplay/bobParser.ts b/src/ui/widgets/EmbeddedDisplay/bobParser.ts index 7e743560..53fa61e5 100644 --- a/src/ui/widgets/EmbeddedDisplay/bobParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/bobParser.ts @@ -51,7 +51,8 @@ const BOB_WIDGET_MAPPING: { [key: string]: any } = { progressbar: "progressbar", rectangle: "shape", choice: "choicebutton", - scaledslider: "slidecontrol" + scaledslider: "slidecontrol", + symbol: "symbol" }; // Default width and height of widgets in Phoebus @@ -75,7 +76,8 @@ export const WIDGET_DEFAULT_SIZES: { [key: string]: [number, number] } = { polyline: [100, 20], progressbar: [100, 20], rectangle: [100, 20], - scaledslider: [400, 55] + scaledslider: [400, 55], + symbol: [100, 100] }; function bobParseType(props: any): string { @@ -206,6 +208,16 @@ function bobParseResizing(jsonProp: ElementCompact): string { } } +function bobParseSymbols(jsonProp: ElementCompact): string[] { + const symbols: string[] = []; + Object.values(jsonProp["symbol"]).forEach((item: any) => { + // For a single symbol, we are passed a string. For multiple symbols + // we are passed an object, so we need to return string from it + symbols.push(typeof item === "string" ? item : item._text); + }); + return symbols; +} + function bobGetTargetWidget(props: any): React.FC { const typeid = bobParseType(props); let targetWidget; @@ -257,7 +269,12 @@ export function parseBob( squareLed: ["square", opiParseBoolean], formatType: ["format", bobParseFormatType], stretchToFit: ["stretch_image", opiParseBoolean], - macros: ["macros", opiParseMacros] + macros: ["macros", opiParseMacros], + symbols: ["symbols", bobParseSymbols], + initialIndex: ["initial_index", bobParseNumber], + showIndex: ["show_index", opiParseBoolean], + fallbackSymbol: ["fallback_symbol", opiParseString], + rotation: ["rotation", bobParseNumber] }; const complexParsers = { diff --git a/src/ui/widgets/Image/image.tsx b/src/ui/widgets/Image/image.tsx index 01a06c42..1193df57 100644 --- a/src/ui/widgets/Image/image.tsx +++ b/src/ui/widgets/Image/image.tsx @@ -23,7 +23,8 @@ const ImageProps = { rotation: FloatPropOpt, flipHorizontal: BoolPropOpt, flipVertical: BoolPropOpt, - onClick: FuncPropOpt + onClick: FuncPropOpt, + overflow: BoolPropOpt }; export const ImageComponent = ( @@ -39,7 +40,7 @@ export const ImageComponent = ( let imageHeight: string | undefined = undefined; let imageWidth: string | undefined = undefined; - const overflow = "hidden"; + const overflow = props.overflow ? "visible" : "hidden"; if (props.stretchToFit) { imageWidth = "100%"; imageHeight = "100%"; diff --git a/src/ui/widgets/Symbol/__snapshots__/symbol.test.tsx.snap b/src/ui/widgets/Symbol/__snapshots__/symbol.test.tsx.snap index 91dd8e17..10b4f8e6 100644 --- a/src/ui/widgets/Symbol/__snapshots__/symbol.test.tsx.snap +++ b/src/ui/widgets/Symbol/__snapshots__/symbol.test.tsx.snap @@ -1,17 +1,83 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[` > matches snapshot 1`] = ` +exports[` from .bob file > matches snapshot (using fallback symbol) 1`] = `
+ +
+
+`; + +exports[` from .bob file > matches snapshot (with index) 1`] = ` + +
+ +
+
+`; + +exports[` from .bob file > matches snapshot (with rotation) 1`] = ` + +
+ +
+
+`; + +exports[` from .bob file > matches snapshot (without index) 1`] = ` + +
+ +
+
+`; + +exports[` from .opi file > matches snapshot (with rotation) 1`] = ` + +
+ +
+
+`; + +exports[` from .opi file > matches snapshot 1`] = ` + +
", (): void => { +describe(" from .opi file", (): void => { test("label is not shown if showLabel is false", (): void => { const symbolProps = { showBooleanLabel: false, @@ -40,4 +42,120 @@ describe("", (): void => { expect(asFragment()).toMatchSnapshot(); }); + + test("matches snapshot (with rotation)", (): void => { + const symbolProps = { + showBooleanLabel: false, + imageFile: "img 1.gif", + value: fakeValue, + rotation: 45 + }; + + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); +}); + +describe(" from .bob file", (): void => { + test("index is not shown if showIndex is false", (): void => { + const symbolProps = { + symbols: ["img 1.gif"], + value: new DType({ stringValue: "0" }) + }; + + render(); + + expect(screen.queryByText("0")).not.toBeInTheDocument(); + }); + + test("index is added", (): void => { + const symbolProps = { + showIndex: true, + symbols: ["img 1.gif", "img 2.png"], + value: stringValue + }; + render(); + + expect(screen.getByText("1")).toBeInTheDocument(); + }); + + test("use initialIndex if no props value provided", (): void => { + const symbolProps = { + showIndex: true, + initialIndex: 2, + symbols: ["img 1.gif", "img 2.png", "img 3.svg"], + value: undefined + }; + + render(); + + expect(screen.getByText("2")).toBeInTheDocument(); + }); + + test("use arrayIndex to find index if value is an array", (): void => { + const symbolProps = { + arrayIndex: 0, + showIndex: true, + symbols: ["img 1.gif", "img 2.png", "img 3.svg"], + value: arrayValue + }; + render(); + + expect(screen.getByText("2")).toBeInTheDocument(); + }); + + test("matches snapshot (without index)", (): void => { + const symbolProps = { + symbols: ["img 1.gif"], + value: new DType({ stringValue: "0" }) + }; + + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + test("matches snapshot (with index)", (): void => { + const symbolProps = { + symbols: ["img 1.gif", "img 2.png", "img 3.svg"], + value: new DType({ stringValue: "2" }) + }; + + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + test("matches snapshot (using fallback symbol)", (): void => { + const symbolProps = { + symbols: ["img 1.gif"], + value: new DType({ doubleValue: 1 }) + }; + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); + + test("matches snapshot (with rotation)", (): void => { + const symbolProps = { + symbols: ["img 1.gif"], + value: new DType({ stringValue: "0" }), + rotation: 45 + }; + + const { asFragment } = render( + + ); + + expect(asFragment()).toMatchSnapshot(); + }); }); diff --git a/src/ui/widgets/Symbol/symbol.tsx b/src/ui/widgets/Symbol/symbol.tsx index 403b21de..ef94c242 100644 --- a/src/ui/widgets/Symbol/symbol.tsx +++ b/src/ui/widgets/Symbol/symbol.tsx @@ -9,10 +9,10 @@ import { ColorPropOpt, FloatPropOpt, BorderPropOpt, - StringProp, ChoicePropOpt, FontPropOpt, - ActionsPropType + ActionsPropType, + StringArrayPropOpt } from "../propTypes"; import { registerWidget } from "../register"; import { ImageComponent } from "../Image/image"; @@ -22,9 +22,11 @@ import { executeActions, WidgetActions } from "../widgetActions"; import { MacroContext } from "../../../types/macros"; import { ExitFileContext, FileContext } from "../../../misc/fileContext"; import { DType } from "../../../types/dtypes"; +import classes from "./symbol.module.css"; const SymbolProps = { - imageFile: StringProp, + imageFile: StringPropOpt, + symbols: StringArrayPropOpt, alt: StringPropOpt, backgroundColor: ColorPropOpt, showBooleanLabel: BoolPropOpt, @@ -46,7 +48,13 @@ const SymbolProps = { visible: BoolPropOpt, stretchToFit: BoolPropOpt, actions: ActionsPropType, - font: FontPropOpt + font: FontPropOpt, + initialIndex: FloatPropOpt, + showIndex: BoolPropOpt, + arrayIndex: FloatPropOpt, + enabled: BoolPropOpt, + fallbackSymbol: StringPropOpt, + transparent: BoolPropOpt }; export type SymbolComponentProps = InferWidgetProps & @@ -58,13 +66,31 @@ export type SymbolComponentProps = InferWidgetProps & * @param props */ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => { + const { + showIndex = false, + arrayIndex = 0, + initialIndex = 0, + fallbackSymbol = "https://cs-web-symbol.diamond.ac.uk/catalogue/default.svg", + transparent = true, + backgroundColor = "white", + showBooleanLabel = false, + enabled = true + } = props; const style = commonCss(props as any); + // If symbols and not imagefile, we're in a bob file + const isBob = props.symbols ? true : false; + const symbols = props.symbols ? props.symbols : []; + + // Convert our value to an index, or use the initialIndex + const index = convertValueToIndex(props.value, initialIndex, arrayIndex); - let imageFile = props.imageFile; const regex = / [0-9]\./; + let imageFile = isBob ? symbols[index] : props.imageFile; + // If no provided image file + if (!imageFile) imageFile = fallbackSymbol; const intValue = DType.coerceDouble(props.value); - if (!isNaN(intValue)) { - imageFile = props.imageFile.replace(regex, ` ${intValue.toFixed(0)}.`); + if (!isNaN(intValue) && !isBob) { + imageFile = imageFile.replace(regex, ` ${intValue.toFixed(0)}.`); } let alignItems = "center"; @@ -104,7 +130,7 @@ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => { const exitContext = useContext(ExitFileContext); const parentMacros = useContext(MacroContext).macros; function onClick(event: React.MouseEvent): void { - if (props.actions !== undefined) { + if (props.actions !== undefined && enabled) { executeActions( props.actions as WidgetActions, files, @@ -114,27 +140,36 @@ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => { } } + // Define label appearance + let labelDiv; + if (isBob) labelDiv = generateIndexLabel(index, showIndex); + // Note: I would've preferred to define the onClick on div that wraps // both sub-components, but replacing the fragment with a div, with the way // the image component is written causes many images to be of the incorrect size return ( <> - - {props.showBooleanLabel && ( + + {isBob ? ( + labelDiv + ) : showBooleanLabel ? ( <>
@@ -146,11 +181,53 @@ export const SymbolComponent = (props: SymbolComponentProps): JSX.Element => {
+ ) : ( + <> )} ); }; +/** + * Return a div element describing how the label should look + */ +function generateIndexLabel(index: number, showIndex: boolean): JSX.Element { + if (!showIndex) return <>; + // Create span + return ( +
+ + {index} + +
+ ); +} + +/** + * Convert the input value into an index for symbols + * @param value + */ +function convertValueToIndex( + value: DType | undefined, + initialIndex: number, + arrayIndex: number +): number { + // If no value, use initialIndex + if (value === undefined) return initialIndex; + // First we check if we have a string + const isArray = value.getArrayValue()?.length !== undefined ? true : false; + if (isArray) { + // If is array, get index + const arrayValue = DType.coerceArray(value); + const idx = Number(arrayValue[arrayIndex]); + return Math.floor(idx); + } else { + const intValue = DType.coerceDouble(value); + if (!isNaN(intValue)) return Math.floor(intValue); + } + return initialIndex; +} + const SymbolWidgetProps = { ...SymbolProps, ...PVWidgetPropType