Default Project Properties

Is it possible to add project settings for a custom module?

i.e.

Hello @kgamble ,

Yes, it is possible.

If you want to configure this settings in the gateway I would recommend checking out the Home Connect Example .

If you want it to be in the designer I would recommend @PGriffith 's project. Also I would check out this thread.

Hope this helps!

2 Likes

Note that if you want your settings to appear in the same project properties UI as the other settings, you’ll (probably) want to create a singleton resource [1], and need to register your own property editor panel on the DesignerContext you get access to in your designer module hook (com.inductiveautomation.ignition.designer.DesignerContext#addPropertyEditor).

[1]: A singleton resource is just defined as a resource with no ‘folder path’; unlike standard resources that can be inside arbitrary folders, singletons are, as the name implies, always a single resource per project. Think gateway event scripts, client event scripts, custom session properties, etc.

1 Like

This is exactly what I am looking for! Is there an example of this? I took a look at the Markdown example above, but that’s more of a resource than a property.

No public examples (yet?), but here’s the highlights…

  1. Create a mutable, Java serializable class. Have a public static final ResourceType for it with your module ID.
  2. Create your editor class, extending AbstractPropertyEditorPanel. In the constructor, set up your text fields/dropdowns/checkboxes/etc (make them fields). Call the protected listenTo methods on AbstractPropertyEditorPanel with your controls; that helps set up the editor panel’s apply/ok staging.
  3. Implement initProps - the Object you receive will be null, or an instance of your custom resource type. If it’s not null, use it to set your various edit field’s initial value.
  4. Implement commit - take the values of the various edit fields and write them back to your mutable prop object.

That should be enough to get you started; the rest should be pretty self explanatory. The AbstractPropertyEditorPanel will automatically serialize your resource into a data.bin file using Java serialization.

3 Likes

Considering my initial reaction was this gif

I have gotten further than I expected.

As of right now my only two issues are:

  1. I can’t seem to get my editor to show up under the Project category no matter what I try. It just always shows up underneath Perspective.
    image
  2. The commit event does not fire when I hit OK, or Apply and I am not sure if I am missing something there as well, so I my changes never save and the Object in initProps always come back as null.

Here is a copy of the gateway scope of the module, it only really includes this so I hope its easy to see what I am missing.

GatewayScope.zip (5.5 KB)

I appreciate any help!

Oh, right. For better or for worse, the way things currently work this is a strictly ordered list, there’s no actual hierarchy. You can provide your own ‘category’ by creating a subclass of com.inductiveautomation.ignition.designer.propertyeditor.CategoryPanel, but I don’t see any way (without gross reflection hacks) to put your own panel below Project.

You missed the last half of step 2 :slight_smile: Call listenTo in your constructor to tell the panel what components it needs to ‘care’ about having changed; like this:

dataKeyTextField = new JTextField(16);
listenTo(dataKeyTextField);

Also, not that important, but your method in DesignerHook doesn’t make much sense:

    public Class<? extends AbstractPropertyEditorPanel> PropertyEditor() {
        return PropertyEditor.class;
    }
    public void init() {
        context.addPropertyEditor(PropertyEditor());
    }

That could just be:

public void init() {
        context.addPropertyEditor(PropertyEditor.class);
    }

Doh! Added that and it works beautifully.

This is what happens when you start learning java and within one week you end up attempting to build something… lol

Last question, This is a default property per project right, so how exactly do I get it back out based on what project it was called in? I would presume it would be somewhere in the client scope in my scripting module where I implement the rpc handlers?

1 Like

So, since you’re defining a resource (that may or may not exist, but there will only ever be one of per project) - you would just interact with the project to retrieve your resource and deserialize it from the data.bin wherever you need to read this value. There’s a ResourceUtil helper class with some methods you can use.
Something like:

var properties = gatewayContext.getProjectManager()
	.getProject("name")
	.map(RuntimeProject::validateOrThrow)
	.flatMap(project -> project.getSingletonResource(Neo4JProperties.RESOURCE_TYPE))
	.map(resource -> ResourceUtil.decodeOrNull(resource, gatewayContext.createDeserializer(), Neo4JProperties.class))
	.orElseGet(Neo4JProperties::new);
1 Like

Out of curiosity, is the class used to define these label/combobox/refreshicon exposed in any way via the sdk? It looks like its simplemented as com.inductiveautomation.ignition.client.util.gui.AbstractProfileOptionDropdown I am just struggling to create it.

Screen Shot 2021-04-06 at 10.43.21 PM

I recreated my own from scratch, but I feel like if I can latch into the pre-defined one that’s already used it may be better for future proofing.

2 Likes

To anyone following along at home, this looks like it was the answer I was looking for.

Creating an implementation of that class, and providing its contained dropdown with my options gave me the dropdown and refresh icon. I am guessing that the labels in the above image are just labels and not a part of the rest of the component.

I created the subclass but how do I add it into the designer context?

Unfortunately context.addPropertyCategory(CustomCategory.class) doesnt seem to exist… lol

Just add it as a PropertyPanel (it technically is); the list will automatically render it as a category and not allow you to select it.

1 Like

Is there an obvious reason as to why these are reset anytime the module restarts? The binary files are still there, but the method I use to get the data out start returning null as soon as the module restarts and until I open the designer and specifically the property editor panel for it.

I tried moving my ResourceType outside of the designer scope into common, thinking potentially that was related but it doesn’t seem to have fixed the problem.

It seems like you’re maybe accidentally mutating a singleton? Your ResourceType field should be the only static field; it’s just a marker to the rest of the project resource system to identify your resource. You should be retrieving/creating fresh copies of the actual resource object as needed; see the ResourceUtil snippet above.

I ran through it and at least I think I am creating and retrieving copies as needed?

This is my resource

public class Neo4JProperties {
   public static final ResourceType RESOURCE_TYPE = new ResourceType("com.kgamble.neo4j", "Neo4JProperties");
   
   private String defaultDatabase;

   public String getDefaultDatabase() {
       return defaultDatabase;
   }

   public void setDefaultDatabase(String database) {
       defaultDatabase = database;
   }
}

Where I provide it to the script in the rpc handler:

@Override
public Object getRPCHandler(ClientReqSession session, String projectName) {
    Neo4JProperties properties = getProjectSettings(projectName);
    scriptModule.setDefaultDatabase(properties.getDefaultDatabase());
    return scriptModule;
}

The implementation of the ResourceUtil example you provided (I originally just copy and pasted yours, but vsCode really did not like the map method)

private Neo4JProperties getProjectSettings(String projectName) {
        Neo4JProperties properties = context.getProjectManager()
            .getProject(projectName)
            .map(t -> {
                try {  
                    return t.validateOrThrow();
                } catch (ProjectInvalidException e) {
                    e.printStackTrace();
                }
                return t;
            })
            .flatMap(project -> project.getSingletonResource(Neo4JProperties.RESOURCE_TYPE))
            .map(resource -> {
                try {
                    return ResourceUtil.decodeOrNull(resource, context.createDeserializer(), Neo4JProperties.class);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            })
            .orElseGet(Neo4JProperties::new);
       
        return properties;
    }

Oh, right. You can write all that code imperatively; Java’s checked exceptions don’t play nicely with lambdas (sigh). Something like this:

var project = gatewayContext.getProjectManager()
	.getProject("name")
	.orElseThrow() // throws NoSuchElementException if project not found
	.validateOrThrow(); // throws ProjectInvalidException if the project is invalid

Neo4JProperties properties = null;
var maybeResource = project.getSingletonResource(Neo4JProperties.RESOURCE_TYPE);
if (maybeResource.isPresent()) {
	var resource = maybeResource.get();
	try {
		properties = ResourceUtil.decodeOrNull(resource, gatewayContext.createDeserializer(), Neo4JProperties.class);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

if (properties == null) {
	properties = new Neo4JProperties();
}

Still didnt seem to resolve the problem, when I publish the module and it posts to the gateway (or restart the module or gateway) the record is null until I open the designer property editor.

The only other thing I can think of is if I potentially implemented the property editor incorrectly?

public class GeneralPropertyEditor extends AbstractPropertyEditorPanel {

    private final ScriptingFunctions rpc;
    private Neo4JProperties neo4jProps = new Neo4JProperties();
    private final DatabaseDropdown dropdown;

    public GeneralPropertyEditor(DesignerContext context) {
        super(new MigLayout("fill", "[pref!][grow,fill]", "[]15[]"));

        rpc = ModuleRPCFactory.create(
            "com.kgamble.neo4j.neo4j-driver",
            ScriptingFunctions.class
        );
        dropdown = new DatabaseDropdown(false, rpc);


        add(HeaderLabel.forKey("GeneralPropertyEditor.Database.Header"), "wrap r");

        add(new JLabel(BundleUtil.get().getString("GeneralPropertyEditor.Database.Label")), "");
        add(dropdown, "wrap");
        listenTo(dropdown.getDropdown());


    }

    public List<String> getConnections() {
        return rpc.getConnections();
    }

    @Override
    public Object commit() {
        if ( dropdown.getSelectedIndex() != -1 ) {
            neo4jProps.setDefaultDatabase(dropdown.getSelectedItem().toString());
        } else {
            return null;
        }
        
        return neo4jProps;
    }

    @Override
    public String getCategory() {
        return "Neo4J";
    }

    @Override
    public ResourceType getResourceType() {
        return Neo4JProperties.RESOURCE_TYPE;
    }

    @Override
    public String getTitleKey() {
        return "GeneralPropertyEditor.General.Title";
    }

    @Override
    public void initProps(Object props) {
        if ( props == null ) {
            dropdown.setSelectedItem(null);
        } else {
            Neo4JProperties updatedProps = (Neo4JProperties) props;
            this.neo4jProps = updatedProps;
    
            dropdown.setSelectedItem(neo4jProps.getDefaultDatabase());
        }
    }
    
}

I don’t think that’s what you want. I think you should always return neo4jProps - but if the dropdown has nothing selected, set the default database on the instance to null and handle it when you try to make a connection.

Unfortunately no luck with that either, will keep digging, but unfortunately my logging doesn’t seem to want to work from inside that getRpcHandler method.