rocket.widget.client.ResizablePanel.java Source code

Java tutorial

Introduction

Here is the source code for rocket.widget.client.ResizablePanel.java

Source

/*
 * Copyright Miroslav Pokorny
 *
 * 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.
 */
package rocket.widget.client;

import java.util.Iterator;
import java.util.NoSuchElementException;

import rocket.event.client.ChangeEventListener;
import rocket.event.client.Event;
import rocket.event.client.EventBitMaskConstants;
import rocket.event.client.EventListenerAdapter;
import rocket.event.client.MouseDownEvent;
import rocket.event.client.MouseMoveEvent;
import rocket.event.client.MouseUpEvent;
import rocket.selection.client.Selection;
import rocket.style.client.ComputedStyle;
import rocket.style.client.Css;
import rocket.style.client.CssUnit;
import rocket.style.client.InlineStyle;
import rocket.util.client.Checker;
import rocket.util.client.Utilities;

import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Panel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;

/**
 * A ResizablePanel is a special panel that contains a single child widget which
 * can be resized by the user dragging any of the handles.
 * 
 * The width and height of the panel cannot be implicitly set but must be
 * controlled by sizing the child widget which will cause the panel to surround
 * it to expand /shrink to accomodate the new child widget's dimensions.
 * 
 * The width/height may be constrained by the following properties
 * <ul>
 * <li>minimum width/height</li>
 * <li>maximum width/height</li>
 * </ul>
 * 
 * When resizing elements such as images it is often useful to keep the aspect
 * ratio when the element is resized. This may be enabled by setting the
 * keepAspectRatio property to true via {@link #setKeepAspectRatio(boolean)}
 * 
 * @author Miroslav Pokorny (mP)
 */
public class ResizablePanel extends CompositePanel {

    public ResizablePanel() {
        super();
    }

    @Override
    protected void beforeCreatePanel() {
        final EventListenerDispatcher dispatcher = this.createEventListenerDispatcher();
        dispatcher.prepareListenerCollections(EventBitMaskConstants.CHANGE | EventBitMaskConstants.FOCUS
                | EventBitMaskConstants.MOUSE_OUT | EventBitMaskConstants.MOUSE_OVER);
        this.setEventListenerDispatcher(dispatcher);
    }

    @Override
    protected void checkPanel(final Panel panel) {
        throw new UnsupportedOperationException("checkPanel");
    }

    @Override
    protected Panel createPanel() {
        final Grid grid = new Grid(2, 2);

        grid.setBorderWidth(0);
        grid.setCellPadding(0);
        grid.setCellSpacing(0);
        grid.setStyleName("");

        return grid;
    }

    protected Grid getGrid() {
        return (Grid) this.getPanel();
    }

    /**
     * Prepares the 4 cells, adding the right style
     */
    @Override
    protected void afterCreatePanel() {
        this.prepareCell(0, 0, this.getWidgetStyle());
        this.prepareCell(0, 1, this.getRightHandleStyle());
        this.prepareCell(1, 0, this.getBottomHandleStyle());
        this.prepareCell(1, 1, this.getCornerHandleStyle());
    }

    protected String getWidgetStyle() {
        return WidgetConstants.RESIZABLE_PANEL_WIDGET_STYLE;
    }

    protected String getRightHandleStyle() {
        return WidgetConstants.RESIZABLE_PANEL_RIGHT_HANDLE_STYLE;
    }

    protected String getBottomHandleStyle() {
        return WidgetConstants.RESIZABLE_PANEL_BOTTOM_HANDLE_STYLE;
    }

    protected String getCornerHandleStyle() {
        return WidgetConstants.RESIZABLE_PANEL_CORNER_HANDLE_STYLE;
    }

    /**
     * Prepares one of the cells that belong to the grid that is used to create
     * and display the ResizablePanel.
     * 
     * @param row
     * @param column
     * @param styleName
     */
    protected void prepareCell(final int row, final int column, final String styleName) {
        Checker.notNull("parameter:styleName", styleName);

        final Element element = this.getGrid().getCellFormatter().getElement(row, column);
        element.setClassName(styleName);

        element.setInnerHTML(" ");
    }

    @Override
    protected void onAttach() {
        super.onAttach();

        // only attach listeners when widget is attached.
        this.setHandleEventListeners(this.createMouseListener(true, false), this.createMouseListener(true, true),
                this.createMouseListener(false, true));
    }

    @Override
    protected void onDetach() {
        super.onDetach();

        // remove event listeners from handles..
        this.setHandleEventListeners(null, null, null);
    }

    /**
     * Helper which sets or removes out the listeners for the right, corner and
     * bottom resizable panel handles.
     * 
     * Null parameters actually remove the event listener for that particular
     * element.
     * 
     * @param right
     * @param corner
     * @param bottom
     */
    protected void setHandleEventListeners(final EventListener right, final EventListener corner,
            final EventListener bottom) {
        final CellFormatter formatter = this.getGrid().getCellFormatter();
        final Element rightElement = formatter.getElement(0, 1);
        DOM.setEventListener(rightElement, right);
        DOM.sinkEvents(rightElement, EventBitMaskConstants.MOUSE_DOWN);

        final Element cornerElement = formatter.getElement(1, 1);
        DOM.setEventListener(cornerElement, corner);
        DOM.sinkEvents(cornerElement, EventBitMaskConstants.MOUSE_DOWN);

        final Element bottomElement = formatter.getElement(1, 0);
        DOM.setEventListener(bottomElement, bottom);
        DOM.sinkEvents(bottomElement, EventBitMaskConstants.MOUSE_DOWN);
    }

    /**
     * Factory which creates the appropriate mouse listener for a particular
     * handle.
     * 
     * @param canUpdateWidth
     * @param canUpdateHeight
     * @return
     */
    protected HandleMouseDragger createMouseListener(final boolean canUpdateWidth, final boolean canUpdateHeight) {
        return new HandleMouseDragger() {
            @Override
            protected boolean canUpdateWidth() {
                return canUpdateWidth;
            }

            @Override
            protected boolean canUpdateHeight() {
                return canUpdateHeight;
            }
        };
    }

    /**
     * Abstract class with several methods that tell whether or not the panel
     * should stretch in that particular direction.
     * 
     * @author n9834386
     */
    abstract class HandleMouseDragger extends EventListenerAdapter
            implements com.google.gwt.user.client.EventPreview {

        /**
         * This method is fired when the user initiates a resize by pressing the
         * mouse button.
         */
        @Override
        protected void onMouseDown(final MouseDownEvent event) {
            Checker.notNull("parameter:event", event);

            // stop mouse selection...
            Selection.disableTextSelection();
            event.stop();

            // save the panel's width/height for later usage, particularly when
            // attempting to maintain the aspect ratio.
            final Widget widget = ResizablePanel.this.getWidget();
            widget.addStyleName(ResizablePanel.this.getDraggedWidgetStyle());

            final Element element = widget.getElement();
            final ComputedStyle elementComputedStyle = ComputedStyle.getComputedStyle(element);

            final int width = elementComputedStyle.getInteger(Css.WIDTH, CssUnit.PX, 0);
            this.setInitialWidgetWidth(width);

            final int height = elementComputedStyle.getInteger(Css.HEIGHT, CssUnit.PX, 0);
            this.setInitialWidgetHeight(height);

            // record the initial mouse position
            this.setInitialMouseX(event.getPageX());
            this.setInitialMouseY(event.getPageY());

            // install self as a event preview
            this.install();
        }

        /**
         * Routes the event preview event.
         */
        public boolean onEventPreview(final com.google.gwt.user.client.Event rawEvent) {
            Event event = null;
            boolean cancelled = false;

            try {
                event = Event.getEvent(rawEvent);

                this.beforeDispatching(event);
                this.dispatch(event);
                this.afterDispatching(event);

                cancelled = event.isCancelled();

            } finally {
                Utilities.destroyIfNecessary(event);
            }

            return !cancelled;
        }

        void install() {
            DOM.addEventPreview(this);
        }

        void uninstall() {
            DOM.removeEventPreview(this);
        }

        /**
         * The position of the mouse when the drag was initiated.
         */
        int initialMouseX;

        int getInitialMouseX() {
            return this.initialMouseX;
        }

        void setInitialMouseX(final int initialMouseX) {
            this.initialMouseX = initialMouseX;
        }

        int initialMouseY;

        int getInitialMouseY() {
            return this.initialMouseY;
        }

        void setInitialMouseY(final int initialMouseY) {
            this.initialMouseY = initialMouseY;
        }

        protected void onMouseMove(final MouseMoveEvent event) {
            boolean changed = false;
            if (this.canUpdateWidth()) {
                changed = this.updateWidth(event);
            }
            if (this.canUpdateHeight()) {
                this.updateHeight(event);
                changed = true;
            }

            if (changed) {
                ResizablePanel.this.getEventListenerDispatcher().getChangeEventListeners()
                        .fireChange(ResizablePanel.this);
            }

            event.cancelBubble(true);
            event.stop();
        }

        protected boolean updateWidth(final MouseMoveEvent event) {
            final int initialMousePageX = this.getInitialMouseX();

            final int deltaX = event.getPageX() - initialMousePageX;

            final int width = this.getInitialWidgetWidth();
            int newWidth = width + deltaX;

            newWidth = Math.min(Math.max(newWidth, ResizablePanel.this.getMinimumWidth()),
                    ResizablePanel.this.getMaximumWidth());

            final Widget widget = ResizablePanel.this.getWidget();
            widget.setWidth(newWidth + "px");

            // if aspect ratio is enabled also update the height...
            if (ResizablePanel.this.isKeepAspectRatio()) {
                final float ratio = this.getAspectRatio();
                final int newHeight = (int) (newWidth / ratio);
                widget.setHeight(newHeight + "px");
            }
            return deltaX != 0;
        }

        protected boolean updateHeight(final MouseMoveEvent event) {
            final int initialMousePageY = this.getInitialMouseY();

            final int deltaY = event.getPageY() - initialMousePageY;

            final int height = this.getInitialWidgetHeight();
            int newHeight = height + deltaY;

            newHeight = Math.min(Math.max(newHeight, ResizablePanel.this.getMinimumHeight()),
                    ResizablePanel.this.getMaximumHeight());

            final Widget widget = ResizablePanel.this.getWidget();
            widget.setHeight(newHeight + "px");

            // if aspect ratio is enabled also update the height...
            if (ResizablePanel.this.isKeepAspectRatio()) {
                final float ratio = this.getAspectRatio();
                final int newWidth = (int) (newHeight * ratio);
                widget.setWidth(newWidth + "px");
            }

            return deltaY != 0;
        }

        /**
         * When the mouse is released fire interested event listeners, uninstall
         * the evnet previewer and reenable text selection.
         */
        protected void onMouseUp(final MouseUpEvent event) {
            final Widget widget = ResizablePanel.this.getWidget();
            widget.removeStyleName(ResizablePanel.this.getDraggedWidgetStyle());

            Selection.enableTextSelection();
            event.cancelBubble(true);
            event.stop();

            this.uninstall();
        }

        /**
         * The initial width of the ResizablePanel's only child widget
         */
        int initialWidgetWidth;

        protected int getInitialWidgetWidth() {
            return initialWidgetWidth;
        }

        protected void setInitialWidgetWidth(final int initialWidgetWidth) {
            this.initialWidgetWidth = initialWidgetWidth;
        }

        /**
         * The initial height of the ResizablePanel's only child widget
         */
        int initialWidgetHeight;

        protected int getInitialWidgetHeight() {
            return this.initialWidgetHeight;
        }

        protected void setInitialWidgetHeight(final int initialWidgetHeight) {
            this.initialWidgetHeight = initialWidgetHeight;
        }

        /**
         * Returns the aspect ratio of the resizable panel expressed as a ratio
         * of width/height. A value of 2.0 indicates the width is twice as wide
         * as the height.
         * 
         * @return
         */
        protected float getAspectRatio() {
            return this.getInitialWidgetWidth() * 1.0f / this.getInitialWidgetHeight();
        }

        /**
         * Corner and Right handle sub classes will return true otherwise
         * returns false.
         * 
         * @return
         */
        abstract protected boolean canUpdateWidth();

        /**
         * Corner and bottom handle sub classes will return true otherwise
         * returns false.
         * 
         * @return
         */
        abstract protected boolean canUpdateHeight();
    }

    @Override
    protected String getInitialStyleName() {
        return WidgetConstants.RESIZABLE_PANEL_STYLE;
    }

    @Override
    protected int getSunkEventsBitMask() {
        return 0;
    }

    public Widget getWidget() {
        return this.getGrid().getWidget(0, 0);
    }

    public void setWidget(final Widget widget) {
        this.getGrid().setWidget(0, 0, widget);
    }

    @Override
    public void insert(final Widget widget, final int indexBefore) {
        if (this.getWidgetCount() > 0 || indexBefore > 0) {
            throw new IllegalArgumentException(this.getClass().getName() + " can only have one widget.");
        }
        this.setWidget(widget);
    }

    @Override
    public boolean remove(final Widget widget) {
        return this.getGrid().remove(widget);
    }

    /**
     * This iterator may be used to iterate over the solitary widget contained
     * by this ResizablePanel
     */
    @Override
    public Iterator iterator() {
        return new Iterator<Widget>() {

            int state = 0;

            public boolean hasNext() {
                return this.state == 0 && ResizablePanel.this.getWidget() != null;
            }

            public Widget next() {
                if (false == this.hasNext()) {
                    throw new NoSuchElementException();
                }
                this.state = 1;
                return ResizablePanel.this.getWidget();
            }

            public void remove() {
                if (this.state != 1) {
                    throw new IllegalStateException();
                }
                this.state = 2;
            }
        };
    }

    protected String getDraggedWidgetStyle() {
        return WidgetConstants.RESIZABLE_PANEL_DRAGGED_WIDGET_STYLE;
    }

    @Override
    public void setWidth(final String width) {
        throw new UnsupportedOperationException("setWidth()");
    }

    @Override
    public void setHeight(final String height) {
        throw new UnsupportedOperationException("setHeight()");
    }

    /**
     * The minimum width in pixels that the child widget may be set.
     */
    private int minimumWidth;

    public int getMinimumWidth() {
        Checker.greaterThan("field:minimumWidth", 0, minimumWidth);
        return this.minimumWidth;
    }

    public void setMinimumWidth(final int minimumWidth) {
        Checker.greaterThan("parameter:minimumWidth", 0, minimumWidth);
        this.minimumWidth = minimumWidth;
    }

    /**
     * The maximum width in pixels that the child widget may be set.
     */
    private int maximumWidth;

    public int getMaximumWidth() {
        Checker.greaterThan("field:maximumWidth", 0, maximumWidth);
        return this.maximumWidth;
    }

    public void setMaximumWidth(final int maximumWidth) {
        Checker.greaterThan("parameter:maximumWidth", 0, maximumWidth);
        this.maximumWidth = maximumWidth;
    }

    /**
     * The minimum height in pixels that the child widget may be set.
     */
    private int minimumHeight;

    public int getMinimumHeight() {
        Checker.greaterThan("field:minimumHeight", 0, minimumHeight);
        return this.minimumHeight;
    }

    public void setMinimumHeight(final int minimumHeight) {
        Checker.greaterThan("parameter:minimumHeight", 0, minimumHeight);
        this.minimumHeight = minimumHeight;
    }

    /**
     * The maximum height in pixels that the child widget may be set.
     */
    private int maximumHeight;

    public int getMaximumHeight() {
        Checker.greaterThan("field:maximumHeight", 0, maximumHeight);
        return this.maximumHeight;
    }

    public void setMaximumHeight(final int maximumHeight) {
        Checker.greaterThan("parameter:maximumHeight", 0, maximumHeight);
        this.maximumHeight = maximumHeight;
    }

    /**
     * When this flag is true any increases/decreases in width or height also
     * change the other coordinate maintaining the aspect ratio for the child
     * widget. This is especially useful for images where the aspect ratio is
     * especially important.
     */
    private boolean keepAspectRatio;

    public boolean isKeepAspectRatio() {
        return this.keepAspectRatio;
    }

    public void setKeepAspectRatio(final boolean keepAspectRatio) {
        this.keepAspectRatio = keepAspectRatio;
        this.setCornerHandleVisibility(!keepAspectRatio);
    }

    /**
     * The corner handle should not be visible when keeping the aspect ratio.
     * 
     * @param visible
     */
    protected void setCornerHandleVisibility(final boolean visible) {
        final Element element = this.getGrid().getCellFormatter().getElement(1, 1);
        InlineStyle.getInlineStyle(element).setString(Css.VISIBILITY, visible ? "visible" : "hidden");
    }

    public void addChangeEventListener(final ChangeEventListener changeEventListener) {
        this.getEventListenerDispatcher().addChangeEventListener(changeEventListener);
    }

    public void removeChangeEventListener(final ChangeEventListener changeEventListener) {
        this.getEventListenerDispatcher().removeChangeEventListener(changeEventListener);
    }

    @Override
    public String toString() {
        return super.toString() + ", minimumWidth: " + this.minimumWidth + ", maximumWidth: " + maximumWidth
                + ", minimumHeight: " + this.minimumHeight + ", maximumHeight: " + maximumHeight
                + ", keepAspectRatio: " + this.keepAspectRatio;
    }
}