The Source for Java Technology Collaboration
User: Password:



Kirill Grouchnikov's Blog

November 2005 Archives


Proposal for uniform support of third-party components in custom look-and-feels

Posted by kirillcool on November 28, 2005 at 01:21 PM | Permalink | Comments (1)

In the recent years, Java Swing market has seen a surge in custom components that aim to provide UI widgets common in desktop applications yet missing from JDK. These components include date picker, task pane, tree table and many more. Among others, you can find those at SwingX and at JideSoft. The main problem with these components is the support of custom (third-party) look-and-feels.

The general mechanism employed in Swing is to declare a UI delegate ID string in a component class, say ToggleButtonUI and have UIManager contain an entry with this key. The entry value is a fully-qualified class name of the UI delegate, say javax.swing.plaf.metal.MetalToggleButtonUI. The look-and-feel class (either system or custom) specifies such pairs for all supported components. But what happens with third-party components that are not bundled with JDK?

Here the water gets a little murky. First, the component writer should follow the same guidelines that are used in core components, allowing specifying the UI delegate in the application code. But more important is another question - how does the look-and-feel know that you have additional components with pluggable UI delegates?

Unless you have access to the look-and-feel code, you have to update the UIManager from the outside code. This can either be some initialization function supplied with the components that you are using, or the direct manipulation of UIManager tables from your own code. And things get more complicated when you want to switch look-and-feel at runtime.

The Laf-Plugin project proposes an approach that shares the responsibility between the look-and-feel and the component writers. The approach borrows ideas from the plugin mechanisms developed in the last years, where the main library looks up the configuration files in the classpath, and the plugin writers follow the conventions dictated by the main library to specify additional behaviour.

The main LookAndFeel class calls PluginManager to get the details of third-party UI components. The constructor of PluginManager gets three parameters:
  • the XML descriptor name
  • the main tag name (may be it's not needed really, but serves as a fault-protection mechanism)
  • the name of the tag that contains the fully-qualified class name of the plugin class.
The plugin class LafPlugin contains a number of functions:
  • initialize and reset - should be called by the main LookAndFeel class on initialization and theme switch.
  • getUiDelegates - returns a collection of all UI delegates for custom components. Can also return UI delegate for core Swing component, effectively overriding the UI delegate of the LAF
  • getFonts - returns font settings for custom components. The same as above - can override core settings for core Swing components
  • getDefaults - returns other settings for custom components. Same as above.
This way, the base look-and-feel library has no dependencies on third-party components, and can have as many plugins as you wish. Plugin writer must comply with the settings of the particular look-and-feel for XML name and XML tag names. In addition, she must implement the LafPlugin interface with the above functions.

If you are a provider of custom component, you will need to provide the following for each LAF:
  1. XML descriptor.
  2. Class that implements LafPlugin interface.
  3. UI delegate for each custom component.
The laf-plugin.jar library (in Documents & Files section) contains the runtime classes. You can use the following simple Ant task to put the laf-plugin classes in your main LAF runtime library:
<target name="jar-bin" description="create runtime jar">
  <delete file="${substance.drop.dir}/substance.jar" />
  <unjar src="${substance.drop.dir}/laf-plugin.jar" dest="${substance.output.dir}/"/>
  <jar compress="true" destfile="${substance.drop.dir}/substance.jar" 
        manifest="${substance.src.dir}/META-INF/MANIFEST.MF">
    <fileset dir="${substance.output.dir}/" excludes="test/**" />
    <fileset dir="${module.substance.basedir}/" includes="resources/**" />
  </jar>
</target>
This project was developed over the last month by Erik Vickroy and myself and has been successfully integrated into Substance and Liquid look-and-feels. Two examples of Substance plugins are the plugin for NetBeans components and the plugin for Ribbon components. The code itself is 1.4-compliant.

Update: the owners of Squareness and Pgs LAFs have expressed their design to adopt the proposed approach in the next versions of the respective libraries.

Crash course in writing code

Posted by kirillcool on November 23, 2005 at 04:59 AM | Permalink | Comments (3)

Following the previous entry on bug handling, here is the second chapter - writing code.
public class Developer {
   public void writeCode(AssignmentDetails details) {
      Document designDocument = details.getDesignDocument();

      // see if it was done already somewhere in the project
      if (details.isEnhancement()) {
         int myExperience = this.getExperience(Calendar.YEAR);
         if (myExperience <= 2) {
            // Can safely ignore the design requirements. If they are
            // so clever, why don't they write code anymore?
            designDocument.ignore();
            // Time to reinvent the wheel. Surely the guys before me
            // didn't know what they were doing (those "gurus"). It
            // will take me half the time with zero bugs
            details.setClosingDate(Calendar.getCurrent().add(Calendar.DAY, 2));
            // Like i need to see what's out there
            Internet.getDefaultBrowser().close();
            while (true) {
               try {
                  System.hang();
               }
               catch (InterruptedException exc) {
                  System.out.println("Will be ready in a few days");
               }
            }
         }
         else {
            if (myExperience <= 5) {
               // Time to copy-paste. If I change the existing function or
               // refactor it, I'll have to run tons of backwards-compatibility
               // tests. 
               Set<Method> methodsToCopy = details.getBase().getImplementation();
               Package newPackage = this.getProject().createNewPackage(
                   details.getBase().getPackage().getName() + ".issue" + details.getID());
               newPackage.create();
               for (Method method : methodsToCopy) {
                  method.update(details);
                  newPackage.getClass(method.getClass()).add(method);
               }
               // test
               for (TestCase case : designDocument.getTestCases()) {
                  boolean success = case.run();
                  if (!success) {
                     // damn!!
                     if (!designDocument.isReadOnly()) {
                        designDocument.getTestCases().remove(case);
                     }
                     else {
                        // damn!!!
                        designDocument.descope(case.getFunctionality());
                     }
                  }
               }
               // All tests passed successfully (woot)!
               return;
            }
            else {
               // I'm a guru. WTF? Don't they know that gurus' time is more important?
               List<Developer> devs = this.getProject().getDevelopers();
               Collections.sort(devs, new Comparator<Developer>() {
                  public int compare(Developer d1, Developer d2) {
                     return d1.getExperience(Calendar.YEAR) - d2.getExperience(Calendar.YEAR);
                  }
               });
               Developer junior = devs.get(0);
               // 0wned!!!
               return junior.writeCode(details);
            }
         }
      }
      
      // A new feature. Let's try the Internet first.
      Browser browser = Internet.getDefaultBrowser().open();
      browser.go("http://slashdot.org");
      browser.hangAround(Date.HOUR, 1);
      browser.go("http://javablogs.com");
      browser.hangAround(Date.MINUTE, 20);
      browser.search("BileBlog");
      browser.hangAround(Date.SECOND, 5);
      
      browser.go("http://www.google.com");
      browser.type(details.getShortDescription);
      browser.click(Key.ENTER);
      // No need to check that the code actually does what it's supposed
      // to. Google knows what it's doing.
      String contents = browser.getFirstLink().copy();
      
      Package package = this.getProject().getRootPackage();
      package.getClass(details.getShortDescription().trim()).write(contents);
      
      for (TestCase case : designDocument.getTestCases()) {
         boolean success = case.run();
            if (!success) {
               // Ignore - if the testers are smart enough to catch them,
               // hopefully this will be after I move to another project.
            }
         }
      }    
      
   }
}


Crash course in bug handling

Posted by kirillcool on November 17, 2005 at 05:55 AM | Permalink | Comments (1)

public class Developer {
   public void fixBugs(Set<Bug> bugs) {
      if ((bugs == null) || (bugs.size() == 0)) {
         // does this ever happen?
         throw new UnsupportedException();
      }
      if (bugs.size() > 10) {
         // unsupported
         throw new UnsupportedException("Too many bugs");
         // most probably was ignored, give another chance
         throw new IllegalArgumentException("Too many bugs");
         // most probably was ignored, nuke the bastards
         Thread.currentThread().destroy();
      }
      
      // start handling
      for (Bug bug : bugs) {
         // wait for a while, hoping it will fix itself
         Thread.sleep(10000);
         if (!bug.isActive()) {
            // that was nice
            continue;
         }
         // let's hope somebody else will take it
         Thread.yield();
         if (!bug.isActive()) {
            // what a sucker
            continue;
         }
         // come on!!! it's still active??? wtf???
         String filename = bug.getFilename();
         int tries = 0;
         while (bug.isActive() && (tries <= 10)) {
            RandomAccessFile raf = new RandomAccessFile(filename);
            // pick random file portion and change it
            raf.seek(Math.random()*raf.size());
            raf.write(" // fix for bug " + bug.getID() + " by @author\n");
            String[] keywords = {"for", "while", "do", "continue", "return", "null"};
            for (int i=0; i<100; i++) {
               raf.write(keywords[Math.random()*keywords.length]);
            }
            tries++;
         }
         
         // most probably it's a misinterpreted feature.
         if (Documentation.indexOf(bug.getDescription()) >= 0) {
            // lazy testers... with so much time on their hands why don't 
            // they just read documentation?
            bug.close();
            continue;
         }
         
         // it *should* be a feature
         Designer designer = this.getProject().getLeadDesigner();
         if (designer.acceptsAsFeature(bug.getDescription())) {
            // sucker!!! lol rotfl 0wned!!!
            bug.close();
            continue;
         }
         
         // omg, must be a real bug
         Set<Bug> delegatedBugSet = new HashSet<Bug>();
         delegatedBugSet.add(bug);
         Developer guru = Developer.fetchGuru();
         if (this == guru) {
            // damn!
            throw new UnsupportedException("Will be fixed in service pack");
         }
         guru.fixBugs(delegatedBugSet);
      }
    
      // TODO write tests
      // TODO write documentation
      // TODO update bug tracking system
   
      // seems all is done. 
   }
}


Ribbon and drop-down galleries

Posted by kirillcool on November 10, 2005 at 01:11 PM | Permalink | Comments (0)

For those who have missed, here are the previous two blog entries that showcase Office 12 ribbon component and discuss its Java implementation: Since, the Java implementation of Ribbon has improved substantially. After you are done with this entry, you are welcome to run the Web Start demo, resize and click on various ribbon components (especially on those shown in the screenshots).

If the heart of the new Office UI is ribbon, than the soul is a gallery. You are welcome to see this movie (618 MB, 41 minutes) or browse to Jensen's blog to see Office galleries. Basically, there are two types of galleries. The first type is simple - you click on some button (say "Format text") and a drop-down (or more formally popup) window is displayed. This window contains a collection of icons, each icon visually representing the action that will be performed when this icon is selected. The second type is integrated directly into the ribbon and is internally called "in-ribbon gallery". Such gallery resides directly in the ribbon space (taking as much space as it can), and provides two scrolling buttons and a drop-down button that shows the entire gallery.

Here is how it looks in Java implementation:

In-ribbon gallery (group of five icons to the left) with vertical strip of scroll and popup buttons - click to see fullsize version:



The same in-ribbon gallery with popup button clicked - the popup window containing the whole gallery is shown. Note the custom layout of icon gallery, providing options for grouping icons into logical groups and providing custom-set dimension of the popup itself with as-needed scroll. Click to see fullsize version:



The same popup gallery is opened from a simple gallery button (Theme Gallery) on the same task - click to see fullsize version:



Additional resizing behaviour of Ribbon tasks can be seen when there's too little space left to show all controls of a ribbon band (even as small icons). In this case, the entire band is shown as a single button (with "stacked" version of its main icon). Clicking on this button brings up a popup window that contains all the controls in the band (with the desired size). The controls in popup window behave in exactly the same way as they did in the ribbon, providing cascading popups.

The entire Theme band has been collapsed to a single button. Clicking on this button brings up a popup with the band controls - click to see fullsize version:



Clicking on a popup button of the in-ribbon gallery that is shown in popup brings up cascading popup with the entire gallery - click to see fullsize version:



Clicking on a regular button that is shown in popup brings up cascading popup with the entire gallery (finding the deepest common ancestor with the currently shown popup cascade and closing all its descendants) - click to see fullsize version:



As usual, the entire code (including complete Javadocs) is available at Flamingo project (the Ribbon itself can be run without Substance LAF). Currently, only one popup gallery layout has been implemented (extend IconPopupGallery). See how easy it is to create a fully-functional ribbon popup gallery using this layout:
   public SubstanceThemePopupGallery(final JFrame frame) {
      // set maximum width, maximum height (as-needed vertical scroll
      // bar will be shown) and height of a single icon row
      super(350, 400, 80);

      Map<ThemeKind, IconGroup> groups = new HashMap<ThemeKind, IconGroup>();
      // use createNewGroup function to create icon group at desired visual index
      groups.put(ThemeKind.BRIGHT, this.createNewGroup("Bright themes", 0));
      groups.put(ThemeKind.COLD, this.createNewGroup("Cold themes", 1));
      groups.put(ThemeKind.DARK, this.createNewGroup("Dark themes", 2));

      Map<String, ThemeInfo> themes = SubstanceLookAndFeel.getAllThemes();
      for (Map.Entry<String, ThemeInfo> entry : themes.entrySet()) {
         final String themeClassName = entry.getValue().getThemeClass();
         try {
            Class themeClass = Class.forName(themeClassName);
            SubstanceTheme themeInstance = (SubstanceTheme) themeClass
                  .newInstance();
            // use addIcon to add new icon to some icon group. The icon has:
            // The graphic icon itself
            // Optional title (will be displayed as two-line caption)
            // Action listener (will be activated on mouse click)
            groups.get(entry.getValue().getThemeKind()).addIcon(
                  new ThemeResizableIcon(themeInstance, 60, 40),
                  entry.getKey(), new ActionListener() {
                     public void actionPerformed(ActionEvent e) {
                        SwingUtilities.invokeLater(new Runnable() {
                           public void run() {
                              JPopupGallery.hidePopups(null);
                              SubstanceLookAndFeel
                                    .setCurrentTheme(themeClassName);
                              SwingUtilities
                                    .updateComponentTreeUI(frame
                                          .getRootPane());
                           }
                        });
                     }
                  });
         } catch (Exception e) {
            continue;
         }
      }
   }


Mylar - a very useful Eclipse plugin

Posted by kirillcool on November 06, 2005 at 01:29 AM | Permalink | Comments (4)

The Mylar plugin for Eclipse (available for versions 3.1 and 3.2M3 only) is, without doubt, one of the most innovative ways to change out interaction with IDEs.

Let's see a typical example of our daily work on Java project. You get a task, which can be an enhancement, new feature or bug fix. Typically, you have your project as a tree on the left-hand side, along with the list of all methods / fields (either as sub-branches of the project tree or as a separate tree). When you need to view or change source code, you either locate the corresponding entry in the project tree, or use one of the many shortcuts (such as Ctrl+Shift+T in Eclipse) to locate that class. Typically, you will need to access a number of classes, with a couple of methods in each one of them. Now, consider what happens in an existing project.

You have hundreds (if not thousands) of classes in tens of packages, each class having quite a few functions (depending on the previous team members, it can get to hundreds). The classes that you need to change for a particular bug fix are most likely under different packages. When you need to go back and forth between these classes, you waste valuable time (and energy) to do so. Wouldn't it be nice to have a context view of your workspace. This view would contain only relevant branches of the project tree, the classes you are working on and the methods that you are changing. Ideally, the IDE itself would track the changes you are making to the codebase, continually updating the context view. Mylar plugin for Eclipse does exactly that.

Here are few screenshots that show the differences (before and after applying Mylar):

Outline of a single class - before Mylar (click to view full-size)

mylar-outline-no-context-small.png

Outline of a single class - after Mylar (click to view full-size)

mylar-outline-context-small.png

Outline of class tree - before Mylar (click to view full-size)

mylar-package-no-context-small.png

Outline of class tree - after Mylar (click to view full-size)

mylar-package-context-small.png

Few things should be noted. First of all, Mylar is very much work in progress, so there are quite a few quirks and exceptions. In addition, if your task spans more than 5-6 classes, and you work on more than one function in each class, the UI gets a little bit overstuffed (however, you can remove the context entries manually):

mylar-too-much-small.png



"Death to the Browser" - bring on a real application platform

Posted by kirillcool on November 01, 2005 at 03:41 AM | Permalink | Comments (16)

Quote from Sean Rhody's entry on WebServices Journal (marked by me in bold):
What is needed is the Post Browser, the Next Browser, whatever name you want to give to it. Sure, it can still run HTML (the old stuff), in a container that is essentially the same as today's browser. However it should be capable of complete look-and-feel customization via a standard markup language. It should provide a rich set of custom controls and be able to access the desktop (with appropriate security, of course). It should have a native, secure, bidirectional mechanism, and one that supports multiple connections so that we can access services from multiple sources in a composite application. It should also have extensible controls so that we can extend and improve the behavior of controls and applications as needed. Furthermore those extensions should become part of the next release of the standard, which shouldn't take years to come forward.
Hmmm, kind of reminds me of what Java gives you today in form of Swing applications (surprise). Synth for XML customizable look-and-feel, JNLP security mechanism for accessing the desktop, JDIC for integrating the browser (until Flying saucer is ready), all the power of networking you can want (from raw sockets to JAX WS), and a lot of custom controls from third-party vendors.

So, all in favor of reinventing the wheel - raise your hand. All others - continue using Swing.



Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds