Custom Create in form - Fields don't get initialized when "Generate GUI into" is set to "Binary class files"

I want to create a form using the GUI Designer that contains an EditorTextField. To initialize this text field, I need to manually instantiate it and pass it the current project. According to the docs, this should be possible via the "Custom Create" checkbox and the createUIComponents() method, which is done like this in multiple places in IDEA itself. Since I need the current project for instantiation, I set it as a field when instantiating the class.

My code when using "Generate GUI into: Binary class files":

public class MyComponent {

private final Project project;

private JPanel panel;

private EditorTextField packageSelectField;

[other bound fields in the form]


public MyComponent(@NonNull Project project) {
this.project = project;

[code for setting up other form components]
}


private void createUIComponents() {
packageSelectField = new EditorTextField(JavaReferenceEditorUtil.createDocument(
"com.default.package", project, false), project, StdFileTypes.JAVA);
}

Unfortunately, the project field is null in createUIComponents() and thus the EditorTextField can't be instantiated. It seems like the method is somehow called before the constructor is called and thus the fields don't get initialized. Although I don't see how that is possible, given that I manually instantiate the MyComponent class. Now, if I set the "Generate GUI into" setting to "Java source code" and thus have IntelliJ generate the hidden setup methods, it works fine.

My code when using "Generate GUI into: Java source code":

public class MyComponent {

private final Project project;

private JPanel panel;

private EditorTextField packageSelectField;

[other bound fields in the form]


public MyComponent(@NonNull Project project) {
this.project = project;
$$$setupUI$$$();

[code for setting up other form components]
}


private void createUIComponents() {
packageSelectField = new EditorTextField(JavaReferenceEditorUtil.createDocument(
"com.default.package", project, false), project, StdFileTypes.JAVA);
}


/**
* Method generated by IntelliJ IDEA GUI Designer
* >>> IMPORTANT!! <<<
* DO NOT edit this method OR call it in your code!
*
* @noinspection ALL
*/
private void $$$setupUI$$$() {
createUIComponents();
panel = new JPanel();
...
}

...
}

As you can see, in the generated code the constructor calls $$$setupUI$$$() after assigning the field (as expected), which then calls createUIComponents(). I.e. if I let IntelliJ generate the GUI into Java sources, the field is assigned correctly, but if I have it generate to a Binary class file, the field is not assigned and remains null, thus being unusable in createUIComponents(). Am I doing something wrong or is this unintended behavior?

0
16 comments

Please sets "Binary class files" and:

-show full java code and .form file

-insert to `createUIComponents()`

new Throwable().printStackTrace();

and show full stack trace

-for .class file run `javap -c MyComponent` and show full listing

0

Here's the full code of the components involved (set to "Binary class files").

MyComponent.java:

package com.mycompany.plugin.modules.mymodule.settings;

import javax.swing.JButton;
import javax.swing.JPanel;

import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.ui.EditorTextField;
import com.intellij.ui.JavaReferenceEditorUtil;
import com.mycompany.plugin.core.settings.PluginState;
import com.mycompany.plugin.core.settings.SettingsTab;

import lombok.Getter;
import lombok.NonNull;

@Getter
public class MyComponent implements SettingsTab {

private Project project;
private PluginState state;

private JPanel panel;
private EditorTextField packageSelectField;
private JButton resetPackageButton;


public MyComponent(@NonNull Project project) {
this.project = project;
this.state = PluginState.getInstance(project);

resetPackageButton.addActionListener(e ->
packageSelectField.setText("com.default.package"));
}


private void createUIComponents() {
new Throwable().printStackTrace();

packageSelectField = new EditorTextField(JavaReferenceEditorUtil.createDocument(
"com.default.package", project, false), project, StdFileTypes.JAVA);
}


@Override
public boolean isModified() {
return false;
}


@Override
public void apply() {
// Save contents of package field in state instance
}


@NonNull
@Override
public String getTitle() {
return "First Settings Tab";
}
}

 

MyComponent.form:

<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="com.mycompany.plugin.modules.mymodule.settings.MyComponent">
<grid id="27dc6" binding="panel" layout-manager="GridLayoutManager" row-count="3" column-count="2" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="26d12" class="javax.swing.JLabel">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Enter package:"/>
</properties>
</component>
<component id="50a42" class="com.intellij.ui.EditorTextField" binding="packageSelectField" custom-create="true">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
</component>
<vspacer id="d3f2a">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="e000c" class="javax.swing.JButton" binding="resetPackageButton">
<constraints>
<grid row="1" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Reset to default"/>
</properties>
</component>
</children>
</grid>
</form>

 

MyComponent is used as a page in my plugin's settings, which are tabbed. It is added here as follows:

package com.mycompany.plugin.core.settings;

import java.util.ArrayList;
import java.util.List;

import javax.swing.JComponent;

import org.jetbrains.annotations.Nls;

import com.intellij.openapi.options.Configurable;
import com.intellij.openapi.project.Project;
import com.intellij.ui.components.JBTabbedPane;
import com.mycompany.plugin.modules.mymodule.settings.MyComponent;

import lombok.NonNull;

public class PluginConfigurable implements Configurable {

private final List<SettingsTab> settingsTabs = new ArrayList<>();


public PluginConfigurable(@NonNull Project project) {
settingsTabs.add(new MyComponent(project));
}


@Nls
@Override
public String getDisplayName() {
return "Plugin Settings";
}


@NonNull
@Override
public JComponent createComponent() {
JBTabbedPane tabbedPane = new JBTabbedPane();
settingsTabs.forEach(tab -> tabbedPane.add(tab.getTitle(), tab.getPanel()));
return tabbedPane;
}


@Override
public boolean isModified() {
return settingsTabs.stream().anyMatch(SettingsTab::isModified);
}


@Override
public void apply() {
settingsTabs.forEach(SettingsTab::apply);
}
}

 

SettingsTab interface:

public interface SettingsTab {

@NonNull
JPanel getPanel();

boolean isModified();

void apply();

@NonNull
String getTitle();
}

 

Following is the output you requested:

Throwable stack trace: See on Hastebin

Output of javap: See on Hastebin

0

Hello,

In your bytecode I don't see $$$setupUI$$$() and other special methods.

What type of projects do you use? Idea plugin project?

How do you compile a project?

After turn to "Binary class files" try clear all classes, IDE caches and rebuild project.

After its, if steel not working please send idea.log and build.log

0

Hi,

the project is a standard Gradle project using the gradle-intellij-plugin. Here's my gradle.build:

buildscript {
repositories {
mavenCentral()
}
}

plugins {
id 'org.jetbrains.intellij' version '0.4.2'
id 'io.franzbecker.gradle-lombok' version '2.0'
}

apply plugin: 'idea'
apply plugin: 'org.jetbrains.intellij'
apply plugin: 'java'

intellij {
version '2018.3'
pluginName 'my-plugin'
plugins 'git4idea'
}

group 'com.mycompany'
version '1.0.0'
sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
compile 'com.miglayout:miglayout-swing:5.2'
compile 'org.gitlab4j:gitlab4j-api:4.9.14'
}

 

I compile the project using the Gradle build task, which gets executed automatically as part of the runIde task.

build task:

14:45:28: Executing task 'build'...

> Task :compileJava

> Task :patchPluginXml
Patching plugin.xml: attribute `since-build=[183]` of `idea-version` tag will be set to `183.4284`
Patching plugin.xml: value of `version[1.0.0]` tag will be set to `1.0.0`

> Task :processResources
> Task :classes
> Task :instrumentCode
> Task :postInstrumentCode
> Task :jar
> Task :prepareSandbox
> Task :buildSearchableOptions SKIPPED
> Task :jarSearchableOptions SKIPPED
> Task :buildPlugin
> Task :assemble
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :instrumentTestCode NO-SOURCE
> Task :postInstrumentTestCode
> Task :prepareTestingSandbox
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build

BUILD SUCCESSFUL in 25s
10 actionable tasks: 10 executed
14:45:56: Task execution finished 'build'.

 

runIde task:

14:50:15: Executing task 'runIde'...

> Task :compileJava UP-TO-DATE
> Task :patchPluginXml UP-TO-DATE
> Task :processResources UP-TO-DATE
> Task :classes UP-TO-DATE
> Task :instrumentCode UP-TO-DATE
> Task :postInstrumentCode
> Task :jar UP-TO-DATE
> Task :prepareSandbox UP-TO-DATE

> Task :runIde
OpenJDK 64-Bit Server VM warning: ignoring option MaxPermSize=250m; support was removed in 8.0

 

Before executing the above tasks, I

  • ran a Gradle clean,
  • manually deleted any build/ folders, and
  • invalidated IntelliJ's caches.

Unfortunately, this does not make a difference. The hidden $$$ methods are not compiled into the class file.

The host IDE's idea.log and build.log

The sandbox IDE's idea.log (does not have a build.log)

0

Hi, please run gradle `clean instrumentCode --info` and attach the output.

Btw, how are you checking that $$$setupUI$$$ is not there? What's the class file you check?

0

> Btw, how are you checking that $$$setupUI$$$ is not there? What's the class file you check?

I'm checking the class file of MyComponent using both IntelliJ's decompiler and `javap` as suggested before.


> please run gradle `clean instrumentCode --info` and attach the output.

Here you go.

0

> I'm checking the class file of MyComponent

Which one? There are at least four of them in case of building the plugin.

> Task :instrumentCode
Task ':instrumentCode' is not up-to-date because:
  Output property 'outputDir' file /home/benedikt/IdeaProjects/my-plugin/build/classes/java/main-instrumented has been removed.
  Output property 'outputDir' file /home/benedikt/IdeaProjects/my-plugin/build/classes/java/main-instrumented/com has been removed.
  Output property 'outputDir' file /home/benedikt/IdeaProjects/my-plugin/build/classes/java/main-instrumented/com/mycompany has been removed.
Compiling forms and instrumenting code with nullability preconditions
[ant:instrumentIdeaExtensions] Added @NotNull assertions to 28 files
:instrumentCode (Thread[Execution worker for ':' Thread 6,5,main]) completed. Took 1.32 secs.

According to logs the instrumentation succeed. Where is the form sources located?

0

> Which one? There are at least four of them in case of building the plugin.

Good to know, I just checked in build/classes/java/main/com/mycompany/plugin/modules/mymodule/settings.

I can only find one other class file for MyComponent in main-instrumented/... and it actually calls `$$$setupUI$$$()` as the first thing in its constructor. This is incorrect in this case, as the fields are assigned after that within Lombok's generated guard clause.

Looks like the problem is that it's not taking Lombok's @NonNull guard into account?

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.mycompany.plugin.modules.mymodule.settings;

import com.intellij.openapi.fileTypes.StdFileTypes;
import com.intellij.openapi.project.Project;
import com.intellij.ui.EditorTextField;
import com.intellij.ui.JavaReferenceEditorUtil;
import com.mycompany.plugin.core.settings.PluginState;
import com.mycompany.plugin.core.settings.SettingsTab;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JPanel;
import lombok.NonNull;

public class MyComponent implements SettingsTab {
private Project project;
private PluginState state;
private JPanel panel;
private EditorTextField packageSelectField;
private JButton resetPackageButton;

public MyComponent(@NonNull Project project) {
this.$$$setupUI$$$();
if (project == null) {
throw new NullPointerException("project is marked @NonNull but is null");
} else {
this.project = project;
this.state = PluginState.getInstance(project);
this.resetPackageButton.addActionListener((e) -> {
this.packageSelectField.setText("com.default.package");
});
}
}

private void createUIComponents() {
(new Throwable()).printStackTrace();
this.packageSelectField = new EditorTextField(JavaReferenceEditorUtil.createDocument("com.default.package", this.project, false), this.project, StdFileTypes.JAVA);
}

public boolean isModified() {
return false;
}

public void apply() {
}

@NonNull
public String getTitle() {
return "First Settings Tab";
}

public Project getProject() {
return this.project;
}

public PluginState getState() {
return this.state;
}

public JPanel getPanel() {
return this.panel;
}

public EditorTextField getPackageSelectField() {
return this.packageSelectField;
}

public JButton getResetPackageButton() {
return this.resetPackageButton;
}
}

 

> Where is the form sources located?

Right next to the MyComponent.java file if that's what you mean.

0

Configuration looks good to me, don't see what can goes wrong. Could you provide a project sample to recreate this, please?

@Alexander, any ideas why compiler works like that?

0

---------------JAVA-------------------------

package dddd;

import org.jetbrains.annotations.NotNull;

import javax.swing.*;

public class TestForm {
@NotNull
private final String name;
private JTextField textField1;
private JPanel myPanel;

public TestForm(@NotNull String name) {
this.name = name;
myPanel.putClientProperty("name", name);
}

private void createUIComponents() {
textField1 = new JTextField("" + name.length());
}
}

------- ByteCode---------------------------------------
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package dddd;

import javax.swing.JPanel;
import javax.swing.JTextField;
import org.jetbrains.annotations.NotNull;

public class TestForm {
@NotNull
private final String name;
private JTextField textField1;
private JPanel myPanel;

public TestForm(@NotNull String name) {
if (name == null) {
$$$reportNull$$$0(0);
}

super();
this.name = name;
this.$$$setupUI$$$();
this.myPanel.putClientProperty("name", name);
}

private void createUIComponents() {
this.textField1 = new JTextField("" + this.name.length());
}
}
0

Hm,

lombok.NonNull

 

Lombok generate code like:

if (FOO == null) { ....}

else {....}

 

and this broken our form byte code generator....

Please use 

import org.jetbrains.annotations.NotNull;
0

Thanks both of you for debugging this with me; I'll use @NotNull in this particular component for now.


However, I seem to have found another issue with this. When initializing an attribute in-line, for example via:

private final List<String> aList = new ArrayList<>();

the `$$$setupUI$$$` method is also called at the wrong place.

Directly initializing a field such as above results in the following code being generated, i.e. the initialization is automatically moved into the constructor:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.mycompany.plugin.modules.mymodule.settings;

import com.intellij.openapi.project.Project;
import com.intellij.ui.EditorTextField;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JPanel;
import org.jetbrains.annotations.NotNull;

public class MyComponent {
private Project project;
private JPanel panel;
private EditorTextField packageSelectField;
private JButton resetPackageButton;
private final List<String> aList;

public MyComponent(@NotNull Project project) {
if (project == null) {
$$$reportNull$$$0(0);
}

super();
this.$$$setupUI$$$();
this.aList = new ArrayList();
this.project = project;
}
}

 

When the field is manually initialized within the constructor like this:

private final List<String> aList;

public MyComponent(@NotNull Project project) {
this.project = project;
aList = new ArrayList<>();
}

the code generation also doesn't work as expected when the field is required to be already initialized in `createUIComponents()`. Looks like only parameter-to-field assignments are prioritized higher than the `$$$setupUI$$$()` call:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.mycompany.plugin.modules.mymodule.settings;

import com.intellij.openapi.project.Project;
import com.intellij.ui.EditorTextField;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JPanel;
import org.jetbrains.annotations.NotNull;

public class MyComponent {
private Project project;
private JPanel panel;
private EditorTextField packageSelectField;
private JButton resetPackageButton;
private final List<String> aList;

public MyComponent(@NotNull Project project) {
if (project == null) {
$$$reportNull$$$0(0);
}

super();
this.project = project;
this.$$$setupUI$$$();
this.aList = new ArrayList();
}
}

 

This can be worked around by initializing the field in `createUIComponents()` instead of in-line or in the constructor.

0

BVollmerhaus, Please create issue in YouTrack with all corner cases.

0

Will do, but it may take quite a while until I get around to it.

0

So it is solved right now? I found the method 

createUIComponents will be executed before a intellij idea form's constructor is it normal?
0

Chuanyi from docs https://www.jetbrains.com/help/idea/creating-form-initialization-code.html

Sometimes you might need to provide initialization code of your own. For example, you want a GUI component to be instantiated by a non-default constructor with certain parameters. In this case, IntelliJ IDEA will not generate instantiation of the component, and it is your responsibility to provide the call to constructor in the createUIComponents() method. Otherwise, a Null Pointer Exception will be reported. Follow the general technique described below.

0

Please sign in to leave a comment.