/* * Copyright 2000-2022 Vaadin Ltd. * * 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.vaadin.v7.client.widgets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.animation.client.Animation; import com.google.gwt.animation.client.AnimationScheduler; import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback; import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle; import com.google.gwt.core.client.Duration; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.core.client.JsArray; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.DivElement; 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.Node; import com.google.gwt.dom.client.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; import com.google.gwt.dom.client.TableRowElement; import com.google.gwt.dom.client.TableSectionElement; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.logging.client.LogConfiguration; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.RootPanel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.DeferredWorker; import com.vaadin.client.Profiler; import com.vaadin.client.WidgetUtil; import com.vaadin.client.ui.SubPartAware; import com.vaadin.shared.Range; import com.vaadin.shared.util.SharedUtil; import com.vaadin.v7.client.widget.escalator.Cell; import com.vaadin.v7.client.widget.escalator.ColumnConfiguration; import com.vaadin.v7.client.widget.escalator.EscalatorUpdater; import com.vaadin.v7.client.widget.escalator.FlyweightCell; import com.vaadin.v7.client.widget.escalator.FlyweightRow; import com.vaadin.v7.client.widget.escalator.PositionFunction; import com.vaadin.v7.client.widget.escalator.PositionFunction.AbsolutePosition; import com.vaadin.v7.client.widget.escalator.PositionFunction.Translate3DPosition; import com.vaadin.v7.client.widget.escalator.PositionFunction.TranslatePosition; import com.vaadin.v7.client.widget.escalator.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.v7.client.widget.escalator.Row; import com.vaadin.v7.client.widget.escalator.RowContainer; import com.vaadin.v7.client.widget.escalator.RowContainer.BodyRowContainer; import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeEvent; import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.v7.client.widget.escalator.ScrollbarBundle; import com.vaadin.v7.client.widget.escalator.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.v7.client.widget.escalator.ScrollbarBundle.VerticalScrollbarBundle; import com.vaadin.v7.client.widget.escalator.Spacer; import com.vaadin.v7.client.widget.escalator.SpacerUpdater; import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedEvent; import com.vaadin.v7.client.widget.escalator.events.SpacerVisibilityChangedEvent; import com.vaadin.v7.client.widget.grid.events.ScrollEvent; import com.vaadin.v7.client.widget.grid.events.ScrollHandler; import com.vaadin.v7.client.widgets.Escalator.JsniUtil.TouchHandlerBundle; import com.vaadin.v7.shared.ui.grid.HeightMode; import com.vaadin.v7.shared.ui.grid.ScrollDestination; /*- Maintenance Notes! Reading these might save your day. (note for editors: line width is 80 chars, including the one-space indentation) == Row Container Structure AbstractRowContainer |-- AbstractStaticRowContainer | |-- HeaderRowContainer | `-- FooterContainer `---- BodyRowContainerImpl AbstractRowContainer is intended to contain all common logic between RowContainers. It manages the bookkeeping of row count, makes sure that all individual cells are rendered the same way, and so on. AbstractStaticRowContainer has some special logic that is required by all RowContainers that don't scroll (hence the word "static"). HeaderRowContainer and FooterRowContainer are pretty thin special cases of a StaticRowContainer (mostly relating to positioning of the root element). BodyRowContainerImpl could also be split into an additional "AbstractScrollingRowContainer", but I felt that no more inner classes were needed. So it contains both logic required for making things scroll about, and equivalent special cases for layouting, as are found in Header/FooterRowContainers. == The Three Indices Each RowContainer can be thought to have three levels of indices for any given displayed row (but the distinction matters primarily for the BodyRowContainerImpl, because of the way it scrolls through data): - Logical index - Physical (or DOM) index - Visual index LOGICAL INDEX is the index that is linked to the data source. If you want your data source to represent a SQL database with 10 000 rows, the 7 000:th row in the SQL has a logical index of 6 999, since the index is 0-based (unless that data source does some funky logic). PHYSICAL INDEX is the index for a row that you see in a browser's DOM inspector. If your row is the second element within a tag, it has a physical index of 1 (because of 0-based indices). In Header and FooterRowContainers, you are safe to assume that the logical index is the same as the physical index. But because the BodyRowContainerImpl never displays large data sources entirely in the DOM, a physical index usually has no apparent direct relationship with its logical index. VISUAL INDEX is the index relating to the order that you see a row in, in the browser, as it is rendered. The topmost row is 0, the second is 1, and so on. The visual index is similar to the physical index in the sense that Header and FooterRowContainers can assume a 1:1 relationship between visual index and logical index. And again, BodyRowContainerImpl has no such relationship. The body's visual index has additionally no apparent relationship with its physical index. Because the tags are reused in the body and visually repositioned with CSS as the user scrolls, the relationship between physical index and visual index is quickly broken. You can get an element's visual index via the field BodyRowContainerImpl.visualRowOrder. Currently, the physical and visual indices are kept in sync _most of the time_ by a deferred rearrangement of rows. They become desynced when scrolling. This is to help screen readers to read the contents from the DOM in a natural order. See BodyRowContainerImpl.DeferredDomSorter for more about that. */ /** * A workaround-class for GWT and JSNI. *

* GWT is unable to handle some method calls to Java methods in inner-classes * from within JSNI blocks. Having that inner class extend a non-inner-class (or * implement such an interface), makes it possible for JSNI to indirectly refer * to the inner class, by invoking methods and fields in the non-inner-class * API. * * @see Escalator.Scroller */ abstract class JsniWorkaround { /** * A JavaScript function that handles the scroll DOM event, and passes it on * to Java code. * * @see #createScrollListenerFunction(Escalator) * @see Escalator#onScroll() * @see Escalator.Scroller#onScroll() */ protected final JavaScriptObject scrollListenerFunction; /** * A JavaScript function that handles the mousewheel DOM event, and passes * it on to Java code. * * @see #createMousewheelListenerFunction(Escalator) * @see Escalator#onScroll() * @see Escalator.Scroller#onScroll() */ protected final JavaScriptObject mousewheelListenerFunction; /** * A JavaScript function that handles the touch start DOM event, and passes * it on to Java code. * * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) */ protected JavaScriptObject touchStartFunction; /** * A JavaScript function that handles the touch move DOM event, and passes * it on to Java code. * * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) */ protected JavaScriptObject touchMoveFunction; /** * A JavaScript function that handles the touch end and cancel DOM events, * and passes them on to Java code. * * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent) */ protected JavaScriptObject touchEndFunction; protected TouchHandlerBundle touchHandlerBundle; protected JsniWorkaround(final Escalator escalator) { scrollListenerFunction = createScrollListenerFunction(escalator); mousewheelListenerFunction = createMousewheelListenerFunction( escalator); touchHandlerBundle = new TouchHandlerBundle(escalator); touchStartFunction = touchHandlerBundle.getTouchStartHandler(); touchMoveFunction = touchHandlerBundle.getTouchMoveHandler(); touchEndFunction = touchHandlerBundle.getTouchEndHandler(); } /** * A method that constructs the JavaScript function that will be stored into * {@link #scrollListenerFunction}. * * @param esc * a reference to the current instance of {@link Escalator} * @see Escalator#onScroll() */ protected abstract JavaScriptObject createScrollListenerFunction( Escalator esc); /** * A method that constructs the JavaScript function that will be stored into * {@link #mousewheelListenerFunction}. * * @param esc * a reference to the current instance of {@link Escalator} * @see Escalator#onScroll() */ protected abstract JavaScriptObject createMousewheelListenerFunction( Escalator esc); } /** * A low-level table-like widget that features a scrolling virtual viewport and * lazily generated rows. * * @since 7.4 * @author Vaadin Ltd */ public class Escalator extends Widget implements RequiresResize, DeferredWorker, SubPartAware { // todo comments legend /* * [[optimize]]: There's an opportunity to rewrite the code in such a way * that it _might_ perform better (remember to measure, implement, * re-measure) */ /* * [[mpixscroll]]: This code will require alterations that are relevant for * supporting the scrolling through more pixels than some browsers normally * would support. (i.e. when we support more than "a million" pixels in the * escalator DOM). NOTE: these bits can most often also be identified by * searching for code that call scrollElem.getScrollTop();. */ /* * [[spacer]]: Code that is important to make spacers work. */ /** * A utility class that contains utility methods that are usually called * from JSNI. *

* The methods are moved in this class to minimize the amount of JSNI code * as much as feasible. */ static class JsniUtil { public static class TouchHandlerBundle { public static final String POINTER_EVENT_TYPE_TOUCH = "touch"; /** * A JavaScriptObject overlay for the * JavaScript * TouchEvent object. *

* This needs to be used in the touch event handlers, since GWT's * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent} * can't be cast from the JSNI call, and the * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't * properly populated with the correct values. */ private static final class CustomTouchEvent extends JavaScriptObject { protected CustomTouchEvent() { } public native NativeEvent getNativeEvent() /*-{ return this; }-*/; public native int getPageX() /*-{ return this.targetTouches[0].pageX; }-*/; public native int getPageY() /*-{ return this.targetTouches[0].pageY; }-*/; public native String getPointerType() /*-{ return this.pointerType; }-*/; } private final Escalator escalator; public TouchHandlerBundle(final Escalator escalator) { this.escalator = escalator; } public native JavaScriptObject getTouchStartHandler() /*-{ // we need to store "this", since it won't be preserved on call. var self = this; return $entry(function (e) { self.@com.vaadin.v7.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e); }); }-*/; public native JavaScriptObject getTouchMoveHandler() /*-{ // we need to store "this", since it won't be preserved on call. var self = this; return $entry(function (e) { self.@com.vaadin.v7.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e); }); }-*/; public native JavaScriptObject getTouchEndHandler() /*-{ // we need to store "this", since it won't be preserved on call. var self = this; return $entry(function (e) { self.@com.vaadin.v7.client.widgets.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e); }); }-*/; // Duration of the inertial scrolling simulation. Devices with // larger screens take longer durations. private static final int DURATION = Window.getClientHeight(); // multiply scroll velocity with repeated touching private int acceleration = 1; private boolean touching = false; // Two movement objects for storing status and processing touches private Movement yMov, xMov; static final double MIN_VEL = 0.6, MAX_VEL = 4, F_VEL = 1500, F_ACC = 0.7, F_AXIS = 1; // The object to deal with one direction scrolling private class Movement { final List speeds = new ArrayList(); final ScrollbarBundle scroll; double position, offset, velocity, prevPos, prevTime, delta; boolean run, vertical; public Movement(boolean vertical) { this.vertical = vertical; scroll = vertical ? escalator.verticalScrollbar : escalator.horizontalScrollbar; } public void startTouch(CustomTouchEvent event) { speeds.clear(); prevPos = pagePosition(event); prevTime = Duration.currentTimeMillis(); } public void moveTouch(CustomTouchEvent event) { double pagePosition = pagePosition(event); if (pagePosition > -1) { delta = prevPos - pagePosition; double now = Duration.currentTimeMillis(); double ellapsed = now - prevTime; velocity = delta / ellapsed; // if last speed was so low, reset speeds and start // storing again if (!speeds.isEmpty() && !validSpeed(speeds.get(0))) { speeds.clear(); run = true; } speeds.add(0, velocity); prevTime = now; prevPos = pagePosition; } } public void endTouch(CustomTouchEvent event) { // Compute average speed velocity = 0; for (double s : speeds) { velocity += s / speeds.size(); } position = scroll.getScrollPos(); // Compute offset, and adjust it with an easing curve so as // movement is smoother. offset = F_VEL * velocity * acceleration * easingInOutCos(velocity, MAX_VEL); // Enable or disable inertia movement in this axis run = validSpeed(velocity); if (run) { event.getNativeEvent().preventDefault(); } } void validate(Movement other) { if (!run || other.velocity > 0 && Math.abs(velocity / other.velocity) < F_AXIS) { delta = offset = 0; run = false; } } void stepAnimation(double progress) { scroll.setScrollPos(position + offset * progress); } int pagePosition(CustomTouchEvent event) { // Use native event's screen x and y for IE11 and Edge // since there is no touches for these browsers (#18737) if (isCurrentBrowserIE11OrEdge()) { return vertical ? event.getNativeEvent().getClientY() + Window.getScrollTop() : event.getNativeEvent().getClientX() + Window.getScrollLeft(); } JsArray a = event.getNativeEvent().getTouches(); return vertical ? a.get(0).getPageY() : a.get(0).getPageX(); } boolean validSpeed(double speed) { return Math.abs(speed) > MIN_VEL; } } // Using GWT animations which take care of native animation frames. private Animation animation = new Animation() { @Override public void onUpdate(double progress) { xMov.stepAnimation(progress); yMov.stepAnimation(progress); } @Override public double interpolate(double progress) { return easingOutCirc(progress); }; @Override public void onComplete() { touching = false; escalator.body.domSorter.reschedule(); }; @Override public void run(int duration) { if (xMov.run || yMov.run) { super.run(duration); } else { onComplete(); } }; }; public void touchStart(final CustomTouchEvent event) { if (allowTouch(event)) { if (yMov == null) { yMov = new Movement(true); xMov = new Movement(false); } if (animation.isRunning()) { acceleration += F_ACC; event.getNativeEvent().preventDefault(); animation.cancel(); } else { acceleration = 1; } xMov.startTouch(event); yMov.startTouch(event); touching = true; } else { touching = false; animation.cancel(); acceleration = 1; } } public void touchMove(final CustomTouchEvent event) { if (touching) { xMov.moveTouch(event); yMov.moveTouch(event); xMov.validate(yMov); yMov.validate(xMov); event.getNativeEvent().preventDefault(); moveScrollFromEvent(escalator, xMov.delta, yMov.delta, event.getNativeEvent()); } } public void touchEnd(final CustomTouchEvent event) { if (touching) { xMov.endTouch(event); yMov.endTouch(event); xMov.validate(yMov); yMov.validate(xMov); // Adjust duration so as longer movements take more duration boolean vert = !xMov.run || yMov.run && Math.abs(yMov.offset) > Math.abs(xMov.offset); double delta = Math.abs((vert ? yMov : xMov).offset); animation.run((int) (3 * DURATION * easingOutExp(delta))); } } // Allow touchStart for IE11 and Edge even though there is no touch // (#18737), // otherwise allow touch only if there is a single touch in the // event private boolean allowTouch( final TouchHandlerBundle.CustomTouchEvent event) { if (isCurrentBrowserIE11OrEdge()) { return (POINTER_EVENT_TYPE_TOUCH .equals(event.getPointerType())); } else { return (event.getNativeEvent().getTouches().length() == 1); } } private double easingInOutCos(double val, double max) { return 0.5 - 0.5 * Math.cos(Math.PI * Math.signum(val) * Math.min(Math.abs(val), max) / max); } private double easingOutExp(double delta) { return (1 - Math.pow(2, -delta / 1000)); } private double easingOutCirc(double progress) { return Math.sqrt(1 - (progress - 1) * (progress - 1)); } } public static void moveScrollFromEvent(final Escalator escalator, final double deltaX, final double deltaY, final NativeEvent event) { if (!Double.isNaN(deltaX)) { escalator.horizontalScrollbar.setScrollPosByDelta(deltaX); } if (!Double.isNaN(deltaY)) { escalator.verticalScrollbar.setScrollPosByDelta(deltaY); } /* * TODO: only prevent if not scrolled to end/bottom. Or no? UX team * needs to decide. */ final boolean warrantedYScroll = deltaY != 0 && escalator.verticalScrollbar.showsScrollHandle(); final boolean warrantedXScroll = deltaX != 0 && escalator.horizontalScrollbar.showsScrollHandle(); if (warrantedYScroll || warrantedXScroll) { event.preventDefault(); } } } /** * ScrollDestination case-specific handling logic. */ private static double getScrollPos(final ScrollDestination destination, final double targetStartPx, final double targetEndPx, final double viewportStartPx, final double viewportEndPx, final double padding) { final double viewportLength = viewportEndPx - viewportStartPx; switch (destination) { /* * Scroll as little as possible to show the target element. If the * element fits into view, this works as START or END depending on the * current scroll position. If the element does not fit into view, this * works as START. */ case ANY: { final double startScrollPos = targetStartPx - padding; final double endScrollPos = targetEndPx + padding - viewportLength; if (startScrollPos < viewportStartPx) { return startScrollPos; } else if (targetEndPx + padding > viewportEndPx) { return endScrollPos; } else { // NOOP, it's already visible return viewportStartPx; } } /* * Scrolls so that the element is shown at the end of the viewport. The * viewport will, however, not scroll before its first element. */ case END: { return targetEndPx + padding - viewportLength; } /* * Scrolls so that the element is shown in the middle of the viewport. * The viewport will, however, not scroll beyond its contents, given * more elements than what the viewport is able to show at once. Under * no circumstances will the viewport scroll before its first element. */ case MIDDLE: { final double targetMiddle = targetStartPx + (targetEndPx - targetStartPx) / 2; return targetMiddle - viewportLength / 2; } /* * Scrolls so that the element is shown at the start of the viewport. * The viewport will, however, not scroll beyond its contents. */ case START: { return targetStartPx - padding; } /* * Throw an error if we're here. This can only mean that * ScrollDestination has been carelessly amended.. */ default: { throw new IllegalArgumentException( "Internal: ScrollDestination has been modified, " + "but Escalator.getScrollPos has not been updated " + "to match new values."); } } } /** An inner class that handles all logic related to scrolling. */ private class Scroller extends JsniWorkaround { private double lastScrollTop = 0; private double lastScrollLeft = 0; public Scroller() { super(Escalator.this); } @Override protected native JavaScriptObject createScrollListenerFunction( Escalator esc) /*-{ var vScroll = esc.@com.vaadin.v7.client.widgets.Escalator::verticalScrollbar; var vScrollElem = vScroll.@com.vaadin.v7.client.widget.escalator.ScrollbarBundle::getElement()(); var hScroll = esc.@com.vaadin.v7.client.widgets.Escalator::horizontalScrollbar; var hScrollElem = hScroll.@com.vaadin.v7.client.widget.escalator.ScrollbarBundle::getElement()(); return $entry(function(e) { var target = e.target; // in case the scroll event was native (i.e. scrollbars were dragged, or // the scrollTop/Left was manually modified), the bundles have old cache // values. We need to make sure that the caches are kept up to date. if (target === vScrollElem) { vScroll.@com.vaadin.v7.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()(); } else if (target === hScrollElem) { hScroll.@com.vaadin.v7.client.widget.escalator.ScrollbarBundle::updateScrollPosFromDom()(); } else { $wnd.console.error("unexpected scroll target: "+target); } }); }-*/; @Override protected native JavaScriptObject createMousewheelListenerFunction( Escalator esc) /*-{ return $entry(function(e) { var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX; var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY; // Delta mode 0 is in pixels; we don't need to do anything... // A delta mode of 1 means we're scrolling by lines instead of pixels // We need to scale the number of lines by the default line height if (e.deltaMode === 1) { var brc = esc.@com.vaadin.v7.client.widgets.Escalator::body; deltaY *= brc.@com.vaadin.v7.client.widgets.Escalator.AbstractRowContainer::getDefaultRowHeight()(); } // Other delta modes aren't supported if ((e.deltaMode !== undefined) && (e.deltaMode >= 2 || e.deltaMode < 0)) { var msg = "Unsupported wheel delta mode \"" + e.deltaMode + "\""; // Print warning message esc.@com.vaadin.v7.client.widgets.Escalator::logWarning(*)(msg); } // IE8 has only delta y if (isNaN(deltaY)) { deltaY = -0.5*e.wheelDelta; } @com.vaadin.v7.client.widgets.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e); }); }-*/; /** * Recalculates the virtual viewport represented by the scrollbars, so * that the sizes of the scroll handles appear correct in the browser */ public void recalculateScrollbarsForVirtualViewport() { double scrollContentHeight = body.calculateTotalRowHeight() + body.spacerContainer.getSpacerHeightsSum(); double scrollContentWidth = columnConfiguration.calculateRowWidth(); double tableWrapperHeight = heightOfEscalator; double tableWrapperWidth = widthOfEscalator; boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + WidgetUtil.PIXEL_EPSILON - header.getHeightOfSection() - footer.getHeightOfSection(); boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth + WidgetUtil.PIXEL_EPSILON; // One dimension got scrollbars, but not the other. Recheck time! if (verticalScrollNeeded != horizontalScrollNeeded) { if (!verticalScrollNeeded && horizontalScrollNeeded) { verticalScrollNeeded = scrollContentHeight > tableWrapperHeight + WidgetUtil.PIXEL_EPSILON - header.getHeightOfSection() - footer.getHeightOfSection() - horizontalScrollbar.getScrollbarThickness(); } else { horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth + WidgetUtil.PIXEL_EPSILON - verticalScrollbar.getScrollbarThickness(); } } // let's fix the table wrapper size, since it's now stable. if (verticalScrollNeeded) { tableWrapperWidth -= verticalScrollbar.getScrollbarThickness(); tableWrapperWidth = Math.max(0, tableWrapperWidth); } if (horizontalScrollNeeded) { tableWrapperHeight -= horizontalScrollbar .getScrollbarThickness(); tableWrapperHeight = Math.max(0, tableWrapperHeight); } tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX); tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX); double footerHeight = footer.getHeightOfSection(); double headerHeight = header.getHeightOfSection(); double vScrollbarHeight = Math.max(0, tableWrapperHeight - footerHeight - headerHeight); verticalScrollbar.setOffsetSize(vScrollbarHeight); verticalScrollbar.setScrollSize(scrollContentHeight); /* * If decreasing the amount of frozen columns, and scrolled to the * right, the scroll position might reset. So we need to remember * the scroll position, and re-apply it once the scrollbar size has * been adjusted. */ double prevScrollPos = horizontalScrollbar.getScrollPos(); double unfrozenPixels = columnConfiguration .getCalculatedColumnsWidth(Range.between( columnConfiguration.getFrozenColumnCount(), columnConfiguration.getColumnCount())); double frozenPixels = scrollContentWidth - unfrozenPixels; double hScrollOffsetWidth = tableWrapperWidth - frozenPixels; horizontalScrollbar.setOffsetSize(hScrollOffsetWidth); horizontalScrollbar.setScrollSize(unfrozenPixels); horizontalScrollbar.getElement().getStyle().setLeft(frozenPixels, Unit.PX); horizontalScrollbar.setScrollPos(prevScrollPos); /* * only show the scrollbar wrapper if the scrollbar itself is * visible. */ if (horizontalScrollbar.showsScrollHandle()) { horizontalScrollbarDeco.getStyle().clearDisplay(); } else { horizontalScrollbarDeco.getStyle().setDisplay(Display.NONE); } /* * only show corner background divs if the vertical scrollbar is * visible. */ Style hCornerStyle = headerDeco.getStyle(); Style fCornerStyle = footerDeco.getStyle(); if (verticalScrollbar.showsScrollHandle()) { hCornerStyle.clearDisplay(); fCornerStyle.clearDisplay(); if (horizontalScrollbar.showsScrollHandle()) { double offset = horizontalScrollbar.getScrollbarThickness(); fCornerStyle.setBottom(offset, Unit.PX); } else { fCornerStyle.clearBottom(); } } else { hCornerStyle.setDisplay(Display.NONE); fCornerStyle.setDisplay(Display.NONE); } } /** * Logical scrolling event handler for the entire widget. */ public void onScroll() { final double scrollTop = verticalScrollbar.getScrollPos(); final double scrollLeft = horizontalScrollbar.getScrollPos(); if (lastScrollLeft != scrollLeft) { for (int i = 0; i < columnConfiguration.frozenColumns; i++) { header.updateFreezePosition(i, scrollLeft); body.updateFreezePosition(i, scrollLeft); footer.updateFreezePosition(i, scrollLeft); } position.set(headElem, -scrollLeft, 0); /* * TODO [[optimize]]: cache this value in case the instanceof * check has undesirable overhead. This could also be a * candidate for some deferred binding magic so that e.g. * AbsolutePosition is not even considered in permutations that * we know support something better. That would let the compiler * completely remove the entire condition since it knows that * the if will never be true. */ if (position instanceof AbsolutePosition) { /* * we don't want to put "top: 0" on the footer, since it'll * render wrong, as we already have * "bottom: $footer-height". */ footElem.getStyle().setLeft(-scrollLeft, Unit.PX); } else { position.set(footElem, -scrollLeft, 0); } lastScrollLeft = scrollLeft; } body.setBodyScrollPosition(scrollLeft, scrollTop); lastScrollTop = scrollTop; body.updateEscalatorRowsOnScroll(); body.spacerContainer.updateSpacerDecosVisibility(); /* * TODO [[optimize]]: Might avoid a reflow by first calculating new * scrolltop and scrolleft, then doing the escalator magic based on * those numbers and only updating the positions after that. */ } public native void attachScrollListener(Element element) /* * Attaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ if (element.addEventListener) { element.addEventListener("scroll", this.@com.vaadin.v7.client.widgets.JsniWorkaround::scrollListenerFunction); } else { element.attachEvent("onscroll", this.@com.vaadin.v7.client.widgets.JsniWorkaround::scrollListenerFunction); } }-*/; public native void detachScrollListener(Element element) /* * Detaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ if (element.addEventListener) { element.removeEventListener("scroll", this.@com.vaadin.v7.client.widgets.JsniWorkaround::scrollListenerFunction); } else { element.detachEvent("onscroll", this.@com.vaadin.v7.client.widgets.JsniWorkaround::scrollListenerFunction); } }-*/; public native void attachMousewheelListener(Element element) /* * Attaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ // firefox likes "wheel", while others use "mousewheel" var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel'; element.addEventListener(eventName, this.@com.vaadin.v7.client.widgets.JsniWorkaround::mousewheelListenerFunction); }-*/; public native void detachMousewheelListener(Element element) /* * Detaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ // firefox likes "wheel", while others use "mousewheel" var eventName = element.onwheel===undefined?"mousewheel":"wheel"; element.removeEventListener(eventName, this.@com.vaadin.v7.client.widgets.JsniWorkaround::mousewheelListenerFunction); }-*/; public native void attachTouchListeners(Element element) /* * Detaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ element.addEventListener("touchstart", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchStartFunction); element.addEventListener("touchmove", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchMoveFunction); element.addEventListener("touchend", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); element.addEventListener("touchcancel", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); }-*/; public native void detachTouchListeners(Element element) /* * Detaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ element.removeEventListener("touchstart", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchStartFunction); element.removeEventListener("touchmove", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchMoveFunction); element.removeEventListener("touchend", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); element.removeEventListener("touchcancel", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); }-*/; /** * Using pointerdown, pointermove, pointerup, and pointercancel for IE11 * and Edge instead of touch* listeners (#18737) * * @param element */ public native void attachPointerEventListeners(Element element) /* * Attaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ element.addEventListener("pointerdown", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchStartFunction); element.addEventListener("pointermove", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchMoveFunction); element.addEventListener("pointerup", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); element.addEventListener("pointercancel", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); }-*/; /** * Using pointerdown, pointermove, pointerup, and pointercancel for IE11 * and Edge instead of touch* listeners (#18737) * * @param element */ public native void detachPointerEventListeners(Element element) /* * Detaching events with JSNI instead of the GWT event mechanism because * GWT didn't provide enough details in events, or triggering the event * handlers with GWT bindings was unsuccessful. Maybe, with more time * and skill, it could be done with better success. JavaScript overlay * types might work. This might also get rid of the JsniWorkaround * class. */ /*-{ element.removeEventListener("pointerdown", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchStartFunction); element.removeEventListener("pointermove", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchMoveFunction); element.removeEventListener("pointerup", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); element.removeEventListener("pointercancel", this.@com.vaadin.v7.client.widgets.JsniWorkaround::touchEndFunction); }-*/; public void scrollToColumn(final int columnIndex, final ScrollDestination destination, final int padding) { assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column"; /* * To cope with frozen columns, we just pretend those columns are * not there at all when calculating the position of the target * column and the boundaries of the viewport. The resulting * scrollLeft will be correct without compensation since the DOM * structure effectively means that scrollLeft also ignores the * frozen columns. */ final double frozenPixels = columnConfiguration .getCalculatedColumnsWidth(Range.withLength(0, columnConfiguration.frozenColumns)); final double targetStartPx = columnConfiguration .getCalculatedColumnsWidth(Range.withLength(0, columnIndex)) - frozenPixels; final double targetEndPx = targetStartPx + columnConfiguration.getColumnWidthActual(columnIndex); final double viewportStartPx = getScrollLeft(); double viewportEndPx = viewportStartPx + WidgetUtil .getRequiredWidthBoundingClientRectDouble(getElement()) - frozenPixels; if (verticalScrollbar.showsScrollHandle()) { viewportEndPx -= WidgetUtil.getNativeScrollbarSize(); } final double scrollLeft = getScrollPos(destination, targetStartPx, targetEndPx, viewportStartPx, viewportEndPx, padding); /* * note that it doesn't matter if the scroll would go beyond the * content, since the browser will adjust for that, and everything * fall into line accordingly. */ setScrollLeft(scrollLeft); } public void scrollToRow(final int rowIndex, final ScrollDestination destination, final double padding) { final double targetStartPx = (body.getDefaultRowHeight() * rowIndex) + body.spacerContainer .getSpacerHeightsSumUntilIndex(rowIndex); final double targetEndPx = targetStartPx + body.getDefaultRowHeight(); final double viewportStartPx = getScrollTop(); final double viewportEndPx = viewportStartPx + body.getHeightOfSection(); final double scrollTop = getScrollPos(destination, targetStartPx, targetEndPx, viewportStartPx, viewportEndPx, padding); /* * note that it doesn't matter if the scroll would go beyond the * content, since the browser will adjust for that, and everything * falls into line accordingly. */ setScrollTop(scrollTop); } } public abstract class AbstractRowContainer implements RowContainer { private EscalatorUpdater updater = EscalatorUpdater.NULL; private int rows; /** * The table section element ({@code }, {@code } or * {@code }) the rows (i.e. <tr> tags) are * contained in. */ protected final TableSectionElement root; /** * The primary style name of the escalator. Most commonly provided by * Escalator as "v-escalator". */ private String primaryStyleName = null; private boolean defaultRowHeightShouldBeAutodetected = true; private double defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT; public AbstractRowContainer( final TableSectionElement rowContainerElement) { root = rowContainerElement; } @Override public TableSectionElement getElement() { return root; } /** * Gets the tag name of an element to represent a cell in a row. *

* Usually {@code "th"} or {@code "td"}. *

* Note: To actually create such an element, use * {@link #createCellElement(int, int)} instead. * * @return the tag name for the element to represent cells as * @see #createCellElement(int, int) */ protected abstract String getCellElementTagName(); @Override public EscalatorUpdater getEscalatorUpdater() { return updater; } /** * {@inheritDoc} *

* Implementation detail: This method does no DOM modifications * (i.e. is very cheap to call) if there is no data for rows or columns * when this method is called. * * @see #hasColumnAndRowData() */ @Override public void setEscalatorUpdater( final EscalatorUpdater escalatorUpdater) { if (escalatorUpdater == null) { throw new IllegalArgumentException( "escalator updater cannot be null"); } updater = escalatorUpdater; if (hasColumnAndRowData() && getRowCount() > 0) { refreshRows(0, getRowCount()); } } /** * {@inheritDoc} *

* Implementation detail: This method does no DOM modifications * (i.e. is very cheap to call) if there are no rows in the DOM when * this method is called. * * @see #hasSomethingInDom() */ @Override public void removeRows(final int index, final int numberOfRows) { assertArgumentsAreValidAndWithinRange(index, numberOfRows); rows -= numberOfRows; if (!isAttached()) { return; } if (hasSomethingInDom()) { paintRemoveRows(index, numberOfRows); } } /** * Removes those row elements from the DOM that correspond to the given * range of logical indices. This may be fewer than {@code numberOfRows} * , even zero, if not all the removed rows are actually visible. *

* The implementation must call {@link #paintRemoveRow(Element, int)} * for each row that is removed from the DOM. * * @param index * the logical index of the first removed row * @param numberOfRows * number of logical rows to remove */ protected abstract void paintRemoveRows(final int index, final int numberOfRows); /** * Removes a row element from the DOM, invoking * {@link #getEscalatorUpdater()} * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before * and after removing the row, respectively. *

* This method must be called for each removed DOM row by any * {@link #paintRemoveRows(int, int)} implementation. * * @param tr * the row element to remove. */ protected void paintRemoveRow(final TableRowElement tr, final int logicalRowIndex) { flyweightRow.setup(tr, logicalRowIndex, columnConfiguration.getCalculatedColumnWidths()); getEscalatorUpdater().preDetach(flyweightRow, flyweightRow.getCells()); tr.removeFromParent(); getEscalatorUpdater().postDetach(flyweightRow, flyweightRow.getCells()); /* * the "assert" guarantees that this code is run only during * development/debugging. */ assert flyweightRow.teardown(); } protected void assertArgumentsAreValidAndWithinRange(final int index, final int numberOfRows) throws IllegalArgumentException, IndexOutOfBoundsException { if (numberOfRows < 1) { throw new IllegalArgumentException( "Number of rows must be 1 or greater (was " + numberOfRows + ")"); } if (index < 0 || index + numberOfRows > getRowCount()) { throw new IndexOutOfBoundsException("The given " + "row range (" + index + ".." + (index + numberOfRows) + ") was outside of the current number of rows (" + getRowCount() + ")"); } } @Override public int getRowCount() { return rows; } /** * {@inheritDoc} *

* Implementation detail: This method does no DOM modifications * (i.e. is very cheap to call) if there is no data for columns when * this method is called. * * @see #hasColumnAndRowData() */ @Override public void insertRows(final int index, final int numberOfRows) { if (index < 0 || index > getRowCount()) { throw new IndexOutOfBoundsException("The given index (" + index + ") was outside of the current number of rows (0.." + getRowCount() + ")"); } if (numberOfRows < 1) { throw new IllegalArgumentException( "Number of rows must be 1 or greater (was " + numberOfRows + ")"); } rows += numberOfRows; /* * only add items in the DOM if the widget itself is attached to the * DOM. We can't calculate sizes otherwise. */ if (isAttached()) { paintInsertRows(index, numberOfRows); if (rows == numberOfRows) { /* * We are inserting the first rows in this container. We * potentially need to set the widths for the cells for the * first time. */ Map colWidths = new HashMap(); for (int i = 0; i < getColumnConfiguration() .getColumnCount(); i++) { Double width = Double.valueOf( getColumnConfiguration().getColumnWidth(i)); Integer col = Integer.valueOf(i); colWidths.put(col, width); } getColumnConfiguration().setColumnWidths(colWidths); } } } /** * Actually add rows into the DOM, now that everything can be * calculated. * * @param visualIndex * the DOM index to add rows into * @param numberOfRows * the number of rows to insert */ protected abstract void paintInsertRows(final int visualIndex, final int numberOfRows); protected List paintInsertStaticRows( final int visualIndex, final int numberOfRows) { assert isAttached() : "Can't paint rows if Escalator is not attached"; final List addedRows = new ArrayList(); if (numberOfRows < 1) { return addedRows; } Node referenceRow; if (root.getChildCount() != 0 && visualIndex != 0) { // get the row node we're inserting stuff after referenceRow = root.getChild(visualIndex - 1); } else { // index is 0, so just prepend. referenceRow = null; } for (int row = visualIndex; row < visualIndex + numberOfRows; row++) { final TableRowElement tr = TableRowElement.as(DOM.createTR()); addedRows.add(tr); tr.addClassName(getStylePrimaryName() + "-row"); for (int col = 0; col < columnConfiguration .getColumnCount(); col++) { final double colWidth = columnConfiguration .getColumnWidthActual(col); final TableCellElement cellElem = createCellElement( colWidth); tr.appendChild(cellElem); // Set stylename and position if new cell is frozen if (col < columnConfiguration.frozenColumns) { cellElem.addClassName("frozen"); position.set(cellElem, scroller.lastScrollLeft, 0); } if (columnConfiguration.frozenColumns > 0 && col == columnConfiguration.frozenColumns - 1) { cellElem.addClassName("last-frozen"); } } referenceRow = paintInsertRow(referenceRow, tr, row); } reapplyRowWidths(); recalculateSectionHeight(); return addedRows; } /** * Inserts a single row into the DOM, invoking * {@link #getEscalatorUpdater()} * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before * and after inserting the row, respectively. The row should have its * cells already inserted. * * @param referenceRow * the row after which to insert or null if insert as first * @param tr * the row to be inserted * @param logicalRowIndex * the logical index of the inserted row * @return the inserted row to be used as the new reference */ protected Node paintInsertRow(Node referenceRow, final TableRowElement tr, int logicalRowIndex) { flyweightRow.setup(tr, logicalRowIndex, columnConfiguration.getCalculatedColumnWidths()); getEscalatorUpdater().preAttach(flyweightRow, flyweightRow.getCells()); referenceRow = insertAfterReferenceAndUpdateIt(root, tr, referenceRow); getEscalatorUpdater().postAttach(flyweightRow, flyweightRow.getCells()); updater.update(flyweightRow, flyweightRow.getCells()); /* * the "assert" guarantees that this code is run only during * development/debugging. */ assert flyweightRow.teardown(); return referenceRow; } private Node insertAfterReferenceAndUpdateIt(final Element parent, final Element elem, final Node referenceNode) { if (referenceNode != null) { parent.insertAfter(elem, referenceNode); } else { /* * referencenode being null means we have offset 0, i.e. make it * the first row */ /* * TODO [[optimize]]: Is insertFirst or append faster for an * empty root? */ parent.insertFirst(elem); } return elem; } protected abstract void recalculateSectionHeight(); /** * Returns the height of all rows in the row container. */ protected double calculateTotalRowHeight() { return getDefaultRowHeight() * getRowCount(); } /** * {@inheritDoc} *

* Implementation detail: This method does no DOM modifications * (i.e. is very cheap to call) if there is no data for columns when * this method is called. * * @see #hasColumnAndRowData() */ @Override // overridden because of JavaDoc public void refreshRows(final int index, final int numberOfRows) { Range rowRange = Range.withLength(index, numberOfRows); Range colRange = Range.withLength(0, getColumnConfiguration().getColumnCount()); refreshCells(rowRange, colRange); } protected abstract void refreshCells(Range logicalRowRange, Range colRange); void refreshRow(TableRowElement tr, int logicalRowIndex) { refreshRow(tr, logicalRowIndex, Range.withLength(0, getColumnConfiguration().getColumnCount())); } void refreshRow(final TableRowElement tr, final int logicalRowIndex, Range colRange) { flyweightRow.setup(tr, logicalRowIndex, columnConfiguration.getCalculatedColumnWidths()); Iterable cellsToUpdate = flyweightRow .getCells(colRange.getStart(), colRange.length()); updater.update(flyweightRow, cellsToUpdate); /* * the "assert" guarantees that this code is run only during * development/debugging. */ assert flyweightRow.teardown(); } /** * Create and setup an empty cell element. * * @param width * the width of the cell, in pixels * * @return a set-up empty cell element */ public TableCellElement createCellElement(final double width) { final TableCellElement cellElem = TableCellElement .as(DOM.createElement(getCellElementTagName())); final double height = getDefaultRowHeight(); assert height >= 0 : "defaultRowHeight was negative. There's a setter leak somewhere."; cellElem.getStyle().setHeight(height, Unit.PX); if (width >= 0) { cellElem.getStyle().setWidth(width, Unit.PX); } cellElem.addClassName(getStylePrimaryName() + "-cell"); return cellElem; } @Override public TableRowElement getRowElement(int index) { return getTrByVisualIndex(index); } /** * Gets the child element that is visually at a certain index. * * @param index * the index of the element to retrieve * @return the element at position {@code index} * @throws IndexOutOfBoundsException * if {@code index} is not valid within {@link #root} */ protected abstract TableRowElement getTrByVisualIndex(int index) throws IndexOutOfBoundsException; protected void paintRemoveColumns(final int offset, final int numberOfColumns) { for (int i = 0; i < getDomRowCount(); i++) { TableRowElement row = getTrByVisualIndex(i); flyweightRow.setup(row, i, columnConfiguration.getCalculatedColumnWidths()); Iterable attachedCells = flyweightRow .getCells(offset, numberOfColumns); getEscalatorUpdater().preDetach(flyweightRow, attachedCells); for (int j = 0; j < numberOfColumns; j++) { row.getCells().getItem(offset).removeFromParent(); } Iterable detachedCells = flyweightRow .getUnattachedCells(offset, numberOfColumns); getEscalatorUpdater().postDetach(flyweightRow, detachedCells); assert flyweightRow.teardown(); } } protected void paintInsertColumns(final int offset, final int numberOfColumns, boolean frozen) { for (int row = 0; row < getDomRowCount(); row++) { final TableRowElement tr = getTrByVisualIndex(row); int logicalRowIndex = getLogicalRowIndex(tr); paintInsertCells(tr, logicalRowIndex, offset, numberOfColumns); } reapplyRowWidths(); if (frozen) { for (int col = offset; col < offset + numberOfColumns; col++) { setColumnFrozen(col, true); } } } /** * Inserts new cell elements into a single row element, invoking * {@link #getEscalatorUpdater()} * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before * and after inserting the cells, respectively. *

* Precondition: The row must be already attached to the DOM and the * FlyweightCell instances corresponding to the new columns added to * {@code flyweightRow}. * * @param tr * the row in which to insert the cells * @param logicalRowIndex * the index of the row * @param offset * the index of the first cell * @param numberOfCells * the number of cells to insert */ private void paintInsertCells(final TableRowElement tr, int logicalRowIndex, final int offset, final int numberOfCells) { assert root.isOrHasChild( tr) : "The row must be attached to the document"; flyweightRow.setup(tr, logicalRowIndex, columnConfiguration.getCalculatedColumnWidths()); Iterable cells = flyweightRow .getUnattachedCells(offset, numberOfCells); for (FlyweightCell cell : cells) { final double colWidth = columnConfiguration .getColumnWidthActual(cell.getColumn()); final TableCellElement cellElem = createCellElement(colWidth); cell.setElement(cellElem); } getEscalatorUpdater().preAttach(flyweightRow, cells); Node referenceCell; if (offset != 0) { referenceCell = tr.getChild(offset - 1); } else { referenceCell = null; } for (FlyweightCell cell : cells) { referenceCell = insertAfterReferenceAndUpdateIt(tr, cell.getElement(), referenceCell); } getEscalatorUpdater().postAttach(flyweightRow, cells); getEscalatorUpdater().update(flyweightRow, cells); assert flyweightRow.teardown(); } public void setColumnFrozen(int column, boolean frozen) { toggleFrozenColumnClass(column, frozen, "frozen"); if (frozen) { updateFreezePosition(column, scroller.lastScrollLeft); } } private void toggleFrozenColumnClass(int column, boolean frozen, String className) { final NodeList childRows = root.getRows(); for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); if (!rowCanBeFrozen(tr)) { continue; } TableCellElement cell = tr.getCells().getItem(column); if (frozen) { cell.addClassName(className); } else { cell.removeClassName(className); position.reset(cell); } } } public void setColumnLastFrozen(int column, boolean lastFrozen) { toggleFrozenColumnClass(column, lastFrozen, "last-frozen"); } public void updateFreezePosition(int column, double scrollLeft) { final NodeList childRows = root.getRows(); for (int row = 0; row < childRows.getLength(); row++) { final TableRowElement tr = childRows.getItem(row); if (rowCanBeFrozen(tr)) { TableCellElement cell = tr.getCells().getItem(column); position.set(cell, scrollLeft, 0); } } } /** * Checks whether a row is an element, or contains such elements, that * can be frozen. *

* In practice, this applies for all header and footer rows. For body * rows, it applies for all rows except spacer rows. * * @since 7.5.0 * * @param tr * the row element to check for if it is or has elements that * can be frozen * @return true if this the given element, or any of its * descendants, can be frozen */ protected abstract boolean rowCanBeFrozen(TableRowElement tr); /** * Iterates through all the cells in a column and returns the width of * the widest element in this RowContainer. * * @param index * the index of the column to inspect * @return the pixel width of the widest element in the indicated column */ public double calculateMaxColWidth(int index) { TableRowElement row = TableRowElement .as(root.getFirstChildElement()); double maxWidth = 0; while (row != null) { final TableCellElement cell = row.getCells().getItem(index); final boolean isVisible = !cell.getStyle().getDisplay() .equals(Display.NONE.getCssName()); if (isVisible) { maxWidth = Math.max(maxWidth, WidgetUtil .getRequiredWidthBoundingClientRectDouble(cell)); } row = TableRowElement.as(row.getNextSiblingElement()); } return maxWidth; } /** * Reapplies all the cells' widths according to the calculated widths in * the column configuration. */ public void reapplyColumnWidths() { Element row = root.getFirstChildElement(); while (row != null) { // Only handle non-spacer rows if (!body.spacerContainer.isSpacer(row)) { Element cell = row.getFirstChildElement(); int columnIndex = 0; while (cell != null) { final double width = getCalculatedColumnWidthWithColspan( cell, columnIndex); /* * TODO Should Escalator implement ProvidesResize at * some point, this is where we need to do that. */ cell.getStyle().setWidth(width, Unit.PX); cell = cell.getNextSiblingElement(); columnIndex++; } } row = row.getNextSiblingElement(); } reapplyRowWidths(); } private double getCalculatedColumnWidthWithColspan(final Element cell, final int columnIndex) { final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR); Range spannedColumns = Range.withLength(columnIndex, colspan); /* * Since browsers don't explode with overflowing colspans, escalator * shouldn't either. */ if (spannedColumns.getEnd() > columnConfiguration .getColumnCount()) { spannedColumns = Range.between(columnIndex, columnConfiguration.getColumnCount()); } return columnConfiguration .getCalculatedColumnsWidth(spannedColumns); } /** * Applies the total length of the columns to each row element. *

* Note: In contrast to {@link #reapplyColumnWidths()}, this * method only modifies the width of the {@code * * } element, not the cells within. */ protected void reapplyRowWidths() { double rowWidth = columnConfiguration.calculateRowWidth(); if (rowWidth < 0) { return; } Element row = root.getFirstChildElement(); while (row != null) { // IF there is a rounding error when summing the columns, we // need to round the tr width up to ensure that columns fit and // do not wrap // E.g.122.95+123.25+103.75+209.25+83.52+88.57+263.45+131.21+126.85+113.13=1365.9299999999998 // For this we must set 1365.93 or the last column will wrap row.getStyle().setWidth(WidgetUtil.roundSizeUp(rowWidth), Unit.PX); row = row.getNextSiblingElement(); } } /** * The primary style name for the container. * * @param primaryStyleName * the style name to use as prefix for all row and cell style * names. */ protected void setStylePrimaryName(String primaryStyleName) { String oldStyle = getStylePrimaryName(); if (SharedUtil.equals(oldStyle, primaryStyleName)) { return; } this.primaryStyleName = primaryStyleName; // Update already rendered rows and cells Element row = root.getRows().getItem(0); while (row != null) { UIObject.setStylePrimaryName(row, primaryStyleName + "-row"); Element cell = TableRowElement.as(row).getCells().getItem(0); while (cell != null) { assert TableCellElement.is(cell); UIObject.setStylePrimaryName(cell, primaryStyleName + "-cell"); cell = cell.getNextSiblingElement(); } row = row.getNextSiblingElement(); } } /** * Returns the primary style name of the container. * * @return The primary style name or null if not set. */ protected String getStylePrimaryName() { return primaryStyleName; } @Override public void setDefaultRowHeight(double px) throws IllegalArgumentException { if (px < 1) { throw new IllegalArgumentException( "Height must be positive. " + px + " was given."); } defaultRowHeightShouldBeAutodetected = false; defaultRowHeight = px; reapplyDefaultRowHeights(); } @Override public double getDefaultRowHeight() { return defaultRowHeight; } /** * The default height of rows has (most probably) changed. *

* Make sure that the displayed rows with a default height are updated * in height and top position. *

* Note:This implementation should not call * {@link Escalator#recalculateElementSizes()} - it is done by the * discretion of the caller of this method. */ protected abstract void reapplyDefaultRowHeights(); protected void reapplyRowHeight(final TableRowElement tr, final double heightPx) { assert heightPx >= 0 : "Height must not be negative"; Element cellElem = tr.getFirstChildElement(); while (cellElem != null) { cellElem.getStyle().setHeight(heightPx, Unit.PX); cellElem = cellElem.getNextSiblingElement(); } /* * no need to apply height to tr-element, it'll be resized * implicitly. */ } protected void setRowPosition(final TableRowElement tr, final int x, final double y) { positions.set(tr, x, y); } /** * Returns the assigned top position for the given element. *

* Note: This method does not calculate what a row's top * position should be. It just returns an assigned value, correct or * not. * * @param tr * the table row element to measure * @return the current top position for {@code tr} * @see BodyRowContainerImpl#getRowTop(int) */ protected double getRowTop(final TableRowElement tr) { return positions.getTop(tr); } protected void removeRowPosition(TableRowElement tr) { positions.remove(tr); } public void autodetectRowHeightLater() { Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { @Override public void execute() { if (defaultRowHeightShouldBeAutodetected && isAttached()) { autodetectRowHeightNow(); defaultRowHeightShouldBeAutodetected = false; } } }); } private void fireRowHeightChangedEventFinally() { if (!rowHeightChangedEventFired) { rowHeightChangedEventFired = true; Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { fireEvent(new RowHeightChangedEvent()); rowHeightChangedEventFired = false; } }); } } public void autodetectRowHeightNow() { if (!isAttached()) { // Run again when attached defaultRowHeightShouldBeAutodetected = true; return; } final double oldRowHeight = defaultRowHeight; final Element detectionTr = DOM.createTR(); detectionTr.setClassName(getStylePrimaryName() + "-row"); final Element cellElem = DOM.createElement(getCellElementTagName()); cellElem.setClassName(getStylePrimaryName() + "-cell"); cellElem.setInnerText("Ij"); detectionTr.appendChild(cellElem); root.appendChild(detectionTr); double boundingHeight = WidgetUtil .getRequiredHeightBoundingClientRectDouble(cellElem); defaultRowHeight = Math.max(1.0d, boundingHeight); root.removeChild(detectionTr); if (root.hasChildNodes()) { reapplyDefaultRowHeights(); applyHeightByRows(); } if (oldRowHeight != defaultRowHeight) { fireRowHeightChangedEventFinally(); } } @Override public Cell getCell(final Element element) { if (element == null) { throw new IllegalArgumentException("Element cannot be null"); } /* * Ensure that element is not root nor the direct descendant of root * (a row) and ensure the element is inside the dom hierarchy of the * root element. If not, return. */ if (root == element || element.getParentElement() == root || !root.isOrHasChild(element)) { return null; } /* * Ensure element is the cell element by iterating up the DOM * hierarchy until reaching cell element. */ Element cellElementCandidate = element; while (cellElementCandidate.getParentElement() .getParentElement() != root) { cellElementCandidate = cellElementCandidate.getParentElement(); } final TableCellElement cellElement = TableCellElement .as(cellElementCandidate); // Find dom column int domColumnIndex = -1; for (Element e = cellElement; e != null; e = e .getPreviousSiblingElement()) { domColumnIndex++; } // Find dom row int domRowIndex = -1; for (Element e = cellElement.getParentElement(); e != null; e = e .getPreviousSiblingElement()) { domRowIndex++; } return new Cell(domRowIndex, domColumnIndex, cellElement); } double measureCellWidth(TableCellElement cell, boolean withContent) { /* * To get the actual width of the contents, we need to get the cell * content without any hardcoded height or width. * * But we don't want to modify the existing column, because that * might trigger some unnecessary listeners and whatnot. So, * instead, we make a deep clone of that cell, but without any * explicit dimensions, and measure that instead. */ TableCellElement cellClone = TableCellElement .as((Element) cell.cloneNode(withContent)); cellClone.getStyle().clearHeight(); cellClone.getStyle().clearWidth(); cell.getParentElement().insertBefore(cellClone, cell); double requiredWidth = WidgetUtil .getRequiredWidthBoundingClientRectDouble(cellClone); if (BrowserInfo.get().isIE()) { /* * IE browsers have some issues with subpixels. Occasionally * content is overflown even if not necessary. Increase the * counted required size by 0.01 just to be on the safe side. */ requiredWidth += 0.01; } cellClone.removeFromParent(); return requiredWidth; } /** * Gets the minimum width needed to display the cell properly. * * @param colIndex * index of column to measure * @param withContent * true if content is taken into account, * false if not * @return cell width needed for displaying correctly */ double measureMinCellWidth(int colIndex, boolean withContent) { assert isAttached() : "Can't measure max width of cell, since Escalator is not attached to the DOM."; double minCellWidth = -1; NodeList rows = root.getRows(); for (int row = 0; row < rows.getLength(); row++) { TableCellElement cell = rows.getItem(row).getCells() .getItem(colIndex); if (cell != null && !cellIsPartOfSpan(cell)) { double cellWidth = measureCellWidth(cell, withContent); minCellWidth = Math.max(minCellWidth, cellWidth); } } return minCellWidth; } private boolean cellIsPartOfSpan(TableCellElement cell) { boolean cellHasColspan = cell.getColSpan() > 1; boolean cellIsHidden = Display.NONE.getCssName() .equals(cell.getStyle().getDisplay()); return cellHasColspan || cellIsHidden; } void refreshColumns(int index, int numberOfColumns) { if (getRowCount() > 0) { Range rowRange = Range.withLength(0, getRowCount()); Range colRange = Range.withLength(index, numberOfColumns); refreshCells(rowRange, colRange); } } /** * The height of this table section. *

* Note that {@link Escalator#getBody() the body} will calculate its * height, while the others will return a precomputed value. * * @since 7.5.0 * * @return the height of this table section */ protected abstract double getHeightOfSection(); /** * Gets the logical row index for the given table row element. * * @param tr * the table row element inside this container * @return the logical index of the given element */ public int getLogicalRowIndex(final TableRowElement tr) { return tr.getSectionRowIndex(); }; } private abstract class AbstractStaticRowContainer extends AbstractRowContainer { /** The height of the combined rows in the DOM. Never negative. */ private double heightOfSection = 0; public AbstractStaticRowContainer( final TableSectionElement headElement) { super(headElement); } @Override public int getDomRowCount() { return root.getChildCount(); } @Override protected void paintRemoveRows(final int index, final int numberOfRows) { for (int i = index; i < index + numberOfRows; i++) { final TableRowElement tr = root.getRows().getItem(index); paintRemoveRow(tr, index); } recalculateSectionHeight(); } @Override protected TableRowElement getTrByVisualIndex(final int index) throws IndexOutOfBoundsException { if (index >= 0 && index < root.getChildCount()) { return root.getRows().getItem(index); } else { throw new IndexOutOfBoundsException( "No such visual index: " + index); } } @Override public void insertRows(int index, int numberOfRows) { super.insertRows(index, numberOfRows); recalculateElementSizes(); applyHeightByRows(); } @Override public void removeRows(int index, int numberOfRows) { /* * While the rows in a static section are removed, the scrollbar is * temporarily shrunk and then re-expanded. This leads to the fact * that the scroll position is scooted up a bit. This means that we * need to reset the position here. * * If Escalator, at some point, gets a JIT evaluation functionality, * this re-setting is a strong candidate for removal. */ double oldScrollPos = verticalScrollbar.getScrollPos(); super.removeRows(index, numberOfRows); recalculateElementSizes(); applyHeightByRows(); verticalScrollbar.setScrollPos(oldScrollPos); } @Override protected void reapplyDefaultRowHeights() { if (root.getChildCount() == 0) { return; } Profiler.enter( "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights"); Element tr = root.getRows().getItem(0); while (tr != null) { reapplyRowHeight(TableRowElement.as(tr), getDefaultRowHeight()); tr = tr.getNextSiblingElement(); } /* * Because all rows are immediately displayed in the static row * containers, the section's overall height has most probably * changed. */ recalculateSectionHeight(); Profiler.leave( "Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights"); } @Override protected void recalculateSectionHeight() { Profiler.enter( "Escalator.AbstractStaticRowContainer.recalculateSectionHeight"); double newHeight = calculateTotalRowHeight(); if (newHeight != heightOfSection) { heightOfSection = newHeight; sectionHeightCalculated(); /* * We need to update the scrollbar dimension at this point. If * we are scrolled too far down and the static section shrinks, * the body will try to render rows that don't exist during * body.verifyEscalatorCount. This is because the logical row * indices are calculated from the scrollbar position. */ verticalScrollbar.setOffsetSize( heightOfEscalator - header.getHeightOfSection() - footer.getHeightOfSection()); body.verifyEscalatorCount(); body.spacerContainer.updateSpacerDecosVisibility(); } Profiler.leave( "Escalator.AbstractStaticRowContainer.recalculateSectionHeight"); } /** * Informs the row container that the height of its respective table * section has changed. *

* These calculations might affect some layouting logic, such as the * body is being offset by the footer, the footer needs to be readjusted * according to its height, and so on. *

* A table section is either header, body or footer. */ protected abstract void sectionHeightCalculated(); @Override protected void refreshCells(Range logicalRowRange, Range colRange) { assertArgumentsAreValidAndWithinRange(logicalRowRange.getStart(), logicalRowRange.length()); if (!isAttached()) { return; } Profiler.enter("Escalator.AbstractStaticRowContainer.refreshCells"); if (hasColumnAndRowData()) { for (int row = logicalRowRange.getStart(); row < logicalRowRange .getEnd(); row++) { final TableRowElement tr = getTrByVisualIndex(row); refreshRow(tr, row, colRange); } } Profiler.leave("Escalator.AbstractStaticRowContainer.refreshCells"); } @Override protected void paintInsertRows(int visualIndex, int numberOfRows) { paintInsertStaticRows(visualIndex, numberOfRows); } @Override protected boolean rowCanBeFrozen(TableRowElement tr) { assert root.isOrHasChild( tr) : "Row does not belong to this table section"; return true; } @Override protected double getHeightOfSection() { return Math.max(0, heightOfSection); } } private class HeaderRowContainer extends AbstractStaticRowContainer { public HeaderRowContainer(final TableSectionElement headElement) { super(headElement); } @Override protected void sectionHeightCalculated() { double heightOfSection = getHeightOfSection(); bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX); spacerDecoContainer.getStyle().setMarginTop(heightOfSection, Unit.PX); verticalScrollbar.getElement().getStyle().setTop(heightOfSection, Unit.PX); headerDeco.getStyle().setHeight(heightOfSection, Unit.PX); } @Override protected String getCellElementTagName() { return "th"; } @Override public void setStylePrimaryName(String primaryStyleName) { super.setStylePrimaryName(primaryStyleName); UIObject.setStylePrimaryName(root, primaryStyleName + "-header"); } } private class FooterRowContainer extends AbstractStaticRowContainer { public FooterRowContainer(final TableSectionElement footElement) { super(footElement); } @Override public void setStylePrimaryName(String primaryStyleName) { super.setStylePrimaryName(primaryStyleName); UIObject.setStylePrimaryName(root, primaryStyleName + "-footer"); } @Override protected String getCellElementTagName() { return "td"; } @Override protected void sectionHeightCalculated() { double headerHeight = header.getHeightOfSection(); double footerHeight = footer.getHeightOfSection(); int vscrollHeight = (int) Math .floor(heightOfEscalator - headerHeight - footerHeight); final boolean horizontalScrollbarNeeded = columnConfiguration .calculateRowWidth() > widthOfEscalator; if (horizontalScrollbarNeeded) { vscrollHeight -= horizontalScrollbar.getScrollbarThickness(); } footerDeco.getStyle().setHeight(footer.getHeightOfSection(), Unit.PX); verticalScrollbar.setOffsetSize(vscrollHeight); } } private class BodyRowContainerImpl extends AbstractRowContainer implements BodyRowContainer { /* * TODO [[optimize]]: check whether a native JsArray might be faster * than LinkedList */ /** * The order in which row elements are rendered visually in the browser, * with the help of CSS tricks. Usually has nothing to do with the DOM * order. * * @see #sortDomElements() */ private final LinkedList visualRowOrder = new LinkedList(); /** * The logical index of the topmost row. * * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)}, * {@link #updateTopRowLogicalIndex(int)} and * {@link #getTopRowLogicalIndex()} instead */ @Deprecated private int topRowLogicalIndex = 0; private void setTopRowLogicalIndex(int topRowLogicalIndex) { if (LogConfiguration.loggingIsEnabled(Level.INFO)) { Logger.getLogger("Escalator.BodyRowContainer") .fine("topRowLogicalIndex: " + this.topRowLogicalIndex + " -> " + topRowLogicalIndex); } assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative (top left cell contents: " + visualRowOrder.getFirst().getCells().getItem(0) .getInnerText() + ") "; /* * if there's a smart way of evaluating and asserting the max index, * this would be a nice place to put it. I haven't found out an * effective and generic solution. */ this.topRowLogicalIndex = topRowLogicalIndex; } public int getTopRowLogicalIndex() { return topRowLogicalIndex; } private void updateTopRowLogicalIndex(int diff) { setTopRowLogicalIndex(topRowLogicalIndex + diff); } private class DeferredDomSorter { private static final int SORT_DELAY_MILLIS = 50; // as it happens, 3 frames = 50ms @ 60fps. private static final int REQUIRED_FRAMES_PASSED = 3; private final AnimationCallback frameCounter = new AnimationCallback() { @Override public void execute(double timestamp) { framesPassed++; boolean domWasSorted = sortIfConditionsMet(); if (!domWasSorted) { animationHandle = AnimationScheduler.get() .requestAnimationFrame(this); } else { waiting = false; } } }; private int framesPassed; private double startTime; private AnimationHandle animationHandle; /** true if a sort is scheduled */ public boolean waiting = false; public void reschedule() { waiting = true; resetConditions(); animationHandle = AnimationScheduler.get() .requestAnimationFrame(frameCounter); } private boolean sortIfConditionsMet() { boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED; boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS; boolean notTouchActivity = !scroller.touchHandlerBundle.touching; boolean conditionsMet = enoughFramesHavePassed && enoughTimeHasPassed && notTouchActivity; if (conditionsMet) { resetConditions(); sortDomElements(); } return conditionsMet; } private void resetConditions() { if (animationHandle != null) { animationHandle.cancel(); animationHandle = null; } startTime = Duration.currentTimeMillis(); framesPassed = 0; } } private DeferredDomSorter domSorter = new DeferredDomSorter(); private final SpacerContainer spacerContainer = new SpacerContainer(); public BodyRowContainerImpl(final TableSectionElement bodyElement) { super(bodyElement); } @Override public void insertRows(int index, int numberOfRows) { super.insertRows(index, numberOfRows); if (heightMode == HeightMode.UNDEFINED) { heightByRows = getRowCount(); } } @Override public void removeRows(int index, int numberOfRows) { super.removeRows(index, numberOfRows); if (heightMode == HeightMode.UNDEFINED) { heightByRows = getRowCount(); } } @Override public void setStylePrimaryName(String primaryStyleName) { super.setStylePrimaryName(primaryStyleName); UIObject.setStylePrimaryName(root, primaryStyleName + "-body"); spacerContainer.setStylePrimaryName(primaryStyleName); } public void updateEscalatorRowsOnScroll() { if (visualRowOrder.isEmpty()) { return; } boolean rowsWereMoved = false; final double topElementPosition; final double nextRowBottomOffset; SpacerContainer.SpacerImpl topSpacer = spacerContainer .getSpacer(getTopRowLogicalIndex() - 1); if (topSpacer != null) { topElementPosition = topSpacer.getTop(); nextRowBottomOffset = topSpacer.getHeight() + getDefaultRowHeight(); } else { topElementPosition = getRowTop(visualRowOrder.getFirst()); nextRowBottomOffset = getDefaultRowHeight(); } // TODO [[mpixscroll]] final double scrollTop = tBodyScrollTop; final double viewportOffset = topElementPosition - scrollTop; /* * TODO [[optimize]] this if-else can most probably be refactored * into a neater block of code */ if (viewportOffset > 0) { // there's empty room on top double rowPx = getRowHeightsSumBetweenPx(scrollTop, topElementPosition); int originalRowsToMove = (int) Math .ceil(rowPx / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, visualRowOrder.size()); final int end = visualRowOrder.size(); final int start = end - rowsToMove; final int logicalRowIndex = getLogicalRowIndex(scrollTop); moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); setTopRowLogicalIndex(logicalRowIndex); rowsWereMoved = true; } else if (viewportOffset + nextRowBottomOffset <= 0) { /* * the viewport has been scrolled more than the topmost visual * row. */ double rowPx = getRowHeightsSumBetweenPx(topElementPosition, scrollTop); int originalRowsToMove = (int) (rowPx / getDefaultRowHeight()); int rowsToMove = Math.min(originalRowsToMove, visualRowOrder.size()); int logicalRowIndex; if (rowsToMove < visualRowOrder.size()) { /* * We scroll so little that we can just keep adding the rows * below the current escalator */ logicalRowIndex = getLogicalRowIndex( visualRowOrder.getLast()) + 1; } else { /* * Since we're moving all escalator rows, we need to * calculate the first logical row index from the scroll * position. */ logicalRowIndex = getLogicalRowIndex(scrollTop); } /* * Since we're moving the viewport downwards, the visual index * is always at the bottom. Note: Due to how * moveAndUpdateEscalatorRows works, this will work out even if * we move all the rows, and try to place them "at the end". */ final int targetVisualIndex = visualRowOrder.size(); // make sure that we don't move rows over the data boundary boolean aRowWasLeftBehind = false; if (logicalRowIndex + rowsToMove > getRowCount()) { /* * TODO [[spacer]]: with constant row heights, there's * always exactly one row that will be moved beyond the data * source, when viewport is scrolled to the end. This, * however, isn't guaranteed anymore once row heights start * varying. */ rowsToMove--; aRowWasLeftBehind = true; } /* * Make sure we don't scroll beyond the row content. This can * happen if we have spacers for the last rows. */ rowsToMove = Math.max(0, Math.min(rowsToMove, getRowCount() - logicalRowIndex)); moveAndUpdateEscalatorRows(Range.between(0, rowsToMove), targetVisualIndex, logicalRowIndex); if (aRowWasLeftBehind) { /* * To keep visualRowOrder as a spatially contiguous block of * rows, let's make sure that the one row we didn't move * visually still stays with the pack. */ final Range strayRow = Range.withOnly(0); /* * We cannot trust getLogicalRowIndex, because it hasn't yet * been updated. But since we're leaving rows behind, it * means we've scrolled to the bottom. So, instead, we * simply count backwards from the end. */ final int topLogicalIndex = getRowCount() - visualRowOrder.size(); moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); } final int naiveNewLogicalIndex = getTopRowLogicalIndex() + originalRowsToMove; final int maxLogicalIndex = getRowCount() - visualRowOrder.size(); setTopRowLogicalIndex( Math.min(naiveNewLogicalIndex, maxLogicalIndex)); rowsWereMoved = true; } if (rowsWereMoved) { fireRowVisibilityChangeEvent(); domSorter.reschedule(); } } private double getRowHeightsSumBetweenPx(double y1, double y2) { assert y1 < y2 : "y1 must be smaller than y2"; double viewportPx = y2 - y1; double spacerPx = spacerContainer.getSpacerHeightsSumBetweenPx(y1, SpacerInclusionStrategy.PARTIAL, y2, SpacerInclusionStrategy.PARTIAL); return viewportPx - spacerPx; } private int getLogicalRowIndex(final double px) { double rowPx = px - spacerContainer.getSpacerHeightsSumUntilPx(px); return (int) (rowPx / getDefaultRowHeight()); } @Override protected void paintInsertRows(final int index, final int numberOfRows) { if (numberOfRows == 0) { return; } spacerContainer.shiftSpacersByRows(index, numberOfRows); /* * TODO: this method should probably only add physical rows, and not * populate them - let everything be populated as appropriate by the * logic that follows. * * This also would lead to the fact that paintInsertRows wouldn't * need to return anything. */ final List addedRows = fillAndPopulateEscalatorRowsIfNeeded( index, numberOfRows); /* * insertRows will always change the number of rows - update the * scrollbar sizes. */ scroller.recalculateScrollbarsForVirtualViewport(); final boolean addedRowsAboveCurrentViewport = index * getDefaultRowHeight() < getScrollTop(); final boolean addedRowsBelowCurrentViewport = index * getDefaultRowHeight() > getScrollTop() + getHeightOfSection(); if (addedRowsAboveCurrentViewport) { /* * We need to tweak the virtual viewport (scroll handle * positions, table "scroll position" and row locations), but * without re-evaluating any rows. */ final double yDelta = numberOfRows * getDefaultRowHeight(); moveViewportAndContent(yDelta); updateTopRowLogicalIndex(numberOfRows); } else if (addedRowsBelowCurrentViewport) { // NOOP, we already recalculated scrollbars. } else { // some rows were added inside the current viewport final int unupdatedLogicalStart = index + addedRows.size(); final int visualOffset = getLogicalRowIndex( visualRowOrder.getFirst()); /* * At this point, we have added new escalator rows, if so * needed. * * If more rows were added than the new escalator rows can * account for, we need to start to spin the escalator to update * the remaining rows as well. */ final int rowsStillNeeded = numberOfRows - addedRows.size(); if (rowsStillNeeded > 0) { final Range unupdatedVisual = convertToVisual( Range.withLength(unupdatedLogicalStart, rowsStillNeeded)); final int end = getDomRowCount(); final int start = end - unupdatedVisual.length(); final int visualTargetIndex = unupdatedLogicalStart - visualOffset; moveAndUpdateEscalatorRows(Range.between(start, end), visualTargetIndex, unupdatedLogicalStart); // move the surrounding rows to their correct places. double rowTop = (unupdatedLogicalStart + (end - start)) * getDefaultRowHeight(); // TODO: Get rid of this try/catch block by fixing the // underlying issue. The reason for this erroneous behavior // might be that Escalator actually works 'by mistake', and // the order of operations is, in fact, wrong. try { final ListIterator i = visualRowOrder .listIterator( visualTargetIndex + (end - start)); int logicalRowIndexCursor = unupdatedLogicalStart; while (i.hasNext()) { rowTop += spacerContainer .getSpacerHeight(logicalRowIndexCursor++); final TableRowElement tr = i.next(); setRowPosition(tr, 0, rowTop); rowTop += getDefaultRowHeight(); } } catch (Exception e) { Logger logger = getLogger(); logger.warning( "Ignored out-of-bounds row element access"); logger.warning("Escalator state: start=" + start + ", end=" + end + ", visualTargetIndex=" + visualTargetIndex + ", visualRowOrder.size()=" + visualRowOrder.size()); logger.warning(e.toString()); } } fireRowVisibilityChangeEvent(); sortDomElements(); } } /** * Move escalator rows around, and make sure everything gets * appropriately repositioned and repainted. * * @param visualSourceRange * the range of rows to move to a new place * @param visualTargetIndex * the visual index where the rows will be placed to * @param logicalTargetIndex * the logical index to be assigned to the first moved row */ private void moveAndUpdateEscalatorRows(final Range visualSourceRange, final int visualTargetIndex, final int logicalTargetIndex) throws IllegalArgumentException { if (visualSourceRange.isEmpty()) { return; } assert visualSourceRange.getStart() >= 0 : "Visual source start " + "must be 0 or greater (was " + visualSourceRange.getStart() + ")"; assert logicalTargetIndex >= 0 : "Logical target must be 0 or " + "greater (was " + logicalTargetIndex + ")"; assert visualTargetIndex >= 0 : "Visual target must be 0 or greater (was " + visualTargetIndex + ")"; assert visualTargetIndex <= getDomRowCount() : "Visual target " + "must not be greater than the number of escalator rows (was " + visualTargetIndex + ", escalator rows " + getDomRowCount() + ")"; assert logicalTargetIndex + visualSourceRange.length() <= getRowCount() : "Logical " + "target leads to rows outside of the data range (" + Range.withLength(logicalTargetIndex, visualSourceRange.length()) + " goes beyond " + Range.withLength(0, getRowCount()) + ")"; /* * Since we move a range into another range, the indices might move * about. Having 10 rows, if we move 0..1 to index 10 (to the end of * the collection), the target range will end up being 8..9, instead * of 10..11. * * This applies only if we move elements forward in the collection, * not backward. */ final int adjustedVisualTargetIndex; if (visualSourceRange.getStart() < visualTargetIndex) { adjustedVisualTargetIndex = visualTargetIndex - visualSourceRange.length(); } else { adjustedVisualTargetIndex = visualTargetIndex; } if (visualSourceRange.getStart() != adjustedVisualTargetIndex) { /* * Reorder the rows to their correct places within * visualRowOrder (unless rows are moved back to their original * places) */ /* * TODO [[optimize]]: move whichever set is smaller: the ones * explicitly moved, or the others. So, with 10 escalator rows, * if we are asked to move idx[0..8] to the end of the list, * it's faster to just move idx[9] to the beginning. */ final List removedRows = new ArrayList( visualSourceRange.length()); for (int i = 0; i < visualSourceRange.length(); i++) { final TableRowElement tr = visualRowOrder .remove(visualSourceRange.getStart()); removedRows.add(tr); } visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows); } { // Refresh the contents of the affected rows final ListIterator iter = visualRowOrder .listIterator(adjustedVisualTargetIndex); for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex + visualSourceRange.length(); logicalIndex++) { final TableRowElement tr = iter.next(); refreshRow(tr, logicalIndex); } } { // Reposition the rows that were moved double newRowTop = getRowTop(logicalTargetIndex); final ListIterator iter = visualRowOrder .listIterator(adjustedVisualTargetIndex); for (int i = 0; i < visualSourceRange.length(); i++) { final TableRowElement tr = iter.next(); setRowPosition(tr, 0, newRowTop); newRowTop += getDefaultRowHeight(); newRowTop += spacerContainer .getSpacerHeight(logicalTargetIndex + i); } } } /** * Adjust the scroll position and move the contained rows. *

* The difference between using this method and simply scrolling is that * this method "takes the rows and spacers with it" and renders them * appropriately. The viewport may be scrolled any arbitrary amount, and * the contents are moved appropriately, but always snapped into a * plausible place. *

*

*
Example 1
*
An Escalator with default row height 20px. Adjusting the scroll * position with 7.5px will move the viewport 7.5px down, but leave the * row where it is.
*
Example 2
*
An Escalator with default row height 20px. Adjusting the scroll * position with 27.5px will move the viewport 27.5px down, and place * the row at 20px.
*
* * @param yDelta * the delta of pixels by which to move the viewport and * content. A positive value moves everything downwards, * while a negative value moves everything upwards */ public void moveViewportAndContent(final double yDelta) { if (yDelta == 0) { return; } double newTop = tBodyScrollTop + yDelta; verticalScrollbar.setScrollPos(newTop); final double defaultRowHeight = getDefaultRowHeight(); double rowPxDelta = yDelta - (yDelta % defaultRowHeight); int rowIndexDelta = (int) (yDelta / defaultRowHeight); if (!WidgetUtil.pixelValuesEqual(rowPxDelta, 0)) { Collection spacers = spacerContainer .getSpacersAfterPx(tBodyScrollTop, SpacerInclusionStrategy.PARTIAL); for (SpacerContainer.SpacerImpl spacer : spacers) { spacer.setPositionDiff(0, rowPxDelta); spacer.setRowIndex(spacer.getRow() + rowIndexDelta); } for (TableRowElement tr : visualRowOrder) { setRowPosition(tr, 0, getRowTop(tr) + rowPxDelta); } } setBodyScrollPosition(tBodyScrollLeft, newTop); } /** * Adds new physical escalator rows to the DOM at the given index if * there's still a need for more escalator rows. *

* If Escalator already is at (or beyond) max capacity, this method does * nothing to the DOM. * * @param index * the index at which to add new escalator rows. * Note:It is assumed that the index is both the * visual index and the logical index. * @param numberOfRows * the number of rows to add at index * @return a list of the added rows */ private List fillAndPopulateEscalatorRowsIfNeeded( final int index, final int numberOfRows) { final int escalatorRowsStillFit = getMaxVisibleRowCount() - getDomRowCount(); final int escalatorRowsNeeded = Math.min(numberOfRows, escalatorRowsStillFit); if (escalatorRowsNeeded > 0) { final List addedRows = paintInsertStaticRows( index, escalatorRowsNeeded); visualRowOrder.addAll(index, addedRows); double y = index * getDefaultRowHeight() + spacerContainer.getSpacerHeightsSumUntilIndex(index); for (int i = index; i < visualRowOrder.size(); i++) { final TableRowElement tr; if (i - index < addedRows.size()) { tr = addedRows.get(i - index); } else { tr = visualRowOrder.get(i); } setRowPosition(tr, 0, y); y += getDefaultRowHeight(); y += spacerContainer.getSpacerHeight(i); } return addedRows; } else { return Collections.emptyList(); } } private int getMaxVisibleRowCount() { double heightOfSection = getHeightOfSection(); // By including the possibly shown scrollbar height, we get a // consistent count and do not add/remove rows whenever a scrollbar // is shown heightOfSection += horizontalScrollbarDeco.getOffsetHeight(); double defaultRowHeight = getDefaultRowHeight(); final int maxVisibleRowCount = (int) Math .ceil(heightOfSection / defaultRowHeight) + 1; /* * maxVisibleRowCount can become negative if the headers and footers * start to overlap. This is a crazy situation, but Vaadin blinks * the components a lot, so it's feasible. */ return Math.max(0, maxVisibleRowCount); } @Override protected void paintRemoveRows(final int index, final int numberOfRows) { if (numberOfRows == 0) { return; } final Range viewportRange = getVisibleRowRange(); final Range removedRowsRange = Range.withLength(index, numberOfRows); /* * Removing spacers as the very first step will correct the * scrollbars and row offsets right away. * * TODO: actually, it kinda sounds like a Grid feature that a spacer * would be associated with a particular row. Maybe it would be * better to have a spacer separate from rows, and simply collapse * them if they happen to end up on top of each other. This would * probably make supporting the -1 row pretty easy, too. */ spacerContainer.paintRemoveSpacers(removedRowsRange); final Range[] partitions = removedRowsRange .partitionWith(viewportRange); final Range removedAbove = partitions[0]; final Range removedLogicalInside = partitions[1]; final Range removedVisualInside = convertToVisual( removedLogicalInside); /* * TODO: extract the following if-block to a separate method. I'll * leave this be inlined for now, to make linediff-based code * reviewing easier. Probably will be moved in the following patch * set. */ /* * Adjust scroll position in one of two scenarios: * * 1) Rows were removed above. Then we just need to adjust the * scrollbar by the height of the removed rows. * * 2) There are no logical rows above, and at least the first (if * not more) visual row is removed. Then we need to snap the scroll * position to the first visible row (i.e. reset scroll position to * absolute 0) * * The logic is optimized in such a way that the * moveViewportAndContent is called only once, to avoid extra * reflows, and thus the code might seem a bit obscure. */ final boolean firstVisualRowIsRemoved = !removedVisualInside .isEmpty() && removedVisualInside.getStart() == 0; if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) { final double yDelta = removedAbove.length() * getDefaultRowHeight(); final double firstLogicalRowHeight = getDefaultRowHeight(); final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar .getScrollPos() - yDelta < firstLogicalRowHeight; if (removedVisualInside.isEmpty() && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) { /* * rows were removed from above the viewport, so all we need * to do is to adjust the scroll position to account for the * removed rows */ moveViewportAndContent(-yDelta); } else if (removalScrollsToShowFirstLogicalRow) { /* * It seems like we've removed all rows from above, and also * into the current viewport. This means we'll need to even * out the scroll position to exactly 0 (i.e. adjust by the * current negative scrolltop, presto!), so that it isn't * aligned funnily */ moveViewportAndContent(-verticalScrollbar.getScrollPos()); } } // ranges evaluated, let's do things. if (!removedVisualInside.isEmpty()) { int escalatorRowCount = body.getDomRowCount(); /* * remember: the rows have already been subtracted from the row * count at this point */ int rowsLeft = getRowCount(); if (rowsLeft < escalatorRowCount) { int escalatorRowsToRemove = escalatorRowCount - rowsLeft; for (int i = 0; i < escalatorRowsToRemove; i++) { final TableRowElement tr = visualRowOrder .remove(removedVisualInside.getStart()); paintRemoveRow(tr, index); removeRowPosition(tr); } escalatorRowCount -= escalatorRowsToRemove; /* * Because we're removing escalator rows, we don't have * anything to scroll by. Let's make sure the viewport is * scrolled to top, to render any rows possibly left above. */ body.setBodyScrollPosition(tBodyScrollLeft, 0); /* * We might have removed some rows from the middle, so let's * make sure we're not left with any holes. Also remember: * visualIndex == logicalIndex applies now. */ final int dirtyRowsStart = removedLogicalInside.getStart(); double y = getRowTop(dirtyRowsStart); for (int i = dirtyRowsStart; i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); setRowPosition(tr, 0, y); y += getDefaultRowHeight(); y += spacerContainer.getSpacerHeight(i); } /* * this is how many rows appeared into the viewport from * below */ final int rowsToUpdateDataOn = numberOfRows - escalatorRowsToRemove; final int start = Math.max(0, escalatorRowCount - rowsToUpdateDataOn); final int end = escalatorRowCount; for (int i = start; i < end; i++) { final TableRowElement tr = visualRowOrder.get(i); refreshRow(tr, i); } } else { // No escalator rows need to be removed. /* * Two things (or a combination thereof) can happen: * * 1) We're scrolled to the bottom, the last rows are * removed. SOLUTION: moveAndUpdateEscalatorRows the * bottommost rows, and place them at the top to be * refreshed. * * 2) We're scrolled somewhere in the middle, arbitrary rows * are removed. SOLUTION: moveAndUpdateEscalatorRows the * removed rows, and place them at the bottom to be * refreshed. * * Since a combination can also happen, we need to handle * this in a smart way, all while avoiding * double-refreshing. */ final double contentBottom = getRowCount() * getDefaultRowHeight(); final double viewportBottom = tBodyScrollTop + getHeightOfSection(); if (viewportBottom <= contentBottom) { /* * We're in the middle of the row container, everything * is added to the bottom */ paintRemoveRowsAtMiddle(removedLogicalInside, removedVisualInside, 0); } else if (removedVisualInside.contains(0) && numberOfRows >= visualRowOrder.size()) { /* * We're removing so many rows that the viewport is * pushed up more than a screenful. This means we can * simply scroll up and everything will work without a * sweat. */ double left = horizontalScrollbar.getScrollPos(); double top = contentBottom - visualRowOrder.size() * getDefaultRowHeight(); setBodyScrollPosition(left, top); Range allEscalatorRows = Range.withLength(0, visualRowOrder.size()); int logicalTargetIndex = getRowCount() - allEscalatorRows.length(); moveAndUpdateEscalatorRows(allEscalatorRows, 0, logicalTargetIndex); /* * moveAndUpdateEscalatorRows recalculates the rows, but * logical top row index bookkeeping is handled in this * method. * * TODO: Redesign how to keep it easy to track this. */ updateTopRowLogicalIndex( -removedLogicalInside.length()); /* * Scrolling the body to the correct location will be * fixed automatically. Because the amount of rows is * decreased, the viewport is pushed up as the scrollbar * shrinks. So no need to do anything there. * * TODO [[optimize]]: This might lead to a double body * refresh. Needs investigation. */ } else if (contentBottom + (numberOfRows * getDefaultRowHeight()) - viewportBottom < getDefaultRowHeight()) { /* * We're at the end of the row container, everything is * added to the top. */ /* * FIXME [[spacer]]: above if-clause is coded to only * work with default row heights - will not work with * variable row heights */ paintRemoveRowsAtBottom(removedLogicalInside, removedVisualInside); updateTopRowLogicalIndex( -removedLogicalInside.length()); } else { /* * We're in a combination, where we need to both scroll * up AND show new rows at the bottom. * * Example: Scrolled down to show the second to last * row. Remove two. Viewport scrolls up, revealing the * row above row. The last element collapses up and into * view. * * Reminder: this use case handles only the case when * there are enough escalator rows to still render a * full view. I.e. all escalator rows will _always_ be * populated */ /*- * 1 1 |1| <- newly rendered * |2| |2| |2| * |3| ==> |*| ==> |5| <- newly rendered * |4| |*| * 5 5 * * 1 1 |1| <- newly rendered * |2| |*| |4| * |3| ==> |*| ==> |5| <- newly rendered * |4| |4| * 5 5 */ /* * STEP 1: * * reorganize deprecated escalator rows to bottom, but * don't re-render anything yet */ /*- * 1 1 1 * |2| |*| |4| * |3| ==> |*| ==> |*| * |4| |4| |*| * 5 5 5 */ double newTop = getRowTop(visualRowOrder .get(removedVisualInside.getStart())); for (int i = 0; i < removedVisualInside.length(); i++) { final TableRowElement tr = visualRowOrder .remove(removedVisualInside.getStart()); visualRowOrder.addLast(tr); } for (int i = removedVisualInside .getStart(); i < escalatorRowCount; i++) { final TableRowElement tr = visualRowOrder.get(i); setRowPosition(tr, 0, (int) newTop); newTop += getDefaultRowHeight(); newTop += spacerContainer.getSpacerHeight( i + removedLogicalInside.getStart()); } /* * STEP 2: * * manually scroll */ /*- * 1 |1| <-- newly rendered (by scrolling) * |4| |4| * |*| ==> |*| * |*| * 5 5 */ final double newScrollTop = contentBottom - getHeightOfSection(); setScrollTop(newScrollTop); /* * Manually call the scroll handler, so we get immediate * effects in the escalator. */ scroller.onScroll(); /* * Move the bottommost (n+1:th) escalator row to top, * because scrolling up doesn't handle that for us * automatically */ moveAndUpdateEscalatorRows( Range.withOnly(escalatorRowCount - 1), 0, getLogicalRowIndex(visualRowOrder.getFirst()) - 1); updateTopRowLogicalIndex(-1); /* * STEP 3: * * update remaining escalator rows */ /*- * |1| |1| * |4| ==> |4| * |*| |5| <-- newly rendered * * 5 */ final int rowsScrolled = (int) (Math .ceil((viewportBottom - contentBottom) / getDefaultRowHeight())); final int start = escalatorRowCount - (removedVisualInside.length() - rowsScrolled); final Range visualRefreshRange = Range.between(start, escalatorRowCount); final int logicalTargetIndex = getLogicalRowIndex( visualRowOrder.getFirst()) + start; // in-place move simply re-renders the rows. moveAndUpdateEscalatorRows(visualRefreshRange, start, logicalTargetIndex); } } fireRowVisibilityChangeEvent(); sortDomElements(); } updateTopRowLogicalIndex(-removedAbove.length()); /* * this needs to be done after the escalator has been shrunk down, * or it won't work correctly (due to setScrollTop invocation) */ scroller.recalculateScrollbarsForVirtualViewport(); } private void paintRemoveRowsAtMiddle(final Range removedLogicalInside, final Range removedVisualInside, final int logicalOffset) { /*- * : : : * |2| |2| |2| * |3| ==> |*| ==> |4| * |4| |4| |6| <- newly rendered * : : : */ final int escalatorRowCount = visualRowOrder.size(); final int logicalTargetIndex = getLogicalRowIndex( visualRowOrder.getLast()) - (removedVisualInside.length() - 1) + logicalOffset; moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount, logicalTargetIndex); // move the surrounding rows to their correct places. final ListIterator iterator = visualRowOrder .listIterator(removedVisualInside.getStart()); double rowTop = getRowTop( removedLogicalInside.getStart() + logicalOffset); for (int i = removedVisualInside.getStart(); i < escalatorRowCount - removedVisualInside.length(); i++) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); rowTop += getDefaultRowHeight(); rowTop += spacerContainer .getSpacerHeight(i + removedLogicalInside.getStart()); } } private void paintRemoveRowsAtBottom(final Range removedLogicalInside, final Range removedVisualInside) { /*- * : * : : |4| <- newly rendered * |5| |5| |5| * |6| ==> |*| ==> |7| * |7| |7| */ final int logicalTargetIndex = getLogicalRowIndex( visualRowOrder.getFirst()) - removedVisualInside.length(); moveAndUpdateEscalatorRows(removedVisualInside, 0, logicalTargetIndex); // move the surrounding rows to their correct places. int firstUpdatedIndex = removedVisualInside.getEnd(); final ListIterator iterator = visualRowOrder .listIterator(firstUpdatedIndex); double rowTop = getRowTop(removedLogicalInside.getStart()); int i = 0; while (iterator.hasNext()) { final TableRowElement tr = iterator.next(); setRowPosition(tr, 0, rowTop); rowTop += getDefaultRowHeight(); rowTop += spacerContainer .getSpacerHeight(firstUpdatedIndex + i++); } } @Override public int getLogicalRowIndex(final TableRowElement tr) { assert tr .getParentNode() == root : "The given element isn't a row element in the body"; int internalIndex = visualRowOrder.indexOf(tr); return getTopRowLogicalIndex() + internalIndex; } @Override protected void recalculateSectionHeight() { // NOOP for body, since it doesn't make any sense. } /** * Adjusts the row index and number to be relevant for the current * virtual viewport. *

* It converts a logical range of rows index to the matching visual * range, truncating the resulting range with the viewport. *

*

* * @return a logical range converted to a visual range, truncated to the * current viewport. The first visual row has the index 0. */ private Range convertToVisual(final Range logicalRange) { if (logicalRange.isEmpty()) { return logicalRange; } else if (visualRowOrder.isEmpty()) { // empty range return Range.withLength(0, 0); } /* * TODO [[spacer]]: these assumptions will be totally broken with * spacers. */ final int maxVisibleRowCount = getMaxVisibleRowCount(); final int currentTopRowIndex = getLogicalRowIndex( visualRowOrder.getFirst()); final Range[] partitions = logicalRange.partitionWith( Range.withLength(currentTopRowIndex, maxVisibleRowCount)); final Range insideRange = partitions[1]; return insideRange.offsetBy(-currentTopRowIndex); } @Override protected String getCellElementTagName() { return "td"; } @Override protected double getHeightOfSection() { final int tableHeight = tableWrapper.getOffsetHeight(); final double footerHeight = footer.getHeightOfSection(); final double headerHeight = header.getHeightOfSection(); double heightOfSection = tableHeight - footerHeight - headerHeight; return Math.max(0, heightOfSection); } @Override protected void refreshCells(Range logicalRowRange, Range colRange) { Profiler.enter("Escalator.BodyRowContainer.refreshRows"); final Range visualRange = convertToVisual(logicalRowRange); if (!visualRange.isEmpty()) { final int firstLogicalRowIndex = getLogicalRowIndex( visualRowOrder.getFirst()); for (int rowNumber = visualRange .getStart(); rowNumber < visualRange .getEnd(); rowNumber++) { refreshRow(visualRowOrder.get(rowNumber), firstLogicalRowIndex + rowNumber, colRange); } } Profiler.leave("Escalator.BodyRowContainer.refreshRows"); } @Override protected TableRowElement getTrByVisualIndex(final int index) throws IndexOutOfBoundsException { if (index >= 0 && index < visualRowOrder.size()) { return visualRowOrder.get(index); } else { throw new IndexOutOfBoundsException( "No such visual index: " + index); } } @Override public TableRowElement getRowElement(int index) { if (index < 0 || index >= getRowCount()) { throw new IndexOutOfBoundsException( "No such logical index: " + index); } int visualIndex = index - getLogicalRowIndex(visualRowOrder.getFirst()); if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) { return super.getRowElement(visualIndex); } else { throw new IllegalStateException("Row with logical index " + index + " is currently not available in the DOM"); } } private void setBodyScrollPosition(final double scrollLeft, final double scrollTop) { tBodyScrollLeft = scrollLeft; tBodyScrollTop = scrollTop; position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop); position.set(spacerDecoContainer, 0, -tBodyScrollTop); } /** * Make sure that there is a correct amount of escalator rows: Add more * if needed, or remove any superfluous ones. *

* This method should be called when e.g. the height of the Escalator * changes. *

* Note: This method will make sure that the escalator rows are * placed in the proper places. By default new rows are added below, but * if the content is scrolled down, the rows are populated on top * instead. */ public void verifyEscalatorCount() { /* * This method indeed has a smell very similar to paintRemoveRows * and paintInsertRows. * * Unfortunately, those the code can't trivially be shared, since * there are some slight differences in the respective * responsibilities. The "paint" methods fake the addition and * removal of rows, and make sure to either push existing data out * of view, or draw new data into view. Only in some special cases * will the DOM element count change. * * This method, however, has the explicit responsibility to verify * that when "something" happens, we still have the correct amount * of escalator rows in the DOM, and if not, we make sure to modify * that count. Only in some special cases do we need to take into * account other things than simply modifying the DOM element count. */ Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount"); if (!isAttached()) { return; } final int maxVisibleRowCount = getMaxVisibleRowCount(); final int neededEscalatorRows = Math.min(maxVisibleRowCount, body.getRowCount()); final int neededEscalatorRowsDiff = neededEscalatorRows - visualRowOrder.size(); if (neededEscalatorRowsDiff > 0) { // needs more /* * This is a workaround for the issue where we might be scrolled * to the bottom, and the widget expands beyond the content * range */ final int index = visualRowOrder.size(); final int nextLastLogicalIndex; if (!visualRowOrder.isEmpty()) { nextLastLogicalIndex = getLogicalRowIndex( visualRowOrder.getLast()) + 1; } else { nextLastLogicalIndex = 0; } final boolean contentWillFit = nextLastLogicalIndex < getRowCount() - neededEscalatorRowsDiff; if (contentWillFit) { final List addedRows = fillAndPopulateEscalatorRowsIfNeeded( index, neededEscalatorRowsDiff); /* * Since fillAndPopulateEscalatorRowsIfNeeded operates on * the assumption that index == visual index == logical * index, we thank for the added escalator rows, but since * they're painted in the wrong CSS position, we need to * move them to their actual locations. * * Note: this is the second (see body.paintInsertRows) * occasion where fillAndPopulateEscalatorRowsIfNeeded would * behave "more correctly" if it only would add escalator * rows to the DOM and appropriate bookkeping, and not * actually populate them :/ */ moveAndUpdateEscalatorRows( Range.withLength(index, addedRows.size()), index, nextLastLogicalIndex); } else { /* * TODO [[optimize]] * * We're scrolled so far down that all rows can't be simply * appended at the end, since we might start displaying * escalator rows that don't exist. To avoid the mess that * is body.paintRemoveRows, this is a dirty hack that dumbs * the problem down to a more basic and already-solved * problem: * * 1) scroll all the way up 2) add the missing escalator * rows 3) scroll back to the original position. * * Letting the browser scroll back to our original position * will automatically solve any possible overflow problems, * since the browser will not allow us to scroll beyond the * actual content. */ final double oldScrollTop = getScrollTop(); setScrollTop(0); scroller.onScroll(); fillAndPopulateEscalatorRowsIfNeeded(index, neededEscalatorRowsDiff); setScrollTop(oldScrollTop); scroller.onScroll(); } } else if (neededEscalatorRowsDiff < 0) { // needs less final ListIterator iter = visualRowOrder .listIterator(visualRowOrder.size()); for (int i = 0; i < -neededEscalatorRowsDiff; i++) { final Element last = iter.previous(); last.removeFromParent(); iter.remove(); } /* * If we were scrolled to the bottom so that we didn't have an * extra escalator row at the bottom, we'll probably end up with * blank space at the bottom of the escalator, and one extra row * above the header. * * Experimentation idea #1: calculate "scrollbottom" vs content * bottom and remove one row from top, rest from bottom. This * FAILED, since setHeight has already happened, thus we never * will detect ourselves having been scrolled all the way to the * bottom. */ if (!visualRowOrder.isEmpty()) { final double firstRowTop = getRowTop( visualRowOrder.getFirst()); final double firstRowMinTop = tBodyScrollTop - getDefaultRowHeight(); if (firstRowTop < firstRowMinTop) { final int newLogicalIndex = getLogicalRowIndex( visualRowOrder.getLast()) + 1; moveAndUpdateEscalatorRows(Range.withOnly(0), visualRowOrder.size(), newLogicalIndex); updateTopRowLogicalIndex(1); } } } if (neededEscalatorRowsDiff != 0) { fireRowVisibilityChangeEvent(); } Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount"); } @Override protected void reapplyDefaultRowHeights() { if (visualRowOrder.isEmpty()) { return; } Profiler.enter( "Escalator.BodyRowContainer.reapplyDefaultRowHeights"); /* step 1: resize and reposition rows */ for (int i = 0; i < visualRowOrder.size(); i++) { TableRowElement tr = visualRowOrder.get(i); reapplyRowHeight(tr, getDefaultRowHeight()); final int logicalIndex = getTopRowLogicalIndex() + i; setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight()); } /* * step 2: move scrollbar so that it corresponds to its previous * place */ /* * This ratio needs to be calculated with the scrollsize (not max * scroll position) in order to align the top row with the new * scroll position. */ double scrollRatio = verticalScrollbar.getScrollPos() / verticalScrollbar.getScrollSize(); scroller.recalculateScrollbarsForVirtualViewport(); verticalScrollbar.setScrollPos((int) (getDefaultRowHeight() * getRowCount() * scrollRatio)); setBodyScrollPosition(horizontalScrollbar.getScrollPos(), verticalScrollbar.getScrollPos()); scroller.onScroll(); /* * step 3: make sure we have the correct amount of escalator rows. */ verifyEscalatorCount(); int logicalLogical = (int) (getRowTop(visualRowOrder.getFirst()) / getDefaultRowHeight()); setTopRowLogicalIndex(logicalLogical); Profiler.leave( "Escalator.BodyRowContainer.reapplyDefaultRowHeights"); } /** * Sorts the rows in the DOM to correspond to the visual order. * * @see #visualRowOrder */ private void sortDomElements() { final String profilingName = "Escalator.BodyRowContainer.sortDomElements"; Profiler.enter(profilingName); /* * Focus is lost from an element if that DOM element is (or any of * its parents are) removed from the document. Therefore, we sort * everything around that row instead. */ final TableRowElement focusedRow = getRowWithFocus(); if (focusedRow != null) { assert focusedRow .getParentElement() == root : "Trying to sort around a row that doesn't exist in body"; assert visualRowOrder.contains(focusedRow) || body.spacerContainer.isSpacer( focusedRow) : "Trying to sort around a row that doesn't exist in visualRowOrder or is not a spacer."; } /* * Two cases handled simultaneously: * * 1) No focus on rows. We iterate visualRowOrder backwards, and * take the respective element in the DOM, and place it as the first * child in the body element. Then we take the next-to-last from * visualRowOrder, and put that first, pushing the previous row as * the second child. And so on... * * 2) Focus on some row within Escalator body. Again, we iterate * visualRowOrder backwards. This time, we use the focused row as a * pivot: Instead of placing rows from the bottom of visualRowOrder * and placing it first, we place it underneath the focused row. * Once we hit the focused row, we don't move it (to not reset * focus) but change sorting mode. After that, we place all rows as * the first child. */ List orderedBodyRows = new ArrayList( visualRowOrder); Map spacers = body.spacerContainer .getSpacers(); /* * Start at -1 to include a spacer that is rendered above the * viewport, but its parent row is still not shown */ for (int i = -1; i < visualRowOrder.size(); i++) { SpacerContainer.SpacerImpl spacer = spacers .remove(Integer.valueOf(getTopRowLogicalIndex() + i)); if (spacer != null) { orderedBodyRows.add(i + 1, spacer.getRootElement()); spacer.show(); } } /* * At this point, invisible spacers aren't reordered, so their * position in the DOM will remain undefined. */ // If a spacer was not reordered, it means that it's out of view. for (SpacerContainer.SpacerImpl unmovedSpacer : spacers.values()) { unmovedSpacer.hide(); } /* * If we have a focused row, start in the mode where we put * everything underneath that row. Otherwise, all rows are placed as * first child. */ boolean insertFirst = (focusedRow == null); final ListIterator i = orderedBodyRows .listIterator(orderedBodyRows.size()); while (i.hasPrevious()) { TableRowElement tr = i.previous(); if (tr == focusedRow) { insertFirst = true; } else if (insertFirst) { root.insertFirst(tr); } else { root.insertAfter(tr, focusedRow); } } Profiler.leave(profilingName); } /** * Get the {@literal } row that contains (or has) focus. * * @return The {@literal } row that contains a focused DOM * element, or null if focus is outside of a body * row. */ private TableRowElement getRowWithFocus() { TableRowElement rowContainingFocus = null; final Element focusedElement = WidgetUtil.getFocusedElement(); if (focusedElement != null && root.isOrHasChild(focusedElement)) { Element e = focusedElement; while (e != null && e != root) { /* * You never know if there's several tables embedded in a * cell... We'll take the deepest one. */ if (TableRowElement.is(e)) { rowContainingFocus = TableRowElement.as(e); } e = e.getParentElement(); } } return rowContainingFocus; } @Override public Cell getCell(Element element) { Cell cell = super.getCell(element); if (cell == null) { return null; } // Convert DOM coordinates to logical coordinates for rows TableRowElement rowElement = (TableRowElement) cell.getElement() .getParentElement(); return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(), cell.getElement()); } @Override public void setSpacer(int rowIndex, double height) throws IllegalArgumentException { spacerContainer.setSpacer(rowIndex, height); } @Override public void setSpacerUpdater(SpacerUpdater spacerUpdater) throws IllegalArgumentException { spacerContainer.setSpacerUpdater(spacerUpdater); } @Override public SpacerUpdater getSpacerUpdater() { return spacerContainer.getSpacerUpdater(); } /** * Calculates the correct top position of a row at a logical * index, regardless if there is one there or not. *

* A correct result requires that both {@link #getDefaultRowHeight()} is * consistent, and the placement and height of all spacers above the * given logical index are consistent. * * @param logicalIndex * the logical index of the row for which to calculate the * top position * @return the position at which to place a row in {@code logicalIndex} * @see #getRowTop(TableRowElement) */ private double getRowTop(int logicalIndex) { double top = spacerContainer .getSpacerHeightsSumUntilIndex(logicalIndex); return top + (logicalIndex * getDefaultRowHeight()); } public void shiftRowPositions(int row, double diff) { for (TableRowElement tr : getVisibleRowsAfter(row)) { setRowPosition(tr, 0, getRowTop(tr) + diff); } } private List getVisibleRowsAfter(int logicalRow) { Range visibleRowLogicalRange = getVisibleRowRange(); boolean allRowsAreInView = logicalRow < visibleRowLogicalRange .getStart(); boolean noRowsAreInView = logicalRow >= visibleRowLogicalRange .getEnd() - 1; if (allRowsAreInView) { return Collections.unmodifiableList(visualRowOrder); } else if (noRowsAreInView) { return Collections.emptyList(); } else { int fromIndex = (logicalRow - visibleRowLogicalRange.getStart()) + 1; int toIndex = visibleRowLogicalRange.length(); List sublist = visualRowOrder .subList(fromIndex, toIndex); return Collections.unmodifiableList(sublist); } } @Override public int getDomRowCount() { return root.getChildCount() - spacerContainer.getSpacersInDom().size(); } @Override protected boolean rowCanBeFrozen(TableRowElement tr) { return visualRowOrder.contains(tr); } void reapplySpacerWidths() { spacerContainer.reapplySpacerWidths(); } void scrollToSpacer(int spacerIndex, ScrollDestination destination, int padding) { spacerContainer.scrollToSpacer(spacerIndex, destination, padding); } } private class ColumnConfigurationImpl implements ColumnConfiguration { public class Column { public static final double DEFAULT_COLUMN_WIDTH_PX = 100; private double definedWidth = -1; private double calculatedWidth = DEFAULT_COLUMN_WIDTH_PX; private boolean measuringRequested = false; public void setWidth(double px) { definedWidth = px; if (px < 0) { if (isAttached()) { calculateWidth(); } else { /* * the column's width is calculated at Escalator.onLoad * via measureAndSetWidthIfNeeded! */ measuringRequested = true; } } else { calculatedWidth = px; } } public double getDefinedWidth() { return definedWidth; } /** * Returns the actual width in the DOM. * * @return the width in pixels in the DOM. Returns -1 if the column * needs measuring, but has not been yet measured */ public double getCalculatedWidth() { /* * This might return an untrue value (e.g. during init/onload), * since we haven't had a proper chance to actually calculate * widths yet. * * This is fixed during Escalator.onLoad, by the call to * "measureAndSetWidthIfNeeded", which fixes "everything". */ if (!measuringRequested) { return calculatedWidth; } else { return -1; } } /** * Checks if the column needs measuring, and then measures it. *

* Called by {@link Escalator#onLoad()}. */ public boolean measureAndSetWidthIfNeeded() { assert isAttached() : "Column.measureAndSetWidthIfNeeded() was called even though Escalator was not attached!"; if (measuringRequested) { measuringRequested = false; setWidth(definedWidth); return true; } return false; } private void calculateWidth() { calculatedWidth = getMaxCellWidth(columns.indexOf(this)); } } private final List columns = new ArrayList(); private int frozenColumns = 0; /* * TODO: this is a bit of a duplicate functionality with the * Column.calculatedWidth caching. Probably should use one or the other, * not both */ /** * A cached array of all the calculated column widths. * * @see #getCalculatedColumnWidths() */ private double[] widthsArray = null; /** * {@inheritDoc} *

* Implementation detail: This method does no DOM modifications * (i.e. is very cheap to call) if there are no rows in the DOM when * this method is called. * * @see #hasSomethingInDom() */ @Override public void removeColumns(final int index, final int numberOfColumns) { // Validate assertArgumentsAreValidAndWithinRange(index, numberOfColumns); // Move the horizontal scrollbar to the left, if removed columns are // to the left of the viewport removeColumnsAdjustScrollbar(index, numberOfColumns); // Remove from DOM header.paintRemoveColumns(index, numberOfColumns); body.paintRemoveColumns(index, numberOfColumns); footer.paintRemoveColumns(index, numberOfColumns); // Remove from bookkeeping flyweightRow.removeCells(index, numberOfColumns); columns.subList(index, index + numberOfColumns).clear(); // Adjust frozen columns if (index < getFrozenColumnCount()) { if (index + numberOfColumns < frozenColumns) { /* * Last removed column was frozen, meaning that all removed * columns were frozen. Just decrement the number of frozen * columns accordingly. */ frozenColumns -= numberOfColumns; } else { /* * If last removed column was not frozen, we have removed * columns beyond the frozen range, so all remaining frozen * columns are to the left of the removed columns. */ frozenColumns = index; } } scroller.recalculateScrollbarsForVirtualViewport(); body.verifyEscalatorCount(); if (getColumnConfiguration().getColumnCount() > 0) { reapplyRowWidths(header); reapplyRowWidths(body); reapplyRowWidths(footer); } /* * Colspans make any kind of automatic clever content re-rendering * impossible: As soon as anything has colspans, removing one might * reveal further colspans, modifying the DOM structure once again, * ending in a cascade of updates. Because we don't know how the * data is updated. * * So, instead, we don't do anything. The client code is responsible * for re-rendering the content (if so desired). Everything Just * Works (TM) if colspans aren't used. */ } private void reapplyRowWidths(AbstractRowContainer container) { if (container.getRowCount() > 0) { container.reapplyRowWidths(); } } private void removeColumnsAdjustScrollbar(int index, int numberOfColumns) { if (horizontalScrollbar.getOffsetSize() >= horizontalScrollbar .getScrollSize()) { return; } double leftPosOfFirstColumnToRemove = getCalculatedColumnsWidth( Range.between(0, index)); double widthOfColumnsToRemove = getCalculatedColumnsWidth( Range.withLength(index, numberOfColumns)); double scrollLeft = horizontalScrollbar.getScrollPos(); if (scrollLeft <= leftPosOfFirstColumnToRemove) { /* * viewport is scrolled to the left of the first removed column, * so there's no need to adjust anything */ return; } double adjustedScrollLeft = Math.max(leftPosOfFirstColumnToRemove, scrollLeft - widthOfColumnsToRemove); horizontalScrollbar.setScrollPos(adjustedScrollLeft); } /** * Calculate the width of a row, as the sum of columns' widths. * * @return the width of a row, in pixels */ public double calculateRowWidth() { return getCalculatedColumnsWidth( Range.between(0, getColumnCount())); } private void assertArgumentsAreValidAndWithinRange(final int index, final int numberOfColumns) { if (numberOfColumns < 1) { throw new IllegalArgumentException( "Number of columns can't be less than 1 (was " + numberOfColumns + ")"); } if (index < 0 || index + numberOfColumns > getColumnCount()) { throw new IndexOutOfBoundsException("The given " + "column range (" + index + ".." + (index + numberOfColumns) + ") was outside of the current " + "number of columns (" + getColumnCount() + ")"); } } /** * {@inheritDoc} *

* Implementation detail: This method does no DOM modifications * (i.e. is very cheap to call) if there is no data for rows when this * method is called. * * @see #hasColumnAndRowData() */ @Override public void insertColumns(final int index, final int numberOfColumns) { // Validate if (index < 0 || index > getColumnCount()) { throw new IndexOutOfBoundsException("The given index(" + index + ") was outside of the current number of columns (0.." + getColumnCount() + ")"); } if (numberOfColumns < 1) { throw new IllegalArgumentException( "Number of columns must be 1 or greater (was " + numberOfColumns); } // Add to bookkeeping flyweightRow.addCells(index, numberOfColumns); for (int i = 0; i < numberOfColumns; i++) { columns.add(index, new Column()); } // Adjust frozen columns boolean frozen = index < frozenColumns; if (frozen) { frozenColumns += numberOfColumns; } // Add to DOM header.paintInsertColumns(index, numberOfColumns, frozen); body.paintInsertColumns(index, numberOfColumns, frozen); footer.paintInsertColumns(index, numberOfColumns, frozen); // this needs to be before the scrollbar adjustment. boolean scrollbarWasNeeded = horizontalScrollbar .getOffsetSize() < horizontalScrollbar.getScrollSize(); scroller.recalculateScrollbarsForVirtualViewport(); boolean scrollbarIsNowNeeded = horizontalScrollbar .getOffsetSize() < horizontalScrollbar.getScrollSize(); if (!scrollbarWasNeeded && scrollbarIsNowNeeded) { // This might as a side effect move rows around (when scrolled // all the way down) and require the DOM to be up to date, i.e. // the column to be added body.verifyEscalatorCount(); } // fix initial width if (header.getRowCount() > 0 || body.getRowCount() > 0 || footer.getRowCount() > 0) { Map colWidths = new HashMap(); Double width = Double.valueOf(Column.DEFAULT_COLUMN_WIDTH_PX); for (int i = index; i < index + numberOfColumns; i++) { Integer col = Integer.valueOf(i); colWidths.put(col, width); } getColumnConfiguration().setColumnWidths(colWidths); } // Adjust scrollbar double pixelsToInsertedColumn = columnConfiguration .getCalculatedColumnsWidth(Range.withLength(0, index)); final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn; if (columnsWereAddedToTheLeftOfViewport) { double insertedColumnsWidth = columnConfiguration .getCalculatedColumnsWidth( Range.withLength(index, numberOfColumns)); horizontalScrollbar.setScrollPos( scroller.lastScrollLeft + insertedColumnsWidth); } /* * Colspans make any kind of automatic clever content re-rendering * impossible: As soon as anything has colspans, adding one might * affect surrounding colspans, modifying the DOM structure once * again, ending in a cascade of updates. Because we don't know how * the data is updated. * * So, instead, we don't do anything. The client code is responsible * for re-rendering the content (if so desired). Everything Just * Works (TM) if colspans aren't used. */ } @Override public int getColumnCount() { return columns.size(); } @Override public void setFrozenColumnCount(int count) throws IllegalArgumentException { if (count < 0 || count > getColumnCount()) { throw new IllegalArgumentException( "count must be between 0 and the current number of columns (" + getColumnCount() + ")"); } int oldCount = frozenColumns; if (count == oldCount) { return; } frozenColumns = count; if (hasSomethingInDom()) { // Are we freezing or unfreezing? boolean frozen = count > oldCount; int firstAffectedCol; int firstUnaffectedCol; if (frozen) { firstAffectedCol = oldCount; firstUnaffectedCol = count; } else { firstAffectedCol = count; firstUnaffectedCol = oldCount; } if (oldCount > 0) { header.setColumnLastFrozen(oldCount - 1, false); body.setColumnLastFrozen(oldCount - 1, false); footer.setColumnLastFrozen(oldCount - 1, false); } if (count > 0) { header.setColumnLastFrozen(count - 1, true); body.setColumnLastFrozen(count - 1, true); footer.setColumnLastFrozen(count - 1, true); } for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) { header.setColumnFrozen(col, frozen); body.setColumnFrozen(col, frozen); footer.setColumnFrozen(col, frozen); } } scroller.recalculateScrollbarsForVirtualViewport(); } @Override public int getFrozenColumnCount() { return frozenColumns; } @Override public void setColumnWidth(int index, double px) throws IllegalArgumentException { setColumnWidths(Collections.singletonMap(Integer.valueOf(index), Double.valueOf(px))); } @Override public void setColumnWidths(Map indexWidthMap) throws IllegalArgumentException { if (indexWidthMap == null) { throw new IllegalArgumentException("indexWidthMap was null"); } if (indexWidthMap.isEmpty()) { return; } for (Entry entry : indexWidthMap.entrySet()) { int index = entry.getKey().intValue(); double width = entry.getValue().doubleValue(); checkValidColumnIndex(index); // Not all browsers will accept any fractional size.. width = WidgetUtil.roundSizeDown(width); columns.get(index).setWidth(width); } widthsArray = null; header.reapplyColumnWidths(); body.reapplyColumnWidths(); footer.reapplyColumnWidths(); recalculateElementSizes(); } private void checkValidColumnIndex(int index) throws IllegalArgumentException { if (!Range.withLength(0, getColumnCount()).contains(index)) { throw new IllegalArgumentException("The given column index (" + index + ") does not exist"); } } @Override public double getColumnWidth(int index) throws IllegalArgumentException { checkValidColumnIndex(index); return columns.get(index).getDefinedWidth(); } @Override public double getColumnWidthActual(int index) { return columns.get(index).getCalculatedWidth(); } private double getMaxCellWidth(int colIndex) throws IllegalArgumentException { double headerWidth = header.measureMinCellWidth(colIndex, true); double bodyWidth = body.measureMinCellWidth(colIndex, true); double footerWidth = footer.measureMinCellWidth(colIndex, true); double maxWidth = Math.max(headerWidth, Math.max(bodyWidth, footerWidth)); assert maxWidth >= 0 : "Got a negative max width for a column, which should be impossible."; return maxWidth; } private double getMinCellWidth(int colIndex) throws IllegalArgumentException { double headerWidth = header.measureMinCellWidth(colIndex, false); double bodyWidth = body.measureMinCellWidth(colIndex, false); double footerWidth = footer.measureMinCellWidth(colIndex, false); double minWidth = Math.max(headerWidth, Math.max(bodyWidth, footerWidth)); assert minWidth >= 0 : "Got a negative max width for a column, which should be impossible."; return minWidth; } /** * Calculates the width of the columns in a given range. * * @param columns * the columns to calculate * @return the total width of the columns in the given * columns */ double getCalculatedColumnsWidth(final Range columns) { /* * This is an assert instead of an exception, since this is an * internal method. */ assert columns .isSubsetOf(Range.between(0, getColumnCount())) : "Range " + "was outside of current column range (i.e.: " + Range.between(0, getColumnCount()) + ", but was given :" + columns; double sum = 0; for (int i = columns.getStart(); i < columns.getEnd(); i++) { double columnWidthActual = getColumnWidthActual(i); sum += columnWidthActual; } return sum; } double[] getCalculatedColumnWidths() { if (widthsArray == null || widthsArray.length != getColumnCount()) { widthsArray = new double[getColumnCount()]; for (int i = 0; i < columns.size(); i++) { widthsArray[i] = columns.get(i).getCalculatedWidth(); } } return widthsArray; } @Override public void refreshColumns(int index, int numberOfColumns) throws IndexOutOfBoundsException, IllegalArgumentException { if (numberOfColumns < 1) { throw new IllegalArgumentException( "Number of columns must be 1 or greater (was " + numberOfColumns + ")"); } if (index < 0 || index + numberOfColumns > getColumnCount()) { throw new IndexOutOfBoundsException("The given " + "column range (" + index + ".." + (index + numberOfColumns) + ") was outside of the current number of columns (" + getColumnCount() + ")"); } header.refreshColumns(index, numberOfColumns); body.refreshColumns(index, numberOfColumns); footer.refreshColumns(index, numberOfColumns); } } /** * A decision on how to measure a spacer when it is partially within a * designated range. *

* The meaning of each value may differ depending on the context it is being * used in. Check that particular method's JavaDoc. */ private enum SpacerInclusionStrategy { /** A representation of "the entire spacer". */ COMPLETE, /** A representation of "a partial spacer". */ PARTIAL, /** A representation of "no spacer at all". */ NONE } private class SpacerContainer { /** This is used mainly for testing purposes */ private static final String SPACER_LOGICAL_ROW_PROPERTY = "vLogicalRow"; private final class SpacerImpl implements Spacer { private TableCellElement spacerElement; private TableRowElement root; private DivElement deco; private int rowIndex; private double height = -1; private boolean domHasBeenSetup = false; private double decoHeight; private double defaultCellBorderBottomSize = -1; public SpacerImpl(int rowIndex) { this.rowIndex = rowIndex; root = TableRowElement.as(DOM.createTR()); spacerElement = TableCellElement.as(DOM.createTD()); root.appendChild(spacerElement); root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); deco = DivElement.as(DOM.createDiv()); } public void setPositionDiff(double x, double y) { setPosition(getLeft() + x, getTop() + y); } public void setupDom(double height) { assert !domHasBeenSetup : "DOM can't be set up twice."; assert RootPanel.get().getElement().isOrHasChild( root) : "Root element should've been attached to the DOM by now."; domHasBeenSetup = true; getRootElement().getStyle().setWidth(getInnerWidth(), Unit.PX); setHeight(height); spacerElement .setColSpan(getColumnConfiguration().getColumnCount()); setStylePrimaryName(getStylePrimaryName()); } public TableRowElement getRootElement() { return root; } @Override public Element getDecoElement() { return deco; } public void setPosition(double x, double y) { positions.set(getRootElement(), x, y); positions.set(getDecoElement(), 0, y - getSpacerDecoTopOffset()); } private double getSpacerDecoTopOffset() { return getBody().getDefaultRowHeight(); } public void setStylePrimaryName(String style) { UIObject.setStylePrimaryName(root, style + "-spacer"); UIObject.setStylePrimaryName(deco, style + "-spacer-deco"); } public void setHeight(double height) { assert height >= 0 : "Height must be more >= 0 (was " + height + ")"; final double heightDiff = height - Math.max(0, this.height); final double oldHeight = this.height; this.height = height; // since the spacer might be rendered on top of the previous // rows border (done with css), need to increase height the // amount of the border thickness if (defaultCellBorderBottomSize < 0) { defaultCellBorderBottomSize = WidgetUtil .getBorderBottomThickness(body .getRowElement( getVisibleRowRange().getStart()) .getFirstChildElement()); } root.getStyle().setHeight(height + defaultCellBorderBottomSize, Unit.PX); // move the visible spacers getRow row onwards. shiftSpacerPositionsAfterRow(getRow(), heightDiff); /* * If we're growing, we'll adjust the scroll size first, then * adjust scrolling. If we're shrinking, we do it after the * second if-clause. */ boolean spacerIsGrowing = heightDiff > 0; if (spacerIsGrowing) { verticalScrollbar.setScrollSize( verticalScrollbar.getScrollSize() + heightDiff); } /* * Don't modify the scrollbars if we're expanding the -1 spacer * while we're scrolled to the top. */ boolean minusOneSpacerException = spacerIsGrowing && getRow() == -1 && body.getTopRowLogicalIndex() == 0; boolean viewportNeedsScrolling = getRow() < body .getTopRowLogicalIndex() && !minusOneSpacerException; if (viewportNeedsScrolling) { /* * We can't use adjustScrollPos here, probably because of a * bookkeeping-related race condition. * * This particular situation is easier, however, since we * know exactly how many pixels we need to move (heightDiff) * and all elements below the spacer always need to move * that pixel amount. */ for (TableRowElement row : body.visualRowOrder) { body.setRowPosition(row, 0, body.getRowTop(row) + heightDiff); } double top = getTop(); double bottom = top + oldHeight; double scrollTop = verticalScrollbar.getScrollPos(); boolean viewportTopIsAtMidSpacer = top < scrollTop && scrollTop < bottom; final double moveDiff; if (viewportTopIsAtMidSpacer && !spacerIsGrowing) { /* * If the scroll top is in the middle of the modified * spacer, we want to scroll the viewport up as usual, * but we don't want to scroll past the top of it. * * Math.max ensures this (remember: the result is going * to be negative). */ moveDiff = Math.max(heightDiff, top - scrollTop); } else { moveDiff = heightDiff; } body.setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + moveDiff); verticalScrollbar.setScrollPosByDelta(moveDiff); } else { body.shiftRowPositions(getRow(), heightDiff); } if (!spacerIsGrowing) { verticalScrollbar.setScrollSize( verticalScrollbar.getScrollSize() + heightDiff); } updateDecoratorGeometry(height); } /** Resizes and places the decorator. */ private void updateDecoratorGeometry(double detailsHeight) { Style style = deco.getStyle(); decoHeight = detailsHeight + getBody().getDefaultRowHeight(); style.setHeight(decoHeight, Unit.PX); } @Override public Element getElement() { return spacerElement; } @Override public int getRow() { return rowIndex; } public double getHeight() { assert height >= 0 : "Height was not previously set by setHeight."; return height; } public double getTop() { return positions.getTop(getRootElement()); } public double getLeft() { return positions.getLeft(getRootElement()); } /** * Sets a new row index for this spacer. Also updates the bookeeping * at {@link SpacerContainer#rowIndexToSpacer}. */ @SuppressWarnings("boxing") public void setRowIndex(int rowIndex) { SpacerImpl spacer = rowIndexToSpacer.remove(this.rowIndex); assert this == spacer : "trying to move an unexpected spacer."; this.rowIndex = rowIndex; root.setPropertyInt(SPACER_LOGICAL_ROW_PROPERTY, rowIndex); rowIndexToSpacer.put(this.rowIndex, this); } /** * Updates the spacer's visibility parameters, based on whether it * is being currently visible or not. */ public void updateVisibility() { if (isInViewport()) { show(); } else { hide(); } } private boolean isInViewport() { int top = (int) Math.ceil(getTop()); int height = (int) Math.floor(getHeight()); Range location = Range.withLength(top, height); return getViewportPixels().intersects(location); } public void show() { getRootElement().getStyle().clearDisplay(); getDecoElement().getStyle().clearDisplay(); fireEvent(new SpacerVisibilityChangedEvent(getRow(), true)); } public void hide() { getRootElement().getStyle().setDisplay(Display.NONE); getDecoElement().getStyle().setDisplay(Display.NONE); fireEvent(new SpacerVisibilityChangedEvent(getRow(), false)); } /** * Crop the decorator element so that it doesn't overlap the header * and footer sections. * * @param bodyTop * the top cordinate of the escalator body * @param bodyBottom * the bottom cordinate of the escalator body * @param decoWidth * width of the deco */ private void updateDecoClip(final double bodyTop, final double bodyBottom, final double decoWidth) { final int top = deco.getAbsoluteTop(); final int bottom = deco.getAbsoluteBottom(); /* * FIXME * * Height and its use is a workaround for the issue where * coordinates of the deco are not calculated yet. This will * prevent a deco from being displayed when it's added to DOM */ final int height = bottom - top; if (top < bodyTop || bottom > bodyBottom) { final double topClip = Math.max(0.0D, bodyTop - top); final double bottomClip = height - Math.max(0.0D, bottom - bodyBottom); // TODO [optimize] not sure how GWT compiles this final String clip = new StringBuilder("rect(") .append(topClip).append("px,").append(decoWidth) .append("px,").append(bottomClip).append("px,0)") .toString(); deco.getStyle().setProperty("clip", clip); } else { deco.getStyle().setProperty("clip", "auto"); } } } private final TreeMap rowIndexToSpacer = new TreeMap(); private SpacerUpdater spacerUpdater = SpacerUpdater.NULL; private final ScrollHandler spacerScroller = new ScrollHandler() { private double prevScrollX = 0; @Override public void onScroll(ScrollEvent event) { if (WidgetUtil.pixelValuesEqual(getScrollLeft(), prevScrollX)) { return; } prevScrollX = getScrollLeft(); for (SpacerImpl spacer : rowIndexToSpacer.values()) { spacer.setPosition(prevScrollX, spacer.getTop()); } } }; private HandlerRegistration spacerScrollerRegistration; /** Width of the spacers' decos. Calculated once then cached. */ private double spacerDecoWidth = 0.0D; public void setSpacer(int rowIndex, double height) throws IllegalArgumentException { if (rowIndex < -1 || rowIndex >= getBody().getRowCount()) { throw new IllegalArgumentException("invalid row index: " + rowIndex + ", while the body only has " + getBody().getRowCount() + " rows."); } if (height >= 0) { if (!spacerExists(rowIndex)) { insertNewSpacer(rowIndex, height); } else { updateExistingSpacer(rowIndex, height); } } else if (spacerExists(rowIndex)) { removeSpacer(rowIndex); } updateSpacerDecosVisibility(); } /** Checks if a given element is a spacer element */ public boolean isSpacer(Element row) { /* * If this needs optimization, we could do a more heuristic check * based on stylenames and stuff, instead of iterating through the * map. */ for (SpacerImpl spacer : rowIndexToSpacer.values()) { if (spacer.getRootElement().equals(row)) { return true; } } return false; } @SuppressWarnings("boxing") void scrollToSpacer(int spacerIndex, ScrollDestination destination, int padding) { assert !destination.equals(ScrollDestination.MIDDLE) || padding != 0 : "destination/padding check should be done before this method"; if (!rowIndexToSpacer.containsKey(spacerIndex)) { throw new IllegalArgumentException( "No spacer open at index " + spacerIndex); } SpacerImpl spacer = rowIndexToSpacer.get(spacerIndex); double targetStartPx = spacer.getTop(); double targetEndPx = targetStartPx + spacer.getHeight(); Range viewportPixels = getViewportPixels(); double viewportStartPx = viewportPixels.getStart(); double viewportEndPx = viewportPixels.getEnd(); double scrollTop = getScrollPos(destination, targetStartPx, targetEndPx, viewportStartPx, viewportEndPx, padding); setScrollTop(scrollTop); } public void reapplySpacerWidths() { // FIXME #16266 , spacers get couple pixels too much because borders final double width = getInnerWidth() - spacerDecoWidth; for (SpacerImpl spacer : rowIndexToSpacer.values()) { spacer.getRootElement().getStyle().setWidth(width, Unit.PX); } } public void paintRemoveSpacers(Range removedRowsRange) { removeSpacers(removedRowsRange); shiftSpacersByRows(removedRowsRange.getStart(), -removedRowsRange.length()); } @SuppressWarnings("boxing") public void removeSpacers(Range removedRange) { Map removedSpacers = rowIndexToSpacer.subMap( removedRange.getStart(), true, removedRange.getEnd(), false); if (removedSpacers.isEmpty()) { return; } for (SpacerImpl spacer : removedSpacers.values()) { /* * [[optimization]] TODO: Each invocation of the setHeight * method has a cascading effect in the DOM. if this proves to * be slow, the DOM offset could be updated as a batch. */ destroySpacerContent(spacer); spacer.setHeight(0); // resets row offsets spacer.getRootElement().removeFromParent(); spacer.getDecoElement().removeFromParent(); } removedSpacers.clear(); if (rowIndexToSpacer.isEmpty()) { assert spacerScrollerRegistration != null : "Spacer scroller registration was null"; spacerScrollerRegistration.removeHandler(); spacerScrollerRegistration = null; } } public Map getSpacers() { return new HashMap(rowIndexToSpacer); } /** * Calculates the sum of all spacers. * * @return sum of all spacers, or 0 if no spacers present */ public double getSpacerHeightsSum() { return getHeights(rowIndexToSpacer.values()); } /** * Calculates the sum of all spacers from one row index onwards. * * @param logicalRowIndex * the spacer to include as the first calculated spacer * @return the sum of all spacers from {@code logicalRowIndex} and * onwards, or 0 if no suitable spacers were found */ @SuppressWarnings("boxing") public Collection getSpacersForRowAndAfter( int logicalRowIndex) { return new ArrayList( rowIndexToSpacer.tailMap(logicalRowIndex, true).values()); } /** * Get all spacers from one pixel point onwards. *

* * In this method, the {@link SpacerInclusionStrategy} has the following * meaning when a spacer lies in the middle of either pixel argument: *

*
{@link SpacerInclusionStrategy#COMPLETE COMPLETE} *
include the spacer *
{@link SpacerInclusionStrategy#PARTIAL PARTIAL} *
include the spacer *
{@link SpacerInclusionStrategy#NONE NONE} *
ignore the spacer *
* * @param px * the pixel point after which to return all spacers * @param strategy * the inclusion strategy regarding the {@code px} * @return a collection of the spacers that exist after {@code px} */ public Collection getSpacersAfterPx(final double px, final SpacerInclusionStrategy strategy) { List spacers = new ArrayList( rowIndexToSpacer.values()); for (int i = 0; i < spacers.size(); i++) { SpacerImpl spacer = spacers.get(i); double top = spacer.getTop(); double bottom = top + spacer.getHeight(); if (top > px) { return spacers.subList(i, spacers.size()); } else if (bottom > px) { if (strategy == SpacerInclusionStrategy.NONE) { return spacers.subList(i + 1, spacers.size()); } else { return spacers.subList(i, spacers.size()); } } } return Collections.emptySet(); } /** * Gets the spacers currently rendered in the DOM. * * @return an unmodifiable (but live) collection of the spacers * currently in the DOM */ public Collection getSpacersInDom() { return Collections .unmodifiableCollection(rowIndexToSpacer.values()); } /** * Gets the amount of pixels occupied by spacers between two pixel * points. *

* In this method, the {@link SpacerInclusionStrategy} has the following * meaning when a spacer lies in the middle of either pixel argument: *

*
{@link SpacerInclusionStrategy#COMPLETE COMPLETE} *
take the entire spacer into account *
{@link SpacerInclusionStrategy#PARTIAL PARTIAL} *
take only the visible area into account *
{@link SpacerInclusionStrategy#NONE NONE} *
ignore that spacer *
* * @param rangeTop * the top pixel point * @param topInclusion * the inclusion strategy regarding {@code rangeTop}. * @param rangeBottom * the bottom pixel point * @param bottomInclusion * the inclusion strategy regarding {@code rangeBottom}. * @return the pixels occupied by spacers between {@code rangeTop} and * {@code rangeBottom} */ public double getSpacerHeightsSumBetweenPx(double rangeTop, SpacerInclusionStrategy topInclusion, double rangeBottom, SpacerInclusionStrategy bottomInclusion) { assert rangeTop <= rangeBottom : "rangeTop must be less than rangeBottom"; double heights = 0; /* * TODO [[optimize]]: this might be somewhat inefficient (due to * iterator-based scanning, instead of using the treemap's search * functionalities). But it should be easy to write, read, verify * and maintain. */ for (SpacerImpl spacer : rowIndexToSpacer.values()) { double top = spacer.getTop(); double height = spacer.getHeight(); double bottom = top + height; /* * If we happen to implement a DoubleRange (in addition to the * int-based Range) at some point, the following logic should * probably be converted into using the * Range.partitionWith-equivalent. */ boolean topIsAboveRange = top < rangeTop; boolean topIsInRange = rangeTop <= top && top <= rangeBottom; boolean topIsBelowRange = rangeBottom < top; boolean bottomIsAboveRange = bottom < rangeTop; boolean bottomIsInRange = rangeTop <= bottom && bottom <= rangeBottom; boolean bottomIsBelowRange = rangeBottom < bottom; assert topIsAboveRange ^ topIsBelowRange ^ topIsInRange : "Bad top logic"; assert bottomIsAboveRange ^ bottomIsBelowRange ^ bottomIsInRange : "Bad bottom logic"; if (bottomIsAboveRange) { continue; } else if (topIsBelowRange) { return heights; } else if (topIsAboveRange && bottomIsInRange) { switch (topInclusion) { case PARTIAL: heights += bottom - rangeTop; break; case COMPLETE: heights += height; break; default: break; } } else if (topIsAboveRange && bottomIsBelowRange) { /* * Here we arbitrarily decide that the top inclusion will * have the honor of overriding the bottom inclusion if * happens to be a conflict of interests. */ switch (topInclusion) { case NONE: return 0; case COMPLETE: return height; case PARTIAL: return rangeBottom - rangeTop; default: throw new IllegalArgumentException( "Unexpected inclusion state :" + topInclusion); } } else if (topIsInRange && bottomIsInRange) { heights += height; } else if (topIsInRange && bottomIsBelowRange) { switch (bottomInclusion) { case PARTIAL: heights += rangeBottom - top; break; case COMPLETE: heights += height; break; default: break; } return heights; } else { assert false : "Unnaccounted-for situation"; } } return heights; } /** * Gets the amount of pixels occupied by spacers from the top until a * certain spot from the top of the body. * * @param px * pixels counted from the top * @return the pixels occupied by spacers up until {@code px} */ public double getSpacerHeightsSumUntilPx(double px) { return getSpacerHeightsSumBetweenPx(0, SpacerInclusionStrategy.PARTIAL, px, SpacerInclusionStrategy.PARTIAL); } /** * Gets the amount of pixels occupied by spacers until a logical row * index. * * @param logicalIndex * a logical row index * @return the pixels occupied by spacers up until {@code logicalIndex} */ @SuppressWarnings("boxing") public double getSpacerHeightsSumUntilIndex(int logicalIndex) { return getHeights( rowIndexToSpacer.headMap(logicalIndex, false).values()); } private double getHeights(Collection spacers) { double heights = 0; for (SpacerImpl spacer : spacers) { heights += spacer.getHeight(); } return heights; } /** * Gets the height of the spacer for a row index. * * @param rowIndex * the index of the row where the spacer should be * @return the height of the spacer at index {@code rowIndex}, or 0 if * there is no spacer there */ public double getSpacerHeight(int rowIndex) { SpacerImpl spacer = getSpacer(rowIndex); if (spacer != null) { return spacer.getHeight(); } else { return 0; } } private boolean spacerExists(int rowIndex) { return rowIndexToSpacer.containsKey(Integer.valueOf(rowIndex)); } @SuppressWarnings("boxing") private void insertNewSpacer(int rowIndex, double height) { if (spacerScrollerRegistration == null) { spacerScrollerRegistration = addScrollHandler(spacerScroller); } final SpacerImpl spacer = new SpacerImpl(rowIndex); rowIndexToSpacer.put(rowIndex, spacer); // set the position before adding it to DOM positions.set(spacer.getRootElement(), getScrollLeft(), calculateSpacerTop(rowIndex)); TableRowElement spacerRoot = spacer.getRootElement(); spacerRoot.getStyle() .setWidth(columnConfiguration.calculateRowWidth(), Unit.PX); body.getElement().appendChild(spacerRoot); spacer.setupDom(height); // set the deco position, requires that spacer is in the DOM positions.set(spacer.getDecoElement(), 0, spacer.getTop() - spacer.getSpacerDecoTopOffset()); spacerDecoContainer.appendChild(spacer.getDecoElement()); if (spacerDecoContainer.getParentElement() == null) { getElement().appendChild(spacerDecoContainer); // calculate the spacer deco width, it won't change spacerDecoWidth = WidgetUtil .getRequiredWidthBoundingClientRectDouble( spacer.getDecoElement()); } initSpacerContent(spacer); body.sortDomElements(); } private void updateExistingSpacer(int rowIndex, double newHeight) { getSpacer(rowIndex).setHeight(newHeight); } public SpacerImpl getSpacer(int rowIndex) { return rowIndexToSpacer.get(Integer.valueOf(rowIndex)); } private void removeSpacer(int rowIndex) { removeSpacers(Range.withOnly(rowIndex)); } public void setStylePrimaryName(String style) { for (SpacerImpl spacer : rowIndexToSpacer.values()) { spacer.setStylePrimaryName(style); } } public void setSpacerUpdater(SpacerUpdater spacerUpdater) throws IllegalArgumentException { if (spacerUpdater == null) { throw new IllegalArgumentException( "spacer updater cannot be null"); } destroySpacerContent(rowIndexToSpacer.values()); this.spacerUpdater = spacerUpdater; initSpacerContent(rowIndexToSpacer.values()); } public SpacerUpdater getSpacerUpdater() { return spacerUpdater; } private void destroySpacerContent(Iterable spacers) { for (SpacerImpl spacer : spacers) { destroySpacerContent(spacer); } } private void destroySpacerContent(SpacerImpl spacer) { assert getElement().isOrHasChild(spacer .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; assert getElement().isOrHasChild(spacer .getElement()) : "Spacer element somehow got detached from Escalator before detaching"; spacerUpdater.destroy(spacer); assert getElement().isOrHasChild(spacer .getRootElement()) : "Spacer's root element somehow got detached from Escalator before detaching"; assert getElement().isOrHasChild(spacer .getElement()) : "Spacer element somehow got detached from Escalator before detaching"; } private void initSpacerContent(Iterable spacers) { for (SpacerImpl spacer : spacers) { initSpacerContent(spacer); } } private void initSpacerContent(SpacerImpl spacer) { assert getElement().isOrHasChild(spacer .getRootElement()) : "Spacer's root element somehow got detached from Escalator before attaching"; assert getElement().isOrHasChild(spacer .getElement()) : "Spacer element somehow got detached from Escalator before attaching"; spacerUpdater.init(spacer); assert getElement().isOrHasChild(spacer .getRootElement()) : "Spacer's root element somehow got detached from Escalator during attaching"; assert getElement().isOrHasChild(spacer .getElement()) : "Spacer element somehow got detached from Escalator during attaching"; spacer.updateVisibility(); } public String getSubPartName(Element subElement) { for (SpacerImpl spacer : rowIndexToSpacer.values()) { if (spacer.getRootElement().isOrHasChild(subElement)) { return "spacer[" + spacer.getRow() + "]"; } } return null; } public Element getSubPartElement(int index) { SpacerImpl spacer = rowIndexToSpacer.get(Integer.valueOf(index)); if (spacer != null) { return spacer.getElement(); } else { return null; } } private double calculateSpacerTop(int logicalIndex) { return body.getRowTop(logicalIndex) + body.getDefaultRowHeight(); } @SuppressWarnings("boxing") private void shiftSpacerPositionsAfterRow(int changedRowIndex, double diffPx) { for (SpacerImpl spacer : rowIndexToSpacer .tailMap(changedRowIndex, false).values()) { spacer.setPositionDiff(0, diffPx); } } /** * Shifts spacers at and after a specific row by an amount of rows. *

* This moves both their associated row index and also their visual * placement. *

* Note: This method does not check for the validity of any * arguments. * * @param index * the index of first row to move * @param numberOfRows * the number of rows to shift the spacers with. A positive * value is downwards, a negative value is upwards. */ public void shiftSpacersByRows(int index, int numberOfRows) { final double pxDiff = numberOfRows * body.getDefaultRowHeight(); for (SpacerContainer.SpacerImpl spacer : getSpacersForRowAndAfter( index)) { spacer.setPositionDiff(0, pxDiff); spacer.setRowIndex(spacer.getRow() + numberOfRows); } } private void updateSpacerDecosVisibility() { final Range visibleRowRange = getVisibleRowRange(); Collection visibleSpacers = rowIndexToSpacer .subMap(visibleRowRange.getStart() - 1, visibleRowRange.getEnd() + 1) .values(); if (!visibleSpacers.isEmpty()) { final double top = tableWrapper.getAbsoluteTop() + header.getHeightOfSection(); final double bottom = tableWrapper.getAbsoluteBottom() - footer.getHeightOfSection(); for (SpacerImpl spacer : visibleSpacers) { spacer.updateDecoClip(top, bottom, spacerDecoWidth); } } } } private class ElementPositionBookkeeper { /** * A map containing cached values of an element's current top position. */ private final Map elementTopPositionMap = new HashMap(); private final Map elementLeftPositionMap = new HashMap(); public void set(final Element e, final double x, final double y) { assert e != null : "Element was null"; position.set(e, x, y); elementTopPositionMap.put(e, Double.valueOf(y)); elementLeftPositionMap.put(e, Double.valueOf(x)); } public double getTop(final Element e) { Double top = elementTopPositionMap.get(e); if (top == null) { throw new IllegalArgumentException("Element " + e + " was not found in the position bookkeeping"); } return top.doubleValue(); } public double getLeft(final Element e) { Double left = elementLeftPositionMap.get(e); if (left == null) { throw new IllegalArgumentException("Element " + e + " was not found in the position bookkeeping"); } return left.doubleValue(); } public void remove(Element e) { elementTopPositionMap.remove(e); elementLeftPositionMap.remove(e); } } /** * Utility class for parsing and storing SubPart request string attributes * for Grid and Escalator. * * @since 7.5.0 */ public static class SubPartArguments { private String type; private int[] indices; private SubPartArguments(String type, int[] indices) { /* * The constructor is private so that no third party would by * mistake start using this parsing scheme, since it's not official * by TestBench (yet?). */ this.type = type; this.indices = indices; } public String getType() { return type; } public int getIndicesLength() { return indices.length; } public int getIndex(int i) { return indices[i]; } public int[] getIndices() { return Arrays.copyOf(indices, indices.length); } static SubPartArguments create(String subPart) { String[] splitArgs = subPart.split("\\["); String type = splitArgs[0]; int[] indices = new int[splitArgs.length - 1]; for (int i = 0; i < indices.length; ++i) { String tmp = splitArgs[i + 1]; indices[i] = Integer .parseInt(tmp.substring(0, tmp.indexOf("]", 1))); } return new SubPartArguments(type, indices); } } // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y /** * The solution to * |tan-1(x)|×(180/π) = 30 * . *

* This constant is placed in the Escalator class, instead of an inner * class, since even mathematical expressions aren't allowed in non-static * inner classes for constants. */ private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3); /** * The solution to * |tan-1(x)|×(180/π) = 40 * . *

* This constant is placed in the Escalator class, instead of an inner * class, since even mathematical expressions aren't allowed in non-static * inner classes for constants. */ private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9); private static final String DEFAULT_WIDTH = "500.0px"; private static final String DEFAULT_HEIGHT = "400.0px"; private FlyweightRow flyweightRow = new FlyweightRow(); /** The {@code } tag. */ private final TableSectionElement headElem = TableSectionElement .as(DOM.createTHead()); /** The {@code } tag. */ private final TableSectionElement bodyElem = TableSectionElement .as(DOM.createTBody()); /** The {@code } tag. */ private final TableSectionElement footElem = TableSectionElement .as(DOM.createTFoot()); /** * TODO: investigate whether this field is now unnecessary, as * {@link ScrollbarBundle} now caches its values. * * @deprecated maybe... */ @Deprecated private double tBodyScrollTop = 0; /** * TODO: investigate whether this field is now unnecessary, as * {@link ScrollbarBundle} now caches its values. * * @deprecated maybe... */ @Deprecated private double tBodyScrollLeft = 0; private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle(); private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle(); private final HeaderRowContainer header = new HeaderRowContainer(headElem); private final BodyRowContainerImpl body = new BodyRowContainerImpl( bodyElem); private final FooterRowContainer footer = new FooterRowContainer(footElem); /** * Flag for keeping track of {@link RowHeightChangedEvent}s */ private boolean rowHeightChangedEventFired = false; private final Scroller scroller = new Scroller(); private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl(); private final DivElement tableWrapper; private final DivElement horizontalScrollbarDeco = DivElement .as(DOM.createDiv()); private final DivElement headerDeco = DivElement.as(DOM.createDiv()); private final DivElement footerDeco = DivElement.as(DOM.createDiv()); private final DivElement spacerDecoContainer = DivElement .as(DOM.createDiv()); private PositionFunction position; /** The cached width of the escalator, in pixels. */ private double widthOfEscalator = 0; /** The cached height of the escalator, in pixels. */ private double heightOfEscalator = 0; /** The height of Escalator in terms of body rows. */ private double heightByRows = 10.0d; /** The height of Escalator, as defined by {@link #setHeight(String)} */ private String heightByCss = ""; private HeightMode heightMode = HeightMode.CSS; private boolean layoutIsScheduled = false; private ScheduledCommand layoutCommand = new ScheduledCommand() { @Override public void execute() { recalculateElementSizes(); layoutIsScheduled = false; } }; private final ElementPositionBookkeeper positions = new ElementPositionBookkeeper(); /** * Creates a new Escalator widget instance. */ public Escalator() { detectAndApplyPositionFunction(); getLogger().info("Using " + position.getClass().getSimpleName() + " for position"); final Element root = DOM.createDiv(); setElement(root); setupScrollbars(root); tableWrapper = DivElement.as(DOM.createDiv()); root.appendChild(tableWrapper); final Element table = DOM.createTable(); tableWrapper.appendChild(table); table.appendChild(headElem); table.appendChild(bodyElem); table.appendChild(footElem); Style hCornerStyle = headerDeco.getStyle(); hCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(), Unit.PX); hCornerStyle.setDisplay(Display.NONE); root.appendChild(headerDeco); Style fCornerStyle = footerDeco.getStyle(); fCornerStyle.setWidth(verticalScrollbar.getScrollbarThickness(), Unit.PX); fCornerStyle.setDisplay(Display.NONE); root.appendChild(footerDeco); Style hWrapperStyle = horizontalScrollbarDeco.getStyle(); hWrapperStyle.setDisplay(Display.NONE); hWrapperStyle.setHeight(horizontalScrollbar.getScrollbarThickness(), Unit.PX); root.appendChild(horizontalScrollbarDeco); setStylePrimaryName("v-escalator"); spacerDecoContainer.setAttribute("aria-hidden", "true"); // init default dimensions setHeight(null); setWidth(null); publishJSHelpers(root); } private int getBodyRowCount() { return getBody().getRowCount(); } private native void publishJSHelpers(Element root) /*-{ var self = this; root.getBodyRowCount = $entry(function () { return self.@Escalator::getBodyRowCount()(); }); }-*/; private void setupScrollbars(final Element root) { ScrollHandler scrollHandler = new ScrollHandler() { @Override public void onScroll(ScrollEvent event) { scroller.onScroll(); fireEvent(new ScrollEvent()); } }; int scrollbarThickness = WidgetUtil.getNativeScrollbarSize(); if (BrowserInfo.get().isIE()) { /* * IE refuses to scroll properly if the DIV isn't at least one pixel * larger than the scrollbar controls themselves. */ scrollbarThickness += 1; } root.appendChild(verticalScrollbar.getElement()); verticalScrollbar.addScrollHandler(scrollHandler); verticalScrollbar.setScrollbarThickness(scrollbarThickness); root.appendChild(horizontalScrollbar.getElement()); horizontalScrollbar.addScrollHandler(scrollHandler); horizontalScrollbar.setScrollbarThickness(scrollbarThickness); horizontalScrollbar .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() { private boolean queued = false; @Override public void visibilityChanged( ScrollbarBundle.VisibilityChangeEvent event) { if (queued) { return; } queued = true; /* * We either lost or gained a scrollbar. In any case, we * need to change the height, if it's defined by rows. */ Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { applyHeightByRows(); queued = false; } }); } }); /* * Because of all the IE hacks we've done above, we now have scrollbars * hiding underneath a lot of DOM elements. * * This leads to problems with OSX (and many touch-only devices) when * scrollbars are only shown when scrolling, as the scrollbar elements * are hidden underneath everything. We trust that the scrollbars behave * properly in these situations and simply pop them out with a bit of * z-indexing. */ if (WidgetUtil.getNativeScrollbarSize() == 0) { verticalScrollbar.getElement().getStyle().setZIndex(90); horizontalScrollbar.getElement().getStyle().setZIndex(90); } } @Override protected void onLoad() { super.onLoad(); header.autodetectRowHeightLater(); body.autodetectRowHeightLater(); footer.autodetectRowHeightLater(); header.paintInsertRows(0, header.getRowCount()); footer.paintInsertRows(0, footer.getRowCount()); // recalculateElementSizes(); Scheduler.get().scheduleDeferred(new Command() { @Override public void execute() { /* * Not a faintest idea why we have to defer this call, but * unless it is deferred, the size of the escalator will be 0x0 * after it is first detached and then reattached to the DOM. * This only applies to a bare Escalator; inside a Grid * everything works fine either way. * * The three autodetectRowHeightLater calls above seem obvious * suspects at first. However, they don't seem to have anything * to do with the issue, as they are no-ops in the * detach-reattach case. */ recalculateElementSizes(); } }); /* * Note: There's no need to explicitly insert rows into the body. * * recalculateElementSizes will recalculate the height of the body. This * has the side-effect that as the body's size grows bigger (i.e. from 0 * to its actual height), more escalator rows are populated. Those * escalator rows are then immediately rendered. This, in effect, is the * same thing as inserting those rows. * * In fact, having an extra paintInsertRows here would lead to duplicate * rows. */ boolean columnsChanged = false; for (ColumnConfigurationImpl.Column column : columnConfiguration.columns) { boolean columnChanged = column.measureAndSetWidthIfNeeded(); if (columnChanged) { columnsChanged = true; } } if (columnsChanged) { header.reapplyColumnWidths(); body.reapplyColumnWidths(); footer.reapplyColumnWidths(); } verticalScrollbar.onLoad(); horizontalScrollbar.onLoad(); scroller.attachScrollListener(verticalScrollbar.getElement()); scroller.attachScrollListener(horizontalScrollbar.getElement()); scroller.attachMousewheelListener(getElement()); if (isCurrentBrowserIE11OrEdge()) { // Touch listeners doesn't work for IE11 and Edge (#18737) scroller.attachPointerEventListeners(getElement()); } else { scroller.attachTouchListeners(getElement()); } } @Override protected void onUnload() { scroller.detachScrollListener(verticalScrollbar.getElement()); scroller.detachScrollListener(horizontalScrollbar.getElement()); scroller.detachMousewheelListener(getElement()); if (isCurrentBrowserIE11OrEdge()) { // Touch listeners doesn't work for IE11 and Edge (#18737) scroller.detachPointerEventListeners(getElement()); } else { scroller.detachTouchListeners(getElement()); } /* * We can call paintRemoveRows here, because static ranges are simple to * remove. */ header.paintRemoveRows(0, header.getRowCount()); footer.paintRemoveRows(0, footer.getRowCount()); /* * We can't call body.paintRemoveRows since it relies on rowCount to be * updated correctly. Since it isn't, we'll simply and brutally rip out * the DOM elements (in an elegant way, of course). */ int rowsToRemove = body.getDomRowCount(); for (int i = 0; i < rowsToRemove; i++) { int index = rowsToRemove - i - 1; TableRowElement tr = bodyElem.getRows().getItem(index); body.paintRemoveRow(tr, index); positions.remove(tr); } body.visualRowOrder.clear(); body.setTopRowLogicalIndex(0); super.onUnload(); } private void detectAndApplyPositionFunction() { /* * firefox has a bug in its translate operation, showing white space * when adjusting the scrollbar in BodyRowContainer.paintInsertRows */ if (Window.Navigator.getUserAgent().contains("Firefox")) { position = new AbsolutePosition(); return; } final Style docStyle = Document.get().getBody().getStyle(); if (hasProperty(docStyle, "transform")) { if (hasProperty(docStyle, "transformStyle")) { position = new Translate3DPosition(); } else { position = new TranslatePosition(); } } else if (hasProperty(docStyle, "webkitTransform")) { position = new WebkitTranslate3DPosition(); } else { position = new AbsolutePosition(); } } private Logger getLogger() { return Logger.getLogger(getClass().getName()); } private static native boolean hasProperty(Style style, String name) /*-{ return style[name] !== undefined; }-*/; /** * Check whether there are both columns and any row data (for either * headers, body or footer). * * @return true if header, body or footer has rows and there * are columns */ private boolean hasColumnAndRowData() { return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer.getRowCount() > 0) && columnConfiguration.getColumnCount() > 0; } /** * Check whether there are any cells in the DOM. * * @return true if header, body or footer has any child * elements */ private boolean hasSomethingInDom() { return headElem.hasChildNodes() || bodyElem.hasChildNodes() || footElem.hasChildNodes(); } /** * Returns the row container for the header in this Escalator. * * @return the header. Never null */ public RowContainer getHeader() { return header; } /** * Returns the row container for the body in this Escalator. * * @return the body. Never null */ public BodyRowContainer getBody() { return body; } /** * Returns the row container for the footer in this Escalator. * * @return the footer. Never null */ public RowContainer getFooter() { return footer; } /** * Returns the configuration object for the columns in this Escalator. * * @return the configuration object for the columns in this Escalator. Never * null */ public ColumnConfiguration getColumnConfiguration() { return columnConfiguration; } @Override public void setWidth(final String width) { if (width != null && !width.isEmpty()) { super.setWidth(width); } else { super.setWidth(DEFAULT_WIDTH); } recalculateElementSizes(); } /** * {@inheritDoc} *

* If Escalator is currently not in {@link HeightMode#CSS}, the given value * is remembered, and applied once the mode is applied. * * @see #setHeightMode(HeightMode) */ @Override public void setHeight(String height) { /* * TODO remove method once RequiresResize and the Vaadin layoutmanager * listening mechanisms are implemented */ if (height != null && !height.isEmpty()) { heightByCss = height; } else { if (getHeightMode() == HeightMode.UNDEFINED) { heightByRows = body.getRowCount(); applyHeightByRows(); return; } else { heightByCss = DEFAULT_HEIGHT; } } if (getHeightMode() == HeightMode.CSS) { setHeightInternal(height); } } private void setHeightInternal(final String height) { final int escalatorRowsBefore = body.visualRowOrder.size(); if (height != null && !height.isEmpty()) { super.setHeight(height); } else { if (getHeightMode() == HeightMode.UNDEFINED) { int newHeightByRows = body.getRowCount(); if (heightByRows != newHeightByRows) { heightByRows = newHeightByRows; applyHeightByRows(); } return; } else { super.setHeight(DEFAULT_HEIGHT); } } recalculateElementSizes(); if (escalatorRowsBefore != body.visualRowOrder.size()) { fireRowVisibilityChangeEvent(); } } /** * Returns the vertical scroll offset. Note that this is not necessarily the * same as the {@code scrollTop} attribute in the DOM. * * @return the logical vertical scroll offset */ public double getScrollTop() { return verticalScrollbar.getScrollPos(); } /** * Sets the vertical scroll offset. Note that this will not necessarily * become the same as the {@code scrollTop} attribute in the DOM. * * @param scrollTop * the number of pixels to scroll vertically */ public void setScrollTop(final double scrollTop) { verticalScrollbar.setScrollPos(scrollTop); } /** * Returns the logical horizontal scroll offset. Note that this is not * necessarily the same as the {@code scrollLeft} attribute in the DOM. * * @return the logical horizontal scroll offset */ public double getScrollLeft() { return horizontalScrollbar.getScrollPos(); } /** * Sets the logical horizontal scroll offset. Note that will not necessarily * become the same as the {@code scrollLeft} attribute in the DOM. * * @param scrollLeft * the number of pixels to scroll horizontally */ public void setScrollLeft(final double scrollLeft) { horizontalScrollbar.setScrollPos(scrollLeft); } /** * Returns the scroll width for the escalator. Note that this is not * necessary the same as {@code Element.scrollWidth} in the DOM. * * @since 7.5.0 * @return the scroll width in pixels */ public double getScrollWidth() { return horizontalScrollbar.getScrollSize(); } /** * Returns the scroll height for the escalator. Note that this is not * necessary the same as {@code Element.scrollHeight} in the DOM. * * @since 7.5.0 * @return the scroll height in pixels */ public double getScrollHeight() { return verticalScrollbar.getScrollSize(); } /** * Scrolls the body horizontally so that the column at the given index is * visible and there is at least {@code padding} pixels in the direction of * the given scroll destination. * * @param columnIndex * the index of the column to scroll to * @param destination * where the column should be aligned visually after scrolling * @param padding * the number pixels to place between the scrolled-to column and * the viewport edge. * @throws IndexOutOfBoundsException * if {@code columnIndex} is not a valid index for an existing * column * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} * and padding is nonzero; or if the indicated column is frozen; * or if {@code destination == null} */ public void scrollToColumn(final int columnIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { validateScrollDestination(destination, padding); verifyValidColumnIndex(columnIndex); if (columnIndex < columnConfiguration.frozenColumns) { throw new IllegalArgumentException( "The given column index " + columnIndex + " is frozen."); } scroller.scrollToColumn(columnIndex, destination, padding); } private void verifyValidColumnIndex(final int columnIndex) throws IndexOutOfBoundsException { if (columnIndex < 0 || columnIndex >= columnConfiguration.getColumnCount()) { throw new IndexOutOfBoundsException("The given column index " + columnIndex + " does not exist."); } } /** * Scrolls the body vertically so that the row at the given index is visible * and there is at least {@literal padding} pixels to the given scroll * destination. * * @param rowIndex * the index of the logical row to scroll to * @param destination * where the row should be aligned visually after scrolling * @param padding * the number pixels to place between the scrolled-to row and the * viewport edge. * @throws IndexOutOfBoundsException * if {@code rowIndex} is not a valid index for an existing row * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} * and padding is nonzero; or if {@code destination == null} * @see #scrollToRowAndSpacer(int, ScrollDestination, int) * @see #scrollToSpacer(int, ScrollDestination, int) */ public void scrollToRow(final int rowIndex, final ScrollDestination destination, final int padding) throws IndexOutOfBoundsException, IllegalArgumentException { Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { validateScrollDestination(destination, padding); verifyValidRowIndex(rowIndex); scroller.scrollToRow(rowIndex, destination, padding); } }); } private void verifyValidRowIndex(final int rowIndex) { if (rowIndex < 0 || rowIndex >= body.getRowCount()) { throw new IndexOutOfBoundsException( "The given row index " + rowIndex + " does not exist."); } } /** * Scrolls the body vertically so that the spacer at the given row index is * visible and there is at least {@literal padding} pixesl to the given * scroll destination. * * @since 7.5.0 * @param spacerIndex * the row index of the spacer to scroll to * @param destination * where the spacer should be aligned visually after scrolling * @param padding * the number of pixels to place between the scrolled-to spacer * and the viewport edge * @throws IllegalArgumentException * if {@code spacerIndex} is not an opened spacer; or if * {@code destination} is {@link ScrollDestination#MIDDLE} and * padding is nonzero; or if {@code destination == null} * @see #scrollToRow(int, ScrollDestination, int) * @see #scrollToRowAndSpacer(int, ScrollDestination, int) */ public void scrollToSpacer(final int spacerIndex, ScrollDestination destination, final int padding) throws IllegalArgumentException { validateScrollDestination(destination, padding); body.scrollToSpacer(spacerIndex, destination, padding); } /** * Scrolls vertically to a row and the spacer below it. *

* If a spacer is not open at that index, this method behaves like * {@link #scrollToRow(int, ScrollDestination, int)} * * @since 7.5.0 * @param rowIndex * the index of the logical row to scroll to. -1 takes the * topmost spacer into account as well. * @param destination * where the row should be aligned visually after scrolling * @param padding * the number pixels to place between the scrolled-to row and the * viewport edge. * @see #scrollToRow(int, ScrollDestination, int) * @see #scrollToSpacer(int, ScrollDestination, int) * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} * and {@code padding} is not zero; or if {@code rowIndex} is * not a valid row index, or -1; or if * {@code destination == null}; or if {@code rowIndex == -1} and * there is no spacer open at that index. */ public void scrollToRowAndSpacer(final int rowIndex, final ScrollDestination destination, final int padding) throws IllegalArgumentException { Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { validateScrollDestination(destination, padding); if (rowIndex != -1) { verifyValidRowIndex(rowIndex); } // row range final Range rowRange; if (rowIndex != -1) { int rowTop = (int) Math.floor(body.getRowTop(rowIndex)); int rowHeight = (int) Math.ceil(body.getDefaultRowHeight()); rowRange = Range.withLength(rowTop, rowHeight); } else { rowRange = Range.withLength(0, 0); } // get spacer final SpacerContainer.SpacerImpl spacer = body.spacerContainer .getSpacer(rowIndex); if (rowIndex == -1 && spacer == null) { throw new IllegalArgumentException( "Cannot scroll to row index " + "-1, as there is no spacer open at that index."); } // make into target range final Range targetRange; if (spacer != null) { final int spacerTop = (int) Math.floor(spacer.getTop()); final int spacerHeight = (int) Math .ceil(spacer.getHeight()); Range spacerRange = Range.withLength(spacerTop, spacerHeight); targetRange = rowRange.combineWith(spacerRange); } else { targetRange = rowRange; } // get params int targetStart = targetRange.getStart(); int targetEnd = targetRange.getEnd(); double viewportStart = getScrollTop(); double viewportEnd = viewportStart + body.getHeightOfSection(); double scrollPos = getScrollPos(destination, targetStart, targetEnd, viewportStart, viewportEnd, padding); setScrollTop(scrollPos); } }); } private static void validateScrollDestination( final ScrollDestination destination, final int padding) { if (destination == null) { throw new IllegalArgumentException("Destination cannot be null"); } if (destination == ScrollDestination.MIDDLE && padding != 0) { throw new IllegalArgumentException( "You cannot have a padding with a MIDDLE destination"); } } /** * Recalculates the dimensions for all elements that require manual * calculations. Also updates the dimension caches. *

* Note: This method has the side-effect * automatically makes sure that an appropriate amount of escalator rows are * present. So, if the body area grows, more escalator rows might be * inserted. Conversely, if the body area shrinks, * escalator rows might be removed. */ private void recalculateElementSizes() { if (!isAttached()) { return; } Profiler.enter("Escalator.recalculateElementSizes"); widthOfEscalator = Math.max(0, WidgetUtil .getRequiredWidthBoundingClientRectDouble(getElement())); heightOfEscalator = Math.max(0, WidgetUtil .getRequiredHeightBoundingClientRectDouble(getElement())); header.recalculateSectionHeight(); body.recalculateSectionHeight(); footer.recalculateSectionHeight(); scroller.recalculateScrollbarsForVirtualViewport(); body.verifyEscalatorCount(); body.reapplySpacerWidths(); Profiler.leave("Escalator.recalculateElementSizes"); } /** * Snap deltas of x and y to the major four axes (up, down, left, right) * with a threshold of a number of degrees from those axes. * * @param deltaX * the delta in the x axis * @param deltaY * the delta in the y axis * @param thresholdRatio * the threshold in ratio (0..1) between x and y for when to snap * @return a two-element array: [snappedX, snappedY] */ private static double[] snapDeltas(final double deltaX, final double deltaY, final double thresholdRatio) { final double[] array = new double[2]; if (deltaX != 0 && deltaY != 0) { final double aDeltaX = Math.abs(deltaX); final double aDeltaY = Math.abs(deltaY); final double yRatio = aDeltaY / aDeltaX; final double xRatio = aDeltaX / aDeltaY; array[0] = (xRatio < thresholdRatio) ? 0 : deltaX; array[1] = (yRatio < thresholdRatio) ? 0 : deltaY; } else { array[0] = deltaX; array[1] = deltaY; } return array; } /** * Adds an event handler that gets notified when the range of visible rows * changes e.g. because of scrolling, row resizing or spacers * appearing/disappearing. * * @param rowVisibilityChangeHandler * the event handler * @return a handler registration for the added handler */ public HandlerRegistration addRowVisibilityChangeHandler( RowVisibilityChangeHandler rowVisibilityChangeHandler) { return addHandler(rowVisibilityChangeHandler, RowVisibilityChangeEvent.TYPE); } private void fireRowVisibilityChangeEvent() { if (!body.visualRowOrder.isEmpty()) { int visibleRangeStart = body .getLogicalRowIndex(body.visualRowOrder.getFirst()); int visibleRangeEnd = body .getLogicalRowIndex(body.visualRowOrder.getLast()) + 1; int visibleRowCount = visibleRangeEnd - visibleRangeStart; fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, visibleRowCount)); } else { fireEvent(new RowVisibilityChangeEvent(0, 0)); } } /** * Gets the logical index range of currently visible rows. * * @return logical index range of visible rows */ public Range getVisibleRowRange() { if (!body.visualRowOrder.isEmpty()) { return Range.withLength(body.getTopRowLogicalIndex(), body.visualRowOrder.size()); } else { return Range.withLength(0, 0); } } /** * Returns the widget from a cell node or null if there is no * widget in the cell * * @param cellNode * The cell node */ static Widget getWidgetFromCell(Node cellNode) { Node possibleWidgetNode = cellNode.getFirstChild(); if (possibleWidgetNode != null && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) { @SuppressWarnings("deprecation") com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode .cast(); Widget w = WidgetUtil.findWidget(castElement, null); // Ensure findWidget did not traverse past the cell element in the // DOM hierarchy if (cellNode.isOrHasChild(w.getElement())) { return w; } } return null; } @Override public void setStylePrimaryName(String style) { super.setStylePrimaryName(style); verticalScrollbar.setStylePrimaryName(style); horizontalScrollbar.setStylePrimaryName(style); UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper"); UIObject.setStylePrimaryName(headerDeco, style + "-header-deco"); UIObject.setStylePrimaryName(footerDeco, style + "-footer-deco"); UIObject.setStylePrimaryName(horizontalScrollbarDeco, style + "-horizontal-scrollbar-deco"); UIObject.setStylePrimaryName(spacerDecoContainer, style + "-spacer-deco-container"); header.setStylePrimaryName(style); body.setStylePrimaryName(style); footer.setStylePrimaryName(style); } /** * Sets the number of rows that should be visible in Escalator's body, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. *

* If Escalator is currently not in {@link HeightMode#ROW}, the given value * is remembered, and applied once the mode is applied. * * @param rows * the number of rows that should be visible in Escalator's body * @throws IllegalArgumentException * if {@code rows} is ≤ 0, {@link Double#isInifinite(double) * infinite} or {@link Double#isNaN(double) NaN}. * @see #setHeightMode(HeightMode) */ public void setHeightByRows(double rows) throws IllegalArgumentException { if (rows <= 0) { throw new IllegalArgumentException( "The number of rows must be a positive number."); } else if (Double.isInfinite(rows)) { throw new IllegalArgumentException( "The number of rows must be finite."); } else if (Double.isNaN(rows)) { throw new IllegalArgumentException("The number must not be NaN."); } heightByRows = rows; applyHeightByRows(); } /** * Gets the amount of rows in Escalator's body that are shown, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. *

* By default, it is 10. * * @return the amount of rows that are being shown in Escalator's body * @see #setHeightByRows(double) */ public double getHeightByRows() { return heightByRows; } /** * Reapplies the row-based height of the Grid, if Grid currently should * define its height that way. */ private void applyHeightByRows() { if (heightMode != HeightMode.ROW && heightMode != HeightMode.UNDEFINED) { return; } double headerHeight = header.getHeightOfSection(); double footerHeight = footer.getHeightOfSection(); double bodyHeight = body.getDefaultRowHeight() * heightByRows; double scrollbar = horizontalScrollbar.showsScrollHandle() ? horizontalScrollbar.getScrollbarThickness() : 0; double spacerHeight = 0; // ignored if HeightMode.ROW if (heightMode == HeightMode.UNDEFINED) { spacerHeight = body.spacerContainer.getSpacerHeightsSum(); } double totalHeight = headerHeight + bodyHeight + spacerHeight + scrollbar + footerHeight; setHeightInternal(totalHeight + "px"); } /** * Defines the mode in which the Escalator widget's height is calculated. *

* If {@link HeightMode#CSS} is given, Escalator will respect the values * given via {@link #setHeight(String)}, and behave as a traditional Widget. *

* If {@link HeightMode#ROW} is given, Escalator will make sure that the * {@link #getBody() body} will display as many rows as * {@link #getHeightByRows()} defines. Note: If headers/footers are * inserted or removed, the widget will resize itself to still display the * required amount of rows in its body. It also takes the horizontal * scrollbar into account. * * @param heightMode * the mode in to which Escalator should be set */ public void setHeightMode(HeightMode heightMode) { /* * This method is a workaround for the fact that Vaadin re-applies * widget dimensions (height/width) on each state change event. The * original design was to have setHeight an setHeightByRow be equals, * and whichever was called the latest was considered in effect. * * But, because of Vaadin always calling setHeight on the widget, this * approach doesn't work. */ if (heightMode != this.heightMode) { this.heightMode = heightMode; switch (this.heightMode) { case CSS: setHeight(heightByCss); break; case ROW: setHeightByRows(heightByRows); break; case UNDEFINED: setHeightByRows(body.getRowCount()); break; default: throw new IllegalStateException("Unimplemented feature " + "- unknown HeightMode: " + this.heightMode); } } } /** * Returns the current {@link HeightMode} the Escalator is in. *

* Defaults to {@link HeightMode#CSS}. * * @return the current HeightMode */ public HeightMode getHeightMode() { return heightMode; } /** * Returns the {@link RowContainer} which contains the element. * * @param element * the element to check for * @return the container the element is in or null if element * is not present in any container. */ public RowContainer findRowContainer(Element element) { if (getHeader().getElement() != element && getHeader().getElement().isOrHasChild(element)) { return getHeader(); } else if (getBody().getElement() != element && getBody().getElement().isOrHasChild(element)) { return getBody(); } else if (getFooter().getElement() != element && getFooter().getElement().isOrHasChild(element)) { return getFooter(); } return null; } /** * Sets whether a scroll direction is locked or not. *

* If a direction is locked, the escalator will refuse to scroll in that * direction. * * @param direction * the orientation of the scroll to set the lock status * @param locked * true to lock, false to unlock */ public void setScrollLocked(ScrollbarBundle.Direction direction, boolean locked) { switch (direction) { case HORIZONTAL: horizontalScrollbar.setLocked(locked); break; case VERTICAL: verticalScrollbar.setLocked(locked); break; default: throw new UnsupportedOperationException( "Unexpected value: " + direction); } } /** * Checks whether or not an direction is locked for scrolling. * * @param direction * the direction of the scroll of which to check the lock status * @return true if the direction is locked */ public boolean isScrollLocked(ScrollbarBundle.Direction direction) { switch (direction) { case HORIZONTAL: return horizontalScrollbar.isLocked(); case VERTICAL: return verticalScrollbar.isLocked(); default: throw new UnsupportedOperationException( "Unexpected value: " + direction); } } /** * Adds a scroll handler to this escalator. * * @param handler * the scroll handler to add * @return a handler registration for the registered scroll handler */ public HandlerRegistration addScrollHandler(ScrollHandler handler) { return addHandler(handler, ScrollEvent.TYPE); } @Override public boolean isWorkPending() { return body.domSorter.waiting || verticalScrollbar.isWorkPending() || horizontalScrollbar.isWorkPending() || layoutIsScheduled; } @Override public void onResize() { if (isAttached() && !layoutIsScheduled) { layoutIsScheduled = true; Scheduler.get().scheduleFinally(layoutCommand); } } /** * Gets the maximum number of body rows that can be visible on the screen at * once. * * @return the maximum capacity */ public int getMaxVisibleRowCount() { return body.getMaxVisibleRowCount(); } /** * Gets the escalator's inner width. This is the entire width in pixels, * without the vertical scrollbar. * * @return escalator's inner width */ public double getInnerWidth() { return WidgetUtil .getRequiredWidthBoundingClientRectDouble(tableWrapper); } /** * Resets all cached pixel sizes and reads new values from the DOM. This * methods should be used e.g. when styles affecting the dimensions of * elements in this escalator have been changed. */ public void resetSizesFromDom() { header.autodetectRowHeightNow(); body.autodetectRowHeightNow(); footer.autodetectRowHeightNow(); for (int i = 0; i < columnConfiguration.getColumnCount(); i++) { columnConfiguration.setColumnWidth(i, columnConfiguration.getColumnWidth(i)); } } private Range getViewportPixels() { int from = (int) Math.floor(verticalScrollbar.getScrollPos()); int to = (int) body.getHeightOfSection(); return Range.withLength(from, to); } @Override @SuppressWarnings("deprecation") public com.google.gwt.user.client.Element getSubPartElement( String subPart) { SubPartArguments args = SubPartArguments.create(subPart); Element tableStructureElement = getSubPartElementTableStructure(args); if (tableStructureElement != null) { return DOM.asOld(tableStructureElement); } Element spacerElement = getSubPartElementSpacer(args); if (spacerElement != null) { return DOM.asOld(spacerElement); } return null; } private Element getSubPartElementTableStructure(SubPartArguments args) { String type = args.getType(); int[] indices = args.getIndices(); // Get correct RowContainer for type from Escalator RowContainer container = null; if (type.equalsIgnoreCase("header")) { container = getHeader(); } else if (type.equalsIgnoreCase("cell")) { // If wanted row is not visible, we need to scroll there. Range visibleRowRange = getVisibleRowRange(); if (indices.length > 0) { // Contains a row number, ensure it is available and visible boolean rowInCache = visibleRowRange.contains(indices[0]); // Scrolling might be a no-op if row is already in the viewport scrollToRow(indices[0], ScrollDestination.ANY, 0); if (!rowInCache) { // Row was not in cache, scrolling caused lazy loading and // the caller needs to wait and call this method again to be // able to get the requested element return null; } } container = getBody(); } else if (type.equalsIgnoreCase("footer")) { container = getFooter(); } if (null != container) { if (indices.length == 0) { // No indexing. Just return the wanted container element return container.getElement(); } else { try { return getSubPart(container, indices); } catch (Exception e) { getLogger().log(Level.SEVERE, e.getMessage()); } } } return null; } private Element getSubPart(RowContainer container, int[] indices) { Element targetElement = container.getRowElement(indices[0]); // Scroll wanted column to view if able if (indices.length > 1 && targetElement != null) { if (getColumnConfiguration().getFrozenColumnCount() <= indices[1]) { scrollToColumn(indices[1], ScrollDestination.ANY, 0); } targetElement = getCellFromRow(TableRowElement.as(targetElement), indices[1]); for (int i = 2; i < indices.length && targetElement != null; ++i) { targetElement = (Element) targetElement.getChild(indices[i]); } } return targetElement; } private static Element getCellFromRow(TableRowElement rowElement, int index) { int childCount = rowElement.getCells().getLength(); if (index < 0 || index >= childCount) { return null; } TableCellElement currentCell = null; boolean indexInColspan = false; int i = 0; while (!indexInColspan) { currentCell = rowElement.getCells().getItem(i); // Calculate if this is the cell we are looking for int colSpan = currentCell.getColSpan(); indexInColspan = index < colSpan + i; // Increment by colspan to skip over hidden cells i += colSpan; } return currentCell; } private Element getSubPartElementSpacer(SubPartArguments args) { if ("spacer".equals(args.getType()) && args.getIndicesLength() == 1) { return body.spacerContainer.getSubPartElement(args.getIndex(0)); } else { return null; } } @Override @SuppressWarnings("deprecation") public String getSubPartName( com.google.gwt.user.client.Element subElement) { /* * The spacer check needs to be before table structure check, because * (for now) the table structure will take spacer elements into account * as well, when it shouldn't. */ String spacer = getSubPartNameSpacer(subElement); if (spacer != null) { return spacer; } String tableStructure = getSubPartNameTableStructure(subElement); if (tableStructure != null) { return tableStructure; } return null; } private String getSubPartNameTableStructure(Element subElement) { List containers = Arrays.asList(getHeader(), getBody(), getFooter()); List containerType = Arrays.asList("header", "cell", "footer"); for (int i = 0; i < containers.size(); ++i) { RowContainer container = containers.get(i); boolean containerRow = (subElement.getTagName() .equalsIgnoreCase("tr") && subElement.getParentElement() == container.getElement()); if (containerRow) { /* * Wanted SubPart is row that is a child of containers root to * get indices, we use a cell that is a child of this row */ subElement = subElement.getFirstChildElement(); } Cell cell = container.getCell(subElement); if (cell != null) { // Skip the column index if subElement was a child of root return containerType.get(i) + "[" + cell.getRow() + (containerRow ? "]" : "][" + cell.getColumn() + "]"); } } return null; } private String getSubPartNameSpacer(Element subElement) { return body.spacerContainer.getSubPartName(subElement); } private void logWarning(String message) { getLogger().warning(message); } /** * This is an internal method for calculating minimum width for Column * resize. * * @return minimum width for column */ double getMinCellWidth(int colIndex) { return columnConfiguration.getMinCellWidth(colIndex); } /** * Internal method for checking whether the browser is IE11 or Edge * * @return true only if the current browser is IE11, or Edge */ private static boolean isCurrentBrowserIE11OrEdge() { return BrowserInfo.get().isIE11() || BrowserInfo.get().isEdge(); } } X-Content-Type-Options: nosniff Content-Security-Policy: default-src 'none' Content-Type: text/plain; charset=UTF-8 Content-Length: 340360 Content-Disposition: inline; filename="Grid.java" Last-Modified: Tue, 08 Jul 2025 18:25:43 GMT Expires: Tue, 08 Jul 2025 18:30:43 GMT ETag: "66ce4d92df354fe09f5ed128d2562f8b534af826" /* * Copyright 2000-2022 Vaadin Ltd. * * 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.vaadin.v7.client.widgets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.EventTarget; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; import com.google.gwt.dom.client.TableRowElement; import com.google.gwt.dom.client.TableSectionElement; import com.google.gwt.dom.client.Touch; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.KeyDownHandler; import com.google.gwt.event.dom.client.KeyEvent; import com.google.gwt.event.dom.client.MouseEvent; import com.google.gwt.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.event.logical.shared.ValueChangeEvent; import com.google.gwt.event.logical.shared.ValueChangeHandler; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.touch.client.Point; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Event.NativePreviewEvent; import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.CheckBox; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HasEnabled; import com.google.gwt.user.client.ui.HasWidgets; import com.google.gwt.user.client.ui.MenuBar; import com.google.gwt.user.client.ui.MenuItem; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.ResizeComposite; import com.google.gwt.user.client.ui.Widget; import com.vaadin.client.BrowserInfo; import com.vaadin.client.DeferredWorker; import com.vaadin.client.Focusable; import com.vaadin.client.WidgetUtil; import com.vaadin.client.data.AbstractRemoteDataSource; import com.vaadin.client.data.DataChangeHandler; import com.vaadin.client.data.DataSource; import com.vaadin.client.data.DataSource.RowHandle; import com.vaadin.client.ui.FocusUtil; import com.vaadin.client.ui.SubPartAware; import com.vaadin.client.ui.dd.DragAndDropHandler; import com.vaadin.client.ui.dd.DragAndDropHandler.DragAndDropCallback; import com.vaadin.client.ui.dd.DragHandle; import com.vaadin.client.ui.dd.DragHandle.DragHandleCallback; import com.vaadin.client.widgets.Overlay; import com.vaadin.shared.Range; import com.vaadin.shared.Registration; import com.vaadin.shared.data.sort.SortDirection; import com.vaadin.shared.util.SharedUtil; import com.vaadin.v7.client.renderers.ComplexRenderer; import com.vaadin.v7.client.renderers.ProgressBarRenderer; import com.vaadin.v7.client.renderers.Renderer; import com.vaadin.v7.client.renderers.TextRenderer; import com.vaadin.v7.client.renderers.WidgetRenderer; import com.vaadin.v7.client.widget.escalator.Cell; import com.vaadin.v7.client.widget.escalator.ColumnConfiguration; import com.vaadin.v7.client.widget.escalator.EscalatorUpdater; import com.vaadin.v7.client.widget.escalator.FlyweightCell; import com.vaadin.v7.client.widget.escalator.Row; import com.vaadin.v7.client.widget.escalator.RowContainer; import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeEvent; import com.vaadin.v7.client.widget.escalator.RowVisibilityChangeHandler; import com.vaadin.v7.client.widget.escalator.ScrollbarBundle.Direction; import com.vaadin.v7.client.widget.escalator.Spacer; import com.vaadin.v7.client.widget.escalator.SpacerUpdater; import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedEvent; import com.vaadin.v7.client.widget.escalator.events.RowHeightChangedHandler; import com.vaadin.v7.client.widget.escalator.events.SpacerVisibilityChangedEvent; import com.vaadin.v7.client.widget.escalator.events.SpacerVisibilityChangedHandler; import com.vaadin.v7.client.widget.grid.AutoScroller; import com.vaadin.v7.client.widget.grid.AutoScroller.AutoScrollerCallback; import com.vaadin.v7.client.widget.grid.AutoScroller.ScrollAxis; import com.vaadin.v7.client.widget.grid.CellReference; import com.vaadin.v7.client.widget.grid.CellStyleGenerator; import com.vaadin.v7.client.widget.grid.DataAvailableEvent; import com.vaadin.v7.client.widget.grid.DataAvailableHandler; import com.vaadin.v7.client.widget.grid.DefaultEditorEventHandler; import com.vaadin.v7.client.widget.grid.DetailsGenerator; import com.vaadin.v7.client.widget.grid.EditorHandler; import com.vaadin.v7.client.widget.grid.EditorHandler.EditorRequest; import com.vaadin.v7.client.widget.grid.EventCellReference; import com.vaadin.v7.client.widget.grid.GridEventHandler; import com.vaadin.v7.client.widget.grid.HeightAwareDetailsGenerator; import com.vaadin.v7.client.widget.grid.RendererCellReference; import com.vaadin.v7.client.widget.grid.RowReference; import com.vaadin.v7.client.widget.grid.RowStyleGenerator; import com.vaadin.v7.client.widget.grid.datasources.ListDataSource; import com.vaadin.v7.client.widget.grid.events.AbstractGridKeyEventHandler; import com.vaadin.v7.client.widget.grid.events.AbstractGridMouseEventHandler; import com.vaadin.v7.client.widget.grid.events.BodyClickHandler; import com.vaadin.v7.client.widget.grid.events.BodyDoubleClickHandler; import com.vaadin.v7.client.widget.grid.events.BodyKeyDownHandler; import com.vaadin.v7.client.widget.grid.events.BodyKeyPressHandler; import com.vaadin.v7.client.widget.grid.events.BodyKeyUpHandler; import com.vaadin.v7.client.widget.grid.events.ColumnReorderEvent; import com.vaadin.v7.client.widget.grid.events.ColumnReorderHandler; import com.vaadin.v7.client.widget.grid.events.ColumnResizeEvent; import com.vaadin.v7.client.widget.grid.events.ColumnResizeHandler; import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeEvent; import com.vaadin.v7.client.widget.grid.events.ColumnVisibilityChangeHandler; import com.vaadin.v7.client.widget.grid.events.FooterClickHandler; import com.vaadin.v7.client.widget.grid.events.FooterDoubleClickHandler; import com.vaadin.v7.client.widget.grid.events.FooterKeyDownHandler; import com.vaadin.v7.client.widget.grid.events.FooterKeyPressHandler; import com.vaadin.v7.client.widget.grid.events.FooterKeyUpHandler; import com.vaadin.v7.client.widget.grid.events.GridClickEvent; import com.vaadin.v7.client.widget.grid.events.GridDoubleClickEvent; import com.vaadin.v7.client.widget.grid.events.GridEnabledEvent; import com.vaadin.v7.client.widget.grid.events.GridEnabledHandler; import com.vaadin.v7.client.widget.grid.events.GridKeyDownEvent; import com.vaadin.v7.client.widget.grid.events.GridKeyPressEvent; import com.vaadin.v7.client.widget.grid.events.GridKeyUpEvent; import com.vaadin.v7.client.widget.grid.events.HeaderClickHandler; import com.vaadin.v7.client.widget.grid.events.HeaderDoubleClickHandler; import com.vaadin.v7.client.widget.grid.events.HeaderKeyDownHandler; import com.vaadin.v7.client.widget.grid.events.HeaderKeyPressHandler; import com.vaadin.v7.client.widget.grid.events.HeaderKeyUpHandler; import com.vaadin.v7.client.widget.grid.events.ScrollEvent; import com.vaadin.v7.client.widget.grid.events.ScrollHandler; import com.vaadin.v7.client.widget.grid.events.SelectAllEvent; import com.vaadin.v7.client.widget.grid.events.SelectAllHandler; import com.vaadin.v7.client.widget.grid.selection.HasSelectionHandlers; import com.vaadin.v7.client.widget.grid.selection.HasUserSelectionAllowed; import com.vaadin.v7.client.widget.grid.selection.MultiSelectionRenderer; import com.vaadin.v7.client.widget.grid.selection.SelectionEvent; import com.vaadin.v7.client.widget.grid.selection.SelectionHandler; import com.vaadin.v7.client.widget.grid.selection.SelectionModel; import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Multi; import com.vaadin.v7.client.widget.grid.selection.SelectionModel.Single; import com.vaadin.v7.client.widget.grid.selection.SelectionModelMulti; import com.vaadin.v7.client.widget.grid.selection.SelectionModelNone; import com.vaadin.v7.client.widget.grid.selection.SelectionModelSingle; import com.vaadin.v7.client.widget.grid.sort.Sort; import com.vaadin.v7.client.widget.grid.sort.SortEvent; import com.vaadin.v7.client.widget.grid.sort.SortHandler; import com.vaadin.v7.client.widget.grid.sort.SortOrder; import com.vaadin.v7.client.widgets.Escalator.AbstractRowContainer; import com.vaadin.v7.client.widgets.Escalator.SubPartArguments; import com.vaadin.v7.client.widgets.Grid.Editor.State; import com.vaadin.v7.client.widgets.Grid.StaticSection.StaticRow; import com.vaadin.v7.shared.ui.grid.ColumnResizeMode; import com.vaadin.v7.shared.ui.grid.GridConstants; import com.vaadin.v7.shared.ui.grid.GridConstants.Section; import com.vaadin.v7.shared.ui.grid.GridStaticCellType; import com.vaadin.v7.shared.ui.grid.HeightMode; import com.vaadin.v7.shared.ui.grid.ScrollDestination; /** * A data grid view that supports columns and lazy loading of data rows from a * data source. * *

Columns

*

* Each column in Grid is represented by a {@link Column}. Each * {@code GridColumn} has a custom implementation for * {@link Column#getValue(Object)} that gets the row object as an argument, and * returns the value for that particular column, extracted from the row object. *

* Each column also has a Renderer. Its function is to take the value that is * given by the {@code GridColumn} and display it to the user. A simple column * might have a {@link TextRenderer} that simply takes in a {@code String} and * displays it as the cell's content. A more complex renderer might be * {@link ProgressBarRenderer} that takes in a floating point number, and * displays a progress bar instead, based on the given number. *

* See: {@link #addColumn(Column)}, {@link #addColumn(Column, int)} and * {@link #addColumns(Column...)}. Also * {@link Column#setRenderer(Renderer)}. * *

Data Sources

*

* Grid gets its data from a {@link DataSource}, providing row objects to Grid * from a user-defined endpoint. It can be either a local in-memory data source * (e.g. {@link ListDataSource}) or even a remote one, retrieving data from e.g. * a REST API (see {@link AbstractRemoteDataSource}). * * * @param * The row type of the grid. The row type is the POJO type from where * the data is retrieved into the column cells. * @since 7.4 * @author Vaadin Ltd */ public class Grid extends ResizeComposite implements HasSelectionHandlers, SubPartAware, DeferredWorker, Focusable, com.google.gwt.user.client.ui.Focusable, HasWidgets, HasEnabled { private static final String STYLE_NAME = "v-grid"; private static final String SELECT_ALL_CHECKBOX_CLASSNAME = "-select-all-checkbox"; /** * Abstract base class for Grid header and footer sections. * * @since 7.5.0 * * @param * the type of the rows in the section */ public abstract static class StaticSection> { /** * A header or footer cell. Has a simple textual caption. * */ public static class StaticCell { private Object content = null; private int colspan = 1; private StaticSection section; private GridStaticCellType type = GridStaticCellType.TEXT; private String styleName = null; /** * Sets the text displayed in this cell. * * @param text * a plain text caption */ public void setText(String text) { content = text; type = GridStaticCellType.TEXT; section.requestSectionRefresh(); } /** * Returns the text displayed in this cell. * * @return the plain text caption */ public String getText() { if (type != GridStaticCellType.TEXT) { throw new IllegalStateException( "Cannot fetch Text from a cell with type " + type); } return (String) content; } protected StaticSection getSection() { assert section != null; return section; } protected void setSection(StaticSection section) { this.section = section; } /** * Returns the amount of columns the cell spans. By default is 1. * * @return The amount of columns the cell spans. */ public int getColspan() { return colspan; } /** * Sets the amount of columns the cell spans. Must be more or equal * to 1. By default is 1. * * @param colspan * the colspan to set */ public void setColspan(int colspan) { if (colspan < 1) { throw new IllegalArgumentException( "Colspan cannot be less than 1"); } this.colspan = colspan; section.requestSectionRefresh(); } /** * Returns the html inside the cell. * * @throws IllegalStateException * if trying to retrive HTML from a cell with a type * other than {@link GridStaticCellType#HTML}. * @return the html content of the cell. */ public String getHtml() { if (type != GridStaticCellType.HTML) { throw new IllegalStateException( "Cannot fetch HTML from a cell with type " + type); } return (String) content; } /** * Sets the content of the cell to the provided html. All previous * content is discarded and the cell type is set to * {@link GridStaticCellType#HTML}. * * @param html * The html content of the cell */ public void setHtml(String html) { content = html; type = GridStaticCellType.HTML; section.requestSectionRefresh(); } /** * Returns the widget in the cell. * * @throws IllegalStateException * if the cell is not {@link GridStaticCellType#WIDGET} * * @return the widget in the cell */ public Widget getWidget() { if (type != GridStaticCellType.WIDGET) { throw new IllegalStateException( "Cannot fetch Widget from a cell with type " + type); } return (Widget) content; } /** * Set widget as the content of the cell. The type of the cell * becomes {@link GridStaticCellType#WIDGET}. All previous content * is discarded. * * @param widget * The widget to add to the cell. Should not be * previously attached anywhere (widget.getParent == * null). */ public void setWidget(Widget widget) { if (content == widget) { return; } if (content instanceof Widget) { // Old widget in the cell, detach it first section.getGrid().detachWidget((Widget) content); } content = widget; type = GridStaticCellType.WIDGET; section.requestSectionRefresh(); } /** * Returns the type of the cell. * * @return the type of content the cell contains. */ public GridStaticCellType getType() { return type; } /** * Returns the custom style name for this cell. * * @return the style name or null if no style name has been set */ public String getStyleName() { return styleName; } /** * Sets a custom style name for this cell. * * @param styleName * the style name to set or null to not use any style * name */ public void setStyleName(String styleName) { this.styleName = styleName; section.requestSectionRefresh(); } /** * Called when the cell is detached from the row * * @since 7.6.3 */ void detach() { if (content instanceof Widget) { // Widget in the cell, detach it section.getGrid().detachWidget((Widget) content); } } } /** * Abstract base class for Grid header and footer rows. * * @param * the type of the cells in the row */ public abstract static class StaticRow { private Map, CELLTYPE> cells = new HashMap, CELLTYPE>(); private StaticSection section; /** * Map from set of spanned columns to cell meta data. */ private Map>, CELLTYPE> cellGroups = new HashMap>, CELLTYPE>(); /** * A custom style name for the row or null if none is set. */ private String styleName = null; /** * Returns the cell on given GridColumn. If the column is merged * returned cell is the cell for the whole group. * * @param column * the column in grid * @return the cell on given column, merged cell for merged columns, * null if not found */ public CELLTYPE getCell(Column column) { Set> cellGroup = getCellGroupForColumn(column); if (cellGroup != null) { return cellGroups.get(cellGroup); } return cells.get(column); } /** * Returns true if this row contains spanned cells. * * @since 7.5.0 * @return does this row contain spanned cells */ public boolean hasSpannedCells() { return !cellGroups.isEmpty(); } /** * Merges columns cells in a row. * * @param columns * the columns which header should be merged * @return the remaining visible cell after the merge, or the cell * on first column if all are hidden */ public CELLTYPE join(Column... columns) { if (columns.length <= 1) { throw new IllegalArgumentException( "You can't merge less than 2 columns together."); } HashSet> columnGroup = new HashSet>(); // NOTE: this doesn't care about hidden columns, those are // filtered in calculateColspans() for (Column column : columns) { if (!cells.containsKey(column)) { throw new IllegalArgumentException( "Given column does not exists on row " + column); } else if (getCellGroupForColumn(column) != null) { throw new IllegalStateException( "Column is already in a group."); } columnGroup.add(column); } CELLTYPE joinedCell = createCell(); cellGroups.put(columnGroup, joinedCell); joinedCell.setSection(getSection()); calculateColspans(); return joinedCell; } /** * Merges columns cells in a row. * * @param cells * The cells to merge. Must be from the same row. * @return The remaining visible cell after the merge, or the first * cell if all columns are hidden */ public CELLTYPE join(CELLTYPE... cells) { if (cells.length <= 1) { throw new IllegalArgumentException( "You can't merge less than 2 cells together."); } Column[] columns = new Column[cells.length]; int j = 0; for (Column column : this.cells.keySet()) { CELLTYPE cell = this.cells.get(column); if (!this.cells.containsValue(cells[j])) { throw new IllegalArgumentException( "Given cell does not exists on row"); } else if (cell.equals(cells[j])) { columns[j++] = column; if (j == cells.length) { break; } } } return join(columns); } private Set> getCellGroupForColumn( Column column) { for (Set> group : cellGroups.keySet()) { if (group.contains(column)) { return group; } } return null; } void calculateColspans() { // Reset all cells for (CELLTYPE cell : this.cells.values()) { cell.setColspan(1); } // Set colspan for grouped cells for (Set> group : cellGroups.keySet()) { if (!checkMergedCellIsContinuous(group)) { // on error simply break the merged cell cellGroups.get(group).setColspan(1); } else { int colSpan = 0; for (Column column : group) { if (!column.isHidden()) { colSpan++; } } // colspan can't be 0 cellGroups.get(group).setColspan(Math.max(1, colSpan)); } } } private boolean checkMergedCellIsContinuous( Set> mergedCell) { // no matter if hidden or not, just check for continuous order final List> columnOrder = new ArrayList>( section.grid.getColumns()); if (!columnOrder.containsAll(mergedCell)) { return false; } for (int i = 0; i < columnOrder.size(); ++i) { if (!mergedCell.contains(columnOrder.get(i))) { continue; } for (int j = 1; j < mergedCell.size(); ++j) { if (!mergedCell.contains(columnOrder.get(i + j))) { return false; } } return true; } return false; } protected void addCell(Column column) { CELLTYPE cell = createCell(); cell.setSection(getSection()); cells.put(column, cell); } protected void removeCell(Column column) { cells.remove(column); } protected abstract CELLTYPE createCell(); protected StaticSection getSection() { return section; } protected void setSection(StaticSection section) { this.section = section; } /** * Returns the custom style name for this row. * * @return the style name or null if no style name has been set */ public String getStyleName() { return styleName; } /** * Sets a custom style name for this row. * * @param styleName * the style name to set or null to not use any style * name */ public void setStyleName(String styleName) { this.styleName = styleName; section.requestSectionRefresh(); } /** * Called when the row is detached from the grid * * @since 7.6.3 */ void detach() { // Avoid calling detach twice for a merged cell HashSet cells = new HashSet(); for (Column column : getSection().grid.getColumns()) { cells.add(getCell(column)); } for (CELLTYPE cell : cells) { cell.detach(); } } } private Grid grid; private List rows = new ArrayList(); private boolean visible = true; /** * Creates and returns a new instance of the row type. * * @return the created row */ protected abstract ROWTYPE createRow(); /** * Informs the grid that this section should be re-rendered. *

* Note that re-render means calling update() on each cell, * preAttach()/postAttach()/preDetach()/postDetach() is not called as * the cells are not removed from the DOM. */ protected abstract void requestSectionRefresh(); /** * Sets the visibility of the whole section. * * @param visible * true to show this section, false to hide */ public void setVisible(boolean visible) { this.visible = visible; requestSectionRefresh(); } /** * Returns the visibility of this section. * * @return true if visible, false otherwise. */ public boolean isVisible() { return visible; } /** * Inserts a new row at the given position. Shifts the row currently at * that position and any subsequent rows down (adds one to their * indices). * * @param index * the position at which to insert the row * @return the new row * * @throws IndexOutOfBoundsException * if the index is out of bounds * @see #appendRow() * @see #prependRow() * @see #removeRow(int) * @see #removeRow(StaticRow) */ public ROWTYPE addRowAt(int index) { ROWTYPE row = createRow(); row.setSection(this); for (int i = 0; i < getGrid().getColumnCount(); ++i) { row.addCell(grid.getColumn(i)); } rows.add(index, row); requestSectionRefresh(); return row; } /** * Adds a new row at the top of this section. * * @return the new row * @see #appendRow() * @see #addRowAt(int) * @see #removeRow(int) * @see #removeRow(StaticRow) */ public ROWTYPE prependRow() { return addRowAt(0); } /** * Adds a new row at the bottom of this section. * * @return the new row * @see #prependRow() * @see #addRowAt(int) * @see #removeRow(int) * @see #removeRow(StaticRow) */ public ROWTYPE appendRow() { return addRowAt(rows.size()); } /** * Removes the row at the given position. * * @param index * the position of the row * * @throws IndexOutOfBoundsException * if the index is out of bounds * @see #addRowAt(int) * @see #appendRow() * @see #prependRow() * @see #removeRow(StaticRow) */ public void removeRow(int index) { ROWTYPE row = rows.remove(index); row.detach(); requestSectionRefresh(); } /** * Removes the given row from the section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #addRowAt(int) * @see #appendRow() * @see #prependRow() * @see #removeRow(int) */ public void removeRow(ROWTYPE row) { try { removeRow(rows.indexOf(row)); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException( "Section does not contain the given row"); } } /** * Returns the row at the given position. * * @param index * the position of the row * @return the row with the given index * * @throws IndexOutOfBoundsException * if the index is out of bounds */ public ROWTYPE getRow(int index) { try { return rows.get(index); } catch (IndexOutOfBoundsException e) { throw new IllegalArgumentException( "Row with index " + index + " does not exist"); } } /** * Returns the number of rows in this section. * * @return the number of rows */ public int getRowCount() { return rows.size(); } protected List getRows() { return rows; } protected int getVisibleRowCount() { return isVisible() ? getRowCount() : 0; } protected void addColumn(Column column) { for (ROWTYPE row : rows) { row.addCell(column); } } protected void removeColumn(Column column) { for (ROWTYPE row : rows) { row.removeCell(column); } } protected void setGrid(Grid grid) { this.grid = grid; } protected Grid getGrid() { assert grid != null; return grid; } protected void updateColSpans() { for (ROWTYPE row : rows) { if (row.hasSpannedCells()) { row.calculateColspans(); } } } } /** * Represents the header section of a Grid. A header consists of a single * header row containing a header cell for each column. Each cell has a * simple textual caption. */ protected static class Header extends StaticSection { private HeaderRow defaultRow; private boolean markAsDirty = false; @Override public void removeRow(int index) { HeaderRow removedRow = getRow(index); super.removeRow(index); if (removedRow == defaultRow) { setDefaultRow(null); } } /** * Sets the default row of this header. The default row is a special * header row providing a user interface for sorting columns. * * @param row * the new default row, or null for no default row * * @throws IllegalArgumentException * this header does not contain the row */ public void setDefaultRow(HeaderRow row) { if (row == defaultRow) { return; } if (row != null && !getRows().contains(row)) { throw new IllegalArgumentException( "Cannot set a default row that does not exist in the container"); } if (defaultRow != null) { defaultRow.setDefault(false); } if (row != null) { row.setDefault(true); } defaultRow = row; requestSectionRefresh(); } /** * Returns the current default row of this header. The default row is a * special header row providing a user interface for sorting columns. * * @return the default row or null if no default row set */ public HeaderRow getDefaultRow() { return defaultRow; } @Override protected HeaderRow createRow() { return new HeaderRow(); } @Override protected void requestSectionRefresh() { markAsDirty = true; /* * Defer the refresh so if we multiple times call refreshSection() * (for example when updating cell values) we only get one actual * refresh in the end. */ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { @Override public void execute() { if (markAsDirty) { markAsDirty = false; getGrid().refreshHeader(); } } }); } /** * Returns the events consumed by the header. * * @return a collection of BrowserEvents */ public Collection getConsumedEvents() { return Arrays.asList(BrowserEvents.TOUCHSTART, BrowserEvents.TOUCHMOVE, BrowserEvents.TOUCHEND, BrowserEvents.TOUCHCANCEL, BrowserEvents.CLICK); } @Override protected void addColumn(Column column) { super.addColumn(column); // Add default content for new columns. if (defaultRow != null) { column.setDefaultHeaderContent(defaultRow.getCell(column)); } } } /** * A single row in a grid header section. * */ public static class HeaderRow extends StaticSection.StaticRow { private boolean isDefault = false; protected void setDefault(boolean isDefault) { this.isDefault = isDefault; if (isDefault) { for (Column column : getSection().grid.getColumns()) { column.setDefaultHeaderContent(getCell(column)); } } } public boolean isDefault() { return isDefault; } @Override protected HeaderCell createCell() { return new HeaderCell(); } } /** * A single cell in a grid header row. Has a caption and, if it's in a * default row, a drag handle. */ public static class HeaderCell extends StaticSection.StaticCell { } /** * Represents the footer section of a Grid. The footer is always empty. */ protected static class Footer extends StaticSection { private boolean markAsDirty = false; @Override protected FooterRow createRow() { return new FooterRow(); } @Override protected void requestSectionRefresh() { markAsDirty = true; /* * Defer the refresh so if we multiple times call refreshSection() * (for example when updating cell values) we only get one actual * refresh in the end. */ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { @Override public void execute() { if (markAsDirty) { markAsDirty = false; getGrid().refreshFooter(); } } }); } } /** * A single cell in a grid Footer row. Has a textual caption. * */ public static class FooterCell extends StaticSection.StaticCell { } /** * A single row in a grid Footer section. * */ public static class FooterRow extends StaticSection.StaticRow { @Override protected FooterCell createCell() { return new FooterCell(); } } private static class EditorRequestImpl implements EditorRequest { /** * A callback interface used to notify the invoker of the editor handler * of completed editor requests. * * @param * the row data type */ public static interface RequestCallback { /** * The method that must be called when the request has been * processed correctly. * * @param request * the original request object */ public void onSuccess(EditorRequest request); /** * The method that must be called when processing the request has * produced an aborting error. * * @param request * the original request object */ public void onError(EditorRequest request); } private Grid grid; private final int rowIndex; private final int columnIndexDOM; private RequestCallback callback; private boolean completed = false; public EditorRequestImpl(Grid grid, int rowIndex, int columnIndexDOM, RequestCallback callback) { this.grid = grid; this.rowIndex = rowIndex; this.columnIndexDOM = columnIndexDOM; this.callback = callback; } @Override public int getRowIndex() { return rowIndex; } @Override public int getColumnIndex() { return columnIndexDOM; } @Override public T getRow() { return grid.getDataSource().getRow(rowIndex); } @Override public Grid getGrid() { return grid; } @Override public Widget getWidget(Grid.Column column) { Widget w = grid.getEditorWidget(column); assert w != null; return w; } private void complete(String errorMessage, Collection> errorColumns) { if (completed) { throw new IllegalStateException( "An EditorRequest must be completed exactly once"); } completed = true; if (errorColumns == null) { errorColumns = Collections.emptySet(); } grid.getEditor().setEditorError(errorMessage, errorColumns); } @Override public void success() { complete(null, null); if (callback != null) { callback.onSuccess(this); } } @Override public void failure(String errorMessage, Collection> errorColumns) { complete(errorMessage, errorColumns); if (callback != null) { callback.onError(this); } } @Override public boolean isCompleted() { return completed; } } /** * A wrapper for native DOM events originating from Grid. In addition to the * native event, contains a {@link CellReference} instance specifying which * cell the event originated from. * * @since 7.6 * @param * The row type of the grid */ public static class GridEvent { private Event event; private EventCellReference cell; private boolean handled = false; protected GridEvent(Event event, EventCellReference cell) { this.event = event; this.cell = cell; } /** * Returns the wrapped DOM event. * * @return the DOM event */ public Event getDomEvent() { return event; } /** * Returns the Grid cell this event originated from. * * @return the event cell */ public EventCellReference getCell() { return cell; } /** * Returns the Grid instance this event originated from. * * @return the grid */ public Grid getGrid() { return cell.getGrid(); } /** * Check whether this event has already been marked as handled. * * @return whether this event has already been marked as handled */ public boolean isHandled() { return handled; } /** * Set the status of this event. Setting to {@code true} effectively * marks this event as having already been handled. * * @param handled */ public void setHandled(boolean handled) { this.handled = handled; } } /** * A wrapper for native DOM events related to the {@link Editor Grid editor} * . * * @since 7.6 * @param * the row type of the grid */ public static class EditorDomEvent extends GridEvent { private final Widget editorWidget; protected EditorDomEvent(Event event, EventCellReference cell, Widget editorWidget) { super(event, cell); this.editorWidget = editorWidget; } /** * Returns the editor of the Grid this event originated from. * * @return the related editor instance */ public Editor getEditor() { return getGrid().getEditor(); } /** * Returns the currently focused editor widget. * * @return the focused editor widget or {@code null} if not editable */ public Widget getEditorWidget() { return editorWidget; } /** * Returns the row index the editor is open at. If the editor is not * open, returns -1. * * @return the index of the edited row or -1 if editor is not open */ public int getRowIndex() { return getEditor().rowIndex; } /** * Returns the DOM column index (excluding hidden columns) the editor * was opened at. If the editor is not open, returns -1. * * @return the column index or -1 if editor is not open */ public int getFocusedColumnIndex() { return getEditor().focusedColumnIndexDOM; } } /** * An editor UI for Grid rows. A single Grid row at a time can be opened for * editing. * * @since 7.6 * @param * the row type of the grid */ public static class Editor implements DeferredWorker { public static final int KEYCODE_SHOW = KeyCodes.KEY_ENTER; public static final int KEYCODE_HIDE = KeyCodes.KEY_ESCAPE; private static final String ERROR_CLASS_NAME = "error"; private static final String NOT_EDITABLE_CLASS_NAME = "not-editable"; ScheduledCommand fieldFocusCommand = new ScheduledCommand() { private int count = 0; @Override public void execute() { Element focusedElement = WidgetUtil.getFocusedElement(); if (focusedElement == grid.getElement() || focusedElement == Document.get().getBody() || count > 2) { focusColumn(focusedColumnIndexDOM); } else { ++count; Scheduler.get().scheduleDeferred(this); } } }; /** * A handler for events related to the Grid editor. Responsible for * opening, moving or closing the editor based on the received event. * * @since 7.6 * @author Vaadin Ltd * @param * the row type of the grid */ public interface EventHandler { /** * Handles editor-related events in an appropriate way. Opens, * moves, or closes the editor based on the given event. * * @param event * the received event * @return true if the event was handled and nothing else should be * done, false otherwise */ boolean handleEvent(EditorDomEvent event); } protected enum State { INACTIVE, ACTIVATING, BINDING, ACTIVE, SAVING } private Grid grid; private EditorHandler handler; private EventHandler eventHandler = GWT .create(DefaultEditorEventHandler.class); private DivElement editorOverlay = DivElement.as(DOM.createDiv()); private DivElement cellWrapper = DivElement.as(DOM.createDiv()); private DivElement frozenCellWrapper = DivElement.as(DOM.createDiv()); private DivElement messageAndButtonsWrapper = DivElement .as(DOM.createDiv()); private DivElement messageWrapper = DivElement.as(DOM.createDiv()); private DivElement buttonsWrapper = DivElement.as(DOM.createDiv()); // Element which contains the error message for the editor // Should only be added to the DOM when there's a message to show private DivElement message = DivElement.as(DOM.createDiv()); private Map, Widget> columnToWidget = new HashMap, Widget>(); private List focusHandlers = new ArrayList(); private boolean enabled = false; private State state = State.INACTIVE; private int rowIndex = -1; private int focusedColumnIndexDOM = -1; private String styleName = null; private HandlerRegistration hScrollHandler; private HandlerRegistration vScrollHandler; private final Button saveButton; private final Button cancelButton; private static final int SAVE_TIMEOUT_MS = 5000; private final Timer saveTimeout = new Timer() { @Override public void run() { getLogger().warning( "Editor save action is taking longer than expected (" + SAVE_TIMEOUT_MS + "ms). Does your " + EditorHandler.class.getSimpleName() + " remember to call success() or fail()?"); } }; private final EditorRequestImpl.RequestCallback saveRequestCallback = new EditorRequestImpl.RequestCallback() { @Override public void onSuccess(EditorRequest request) { if (state == State.SAVING) { cleanup(); cancel(); grid.clearSortOrder(); } } @Override public void onError(EditorRequest request) { if (state == State.SAVING) { cleanup(); } } private void cleanup() { state = State.ACTIVE; setButtonsEnabled(true); saveTimeout.cancel(); } }; private static final int BIND_TIMEOUT_MS = 5000; private final Timer bindTimeout = new Timer() { @Override public void run() { getLogger().warning( "Editor bind action is taking longer than expected (" + BIND_TIMEOUT_MS + "ms). Does your " + EditorHandler.class.getSimpleName() + " remember to call success() or fail()?"); } }; private final EditorRequestImpl.RequestCallback bindRequestCallback = new EditorRequestImpl.RequestCallback() { @Override public void onSuccess(EditorRequest request) { if (state == State.BINDING) { state = State.ACTIVE; bindTimeout.cancel(); rowIndex = request.getRowIndex(); focusedColumnIndexDOM = request.getColumnIndex(); if (focusedColumnIndexDOM >= 0) { // Update internal focus of Grid grid.focusCell(rowIndex, focusedColumnIndexDOM); } showOverlay(); } } @Override public void onError(EditorRequest request) { if (state == State.BINDING) { if (rowIndex == -1) { doCancel(); } else { state = State.ACTIVE; // TODO: Maybe restore focus? } bindTimeout.cancel(); } } }; /** A set of all the columns that display an error flag. */ private final Set> columnErrors = new HashSet>(); private boolean buffered = true; /** Original position of editor */ private double originalTop; /** Original scroll position of grid when editor was opened */ private double originalScrollTop; private RowHandle pinnedRowHandle; public Editor() { saveButton = new Button(); saveButton.setText(GridConstants.DEFAULT_SAVE_CAPTION); saveButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { save(); } }); cancelButton = new Button(); cancelButton.setText(GridConstants.DEFAULT_CANCEL_CAPTION); cancelButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { cancel(); } }); } public void setEditorError(String errorMessage, Collection> errorColumns) { if (errorMessage == null) { message.removeFromParent(); } else { message.setInnerText(errorMessage); if (message.getParentElement() == null) { messageWrapper.appendChild(message); } } // In unbuffered mode only show message wrapper if there is an error if (!isBuffered()) { setMessageAndButtonsWrapperVisible(errorMessage != null); } if (state == State.ACTIVE || state == State.SAVING) { for (Column c : grid.getColumns()) { grid.getEditor().setEditorColumnError(c, errorColumns.contains(c)); } } } public int getRow() { return rowIndex; } /** * If a cell of this Grid had focus once this editRow call was * triggered, the editor component at the previously focused column * index will be focused. * * If a Grid cell was not focused prior to calling this method, it will * be equivalent to {@code editRow(rowIndex, -1)}. * * @see #editRow(int, int) */ public void editRow(int rowIndex) { // Focus the last focused column in the editor if grid or its child // was focused before the edit request Cell focusedCell = grid.cellFocusHandler.getFocusedCell(); Element focusedElement = WidgetUtil.getFocusedElement(); if (focusedCell != null && focusedElement != null && grid.getElement().isOrHasChild(focusedElement)) { editRow(rowIndex, focusedCell.getColumn()); } else { editRow(rowIndex, -1); } } /** * Opens the editor over the row with the given index and attempts to * focus the editor widget in the given column index. Does not move * focus if the widget is not focusable or if the column index is -1. * * @param rowIndex * the index of the row to be edited * @param columnIndexDOM * the column index (excluding hidden columns) of the editor * widget that should be initially focused or -1 to not set * focus * * @throws IllegalStateException * if this editor is not enabled * @throws IllegalStateException * if this editor is already in edit mode and in buffered * mode * * @since 7.5 */ public void editRow(final int rowIndex, final int columnIndexDOM) { if (!enabled) { throw new IllegalStateException( "Cannot edit row: editor is not enabled"); } if (isWorkPending()) { // Request pending a response, don't move try to start another // request. return; } if (state != State.INACTIVE && this.rowIndex != rowIndex) { if (isBuffered()) { throw new IllegalStateException( "Cannot edit row: editor already in edit mode"); } else if (!columnErrors.isEmpty()) { // Don't move row if errors are present // FIXME: Should attempt bind if error field values have // changed. return; } } if (columnIndexDOM >= grid.getVisibleColumns().size()) { throw new IllegalArgumentException( "Edited column index " + columnIndexDOM + " was bigger than visible column count."); } if (this.rowIndex == rowIndex && focusedColumnIndexDOM == columnIndexDOM) { // NO-OP return; } if (this.rowIndex == rowIndex) { if (focusedColumnIndexDOM != columnIndexDOM) { if (columnIndexDOM >= grid.getFrozenColumnCount()) { // Scroll to new focused column. grid.getEscalator().scrollToColumn(columnIndexDOM, ScrollDestination.ANY, 0); } focusedColumnIndexDOM = columnIndexDOM; } updateHorizontalScrollPosition(); // Update Grid internal focus and focus widget if possible if (focusedColumnIndexDOM >= 0) { grid.focusCell(rowIndex, focusedColumnIndexDOM); focusColumn(focusedColumnIndexDOM); } // No need to request anything from the editor handler. return; } state = State.ACTIVATING; final Escalator escalator = grid.getEscalator(); if (escalator.getVisibleRowRange().contains(rowIndex)) { show(rowIndex, columnIndexDOM); } else { vScrollHandler = grid.addScrollHandler(new ScrollHandler() { @Override public void onScroll(ScrollEvent event) { if (escalator.getVisibleRowRange().contains(rowIndex)) { show(rowIndex, columnIndexDOM); vScrollHandler.removeHandler(); } } }); grid.scrollToRow(rowIndex, isBuffered() ? ScrollDestination.MIDDLE : ScrollDestination.ANY); } } /** * Cancels the currently active edit and hides the editor. Any changes * that are not {@link #save() saved} are lost. * * @throws IllegalStateException * if this editor is not enabled * @throws IllegalStateException * if this editor is not in edit mode */ public void cancel() { if (!enabled) { throw new IllegalStateException( "Cannot cancel edit: editor is not enabled"); } if (state == State.INACTIVE) { throw new IllegalStateException( "Cannot cancel edit: editor is not in edit mode"); } handler.cancel(new EditorRequestImpl(grid, rowIndex, focusedColumnIndexDOM, null)); doCancel(); } private void doCancel() { hideOverlay(); state = State.INACTIVE; rowIndex = -1; focusedColumnIndexDOM = -1; grid.getEscalator().setScrollLocked(Direction.VERTICAL, false); updateSelectionCheckboxesAsNeeded(true); } private void updateSelectionCheckboxesAsNeeded(boolean isEnabled) { // FIXME: This is too much guessing. Define a better way to do this. if (grid.selectionColumn != null && grid.selectionColumn .getRenderer() instanceof MultiSelectionRenderer) { grid.refreshBody(); CheckBox checkBox = (CheckBox) grid.getDefaultHeaderRow() .getCell(grid.selectionColumn).getWidget(); checkBox.setEnabled(isEnabled); } } /** * Saves any unsaved changes to the data source and hides the editor. * * @throws IllegalStateException * if this editor is not enabled * @throws IllegalStateException * if this editor is not in edit mode */ public void save() { if (!enabled) { throw new IllegalStateException( "Cannot save: editor is not enabled"); } if (state != State.ACTIVE) { throw new IllegalStateException( "Cannot save: editor is not in edit mode"); } state = State.SAVING; setButtonsEnabled(false); saveTimeout.schedule(SAVE_TIMEOUT_MS); EditorRequest request = new EditorRequestImpl(grid, rowIndex, focusedColumnIndexDOM, saveRequestCallback); handler.save(request); updateSelectionCheckboxesAsNeeded(true); } /** * Returns the handler responsible for binding data and editor widgets * to this editor. * * @return the editor handler or null if not set */ public EditorHandler getHandler() { return handler; } /** * Sets the handler responsible for binding data and editor widgets to * this editor. * * @param rowHandler * the new editor handler * * @throws IllegalStateException * if this editor is currently in edit mode */ public void setHandler(EditorHandler rowHandler) { if (state != State.INACTIVE) { throw new IllegalStateException( "Cannot set EditorHandler: editor is currently in edit mode"); } handler = rowHandler; } public boolean isEnabled() { return enabled; } /** * Sets the enabled state of this editor. * * @param enabled * true if enabled, false otherwise * * @throws IllegalStateException * if in edit mode and trying to disable * @throws IllegalStateException * if the editor handler is not set */ public void setEnabled(boolean enabled) { if (!enabled && state != State.INACTIVE) { throw new IllegalStateException( "Cannot disable: editor is in edit mode"); } else if (enabled && getHandler() == null) { throw new IllegalStateException( "Cannot enable: EditorHandler not set"); } this.enabled = enabled; } protected void show(int rowIndex, int columnIndex) { if (state == State.ACTIVATING) { state = State.BINDING; bindTimeout.schedule(BIND_TIMEOUT_MS); EditorRequest request = new EditorRequestImpl(grid, rowIndex, columnIndex, bindRequestCallback); handler.bind(request); grid.getEscalator().setScrollLocked(Direction.VERTICAL, isBuffered()); updateSelectionCheckboxesAsNeeded(false); } } protected void setGrid(final Grid grid) { assert grid != null : "Grid cannot be null"; assert this.grid == null : "Can only attach editor to Grid once"; this.grid = grid; } protected State getState() { return state; } protected void setState(State state) { this.state = state; } /** * Returns the editor widget associated with the given column. If the * editor is not active or the column is not * {@link Grid.Column#isEditable() editable}, returns null. * * @param column * the column * @return the widget if the editor is open and the column is editable, * null otherwise */ protected Widget getWidget(Column column) { return columnToWidget.get(column); } /** * Equivalent to {@code showOverlay()}. The argument is ignored. * * @param unused * ignored argument * * @deprecated As of 7.5, use {@link #showOverlay()} instead. */ @Deprecated protected void showOverlay(TableRowElement unused) { showOverlay(); } /** * Opens the editor overlay over the table row indicated by * {@link #getRow()}. * * @since 7.5 */ protected void showOverlay() { // Ensure overlay is hidden initially hideOverlay(); DivElement gridElement = DivElement.as(grid.getElement()); TableRowElement tr = grid.getEscalator().getBody() .getRowElement(rowIndex); hScrollHandler = grid.addScrollHandler(new ScrollHandler() { @Override public void onScroll(ScrollEvent event) { updateHorizontalScrollPosition(); updateVerticalScrollPosition(); } }); gridElement.appendChild(editorOverlay); editorOverlay.appendChild(frozenCellWrapper); editorOverlay.appendChild(cellWrapper); editorOverlay.appendChild(messageAndButtonsWrapper); updateBufferedStyleName(); int frozenColumns = grid.getVisibleFrozenColumnCount(); double frozenColumnsWidth = 0; double cellHeight = 0; for (int i = 0; i < tr.getCells().getLength(); i++) { Element cell = createCell(tr.getCells().getItem(i)); cellHeight = Math.max(cellHeight, WidgetUtil.getRequiredHeightBoundingClientRectDouble( tr.getCells().getItem(i))); Column column = grid.getVisibleColumn(i); if (i < frozenColumns) { frozenCellWrapper.appendChild(cell); frozenColumnsWidth += WidgetUtil .getRequiredWidthBoundingClientRectDouble( tr.getCells().getItem(i)); } else { cellWrapper.appendChild(cell); } if (column.isEditable()) { Widget editor = getHandler().getWidget(column); if (editor != null) { columnToWidget.put(column, editor); grid.attachWidget(editor, cell); } if (i == focusedColumnIndexDOM) { if (BrowserInfo.get().isIE8()) { Scheduler.get().scheduleDeferred(fieldFocusCommand); } else { focusColumn(focusedColumnIndexDOM); } } } else { cell.addClassName(NOT_EDITABLE_CLASS_NAME); cell.addClassName(tr.getCells().getItem(i).getClassName()); // If the focused or frozen stylename is present it should // not be inherited by the editor cell as it is not useful // in the editor and would look broken without additional // style rules. This is a bit of a hack. cell.removeClassName(grid.cellFocusStyleName); cell.removeClassName("frozen"); if (column == grid.selectionColumn) { // Duplicate selection column CheckBox pinnedRowHandle = grid.getDataSource().getHandle( grid.getDataSource().getRow(rowIndex)); pinnedRowHandle.pin(); // We need to duplicate the selection CheckBox for the // editor overlay since the original one is hidden by // the overlay final CheckBox checkBox = GWT.create(CheckBox.class); checkBox.setValue( grid.isSelected(pinnedRowHandle.getRow())); checkBox.sinkEvents(Event.ONCLICK); checkBox.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { if (!grid.isUserSelectionAllowed()) { return; } T row = pinnedRowHandle.getRow(); if (grid.isSelected(row)) { grid.deselect(row); } else { grid.select(row); } } }); grid.attachWidget(checkBox, cell); columnToWidget.put(column, checkBox); // Only enable CheckBox in non-buffered mode checkBox.setEnabled(!isBuffered()); } else if (!(column .getRenderer() instanceof WidgetRenderer)) { // Copy non-widget content directly cell.setInnerHTML( tr.getCells().getItem(i).getInnerHTML()); } } } setBounds(frozenCellWrapper, 0, 0, frozenColumnsWidth, 0); setBounds(cellWrapper, frozenColumnsWidth, 0, tr.getOffsetWidth() - frozenColumnsWidth, cellHeight); // Only add these elements once if (!messageAndButtonsWrapper.isOrHasChild(messageWrapper)) { messageAndButtonsWrapper.appendChild(messageWrapper); messageAndButtonsWrapper.appendChild(buttonsWrapper); } if (isBuffered()) { grid.attachWidget(saveButton, buttonsWrapper); grid.attachWidget(cancelButton, buttonsWrapper); } setMessageAndButtonsWrapperVisible(isBuffered()); updateHorizontalScrollPosition(); AbstractRowContainer body = (AbstractRowContainer) grid .getEscalator().getBody(); double rowTop = body.getRowTop(tr); int bodyTop = body.getElement().getAbsoluteTop(); int gridTop = gridElement.getAbsoluteTop(); double overlayTop = rowTop + bodyTop - gridTop; originalScrollTop = grid.getScrollTop(); if (!isBuffered() || buttonsShouldBeRenderedBelow(tr)) { // Default case, editor buttons are below the edited row editorOverlay.getStyle().setTop(overlayTop, Unit.PX); originalTop = overlayTop; editorOverlay.getStyle().clearBottom(); } else { // Move message and buttons wrapper on top of cell wrapper if // there is not enough space visible space under and fix the // overlay from the bottom editorOverlay.insertFirst(messageAndButtonsWrapper); int gridHeight = grid.getElement().getOffsetHeight(); editorOverlay.getStyle().setBottom( gridHeight - overlayTop - tr.getOffsetHeight(), Unit.PX); editorOverlay.getStyle().clearTop(); } // Do not render over the vertical scrollbar editorOverlay.getStyle().setWidth(grid.escalator.getInnerWidth(), Unit.PX); } private void focusColumn(int columnIndexDOM) { if (columnIndexDOM < 0 || columnIndexDOM >= grid.getVisibleColumns().size()) { // NO-OP return; } Widget editor = getWidget(grid.getVisibleColumn(columnIndexDOM)); if (editor instanceof Focusable) { ((Focusable) editor).focus(); } else if (editor instanceof com.google.gwt.user.client.ui.Focusable) { ((com.google.gwt.user.client.ui.Focusable) editor) .setFocus(true); } else { grid.focus(); } } private boolean buttonsShouldBeRenderedBelow(TableRowElement tr) { TableSectionElement tfoot = grid.escalator.getFooter().getElement(); double tfootPageTop = WidgetUtil.getBoundingClientRect(tfoot) .getTop(); double trPageBottom = WidgetUtil.getBoundingClientRect(tr) .getBottom(); int messageAndButtonsHeight = messageAndButtonsWrapper .getOffsetHeight(); double bottomOfButtons = trPageBottom + messageAndButtonsHeight; return bottomOfButtons < tfootPageTop; } protected void hideOverlay() { if (editorOverlay.getParentElement() == null) { return; } if (pinnedRowHandle != null) { pinnedRowHandle.unpin(); pinnedRowHandle = null; } for (HandlerRegistration r : focusHandlers) { r.removeHandler(); } focusHandlers.clear(); for (Widget w : columnToWidget.values()) { setParent(w, null); } columnToWidget.clear(); if (isBuffered()) { grid.detachWidget(saveButton); grid.detachWidget(cancelButton); } editorOverlay.removeAllChildren(); cellWrapper.removeAllChildren(); frozenCellWrapper.removeAllChildren(); editorOverlay.removeFromParent(); hScrollHandler.removeHandler(); clearEditorColumnErrors(); } private void updateBufferedStyleName() { if (isBuffered()) { editorOverlay.removeClassName("unbuffered"); editorOverlay.addClassName("buffered"); } else { editorOverlay.removeClassName("buffered"); editorOverlay.addClassName("unbuffered"); } } protected void setStylePrimaryName(String primaryName) { if (styleName != null) { editorOverlay.removeClassName(styleName); cellWrapper.removeClassName(styleName + "-cells"); frozenCellWrapper.removeClassName(styleName + "-cells"); messageAndButtonsWrapper.removeClassName(styleName + "-footer"); messageWrapper.removeClassName(styleName + "-message"); buttonsWrapper.removeClassName(styleName + "-buttons"); saveButton.removeStyleName(styleName + "-save"); cancelButton.removeStyleName(styleName + "-cancel"); } styleName = primaryName + "-editor"; editorOverlay.setClassName(styleName); cellWrapper.setClassName(styleName + "-cells"); frozenCellWrapper.setClassName(styleName + "-cells frozen"); messageAndButtonsWrapper.setClassName(styleName + "-footer"); messageWrapper.setClassName(styleName + "-message"); buttonsWrapper.setClassName(styleName + "-buttons"); saveButton.setStyleName(styleName + "-save"); cancelButton.setStyleName(styleName + "-cancel"); } /** * Creates an editor cell corresponding to the given table cell. The * returned element is empty and has the same dimensions and position as * the table cell. * * @param td * the table cell used as a reference * @return an editor cell corresponding to the given cell */ protected Element createCell(TableCellElement td) { DivElement cell = DivElement.as(DOM.createDiv()); double width = WidgetUtil .getRequiredWidthBoundingClientRectDouble(td); double height = WidgetUtil .getRequiredHeightBoundingClientRectDouble(td); setBounds(cell, td.getOffsetLeft(), td.getOffsetTop(), width, height); return cell; } private static void setBounds(Element e, double left, double top, double width, double height) { Style style = e.getStyle(); style.setLeft(left, Unit.PX); style.setTop(top, Unit.PX); style.setWidth(width, Unit.PX); style.setHeight(height, Unit.PX); } private void updateHorizontalScrollPosition() { double scrollLeft = grid.getScrollLeft(); int frozenWidth = frozenCellWrapper.getOffsetWidth(); double newLeft = frozenWidth - scrollLeft; cellWrapper.getStyle().setLeft(newLeft, Unit.PX); // sometimes focus handling twists the editor row out of alignment // with the grid itself and the position needs to be compensated for try { TableRowElement rowElement = grid.getEscalator().getBody() .getRowElement(grid.getEditor().getRow()); int rowLeft = rowElement.getAbsoluteLeft(); int editorLeft = cellWrapper.getAbsoluteLeft(); if (editorLeft != rowLeft + frozenWidth) { cellWrapper.getStyle() .setLeft(newLeft + rowLeft - editorLeft, Unit.PX); } } catch (IllegalStateException e) { // IllegalStateException may occur if user has scrolled Grid so // that Escalator has updated, and row under Editor is no longer // there } } /** * Moves the editor overlay on scroll so that it stays on top of the * edited row. This will also snap the editor to top or bottom of the * row container if the edited row is scrolled out of the visible area. */ private void updateVerticalScrollPosition() { if (isBuffered()) { return; } double newScrollTop = grid.getScrollTop(); int gridTop = grid.getElement().getAbsoluteTop(); int editorHeight = editorOverlay.getOffsetHeight(); Escalator escalator = grid.getEscalator(); TableSectionElement header = escalator.getHeader().getElement(); int footerTop = escalator.getFooter().getElement().getAbsoluteTop(); int headerBottom = header.getAbsoluteBottom(); double newTop = originalTop - (newScrollTop - originalScrollTop); if (newTop + gridTop < headerBottom) { // Snap editor to top of the row container newTop = header.getOffsetHeight(); } else if (newTop + gridTop > footerTop - editorHeight) { // Snap editor to the bottom of the row container newTop = footerTop - editorHeight - gridTop; } editorOverlay.getStyle().setTop(newTop, Unit.PX); } protected void setGridEnabled(boolean enabled) { // TODO: This should be informed to handler as well so possible // fields can be disabled. setButtonsEnabled(enabled); } private void setButtonsEnabled(boolean enabled) { saveButton.setEnabled(enabled); cancelButton.setEnabled(enabled); } public void setSaveCaption(String saveCaption) throws IllegalArgumentException { if (saveCaption == null) { throw new IllegalArgumentException( "Save caption cannot be null"); } saveButton.setText(saveCaption); } public String getSaveCaption() { return saveButton.getText(); } public void setCancelCaption(String cancelCaption) throws IllegalArgumentException { if (cancelCaption == null) { throw new IllegalArgumentException( "Cancel caption cannot be null"); } cancelButton.setText(cancelCaption); } public String getCancelCaption() { return cancelButton.getText(); } public void setEditorColumnError(Column column, boolean hasError) { if (state != State.ACTIVE && state != State.SAVING) { throw new IllegalStateException("Cannot set cell error " + "status: editor is neither active nor saving."); } if (isEditorColumnError(column) == hasError) { return; } Element editorCell = getWidget(column).getElement() .getParentElement(); if (hasError) { editorCell.addClassName(ERROR_CLASS_NAME); columnErrors.add(column); } else { editorCell.removeClassName(ERROR_CLASS_NAME); columnErrors.remove(column); } } public void clearEditorColumnErrors() { /* * editorOverlay has no children if it's not active, effectively * making this loop a NOOP. */ Element e = editorOverlay.getFirstChildElement(); while (e != null) { e.removeClassName(ERROR_CLASS_NAME); e = e.getNextSiblingElement(); } columnErrors.clear(); } public boolean isEditorColumnError(Column column) { return columnErrors.contains(column); } public void setBuffered(boolean buffered) { this.buffered = buffered; setMessageAndButtonsWrapperVisible(buffered); } public boolean isBuffered() { return buffered; } private void setMessageAndButtonsWrapperVisible(boolean visible) { if (visible) { messageAndButtonsWrapper.getStyle().clearDisplay(); } else { messageAndButtonsWrapper.getStyle().setDisplay(Display.NONE); } } /** * Sets the event handler for this Editor. * * @since 7.6 * @param handler * the new event handler */ public void setEventHandler(EventHandler handler) { eventHandler = handler; } /** * Returns the event handler of this Editor. * * @since 7.6 * @return the current event handler */ public EventHandler getEventHandler() { return eventHandler; } @Override public boolean isWorkPending() { return saveTimeout.isRunning() || bindTimeout.isRunning(); } protected int getElementColumn(Element e) { int frozenCells = frozenCellWrapper.getChildCount(); if (frozenCellWrapper.isOrHasChild(e)) { for (int i = 0; i < frozenCells; ++i) { if (frozenCellWrapper.getChild(i).isOrHasChild(e)) { return i; } } } if (cellWrapper.isOrHasChild(e)) { for (int i = 0; i < cellWrapper.getChildCount(); ++i) { if (cellWrapper.getChild(i).isOrHasChild(e)) { return i + frozenCells; } } } return -1; } } public abstract static class AbstractGridKeyEvent extends KeyEvent { /** * @since 7.7.9 */ public AbstractGridKeyEvent() { } /** * @deprecated This constructor's arguments are no longer used. Use the * no-args constructor instead. */ @Deprecated public AbstractGridKeyEvent(Grid grid, CellReference targetCell) { } protected abstract String getBrowserEventType(); /** * Gets the Grid instance for this event, if it originated from a Grid. * * @return the grid this event originated from, or {@code null} if this * event did not originate from a grid */ public Grid getGrid() { EventTarget target = getNativeEvent().getEventTarget(); if (!Element.is(target)) { return null; } return WidgetUtil.findWidget(Element.as(target), Grid.class, false); } /** * Gets the reference of target cell for this event, if this event * originated from a Grid. * * @return target cell, or {@code null} if this event did not originate * from a grid */ public CellReference getFocusedCell() { return getGrid().getEventCell(); } @Override protected void dispatch(HANDLER handler) { EventTarget target = getNativeEvent().getEventTarget(); Grid grid = getGrid(); if (Element.is(target) && grid != null && !grid.isElementInChildWidget(Element.as(target))) { Section section = Section.FOOTER; final RowContainer container = grid.cellFocusHandler.containerWithFocus; if (container == grid.escalator.getHeader()) { section = Section.HEADER; } else if (container == getGrid().escalator.getBody()) { section = Section.BODY; } doDispatch(handler, section); } } protected abstract void doDispatch(HANDLER handler, Section section); } public abstract static class AbstractGridMouseEvent extends MouseEvent { /** * @since 7.7.9 */ public AbstractGridMouseEvent() { } /** * @deprecated This constructor's arguments are no longer used. Use the * no-args constructor instead. */ @Deprecated public AbstractGridMouseEvent(Grid grid, CellReference targetCell) { } protected abstract String getBrowserEventType(); /** * Gets the Grid instance for this event, if it originated from a Grid. * * @return the grid this event originated from, or {@code null} if this * event did not originate from a grid */ public Grid getGrid() { EventTarget target = getNativeEvent().getEventTarget(); if (!Element.is(target)) { return null; } return WidgetUtil.findWidget(Element.as(target), Grid.class, false); } /** * Gets the reference of target cell for this event, if this event * originated from a Grid. * * @return target cell, or {@code null} if this event did not originate * from a grid */ public CellReference getTargetCell() { Grid grid = getGrid(); if (grid == null) { return null; } return grid.getEventCell(); } @Override protected void dispatch(HANDLER handler) { EventTarget target = getNativeEvent().getEventTarget(); if (!Element.is(target)) { // Target is not an element return; } Grid grid = getGrid(); if (grid == null) { // Target is not an element of a grid return; } Element targetElement = Element.as(target); if (grid.isElementInChildWidget(targetElement)) { // Target is some widget inside of Grid return; } final RowContainer container = grid.escalator .findRowContainer(targetElement); if (container == null) { // No container for given element return; } Section section = Section.FOOTER; if (container == grid.escalator.getHeader()) { section = Section.HEADER; } else if (container == grid.escalator.getBody()) { section = Section.BODY; } doDispatch(handler, section); } protected abstract void doDispatch(HANDLER handler, Section section); } private static final String CUSTOM_STYLE_PROPERTY_NAME = "customStyle"; /** * An initial height that is given to new details rows before rendering the * appropriate widget that we then can be measure * * @see GridSpacerUpdater */ private static final double DETAILS_ROW_INITIAL_HEIGHT = 50; private EventCellReference eventCell = new EventCellReference(this); private class CellFocusHandler { private RowContainer containerWithFocus = escalator.getBody(); private int rowWithFocus = 0; private Range cellFocusRange = Range.withLength(0, 1); private int lastFocusedBodyRow = 0; private int lastFocusedHeaderRow = 0; private int lastFocusedFooterRow = 0; private TableCellElement cellWithFocusStyle = null; private TableRowElement rowWithFocusStyle = null; public CellFocusHandler() { sinkEvents(getNavigationEvents()); } private Cell getFocusedCell() { return new Cell(rowWithFocus, cellFocusRange.getStart(), cellWithFocusStyle); } /** * Sets style names for given cell when needed. */ public void updateFocusedCellStyle(FlyweightCell cell, RowContainer cellContainer) { int cellRow = cell.getRow(); int cellColumn = cell.getColumn(); int colSpan = cell.getColSpan(); boolean columnHasFocus = Range.withLength(cellColumn, colSpan) .intersects(cellFocusRange); if (cellContainer == containerWithFocus) { // Cell is in the current container if (cellRow == rowWithFocus && columnHasFocus) { if (cellWithFocusStyle != cell.getElement()) { // Cell is correct but it does not have focused style if (cellWithFocusStyle != null) { // Remove old focus style setStyleName(cellWithFocusStyle, cellFocusStyleName, false); } cellWithFocusStyle = cell.getElement(); // Add focus style to correct cell. setStyleName(cellWithFocusStyle, cellFocusStyleName, true); } } else if (cellWithFocusStyle == cell.getElement()) { // Due to escalator reusing cells, a new cell has the same // element but is not the focused cell. setStyleName(cellWithFocusStyle, cellFocusStyleName, false); cellWithFocusStyle = null; } } } /** * Sets focus style for the given row if needed. * * @param row * a row object */ public void updateFocusedRowStyle(Row row) { if (rowWithFocus == row.getRow() && containerWithFocus == escalator.getBody()) { if (row.getElement() != rowWithFocusStyle) { // Row should have focus style but does not have it. if (rowWithFocusStyle != null) { setStyleName(rowWithFocusStyle, rowFocusStyleName, false); } rowWithFocusStyle = row.getElement(); setStyleName(rowWithFocusStyle, rowFocusStyleName, true); } } else if (rowWithFocusStyle == row.getElement() || containerWithFocus != escalator.getBody() && rowWithFocusStyle != null) { // Remove focus style. setStyleName(rowWithFocusStyle, rowFocusStyleName, false); rowWithFocusStyle = null; } } /** * Sets the currently focused. *

* NOTE: the column index is the index in DOM, not the logical * column index which includes hidden columns. * * @param rowIndex * the index of the row having focus * @param columnIndexDOM * the index of the cell having focus * @param container * the row container having focus */ private void setCellFocus(int rowIndex, int columnIndexDOM, RowContainer container) { if (container == null || rowIndex == rowWithFocus && cellFocusRange.contains(columnIndexDOM) && container == this.containerWithFocus) { return; } int oldRow = rowWithFocus; rowWithFocus = rowIndex; Range oldRange = cellFocusRange; if (container == escalator.getBody()) { scrollToRow(rowWithFocus); cellFocusRange = Range.withLength(columnIndexDOM, 1); } else { int i = 0; Element cell = container.getRowElement(rowWithFocus) .getFirstChildElement(); do { int colSpan = cell .getPropertyInt(FlyweightCell.COLSPAN_ATTR); Range cellRange = Range.withLength(i, colSpan); if (cellRange.contains(columnIndexDOM)) { cellFocusRange = cellRange; break; } cell = cell.getNextSiblingElement(); ++i; } while (cell != null); } if (columnIndexDOM >= escalator.getColumnConfiguration() .getFrozenColumnCount()) { escalator.scrollToColumn(columnIndexDOM, ScrollDestination.ANY, 10); } if (this.containerWithFocus == container) { if (oldRange.equals(cellFocusRange) && oldRow != rowWithFocus) { refreshRow(oldRow); } else { refreshHeader(); refreshFooter(); } } else { RowContainer oldContainer = this.containerWithFocus; this.containerWithFocus = container; if (oldContainer == escalator.getBody()) { lastFocusedBodyRow = oldRow; } else if (oldContainer == escalator.getHeader()) { lastFocusedHeaderRow = oldRow; } else { lastFocusedFooterRow = oldRow; } if (!oldRange.equals(cellFocusRange)) { refreshHeader(); refreshFooter(); if (oldContainer == escalator.getBody()) { oldContainer.refreshRows(oldRow, 1); } } else { oldContainer.refreshRows(oldRow, 1); } } refreshRow(rowWithFocus); } /** * Sets focus on a cell. * *

* Note: cell focus is not the same as JavaScript's * {@code document.activeElement}. * * @param cell * a cell object */ public void setCellFocus(CellReference cell) { setCellFocus(cell.getRowIndex(), cell.getColumnIndexDOM(), escalator.findRowContainer(cell.getElement())); } /** * Gets list of events that can be used for cell focusing. * * @return list of navigation related event types */ public Collection getNavigationEvents() { return Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.CLICK); } /** * Handle events that can move the cell focus. */ public void handleNavigationEvent(Event event, CellReference cell) { if (event.getType().equals(BrowserEvents.CLICK)) { setCellFocus(cell); // Grid should have focus when clicked. getElement().focus(); } else if (event.getType().equals(BrowserEvents.KEYDOWN)) { int newRow = rowWithFocus; RowContainer newContainer = containerWithFocus; int newColumn = cellFocusRange.getStart(); switch (event.getKeyCode()) { case KeyCodes.KEY_DOWN: ++newRow; break; case KeyCodes.KEY_UP: --newRow; break; case KeyCodes.KEY_RIGHT: if (cellFocusRange.getEnd() >= getVisibleColumns().size()) { return; } newColumn = cellFocusRange.getEnd(); break; case KeyCodes.KEY_LEFT: if (newColumn == 0) { return; } --newColumn; break; case KeyCodes.KEY_TAB: if (event.getShiftKey()) { newContainer = getPreviousContainer(containerWithFocus); } else { newContainer = getNextContainer(containerWithFocus); } if (newContainer == containerWithFocus) { return; } break; case KeyCodes.KEY_HOME: if (newContainer.getRowCount() > 0) { newRow = 0; } break; case KeyCodes.KEY_END: if (newContainer.getRowCount() > 0) { newRow = newContainer.getRowCount() - 1; } break; case KeyCodes.KEY_PAGEDOWN: case KeyCodes.KEY_PAGEUP: if (newContainer.getRowCount() > 0) { boolean down = event .getKeyCode() == KeyCodes.KEY_PAGEDOWN; // If there is a visible focused cell, scroll by one // page from its position. Otherwise, use the first or // the last visible row as the scroll start position. // This avoids jumping when using both keyboard and the // scroll bar for scrolling. int firstVisible = getFirstVisibleRowIndex(); int lastVisible = getLastVisibleRowIndex(); if (newRow < firstVisible || newRow > lastVisible) { newRow = down ? lastVisible : firstVisible; } // Scroll by a little less than the visible area to // account for the possibility that the top and the // bottom row are only partially visible. int moveFocusBy = Math.max(1, lastVisible - firstVisible - 1); moveFocusBy *= down ? 1 : -1; newRow += moveFocusBy; newRow = Math.max(0, Math .min(newContainer.getRowCount() - 1, newRow)); } break; default: return; } if (newContainer != containerWithFocus) { if (newContainer == escalator.getBody()) { newRow = lastFocusedBodyRow; } else if (newContainer == escalator.getHeader()) { newRow = lastFocusedHeaderRow; } else { newRow = lastFocusedFooterRow; } } else if (newRow < 0) { newContainer = getPreviousContainer(newContainer); if (newContainer == containerWithFocus) { newRow = 0; } else if (newContainer == escalator.getBody()) { newRow = getLastVisibleRowIndex(); } else { newRow = newContainer.getRowCount() - 1; } } else if (newRow >= containerWithFocus.getRowCount()) { newContainer = getNextContainer(newContainer); if (newContainer == containerWithFocus) { newRow = containerWithFocus.getRowCount() - 1; } else if (newContainer == escalator.getBody()) { newRow = getFirstVisibleRowIndex(); } else { newRow = 0; } } if (newContainer.getRowCount() == 0) { /* * There are no rows in the container. Can't change the * focused cell. */ return; } event.preventDefault(); event.stopPropagation(); setCellFocus(newRow, newColumn, newContainer); } } private RowContainer getPreviousContainer(RowContainer current) { if (current == escalator.getFooter()) { current = escalator.getBody(); } else if (current == escalator.getBody()) { current = escalator.getHeader(); } else { return current; } if (current.getRowCount() == 0) { return getPreviousContainer(current); } return current; } private RowContainer getNextContainer(RowContainer current) { if (current == escalator.getHeader()) { current = escalator.getBody(); } else if (current == escalator.getBody()) { current = escalator.getFooter(); } else { return current; } if (current.getRowCount() == 0) { return getNextContainer(current); } return current; } private void refreshRow(int row) { containerWithFocus.refreshRows(row, 1); } /** * Offsets the focused cell's range. * * @param offset * offset for fixing focused cell's range */ public void offsetRangeBy(int offset) { cellFocusRange = cellFocusRange.offsetBy(offset); } /** * Informs {@link CellFocusHandler} that certain range of rows has been * added to the Grid body. {@link CellFocusHandler} will fix indices * accordingly. * * @param added * a range of added rows */ public void rowsAddedToBody(Range added) { boolean bodyHasFocus = containerWithFocus == escalator.getBody(); boolean insertionIsAboveFocusedCell = added .getStart() <= rowWithFocus; if (bodyHasFocus && insertionIsAboveFocusedCell) { rowWithFocus += added.length(); rowWithFocus = Math.min(rowWithFocus, escalator.getBody().getRowCount() - 1); refreshRow(rowWithFocus); } } /** * Informs {@link CellFocusHandler} that certain range of rows has been * removed from the Grid body. {@link CellFocusHandler} will fix indices * accordingly. * * @param removed * a range of removed rows */ public void rowsRemovedFromBody(Range removed) { if (containerWithFocus != escalator.getBody()) { return; } else if (!removed.contains(rowWithFocus)) { if (removed.getStart() > rowWithFocus) { return; } rowWithFocus = rowWithFocus - removed.length(); } else { if (containerWithFocus.getRowCount() > removed.getEnd()) { rowWithFocus = removed.getStart(); } else if (removed.getStart() > 0) { rowWithFocus = removed.getStart() - 1; } else { if (escalator.getHeader().getRowCount() > 0) { rowWithFocus = Math.min(lastFocusedHeaderRow, escalator.getHeader().getRowCount() - 1); containerWithFocus = escalator.getHeader(); } else if (escalator.getFooter().getRowCount() > 0) { rowWithFocus = Math.min(lastFocusedFooterRow, escalator.getFooter().getRowCount() - 1); containerWithFocus = escalator.getFooter(); } } } refreshRow(rowWithFocus); } } private final List> browserEventHandlers = new ArrayList>(); private CellStyleGenerator cellStyleGenerator; private RowStyleGenerator rowStyleGenerator; private RowReference rowReference = new RowReference(this); private CellReference cellReference = new CellReference(rowReference); private RendererCellReference rendererCellReference = new RendererCellReference( (RowReference) rowReference); public final class SelectionColumn extends Column implements GridEnabledHandler { private boolean initDone = false; private boolean selected = false; private CheckBox selectAllCheckBox; private boolean userSelectionAllowed = true; private boolean enabled = true; private HandlerRegistration headerClickHandler; SelectionColumn(final Renderer selectColumnRenderer) { super(selectColumnRenderer); addEnabledHandler(this); } void initDone() { setWidth(-1); setEditable(false); setResizable(false); initDone = true; } @Override protected void setDefaultHeaderContent(HeaderCell selectionCell) { /* * TODO: Currently the select all check box is shown when multi * selection is in use. This might result in malfunctions if no * SelectAllHandlers are present. * * Later on this could be fixed so that it check such handlers * exist. */ final SelectionModel.Multi model = (Multi) getSelectionModel(); if (selectAllCheckBox == null) { selectAllCheckBox = GWT.create(CheckBox.class); selectAllCheckBox.setEnabled(enabled && userSelectionAllowed); selectAllCheckBox.setStylePrimaryName( getStylePrimaryName() + SELECT_ALL_CHECKBOX_CLASSNAME); selectAllCheckBox.addValueChangeHandler( new ValueChangeHandler() { @Override public void onValueChange( ValueChangeEvent event) { if (!isUserSelectionAllowed()) { return; } if (event.getValue()) { fireEvent(new SelectAllEvent(model)); selected = true; } else { model.deselectAll(); selected = false; } } }); selectAllCheckBox.setValue(selected); headerClickHandler = addHeaderClickHandler( new HeaderClickHandler() { @Override public void onClick(GridClickEvent event) { if (!userSelectionAllowed) { return; } CellReference targetCell = event .getTargetCell(); int defaultRowIndex = getHeader().getRows() .indexOf(getDefaultHeaderRow()); if (targetCell.getColumnIndex() == 0 && targetCell .getRowIndex() == defaultRowIndex) { selectAllCheckBox.setValue( !selectAllCheckBox.getValue(), true); } } }); // Select all with space when "select all" cell is active addHeaderKeyUpHandler(new HeaderKeyUpHandler() { @Override public void onKeyUp(GridKeyUpEvent event) { if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE) { return; } if (!isUserSelectionAllowed()) { return; } HeaderRow targetHeaderRow = getHeader() .getRow(event.getFocusedCell().getRowIndex()); if (!targetHeaderRow.isDefault()) { return; } if (event.getFocusedCell() .getColumn() == SelectionColumn.this) { // Send events to ensure state is updated selectAllCheckBox.setValue( !selectAllCheckBox.getValue(), true); } } }); } else { for (HeaderRow row : header.getRows()) { if (row.getCell(this) .getType() == GridStaticCellType.WIDGET) { // Detach from old header. row.getCell(this).setText(""); } } } selectionCell.setWidget(selectAllCheckBox); } @Override public Column setWidth(double pixels) { if (pixels != getWidth() && initDone) { throw new UnsupportedOperationException("The selection " + "column cannot be modified after init"); } else { super.setWidth(pixels); } return this; } @Override public Boolean getValue(T row) { return Boolean.valueOf(isSelected(row)); } @Override public Column setExpandRatio(int ratio) { throw new UnsupportedOperationException( "can't change the expand ratio of the selection column"); } @Override public int getExpandRatio() { return 0; } @Override public Column setMaximumWidth(double pixels) { throw new UnsupportedOperationException( "can't change the maximum width of the selection column"); } @Override public double getMaximumWidth() { return -1; } @Override public Column setMinimumWidth(double pixels) { throw new UnsupportedOperationException( "can't change the minimum width of the selection column"); } @Override public double getMinimumWidth() { return -1; } @Override public Column setEditable(boolean editable) { if (initDone) { throw new UnsupportedOperationException( "can't set the selection column editable"); } super.setEditable(editable); return this; } /** * Sets whether the selection column is enabled. * * @since 7.7 * @param enabled * true to enable the column, false * to disable it. */ public void setEnabled(boolean enabled) { this.enabled = enabled; if (selectAllCheckBox != null) { selectAllCheckBox.setEnabled(enabled && userSelectionAllowed); } } @Override public void onEnabled(boolean enabled) { setEnabled(enabled); } /** * Sets whether the user is allowed to change the selection. * * @param userSelectionAllowed * true if the user is allowed to change the * selection, false otherwise * @since 7.7.7 */ public void setUserSelectionAllowed(boolean userSelectionAllowed) { if (userSelectionAllowed == this.userSelectionAllowed) { return; } this.userSelectionAllowed = userSelectionAllowed; // Update checkbox state setEnabled(enabled); // Re-render select checkboxes getEscalator().getBody().refreshRows(0, getEscalator().getBody().getRowCount()); } private void cleanup() { if (headerClickHandler != null) { headerClickHandler.removeHandler(); headerClickHandler = null; } } } /** * Helper class for performing sorting through the user interface. Controls * the sort() method, reporting USER as the event originator. This is a * completely internal class, and is, as such, safe to re-name should a more * descriptive name come to mind. */ private final class UserSorter { private final Timer timer; private boolean scheduledMultisort; private Column column; private UserSorter() { timer = new Timer() { @Override public void run() { UserSorter.this.sort(column, scheduledMultisort); } }; } /** * Toggle sorting for a cell. If the multisort parameter is set to true, * the cell's sort order is modified as a natural part of a multi-sort * chain. If false, the sorting order is set to ASCENDING for that * cell's column. If that column was already the only sorted column in * the Grid, the sort direction is flipped. * * @param cell * a valid cell reference * @param multisort * whether the sort command should act as a multi-sort stack * or not */ public void sort(Column column, boolean multisort) { if (!columns.contains(column)) { throw new IllegalArgumentException( "Given column is not a column in this grid. " + column.toString()); } if (!column.isSortable()) { return; } final SortOrder so = getSortOrder(column); if (multisort) { // If the sort order exists, replace existing value with its // opposite if (so != null) { final int idx = sortOrder.indexOf(so); sortOrder.set(idx, so.getOpposite()); } else { // If it doesn't, just add a new sort order to the end of // the list sortOrder.add(new SortOrder(column)); } } else { // Since we're doing single column sorting, first clear the // list. Then, if the sort order existed, add its opposite, // otherwise just add a new sort value int items = sortOrder.size(); sortOrder.clear(); if (so != null && items == 1) { sortOrder.add(so.getOpposite()); } else { sortOrder.add(new SortOrder(column)); } } // sortOrder has been changed; tell the Grid to re-sort itself by // user request. Grid.this.sort(true); } /** * Perform a sort after a delay. * * @param delay * delay, in milliseconds */ public void sortAfterDelay(int delay, boolean multisort) { column = eventCell.getColumn(); scheduledMultisort = multisort; timer.schedule(delay); } /** * Check if a delayed sort command has been issued but not yet carried * out. * * @return a boolean value */ public boolean isDelayedSortScheduled() { return timer.isRunning(); } /** * Cancel a scheduled sort. */ public void cancelDelayedSort() { timer.cancel(); } } /** * @see Grid#autoColumnWidthsRecalculator */ private class AutoColumnWidthsRecalculator { private double lastCalculatedInnerWidth = -1; private final ScheduledCommand calculateCommand = new ScheduledCommand() { @Override public void execute() { if (!isScheduled) { // something cancelled running this. return; } if (header.markAsDirty || footer.markAsDirty) { if (rescheduleCount < 10) { /* * Headers and footers are rendered as finally, this way * we re-schedule this loop as finally, at the end of * the queue, so that the headers have a chance to * render themselves. */ Scheduler.get().scheduleFinally(this); rescheduleCount++; } else { /* * We've tried too many times reschedule finally. Seems * like something is being deferred. Let the queue * execute and retry again. */ rescheduleCount = 0; Scheduler.get().scheduleDeferred(this); } } else if (currentDataAvailable.isEmpty() && dataSource.isWaitingForData()) { // No data available yet but something is incoming soon Scheduler.get().scheduleDeferred(this); } else { calculate(); } } }; private int rescheduleCount = 0; private boolean isScheduled; /** * Calculates and applies column widths, taking into account fixed * widths and column expand rules * * @param immediately * true if the widths should be executed * immediately (ignoring lazy loading completely), or * false if the command should be run after a * while (duplicate non-immediately invocations are ignored). * @see Column#setWidth(double) * @see Column#setExpandRatio(int) * @see Column#setMinimumWidth(double) * @see Column#setMaximumWidth(double) */ public void schedule() { if (!isScheduled && isAttached()) { isScheduled = true; Scheduler.get().scheduleFinally(calculateCommand); } } private void calculate() { isScheduled = false; rescheduleCount = 0; assert !(currentDataAvailable.isEmpty() && dataSource .isWaitingForData()) : "Trying to calculate column widths without data while data is still being fetched."; if (columnsAreGuaranteedToBeWiderThanGrid()) { applyColumnWidths(); } else { applyColumnWidthsWithExpansion(); } // Update latest width to prevent recalculate on height change. lastCalculatedInnerWidth = escalator.getInnerWidth(); } private boolean columnsAreGuaranteedToBeWiderThanGrid() { double freeSpace = escalator.getInnerWidth(); for (Column column : getVisibleColumns()) { if (column.getWidth() >= 0) { freeSpace -= column.getWidth(); } else if (column.getMinimumWidth() >= 0) { freeSpace -= column.getMinimumWidth(); } } return freeSpace < 0; } @SuppressWarnings("boxing") private void applyColumnWidths() { /* Step 1: Apply all column widths as they are. */ Map selfWidths = new LinkedHashMap(); List> columns = getVisibleColumns(); for (int index = 0; index < columns.size(); index++) { selfWidths.put(index, columns.get(index).getWidth()); } Grid.this.escalator.getColumnConfiguration() .setColumnWidths(selfWidths); /* * Step 2: Make sure that each column ends up obeying their min/max * width constraints if defined as autowidth. If constraints are * violated, fix it. */ Map constrainedWidths = new LinkedHashMap(); for (int index = 0; index < columns.size(); index++) { Column column = columns.get(index); boolean hasAutoWidth = column.getWidth() < 0; if (!hasAutoWidth) { continue; } // TODO: bug: these don't honor the CSS max/min. :( double actualWidth = column.getWidthActual(); if (actualWidth < getMinWidth(column)) { constrainedWidths.put(index, column.getMinimumWidth()); } else if (actualWidth > getMaxWidth(column)) { constrainedWidths.put(index, column.getMaximumWidth()); } } Grid.this.escalator.getColumnConfiguration() .setColumnWidths(constrainedWidths); } private void applyColumnWidthsWithExpansion() { boolean defaultExpandRatios = true; int totalRatios = 0; double reservedPixels = 0; final Set> columnsToExpand = new HashSet>(); List> nonFixedColumns = new ArrayList>(); Map columnSizes = new HashMap(); final List> visibleColumns = getVisibleColumns(); /* * Set all fixed widths and also calculate the size-to-fit widths * for the autocalculated columns. * * This way we know with how many pixels we have left to expand the * rest. */ for (Column column : visibleColumns) { final double widthAsIs = column.getWidth(); final boolean isFixedWidth = widthAsIs >= 0; // Check for max width just to be sure we don't break the limits final double widthFixed = Math.max( Math.min(getMaxWidth(column), widthAsIs), column.getMinimumWidth()); defaultExpandRatios = defaultExpandRatios && (column.getExpandRatio() == -1 || column == selectionColumn); if (isFixedWidth) { columnSizes.put(visibleColumns.indexOf(column), widthFixed); reservedPixels += widthFixed; } else { nonFixedColumns.add(column); columnSizes.put(visibleColumns.indexOf(column), -1.0d); } } setColumnSizes(columnSizes); for (Column column : nonFixedColumns) { final int expandRatio = defaultExpandRatios ? 1 : column.getExpandRatio(); final double maxWidth = getMaxWidth(column); final double newWidth = Math.min(maxWidth, column.getWidthActual()); boolean shouldExpand = newWidth < maxWidth && expandRatio > 0 && column != selectionColumn; if (shouldExpand) { totalRatios += expandRatio; columnsToExpand.add(column); } reservedPixels += newWidth; columnSizes.put(visibleColumns.indexOf(column), newWidth); } /* * Now that we know how many pixels we need at the very least, we * can distribute the remaining pixels to all columns according to * their expand ratios. */ double pixelsToDistribute = escalator.getInnerWidth() - reservedPixels; if (pixelsToDistribute <= 0 || totalRatios <= 0) { if (pixelsToDistribute <= 0) { // Set column sizes for expanding columns setColumnSizes(columnSizes); } return; } /* * Check for columns that hit their max width. Adjust * pixelsToDistribute and totalRatios accordingly. Recheck. Stop * when no new columns hit their max width */ boolean aColumnHasMaxedOut; do { aColumnHasMaxedOut = false; final double widthPerRatio = pixelsToDistribute / totalRatios; final Iterator> i = columnsToExpand.iterator(); while (i.hasNext()) { final Column column = i.next(); final int expandRatio = getExpandRatio(column, defaultExpandRatios); final int columnIndex = visibleColumns.indexOf(column); final double autoWidth = columnSizes.get(columnIndex); final double maxWidth = getMaxWidth(column); double expandedWidth = autoWidth + widthPerRatio * expandRatio; if (maxWidth <= expandedWidth) { i.remove(); totalRatios -= expandRatio; aColumnHasMaxedOut = true; pixelsToDistribute -= maxWidth - autoWidth; columnSizes.put(columnIndex, maxWidth); } } } while (aColumnHasMaxedOut); if (totalRatios <= 0 && columnsToExpand.isEmpty()) { setColumnSizes(columnSizes); return; } assert pixelsToDistribute > 0 : "We've run out of pixels to distribute (" + pixelsToDistribute + "px to " + totalRatios + " ratios between " + columnsToExpand.size() + " columns)"; assert totalRatios > 0 && !columnsToExpand .isEmpty() : "Bookkeeping out of sync. Ratios: " + totalRatios + " Columns: " + columnsToExpand.size(); /* * If we still have anything left, distribute the remaining pixels * to the remaining columns. */ final double widthPerRatio; int leftOver = 0; if (BrowserInfo.get().isIE8() || BrowserInfo.get().isIE9() || BrowserInfo.getBrowserString().contains("PhantomJS")) { // These browsers report subpixels as integers. this usually // results into issues.. widthPerRatio = (int) (pixelsToDistribute / totalRatios); leftOver = (int) (pixelsToDistribute - widthPerRatio * totalRatios); } else { widthPerRatio = pixelsToDistribute / totalRatios; } for (Column column : columnsToExpand) { final int expandRatio = getExpandRatio(column, defaultExpandRatios); final int columnIndex = visibleColumns.indexOf(column); final double autoWidth = columnSizes.get(columnIndex); double totalWidth = autoWidth + widthPerRatio * expandRatio; if (leftOver > 0) { totalWidth += 1; leftOver--; } columnSizes.put(columnIndex, totalWidth); totalRatios -= expandRatio; } assert totalRatios == 0 : "Bookkeeping error: there were still some ratios left undistributed: " + totalRatios; /* * Check the guarantees for minimum width and scoot back the columns * that don't care. */ boolean minWidthsCausedReflows; do { minWidthsCausedReflows = false; /* * First, let's check which columns were too cramped, and expand * them. Also keep track on how many pixels we grew - we need to * remove those pixels from other columns */ double pixelsToRemoveFromOtherColumns = 0; for (Column column : visibleColumns) { /* * We can't iterate over columnsToExpand, even though that * would be convenient. This is because some column without * an expand ratio might still have a min width - those * wouldn't show up in that set. */ double minWidth = getMinWidth(column); final int columnIndex = visibleColumns.indexOf(column); double currentWidth = columnSizes.get(columnIndex); boolean hasAutoWidth = column.getWidth() < 0; if (hasAutoWidth && currentWidth < minWidth) { columnSizes.put(columnIndex, minWidth); pixelsToRemoveFromOtherColumns += minWidth - currentWidth; minWidthsCausedReflows = true; /* * Remove this column form the set if it exists. This * way we make sure that it doesn't get shrunk in the * next step. */ columnsToExpand.remove(column); } } /* * Now we need to shrink the remaining columns according to * their ratios. Recalculate the sum of remaining ratios. */ totalRatios = 0; for (Column column : columnsToExpand) { totalRatios += getExpandRatio(column, defaultExpandRatios); } final double pixelsToRemovePerRatio = pixelsToRemoveFromOtherColumns / totalRatios; for (Column column : columnsToExpand) { final double pixelsToRemove = pixelsToRemovePerRatio * getExpandRatio(column, defaultExpandRatios); int colIndex = visibleColumns.indexOf(column); columnSizes.put(colIndex, columnSizes.get(colIndex) - pixelsToRemove); } } while (minWidthsCausedReflows); // Finally set all the column sizes. setColumnSizes(columnSizes); } private void setColumnSizes(Map columnSizes) { // Set all widths at once escalator.getColumnConfiguration().setColumnWidths(columnSizes); } private int getExpandRatio(Column column, boolean defaultExpandRatios) { int expandRatio = column.getExpandRatio(); if (expandRatio > 0) { return expandRatio; } else if (expandRatio < 0) { assert defaultExpandRatios : "No columns should've expanded"; return 1; } else { assert false : "this method should've not been called at all if expandRatio is 0"; return 0; } } /** * Returns the maximum width of the column, or {@link Double#MAX_VALUE} * if defined as negative. */ private double getMaxWidth(Column column) { double maxWidth = column.getMaximumWidth(); if (maxWidth >= 0) { return maxWidth; } else { return Double.MAX_VALUE; } } /** * Returns the minimum width of the column, or {@link Double#MIN_VALUE} * if defined as negative. */ private double getMinWidth(Column column) { double minWidth = column.getMinimumWidth(); if (minWidth >= 0) { return minWidth; } else { return Double.MIN_VALUE; } } /** * Check whether the auto width calculation is currently scheduled. * * @return true if auto width calculation is currently * scheduled */ public boolean isScheduled() { return isScheduled; } } private class GridSpacerUpdater implements SpacerUpdater { private static final String STRIPE_CLASSNAME = "stripe"; private final Map elementToWidgetMap = new HashMap(); @Override public void init(Spacer spacer) { initTheming(spacer); int rowIndex = spacer.getRow(); Widget detailsWidget = null; try { detailsWidget = detailsGenerator.getDetails(rowIndex); } catch (Throwable e) { getLogger().log(Level.SEVERE, "Exception while generating details for row " + rowIndex, e); } final double spacerHeight; Element spacerElement = spacer.getElement(); if (detailsWidget == null) { spacerElement.removeAllChildren(); spacerHeight = DETAILS_ROW_INITIAL_HEIGHT; } else { Element element = detailsWidget.getElement(); spacerElement.appendChild(element); setParent(detailsWidget, Grid.this); Widget previousWidget = elementToWidgetMap.put(element, detailsWidget); assert previousWidget == null : "Overwrote a pre-existing widget on row " + rowIndex + " without proper removal first."; /* * Once we have the content properly inside the DOM, we should * re-measure it to make sure that it's the correct height. * * This is rather tricky, since the row (tr) will get the * height, but the spacer cell (td) has the borders, which * should go on top of the previous row and next row. */ double contentHeight; if (detailsGenerator instanceof HeightAwareDetailsGenerator) { HeightAwareDetailsGenerator sadg = (HeightAwareDetailsGenerator) detailsGenerator; contentHeight = sadg.getDetailsHeight(rowIndex); } else { contentHeight = WidgetUtil .getRequiredHeightBoundingClientRectDouble(element); } double borderTopAndBottomHeight = WidgetUtil .getBorderTopAndBottomThickness(spacerElement); double measuredHeight = contentHeight + borderTopAndBottomHeight; assert getElement().isOrHasChild( spacerElement) : "The spacer element wasn't in the DOM during measurement, but was assumed to be."; spacerHeight = measuredHeight; } escalator.getBody().setSpacer(rowIndex, spacerHeight); if (getHeightMode() == HeightMode.UNDEFINED) { setHeightByRows(getEscalator().getBody().getRowCount()); } } @Override public void destroy(Spacer spacer) { Element spacerElement = spacer.getElement(); assert getElement().isOrHasChild(spacerElement) : "Trying " + "to destroy a spacer that is not connected to this " + "Grid's DOM. (row: " + spacer.getRow() + ", element: " + spacerElement + ")"; Widget detailsWidget = elementToWidgetMap .remove(spacerElement.getFirstChildElement()); if (detailsWidget != null) { /* * The widget may be null here if the previous generator * returned a null widget. */ assert spacerElement.getFirstChild() != null : "The " + "details row to destroy did not contain a widget - " + "probably removed by something else without " + "permission? (row: " + spacer.getRow() + ", element: " + spacerElement + ")"; setParent(detailsWidget, null); spacerElement.removeAllChildren(); if (getHeightMode() == HeightMode.UNDEFINED) { // update spacer height escalator.getBody().setSpacer(spacer.getRow(), 0); setHeightByRows(getEscalator().getBody().getRowCount()); } } } private void initTheming(Spacer spacer) { Element spacerRoot = spacer.getElement(); if (spacer.getRow() % 2 == 1) { spacerRoot.getParentElement().addClassName(STRIPE_CLASSNAME); } else { spacerRoot.getParentElement().removeClassName(STRIPE_CLASSNAME); } } } /** * Sidebar displaying toggles for hidable columns and custom widgets * provided by the application. *

* The button for opening the sidebar is automatically visible inside the * grid, if it contains any column hiding options or custom widgets. The * column hiding toggles and custom widgets become visible once the sidebar * has been opened. * * @since 7.5.0 */ private static class Sidebar extends Composite implements HasEnabled { private final ClickHandler openCloseButtonHandler = new ClickHandler() { @Override public void onClick(ClickEvent event) { if (!isOpen()) { open(); } else { close(); } } }; private final FlowPanel rootContainer; private final FlowPanel content; private final MenuBar menuBar; private final Button openCloseButton; private final Grid grid; private Overlay overlay; private Sidebar(Grid grid) { this.grid = grid; rootContainer = new FlowPanel(); initWidget(rootContainer); openCloseButton = new Button(); openCloseButton.addClickHandler(openCloseButtonHandler); rootContainer.add(openCloseButton); content = new FlowPanel() { @Override public boolean remove(Widget w) { // Check here to catch child.removeFromParent() calls boolean removed = super.remove(w); if (removed) { updateVisibility(); } return removed; } }; createOverlay(); menuBar = new MenuBar(true) { @Override public MenuItem insertItem(MenuItem item, int beforeIndex) throws IndexOutOfBoundsException { if (getParent() == null) { content.insert(this, 0); updateVisibility(); } return super.insertItem(item, beforeIndex); } @Override public void removeItem(MenuItem item) { super.removeItem(item); if (getItems().isEmpty()) { menuBar.removeFromParent(); } } @Override public void onBrowserEvent(Event event) { // selecting a item with enter will lose the focus and // selected item, which means that further keyboard // selection won't work unless we do this: if (event.getTypeInt() == Event.ONKEYDOWN && event.getKeyCode() == KeyCodes.KEY_ENTER) { final MenuItem item = getSelectedItem(); super.onBrowserEvent(event); Scheduler.get() .scheduleDeferred(new ScheduledCommand() { @Override public void execute() { selectItem(item); focus(); } }); } else { super.onBrowserEvent(event); } } }; KeyDownHandler keyDownHandler = new KeyDownHandler() { @Override public void onKeyDown(KeyDownEvent event) { if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) { close(); } } }; openCloseButton.addDomHandler(keyDownHandler, KeyDownEvent.getType()); menuBar.addDomHandler(keyDownHandler, KeyDownEvent.getType()); } /** * Creates and initializes the overlay. */ private void createOverlay() { overlay = GWT.create(Overlay.class); overlay.setOwner(grid); overlay.setAutoHideEnabled(true); overlay.addStyleDependentName("popup"); overlay.add(content); overlay.addAutoHidePartner(rootContainer.getElement()); overlay.addCloseHandler(new CloseHandler() { @Override public void onClose(CloseEvent event) { removeStyleName("open"); addStyleName("closed"); } }); overlay.setFitInWindow(true); } /** * Opens the sidebar if not yet opened. Opening the sidebar has no * effect if it is empty. */ public void open() { if (!isOpen() && isInDOM()) { addStyleName("open"); removeStyleName("closed"); overlay.showRelativeTo(rootContainer); } } /** * Closes the sidebar if not yet closed. */ public void close() { overlay.hide(); } /** * Returns whether the sidebar is open or not. * * @return true if open, false if not */ public boolean isOpen() { return overlay != null && overlay.isShowing(); } @Override public void setStylePrimaryName(String styleName) { super.setStylePrimaryName(styleName); overlay.setStylePrimaryName(styleName); content.setStylePrimaryName(styleName + "-content"); openCloseButton.setStylePrimaryName(styleName + "-button"); if (isOpen()) { addStyleName("open"); removeStyleName("closed"); } else { removeStyleName("open"); addStyleName("closed"); } } @Override public void addStyleName(String style) { super.addStyleName(style); overlay.addStyleName(style); } @Override public void removeStyleName(String style) { super.removeStyleName(style); overlay.removeStyleName(style); } private void setHeightToHeaderCellHeight() { RowContainer header = grid.escalator.getHeader(); if (header.getRowCount() == 0 || !header.getRowElement(0).hasChildNodes()) { getLogger().info( "No header cell available when calculating sidebar button height"); openCloseButton.setHeight(header.getDefaultRowHeight() + "px"); return; } Element firstHeaderCell = header.getRowElement(0) .getFirstChildElement(); double height = WidgetUtil .getRequiredHeightBoundingClientRectDouble(firstHeaderCell) - WidgetUtil.measureVerticalBorder(getElement()) / 2; openCloseButton.setHeight(height + "px"); } private void updateVisibility() { final boolean hasWidgets = content.getWidgetCount() > 0; final boolean isVisible = isInDOM(); if (isVisible && !hasWidgets) { Grid.setParent(this, null); getElement().removeFromParent(); } else if (!isVisible && hasWidgets) { close(); grid.getElement().appendChild(getElement()); Grid.setParent(this, grid); // border calculation won't work until attached setHeightToHeaderCellHeight(); } } private boolean isInDOM() { return getParent() != null; } @Override protected void onAttach() { super.onAttach(); // make sure the button will get correct height if the button should // be visible when the grid is rendered the first time. Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { setHeightToHeaderCellHeight(); } }); } @Override public boolean isEnabled() { return openCloseButton.isEnabled(); } @Override public void setEnabled(boolean enabled) { if (!enabled && isOpen()) { close(); } openCloseButton.setEnabled(enabled); } } /** * UI and functionality related to hiding columns with toggles in the * sidebar. */ private final class ColumnHider { /** Map from columns to their hiding toggles, component might change */ private Map, MenuItem> columnToHidingToggleMap = new HashMap, MenuItem>(); /** * When column is being hidden with a toggle, do not refresh toggles for * no reason. Also helps for keeping the keyboard navigation working. */ private boolean hidingColumn; private void updateColumnHidable(final Column column) { if (column.isHidable()) { MenuItem toggle = columnToHidingToggleMap.get(column); if (toggle == null) { toggle = createToggle(column); } toggle.setStyleName("hidden", column.isHidden()); } else if (columnToHidingToggleMap.containsKey(column)) { sidebar.menuBar .removeItem(columnToHidingToggleMap.remove(column)); } updateTogglesOrder(); } private MenuItem createToggle(final Column column) { MenuItem toggle = new MenuItem(createHTML(column), true, new ScheduledCommand() { @Override public void execute() { hidingColumn = true; column.setHidden(!column.isHidden(), true); hidingColumn = false; } }); toggle.addStyleName("column-hiding-toggle"); columnToHidingToggleMap.put(column, toggle); return toggle; } private String createHTML(Column column) { final StringBuffer buf = new StringBuffer(); buf.append("

"); String caption = column.getHidingToggleCaption(); if (caption == null) { caption = column.headerCaption; } buf.append(caption); buf.append("
"); return buf.toString(); } private void updateTogglesOrder() { if (!hidingColumn) { int lastIndex = 0; for (Column column : getColumns()) { if (column.isHidable()) { final MenuItem menuItem = columnToHidingToggleMap .get(column); sidebar.menuBar.removeItem(menuItem); sidebar.menuBar.insertItem(menuItem, lastIndex++); } } } } private void updateHidingToggle(Column column) { if (column.isHidable()) { MenuItem toggle = columnToHidingToggleMap.get(column); toggle.setHTML(createHTML(column)); toggle.setStyleName("hidden", column.isHidden()); } // else we can just ignore } private void removeColumnHidingToggle(Column column) { sidebar.menuBar.removeItem(columnToHidingToggleMap.get(column)); } } /** * Escalator used internally by grid to render the rows */ private Escalator escalator = GWT.create(Escalator.class); private final Header header = GWT.create(Header.class); private final Footer footer = GWT.create(Footer.class); private final Sidebar sidebar = new Sidebar(this); /** * List of columns in the grid. Order defines the visible order. */ private List> columns = new ArrayList>(); /** * The datasource currently in use. Note: it is null * on initialization, but not after that. */ private DataSource dataSource; private Registration changeHandler; /** * Currently available row range in DataSource. */ private Range currentDataAvailable = Range.withLength(0, 0); /** * The number of frozen columns, 0 freezes the selection column if * displayed, -1 also prevents selection col from freezing. */ private int frozenColumnCount = 0; /** * Current sort order. The (private) sort() method reads this list to * determine the order in which to present rows. */ private List sortOrder = new ArrayList(); private Renderer selectColumnRenderer = null; private SelectionColumn selectionColumn; private String rowStripeStyleName; private String rowHasDataStyleName; private String rowSelectedStyleName; private String cellFocusStyleName; private String rowFocusStyleName; /** * Current selection model. */ private SelectionModel selectionModel; protected final CellFocusHandler cellFocusHandler; private final UserSorter sorter = new UserSorter(); private final Editor editor = GWT.create(Editor.class); /** * The cell a click event originated from *

* This is a workaround to make Chrome work like Firefox. In Chrome, * normally if you start a drag on one cell and release on: *

    *
  • that same cell, the click event is that <td>. *
  • a cell on that same row, the click event is the parent * <tr>. *
  • a cell on another row, the click event is the table section ancestor * ({@code }, {@code } or {@code }). *
* * @see #onBrowserEvent(Event) */ private Cell cellOnPrevMouseDown; /** * A scheduled command to re-evaluate the widths of all columns * that have calculated widths. Most probably called because * minwidth/maxwidth/expandratio has changed. */ private final AutoColumnWidthsRecalculator autoColumnWidthsRecalculator = new AutoColumnWidthsRecalculator(); private boolean enabled = true; private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; private GridSpacerUpdater gridSpacerUpdater = new GridSpacerUpdater(); /** A set keeping track of the indices of all currently open details */ private Set visibleDetails = new HashSet(); /** A set of indices of details to reopen after detach and on attach */ private final Set reattachVisibleDetails = new HashSet(); private boolean columnReorderingAllowed; private ColumnHider columnHider = new ColumnHider(); private DragAndDropHandler dndHandler = new DragAndDropHandler(); private AutoScroller autoScroller = new AutoScroller(this); private ColumnResizeMode columnResizeMode = ColumnResizeMode.ANIMATED; private DragAndDropHandler.DragAndDropCallback headerCellDndCallback = new DragAndDropCallback() { private final AutoScrollerCallback autoScrollerCallback = new AutoScrollerCallback() { @Override public void onAutoScroll(int scrollDiff) { autoScrollX = scrollDiff; onDragUpdate(null); } @Override public void onAutoScrollReachedMin() { // make sure the drop marker is visible on the left autoScrollX = 0; updateDragDropMarker(clientX); } @Override public void onAutoScrollReachedMax() { // make sure the drop marker is visible on the right autoScrollX = 0; updateDragDropMarker(clientX); } }; /** * Elements for displaying the dragged column(s) and drop marker * properly */ private Element table; private Element tableHeader; /** Marks the column drop location */ private Element dropMarker; /** A copy of the dragged column(s), moves with cursor. */ private Element dragElement; /** Tracks index of the column whose left side the drop would occur */ private int latestColumnDropIndex; /** * Map of possible drop positions for the column and the corresponding * column index. */ private final TreeMap possibleDropPositions = new TreeMap(); /** * Makes sure that drag cancel doesn't cause anything unwanted like sort */ private HandlerRegistration columnSortPreventRegistration; private int clientX; /** How much the grid is being auto scrolled while dragging. */ private int autoScrollX; /** Captures the value of the focused column before reordering */ private int focusedColumnIndex; /** Offset caused by the drag and drop marker width */ private double dropMarkerWidthOffset; private void initHeaderDragElementDOM() { if (table == null) { tableHeader = DOM.createTHead(); dropMarker = DOM.createDiv(); tableHeader.appendChild(dropMarker); table = DOM.createTable(); table.appendChild(tableHeader); table.setClassName("header-drag-table"); } // update the style names on each run in case primary name has been // modified tableHeader.setClassName( escalator.getHeader().getElement().getClassName()); dropMarker.setClassName(getStylePrimaryName() + "-drop-marker"); int topOffset = 0; for (int i = 0; i < eventCell.getRowIndex(); i++) { topOffset += escalator.getHeader().getRowElement(i) .getFirstChildElement().getOffsetHeight(); } tableHeader.getStyle().setTop(topOffset, Unit.PX); getElement().appendChild(table); dropMarkerWidthOffset = WidgetUtil .getRequiredWidthBoundingClientRectDouble(dropMarker) / 2; } @Override public void onDragUpdate(Event e) { if (e != null) { clientX = WidgetUtil.getTouchOrMouseClientX(e); autoScrollX = 0; } resolveDragElementHorizontalPosition(clientX); updateDragDropMarker(clientX); } private void updateDragDropMarker(final int clientX) { final double scrollLeft = getScrollLeft(); final double cursorXCoordinate = clientX - escalator.getHeader().getElement().getAbsoluteLeft(); final Entry cellEdgeOnRight = possibleDropPositions .ceilingEntry(cursorXCoordinate); final Entry cellEdgeOnLeft = possibleDropPositions .floorEntry(cursorXCoordinate); final double diffToRightEdge = cellEdgeOnRight == null ? Double.MAX_VALUE : cellEdgeOnRight.getKey() - cursorXCoordinate; final double diffToLeftEdge = cellEdgeOnLeft == null ? Double.MAX_VALUE : cursorXCoordinate - cellEdgeOnLeft.getKey(); double dropMarkerLeft = 0 - scrollLeft; if (diffToRightEdge > diffToLeftEdge) { latestColumnDropIndex = cellEdgeOnLeft.getValue(); dropMarkerLeft += cellEdgeOnLeft.getKey(); } else { latestColumnDropIndex = cellEdgeOnRight.getValue(); dropMarkerLeft += cellEdgeOnRight.getKey(); } dropMarkerLeft += autoScrollX; final double frozenColumnsWidth = autoScroller .getFrozenColumnsWidth(); final double rightBoundaryForDrag = getSidebarBoundaryComparedTo( dropMarkerLeft); final int visibleColumns = getVisibleColumns().size(); // First check if the drop marker should move left because of the // sidebar opening button. this only the case if the grid is // scrolled to the right if (latestColumnDropIndex == visibleColumns && rightBoundaryForDrag < dropMarkerLeft && dropMarkerLeft <= escalator.getInnerWidth()) { dropMarkerLeft = rightBoundaryForDrag - dropMarkerWidthOffset; } // Check if the drop marker shouldn't be shown at all else if (dropMarkerLeft < frozenColumnsWidth || dropMarkerLeft > Math.min(rightBoundaryForDrag, escalator.getInnerWidth()) || dropMarkerLeft < 0) { dropMarkerLeft = -10000000; } dropMarker.getStyle().setLeft(dropMarkerLeft, Unit.PX); } private void resolveDragElementHorizontalPosition(final int clientX) { double left = clientX - table.getAbsoluteLeft(); // Do not show the drag element beyond a spanned header cell // limitation final Double leftBound = possibleDropPositions.firstKey(); final Double rightBound = possibleDropPositions.lastKey(); final double scrollLeft = getScrollLeft(); if (left + scrollLeft < leftBound) { left = leftBound - scrollLeft + autoScrollX; } else if (left + scrollLeft > rightBound) { left = rightBound - scrollLeft + autoScrollX; } // Do not show the drag element beyond the grid final double sidebarBoundary = getSidebarBoundaryComparedTo(left); final double gridBoundary = escalator.getInnerWidth(); final double rightBoundary = Math.min(sidebarBoundary, gridBoundary); // Do not show on left of the frozen columns (even if scrolled) final int frozenColumnsWidth = (int) autoScroller .getFrozenColumnsWidth(); left = Math.max(frozenColumnsWidth, Math.min(left, rightBoundary)); left -= dragElement.getClientWidth() / 2; dragElement.getStyle().setLeft(left, Unit.PX); } private boolean isSidebarOnDraggedRow() { return eventCell.getRowIndex() == 0 && sidebar.isInDOM() && !sidebar.isOpen(); } /** * Returns the sidebar left coordinate, in relation to the grid. Or * Double.MAX_VALUE if it doesn't cause a boundary. */ private double getSidebarBoundaryComparedTo(double left) { if (isSidebarOnDraggedRow()) { double absoluteLeft = left + getElement().getAbsoluteLeft(); double sidebarLeft = sidebar.getElement().getAbsoluteLeft(); double diff = absoluteLeft - sidebarLeft; if (diff > 0) { return left - diff; } } return Double.MAX_VALUE; } @Override public boolean onDragStart(Event e) { calculatePossibleDropPositions(); if (possibleDropPositions.isEmpty()) { return false; } initHeaderDragElementDOM(); // needs to clone focus and sorting indicators too (UX) dragElement = DOM.clone(eventCell.getElement(), true); dragElement.getStyle().clearWidth(); dropMarker.getStyle().setProperty("height", dragElement.getStyle().getHeight()); tableHeader.appendChild(dragElement); // mark the column being dragged for styling eventCell.getElement().addClassName("dragged"); // mark the floating cell, for styling & testing dragElement.addClassName("dragged-column-header"); // start the auto scroll handler autoScroller.setScrollArea(60); autoScroller.start(e, ScrollAxis.HORIZONTAL, autoScrollerCallback); return true; } @Override public void onDragEnd() { table.removeFromParent(); dragElement.removeFromParent(); eventCell.getElement().removeClassName("dragged"); } @Override public void onDrop() { final int draggedColumnIndex = eventCell.getColumnIndex(); final StaticRow draggedCellRow = header .getRow(eventCell.getRowIndex()); Set> cellGroup = draggedCellRow .getCellGroupForColumn(getColumn(draggedColumnIndex)); final int colspan = cellGroup == null ? 1 : cellGroup.size(); if (latestColumnDropIndex != draggedColumnIndex && latestColumnDropIndex != draggedColumnIndex + colspan) { List> columns = getColumns(); List> reordered = new ArrayList>(); if (draggedColumnIndex < latestColumnDropIndex) { reordered.addAll(columns.subList(0, draggedColumnIndex)); reordered.addAll( columns.subList(draggedColumnIndex + colspan, latestColumnDropIndex)); reordered.addAll(columns.subList(draggedColumnIndex, draggedColumnIndex + colspan)); reordered.addAll(columns.subList(latestColumnDropIndex, columns.size())); } else { reordered.addAll(columns.subList(0, latestColumnDropIndex)); reordered.addAll(columns.subList(draggedColumnIndex, draggedColumnIndex + colspan)); reordered.addAll(columns.subList(latestColumnDropIndex, draggedColumnIndex)); reordered.addAll(columns.subList( draggedColumnIndex + colspan, columns.size())); } // since setColumnOrder will add it anyway! reordered.remove(selectionColumn); // capture focused cell column before reorder Cell focusedCell = cellFocusHandler.getFocusedCell(); if (focusedCell != null) { // take hidden columns into account focusedColumnIndex = getColumns() .indexOf(getVisibleColumn(focusedCell.getColumn())); } Column[] array = reordered .toArray(new Column[reordered.size()]); setColumnOrder(array); transferCellFocusOnDrop(); } // else no reordering } private void transferCellFocusOnDrop() { final Cell focusedCell = cellFocusHandler.getFocusedCell(); if (focusedCell != null) { final int focusedColumnIndexDOM = focusedCell.getColumn(); final int focusedRowIndex = focusedCell.getRow(); final int draggedColumnIndex = eventCell.getColumnIndex(); // transfer focus if it was effected by the new column order final RowContainer rowContainer = escalator .findRowContainer(focusedCell.getElement()); if (focusedColumnIndex == draggedColumnIndex) { // move with the dragged column int adjustedDropIndex = latestColumnDropIndex > draggedColumnIndex ? latestColumnDropIndex - 1 : latestColumnDropIndex; // remove hidden columns from indexing adjustedDropIndex = getVisibleColumns() .indexOf(getColumn(adjustedDropIndex)); cellFocusHandler.setCellFocus(focusedRowIndex, adjustedDropIndex, rowContainer); } else if (latestColumnDropIndex <= focusedColumnIndex && draggedColumnIndex > focusedColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedColumnIndexDOM + 1, rowContainer); } else if (latestColumnDropIndex > focusedColumnIndex && draggedColumnIndex < focusedColumnIndex) { cellFocusHandler.setCellFocus(focusedRowIndex, focusedColumnIndexDOM - 1, rowContainer); } } } @Override public void onDragCancel() { // cancel next click so that we may prevent column sorting if // mouse was released on top of the dragged cell if (columnSortPreventRegistration == null) { columnSortPreventRegistration = Event .addNativePreviewHandler(new NativePreviewHandler() { @Override public void onPreviewNativeEvent( NativePreviewEvent event) { if (event.getTypeInt() == Event.ONCLICK) { event.cancel(); event.getNativeEvent().preventDefault(); columnSortPreventRegistration .removeHandler(); columnSortPreventRegistration = null; } } }); } autoScroller.stop(); } /** * Returns the amount of frozen columns. The selection column is always * considered frozen, since it can't be moved. */ private int getSelectionAndFrozenColumnCount() { // no matter if selection column is frozen or not, it is considered // frozen for column dnd reorder if (getSelectionModel().getSelectionColumnRenderer() != null) { return Math.max(0, getFrozenColumnCount()) + 1; } else { return Math.max(0, getFrozenColumnCount()); } } @SuppressWarnings("boxing") private void calculatePossibleDropPositions() { possibleDropPositions.clear(); final int draggedColumnIndex = eventCell.getColumnIndex(); final StaticRow draggedCellRow = header .getRow(eventCell.getRowIndex()); final int draggedColumnRightIndex = draggedColumnIndex + draggedCellRow.getCell(eventCell.getColumn()) .getColspan(); final int frozenColumns = getSelectionAndFrozenColumnCount(); final Range draggedCellRange = Range.between(draggedColumnIndex, draggedColumnRightIndex); /* * If the dragged cell intersects with a spanned cell in any other * header or footer row, then the drag is limited inside that * spanned cell. The same rules apply: the cell can't be dropped * inside another spanned cell. The left and right bounds keep track * of the edges of the most limiting spanned cell. */ int leftBound = -1; int rightBound = getColumnCount() + 1; final HashSet unavailableColumnDropIndices = new HashSet(); final List> rows = new ArrayList>(); rows.addAll(header.getRows()); rows.addAll(footer.getRows()); for (StaticRow row : rows) { if (!row.hasSpannedCells()) { continue; } final boolean isDraggedCellRow = row.equals(draggedCellRow); for (int cellColumnIndex = frozenColumns; cellColumnIndex < getColumnCount(); cellColumnIndex++) { // some of the columns might be hidden, use cell groups // rather than cell spans to determine actual span Set> cellGroup = row .getCellGroupForColumn(getColumn(cellColumnIndex)); if (cellGroup == null) { continue; } int colspan = cellGroup.size(); final int cellColumnRightIndex = cellColumnIndex + colspan; final Range cellRange = Range.between(cellColumnIndex, cellColumnRightIndex); final boolean intersects = draggedCellRange .intersects(cellRange); if (intersects && !isDraggedCellRow) { // if the currently iterated cell is inside or same as // the dragged cell, then it doesn't restrict the drag if (cellRange.isSubsetOf(draggedCellRange)) { cellColumnIndex = cellColumnRightIndex - 1; continue; } /* * if the dragged cell is a spanned cell and it crosses * with the currently iterated cell without sharing * either start or end then not possible to drag the * cell. */ if (!draggedCellRange.isSubsetOf(cellRange)) { return; } // the spanned cell overlaps the dragged cell (but is // not the dragged cell) if (cellColumnIndex <= draggedColumnIndex && cellColumnIndex > leftBound) { leftBound = cellColumnIndex; } if (cellColumnRightIndex < rightBound) { rightBound = cellColumnRightIndex; } cellColumnIndex = cellColumnRightIndex - 1; } else { // can't drop inside a spanned cell, or this is the // dragged cell while (colspan > 1) { cellColumnIndex++; colspan--; unavailableColumnDropIndices.add(cellColumnIndex); } } } } if (leftBound == rightBound - 1) { return; } double position = autoScroller.getFrozenColumnsWidth(); // iterate column indices and add possible drop positions for (int i = frozenColumns; i < getColumnCount(); i++) { Column column = getColumn(i); if (!unavailableColumnDropIndices.contains(i) && !column.isHidden()) { if (leftBound != -1) { if (i >= leftBound && i <= rightBound) { possibleDropPositions.put(position, i); } } else { possibleDropPositions.put(position, i); } } position += column.getWidthActual(); } if (leftBound == -1) { // add the right side of the last column as columns.size() possibleDropPositions.put(position, getColumnCount()); } } }; /** * Enumeration for easy setting of selection mode. */ public enum SelectionMode { /** * Shortcut for {@link SelectionModelSingle}. */ SINGLE { @Override protected SelectionModel createModel() { return GWT.create(SelectionModelSingle.class); } }, /** * Shortcut for {@link SelectionModelMulti}. */ MULTI { @Override protected SelectionModel createModel() { return GWT.create(SelectionModelMulti.class); } }, /** * Shortcut for {@link SelectionModelNone}. */ NONE { @Override protected SelectionModel createModel() { return GWT.create(SelectionModelNone.class); } }; protected abstract SelectionModel createModel(); } /** * Base class for grid columns internally used by the Grid. The user should * use {@link Column} when creating new columns. * * @param * the column type * * @param * the row type */ public abstract static class Column { /** * Default renderer for GridColumns. Renders everything into text * through {@link Object#toString()}. */ private final class DefaultTextRenderer implements Renderer { boolean warned = false; private static final String DEFAULT_RENDERER_WARNING = "This column uses a dummy default TextRenderer. " + "A more suitable renderer should be set using the setRenderer() method."; @Override public void render(RendererCellReference cell, Object data) { if (!warned && !(data instanceof String)) { getLogger().warning(Column.this.toString() + ": " + DEFAULT_RENDERER_WARNING); warned = true; } final String text; if (data == null) { text = ""; } else { text = data.toString(); } cell.getElement().setInnerText(text); } } /** * the column is associated with */ private Grid grid; /** * Width of column in pixels as {@link #setWidth(double)} has been * called. */ protected double widthUser = GridConstants.DEFAULT_COLUMN_WIDTH_PX; /** * Renderer for rendering a value into the cell */ private Renderer bodyRenderer; /** * The sortable state of this column. */ protected boolean sortable = false; /** * The editable state of this column. */ protected boolean editable = true; /** * The resizable state of this column. */ protected boolean resizable = true; /** * The hidden state of this column. */ protected boolean hidden = false; /** * The hidable state of this column. */ protected boolean hidable = false; /** * The header-caption of this column. */ protected String headerCaption = ""; /** * The hiding-toggle-caption of this column. */ protected String hidingToggleCaption = null; /** * The minimum width in pixels of this column. */ protected double minimumWidthPx = GridConstants.DEFAULT_MIN_WIDTH; /** * The maximum width in pixels of this column. */ protected double maximumWidthPx = GridConstants.DEFAULT_MAX_WIDTH; /** * The expand ratio of this column. */ protected int expandRatio = GridConstants.DEFAULT_EXPAND_RATIO; /** * Constructs a new column with a simple TextRenderer. */ public Column() { setRenderer(new DefaultTextRenderer()); } /** * Constructs a new column with a simple TextRenderer. * * @param caption * The header caption for this column * * @throws IllegalArgumentException * if given header caption is null */ public Column(String caption) throws IllegalArgumentException { this(); setHeaderCaption(caption); } /** * Constructs a new column with a custom renderer. * * @param renderer * The renderer to use for rendering the cells * * @throws IllegalArgumentException * if given Renderer is null */ public Column(Renderer renderer) throws IllegalArgumentException { setRenderer(renderer); } /** * Constructs a new column with a custom renderer. * * @param renderer * The renderer to use for rendering the cells * @param caption * The header caption for this column * * @throws IllegalArgumentException * if given Renderer or header caption is null */ public Column(String caption, Renderer renderer) throws IllegalArgumentException { this(renderer); setHeaderCaption(caption); } /** * Internally used by the grid to set itself * * @param grid */ private void setGrid(Grid grid) { if (this.grid != null && grid != null) { // Trying to replace grid throw new IllegalStateException("Column already is attached " + "to a grid. Remove the column first from the grid " + "and then add it. (in: " + toString() + ")"); } if (this.grid != null) { this.grid.recalculateColumnWidths(); } this.grid = grid; if (this.grid != null) { this.grid.recalculateColumnWidths(); } } /** * Sets a header caption for this column. * * @param caption * The header caption for this column * @return the column itself * */ public Column setHeaderCaption(String caption) { if (caption == null) { caption = ""; } if (!this.headerCaption.equals(caption)) { this.headerCaption = caption; if (grid != null) { updateHeader(); } } return this; } /** * Returns the current header caption for this column. * * @since 7.6 * @return the header caption string */ public String getHeaderCaption() { return headerCaption; } private void updateHeader() { HeaderRow row = grid.getHeader().getDefaultRow(); if (row != null) { row.getCell(this).setText(headerCaption); if (isHidable()) { grid.columnHider.updateHidingToggle(this); } } } /** * Returns the data that should be rendered into the cell. By default * returning Strings and Widgets are supported. If the return type is a * String then it will be treated as preformatted text. *

* To support other types you will need to pass a custom renderer to the * column via the column constructor. * * @param row * The row object that provides the cell content. * * @return The cell content */ public abstract C getValue(T row); /** * The renderer to render the cell with. By default renders the data as * a String or adds the widget into the cell if the column type is of * widget type. * * @return The renderer to render the cell content with */ public Renderer getRenderer() { return bodyRenderer; } /** * Sets a custom {@link Renderer} for this column. * * @param renderer * The renderer to use for rendering the cells * @return the column itself * * @throws IllegalArgumentException * if given Renderer is null */ public Column setRenderer(Renderer renderer) throws IllegalArgumentException { if (renderer == null) { throw new IllegalArgumentException("Renderer cannot be null."); } if (renderer != bodyRenderer) { // Variables used to restore removed column. boolean columnRemoved = false; double widthInConfiguration = 0.0d; ColumnConfiguration conf = null; int index = 0; if (grid != null && (bodyRenderer instanceof WidgetRenderer || renderer instanceof WidgetRenderer)) { // Column needs to be recreated. index = grid.getColumns().indexOf(this); conf = grid.escalator.getColumnConfiguration(); widthInConfiguration = conf.getColumnWidth(index); conf.removeColumns(index, 1); columnRemoved = true; } // Complex renderers need to be destroyed. if (bodyRenderer instanceof ComplexRenderer) { ((ComplexRenderer) bodyRenderer).destroy(); } bodyRenderer = renderer; if (columnRemoved) { // Restore the column. conf.insertColumns(index, 1); conf.setColumnWidth(index, widthInConfiguration); } if (grid != null) { grid.refreshBody(); } } return this; } /** * Sets the pixel width of the column. Use a negative value for the grid * to autosize column based on content and available space. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. *

* If the column is currently {@link #isHidden() hidden}, then this set * width has effect only once the column has been made visible again. * * @param pixels * the width in pixels or negative for auto sizing */ public Column setWidth(double pixels) { if (!WidgetUtil.pixelValuesEqual(widthUser, pixels)) { widthUser = pixels; if (!isHidden()) { scheduleColumnWidthRecalculator(); } } return this; } void doSetWidth(double pixels) { assert !isHidden() : "applying width for a hidden column"; if (grid != null) { int index = grid.getVisibleColumns().indexOf(this); ColumnConfiguration conf = grid.escalator .getColumnConfiguration(); conf.setColumnWidth(index, pixels); } } /** * Returns the pixel width of the column as given by the user. *

* Note: If a negative value was given to * {@link #setWidth(double)}, that same negative value is returned here. *

* Note: Returns the value, even if the column is currently * {@link #isHidden() hidden}. * * @return pixel width of the column, or a negative number if the column * width has been automatically calculated. * @see #setWidth(double) * @see #getWidthActual() */ public double getWidth() { return widthUser; } /** * Returns the effective pixel width of the column. *

* This differs from {@link #getWidth()} only when the column has been * automatically resized, or when the column is currently * {@link #isHidden() hidden}, when the value is 0. * * @return pixel width of the column. */ public double getWidthActual() { if (isHidden()) { return 0; } return grid.escalator.getColumnConfiguration().getColumnWidthActual( grid.getVisibleColumns().indexOf(this)); } void reapplyWidth() { scheduleColumnWidthRecalculator(); } /** * Sets whether the column should be sortable by the user. The grid can * be sorted by a sortable column by clicking or tapping the column's * default header. Programmatic sorting using the Grid#sort methods is * not affected by this setting. * * @param sortable * {@code true} if the user should be able to sort the * column, {@code false} otherwise * @return the column itself */ public Column setSortable(boolean sortable) { if (this.sortable != sortable) { this.sortable = sortable; if (grid != null) { grid.refreshHeader(); } } return this; } /** * Returns whether the user can sort the grid by this column. *

* Note: it is possible to sort by this column programmatically * using the Grid#sort methods regardless of the returned value. * * @return {@code true} if the column is sortable by the user, * {@code false} otherwise */ public boolean isSortable() { return sortable; } /** * Sets whether this column can be resized by the user. * * @since 7.6 * * @param resizable * {@code true} if this column should be resizable, * {@code false} otherwise */ public Column setResizable(boolean resizable) { if (this.resizable != resizable) { this.resizable = resizable; if (grid != null) { grid.refreshHeader(); } } return this; } /** * Returns whether this column can be resized by the user. Default is * {@code true}. *

* Note: the column can be programmatically resized using * {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless * of the returned value. * * @since 7.6 * * @return {@code true} if this column is resizable, {@code false} * otherwise */ public boolean isResizable() { return resizable; } /** * Hides or shows the column. By default columns are visible before * explicitly hiding them. * * @since 7.5.0 * @param hidden * true to hide the column, false * to show */ public Column setHidden(boolean hidden) { setHidden(hidden, false); return this; } private void setHidden(boolean hidden, boolean userOriginated) { if (this.hidden != hidden) { if (hidden) { grid.escalator.getColumnConfiguration().removeColumns( grid.getVisibleColumns().indexOf(this), 1); this.hidden = hidden; } else { this.hidden = hidden; final int columnIndex = grid.getVisibleColumns() .indexOf(this); grid.escalator.getColumnConfiguration() .insertColumns(columnIndex, 1); // make sure column is set to frozen if it needs to be, // escalator doesn't handle situation where the added column // would be the last frozen column int gridFrozenColumns = grid.getFrozenColumnCount(); int escalatorFrozenColumns = grid.escalator .getColumnConfiguration().getFrozenColumnCount(); if (gridFrozenColumns > escalatorFrozenColumns && escalatorFrozenColumns == columnIndex) { grid.escalator.getColumnConfiguration() .setFrozenColumnCount(++escalatorFrozenColumns); } } grid.columnHider.updateHidingToggle(this); grid.header.updateColSpans(); grid.footer.updateColSpans(); scheduleColumnWidthRecalculator(); this.grid.fireEvent(new ColumnVisibilityChangeEvent(this, hidden, userOriginated)); } } /** * Returns whether this column is hidden. Default is {@code false}. * * @since 7.5.0 * @return {@code true} if the column is currently hidden, {@code false} * otherwise */ public boolean isHidden() { return hidden; } /** * Set whether it is possible for the user to hide this column or not. * Default is {@code false}. *

* Note: it is still possible to hide the column * programmatically using {@link #setHidden(boolean)}. * * @since 7.5.0 * @param hidable * {@code true} the user can hide this column, {@code false} * otherwise */ public Column setHidable(boolean hidable) { if (this.hidable != hidable) { this.hidable = hidable; grid.columnHider.updateColumnHidable(this); } return this; } /** * Is it possible for the the user to hide this column. Default is * {@code false}. *

* Note: the column can be programmatically hidden using * {@link #setHidden(boolean)} regardless of the returned value. * * @since 7.5.0 * @return true if the user can hide the column, * false if not */ public boolean isHidable() { return hidable; } /** * Sets the hiding toggle's caption for this column. Shown in the toggle * for this column in the grid's sidebar when the column is * {@link #isHidable() hidable}. *

* The default value is null. In this case the header * caption is used, see {@link #setHeaderCaption(String)}. * * @since 7.5.0 * @param hidingToggleCaption * the caption for the hiding toggle for this column */ public Column setHidingToggleCaption(String hidingToggleCaption) { this.hidingToggleCaption = hidingToggleCaption; if (isHidable()) { grid.columnHider.updateHidingToggle(this); } return this; } /** * Gets the hiding toggle caption for this column. * * @since 7.5.0 * @see #setHidingToggleCaption(String) * @return the hiding toggle's caption for this column */ public String getHidingToggleCaption() { return hidingToggleCaption; } @Override public String toString() { String details = ""; if (headerCaption != null && !headerCaption.isEmpty()) { details += "header:\"" + headerCaption + "\" "; } else { details += "header:empty "; } if (grid != null) { int index = grid.getColumns().indexOf(this); if (index != -1) { details += "attached:#" + index + " "; } else { details += "attached:unindexed "; } } else { details += "detached "; } details += "sortable:" + sortable + " "; return getClass().getSimpleName() + "[" + details.trim() + "]"; } /** * Sets the minimum width for this column. *

* This defines the minimum guaranteed pixel width of the column * when it is set to expand. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. * * @param pixels * the minimum width * @return this column */ public Column setMinimumWidth(double pixels) { final double maxwidth = getMaximumWidth(); if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) { throw new IllegalArgumentException("New minimum width (" + pixels + ") was greater than maximum width (" + maxwidth + ")"); } if (minimumWidthPx != pixels) { minimumWidthPx = pixels; scheduleColumnWidthRecalculator(); } return this; } /** * Sets the maximum width for this column. *

* This defines the maximum allowed pixel width of the column when * it is set to expand. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. * * @param pixels * the maximum width * @return this column */ public Column setMaximumWidth(double pixels) { final double minwidth = getMinimumWidth(); if (pixels >= 0 && pixels < minwidth && minwidth >= 0) { throw new IllegalArgumentException("New maximum width (" + pixels + ") was less than minimum width (" + minwidth + ")"); } if (maximumWidthPx != pixels) { maximumWidthPx = pixels; scheduleColumnWidthRecalculator(); } return this; } /** * Sets the ratio with which the column expands. *

* By default, all columns expand equally (treated as if all of them had * an expand ratio of 1). Once at least one column gets a defined expand * ratio, the implicit expand ratio is removed, and only the defined * expand ratios are taken into account. *

* If a column has a defined width ({@link #setWidth(double)}), it * overrides this method's effects. *

* Example: A grid with three columns, with expand ratios 0, 1 * and 2, respectively. The column with a ratio of 0 is exactly * as wide as its contents requires. The column with a ratio of * 1 is as wide as it needs, plus a third of any excess * space, bceause we have 3 parts total, and this column * reservs only one of those. The column with a ratio of 2, is as wide * as it needs to be, plus two thirds of the excess * width. *

* This action is done "finally", once the current execution loop * returns. This is done to reduce overhead of unintentionally always * recalculate all columns, when modifying several columns at once. * * @param ratio * the expand ratio of this column. {@code 0} to not have it * expand at all. A negative number to clear the expand * value. * @return this column */ public Column setExpandRatio(int ratio) { if (expandRatio != ratio) { expandRatio = ratio; scheduleColumnWidthRecalculator(); } return this; } /** * Clears the column's expand ratio. *

* Same as calling {@link #setExpandRatio(int) setExpandRatio(-1)} * * @return this column */ public Column clearExpandRatio() { return setExpandRatio(-1); } /** * Gets the minimum width for this column. * * @return the minimum width for this column * @see #setMinimumWidth(double) */ public double getMinimumWidth() { return minimumWidthPx; } /** * Gets the maximum width for this column. * * @return the maximum width for this column * @see #setMaximumWidth(double) */ public double getMaximumWidth() { return maximumWidthPx; } /** * Gets the expand ratio for this column. * * @return the expand ratio for this column * @see #setExpandRatio(int) */ public int getExpandRatio() { return expandRatio; } /** * Sets whether the values in this column should be editable by the user * when the row editor is active. By default columns are editable. * * @param editable * {@code true} to set this column editable, {@code false} * otherwise * @return this column * * @throws IllegalStateException * if the editor is currently active * * @see Grid#editRow(int) * @see Grid#isEditorActive() */ public Column setEditable(boolean editable) { if (editable != this.editable && grid.isEditorActive()) { throw new IllegalStateException( "Cannot change column editable status while the editor is active"); } this.editable = editable; return this; } /** * Returns whether the values in this column are editable by the user * when the row editor is active. * * @return {@code true} if this column is editable, {@code false} * otherwise * * @see #setEditable(boolean) */ public boolean isEditable() { return editable; } private void scheduleColumnWidthRecalculator() { if (grid != null) { grid.recalculateColumnWidths(); } else { /* * NOOP * * Since setGrid() will call reapplyWidths as the colum is * attached to a grid, it will call setWidth, which, in turn, * will call this method again. Therefore, it's guaranteed that * the recalculation is scheduled eventually, once the column is * attached to a grid. */ } } /** * Resets the default header cell contents to column header captions. * * @since 7.5.1 * @param cell * default header cell for this column */ protected void setDefaultHeaderContent(HeaderCell cell) { cell.setText(headerCaption); } } protected class BodyUpdater implements EscalatorUpdater { @Override public void preAttach(Row row, Iterable cellsToAttach) { int rowIndex = row.getRow(); rowReference.set(rowIndex, getDataSource().getRow(rowIndex), row.getElement()); for (FlyweightCell cell : cellsToAttach) { Renderer renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { Column column = getVisibleColumn( cell.getColumn()); rendererCellReference.set(cell, getColumns().indexOf(column), column); ((ComplexRenderer) renderer) .init(rendererCellReference); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error initing cell in column " + cell.getColumn(), e); } } } } @Override public void postAttach(Row row, Iterable attachedCells) { for (FlyweightCell cell : attachedCells) { Renderer renderer = findRenderer(cell); if (renderer instanceof WidgetRenderer) { try { WidgetRenderer widgetRenderer = (WidgetRenderer) renderer; Widget widget = widgetRenderer.createWidget(); assert widget != null : "WidgetRenderer.createWidget() returned null. It should return a widget."; assert widget .getParent() == null : "WidgetRenderer.createWidget() returned a widget which already is attached."; assert cell.getElement() .getChildCount() == 0 : "Cell content should be empty when adding Widget"; // Physical attach cell.getElement().appendChild(widget.getElement()); // Logical attach setParent(widget, Grid.this); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error attaching child widget in column " + cell.getColumn(), e); } } } } @Override public void update(Row row, Iterable cellsToUpdate) { int rowIndex = row.getRow(); TableRowElement rowElement = row.getElement(); T rowData = dataSource.getRow(rowIndex); boolean hasData = rowData != null; /* * TODO could be more efficient to build a list of all styles that * should be used and update the element only once instead of * attempting to update only the ones that have changed. */ // Assign stylename for rows with data boolean usedToHaveData = rowElement .hasClassName(rowHasDataStyleName); if (usedToHaveData != hasData) { setStyleName(rowElement, rowHasDataStyleName, hasData); } boolean isEvenIndex = row.getRow() % 2 == 0; setStyleName(rowElement, rowStripeStyleName, !isEvenIndex); rowReference.set(rowIndex, rowData, rowElement); if (hasData) { setStyleName(rowElement, rowSelectedStyleName, isSelected(rowData)); if (rowStyleGenerator != null) { try { String rowStylename = rowStyleGenerator .getStyle(rowReference); setCustomStyleName(rowElement, rowStylename); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error generating styles for row " + row.getRow(), e); } } else { // Remove in case there was a generator previously setCustomStyleName(rowElement, null); } } else if (usedToHaveData) { setStyleName(rowElement, rowSelectedStyleName, false); setCustomStyleName(rowElement, null); } cellFocusHandler.updateFocusedRowStyle(row); for (FlyweightCell cell : cellsToUpdate) { Column column = getVisibleColumn(cell.getColumn()); final int columnIndex = getColumns().indexOf(column); assert column != null : "Column was not found from cell (" + cell.getColumn() + "," + cell.getRow() + ")"; cellFocusHandler.updateFocusedCellStyle(cell, escalator.getBody()); if (hasData && cellStyleGenerator != null) { try { cellReference.set(cell.getColumn(), columnIndex, column); String generatedStyle = cellStyleGenerator .getStyle(cellReference); setCustomStyleName(cell.getElement(), generatedStyle); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error generating style for cell in column " + cell.getColumn(), e); } } else if (hasData || usedToHaveData) { setCustomStyleName(cell.getElement(), null); } Renderer renderer = column.getRenderer(); try { rendererCellReference.set(cell, columnIndex, column); if (renderer instanceof ComplexRenderer) { // Hide cell content if needed ComplexRenderer clxRenderer = (ComplexRenderer) renderer; if (hasData) { if (!usedToHaveData) { // Prepare cell for rendering clxRenderer.setContentVisible( rendererCellReference, true); } Object value = column.getValue(rowData); clxRenderer.render(rendererCellReference, value); } else { // Prepare cell for no data clxRenderer.setContentVisible(rendererCellReference, false); } } else if (hasData) { // Simple renderers just render Object value = column.getValue(rowData); renderer.render(rendererCellReference, value); } else { // Clear cell if there is no data cell.getElement().removeAllChildren(); } } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error rendering cell in column " + cell.getColumn(), e); } } } @Override public void preDetach(Row row, Iterable cellsToDetach) { for (FlyweightCell cell : cellsToDetach) { Renderer renderer = findRenderer(cell); if (renderer instanceof WidgetRenderer) { try { Widget w = WidgetUtil.findWidget( cell.getElement().getFirstChildElement()); if (w != null) { // Logical detach setParent(w, null); // Physical detach cell.getElement().removeChild(w.getElement()); } } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error detaching widget in column " + cell.getColumn(), e); } } } } @Override public void postDetach(Row row, Iterable detachedCells) { int rowIndex = row.getRow(); // Passing null row data since it might not exist in the data source // any more rowReference.set(rowIndex, null, row.getElement()); for (FlyweightCell cell : detachedCells) { Renderer renderer = findRenderer(cell); if (renderer instanceof ComplexRenderer) { try { Column column = getVisibleColumn( cell.getColumn()); rendererCellReference.set(cell, getColumns().indexOf(column), column); ((ComplexRenderer) renderer) .destroy(rendererCellReference); } catch (RuntimeException e) { getLogger().log(Level.SEVERE, "Error destroying cell in column " + cell.getColumn(), e); } } } } } protected class StaticSectionUpdater implements EscalatorUpdater { private StaticSection section; private RowContainer container; public StaticSectionUpdater(StaticSection section, RowContainer container) { super(); this.section = section; this.container = container; } @Override public void update(Row row, Iterable cellsToUpdate) { StaticSection.StaticRow staticRow = section.getRow(row.getRow()); final List> columns = getVisibleColumns(); setCustomStyleName(row.getElement(), staticRow.getStyleName()); for (FlyweightCell cell : cellsToUpdate) { final StaticSection.StaticCell metadata = staticRow .getCell(columns.get(cell.getColumn())); // Decorate default row with sorting indicators if (staticRow instanceof HeaderRow) { addSortingIndicatorsToHeaderRow((HeaderRow) staticRow, cell); } // Assign colspan to cell before rendering cell.setColSpan(metadata.getColspan()); Element td = cell.getElement(); td.removeAllChildren(); setCustomStyleName(td, metadata.getStyleName()); Element content; // Wrap text or html content in default header to isolate // the content from the possible column resize drag handle // next to it if (metadata.getType() != GridStaticCellType.WIDGET) { content = DOM.createDiv(); if (staticRow instanceof HeaderRow) { content.setClassName(getStylePrimaryName() + "-column-header-content"); if (((HeaderRow) staticRow).isDefault()) { content.setClassName(content.getClassName() + " " + getStylePrimaryName() + "-column-default-header-content"); } } else if (staticRow instanceof FooterRow) { content.setClassName(getStylePrimaryName() + "-column-footer-content"); } else { getLogger().severe("Unhandled static row type " + staticRow.getClass().getCanonicalName()); } td.appendChild(content); } else { content = td; } switch (metadata.getType()) { case TEXT: content.setInnerText(metadata.getText()); break; case HTML: content.setInnerHTML(metadata.getHtml()); break; case WIDGET: preDetach(row, Arrays.asList(cell)); content.setInnerHTML(""); postAttach(row, Arrays.asList(cell)); break; } // XXX: Should add only once in preAttach/postAttach or when // resizable status changes // Only add resize handles to default header row for now if (columns.get(cell.getColumn()).isResizable() && staticRow instanceof HeaderRow && ((HeaderRow) staticRow).isDefault()) { final DivElement resizeElement = Document.get() .createDivElement(); resizeElement.addClassName(getStylePrimaryName() + "-column-resize-simple-indicator"); final int column = cell.getColumn(); final DragHandle dragger = new DragHandle( getStylePrimaryName() + "-column-resize-handle"); dragger.addTo(td); // Common functionality for drag handle callback // implementations abstract class AbstractDHCallback implements DragHandleCallback { protected Column col = getVisibleColumn(column); protected double initialWidth = 0; protected double minCellWidth; protected double width; protected void dragStarted() { initialWidth = col.getWidthActual(); width = initialWidth; minCellWidth = escalator.getMinCellWidth( getVisibleColumns().indexOf(col)); for (Column c : getVisibleColumns()) { if (selectionColumn == c) { // Don't modify selection column. continue; } if (c.getWidth() < 0) { c.setWidth(c.getWidthActual()); fireEvent(new ColumnResizeEvent(c)); } } WidgetUtil.setTextSelectionEnabled(getElement(), false); } protected void dragEnded() { WidgetUtil.setTextSelectionEnabled(getElement(), true); } } final DragHandleCallback simpleResizeMode = new AbstractDHCallback() { @Override protected void dragEnded() { super.dragEnded(); dragger.getElement().removeChild(resizeElement); } @Override public void onStart() { dragStarted(); dragger.getElement().appendChild(resizeElement); resizeElement.getStyle().setLeft( (dragger.getElement().getOffsetWidth() - resizeElement.getOffsetWidth()) * .5, Unit.PX); resizeElement.getStyle().setHeight( col.grid.getOffsetHeight(), Unit.PX); } @Override public void onUpdate(double deltaX, double deltaY) { width = Math.max(minCellWidth, initialWidth + deltaX); resizeElement.getStyle().setLeft( (dragger.getElement().getOffsetWidth() - resizeElement.getOffsetWidth()) * .5 + (width - initialWidth), Unit.PX); } @Override public void onCancel() { dragEnded(); } @Override public void onComplete() { dragEnded(); col.setWidth(width); // Need to wait for column width recalculation // scheduled by setWidth() before firing the event Scheduler.get() .scheduleDeferred(new ScheduledCommand() { @Override public void execute() { fireEvent(new ColumnResizeEvent( col)); } }); } }; final DragHandleCallback animatedResizeMode = new AbstractDHCallback() { @Override public void onStart() { dragStarted(); } @Override public void onUpdate(double deltaX, double deltaY) { width = Math.max(minCellWidth, initialWidth + deltaX); col.setWidth(width); } @Override public void onCancel() { dragEnded(); col.setWidth(initialWidth); } @Override public void onComplete() { dragEnded(); col.setWidth(width); fireEvent(new ColumnResizeEvent(col)); } }; // DragHandle gets assigned a 'master callback' that // delegates // functionality to the correct case-specific implementation dragger.setCallback(new DragHandleCallback() { private DragHandleCallback currentCallback; @Override public void onStart() { switch (getColumnResizeMode()) { case SIMPLE: currentCallback = simpleResizeMode; break; case ANIMATED: currentCallback = animatedResizeMode; break; default: throw new UnsupportedOperationException( "Support for current column resize mode is not yet implemented"); } currentCallback.onStart(); } @Override public void onUpdate(double deltaX, double deltaY) { currentCallback.onUpdate(deltaX, deltaY); } @Override public void onCancel() { currentCallback.onCancel(); } @Override public void onComplete() { currentCallback.onComplete(); } }); } cellFocusHandler.updateFocusedCellStyle(cell, container); } } private void addSortingIndicatorsToHeaderRow(HeaderRow headerRow, FlyweightCell cell) { Element cellElement = cell.getElement(); boolean sortedBefore = cellElement.hasClassName("sort-asc") || cellElement.hasClassName("sort-desc"); cleanup(cell); if (!headerRow.isDefault()) { // Nothing more to do if not in the default row return; } final Column column = getVisibleColumn(cell.getColumn()); SortOrder sortingOrder = getSortOrder(column); boolean sortable = column.isSortable(); if (sortable) { cellElement.addClassName("sortable"); } if (!sortable || sortingOrder == null) { // Only apply sorting indicators to sortable header columns return; } if (SortDirection.ASCENDING == sortingOrder.getDirection()) { cellElement.addClassName("sort-asc"); } else { cellElement.addClassName("sort-desc"); } int sortIndex = Grid.this.getSortOrder().indexOf(sortingOrder); if (sortIndex > -1 && Grid.this.getSortOrder().size() > 1) { // Show sort order indicator if column is // sorted and other sorted columns also exists. cellElement.setAttribute("sort-order", String.valueOf(sortIndex + 1)); } if (!sortedBefore) { verifyColumnWidth(column); } } /** * Sort indicator requires a bit more space from the cell than normally. * This method check that the now sorted column has enough width. * * @param column * sorted column */ private void verifyColumnWidth(Column column) { int colIndex = getColumns().indexOf(column); double minWidth = escalator.getMinCellWidth(colIndex); if (column.getWidthActual() < minWidth) { // Fix column size escalator.getColumnConfiguration().setColumnWidth(colIndex, minWidth); fireEvent(new ColumnResizeEvent(column)); } } /** * Finds the sort order for this column */ private SortOrder getSortOrder(Column column) { for (SortOrder order : Grid.this.getSortOrder()) { if (order.getColumn() == column) { return order; } } return null; } private void cleanup(FlyweightCell cell) { Element cellElement = cell.getElement(); cellElement.removeAttribute("sort-order"); cellElement.removeClassName("sort-desc"); cellElement.removeClassName("sort-asc"); cellElement.removeClassName("sortable"); } @Override public void preAttach(Row row, Iterable cellsToAttach) { } @Override public void postAttach(Row row, Iterable attachedCells) { StaticSection.StaticRow gridRow = section.getRow(row.getRow()); List> columns = getVisibleColumns(); for (FlyweightCell cell : attachedCells) { StaticSection.StaticCell metadata = gridRow .getCell(columns.get(cell.getColumn())); /* * If the cell contains widgets that are not currently attached * then attach them now. */ if (GridStaticCellType.WIDGET.equals(metadata.getType())) { final Widget widget = metadata.getWidget(); if (widget != null && !widget.isAttached()) { getGrid().attachWidget(metadata.getWidget(), cell.getElement()); } } } } @Override public void preDetach(Row row, Iterable cellsToDetach) { if (section.getRowCount() > row.getRow()) { StaticSection.StaticRow gridRow = section .getRow(row.getRow()); List> columns = getVisibleColumns(); for (FlyweightCell cell : cellsToDetach) { StaticSection.StaticCell metadata = gridRow .getCell(columns.get(cell.getColumn())); if (GridStaticCellType.WIDGET.equals(metadata.getType()) && metadata.getWidget() != null && metadata.getWidget().isAttached()) { getGrid().detachWidget(metadata.getWidget()); } } } } protected Grid getGrid() { return section.grid; } @Override public void postDetach(Row row, Iterable detachedCells) { } }; /** * Creates a new instance. */ public Grid() { initWidget(escalator); getElement().setTabIndex(0); cellFocusHandler = new CellFocusHandler(); setStylePrimaryName(STYLE_NAME); escalator.getHeader().setEscalatorUpdater(createHeaderUpdater()); escalator.getBody().setEscalatorUpdater(createBodyUpdater()); escalator.getFooter().setEscalatorUpdater(createFooterUpdater()); header.setGrid(this); HeaderRow defaultRow = header.appendRow(); header.setDefaultRow(defaultRow); footer.setGrid(this); editor.setGrid(this); setSelectionMode(SelectionMode.SINGLE); escalator.getBody().setSpacerUpdater(gridSpacerUpdater); escalator.addScrollHandler(new ScrollHandler() { @Override public void onScroll(ScrollEvent event) { fireEvent(new ScrollEvent()); } }); escalator.addRowVisibilityChangeHandler( new RowVisibilityChangeHandler() { @Override public void onRowVisibilityChange( RowVisibilityChangeEvent event) { if (dataSource != null && dataSource.size() != 0) { dataSource.ensureAvailability( event.getFirstVisibleRow(), event.getVisibleRowCount()); } } }); // Default action on SelectionEvents. Refresh the body so changed // become visible. addSelectionHandler(new SelectionHandler() { @Override public void onSelect(SelectionEvent event) { refreshBody(); } }); // Sink header events and key events sinkEvents(getHeader().getConsumedEvents()); sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP, BrowserEvents.KEYPRESS, BrowserEvents.DBLCLICK, BrowserEvents.MOUSEDOWN, BrowserEvents.CLICK)); // Make ENTER and SHIFT+ENTER in the header perform sorting addHeaderKeyUpHandler(new HeaderKeyUpHandler() { @Override public void onKeyUp(GridKeyUpEvent event) { if (event.getNativeKeyCode() != KeyCodes.KEY_ENTER) { return; } if (getHeader().getRow(event.getFocusedCell().getRowIndex()) .isDefault()) { // Only sort for enter on the default header sorter.sort(event.getFocusedCell().getColumn(), event.isShiftKeyDown()); } } }); browserEventHandlers.addAll(Arrays.asList( // Opening, closing and navigating in the editor new EditorEventHandler(), // Keyboard and click handlers, Escalator events new SuperEventHandler(), // Column reordering via header drag&drop new HeaderCellDragStartHandler(), // Column sorting via header click new HeaderDefaultRowEventHandler(), // Invoking event-aware renderers new RendererEventHandler(), // Moving cell focus by keyboard or mouse new CellFocusEventHandler())); } @Override public boolean isEnabled() { return enabled; } @Override public void setEnabled(boolean enabled) { if (enabled == this.enabled) { return; } this.enabled = enabled; getElement().setTabIndex(enabled ? 0 : -1); // Editor save and cancel buttons need to be disabled. boolean editorOpen = editor.getState() != State.INACTIVE; if (editorOpen) { editor.setGridEnabled(enabled); } sidebar.setEnabled(enabled); getEscalator().setScrollLocked(Direction.VERTICAL, !enabled || editorOpen); getEscalator().setScrollLocked(Direction.HORIZONTAL, !enabled); fireEvent(new GridEnabledEvent(enabled)); } /** * Sets the column resize mode to use. The default mode is * {@link ColumnResizeMode.ANIMATED}. * * @param mode * a ColumnResizeMode value * @since 7.7.5 */ public void setColumnResizeMode(ColumnResizeMode mode) { columnResizeMode = mode; } /** * Returns the current column resize mode. The default mode is * {@link ColumnResizeMode.ANIMATED}. * * @return a ColumnResizeMode value * * @since 7.7.5 */ public ColumnResizeMode getColumnResizeMode() { return columnResizeMode; } @Override public void setStylePrimaryName(String style) { super.setStylePrimaryName(style); escalator.setStylePrimaryName(style); editor.setStylePrimaryName(style); sidebar.setStylePrimaryName(style + "-sidebar"); sidebar.addStyleName("v-contextmenu"); String rowStyle = getStylePrimaryName() + "-row"; rowHasDataStyleName = rowStyle + "-has-data"; rowSelectedStyleName = rowStyle + "-selected"; rowStripeStyleName = rowStyle + "-stripe"; cellFocusStyleName = getStylePrimaryName() + "-cell-focused"; rowFocusStyleName = getStylePrimaryName() + "-row-focused"; if (isAttached()) { refreshHeader(); refreshBody(); refreshFooter(); } } /** * Creates the escalator updater used to update the header rows in this * grid. The updater is invoked when header rows or columns are added or * removed, or the content of existing header cells is changed. * * @return the new header updater instance * * @see GridHeader * @see Grid#getHeader() */ protected EscalatorUpdater createHeaderUpdater() { return new StaticSectionUpdater(header, escalator.getHeader()); } /** * Creates the escalator updater used to update the body rows in this grid. * The updater is invoked when body rows or columns are added or removed, * the content of body cells is changed, or the body is scrolled to expose * previously hidden content. * * @return the new body updater instance */ protected EscalatorUpdater createBodyUpdater() { return new BodyUpdater(); } /** * Creates the escalator updater used to update the footer rows in this * grid. The updater is invoked when header rows or columns are added or * removed, or the content of existing header cells is changed. * * @return the new footer updater instance * * @see GridFooter * @see #getFooter() */ protected EscalatorUpdater createFooterUpdater() { return new StaticSectionUpdater(footer, escalator.getFooter()); } /** * Refreshes header or footer rows on demand * * @param rows * The row container * @param firstRowIsVisible * is the first row visible * @param isHeader * true if we refreshing the header, else assumed * the footer */ private void refreshRowContainer(RowContainer rows, StaticSection section) { // Add or Remove rows on demand int rowDiff = section.getVisibleRowCount() - rows.getRowCount(); if (rowDiff > 0) { rows.insertRows(0, rowDiff); } else if (rowDiff < 0) { rows.removeRows(0, -rowDiff); } // Refresh all the rows if (rows.getRowCount() > 0) { rows.refreshRows(0, rows.getRowCount()); } } /** * Focus a body cell by row and column index. * * @param rowIndex * index of row to focus * @param columnIndexDOM * index (excluding hidden columns) of cell to focus */ void focusCell(int rowIndex, int columnIndexDOM) { final Range rowRange = Range.between(0, dataSource.size()); final Range columnRange = Range.between(0, getVisibleColumns().size()); assert rowRange.contains( rowIndex) : "Illegal row index. Should be in range " + rowRange; assert columnRange.contains( columnIndexDOM) : "Illegal column index. Should be in range " + columnRange; if (rowRange.contains(rowIndex) && columnRange.contains(columnIndexDOM)) { cellFocusHandler.setCellFocus(rowIndex, columnIndexDOM, escalator.getBody()); WidgetUtil.focus(getElement()); } } /** * Refreshes all header rows */ void refreshHeader() { refreshRowContainer(escalator.getHeader(), header); } /** * Refreshes all body rows */ private void refreshBody() { escalator.getBody().refreshRows(0, escalator.getBody().getRowCount()); } /** * Refreshes all footer rows */ void refreshFooter() { refreshRowContainer(escalator.getFooter(), footer); } /** * Adds columns as the last columns in the grid. * * @param columns * the columns to add */ public void addColumns(Column... columns) { final int count = getColumnCount(); for (Column column : columns) { checkColumnIsValidToAdd(column, count); } addColumnsSkipSelectionColumnCheck(Arrays.asList(columns), count); } /** * Checks the given column is valid to add at the given index. */ private void checkColumnIsValidToAdd(Column column, int index) { if (column == this.selectionColumn) { throw new IllegalArgumentException( "The selection column may not be added manually"); } else if (this.selectionColumn != null && index == 0) { throw new IllegalStateException("A column cannot be inserted " + "before the selection column"); } } /** * Adds a column as the last column in the grid. * * @param column * the column to add * @return given column */ public > C addColumn(C column) { addColumn(column, getColumnCount()); return column; } /** * Inserts a column into a specific position in the grid. * * @param index * the index where the column should be inserted into * @param column * the column to add * @return given column * * @throws IllegalStateException * if Grid's current selection model renders a selection column, * and {@code index} is 0. */ public > C addColumn(C column, int index) { checkColumnIsValidToAdd(column, index); addColumnsSkipSelectionColumnCheck(Collections.singleton(column), index); return column; } private > void addColumnsSkipSelectionColumnCheck( Collection columnCollection, int index) { int visibleNewColumns = 0; int currentIndex = index; // prevent updates of hiding toggles. // it will be updated finally all at once. this.columnHider.hidingColumn = true; for (final Column column : columnCollection) { // Register column with grid this.columns.add(currentIndex++, column); this.footer.addColumn(column); this.header.addColumn(column); // Register this grid instance with the column column.setGrid(this); if (!column.isHidden()) { visibleNewColumns++; } } if (visibleNewColumns > 0) { final ColumnConfiguration columnConfiguration = this.escalator .getColumnConfiguration(); columnConfiguration.insertColumns(index, visibleNewColumns); } for (final Column column : columnCollection) { // Reapply column width column.reapplyWidth(); // Sink all renderer events final Set events = new HashSet(); events.addAll(getConsumedEventsForRenderer(column.getRenderer())); if (column.isHidable()) { this.columnHider.updateColumnHidable(column); } sinkEvents(events); } // now we do the update of the hiding toggles. this.columnHider.hidingColumn = false; this.columnHider.updateTogglesOrder(); refreshHeader(); this.header.updateColSpans(); this.footer.updateColSpans(); } private void sinkEvents(Collection events) { assert events != null; int eventsToSink = 0; for (String typeName : events) { int typeInt = Event.getTypeInt(typeName); if (typeInt < 0) { // Type not recognized by typeInt sinkBitlessEvent(typeName); } else { eventsToSink |= typeInt; } } if (eventsToSink > 0) { sinkEvents(eventsToSink); } } private Renderer findRenderer(FlyweightCell cell) { Column column = getVisibleColumn(cell.getColumn()); assert column != null : "Could not find column at index:" + cell.getColumn(); return column.getRenderer(); } /** * Removes a column from the grid. * * @param column * the column to remove */ public void removeColumn(Column column) { if (column != null && column.equals(selectionColumn)) { throw new IllegalArgumentException( "The selection column may not be removed manually."); } removeColumnSkipSelectionColumnCheck(column); } private void removeColumnSkipSelectionColumnCheck(Column column) { int columnIndex = columns.indexOf(column); // Remove from column configuration int visibleColumnIndex = getVisibleColumns().indexOf(column); if (visibleColumnIndex < 0) { assert column.isHidden(); // Hidden columns are not included in Escalator } else { getEscalator().getColumnConfiguration() .removeColumns(visibleColumnIndex, 1); } updateFrozenColumns(); header.removeColumn(column); footer.removeColumn(column); // de-register column with grid ((Column) column).setGrid(null); columns.remove(columnIndex); if (column.isHidable()) { columnHider.removeColumnHidingToggle(column); } } /** * Returns the amount of columns in the grid. *

* NOTE: this includes the hidden columns in the count. * * @return The number of columns in the grid */ public int getColumnCount() { return columns.size(); } /** * Returns a list columns in the grid, including hidden columns. *

* For currently visible columns, use {@link #getVisibleColumns()}. * * @return A unmodifiable list of the columns in the grid */ public List> getColumns() { return Collections .unmodifiableList(new ArrayList>(columns)); } /** * Returns a list of the currently visible columns in the grid. *

* No {@link Column#isHidden() hidden} columns included. * * @since 7.5.0 * @return A unmodifiable list of the currently visible columns in the grid */ public List> getVisibleColumns() { List> visible = new ArrayList>(); for (Column c : columns) { if (!c.isHidden()) { visible.add(c); } } return Collections.unmodifiableList(visible); } /** * Returns a column by its index in the grid. *

* NOTE: The indexing includes hidden columns. * * @param index * the index of the column * @return The column in the given index * @throws IllegalArgumentException * if the column index does not exist in the grid */ public Column getColumn(int index) throws IllegalArgumentException { if (index < 0 || index >= columns.size()) { throw new IllegalStateException("Column not found."); } return columns.get(index); } private Column getVisibleColumn(int columnIndexDOM) throws IllegalArgumentException { List> visibleColumns = getVisibleColumns(); if (columnIndexDOM < 0 || columnIndexDOM >= visibleColumns.size()) { throw new IllegalStateException("Column not found."); } return visibleColumns.get(columnIndexDOM); } /** * Returns the header section of this grid. The default header contains a * single row displaying the column captions. * * @return the header */ protected Header getHeader() { return header; } /** * Gets the header row at given index. * * @param rowIndex * 0 based index for row. Counted from top to bottom * @return header row at given index * @throws IllegalArgumentException * if no row exists at given index */ public HeaderRow getHeaderRow(int rowIndex) { return header.getRow(rowIndex); } /** * Inserts a new row at the given position to the header section. Shifts the * row currently at that position and any subsequent rows down (adds one to * their indices). * * @param index * the position at which to insert the row * @return the new row * * @throws IllegalArgumentException * if the index is less than 0 or greater than row count * @see #appendHeaderRow() * @see #prependHeaderRow() * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow addHeaderRowAt(int index) { return header.addRowAt(index); } /** * Adds a new row at the bottom of the header section. * * @return the new row * @see #prependHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow appendHeaderRow() { return header.appendRow(); } /** * Returns the current default row of the header section. The default row is * a special header row providing a user interface for sorting columns. * Setting a header caption for column updates cells in the default header. * * @return the default row or null if no default row set */ public HeaderRow getDefaultHeaderRow() { return header.getDefaultRow(); } /** * Gets the row count for the header section. * * @return row count */ public int getHeaderRowCount() { return header.getRowCount(); } /** * Adds a new row at the top of the header section. * * @return the new row * @see #appendHeaderRow() * @see #addHeaderRowAt(int) * @see #removeHeaderRow(HeaderRow) * @see #removeHeaderRow(int) */ public HeaderRow prependHeaderRow() { return header.prependRow(); } /** * Removes the given row from the header section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #removeHeaderRow(int) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(HeaderRow row) { header.removeRow(row); } /** * Removes the row at the given position from the header section. * * @param rowIndex * the index of the row * * @throws IllegalArgumentException * if no row exists at given index * @see #removeHeaderRow(HeaderRow) * @see #addHeaderRowAt(int) * @see #appendHeaderRow() * @see #prependHeaderRow() */ public void removeHeaderRow(int rowIndex) { header.removeRow(rowIndex); } /** * Sets the default row of the header. The default row is a special header * row providing a user interface for sorting columns. *

* Note: Setting the default header row will reset all cell contents to * Column defaults. * * @param row * the new default row, or null for no default row * * @throws IllegalArgumentException * header does not contain the row */ public void setDefaultHeaderRow(HeaderRow row) { header.setDefaultRow(row); } /** * Sets the visibility of the header section. * * @param visible * true to show header section, false to hide */ public void setHeaderVisible(boolean visible) { header.setVisible(visible); } /** * Returns the visibility of the header section. * * @return true if visible, false otherwise. */ public boolean isHeaderVisible() { return header.isVisible(); } /* Grid Footers */ /** * Returns the footer section of this grid. The default footer is empty. * * @return the footer */ protected Footer getFooter() { return footer; } /** * Gets the footer row at given index. * * @param rowIndex * 0 based index for row. Counted from top to bottom * @return footer row at given index * @throws IllegalArgumentException * if no row exists at given index */ public FooterRow getFooterRow(int rowIndex) { return footer.getRow(rowIndex); } /** * Inserts a new row at the given position to the footer section. Shifts the * row currently at that position and any subsequent rows down (adds one to * their indices). * * @param index * the position at which to insert the row * @return the new row * * @throws IllegalArgumentException * if the index is less than 0 or greater than row count * @see #appendFooterRow() * @see #prependFooterRow() * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow addFooterRowAt(int index) { return footer.addRowAt(index); } /** * Adds a new row at the bottom of the footer section. * * @return the new row * @see #prependFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow appendFooterRow() { return footer.appendRow(); } /** * Gets the row count for the footer. * * @return row count */ public int getFooterRowCount() { return footer.getRowCount(); } /** * Adds a new row at the top of the footer section. * * @return the new row * @see #appendFooterRow() * @see #addFooterRowAt(int) * @see #removeFooterRow(FooterRow) * @see #removeFooterRow(int) */ public FooterRow prependFooterRow() { return footer.prependRow(); } /** * Removes the given row from the footer section. * * @param row * the row to be removed * * @throws IllegalArgumentException * if the row does not exist in this section * @see #removeFooterRow(int) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(FooterRow row) { footer.removeRow(row); } /** * Removes the row at the given position from the footer section. * * @param rowIndex * the position of the row * * @throws IllegalArgumentException * if no row exists at given index * @see #removeFooterRow(FooterRow) * @see #addFooterRowAt(int) * @see #appendFooterRow() * @see #prependFooterRow() */ public void removeFooterRow(int rowIndex) { footer.removeRow(rowIndex); } /** * Sets the visibility of the footer section. * * @param visible * true to show footer section, false to hide */ public void setFooterVisible(boolean visible) { footer.setVisible(visible); } /** * Returns the visibility of the footer section. * * @return true if visible, false otherwise. */ public boolean isFooterVisible() { return footer.isVisible(); } public Editor getEditor() { return editor; } /** * Gets the {@link Escalator} used by this Grid instance. * * @return the escalator instance, never null */ public Escalator getEscalator() { return escalator; } /** * {@inheritDoc} *

* Note: This method will change the widget's size in the browser * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}. * * @see #setHeightMode(HeightMode) */ @Override public void setHeight(String height) { escalator.setHeight(height); } @Override public void setWidth(String width) { escalator.setWidth(width); } /** * Sets the data source used by this grid. * * @param dataSource * the data source to use, not null * @throws IllegalArgumentException * if dataSource is null */ public void setDataSource(final DataSource dataSource) throws IllegalArgumentException { if (dataSource == null) { throw new IllegalArgumentException("dataSource can't be null."); } selectionModel.reset(); if (changeHandler != null) { changeHandler.remove(); changeHandler = null; } this.dataSource = dataSource; changeHandler = dataSource .addDataChangeHandler(new DataChangeHandler() { @Override public void dataUpdated(int firstIndex, int numberOfItems) { escalator.getBody().refreshRows(firstIndex, numberOfItems); } @Override public void dataRemoved(int firstIndex, int numberOfItems) { escalator.getBody().removeRows(firstIndex, numberOfItems); Range removed = Range.withLength(firstIndex, numberOfItems); cellFocusHandler.rowsRemovedFromBody(removed); } @Override public void dataAdded(int firstIndex, int numberOfItems) { escalator.getBody().insertRows(firstIndex, numberOfItems); Range added = Range.withLength(firstIndex, numberOfItems); cellFocusHandler.rowsAddedToBody(added); } @Override public void dataAvailable(int firstIndex, int numberOfItems) { currentDataAvailable = Range.withLength(firstIndex, numberOfItems); fireEvent(new DataAvailableEvent(currentDataAvailable)); } @Override public void resetDataAndSize(int newSize) { RowContainer body = escalator.getBody(); int oldSize = body.getRowCount(); // Hide all details. Set oldDetails = new HashSet( visibleDetails); for (int i : oldDetails) { setDetailsVisible(i, false); } if (newSize > oldSize) { body.insertRows(oldSize, newSize - oldSize); cellFocusHandler.rowsAddedToBody(Range .withLength(oldSize, newSize - oldSize)); } else if (newSize < oldSize) { body.removeRows(newSize, oldSize - newSize); cellFocusHandler.rowsRemovedFromBody(Range .withLength(newSize, oldSize - newSize)); } if (newSize > 0) { Range visibleRowRange = escalator .getVisibleRowRange(); dataSource.ensureAvailability( visibleRowRange.getStart(), visibleRowRange.length()); } else { // We won't expect any data more data updates, so // just make the bookkeeping happy dataAvailable(0, 0); } assert body.getRowCount() == newSize; } }); int previousRowCount = escalator.getBody().getRowCount(); if (previousRowCount != 0) { escalator.getBody().removeRows(0, previousRowCount); } setEscalatorSizeFromDataSource(); } private void setEscalatorSizeFromDataSource() { assert escalator.getBody().getRowCount() == 0; int size = dataSource.size(); if (size == -1 && isAttached()) { // Exact size is not yet known, start with some reasonable guess // just to get an initial backend request going size = getEscalator().getMaxVisibleRowCount(); } if (size > 0) { escalator.getBody().insertRows(0, size); } } /** * Gets the {@Link DataSource} for this Grid. * * @return the data source used by this grid */ public DataSource getDataSource() { return dataSource; } /** * Sets the number of frozen columns in this grid. Setting the count to 0 * means that no data columns will be frozen, but the built-in selection * checkbox column will still be frozen if it's in use. Setting the count to * -1 will also disable the selection column. *

* The default value is 0. * * @param numberOfColumns * the number of columns that should be frozen * * @throws IllegalArgumentException * if the column count is < -1 or > the number of visible * columns */ public void setFrozenColumnCount(int numberOfColumns) { if (numberOfColumns < -1 || numberOfColumns > getColumnCount()) { throw new IllegalArgumentException( "count must be between -1 and the current number of columns (" + getColumnCount() + ")"); } frozenColumnCount = numberOfColumns; updateFrozenColumns(); } private void updateFrozenColumns() { escalator.getColumnConfiguration() .setFrozenColumnCount(getVisibleFrozenColumnCount()); } private int getVisibleFrozenColumnCount() { int numberOfColumns = getFrozenColumnCount(); // for the escalator the hidden columns are not in the frozen column // count, but for grid they are. thus need to convert the index for (int i = 0; i < frozenColumnCount; i++) { if (getColumn(i).isHidden()) { numberOfColumns--; } } if (numberOfColumns == -1) { numberOfColumns = 0; } else if (selectionColumn != null) { numberOfColumns++; } return numberOfColumns; } /** * Gets the number of frozen columns in this grid. 0 means that no data * columns will be frozen, but the built-in selection checkbox column will * still be frozen if it's in use. -1 means that not even the selection * column is frozen. *

* NOTE: This includes {@link Column#isHidden() hidden columns} in * the count. * * @return the number of frozen columns */ public int getFrozenColumnCount() { return frozenColumnCount; } public HandlerRegistration addRowVisibilityChangeHandler( RowVisibilityChangeHandler handler) { /* * Reusing Escalator's RowVisibilityChangeHandler, since a scroll * concept is too abstract. e.g. the event needs to be re-sent when the * widget is resized. */ return escalator.addRowVisibilityChangeHandler(handler); } /** * Scrolls to a certain row, using {@link ScrollDestination#ANY}. *

* If the details for that row are visible, those will be taken into account * as well. * * @param rowIndex * zero-based index of the row to scroll to. * @throws IllegalArgumentException * if rowIndex is below zero, or above the maximum value * supported by the data source. */ public void scrollToRow(int rowIndex) throws IllegalArgumentException { scrollToRow(rowIndex, ScrollDestination.ANY, GridConstants.DEFAULT_PADDING); } /** * Scrolls to a certain row, using user-specified scroll destination. *

* If the details for that row are visible, those will be taken into account * as well. * * @param rowIndex * zero-based index of the row to scroll to. * @param destination * desired destination placement of scrolled-to-row. See * {@link ScrollDestination} for more information. * @throws IllegalArgumentException * if rowIndex is below zero, or above the maximum value * supported by the data source. */ public void scrollToRow(int rowIndex, ScrollDestination destination) throws IllegalArgumentException { scrollToRow(rowIndex, destination, destination == ScrollDestination.MIDDLE ? 0 : GridConstants.DEFAULT_PADDING); } /** * Scrolls to a certain row using only user-specified parameters. *

* If the details for that row are visible, those will be taken into account * as well. * * @param rowIndex * zero-based index of the row to scroll to. * @param destination * desired destination placement of scrolled-to-row. See * {@link ScrollDestination} for more information. * @param paddingPx * number of pixels to overscroll. Behavior depends on * destination. * @throws IllegalArgumentException * if {@code destination} is {@link ScrollDestination#MIDDLE} * and padding is nonzero, because having a padding on a * centered row is undefined behavior, or if rowIndex is below * zero or above the row count of the data source. */ private void scrollToRow(int rowIndex, ScrollDestination destination, int paddingPx) throws IllegalArgumentException { int maxsize = escalator.getBody().getRowCount() - 1; if (rowIndex < 0) { throw new IllegalArgumentException( "Row index (" + rowIndex + ") is below zero!"); } if (rowIndex > maxsize) { throw new IllegalArgumentException("Row index (" + rowIndex + ") is above maximum (" + maxsize + ")!"); } escalator.scrollToRowAndSpacer(rowIndex, destination, paddingPx); } /** * Scrolls to the beginning of the very first row. */ public void scrollToStart() { scrollToRow(0, ScrollDestination.START); } /** * Scrolls to the end of the very last row. */ public void scrollToEnd() { scrollToRow(escalator.getBody().getRowCount() - 1, ScrollDestination.END); } /** * Sets the vertical scroll offset. * * @param px * the number of pixels this grid should be scrolled down */ public void setScrollTop(double px) { escalator.setScrollTop(px); } /** * Gets the vertical scroll offset. * * @return the number of pixels this grid is scrolled down */ public double getScrollTop() { return escalator.getScrollTop(); } /** * Sets the horizontal scroll offset. * * @since 7.5.0 * @param px * the number of pixels this grid should be scrolled right */ public void setScrollLeft(double px) { escalator.setScrollLeft(px); } /** * Gets the horizontal scroll offset. * * @return the number of pixels this grid is scrolled to the right */ public double getScrollLeft() { return escalator.getScrollLeft(); } /** * Returns the height of the scrollable area in pixels. * * @since 7.5.0 * @return the height of the scrollable area in pixels */ public double getScrollHeight() { return escalator.getScrollHeight(); } /** * Returns the width of the scrollable area in pixels. * * @since 7.5.0 * @return the width of the scrollable area in pixels. */ public double getScrollWidth() { return escalator.getScrollWidth(); } private static final Logger getLogger() { return Logger.getLogger(Grid.class.getName()); } /** * Sets the number of rows that should be visible in Grid's body, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. *

* If Grid is currently not in {@link HeightMode#ROW}, the given value is * remembered, and applied once the mode is applied. * * @param rows * The height in terms of number of rows displayed in Grid's * body. If Grid doesn't contain enough rows, white space is * displayed instead. * @throws IllegalArgumentException * if {@code rows} is zero or less * @throws IllegalArgumentException * if {@code rows} is {@link Double#isInifinite(double) * infinite} * @throws IllegalArgumentException * if {@code rows} is {@link Double#isNaN(double) NaN} * * @see #setHeightMode(HeightMode) */ public void setHeightByRows(double rows) throws IllegalArgumentException { escalator.setHeightByRows(rows); } /** * Gets the amount of rows in Grid's body that are shown, while * {@link #getHeightMode()} is {@link HeightMode#ROW}. *

* By default, it is {@value Escalator#DEFAULT_HEIGHT_BY_ROWS}. * * @return the amount of rows that should be shown in Grid's body, while in * {@link HeightMode#ROW}. * @see #setHeightByRows(double) */ public double getHeightByRows() { return escalator.getHeightByRows(); } /** * Defines the mode in which the Grid widget's height is calculated. *

* If {@link HeightMode#CSS} is given, Grid will respect the values given * via {@link #setHeight(String)}, and behave as a traditional Widget. *

* If {@link HeightMode#ROW} is given, Grid will make sure that the body * will display as many rows as {@link #getHeightByRows()} defines. * Note: If headers/footers are inserted or removed, the widget * will resize itself to still display the required amount of rows in its * body. It also takes the horizontal scrollbar into account. * * @param heightMode * the mode in to which Grid should be set */ public void setHeightMode(HeightMode heightMode) { /* * This method is a workaround for the fact that Vaadin re-applies * widget dimensions (height/width) on each state change event. The * original design was to have setHeight an setHeightByRow be equals, * and whichever was called the latest was considered in effect. * * But, because of Vaadin always calling setHeight on the widget, this * approach doesn't work. */ escalator.setHeightMode(heightMode); } /** * Returns the current {@link HeightMode} the Grid is in. *

* Defaults to {@link HeightMode#CSS}. * * @return the current HeightMode */ public HeightMode getHeightMode() { return escalator.getHeightMode(); } private Set getConsumedEventsForRenderer(Renderer renderer) { Set events = new HashSet(); if (renderer instanceof ComplexRenderer) { Collection consumedEvents = ((ComplexRenderer) renderer) .getConsumedEvents(); if (consumedEvents != null) { events.addAll(consumedEvents); } } return events; } @Override public void onBrowserEvent(Event event) { if (!isEnabled()) { return; } String eventType = event.getType(); if (eventType.equals(BrowserEvents.FOCUS) || eventType.equals(BrowserEvents.BLUR)) { super.onBrowserEvent(event); return; } EventTarget target = event.getEventTarget(); if (!Element.is(target) || isOrContainsInSpacer(Element.as(target))) { return; } Element e = Element.as(target); RowContainer container = escalator.findRowContainer(e); Cell cell; if (container == null) { if (eventType.equals(BrowserEvents.KEYDOWN) || eventType.equals(BrowserEvents.KEYUP) || eventType.equals(BrowserEvents.KEYPRESS)) { cell = cellFocusHandler.getFocusedCell(); container = cellFocusHandler.containerWithFocus; } else { // Click might be in an editor cell, should still map. if (editor.editorOverlay != null && editor.editorOverlay.isOrHasChild(e)) { container = escalator.getBody(); int rowIndex = editor.getRow(); int colIndex = editor.getElementColumn(e); if (colIndex < 0) { // Click in editor, but not for any column. return; } TableCellElement cellElement = container .getRowElement(rowIndex).getCells() .getItem(colIndex); cell = new Cell(rowIndex, colIndex, cellElement); } else { if (escalator.getElement().isOrHasChild(e)) { eventCell.set(new Cell(-1, -1, null), Section.BODY); // Fire native events. super.onBrowserEvent(event); } return; } } } else { cell = container.getCell(e); if (eventType.equals(BrowserEvents.MOUSEDOWN)) { cellOnPrevMouseDown = cell; } else if (cell == null && eventType.equals(BrowserEvents.CLICK)) { /* * Chrome has an interesting idea on click targets (see * cellOnPrevMouseDown javadoc). Firefox, on the other hand, has * the mousedown target as the click target. */ cell = cellOnPrevMouseDown; } } assert cell != null : "received " + eventType + "-event with a null cell target"; eventCell.set(cell, getSectionFromContainer(container)); GridEvent gridEvent = new GridEvent(event, eventCell); for (GridEventHandler handler : browserEventHandlers) { handler.onEvent(gridEvent); } } private Section getSectionFromContainer(RowContainer container) { assert container != null : "RowContainer should not be null"; if (container == escalator.getBody()) { return Section.BODY; } else if (container == escalator.getFooter()) { return Section.FOOTER; } else if (container == escalator.getHeader()) { return Section.HEADER; } assert false : "RowContainer was not header, footer or body."; return null; } private boolean isOrContainsInSpacer(Node node) { Node n = node; while (n != null && n != getElement()) { boolean isElement = Element.is(n); if (isElement) { String className = Element.as(n).getClassName(); if (className.contains(getStylePrimaryName() + "-spacer")) { return true; } } n = n.getParentNode(); } return false; } private boolean isElementInChildWidget(Element e) { Widget w = WidgetUtil.findWidget(e); if (w == this) { return false; } /* * If e is directly inside this grid, but the grid is wrapped in a * Composite, findWidget is not going to find this, only the wrapper. * Thus we need to check its parents to see if we encounter this; if we * don't, the found widget is actually a parent of this, so we should * return false. */ while (w != null && w != this) { w = w.getParent(); } return w != null; } private class EditorEventHandler implements GridEventHandler { @Override public void onEvent(GridEvent event) { if (!isEditorEnabled()) { return; } Widget widget; if (editor.focusedColumnIndexDOM < 0) { widget = null; } else { widget = editor.getWidget( getVisibleColumn(editor.focusedColumnIndexDOM)); } EditorDomEvent editorEvent = new EditorDomEvent( event.getDomEvent(), event.getCell(), widget); event.setHandled( getEditor().getEventHandler().handleEvent(editorEvent)); } }; private class SuperEventHandler implements GridEventHandler { @Override public void onEvent(GridEvent event) { if (event.isHandled()) { return; } Grid.super.onBrowserEvent(event.getDomEvent()); } }; private abstract class AbstractGridEventHandler implements GridEventHandler { @Override public void onEvent(GridEvent event) { if (event.isHandled()) { return; } event.setHandled(isElementInChildWidget( Element.as(event.getDomEvent().getEventTarget()))); } }; private class RendererEventHandler extends AbstractGridEventHandler { @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } if (!event.getCell().isBody()) { return; } Column gridColumn = event.getCell().getColumn(); boolean enterKey = event.getDomEvent().getType() .equals(BrowserEvents.KEYDOWN) && event.getDomEvent().getKeyCode() == KeyCodes.KEY_ENTER; boolean doubleClick = event.getDomEvent().getType() .equals(BrowserEvents.DBLCLICK); if (gridColumn.getRenderer() instanceof ComplexRenderer) { ComplexRenderer cplxRenderer = (ComplexRenderer) gridColumn .getRenderer(); if (cplxRenderer.getConsumedEvents() .contains(event.getDomEvent().getType())) { if (cplxRenderer.onBrowserEvent(event.getCell(), event.getDomEvent())) { event.setHandled(true); } } // Calls onActivate if KeyDown and Enter or double click if ((enterKey || doubleClick) && cplxRenderer.onActivate(event.getCell())) { event.setHandled(true); } } } }; private class CellFocusEventHandler extends AbstractGridEventHandler { @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } Collection navigation = cellFocusHandler .getNavigationEvents(); if (navigation.contains(event.getDomEvent().getType())) { cellFocusHandler.handleNavigationEvent(event.getDomEvent(), event.getCell()); } } }; private class HeaderCellDragStartHandler extends AbstractGridEventHandler { @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } if (!isColumnReorderingAllowed()) { return; } if (!event.getCell().isHeader()) { return; } if (event.getCell().getColumnIndex() < getFrozenColumnCount()) { return; } if (event.getDomEvent().getTypeInt() == Event.ONMOUSEDOWN && event.getDomEvent() .getButton() == NativeEvent.BUTTON_LEFT || event.getDomEvent().getTypeInt() == Event.ONTOUCHSTART) { dndHandler.onDragStartOnDraggableElement(event.getDomEvent(), headerCellDndCallback); event.getDomEvent().preventDefault(); event.getDomEvent().stopPropagation(); event.setHandled(true); } } }; private class HeaderDefaultRowEventHandler extends AbstractGridEventHandler { private Point rowEventTouchStartingPoint; @Override public void onEvent(GridEvent event) { super.onEvent(event); if (event.isHandled()) { return; } if (!event.getCell().isHeader()) { return; } if (!getHeader().getRow(event.getCell().getRowIndex()) .isDefault()) { return; } if (!event.getCell().getColumn().isSortable()) { // Only handle sorting events if the column is sortable return; } if (BrowserEvents.MOUSEDOWN.equals(event.getDomEvent().getType()) && event.getDomEvent().getShiftKey()) { // Don't select text when shift clicking on a header. event.getDomEvent().preventDefault(); } if (BrowserEvents.TOUCHSTART .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } event.getDomEvent().preventDefault(); Touch touch = event.getDomEvent().getChangedTouches().get(0); rowEventTouchStartingPoint = new Point(touch.getClientX(), touch.getClientY()); sorter.sortAfterDelay(GridConstants.LONG_TAP_DELAY, true); event.setHandled(true); } else if (BrowserEvents.TOUCHMOVE .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } event.getDomEvent().preventDefault(); Touch touch = event.getDomEvent().getChangedTouches().get(0); double diffX = Math.abs( touch.getClientX() - rowEventTouchStartingPoint.getX()); double diffY = Math.abs( touch.getClientY() - rowEventTouchStartingPoint.getY()); // Cancel long tap if finger strays too far from // starting point if (diffX > GridConstants.LONG_TAP_THRESHOLD || diffY > GridConstants.LONG_TAP_THRESHOLD) { sorter.cancelDelayedSort(); } event.setHandled(true); } else if (BrowserEvents.TOUCHEND .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } if (sorter.isDelayedSortScheduled()) { // Not a long tap yet, perform single sort sorter.cancelDelayedSort(); sorter.sort(event.getCell().getColumn(), false); } event.setHandled(true); } else if (BrowserEvents.TOUCHCANCEL .equals(event.getDomEvent().getType())) { if (event.getDomEvent().getTouches().length() > 1) { return; } sorter.cancelDelayedSort(); event.setHandled(true); } else if (BrowserEvents.CLICK .equals(event.getDomEvent().getType())) { sorter.sort(event.getCell().getColumn(), event.getDomEvent().getShiftKey()); } } }; @Override @SuppressWarnings("deprecation") public com.google.gwt.user.client.Element getSubPartElement( String subPart) { /* * handles details[] (translated to spacer[] for Escalator), cell[], * header[] and footer[] */ // "#header[0][0]/DRAGhANDLE" Element escalatorElement = escalator.getSubPartElement( subPart.replaceFirst("^details\\[", "spacer[")); if (escalatorElement != null) { int detailIdx = subPart.indexOf("/"); if (detailIdx > 0) { String detail = subPart.substring(detailIdx + 1); getLogger().severe("Looking up detail from index " + detailIdx + " onward: \"" + detail + "\""); if (detail.equalsIgnoreCase("content")) { // XXX: Fix this to look up by class name! return DOM.asOld(Element.as(escalatorElement.getChild(0))); } if (detail.equalsIgnoreCase("draghandle")) { // XXX: Fix this to look up by class name! return DOM.asOld(Element.as(escalatorElement.getChild(1))); } } return DOM.asOld(escalatorElement); } SubPartArguments args = SubPartArguments.create(subPart); Element editor = getSubPartElementEditor(args); if (editor != null) { return DOM.asOld(editor); } return null; } private Element getSubPartElementEditor(SubPartArguments args) { if (!args.getType().equalsIgnoreCase("editor") || editor.getState() != State.ACTIVE) { return null; } if (args.getIndicesLength() == 0) { return editor.editorOverlay; } else if (args.getIndicesLength() == 1) { int index = args.getIndex(0); if (index >= columns.size()) { return null; } escalator.scrollToColumn(index, ScrollDestination.ANY, 0); Widget widget = editor.getWidget(columns.get(index)); if (widget != null) { return widget.getElement(); } // No widget for the column. return null; } return null; } @Override @SuppressWarnings("deprecation") public String getSubPartName( com.google.gwt.user.client.Element subElement) { String escalatorStructureName = escalator.getSubPartName(subElement); if (escalatorStructureName != null) { return escalatorStructureName.replaceFirst("^spacer", "details"); } String editorName = getSubPartNameEditor(subElement); if (editorName != null) { return editorName; } return null; } private String getSubPartNameEditor(Element subElement) { if (editor.getState() != State.ACTIVE || !editor.editorOverlay.isOrHasChild(subElement)) { return null; } int i = 0; for (Column column : columns) { if (editor.getWidget(column).getElement() .isOrHasChild(subElement)) { return "editor[" + i + "]"; } ++i; } return "editor"; } private void setSelectColumnRenderer( final Renderer selectColumnRenderer) { if (this.selectColumnRenderer == selectColumnRenderer) { return; } if (this.selectionColumn != null) { selectionColumn.cleanup(); } if (this.selectColumnRenderer != null) { if (this.selectColumnRenderer instanceof ComplexRenderer) { // End of Life for the old selection column renderer. ((ComplexRenderer) this.selectColumnRenderer).destroy(); } // Clear field so frozen column logic in the remove method knows // what to do Column colToRemove = selectionColumn; selectionColumn = null; removeColumnSkipSelectionColumnCheck(colToRemove); cellFocusHandler.offsetRangeBy(-1); } this.selectColumnRenderer = selectColumnRenderer; if (selectColumnRenderer != null) { cellFocusHandler.offsetRangeBy(1); selectionColumn = new SelectionColumn(selectColumnRenderer); addColumnsSkipSelectionColumnCheck( Collections.singleton(selectionColumn), 0); selectionColumn.setEnabled(isEnabled()); selectionColumn.initDone(); } else { selectionColumn = null; refreshBody(); } updateFrozenColumns(); } /** * Sets the current selection model. *

* This function will call {@link SelectionModel#setGrid(Grid)}. * * @param selectionModel * a selection model implementation. * @throws IllegalArgumentException * if selection model argument is null */ public void setSelectionModel(SelectionModel selectionModel) { if (selectionModel == null) { throw new IllegalArgumentException("Selection model can't be null"); } if (this.selectionModel != null) { // Detach selection model from Grid. this.selectionModel.setGrid(null); } this.selectionModel = selectionModel; selectionModel.setGrid(this); setSelectColumnRenderer( this.selectionModel.getSelectionColumnRenderer()); // Refresh rendered rows to update selection, if it has changed refreshBody(); } /** * Gets a reference to the current selection model. * * @return the currently used SelectionModel instance. */ public SelectionModel getSelectionModel() { return selectionModel; } /** * Sets current selection mode. *

* This is a shorthand method for {@link Grid#setSelectionModel}. * * @param mode * a selection mode value * @see SelectionMode */ public void setSelectionMode(SelectionMode mode) { SelectionModel model = mode.createModel(); setSelectionModel(model); } /** * Test if a row is selected. * * @param row * a row object * @return true, if the current selection model considers the provided row * object selected. */ public boolean isSelected(T row) { return selectionModel.isSelected(row); } /** * Select a row using the current selection model. *

* Only selection models implementing {@link SelectionModel.Single} and * {@link SelectionModel.Multi} are supported; for anything else, an * exception will be thrown. * * @param row * a row object * @return true if the current selection changed * @throws IllegalStateException * if the current selection model is not an instance of * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ public boolean select(T row) { if (selectionModel instanceof SelectionModel.Single) { return ((SelectionModel.Single) selectionModel).select(row); } else if (selectionModel instanceof SelectionModel.Multi) { return ((SelectionModel.Multi) selectionModel) .select(Collections.singleton(row)); } else { throw new IllegalStateException("Unsupported selection model"); } } /** * Deselect a row using the current selection model. *

* Only selection models implementing {@link SelectionModel.Single} and * {@link SelectionModel.Multi} are supported; for anything else, an * exception will be thrown. * * @param row * a row object * @return true if the current selection changed * @throws IllegalStateException * if the current selection model is not an instance of * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ public boolean deselect(T row) { if (selectionModel instanceof SelectionModel.Single) { return ((SelectionModel.Single) selectionModel).deselect(row); } else if (selectionModel instanceof SelectionModel.Multi) { return ((SelectionModel.Multi) selectionModel) .deselect(Collections.singleton(row)); } else { throw new IllegalStateException("Unsupported selection model"); } } /** * Deselect all rows using the current selection model. * * @return true if the current selection changed * @throws IllegalStateException * if the current selection model is not an instance of * {@link SelectionModel.Single} or {@link SelectionModel.Multi} */ public boolean deselectAll() { if (selectionModel instanceof SelectionModel.Single) { Single single = (SelectionModel.Single) selectionModel; if (single.getSelectedRow() != null) { return single.deselect(single.getSelectedRow()); } else { return false; } } else if (selectionModel instanceof SelectionModel.Multi) { return ((SelectionModel.Multi) selectionModel).deselectAll(); } else { throw new IllegalStateException("Unsupported selection model"); } } /** * Gets last selected row from the current SelectionModel. *

* Only selection models implementing {@link SelectionModel.Single} are * valid for this method; for anything else, use the * {@link Grid#getSelectedRows()} method. * * @return a selected row reference, or null, if no row is selected * @throws IllegalStateException * if the current selection model is not an instance of * {@link SelectionModel.Single} */ public T getSelectedRow() { if (selectionModel instanceof SelectionModel.Single) { return ((SelectionModel.Single) selectionModel).getSelectedRow(); } else { throw new IllegalStateException( "Unsupported selection model; can not get single selected row"); } } /** * Gets currently selected rows from the current selection model. * * @return a non-null collection containing all currently selected rows. */ public Collection getSelectedRows() { return selectionModel.getSelectedRows(); } @Override public HandlerRegistration addSelectionHandler( final SelectionHandler handler) { return addHandler(handler, SelectionEvent.getType()); } /** * Sets the current sort order using the fluid Sort API. Read the * documentation for {@link Sort} for more information. * * @param s * a sort instance */ public void sort(Sort s) { setSortOrder(s.build()); } /** * Sorts the Grid data in ascending order along one column. * * @param column * a grid column reference */ public void sort(Column column) { sort(column, SortDirection.ASCENDING); } /** * Sorts the Grid data along one column. * * @param column * a grid column reference * @param direction * a sort direction value */ public void sort(Column column, SortDirection direction) { sort(Sort.by(column, direction)); } /** * Sets the sort order to use. Setting this causes the Grid to re-sort * itself. * * @param order * a sort order list. If set to null, the sort order is cleared. */ public void setSortOrder(List order) { setSortOrder(order, false); } /** * Clears the sort order and indicators without re-sorting. */ private void clearSortOrder() { sortOrder.clear(); refreshHeader(); } private void setSortOrder(List order, boolean userOriginated) { if (order != sortOrder) { sortOrder.clear(); if (order != null) { sortOrder.addAll(order); } } sort(userOriginated); } /** * Get a copy of the current sort order array. * * @return a copy of the current sort order array */ public List getSortOrder() { return Collections.unmodifiableList(sortOrder); } /** * Finds the sorting order for this column */ private SortOrder getSortOrder(Column column) { for (SortOrder order : getSortOrder()) { if (order.getColumn() == column) { return order; } } return null; } /** * Register a GWT event handler for a sorting event. This handler gets * called whenever this Grid needs its data source to provide data sorted in * a specific order. * * @param handler * a sort event handler * @return the registration for the event */ public HandlerRegistration addSortHandler(SortHandler handler) { return addHandler(handler, SortEvent.getType()); } /** * Register a GWT event handler for a select all event. This handler gets * called whenever Grid needs all rows selected. * * @param handler * a select all event handler */ public HandlerRegistration addSelectAllHandler( SelectAllHandler handler) { return addHandler(handler, SelectAllEvent.getType()); } /** * Register a GWT event handler for a data available event. This handler * gets called whenever the {@link DataSource} for this Grid has new data * available. *

* This handle will be fired with the current available data after * registration is done. * * @param handler * a data available event handler * @return the registartion for the event */ public HandlerRegistration addDataAvailableHandler( final DataAvailableHandler handler) { // Deferred call to handler with current row range Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { if (!dataSource.isWaitingForData()) { handler.onDataAvailable( new DataAvailableEvent(currentDataAvailable)); } } }); return addHandler(handler, DataAvailableEvent.TYPE); } /** * Register a BodyKeyDownHandler to this Grid. The event for this handler is * fired when a KeyDown event occurs while cell focus is in the Body of this * Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addBodyKeyDownHandler( BodyKeyDownHandler handler) { return addHandler(handler, GridKeyDownEvent.TYPE); } /** * Register a BodyKeyUpHandler to this Grid. The event for this handler is * fired when a KeyUp event occurs while cell focus is in the Body of this * Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addBodyKeyUpHandler(BodyKeyUpHandler handler) { return addHandler(handler, GridKeyUpEvent.TYPE); } /** * Register a BodyKeyPressHandler to this Grid. The event for this handler * is fired when a KeyPress event occurs while cell focus is in the Body of * this Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addBodyKeyPressHandler( BodyKeyPressHandler handler) { return addHandler(handler, GridKeyPressEvent.TYPE); } /** * Register a HeaderKeyDownHandler to this Grid. The event for this handler * is fired when a KeyDown event occurs while cell focus is in the Header of * this Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addHeaderKeyDownHandler( HeaderKeyDownHandler handler) { return addHandler(handler, GridKeyDownEvent.TYPE); } /** * Register a HeaderKeyUpHandler to this Grid. The event for this handler is * fired when a KeyUp event occurs while cell focus is in the Header of this * Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addHeaderKeyUpHandler( HeaderKeyUpHandler handler) { return addHandler(handler, GridKeyUpEvent.TYPE); } /** * Register a HeaderKeyPressHandler to this Grid. The event for this handler * is fired when a KeyPress event occurs while cell focus is in the Header * of this Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addHeaderKeyPressHandler( HeaderKeyPressHandler handler) { return addHandler(handler, GridKeyPressEvent.TYPE); } /** * Register a FooterKeyDownHandler to this Grid. The event for this handler * is fired when a KeyDown event occurs while cell focus is in the Footer of * this Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addFooterKeyDownHandler( FooterKeyDownHandler handler) { return addHandler(handler, GridKeyDownEvent.TYPE); } /** * Register a FooterKeyUpHandler to this Grid. The event for this handler is * fired when a KeyUp event occurs while cell focus is in the Footer of this * Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addFooterKeyUpHandler( FooterKeyUpHandler handler) { return addHandler(handler, GridKeyUpEvent.TYPE); } /** * Register a FooterKeyPressHandler to this Grid. The event for this handler * is fired when a KeyPress event occurs while cell focus is in the Footer * of this Grid. * * @param handler * the key handler to register * @return the registration for the event */ public HandlerRegistration addFooterKeyPressHandler( FooterKeyPressHandler handler) { return addHandler(handler, GridKeyPressEvent.TYPE); } /** * Register a BodyClickHandler to this Grid. The event for this handler is * fired when a Click event occurs in the Body of this Grid. * * @param handler * the click handler to register * @return the registration for the event */ public HandlerRegistration addBodyClickHandler(BodyClickHandler handler) { return addHandler(handler, GridClickEvent.TYPE); } /** * Register a HeaderClickHandler to this Grid. The event for this handler is * fired when a Click event occurs in the Header of this Grid. * * @param handler * the click handler to register * @return the registration for the event */ public HandlerRegistration addHeaderClickHandler( HeaderClickHandler handler) { return addHandler(handler, GridClickEvent.TYPE); } /** * Register a FooterClickHandler to this Grid. The event for this handler is * fired when a Click event occurs in the Footer of this Grid. * * @param handler * the click handler to register * @return the registration for the event */ public HandlerRegistration addFooterClickHandler( FooterClickHandler handler) { return addHandler(handler, GridClickEvent.TYPE); } /** * Register a BodyDoubleClickHandler to this Grid. The event for this * handler is fired when a double click event occurs in the Body of this * Grid. * * @param handler * the double click handler to register * @return the registration for the event */ public HandlerRegistration addBodyDoubleClickHandler( BodyDoubleClickHandler handler) { return addHandler(handler, GridDoubleClickEvent.TYPE); } /** * Register a HeaderDoubleClickHandler to this Grid. The event for this * handler is fired when a double click event occurs in the Header of this * Grid. * * @param handler * the double click handler to register * @return the registration for the event */ public HandlerRegistration addHeaderDoubleClickHandler( HeaderDoubleClickHandler handler) { return addHandler(handler, GridDoubleClickEvent.TYPE); } /** * Register a FooterDoubleClickHandler to this Grid. The event for this * handler is fired when a double click event occurs in the Footer of this * Grid. * * @param handler * the double click handler to register * @return the registration for the event */ public HandlerRegistration addFooterDoubleClickHandler( FooterDoubleClickHandler handler) { return addHandler(handler, GridDoubleClickEvent.TYPE); } /** * Register a column reorder handler to this Grid. The event for this * handler is fired when the Grid's columns are reordered. * * @since 7.5.0 * @param handler * the handler for the event * @return the registration for the event */ public HandlerRegistration addColumnReorderHandler( ColumnReorderHandler handler) { return addHandler(handler, ColumnReorderEvent.getType()); } /** * Register a column visibility change handler to this Grid. The event for * this handler is fired when the Grid's columns change visibility. * * @since 7.5.0 * @param handler * the handler for the event * @return the registration for the event */ public HandlerRegistration addColumnVisibilityChangeHandler( ColumnVisibilityChangeHandler handler) { return addHandler(handler, ColumnVisibilityChangeEvent.getType()); } /** * Register a column resize handler to this Grid. The event for this handler * is fired when the Grid's columns are resized. * * @since 7.6 * @param handler * the handler for the event * @return the registration for the event */ public HandlerRegistration addColumnResizeHandler( ColumnResizeHandler handler) { return addHandler(handler, ColumnResizeEvent.getType()); } /** * Register a enabled status change handler to this Grid. The event for this * handler is fired when the Grid changes from disabled to enabled and * vice-versa. * * @param handler * the handler for the event * @return the registration for the event */ public HandlerRegistration addEnabledHandler(GridEnabledHandler handler) { return addHandler(handler, GridEnabledEvent.TYPE); } public HandlerRegistration addRowHeightChangedHandler( RowHeightChangedHandler handler) { return escalator.addHandler(handler, RowHeightChangedEvent.TYPE); } /** * Adds a spacer visibility changed handler to the underlying escalator. * * @param handler * the handler to be called when a spacer's visibility changes * @return the registration object with which the handler can be removed * @since 7.7.13 */ public HandlerRegistration addSpacerVisibilityChangedHandler( SpacerVisibilityChangedHandler handler) { return escalator.addHandler(handler, SpacerVisibilityChangedEvent.TYPE); } /** * Adds a low-level DOM event handler to this Grid. The handler is inserted * into the given position in the list of handlers. The handlers are invoked * in order. If the * {@link GridEventHandler#onEvent(Event, EventCellReference) onEvent} * method of a handler returns true, subsequent handlers are not invoked. * * @param index * the index to insert the handler to * @param handler * the handler to add */ public void addBrowserEventHandler(int index, GridEventHandler handler) { browserEventHandlers.add(index, handler); } /** * Apply sorting to data source. */ private void sort(boolean userOriginated) { refreshHeader(); fireEvent(new SortEvent(this, Collections.unmodifiableList(sortOrder), userOriginated)); } private int getLastVisibleRowIndex() { int lastRowIndex = escalator.getVisibleRowRange().getEnd(); int footerTop = escalator.getFooter().getElement().getAbsoluteTop(); Element lastRow; do { lastRow = escalator.getBody().getRowElement(--lastRowIndex); } while (lastRow.getAbsoluteTop() > footerTop); return lastRowIndex; } private int getFirstVisibleRowIndex() { int firstRowIndex = escalator.getVisibleRowRange().getStart(); int headerBottom = escalator.getHeader().getElement() .getAbsoluteBottom(); Element firstRow = escalator.getBody().getRowElement(firstRowIndex); while (firstRow.getAbsoluteBottom() < headerBottom) { firstRow = escalator.getBody().getRowElement(++firstRowIndex); } return firstRowIndex; } /** * Adds a scroll handler to this grid. * * @param handler * the scroll handler to add * @return a handler registration for the registered scroll handler */ public HandlerRegistration addScrollHandler(ScrollHandler handler) { return addHandler(handler, ScrollEvent.TYPE); } @Override public boolean isWorkPending() { return escalator.isWorkPending() || dataSource.isWaitingForData() || autoColumnWidthsRecalculator.isScheduled() || editor.isWorkPending(); } /** * Returns whether columns can be reordered with drag and drop. * * @since 7.5.0 * @return true if columns can be reordered, false otherwise */ public boolean isColumnReorderingAllowed() { return columnReorderingAllowed; } /** * Sets whether column reordering with drag and drop is allowed or not. * * @since 7.5.0 * @param columnReorderingAllowed * specifies whether column reordering is allowed */ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) { this.columnReorderingAllowed = columnReorderingAllowed; } /** * Sets a new column order for the grid. All columns which are not ordered * here will remain in the order they were before as the last columns of * grid. * * @param orderedColumns * array of columns in wanted order */ public void setColumnOrder(Column... orderedColumns) { ColumnConfiguration conf = getEscalator().getColumnConfiguration(); // Trigger ComplexRenderer.destroy for old content conf.removeColumns(0, conf.getColumnCount()); List> newOrder = new ArrayList>(); if (selectionColumn != null) { newOrder.add(selectionColumn); } int i = 0; for (Column column : orderedColumns) { if (columns.contains(column)) { newOrder.add(column); ++i; } else { throw new IllegalArgumentException("Given column at index " + i + " does not exist in Grid"); } } if (columns.size() != newOrder.size()) { columns.removeAll(newOrder); newOrder.addAll(columns); } columns = newOrder; List> visibleColumns = getVisibleColumns(); // Do ComplexRenderer.init and render new content conf.insertColumns(0, visibleColumns.size()); // Number of frozen columns should be kept same #16901 updateFrozenColumns(); // Update column widths. for (Column column : columns) { column.reapplyWidth(); } // Recalculate all the colspans for (HeaderRow row : header.getRows()) { row.calculateColspans(); } for (FooterRow row : footer.getRows()) { row.calculateColspans(); } columnHider.updateTogglesOrder(); fireEvent(new ColumnReorderEvent()); } /** * Sets the style generator that is used for generating styles for cells. * * @param cellStyleGenerator * the cell style generator to set, or null to * remove a previously set generator */ public void setCellStyleGenerator( CellStyleGenerator cellStyleGenerator) { this.cellStyleGenerator = cellStyleGenerator; refreshBody(); } /** * Gets the style generator that is used for generating styles for cells. * * @return the cell style generator, or null if no generator is * set */ public CellStyleGenerator getCellStyleGenerator() { return cellStyleGenerator; } /** * Sets the style generator that is used for generating styles for rows. * * @param rowStyleGenerator * the row style generator to set, or null to remove * a previously set generator */ public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) { this.rowStyleGenerator = rowStyleGenerator; refreshBody(); } /** * Gets the style generator that is used for generating styles for rows. * * @return the row style generator, or null if no generator is * set */ public RowStyleGenerator getRowStyleGenerator() { return rowStyleGenerator; } private static void setCustomStyleName(Element element, String styleName) { assert element != null; String oldStyleName = element .getPropertyString(CUSTOM_STYLE_PROPERTY_NAME); if (!SharedUtil.equals(oldStyleName, styleName)) { if (oldStyleName != null && !oldStyleName.isEmpty()) { element.removeClassName(oldStyleName); } if (styleName != null && !styleName.isEmpty()) { element.addClassName(styleName); } element.setPropertyString(CUSTOM_STYLE_PROPERTY_NAME, styleName); } } /** * Opens the editor over the row with the given index. * * @param rowIndex * the index of the row to be edited * * @throws IllegalStateException * if the editor is not enabled * @throws IllegalStateException * if the editor is already in edit mode */ public void editRow(int rowIndex) { editor.editRow(rowIndex); } /** * Returns whether the editor is currently open on some row. * * @return {@code true} if the editor is active, {@code false} otherwise. */ public boolean isEditorActive() { return editor.getState() != State.INACTIVE; } /** * Saves any unsaved changes in the editor to the data source. * * @throws IllegalStateException * if the editor is not enabled * @throws IllegalStateException * if the editor is not in edit mode */ public void saveEditor() { editor.save(); } /** * Cancels the currently active edit and hides the editor. Any changes that * are not {@link #saveEditor() saved} are lost. * * @throws IllegalStateException * if the editor is not enabled * @throws IllegalStateException * if the editor is not in edit mode */ public void cancelEditor() { editor.cancel(); } /** * Returns the handler responsible for binding data and editor widgets to * the editor. * * @return the editor handler or null if not set */ public EditorHandler getEditorHandler() { return editor.getHandler(); } /** * Sets the handler responsible for binding data and editor widgets to the * editor. * * @param handler * the new editor handler * * @throws IllegalStateException * if the editor is currently in edit mode */ public void setEditorHandler(EditorHandler handler) { editor.setHandler(handler); } /** * Returns the enabled state of the editor. * * @return true if editing is enabled, false otherwise */ public boolean isEditorEnabled() { return editor.isEnabled(); } /** * Sets the enabled state of the editor. * * @param enabled * true to enable editing, false to disable * * @throws IllegalStateException * if in edit mode and trying to disable * @throws IllegalStateException * if the editor handler is not set */ public void setEditorEnabled(boolean enabled) { editor.setEnabled(enabled); } /** * Returns the editor widget associated with the given column. If the editor * is not active, returns null. * * @param column * the column * @return the widget if the editor is open, null otherwise */ public Widget getEditorWidget(Column column) { return editor.getWidget(column); } /** * Sets the caption on the save button in the Grid editor. * * @param saveCaption * the caption to set * @throws IllegalArgumentException * if {@code saveCaption} is {@code null} */ public void setEditorSaveCaption(String saveCaption) throws IllegalArgumentException { editor.setSaveCaption(saveCaption); } /** * Gets the current caption on the save button in the Grid editor. * * @return the current caption on the save button */ public String getEditorSaveCaption() { return editor.getSaveCaption(); } /** * Sets the caption on the cancel button in the Grid editor. * * @param cancelCaption * the caption to set * @throws IllegalArgumentException * if {@code cancelCaption} is {@code null} */ public void setEditorCancelCaption(String cancelCaption) throws IllegalArgumentException { editor.setCancelCaption(cancelCaption); } /** * Gets the caption on the cancel button in the Grid editor. * * @return the current caption on the cancel button */ public String getEditorCancelCaption() { return editor.getCancelCaption(); } @Override protected void onAttach() { super.onAttach(); if (getEscalator().getBody().getRowCount() == 0 && dataSource != null) { setEscalatorSizeFromDataSource(); } // Grid was just attached to DOM. Column widths should be calculated. recalculateColumnWidths(); for (int row : reattachVisibleDetails) { setDetailsVisible(row, true); } reattachVisibleDetails.clear(); } @Override protected void onDetach() { Set details = new HashSet(visibleDetails); reattachVisibleDetails.clear(); reattachVisibleDetails.addAll(details); for (int row : details) { setDetailsVisible(row, false); } super.onDetach(); } @Override public void onResize() { super.onResize(); /* * Delay calculation to be deferred so Escalator can do it's magic. */ Scheduler.get().scheduleFinally(new ScheduledCommand() { @Override public void execute() { if (escalator .getInnerWidth() != autoColumnWidthsRecalculator.lastCalculatedInnerWidth) { recalculateColumnWidths(); } // Vertical resizing could make editor positioning invalid so it // needs to be recalculated on resize if (isEditorActive()) { editor.updateVerticalScrollPosition(); } // if there is a resize, we need to refresh the body to avoid an // off-by-one error which occurs when the user scrolls all the // way to the bottom. refreshBody(); } }); } /** * Grid does not support adding Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @param w * irrelevant * @throws UnsupportedOperationException * always */ @Override @Deprecated public void add(Widget w) { throw new UnsupportedOperationException( "Cannot add widgets to Grid with this method"); } /** * Grid does not support clearing Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @throws UnsupportedOperationException * always */ @Override @Deprecated public void clear() { throw new UnsupportedOperationException( "Cannot clear widgets from Grid this way"); } /** * Grid does not support iterating through Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @return never * @throws UnsupportedOperationException * always */ @Override @Deprecated public Iterator iterator() { throw new UnsupportedOperationException( "Cannot iterate through widgets in Grid this way"); } /** * Grid does not support removing Widgets this way. *

* This method is implemented only because removing widgets from Grid (added * via e.g. {@link Renderer}s) requires the {@link HasWidgets} interface. * * @return always false */ @Override @Deprecated public boolean remove(Widget w) { /* * This is the method that is the sole reason to have Grid implement * HasWidget - when Vaadin removes a Component from the hierarchy, the * corresponding Widget will call removeFromParent() on itself. GWT will * check there that its parent (i.e. Grid) implements HasWidgets, and * will call this remove(Widget) method. * * tl;dr: all this song and dance to make sure GWT's sanity checks * aren't triggered, even though they effectively do nothing interesting * from Grid's perspective. */ return false; } /** * Accesses the package private method Widget#setParent() * * @param widget * The widget to access * @param parent * The parent to set */ private static native final void setParent(Widget widget, Grid parent) /*-{ widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent); }-*/; private static native final void onAttach(Widget widget) /*-{ widget.@Widget::onAttach()(); }-*/; private static native final void onDetach(Widget widget) /*-{ widget.@Widget::onDetach()(); }-*/; @Override protected void doAttachChildren() { if (sidebar.getParent() == this) { onAttach(sidebar); } } @Override protected void doDetachChildren() { if (sidebar.getParent() == this) { onDetach(sidebar); } } private void attachWidget(Widget w, Element parent) { assert w.getParent() == null; parent.appendChild(w.getElement()); setParent(w, this); } private void detachWidget(Widget w) { assert w.getParent() == this; setParent(w, null); w.getElement().removeFromParent(); } /** * Resets all cached pixel sizes and reads new values from the DOM. This * methods should be used e.g. when styles affecting the dimensions of * elements in this grid have been changed. */ public void resetSizesFromDom() { getEscalator().resetSizesFromDom(); } /** * Sets a new details generator for row details. *

* The currently opened row details will be re-rendered. * * @since 7.5.0 * @param detailsGenerator * the details generator to set * @throws IllegalArgumentException * if detailsGenerator is null; */ public void setDetailsGenerator(DetailsGenerator detailsGenerator) throws IllegalArgumentException { if (detailsGenerator == null) { throw new IllegalArgumentException( "Details generator may not be null"); } for (Integer index : visibleDetails) { setDetailsVisible(index, false); } this.detailsGenerator = detailsGenerator; // this will refresh all visible spacers escalator.getBody().setSpacerUpdater(gridSpacerUpdater); } /** * Gets the current details generator for row details. * * @since 7.5.0 * @return the detailsGenerator the current details generator */ public DetailsGenerator getDetailsGenerator() { return detailsGenerator; } /** * Shows or hides the details for a specific row. *

* This method does nothing if trying to set show already-visible details, * or hide already-hidden details. * * @since 7.5.0 * @param rowIndex * the index of the affected row * @param visible * true to show the details, or false * to hide them * @see #isDetailsVisible(int) */ public void setDetailsVisible(int rowIndex, boolean visible) { if (DetailsGenerator.NULL.equals(detailsGenerator)) { return; } Integer rowIndexInteger = Integer.valueOf(rowIndex); /* * We want to prevent opening a details row twice, so any subsequent * openings (or closings) of details is a NOOP. * * When a details row is opened, it is given an arbitrary height * (because Escalator requires a height upon opening). Only when it's * opened, Escalator will ask the generator to generate a widget, which * we then can measure. When measured, we correct the initial height by * the original height. * * Without this check, we would override the measured height, and revert * back to the initial, arbitrary, height which would most probably be * wrong. * * see GridSpacerUpdater.init for implementation details. */ boolean isVisible = isDetailsVisible(rowIndex); if (visible && !isVisible) { escalator.getBody().setSpacer(rowIndex, DETAILS_ROW_INITIAL_HEIGHT); visibleDetails.add(rowIndexInteger); } else if (!visible && isVisible) { escalator.getBody().setSpacer(rowIndex, -1); visibleDetails.remove(rowIndexInteger); } } /** * Check whether the details for a row is visible or not. * * @since 7.5.0 * @param rowIndex * the index of the row for which to check details * @return true if the details for the given row is visible * @see #setDetailsVisible(int, boolean) */ public boolean isDetailsVisible(int rowIndex) { return visibleDetails.contains(Integer.valueOf(rowIndex)); } /** * Update details row height. * * @since 7.7.11 * @param rowIndex * the index of the row for which to update details height * @param height * new height of the details row */ public void setDetailsHeight(int rowIndex, double height) { escalator.getBody().setSpacer(rowIndex, height); } /** * Requests that the column widths should be recalculated. *

* The actual recalculation is not necessarily done immediately so you * cannot rely on the columns being the correct width after the call * returns. * * @since 7.4.1 */ public void recalculateColumnWidths() { autoColumnWidthsRecalculator.schedule(); } /** * Gets the customizable menu bar that is by default used for toggling * column hidability. The application developer is allowed to add their * custom items to the end of the menu, but should try to avoid modifying * the items in the beginning of the menu that control the column hiding if * any columns are marked as hidable. A toggle for opening the menu will be * displayed whenever the menu contains at least one item. * * @since 7.5.0 * @return the menu bar */ public MenuBar getSidebarMenu() { return sidebar.menuBar; } /** * Tests whether the sidebar menu is currently open. * * @since 7.5.0 * @see #getSidebarMenu() * @return true if the sidebar is open; false if * it is closed */ public boolean isSidebarOpen() { return sidebar.isOpen(); } /** * Sets whether the sidebar menu is open. * * * @since 7.5.0 * @see #getSidebarMenu() * @see #isSidebarOpen() * @param sidebarOpen * true to open the sidebar; false to * close it */ public void setSidebarOpen(boolean sidebarOpen) { if (sidebarOpen) { sidebar.open(); } else { sidebar.close(); } } @Override public int getTabIndex() { return FocusUtil.getTabIndex(this); } @Override public void setAccessKey(char key) { FocusUtil.setAccessKey(this, key); } @Override public void setFocus(boolean focused) { FocusUtil.setFocus(this, focused); } @Override public void setTabIndex(int index) { FocusUtil.setTabIndex(this, index); } @Override public void focus() { setFocus(true); } /** * Sets the buffered editor mode. * * @since 7.6 * @param editorBuffered * {@code true} to enable buffered editor, {@code false} to * disable it */ public void setEditorBuffered(boolean editorBuffered) { editor.setBuffered(editorBuffered); } /** * Gets the buffered editor mode. * * @since 7.6 * @return true if buffered editor is enabled, * false otherwise */ public boolean isEditorBuffered() { return editor.isBuffered(); } /** * Returns the {@link EventCellReference} for the latest event fired from * this Grid. *

* Note: This cell reference will be updated when firing the next event. * * @since 7.5 * @return event cell reference */ public EventCellReference getEventCell() { return eventCell; } /** * Returns a CellReference for the cell to which the given element belongs * to. * * @since 7.6 * @param element * Element to find from the cell's content. * @return CellReference or null if cell was not found. */ public CellReference getCellReference(Element element) { RowContainer container = getEscalator().findRowContainer(element); if (container != null) { Cell cell = container.getCell(element); if (cell != null) { EventCellReference cellRef = new EventCellReference(this); cellRef.set(cell, getSectionFromContainer(container)); return cellRef; } } return null; } /** * Checks if selection by the user is allowed in the grid. * * @return true if selection by the user is allowed by the * selection model (the default), false otherwise * @since 7.7.7 */ public boolean isUserSelectionAllowed() { if (!(getSelectionModel() instanceof HasUserSelectionAllowed)) { // Selection model does not support toggling user selection allowed // - old default is to always allow selection return true; } return ((HasUserSelectionAllowed) getSelectionModel()) .isUserSelectionAllowed(); } }