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";