blob: 0a2422f813b09dc5996f611b4cf7eff163534be5 [file] [log] [blame]
/*
* Copyright 2009 Google Inc.
*
* 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 com.google.gwt.user.client.ui;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
/**
* A panel that adds user-positioned splitters between each of its child
* widgets.
*
* <p>
* This panel is used in the same way as {@link DockLayoutPanel}, except that
* its children's sizes are always specified in {@link Unit#PX} units, and each
* pair of child widgets has a splitter between them that the user can drag.
* </p>
*
* <p>
* This widget will <em>only</em> work in standards mode, which requires that
* the HTML page in which it is run have an explicit &lt;!DOCTYPE&gt;
* declaration.
* </p>
*
* <h3>CSS Style Rules</h3>
* <ul class='css'>
* <li>.gwt-SplitLayoutPanel { the panel itself }</li>
* <li>.gwt-SplitLayoutPanel .gwt-SplitLayoutPanel-HDragger { horizontal dragger
* }</li>
* <li>.gwt-SplitLayoutPanel .gwt-SplitLayoutPanel-VDragger { vertical dragger }
* </li>
* </ul>
*
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.SplitLayoutPanelExample}
* </p>
*/
public class SplitLayoutPanel extends DockLayoutPanel {
class HSplitter extends Splitter {
public HSplitter(Widget target, boolean reverse) {
super(target, reverse);
getElement().getStyle().setPropertyPx("width", splitterSize);
setStyleName("gwt-SplitLayoutPanel-HDragger");
}
@Override
protected int getAbsolutePosition() {
return getAbsoluteLeft();
}
@Override
protected double getCenterSize() {
return getCenterWidth();
}
@Override
protected int getEventPosition(Event event) {
return event.getClientX();
}
@Override
protected int getTargetPosition() {
return target.getAbsoluteLeft();
}
@Override
protected int getTargetSize() {
return target.getOffsetWidth();
}
}
abstract class Splitter extends Widget {
protected final Widget target;
private int offset;
private boolean mouseDown;
private ScheduledCommand layoutCommand;
private final boolean reverse;
private int minSize;
private double centerSize, syncedCenterSize;
public Splitter(Widget target, boolean reverse) {
this.target = target;
this.reverse = reverse;
setElement(Document.get().createDivElement());
sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONMOUSEMOVE
| Event.ONDBLCLICK);
}
@Override
public void onBrowserEvent(Event event) {
switch (event.getTypeInt()) {
case Event.ONMOUSEDOWN:
mouseDown = true;
/*
* Resize glassElem to take up the entire scrollable window area,
* which is the greater of the scroll size and the client size.
*/
int width = Math.max(Window.getClientWidth(),
Document.get().getScrollWidth());
int height = Math.max(Window.getClientHeight(),
Document.get().getScrollHeight());
glassElem.getStyle().setHeight(height, Unit.PX);
glassElem.getStyle().setWidth(width, Unit.PX);
Document.get().getBody().appendChild(glassElem);
offset = getEventPosition(event) - getAbsolutePosition();
Event.setCapture(getElement());
event.preventDefault();
break;
case Event.ONMOUSEUP:
mouseDown = false;
glassElem.removeFromParent();
Event.releaseCapture(getElement());
event.preventDefault();
break;
case Event.ONMOUSEMOVE:
if (mouseDown) {
int size;
if (reverse) {
size = getTargetPosition() + getTargetSize()
- getEventPosition(event) - offset;
} else {
size = getEventPosition(event) - getTargetPosition() - offset;
}
setAssociatedWidgetSize(size);
event.preventDefault();
}
break;
}
}
public void setMinSize(int minSize) {
this.minSize = minSize;
LayoutData layout = (LayoutData) target.getLayoutData();
// Try resetting the associated widget's size, which will enforce the new
// minSize value.
setAssociatedWidgetSize((int) layout.size);
}
protected abstract int getAbsolutePosition();
protected abstract double getCenterSize();
protected abstract int getEventPosition(Event event);
protected abstract int getTargetPosition();
protected abstract int getTargetSize();
private double getMaxSize() {
// To avoid seeing stale center size values due to deferred layout
// updates, maintain our own copy up to date and resync when the
// DockLayoutPanel value changes.
double newCenterSize = getCenterSize();
if (syncedCenterSize != newCenterSize) {
syncedCenterSize = newCenterSize;
centerSize = newCenterSize;
}
return Math.max(((LayoutData) target.getLayoutData()).size + centerSize,
0);
}
private void setAssociatedWidgetSize(double size) {
double maxSize = getMaxSize();
if (size > maxSize) {
size = maxSize;
}
if (size < minSize) {
size = minSize;
}
LayoutData layout = (LayoutData) target.getLayoutData();
if (size == layout.size) {
return;
}
// Adjust our view until the deferred layout gets scheduled.
centerSize += layout.size - size;
layout.size = size;
// Defer actually updating the layout, so that if we receive many
// mouse events before layout/paint occurs, we'll only update once.
if (layoutCommand == null) {
layoutCommand = new Command() {
public void execute() {
layoutCommand = null;
forceLayout();
}
};
Scheduler.get().scheduleDeferred(layoutCommand);
}
}
}
class VSplitter extends Splitter {
public VSplitter(Widget target, boolean reverse) {
super(target, reverse);
getElement().getStyle().setPropertyPx("height", splitterSize);
setStyleName("gwt-SplitLayoutPanel-VDragger");
}
@Override
protected int getAbsolutePosition() {
return getAbsoluteTop();
}
@Override
protected double getCenterSize() {
return getCenterHeight();
}
@Override
protected int getEventPosition(Event event) {
return event.getClientY();
}
@Override
protected int getTargetPosition() {
return target.getAbsoluteTop();
}
@Override
protected int getTargetSize() {
return target.getOffsetHeight();
}
}
private static final int DEFAULT_SPLITTER_SIZE = 8;
/**
* The element that masks the screen so we can catch mouse events over
* iframes.
*/
private static Element glassElem = null;
private final int splitterSize;
/**
* Construct a new {@link SplitLayoutPanel} with the default splitter size of
* 8px.
*/
public SplitLayoutPanel() {
this(DEFAULT_SPLITTER_SIZE);
}
/**
* Construct a new {@link SplitLayoutPanel} with the specified splitter size
* in pixels.
*
* @param splitterSize the size of the splitter in pixels
*/
public SplitLayoutPanel(int splitterSize) {
super(Unit.PX);
this.splitterSize = splitterSize;
setStyleName("gwt-SplitLayoutPanel");
if (glassElem == null) {
glassElem = Document.get().createDivElement();
glassElem.getStyle().setPosition(Position.ABSOLUTE);
glassElem.getStyle().setTop(0, Unit.PX);
glassElem.getStyle().setLeft(0, Unit.PX);
glassElem.getStyle().setMargin(0, Unit.PX);
glassElem.getStyle().setPadding(0, Unit.PX);
glassElem.getStyle().setBorderWidth(0, Unit.PX);
// We need to set the background color or mouse events will go right
// through the glassElem. If the SplitPanel contains an iframe, the
// iframe will capture the event and the slider will stop moving.
glassElem.getStyle().setProperty("background", "white");
glassElem.getStyle().setOpacity(0.0);
}
}
/**
* Return the size of the splitter in pixels.
*
* @return the splitter size
*/
public int getSplitterSize() {
return splitterSize;
}
@Override
public void insert(Widget child, Direction direction, double size, Widget before) {
super.insert(child, direction, size, before);
if (direction != Direction.CENTER) {
insertSplitter(child, before);
}
}
@Override
public boolean remove(Widget child) {
assert !(child instanceof Splitter) : "Splitters may not be directly removed";
int idx = getWidgetIndex(child);
if (super.remove(child)) {
// Remove the associated splitter, if any.
// Now that the widget is removed, idx is the index of the splitter.
if (idx < getWidgetCount()) {
// Call super.remove(), or we'll end up recursing.
super.remove(getWidget(idx));
}
return true;
}
return false;
}
/**
* Sets the minimum allowable size for the given widget.
*
* <p>
* Its associated splitter cannot be dragged to a position that would make it
* smaller than this size. This method has no effect for the
* {@link DockLayoutPanel.Direction#CENTER} widget.
* </p>
*
* @param child the child whose minimum size will be set
* @param minSize the minimum size for this widget
*/
public void setWidgetMinSize(Widget child, int minSize) {
assertIsChild(child);
Splitter splitter = getAssociatedSplitter(child);
// The splitter is null for the center element.
if (splitter != null) {
splitter.setMinSize(minSize);
}
}
private Splitter getAssociatedSplitter(Widget child) {
// If a widget has a next sibling, it must be a splitter, because the only
// widget that *isn't* followed by a splitter must be the CENTER, which has
// no associated splitter.
int idx = getWidgetIndex(child);
if (idx > -1 && idx < getWidgetCount() - 1) {
Widget splitter = getWidget(idx + 1);
assert splitter instanceof Splitter : "Expected child widget to be splitter";
return (Splitter) splitter;
}
return null;
}
private void insertSplitter(Widget widget, Widget before) {
assert getChildren().size() > 0 : "Can't add a splitter before any children";
LayoutData layout = (LayoutData) widget.getLayoutData();
Splitter splitter = null;
switch (getResolvedDirection(layout.direction)) {
case WEST:
splitter = new HSplitter(widget, false);
break;
case EAST:
splitter = new HSplitter(widget, true);
break;
case NORTH:
splitter = new VSplitter(widget, false);
break;
case SOUTH:
splitter = new VSplitter(widget, true);
break;
default:
assert false : "Unexpected direction";
}
super.insert(splitter, layout.direction, splitterSize, before);
}
}