/*
* Copyright 2015 Red Hat, Inc. and/or its affiliates.
*
* 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 org.uberfire.client.util;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.ProvidesResize;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.Widget;
import org.uberfire.client.workbench.panels.SplitPanel;
import org.uberfire.debug.Debug;
import org.uberfire.workbench.model.CompassPosition;
import org.uberfire.workbench.model.PanelDefinition;
import static org.uberfire.plugin.PluginUtil.toInteger;
public class Layouts {
public static final int DEFAULT_CHILD_SIZE = 100;
/**
* Sets the CSS on the given widget so it automatically fills the available space, rather than being sized based on
* the amount of space required by its contents. This tends to be useful when building a UI that always fills the
* available space on the screen, as most desktop application windows do.
* <p>
* To achieve this, the element is given relative positioning with top and left set to 0px and width and height set
* to 100%. This makes the widget fill its nearest ancestor which has relative or absolute positioning. This
* technique is compatible with GWT's LayoutPanel system. Note that, like LayoutPanels, this only works if the host
* page is in standards mode (has a {@code <!DOCTYPE html>} header).
* @param w the widget that should always fill its available space, rather than being sized to fit its contents.
*/
public static void setToFillParent(Widget w) {
Element e = w.getElement();
Style s = e.getStyle();
s.setPosition(Position.RELATIVE);
s.setTop(0.0,
Unit.PX);
s.setLeft(0.0,
Unit.PX);
s.setWidth(100.0,
Unit.PCT);
s.setHeight(100.0,
Unit.PCT);
}
/**
* Returns a multi-line string detailing layout information about the given widget and each of its ancestors in the
* widget tree.
* @param w the widget to start at. Null is permitted, and results in this method returning an empty string.
* @return information about w and its ancestors, one widget per line.
*/
public static String getContainmentHierarchy(Widget w) {
return getContainmentHierarchy(w,
false);
}
/**
* Returns a multi-line string detailing layout information about the given widget and each of its ancestors in the
* widget tree, optionally setting debug IDs on each widget to assist in locating them in browser DOM explorer
* tools.
* @param w the widget to start at. Null is permitted, and results in this method returning an empty string.
* @param setDebugIds if true, the element and each of its ancestors will have its ID set to
* <code>"containment-parent-<i>depth</i>"</code>, where depth is 0 for the given widget, 1 for
* its parent, 2 for its grandparent, and so on. This ID will replace any ID that was previously set on
* the element, so it may break some CSS and even javascript functionality. Use with caution.
* @return information about w and its ancestors, one widget per line.
*/
public static String getContainmentHierarchy(Widget w,
boolean setDebugIds) {
StringBuilder sb = new StringBuilder();
int depth = 0;
while (w != null) {
if (setDebugIds) {
w.getElement().setId("containment-parent-" + depth);
}
sb.append(" " + depth + " - " + widgetInfo(w));
w = w.getParent();
depth++;
}
return sb.toString();
}
private static String widgetInfo(Widget w) {
String widgetInfo;
try {
String id = w.getElement().getId();
widgetInfo = w.getOffsetWidth() + "x" + w.getOffsetHeight() + " - " +
Debug.objectId(w) +
(id != null && id.length() > 0 ? " id=" + id : "") +
(w instanceof SplitPanel ? " divider at " + ((SplitPanel) w).getFixedWidgetSize() : "") +
(w instanceof RequiresResize ? " RequiresResize" : "") +
(w instanceof ProvidesResize ? " ProvidesResize" : "") +
" position: " + w.getElement().getStyle().getPosition() + "\n";
} catch (Throwable t) {
widgetInfo = "?x? - " +
Debug.objectId(w) +
": " + t.toString() + "\n";
}
return widgetInfo;
}
/**
* Returns a multi-line string detailing layout information about the given widget and each of its descendants in
* the widget tree.
* @param startAt the widget to start at. Null is permitted.
* @return information about w and its descendants, one widget per line. Each line is indented with leading spaces
* to illustrate the containment hierarchy.
*/
public static String getContainedHierarchy(final Widget startAt) {
IndentedLineAccumulator result = new IndentedLineAccumulator();
getContainedHierarchyRecursively(startAt,
0,
result);
return result.toString();
}
private static void getContainedHierarchyRecursively(final Widget startAt,
int depth,
IndentedLineAccumulator result) {
if (startAt == null) {
result.append(depth,
"(null)");
return;
}
result.append(depth,
widgetInfo(startAt));
if (startAt instanceof HasWidgets) {
for (Widget child : ((HasWidgets) startAt)) {
getContainedHierarchyRecursively(child,
depth + 1,
result);
}
} else if (startAt instanceof Composite) {
getContainedHierarchyRecursively(extractWidget(((Composite) startAt)),
depth + 1,
result);
}
}
private static native Widget extractWidget(Composite composite) /*-{
return composite.@com.google.gwt.user.client.ui.Composite::widget;
}-*/;
/**
* Returns the current width or height of the given panel definition.
* @param position determines which dimension (width or height) to return.
* @param definition the definition to get the size information from.
* @return the with if position is EAST or WEST; the height if position is NORTH or SOUTH. If no size is provided by the PanelDefinition the DEFAULT_CHILD_SIZE is used.
*/
public static int widthOrHeight(CompassPosition position,
PanelDefinition definition) {
switch (position) {
case NORTH:
case SOUTH:
return heightOrDefault(definition);
case EAST:
case WEST:
return widthOrDefault(definition);
default:
throw new IllegalArgumentException("Position " + position + " has no horizontal or vertial aspect.");
}
}
public static int heightOrDefault(PanelDefinition def) {
Integer height = toInteger(def.getHeightAsInt());
return height == null ? DEFAULT_CHILD_SIZE : height;
}
public static int widthOrDefault(PanelDefinition def) {
Integer width = toInteger(def.getWidthAsInt());
return width == null ? DEFAULT_CHILD_SIZE : width;
}
/**
* Disables the scrolling behaviour of the nearest scrollpanel found in the given widget's containment hierarchy.
* <p>
* FIXME this is a really horrible workaround! should instead modify UF API to allow PanelDefinition to opt out of having a scroll panel.
* The better fix would require changes to:
* <ul>
* <li>WorkbenchPartPresenter.View
* <li>WorkbenchPartView and its mock
* <li>The @WorkbenchPanel annotation
* <li>The annotation processor code generators and their tests
* </ul>
* @return true if a scroll panel was found and disabled; false if no scroll panel was found.
*/
public static boolean disableNearestScrollPanel(Widget w) {
while (w != null) {
if (w instanceof ScrollPanel) {
w.getElement().getStyle().clearOverflow();
w.getElement().getParentElement().getStyle().clearOverflow();
return true;
}
w = w.getParent();
}
return false;
}
private static class IndentedLineAccumulator {
final StringBuilder sb = new StringBuilder();
private void append(int depth,
String s) {
for (int i = 0; i < depth; i++) {
sb.append(" ");
}
sb.append(s);
}
@Override
public String toString() {
return sb.toString();
}
}
}