From f569797982d6dfb5b3db65ad26572fc1657f4fb2 Mon Sep 17 00:00:00 2001 From: Gerardo Bolanos Date: Tue, 29 Jul 2025 11:57:28 -0700 Subject: [PATCH] IGN-12972: Report-component SDK example --- .../report/component/shapes/RMSmiley.java | 209 ++++++++++++++++++ .../src/main/resources/component.properties | 12 +- .../component/designer/DesignerHook.java | 4 + .../report/component/gateway/GatewayHook.java | 2 + 4 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 report-component/report-component-common/src/main/java/com/inductiveautomation/ignition/examples/report/component/shapes/RMSmiley.java diff --git a/report-component/report-component-common/src/main/java/com/inductiveautomation/ignition/examples/report/component/shapes/RMSmiley.java b/report-component/report-component-common/src/main/java/com/inductiveautomation/ignition/examples/report/component/shapes/RMSmiley.java new file mode 100644 index 00000000..85a14912 --- /dev/null +++ b/report-component/report-component-common/src/main/java/com/inductiveautomation/ignition/examples/report/component/shapes/RMSmiley.java @@ -0,0 +1,209 @@ +package com.inductiveautomation.ignition.examples.report.component.shapes; + +import com.inductiveautomation.reporting.common.api.shape.KeyBase; +import com.inductiveautomation.reporting.common.api.shape.ShapeMeta; +import com.inductiveautomation.reporting.common.api.shape.ShapeProperty; +import com.inductiveautomation.rm.archiver.RXArchiver; +import com.inductiveautomation.rm.archiver.RXElement; +import com.inductiveautomation.rm.shape.RMShape; +import com.inductiveautomation.rm.shape.ReportOwner; +import com.inductiveautomation.rm.shape.rm2dshapes.AbstractRM2DShape; +import java.awt.*; + +/** + * This class extends AbstractRM2DShape (itself extending {@link RMShape}) to provide an easy way to add + * simple shapes to the Report Design Palette. + *

+ * Use {@link ShapeMeta} to declare what category of the palette we want our component to be in, and then give the path + * to our component's icon resource, which has a base path of 'images/'. + */ +@ShapeMeta(category = "reporting.Category.Shapes", iconPath = "smiley") +@KeyBase("component.Shape.RMSmiley") +public class RMSmiley extends AbstractRM2DShape { + + /* + * Unique id of the serialization archive. Needs to be registered in gateway hook along with RMSmiley.class + */ + public static final String ARCHIVE_NAME = "report-rm-smiley-component"; + public static final int ORIGIN = 0; + private boolean isHappy = true; + private Color background = Color.YELLOW; + private Color foreground = Color.BLACK; + + @Override + protected void render(Graphics2D g2, int width, int height) { + // don't draw if superclass visibility is disabled + if (isVisible()) { + g2.setRenderingHint( + RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON + ); + g2.setRenderingHint( + RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR + ); + + // draw an outline + g2.setColor(foreground); + g2.drawOval(ORIGIN, ORIGIN, width - 1, height - 1); + + // fill in with background color + g2.setColor(background); + g2.fillOval(ORIGIN + 1, ORIGIN + 1, width - 2, height - 2); + + // eyes + g2.setColor(foreground); + g2.fillOval( + (int) (width * 0.25), + (int) (height * 0.3), + width / 6, + height / 6 + ); + g2.fillOval( + (int) (width * 0.60), + (int) (height * 0.3), + width / 6, + height / 6 + ); + + // values for a happy face + int arc = -180; + int mouthY = (int) (height * 0.28); + int mouthYBottom = (int) (height * 0.23); + int mouthWidth = (int) (width * 0.7); + int mouthHeight = (int) (height * 0.6); + int mouthX = (int) (width * .15); + + // alter mouth values for sad face + if (!isHappy) { + arc = 180; + int temp = mouthY; + mouthY = mouthYBottom + (int) (height * .4); + mouthYBottom = temp + (int) (height * .4); + mouthWidth = (int) (mouthWidth * .8); + mouthX = (int) (mouthX * 1.5); + mouthHeight = (int) (mouthHeight * .6); + } + + g2.fillArc(mouthX, mouthY, mouthWidth, mouthHeight, 0, arc); + // draw a second/offset arc for a better mouth appearance + g2.setColor(background); + g2.fillArc(mouthX, mouthYBottom, mouthWidth, mouthHeight, 0, arc); + } + } + + /* The ShapeProperty annotations work in concert with the KeyBase annotation on the class. By giving this + * method the annotation (and assuming we have both getter and setter), we enable this field to be added + * to the property table when designing/editing our component. So (keybase.Background, or in this case + * 'component.Shape.RMSmiley.Background' will be checked for a .Name, .Description and .Category to fill in + * values. Take a look at common/src/main/resources/component.properties to see examples. + */ + @ShapeProperty("Background") + public Color getBackground() { + return background; + } + + public void setBackground(Color background) { + if (!this.background.equals(background)) { + this.background = background; + setDirty(); + } + } + + @ShapeProperty("Foreground") + public Color getForeground() { + return foreground; + } + + public void setForeground(Color foreground) { + if (!this.foreground.equals(foreground)) { + this.foreground = foreground; + setDirty(); + } + } + + @ShapeProperty("Happy") + public boolean isHappy() { + return isHappy; + } + + public void setHappy(boolean isHappy) { + this.isHappy = isHappy; + setDirty(); + } + + /* override to add setDirty() call to trigger the render() when changed */ + @Override + public void setVisible(boolean aValue) { + super.setVisible(aValue); + setDirty(); + } + + /** + * Shapes and components go through an XML serialization process. To add things to the archiver, use + * {@link RXElement} and add(String, name), where the String is a key name you will use to pull the value + * from the archiver when deserializing. + */ + @Override + public RXElement toXML(RXArchiver anArchiver) { + RXElement e = super.toXML(anArchiver); + + e.setName(ARCHIVE_NAME); // set a unique name for this archive + + e.add("isHappy", isHappy); + e.add("background", background); + e.add("foreground", foreground); + return e; + } + + /** + * Pull data out of serialization by using the various RXElement.get() calls for your types. + */ + @Override + public RMShape fromXML(RXArchiver archiver, RXElement e) { + super.fromXML(archiver, e); + + background = e.getAttributeColorValue("background", Color.YELLOW); + foreground = e.getAttributeColorValue("foreground", Color.BLACK); + setHappy(e.getAttributeBooleanValue("isHappy", true)); + + return this; + } + + /** + * Called when the report is generated. If our shape were to require data to create/populate itself, we would do so + * in this method by building a dataset and instantiating a clone() of our shape with the associated dataset. + * In this case, we aren't doing anything computational, so our RPG can just return a clone; + */ + @Override + protected RMShape rpgShape(ReportOwner owner, RMShape shape) { + RMSmiley rpg = (RMSmiley) clone(); + + if (rpg == null) { + rpg = new RMSmiley(); + } + rpg.setIsRpg(true); // Enables vector rendering in the generated report + rpg.setHappy(isHappy); + rpg.setBackground(background); + rpg.setForeground(foreground); + rpg.setVisible(isVisible()); + return rpg; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!super.equals(obj)) { + return false; + } + + RMSmiley other = (RMSmiley) obj; + return !( + !other.background.equals(background) || + other.isHappy != isHappy || + other.foreground.equals(foreground) + ); + } +} diff --git a/report-component/report-component-common/src/main/resources/component.properties b/report-component/report-component-common/src/main/resources/component.properties index 8447fe42..0baf5123 100644 --- a/report-component/report-component-common/src/main/resources/component.properties +++ b/report-component/report-component-common/src/main/resources/component.properties @@ -6,4 +6,14 @@ Shape.Smiley.Background.Name=Background Shape.Smiley.Foreground.Category=Color Shape.Smiley.Foreground.Name=Foreground Shape.Smiley.Happy.Category=Fun -Shape.Smiley.Happy.Name=Is Happy \ No newline at end of file +Shape.Smiley.Happy.Name=Is Happy + +Shape.RMSmiley.Name=Smiley Face +Shape.RMSmiley.Category=Shape +Shape.RMSmiley.Description=Add a smiley face to your report. +Shape.RMSmiley.Background.Category=Color +Shape.RMSmiley.Background.Name=Background +Shape.RMSmiley.Foreground.Category=Color +Shape.RMSmiley.Foreground.Name=Foreground +Shape.RMSmiley.Happy.Category=Fun +Shape.RMSmiley.Happy.Name=Is Happy \ No newline at end of file diff --git a/report-component/report-component-designer/src/main/java/com/inductiveautomation/ignition/examples/report/component/designer/DesignerHook.java b/report-component/report-component-designer/src/main/java/com/inductiveautomation/ignition/examples/report/component/designer/DesignerHook.java index c87b1036..091c0ee9 100644 --- a/report-component/report-component-designer/src/main/java/com/inductiveautomation/ignition/examples/report/component/designer/DesignerHook.java +++ b/report-component/report-component-designer/src/main/java/com/inductiveautomation/ignition/examples/report/component/designer/DesignerHook.java @@ -5,6 +5,7 @@ import com.inductiveautomation.ignition.designer.model.AbstractDesignerModuleHook; import com.inductiveautomation.ignition.designer.model.DesignerContext; import com.inductiveautomation.ignition.designer.model.DesignerModuleHook; +import com.inductiveautomation.ignition.examples.report.component.shapes.RMSmiley; import com.inductiveautomation.reporting.designer.api.DesignerShapeRegistry; import com.inductiveautomation.ignition.examples.report.component.shapes.Smiley; import com.inductiveautomation.rm.archiver.RMArchiver; @@ -26,6 +27,9 @@ public void startup(DesignerContext context, LicenseState activationState) throw /* This is where our new shape registered for the Report Designer */ RMArchiver.registerClass(Smiley.ARCHIVE_NAME, Smiley.class); DesignerShapeRegistry.get(context).register(Smiley.class); + + RMArchiver.registerClass(RMSmiley.ARCHIVE_NAME, RMSmiley.class); + DesignerShapeRegistry.get(context).register(RMSmiley.class); } } diff --git a/report-component/report-component-gateway/src/main/java/com/inductiveautomation/ignition/examples/report/component/gateway/GatewayHook.java b/report-component/report-component-gateway/src/main/java/com/inductiveautomation/ignition/examples/report/component/gateway/GatewayHook.java index afdd82c5..357fe792 100644 --- a/report-component/report-component-gateway/src/main/java/com/inductiveautomation/ignition/examples/report/component/gateway/GatewayHook.java +++ b/report-component/report-component-gateway/src/main/java/com/inductiveautomation/ignition/examples/report/component/gateway/GatewayHook.java @@ -1,6 +1,7 @@ package com.inductiveautomation.ignition.examples.report.component.gateway; import com.inductiveautomation.ignition.common.licensing.LicenseState; +import com.inductiveautomation.ignition.examples.report.component.shapes.RMSmiley; import com.inductiveautomation.ignition.gateway.model.AbstractGatewayModuleHook; import com.inductiveautomation.ignition.gateway.model.GatewayContext; import com.inductiveautomation.ignition.examples.report.component.shapes.Smiley; @@ -21,6 +22,7 @@ public void setup(GatewayContext context) { public void startup(LicenseState activationState) { // shape classes need to be registered from the gateway and designer hooks RMArchiver.registerClass(Smiley.ARCHIVE_NAME, Smiley.class); + RMArchiver.registerClass(RMSmiley.ARCHIVE_NAME, RMSmiley.class); } @Override