com.google.appinventor.client.widgets.boxes.Box.java Source code

Java tutorial

Introduction

Here is the source code for com.google.appinventor.client.widgets.boxes.Box.java

Source

// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

package com.google.appinventor.client.widgets.boxes;

import com.google.appinventor.client.Images;
import com.google.appinventor.client.Ode;
import static com.google.appinventor.client.Ode.MESSAGES;
import com.google.appinventor.client.widgets.ContextMenu;
import com.google.appinventor.client.widgets.TextButton;
import com.google.appinventor.shared.properties.json.JSONObject;
import com.google.appinventor.shared.properties.json.JSONUtil;
import com.google.appinventor.shared.properties.json.JSONValue;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.DockPanel;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.gwt.user.client.ui.PushButton;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

import java.util.Map;

/**
 * Abstract superclass for all boxes.
 *
 * <p>A box is a container widget. It automatically handles scrolling for
 * embedded widgets. Boxes can be resized, minimized and restored.
 *
 */
public abstract class Box extends HandlerPanel {

    /**
     * Describes a box in the context of a layout.
     */
    public static final class BoxDescriptor {
        // Field names for JSON encoding of box descriptors
        private static final String NAME_TYPE = "type";
        private static final String NAME_WIDTH = "width";
        private static final String NAME_HEIGHT = "height";
        private static final String NAME_MINIMIZED = "minimized";

        // Information needed to create a box in a layout
        private final String type;
        private final int width;
        private final int height;
        private final boolean minimized;

        /**
         * Creates a new box description.
         *
         * @param type  type of box
         * @param width  width of box in pixels
         * @param height  height of box in pixels if not minimized
         * @param minimized  indicates whether box is minimized
         */
        private BoxDescriptor(String type, int width, int height, boolean minimized) {
            this.type = type;
            this.width = width;
            this.height = height;
            this.minimized = minimized;
        }

        /**
         * Creates a new box description.
         *
         * @param type  type of box
         * @param width  width of box in pixels
         * @param height  height of box in pixels if not minimized
         * @param minimized  indicates whether box is minimized
         */
        public BoxDescriptor(Class<? extends Box> type, int width, int height, boolean minimized) {
            this(type.getName(), width, height, minimized);
        }

        /**
         * Returns the box type (for use by a {@link BoxRegistry}).
         *
         * @return  box type
         */
        public String getType() {
            return type;
        }

        /**
         * Encodes the box information into JSON format.
         */
        public String toJson() {
            return "{" + "\"" + NAME_TYPE + "\":" + JSONUtil.toJson(type) + "," + "\"" + NAME_WIDTH + "\":"
                    + JSONUtil.toJson(width) + "," + "\"" + NAME_HEIGHT + "\":" + JSONUtil.toJson(height) + ","
                    + "\"" + NAME_MINIMIZED + "\":" + JSONUtil.toJson(minimized) + "}";
        }

        /**
         * Creates a new box descriptor from a JSON object.
         *
         * @param object  box descriptor in JSON format
         */
        public static BoxDescriptor fromJson(JSONObject object) {
            Map<String, JSONValue> properties = object.getProperties();

            return new BoxDescriptor(JSONUtil.stringFromJsonValue(properties.get(NAME_TYPE)),
                    JSONUtil.intFromJsonValue(properties.get(NAME_WIDTH)),
                    JSONUtil.intFromJsonValue(properties.get(NAME_HEIGHT)),
                    JSONUtil.booleanFromJsonValue(properties.get(NAME_MINIMIZED)));
        }

        /**
         * Returns the type of box from a JSON object.
         *
         * @param object  box descriptor in JSON format
         */
        public static String boxTypeFromJson(JSONObject object) {
            Map<String, JSONValue> properties = object.getProperties();

            return JSONUtil.stringFromJsonValue(properties.get(NAME_TYPE));
        }
    }

    /**
     * Control for resizing boxes.
     */
    private final class ResizeControl extends PopupPanel {

        /**
         * Creates a control to resize the box.
         */
        private ResizeControl() {
            super(false); // no autohide

            VerticalPanel buttonPanel = new VerticalPanel();
            buttonPanel.setSpacing(10);
            addControlButton(buttonPanel, "-", new Command() {
                @Override
                public void execute() {
                    height = Math.max(100, height - 20);
                    restoreHeight = height;
                    onResize(width, height);
                }
            });
            addControlButton(buttonPanel, "+", new Command() {
                @Override
                public void execute() {
                    height = height + 20;
                    restoreHeight = height;
                    onResize(width, height);
                }
            });
            addControlButton(buttonPanel, MESSAGES.done(), new Command() {
                @Override
                public void execute() {
                    hide();
                }
            });
            add(buttonPanel);

            setModal(true);
            setStylePrimaryName("ode-BoxResizeControl");
        }

        /**
         * Creates a button with a click handler which will execute the given command.
         */
        private void addControlButton(VerticalPanel panel, String caption, final Command command) {
            TextButton button = new TextButton(caption);
            button.addClickHandler(new ClickHandler() {
                @Override
                public void onClick(ClickEvent event) {
                    command.execute();
                }
            });
            panel.add(button);
            panel.setCellHorizontalAlignment(button, VerticalPanel.ALIGN_CENTER);
        }
    }

    // Height of minimized box
    private static final int MINIMIZED_HEIGHT = 31;

    // Padding between header controls
    private static final int HEADER_CONTROL_PADDING = 2;

    // Constants for box decorations (note that these constants correspond to the box's style
    // definition)
    private static final int BOX_PADDING = 5;
    private static final int BOX_BORDER = 1;

    // UI elements
    private final SimplePanel body;
    private final Label captionLabel;
    private final HandlerPanel header;
    private final DockPanel headerContainer;
    private final ScrollPanel scrollPanel;
    private final PushButton minimizeButton;
    private final PushButton menuButton;

    // Indicates that the box height is changed through resize operations of the layout
    private boolean variableHeightBoxes;

    // Box dimensions
    private int width;
    private int height;

    // Height of non-minimized box
    private int restoreHeight;

    // Whether box should always begin minimized
    private boolean startMinimized;

    // Whether new captions should be highlighted
    private boolean highlightCaption;

    // Whether user has seen/acknowledged the new caption yet
    private boolean captionAlreadySeen = false;

    /**
     * Creates a new box.
     *
     * @param caption  box caption
     * @param height  box initial height in pixel
     * @param minimizable  indicates whether box can be minimized
     * @param removable  indicates whether box can be closed/removed
     * @param startMinimized indicates whether box should always start minimized
     * @param bodyPadding indicates whether box should have padding
     * @param highlightCaption indicates whether caption should be highlighted
     *                         until user has "seen" it (interacts with the box)
     */
    protected Box(String caption, int height, boolean minimizable, boolean removable, boolean startMinimized,
            boolean bodyPadding, boolean highlightCaption) {
        this.height = height;
        this.restoreHeight = height;
        this.startMinimized = startMinimized;
        this.highlightCaption = highlightCaption;

        captionLabel = new Label(caption, false);
        captionAlreadySeen = false;
        if (highlightCaption) {
            captionLabel.setStylePrimaryName("ode-Box-header-caption-highlighted");
        } else {
            captionLabel.setStylePrimaryName("ode-Box-header-caption");
        }
        header = new HandlerPanel();
        header.add(captionLabel);
        header.setWidth("100%");

        headerContainer = new DockPanel();
        headerContainer.setStylePrimaryName("ode-Box-header");
        headerContainer.setWidth("100%");
        headerContainer.add(header, DockPanel.LINE_START);

        Images images = Ode.getImageBundle();

        if (removable) {
            PushButton closeButton = Ode.createPushButton(images.boxClose(), MESSAGES.hdrClose(),
                    new ClickHandler() {
                        @Override
                        public void onClick(ClickEvent event) {
                            // TODO(user) - remove the box
                            Window.alert("Not implemented yet!");
                        }
                    });
            headerContainer.add(closeButton, DockPanel.LINE_END);
            headerContainer.setCellWidth(closeButton,
                    (closeButton.getOffsetWidth() + HEADER_CONTROL_PADDING) + "px");
        }

        if (!minimizable) {
            minimizeButton = null;
        } else {
            minimizeButton = Ode.createPushButton(images.boxMinimize(), MESSAGES.hdrMinimize(), new ClickHandler() {
                @Override
                public void onClick(ClickEvent event) {
                    if (isMinimized()) {
                        restore();
                    } else {
                        minimize();
                    }
                }
            });
            headerContainer.add(minimizeButton, DockPanel.LINE_END);
            headerContainer.setCellWidth(minimizeButton,
                    (minimizeButton.getOffsetWidth() + HEADER_CONTROL_PADDING) + "px");
        }

        if (minimizable || removable) {
            menuButton = Ode.createPushButton(images.boxMenu(), MESSAGES.hdrSettings(), new ClickHandler() {
                @Override
                public void onClick(ClickEvent event) {
                    final ContextMenu contextMenu = new ContextMenu();
                    contextMenu.addItem(MESSAGES.cmMinimize(), new Command() {
                        @Override
                        public void execute() {
                            if (!isMinimized()) {
                                minimize();
                            }
                        }
                    });
                    contextMenu.addItem(MESSAGES.cmRestore(), new Command() {
                        @Override
                        public void execute() {
                            if (isMinimized()) {
                                restore();
                            }
                        }
                    });
                    if (!variableHeightBoxes) {
                        contextMenu.addItem(MESSAGES.cmResize(), new Command() {
                            @Override
                            public void execute() {
                                restore();
                                final ResizeControl resizeControl = new ResizeControl();
                                resizeControl.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
                                    @Override
                                    public void setPosition(int offsetWidth, int offsetHeight) {
                                        // SouthEast
                                        int left = menuButton.getAbsoluteLeft() + menuButton.getOffsetWidth()
                                                - offsetWidth;
                                        int top = menuButton.getAbsoluteTop() + menuButton.getOffsetHeight();
                                        resizeControl.setPopupPosition(left, top);
                                    }
                                });
                            }
                        });
                    }
                    contextMenu.setPopupPositionAndShow(new PopupPanel.PositionCallback() {
                        @Override
                        public void setPosition(int offsetWidth, int offsetHeight) {
                            // SouthEast
                            int left = menuButton.getAbsoluteLeft() + menuButton.getOffsetWidth() - offsetWidth;
                            int top = menuButton.getAbsoluteTop() + menuButton.getOffsetHeight();
                            contextMenu.setPopupPosition(left, top);
                        }
                    });
                }
            });
            headerContainer.add(menuButton, DockPanel.LINE_END);
            headerContainer.setCellWidth(menuButton, (menuButton.getOffsetWidth() + HEADER_CONTROL_PADDING) + "px");
        } else {
            menuButton = null;
        }

        body = new SimplePanel();
        body.setSize("100%", "100%");

        scrollPanel = new ScrollPanel();
        scrollPanel.setStylePrimaryName("ode-Box-body");
        if (bodyPadding) {
            scrollPanel.addStyleName("ode-Box-body-padding");
        }
        scrollPanel.add(body);

        FlowPanel boxContainer = new FlowPanel();
        boxContainer.setStyleName("ode-Box-content");
        boxContainer.add(headerContainer);
        boxContainer.add(scrollPanel);

        setStylePrimaryName("ode-Box");
        setWidget(boxContainer);
    }

    protected Box(String caption, int height, boolean minimizable, boolean removable, boolean startMinimized,
            boolean highlightCaption) {
        this(caption, height, minimizable, removable, startMinimized, true, highlightCaption);
    }

    protected Box(String caption, int height, boolean minimizable, boolean removable, boolean startMinimized) {
        this(caption, height, minimizable, removable, startMinimized, true, false);
    }

    protected Box(String caption, int height, boolean minimizable, boolean removable) {
        this(caption, height, minimizable, removable, false, true, false);
    }

    @Override
    public void clear() {
        body.clear();
    }

    /**
     * Sets the resizing behavior of the box.
     *
     * @param variableHeightBoxes  indicates whether the box height will be
     *                             updated upon layout resize operations
     */
    public void setVariableHeightBoxes(boolean variableHeightBoxes) {
        this.variableHeightBoxes = variableHeightBoxes;
    }

    /**
     * Shows the given widget in the box.
     *
     * @param w  widget to show
     */
    public void setContent(Widget w) {
        body.setWidget(w);
    }

    /**
     * Sets the given caption for the box.
     *
     * @param caption  box caption to show
     */
    public void setCaption(String caption) {
        if (highlightCaption) {
            captionLabel.setStylePrimaryName("ode-Box-header-caption-highlighted");
            captionAlreadySeen = false;
        }
        captionLabel.setText(caption);
    }

    /**
     * Returns the box header.
     *
     * @return  box header widget
     */
    Widget getHeader() {
        return header;
    }

    /**
     * Invoked upon resizing of the box by the layout. Box height will remain
     * unmodified.
     *
     * @see Layout#onResize(int, int)
     *
     * @param width  new column width for box in pixel
     */
    protected void onResize(int width) {
        onResize(width, height);
    }

    /**
     * Invoked upon resizing of the box by the layout.
     *
     * @see Layout#onResize(int, int)
     *
     * @param width  new column width for box in pixel
     * @param height  new column height for box in pixel
     */
    protected void onResize(int width, int height) {
        this.width = width;
        this.height = height;

        if (!isMinimized()) {
            restoreHeight = height;
        }

        setSize(this.width + "px", this.height + "px");

        // In order to get the correct size for the scroll panel we need to subtract the dimensions
        // of all decorations such as padding, borders, margin etc. It is also important to set the size
        // for the scroll panel in pixels, as this seems to be the only reliably working unit.
        // We subtract padding and border sizes from top and bottom as well as the height of the box
        // header.
        int w = getOffsetWidth() - 2 * (BOX_PADDING + BOX_BORDER);
        int h = getOffsetHeight() - 2 * (BOX_PADDING + BOX_BORDER) - headerContainer.getOffsetHeight();

        // On startup it can happen that we receive a window resize event before the boxes are attached
        // to the DOM. In that case, offset width and height are 0, we can safely abort because there
        // will soon be another resize event after the boxes are attached to the DOM.
        if (w > 0 && h > 0) {
            scrollPanel.setSize(w + "px", h + "px");
        }
    }

    /**
     * Restores the box layout.
     *
     * @param bd  box descriptor with layout settings of box
     */
    public void restoreLayoutSettings(BoxDescriptor bd) {
        restoreHeight = bd.height;
        height = bd.height;

        if (bd.minimized || startMinimized) {
            minimize();
        } else {
            restore();
        }
    }

    /**
     * Returns box layout settings.
     *
     * @return  box layout settings
     */
    public BoxDescriptor getLayoutSettings() {
        return new BoxDescriptor(getClass().getName(), width, restoreHeight, isMinimized());
    }

    /**
     * Indicates whether the box is minimized.
     */
    private boolean isMinimized() {
        return height != restoreHeight;
    }

    /**
     * Minimizes a box.
     */
    private void minimize() {
        scrollPanel.setVisible(false);
        minimizeButton.getUpFace().setImage(new Image(Ode.getImageBundle().boxRestore()));
        minimizeButton.setTitle(MESSAGES.hdrRestore());

        if (highlightCaption && captionAlreadySeen) {
            captionLabel.setStylePrimaryName("ode-Box-header-caption");
        }
        captionAlreadySeen = true;

        restoreHeight = height;
        height = MINIMIZED_HEIGHT;
        onResize(width, height);
    }

    /**
     * Restores a minimized box to its previous height.
     */
    private void restore() {
        minimizeButton.getUpFace().setImage(new Image(Ode.getImageBundle().boxMinimize()));
        minimizeButton.setTitle(MESSAGES.hdrMinimize());
        scrollPanel.setVisible(true);

        if (highlightCaption && captionAlreadySeen) {
            captionLabel.setStylePrimaryName("ode-Box-header-caption");
        }
        captionAlreadySeen = true;

        height = restoreHeight;
        onResize(width, height);
    }

    /**
     * Helper method for adding style elements (in particular the rounded corners).
     */
    private void appendDecorationElement(String styleClass) {
        Element element = DOM.createDiv();
        element.setClassName(styleClass);
        getElement().appendChild(element);
    }
}