Multi Node Workspace Example

Are there any public examples of modules that implement multiple Nav tree nodes for their designer workspace?

i.e. Perspective has the Style workspace and the View workspace
image

I have been using the TabbedResourceWorkspace, but that handles all of the Project resource nav tree work for me, and seems to be restricting the ability to customize the root node of the workspace.

I started digging around in the other workspace types, and playing with context.getProjectBrowserRoot()#addChild(), but when I register my TabbedResourceWorkspace its automatically adding the project browser node, and then I end up with a duplicate node.

I am mostly working off this 10 year old comment, so who knows if this has been updated and I am on the wrong path.

If anyone knows of any examples that would be great, or just a few comments on how to get headed in the right direction.

My only foray into this area implements ResourceWorkspace subclassing JideTabbedPane. The load in the designer startup hook looks like this:

        EnipWorkspace workspace = new EnipWorkspace(context);
        context.registerResourceWorkspace(workspace);
        backplane = new EnipBackplaneNode(workspace, context);
        context.getProjectBrowserRoot().addChild(backplane);

You might inspect the TabbedResourceWorkspace to figure out which method is adding your node and override that part.

I don’t remember all the details, but this was a slog.

I’ve done this, but in a project that I can’t directly share.

Basically it involves creating your own root node and making your resource workspaces use that root node by overriding getNavTreeNodeParent.

I’ll see if I can put together a short example.

1 Like

This is definitely an effort… lol

I am slowly making my way through this, but essentially I started with what @bmusson and so far this is what I have:

  1. I created an implementation of the AbstractNavTreeNode with no children, this will be the parent node
  2. I then created two classes (one for each node I want) that extend TabbedResourceWorkspace
  3. In my constructor for the workspaces, I made the parent node a constructor arg, and then override getNavTreeNodeParent to return that provided parent node
  4. I register my parent node in the designer hook with this.context.getProjectBrowserRoot()#addChild
  5. I then register each workspace with this.context#registerResourceWorkspace
  6. At this point, I have a node element with two child nodes. Progress! I will now refer to these as the A node and the B node
  7. I created a class that extends WorkspaceWelcomePanel in a way that is abstract between my two nodes, so that both the A and B nodes can share the same panel, though they each create their own.
  8. That’s as far as I have gotten… now I am in “which class Is initializing in what order, and where does this mysterious NPE come from” purgatory…

Will update when I have more useful steps.

EDIT:

The NPE for prosperities sake:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.registerNavTreeNode(TabbedResourceWorkspace.java:206)
	at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.lambda$new$0(TabbedResourceWorkspace.java:125)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.registerNavTreeNode(TabbedResourceWorkspace.java:206)
	at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.lambda$new$0(TabbedResourceWorkspace.java:125)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
1 Like

Should only need one workspace. Then two types tabs within them.

I thought the tabs were for having multiple resources of the same type open? I.e multiple scripts in the project scripts.

I am going to have two different types of resources, one that’s gateway scoped and one that’s project scoped

This should extend MutableNavTreeNode, so you can manually add your child nodes.

Are you manually adding these nodes as children of your root node? Ensure you attach them to the parent after you’ve added the root node to the project browser root.

I’m interested in how to accomplish this. My current implementation has workspaces for each resource, but some of them would definitely be better as tabs of a single workspace.

Taking a shot in the dark - does this involve AbstractNavTreeNode#getWorkspaceName() and ResourceWorkspace#getKey()?

1 Like

I guess maybe the question is, am I attaching these incorrectly, because I feel like it’s the right order?

Currently in my designer startup I am doing this:

// Intialize the root navigation node in the project browser
ConfigManagerNavTreeNode configManagerNavTreeNode = new ConfigManagerNavTreeNode();
this.context.getProjectBrowserRoot().addChild(configManagerNavTreeNode);

// Create and register my workspaces
ProjectConfigWorkspace projectWorkspace =  new ProjectConfigWorkspace(this.context, configManagerNavTreeNode);
GatewayConfigWorkspace gatewayWorkspace = new GatewayConfigWorkspace(this.context, configManagerNavTreeNode);
this.context.registerResourceWorkspace(projectWorkspace);
this.context.registerResourceWorkspace(gatewayWorkspace);


// Add my workspaces to the top level navTree
configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());
configManagerNavTreeNode.addChild(projectWorkspace.getRootNode());

Then in the constructor for my workspaces, I am doing

public GatewayConfigWorkspace(DesignerContext context, AbstractNavTreeNode navTreeNode) {
    super(context, ResourceDescriptor.builder()
            .resourceType(ConfigResource.RESOURCE_TYPE)
            .nounKey("ConfigManager.config.noun")
            
            .rootFolderText("Gateway")
            .rootIcon(VectorIcons.get("gateway"))
            .navTreeLocation(999)

            .build(), navTreeNode);
}

With this.rootNode being what’s returned in the getNavTreeNodeParent method, and set in the AbstractConfigWorkspace constructor

protected AbstractConfigWorkspace(DesignerContext context, ResourceDescriptor resourceDescriptor, AbstractNavTreeNode rootNode) {
    super(context, resourceDescriptor);
    this.rootNode = rootNode;
}

@Override
public MutableNavTreeNode getNavTreeNodeParent() {
    return (MutableNavTreeNode) this.rootNode;
}

That seems correct, at a glance. Are you still getting the same NPE?

This is the one that just showed up when I started the designer the last time

13:08:10.525 [Designer-Startup] INFO designer.main - Starting module: Process Config Manager [+6695]
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.add(MutableNavTreeNode.java:37)
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.lambda$addChild$0(MutableNavTreeNode.java:25)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.add(MutableNavTreeNode.java:37)
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.lambda$addChild$0(MutableNavTreeNode.java:25)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
13:08:10.538 [Designer-Startup] INFO designer.main - Initializing Scripting... [+6708]

This also happens when the designer hook starts up, before it starts initializing scripting.

EDIT: This looks like its a different NPE, however I am still not sure what’s causing it since I don’t see my package anywhere in this stack trace

Look for a call to com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode#addChild; you’re providing a null (or not-yet-constructed?) node to addChild, which is the cause of that NPE.

Wait, this isn’t right:

// Intialize the root navigation node in the project browser
ConfigManagerNavTreeNode configManagerNavTreeNode = new ConfigManagerNavTreeNode();
this.context.getProjectBrowserRoot().addChild(configManagerNavTreeNode);

// Create and register my workspaces
ProjectConfigWorkspace projectWorkspace =  new ProjectConfigWorkspace(this.context, configManagerNavTreeNode);
GatewayConfigWorkspace gatewayWorkspace = new GatewayConfigWorkspace(this.context, configManagerNavTreeNode);
this.context.registerResourceWorkspace(projectWorkspace);
this.context.registerResourceWorkspace(gatewayWorkspace);


// Add my workspaces to the top level navTree
configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());
configManagerNavTreeNode.addChild(projectWorkspace.getRootNode());

Isn’t this:
configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());
Trying to add configManagerNavTreeNode to itself, as a child?

I don’t think you need

configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());
configManagerNavTreeNode.addChild(projectWorkspace.getRootNode());

at all since you’re overriding getNavTreeNodeParent?

Y’all are right, that was the problem!

image

I hope this thread makes this easier for anyone trying to attempt this in the future, as in the wise words of @pturmel this was “a slog”…

1 Like

I’ve got too much on my plate for the level of effort it would require to get back up to speed on that code (cringe). In a week or two, perhaps.

1 Like

Again, at a glance, but I don’t think it would be too hard to extend TabbedResourceWorkspace to support different editors. You would just have to conditionally return the appropriate editor from newResourceEditor(ResourcePath).

As I have gotten a little further, I am realizing I don’t see an obvious construct for “Gateway scoped resources” just “Project scoped resources”. Before I go down the rabbit hole of managing my own resource folders on the gateway, is there already something in place for this?

i.e. Perspective themes:

-data
-----modules
------------com.inductiveautomation.perspective
-----------------------------------------------themes
-----------------------------------------------icons
-----------------------------------------------fonts
------projects
--------------ProjectName
-------------------------com.inductiveautomation.perspective
-------------------------------------------------------------views
-------------------------------------------------------------styles

EDIT: Probably not the right thread for that question, but I’ve created a lot of new threads recently, so figured I’d save the space… lol

I think you’re on your own unless you want to add Perspective as a dependency to use its asset manager concept.

https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.18/com/inductiveautomation/perspective/gateway/assets/AbstractAssetManager.html