Oops Null Pointer

Java programming related

Monthly Archives: August 2011

GXT Hover BorderLayout

I created a GXT Borderlayout that will hover out a collapsed panel (as a popup) and hover it back in when you mouse out.

There is also a hook to allow you to collapse and expand the popup panel from the BorderLayout. In my case I collapse the popup when windows are activated so they don’t cover over the popup panel.

I can’t quite get the mouse out to always close the popup – if another window is overlapping the popup and you mouse into it the popup will not close. I only respond to mouse out’s that are outside the bounds of the panel (as many elements in the panel fire mouse out events). I was also collapsing the popup of it was no longer the front most component, but this means you can’t open popup menus from the panel without closing the popup (as they become the front most panel). Let me know if you have any improvements.

Here’s the HoverBorderLayout

/**
 * Copyright 2011 Calibre Financial Technology
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
import com.extjs.gxt.ui.client.Style.LayoutRegion;
import com.extjs.gxt.ui.client.event.BaseEvent;
import com.extjs.gxt.ui.client.widget.BoxComponent;
import com.extjs.gxt.ui.client.widget.CollapsePanel;
import com.extjs.gxt.ui.client.widget.Component;
import com.extjs.gxt.ui.client.widget.ComponentHelper;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.layout.BorderLayout;
import com.extjs.gxt.ui.client.widget.layout.BorderLayoutData;

/**
 * Border layout that supports "hovering" of collapsed regions - mouse over to toggle popup, and close popup on mouse out.
 *
 * @author cpritchett
 */
public class HoverBorderLayoutGXT extends BorderLayout {
    public static final int STANDARD_COLLAPSED_WIDTH = 24;
    public static final int MIN_COLLAPSED_WIDTH = 2;

    private boolean hoverEnabled = true;
    private int collapsedWidth = MIN_COLLAPSED_WIDTH;
    private boolean showCollapseTool = false;

    public HoverBorderLayoutGXT() {}

    public HoverBorderLayoutGXT(boolean hoverEnabled) {
        this.hoverEnabled = hoverEnabled;
        if (!hoverEnabled) {
            collapsedWidth = STANDARD_COLLAPSED_WIDTH;
        }
    }

    public HoverBorderLayoutGXT(int collapsedWidth) {
        this.collapsedWidth = collapsedWidth;
    }

    public HoverBorderLayoutGXT(boolean hoverEnabled, int collapsedWidth, boolean showCollapseTool) {
        this.hoverEnabled = hoverEnabled;
        this.collapsedWidth = collapsedWidth;
        this.showCollapseTool = showCollapseTool;
    }

    // As onExpandClick is private in GXT 2.2.4 use JSNI to break encapsulation until this is fixed in 2.2.5.
    private native void onExpandClickWrapper(CollapsePanel cp) /*-{
        this.@com.extjs.gxt.ui.client.widget.layout.BorderLayout::onExpandClick(Lcom/extjs/gxt/ui/client/widget/CollapsePanel;)(cp);
    }-*/;

    @Override
    protected CollapsePanel createCollapsePanel(ContentPanel panel, BorderLayoutData data) {
        CollapsePanel cp;
        if (hoverEnabled) {
            cp = new HoverCollapsePanelGXT(panel, data, showCollapseTool) {
                protected void onExpandButton(BaseEvent be) {
                    if (isExpanded()) {
                        setExpanded(false);
                    }
                    onExpandClickWrapper(this);
                }
            };
        }
        else {
            cp = new CollapsePanel(panel, data) {
                protected void onExpandButton(BaseEvent be) {
                    if (isExpanded()) {
                        setExpanded(false);
                    }
                    onExpandClickWrapper(this);
                }
            };
        }

        BorderLayoutData collapseData = new BorderLayoutData(data.getRegion());
        collapseData.setSize(collapsedWidth);
        collapseData.setMargins(data.getMargins());
        ComponentHelper.setLayoutData(cp, collapseData);
        cp.setData("panel", panel);
        panel.setData("collapse", cp);
        return cp;
    }

    // As getRegionWidget is private in GXT 2.2.4 use JSNI to break encapsulation until this is fixed in 2.2.5.
    private native BoxComponent getRegionWidgetWrapper(LayoutRegion region) /*-{
        return this.@com.extjs.gxt.ui.client.widget.layout.BorderLayout::getRegionWidget(Lcom/extjs/gxt/ui/client/Style$LayoutRegion;)(region);
    }-*/;

    public void setPopupExpanded(LayoutRegion region, boolean expanded)  {
        Component c = getRegionWidgetWrapper(region);
        CollapsePanel cp = null;
        if (c != null && c instanceof CollapsePanel)  {
           cp = ((CollapsePanel)c);
        }
        else if (c != null && c instanceof ContentPanel) {
            cp = (CollapsePanel) ((ContentPanel)c).getData("collapse");
        }

        if (cp != null) {
            if (expanded == false && cp instanceof HoverCollapsePanelGXT) {
                ((HoverCollapsePanelGXT)cp).forceHide();
            }
            else {
                cp.setExpanded(expanded);
            }
        }
    }
}

And here is the supporting classes – the HoverCollapsePanel

/**
 * Copyright 2011 Calibre Financial Technology
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.widget.CollapsePanel;
import com.extjs.gxt.ui.client.widget.ContentPanel;
import com.extjs.gxt.ui.client.widget.layout.BorderLayoutData;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;

/**
 * In collapsed mode this supports the hiding and showing of the popup panel when hovering (mouse over / out of the
 * collapse panel and of the popup panel).
 *
 * @author cpritchett
 */
public class HoverCollapsePanelGXT extends CollapsePanel {
    // Tracks if the mouse is currently over the panel to ensure the toggle behaves correctly
    private boolean over = false;
    private boolean showHeader = true;

    /**
     * Creates a new hover supporting collapse panel
     *
     * @param panel the parent content panel
     * @param data the border layout data
     * @param showHeader Show header with expand tool. Set to false when panel size is too small for expand tool.
     */
    public HoverCollapsePanelGXT(ContentPanel panel, BorderLayoutData data, boolean showHeader) {
        super(panel, data);
        this.showHeader = showHeader;

        // Replace the standard Popup with a hover Popup
        popup = new HoverAutoHidePopupGXT()  {
            // Disable auto hide as we want to support clicks in the panel with hiding
            @Override
            protected boolean onAutoHide(Event event) {
                return false;
            }

            @Override
            protected void onMouseOut(ComponentEvent ce) {
                setExpanded(false);

                // On Popup mouseout clear the over flag unless mouseout was to this panel
                // (as the pointer is still "over" this component and we don't want to trigger another expand
                if (!HoverCollapsePanelGXT.this.el().getBounds().contains(ce.getXY())){
                    over = false;
                }
            };
        };
    }

    /**
     * Hides the popup and resets the over flag - call this when responding externals events that hide the navigator
     * (like a window activation). Resetting the over flag allows the next mouse over to respond (trigger an expand)
     */
    public void forceHide() {
        setExpanded(false);
        over = false;
    }

    @Override
    protected void onRender(Element target, int index) {
        super.onRender(target, index);

        // Ignore auto hide when clicking on this panel as the onComponentEvent will handle this.
        popup.getIgnoreList().add(this.getElement());

        if (!showHeader) {
            el().selectNode(".x-panel-header").hide();
        }
    }

    @Override
    public void onComponentEvent(ComponentEvent ce) {
        super.onComponentEvent(ce);

        // As mouseout / mouseover events are triggered multiple times in the header section,
        // store when the mouse really leaves this panel (the X and Y are outside theh bounds)
        boolean within = el().getBounds().contains(ce.getXY());
        boolean withinPopup = popup.isRendered() ? popup.el().getBounds().contains(ce.getXY()) : false;

        if (ce.getType().getEventCode() == Event.ONMOUSEOUT && !within && !withinPopup) {
            over = false;
            setExpanded(false);
        }

        if (ce.getType().getEventCode() == Event.ONMOUSEOVER && !over) {
            setExpanded(!isExpanded());
        }

        // Once the first mouseover is detected prevent other mouseovers from triggering expands
        if (ce.getType().getEventCode() == Event.ONMOUSEOVER && !over) {
            over = true;
        }
    }
}

and the HoverAutoHidePopup

/**
 * Copyright 2011 Calibre Financial Technology
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
import com.extjs.gxt.ui.client.event.ComponentEvent;
import com.extjs.gxt.ui.client.event.Events;
import com.extjs.gxt.ui.client.event.Listener;
import com.extjs.gxt.ui.client.widget.Popup;

/**
 * A Popup that hides itself on hover - on a normal mouse out (when the event is fired out side the bounds of the Popup)
 * @author cpritchett
 */
public class HoverAutoHidePopupGXT extends Popup {
    protected void onMouseOut(@SuppressWarnings("unused") ComponentEvent ce) {
        hide();
    }

    public HoverAutoHidePopupGXT() {
        addListener(Events.OnMouseOut, new Listener() {
            public void handleEvent(ComponentEvent be) {
                // Hide popup then mouse moves out of bounds
                if (!HoverAutoHidePopupGXT.this.el().getBounds().contains(be.getXY())) {
                    onMouseOut(be);
                }
            }
        });
    }
}

GXT: BorderLayout starting with a collapsed region

Calling collapse on a BorderLayout region before it has been rendered doesn’t work. One suggestion was to add a deferred command to collapse the region. I found this was “flashing” the panel (showing and then hiding it) and also that when you pop out a collapsed panel (clicking on the collapsed section) the popup was empty as panel was still effectively collapsed. Instead I used a once only AfterLayout event listener:

layoutContainer.addListener(Events.AfterLayout, new Listener<ComponentEvent>() {
  public void handleEvent(ComponentEvent be) {
    borderLayout.collapse(LayoutRegion.WEST);
    be.getComponent().removeListener(Events.AfterLayout, this);
  }
});

Using this method some child components may have trouble rendering inside the collapsed panel. One such component is a TreePanel – if you add it and then call expandAll() it will throw a JavaScriptException, caused by the parent of the collapsed panel being null.
com.google.gwt.core.client.JavaScriptException: (TypeError): this.appendChild is not a function

To counter this I added a once only Attach event listener to the panel that will be collapsed:

myContentPanel.addListener(Events.Attach, new Listener<ComponentEvent>() {
  public void handleEvent(ComponentEvent ce) {
    myTreePanel.expandAll();
    // Remove this listener - expand only once
    ce.getComponent().removeListener(Events.Attach, this);
  }
});