Wednesday, April 4, 2018

Re: GWT 2.5.1 SplitLayoutPanel draggers and touch screen devices

/*
* 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.basis.bbj.web.gwt.client.ui;

import com.google.gwt.core.client.Duration;
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.NativeEvent;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.TouchEvent;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.Widget;

/**
* 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>.BBjSplitPanel { the panel itself }</li>
* <li>.BBjSplitPanel .BBjSplitPanel-horizontalSplitter { horizontal dragger }</li>
* <li>.BBjSplitPanel .BBjSplitPanel-verticalSplitter { vertical dragger }</li>
* </ul>
*
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.SplitLayoutPanelExample}
* </p>
*/
public class SplitLayoutPanel extends DockLayoutPanel
{
private static final boolean m_touch = TouchEvent.isSupported();

class HSplitter extends Splitter
{
public HSplitter(Widget target, boolean reverse)
{
super(target, reverse);
getElement().getStyle().setPropertyPx("width", splitterSize);
setStyleName("BBjSplitPanel-horizontalSplitter");
}

@Override
protected int getAbsolutePosition()
{
return getAbsoluteLeft();
}

@Override
protected int getScrollPosition()
{
return getElement().getScrollLeft();
}

@Override
protected double getCenterSize()
{
return getCenterWidth();
}

@Override
protected int getEventPosition(Event event, boolean touch)
{
if (touch)
return event.getTouches().get(0).getClientX();
else
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 int snapClosedSize = -1;
private double centerSize, syncedCenterSize;

private boolean toggleDisplayAllowed = false;
private double lastClick = 0;

public Splitter(Widget target, boolean reverse)
{
this.target = target;
this.reverse = reverse;

setElement(Document.get().createDivElement());
if (m_touch)
sinkEvents(Event.TOUCHEVENTS);
else
sinkEvents(Event.MOUSEEVENTS);
}

@Override
public void onBrowserEvent(Event event)
{
int type = event.getTypeInt();
switch (type)
{
case Event.ONMOUSEDOWN:
case Event.ONTOUCHSTART:
if (type == Event.ONTOUCHSTART
|| event.getButton() == NativeEvent.BUTTON_LEFT)
{
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);
boolean touch = type == Event.ONTOUCHSTART;
offset = getEventPosition(event, touch) - getAbsolutePosition();
if (offset < 0)
offset = -(offset - splitterSize);
Event.setCapture(getElement());
event.preventDefault();
}
break;

case Event.ONMOUSEUP:
case Event.ONTOUCHEND:
case Event.ONTOUCHCANCEL:
mouseDown = false;

glassElem.removeFromParent();

// Handle double-clicks.
// Fake them since the double-click event aren't fired.
if (this.toggleDisplayAllowed)
{
double now = Duration.currentTimeMillis();
if (now - this.lastClick < DOUBLE_CLICK_TIMEOUT)
{
now = 0;
LayoutData layout =
(LayoutData)target.getLayoutData();
if (layout.size == 0)
{
// Restore the old size.
setAssociatedWidgetSize(layout.oldSize);
}
else
{
/*
* Collapse to size 0. We change the size
* instead of hiding the widget because hiding
* the widget can cause issues if the widget
* contains a flash component.
*/
layout.oldSize = layout.size;
setAssociatedWidgetSize(0);
}
}
this.lastClick = now;
}

Event.releaseCapture(getElement());
event.preventDefault();
break;

case Event.ONMOUSEMOVE:
case Event.ONTOUCHMOVE:
if (mouseDown)
{
boolean touch = type == Event.ONTOUCHMOVE;
int size;
if (reverse)
{
size = getTargetPosition() + getTargetSize()
- getEventPosition(event, touch) - offset;
}
else
{
size = getEventPosition(event, touch)
- getTargetPosition() - offset;
}
((LayoutData)target.getLayoutData()).hidden = false;
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);
}

public void setSnapClosedSize(int snapClosedSize)
{
this.snapClosedSize = snapClosedSize;
}

public void setToggleDisplayAllowed(boolean allowed)
{
this.toggleDisplayAllowed = allowed;
}

protected abstract int getAbsolutePosition();

protected abstract int getScrollPosition();

protected abstract double getCenterSize();

protected abstract int getEventPosition(Event event, boolean touch);

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 (snapClosedSize > 0 && size < snapClosedSize)
{
size = 0;
}
else 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 ScheduledCommand()
{
@Override
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("BBjSplitPanel-verticalSplitter");
}

@Override
protected int getAbsolutePosition()
{
return getAbsoluteTop();
}

@Override
protected int getScrollPosition()
{
return getElement().getScrollTop();
}

@Override
protected double getCenterSize()
{
return getCenterHeight();
}

@Override
protected int getEventPosition(Event event, boolean touch)
{
if (touch)
return event.getTouches().get(0).getClientY();
else
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;
private static final int DOUBLE_CLICK_TIMEOUT = 500;

/**
* 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("BBjSplitPanel");

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;
}

@Override
public void setWidgetHidden(Widget widget, boolean hidden)
{
super.setWidgetHidden(widget, hidden);
Splitter splitter = getAssociatedSplitter(widget);
if (splitter != null)
{
// The splitter is null for the center element.
super.setWidgetHidden(splitter, hidden);
}
}

/**
* 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 com.google.gwt.user.client.ui.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);
}
}

/**
* Sets a size below which the slider will close completely. This can be
* used in conjunction with {@link #setWidgetMinSize} to provide a
* speed-bump effect where the slider will stick to a preferred minimum
* size before closing completely.
*
* <p>
* This method has no effect for the
* {@link com.google.gwt.user.client.ui.DockLayoutPanel.Direction#CENTER}
* widget.
* </p>
*
* @param child
* the child whose slider should snap closed
* @param snapClosedSize
* the width below which the widget will close or -1 to disable.
*/
public void setWidgetSnapClosedSize(Widget child, int snapClosedSize)
{
assertIsChild(child);
Splitter splitter = getAssociatedSplitter(child);
// The splitter is null for the center element.
if (splitter != null)
{
splitter.setSnapClosedSize(snapClosedSize);
}
}

/**
* Sets whether or not double-clicking on the splitter should toggle the
* display of the widget.
*
* @param child
* the child whose display toggling will be allowed or not.
* @param allowed
* whether or not display toggling is allowed for this widget
*/
public void setWidgetToggleDisplayAllowed(Widget child, boolean allowed)
{
assertIsChild(child);
Splitter splitter = getAssociatedSplitter(child);
// The splitter is null for the center element.
if (splitter != null)
{
splitter.setToggleDisplayAllowed(allowed);
}
}

private void assertIsChild(Widget widget)
{
assert (widget == null) || (widget.getParent() == this)
: "The specified widget is not a child of this panel";
}

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);
}
}
FWIW, here's my hacked version of SplitLayoutPanel, which does work in iOS Mobile Safari.

You can see it running here:

https://poweredbybbj.com/apps/sc-fontchooser

On Wednesday, April 4, 2018 at 1:23:04 PM UTC-7, Whitfield wrote:
Does anyone know if the draggers for the SplitLayoutPanel can be moved when using a touch screen device such as an iPad or chromebook?  I've discovered in the application I'm working on that the draggers won't move if you try using your finger on a touch screen; other actions like clicking a button work as expected.  I'm wondering if this is the default behavior as I cannot get the draggers to work when I try the SplitLayoutPanel in the GWT Showcase example.

Please note that I do realize 2.5.1 is an old version of GWT, but cannot upgrade the version at this time.

--
You received this message because you are subscribed to the Google Groups "GWT Users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to google-web-toolkit+unsubscribe@googlegroups.com.
To post to this group, send email to google-web-toolkit@googlegroups.com.
Visit this group at https://groups.google.com/group/google-web-toolkit.
For more options, visit https://groups.google.com/d/optout.

No comments:

Post a Comment