// -*- 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);
}
}