Skip to content

Commit

Permalink
Merge pull request #54 from dls-controls/add-choice-widget
Browse files Browse the repository at this point in the history
Add choice widget
  • Loading branch information
abigailalexander authored Apr 11, 2024
2 parents 609b4cc + ae953f6 commit 0aafb93
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/types/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Points } from "./points";

export type GenericProp =
| string
| string[]
| boolean
| number
| PV
Expand Down
16 changes: 16 additions & 0 deletions src/ui/widgets/ChoiceButton/choiceButton.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
75 changes: 75 additions & 0 deletions src/ui/widgets/ChoiceButton/choiceButton.test.tsx
Original file line number Diff line number Diff line change
@@ -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 <ChoiceButtonComponent {...choiceButtonProps} />;
};

describe("<BoolButton />", (): void => {
test("it renders ChoiceButton with default props", (): void => {
const choiceButtonProps = {
value: null
};
const { getAllByRole } = render(ChoiceButtonRenderer(choiceButtonProps));
const buttons = getAllByRole("button") as Array<HTMLButtonElement>;

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

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

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");
});
});
144 changes: 144 additions & 0 deletions src/ui/widgets/ChoiceButton/choiceButton.tsx
Original file line number Diff line number Diff line change
@@ -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<JSX.Element> = [];
options.forEach((item: string | null | undefined, idx: number) => {
if (typeof item === "string") {
elements.push(
<button
className={classes.ChoiceButton}
disabled={enabled ? false : true}
onClick={() => handleClick(idx)}
style={{
...style,
backgroundColor:
selected === idx
? selectedColor.toString()
: backgroundColor.toString(),
boxShadow:
selected === idx
? `inset 0px ${Math.round(height / 6)}px ${Math.round(
height / 4
)}px 0px rgba(0,0,0,0.3)`
: "none"
}}
key={item}
>
{item}
</button>
);
}
});

return (
<div
style={{ display: "flex", flexDirection: horizontal ? "row" : "column" }}
>
{elements}
</div>
);
};

const ChoiceButtonWidgetProps = {
...ChoiceButtonProps,
...PVWidgetPropType
};

export const ChoiceButton = (
props: InferWidgetProps<typeof ChoiceButtonWidgetProps>
): JSX.Element => <Widget baseWidget={ChoiceButtonComponent} {...props} />;

registerWidget(ChoiceButton, ChoiceButtonWidgetProps, "choicebutton");
10 changes: 10 additions & 0 deletions src/ui/widgets/EmbeddedDisplay/bobParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const BOB_WIDGET_MAPPING: { [key: string]: any } = {
polyline: "line",
progressbar: "progressbar",
rectangle: "shape",
choice: "choicebutton",
scaledslider: "slidecontrol"
};

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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],
Expand Down
17 changes: 17 additions & 0 deletions src/ui/widgets/EmbeddedDisplay/opiParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
};
Expand Down
1 change: 1 addition & 0 deletions src/ui/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down

0 comments on commit 0aafb93

Please sign in to comment.