Configure a module with project resources

Hello!
I made a scripting function module that needs some improving. This function has some parameters that I would like to erase, and get the information from somewhere else. The plan is to make a new folder in the Designer’s Project Browser which would open a window similar to “Transaction groups” . In this window I would like to drag tags and maybe enter some parameters. Can I get any input in how to do it?
For now I have read this guide: https://docs.inductiveautomation.com/display/SE/Working+with+Project+Resources
and this post:
Project Resources
but I don’t really know how to make this happen.

Thank you!

The SDK example is completely outdated, as the forum post you linked mentioned. The details in the forum post are mostly correct, but don't answer a lot of your questions.

If you want something to work off of, I actually recently did a standalone project resource example - the only (potential) problem being that it's in Kotlin, rather than Java: GitHub - paul-griffith/markdown-resources: Example of creating a custom project resource type, with editor, in Ignition

Briefly, Kotlin is an alternate language that can compile down to Java bytecode to run on the JVM. Its syntax is roughly similar to Java, but without a lot of boilerplate - so that may be enough of a hint to get you started, but maybe not.

For a start:

Notice how my DesignerHook registers a ResourceWorkspace implementation:

That MarkdownResourceWorkspace, when constructing its parent class, creates a ResourceDescriptor:

Thank you so much @PGriffith, this is really useful. I will look into this thoroughly.

Cheers!

Hello again @PGriffith,

I’m having some trouble understanding this part of the code:

override fun createWorkspaceHomeTab(): Optional<JComponent> {
        return object : WorkspaceWelcomePanel("Markdown Notes Workspace Title", null, null) {
            override fun createPanels(): List<JComponent> {
                return listOf(
                    ResourceBuilderPanel(context, "markdown note", MarkdownResource.RESOURCE_TYPE.rootPath(), listOf(
                        ResourceBuilderDelegate.build(
                            "markdown note",
                            VectorIcons.get("resource-note"),
                            newMarkdownNote
                        )
                    ), this@MarkdownWorkspace::open),
                    RecentlyModifiedTablePanel(context,
                        MarkdownResource.RESOURCE_TYPE,
                        "markdown notes",
                        this@MarkdownWorkspace::open)
                )
            }
        }.toOptional()
    }

I can’t see clearly what panels is the method creating.
Specially this part:

this@MarkdownWorkspace::open

What would be the Java substitute?

Also, is there any templates available for a panel similar to the Transactions group one? Or any template at all :slight_smile: I’m having troubles seeing where can I add components with JFrame.add(JComponent)

Thank you again,

Miguel Luna

this@MarkdownWorkspace::open

Is just a non-local method reference - you can do method references in Java (8+), but with some slightly different restrictions - the more typical lambda syntax (which the method reference is just syntactic sugar for) would be something like resourcePath -> this.open(resourcePath) - you're providing a lambda, that conforms to the Consumer<ResourcePath> functional interface, and thus 'accepts' a ResourcePath argument. Method references are allowed as sugar, because as long as your interface only defines one method, and that method only defines one argument, there's no ambiguity for the compiler to resolve - so you could also use a Java method reference, eg:
PipelineBlockWorkspace.this::openPipeline

There's no examples for sql bridge - it's a one-off workspace that was basically 'ported' over from the original implementation in FactorySQL. Are you just looking for a 'singleton' resource editor? You don't have to extend TabbedResourceWorkspace - it's just that that's usually the simplest path and covers most use cases.

Thank you for your response,
Right now I’m trying to make a resource from where I can get different settings for my module, extending TabbedResourceWorkspace. To simplify the process of making the layout, I’m using Netbean’s swing workspace, making something similar to this:


Which results in the following code:

        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGap(48, 48, 48)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addComponent(jLabel3, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jLabel1, javax.swing.GroupLayout.PREFERRED_SIZE, 140, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jScrollPaneIrradiance, javax.swing.GroupLayout.DEFAULT_SIZE, 292, Short.MAX_VALUE)
                    .addComponent(jScrollPaneTemperature)
                    .addComponent(jScrollPaneEnergy))
                .addGap(93, 93, 93)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jLabel4)
                    .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jLabel5)
                    .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jLabel6)
                    .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addContainerGap(290, Short.MAX_VALUE))
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGap(40, 40, 40)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jLabel1)
                    .addComponent(jLabel4))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING, false)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(jScrollPaneTemperature, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addGap(18, 18, 18)
                        .addComponent(jLabel2)
                        .addGap(18, 18, 18)
                        .addComponent(jScrollPaneIrradiance, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE))
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addGap(18, 18, 18)
                        .addComponent(jLabel5)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                        .addComponent(jLabel6)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(jTextField3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jLabel3)
                .addGap(18, 18, 18)
                .addComponent(jScrollPaneEnergy, javax.swing.GroupLayout.PREFERRED_SIZE, 80, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap(60, Short.MAX_VALUE))
        );

        pack();

Then, I add that layout to the method init() in MarkdownEditor, using this.setLayout(…). Keep in mind that this is in an extended ResourceEditor class. The problem that I’m finding is, when I try to create a new Resource Editor, I find the following error:

If this method of making the layout is wrong, please, let me know! I’ve looked into MigLayout but it seems pretty complicated, at least compared to Netbean’s graphic workspace.
Thank you so much for your help!
Miguel Luna

That layout is fine - or at least, the error message isn’t indicating any problem in the layout. Your PREditor class isn’t handling null data coming in properly - most likely because the resource you’re creating is malformed, or has the wrong data keys, or something along those lines. Notice the stack trace going into ResourceEditor#loadResource, then deserialize. When implementing a ResourceEditor for a custom resource type, you need to choose between:

  1. Leaving the resource ‘generic’ - your resource object must be Java serializable (implementing the Serializable interface, and a few other limitations) and will be stored as an opaque data.bin file - meaning no ‘nice’ diffing between versions for other users.
  2. Implement serialize(T): byte[]/deserialize(byte[]): T - your resource will still be stored in the data.bin file, but you can choose to use your own object serialization/deserialization strategy.
  3. Go fully custom and implement serializeResource and deserialize(ProjectResource), as I did in the MarkdownEditor class. That way, you can choose to store your resource file(s) as their own attributes on disk - in the markdown editor, this is just the note.md file.

Assuming you’re still working directly off my example, then the most likely issue is that you have a malformed resource present - you should delete your existing TestResource, and ensure that the action to create a new note (in the workspace configuration) is creating a resource with the appropriate data key - which, I just noticed, my example is not :man_facepalming:.

1 Like

I used mostly MigLayout("fill") and MigLayout("") for my Ethernet/IP workspace. Easy-peasy.

2 Likes

What would be the right data key? I mean, how do I know it’s correct?

Hello Phil. Did you find it hard adding JComponents to your layout using MigLayout? Any advices?

The example creates a Consumer<ProjectResourceBuilder>, which puts a readme.md key into the resource; that should instead be note.md/the constant value of MarkdownResource.DATA_KEY.

And I’ll echo MigLayout - there’s a learning curve, but it can do basically everything you could possibly want for layout, and it’s a whole lot less verbose than what most layout builders spit out. But, that’s a secondary concern compared to actually getting this working.

2 Likes

Okay, thanks!

Hello again @PGriffith,
I’m trying to implement these methods in class MarkdownResource:

 override fun deserialize(resource: ProjectResource): MarkdownResource {
        return resource.getData(MarkdownResource.DATA_KEY)
            ?.takeIf(ByteArray::isNotEmpty)
            ?.toString(Charsets.UTF_8)
            .let { MarkdownResource(it.orEmpty()) }
    }

    override fun serializeResource(builder: ProjectResourceBuilder, resource: MarkdownResource) {
        builder.putData(MarkdownResource.DATA_KEY, resource.note.toByteArray())
    }

And this method in class MarkdownWorkspace

private val newMarkdownNote: (ProjectResourceBuilder) -> Unit = { builder ->
        builder.putData("readme", "Enter a note".toByteArray())
    }

Because, as you said, I wasn’t serializing correctly. The problem is that I’m having trouble understanding the kotlin code, and decompiling and compiling to Java doesn’t do the trick for me :sweat:

I think that after this, I will be able to continue with no problems (hopefully) with my module.

Thanks a lot!

deserialize in Java would be something like:

    @Override
    protected MarkdownResource deserialize(ProjectResource resource) {
        byte[] data = resource.getData(MarkdownResource.DATA_KEY);
        if (data != null && data.length > 0) {
            return MarkdownResource(String(data, UTF_8));
        } else {
            return MarkdownResource("");
        }
    }

serializeResource should directly translate to Java code - toByteArray() is just an extension method that calls String.getBytes(UTF-8).

newMarkdownNote would be a field of type Consumer<ProjectResourceBuilder>, something like:

 private Consumer<ProjectResourceBuilder> newMarkdownNote = (builder) ->
        builder.putData(MarkdownResource.DATA_KEY, "Enter a note".getBytes(UTF_8));```
1 Like

Thank you Paul!

We are facing the same exact serialization error when we converted the code to Java.
Were you able to resolve this error; if yes, can you please share what you had to change . We have followed the same exact steps as indicated in all the above discussions. We have also changed the “private Consumer newMarkdownNote = (builder) ->
builder.putData(MarkdownResource.DATA_KEY, “Enter a note”.getBytes(UTF_8));```”. The original source code on github had “readme.md” instead of MarkdownResource.DATA_KEY.

1 Like

Hello pdalwalla,

Yes, I got through the error overriding this methods in the class MarkdownEditor (or whatever your class is called):

@Override
    protected void serializeResource(ProjectResourceBuilder builder, PRResource object){
        builder.putData(PRResource.DATA_KEY,serializeToByte(object));
    }

@Override
    protected PRResource deserialize(ProjectResource resource) {
        byte[] data = resource.getData(PRResource.DATA_KEY);
        if (data != null && data.length > 0) {
            return deserializeToByte(data);
        } else {
            return new PRResource();
        }
    }

    private static byte[] serializeToByte(PRResource obj){
        try{
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            ObjectOutputStream os = new ObjectOutputStream(out);
            os.writeObject(obj);
            return out.toByteArray();

        }catch(Exception e){

        }
        return null;
    }

    private static PRResource deserializeToByte(byte[] data){
        try{
            ByteArrayInputStream in = new ByteArrayInputStream(data);
            ObjectInputStream is = new ObjectInputStream(in);
            return (PRResource) is.readObject();
        }catch(Exception e){

        }
        return null;
    }

The only problem I’m running into right now is that I’m not able to save the resource (and load it) properly. If you guys could help me with that :wink:

Are either of you able to post your converted Java code to a public Github instance? I could fork it and try to see what’s going wrong.

1 Like

I think we were able to save the resource with the following code. I received this from our programming team.

Thanks

@Override
public void addNewResourceActions(ResourceFolderNode arg0, JPopupMenu arg1) {
arg1.add(new NewResourceAction(this,arg0,newTestNote) {

@Override
protected String newResourceName() {
return “note”;
}

void init(){
putValue(Action.NAME, “New Note”);
putValue(Action.SMALL_ICON, VectorIcons.get(“resource-note”));
}

});
}

1 Like

Paul,

We have found some workarounds and still working on some of the code. We will have to separate the code from all the other module code that we have integrated into it. We will try to isolate and upload some working part of the code in a few days. Is github the place to upload?

Thank you

1 Like