Run configuration is never saved

Answered

I am writing a plugin for CLion that adds a run configuration type to run an ISO in QEMU.

The problem is that when changes are made to the run configuration editor, the IDE does not seem to be aware, and the "Apply" button is never enabled. Even if I edit my run configuration, go to another run configuration, edit it, and click "Apply", once I close and re-open the run configurations dialog, all changes to my run configuration are lost.

Moreover, in debugging, I noticed that the `Settingseditor#applyEditorTo` method is called repeated about twice a second. It is my understanding that that method is called when changes are applied, no?

Here is my editor code:

// I hate Swing...
public class QemuRunConfigurationEditor extends SettingsEditor<QemuRunConfiguration> {

  // Constructors
  //--------------------------------------------------

  public QemuRunConfigurationEditor(final Project project) {
    super();

    this.project = project;
  }

  // Fields
  //--------------------------------------------------

  private final Project project;

  private Border qemuExecutableFieldDefaultBorder;

  private Border qemuExecutableFieldErrorBorder;

  // UI components
  //--------------------------------------------------

  private JPanel rootPanel;

  // TODO: Allow custom items.
  private JComboBox<String> qemuExecutableField;

  private JRadioButton cmakeTargetRadio;

  private JComboBox<CMakeTarget> cmakeTargetField;

  private JRadioButton cdromFileRadio;

  private TextFieldWithBrowseButton cdromFileField;

  // SettingsEditor methods
  //--------------------------------------------------

  @Override
  protected final void resetEditorFrom(final QemuRunConfiguration runConfig) {
    qemuExecutableField.setSelectedItem(runConfig.getQemuExecutable());

    switch(runConfig.getDiskImageSource()) {
      case CMAKE_TARGET:
        cmakeTargetRadio.doClick();
        break;
      case CDROM_FILE:
        cdromFileRadio.doClick();
        break;
    }

    cmakeTargetField.setSelectedItem(runConfig.getCMakeTarget());
    cdromFileField.setText(runConfig.getCdromFile());
  }

  @Override
  protected final void applyEditorTo(final QemuRunConfiguration runConfig) throws ConfigurationException {
    runConfig.setQemuExecutable((String)qemuExecutableField.getSelectedItem());

    if(cmakeTargetRadio.isSelected()) {
      runConfig.setDiskImageSource(QemuRunConfigurationOptions.DiskImageSource.CMAKE_TARGET);
    } else if(cdromFileRadio.isSelected()) {
      runConfig.setDiskImageSource(QemuRunConfigurationOptions.DiskImageSource.CDROM_FILE);
    }

    runConfig.setCMakeTarget((CMakeTarget)cmakeTargetField.getSelectedItem());
    runConfig.setCdromFile(cdromFileField.getText());
  }

  @Override
  protected final JComponent createEditor() {
    qemuExecutableFieldDefaultBorder = qemuExecutableField.getBorder();

    qemuExecutableField.setModel(new DefaultComboBoxModel<>(QemuExecutableFinder.getQemuExecutables().stream()
        .map(File::getAbsolutePath)
        .toArray(String[]::new)));
    qemuExecutableField.addItemListener(this::qemuExecutableFieldChanged);

    cmakeTargetRadio.addActionListener(this::diskImageSourceRadioChanged);
    cdromFileRadio.addActionListener(this::diskImageSourceRadioChanged);

    cmakeTargetField.setModel(new ListComboBoxModel<>(new CMakeBuildConfigurationHelper(project).getTargets()));
    cmakeTargetField.setRenderer(new CMakeTargetCellRenderer());

    return rootPanel;
  }

  // Listener callbacks
  //--------------------------------------------------

  private void qemuExecutableFieldChanged(final ItemEvent event) {
    final String qemuExecutableCandidate = (String)event.getItem();

    new Thread(() -> {
      // TODO: Better way of doing border.
      if(ExecutableUtils.canExecute(qemuExecutableCandidate)) {
        qemuExecutableField.setBorder(qemuExecutableFieldDefaultBorder);
      } else {
        qemuExecutableField.setBorder(BorderFactory.createLineBorder(JBColor.RED));
      }
    }).start();
  }

  private void diskImageSourceRadioChanged(final ActionEvent event) {
    final JRadioButton source = (JRadioButton)event.getSource();

    if(source == cmakeTargetRadio) {
      cmakeTargetField.setEnabled(true);
      cdromFileField.setEnabled(false);
    } else if(source == cdromFileRadio) {
      cmakeTargetField.setEnabled(false);
      cdromFileField.setEnabled(true);
    }
  }

  // CMakeTargetCellRenderer class
  //--------------------------------------------------

  private static final class CMakeTargetCellRenderer extends ListCellRendererWithRightAlignedComponent<CMakeTarget> {

    // Constructors
    //--------------------------------------------------

    private CMakeTargetCellRenderer() {
      super();
    }

    // ListCellRendererWithRightAlignedComponent methods
    //--------------------------------------------------

    @Override
    protected final void customize(final CMakeTarget cmakeTarget) {
      if(cmakeTarget != null) {
        setIcon(cmakeTarget.getIcon());
        setLeftText(cmakeTarget.getName());
      }
    }

  }

}

The full code can be found on GitHub. It is not much.

Thanks to all who took their time to help!

7 comments
Comment actions Permalink

Hi Oliver,

Please try to call fireEditorStateChanged() when you modify the state of the editor fields.

BTW, cmakeTargetRadio.doClick(); - seems strange. Why not cmakeTargetRadio.setSelected(true) and setSelected(false) for the rest?

0
Comment actions Permalink

Thank you for your response.

I tried, e.g., qemuExecutableField.addActionListener(event -> fireEditorStateChanged()), and while the listener is called, it did not solve my problem. The "Apply" button was not enabled and the editor state is lost if I close and open the run configuration dialog.

I use doClick() so that the ActionListener for cmakeTargetRadio and cdromFileRadio is called once. I have not used Swing in a very long time. Is there a better way?

0
Comment actions Permalink

I need to reproduce it then.

Please make your project compiling correctly, so I can take a look. Currently, I'm getting this error:

Could not determine the dependencies of task ':prepareSandbox'.
> Could not resolve all files for configuration ':runtimeClasspath'.
   > Could not find com.oliveryasuna:commons-language:5.2.0.
     Searched in the following locations:
       - file:/Users/karol.lewandowski/.m2/repository/com/oliveryasuna/commons-language/5.2.0/commons-language-5.2.0.pom
       - https://repo.maven.apache.org/maven2/com/oliveryasuna/commons-language/5.2.0/commons-language-5.2.0.pom
     Required by:
         project :

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

1
Comment actions Permalink

The JAR is now on Maven Central. Otherwise, you can build from the repo: https://github.com/oliveryasuna/commons-language.

Thanks for the time you've put into this!

0
Comment actions Permalink

Hi Oliver,

Sorry, but it's still not building, I'm getting the same error.

Please make sure it is buildable before the next update post, provide the branch I should use, and ideally the test project. We can't spend time trying to fix issues like this or trying to build additional libraries from sources.

0
Comment actions Permalink

I tested it on another machine and was able to open and run the project.

I just zipped up the entire project folder: https://easyupload.io/wxbi4k.

And the CLion project: https://easyupload.io/hwka21

It will be easier to just create a new project in CLion, instead of trying to actually build mine. The run configuration has fields that you can modify to test.

0
Comment actions Permalink

Hi Oliver,

Getting back to the first message:

Moreover, in debugging, I noticed that the `Settingseditor#applyEditorTo` method is called repeated about twice a second. It is my understanding that that method is called when changes are applied, no?

The "applyEditorTo()" method is called when changes are applied, but also it is called every 500 ms to update the "Apply" button state. The button state update logic compares the original settings from the moment of dialog creation to the snapshot that is created at the moment of comparison. The issue is that they are compared by serializing it to XML, see:
https://github.com/JetBrains/intellij-community/blob/master/platform/execution-impl/src/com/intellij/execution/impl/BaseRCSettingsConfigurable.java#L46

Please implement writeExternal and readExternal methods in your configuration class (you can check some examples using https://jb.gg/ipe) or one from the community repository (it is Java though):
https://github.com/JetBrains/intellij-community/blob/master/plugins/sh/src/com/intellij/sh/run/ShRunConfiguration.java

0

Please sign in to leave a comment.