--- /dev/null
- client.updateVariable(paintableId, "text", getText(), false);
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.textfield;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+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.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.EventId;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Field;
+
+/**
+ * This class represents a basic text input field with one row.
+ *
+ * @author Vaadin Ltd.
+ *
+ */
+public class VTextField extends TextBoxBase implements Field, ChangeHandler,
+ FocusHandler, BlurHandler, KeyDownHandler {
+
+ public static final String VAR_CUR_TEXT = "curText";
+
+ public static final String ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS = "nvc";
+ /**
+ * The input node CSS classname.
+ */
+ public static final String CLASSNAME = "v-textfield";
+ /**
+ * This CSS classname is added to the input node on hover.
+ */
+ public static final String CLASSNAME_FOCUS = "focus";
+
+ protected String paintableId;
+
+ protected ApplicationConnection client;
+
+ protected String valueBeforeEdit = null;
+
+ /**
+ * Set to false if a text change event has been sent since the last value
+ * change event. This means that {@link #valueBeforeEdit} should not be
+ * trusted when determining whether a text change even should be sent.
+ */
+ private boolean valueBeforeEditIsSynced = true;
+
+ private boolean immediate = false;
+ private int maxLength = -1;
+
+ private static final String CLASSNAME_PROMPT = "prompt";
+ public static final String ATTR_TEXTCHANGE_TIMEOUT = "iet";
+ public static final String VAR_CURSOR = "c";
+ public static final String ATTR_TEXTCHANGE_EVENTMODE = "iem";
+ protected static final String TEXTCHANGE_MODE_EAGER = "EAGER";
+ private static final String TEXTCHANGE_MODE_TIMEOUT = "TIMEOUT";
+
+ private String inputPrompt = null;
+ private boolean prompting = false;
+ private int lastCursorPos = -1;
+
+ public VTextField() {
+ this(DOM.createInputText());
+ }
+
+ protected VTextField(Element node) {
+ super(node);
+ setStyleName(CLASSNAME);
+ addChangeHandler(this);
+ if (BrowserInfo.get().isIE()) {
+ // IE does not send change events when pressing enter in a text
+ // input so we handle it using a key listener instead
+ addKeyDownHandler(this);
+ }
+ addFocusHandler(this);
+ addBlurHandler(this);
+ }
+
+ /*
+ * TODO When GWT adds ONCUT, add it there and remove workaround. See
+ * http://code.google.com/p/google-web-toolkit/issues/detail?id=4030
+ *
+ * Also note that the cut/paste are not totally crossbrowsers compatible.
+ * E.g. in Opera mac works via context menu, but on via File->Paste/Cut.
+ * Opera might need the polling method for 100% working textchanceevents.
+ * Eager polling for a change is bit dum and heavy operation, so I guess we
+ * should first try to survive without.
+ */
+ protected static final int TEXTCHANGE_EVENTS = Event.ONPASTE
+ | Event.KEYEVENTS | Event.ONMOUSEUP;
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ if (listenTextChangeEvents
+ && (event.getTypeInt() & TEXTCHANGE_EVENTS) == event
+ .getTypeInt()) {
+ deferTextChangeEvent();
+ }
+
+ }
+
+ /*
+ * TODO optimize this so that only changes are sent + make the value change
+ * event just a flag that moves the current text to value
+ */
+ private String lastTextChangeString = null;
+
+ private String getLastCommunicatedString() {
+ return lastTextChangeString;
+ }
+
+ private void communicateTextValueToServer() {
+ String text = getText();
+ if (prompting) {
+ // Input prompt visible, text is actually ""
+ text = "";
+ }
+ if (!text.equals(getLastCommunicatedString())) {
+ if (valueBeforeEditIsSynced && text.equals(valueBeforeEdit)) {
+ /*
+ * Value change for the current text has been enqueued since the
+ * last text change event was sent, but we can't know that it
+ * has been sent to the server. Ensure that all pending changes
+ * are sent now. Sending a value change without a text change
+ * will simulate a TextChangeEvent on the server.
+ */
+ client.sendPendingVariableChanges();
+ } else {
+ // Default case - just send an immediate text change message
+ client.updateVariable(paintableId, VAR_CUR_TEXT, text, true);
+
+ // Shouldn't investigate valueBeforeEdit to avoid duplicate text
+ // change events as the states are not in sync any more
+ valueBeforeEditIsSynced = false;
+ }
+ lastTextChangeString = text;
+ }
+ }
+
+ private Timer textChangeEventTrigger = new Timer() {
+
+ @Override
+ public void run() {
+ if (isAttached()) {
+ updateCursorPosition();
+ communicateTextValueToServer();
+ scheduled = false;
+ }
+ }
+ };
+ private boolean scheduled = false;
+ protected boolean listenTextChangeEvents;
+ protected String textChangeEventMode;
+ protected int textChangeEventTimeout;
+
+ private void deferTextChangeEvent() {
+ if (textChangeEventMode.equals(TEXTCHANGE_MODE_TIMEOUT) && scheduled) {
+ return;
+ } else {
+ textChangeEventTrigger.cancel();
+ }
+ textChangeEventTrigger.schedule(getTextChangeEventTimeout());
+ scheduled = true;
+ }
+
+ private int getTextChangeEventTimeout() {
+ return textChangeEventTimeout;
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ boolean wasReadOnly = isReadOnly();
+
+ if (readOnly) {
+ setTabIndex(-1);
+ } else if (wasReadOnly && !readOnly && getTabIndex() == -1) {
+ /*
+ * Need to manually set tab index to 0 since server will not send
+ * the tab index if it is 0.
+ */
+ setTabIndex(0);
+ }
+
+ super.setReadOnly(readOnly);
+ }
+
+ protected void updateFieldContent(final String text) {
+ setPrompting(inputPrompt != null && focusedTextField != this
+ && (text.equals("")));
+
+ String fieldValue;
+ if (prompting) {
+ fieldValue = isReadOnly() ? "" : inputPrompt;
+ addStyleDependentName(CLASSNAME_PROMPT);
+ } else {
+ fieldValue = text;
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ setText(fieldValue);
+
+ lastTextChangeString = valueBeforeEdit = text;
+ valueBeforeEditIsSynced = true;
+ }
+
+ protected void onCut() {
+ if (listenTextChangeEvents) {
+ deferTextChangeEvent();
+ }
+ }
+
+ protected native void attachCutEventListener(Element el)
+ /*-{
+ var me = this;
+ el.oncut = $entry(function() {
+ me.@com.vaadin.terminal.gwt.client.ui.textfield.VTextField::onCut()();
+ });
+ }-*/;
+
+ protected native void detachCutEventListener(Element el)
+ /*-{
+ el.oncut = null;
+ }-*/;
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ detachCutEventListener(getElement());
+ if (focusedTextField == this) {
+ focusedTextField = null;
+ }
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (listenTextChangeEvents) {
+ detachCutEventListener(getElement());
+ }
+ }
+
+ protected void setMaxLength(int newMaxLength) {
+ if (newMaxLength >= 0) {
+ maxLength = newMaxLength;
+ } else {
+ maxLength = -1;
+ }
+ setMaxLengthToElement(newMaxLength);
+ }
+
+ protected void setMaxLengthToElement(int newMaxLength) {
+ if (newMaxLength >= 0) {
+ getElement().setPropertyInt("maxLength", newMaxLength);
+ } else {
+ getElement().removeAttribute("maxLength");
+ }
+ }
+
+ public int getMaxLength() {
+ return maxLength;
+ }
+
+ @Override
+ public void onChange(ChangeEvent event) {
+ valueChange(false);
+ }
+
+ /**
+ * Called when the field value might have changed and/or the field was
+ * blurred. These are combined so the blur event is sent in the same batch
+ * as a possible value change event (these are often connected).
+ *
+ * @param blurred
+ * true if the field was blurred
+ */
+ public void valueChange(boolean blurred) {
+ if (client != null && paintableId != null) {
+ boolean sendBlurEvent = false;
+ boolean sendValueChange = false;
+
+ if (blurred && client.hasEventListeners(this, EventId.BLUR)) {
+ sendBlurEvent = true;
+ client.updateVariable(paintableId, EventId.BLUR, "", false);
+ }
+
+ String newText = getText();
+ if (!prompting && newText != null
+ && !newText.equals(valueBeforeEdit)) {
+ sendValueChange = immediate;
++ client.updateVariable(paintableId, "text", newText, false);
+ valueBeforeEdit = newText;
+ valueBeforeEditIsSynced = true;
+ }
+
+ /*
+ * also send cursor position, no public api yet but for easier
+ * extension
+ */
+ updateCursorPosition();
+
+ if (sendBlurEvent || sendValueChange) {
+ /*
+ * Avoid sending text change event as we will simulate it on the
+ * server side before value change events.
+ */
+ textChangeEventTrigger.cancel();
+ scheduled = false;
+ client.sendPendingVariableChanges();
+ }
+ }
+ }
+
+ /**
+ * Updates the cursor position variable if it has changed since the last
+ * update.
+ *
+ * @return true iff the value was updated
+ */
+ protected boolean updateCursorPosition() {
+ if (Util.isAttachedAndDisplayed(this)) {
+ int cursorPos = getCursorPos();
+ if (lastCursorPos != cursorPos) {
+ client.updateVariable(paintableId, VAR_CURSOR, cursorPos, false);
+ lastCursorPos = cursorPos;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static VTextField focusedTextField;
+
+ public static void flushChangesFromFocusedTextField() {
+ if (focusedTextField != null) {
+ focusedTextField.onChange(null);
+ }
+ }
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ addStyleDependentName(CLASSNAME_FOCUS);
+ if (prompting) {
+ setText("");
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ setPrompting(false);
+ }
+ focusedTextField = this;
+ if (client.hasEventListeners(this, EventId.FOCUS)) {
+ client.updateVariable(paintableId, EventId.FOCUS, "", true);
+ }
+ }
+
+ @Override
+ public void onBlur(BlurEvent event) {
++ // this is called twice on Chrome when e.g. changing tab while prompting
++ // field focused - do not change settings on the second time
++ if (focusedTextField != this) {
++ return;
++ }
+ removeStyleDependentName(CLASSNAME_FOCUS);
+ focusedTextField = null;
+ String text = getText();
+ setPrompting(inputPrompt != null && (text == null || "".equals(text)));
+ if (prompting) {
+ setText(isReadOnly() ? "" : inputPrompt);
+ addStyleDependentName(CLASSNAME_PROMPT);
+ }
+
+ valueChange(true);
+ }
+
+ private void setPrompting(boolean prompting) {
+ this.prompting = prompting;
+ }
+
+ public void setColumns(int columns) {
+ if (columns <= 0) {
+ return;
+ }
+
+ setWidth(columns + "em");
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+ valueChange(false);
+ }
+ }
+
+ public void setImmediate(boolean immediate) {
+ this.immediate = immediate;
+ }
+
+ public void setInputPrompt(String inputPrompt) {
+ this.inputPrompt = inputPrompt;
+ }
+
+}
--- /dev/null
- private int identWidth = -1;
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.treetable;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.google.gwt.animation.client.Animation;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.ImageElement;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ComputedStyle;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.table.VScrollTable;
+import com.vaadin.terminal.gwt.client.ui.treetable.VTreeTable.VTreeTableScrollBody.VTreeTableRow;
+
+public class VTreeTable extends VScrollTable {
+
+ static class PendingNavigationEvent {
+ final int keycode;
+ final boolean ctrl;
+ final boolean shift;
+
+ public PendingNavigationEvent(int keycode, boolean ctrl, boolean shift) {
+ this.keycode = keycode;
+ this.ctrl = ctrl;
+ this.shift = shift;
+ }
+
+ @Override
+ public String toString() {
+ String string = "Keyboard event: " + keycode;
+ if (ctrl) {
+ string += " + ctrl";
+ }
+ if (shift) {
+ string += " + shift";
+ }
+ return string;
+ }
+ }
+
+ boolean collapseRequest;
+ private boolean selectionPending;
+ int colIndexOfHierarchy;
+ String collapsedRowKey;
+ VTreeTableScrollBody scrollBody;
+ boolean animationsEnabled;
+ LinkedList<PendingNavigationEvent> pendingNavigationEvents = new LinkedList<VTreeTable.PendingNavigationEvent>();
+ boolean focusParentResponsePending;
+
+ @Override
+ protected VScrollTableBody createScrollBody() {
+ scrollBody = new VTreeTableScrollBody();
+ return scrollBody;
+ }
+
+ /*
+ * Overridden to allow animation of expands and collapses of nodes.
+ */
+ @Override
+ protected void addAndRemoveRows(UIDL partialRowAdditions) {
+ if (partialRowAdditions == null) {
+ return;
+ }
+
+ if (animationsEnabled) {
+ if (partialRowAdditions.hasAttribute("hide")) {
+ scrollBody.unlinkRowsAnimatedAndUpdateCacheWhenFinished(
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ } else {
+ scrollBody.insertRowsAnimated(partialRowAdditions,
+ partialRowAdditions.getIntAttribute("firstprowix"),
+ partialRowAdditions.getIntAttribute("numprows"));
+ discardRowsOutsideCacheWindow();
+ }
+ } else {
+ super.addAndRemoveRows(partialRowAdditions);
+ }
+ }
+
+ class VTreeTableScrollBody extends VScrollTable.VScrollTableBody {
- setIdent();
++ private int indentWidth = -1;
+
+ VTreeTableScrollBody() {
+ super();
+ }
+
+ @Override
+ protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) {
+ if (uidl.hasAttribute("gen_html")) {
+ // This is a generated row.
+ return new VTreeTableGeneratedRow(uidl, aligns2);
+ }
+ return new VTreeTableRow(uidl, aligns2);
+ }
+
+ class VTreeTableRow extends
+ VScrollTable.VScrollTableBody.VScrollTableRow {
+
+ private boolean isTreeCellAdded = false;
+ private SpanElement treeSpacer;
+ private boolean open;
+ private int depth;
+ private boolean canHaveChildren;
+ protected Widget widgetInHierarchyColumn;
+
+ public VTreeTableRow(UIDL uidl, char[] aligns2) {
+ super(uidl, aligns2);
+ }
+
+ @Override
+ public void addCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean isSorted,
+ String description) {
+ super.addCell(rowUidl, text, align, style, textIsHTML,
+ isSorted, description);
+
+ addTreeSpacer(rowUidl);
+ }
+
+ protected boolean addTreeSpacer(UIDL rowUidl) {
+ if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) {
+ Element container = (Element) getElement().getLastChild()
+ .getFirstChild();
+
+ if (rowUidl.hasAttribute("icon")) {
+ // icons are in first content cell in TreeTable
+ ImageElement icon = Document.get().createImageElement();
+ icon.setClassName("v-icon");
+ icon.setAlt("icon");
+ icon.setSrc(client.translateVaadinUri(rowUidl
+ .getStringAttribute("icon")));
+ container.insertFirst(icon);
+ }
+
+ String classname = "v-treetable-treespacer";
+ if (rowUidl.getBooleanAttribute("ca")) {
+ canHaveChildren = true;
+ open = rowUidl.getBooleanAttribute("open");
+ classname += open ? " v-treetable-node-open"
+ : " v-treetable-node-closed";
+ }
+
+ treeSpacer = Document.get().createSpanElement();
+
+ treeSpacer.setClassName(classname);
+ container.insertFirst(treeSpacer);
+ depth = rowUidl.hasAttribute("depth") ? rowUidl
+ .getIntAttribute("depth") : 0;
- private void setIdent() {
- if (getIdentWidth() > 0 && depth != 0) {
- treeSpacer.getStyle().setWidth(
- (depth + 1) * getIdentWidth(), Unit.PX);
++ setIndent();
+ isTreeCellAdded = true;
+ return true;
+ }
+ return false;
+ }
+
+ private boolean cellShowsTreeHierarchy(int curColIndex) {
+ if (isTreeCellAdded) {
+ return false;
+ }
+ return curColIndex == colIndexOfHierarchy
+ + (showRowHeaders ? 1 : 0);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (event.getEventTarget().cast() == treeSpacer
+ && treeSpacer.getClassName().contains("node")) {
+ if (event.getTypeInt() == Event.ONMOUSEUP) {
+ sendToggleCollapsedUpdate(getKey());
+ }
+ return;
+ }
+ super.onBrowserEvent(event);
+ }
+
+ @Override
+ public void addCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean isSorted) {
+ super.addCell(rowUidl, w, align, style, isSorted);
+ if (addTreeSpacer(rowUidl)) {
+ widgetInHierarchyColumn = w;
+ }
+
+ }
+
- if (getIdentWidth() < 0) {
- detectIdent(this);
++ private void setIndent() {
++ if (getIndentWidth() > 0) {
++ treeSpacer.getParentElement().getStyle()
++ .setPaddingLeft(getIndent(), Unit.PX);
++ treeSpacer.getStyle().setWidth(getIndent(), Unit.PX);
+ }
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
- private int getIdentWidth() {
- return identWidth;
++ if (getIndentWidth() < 0) {
++ detectIndent(this);
+ }
+ }
+
+ private int getHierarchyAndIconWidth() {
+ int consumedSpace = treeSpacer.getOffsetWidth();
+ if (treeSpacer.getParentElement().getChildCount() > 2) {
+ // icon next to tree spacer
+ consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer
+ .getNextSibling()).getOffsetWidth();
+ }
+ return consumedSpace;
+ }
+
++ @Override
++ protected void setCellWidth(int cellIx, int width) {
++ if (cellIx == colIndexOfHierarchy + (showRowHeaders ? 1 : 0)) {
++ // take indentation padding into account if this is the
++ // hierarchy column
++ width = Math.max(width - getIndent(), 0);
++ }
++ super.setCellWidth(cellIx, width);
++ }
++
++ private int getIndent() {
++ return (depth + 1) * getIndentWidth();
++ }
+ }
+
+ protected class VTreeTableGeneratedRow extends VTreeTableRow {
+ private boolean spanColumns;
+ private boolean htmlContentAllowed;
+
+ public VTreeTableGeneratedRow(UIDL uidl, char[] aligns) {
+ super(uidl, aligns);
+ addStyleName("v-table-generated-row");
+ }
+
+ public boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ @Override
+ protected void initCellWidths() {
+ if (spanColumns) {
+ setSpannedColumnWidthAfterDOMFullyInited();
+ } else {
+ super.initCellWidths();
+ }
+ }
+
+ private void setSpannedColumnWidthAfterDOMFullyInited() {
+ // Defer setting width on spanned columns to make sure that
+ // they are added to the DOM before trying to calculate
+ // widths.
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (showRowHeaders) {
+ setCellWidth(0, tHead.getHeaderCell(0).getWidth());
+ calcAndSetSpanWidthOnCell(1);
+ } else {
+ calcAndSetSpanWidthOnCell(0);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected boolean isRenderHtmlInCells() {
+ return htmlContentAllowed;
+ }
+
+ @Override
+ protected void addCellsFromUIDL(UIDL uidl, char[] aligns, int col,
+ int visibleColumnIndex) {
+ htmlContentAllowed = uidl.getBooleanAttribute("gen_html");
+ spanColumns = uidl.getBooleanAttribute("gen_span");
+
+ final Iterator<?> cells = uidl.getChildIterator();
+ if (spanColumns) {
+ int colCount = uidl.getChildCount();
+ if (cells.hasNext()) {
+ final Object cell = cells.next();
+ if (cell instanceof String) {
+ addSpannedCell(uidl, cell.toString(), aligns[0],
+ "", htmlContentAllowed, false, null,
+ colCount);
+ } else {
+ addSpannedCell(uidl, (Widget) cell, aligns[0], "",
+ false, colCount);
+ }
+ }
+ } else {
+ super.addCellsFromUIDL(uidl, aligns, col,
+ visibleColumnIndex);
+ }
+ }
+
+ private void addSpannedCell(UIDL rowUidl, Widget w, char align,
+ String style, boolean sorted, int colCount) {
+ TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithWidget(w, align, style, sorted, td);
+ td.getStyle().setHeight(getRowHeight(), Unit.PX);
+ if (addTreeSpacer(rowUidl)) {
+ widgetInHierarchyColumn = w;
+ }
+ }
+
+ private void addSpannedCell(UIDL rowUidl, String text, char align,
+ String style, boolean textIsHTML, boolean sorted,
+ String description, int colCount) {
+ // String only content is optimized by not using Label widget
+ final TableCellElement td = DOM.createTD().cast();
+ td.setColSpan(colCount);
+ initCellWithText(text, align, style, textIsHTML, sorted,
+ description, td);
+ td.getStyle().setHeight(getRowHeight(), Unit.PX);
+ addTreeSpacer(rowUidl);
+ }
+
+ @Override
+ protected void setCellWidth(int cellIx, int width) {
+ if (isSpanColumns()) {
+ if (showRowHeaders) {
+ if (cellIx == 0) {
+ super.setCellWidth(0, width);
+ } else {
+ // We need to recalculate the spanning TDs width for
+ // every cellIx in order to support column resizing.
+ calcAndSetSpanWidthOnCell(1);
+ }
+ } else {
+ // Same as above.
+ calcAndSetSpanWidthOnCell(0);
+ }
+ } else {
+ super.setCellWidth(cellIx, width);
+ }
+ }
+
+ private void calcAndSetSpanWidthOnCell(final int cellIx) {
+ int spanWidth = 0;
+ for (int ix = (showRowHeaders ? 1 : 0); ix < tHead
+ .getVisibleCellCount(); ix++) {
+ spanWidth += tHead.getHeaderCell(ix).getOffsetWidth();
+ }
+ Util.setWidthExcludingPaddingAndBorder((Element) getElement()
+ .getChild(cellIx), spanWidth, 13, false);
+ }
+ }
+
- private void detectIdent(VTreeTableRow vTreeTableRow) {
- identWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
- if (identWidth == 0) {
- identWidth = -1;
++ private int getIndentWidth() {
++ return indentWidth;
+ }
+
- next.setIdent();
++ private void detectIndent(VTreeTableRow vTreeTableRow) {
++ indentWidth = vTreeTableRow.treeSpacer.getOffsetWidth();
++ if (indentWidth == 0) {
++ indentWidth = -1;
+ return;
+ }
+ Iterator<Widget> iterator = iterator();
+ while (iterator.hasNext()) {
+ VTreeTableRow next = (VTreeTableRow) iterator.next();
++ next.setIndent();
+ }
+ }
+
+ protected void unlinkRowsAnimatedAndUpdateCacheWhenFinished(
+ final int firstIndex, final int rows) {
+ List<VScrollTableRow> rowsToDelete = new ArrayList<VScrollTableRow>();
+ for (int ix = firstIndex; ix < firstIndex + rows; ix++) {
+ VScrollTableRow row = getRowByRowIndex(ix);
+ if (row != null) {
+ rowsToDelete.add(row);
+ }
+ }
+ if (!rowsToDelete.isEmpty()) {
+ // #8810 Only animate if there's something to animate
+ RowCollapseAnimation anim = new RowCollapseAnimation(
+ rowsToDelete) {
+ @Override
+ protected void onComplete() {
+ super.onComplete();
+ // Actually unlink the rows and update the cache after
+ // the
+ // animation is done.
+ unlinkAndReindexRows(firstIndex, rows);
+ discardRowsOutsideCacheWindow();
+ ensureCacheFilled();
+ }
+ };
+ anim.run(150);
+ }
+ }
+
+ protected List<VScrollTableRow> insertRowsAnimated(UIDL rowData,
+ int firstIndex, int rows) {
+ List<VScrollTableRow> insertedRows = insertAndReindexRows(rowData,
+ firstIndex, rows);
+ if (!insertedRows.isEmpty()) {
+ // Only animate if there's something to animate (#8810)
+ RowExpandAnimation anim = new RowExpandAnimation(insertedRows);
+ anim.run(150);
+ }
+ return insertedRows;
+ }
+
+ /**
+ * Prepares the table for animation by copying the background colors of
+ * all TR elements to their respective TD elements if the TD element is
+ * transparent. This is needed, since if TDs have transparent
+ * backgrounds, the rows sliding behind them are visible.
+ */
+ private class AnimationPreparator {
+ private final int lastItemIx;
+
+ public AnimationPreparator(int lastItemIx) {
+ this.lastItemIx = lastItemIx;
+ }
+
+ public void prepareTableForAnimation() {
+ int ix = lastItemIx;
+ VScrollTableRow row = null;
+ while ((row = getRowByRowIndex(ix)) != null) {
+ copyTRBackgroundsToTDs(row);
+ --ix;
+ }
+ }
+
+ private void copyTRBackgroundsToTDs(VScrollTableRow row) {
+ Element tr = row.getElement();
+ ComputedStyle cs = new ComputedStyle(tr);
+ String backgroundAttachment = cs
+ .getProperty("backgroundAttachment");
+ String backgroundClip = cs.getProperty("backgroundClip");
+ String backgroundColor = cs.getProperty("backgroundColor");
+ String backgroundImage = cs.getProperty("backgroundImage");
+ String backgroundOrigin = cs.getProperty("backgroundOrigin");
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ Element td = tr.getChild(ix).cast();
+ if (!elementHasBackground(td)) {
+ td.getStyle().setProperty("backgroundAttachment",
+ backgroundAttachment);
+ td.getStyle().setProperty("backgroundClip",
+ backgroundClip);
+ td.getStyle().setProperty("backgroundColor",
+ backgroundColor);
+ td.getStyle().setProperty("backgroundImage",
+ backgroundImage);
+ td.getStyle().setProperty("backgroundOrigin",
+ backgroundOrigin);
+ }
+ }
+ }
+
+ private boolean elementHasBackground(Element element) {
+ ComputedStyle cs = new ComputedStyle(element);
+ String clr = cs.getProperty("backgroundColor");
+ String img = cs.getProperty("backgroundImage");
+ return !("rgba(0, 0, 0, 0)".equals(clr.trim())
+ || "transparent".equals(clr.trim()) || img == null);
+ }
+
+ public void restoreTableAfterAnimation() {
+ int ix = lastItemIx;
+ VScrollTableRow row = null;
+ while ((row = getRowByRowIndex(ix)) != null) {
+ restoreStyleForTDsInRow(row);
+
+ --ix;
+ }
+ }
+
+ private void restoreStyleForTDsInRow(VScrollTableRow row) {
+ Element tr = row.getElement();
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ Element td = tr.getChild(ix).cast();
+ td.getStyle().clearProperty("backgroundAttachment");
+ td.getStyle().clearProperty("backgroundClip");
+ td.getStyle().clearProperty("backgroundColor");
+ td.getStyle().clearProperty("backgroundImage");
+ td.getStyle().clearProperty("backgroundOrigin");
+ }
+ }
+ }
+
+ /**
+ * Animates row expansion using the GWT animation framework.
+ *
+ * The idea is as follows:
+ *
+ * 1. Insert all rows normally
+ *
+ * 2. Insert a newly created DIV containing a new TABLE element below
+ * the DIV containing the actual scroll table body.
+ *
+ * 3. Clone the rows that were inserted in step 1 and attach the clones
+ * to the new TABLE element created in step 2.
+ *
+ * 4. The new DIV from step 2 is absolutely positioned so that the last
+ * inserted row is just behind the row that was expanded.
+ *
+ * 5. Hide the contents of the originally inserted rows by setting the
+ * DIV.v-table-cell-wrapper to display:none;.
+ *
+ * 6. Set the height of the originally inserted rows to 0.
+ *
+ * 7. The animation loop slides the DIV from step 2 downwards, while at
+ * the same pace growing the height of each of the inserted rows from 0
+ * to full height. The first inserted row grows from 0 to full and after
+ * this the second row grows from 0 to full, etc until all rows are full
+ * height.
+ *
+ * 8. Remove the DIV from step 2
+ *
+ * 9. Restore display:block; to the DIV.v-table-cell-wrapper elements.
+ *
+ * 10. DONE
+ */
+ private class RowExpandAnimation extends Animation {
+
+ private final List<VScrollTableRow> rows;
+ private Element cloneDiv;
+ private Element cloneTable;
+ private AnimationPreparator preparator;
+
+ /**
+ * @param rows
+ * List of rows to animate. Must not be empty.
+ */
+ public RowExpandAnimation(List<VScrollTableRow> rows) {
+ this.rows = rows;
+ buildAndInsertAnimatingDiv();
+ preparator = new AnimationPreparator(rows.get(0).getIndex() - 1);
+ preparator.prepareTableForAnimation();
+ for (VScrollTableRow row : rows) {
+ cloneAndAppendRow(row);
+ row.addStyleName("v-table-row-animating");
+ setCellWrapperDivsToDisplayNone(row);
+ row.setHeight(getInitialHeight());
+ }
+ }
+
+ protected String getInitialHeight() {
+ return "0px";
+ }
+
+ private void cloneAndAppendRow(VScrollTableRow row) {
+ Element clonedTR = null;
+ clonedTR = row.getElement().cloneNode(true).cast();
+ clonedTR.getStyle().setVisibility(Visibility.VISIBLE);
+ cloneTable.appendChild(clonedTR);
+ }
+
+ protected double getBaseOffset() {
+ return rows.get(0).getAbsoluteTop()
+ - rows.get(0).getParent().getAbsoluteTop()
+ - rows.size() * getRowHeight();
+ }
+
+ private void buildAndInsertAnimatingDiv() {
+ cloneDiv = DOM.createDiv();
+ cloneDiv.addClassName("v-treetable-animation-clone-wrapper");
+ cloneTable = DOM.createTable();
+ cloneTable.addClassName("v-treetable-animation-clone");
+ cloneDiv.appendChild(cloneTable);
+ insertAnimatingDiv();
+ }
+
+ private void insertAnimatingDiv() {
+ Element tableBody = getElement().cast();
+ Element tableBodyParent = tableBody.getParentElement().cast();
+ tableBodyParent.insertAfter(cloneDiv, tableBody);
+ }
+
+ @Override
+ protected void onUpdate(double progress) {
+ animateDiv(progress);
+ animateRowHeights(progress);
+ }
+
+ private void animateDiv(double progress) {
+ double offset = calculateDivOffset(progress, getRowHeight());
+
+ cloneDiv.getStyle().setTop(getBaseOffset() + offset, Unit.PX);
+ }
+
+ private void animateRowHeights(double progress) {
+ double rh = getRowHeight();
+ double vlh = calculateHeightOfAllVisibleLines(progress, rh);
+ int ix = 0;
+
+ while (ix < rows.size()) {
+ double height = vlh < rh ? vlh : rh;
+ rows.get(ix).setHeight(height + "px");
+ vlh -= height;
+ ix++;
+ }
+ }
+
+ protected double calculateHeightOfAllVisibleLines(double progress,
+ double rh) {
+ return rows.size() * rh * progress;
+ }
+
+ protected double calculateDivOffset(double progress, double rh) {
+ return progress * rows.size() * rh;
+ }
+
+ @Override
+ protected void onComplete() {
+ preparator.restoreTableAfterAnimation();
+ for (VScrollTableRow row : rows) {
+ resetCellWrapperDivsDisplayProperty(row);
+ row.removeStyleName("v-table-row-animating");
+ }
+ Element tableBodyParent = (Element) getElement()
+ .getParentElement();
+ tableBodyParent.removeChild(cloneDiv);
+ }
+
+ private void setCellWrapperDivsToDisplayNone(VScrollTableRow row) {
+ Element tr = row.getElement();
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ getWrapperDiv(tr, ix).getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ private Element getWrapperDiv(Element tr, int tdIx) {
+ Element td = tr.getChild(tdIx).cast();
+ return td.getChild(0).cast();
+ }
+
+ private void resetCellWrapperDivsDisplayProperty(VScrollTableRow row) {
+ Element tr = row.getElement();
+ for (int ix = 0; ix < tr.getChildCount(); ix++) {
+ getWrapperDiv(tr, ix).getStyle().clearProperty("display");
+ }
+ }
+
+ }
+
+ /**
+ * This is the inverse of the RowExpandAnimation and is implemented by
+ * extending it and overriding the calculation of offsets and heights.
+ */
+ private class RowCollapseAnimation extends RowExpandAnimation {
+
+ private final List<VScrollTableRow> rows;
+
+ /**
+ * @param rows
+ * List of rows to animate. Must not be empty.
+ */
+ public RowCollapseAnimation(List<VScrollTableRow> rows) {
+ super(rows);
+ this.rows = rows;
+ }
+
+ @Override
+ protected String getInitialHeight() {
+ return getRowHeight() + "px";
+ }
+
+ @Override
+ protected double getBaseOffset() {
+ return getRowHeight();
+ }
+
+ @Override
+ protected double calculateHeightOfAllVisibleLines(double progress,
+ double rh) {
+ return rows.size() * rh * (1 - progress);
+ }
+
+ @Override
+ protected double calculateDivOffset(double progress, double rh) {
+ return -super.calculateDivOffset(progress, rh);
+ }
+ }
+ }
+
+ /**
+ * Icons rendered into first actual column in TreeTable, not to row header
+ * cell
+ */
+ @Override
+ protected String buildCaptionHtmlSnippet(UIDL uidl) {
+ if (uidl.getTag().equals("column")) {
+ return super.buildCaptionHtmlSnippet(uidl);
+ } else {
+ String s = uidl.getStringAttribute("caption");
+ return s;
+ }
+ }
+
+ @Override
+ protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) {
+ if (collapseRequest || focusParentResponsePending) {
+ // Enqueue the event if there might be pending content changes from
+ // the server
+ if (pendingNavigationEvents.size() < 10) {
+ // Only keep 10 keyboard events in the queue
+ PendingNavigationEvent pendingNavigationEvent = new PendingNavigationEvent(
+ keycode, ctrl, shift);
+ pendingNavigationEvents.add(pendingNavigationEvent);
+ }
+ return true;
+ }
+
+ VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow();
+ if (focusedRow != null) {
+ if (focusedRow.canHaveChildren
+ && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) {
+ if (!ctrl) {
+ client.updateVariable(paintableId, "selectCollapsed", true,
+ false);
+ }
+ sendSelectedRows(false);
+ sendToggleCollapsedUpdate(focusedRow.getKey());
+ return true;
+ } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) {
+ // already expanded, move selection down if next is on a deeper
+ // level (is-a-child)
+ VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow
+ .getParent();
+ Iterator<Widget> iterator = body.iterator();
+ VTreeTableRow next = null;
+ while (iterator.hasNext()) {
+ next = (VTreeTableRow) iterator.next();
+ if (next == focusedRow) {
+ next = (VTreeTableRow) iterator.next();
+ break;
+ }
+ }
+ if (next != null) {
+ if (next.depth > focusedRow.depth) {
+ selectionPending = true;
+ return super.handleNavigation(getNavigationDownKey(),
+ ctrl, shift);
+ }
+ } else {
+ // Note, a minor change here for a bit false behavior if
+ // cache rows is disabled + last visible row + no childs for
+ // the node
+ selectionPending = true;
+ return super.handleNavigation(getNavigationDownKey(), ctrl,
+ shift);
+ }
+ } else if (keycode == KeyCodes.KEY_LEFT) {
+ // already collapsed move selection up to parent node
+ // do on the server side as the parent is not necessary
+ // rendered on the client, could check if parent is visible if
+ // a performance issue arises
+
+ client.updateVariable(paintableId, "focusParent",
+ focusedRow.getKey(), true);
+
+ // Set flag that we should enqueue navigation events until we
+ // get a response to this request
+ focusParentResponsePending = true;
+
+ return true;
+ }
+ }
+ return super.handleNavigation(keycode, ctrl, shift);
+ }
+
+ private void sendToggleCollapsedUpdate(String rowKey) {
+ collapsedRowKey = rowKey;
+ collapseRequest = true;
+ client.updateVariable(paintableId, "toggleCollapsed", rowKey, true);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONKEYUP && selectionPending) {
+ sendSelectedRows();
+ }
+ }
+
+ @Override
+ protected void sendSelectedRows(boolean immediately) {
+ super.sendSelectedRows(immediately);
+ selectionPending = false;
+ }
+
+ @Override
+ protected void reOrderColumn(String columnKey, int newIndex) {
+ super.reOrderColumn(columnKey, newIndex);
+ // current impl not intelligent enough to survive without visiting the
+ // server to redraw content
+ client.sendPendingVariableChanges();
+ }
+
+ @Override
+ public void setStyleName(String style) {
+ super.setStyleName(style + " v-treetable");
+ }
+
+ @Override
+ protected void updateTotalRows(UIDL uidl) {
+ // Make sure that initializedAndAttached & al are not reset when the
+ // totalrows are updated on expand/collapse requests.
+ int newTotalRows = uidl.getIntAttribute("totalrows");
+ setTotalRows(newTotalRows);
+ }
+
+}