import React from "react";
import { observable, makeObservable, computed } from "mobx";
import QRC from "../qrcodegen";
import { remap } from "eez-studio-shared/util";
import { roundNumber } from "eez-studio-shared/roundNumber";
import { to16bitsColor } from "eez-studio-shared/color";
import {
IEezObject,
registerClass,
PropertyType,
makeDerivedClassInfo,
getParent,
MessageType,
EezObject,
ClassInfo,
RectObject,
IMessage,
PropertyProps
} from "project-editor/core/object";
import {
ProjectStore,
Message,
propertyNotFoundMessage,
propertyNotSetMessage,
getChildOfObject,
propertySetButNotUsedMessage,
getProjectStore
} from "project-editor/store";
import {
isProjectWithFlowSupport,
isNotV1Project,
isNotProjectWithFlowSupport,
isV3OrNewerProject,
hasNotFlowSupport,
hasFlowSupport
} from "project-editor/project/project-type-traits";
import {
getProject,
Project,
ProjectType,
findPage,
findVariable,
checkObjectReference,
findBitmap
} from "project-editor/project/project";
import type {
IDataContext,
IFlowContext
} from "project-editor/flow/flow-interfaces";
import {
ComponentEnclosure,
ComponentCanvas
} from "project-editor/flow/editor/render";
import { Style } from "project-editor/features/style/style";
import {
DROP_DOWN_LIST_CHANGE_EVENT_STRUCT_NAME,
ValueType
} from "project-editor/features/variable/value-type";
import * as draw from "project-editor/flow/editor/eez-gui-draw";
import { Font } from "project-editor/features/font/font";
import {
Widget,
makeDataPropertyInfo,
makeStylePropertyInfo,
makeTextPropertyInfo,
migrateStyleProperty,
makeExpressionProperty
} from "project-editor/flow/component";
import { Assets, DataBuffer } from "project-editor/build/assets";
import {
WIDGET_TYPE_DISPLAY_DATA,
WIDGET_TYPE_MULTILINE_TEXT,
WIDGET_TYPE_TOGGLE_BUTTON,
WIDGET_TYPE_BAR_GRAPH,
WIDGET_TYPE_YT_GRAPH,
WIDGET_TYPE_UP_DOWN,
WIDGET_TYPE_LIST_GRAPH,
WIDGET_TYPE_APP_VIEW,
WIDGET_TYPE_SCROLL_BAR,
WIDGET_TYPE_CANVAS,
WIDGET_TYPE_GAUGE,
WIDGET_TYPE_INPUT,
WIDGET_TYPE_ROLLER,
WIDGET_TYPE_SWITCH,
WIDGET_TYPE_SLIDER,
WIDGET_TYPE_LINE_CHART,
WIDGET_TYPE_BUTTON_GROUP,
WIDGET_TYPE_QR_CODE,
WIDGET_TYPE_DROP_DOWN_LIST,
WIDGET_TYPE_PROGRESS,
WIDGET_TYPE_BUTTON,
WIDGET_TYPE_TEXT,
WIDGET_TYPE_RECTANGLE,
WIDGET_TYPE_BITMAP
} from "project-editor/flow/components/component-types";
import {
buildExpression,
checkExpression,
evalConstantExpression
} from "project-editor/flow/expression";
import { ProjectEditor } from "project-editor/project-editor-interface";
import {
generalGroup,
indentationGroup,
specificGroup
} from "project-editor/ui-components/PropertyGrid/groups";
import {
getBooleanValue,
evalProperty,
buildWidgetText,
getTextValue,
getNumberValue,
getAnyValue
} from "project-editor/flow/helper";
import {
GAUGE_ICON,
LINE_CHART_ICON,
SWITCH_WIDGET_ICON
} from "project-editor/ui-components/icons";
import { getComponentName } from "project-editor/flow/components/components-registry";
import classNames from "classnames";
import { observer } from "mobx-react";
import { Button } from "eez-studio-ui/button";
import type * as FileTypeModule from "instrument/connection/file-type";
const BAR_GRAPH_ORIENTATION_LEFT_RIGHT = 1;
const BAR_GRAPH_ORIENTATION_RIGHT_LEFT = 2;
const BAR_GRAPH_ORIENTATION_TOP_BOTTOM = 3;
const BAR_GRAPH_ORIENTATION_BOTTOM_TOP = 4;
const BAR_GRAPH_DO_NOT_DISPLAY_VALUE = 1 << 4;
import { isArray } from "eez-studio-shared/util";
////////////////////////////////////////////////////////////////////////////////
enum DisplayOption {
All = 0,
Integer = 1,
FractionAndUnit = 2,
Fraction = 3,
Unit = 4,
IntegerAndFraction = 5
}
export class DisplayDataWidget extends Widget {
focusStyle: Style;
displayOption: DisplayOption;
refreshRate: string;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_DISPLAY_DATA,
properties: [
makeDataPropertyInfo("data", {}, "any"),
makeStylePropertyInfo("style", "Default style"),
Object.assign(
makeStylePropertyInfo("focusStyle", "Focused style"),
{ hideInPropertyGrid: isNotV1Project },
{
isOptional: true
}
),
{
name: "displayOption",
type: PropertyType.Enum,
enumItems: (widget: DisplayDataWidget) =>
hasFlowSupport(widget)
? [
{
id: DisplayOption.All,
label: "All"
},
{
id: DisplayOption.Integer,
label: "Integer"
},
{
id: DisplayOption.Fraction,
label: "Fraction"
}
]
: [
{
id: DisplayOption.All,
label: "All"
},
{
id: DisplayOption.Integer,
label: "Integer"
},
{
id: DisplayOption.FractionAndUnit,
label: "Fraction and unit"
},
{
id: DisplayOption.Fraction,
label: "Fraction"
},
{
id: DisplayOption.Unit,
label: "Unit"
},
{
id: DisplayOption.IntegerAndFraction,
label: "Integer and fraction"
}
],
propertyGridGroup: specificGroup
},
makeDataPropertyInfo("refreshRate", {
disabled: hasNotFlowSupport
})
],
beforeLoadHook: (
object: IEezObject,
jsObject: any,
project: Project
) => {
migrateStyleProperty(jsObject, "focusStyle");
if (jsObject.refreshRate == undefined) {
if (project.projectTypeTraits.hasFlowSupport) {
jsObject.refreshRate = "0";
}
}
},
defaultValue: {
data: "data",
left: 0,
top: 0,
width: 64,
height: 32,
displayOption: 0,
refreshRate: "0"
},
icon: (
),
check: (object: DisplayDataWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
if (object.displayOption === undefined) {
if (
getProject(object).settings.general.projectVersion !== "v1"
) {
messages.push(
propertyNotSetMessage(object, "displayOption")
);
}
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
focusStyle: observable,
displayOption: observable,
refreshRate: observable
});
}
applyDisplayOption(text: string) {
text = text.toString();
function findStartOfFraction() {
let i;
for (
i = 0;
text[i] &&
(text[i] == "<" ||
text[i] == " " ||
text[i] == "-" ||
(text[i] >= "0" && text[i] <= "9"));
i++
) {}
return i;
}
function findStartOfUnit(i: number) {
for (
i = 0;
text[i] &&
(text[i] == "<" ||
text[i] == " " ||
text[i] == "-" ||
(text[i] >= "0" && text[i] <= "9") ||
text[i] == ".");
i++
) {}
return i;
}
if (this.displayOption === DisplayOption.Integer) {
let i = findStartOfFraction();
text = text.substr(0, i);
} else if (this.displayOption === DisplayOption.FractionAndUnit) {
let i = findStartOfFraction();
text = text.substr(i);
} else if (this.displayOption === DisplayOption.Fraction) {
let i = findStartOfFraction();
let k = findStartOfUnit(i);
if (i < k) {
text = text.substring(i, k);
} else {
text = ".00";
}
} else if (this.displayOption === DisplayOption.Unit) {
let i = findStartOfUnit(0);
text = text.substr(i);
} else if (this.displayOption === DisplayOption.IntegerAndFraction) {
let i = findStartOfUnit(0);
text = text.substr(0, i);
}
if (typeof text === "string") {
text = text.trim();
}
return text;
}
override render(flowContext: IFlowContext, width: number, height: number) {
const result = getTextValue(
flowContext,
this,
"data",
undefined,
undefined
);
let text = this.applyDisplayOption(
typeof result == "object" ? result.text : result
);
return (
<>
{
draw.drawText(
ctx,
text,
0,
0,
width,
height,
this.style,
false
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
if (isV3OrNewerProject(this)) {
// refreshRate
dataBuffer.writeInt16(
assets.projectStore.projectTypeTraits.hasFlowSupport
? assets.getWidgetDataItemIndex(this, "refreshRate")
: 0
);
}
// displayOption
dataBuffer.writeUint8(this.displayOption || 0);
}
}
registerClass("DisplayDataWidget", DisplayDataWidget);
////////////////////////////////////////////////////////////////////////////////
export class TextWidget extends Widget {
name: string;
text?: string;
ignoreLuminocity: boolean;
focusStyle: Style;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_TEXT,
label: (widget: TextWidget) => {
let name = getComponentName(widget.type);
const project = ProjectEditor.getProject(widget);
if (!project.projectTypeTraits.hasFlowSupport) {
if (widget.text) {
return `${name}: ${widget.text}`;
}
}
if (widget.name) {
return `${name}: ${widget.name}`;
}
if (widget.data) {
return `${name}: ${widget.data}`;
}
return name;
},
properties: [
{
name: "name",
type: PropertyType.String,
propertyGridGroup: generalGroup
},
makeDataPropertyInfo("data", {
displayName: (widget: TextWidget) => {
const project = ProjectEditor.getProject(widget);
if (project.projectTypeTraits.hasFlowSupport) {
return "Text";
}
return "Data";
}
}),
makeTextPropertyInfo("text", {
displayName: "Static text",
disabled: isProjectWithFlowSupport
}),
{
name: "ignoreLuminocity",
type: PropertyType.Boolean,
defaultValue: false,
propertyGridGroup: specificGroup,
disabled: isV3OrNewerProject
},
makeStylePropertyInfo("style", "Default style"),
Object.assign(
makeStylePropertyInfo("focusStyle", "Focused style"),
{ enabled: isNotV1Project },
{
isOptional: true
}
)
],
beforeLoadHook: (widget: Widget, jsObject: any, project: Project) => {
if (jsObject.text) {
if (project.projectTypeTraits.hasFlowSupport) {
if (!jsObject.data) {
jsObject.data = `"${jsObject.text}"`;
}
delete jsObject.text;
}
}
migrateStyleProperty(jsObject, "focusStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
icon: (
),
check: (widget: TextWidget, messages: IMessage[]) => {
const project = ProjectEditor.getProject(widget);
if (!project.projectTypeTraits.hasFlowSupport) {
if (!widget.text && !widget.data) {
messages.push(propertyNotSetMessage(widget, "text"));
}
} else {
if (!widget.data) {
messages.push(propertyNotSetMessage(widget, "data"));
}
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
name: observable,
text: observable,
ignoreLuminocity: observable,
focusStyle: observable
});
}
getClassName(flowContext: IFlowContext) {
return classNames("eez-widget", this.type);
}
styleHook(style: React.CSSProperties, flowContext: IFlowContext) {
super.styleHook(style, flowContext);
if (this.style.alignHorizontalProperty == "left") {
style.textAlign = "left";
} else if (this.style.alignHorizontalProperty == "center") {
style.textAlign = "center";
} else if (this.style.alignHorizontalProperty == "right") {
style.textAlign = "right";
}
}
override render(flowContext: IFlowContext, width: number, height: number) {
const result = getTextValue(
flowContext,
this,
"data",
this.name,
this.text
);
let text: string;
if (typeof result == "object") {
text = result.text;
} else {
text = result;
}
const style: React.CSSProperties = {};
this.styleHook(style, flowContext);
return (
<>
{
draw.drawText(
ctx,
text,
0,
0,
width,
height,
this.style,
false
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// text
buildWidgetText(assets, dataBuffer, this.text);
// flags
let flags: number = 0;
// ignoreLuminocity
if (this.ignoreLuminocity) {
flags |= 1 << 0;
}
dataBuffer.writeInt8(flags);
}
}
registerClass("TextWidget", TextWidget);
////////////////////////////////////////////////////////////////////////////////
enum MultilineTextRenderStep {
MEASURE,
RENDER
}
class MultilineTextRender {
constructor(
private ctx: CanvasRenderingContext2D,
private text: string,
private x1: number,
private y1: number,
private x2: number,
private y2: number,
private style: Style,
private firstLineIndent: number,
private hangingIndent: number
) {}
private font: Font;
private spaceWidth: number;
private lineHeight: number;
private textHeight: number;
private line: string;
private lineIndent: number;
private lineWidth: number;
flushLine(y: number, step: MultilineTextRenderStep) {
if (this.line != "" && this.lineWidth > 0) {
if (step == MultilineTextRenderStep.RENDER) {
let x;
if (draw.styleIsHorzAlignLeft(this.style)) {
x = this.x1;
} else if (draw.styleIsHorzAlignRight(this.style)) {
x = this.x2 + 1 - this.lineWidth;
} else {
x =
this.x1 +
Math.floor(
(this.x2 - this.x1 + 1 - this.lineWidth) / 2
);
}
draw.setBackColor(this.style.backgroundColorProperty);
draw.setColor(this.style.colorProperty);
draw.drawStr(
this.ctx,
this.line,
x + this.lineIndent,
y,
x + this.lineWidth - 1,
y + this.font.height - 1,
this.font
);
} else {
this.textHeight = Math.max(
this.textHeight,
y + this.lineHeight - this.y1
);
}
this.line = "";
this.lineWidth = this.lineIndent = this.hangingIndent;
}
}
executeStep(step: MultilineTextRenderStep) {
this.textHeight = 0;
let y = this.y1;
this.line = "";
this.lineWidth = this.lineIndent = this.firstLineIndent;
let i = 0;
while (true) {
let word = "";
while (
i < this.text.length &&
this.text[i] != " " &&
this.text[i] != "\n"
) {
word += this.text[i++];
}
let width = draw.measureStr(word, this.font, 0);
while (
this.lineWidth +
(this.line != "" ? this.spaceWidth : 0) +
width >
this.x2 - this.x1 + 1
) {
this.flushLine(y, step);
y += this.lineHeight;
if (y + this.lineHeight - 1 > this.y2) {
break;
}
}
if (y + this.lineHeight - 1 > this.y2) {
break;
}
if (this.line != "") {
this.line += " ";
this.lineWidth += this.spaceWidth;
}
this.line += word;
this.lineWidth += width;
while (this.text[i] == " ") {
i++;
}
if (i == this.text.length || this.text[i] == "\n") {
this.flushLine(y, step);
y += this.lineHeight;
if (i == this.text.length) {
break;
}
i++;
let extraHeightBetweenParagraphs = Math.floor(
0.2 * this.lineHeight
);
y += extraHeightBetweenParagraphs;
if (y + this.lineHeight - 1 > this.y2) {
break;
}
}
}
this.flushLine(y, step);
return this.textHeight + this.font.height - this.lineHeight;
}
render() {
let x1 = this.x1;
let y1 = this.y1;
let x2 = this.x2;
let y2 = this.y2;
({ x1, y1, x2, y2 } = draw.drawBackground(
this.ctx,
x1,
y1,
x2,
y2,
this.style,
true
));
const font = draw.styleGetFont(this.style);
if (!font) {
return;
}
let lineHeight = Math.floor(0.9 * font.height);
if (lineHeight <= 0) {
return;
}
try {
this.text = JSON.parse('"' + this.text + '"');
} catch (e) {}
this.font = font;
this.lineHeight = lineHeight;
this.x1 += this.style.paddingRect.left;
this.x2 -= this.style.paddingRect.right;
this.y1 += this.style.paddingRect.top;
this.y2 -= this.style.paddingRect.bottom;
const spaceGlyph = font.glyphs.find(glyph => glyph.encoding == 32);
this.spaceWidth = (spaceGlyph && spaceGlyph.dx) || 0;
const textHeight = this.executeStep(MultilineTextRenderStep.MEASURE);
if (draw.styleIsVertAlignTop(this.style)) {
} else if (draw.styleIsVertAlignBottom(this.style)) {
this.y1 = this.y2 + 1 - textHeight;
} else {
this.y1 += Math.floor((this.y2 - this.y1 + 1 - textHeight) / 2);
}
this.y2 = this.y1 + textHeight - 1;
this.executeStep(MultilineTextRenderStep.RENDER);
}
}
export class MultilineTextWidget extends Widget {
name: string;
text?: string;
firstLineIndent: number;
hangingIndent: number;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_MULTILINE_TEXT,
label: (widget: MultilineTextWidget) => {
let name = getComponentName(widget.type);
const project = ProjectEditor.getProject(widget);
if (!project.projectTypeTraits.hasFlowSupport) {
if (widget.text) {
return `${name}: ${widget.text}`;
}
}
if (widget.name) {
return `${name}: ${widget.name}`;
}
if (widget.data) {
return `${name}: ${widget.data}`;
}
return name;
},
properties: [
{
name: "name",
type: PropertyType.String,
propertyGridGroup: generalGroup
},
makeDataPropertyInfo("data", {
displayName: (widget: MultilineTextWidget) => {
const project = ProjectEditor.getProject(widget);
if (project.projectTypeTraits.hasFlowSupport) {
return "Text";
}
return "Data";
}
}),
makeTextPropertyInfo("text", {
displayName: "Static text",
disabled: isProjectWithFlowSupport
}),
{
name: "firstLineIndent",
displayName: "First line",
type: PropertyType.Number,
propertyGridGroup: indentationGroup
},
{
name: "hangingIndent",
displayName: "Hanging",
type: PropertyType.Number,
propertyGridGroup: indentationGroup
},
makeStylePropertyInfo("style", "Default style")
],
beforeLoadHook: (widget: Widget, jsObject: any, project: Project) => {
if (jsObject.text) {
if (project.projectTypeTraits.hasFlowSupport) {
if (!jsObject.data) {
jsObject.data = `"${jsObject.text}"`;
}
delete jsObject.text;
}
}
},
defaultValue: {
text: "Multiline text",
left: 0,
top: 0,
width: 64,
height: 32,
firstLineIndent: 0,
hangingIndent: 0
},
icon: (
),
check: (widget: MultilineTextWidget, messages: IMessage[]) => {
const project = ProjectEditor.getProject(widget);
if (!project.projectTypeTraits.hasFlowSupport) {
if (!widget.text && !widget.data) {
messages.push(propertyNotSetMessage(widget, "text"));
}
} else {
if (!widget.data) {
messages.push(propertyNotSetMessage(widget, "text"));
}
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
name: observable,
text: observable,
firstLineIndent: observable,
hangingIndent: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
const result = getTextValue(
flowContext,
this,
"data",
this.name,
this.text
);
let text = typeof result == "object" ? result.text : result;
return (
<>
{
var multilineTextRender = new MultilineTextRender(
ctx,
text,
0,
0,
width,
height,
this.style,
this.firstLineIndent || 0,
this.hangingIndent || 0
);
multilineTextRender.render();
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// text
buildWidgetText(assets, dataBuffer, this.text);
// first line
dataBuffer.writeInt16(this.firstLineIndent || 0);
// hanging
dataBuffer.writeInt16(this.hangingIndent || 0);
}
}
registerClass("MultilineTextWidget", MultilineTextWidget);
////////////////////////////////////////////////////////////////////////////////
export class RectangleWidget extends Widget {
ignoreLuminocity: boolean;
invertColors: boolean;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_RECTANGLE,
properties: [
{
name: "invertColors",
type: PropertyType.Boolean,
propertyGridGroup: specificGroup,
defaultValue: false,
disabled: isV3OrNewerProject
},
{
name: "ignoreLuminocity",
type: PropertyType.Boolean,
propertyGridGroup: specificGroup,
defaultValue: false,
disabled: isV3OrNewerProject
},
makeStylePropertyInfo("style", "Default style")
],
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
icon: (
),
check: (object: RectangleWidget, messages: IMessage[]) => {
if (object.data) {
messages.push(propertySetButNotUsedMessage(object, "data"));
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
ignoreLuminocity: observable,
invertColors: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
const invertColors = isV3OrNewerProject(this)
? true
: this.invertColors;
return (
<>
{
draw.drawBackground(
ctx,
0,
0,
width,
height,
this.style,
invertColors
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// flags
let flags: number = 0;
// invertColors
if (isV3OrNewerProject(this) || this.invertColors) {
flags |= 1 << 0;
}
// ignoreLuminocity
if (this.ignoreLuminocity) {
flags |= 1 << 1;
}
dataBuffer.writeUint8(flags);
}
}
registerClass("RectangleWidget", RectangleWidget);
////////////////////////////////////////////////////////////////////////////////
const BitmapWidgetPropertyGridUI = observer(
class BitmapWidgetPropertyGridUI extends React.Component {
get bitmapWidget() {
return this.props.objects[0] as BitmapWidget;
}
resizeToFitBitmap = () => {
getProjectStore(this.props.objects[0]).updateObject(
this.props.objects[0],
{
width: this.bitmapWidget.bitmapObject!.imageElement!.width,
height: this.bitmapWidget.bitmapObject!.imageElement!.height
}
);
};
render() {
if (this.props.readOnly) {
return null;
}
if (this.props.objects.length > 1) {
return null;
}
const bitmapObject = this.bitmapWidget.bitmapObject;
if (!bitmapObject) {
return null;
}
const imageElement = bitmapObject.imageElement;
if (!imageElement) {
return null;
}
const widget = this.props.objects[0] as Widget;
if (
widget.width == imageElement.width &&
widget.height == imageElement.height
) {
return null;
}
return (
);
}
}
);
export class BitmapWidget extends Widget {
bitmap?: string;
constructor() {
super();
makeObservable(this, {
bitmapObject: computed
});
}
override makeEditable() {
super.makeEditable();
makeObservable(this, {
bitmap: observable
});
}
get label() {
return this.bitmap ? `${this.type}: ${this.bitmap}` : this.type;
}
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_BITMAP,
properties: [
makeDataPropertyInfo("data", {}, "integer"),
{
name: "bitmap",
type: PropertyType.ObjectReference,
referencedObjectCollectionPath: "bitmaps",
propertyGridGroup: specificGroup
},
{
name: "customUI",
type: PropertyType.Any,
propertyGridGroup: specificGroup,
computed: true,
propertyGridRowComponent: BitmapWidgetPropertyGridUI
},
makeStylePropertyInfo("style", "Default style")
],
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
icon: (
),
check: (object: BitmapWidget, messages: IMessage[]) => {
if (!object.data && !object.bitmap) {
messages.push(
new Message(
MessageType.ERROR,
"Either Bitmap or Data must be set",
object
)
);
} else {
if (object.data && object.bitmap) {
messages.push(
new Message(
MessageType.ERROR,
"Both Bitmap and Data set, only Data is used",
object
)
);
}
if (object.bitmap) {
let bitmap = findBitmap(getProject(object), object.bitmap);
if (!bitmap) {
messages.push(
propertyNotFoundMessage(object, "bitmap")
);
}
}
}
}
});
get bitmapObject() {
return this.getBitmapObject(getProjectStore(this).dataContext);
}
getBitmapObject(dataContext: IDataContext) {
return this.bitmap
? findBitmap(getProject(this), this.bitmap)
: this.data
? findBitmap(getProject(this), dataContext.get(this.data) as string)
: undefined;
}
getBitmap(flowContext: IFlowContext) {
if (this.bitmap) {
return findBitmap(getProject(this), this.bitmap);
}
if (this.data) {
let data;
if (flowContext.flowState) {
data = evalProperty(flowContext, this, "data");
} else {
data = flowContext.dataContext.get(this.data);
}
if (typeof data === "string") {
if (data.startsWith("data:image/png;base64,")) {
return data;
}
const bitmap = findBitmap(getProject(this), data as string);
if (bitmap) {
return bitmap;
}
return undefined;
}
if (data instanceof Uint8Array) {
const { detectFileType } =
require("instrument/connection/file-type") as typeof FileTypeModule;
const fileType = detectFileType(data);
return URL.createObjectURL(
new Blob([data], { type: fileType.mime } /* (1) */)
);
}
return data;
}
return undefined;
}
override render(flowContext: IFlowContext, width: number, height: number) {
const bitmap = this.getBitmap(flowContext);
return (
<>
{
const w = width;
const h = height;
const style = this.style;
if (bitmap) {
const imageElement = bitmap.imageElement;
if (!imageElement) {
return;
}
let x1 = 0;
let y1 = 0;
let x2 = w - 1;
let y2 = h - 1;
let width = imageElement.width;
let height = imageElement.height;
let x_offset: number;
if (draw.styleIsHorzAlignLeft(style)) {
x_offset = x1 + style.paddingRect.left;
} else if (draw.styleIsHorzAlignRight(style)) {
x_offset = x2 - style.paddingRect.right - width;
} else {
x_offset = Math.floor(
x1 + (x2 - x1 + 1 - width) / 2
);
}
if (x_offset < x1) {
x_offset = x1;
}
let y_offset: number;
if (draw.styleIsVertAlignTop(style)) {
y_offset = y1 + style.paddingRect.top;
} else if (draw.styleIsVertAlignBottom(style)) {
y_offset =
y2 - style.paddingRect.bottom - height;
} else {
y_offset = Math.floor(
y1 + (y2 - y1 + 1 - height) / 2
);
}
if (y_offset < y1) {
y_offset = y1;
}
if (bitmap.backgroundColor !== "transparent") {
ctx.fillStyle = bitmap.backgroundColor;
ctx.fillRect(x_offset, y_offset, width, height);
} else {
ctx.clearRect(0, 0, width, height);
}
draw.drawBitmap(
ctx,
imageElement,
x_offset,
y_offset,
width,
height
);
}
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// bitmap
let bitmap: number = 0;
if (this.bitmap) {
bitmap = assets.getBitmapIndex(this, "bitmap");
}
dataBuffer.writeInt16(bitmap);
}
}
registerClass("BitmapWidget", BitmapWidget);
////////////////////////////////////////////////////////////////////////////////
export class ButtonWidget extends Widget {
text?: string;
enabled?: string;
disabledStyle: Style;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_BUTTON,
properties: [
makeDataPropertyInfo("data", {
displayName: (widget: ButtonWidget) => {
const project = ProjectEditor.getProject(widget);
if (project.projectTypeTraits.hasFlowSupport) {
return "Label";
}
return "Data";
}
}),
makeTextPropertyInfo("text", {
disabled: isProjectWithFlowSupport
}),
makeDataPropertyInfo("enabled"),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("disabledStyle")
],
beforeLoadHook: (
widget: IEezObject,
jsObject: any,
project: Project
) => {
if (jsObject.text) {
if (project.projectTypeTraits.hasFlowSupport) {
if (!jsObject.data) {
jsObject.data = `"${jsObject.text}"`;
}
delete jsObject.text;
}
}
migrateStyleProperty(jsObject, "disabledStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 80,
height: 40,
data: `"Button"`,
eventHandlers: [
{
eventName: "CLICKED",
handlerType: "flow"
}
]
},
componentDefaultValue: (projectStore: ProjectStore) => {
return projectStore.projectTypeTraits.isFirmwareModule ||
projectStore.projectTypeTraits.isApplet ||
projectStore.projectTypeTraits.isResource
? {
style: {
useStyle: "button_M"
},
disabledStyle: {
useStyle: "button_M_disabled"
}
}
: projectStore.projectTypeTraits.isFirmware
? {
style: {
useStyle: "button"
},
disabledStyle: {
useStyle: "button_disabled"
}
}
: {};
},
icon: (
),
check: (widget: ButtonWidget, messages: IMessage[]) => {
const project = ProjectEditor.getProject(widget);
if (!project.projectTypeTraits.hasFlowSupport) {
if (
!widget.text &&
!widget.data &&
!widget.isInputProperty("data")
) {
messages.push(propertyNotSetMessage(widget, "text"));
}
checkObjectReference(widget, "enabled", messages, true);
} else {
if (!widget.data) {
messages.push(propertyNotSetMessage(widget, "text"));
}
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
text: observable,
enabled: observable,
disabledStyle: observable
});
}
getClassName(flowContext: IFlowContext) {
return classNames("eez-widget", this.type);
}
override render(flowContext: IFlowContext, width: number, height: number) {
const result = getTextValue(
flowContext,
this,
"data",
undefined,
this.text
);
let text: string;
if (typeof result == "object") {
text = result.text;
} else {
text = result;
}
let buttonEnabled = getBooleanValue(
flowContext,
this,
"enabled",
flowContext.flowState ? !this.enabled : true
);
let buttonStyle = buttonEnabled ? this.style : this.disabledStyle;
const style: React.CSSProperties = {};
this.styleHook(style, flowContext);
return (
<>
{
draw.drawText(
ctx,
text,
0,
0,
width,
height,
buttonStyle,
false
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// text
buildWidgetText(assets, dataBuffer, this.text);
// enabled
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "enabled"));
// disabledStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "disabledStyle"));
}
}
registerClass("ButtonWidget", ButtonWidget);
////////////////////////////////////////////////////////////////////////////////
export class ToggleButtonWidget extends Widget {
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_TOGGLE_BUTTON,
properties: [
makeDataPropertyInfo("data", {}, "boolean"),
{
name: "text1",
type: PropertyType.String,
propertyGridGroup: specificGroup
},
{
name: "text2",
type: PropertyType.String,
propertyGridGroup: specificGroup
},
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("checkedStyle")
],
defaultValue: {
left: 0,
top: 0,
width: 32,
height: 32
},
icon: (
),
check: (object: ToggleButtonWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
if (!object.text1) {
messages.push(propertyNotSetMessage(object, "text1"));
}
if (!object.text2) {
messages.push(propertyNotSetMessage(object, "text2"));
}
}
});
text1?: string;
text2?: string;
checkedStyle: Style;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
text1: observable,
text2: observable,
checkedStyle: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
draw.drawText(
ctx,
this.text1 || "",
0,
0,
width,
height,
this.style,
false
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// text 1
buildWidgetText(assets, dataBuffer, this.text1);
// text 2
buildWidgetText(assets, dataBuffer, this.text2);
// checkedStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "checkedStyle"));
}
}
registerClass("ToggleButtonWidget", ToggleButtonWidget);
////////////////////////////////////////////////////////////////////////////////
export class ButtonGroupWidget extends Widget {
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_BUTTON_GROUP,
properties: [
makeDataPropertyInfo("data", {
displayName: (widget: ButtonGroupWidget) => {
const project = ProjectEditor.getProject(widget);
if (
project.projectTypeTraits.hasFlowSupport &&
project.settings.general.projectVersion == "v3"
) {
return "Button labels";
}
return "Data";
}
}),
makeDataPropertyInfo("selectedButton", {
disabled: (widget: ButtonGroupWidget) => {
const project = ProjectEditor.getProject(widget);
return !(
project.projectTypeTraits.hasFlowSupport &&
project.settings.general.projectVersion == "v3"
);
}
}),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("selectedStyle")
],
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
beforeLoadHook: (object: IEezObject, jsObject: any) => {
migrateStyleProperty(jsObject, "selectedStyle");
},
icon: (
),
check: (object: ButtonGroupWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
}
});
selectedStyle: Style;
selectedButton: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
selectedStyle: observable,
selectedButton: observable
});
}
getButtonLabels(flowContext: IFlowContext): string[] {
if (
flowContext.projectStore.project.projectTypeTraits.hasFlowSupport &&
flowContext.projectStore.project.settings.general.projectVersion ==
"v3"
) {
const buttonLabels = getAnyValue(flowContext, this, "data", []);
if (isArray(buttonLabels)) {
return buttonLabels.map(label => label.toString());
}
return [];
}
return (
(this.data && flowContext.dataContext.getValueList(this.data)) || []
);
}
getSelectedButton(flowContext: IFlowContext): number {
if (
flowContext.projectStore.project.projectTypeTraits.hasFlowSupport &&
flowContext.projectStore.project.settings.general.projectVersion ==
"v3"
) {
return 0;
}
return (this.data && flowContext.dataContext.get(this.data)) || 0;
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
let buttonLabels = this.getButtonLabels(flowContext);
let selectedButton =
this.getSelectedButton(flowContext);
let x = 0;
let y = 0;
let w = width;
let h = height;
if (w > h) {
// horizontal orientation
let buttonWidth = Math.floor(
w / buttonLabels.length
);
let buttonHeight = h;
for (let i = 0; i < buttonLabels.length; i++) {
if (i < buttonLabels.length - 1) {
draw.drawText(
ctx,
buttonLabels[i],
x,
y,
buttonWidth,
buttonHeight,
i == selectedButton
? this.selectedStyle
: this.style,
false
);
x += buttonWidth;
} else {
draw.drawText(
ctx,
buttonLabels[i],
x,
y,
width - x,
buttonHeight,
i == selectedButton
? this.selectedStyle
: this.style,
false
);
}
}
} else {
// vertical orientation
let buttonWidth = w;
let buttonHeight = Math.floor(
h / buttonLabels.length
);
y += Math.floor(
(h - buttonHeight * buttonLabels.length) / 2
);
let labelHeight = Math.min(
buttonWidth,
buttonHeight
);
let yOffset = Math.floor(
(buttonHeight - labelHeight) / 2
);
y += yOffset;
for (let i = 0; i < buttonLabels.length; i++) {
if (i == selectedButton) {
draw.drawText(
ctx,
buttonLabels[i],
x,
y,
buttonWidth,
labelHeight,
this.selectedStyle,
false
);
} else {
draw.drawText(
ctx,
buttonLabels[i],
x,
y,
buttonWidth,
labelHeight,
this.style,
false
);
}
y += buttonHeight;
}
}
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// selectedStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "selectedStyle"));
// selectedButton
let selectedButton =
assets.projectStore.project.projectTypeTraits.hasFlowSupport &&
assets.projectStore.project.settings.general.projectVersion == "v3"
? assets.getWidgetDataItemIndex(this, "selectedButton")
: 0;
dataBuffer.writeInt16(selectedButton);
}
}
registerClass("ButtonGroupWidget", ButtonGroupWidget);
////////////////////////////////////////////////////////////////////////////////
export class BarGraphWidget extends Widget {
orientation?: string;
displayValue: boolean;
textStyle: Style;
line1Data?: string;
line1Style: Style;
line2Data?: string;
line2Style: Style;
min: string;
max: string;
refreshRate: string;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_BAR_GRAPH,
properties: [
{
name: "orientation",
type: PropertyType.Enum,
propertyGridGroup: specificGroup,
enumItems: [
{
id: "left-right"
},
{
id: "right-left"
},
{
id: "top-bottom"
},
{
id: "bottom-top"
}
]
},
{
name: "displayValue",
type: PropertyType.Boolean,
propertyGridGroup: specificGroup
},
makeDataPropertyInfo("data"),
makeDataPropertyInfo("line1Data", { displayName: "Threshold1" }),
makeDataPropertyInfo("line2Data", { displayName: "Threshold2" }),
makeDataPropertyInfo("min", {
disabled: hasNotFlowSupport
}),
makeDataPropertyInfo("max", {
disabled: hasNotFlowSupport
}),
makeDataPropertyInfo("refreshRate", {
disabled: hasNotFlowSupport
}),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("textStyle"),
makeStylePropertyInfo("line1Style", "Threshold1 style"),
makeStylePropertyInfo("line2Style", "Threshold2 style")
],
beforeLoadHook: (
object: IEezObject,
jsObject: any,
project: Project
) => {
migrateStyleProperty(jsObject, "textStyle");
migrateStyleProperty(jsObject, "line1Style");
migrateStyleProperty(jsObject, "line2Style");
if (jsObject.refreshRate == undefined) {
if (project.projectTypeTraits.hasFlowSupport) {
jsObject.refreshRate = "0";
}
}
},
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32,
refreshRate: "0",
orientation: "left-right"
},
icon: (
),
check: (object: BarGraphWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
if (hasFlowSupport(object)) {
if (!object.min) {
messages.push(propertyNotSetMessage(object, "min"));
}
if (!object.max) {
messages.push(propertyNotSetMessage(object, "max"));
}
if (!object.refreshRate) {
messages.push(propertyNotSetMessage(object, "refreshRate"));
}
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
orientation: observable,
displayValue: observable,
textStyle: observable,
line1Data: observable,
line1Style: observable,
line2Data: observable,
line2Style: observable,
min: observable,
max: observable,
refreshRate: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
let barGraphWidget = this;
let style = barGraphWidget.style;
let valueText =
(barGraphWidget.data &&
flowContext.dataContext.get(
barGraphWidget.data
)) ||
"0";
let value = parseFloat(valueText);
if (isNaN(value)) {
value = 0;
}
let horizontal =
barGraphWidget.orientation == "left-right" ||
barGraphWidget.orientation == "right-left";
let d = horizontal ? width : height;
function calcPos(value: number) {
let pos = Math.round(d / 3);
if (pos < 0) {
pos = 0;
}
if (pos > d) {
pos = d;
}
return pos;
}
let pos = calcPos(value);
if (barGraphWidget.orientation == "left-right") {
draw.setColor(style.colorProperty);
draw.fillRect(ctx, 0, 0, pos - 1, height - 1);
draw.setColor(style.backgroundColorProperty);
draw.fillRect(ctx, pos, 0, width - 1, height - 1);
} else if (barGraphWidget.orientation == "right-left") {
draw.setColor(style.backgroundColorProperty);
draw.fillRect(
ctx,
0,
0,
width - pos - 1,
height - 1
);
draw.setColor(style.colorProperty);
draw.fillRect(
ctx,
width - pos,
0,
width - 1,
height - 1
);
} else if (barGraphWidget.orientation == "top-bottom") {
draw.setColor(style.colorProperty);
draw.fillRect(ctx, 0, 0, width - 1, pos - 1);
draw.setColor(style.backgroundColorProperty);
draw.fillRect(ctx, 0, pos, width - 1, height - 1);
} else {
draw.setColor(style.backgroundColorProperty);
draw.fillRect(
ctx,
0,
0,
width - 1,
height - pos - 1
);
draw.setColor(style.colorProperty);
draw.fillRect(
ctx,
0,
height - pos,
width - 1,
height - 1
);
}
if (this.displayValue) {
if (horizontal) {
let textStyle = barGraphWidget.textStyle;
const font = draw.styleGetFont(textStyle);
if (font) {
let w = draw.measureStr(
valueText,
font,
width
);
w += style.paddingRect.left;
if (w > 0 && height > 0) {
let backgroundColor: string;
let x: number;
if (pos + w <= width) {
backgroundColor =
style.backgroundColorProperty;
x = pos;
} else {
backgroundColor =
style.colorProperty;
x =
pos -
w -
style.paddingRect.right;
}
draw.drawText(
ctx,
valueText,
x,
0,
w,
height,
textStyle,
false,
backgroundColor
);
}
}
}
}
function drawLine(
lineData: string | undefined,
lineStyle: Style
) {
let value =
(lineData &&
parseFloat(
flowContext.dataContext.get(lineData)
)) ||
0;
if (isNaN(value)) {
value = 0;
}
let pos = calcPos(value);
if (pos == d) {
pos = d - 1;
}
draw.setColor(lineStyle.colorProperty);
if (barGraphWidget.orientation == "left-right") {
draw.drawVLine(ctx, pos, 0, height - 1);
} else if (
barGraphWidget.orientation == "right-left"
) {
draw.drawVLine(ctx, width - pos, 0, height - 1);
} else if (
barGraphWidget.orientation == "top-bottom"
) {
draw.drawHLine(ctx, 0, pos, width - 1);
} else {
draw.drawHLine(ctx, 0, height - pos, width - 1);
}
}
drawLine(
barGraphWidget.line1Data,
barGraphWidget.line1Style
);
drawLine(
barGraphWidget.line2Data,
barGraphWidget.line2Style
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// textStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "textStyle"));
// line1Data
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "line1Data"));
// line1Style
dataBuffer.writeInt16(assets.getStyleIndex(this, "line1Style"));
// line2Data
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "line2Data"));
// line2Style
dataBuffer.writeInt16(assets.getStyleIndex(this, "line2Style"));
if (isV3OrNewerProject(this)) {
// min
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "min"));
// max
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "max"));
// refreshRate
dataBuffer.writeInt16(
assets.projectStore.projectTypeTraits.hasFlowSupport
? assets.getWidgetDataItemIndex(this, "refreshRate")
: 0
);
}
// orientation
let orientation: number;
switch (this.orientation) {
case "left-right":
orientation = BAR_GRAPH_ORIENTATION_LEFT_RIGHT;
break;
case "right-left":
orientation = BAR_GRAPH_ORIENTATION_RIGHT_LEFT;
break;
case "top-bottom":
orientation = BAR_GRAPH_ORIENTATION_TOP_BOTTOM;
break;
default:
orientation = BAR_GRAPH_ORIENTATION_BOTTOM_TOP;
}
if (!this.displayValue) {
orientation |= BAR_GRAPH_DO_NOT_DISPLAY_VALUE;
}
dataBuffer.writeUint8(orientation);
}
}
registerClass("BarGraphWidget", BarGraphWidget);
////////////////////////////////////////////////////////////////////////////////
export class YTGraphWidget extends Widget {
y1Style: Style;
y2Data?: string;
y2Style: Style;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_YT_GRAPH,
properties: [
Object.assign(makeStylePropertyInfo("y1Style"), {
hideInPropertyGrid: isNotV1Project
}),
Object.assign(makeStylePropertyInfo("y2Style"), {
hideInPropertyGrid: isNotV1Project
}),
Object.assign(makeDataPropertyInfo("y2Data"), {
hideInPropertyGrid: isNotV1Project
})
],
beforeLoadHook: (object: IEezObject, jsObject: any) => {
migrateStyleProperty(jsObject, "y1Style");
migrateStyleProperty(jsObject, "y2Style");
},
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
icon: (
),
check: (object: YTGraphWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
const project = getProject(object);
if (project.settings.general.projectVersion === "v1") {
if (object.y2Data) {
if (!findVariable(project, object.y2Data)) {
messages.push(
propertyNotFoundMessage(object, "y2Data")
);
}
} else {
messages.push(propertyNotSetMessage(object, "y2Data"));
}
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
y1Style: observable,
y2Data: observable,
y2Style: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
draw.drawBackground(
ctx,
0,
0,
width,
height,
this.style,
true
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {}
}
registerClass("YTGraphWidget", YTGraphWidget);
////////////////////////////////////////////////////////////////////////////////
export class UpDownWidget extends Widget {
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Input",
flowComponentId: WIDGET_TYPE_UP_DOWN,
properties: [
makeDataPropertyInfo("data", {}, "integer"),
makeTextPropertyInfo("downButtonText"),
makeTextPropertyInfo("upButtonText"),
makeDataPropertyInfo("min", {
disabled: isNotProjectWithFlowSupport
}),
makeDataPropertyInfo("max", {
disabled: isNotProjectWithFlowSupport
}),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("buttonsStyle")
],
beforeLoadHook: (
widget: UpDownWidget,
jsWidget: Partial,
project: Project
) => {
if (project.projectTypeTraits.hasFlowSupport) {
if (widget.min == undefined) {
widget.min = "0";
}
if (widget.max == undefined) {
widget.max = "100";
}
}
migrateStyleProperty(jsWidget, "buttonsStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32,
upButtonText: ">",
downButtonText: "<"
},
icon: (
),
check: (object: UpDownWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
if (!object.downButtonText) {
messages.push(propertyNotSetMessage(object, "downButtonText"));
}
if (!object.upButtonText) {
messages.push(propertyNotSetMessage(object, "upButtonText"));
}
}
});
buttonsStyle: Style;
downButtonText?: string;
upButtonText?: string;
min: string;
max: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
buttonsStyle: observable,
downButtonText: observable,
upButtonText: observable,
min: observable,
max: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
let text = this.data
? (flowContext.dataContext.get(this.data) as string)
: "";
return (
<>
{
let upDownWidget = this;
let style = upDownWidget.style;
let buttonsStyle = upDownWidget.buttonsStyle;
const buttonsFont = draw.styleGetFont(buttonsStyle);
if (!buttonsFont) {
return;
}
const buttonWidth =
buttonsStyle.paddingRect.left +
buttonsFont.height +
buttonsStyle.paddingRect.right;
draw.drawText(
ctx,
upDownWidget.downButtonText || "<",
0,
0,
buttonWidth,
height,
buttonsStyle,
false
);
draw.drawText(
ctx,
text,
buttonWidth,
0,
width - 2 * buttonWidth,
height,
style,
false
);
draw.drawText(
ctx,
upDownWidget.upButtonText || ">",
width - buttonWidth,
0,
buttonWidth,
height,
buttonsStyle,
false
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// down button text
buildWidgetText(assets, dataBuffer, this.downButtonText, "<");
// up button text
buildWidgetText(assets, dataBuffer, this.upButtonText, ">");
// buttonStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "buttonsStyle"));
if (isV3OrNewerProject(this)) {
// min
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "min"));
// max
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "max"));
}
}
}
registerClass("UpDownWidget", UpDownWidget);
////////////////////////////////////////////////////////////////////////////////
export class ListGraphWidget extends Widget {
dwellData?: string;
y1Data?: string;
y1Style: Style;
y2Data?: string;
y2Style: Style;
cursorData?: string;
cursorStyle: Style;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_LIST_GRAPH,
properties: [
makeDataPropertyInfo("dwellData"),
makeDataPropertyInfo("y1Data"),
makeStylePropertyInfo("y1Style"),
makeStylePropertyInfo("y2Style"),
makeStylePropertyInfo("cursorStyle"),
makeDataPropertyInfo("y2Data"),
makeDataPropertyInfo("cursorData")
],
beforeLoadHook: (object: IEezObject, jsObject: any) => {
migrateStyleProperty(jsObject, "y1Style");
migrateStyleProperty(jsObject, "y2Style");
migrateStyleProperty(jsObject, "cursorStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
icon: (
),
check: (object: ListGraphWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
const project = getProject(object);
if (object.dwellData) {
if (!findVariable(project, object.dwellData)) {
messages.push(propertyNotFoundMessage(object, "dwellData"));
}
} else {
messages.push(propertyNotSetMessage(object, "dwellData"));
}
if (object.y1Data) {
if (!findVariable(project, object.y1Data)) {
messages.push(propertyNotFoundMessage(object, "y1Data"));
}
} else {
messages.push(propertyNotSetMessage(object, "y1Data"));
}
if (object.y2Data) {
if (!findVariable(project, object.y2Data)) {
messages.push(propertyNotFoundMessage(object, "y2Data"));
}
} else {
messages.push(propertyNotSetMessage(object, "y2Data"));
}
if (object.cursorData) {
if (!findVariable(project, object.cursorData)) {
messages.push(
propertyNotFoundMessage(object, "cursorData")
);
}
} else {
messages.push(propertyNotSetMessage(object, "cursorData"));
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
dwellData: observable,
y1Data: observable,
y1Style: observable,
y2Data: observable,
y2Style: observable,
cursorData: observable,
cursorStyle: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
draw.drawBackground(
ctx,
0,
0,
width,
height,
this.style,
true
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// dwellData
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "dwellData"));
// y1Data
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "y1Data"));
// y1Style
dataBuffer.writeInt16(assets.getStyleIndex(this, "y1Style"));
// y2Data
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "y2Data"));
// y2Style
dataBuffer.writeInt16(assets.getStyleIndex(this, "y2Style"));
// cursorData
dataBuffer.writeInt16(
assets.getWidgetDataItemIndex(this, "cursorData")
);
// cursorStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "cursorStyle"));
}
}
registerClass("ListGraphWidget", ListGraphWidget);
////////////////////////////////////////////////////////////////////////////////
export class ProgressWidget extends Widget {
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_PROGRESS,
properties: [
makeDataPropertyInfo("data", {}, "integer"),
makeDataPropertyInfo("min", {
disabled: isNotProjectWithFlowSupport
}),
makeDataPropertyInfo("max", {
disabled: isNotProjectWithFlowSupport
}),
{
name: "orientation",
type: PropertyType.Enum,
propertyGridGroup: specificGroup,
enumItems: [
{
id: "horizontal"
},
{
id: "vertical"
}
]
},
makeStylePropertyInfo("style", "Default style")
],
beforeLoadHook: (
progressWidget: ProgressWidget,
jsProgressWidget: Partial,
project: Project
) => {
if (project.projectTypeTraits.hasFlowSupport) {
if (jsProgressWidget.min == undefined) {
jsProgressWidget.min = "0";
}
if (jsProgressWidget.max == undefined) {
jsProgressWidget.max = "100";
}
}
if (jsProgressWidget.orientation == undefined) {
jsProgressWidget.orientation =
jsProgressWidget.width! > jsProgressWidget.height!
? "horizontal"
: "vertical";
}
},
defaultValue: {
left: 0,
top: 0,
width: 128,
height: 32
},
icon: (
)
});
min: string;
max: string;
orientation: string;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
min: observable,
max: observable,
orientation: observable
});
}
getPercent(flowContext: IFlowContext) {
if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) {
if (flowContext.flowState) {
try {
const min = evalProperty(flowContext, this, "min");
const max = evalProperty(flowContext, this, "max");
let value = evalProperty(flowContext, this, "data");
value = ((value - min) * 100) / (max - min);
if (value != null && value != undefined) {
return value;
}
} catch (err) {
//console.error(err);
}
return 0;
}
return 25;
}
if (this.data) {
const result = flowContext.dataContext.get(this.data);
if (result != undefined) {
return result;
}
}
return 25;
}
override render(flowContext: IFlowContext, width: number, height: number) {
const percent = this.getPercent(flowContext);
let isHorizontal = this.orientation == "horizontal";
return (
<>
{
draw.drawBackground(
ctx,
0,
0,
width,
height,
this.style,
true
);
// draw thumb
draw.setColor(this.style.colorProperty);
if (isHorizontal) {
draw.drawBackground(
ctx,
0,
0,
(percent * width) / 100 - 1,
height - 1,
this.style,
false
);
} else {
draw.drawBackground(
ctx,
0,
height - (percent * height) / 100,
width - 1,
height - 1,
this.style,
false
);
}
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// min
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "min"));
// max
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "max"));
// direction
const PROGRESS_WIDGET_ORIENTATION_HORIZONTAL = 0;
const PROGRESS_WIDGET_ORIENTATION_VERTICAL = 1;
dataBuffer.writeUint8(
this.orientation == "horizontal"
? PROGRESS_WIDGET_ORIENTATION_HORIZONTAL
: PROGRESS_WIDGET_ORIENTATION_VERTICAL
);
// reserved1
dataBuffer.writeUint8(0);
}
}
registerClass("ProgressWidget", ProgressWidget);
////////////////////////////////////////////////////////////////////////////////
export class AppViewWidget extends Widget {
page: string;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Containers",
flowComponentId: WIDGET_TYPE_APP_VIEW,
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
icon: (
),
check: (object: AppViewWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
page: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
let element;
if (this.data) {
const pageName = flowContext.dataContext.get(this.data);
if (pageName) {
const page = findPage(getProject(this), pageName);
if (page) {
element = (
);
}
}
}
return (
<>
{
draw.drawBackground(
ctx,
0,
0,
width,
height,
this.style,
true
);
}}
/>
{element}
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {}
}
registerClass("AppViewWidget", AppViewWidget);
////////////////////////////////////////////////////////////////////////////////
export class ScrollBarWidget extends Widget {
thumbStyle: Style;
buttonsStyle: Style;
leftButtonText?: string;
rightButtonText?: string;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
flowComponentId: WIDGET_TYPE_SCROLL_BAR,
properties: [
makeDataPropertyInfo("data", {}, "struct:$ScrollbarState"),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("thumbStyle"),
makeStylePropertyInfo("buttonsStyle"),
makeTextPropertyInfo("leftButtonText"),
makeTextPropertyInfo("rightButtonText")
],
beforeLoadHook: (object: IEezObject, jsObject: any) => {
migrateStyleProperty(jsObject, "thumbStyle");
migrateStyleProperty(jsObject, "buttonsStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 128,
height: 32,
leftButtonText: "<",
rightButtonText: ">"
},
icon: (
),
check: (object: ScrollBarWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
if (!object.leftButtonText) {
messages.push(propertyNotSetMessage(object, "leftButtonText"));
}
if (!object.rightButtonText) {
messages.push(propertyNotSetMessage(object, "rightButtonText"));
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
thumbStyle: observable,
buttonsStyle: observable,
leftButtonText: observable,
rightButtonText: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
let widget = this;
const buttonsFont = draw.styleGetFont(
widget.buttonsStyle
);
if (!buttonsFont) {
return;
}
let isHorizontal = width > height;
let buttonSize = isHorizontal ? height : width;
// draw left button
draw.drawText(
ctx,
widget.leftButtonText || "<",
0,
0,
isHorizontal ? buttonSize : width,
isHorizontal ? height : buttonSize,
widget.buttonsStyle,
false
);
// draw track
let x;
let y;
let w;
let h;
if (isHorizontal) {
x = buttonSize;
y = 0;
w = width - 2 * buttonSize;
h = height;
} else {
x = 0;
y = buttonSize;
w = width;
h = height - 2 * buttonSize;
}
draw.setColor(this.style.colorProperty);
draw.fillRect(ctx, x, y, x + w - 1, y + h - 1);
// draw thumb
let data = (widget.data &&
flowContext.dataContext.get(widget.data)) || [
100, 25, 20
];
if (!isArray(data)) {
data = [
data.numItems,
data.position,
data.itemsPerPage
];
}
const [size, position, pageSize] = data;
let xThumb;
let widthThumb;
let yThumb;
let heightThumb;
if (isHorizontal) {
xThumb = Math.floor((position * w) / size);
widthThumb = Math.max(
Math.floor((pageSize * w) / size),
buttonSize
);
yThumb = y;
heightThumb = h;
} else {
xThumb = x;
widthThumb = w;
yThumb = Math.floor((position * h) / size);
heightThumb = Math.max(
Math.floor((pageSize * h) / size),
buttonSize
);
}
draw.setColor(this.thumbStyle.colorProperty);
draw.fillRect(
ctx,
xThumb,
yThumb,
xThumb + widthThumb - 1,
yThumb + heightThumb - 1
);
// draw right button
draw.drawText(
ctx,
widget.rightButtonText || ">",
isHorizontal ? width - buttonSize : 0,
isHorizontal ? 0 : height - buttonSize,
isHorizontal ? buttonSize : width,
isHorizontal ? height : buttonSize,
widget.buttonsStyle,
false
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// thumbStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "thumbStyle"));
// buttonStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "buttonsStyle"));
// down button text
buildWidgetText(assets, dataBuffer, this.leftButtonText, "<");
// up button text
buildWidgetText(assets, dataBuffer, this.rightButtonText, ">");
}
}
registerClass("ScrollBarWidget", ScrollBarWidget);
////////////////////////////////////////////////////////////////////////////////
export class CanvasWidget extends Widget {
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_CANVAS,
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
icon: (
),
check: (object: CanvasWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
}
});
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
draw.setColor(this.style.backgroundColorProperty);
draw.fillRect(ctx, 0, 0, width - 1, height - 1);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {}
}
registerClass("CanvasWidget", CanvasWidget);
////////////////////////////////////////////////////////////////////////////////
class LineChartLine extends EezObject {
label: string;
color: string;
width: number;
value: string;
static classInfo: ClassInfo = {
properties: [
makeExpressionProperty(
{
name: "label",
type: PropertyType.MultilineText
},
"string"
),
{
name: "color",
type: PropertyType.Color,
propertyGridGroup: specificGroup
},
{
name: "width",
displayName: "Line width",
type: PropertyType.Number,
propertyGridGroup: specificGroup
},
makeExpressionProperty(
{
name: "value",
type: PropertyType.MultilineText
},
"double"
)
],
beforeLoadHook: (
object: LineChartLine,
jsObject: Partial
) => {
if (jsObject.width == undefined) {
jsObject.width = 1.5;
}
},
check: (lineChartTrace: LineChartLine, messages: IMessage[]) => {
try {
checkExpression(
getParent(
getParent(lineChartTrace)!
)! as LineChartEmbeddedWidget,
lineChartTrace.label
);
} catch (err) {
messages.push(
new Message(
MessageType.ERROR,
`Invalid expression: ${err}`,
getChildOfObject(lineChartTrace, "label")
)
);
}
try {
checkExpression(
getParent(
getParent(lineChartTrace)!
)! as LineChartEmbeddedWidget,
lineChartTrace.value
);
} catch (err) {
messages.push(
new Message(
MessageType.ERROR,
`Invalid expression: ${err}`,
getChildOfObject(lineChartTrace, "value")
)
);
}
},
defaultValue: {
color: "#333333",
lineWidth: 1.5
}
};
override makeEditable() {
super.makeEditable();
makeObservable(this, {
label: observable,
color: observable,
width: observable,
value: observable
});
}
}
export class LineChartEmbeddedWidget extends Widget {
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_LINE_CHART,
properties: [
Object.assign(makeDataPropertyInfo("data"), {
hideInPropertyGrid: true
}),
makeExpressionProperty(
{
name: "xValue",
displayName: "X value",
type: PropertyType.MultilineText,
propertyGridGroup: specificGroup
},
"any"
),
{
name: "lines",
type: PropertyType.Array,
typeClass: LineChartLine,
propertyGridGroup: specificGroup,
partOfNavigation: false,
enumerable: false,
defaultValue: []
},
makeDataPropertyInfo("showTitle", {}, "boolean"),
makeDataPropertyInfo("showLegend", {}, "boolean"),
makeDataPropertyInfo(
"showXAxis",
{ displayName: "Show X axis" },
"boolean"
),
makeDataPropertyInfo(
"showYAxis",
{ displayName: "Show Y axis" },
"boolean"
),
makeDataPropertyInfo("showYAxis", {}, "boolean"),
makeDataPropertyInfo("showGrid", {}, "boolean"),
makeDataPropertyInfo("title", {}, "string"),
{
name: "yAxisRangeOption",
type: PropertyType.Enum,
enumItems: [
{
id: "floating",
label: "Floating"
},
{
id: "fixed",
label: "Fixed"
}
],
propertyGridGroup: specificGroup
},
makeDataPropertyInfo(
"yAxisRangeFrom",
{
disabled: (widget: LineChartEmbeddedWidget) =>
widget.yAxisRangeOption != "fixed"
},
"double"
),
makeDataPropertyInfo(
"yAxisRangeTo",
{
disabled: (widget: LineChartEmbeddedWidget) =>
widget.yAxisRangeOption != "fixed"
},
"double"
),
{
name: "maxPoints",
type: PropertyType.Number,
propertyGridGroup: specificGroup
},
{
name: "margin",
type: PropertyType.Object,
typeClass: RectObject,
propertyGridGroup: specificGroup,
enumerable: false
},
makeDataPropertyInfo("marker", {}, "float"),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("titleStyle"),
makeStylePropertyInfo("legendStyle"),
makeStylePropertyInfo("xAxisStyle"),
makeStylePropertyInfo("yAxisStyle"),
makeStylePropertyInfo("markerStyle")
],
beforeLoadHook: (
widget: LineChartEmbeddedWidget,
jsWidget: Partial
) => {
if (jsWidget.showTitle == undefined) {
jsWidget.showTitle = "true";
}
if (jsWidget.showXAxis == undefined) {
jsWidget.showXAxis = "true";
}
if (jsWidget.showYAxis == undefined) {
jsWidget.showYAxis = "true";
}
if (jsWidget.showGrid == undefined) {
jsWidget.showGrid = "true";
}
if (jsWidget.marker == undefined) {
jsWidget.marker = "null";
}
if (jsWidget.markerStyle == undefined) {
(jsWidget as any).markerStyle = {
useStyle: "default"
};
}
migrateStyleProperty(jsWidget, "titleStyle");
migrateStyleProperty(jsWidget, "legendStyle");
migrateStyleProperty(jsWidget, "xAxisStyle");
migrateStyleProperty(jsWidget, "yAxisStyle");
migrateStyleProperty(jsWidget, "markerStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 320,
height: 160,
xValue: "Date.now()",
lines: [],
title: "",
showTitle: "true",
showLegend: "true",
showXAxis: "true",
showYAxis: "true",
showGrid: "true",
yAxisRangeOption: "floating",
yAxisRangeFrom: 0,
yAxisRangeTo: 10,
maxPoints: 40,
minRange: 0,
maxRange: 1,
margin: {
top: 50,
right: 0,
bottom: 50,
left: 50
},
marker: "null",
customInputs: [
{
name: "value",
type: "any"
}
],
titleStyle: {
useStyle: "default"
},
legendStyle: {
useStyle: "default"
},
xAxisStyle: {
useStyle: "default"
},
yAxisStyle: {
useStyle: "default",
alignHorizontal: "right"
},
markerStyle: {
useStyle: "default"
}
},
icon: LINE_CHART_ICON
});
xValue: string;
lines: LineChartLine[];
title: string;
showTitle: string;
showLegend: string;
showXAxis: string;
showYAxis: string;
showGrid: string;
yAxisRangeOption: "floating" | "fixed";
yAxisRangeFrom: string;
yAxisRangeTo: string;
maxPoints: number;
margin: RectObject;
marker: string;
titleStyle: Style;
legendStyle: Style;
xAxisStyle: Style;
yAxisStyle: Style;
markerStyle: Style;
override makeEditable() {
super.makeEditable();
makeObservable(this, {
xValue: observable,
lines: observable,
title: observable,
showTitle: observable,
showLegend: observable,
showXAxis: observable,
showYAxis: observable,
showGrid: observable,
yAxisRangeOption: observable,
yAxisRangeFrom: observable,
yAxisRangeTo: observable,
maxPoints: observable,
margin: observable,
marker: observable,
titleStyle: observable,
legendStyle: observable,
xAxisStyle: observable,
yAxisStyle: observable,
markerStyle: observable
});
}
getInputs() {
return [
{
name: "reset",
type: "any" as ValueType,
isSequenceInput: false,
isOptionalInput: true
},
...super.getInputs()
];
}
getTitle(flowContext: IFlowContext) {
if (!this.title) {
return undefined;
}
if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) {
return evalProperty(flowContext, this, "title");
}
return this.title;
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
interface Axis {
position: "x" | "y";
type: "date" | "number";
rect: {
x: number;
y: number;
w: number;
h: number;
};
min: number;
max: number;
offset: number;
scale: number;
ticksDelta: number;
}
function calcAutoTicks(axis: Axis, maxTicks: number) {
const pxStart =
axis.position == "x"
? axis.rect.x
: axis.rect.y + axis.rect.h;
const pxRange =
axis.position == "x"
? axis.rect.w
: -axis.rect.h;
let range = axis.max - axis.min;
const min = axis.min - 0.05 * range;
const max = axis.max + 0.05 * range;
range = max - min;
axis.scale = pxRange / range;
axis.offset = pxStart - min * axis.scale;
const x = range / maxTicks;
const exp = Math.floor(Math.log10(x));
const nx = x * Math.pow(10, -exp);
const ndelta = nx < 2 ? 2 : nx < 5 ? 5 : 10;
const delta = ndelta * Math.pow(10, exp);
axis.ticksDelta = delta;
}
const drawTitle = (
x: number,
y: number,
w: number,
h: number
) => {
const title = this.getTitle(flowContext);
if (title) {
draw.drawText(
ctx,
title,
x,
y,
w,
h,
this.titleStyle,
false
);
}
};
const LEGEND_ICON_WIDTH = 32;
const measLegendWidth = () => {
if (!showLegend) {
return { legendWidth: 0, legendLineHeight: 0 };
}
const legendFont = draw.styleGetFont(
this.legendStyle
);
if (!legendFont) {
return { legendWidth: 0, legendLineHeight: 0 };
}
let maxWidth = 0;
for (let i = 0; i < this.lines.length; i++) {
chart.legendLabels.push(`Trace ${i}`);
const width = draw.measureStr(
chart.legendLabels[i],
legendFont,
widgetRect.w - LEGEND_ICON_WIDTH
);
if (width > maxWidth) {
maxWidth = width;
}
}
return {
legendWidth: LEGEND_ICON_WIDTH + maxWidth,
legendLineHeight: legendFont.height
};
};
const drawLegend = (
x: number,
y: number,
w: number,
h: number
) => {
const legendFont = draw.styleGetFont(
this.legendStyle
);
if (!legendFont) {
return;
}
y += 0.5;
x = x + w - legendWidth;
for (let i = 0; i < this.lines.length; i++) {
const line = this.lines[i];
if (
y + legendLineHeight >
gridRect.y + gridRect.h
) {
break;
}
draw.setColor(line.color);
draw.fillRect(
ctx,
x,
y + (legendLineHeight - 2) / 2,
x + LEGEND_ICON_WIDTH - 4,
y + (legendLineHeight - 2) / 2 + 2
);
draw.fillCircle(
ctx,
x + (LEGEND_ICON_WIDTH - 4) / 2,
y + legendLineHeight / 2,
3
);
draw.drawText(
ctx,
chart.legendLabels[i],
x + LEGEND_ICON_WIDTH,
y,
legendWidth - LEGEND_ICON_WIDTH,
legendLineHeight,
this.legendStyle,
false
);
y += legendLineHeight;
}
};
const drawXAxis = (axis: Axis) => {
const from =
Math.ceil(axis.min / axis.ticksDelta) *
axis.ticksDelta;
const to =
Math.floor(axis.max / axis.ticksDelta) *
axis.ticksDelta;
const w = axis.ticksDelta * axis.scale;
for (let i = 0; i < 100; i++) {
const tick = from + i * axis.ticksDelta;
if (tick > to) break;
const x = axis.offset + tick * axis.scale;
draw.drawText(
ctx,
tick.toString(),
x - w / 2,
axis.rect.y,
w,
axis.rect.h,
this.xAxisStyle,
false
);
}
};
const drawYAxis = (axis: Axis) => {
const from =
Math.ceil(axis.min / axis.ticksDelta) *
axis.ticksDelta;
const to =
Math.floor(axis.max / axis.ticksDelta) *
axis.ticksDelta;
const h = Math.abs(axis.ticksDelta * axis.scale);
for (let i = 0; i < 100; i++) {
const tick = from + i * axis.ticksDelta;
if (tick > to) break;
const y = axis.offset + tick * axis.scale;
draw.drawText(
ctx,
tick.toString(),
axis.rect.x,
y - h / 2,
axis.rect.w,
h,
this.yAxisStyle,
false
);
}
};
const drawGrid = () => {
const drawVerticalGrid = (
y: number,
h: number,
axis: Axis
) => {
const from =
Math.ceil(axis.min / axis.ticksDelta) *
axis.ticksDelta;
const to =
Math.floor(axis.max / axis.ticksDelta) *
axis.ticksDelta;
for (let i = 0; i < 100; i++) {
const tick = from + i * axis.ticksDelta;
if (tick > to) break;
const x = axis.offset + tick * axis.scale;
draw.drawVLine(ctx, x, y, h);
}
};
const drawHorizontalGrid = (
x: number,
w: number,
axis: Axis
) => {
const from =
Math.ceil(axis.min / axis.ticksDelta) *
axis.ticksDelta;
const to =
Math.floor(axis.max / axis.ticksDelta) *
axis.ticksDelta;
for (let i = 0; i < 100; i++) {
const tick = from + i * axis.ticksDelta;
if (tick > to) break;
const y = axis.offset + tick * axis.scale;
draw.drawHLine(ctx, x, y, w);
}
};
draw.setColor(this.style.borderColorProperty);
drawVerticalGrid(
gridRect.y,
gridRect.h,
chart.xAxis
);
drawHorizontalGrid(
gridRect.x,
gridRect.w,
chart.yAxis
);
};
const drawLines = () => {
ctx.beginPath();
ctx.rect(
gridRect.x,
gridRect.y,
gridRect.w,
gridRect.h
);
ctx.clip();
for (
let lineIndex = 0;
lineIndex < this.lines.length;
lineIndex++
) {
const line = this.lines[lineIndex];
ctx.beginPath();
ctx.moveTo(
chart.xAxis.offset +
chart.x[0] * chart.xAxis.scale,
chart.yAxis.offset +
chart.lines[lineIndex].y[0] *
chart.yAxis.scale
);
for (
let pointIndex = 1;
pointIndex < this.xValue.length - 1;
pointIndex++
) {
ctx.lineTo(
chart.xAxis.offset +
chart.x[pointIndex] *
chart.xAxis.scale,
chart.yAxis.offset +
chart.lines[lineIndex].y[
pointIndex
] *
chart.yAxis.scale
);
}
ctx.strokeStyle = line.color;
ctx.lineWidth = line.width;
ctx.stroke();
}
};
const chart: {
legendLabels: string[];
xAxis: Axis;
yAxis: Axis;
x: number[];
lines: {
y: number[];
}[];
} = {
legendLabels: [],
xAxis: {
position: "x",
type: "number",
rect: { x: 0, y: 0, w: 0, h: 0 },
min: 0,
max: 0,
offset: 0,
scale: 1.0,
ticksDelta: 0
},
yAxis: {
position: "y",
type: "number",
rect: { x: 0, y: 0, w: 0, h: 0 },
min: 0,
max: 0,
offset: 0,
scale: 1.0,
ticksDelta: 0
},
x: [1, 2, 3, 4],
lines: this.lines.map((line, i) => ({
y: [
i + 1,
(i + 1) * 2,
(i + 1) * 3,
(i + 1) * 4
]
}))
};
chart.xAxis.min = Math.min(...chart.x);
chart.xAxis.max = Math.max(...chart.x);
if (chart.xAxis.min >= chart.xAxis.max) {
chart.xAxis.min = 0;
chart.xAxis.max = 1;
}
if (this.yAxisRangeOption == "fixed") {
try {
const result = evalConstantExpression(
ProjectEditor.getProject(this),
this.yAxisRangeFrom
);
chart.yAxis.min =
typeof result.value == "number"
? result.value
: 0;
} catch (err) {
chart.yAxis.min = 0;
}
} else {
chart.yAxis.min = Math.min(
...chart.lines.map(line => Math.min(...line.y))
);
}
if (this.yAxisRangeOption == "fixed") {
try {
const result = evalConstantExpression(
ProjectEditor.getProject(this),
this.yAxisRangeTo
);
chart.yAxis.max =
typeof result.value == "number"
? result.value
: 10;
} catch {
chart.yAxis.max = 10;
}
} else {
chart.yAxis.max = Math.max(
...chart.lines.map(line => Math.max(...line.y))
);
}
if (chart.yAxis.min >= chart.yAxis.max) {
chart.yAxis.min = 0;
chart.yAxis.max = 1;
}
const widgetRect = {
x: 0,
y: 0,
w: width,
h: height
};
let showTitle: boolean;
try {
const result = evalConstantExpression(
ProjectEditor.getProject(this),
this.showTitle
);
showTitle = result.value ? true : false;
} catch (err) {
showTitle = false;
}
let showLegend: boolean;
try {
const result = evalConstantExpression(
ProjectEditor.getProject(this),
this.showLegend
);
showLegend = result.value ? true : false;
} catch (err) {
showLegend = false;
}
let showXAxis: boolean;
try {
const result = evalConstantExpression(
ProjectEditor.getProject(this),
this.showXAxis
);
showXAxis = result.value ? true : false;
} catch (err) {
showXAxis = false;
}
let showYAxis: boolean;
try {
const result = evalConstantExpression(
ProjectEditor.getProject(this),
this.showYAxis
);
showYAxis = result.value ? true : false;
} catch (err) {
showYAxis = false;
}
let showGrid: boolean;
try {
const result = evalConstantExpression(
ProjectEditor.getProject(this),
this.showGrid
);
showGrid = result.value ? true : false;
} catch (err) {
showGrid = false;
}
const { legendWidth, legendLineHeight } =
measLegendWidth();
const marginLeft =
this.margin.left +
this.style.borderSizeRect.left +
Math.max(
this.style.borderRadiusSpec.topLeftX,
this.style.borderRadiusSpec.bottomLeftX
);
const marginTop =
this.margin.top +
this.style.borderSizeRect.top +
Math.max(
this.style.borderRadiusSpec.topLeftY,
this.style.borderRadiusSpec.topRightY
);
const marginRight =
Math.max(this.margin.right, legendWidth) +
this.style.borderSizeRect.right +
Math.max(
this.style.borderRadiusSpec.topRightX,
this.style.borderRadiusSpec.bottomRightX
);
const marginBottom =
this.margin.bottom +
this.style.borderSizeRect.bottom +
Math.max(
this.style.borderRadiusSpec.bottomLeftY,
this.style.borderRadiusSpec.bottomRightY
);
let gridRect = {
x: widgetRect.x + marginLeft,
y: widgetRect.y + marginTop,
w: widgetRect.w - (marginLeft + marginRight),
h: widgetRect.h - (marginTop + marginBottom)
};
chart.xAxis.rect.x = gridRect.x;
chart.xAxis.rect.y = gridRect.y + gridRect.h;
chart.xAxis.rect.w = gridRect.w;
chart.xAxis.rect.h = this.margin.bottom;
chart.yAxis.rect.x =
widgetRect.x + marginLeft - this.margin.left;
chart.yAxis.rect.y = gridRect.y;
chart.yAxis.rect.w = this.margin.left;
chart.yAxis.rect.h = gridRect.h;
const xAxisFont = draw.styleGetFont(this.xAxisStyle);
let xAxisLabelWidth;
if (xAxisFont) {
xAxisLabelWidth = draw.measureStr(
"12345",
xAxisFont,
gridRect.w
);
} else {
xAxisLabelWidth = 50;
}
calcAutoTicks(
chart.xAxis,
Math.round(gridRect.w / xAxisLabelWidth)
);
const yAxisFont = draw.styleGetFont(this.yAxisStyle);
let yAxisLabelHeight;
if (yAxisFont) {
yAxisLabelHeight = Math.round(
yAxisFont.height * 1.25
);
} else {
yAxisLabelHeight = 25;
}
calcAutoTicks(
chart.yAxis,
Math.round(gridRect.h / yAxisLabelHeight)
);
draw.drawBackground(
ctx,
widgetRect.x,
widgetRect.y,
widgetRect.w,
widgetRect.h,
this.style,
true
);
if (showTitle) {
drawTitle(
widgetRect.x,
widgetRect.y,
widgetRect.w,
marginTop
);
}
if (showLegend) {
drawLegend(
gridRect.x + gridRect.w,
gridRect.y,
marginRight,
gridRect.h
);
}
if (showXAxis) {
drawXAxis(chart.xAxis);
}
if (showYAxis) {
drawYAxis(chart.yAxis);
}
if (showGrid) {
drawGrid();
}
drawLines();
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// title
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "title"));
// showTitle
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "showTitle"));
// showLegend
dataBuffer.writeInt16(
assets.getWidgetDataItemIndex(this, "showLegend")
);
// showXAxis
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "showXAxis"));
// showYAxis
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "showYAxis"));
// showGrid
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "showGrid"));
// yAxisRangeFrom
dataBuffer.writeInt16(this.yAxisRangeOption == "fixed" ? 0 : 1);
// yAxisRangeFrom
dataBuffer.writeInt16(
assets.getWidgetDataItemIndex(this, "yAxisRangeFrom")
);
// yAxisRangeTo
dataBuffer.writeInt16(
assets.getWidgetDataItemIndex(this, "yAxisRangeTo")
);
// margin
dataBuffer.writeInt16(this.margin.left);
dataBuffer.writeInt16(this.margin.top);
dataBuffer.writeInt16(this.margin.right);
dataBuffer.writeInt16(this.margin.bottom);
// marker
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "marker"));
// titleStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "titleStyle"));
// legendStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "legendStyle"));
// xAxisStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "xAxisStyle"));
// yAxisStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "yAxisStyle"));
// markerStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "markerStyle"));
// component index
dataBuffer.writeUint16(assets.getComponentIndex(this));
}
buildFlowComponentSpecific(assets: Assets, dataBuffer: DataBuffer) {
// maxPoints
dataBuffer.writeUint32(this.maxPoints);
// xValue
dataBuffer.writeObjectOffset(() =>
buildExpression(assets, dataBuffer, this, this.xValue)
);
// lines
dataBuffer.writeArray(this.lines, line => {
dataBuffer.writeObjectOffset(() =>
buildExpression(assets, dataBuffer, this, line.label)
);
let color = assets.getColorIndexFromColorValue(line.color);
if (isNaN(color)) {
color = 0;
}
dataBuffer.writeUint16(color);
dataBuffer.writeUint16(0);
dataBuffer.writeFloat(line.width);
dataBuffer.writeObjectOffset(() =>
buildExpression(assets, dataBuffer, this, line.value)
);
});
}
}
registerClass("LineChartEmbeddedWidget", LineChartEmbeddedWidget);
////////////////////////////////////////////////////////////////////////////////
export class GaugeEmbeddedWidget extends Widget {
min: string;
max: string;
threshold: string;
unit: string;
barStyle: Style;
valueStyle: Style;
ticksStyle: Style;
thresholdStyle: Style;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_GAUGE,
properties: [
makeDataPropertyInfo("data"),
makeDataPropertyInfo("min"),
makeDataPropertyInfo("max"),
makeDataPropertyInfo("threshold"),
makeDataPropertyInfo("unit"),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("barStyle"),
makeStylePropertyInfo("valueStyle"),
makeStylePropertyInfo("ticksStyle"),
makeStylePropertyInfo("thresholdStyle")
],
beforeLoadHook: (
widget: GaugeEmbeddedWidget,
jsWidget: Partial
) => {
migrateStyleProperty(jsWidget, "barStyle");
migrateStyleProperty(jsWidget, "valueStyle");
migrateStyleProperty(jsWidget, "ticksStyle");
migrateStyleProperty(jsWidget, "thresholdStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 128,
height: 128
},
icon: GAUGE_ICON,
check: (object: CanvasWidget, messages: IMessage[]) => {
if (!object.data) {
messages.push(propertyNotSetMessage(object, "data"));
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
min: observable,
max: observable,
threshold: observable,
unit: observable,
barStyle: observable,
valueStyle: observable,
ticksStyle: observable,
thresholdStyle: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
let widget = this;
let style = widget.style;
// draw border
function arcBorder(
ctx: CanvasRenderingContext2D,
xCenter: number,
yCenter: number,
radOuter: number,
radInner: number
) {
if (radOuter < 0 || radInner < 0) {
return;
}
ctx.moveTo(xCenter - radOuter, yCenter);
ctx.arcTo(
xCenter - radOuter,
yCenter - radOuter,
xCenter + radOuter,
yCenter - radOuter,
radOuter
);
ctx.arcTo(
xCenter + radOuter,
yCenter - radOuter,
xCenter + radOuter,
yCenter,
radOuter
);
ctx.lineTo(xCenter + radInner, yCenter);
ctx.arcTo(
xCenter + radInner,
yCenter - radInner,
xCenter - radInner,
yCenter - radInner,
radInner
);
ctx.arcTo(
xCenter - radInner,
yCenter - radInner,
xCenter - radInner,
yCenter,
radInner
);
ctx.lineTo(xCenter - radOuter, yCenter);
}
// draw bar
function arcBar(
ctx: CanvasRenderingContext2D,
xCenter: number,
yCenter: number,
rad: number
) {
if (rad < 0) {
return;
}
ctx.moveTo(xCenter - rad, yCenter);
ctx.arcTo(
xCenter - rad,
yCenter - rad,
xCenter + rad,
yCenter - rad,
rad
);
ctx.arcTo(
xCenter + rad,
yCenter - rad,
xCenter + rad,
yCenter,
rad
);
}
function firstTick(n: number) {
const p = Math.pow(10, Math.floor(Math.log10(n / 6)));
let f = n / 6 / p;
let i;
if (f > 5) {
i = 10;
} else if (f > 2) {
i = 5;
} else {
i = 2;
}
return i * p;
}
const drawGauge = (
ctx: CanvasRenderingContext2D,
width: number,
height: number
) => {
// min
let min;
let max;
let value;
let threshold;
let unit;
try {
min = evalProperty(flowContext, this, "min");
max = evalProperty(flowContext, this, "max");
value = evalProperty(flowContext, this, "data");
threshold = evalProperty(flowContext, this, "threshold");
unit = evalProperty(flowContext, this, "unit");
} catch (err) {
//console.error(err);
}
if (
!(typeof min == "number") ||
isNaN(min) ||
!isFinite(min) ||
!(typeof max == "number") ||
isNaN(max) ||
!isFinite(max) ||
!(typeof value == "number") ||
isNaN(value) ||
!isFinite(value) ||
min >= max
) {
min = 0;
max = 1.0;
value = 0;
} else {
if (value < min) {
value = min;
} else if (value > max) {
value = max;
}
}
let valueStyle = widget.valueStyle;
let barStyle = widget.barStyle;
let ticksStyle = widget.ticksStyle;
let thresholdStyle = widget.thresholdStyle;
let w = width;
let h = height;
draw.drawBackground(ctx, 0, 0, width, height, this.style, true);
const PADDING_HORZ = 56;
const TICK_LINE_LENGTH = 5;
const TICK_LINE_WIDTH = 1;
const TICK_TEXT_GAP = 1;
const THRESHOLD_LINE_WIDTH = 2;
const xCenter = w / 2;
const yCenter = h - 8;
// draw border
const radBorderOuter = (w - PADDING_HORZ) / 2;
const BORDER_WIDTH = Math.round(radBorderOuter / 3);
const BAR_WIDTH = BORDER_WIDTH / 2;
const radBorderInner = radBorderOuter - BORDER_WIDTH;
ctx.beginPath();
ctx.strokeStyle = style.colorProperty;
ctx.lineWidth = 1.5;
arcBorder(ctx, xCenter, yCenter, radBorderOuter, radBorderInner);
ctx.stroke();
// draw bar
const radBar = (w - PADDING_HORZ) / 2 - BORDER_WIDTH / 2;
const angle = remap(value, min, 0.0, max, 180.0);
ctx.beginPath();
ctx.strokeStyle = barStyle.colorProperty;
ctx.lineWidth = BAR_WIDTH;
ctx.save();
ctx.setLineDash([
(radBar * angle * Math.PI) / 180,
radBar * Math.PI
]);
arcBar(ctx, xCenter, yCenter, radBar);
ctx.stroke();
ctx.restore();
// draw threshold
const thresholdAngleDeg = remap(threshold, min, 180.0, max, 0);
if (thresholdAngleDeg >= 0 && thresholdAngleDeg <= 180.0) {
const tickAngle = (thresholdAngleDeg * Math.PI) / 180;
const x1 =
xCenter + (radBar - BAR_WIDTH / 2) * Math.cos(tickAngle);
const y1 =
yCenter - (radBar - BAR_WIDTH / 2) * Math.sin(tickAngle);
const x2 =
xCenter + (radBar + BAR_WIDTH / 2) * Math.cos(tickAngle);
const y2 =
yCenter - (radBar + BAR_WIDTH / 2) * Math.sin(tickAngle);
ctx.beginPath();
ctx.strokeStyle = thresholdStyle.colorProperty;
ctx.lineWidth = THRESHOLD_LINE_WIDTH;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
// draw ticks
const ticksfont = draw.styleGetFont(ticksStyle);
const ft = firstTick(max - min);
const ticksRad = radBorderOuter + 1;
for (let tickValueIndex = 0; ; tickValueIndex++) {
const tickValue = roundNumber(min + tickValueIndex * ft, 9);
if (tickValue > max) {
break;
}
const tickAngleDeg = remap(tickValue, min, 180.0, max, 0.0);
if (tickAngleDeg <= 180.0) {
const tickAngle = (tickAngleDeg * Math.PI) / 180;
const x1 = xCenter + ticksRad * Math.cos(tickAngle);
const y1 = yCenter - ticksRad * Math.sin(tickAngle);
const x2 =
xCenter +
(ticksRad + TICK_LINE_LENGTH) * Math.cos(tickAngle);
const y2 =
yCenter -
(ticksRad + TICK_LINE_LENGTH) * Math.sin(tickAngle);
ctx.beginPath();
ctx.strokeStyle = ticksStyle.colorProperty;
ctx.lineWidth = TICK_LINE_WIDTH;
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
if (ticksfont) {
const tickText = unit
? `${tickValue} ${unit}`
: tickValue.toString();
const tickTextWidth = draw.measureStr(
tickText,
ticksfont,
-1
);
if (tickAngleDeg == 180.0) {
draw.drawText(
ctx,
tickText,
xCenter -
radBorderOuter -
TICK_TEXT_GAP -
tickTextWidth,
y2 - TICK_TEXT_GAP - ticksfont.ascent,
tickTextWidth,
ticksfont.ascent,
ticksStyle,
false
);
} else if (tickAngleDeg > 90.0) {
draw.drawText(
ctx,
tickText,
x2 - TICK_TEXT_GAP - tickTextWidth,
y2 - TICK_TEXT_GAP - ticksfont.ascent,
tickTextWidth,
ticksfont.ascent,
ticksStyle,
false
);
} else if (tickAngleDeg == 90.0) {
draw.drawText(
ctx,
tickText,
x2 - tickTextWidth / 2,
y2 - TICK_TEXT_GAP - ticksfont.ascent,
tickTextWidth,
ticksfont.ascent,
ticksStyle,
false
);
} else if (tickAngleDeg > 0) {
draw.drawText(
ctx,
tickText,
x2 + TICK_TEXT_GAP,
y2 - TICK_TEXT_GAP - ticksfont.ascent,
tickTextWidth,
ticksfont.ascent,
ticksStyle,
false
);
} else {
draw.drawText(
ctx,
tickText,
xCenter + radBorderOuter + TICK_TEXT_GAP,
y2 - TICK_TEXT_GAP - ticksfont.ascent,
tickTextWidth,
ticksfont.ascent,
ticksStyle,
false
);
}
}
}
}
// draw value
const font = draw.styleGetFont(valueStyle);
if (font) {
const valueText = unit ? `${value} ${unit}` : value.toString();
const valueTextWidth = draw.measureStr(valueText, font, -1);
draw.drawText(
ctx,
valueText,
xCenter - valueTextWidth / 2,
yCenter - font.height,
valueTextWidth,
font.height,
valueStyle,
false
);
}
};
return (
<>
drawGauge(ctx, width, height)
}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// min
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "min"));
// max
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "max"));
// threshold
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "threshold"));
// unit
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "unit"));
// barStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "barStyle"));
// valueStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "valueStyle"));
// ticksStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "ticksStyle"));
// thresholdStyle
dataBuffer.writeInt16(assets.getStyleIndex(this, "thresholdStyle"));
}
}
registerClass("GaugeEmbeddedWidget", GaugeEmbeddedWidget);
////////////////////////////////////////////////////////////////////////////////
export class InputEmbeddedWidget extends Widget {
inputType: "text" | "number";
password: boolean;
min: string;
max: string;
precision: string;
unit: string;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Input",
flowComponentId: WIDGET_TYPE_INPUT,
properties: [
makeDataPropertyInfo("data", {}, "any"),
{
name: "inputType",
type: PropertyType.Enum,
propertyGridGroup: specificGroup,
enumItems: [
{
id: "number"
},
{
id: "text"
}
]
},
{
...makeDataPropertyInfo("min"),
displayName: (widget: InputEmbeddedWidget) =>
widget.inputType === "text" ? "Min (chars)" : "Min"
},
{
...makeDataPropertyInfo("max"),
displayName: (widget: InputEmbeddedWidget) =>
widget.inputType === "text" ? "Max (chars)" : "Max"
},
{
...makeDataPropertyInfo("precision"),
disabled: (widget: InputEmbeddedWidget) =>
widget.inputType != "number"
},
{
...makeDataPropertyInfo("unit"),
disabled: (widget: InputEmbeddedWidget) =>
widget.inputType != "number"
},
{
name: "password",
type: PropertyType.Boolean,
disabled: (widget: InputEmbeddedWidget) =>
widget.inputType != "text",
propertyGridGroup: specificGroup
},
makeStylePropertyInfo("style", "Default style")
],
defaultValue: {
left: 0,
top: 0,
width: 120,
height: 40,
inputType: "number"
},
icon: (
),
check: (widget: InputEmbeddedWidget, messages: IMessage[]) => {
if (!widget.data) {
messages.push(propertyNotSetMessage(widget, "data"));
}
if (!widget.min) {
messages.push(propertyNotSetMessage(widget, "min"));
}
if (!widget.max) {
messages.push(propertyNotSetMessage(widget, "min"));
}
if (widget.type === "number") {
if (!widget.precision) {
messages.push(propertyNotSetMessage(widget, "precision"));
}
}
if (widget.type === "number") {
if (!widget.unit) {
messages.push(propertyNotSetMessage(widget, "unit"));
}
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
inputType: observable,
password: observable,
min: observable,
max: observable,
precision: observable,
unit: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
let text;
if (flowContext.flowState) {
if (this.data) {
try {
text = evalProperty(
flowContext,
this,
"data"
);
} catch (err) {
//console.error(err);
text = "";
}
} else {
text = "";
}
} else {
text = `{${this.data}}`;
}
let unit;
if (flowContext.flowState) {
if (this.unit) {
try {
unit = evalProperty(
flowContext,
this,
"unit"
);
} catch (err) {
//console.error(err);
unit = "";
}
} else {
unit = "";
}
} else {
unit = "";
}
draw.drawText(
ctx,
text + (unit ? " " + unit : ""),
0,
0,
width,
height,
this.style,
false
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
// flags
let flags = 0;
const INPUT_WIDGET_TYPE_TEXT = 0x0001;
const INPUT_WIDGET_TYPE_NUMBER = 0x0002;
const INPUT_WIDGET_PASSWORD_FLAG = 0x0100;
if (this.inputType === "text") {
flags |= INPUT_WIDGET_TYPE_TEXT;
if (this.password) {
flags |= INPUT_WIDGET_PASSWORD_FLAG;
}
} else if (this.inputType === "number") {
flags |= INPUT_WIDGET_TYPE_NUMBER;
}
dataBuffer.writeUint16(flags);
// min
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "min"));
// max
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "max"));
// precision
dataBuffer.writeInt16(
this.inputType === "text"
? 0
: assets.getWidgetDataItemIndex(this, "precision")
);
// unit
dataBuffer.writeInt16(
this.inputType === "text"
? 0
: assets.getWidgetDataItemIndex(this, "unit")
);
// component index
dataBuffer.writeUint16(assets.getComponentIndex(this));
}
}
registerClass("InputEmbeddedWidget", InputEmbeddedWidget);
////////////////////////////////////////////////////////////////////////////////
export class RollerWidget extends Widget {
min: string;
max: string;
text: string;
selectedValueStyle: Style;
unselectedValueStyle: Style;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Input",
flowComponentId: WIDGET_TYPE_ROLLER,
properties: [
makeDataPropertyInfo("data", {}, "integer"),
makeDataPropertyInfo("min"),
makeDataPropertyInfo("max"),
makeDataPropertyInfo("text"),
makeStylePropertyInfo("style", "Default style"),
makeStylePropertyInfo("selectedValueStyle"),
makeStylePropertyInfo("unselectedValueStyle")
],
beforeLoadHook: (
widget: RollerWidget,
jsWidget: Partial
) => {
migrateStyleProperty(jsWidget, "selectedValueStyle");
migrateStyleProperty(jsWidget, "unselectedValueStyle");
},
defaultValue: {
left: 0,
top: 0,
width: 40,
height: 120
},
componentDefaultValue: (projectStore: ProjectStore) => {
return projectStore.projectTypeTraits.isFirmwareModule ||
projectStore.projectTypeTraits.isApplet ||
projectStore.projectTypeTraits.isResource
? {
style: {
useStyle: "default"
},
selectedValueStyle: {
useStyle: "default"
},
unselectedValueStyle: {
useStyle: "default"
}
}
: projectStore.projectTypeTraits.isFirmware
? {
style: {
useStyle: "roller"
},
selectedValueStyle: {
useStyle: "roller_selected_value"
},
unselectedValueStyle: {
useStyle: "roller_unselected_value"
}
}
: {};
},
icon: (
)
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
min: observable,
max: observable,
text: observable,
selectedValueStyle: observable,
unselectedValueStyle: observable
});
}
getInputs() {
return [
{
name: "clear",
type: "any" as ValueType,
isSequenceInput: true,
isOptionalInput: true
},
...super.getInputs()
];
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
draw.drawBackground(
ctx,
0,
0,
width,
height,
this.style,
true
);
const font = draw.styleGetFont(this.selectedValueStyle);
if (!font) {
return;
}
const selectedValueHeight =
this.selectedValueStyle.borderSizeRect.top +
this.selectedValueStyle.paddingRect.top +
font.height +
this.selectedValueStyle.paddingRect.bottom +
this.selectedValueStyle.borderSizeRect.bottom;
draw.drawBackground(
ctx,
0,
(height - selectedValueHeight) / 2,
width,
selectedValueHeight,
this.selectedValueStyle,
true
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "min"));
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "max"));
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "text"));
dataBuffer.writeInt16(assets.getStyleIndex(this, "selectedValueStyle"));
dataBuffer.writeInt16(
assets.getStyleIndex(this, "unselectedValueStyle")
);
// component index
dataBuffer.writeUint16(assets.getComponentIndex(this));
}
}
registerClass("RollerWidget", RollerWidget);
////////////////////////////////////////////////////////////////////////////////
export class SwitchWidget extends Widget {
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Input",
flowComponentId: WIDGET_TYPE_SWITCH,
properties: [
makeDataPropertyInfo("data", {}, "boolean"),
makeStylePropertyInfo("style", "Default style")
],
defaultValue: {
left: 0,
top: 0,
width: 64,
height: 32
},
componentDefaultValue: (projectStore: ProjectStore) => {
return projectStore.projectTypeTraits.isFirmwareModule ||
projectStore.projectTypeTraits.isApplet ||
projectStore.projectTypeTraits.isResource
? {
style: {
useStyle: "default"
}
}
: projectStore.projectTypeTraits.isFirmware
? {
style: {
useStyle: "switch"
}
}
: {};
},
icon: SWITCH_WIDGET_ICON
});
override render(flowContext: IFlowContext, width: number, height: number) {
const enabled = getBooleanValue(flowContext, this, "data", false);
return (
<>
{
let x = this.style.paddingRect.left;
let y = this.style.paddingRect.top;
let w =
width -
this.style.paddingRect.left -
this.style.paddingRect.right;
let h =
height -
this.style.paddingRect.top -
this.style.paddingRect.bottom;
draw.setColor(this.style.borderColorProperty);
draw.setBackColor(
enabled
? this.style.activeBackgroundColorProperty
: this.style.backgroundColorProperty
);
draw.fillRoundedRect(
ctx,
x,
y,
x + w - 1,
y + h - 1,
this.style.borderSizeRect.left,
h / 2
);
h -= 2 * (2 + this.style.borderSizeRect.left);
y += 2 + this.style.borderSizeRect.left;
if (enabled) {
x += w - h - (1 + this.style.borderSizeRect.left);
} else {
x += 1 + this.style.borderSizeRect.left;
}
w = h;
draw.setBackColor(this.style.colorProperty);
draw.fillRoundedRect(
ctx,
x,
y,
x + w - 1,
y + h - 1,
1,
h / 2 + 2
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
}
registerClass("SwitchWidget", SwitchWidget);
////////////////////////////////////////////////////////////////////////////////
export class SliderWidget extends Widget {
min: string;
max: string;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Input",
flowComponentId: WIDGET_TYPE_SLIDER,
properties: [
makeDataPropertyInfo("data", {}, "integer"),
makeDataPropertyInfo("min"),
makeDataPropertyInfo("max"),
makeStylePropertyInfo("style", "Default style")
],
defaultValue: {
left: 0,
top: 0,
width: 120,
height: 32
},
componentDefaultValue: (projectStore: ProjectStore) => {
return projectStore.projectTypeTraits.isFirmwareModule ||
projectStore.projectTypeTraits.isApplet ||
projectStore.projectTypeTraits.isResource
? {
style: {
useStyle: "default"
}
}
: projectStore.projectTypeTraits.isFirmware
? {
style: {
useStyle: "slider"
}
}
: {};
},
icon: (
)
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
min: observable,
max: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
let x = this.style.paddingRect.left;
let y = this.style.paddingRect.top;
let w =
width -
this.style.paddingRect.left -
this.style.paddingRect.right;
let h =
height -
this.style.paddingRect.top -
this.style.paddingRect.bottom;
const barX = x + h / 2.0;
const barW = w - h;
const barH = (h * 8.0) / 20.0;
const barY = y + (h - barH) / 2;
const barBorderRadius = barH / 2.0;
let value = getNumberValue(
flowContext,
this,
"data",
0.5
);
let min = getNumberValue(flowContext, this, "min", 0);
let max = getNumberValue(flowContext, this, "max", 1.0);
let knobRelativePosition = (value - min) / (max - min);
if (knobRelativePosition < 0) knobRelativePosition = 0;
if (knobRelativePosition > 1.0)
knobRelativePosition = 1.0;
const knobPosition =
barX + knobRelativePosition * (barW - 1);
const knobRadius = h / 2;
const knobX = knobPosition;
const knobY = y;
const knobW = h;
const knobH = h;
draw.setBackColor(this.style.backgroundColorProperty);
draw.fillRoundedRect(
ctx,
barX - barBorderRadius,
barY,
barX + barW + barBorderRadius - 1,
barY + barH - 1,
0,
barBorderRadius
);
draw.setBackColor(this.style.colorProperty);
draw.fillRoundedRect(
ctx,
knobX - knobRadius,
knobY,
knobX - knobRadius + knobW - 1,
knobY + knobH - 1,
0,
knobRadius
);
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "min"));
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "max"));
}
}
registerClass("SliderWidget", SliderWidget);
////////////////////////////////////////////////////////////////////////////////
export class DropDownListWidget extends Widget {
options: string;
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Input",
componentPaletteLabel: "Dropdown",
flowComponentId: WIDGET_TYPE_DROP_DOWN_LIST,
properties: [
makeDataPropertyInfo("data", {}, "integer"),
makeDataPropertyInfo("options"),
makeStylePropertyInfo("style", "Default style")
],
defaultValue: {
left: 0,
top: 0,
width: 120,
height: 32
},
componentDefaultValue: (projectStore: ProjectStore) => {
return projectStore.projectTypeTraits.isFirmwareModule ||
projectStore.projectTypeTraits.isApplet ||
projectStore.projectTypeTraits.isResource
? {
style: {
useStyle: "default"
}
}
: projectStore.projectTypeTraits.isFirmware
? {
style: {
useStyle: "drop_down_list"
}
}
: {};
},
icon: (
),
widgetEvents: {
ON_CHANGE: {
code: 1,
paramExpressionType: `struct:${DROP_DOWN_LIST_CHANGE_EVENT_STRUCT_NAME}`,
oldName: "action"
}
}
});
override makeEditable() {
super.makeEditable();
makeObservable(this, {
options: observable
});
}
override render(flowContext: IFlowContext, width: number, height: number) {
return (
<>
{
const { x1, y1, x2, y2 } = draw.drawBackground(
ctx,
0,
0,
width,
height,
this.style,
true
);
const options = getAnyValue(
flowContext,
this,
"options",
[]
);
let x = x1;
let y = y1;
let w = x2 - x1 + 1;
let h = y2 - y1 + 1;
draw.drawText(
ctx,
options.length > 0 && typeof options[0] == "string"
? options[0]
: "",
x,
y,
w - h + (2 * h) / 6,
h,
this.style,
false,
undefined,
true
);
x += w - h;
w = h;
x += (2 * h) / 6;
y += (4 * h) / 10;
w -= (2 * h) / 3;
h -= (4 * h) / 5;
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + w / 2, y + h);
ctx.lineTo(x + w, y);
ctx.strokeStyle = this.style.colorProperty;
ctx.lineWidth = h / 3;
ctx.stroke();
}}
/>
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
dataBuffer.writeInt16(assets.getWidgetDataItemIndex(this, "options"));
}
}
registerClass("DropDownListWidget", DropDownListWidget);
////////////////////////////////////////////////////////////////////////////////
export class QRCodeWidget extends Widget {
errorCorrection: any;
constructor() {
super();
makeObservable(this, {
errorCorrectionValue: computed
});
}
override makeEditable() {
super.makeEditable();
makeObservable(this, {
errorCorrection: observable
});
}
static classInfo = makeDerivedClassInfo(Widget.classInfo, {
enabledInComponentPalette: (projectType: ProjectType) =>
projectType !== ProjectType.LVGL &&
projectType !== ProjectType.DASHBOARD,
componentPaletteGroupName: "!1Visualiser",
flowComponentId: WIDGET_TYPE_QR_CODE,
properties: [
makeDataPropertyInfo("data", {
displayName: "Text"
}),
{
name: "errorCorrection",
type: PropertyType.Enum,
enumItems: [
{
id: "low"
},
{
id: "medium"
},
{
id: "quartile"
},
{
id: "high"
}
],
propertyGridGroup: specificGroup
},
makeStylePropertyInfo("style", "Default style")
],
defaultValue: {
left: 0,
top: 0,
width: 128,
height: 128,
errorCorrection: "medium"
},
icon: (
)
});
getText(flowContext: IFlowContext) {
if (!this.data) {
return undefined;
}
if (flowContext.projectStore.projectTypeTraits.hasFlowSupport) {
return evalProperty(flowContext, this, "data");
}
return this.data;
}
get errorCorrectionValue() {
if (this.errorCorrection == "low") return QRC.Ecc.LOW;
if (this.errorCorrection == "medium") return QRC.Ecc.MEDIUM;
if (this.errorCorrection == "quartile") return QRC.Ecc.QUARTILE;
return QRC.Ecc.HIGH;
}
styleHook(style: React.CSSProperties, flowContext: IFlowContext) {
super.styleHook(style, flowContext);
style.backgroundColor = to16bitsColor(
this.style.backgroundColorProperty
);
}
static toSvgString(
qr: any,
border: number,
lightColor: string,
darkColor: string
) {
let parts: Array = [];
for (let y = 0; y < qr.size; y++) {
for (let x = 0; x < qr.size; x++) {
if (qr.getModule(x, y))
parts.push(`M${x + border},${y + border}h1v1h-1z`);
}
}
return (
);
}
override render(flowContext: IFlowContext, width: number, height: number) {
const text = this.getText(flowContext) || "";
const qr0 = QRC.encodeText(text, this.errorCorrectionValue);
const svg = QRCodeWidget.toSvgString(
qr0,
1,
to16bitsColor(this.style.backgroundColorProperty),
to16bitsColor(this.style.colorProperty)
);
return (
<>
{svg}
{super.render(flowContext, width, height)}
>
);
}
buildFlowWidgetSpecific(assets: Assets, dataBuffer: DataBuffer) {
let errorCorrection;
if (this.errorCorrection == "low") return (errorCorrection = 0);
if (this.errorCorrection == "medium") return (errorCorrection = 1);
if (this.errorCorrection == "quartile") return (errorCorrection = 2);
return 3;
dataBuffer.writeUint8(errorCorrection);
}
}
registerClass("QRCodeWidget", QRCodeWidget);