diff --git a/src/types/props.ts b/src/types/props.ts index 101a154..3db2fc8 100644 --- a/src/types/props.ts +++ b/src/types/props.ts @@ -11,6 +11,7 @@ import { Points } from "./points"; export type GenericProp = | string + | string[] | boolean | number | PV diff --git a/src/ui/widgets/ChoiceButton/choiceButton.module.css b/src/ui/widgets/ChoiceButton/choiceButton.module.css new file mode 100644 index 0000000..8d166cf --- /dev/null +++ b/src/ui/widgets/ChoiceButton/choiceButton.module.css @@ -0,0 +1,16 @@ +.ChoiceButton { + align-items: center; + justify-content: center; + text-overflow: ellipsis; + border-radius: 3px; + margin-right: 1px; + margin-bottom: 1px; + border: 1px solid gray; + word-wrap: break-word; + overflow: hidden; + display: inline-block; +} + +.ChoiceButton:hover { + background-image: linear-gradient(rgb(255 255 255 0.4) 0 0); +} \ No newline at end of file diff --git a/src/ui/widgets/ChoiceButton/choiceButton.test.tsx b/src/ui/widgets/ChoiceButton/choiceButton.test.tsx new file mode 100644 index 0000000..1b28fe7 --- /dev/null +++ b/src/ui/widgets/ChoiceButton/choiceButton.test.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { ChoiceButtonComponent } from "./choiceButton"; +import { render } from "@testing-library/react"; +import { DDisplay, DType } from "../../../types/dtypes"; +import { Color } from "../../../types"; + +const ChoiceButtonRenderer = (choiceButtonProps: any): JSX.Element => { + return ; +}; + +describe("", (): void => { + test("it renders ChoiceButton with default props", (): void => { + const choiceButtonProps = { + value: null + }; + const { getAllByRole } = render(ChoiceButtonRenderer(choiceButtonProps)); + const buttons = getAllByRole("button") as Array; + + expect(buttons[0].textContent).toEqual("Item 1"); + expect(buttons[1].textContent).toEqual("Item 2"); + expect(buttons[0].style.height).toEqual("43px"); + expect(buttons[1].style.width).toEqual("46px"); + expect(buttons[0].style.backgroundColor).toEqual("rgb(210, 210, 210)"); + expect(buttons[1].style.fontSize).toEqual("1.4rem"); + }); + + test("pass props to widget", (): void => { + const choiceButtonProps = { + value: new DType({ doubleValue: 0 }), + width: 60, + height: 140, + items: ["Choice", "Option", "Setting", "Custom"], + horizontal: false, + backgroundColor: Color.fromRgba(20, 20, 200), + selectedColor: Color.fromRgba(10, 60, 40), + itemsFromPv: false, + enabled: false + }; + const { getAllByRole } = render(ChoiceButtonRenderer(choiceButtonProps)); + const buttons = getAllByRole("button") as Array; + + expect(buttons.length).toEqual(4); + // First button is selected therefore different color and box shadow + expect(buttons[0].style.boxShadow).toEqual( + "inset 0px 23px 35px 0px rgba(0,0,0,0.3)" + ); + expect(buttons[0].style.backgroundColor).toEqual("rgb(10, 60, 40)"); + expect(buttons[2].textContent).toEqual("Setting"); + expect(buttons[3].style.cursor).toEqual("not-allowed"); + expect(buttons[3].style.height).toEqual("31px"); + expect(buttons[3].style.backgroundColor).toEqual("rgb(20, 20, 200)"); + }); + + test("pass props to widget, using itemsFromPv", (): void => { + const choiceButtonProps = { + value: new DType( + { doubleValue: 0 }, + undefined, + undefined, + new DDisplay({ choices: ["hi", "Hello"] }) + ), + items: ["one", "two", "three"], + horizontal: false, + itemsFromPv: true, + enabled: true + }; + const { getAllByRole } = render(ChoiceButtonRenderer(choiceButtonProps)); + const buttons = getAllByRole("button") as Array; + + expect(buttons.length).toEqual(2); + // First button is selected therefore different color and box shadow + expect(buttons[0].textContent).toEqual("hi"); + expect(buttons[1].textContent).toEqual("Hello"); + }); +}); diff --git a/src/ui/widgets/ChoiceButton/choiceButton.tsx b/src/ui/widgets/ChoiceButton/choiceButton.tsx new file mode 100644 index 0000000..ebbc040 --- /dev/null +++ b/src/ui/widgets/ChoiceButton/choiceButton.tsx @@ -0,0 +1,144 @@ +import React, { CSSProperties, useEffect, useState } from "react"; + +import { Widget } from "../widget"; +import { PVComponent, PVWidgetPropType } from "../widgetProps"; +import { registerWidget } from "../register"; +import { + BoolPropOpt, + ColorPropOpt, + FontPropOpt, + InferWidgetProps, + IntPropOpt, + StringArrayPropOpt, + StringPropOpt +} from "../propTypes"; +import { DType } from "../../../types/dtypes"; +import classes from "./choiceButton.module.css"; +import { Color } from "../../../types/color"; +import { writePv } from "../../hooks/useSubscription"; +import { Font } from "../../../types/font"; + +const ChoiceButtonProps = { + pvName: StringPropOpt, + height: IntPropOpt, + width: IntPropOpt, + items: StringArrayPropOpt, + selectedColor: ColorPropOpt, + itemsFromPv: BoolPropOpt, + foregroundColor: ColorPropOpt, + backgroundColor: ColorPropOpt, + horizontal: BoolPropOpt, + enabled: BoolPropOpt, + itemsfromPv: BoolPropOpt, + font: FontPropOpt +}; + +export type ChoiceButtonComponentProps = InferWidgetProps< + typeof ChoiceButtonProps +> & + PVComponent; + +export const ChoiceButtonComponent = ( + props: ChoiceButtonComponentProps +): JSX.Element => { + const { + width = 100, + height = 43, + value = null, + enabled = true, + itemsFromPv = true, + pvName, + items = ["Item 1", "Item 2"], + horizontal = true, + backgroundColor = Color.fromRgba(210, 210, 210), + foregroundColor = Color.BLACK, + selectedColor = Color.fromRgba(200, 200, 200), + font = new Font(14) + } = props; + const [selected, setSelected] = useState(value?.getDoubleValue()); + + // Use items from file, unless itemsFRomPv set + let options = items; + if (itemsFromPv && value?.display.choices) options = value?.display.choices; + + // This is necessary in order to set the initial label value + // after connection to PV established, as setState cannot be + // established inside a conditional, or called in the main body + // of the component as it causes too many re-renders error + useEffect(() => { + if (value) { + setSelected(value.getDoubleValue()); + } + }, [value]); + + // Number of buttons to create + const numButtons = options.length || 1; + // Determine width and height of buttons if horizontal or vertically placed + const buttonHeight = horizontal ? height : height / numButtons - 4; + const buttonWidth = horizontal ? width / numButtons - 4 : width; + + const style: CSSProperties = { + height: buttonHeight, + width: buttonWidth, + textAlignLast: "center", + cursor: enabled ? "default" : "not-allowed", + color: foregroundColor?.toString(), + ...font.css() + }; + + function handleClick(index: number) { + // Write to PV + if (pvName) { + writePv(pvName, new DType({ doubleValue: index })); + } + } + + // Iterate over items to create buttons + const elements: Array = []; + options.forEach((item: string | null | undefined, idx: number) => { + if (typeof item === "string") { + elements.push( + + ); + } + }); + + return ( +
+ {elements} +
+ ); +}; + +const ChoiceButtonWidgetProps = { + ...ChoiceButtonProps, + ...PVWidgetPropType +}; + +export const ChoiceButton = ( + props: InferWidgetProps +): JSX.Element => ; + +registerWidget(ChoiceButton, ChoiceButtonWidgetProps, "choicebutton"); diff --git a/src/ui/widgets/EmbeddedDisplay/bobParser.ts b/src/ui/widgets/EmbeddedDisplay/bobParser.ts index 0b6bc30..fe1f673 100644 --- a/src/ui/widgets/EmbeddedDisplay/bobParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/bobParser.ts @@ -50,6 +50,7 @@ const BOB_WIDGET_MAPPING: { [key: string]: any } = { polyline: "line", progressbar: "progressbar", rectangle: "shape", + choice: "choicebutton", scaledslider: "slidecontrol" }; @@ -133,6 +134,14 @@ function bobParseBorder(props: any): Border { } } +function bobParseItems(jsonProp: ElementCompact): string[] { + const items: string[] = []; + jsonProp["item"].forEach((item: any) => { + items.push(item._text); + }); + return items; +} + /** * Parse file for Embedded Display widgets * @param props @@ -235,6 +244,7 @@ export function parseBob( (actions: ElementCompact): WidgetActions => opiParseActions(actions, defaultProtocol) ], + items: ["items", bobParseItems], imageFile: ["file", opiParseString], points: ["points", bobParsePoints], resize: ["resize", bobParseResizing], diff --git a/src/ui/widgets/EmbeddedDisplay/opiParser.ts b/src/ui/widgets/EmbeddedDisplay/opiParser.ts index 0120bf8..4686cbc 100644 --- a/src/ui/widgets/EmbeddedDisplay/opiParser.ts +++ b/src/ui/widgets/EmbeddedDisplay/opiParser.ts @@ -64,6 +64,7 @@ const OPI_WIDGET_MAPPING: { [key: string]: any } = { "org.csstudio.opibuilder.widgets.MenuButton": "menubutton", "org.csstudio.opibuilder.widgets.combo": "menubutton", "org.csstudio.opibuilder.widgets.checkbox": "checkbox", + "org.csstudio.opibuilder.widgets.choiceButton": "choicebutton", "org.csstudio.opibuilder.widgets.linkingContainer": "embeddedDisplay", "org.csstudio.opibuilder.widgets.polyline": "line", "org.csstudio.opibuilder.widgets.polygon": "polygon", @@ -566,6 +567,19 @@ function opiParsePoints(props: any): Points { return new Points(points); } +/** + * Parse an array of items + * @param jsonProp + * @returns items (array of strings) + */ +function opiParseItems(jsonProp: ElementCompact): string[] { + const items: string[] = []; + jsonProp["s"].forEach((item: any) => { + items.push(item._text); + }); + return items; +} + /** * Parse numbers for resizing into strings that say what * time of resizing should be performed @@ -648,6 +662,7 @@ export const OPI_SIMPLE_PARSERS: ParserDict = { bit: ["bit", opiParseNumber], actionsFromPv: ["actions_from_pv", opiParseBoolean], itemsFromPv: ["items_from_pv", opiParseBoolean], + items: ["items", opiParseItems], deviceName: ["device_name", opiParseString], autoZoomToFit: ["auto_zoom_to_fit_all", opiParseBoolean], plotBackgroundColor: ["plot_area_background_color", opiParseColor], //these are all plot props @@ -683,6 +698,8 @@ export const OPI_SIMPLE_PARSERS: ParserDict = { arrows: ["arrows", opiParseNumber], arrowLength: ["arrow_length", opiParseNumber], fillArrow: ["fill_arrow", opiParseBoolean], + selectedColor: ["selected_color", opiParseColor], + enabled: ["enabled", opiParseBoolean], resize: ["resize_behaviour", opiParseResizing], labelsFromPv: ["labels_from_pv", opiParseBoolean] }; diff --git a/src/ui/widgets/index.ts b/src/ui/widgets/index.ts index 9b716fd..097321f 100644 --- a/src/ui/widgets/index.ts +++ b/src/ui/widgets/index.ts @@ -13,6 +13,7 @@ export { ActionButton } from "./ActionButton/actionButton"; export { BoolButton } from "./BoolButton/boolButton"; export { ByteMonitor } from "./ByteMonitor/byteMonitor"; export { Checkbox } from "./Checkbox/checkbox"; +export { ChoiceButton } from "./ChoiceButton/choiceButton"; export { Device } from "./Device/device"; export { DrawerWidget } from "./Drawer/drawer"; export { DropDown } from "./DropDown/dropDown";