The dark arts of NetBeans hackery

Since before it was a NetBeans RCP based application, Praxis had its own Swing look-and-feel – a (minimal) fork of the lovely NimROD. Well, when I was transitioning to the NetBeans platform, I was determined to keep this look-and-feel. Unfortunately, this being a dark (white on black) look-and-feel, a small amount of hackery was required to get NetBeans to play nicely. I thought I’d document three of these here. What follows may be of use to any NetBeans developer working with non-default look and feels; it may not make much sense to a non-NetBeans developer!

Hack #1

I install the Praxis LAF within a ModuleInstall class’s restored() method. This makes the build process nice and simple, but it does have its drawbacks.

Praxis tabs problem

Here you can see an early attempt at installing the Praxis look and feel.  hmm … the look and feel doesn’t have any green in it, so where did that green tab come from?

NetBeans has some code (in the org.netbeans.swing.plaf module) that handles changing the look and feel at runtime, switching out values from the old look and feel. Unfortunately, a few values set in AllLFCustoms are never removed and are set using a class called GuaranteedValue. Most of the GuaranteedValues delegate to another value in the UI table – unfortunately, they only delegate at creation time, which means the old values (in this case the green from my GTK theme) hangs about.

So, what do I do about it? In the code below from my ModuleInstall I take the nuclear option, completely clearing the UIDefaults table before installing my look and feel, then make sure all the necessary values are set in my LFCustoms subclass.

public void restored() {
 try {
   EventQueue.invokeAndWait(new Runnable() {
     @Override
     public void run() {
       UIManager.getDefaults().clear();
       ClassLoader cl = Lookup.getDefault().lookup(ClassLoader.class);
       UIManager.put("ClassLoader", cl);
       UIManager.put("Nb.PraxisLFCustoms", new PraxisLFCustoms());
       try {
         LookAndFeel laf = new PraxisLookAndFeel();
         UIManager.setLookAndFeel(laf);
       } catch (UnsupportedLookAndFeelException ex) {}
     }
   });
 } catch (Exception ex) {}
}

Hack #2

So, now the main text and tabs in my look and feel are looking OK. However, there are still a few issues. Firstly, notice the help pane in the Property component here -

Praxis Property TopComponent visual problem

This isn’t used much in Praxis yet, but that text that currently says “Unrecognized File” is not meant to be black and barely readable! This component is a JEditorPane, and unfortunately JEditorPane’s don’t care much about your default colour scheme. There is a client property for JEditorPane to get it to play ball, but how to get access to this component in order to set it when it’s hidden inside non-public code?

Well, to do this I create a custom UI delegate for JEditorPanes and register it in my LFCustoms class (from #1). In this I set the required client property.

public class HonorDisplayEditorPaneUI extends BasicEditorPaneUI {

  public static ComponentUI createUI(JComponent c) {
    c.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
    return new HonorDisplayEditorPaneUI();
  }
}

Problem solved!

Praxis Property TopComponent fixed

Hack #3

The third and final hack I’m going to demonstrate is with the Options dialog. Here you can see the problem – the Options dialog buttons along the top have colours and borders hard-coded in that don’t fit well with our colour scheme.

Praxis Options dialog with problem

So, apart from wanting to scream why, why, why, why, why??? :-) what can be done about it? The buttons are a hidden inner class, which is inaccessible, and subclass JLabel – ah, there’s our answer, in similar fashion to #2 we can create a custom UI delegate and override some of the behaviour.

In this class I create a custom UI delegate just for JLabel subclasses from the Options module, and listen in and override properties being set.  I also make sure the parent JPanel has its background set to black.

public class OptionsAwareLabelUI extends MetalLabelUI {

  private static final Color oldHighlighted = new Color(224, 232, 246);
  private final Color fgNormal = MetalLookAndFeel.getBlack();
  private final Color bgNormal = MetalLookAndFeel.getWhite();
  private final Color bgSelected = MetalLookAndFeel.getPrimaryControlShadow();
  private final Color bgHighlighted = bgSelected.darker().darker();
  private final Border normalBorder = new EmptyBorder(6, 8, 6, 8);
  private final Border highlightBorder = new CompoundBorder(
    new CompoundBorder(
      new LineBorder(bgNormal),
      new BevelBorder(BevelBorder.LOWERED)),
    new EmptyBorder(3, 5, 3, 5));
  private boolean ignoreChanges;

@Override
public void propertyChange(PropertyChangeEvent e) {
  if (ignoreChanges) {
    super.propertyChange(e);
    return;
  }
  if (!(e.getSource() instanceof JLabel)) {
    super.propertyChange(e);
    return;
  }
  JLabel c = (JLabel) e.getSource();
  checkParent(c);
  if ("background".equals(e.getPropertyName())) {
    ignoreChanges = true;
    Color bgCurrent = c.getBackground();
    if (Color.WHITE.equals(bgCurrent)) {
      c.setBackground(bgNormal);
    } else if (oldHighlighted.equals(bgCurrent)) {
      c.setBackground(bgHighlighted);
    } else if (!bgNormal.equals(bgCurrent)) {
      c.setBackground(bgSelected);
    }
    ignoreChanges = false;
  } else if ("foreground".equals(e.getPropertyName())) {
    ignoreChanges = true;
    if (!fgNormal.equals(c.getForeground())) {
      c.setForeground(fgNormal);
    }
    ignoreChanges = false;
  } else if ("border".equals(e.getPropertyName())) {
    ignoreChanges = true;
    Border current = c.getBorder();
    if (current instanceof EmptyBorder) {
      c.setBorder(normalBorder);
    } else {
      c.setBorder(highlightBorder);
    }
    ignoreChanges = false;
  } else {
    super.propertyChange(e);
  }
}

private void checkParent(JComponent c) {
  Component parent = c.getParent();
  if (parent instanceof JPanel) {
    if (!bgNormal.equals(parent.getBackground())) {
      parent.setBackground(bgNormal);
    }
  }
}

public static ComponentUI createUI(JComponent c) {
  if (c.getClass().getName().startsWith("org.netbeans.modules.options")) {
    return new OptionsAwareLabelUI();
  } else {
    return MetalLabelUI.createUI(c);
  }
}
}

And now we get this – not perfect yet, but at least it’s the right colour-scheme!

Praxis Options dialog fixed

Conclusion

Well, a rather long first post about NetBeans but hopefully this is of use to someone else.  Just remember, they’re hacks and rely on underlying behaviour of non-public code so will need checking against new versions of the NetBeans platform.

You can find all this code in LAF module in the Praxis LIVE repository.

Now, I’m off to write some RFE’s so hopefully these hacks won’t be needed in future! :-)

About these ads

4 thoughts on “The dark arts of NetBeans hackery

  1. Hey Neil

    This is venkat ( from ez-on-da-ice ), I was wondering if you were able to change the color of the title on left hand side TopComponents, e.g. Project, Files, Services etc .. Key names in UIManager.getLookAndFeelDefaults() are not very intuitive and after trying for a while, I am at a loss. The back text in dark gray background is not very readable.

    Thanks
    Venkat

    • Hi Venkat,

      In my repo linked above you’ll notice a class called PraxisLFCustoms. This is a fork of the MetalLFCustoms in the NetBeans source code. I forked the tab UI’s too, so if you don’t want to copy them, you should re-enalble the commented out Metal tab UI’s and disable the Praxis ones.

      Notice how the LFCustoms is set in Hack#1 above, though if you’re using vanilla NimROD you’ll want to set the key as “Nb.NimRODLFCustoms”. The classloader is also important. Then your LFCustoms code will be run automatically after the look and feel is set.

      Any more questions, you might find it easier to get me at http://neilcsmith.net/contact or (even better) the NetBeans platform mailing list. Blog comments aren’t the easiest way to have an in-depth discussion! :-)

      Best wishes, Neil

  2. Pingback: NPE Hunting (Win7, Java7, custom LAF) | praxis

  3. Pingback: Dabbling with the dark arts again | praxis

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s