diff options
Diffstat (limited to 'src/com/vaadin')
-rw-r--r-- | src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java | 852 | ||||
-rw-r--r-- | src/com/vaadin/ui/Component.java | 19 | ||||
-rw-r--r-- | src/com/vaadin/ui/Table.java | 488 |
3 files changed, 1328 insertions, 31 deletions
diff --git a/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java b/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java index 72efa90ab4..fbd762027b 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VScrollTable.java @@ -12,6 +12,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Set; +import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.NativeEvent; import com.google.gwt.dom.client.NodeList; @@ -84,10 +85,19 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, public static final String CLASSNAME = "v-table"; public static final String ITEM_CLICK_EVENT_ID = "itemClick"; + public static final String HEADER_CLICK_EVENT_ID = "handleHeaderClick"; + public static final String FOOTER_CLICK_EVENT_ID = "handleFooterClick"; private static final double CACHE_RATE_DEFAULT = 2; /** + * The default multi select mode where simple left clicks only selects one + * item, CTRL+left click selects multiple items and SHIFT-left click selects + * a range of items. + */ + private static final int MULTISELECT_MODE_DEFAULT = 0; + + /** * multiple of pagelength which component will cache when requesting more * rows */ @@ -117,6 +127,46 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private final HashSet<String> selectedRowKeys = new HashSet<String>(); + /** + * Represents a select range of rows + */ + private class SelectionRange { + /** + * The starting key of the range + */ + private int startRowKey; + + /** + * The ending key of the range + */ + private int endRowKey; + + /** + * Constuctor. + * + * @param startRowKey + * The range start. Must be less than endRowKey + * @param endRowKey + * The range end. Must be bigger than startRowKey + */ + public SelectionRange(int startRowKey, int endRowKey) { + this.startRowKey = startRowKey; + this.endRowKey = endRowKey; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return startRowKey + "-" + endRowKey; + } + }; + + private final HashSet<SelectionRange> selectedRowRanges = new HashSet<SelectionRange>(); + private boolean initializedAndAttached = false; /** @@ -126,6 +176,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private final TableHead tHead = new TableHead(); + private final TableFooter tFoot = new TableFooter(); + private final ScrollPanel bodyContainer = new ScrollPanel(); private int totalRows; @@ -149,6 +201,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private Element scrollPositionElement; private boolean enabled; private boolean showColHeaders; + private boolean showColFooters; /** flag to indicate that table body has changed */ private boolean isNewBody = true; @@ -166,6 +219,9 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private boolean rendering = false; private int dragmode; + private int multiselectmode; + private int lastSelectedRowKey = -1; + public VScrollTable() { bodyContainer.addScrollHandler(this); bodyContainer.setStyleName(CLASSNAME + "-body"); @@ -173,14 +229,23 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, setStyleName(CLASSNAME); add(tHead); add(bodyContainer); + add(tFoot); rowRequestHandler = new RowRequestHandler(); - } @SuppressWarnings("unchecked") public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { rendering = true; + + /* + * We need to do this before updateComponent since updateComponent calls + * this.setHeight() which will calculate a new body height depending on + * the space available. + */ + showColFooters = uidl.getBooleanAttribute("colfooters"); + tFoot.setVisible(showColFooters); + if (client.updateComponent(this, uidl, true)) { rendering = false; return; @@ -199,6 +264,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, if (scrollBody != null) { if (totalRows == 0) { tHead.clear(); + tFoot.clear(); } initializedAndAttached = false; initialContentReceived = false; @@ -210,12 +276,16 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, dragmode = uidl.hasAttribute("dragmode") ? uidl .getIntAttribute("dragmode") : 0; + multiselectmode = uidl.hasAttribute("multiselectmode") ? uidl + .getIntAttribute("multiselectmode") : MULTISELECT_MODE_DEFAULT; + setCacheRate(uidl.hasAttribute("cr") ? uidl.getDoubleAttribute("cr") : CACHE_RATE_DEFAULT); recalcWidths = uidl.hasAttribute("recalcWidths"); if (recalcWidths) { tHead.clear(); + tFoot.clear(); } if (uidl.hasAttribute("pagelength")) { @@ -245,6 +315,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, final Set<String> selectedKeys = uidl .getStringArrayVariableAsSet("selected"); selectedRowKeys.clear(); + selectedRowRanges.clear(); for (String string : selectedKeys) { selectedRowKeys.add(string); } @@ -285,6 +356,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, updateActionMap(c); } else if (c.getTag().equals("visiblecolumns")) { tHead.updateCellsFromUIDL(c); + tFoot.updateCellsFromUIDL(c); } else if (c.getTag().equals("-ac")) { ac = c; } @@ -302,6 +374,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } updateHeader(uidl.getStringArrayAttribute("vcolorder")); + updateFooter(uidl.getStringArrayAttribute("vcolorder")); + if (!recalcWidths && initializedAndAttached) { updateBody(rowData, uidl.getIntAttribute("firstrow"), uidl .getIntAttribute("rows")); @@ -422,6 +496,38 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } /** + * Updates footers. + * <p> + * Update headers whould be called before this method is called! + * </p> + * + * @param strings + */ + private void updateFooter(String[] strings) { + if (strings == null) { + return; + } + + // Add dummy column if row headers are present + int colIndex = 0; + if (showRowHeaders) { + tFoot.enableColumn("0", colIndex); + colIndex++; + } else { + tFoot.removeCell("0"); + } + + int i; + for (i = 0; i < strings.length; i++) { + final String cid = strings[i]; + tFoot.enableColumn(cid, colIndex); + colIndex++; + } + + tFoot.setVisible(showColFooters); + } + + /** * @param uidl * which contains row data * @param firstRow @@ -497,9 +603,18 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } private void setColWidth(int colIndex, int w, boolean isDefinedWidth) { - final HeaderCell cell = tHead.getHeaderCell(colIndex); - cell.setWidth(w, isDefinedWidth); + // Set header column width + final HeaderCell hcell = tHead.getHeaderCell(colIndex); + hcell.setWidth(w, isDefinedWidth); + + // Set body column width scrollBody.setColWidth(colIndex, w); + + // Set footer column width + final FooterCell fcell = tFoot.getFooterCell(colIndex); + if (fcell != null) { + fcell.setWidth(w, isDefinedWidth); + } } private int getColWidth(String colKey) { @@ -528,6 +643,9 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // Change body order scrollBody.moveCol(oldIndex, newIndex); + // Change footer order + tFoot.moveCell(oldIndex, newIndex); + /* * Build new columnOrder and update it to server Note that columnOrder * also contains collapsed columns so we cannot directly build it from @@ -621,6 +739,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, final int[] widths = new int[tHead.visibleCells.size()]; tHead.enableBrowserIntelligence(); + tFoot.enableBrowserIntelligence(); + // first loop: collect natural widths while (headCells.hasNext()) { final HeaderCell hCell = (HeaderCell) headCells.next(); @@ -646,6 +766,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } tHead.disableBrowserIntelligence(); + tFoot.disableBrowserIntelligence(); boolean willHaveScrollbarz = willHaveScrollbars(); @@ -1129,7 +1250,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, @Override public void onBrowserEvent(Event event) { if (enabled && event != null) { - if (isResizing || event.getTarget() == colResizeWidget) { + if (isResizing + || event.getEventTarget().cast() == colResizeWidget) { onResizeEvent(event); } else { handleCaptionEvent(event); @@ -1166,6 +1288,24 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, floatingCopyOfHeaderCell = null; } + /** + * Fires a header click event after the user has clicked a column header cell + * + * @param event + * The click event + */ + private void fireHeaderClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + HEADER_CLICK_EVENT_ID)) { + MouseEventDetails details = new MouseEventDetails(event); + client.updateVariable(paintableId, "headerClickEvent", details + .toString(), false); + client + .updateVariable(paintableId, "headerClickCID", cid, + immediate); + } + } + protected void handleCaptionEvent(Event event) { switch (DOM.eventGetType(event)) { case Event.ONMOUSEDOWN: @@ -1216,8 +1356,10 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, * cache_rate + pageLength)); rowRequestHandler.deferRowFetch(); } + fireHeaderClickedEvent(event); break; } + fireHeaderClickedEvent(event); break; case Event.ONMOUSEMOVE: if (dragging) { @@ -1502,8 +1644,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, headerChangedDuringUpdate = true; } } - } + // check for orphaned header cells for (Iterator<String> cit = availableCells.keySet().iterator(); cit .hasNext();) { @@ -1513,7 +1655,6 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, cit.remove(); } } - } public void enableColumn(String cid, int index) { @@ -1661,7 +1802,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, @Override public void onBrowserEvent(Event event) { if (enabled) { - if (event.getTarget() == columnSelector) { + if (event.getEventTarget().cast() == columnSelector) { final int left = DOM.getAbsoluteLeft(columnSelector); final int top = DOM.getAbsoluteTop(columnSelector) + DOM.getElementPropertyInt(columnSelector, @@ -1795,6 +1936,536 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } /** + * A cell in the footer + */ + public class FooterCell extends Widget { + private Element td = DOM.createTD(); + private Element captionContainer = DOM.createDiv(); + private char align = ALIGN_LEFT; + private int width = -1; + private float expandRatio = 0; + private String cid; + + public FooterCell(String colId, String headerText) { + cid = colId; + + setText(headerText); + + DOM.setElementProperty(captionContainer, "className", CLASSNAME + + "-footer-container"); + + // ensure no clipping initially (problem on column additions) + DOM.setStyleAttribute(captionContainer, "overflow", "visible"); + + DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS); + + DOM.appendChild(td, captionContainer); + + DOM.sinkEvents(td, Event.MOUSEEVENTS); + + setElement(td); + } + + /** + * Sets the text of the footer + * + * @param footerText + * The text in the footer + */ + public void setText(String footerText) { + DOM.setInnerHTML(captionContainer, footerText); + } + + /** + * Set alignment of the text in the cell + * + * @param c + * The alignment which can be ALIGN_CENTER, ALIGN_LEFT, + * ALIGN_RIGHT + */ + public void setAlign(char c) { + if (align != c) { + switch (c) { + case ALIGN_CENTER: + DOM.setStyleAttribute(captionContainer, "textAlign", + "center"); + break; + case ALIGN_RIGHT: + DOM.setStyleAttribute(captionContainer, "textAlign", + "right"); + break; + default: + DOM.setStyleAttribute(captionContainer, "textAlign", ""); + break; + } + } + align = c; + } + + /** + * Get the alignment of the text int the cell + * + * @return Returns either ALIGN_CENTER, ALIGN_LEFT or ALIGN_RIGHT + */ + public char getAlign() { + return align; + } + + /** + * Sets the width of the cell + * + * @param w + * The width of the cell + * @param ensureDefinedWidth + * Ensures the the given width is not recalculated + */ + public void setWidth(int w, boolean ensureDefinedWidth) { + + if (ensureDefinedWidth) { + // on column resize expand ratio becomes zero + expandRatio = 0; + } + if (width == w) { + return; + } + width = w; + if (width <= 0) { + // go to default mode, clip content if necessary + DOM.setStyleAttribute(captionContainer, "overflow", ""); + } + if (w == -1) { + DOM.setStyleAttribute(captionContainer, "width", ""); + setWidth(""); + } else { + + /* + * Reduce width with one pixel for the right border since the + * footers does not have any spacers between them. + * + * IE6 will calculate the footer width wrong by 2 pixels due to + * borders used so add it to border widths. + */ + int borderWidths = 1; + if (BrowserInfo.get().isIE6()) { + borderWidths += 2; + } + + // Set the container width (check for negative value) + if (w - borderWidths >= 0) { + captionContainer.getStyle().setPropertyPx("width", + w - borderWidths); + } else { + captionContainer.getStyle().setPropertyPx("width", 0); + } + + /* + * if we already have tBody, set the header width properly, if + * not defer it. IE will fail with complex float in table header + * unless TD width is not explicitly set. + */ + if (scrollBody != null) { + /* + * Reduce with one since footer does not have any spacers, + * instead a 1 pixel border. + */ + int tdWidth = width + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(tdWidth + "px"); + } else { + DeferredCommand.addCommand(new Command() { + public void execute() { + + int borderWidths = 1; + if (BrowserInfo.get().isIE6()) { + borderWidths += 2; + } + + int tdWidth = width + + scrollBody.getCellExtraWidth() + - borderWidths; + setWidth(tdWidth + "px"); + } + }); + } + } + } + + /** + * Sets the width to undefined + */ + public void setUndefinedWidth() { + setWidth(-1, false); + } + + /** + * Sets the expand ratio of the cell + * + * @param floatAttribute + * The expand ratio + */ + public void setExpandRatio(float floatAttribute) { + expandRatio = floatAttribute; + } + + /** + * Returns the expand ration of the cell + * + * @return The expand ratio + */ + public float getExpandRatio() { + return expandRatio; + } + + /** + * Is the cell enabled? + * + * @return True if enabled else False + */ + public boolean isEnabled() { + return getParent() != null; + } + + /** + * Handle column clicking + */ + + @Override + public void onBrowserEvent(Event event) { + if (enabled && event != null) { + handleCaptionEvent(event); + } + } + + /** + * Handles a event on the captions + * + * @param event + * The event to handle + */ + protected void handleCaptionEvent(Event event) { + if (DOM.eventGetType(event) == Event.ONMOUSEUP) { + fireFooterClickedEvent(event); + } + } + + /** + * Fires a footer click event after the user has clicked a column footer + * cell + * + * @param event + * The click event + */ + private void fireFooterClickedEvent(Event event) { + if (client.hasEventListeners(VScrollTable.this, + FOOTER_CLICK_EVENT_ID)) { + MouseEventDetails details = new MouseEventDetails(event); + client.updateVariable(paintableId, "footerClickEvent", details + .toString(), false); + client.updateVariable(paintableId, "footerClickCID", cid, + immediate); + } + } + + /** + * Returns the column key of the column + * + * @return The column key + */ + public String getColKey() { + return cid; + } + } + + /** + * HeaderCell that is header cell for row headers. + * + * Reordering disabled and clicking on it resets sorting. + */ + public class RowHeadersFooterCell extends FooterCell { + + RowHeadersFooterCell() { + super("0", ""); + } + + @Override + protected void handleCaptionEvent(Event event) { + // NOP: RowHeaders cannot be reordered + // TODO It'd be nice to reset sorting here + } + } + + /** + * The footer of the table which can be seen in the bottom of the Table. + */ + public class TableFooter extends Panel { + + private static final int WRAPPER_WIDTH = 9000; + + ArrayList<Widget> visibleCells = new ArrayList<Widget>(); + HashMap<String, FooterCell> availableCells = new HashMap<String, FooterCell>(); + + Element div = DOM.createDiv(); + Element hTableWrapper = DOM.createDiv(); + Element hTableContainer = DOM.createDiv(); + Element table = DOM.createTable(); + Element headerTableBody = DOM.createTBody(); + Element tr = DOM.createTR(); + + public TableFooter() { + + DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden"); + DOM.setElementProperty(hTableWrapper, "className", CLASSNAME + + "-footer"); + + DOM.appendChild(table, headerTableBody); + DOM.appendChild(headerTableBody, tr); + DOM.appendChild(hTableContainer, table); + DOM.appendChild(hTableWrapper, hTableContainer); + DOM.appendChild(div, hTableWrapper); + setElement(div); + + setStyleName(CLASSNAME + "-footer-wrap"); + + availableCells.put("0", new RowHeadersFooterCell()); + } + + @Override + public void clear() { + for (String cid : availableCells.keySet()) { + removeCell(cid); + } + availableCells.clear(); + availableCells.put("0", new RowHeadersFooterCell()); + } + + /* + * (non-Javadoc) + * + * @see + * com.google.gwt.user.client.ui.Panel#remove(com.google.gwt.user.client + * .ui.Widget) + */ + @Override + public boolean remove(Widget w) { + if (visibleCells.contains(w)) { + visibleCells.remove(w); + orphan(w); + DOM.removeChild(DOM.getParent(w.getElement()), w.getElement()); + return true; + } + return false; + } + + /* + * (non-Javadoc) + * + * @see com.google.gwt.user.client.ui.HasWidgets#iterator() + */ + public Iterator<Widget> iterator() { + return visibleCells.iterator(); + } + + /** + * Gets a footer cell which represents the given columnId + * + * @param cid + * The columnId + * + * @return The cell + */ + public FooterCell getFooterCell(String cid) { + return availableCells.get(cid); + } + + /** + * Gets a footer cell by using a column index + * + * @param index + * The index of the column + * @return The Cell + */ + public FooterCell getFooterCell(int index) { + if (index < visibleCells.size()) { + return (FooterCell) visibleCells.get(index); + } else { + return null; + } + } + + /** + * Updates the cells contents when updateUIDL request is received + * + * @param uidl + * The UIDL + */ + public void updateCellsFromUIDL(UIDL uidl) { + Iterator<?> columnIterator = uidl.getChildIterator(); + HashSet<String> updated = new HashSet<String>(); + updated.add("0"); + while (columnIterator.hasNext()) { + final UIDL col = (UIDL) columnIterator.next(); + final String cid = col.getStringAttribute("cid"); + updated.add(cid); + + String caption = col.getStringAttribute("fcaption"); + FooterCell c = getFooterCell(cid); + if (c == null) { + c = new FooterCell(cid, caption); + availableCells.put(cid, c); + if (initializedAndAttached) { + // we will need a column width recalculation + initializedAndAttached = false; + initialContentReceived = false; + isNewBody = true; + } + } else { + c.setText(caption); + } + + if (col.hasAttribute("align")) { + c.setAlign(col.getStringAttribute("align").charAt(0)); + } + if (col.hasAttribute("width")) { + final String width = col.getStringAttribute("width"); + c.setWidth(Integer.parseInt(width), true); + } else if (recalcWidths) { + c.setUndefinedWidth(); + } + if (col.hasAttribute("er")) { + c.setExpandRatio(col.getFloatAttribute("er")); + } + if (col.hasAttribute("collapsed")) { + // ensure header is properly removed from parent (case when + // collapsing happens via servers side api) + if (c.isAttached()) { + c.removeFromParent(); + headerChangedDuringUpdate = true; + } + } + } + + // check for orphaned header cells + for (Iterator<String> cit = availableCells.keySet().iterator(); cit + .hasNext();) { + String cid = cit.next(); + if (!updated.contains(cid)) { + removeCell(cid); + cit.remove(); + } + } + } + + /** + * Set a footer cell for a specified column index + * + * @param index + * The index + * @param cell + * The footer cell + */ + public void setFooterCell(int index, FooterCell cell) { + if (cell.isEnabled()) { + // we're moving the cell + DOM.removeChild(tr, cell.getElement()); + orphan(cell); + } + if (index < visibleCells.size()) { + // insert to right slot + DOM.insertChild(tr, cell.getElement(), index); + adopt(cell); + visibleCells.add(index, cell); + } else if (index == visibleCells.size()) { + // simply append + DOM.appendChild(tr, cell.getElement()); + adopt(cell); + visibleCells.add(cell); + } else { + throw new RuntimeException( + "Header cells must be appended in order"); + } + } + + /** + * Remove a cell by using the columnId + * + * @param colKey + * The columnId to remove + */ + public void removeCell(String colKey) { + final FooterCell c = getFooterCell(colKey); + remove(c); + } + + /** + * Enable a column (Sets the footer cell) + * + * @param cid + * The columnId + * @param index + * The index of the column + */ + public void enableColumn(String cid, int index) { + final FooterCell c = getFooterCell(cid); + if (!c.isEnabled() || getFooterCell(index) != c) { + setFooterCell(index, c); + if (initializedAndAttached) { + headerChangedDuringUpdate = true; + } + } + } + + /** + * Disable browser measurement of the table width + */ + public void disableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH + + "px"); + } + + /** + * Enable browser measurement of the table width + */ + public void enableBrowserIntelligence() { + DOM.setStyleAttribute(hTableContainer, "width", ""); + } + + /** + * Set the horizontal position in the cell in the footer. This is done + * when a horizontal scrollbar is present. + * + * @param scrollLeft + * The value of the leftScroll + */ + public void setHorizontalScrollPosition(int scrollLeft) { + if (BrowserInfo.get().isIE6()) { + hTableWrapper.getStyle().setProperty("position", "relative"); + hTableWrapper.getStyle().setPropertyPx("left", -scrollLeft); + } else { + hTableWrapper.setScrollLeft(scrollLeft); + } + } + + /** + * Swap cells when the column are dragged + * + * @param oldIndex + * The old index of the cell + * @param newIndex + * The new index of the cell + */ + public void moveCell(int oldIndex, int newIndex) { + final FooterCell hCell = getFooterCell(oldIndex); + final Element cell = hCell.getElement(); + + visibleCells.remove(oldIndex); + DOM.removeChild(tr, cell); + + DOM.insertChild(tr, cell, newIndex); + visibleCells.add(newIndex, hCell); + } + } + + /** * This Panel can only contain VScrollTableRow type of widgets. This * "simulates" very large table, keeping spacers which take room of * unrendered rows. @@ -2353,7 +3024,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } } if (uidl.hasAttribute("selected") && !isSelected()) { - toggleSelection(); + toggleSelection(true); } } @@ -2484,6 +3155,19 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } } + /** + * Add this to the element mouse down event by using + * element.setPropertyJSO + * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it + * then again when the mouse is depressed in the mouse up event. + * + * @return Returns the JSO preventing text selection + */ + private native JavaScriptObject applyDisableTextSelectionIEHack() + /*-{ + return function(){ return false; }; + }-*/; + /* * React on click that occur on content cells only */ @@ -2501,11 +3185,61 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, handleClickEvent(event, targetTdOrTr); if (event.getButton() == Event.BUTTON_LEFT && selectMode > Table.SELECT_MODE_NONE) { - toggleSelection(); + + // Ctrl+Shift click + if ((event.getCtrlKey() || event.getMetaKey()) + && event.getShiftKey() + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + toggleShiftSelection(false); + + // Ctrl click + } else if ((event.getCtrlKey() || event + .getMetaKey()) + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + toggleSelection(true); + + // Shift click + } else if (event.getShiftKey() + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + toggleShiftSelection(true); + + // click + } else { + if (multiselectmode == MULTISELECT_MODE_DEFAULT) { + deselectAll(); + } + toggleSelection(multiselectmode == MULTISELECT_MODE_DEFAULT); + } + + // Remove IE text selection hack + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO( + "onselectstart", null); + } + // Note: changing the immediateness of this // might // require changes to "clickEvent" immediateness // also. + if (multiselectmode == MULTISELECT_MODE_DEFAULT) { + Set<String> ranges = new HashSet<String>(); + for (SelectionRange range : selectedRowRanges) { + ranges.add(range.toString()); + } + client + .updateVariable( + paintableId, + "selectedRanges", + ranges + .toArray(new String[selectedRowRanges + .size()]), + false); + } + client .updateVariable( paintableId, @@ -2567,6 +3301,23 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } event.preventDefault(); event.stopPropagation(); + } else if (event.getCtrlKey() + || event.getShiftKey() + || event.getMetaKey() + && selectMode == SELECT_MODE_MULTI + && multiselectmode == MULTISELECT_MODE_DEFAULT) { + // Prevent default text selection in Firefox + event.preventDefault(); + + // Prevent default text selection in IE + if (BrowserInfo.get().isIE()) { + ((Element) event.getEventTarget().cast()) + .setPropertyJSO( + "onselectstart", + applyDisableTextSelectionIEHack()); + } + + event.stopPropagation(); } break; case Event.ONMOUSEOUT: @@ -2642,19 +3393,27 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, left += Window.getScrollLeft(); client.getContextMenu().showAt(this, left, top); } - event.cancelBubble(true); + event.stopPropagation(); event.preventDefault(); } + /** + * Has the row been selected? + * + * @return Returns true if selected, else false + */ public boolean isSelected() { return selected; } - private void toggleSelection() { + /** + * Toggle the selection of the row + */ + public void toggleSelection(boolean ctrlSelect) { selected = !selected; if (selected) { - if (selectMode == Table.SELECT_MODE_SINGLE) { - deselectAll(); + if (ctrlSelect) { + lastSelectedRowKey = rowKey; } selectedRowKeys.add(String.valueOf(rowKey)); addStyleName("v-selected"); @@ -2664,6 +3423,59 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } } + /** + * Is called when a user clicks an item when holding SHIFT key down. + * This will select a new range from the last cell clicked + * + * @param deselectPrevious + * Should the previous selected range be deselected + */ + private void toggleShiftSelection(boolean deselectPrevious) { + + /* + * Ensures that we are in multiselect mode and that we have a + * previous selection which was not a deselection + */ + if (selectedRowKeys.isEmpty() || lastSelectedRowKey < 0) { + // No previous selection found + deselectAll(); + toggleSelection(true); + return; + } + + // Set the selectable range + int startKey = lastSelectedRowKey; + int endKey = rowKey; + if (endKey < startKey) { + // Swap keys if in the wrong order + startKey ^= endKey; + endKey ^= startKey; + startKey ^= endKey; + } + + // Deselect previous items if so desired + if (deselectPrevious) { + deselectAll(); + } + + // Select the range (not including this row) + for (int r = startKey; r <= endKey; r++) { + if (r != rowKey) { + VScrollTableRow row = getRenderedRowByKey(String + .valueOf(r)); + if (row != null && !row.isSelected()) { + row.toggleSelection(false); + } + } + } + + // Toggle clicked rows selection + toggleSelection(false); + + // Add range + selectedRowRanges.add(new SelectionRange(startKey, endKey)); + } + /* * (non-Javadoc) * @@ -2763,17 +3575,20 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, } } + /** + * Deselects all items + */ public void deselectAll() { final Object[] keys = selectedRowKeys.toArray(); for (int i = 0; i < keys.length; i++) { final VScrollTableRow row = getRenderedRowByKey((String) keys[i]); if (row != null && row.isSelected()) { - row.toggleSelection(); + row.toggleSelection(false); } } // still ensure all selects are removed from (not necessary rendered) selectedRowKeys.clear(); - + selectedRowRanges.clear(); } /** @@ -2943,6 +3758,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, private void setContentWidth(int pixels) { tHead.setWidth(pixels + "px"); bodyContainer.setWidth(pixels + "px"); + tFoot.setWidth(pixels + "px"); } private int borderWidth = -1; @@ -2967,7 +3783,8 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, */ private void setContainerHeight() { if (height != null && !"".equals(height)) { - int contentH = getOffsetHeight() - tHead.getOffsetHeight(); + int contentH = getOffsetHeight() - tHead.getOffsetHeight() + - tFoot.getOffsetHeight(); contentH -= getContentAreaBorderHeight(); if (contentH < 0) { contentH = 0; @@ -3095,6 +3912,9 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, // fix headers horizontal scrolling tHead.setHorizontalScrollPosition(scrollLeft); + // fix footers horizontal scrolling + tFoot.setHorizontalScrollPosition(scrollLeft); + firstRowInViewPort = (int) Math.ceil(scrollTop / scrollBody.getRowHeight()); if (firstRowInViewPort > totalRows - pageLength) { diff --git a/src/com/vaadin/ui/Component.java b/src/com/vaadin/ui/Component.java index 251a415e83..cfc648d9b0 100644 --- a/src/com/vaadin/ui/Component.java +++ b/src/com/vaadin/ui/Component.java @@ -441,7 +441,8 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * <pre> * RichTextArea area = new RichTextArea(); * area.setCaption("You can edit stuff here"); - * area.setValue("<h1>Helpful Heading</h1>" + "<p>All this is for you to edit.</p>"); + * area.setValue("<h1>Helpful Heading</h1>" + * + "<p>All this is for you to edit.</p>"); * </pre> * * <p> @@ -573,7 +574,7 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * Gets the application object to which the component is attached. * * <p> - * The method will return {@code null} if the component has not yet been + * The method will return {@code null} if the component is not currently * attached to an application. This is often a problem in constructors of * regular components and in the initializers of custom composite * components. A standard workaround is to move the problematic @@ -716,7 +717,7 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * as an empty collection. */ public void childRequestedRepaint( - Collection<RepaintRequestListener> alreadyNotified); + Collection<RepaintRequestListener> alreadyNotified); /* Component event framework */ @@ -819,11 +820,13 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * * public void componentEvent(Event event) { * // Act according to the source of the event - * if (event.getSource() == ok && event.getClass() == Button.ClickEvent.class) + * if (event.getSource() == ok + * && event.getClass() == Button.ClickEvent.class) * getWindow().showNotification("Click!"); * * // Display source component and event class names - * status.setValue("Event from " + event.getSource().getClass().getName() + ": " + event.getClass().getName()); + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); * } * } * @@ -851,7 +854,8 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * getWindow().showNotification("Click!"); * * // Display source component and event class names - * status.setValue("Event from " + event.getSource().getClass().getName() + ": " + event.getClass().getName()); + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); * } * </pre> * @@ -898,7 +902,8 @@ public interface Component extends Paintable, VariableOwner, Sizeable, * if (event.getSource() == ok) * getWindow().showNotification("Click!"); * - * status.setValue("Event from " + event.getSource().getClass().getName() + ": " + event.getClass().getName()); + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); * } * } * diff --git a/src/com/vaadin/ui/Table.java b/src/com/vaadin/ui/Table.java index 610d638ab4..234173c2f1 100644 --- a/src/com/vaadin/ui/Table.java +++ b/src/com/vaadin/ui/Table.java @@ -5,6 +5,7 @@ package com.vaadin.ui; import java.io.Serializable; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -93,6 +94,22 @@ public class Table extends AbstractSelect implements Action.Container, MULTIROW } + /** + * Multi select modes that controls how multi select behaves. + */ + public enum MultiSelectMode { + /** + * Simple left clicks only selects one item, CTRL+left click selects + * multiple items and SHIFT-left click selects a range of items. + */ + DEFAULT, + /** + * Uses the old method of selection. CTRL- and SHIFT-clicks are disabled and + * clicking on the items selects/deselects them. + */ + SIMPLE + } + private static final int CELL_KEY = 0; private static final int CELL_HEADER = 1; @@ -225,6 +242,11 @@ public class Table extends AbstractSelect implements Action.Container, private final HashMap<Object, String> columnHeaders = new HashMap<Object, String>(); /** + * Holds footers for visible columns (by propertyId). + */ + private final HashMap<Object, String> columnFooters = new HashMap<Object, String>(); + + /** * Holds icons for visible columns (by propertyId). */ private final HashMap<Object, Resource> columnIcons = new HashMap<Object, Resource>(); @@ -271,6 +293,11 @@ public class Table extends AbstractSelect implements Action.Container, private int columnHeaderMode = COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID; /** + * Should the Table footer be visible? + */ + private boolean columnFootersVisible = false; + + /** * True iff the row captions are hidden. */ private boolean rowCaptionsAreHidden = true; @@ -368,6 +395,12 @@ public class Table extends AbstractSelect implements Action.Container, private DropHandler dropHandler; + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + + private HeaderClickHandler headerClickHandler; + + private FooterClickHandler footerClickHandler; + /* Table constructors */ /** @@ -1830,7 +1863,77 @@ public class Table extends AbstractSelect implements Action.Container, resetPageBuffer(); enableContentRefreshing(true); + } + + /** + * Gets items ids from a range of key values + * + * @param startRowKey + * The start key + * @param endRowKey + * The end key + * @return + */ + private Set<Object> getItemIdsInRange(int startRowKey, int endRowKey) { + HashSet<Object> ids = new HashSet<Object>(); + + Object startItemId = itemIdMapper.get(String.valueOf(startRowKey)); + ids.add(startItemId); + + Object endItemId = itemIdMapper.get(String.valueOf(endRowKey)); + ids.add(endItemId); + Object currentItemId = startItemId; + + Container.Ordered ordered = (Container.Ordered) items; + while (currentItemId != endItemId) { + currentItemId = ordered.nextItemId(currentItemId); + if (currentItemId != null) { + ids.add(currentItemId); + } + } + + return ids; + } + + /** + * Handles selection if selection is a multiselection + * + * @param variables + * The variables + */ + private void handleSelectedItems(Map<String, Object> variables) { + final String[] ka = (String[]) variables.get("selected"); + final String[] ranges = (String[]) variables.get("selectedRanges"); + + // Converts the key-array to id-set + final LinkedList s = new LinkedList(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + // Add range items + for (String range : ranges) { + String[] limits = range.split("-"); + int start = Integer.valueOf(limits[0]); + int end = Integer.valueOf(limits[1]); + s.addAll(getItemIdsInRange(start, end)); + } + + setValue(s, true); } /* Component basics */ @@ -1857,6 +1960,18 @@ public class Table extends AbstractSelect implements Action.Container, variables.remove("selected"); } + /* + * The AbstractSelect cannot handle the multiselection properly, instead + * we handle it ourself + */ + else if (isSelectable() && isMultiSelect() + && variables.containsKey("selected") + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap<String, Object>(variables); + variables.remove("selected"); + } + super.changeVariables(source, variables); // Client might update the pagelength if Table height is fixed @@ -2000,6 +2115,8 @@ public class Table extends AbstractSelect implements Action.Container, * @param variables */ private void handleClickEvent(Map<String, Object> variables) { + + // Item click event if (variables.containsKey("clickEvent")) { String key = (String) variables.get("clickedKey"); Object itemId = itemIdMapper.get(key); @@ -2017,6 +2134,33 @@ public class Table extends AbstractSelect implements Action.Container, evt)); } } + + // Header click event + else if (variables.containsKey("headerClickEvent")) { + + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("headerClickEvent")); + + Object cid = variables.get("headerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new HeaderClickEvent(this, propertyId, details)); + } + + // Footer click event + else if (variables.containsKey("footerClickEvent")) { + MouseEventDetails details = MouseEventDetails + .deSerialize((String) variables.get("footerClickEvent")); + + Object cid = variables.get("footerClickCID"); + Object propertyId = null; + if (cid != null) { + propertyId = columnIdMap.get(cid.toString()); + } + fireEvent(new FooterClickEvent(this, propertyId, details)); + } } /** @@ -2065,6 +2209,10 @@ public class Table extends AbstractSelect implements Action.Container, target.addAttribute("dragmode", dragMode.ordinal()); } + if (multiSelectMode != MultiSelectMode.DEFAULT) { + target.addAttribute("multiselectmode", multiSelectMode.ordinal()); + } + // Initialize temps final Object[] colids = getVisibleColumns(); final int cols = colids.length; @@ -2098,15 +2246,10 @@ public class Table extends AbstractSelect implements Action.Container, // selection support LinkedList<String> selectedKeys = new LinkedList<String>(); - if (isMultiSelect()) { - // only paint selections that are currently visible in the client + if (isMultiSelect()) { HashSet sel = new HashSet((Set) getValue()); - Collection vids = getVisibleItemIds(); - for (Iterator it = vids.iterator(); it.hasNext();) { - Object id = it.next(); - if (sel.contains(id)) { - selectedKeys.add(itemIdMapper.key(id)); - } + for (Object id : sel) { + selectedKeys.add(itemIdMapper.key(id)); } } else { Object value = getValue(); @@ -2146,6 +2289,9 @@ public class Table extends AbstractSelect implements Action.Container, if (rowheads) { target.addAttribute("rowheaders", true); } + if (columnFootersVisible) { + target.addAttribute("colfooters", true); + } // Visible column order final Collection sortables = getSortableContainerPropertyIds(); @@ -2380,6 +2526,8 @@ public class Table extends AbstractSelect implements Action.Container, target.addAttribute("cid", columnIdMap.key(columnId)); final String head = getColumnHeader(columnId); target.addAttribute("caption", (head != null ? head : "")); + final String foot = getColumnFooter(columnId); + target.addAttribute("fcaption", (foot != null ? foot : "")); if (isColumnCollapsed(columnId)) { target.addAttribute("collapsed", true); } @@ -2641,6 +2789,7 @@ public class Table extends AbstractSelect implements Action.Container, columnAlignments.remove(propertyId); columnIcons.remove(propertyId); columnHeaders.remove(propertyId); + columnFooters.remove(propertyId); return super.removeContainerProperty(propertyId); } @@ -3433,6 +3582,27 @@ public class Table extends AbstractSelect implements Action.Container, } /** + * Sets the behavior of how the multi-select mode should behave when the + * table is both selectable and in multi-select mode. + * + * @param mode + * The select mode of the table + */ + public void setMultiSelectMode(MultiSelectMode mode) { + multiSelectMode = mode; + requestRepaint(); + } + + /** + * Returns the select mode in which multi-select is used. + * + * @return The multi select mode + */ + public MultiSelectMode getMultiSelectMode() { + return multiSelectMode; + } + + /** * Lazy loading accept criterion for Table. Accepted target rows are loaded * from server once per drag and drop operation. Developer must override one * method that decides on which rows the currently dragged data can be @@ -3525,4 +3695,306 @@ public class Table extends AbstractSelect implements Action.Container, } + /** + * Click event fired when clicking on the Table headers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class HeaderClickEvent extends Component.Event { + public static final Method HEADER_CLICK_METHOD; + + static { + try { + // Set the header click method + HEADER_CLICK_METHOD = HeaderClickHandler.class + .getDeclaredMethod("handleHeaderClick", + new Class[] { HeaderClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + // The property id of the column which header was pressed + private Object columnPropertyId; + + // The mouse details + private MouseEventDetails details; + + public HeaderClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source); + this.details = details; + columnPropertyId = propertyId; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + + /** + * Returns the details of the mouse event like the mouse coordinates, + * button pressed etc. + * + * @return The mouse details + */ + public MouseEventDetails getEventDetails() { + return details; + } + } + + /** + * Click event fired when clicking on the Table footers. The event includes + * a reference the the Table the event originated from, the property id of + * the column which header was pressed and details about the mouse event + * itself. + */ + public static class FooterClickEvent extends Component.Event { + public static final Method FOOTER_CLICK_METHOD; + + static { + try { + // Set the header click method + FOOTER_CLICK_METHOD = FooterClickHandler.class + .getDeclaredMethod("handleFooterClick", + new Class[] { FooterClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + // The property id of the column which header was pressed + private Object columnPropertyId; + + // The mouse details + private MouseEventDetails details; + + /** + * Constructor + * @param source + * The source of the component + * @param propertyId + * The propertyId of the column + * @param details + * The mouse details of the click + */ + public FooterClickEvent(Component source, Object propertyId, + MouseEventDetails details) { + super(source); + columnPropertyId = propertyId; + this.details = details; + } + + /** + * Gets the property id of the column which header was pressed + * + * @return The column propety id + */ + public Object getPropertyId() { + return columnPropertyId; + } + + /** + * Returns the details of the mouse event like the mouse coordinates, + * button pressed etc. + * + * @return The mouse details + */ + public MouseEventDetails getEventDetails() { + return details; + } + } + + /** + * Interface for the handler listening to column header mouse click events. + * The handleHeaderClick method is called when the user presses a header + * column cell. + */ + public interface HeaderClickHandler { + + /** + * Called when a user clicks a header column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void handleHeaderClick(HeaderClickEvent event); + } + + /** + * Interface for the handler listening to column footer mouse click events. + * The handleHeaderClick method is called when the user presses a footer + * column cell. + */ + public interface FooterClickHandler { + + /** + * Called when a user clicks a footer column cell + * + * @param event + * The event which contains information about the column and + * the mouse click event + */ + public void handleFooterClick(FooterClickEvent event); + } + + /** + * Sets the header click handler which handles the click events when the + * user clicks on a column header cell in the Table. + * <p> + * The handler will receive events which contains information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param handler + * The handler which should handle the header click events. + */ + public void setHeaderClickHandler(HeaderClickHandler handler) { + if (headerClickHandler != handler) { + if (handler == null && headerClickHandler != null) { + // Remove header click handler + removeListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler); + + headerClickHandler = handler; + } else if (headerClickHandler != null) { + // Replace header click handler + removeListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler); + + headerClickHandler = handler; + + addListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler, + HeaderClickEvent.HEADER_CLICK_METHOD); + } else if (handler != null) { + // Set a new header click handler + headerClickHandler = handler; + addListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, headerClickHandler, + HeaderClickEvent.HEADER_CLICK_METHOD); + } + } + } + + /** + * Sets the footer click handler which handles the click events when the + * user clicks on a column footer cell in the Table. + * <p> + * The handler will recieve events which contains information about which + * column was clicked and some details about the mouse event. + * </p> + * + * @param handler + * The handler which should handle the footer click events + */ + public void setFooterClickHandler(FooterClickHandler handler) { + if (footerClickHandler != handler) { + if (handler == null && footerClickHandler != null) { + // Remove header click handler + removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler); + footerClickHandler = handler; + } else if (footerClickHandler != null) { + // Replace footer click handler + removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler); + footerClickHandler = handler; + addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler, + FooterClickEvent.FOOTER_CLICK_METHOD); + } else if (handler != null) { + // Set a new footer click handler + footerClickHandler = handler; + addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, footerClickHandler, + FooterClickEvent.FOOTER_CLICK_METHOD); + } + } + } + + /** + * Returns the header click handler which receives click events from the + * columns header cells when they are clicked on. + * + * @return + */ + public HeaderClickHandler getHeaderClickHandler() { + return headerClickHandler; + } + + /** + * Returns the footer click handler which recieves click events from the + * columns footer cells when they are clicked on. + * + * @return + */ + public FooterClickHandler getFooterClickHandler() { + return footerClickHandler; + } + + /** + * Gets the footer caption beneath the rows + * + * @param propertyId + * The propertyId of the column * + * @return The caption of the footer or NULL if not set + */ + public String getColumnFooter(Object propertyId) { + return columnFooters.get(propertyId); + } + + /** + * Sets the column footer caption. The column footer caption is the text + * displayed beneath the column if footers have been set visible. + * + * @param propertyId + * The properyId of the column + * + * @param footer + * The caption of the footer + */ + public void setColumnFooter(Object propertyId, String footer) { + if (footer == null) { + columnFooters.remove(propertyId); + return; + } + columnFooters.put(propertyId, footer); + + requestRepaint(); + } + + /** + * Sets the footer visible in the bottom of the table. + * <p> + * The footer can be used to add column related data like sums to the bottom + * of the Table using setColumnFooter(Object propertyId, String footer). + * </p> + * + * @param visible + * Should the footer be visible + */ + public void setFooterVisible(boolean visible){ + columnFootersVisible = visible; + + // Assures the visual refresh + refreshRenderedCells(); + } + + /** + * Is the footer currently visible? + * + * @return Returns true if visible else false + */ + public boolean isFooterVisible() { + return columnFootersVisible; + } } |