// -*- 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.widgets.boxes.Box.BoxDescriptor; import com.google.appinventor.common.utils.StringUtils; 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.user.client.ui.HorizontalPanel; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.allen_sauer.gwt.dnd.client.DragEndEvent; import com.allen_sauer.gwt.dnd.client.DragHandler; import com.allen_sauer.gwt.dnd.client.DragStartEvent; import com.allen_sauer.gwt.dnd.client.drop.IndexedDropController; import java.util.ArrayList; import java.util.Set; import java.util.List; import java.util.Map; /** * Defines a column-based layout for the boxes on a work area panel. * */ public final class ColumnLayout extends Layout { /** * Drag handler for detecting changes to the layout. */ public class ChangeDetector implements DragHandler { @Override public void onDragEnd(DragEndEvent event) { fireLayoutChange(); } @Override public void onDragStart(DragStartEvent event) { } @Override public void onPreviewDragEnd(DragEndEvent event) { } @Override public void onPreviewDragStart(DragStartEvent event) { } } /** * Represents a column in the layout. */ public static final class Column extends IndexedDropController { // Field names for JSON object encoding of layout private static final String NAME_WIDTH = "width"; private static final String NAME_BOXES = "boxes"; // Associated column container (this is the state of the layout when it is active) private final VerticalPanel columnPanel; // Relative column width (in percent of work area width) private int relativeWidth; // Absolute width (in pixel) private int absoluteWidth; // List of box description (this is the state of the layout when it is inactive) private final List<BoxDescriptor> boxes; /** * Creates new column. */ private Column(int relativeWidth) { this(new VerticalPanel(), relativeWidth); } /** * Creates new column. */ private Column(VerticalPanel columnPanel, int relativeWidth) { super(columnPanel); boxes = new ArrayList<BoxDescriptor>(); this.columnPanel = columnPanel; this.relativeWidth = relativeWidth; columnPanel.setWidth(relativeWidth + "%"); columnPanel.setSpacing(SPACING); } @Override protected void insert(Widget widget, int beforeIndex) { // columnPanel always contains at least one widget: the 'invisible' end marker. Therefore // beforeIndex cannot become negative. if (beforeIndex == columnPanel.getWidgetCount()) { beforeIndex--; } super.insert(widget, beforeIndex); if (widget instanceof Box) { ((Box) widget).onResize(absoluteWidth); } } /** * Invoked upon resizing of the work area panel. * * @see WorkAreaPanel#onResize(int, int) * * @param width column width in pixel */ public void onResize(int width) { absoluteWidth = width * relativeWidth / 100; columnPanel.setWidth(absoluteWidth + "px"); for (Widget w : columnPanel) { if (w instanceof Box) { ((Box) w).onResize(absoluteWidth); } else { // Top-of-column marker (otherwise invisible widget) w.setWidth(absoluteWidth + "px"); } } } /** * Add a new box to the column. * * @param type type of box * @param height height of box in pixels if not minimized * @param minimized indicates whether box is minimized */ public void add(Class<? extends Box> type, int height, boolean minimized) { boxes.add(new BoxDescriptor(type, absoluteWidth, height, minimized)); } /** * Updates the box descriptors for the boxes in the column. */ private void updateBoxDescriptors() { boxes.clear(); for (Widget w : columnPanel) { if (w instanceof Box) { boxes.add(((Box) w).getLayoutSettings()); } } } /** * Returns JSON encoding for the boxes in a column. */ private String toJson() { List<String> jsonBoxes = new ArrayList<String>(); for (int i = 0; i < columnPanel.getWidgetCount(); i++) { Widget w = columnPanel.getWidget(i); if (w instanceof Box) { jsonBoxes.add(((Box) w).getLayoutSettings().toJson()); } } return "{" + "\"" + NAME_WIDTH + "\":" + JSONUtil.toJson(relativeWidth) + "," + "\"" + NAME_BOXES + "\":[" + StringUtils.join(",", jsonBoxes) + "]" + "}"; } /** * Creates a new column from a JSON encoded layout. * * @param columnIndex index of column * @param object column in JSON format */ private static Column fromJson(int columnIndex, JSONObject object) { Column column = new Column(columnIndex); Map<String, JSONValue> properties = object.getProperties(); column.relativeWidth = JSONUtil.intFromJsonValue(properties.get(NAME_WIDTH)); for (JSONValue boxObject : properties.get(NAME_BOXES).asArray().getElements()) { column.boxes.add(BoxDescriptor.fromJson(boxObject.asObject())); } return column; } /** * Collects box types encoded in the JSON. * * @param object column in JSON format * @param boxTypes set of box types encountered so far */ private static void boxTypesFromJson(JSONObject object, Set<String> boxTypes) { Map<String, JSONValue> properties = object.getProperties(); for (JSONValue boxObject : properties.get(NAME_BOXES).asArray().getElements()) { boxTypes.add(BoxDescriptor.boxTypeFromJson(boxObject.asObject())); } } } // Spacing between columns in pixels private static final int SPACING = 5; // Field names for JSON object encoding of layout private static final String NAME_NAME = "name"; private static final String NAME_COLUMNS = "columns"; // List of columns private final List<Column> columns; // Drag handler for detecting changes to the layout private final DragHandler changeDetector; /** * Creates a new layout. */ public ColumnLayout(String name) { super(name); columns = new ArrayList<Column>(); changeDetector = new ChangeDetector(); } /** * Clears the layout (removes all existing columns etc). */ private void clear(WorkAreaPanel workArea) { for (Column column : columns) { workArea.getWidgetDragController().unregisterDropController(column); } workArea.getWidgetDragController().removeDragHandler(changeDetector); columns.clear(); } /** * Adds a new column to the layout. * * @param relativeWidth relative width of column (width of all columns * should add up to 100) * @return new layout column */ public Column addColumn(int relativeWidth) { Column column = new Column(relativeWidth); columns.add(column); return column; } @Override public void apply(WorkAreaPanel workArea) { // Clear base panel workArea.clear(); // Horizontal panel to hold columns HorizontalPanel horizontalPanel = new HorizontalPanel(); horizontalPanel.setSize("100%", "100%"); workArea.add(horizontalPanel); // Initialize columns for (Column column : columns) { horizontalPanel.add(column.columnPanel); workArea.getWidgetDragController().registerDropController(column); // Add invisible dummy widget to prevent column from collapsing when it contains no boxes column.columnPanel.add(new Label()); // Add boxes from layout List<BoxDescriptor> boxes = column.boxes; for (int index = 0; index < boxes.size(); index++) { BoxDescriptor bd = boxes.get(index); Box box = workArea.createBox(bd); if (box != null) { column.insert(box, index); box.restoreLayoutSettings(bd); } } } workArea.getWidgetDragController().addDragHandler(changeDetector); } @Override public void onResize(int width, int height) { // Calculate the usable width for the columns (which is the width of the browser client area // minus the spacing on each side of the boxes). int usableWidth = (width - ((columns.size() + 1) * SPACING)); // On startup it can happen that we receive a window resize event before the boxes are attached // to the DOM. In that case, width and height are 0, we can safely ignore because there will // soon be another resize event after the boxes are attached to the DOM. if (width > 0) { for (Column column : columns) { column.onResize(usableWidth); } } } @Override public String toJson() { List<String> jsonColumns = new ArrayList<String>(columns.size()); for (Column column : columns) { jsonColumns.add(column.toJson()); } return "{" + "\"" + NAME_NAME + "\":" + JSONUtil.toJson(getName()) + "," + "\"" + NAME_COLUMNS + "\":[" + StringUtils.join(",", jsonColumns) + "]" + "}"; } /** * Creates a new layout from a JSON encoded layout. * * @param object layout in JSON format */ public static Layout fromJson(JSONObject object, WorkAreaPanel workArea) { Map<String, JSONValue> properties = object.getProperties(); String name = properties.get(NAME_NAME).asString().getString(); ColumnLayout layout = (ColumnLayout) workArea.getLayouts().get(name); if (layout == null) { layout = new ColumnLayout(name); } layout.clear(workArea); for (JSONValue columnObject : properties.get(NAME_COLUMNS).asArray().getElements()) { layout.columns.add(Column.fromJson(layout.columns.size(), columnObject.asObject())); } return layout; } /** * Collects box types encoded in the JSON. * * @param object layout in JSON format * @param boxTypes box types encountered so far */ public static void boxTypesFromJson(JSONObject object, Set<String> boxTypes) { Map<String, JSONValue> properties = object.getProperties(); for (JSONValue columnObject : properties.get(NAME_COLUMNS).asArray().getElements()) { Column.boxTypesFromJson(columnObject.asObject(), boxTypes); } } @Override protected void fireLayoutChange() { // Need to update box descriptors before firing change event. // It is easier (maintenance-wise) to do this here instead of doing this in multiple places. for (Column column : columns) { column.updateBoxDescriptors(); } super.fireLayoutChange(); } }