Friday, May 27, 2011

Making Links Work Right in a SmartGWT App on IE

The GUI of RHQ 4.0 and later is built upon SmartGWT. In many places in the SmartGWT app, we embedded raw HTML to render fragment links (e.g. #Inventory/Servers) to other places in the app. We used raw HTML, rather than widgets, for a few reasons:

  1. SmartGWT does not provide a link widget. GWT provides the HyperLink and InlineHyperLink widgets, but we try to avoid using non-SmartGWT widgets when possible to prevent layout issues or CSS issues caused by straying from the SmartGWT framework. A SmartGWT Label or HTMLFlow can be extended to simulate a link using a ClickHandler but it will not be rendered as an 'a' tag and so will not inherit the CSS styles used for 'a' tags and will not display the link's URL in the browser status bar when the user hovers over the link.
  2. Many of our links are inside ListGrid cells. There is no straightforward reliable way to embed arbitrary widgets in ListGrid cells. I tried using the mechanism http://www.smartclient.com/smartgwt/showcase/#grid_cell_widgets described here and encountered overflow and wrapping issues, which I was unable to overcome. The CellFormatter interface only supports returning a String, but that String can include HTML, so that's what we ended up using for cells that need to contain a link.
  3. For FormItems that need to contain links, CanvasItem can be extended in order to embed GWT HyperLink widgets, but using a StaticTextItem with HTML embedded in its value is more straightforward.
Unfortunately, we noticed that clicking on any of our raw HTML fragment links in IE caused a full page refresh, which is not at all desirable in a GWT app, which is intended to be pure AJAX. Further investigation revealed that this is a longstanding quirk (aka bug) in IE; rather than simply generating a history event for the URL with the updated fragment, it sends an unnecessary request for the URL to the server. If you use the GWT HyperLink widget, GWT uses some JavaScript fanciness involving iframes to circumvent the IE bug and make fragment links work properly. However, since we were using raw HTML for all the links in the RHQ GUI, this magic was not there for us. Converting all our HTML links would be a ton of work and was simply not a viable option for links in ListGrid cells for the reasons described above, so we needed to find a way to execute GWT's magic when any of our raw HTML links were clicked. The answer ended up being to add a native preview event handler that intercepts browser click events and executes the magic if the click was on one of our 'a' tags. We did this by making our EntryPoint class implement the GWT Event.NativePreviewHandler interface as follows:
    public void onPreviewNativeEvent(Event.NativePreviewEvent event) {
        if (SC.isIE() && event.getTypeInt() == Event.ONCLICK) {
            NativeEvent nativeEvent = event.getNativeEvent();
            EventTarget target = nativeEvent.getEventTarget();
            if (Element.is(target)) {
                Element element = Element.as(target);
                if ("a".equalsIgnoreCase(element.getTagName())) {
                    // make sure it's not a hyperlink that GWT already
                    // handles
                    if (element.getPropertyString("__listener") == null) {
                        String url = element.getAttribute("href");
                        String historyToken = getHistoryToken(url);
                        if (historyToken != null) {
                            GWT.log("Forcing History.newItem(\"" +
                                historyToken + "\")...");
                            History.newItem(historyToken);
                            nativeEvent.preventDefault();
                        }
                    }
                }
            }
        }
    }

    private static String getHistoryToken(String url) {
        String token;
        if (url.startsWith("#")) {
            token = url.substring(1);
        } else if (url.startsWith("/#")) {
            token = url.substring(2);
        } else if (url.contains(Location.getHost()) && url.indexOf('#') > 0) {
            token = url.substring(url.indexOf('#') + 1);
        } else {
            token = null;
        }
        return token;
    }
We then add the native preview handler at app load time by adding the following line to our EntryPoint class:
Event.addNativePreviewHandler(this);
This solution is working great. However, we still might eventually go back and switch over to using GWT HyperLinks, rather than raw HTML, in places where it is feasible, such as FormItems, since it is generally better to use widgets rather than raw HTML to keep things object-oriented and leave the generation of HTML, CSS, and JavaScript to the framework.

Setting Up IntelliJ IDEA on Fedora

JetBrains does not provide rpm's for IDEA, so setting it up on Fedora requires a bit of extra work. Here are the steps that I do.

Unzip the Distribution
# cd /opt
# tar -xzvf /path/to/ideaIC-10.5.tar.gz

This will create the directory /opt/idea-IC-107.105.

Create Symbolic Links
# cd /opt
# ln -sf /opt/idea-IC-107.105 /opt/idea
# ln -s /opt/idea/bin/idea.sh /usr/local/bin/idea

I overwrite the /opt/idea symlink each time I install a new version of IDEA. Creating the /usr/local/bin/idea symlink effectively adds idea to the system PATH. You'll only need to create this one the very first time you install IDEA.

Set JDK Environment Variable

idea.sh checks the IDEA_JDK, JDK_HOME and JAVA_HOME environment variables, in that order of preference, to determine the JVM it will use to run IDEA. Make sure one of these points to a JDK 6 installation. I am currently using JRockit, but the Sun JDK or OpenJDK will also work.

Update idea.vmoptions

The projects I work on are usually quite large, and my box is pretty beefy, so I always pump up the JVM's heap size and permgen size.
# cd /opt/idea/bin
# mv idea.vmoptions idea.vmoptions.orig
# cp /path/to/my/idea.vmoptions .

For reference, here's my idea.vmoptions file, though you will probably not want to use it verbatim:
-Xms1500M
-Xmx3000M
-XX:MaxPermSize=500M

Create a GNOME .desktop File

This will add an application launcher to the GNOME Applications menu. As root, create a file named idea.desktop in /usr/share/applications with the following contents:
[Desktop Entry]
Encoding=UTF-8
Version=1.0
Name=IntelliJ IDEA
GenericName=Java IDE
Comment=IntelliJ IDEA is a code-centric IDE focused on developer productivity. The editor deeply understands your code and knows its way around the codebase, makes great suggestions right when you need them, and is always ready to help you shape your code.
Exec=idea
Icon=/opt/idea/bin/idea_CE128.png
Terminal=false
Type=Application
Categories=Development;IDE

If you wish to tweak this file at all, the syntax for .desktop files can be found here.