/* * DBeaver - Universal Database Manager * Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jkiss.dbeaver.ui.controls.lightgrid; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.resource.JFaceColors; import org.eclipse.swt.SWT; import org.eclipse.swt.events.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.widgets.*; import org.eclipse.ui.progress.UIJob; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.ui.UIUtils; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.IntKeyMap; import java.util.*; import java.util.List; /** * LightGrid * initially based on Nebula grid. Refactored and mostly redone. * * @author serge@jkiss.org * @author chris.gross@us.ibm.com */ public abstract class LightGrid extends Canvas { private static final Log log = Log.getLog(LightGrid.class); public static final int MAX_TOOLTIP_LENGTH = 1000; public static final int Event_ChangeSort = 1000; public static final int Event_NavigateLink = 1001; /** * Horizontal scrolling increment, in pixels. */ private static final int HORZ_SCROLL_INCREMENT = 5; /** * The area to the left and right of the column boundary/resizer that is * still considered the resizer area. This prevents a user from having to be * *exactly* over the resizer. */ private static final int COLUMN_RESIZER_THRESHOLD = 4; private static final int DEFAULT_ROW_HEADER_WIDTH = 30; private static final int MAX_ROW_HEADER_WIDTH = 400; /** * The minimum width of a column header. */ private static final int MIN_COLUMN_HEADER_WIDTH = 32; /** * Threshold for the selection border used for drag n drop * in mode. */ private static final int SELECTION_DRAG_BORDER_THRESHOLD = 2; public enum EventSource { MOUSE, KEYBOARD, } static class GridNode { GridNode parent; Object[] rows; IGridContentProvider.ElementState state; int level; private GridNode(GridNode parent, Object[] rows, IGridContentProvider.ElementState state, int level) { this.parent = parent; this.rows = rows; this.state = state; this.level = level; } public boolean isParentOf(GridNode node) { for (GridNode p = node; p != null; p = p.parent) { if (p == this) { return true; } } return false; } } // Tooltips private class ToolTipHandler extends UIJob { private String toolTip; public ToolTipHandler() { super("ToolTip handler"); setSystem(true); } @Override public IStatus runInUIThread(IProgressMonitor monitor) { if (!monitor.isCanceled() && !LightGrid.this.isDisposed()) { LightGrid.this.setToolTipText(toolTip); } toolTipHandler = null; return Status.OK_STATUS; } } // Last calculated client area private volatile static Rectangle lastClientArea; private volatile String prevToolTip; private volatile ToolTipHandler toolTipHandler; /** * Tracks whether the scroll values are correct. If not they will be * recomputed in onPaint. This allows us to get a free ride on top of the * OS's paint event merging to assure that we don't perform this expensive * operation when unnecessary. */ private boolean scrollValuesObsolete = false; /** * Reference to the item in focus. */ private int focusItem = -1; private final Set<GridPos> selectedCells = new TreeSet<>(new GridPos.PosComparator()); private final List<GridPos> selectedCellsBeforeRangeSelect = new ArrayList<>(); private final List<GridColumn> selectedColumns = new ArrayList<>(); private final IntKeyMap<Boolean> selectedRows = new IntKeyMap<>(); private boolean cellDragSelectionOccurring = false; private boolean cellRowDragSelectionOccurring = false; private boolean cellColumnDragSelectionOccurring = false; private boolean cellDragCTRL = false; private boolean followupCellSelectionEventOwed = false; private boolean cellSelectedOnLastMouseDown; private boolean cellRowSelectedOnLastMouseDown; private boolean cellColumnSelectedOnLastMouseDown; private GridColumn shiftSelectionAnchorColumn; private GridColumn focusColumn; private final GridPos focusCell = new GridPos(-1, -1); /** * List of table columns in creation/index order. */ private final List<GridColumn> topColumns = new ArrayList<>(); private final List<GridColumn> columns = new ArrayList<>(); private int maxColumnDepth = 0; protected Object[] columnElements = new Object[0]; protected Object[] rowElements = new Object[0]; private GridNode[] parentNodes = new GridNode[0]; private final Map<Object, GridNode> rowNodes = new IdentityHashMap<>(); private int maxColumnDefWidth = 1000; private GridColumnRenderer columnHeaderRenderer; private GridRowRenderer rowHeaderRenderer; private GridCellRenderer cellRenderer; /** * Are row headers visible? */ private boolean rowHeaderVisible = false; /** * Are column headers visible? */ private boolean columnHeadersVisible = false; /** * Type of selection behavior. Valid values are SWT.SINGLE and SWT.MULTI. */ private int selectionType = SWT.SINGLE; /** * Default height of items. */ private int itemHeight = 1; /** * Width of each row header. */ private int rowHeaderWidth = 0; /** * Height of each column header. */ private int headerHeight = 0; boolean hoveringOnHeader = false; boolean hoveringOnColumnSorter = false; boolean hoveringOnLink = false; private GridColumn columnBeingSorted; boolean hoveringOnColumnResizer = false; private GridColumn columnBeingResized; private boolean resizingColumn = false; private int resizingStartX = 0; private int resizingColumnStartWidth = 0; private int hoveringItem; private GridColumn hoveringColumn; /** * String-based detail of what is being hovered over in a cell. This allows * a renderer to differentiate between hovering over different parts of the * cell. For example, hovering over a checkbox in the cell or hovering over * a tree node in the cell. The table does nothing with this string except * to set it back in the renderer when its painted. The renderer sets this * during its notify method (InternalWidget.HOVER) and the table pulls it * back and maintains it so it can be set back when the cell is painted. The * renderer determines what the hover detail means and how it affects * painting. */ private Object hoveringDetail = null; /** * Are the grid lines visible? */ private boolean linesVisible = true; @NotNull private final IGridScrollBar vScroll; @NotNull private final IGridScrollBar hScroll; /** * Item selected when a multiple selection using shift+click first occurs. * This item anchors all further shift+click selections. */ private int shiftSelectionAnchorItem; private boolean columnScrolling = false; private Color cellHeaderSelectionBackground; /** * Dispose listener. This listener is removed during the dispose event to allow re-firing of * the event. */ private Listener disposeListener; GC sizingGC; FontMetrics fontMetrics; Font normalFont; @NotNull private Color lineColor; private Color lineSelectedColor; private Color backgroundColor; private Color foregroundColor; @NotNull private Cursor sortCursor; /** * Index of first visible item. The value must never be read directly. It is cached and * updated when appropriate. #getTopIndex should be called for every client (even internal * callers). A value of -1 indicates that the value is old and will be recomputed. * * @see #bottomIndex */ private int topIndex = -1; /** * Index of last visible item. The value must never be read directly. It is cached and * updated when appropriate. #getBottomIndex() should be called for every client (even internal * callers). A value of -1 indicates that the value is old and will be recomputed. * <p/> * Note that the item with this index is often only partly visible; maybe only * a single line of pixels is visible. In extreme cases, bottomIndex may be the * same as topIndex. * * @see #topIndex */ private int bottomIndex = -1; /** * True if the last visible item is completely visible. The value must never be read directly. It is cached and * updated when appropriate. #isShown() should be called for every client (even internal * callers). * * @see #bottomIndex */ private boolean bottomIndexShownCompletely = false; /** * This is the tooltip text currently used. This could be the tooltip text for the currently * hovered cell, or the general grid tooltip. See handleCellHover. */ private String displayedToolTipText; private boolean hoveringOnSelectionDragArea = false; /** * A range of rows in a <code>Grid</code>. * <p/> * A row in this sense exists only for visible items * Therefore, the items at 'startIndex' and 'endIndex' * are always visible. * * @see LightGrid#getRowRange(int, int, boolean, boolean) */ private static class RowRange { /** * index of first item in range */ public int startIndex; /** * index of last item in range */ public int endIndex; /** * number of rows (i.e. <em>visible</em> items) in this range */ public int rows; /** * height in pixels of this range (including horizontal separator between rows) */ public int height; } /** * Filters out unnecessary styles, adds mandatory styles and generally * manages the style to pass to the super class. * * @param style user specified style. * @return style to pass to the super class. */ private static int checkStyle(int style) { int mask = SWT.BORDER | SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT | SWT.H_SCROLL | SWT.V_SCROLL | SWT.SINGLE | SWT.MULTI | SWT.NO_FOCUS | SWT.CHECK | SWT.VIRTUAL; int newStyle = style & mask; newStyle |= SWT.DOUBLE_BUFFERED; return newStyle; } /** * Constructs a new instance of this class given its parent and a style * value describing its behavior and appearance. * <p/> * * @param parent a composite control which will be the parent of the new * instance (cannot be null) * @param style the style of control to construct * @see SWT#SINGLE * @see SWT#MULTI */ public LightGrid(Composite parent, int style) { super(parent, checkStyle(style)); sizingGC = new GC(this); fontMetrics = sizingGC.getFontMetrics(); normalFont = getFont(); columnHeaderRenderer = new GridColumnRenderer(this); rowHeaderRenderer = new GridRowRenderer(this); cellRenderer = new GridCellRenderer(this); final Display display = getDisplay(); lineColor = JFaceColors.getErrorBackground(display); lineSelectedColor = JFaceColors.getErrorBorder(display);//SWT.COLOR_WIDGET_DARK_SHADOW; //setForeground(JFaceColors.getBannerForeground(display)); //setBackground(JFaceColors.getBannerBackground(display)); /* ColorRegistry colorRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getColorRegistry(); setLineColor(colorRegistry.get(JFacePreferences.QUALIFIER_COLOR)); setForeground(colorRegistry.get(JFacePreferences.CONTENT_ASSIST_FOREGROUND_COLOR)); setBackground(colorRegistry.get(JFacePreferences.CONTENT_ASSIST_BACKGROUND_COLOR)); */ sortCursor = display.getSystemCursor(SWT.CURSOR_HAND); if ((style & SWT.MULTI) != 0) { selectionType = SWT.MULTI; } if (getVerticalBar() != null) { getVerticalBar().setVisible(false); vScroll = new ScrollBarAdapter(getVerticalBar()); } else { vScroll = new NullScrollBar(); } if (getHorizontalBar() != null) { getHorizontalBar().setVisible(false); hScroll = new ScrollBarAdapter(getHorizontalBar()); } else { hScroll = new NullScrollBar(); } scrollValuesObsolete = true; initListeners(); recalculateSizes(); RGB cellSel = UIUtils.blend( display.getSystemColor(SWT.COLOR_LIST_SELECTION).getRGB(), new RGB(255, 255, 255), 50); cellHeaderSelectionBackground = new Color(display, cellSel); setDragDetect(false); } @NotNull public abstract IGridContentProvider getContentProvider(); @NotNull public abstract IGridLabelProvider getLabelProvider(); public boolean hasNodes() { return !rowNodes.isEmpty(); } public void setMaxColumnDefWidth(int maxColumnDefWidth) { this.maxColumnDefWidth = maxColumnDefWidth; } void collectRows(List<Object> result, List<GridNode> parents, @Nullable GridNode parent, Object[] rows, int level) { for (int i = 0; i < rows.length; i++) { Object row = rows[i]; if (row == null) { continue; } result.add(row); parents.add(parent); Object[] children = getContentProvider().getChildren(row); if (children != null) { IGridContentProvider.ElementState state; GridNode node = rowNodes.get(row); if (node == null) { state = getContentProvider().getDefaultState(row); node = new GridNode(parent, children, state, level + 1); } else { state = node.state; } rowNodes.put(row, node); if (state == IGridContentProvider.ElementState.EXPANDED) { collectRows(result, parents, node, children, level + 1); } } } } /** * Refresh grid data */ public void refreshData(boolean refreshColumns, boolean keepState) { GridPos savedFocus = keepState ? getFocusPos() : null; int savedHSB = keepState ? hScroll.getSelection() : -1; int savedVSB = keepState ? vScroll.getSelection() : -1; if (refreshColumns) { this.removeAll(); } else { this.deselectAll(); topIndex = -1; bottomIndex = -1; } IGridContentProvider contentProvider = getContentProvider(); { // Prepare rows Object[] initialElements = contentProvider.getElements(false); this.rowNodes.clear(); List<Object> realRows = new ArrayList<>(initialElements.length); List<GridNode> parents = new ArrayList<>(initialElements.length); collectRows(realRows, parents, null, initialElements, 0); this.rowElements = realRows.toArray(); this.parentNodes = parents.toArray(new GridNode[parents.size()]); } this.displayedToolTipText = null; if (refreshColumns) { this.maxColumnDepth = 0; // Add columns this.columnElements = contentProvider.getElements(true); for (Object columnElement : columnElements) { GridColumn column = new GridColumn(this, columnElement); createChildColumns(column); } // Invalidate columns structure boolean hasChildColumns = false; for (Iterator<GridColumn> iter = columns.iterator(); iter.hasNext(); ) { GridColumn column = iter.next(); if (column.getParent() == null) { topColumns.add(column); } else { hasChildColumns = true; } if (column.getChildren() != null) { iter.remove(); } } if (hasChildColumns) { // Rebuild columns model columnElements = new Object[columns.size()]; for (int i = 0; i < columns.size(); i++) { columnElements[i] = columns.get(i).getElement(); } } scrollValuesObsolete = true; if (getColumnCount() == 1) { // Here we going to maximize single column to entire grid's width // Sometimes (when new grid created and filled with data very fast our client area size is zero // So let's add a workaround for it and use column's width in this case GridColumn column = getColumn(0); int columnWidth = column.computeHeaderWidth(); int gridWidth = getCurrentOrLastClientArea().width - getRowHeaderWidth() - getHScrollSelectionInPixels() - getVerticalBar().getSize().x; if (gridWidth > columnWidth) { columnWidth = gridWidth; } column.setWidth(columnWidth); } else { int totalWidth = 0; for (GridColumn curColumn : topColumns) { curColumn.pack(false); totalWidth += curColumn.getWidth(); } // If grid width more than screen - lets narrow too long columns int clientWidth = getCurrentOrLastClientArea().width; if (totalWidth > clientWidth) { int normalWidth = 0; List<GridColumn> fatColumns = new ArrayList<>(); for (GridColumn curColumn : columns) { if (curColumn.getWidth() > maxColumnDefWidth) { fatColumns.add(curColumn); } else { normalWidth += curColumn.getWidth(); } } if (!fatColumns.isEmpty()) { // Narrow fat columns on decWidth int freeSpace = (clientWidth - normalWidth - getBorderWidth() - rowHeaderWidth - vScroll.getWidth()) / fatColumns.size(); int newFatWidth = (freeSpace > maxColumnDefWidth ? freeSpace : maxColumnDefWidth); for (GridColumn curColumn : fatColumns) { curColumn.setWidth(newFatWidth); } } } } } // Recalculate indexes, sizes and update scrollbars topIndex = -1; bottomIndex = -1; recalculateSizes(); updateScrollbars(); // Restore state if (savedFocus != null) { savedFocus.row = Math.min(savedFocus.row, getItemCount() - 1); savedFocus.col = Math.min(savedFocus.col, getColumnCount() - 1); if (savedFocus.row >= 0) setFocusItem(savedFocus.row); if (savedFocus.col >= 0) setFocusColumn(savedFocus.col); if (savedFocus.isValid()) selectCell(savedFocus); } if (savedHSB >= 0) { hScroll.setSelection(Math.min(hScroll.getMaximum(), savedHSB)); } if (savedVSB >= 0) { vScroll.setSelection(Math.min(vScroll.getMaximum(), savedVSB)); } // // Add focus cell to selection // GridPos focusPos = getFocusPos(); // if (focusPos.isValid()) { // selectCell(focusPos); // } } /** * Returns current or last client area. * If Grid controls are stacked then only the top is visible and has real client area. * So we cache it - all stack has the same client area */ private Rectangle getCurrentOrLastClientArea() { Rectangle clientArea = getClientArea(); if (clientArea.width == 0) { if (lastClientArea == null) { return clientArea; } return lastClientArea; } lastClientArea = clientArea; return clientArea; } private void createChildColumns(GridColumn parent) { Object[] children = getContentProvider().getChildren(parent.getElement()); if (children != null) { for (Object child : children) { GridColumn column = new GridColumn(parent, child); createChildColumns(column); } } this.maxColumnDepth = Math.max(this.maxColumnDepth, parent.getLevel()); } @Nullable public GridCell posToCell(GridPos pos) { if (pos.col < 0 || pos.row < 0) { return null; } return new GridCell(columnElements[pos.col], rowElements[pos.row]); } @NotNull public GridPos cellToPos(GridCell cell) { int colIndex = ArrayUtils.indexOf(columnElements, cell.col); int rowIndex = ArrayUtils.indexOf(rowElements, cell.row); return new GridPos(colIndex, rowIndex); } public Object getColumnElement(int col) { return columnElements[col]; } public Rectangle getColumnBounds(int col) { return getColumn(col).getBounds(); } public Object getRowElement(int row) { return rowElements[row]; } @Override public Color getBackground() { if (backgroundColor == null) { backgroundColor = super.getBackground(); } return backgroundColor; } @Override public void setBackground(Color color) { super.setBackground(backgroundColor = color); } /////////////////////////////////// // Just caching because native impl creates new objects and called too often @Override public Color getForeground() { if (foregroundColor == null) { foregroundColor = super.getForeground(); } return foregroundColor; } @Override public void setForeground(Color color) { super.setForeground(foregroundColor = color); getContentProvider().resetColors(); } /** * Returns the background color of column and row headers when a cell in * the row or header is selected. * * @return cell header selection background color */ public Color getCellHeaderSelectionBackground() { return cellHeaderSelectionBackground; } /** * Adds the listener to the collection of listeners who will be notified * when the receiver's selection changes, by sending it one of the messages * defined in the {@code SelectionListener} interface. * <p/> * Cell selection events may have <code>Event.detail = SWT.DRAG</code> when the * user is drag selecting multiple cells. A follow up selection event will be generated * when the drag is complete. * * @param listener the listener which should be notified */ public void addSelectionListener(SelectionListener listener) { checkWidget(); if (listener == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); } addListener(SWT.Selection, new TypedListener(listener)); addListener(SWT.DefaultSelection, new TypedListener(listener)); } /** * Removes the listener from the collection of listeners who will be * notified when the receiver's selection changes. * * @param listener the listener which should no longer be notified * @see SelectionListener * @see #addSelectionListener(SelectionListener) */ public void removeSelectionListener(SelectionListener listener) { checkWidget(); removeListener(SWT.Selection, listener); removeListener(SWT.DefaultSelection, listener); } @Override public Point computeSize(int wHint, int hHint, boolean changed) { checkWidget(); Point prefSize = null; if (wHint == SWT.DEFAULT || hHint == SWT.DEFAULT) { prefSize = getTableSize(); prefSize.x += 2 * getBorderWidth(); prefSize.y += 2 * getBorderWidth(); } int x = 0; int y = 0; if (wHint == SWT.DEFAULT) { x += prefSize.x; if (getVerticalBar() != null) { x += getVerticalBar().getSize().x; } } else { x = wHint; } if (hHint == SWT.DEFAULT) { y += prefSize.y; if (getHorizontalBar() != null) { y += getHorizontalBar().getSize().y; } } else { y = hHint; } return new Point(x, y); } /** * Deselects all selected items in the receiver. If cell selection is enabled, * all cells are deselected. */ public void deselectAll() { checkWidget(); selectedCells.clear(); updateSelectionCache(); redraw(); } @NotNull GridColumn getColumn(int index) { return columns.get(index); } /** * Returns the column at the given point and a known item in the receiver or null if no such * column exists. The point is in the coordinate system of the receiver. * * @param point the point used to locate the column * @return the column at the given point */ @Nullable GridColumn getColumn(Point point) { checkWidget(); if (point == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); return null; } GridColumn overThis = null; int x2 = 0; if (rowHeaderVisible) { if (point.x <= rowHeaderWidth) { return null; } x2 += rowHeaderWidth; } x2 -= getHScrollSelectionInPixels(); for (GridColumn column : columns) { if (point.x >= x2 && point.x < x2 + column.getWidth()) { for (GridColumn parent = column.getParent(); parent != null; parent = parent.getParent()) { Point parentLoc = getOrigin(parent, -1); if (point.y >= parentLoc.y && point.y <= parentLoc.y + parent.getHeaderHeight(false)) { column = parent; break; } } overThis = column; break; } x2 += column.getWidth(); } if (overThis == null) { return null; } return overThis; } /** * Returns the number of columns contained in the receiver. If no * {@code GridColumn}s were created by the programmer, this value is * zero, despite the fact that visually, one column of items may be visible. * This occurs when the programmer uses the table like a list, adding items * but never creating a column. * * @return the number of columns */ public int getColumnCount() { return columns.size(); } Collection<GridColumn> getColumns() { return columns; } public IGridScrollBar getHorizontalScrollBarProxy() { return hScroll; } public IGridScrollBar getVerticalScrollBarProxy() { return vScroll; } /** * Returns the height of the column headers. If this table has column * groups, the returned value includes the height of group headers. * * @return height of the column header row */ public int getHeaderHeight() { return headerHeight; } public int getRowHeaderWidth() { return rowHeaderWidth; } public int getRow(Point point) { checkWidget(); if (point == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); return -1; } final Rectangle clientArea = getClientArea(); if (point.x < 0 || point.x > clientArea.width) return -1; Point p = new Point(point.x, point.y); int y2 = 0; if (columnHeadersVisible) { if (p.y <= headerHeight) { return -1; } y2 += headerHeight; } int row = getTopIndex(); int currItemHeight = getItemHeight(); int itemCount = getItemCount(); while (row < itemCount && y2 <= clientArea.height) { if (p.y >= y2 && p.y < y2 + currItemHeight + 1) { return row; } y2 += currItemHeight + 1; row++; } return -1; } /** * Returns the number of items contained in the receiver. * * @return the number of items */ public int getItemCount() { return rowElements.length; } /** * Returns the default height of the items * * @return default height of items * @see #setItemHeight(int) */ public int getItemHeight() { return itemHeight; } /** * Sets the default height for this <code>Grid</code>'s items. When * this method is called, all existing items are resized * to the specified height and items created afterwards will be * initially sized to this height. * <p/> * As long as no default height was set by the client through this method, * the preferred height of the first item in this <code>Grid</code> is * used as a default for all items (and is returned by {@link #getItemHeight()}). * * @param height default height in pixels */ public void setItemHeight(int height) { checkWidget(); if (height < 1) SWT.error(SWT.ERROR_INVALID_ARGUMENT); itemHeight = height; setScrollValuesObsolete(); redraw(); } /** * Returns the line color. * * @return Returns the lineColor. */ @NotNull public Color getLineColor() { return lineColor; } public void setLineColor(@NotNull Color lineColor) { this.lineColor = lineColor; } public Color getLineSelectedColor() { return lineSelectedColor; } public void setLineSelectedColor(Color lineSelectedColor) { this.lineSelectedColor = lineSelectedColor; } /** * Returns true if the lines are visible. * * @return Returns the linesVisible. */ public boolean isLinesVisible() { return linesVisible; } /** * Returns the next visible item in the table. * * @return next visible item or null */ public int getNextVisibleItem(int index) { if (index >= getItemCount()) { return -1; } if (index == getItemCount() - 1) { return index; } else { return index + 1; } } /** * Returns the previous visible item in the table. Passing null for the item * will return the last visible item in the table. * * @return previous visible item or if item==null last visible item */ public int getPreviousVisibleItem(int index) { if (index == 0) { return -1; } return index - 1; } /** * Returns the previous visible column in the table. * * @param column column * @return previous visible column or null */ @Nullable public GridColumn getPreviousVisibleColumn(GridColumn column) { int index = indexOf(column); if (index <= 0) return null; return columns.get(index - 1); } /** * Returns the next visible column in the table. * * @param column column * @return next visible column or null */ @Nullable public GridColumn getNextVisibleColumn(GridColumn column) { int index = indexOf(column); if (index < 0 || index >= columns.size() - 1) return null; return columns.get(index + 1); } /** * Returns the number of selected cells contained in the receiver. * * @return the number of selected cells */ public int getCellSelectionCount() { return selectedCells.size(); } /** * Returns the zero-relative index of the item which is currently selected * in the receiver, or -1 if no item is selected. If cell selection is enabled, * returns the index of first item that contains at least one selected cell. * * @return the index of the selected item */ public int getSelectionIndex() { if (selectedCells.isEmpty()) return -1; return selectedCells.iterator().next().row; } /** * Returns the zero-relative index of the item which is currently at the top * of the receiver. This index can change when items are scrolled or new * items are added or removed. * * @return the index of the top item */ public int getTopIndex() { if (topIndex != -1) return topIndex; if (!vScroll.getVisible()) { topIndex = 0; } else { // figure out first visible row and last visible row topIndex = vScroll.getSelection(); } return topIndex; } /** * Returns the zero-relative index of the item which is currently at the bottom * of the receiver. This index can change when items are scrolled, expanded * or collapsed or new items are added or removed. * <p/> * Note that the item with this index is often only partly visible; maybe only * a single line of pixels is visible. Use {@link #isShown(int)} to find * out. * <p/> * In extreme cases, getBottomIndex() may return the same value as * {@link #getTopIndex()}. * * @return the index of the bottom item */ public int getBottomIndex() { if (bottomIndex != -1) return bottomIndex; if (getItemCount() == 0) { bottomIndex = 0; } else if (getVisibleGridHeight() < 1) { bottomIndex = getTopIndex(); } else { RowRange range = getRowRange(getTopIndex(), getVisibleGridHeight(), false, false); bottomIndex = range.endIndex; bottomIndexShownCompletely = range.height <= getVisibleGridHeight(); } return bottomIndex; } /** * Returns a {@link RowRange} ranging from * the grid item at startIndex to that at endIndex. * <p/> * This is primarily used to measure the height * in pixel of such a range and to count the number * of visible grid items within the range. * * @param startIndex index of the first item in the range or -1 to the first visible item in this grid * @param endIndex index of the last item in the range or -1 to use the last visible item in this grid * @return */ @Nullable private RowRange getRowRange(int startIndex, int endIndex) { // parameter preparation int itemCount = getItemCount(); if (startIndex == -1) { // search first visible item startIndex = 0; if (startIndex == itemCount) return null; } if (endIndex == -1) { // search last visible item endIndex = itemCount - 1; if (endIndex <= 0) return null; } // fail fast if (startIndex < 0 || endIndex < 0 || startIndex >= itemCount || endIndex >= itemCount || endIndex < startIndex) SWT.error(SWT.ERROR_INVALID_ARGUMENT); RowRange range = new RowRange(); range.startIndex = startIndex; range.endIndex = endIndex; range.rows = range.endIndex - range.startIndex + 1; range.height = (getItemHeight() + 1) * range.rows - 1; return range; } /** * This method can be used to build a range of grid rows * that is allowed to span a certain height in pixels. * <p/> * It returns a {@link RowRange} that contains information * about the range, especially the index of the last * element in the range (or if inverse == true, then the * index of the first element). * <p/> * Note: Even if 'forceEndCompletelyInside' is set to * true, the last item will not lie completely within * the availableHeight, if (height of item at startIndex < availableHeight). * * @param startIndex index of the first (if inverse==false) or * last (if inverse==true) item in the range * @param availableHeight height in pixels * @param forceEndCompletelyInside if true, the last item in the range will lie completely * within the availableHeight, otherwise it may lie partly outside this range * @param inverse if true, then the first item in the range will be searched, not the last * @return range of grid rows * @see RowRange */ private RowRange getRowRange(int startIndex, int availableHeight, boolean forceEndCompletelyInside, boolean inverse) { // parameter preparation if (startIndex == -1) { if (!inverse) { // search first visible item startIndex = 0; } else { // search last visible item startIndex = getItemCount() - 1; } } RowRange range = new RowRange(); if (startIndex < 0 || startIndex >= getItemCount()) { // something is broken range.startIndex = 0; range.endIndex = 0; range.height = 0; range.rows = 0; return range; } if (availableHeight <= 0) { // special case: empty range range.startIndex = startIndex; range.endIndex = startIndex; range.rows = 0; range.height = 0; return range; } int availableRows = (availableHeight + 1) / (getItemHeight() + 1); if (((getItemHeight() + 1) * range.rows - 1) + 1 < availableHeight) { // not all available space used yet // - so add another row if it need not be completely within availableHeight if (!forceEndCompletelyInside) availableRows++; } int otherIndex = startIndex + ((availableRows - 1) * (!inverse ? 1 : -1)); if (otherIndex < 0) otherIndex = 0; if (otherIndex >= getItemCount()) otherIndex = getItemCount() - 1; range.startIndex = !inverse ? startIndex : otherIndex; range.endIndex = !inverse ? otherIndex : startIndex; range.rows = range.endIndex - range.startIndex + 1; range.height = (getItemHeight() + 1) * range.rows - 1; return range; } /** * Returns the height of the plain grid in pixels. * This does <em>not</em> include the height of the column headers. * * @return height of plain grid */ int getGridHeight() { RowRange range = getRowRange(-1, -1); return range != null ? range.height : 0; } /** * Returns the height of the on-screen area that is available * for showing the grid's rows, i.e. the client area of the * scrollable minus the height of the column headers (if shown). * * @return height of visible grid in pixels */ int getVisibleGridHeight() { return getClientArea().height - (columnHeadersVisible ? headerHeight : 0); } /** * Searches the receiver's list starting at the first column (index 0) until * a column is found that is equal to the argument, and returns the index of * that column. If no column is found, returns -1. * * @param column the search column * @return the index of the column */ int indexOf(GridColumn column) { column = column.getFirstLeaf(); int index = columns.indexOf(column); if (index < 0) { log.warn("Bad column [" + column.getElement() + "]"); } return index; } /** * Returns {@code true} if the receiver's row header is visible, and * {@code false} otherwise. * <p/> * * @return the receiver's row header's visibility state */ public boolean isRowHeaderVisible() { return rowHeaderVisible; } /** * Returns true if the given cell is selected. * * @param cell cell * @return true if the cell is selected. */ public boolean isCellSelected(GridPos cell) { if (cell == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); return selectedCells.contains(cell); } /** * Removes all of the items from the receiver. */ public void removeAll() { checkWidget(); deselectAll(); vScroll.setSelection(0); hScroll.setSelection(0); focusItem = -1; focusColumn = null; topIndex = -1; bottomIndex = -1; shiftSelectionAnchorColumn = null; topColumns.clear(); columns.clear(); columnElements = new Object[0]; rowElements = new Object[0]; } /** * Selects the item at the given zero-relative index in the receiver. If the * item at the index was already selected, it remains selected. Indices that * are out of range are ignored. * <p/> * If cell selection is enabled, selects all cells at the given index. * * @param index the index of the item to select */ public void select(int index) { checkWidget(); if (index < 0 || index >= getItemCount()) return; selectCells(getCells(index)); redraw(); } /** * Selects the items in the range specified by the given zero-relative * indices in the receiver. The range of indices is inclusive. The current * selection is not cleared before the new items are selected. * <p/> * If an item in the given range is not selected, it is selected. If an item * in the given range was already selected, it remains selected. Indices * that are out of range are ignored and no items will be selected if start * is greater than end. If the receiver is single-select and there is more * than one item in the given range, then all indices are ignored. * <p/> * If cell selection is enabled, all cells within the given range are selected. * * @param start the start of the range * @param end the end of the range * @see LightGrid#setSelection(int,int) */ public void select(int start, int end) { checkWidget(); if (selectionType == SWT.SINGLE && start != end) return; for (int i = start; i <= end; i++) { if (i < 0) { continue; } if (i > getItemCount() - 1) { break; } selectCells(getCells(i)); } redraw(); } /** * Selects the items at the given zero-relative indices in the receiver. The * current selection is not cleared before the new items are selected. * <p/> * If the item at a given index is not selected, it is selected. If the item * at a given index was already selected, it remains selected. Indices that * are out of range and duplicate indices are ignored. If the receiver is * single-select and multiple indices are specified, then all indices are * ignored. * <p/> * If cell selection is enabled, all cells within the given indices are * selected. * * @param indices the array of indices for the items to select * @see LightGrid#setSelection(int[]) */ public void select(int[] indices) { checkWidget(); if (indices == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); return; } if (selectionType == SWT.SINGLE && indices.length > 1) return; for (int j : indices) { if (j >= 0 && j < getItemCount()) { selectCells(getCells(j)); } } redraw(); } /** * Selects all of the items in the receiver. * <p/> * If the receiver is single-select, do nothing. If cell selection is enabled, * all cells are selected. */ public void selectAll() { checkWidget(); if (selectionType == SWT.SINGLE) return; selectAllCells(); } /** * Marks the receiver's header as visible if the argument is {@code true}, * and marks it invisible otherwise. * * @param show the new visibility state */ public void setHeaderVisible(boolean show) { checkWidget(); this.columnHeadersVisible = show; redraw(); } /** * Sets the line visibility. * * @param linesVisible Te linesVisible to set. */ public void setLinesVisible(boolean linesVisible) { checkWidget(); this.linesVisible = linesVisible; redraw(); } /** * Marks the receiver's row header as visible if the argument is * {@code true}, and marks it invisible otherwise. When row headers are * visible, horizontal scrolling is always done by column rather than by * pixel. * * @param show the new visibility state */ public void setRowHeaderVisible(boolean show) { checkWidget(); this.rowHeaderVisible = show; setColumnScrolling(true); redraw(); } /** * Selects the item at the given zero-relative index in the receiver. The * current selection is first cleared, then the new item is selected. * <p/> * If cell selection is enabled, all cells within the item at the given index * are selected. * * @param index the index of the item to select */ public void setSelection(int index) { checkWidget(); if (index >= 0 && index < getItemCount()) { selectedCells.clear(); selectCells(getCells(index)); } } /** * Selects the items in the range specified by the given zero-relative * indices in the receiver. The range of indices is inclusive. The current * selection is cleared before the new items are selected. * <p/> * Indices that are out of range are ignored and no items will be selected * if start is greater than end. If the receiver is single-select and there * is more than one item in the given range, then all indices are ignored. * <p/> * If cell selection is enabled, all cells within the given range are selected. * * @param start the start index of the items to select * @param end the end index of the items to select * @see LightGrid#deselectAll() * @see LightGrid#select(int,int) */ public void setSelection(int start, int end) { checkWidget(); if (selectionType == SWT.SINGLE && start != end) return; selectedCells.clear(); for (int i = start; i <= end; i++) { if (i < 0) { continue; } if (i > getItemCount() - 1) { break; } selectCells(getCells(i)); } redraw(); } /** * Selects the items at the given zero-relative indices in the receiver. The * current selection is cleared before the new items are selected. * <p/> * Indices that are out of range and duplicate indices are ignored. If the * receiver is single-select and multiple indices are specified, then all * indices are ignored. * <p/> * If cell selection is enabled, all cells within the given indices are selected. * * @param indices the indices of the items to select * @see LightGrid#deselectAll() * @see LightGrid#select(int[]) */ public void setSelection(int[] indices) { checkWidget(); if (selectionType == SWT.SINGLE && indices.length > 1) return; selectedCells.clear(); for (int j : indices) { if (j < 0) { continue; } if (j > getItemCount() - 1) { break; } selectCells(getCells(j)); } redraw(); } /** * Sets the zero-relative index of the item which is currently at the top of * the receiver. This index can change when items are scrolled or new items * are added and removed. * * @param index the index of the top item */ public void setTopIndex(int index) { checkWidget(); if (index < 0 || index >= getItemCount()) { return; } if (!vScroll.getVisible()) { return; } vScroll.setSelection(index); topIndex = -1; bottomIndex = -1; redraw(); } /** * Shows the column. If the column is already showing in the receiver, this * method simply returns. Otherwise, the columns are scrolled until the * column is visible. * */ public void showColumn(int column) { GridColumn col = getColumn(column); showColumn(col); } public void showColumn(Object element) { for (GridColumn column : columns) { if (column.getElement() == element) { showColumn(column); break; } } } private void showColumn(@NotNull GridColumn col) { checkWidget(); if (!hScroll.getVisible()) { return; } int x = getColumnHeaderXPosition(col); int firstVisibleX = 0; if (rowHeaderVisible) { firstVisibleX = rowHeaderWidth; } // if its visible just return final Rectangle clientArea = getClientArea(); if (x >= firstVisibleX && (x + col.getWidth()) <= (firstVisibleX + (clientArea.width - firstVisibleX))) { return; } if (!getColumnScrolling()) { if (x < firstVisibleX) { hScroll.setSelection(getHScrollSelectionInPixels() - (firstVisibleX - x)); } else { if (col.getWidth() > clientArea.width - firstVisibleX) { hScroll.setSelection(getHScrollSelectionInPixels() + (x - firstVisibleX)); } else { x -= clientArea.width - firstVisibleX - col.getWidth(); hScroll.setSelection(getHScrollSelectionInPixels() + (x - firstVisibleX)); } } } else { if (x < firstVisibleX || col.getWidth() > clientArea.width - firstVisibleX) { int sel = indexOf(col); hScroll.setSelection(sel); } else { int availableWidth = clientArea.width - firstVisibleX - col.getWidth(); GridColumn prevCol = getPreviousVisibleColumn(col); GridColumn currentScrollTo = col; while (true) { if (prevCol == null || prevCol.getWidth() > availableWidth) { int sel = indexOf(currentScrollTo); hScroll.setSelection(sel); break; } else { availableWidth -= prevCol.getWidth(); currentScrollTo = prevCol; prevCol = getPreviousVisibleColumn(prevCol); } } } } redraw(); } /** * Returns true if 'item' is currently being <em>completely</em> * shown in this <code>Grid</code>'s visible on-screen area. * <p/> * <p>Here, "completely" only refers to the item's height, not its * width. This means this method returns true also if some cells * are horizontally scrolled away. * * @param row * @return true if 'item' is shown */ boolean isShown(int row) { checkWidget(); if (row == -1) SWT.error(SWT.ERROR_INVALID_ARGUMENT); int firstVisibleIndex = getTopIndex(); int lastVisibleIndex = getBottomIndex(); return (row >= firstVisibleIndex && row < lastVisibleIndex) || (row == lastVisibleIndex && bottomIndexShownCompletely); } /** * Shows the item. If the item is already showing in the receiver, this * method simply returns. Otherwise, the items are scrolled until the item * is visible. * * @param item the item to be shown */ public void showItem(int item) { checkWidget(); updateScrollbars(); // if no items are visible on screen then abort if (getVisibleGridHeight() < 1) { return; } // if its visible just return if (isShown(item)) { return; } int newTopIndex = item; if (newTopIndex >= getBottomIndex()) { RowRange range = getRowRange(newTopIndex, getVisibleGridHeight(), true, true); // note: inverse==true newTopIndex = range.startIndex; // note: use startIndex because of inverse==true } setTopIndex(newTopIndex); } /** * Shows the selection. If the selection is already showing in the receiver, * this method simply returns. Otherwise, the items are scrolled until the * selection is visible. * */ public void showSelection() { checkWidget(); if (scrollValuesObsolete) updateScrollbars(); if (selectedCells.isEmpty()) return; GridPos cell = selectedCells.iterator().next(); showItem(cell.row); showColumn(cell.col); } /** * Computes and sets the height of the header row. This method will ask for * the preferred size of all the column headers and use the max. */ private void computeHeaderSizes() { // Item height itemHeight = fontMetrics.getHeight() + 3; // Column header height int colHeaderHeight = 0; for (GridColumn column : topColumns) { colHeaderHeight = Math.max(column.getHeaderHeight(true), colHeaderHeight); } headerHeight = colHeaderHeight; // Row header width rowHeaderWidth = DEFAULT_ROW_HEADER_WIDTH; for (int i = 0; i < rowElements.length; i++) { Object row = rowElements[i]; GridNode parentNode = parentNodes[i]; GridNode nr = rowNodes.get(row); int width = rowHeaderRenderer.computeHeaderWidth( row, nr != null ? nr.level : parentNode == null ? 0 : parentNode.level + 1); rowHeaderWidth = Math.max(rowHeaderWidth, width); } if (rowHeaderWidth > MAX_ROW_HEADER_WIDTH) { rowHeaderWidth = MAX_ROW_HEADER_WIDTH; } } /** * Returns the x position of the given column. Takes into account scroll * position. * * @param column given column * @return x position */ private int getColumnHeaderXPosition(@NotNull GridColumn column) { int x = 0; x -= getHScrollSelectionInPixels(); if (rowHeaderVisible) { x += rowHeaderWidth; } column = column.getFirstLeaf(); for (GridColumn column2 : columns) { if (column2 == column) { break; } x += column2.getWidth(); } return x; } /** * Returns the hscroll selection in pixels. This method abstracts away the * differences between column by column scrolling and pixel based scrolling. * * @return the horizontal scroll selection in pixels */ private int getHScrollSelectionInPixels() { int selection = hScroll.getSelection(); if (columnScrolling) { int pixels = 0; for (int i = 0; i < selection && i < columns.size(); i++) { pixels += columns.get(i).getWidth(); } selection = pixels; } return selection; } /** * Returns the size of the preferred size of the inner table. * * @return the preferred size of the table. */ private Point getTableSize() { int x = 0; int y = 0; if (columnHeadersVisible) { y += headerHeight; } y += getGridHeight(); if (rowHeaderVisible) { x += rowHeaderWidth; } for (GridColumn column : columns) { x += column.getWidth(); } return new Point(x, y); } /** * Sets the new width of the column being resized and fires the appropriate * listeners. * * @param x mouse x */ private void handleColumnResizerDragging(int x) { int newWidth = resizingColumnStartWidth + (x - resizingStartX); if (newWidth < MIN_COLUMN_HEADER_WIDTH) { newWidth = MIN_COLUMN_HEADER_WIDTH; } Rectangle clientArea = getClientArea(); if (columnScrolling) { int maxWidth = clientArea.width; if (rowHeaderVisible) maxWidth -= rowHeaderWidth; if (newWidth > maxWidth) newWidth = maxWidth; } if (newWidth == columnBeingResized.getWidth()) { return; } columnBeingResized.setWidth(newWidth, false); scrollValuesObsolete = true; redraw(clientArea.x, clientArea.y, clientArea.width, clientArea.height, false); } /** * Determines if the mouse is hovering on a column resizer and changes the * pointer and sets field appropriately. * Also checks if mouse if hovering on a column sorter control. * * @param x mouse x * @param y mouse y */ private void handleHoverOnColumnHeader(int x, int y) { boolean overSorter = false, overResizer = false; hoveringOnHeader = false; if (y <= headerHeight) { int x2 = 0; if (rowHeaderVisible) { x2 += rowHeaderWidth; } x2 -= getHScrollSelectionInPixels(); if (x < x2) { int ltSort = getContentProvider().getSortOrder(null); if (ltSort != SWT.NONE && x > x2 - GridColumnRenderer.SORT_WIDTH - GridColumnRenderer.ARROW_MARGIN && x < x2 - GridColumnRenderer.ARROW_MARGIN && y > GridColumnRenderer.TOP_MARGIN) { columnBeingSorted = null; overSorter = true; } } else { if (x > getRowHeaderWidth()) { for (GridColumn column : columns) { if (x >= x2 && x <= x2 + column.getWidth()) { hoveringOnHeader = true; if (column.isOverSortArrow(x - x2, y)) { overSorter = true; columnBeingSorted = column; break; } x2 += column.getWidth(); if (x2 >= (x - COLUMN_RESIZER_THRESHOLD) && x2 <= (x + COLUMN_RESIZER_THRESHOLD)) { overResizer = true; columnBeingResized = column; break; } } else { x2 += column.getWidth(); } } } } // Redraw header // GC gc = new GC(this); // try { // paintHeader(gc); // } catch (Exception e) { // gc.dispose(); // } } else if (x <= rowHeaderWidth) { // Hover in row header } if (overSorter != hoveringOnColumnSorter) { if (overSorter) { setCursor(sortCursor); } else { columnBeingSorted = null; setCursor(null); } hoveringOnColumnSorter = overSorter; } if (overResizer != hoveringOnColumnResizer) { if (overResizer) { setCursor(getDisplay().getSystemCursor(SWT.CURSOR_SIZEWE)); } else { columnBeingResized = null; if (!hoveringOnColumnSorter) { setCursor(null); } } hoveringOnColumnResizer = overResizer; } } /** * Returns the cell at the given point in the receiver or null if no such * cell exists. The point is in the coordinate system of the receiver. * * @param point the point used to locate the item * @return the cell at the given point */ @Nullable public GridPos getCell(Point point) { checkWidget(); if (point == null) { SWT.error(SWT.ERROR_NULL_ARGUMENT); return null; } if (point.x < 0 || point.x > getClientArea().width) return null; int item = getRow(point); GridColumn column = getColumn(point); if (item >= 0 && column != null) { return new GridPos(indexOf(column), item); } else { return null; } } /** * Paints. * * @param e paint event */ private void onPaint(@NotNull PaintEvent e) { final GC gc = e.gc; gc.setBackground(getBackground()); //this.drawBackground(gc, 0, 0, getSize().x, getSize().y); if (scrollValuesObsolete) { updateScrollbars(); scrollValuesObsolete = false; } int y = 0; if (columnHeadersVisible) { paintHeader(gc); y += headerHeight; } final Rectangle clientArea = getClientArea(); int availableHeight = clientArea.height - y; int visibleRows = availableHeight / getItemHeight() + 1; if (getItemCount() > 0 && availableHeight > 0) { RowRange range = getRowRange(getTopIndex(), availableHeight, false, false); if (range.height >= availableHeight) visibleRows = range.rows; else visibleRows = range.rows + (availableHeight - range.height) / getItemHeight() + 1; } int firstVisibleIndex = getTopIndex(); int row = firstVisibleIndex; final int hScrollSelectionInPixels = getHScrollSelectionInPixels(); final GridPos testPos = new GridPos(-1, -1); final Rectangle cellBounds = new Rectangle(0, 0, 0, 0); for (int i = 0; i < visibleRows; i++) { int x = 0; x -= hScrollSelectionInPixels; // get the item to draw if (row >= 0 && row < getItemCount()) { boolean cellInRowSelected = selectedRows.containsKey(row); if (rowHeaderVisible) { // row header is actually painted later x += rowHeaderWidth; } // draw regular cells for each column for (int k = 0, columnsSize = columns.size(); k < columnsSize; k++) { GridColumn column = columns.get(k); int width = column.getWidth(); if (x + width >= 0 && x < clientArea.width) { cellBounds.x = x; cellBounds.y = y; cellBounds.width = width; cellBounds.height = getItemHeight(); testPos.col = k; testPos.row = row; cellRenderer.paint( gc, cellBounds, selectedCells.contains(testPos), focusItem == row && focusColumn == column, column.getElement(), rowElements[row]); //gc.setClipping((Rectangle) null); } x += column.getWidth(); } if (x < clientArea.width) { drawEmptyCell(gc, x, y, clientArea.width - x + 1, getItemHeight()); } x = 0; GridNode rowNode = this.rowNodes.get(rowElements[row]); GridNode parentNode = this.parentNodes[row]; if (rowHeaderVisible) { if (y >= headerHeight) { cellBounds.x = 0; cellBounds.y = y; cellBounds.width = rowHeaderWidth; cellBounds.height = getItemHeight() + 1; gc.setClipping(cellBounds); try { rowHeaderRenderer.paint( gc, cellBounds, cellInRowSelected, parentNode == null ? 0 : parentNode.level, rowNode == null ? IGridContentProvider.ElementState.NONE : rowNode.state, rowElements[row]); } finally { gc.setClipping((Rectangle)null); } } x += rowHeaderWidth; } y += getItemHeight() + 1; } else { if (rowHeaderVisible) { //row header is actually painted later x += rowHeaderWidth; } for (GridColumn column : columns) { drawEmptyCell(gc, x, y, column.getWidth(), getItemHeight()); x += column.getWidth(); } if (x < clientArea.width) { drawEmptyCell(gc, x, y, clientArea.width - x + 1, getItemHeight()); } x = 0; if (rowHeaderVisible) { drawEmptyRowHeader(gc, x, y, rowHeaderWidth, getItemHeight() + 1); x += rowHeaderWidth; } y += getItemHeight() + 1; } row++; } } /** * Paints the header. * * @param gc gc from paint event */ private void paintHeader(@NotNull GC gc) { int x = 0; int y; x -= getHScrollSelectionInPixels(); if (rowHeaderVisible) { // skip left corner x += rowHeaderWidth; } final Rectangle clientArea = getClientArea(); for (int i = 0, columnsSize = topColumns.size(); i < columnsSize; i++) { GridColumn column = topColumns.get(i); if (x > clientArea.width) break; int columnHeight = column.getHeaderHeight(false); y = 0; if (x + column.getWidth() >= 0) { paintColumnsHeader(gc, column, x, y, columnHeight, 0); } x += column.getWidth(); } if (x < clientArea.width) { drawEmptyColumnHeader(gc, x, 0, clientArea.width - x, headerHeight); } x = 0; if (rowHeaderVisible) { // paint left corner drawTopLeftCell(gc, 0, 0, rowHeaderWidth, headerHeight); x += rowHeaderWidth; } } private void paintColumnsHeader(GC gc, @NotNull GridColumn column, int x, int y, int columnHeight, int level) { List<GridColumn> children = column.getChildren(); int paintHeight = columnHeight; if (CommonUtils.isEmpty(children)) { paintHeight = columnHeight * (maxColumnDepth - level + 1); } Rectangle bounds = new Rectangle(x, y, column.getWidth(), paintHeight); boolean hover = hoveringOnHeader && hoveringColumn == column; columnHeaderRenderer.paint(gc, bounds, selectedColumns.contains(column), hover, column.getElement()); if (!CommonUtils.isEmpty(children)) { // Draw child columns level++; int childX = x; for (GridColumn child : children) { paintColumnsHeader(gc, child, childX, y + columnHeight, columnHeight, level); childX += child.getWidth(); } } } /** * Manages the state of the scrollbars when new items are added or the * bounds are changed. */ public void updateScrollbars() { Point preferredSize = getTableSize(); Rectangle clientArea = getClientArea(); // First, figure out if the scrollbars should be visible and turn them // on right away // this will allow the computations further down to accommodate the // correct client // area // Turn the scrollbars on if necessary and do it all over again if // necessary. This ensures // that if a scrollbar is turned on/off, the other scrollbars // visibility may be affected (more // area may have been added/removed. for (int doublePass = 1; doublePass <= 2; doublePass++) { if (preferredSize.y > clientArea.height) { vScroll.setVisible(true); } else { vScroll.setVisible(false); vScroll.setValues(0, 0, 1, 1, 1, 1); } if (preferredSize.x > clientArea.width) { hScroll.setVisible(true); } else { hScroll.setVisible(false); hScroll.setValues(0, 0, 1, 1, 1, 1); } // get the clientArea again with the now visible/invisible // scrollbars clientArea = getClientArea(); } // if the scrollbar is visible set its values if (vScroll.getVisible()) { int max = getItemCount(); int thumb = (getVisibleGridHeight() + 1) / (getItemHeight() + 1); // if possible, remember selection, if selection is too large, just // make it the max you can int selection = Math.min(vScroll.getSelection(), max); vScroll.setValues(selection, 0, max, thumb, 1, thumb); } // if the scrollbar is visible set its values if (hScroll.getVisible()) { if (!columnScrolling) { // horizontal scrolling works pixel by pixel int hiddenArea = preferredSize.x - clientArea.width + 1; // if possible, remember selection, if selection is too large, // just // make it the max you can int selection = Math.min(hScroll.getSelection(), hiddenArea - 1); hScroll.setValues(selection, 0, hiddenArea + clientArea.width - 1, clientArea.width, HORZ_SCROLL_INCREMENT, clientArea.width); } else { // horizontal scrolling is column by column int hiddenArea = preferredSize.x - clientArea.width + 1; int max = 0; int i = 0; while (hiddenArea > 0 && i < getColumnCount()) { GridColumn col = columns.get(i); i++; hiddenArea -= col.getWidth(); max++; } max++; // max should never be greater than the number of visible cols int visCols = columns.size(); max = Math.min(visCols, max); // if possible, remember selection, if selection is too large, // just // make it the max you can int selection = Math.min(hScroll.getSelection(), max); hScroll.setValues(selection, 0, max, 1, 1, 1); } } } /** * Updates cell selection. * * @param newCell newly clicked, navigated to cell. * @param stateMask state mask during preceeding mouse or key event. * @param dragging true if the user is dragging. * @param reverseDuplicateSelections true if the user is reversing selection rather than adding to. * @return selection event that will need to be fired or null. */ @Nullable public Event updateCellSelection( @NotNull GridPos newCell, int stateMask, boolean dragging, boolean reverseDuplicateSelections, EventSource eventSource) { return updateCellSelection(Collections.singletonList(newCell), stateMask, dragging, reverseDuplicateSelections, eventSource); } /** * Updates cell selection. * * @param newCells newly clicked, navigated to cells. * @param stateMask state mask during preceeding mouse or key event. * @param dragging true if the user is dragging. * @param reverseDuplicateSelections true if the user is reversing selection rather than adding to. * @return selection event that will need to be fired or null. */ @Nullable private Event updateCellSelection( @NotNull List<GridPos> newCells, int stateMask, boolean dragging, boolean reverseDuplicateSelections, EventSource eventSource) { boolean shift = (stateMask & SWT.MOD2) == SWT.MOD2; boolean ctrl = (stateMask & SWT.MOD1) == SWT.MOD1; if (eventSource == EventSource.KEYBOARD) { ctrl = false; } if (!shift) { shiftSelectionAnchorColumn = null; shiftSelectionAnchorItem = -1; } List<GridPos> oldSelection = null; if (!shift && !ctrl) { if (newCells.size() == 1 && newCells.size() == selectedCells.size() && newCells.get(0).equals(selectedCells.iterator().next())) { return null; } selectedCells.clear(); for (GridPos newCell : newCells) { addToCellSelection(newCell); } } else if (shift) { GridPos newCell = newCells.get(0); //shift selection should only occur with one cell, ignoring others oldSelection = new ArrayList<>(selectedCells); if ((focusColumn == null) || (focusItem < 0)) { return null; } shiftSelectionAnchorColumn = getColumn(newCell.col); shiftSelectionAnchorItem = newCell.row; if (ctrl) { selectedCells.clear(); selectedCells.addAll(selectedCellsBeforeRangeSelect); } else { selectedCells.clear(); } GridColumn currentColumn = focusColumn; int currentItem = focusItem; GridColumn endColumn = getColumn(newCell.col); int endItem = newCell.row; Point newRange = getSelectionRange(currentItem, currentColumn, endItem, endColumn); currentColumn = getColumn(newRange.x); endColumn = getColumn(newRange.y); GridColumn startCol = currentColumn; if (currentItem > endItem) { int temp = currentItem; currentItem = endItem; endItem = temp; } boolean firstLoop = true; do { if (!firstLoop) { currentItem++; } firstLoop = false; boolean firstLoop2 = true; currentColumn = startCol; do { if (!firstLoop2) { int index = indexOf(currentColumn) + 1; if (index < columns.size()) { currentColumn = columns.get(index); } else { currentColumn = null; } if (currentColumn != null) if (indexOf(currentColumn) > indexOf(endColumn)) currentColumn = null; } firstLoop2 = false; if (currentColumn != null) { GridPos cell = new GridPos(indexOf(currentColumn), currentItem); addToCellSelection(cell); } } while (currentColumn != endColumn && currentColumn != null); } while (currentItem != endItem); if (selectedCells.equals(newCells)) { return null; } } else /*if (eventSource == EventSource.MOUSE)*/ { // Ctrl selection works only for mouse events boolean reverse = reverseDuplicateSelections; if (!selectedCells.containsAll(newCells)) reverse = false; if (dragging) { selectedCells.clear(); selectedCells.addAll(selectedCellsBeforeRangeSelect); } if (reverse) { selectedCells.removeAll(newCells); } else { for (GridPos newCell : newCells) { addToCellSelection(newCell); } } } if (oldSelection != null && oldSelection.size() == selectedCells.size() && selectedCells.containsAll(oldSelection)) { return null; } updateSelectionCache(); Event e = new Event(); if (dragging) { e.detail = SWT.DRAG; followupCellSelectionEventOwed = true; } Rectangle clientArea = getClientArea(); redraw(clientArea.x, clientArea.y, clientArea.width, clientArea.height, false); return e; } private boolean addToCellSelection(GridPos newCell) { if (newCell.col < 0 || newCell.col >= columns.size()) return false; if (newCell.row < 0 || newCell.row >= getItemCount()) return false; return selectedCells.add(newCell); } void updateSelectionCache() { //Update the list of which columns have all their cells selected selectedColumns.clear(); selectedRows.clear(); IntKeyMap<Boolean> columnIndices = new IntKeyMap<>(); for (GridPos cell : selectedCells) { columnIndices.put(cell.col, Boolean.TRUE); selectedRows.put(cell.row, Boolean.TRUE); } for (Integer columnIndex : columnIndices.keySet()) { selectedColumns.add(columns.get(columnIndex)); } Collections.sort(selectedColumns, new Comparator<GridColumn>() { @Override public int compare(GridColumn o1, GridColumn o2) { return o1.getIndex() - o2.getIndex(); } }); } /** * Initialize all listeners. */ private void initListeners() { disposeListener = new Listener() { @Override public void handleEvent(Event e) { onDispose(e); } }; addListener(SWT.Dispose, disposeListener); addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent e) { onPaint(e); } }); addListener(SWT.Resize, new Listener() { @Override public void handleEvent(Event e) { onResize(); } }); if (getVerticalBar() != null) { getVerticalBar().addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { onScrollSelection(); } }); } if (getHorizontalBar() != null) { getHorizontalBar().addListener(SWT.Selection, new Listener() { @Override public void handleEvent(Event e) { onScrollSelection(); } }); } addListener(SWT.KeyDown, new Listener() { @Override public void handleEvent(Event e) { onKeyDown(e); } }); addTraverseListener(new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { e.doit = true; } }); addMouseListener(new MouseListener() { @Override public void mouseDoubleClick(MouseEvent e) { onMouseDoubleClick(e); } @Override public void mouseDown(MouseEvent e) { onMouseDown(e); } @Override public void mouseUp(MouseEvent e) { onMouseUp(e); } }); addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent e) { onMouseMove(e); } }); addMouseTrackListener(new MouseTrackListener() { @Override public void mouseEnter(MouseEvent e) { } @Override public void mouseExit(MouseEvent e) { onMouseExit(e); } @Override public void mouseHover(MouseEvent e) { } }); addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { onFocusIn(); redraw(); } @Override public void focusLost(FocusEvent e) { redraw(); } }); // Special code to reflect mouse wheel events if using an external // scroller addListener(SWT.MouseWheel, new Listener() { @Override public void handleEvent(Event e) { onMouseWheel(e); } }); } private void onFocusIn() { if (getItemCount() > 0 && focusItem < 0) { focusItem = 0; } } private void onDispose(Event event) { removeAll(); //We only want to dispose of our items and such *after* anybody else who may have been //listening to the dispose has had a chance to do whatever. removeListener(SWT.Dispose, disposeListener); notifyListeners(SWT.Dispose, event); event.type = SWT.None; UIUtils.dispose(cellHeaderSelectionBackground); } /** * Mouse wheel event handler. * * @param e event */ private void onMouseWheel(Event e) { if (vScroll.getVisible()) { vScroll.handleMouseWheel(e); if (getVerticalBar() == null) e.doit = false; } else if (hScroll.getVisible()) { hScroll.handleMouseWheel(e); if (getHorizontalBar() == null) e.doit = false; } } /** * Mouse down event handler. * * @param e event */ private void onMouseDown(MouseEvent e) { // for some reason, SWT prefers the children to get focus if // there are any children // the setFocus method on Composite will not set focus to the // Composite if one of its // children can get focus instead. This only affects the grid // when an editor is open // and therefore the grid has a child. The solution is to // forceFocus() if ((getStyle() & SWT.NO_FOCUS) != SWT.NO_FOCUS) { forceFocus(); } //if populated will be fired at end of method. Event selectionEvent = null; cellSelectedOnLastMouseDown = false; cellRowSelectedOnLastMouseDown = false; cellColumnSelectedOnLastMouseDown = false; if (hoveringOnColumnSorter) { handleHoverOnColumnHeader(e.x, e.y); if (hoveringOnColumnSorter) { return; } } if (hoveringOnColumnResizer) { if (e.button == 1) { resizingColumn = true; resizingStartX = e.x; resizingColumnStartWidth = columnBeingResized.getWidth(); } return; } Point point = new Point(e.x, e.y); int row = getRow(point); if (isListening(SWT.DragDetect)) { if (hoveringOnSelectionDragArea) { if (dragDetect(e)) { return; } } } GridColumn col = null; if (row >= 0) { col = getColumn(point); boolean isSelectedCell = false; if (col != null) { isSelectedCell = selectedCells.contains(new GridPos(col.getIndex(), row)); } if (col == null && rowHeaderVisible && e.x <= rowHeaderWidth) { boolean shift = ((e.stateMask & SWT.MOD2) != 0); boolean ctrl = false; if (!shift) { ctrl = ((e.stateMask & SWT.MOD1) != 0); } if (e.button == 1 && !shift && !ctrl) { GridNode node = rowNodes.get(rowElements[row]); GridNode parentNode = parentNodes[row]; if (node != null && node.state != IGridContentProvider.ElementState.NONE) { if (GridRowRenderer.isOverExpander(e.x, parentNode == null ? 0 : parentNode.level)) { toggleRowState(row); return; } } } List<GridPos> cells = new ArrayList<>(); if (shift) { getCells(row, focusItem, cells); } else { getCells(row, cells); } int newStateMask = SWT.NONE; if (ctrl) newStateMask = SWT.MOD1; selectionEvent = updateCellSelection(cells, newStateMask, shift, ctrl, EventSource.MOUSE); cellRowSelectedOnLastMouseDown = (getCellSelectionCount() > 0); if (!shift) { //set focus back to the first visible column focusColumn = getColumn(new Point(rowHeaderWidth + 1, e.y)); focusItem = row; } showItem(row); redraw(); //return; } else if (e.button == 1 || (e.button == 3 && col != null && !isSelectedCell)) { if (col != null) { selectionEvent = updateCellSelection(new GridPos(col.getIndex(), row), e.stateMask, false, true, EventSource.MOUSE); cellSelectedOnLastMouseDown = (getCellSelectionCount() > 0); if (e.stateMask != SWT.MOD2) { focusColumn = col; focusItem = row; } //showColumn(col); showItem(row); redraw(); } } else { return; } } else if (e.button == 1 && rowHeaderVisible && e.x <= rowHeaderWidth && e.y < headerHeight) { // Nothing to select if (getItemCount() == 0) { return; } //click on the top left corner means select everything selectionEvent = selectAllCellsInternal(e.stateMask); focusColumn = getColumn(new Point(rowHeaderWidth + 1, 1)); focusItem = getTopIndex(); } else if (e.button == 1 && columnHeadersVisible && e.y <= headerHeight) { //column cell selection col = getColumn(point); if (col == null) return; if (getItemCount() == 0) return; List<GridPos> cells = new ArrayList<>(); getCells(col, cells); selectionEvent = updateCellSelection(cells, e.stateMask, false, true, EventSource.MOUSE); cellColumnSelectedOnLastMouseDown = (getCellSelectionCount() > 0); if (getItemCount() > 0) { focusColumn = col; focusItem = 0; } showColumn(col); redraw(); } else { // Change focus column anyway GridColumn column = getColumn(point); if (column == null) { // Clicked on top-left cell or outside of grid return; } focusColumn = column; } if (selectionEvent != null) { selectionEvent.stateMask = e.stateMask; selectionEvent.button = e.button; selectionEvent.data = new GridCell(col == null ? null : col.getElement(), row < 0 ? null : rowElements[row]); selectionEvent.x = e.x; selectionEvent.y = e.y; notifyListeners(SWT.Selection, selectionEvent); } } private void toggleRowState(int row) { GridNode node = rowNodes.get(rowElements[row]); if (node == null || node.state == IGridContentProvider.ElementState.NONE) { log.error("Row [" + row + "] state can't be toggled"); return; } if (node.state == IGridContentProvider.ElementState.EXPANDED) { // Collapse node. Remove all elements with different parent int deleteTo; for (deleteTo = row + 1; deleteTo < rowElements.length; deleteTo++) { if (!node.isParentOf(parentNodes[deleteTo])) { break; } } rowElements = ArrayUtils.deleteArea(Object.class, rowElements, row + 1, deleteTo - 1); parentNodes = ArrayUtils.deleteArea(GridNode.class, parentNodes, row + 1, deleteTo - 1); node.state = IGridContentProvider.ElementState.COLLAPSED; } else { // Expand node List<Object> result = new ArrayList<>(); List<GridNode> parents = new ArrayList<>(); collectRows(result, parents, node, node.rows, node.level); rowElements = ArrayUtils.insertArea(Object.class, rowElements, row + 1, result.toArray()); parentNodes = ArrayUtils.insertArea(GridNode.class, parentNodes, row + 1, parents.toArray()); node.state = IGridContentProvider.ElementState.EXPANDED; } if (focusItem > row) { focusItem = row; } for (Iterator<GridPos> iter = selectedCells.iterator(); iter.hasNext(); ) { GridPos pos = iter.next(); if (pos.row > row) { iter.remove(); } } updateSelectionCache(); computeHeaderSizes(); this.scrollValuesObsolete = true; redraw(); } /** * Mouse double click event handler. * * @param e event */ private void onMouseDoubleClick(MouseEvent e) { if (e.button == 1) { if (hoveringOnColumnResizer) { columnBeingResized.pack(true); resizingColumn = false; handleHoverOnColumnHeader(e.x, e.y); redraw(); return; } Point point = new Point(e.x, e.y); int row = getRow(point); GridColumn col = getColumn(point); if (row >= 0) { if (col != null) { if (isListening(SWT.DefaultSelection)) { Event newEvent = new Event(); newEvent.data = new GridCell(col.getElement(), rowElements[row]); notifyListeners(SWT.DefaultSelection, newEvent); } } else { GridNode node = rowNodes.get(rowElements[row]); GridNode parentNode = parentNodes[row]; if (node != null && node.state != IGridContentProvider.ElementState.NONE) { if (!GridRowRenderer.isOverExpander(e.x, parentNode == null ? 0 : parentNode.level)) { toggleRowState(row); } } } } } } /** * Mouse up handler. * * @param e event */ private void onMouseUp(MouseEvent e) { if (cellSelectedOnLastMouseDown && focusColumn != null && focusItem >= 0) { if (e.button == 1 && cellRenderer.isOverLink(focusColumn, focusItem, e.x, e.y)) { // Navigate link Event event = new Event(); event.x = e.x; event.y = e.y; event.stateMask = e.stateMask; event.data = new GridCell(focusColumn.getElement(), rowElements[focusItem]); notifyListeners(Event_NavigateLink, event); return; } } cellSelectedOnLastMouseDown = false; if (hoveringOnColumnSorter) { handleHoverOnColumnHeader(e.x, e.y); if (hoveringOnColumnSorter) { if (e.button == 1) { Event event = new Event(); event.x = e.x; event.y = e.y; event.data = columnBeingSorted == null ? null : columnBeingSorted.getElement(); event.stateMask = e.stateMask; notifyListeners(Event_ChangeSort, event); return; } } } if (resizingColumn) { resizingColumn = false; handleHoverOnColumnHeader(e.x, e.y); // resets cursor if // necessary return; } if (cellDragSelectionOccurring || cellRowDragSelectionOccurring || cellColumnDragSelectionOccurring) { cellDragSelectionOccurring = false; cellRowDragSelectionOccurring = false; cellColumnDragSelectionOccurring = false; setCursor(null); if (followupCellSelectionEventOwed) { Event se = new Event(); se.button = e.button; Point point = new Point(e.x, e.y); GridColumn column = getColumn(point); int rowIndex = getRow(point); if (column != null && rowIndex >= 0) { se.data = new GridCell(column.getElement(), rowElements[rowIndex]); } se.stateMask = e.stateMask; se.x = e.x; se.y = e.y; se.detail = SWT.DROP_DOWN; notifyListeners(SWT.Selection, se); followupCellSelectionEventOwed = false; } } } /** * Mouse move event handler. * * @param e event */ private void onMouseMove(MouseEvent e) { //if populated will be fired at end of method. Event selectionEvent = null; if ((e.stateMask & SWT.BUTTON1) == 0) { handleHovering(e.x, e.y); } else { if (resizingColumn) { handleColumnResizerDragging(e.x); return; } { if (!cellDragSelectionOccurring && cellSelectedOnLastMouseDown) { cellDragSelectionOccurring = true; //XXX: make this user definable setCursor(getDisplay().getSystemCursor(SWT.CURSOR_CROSS)); cellDragCTRL = ((e.stateMask & SWT.MOD1) != 0); if (cellDragCTRL) { selectedCellsBeforeRangeSelect.clear(); selectedCellsBeforeRangeSelect.addAll(selectedCells); } } if (!cellRowDragSelectionOccurring && cellRowSelectedOnLastMouseDown) { cellRowDragSelectionOccurring = true; setCursor(getDisplay().getSystemCursor(SWT.CURSOR_CROSS)); cellDragCTRL = ((e.stateMask & SWT.MOD1) != 0); if (cellDragCTRL) { selectedCellsBeforeRangeSelect.clear(); selectedCellsBeforeRangeSelect.addAll(selectedCells); } } if (!cellColumnDragSelectionOccurring && cellColumnSelectedOnLastMouseDown) { cellColumnDragSelectionOccurring = true; setCursor(getDisplay().getSystemCursor(SWT.CURSOR_CROSS)); cellDragCTRL = ((e.stateMask & SWT.MOD1) != 0); if (cellDragCTRL) { selectedCellsBeforeRangeSelect.clear(); selectedCellsBeforeRangeSelect.addAll(selectedCells); } } int ctrlFlag = (cellDragCTRL ? SWT.MOD1 : SWT.NONE); if (cellDragSelectionOccurring && handleCellHover(e.x, e.y)) { GridColumn intentColumn = hoveringColumn; int intentItem = hoveringItem; if (hoveringItem < 0) { if (e.y > headerHeight) { //then we must be hovering way to the bottom intentItem = Math.min(getItemCount() - 1, getBottomIndex() + 1); } else { intentItem = Math.max(0, getTopIndex() - 1); } } if (hoveringColumn == null) { if (e.x > rowHeaderWidth) { //then we must be hovering way to the right intentColumn = columns.get(columns.size() - 1); } else { intentColumn = columns.get(0); } } showColumn(intentColumn); showItem(intentItem); GridPos newCell = new GridPos(intentColumn.getIndex(), intentItem); selectionEvent = updateCellSelection(newCell, ctrlFlag | SWT.MOD2, true, false, EventSource.MOUSE); } if (cellRowDragSelectionOccurring && handleCellHover(e.x, e.y)) { int intentItem = hoveringItem; if (hoveringItem < 0) { if (e.y > headerHeight) { //then we must be hovering way to the bottom intentItem = getTopIndex() + 1; } else { if (getTopIndex() > 0) { intentItem = getTopIndex() - 1; } else { intentItem = 0; } } } List<GridPos> cells = new ArrayList<>(); getCells(intentItem, focusItem, cells); showItem(intentItem); selectionEvent = updateCellSelection(cells, ctrlFlag, true, false, EventSource.MOUSE); } if (cellColumnDragSelectionOccurring && handleCellHover(e.x, e.y)) { GridColumn intentCol = hoveringColumn; if (intentCol == null) return; //temporary GridColumn iterCol = intentCol; List<GridPos> newSelected = new ArrayList<>(); boolean decreasing = (indexOf(iterCol) > indexOf(focusColumn)); while (iterCol != null) { getCells(iterCol, newSelected); if (iterCol == focusColumn) { break; } if (decreasing) { iterCol = getPreviousVisibleColumn(iterCol); } else { iterCol = getNextVisibleColumn(iterCol); } } selectionEvent = updateCellSelection(newSelected, ctrlFlag, true, false, EventSource.MOUSE); } } } if (selectionEvent != null) { selectionEvent.stateMask = e.stateMask; selectionEvent.button = e.button; Point point = new Point(e.x, e.y); GridColumn column = getColumn(point); int rowIndex = getRow(point); if (column != null && rowIndex >= 0) { selectionEvent.data = new GridCell(column.getElement(), rowElements[rowIndex]); } selectionEvent.x = e.x; selectionEvent.y = e.y; notifyListeners(SWT.Selection, selectionEvent); } } /** * Handles the assignment of the correct values to the hover* field * variables that let the painting code now what to paint as hovered. * * @param x mouse x coordinate * @param y mouse y coordinate */ private void handleHovering(int x, int y) { handleCellHover(x, y); if (columnHeadersVisible) { handleHoverOnColumnHeader(x, y); } } /** * Refreshes the hover* variables according to the mouse location and * current state of the table. This is useful is some method call, caused * the state of the table to change and therefore the hover effects may have * become out of date. */ protected void refreshHoverState() { Point p = getDisplay().map(null, this, getDisplay().getCursorLocation()); handleHovering(p.x, p.y); } /** * Mouse exit event handler. * * @param e event */ private void onMouseExit(MouseEvent e) { hoveringItem = -1; hoveringDetail = null; hoveringColumn = null; redraw(); } /** * Key down event handler. * * @param e event */ public void onKeyDown(Event e) { if (focusColumn == null) { if (columns.size() == 0) return; focusColumn = getColumn(0); } if (e.character == '\r' && focusItem >= 0 && focusItem < rowElements.length) { Event newEvent = new Event(); newEvent.data = new GridCell(focusColumn.getElement(), rowElements[focusItem]); notifyListeners(SWT.DefaultSelection, newEvent); return; } int newSelection = -1; GridColumn newColumnFocus = null; //These two variables are used because the key navigation when the shift key is down is //based, not off the focus item/column, but rather off the implied focus (i.e. where the //keyboard has extended focus to). int impliedFocusItem = focusItem; GridColumn impliedFocusColumn = focusColumn; boolean ctrlPressed = ((e.stateMask & SWT.MOD1) != 0); boolean shiftPressed = ((e.stateMask & SWT.MOD2) != 0); //if (shiftPressed) { if (shiftSelectionAnchorColumn != null) { impliedFocusItem = shiftSelectionAnchorItem; impliedFocusColumn = shiftSelectionAnchorColumn; } //} switch (e.keyCode) { case SWT.ARROW_RIGHT: { if (impliedFocusItem >= 0) { newSelection = impliedFocusItem; int index = indexOf(impliedFocusColumn) + 1; if (index < columns.size()) { newColumnFocus = columns.get(index); } else { newColumnFocus = impliedFocusColumn; } } } break; case SWT.ARROW_LEFT: { if (impliedFocusItem >= 0) { newSelection = impliedFocusItem; int index = indexOf(impliedFocusColumn); if (index > 0) { newColumnFocus = columns.get(index - 1); } else { newColumnFocus = impliedFocusColumn; } } } break; case SWT.ARROW_UP: if (impliedFocusItem >= 0) { newSelection = getPreviousVisibleItem(impliedFocusItem); } newColumnFocus = impliedFocusColumn; break; case SWT.ARROW_DOWN: if (impliedFocusItem >= 0) { newSelection = getNextVisibleItem(impliedFocusItem); } else { if (getItemCount() > 0) { newSelection = 0; } } newColumnFocus = impliedFocusColumn; break; case SWT.HOME: if (ctrlPressed || columns.size() == 1) { newSelection = 0; } else { newSelection = impliedFocusItem; } newColumnFocus = columns.get(0); break; case SWT.END: { if ((ctrlPressed || columns.size() == 1) && getItemCount() > 0) { newSelection = getItemCount() - 1; } else { newSelection = impliedFocusItem; } newColumnFocus = columns.get(columns.size() - 1); } break; case SWT.PAGE_UP: int topIndex = getTopIndex(); newSelection = topIndex; if ((impliedFocusItem >= 0 && impliedFocusItem == topIndex) || focusItem == topIndex) { RowRange range = getRowRange(getTopIndex(), getVisibleGridHeight(), false, true); newSelection = range.startIndex; } newColumnFocus = impliedFocusColumn; //newColumnFocus = focusColumn; break; case SWT.PAGE_DOWN: int bottomIndex = getBottomIndex(); newSelection = bottomIndex; if (!isShown(bottomIndex)) { // the item at bottom index is not shown completely int tmpItem = getPreviousVisibleItem(newSelection); if (tmpItem >= 0) newSelection = tmpItem; } if ((impliedFocusItem >= 0 && impliedFocusItem >= bottomIndex - 1) || focusItem == bottomIndex - 1) { RowRange range = getRowRange(getBottomIndex(), getVisibleGridHeight(), true, false); newSelection = range.endIndex; } newColumnFocus = impliedFocusColumn; //newColumnFocus = focusColumn; break; case '+': case '-': case '=': case SWT.KEYPAD_ADD: case SWT.KEYPAD_SUBTRACT: if (focusItem >= 0) { GridNode node = rowNodes.get(rowElements[focusItem]); if (node != null) { boolean isPlus = (e.keyCode == '+' || e.keyCode == '=' || e.keyCode == SWT.KEYPAD_ADD); if ((node.state == IGridContentProvider.ElementState.EXPANDED && !isPlus) || (node.state == IGridContentProvider.ElementState.COLLAPSED && isPlus)) { toggleRowState(focusItem); } } } break; default: break; } if (newSelection < 0) { return; } if (newColumnFocus != null) { //if (e.stateMask != SWT.MOD1) { Event selEvent = updateCellSelection( new GridPos(newColumnFocus.getIndex(), newSelection), e.stateMask, false, false, EventSource.KEYBOARD); //} if (!shiftPressed) focusColumn = newColumnFocus; showColumn(newColumnFocus); if (!shiftPressed) { if (newSelection < 0) { focusItem = -1; } else { focusItem = newSelection; } } showItem(newSelection); GridCell newPos; if (newSelection >= 0 && newSelection < rowElements.length) { newPos = new GridCell(newColumnFocus.getElement(), rowElements[newSelection]); } else { newPos = null; } if (selEvent != null) { selEvent.stateMask = e.stateMask; selEvent.character = e.character; selEvent.keyCode = e.keyCode; selEvent.data = newPos; notifyListeners(SWT.Selection, selEvent); } redraw(); } } /** * Resize event handler. */ private void onResize() { //CGross 1/2/08 - I don't really want to be doing this.... //I shouldn't be changing something you user configured... //leaving out for now // if (columnScrolling) // { // int maxWidth = getClientArea().width; // if (rowHeaderVisible) // maxWidth -= rowHeaderWidth; // // for (Iterator cols = columns.iterator(); cols.hasNext();) { // GridColumn col = (GridColumn) cols.next(); // if (col.getWidth() > maxWidth) // col.setWidth(maxWidth); // } // } scrollValuesObsolete = true; topIndex = -1; bottomIndex = -1; } /** * Scrollbar selection event handler. */ private void onScrollSelection() { topIndex = -1; bottomIndex = -1; refreshHoverState(); final Rectangle clientArea = getClientArea(); redraw(clientArea.x, clientArea.y, clientArea.width, clientArea.height, false); } /** * Returns the intersection of the given column and given item. * * @param column column * @param item item * @return x,y of top left corner of the cell */ Point getOrigin(GridColumn column, int item) { int x = 0; if (rowHeaderVisible) { x += rowHeaderWidth; } x -= getHScrollSelectionInPixels(); for (int i = 0; i < columns.size(); i++) { GridColumn colIter = columns.get(i); if (colIter == column) { break; } x += colIter.getWidth(); } int y = 0; if (item >= 0) { if (columnHeadersVisible) { y += headerHeight; } int currIndex = getTopIndex(); if (item == -1) { SWT.error(SWT.ERROR_INVALID_ARGUMENT); } while (currIndex != item) { if (currIndex < item) { y += getItemHeight() + 1; currIndex++; } else if (currIndex > item) { currIndex--; y -= getItemHeight() + 1; } } } else if (columnHeadersVisible && column.getParent() != null) { for (GridColumn parent = column.getParent(); parent != null; parent = parent.getParent()) { y += parent.getHeaderHeight(false); } } return new Point(x, y); } /** * Sets the hovering variables (hoverItem,hoveringColumn) as well as * hoverDetail by talking to the cell renderers. Triggers a redraw if * necessary. * * @param x mouse x * @param y mouse y * @return true if a new section of the table is now being hovered */ private boolean handleCellHover(int x, int y) { Point point = new Point(x, y); final GridColumn col = getColumn(point); final int row = getRow(point); Integer detail = y; boolean hoverChange = false; if (hoveringItem != row || !CommonUtils.equalObjects(hoveringDetail, detail) || hoveringColumn != col) { hoveringItem = row; hoveringDetail = detail; hoveringColumn = col; hoverChange = true; } // Check for link boolean overLink = false; if (col != null && row >= 0) { if (cellRenderer.isOverLink(col, row, x, y)) { overLink = true; } } if (overLink) { if (!hoveringOnLink) { setCursor(sortCursor); } } else if (hoveringOnLink) { setCursor(null); } hoveringOnLink = overLink; //do normal cell specific tooltip stuff if (hoverChange) { // Check tooltip String newTip = null; if ((hoveringItem >= 0) && (hoveringColumn != null)) { // get cell specific tooltip newTip = getCellToolTip(hoveringColumn, hoveringItem); } else if (columnHeadersVisible && hoveringColumn != null && y <= headerHeight) { // get column header specific tooltip newTip = hoveringColumn.getHeaderTooltip(); } else if (rowHeaderVisible && hoveringItem >= 0 && x <= rowHeaderWidth) { newTip = getLabelProvider().getToolTipText(getRowElement(hoveringItem)); } //Avoid unnecessarily resetting tooltip - this will cause the tooltip to jump around if (newTip != null && !newTip.equals(displayedToolTipText)) { updateToolTipText(newTip); } else if (newTip == null && displayedToolTipText != null) { updateToolTipText(null); } displayedToolTipText = newTip; } return hoverChange; } /** * Sets the tooltip for the whole Grid to the given text. This method is made available * for subclasses to override, when a subclass wants to display a different than the standard * SWT/OS tooltip. Generally, those subclasses would override this event and use this tooltip * text in their own tooltip or just override this method to prevent the SWT/OS tooltip from * displaying. * * @param text */ protected void updateToolTipText(@Nullable String text) { ToolTipHandler curHandler = this.toolTipHandler; if (!CommonUtils.equalObjects(prevToolTip, text)) { // New tooltip if (curHandler != null) { curHandler.cancel(); } prevToolTip = text; this.setToolTipText(""); this.toolTipHandler = new ToolTipHandler(); this.toolTipHandler.toolTip = text; this.toolTipHandler.schedule(500); } } /** * Marks the scroll values obsolete so they will be recalculated. */ protected void setScrollValuesObsolete() { this.scrollValuesObsolete = true; redraw(); } /** * Inserts a new column into the table. * * @param column new column * @param index index to insert new column * @return current number of columns */ void newColumn(GridColumn column, int index) { if (index == -1) { columns.add(column); } else { columns.add(index, column); } } public void recalculateSizes() { int oldHeaderHeight = headerHeight; computeHeaderSizes(); if (oldHeaderHeight != headerHeight) { scrollValuesObsolete = true; } } /** * Returns the current cell in focus. If cell selection is disabled, this method returns null. * * @return cell in focus or {@code null}. x represents the column and y the row the cell is in */ public GridPos getFocusPos() { checkWidget(); int x = -1; if (focusColumn != null) x = focusColumn.getIndex(); focusCell.col = x; focusCell.row = focusItem; return focusCell; } @Nullable public Object getFocusColumnElement() { return focusColumn == null ? null : focusColumn.getElement(); } @Nullable public Object getFocusRowElement() { if (focusItem < 0 || focusItem >= rowElements.length) { return null; } return rowElements[focusItem]; } @Nullable public GridCell getFocusCell() { return posToCell(getFocusPos()); } /** * Sets the focused item to the given item. * * @param item item to focus. */ public void setFocusItem(int item) { checkWidget(); focusItem = item; } /** * Sets the focused item to the given column. Column focus is only applicable when cell * selection is enabled. * * @param col column to focus. */ public void setFocusColumn(int col) { checkWidget(); GridColumn column = getColumn(col); if (column == null || column.getGrid() != this) { SWT.error(SWT.ERROR_INVALID_ARGUMENT); return; } focusColumn = column; } /** * Returns true if the table is set to horizontally scroll column-by-column * rather than pixel-by-pixel. * * @return true if the table is scrolled horizontally by column */ public boolean getColumnScrolling() { checkWidget(); return columnScrolling; } /** * Sets the table scrolling method to either scroll column-by-column (true) * or pixel-by-pixel (false). * * @param columnScrolling true to horizontally scroll by column, false to * scroll by pixel */ public void setColumnScrolling(boolean columnScrolling) { checkWidget(); if (rowHeaderVisible && !columnScrolling) { return; } this.columnScrolling = columnScrolling; scrollValuesObsolete = true; redraw(); } /** * Selects the given cell. Invalid cells are ignored. * * @param cell point whose x values is a column index and y value is an item index */ public void selectCell(@NotNull GridPos cell) { checkWidget(); addToCellSelection(cell); updateSelectionCache(); redraw(); } /** * Selects the given cells. Invalid cells are ignored. * * @param cells an array of points whose x value is a column index and y value is an item index */ public void selectCells(@NotNull Collection<GridPos> cells) { checkWidget(); for (GridPos cell : cells) { addToCellSelection(cell); } updateSelectionCache(); redraw(); } /** * Selects all cells in the receiver. */ public void selectAllCells() { checkWidget(); selectAllCellsInternal(0); } /** * Selects all cells in the receiver. * * @return An Event object */ @Nullable private Event selectAllCellsInternal(int stateMask) { if (columns.size() == 0) return null; if (getItemCount() == 0) return null; GridColumn oldFocusColumn = focusColumn; int oldFocusItem = focusItem; focusColumn = columns.get(0); focusItem = 0; List<GridPos> cells = new ArrayList<>(); getAllCells(cells); Event selectionEvent = updateCellSelection(cells, stateMask, false, true, EventSource.KEYBOARD); focusColumn = oldFocusColumn; focusItem = oldFocusItem; updateSelectionCache(); redraw(); return selectionEvent; } /** * Selects the selection to the given cell. The existing selection is cleared before * selecting the given cell. * * @param cell point whose x values is a column index and y value is an item index */ public void setCellSelection(@NotNull GridPos cell) { checkWidget(); if (!isValidCell(cell)) SWT.error(SWT.ERROR_INVALID_ARGUMENT); selectedCells.clear(); addToCellSelection(cell); updateSelectionCache(); redraw(); } /** * Returns an array of cells that are currently selected in the * receiver. The order of the items is unspecified. An empty array indicates * that no items are selected. * <p> * Note: This is not the actual structure used by the receiver to maintain * its selection, so modifying the array will not affect the receiver. * </p> * * @return an array representing the cell selection */ @NotNull public Collection<GridPos> getSelection() { if (isDisposed()) { return Collections.emptyList(); } return Collections.unmodifiableCollection(selectedCells); } public List<GridCell> getCellSelection() { if (isDisposed() || selectedCells.isEmpty()) { return Collections.emptyList(); } List<GridCell> cells = new ArrayList<>(selectedCells.size()); for (GridPos pos : selectedCells) { cells.add(posToCell(pos)); } return cells; } @NotNull public List<Object> getColumnSelection() { if (selectedColumns.isEmpty()) { return Collections.emptyList(); } List<Object> selection = new ArrayList<>(); for (GridColumn col : selectedColumns) { selection.add(col.getElement()); } return selection; } /** * Returns selected rows indexes * @return indexes of selected rows */ public Collection<Integer> getRowSelection() { return Collections.unmodifiableCollection(selectedRows.keySet()); } private void getCells(GridColumn col, List<GridPos> cells) { if (col.getChildren() != null) { // Get cells for all leafs for (int i = 0; i < columns.size(); i++) { if (columns.get(i).isParent(col)) { for (int k = 0; k < getItemCount(); k++) { cells.add(new GridPos(i, k)); } } } } else { int colIndex = col.getIndex(); for (int i = 0; i < getItemCount(); i++) { cells.add(new GridPos(colIndex, i)); } } } private void getCells(int row, List<GridPos> cells) { for (int i = 0; i < columns.size(); i++) { cells.add(new GridPos(i, row)); } } private void getAllCells(List<GridPos> cells) { for (int i = 0; i < getItemCount(); i++) { for (int k = 0; k < columns.size(); k++) { cells.add(new GridPos(k, i)); } } } private List<GridPos> getCells(int row) { List<GridPos> cells = new ArrayList<>(); getCells(row, cells); return cells; } private void getCells(int startRow, int endRow, List<GridPos> cells) { boolean descending = (startRow < endRow); int iterItem = endRow; do { getCells(iterItem, cells); if (iterItem == startRow) break; if (descending) { iterItem--; } else { iterItem++; } } while (true); } /** * Returns a point whose x and y values are the to and from column indexes of the new selection * range inclusive of all spanned columns. */ private Point getSelectionRange(int fromItem, GridColumn fromColumn, int toItem, GridColumn toColumn) { if (indexOf(fromColumn) > indexOf(toColumn)) { GridColumn temp = fromColumn; fromColumn = toColumn; toColumn = temp; } if (fromItem > toItem) { int temp = fromItem; fromItem = toItem; toItem = temp; } boolean firstTime = true; int iterItem = fromItem; int fromIndex = fromColumn.getIndex(); int toIndex = toColumn.getIndex(); do { if (!firstTime) { iterItem++; } else { firstTime = false; } Point cols = getRowSelectionRange(fromColumn, toColumn); //check and see if column spanning means that the range increased if (cols.x != fromIndex || cols.y != toIndex) { GridColumn newFrom = getColumn(cols.x); GridColumn newTo = getColumn(cols.y); //Unfortunately we have to start all over again from the top with the new range return getSelectionRange(fromItem, newFrom, toItem, newTo); } } while (iterItem != toItem); return new Point(fromColumn.getIndex(), toColumn.getIndex()); } /** * Returns a point whose x and y value are the to and from column indexes of the new selection * range inclusive of all spanned columns. * * @param fromColumn * @param toColumn * @return */ private Point getRowSelectionRange(GridColumn fromColumn, GridColumn toColumn) { int newFrom = fromColumn.getIndex(); int newTo = toColumn.getIndex(); return new Point(newFrom, newTo); } /** * Returns true if the given cell's x and y values are valid column and * item indexes respectively. * * @param cell * @return */ private boolean isValidCell(GridPos cell) { if (cell.col < 0 || cell.col >= columns.size()) return false; if (cell.row < 0 || cell.row >= getItemCount()) { return false; } // Valid return true; } @Override public void setFont(Font font) { super.setFont(font); sizingGC.setFont(font); fontMetrics = sizingGC.getFontMetrics(); normalFont = font; } /** * Determines if the mouse is hovering on the selection drag area and changes the * pointer and sets field appropriately. * <p/> * Note: The 'selection drag area' is that part of the selection, * on which a drag event can be initiated. This is either the border * of the selection (i.e. a cell border between a selected and a non-selected * cell) or the complete selection (i.e. anywhere on a selected cell). * * @param x * @param y * @return */ private boolean handleHoverOnSelectionDragArea(int x, int y) { boolean over = false; // Point inSelection = null; if ((!rowHeaderVisible || x > rowHeaderWidth - SELECTION_DRAG_BORDER_THRESHOLD) && (!columnHeadersVisible || y > headerHeight - SELECTION_DRAG_BORDER_THRESHOLD)) { // not on a header // drag area is the entire selection { Point p = new Point(x, y); GridPos cell = getCell(p); over = cell != null && isCellSelected(cell); } } if (over != hoveringOnSelectionDragArea) { hoveringOnSelectionDragArea = over; } return over; } public String getCellText(Object colElement, Object rowElement) { String text = getContentProvider().getCellText(colElement, rowElement); // Truncate too long texts (they are really bad for performance) if (text.length() > MAX_TOOLTIP_LENGTH) { text = text.substring(0, MAX_TOOLTIP_LENGTH) + " ..."; } return text; } @Nullable public String getCellToolTip(GridColumn col, int row) { String toolTip = getCellText(columnElements[col.getIndex()], rowElements[row]); if (toolTip == null) { return null; } // Show tooltip only if it's larger than column width Point ttSize = sizingGC.textExtent(toolTip); if (ttSize.x > col.getWidth() || ttSize.y > getItemHeight()) { int gridHeight = getBounds().height; if (ttSize.y > gridHeight) { // Too big tool tip - larger than entire grid // Lets chop it StringBuilder newToolTip = new StringBuilder(); StringTokenizer st = new StringTokenizer(toolTip, "'\n"); int maxLineNumbers = gridHeight / getItemHeight(), lineNumber = 0; while (st.hasMoreTokens()) { newToolTip.append(st.nextToken()).append('\n'); lineNumber++; if (lineNumber >= maxLineNumbers) { break; } } toolTip = newToolTip.toString(); } return toolTip; } else { return ""; } } @Nullable public DBPImage getCellImage(Object colElement, Object rowElement) { return getContentProvider().getCellImage(colElement, rowElement); } public Color getCellBackground(Object colElement, Object rowElement) { Color color = getContentProvider().getCellBackground(colElement, rowElement); return color != null ? color : getBackground(); } public Color getCellForeground(Object colElement, Object rowElement) { Color color = getContentProvider().getCellForeground(colElement, rowElement); return color != null ? color : getForeground(); } public Rectangle getCellBounds(int columnIndex, int rowIndex) { if (!isShown(rowIndex)) return new Rectangle(-1000, -1000, 0, 0); GridColumn column = getColumn(columnIndex); Point origin = getOrigin(column, rowIndex); if (origin.x < 0 && isRowHeaderVisible()) return new Rectangle(-1000, -1000, 0, 0); return new Rectangle(origin.x, origin.y, column.getWidth(), getItemHeight()); } void setDefaultBackground(GC gc) { Color background = getLabelProvider().getBackground(null); if (background != null) { gc.setBackground(background); } } private void drawEmptyColumnHeader(GC gc, int x, int y, int width, int height) { setDefaultBackground(gc); gc.fillRectangle( x, y, width + 1, height + 1); } private void drawEmptyRowHeader(GC gc, int x, int y, int width, int height) { gc.setBackground(rowHeaderRenderer.DEFAULT_BACKGROUND); gc.fillRectangle(x, y, width, height + 1); gc.setForeground(rowHeaderRenderer.DEFAULT_FOREGROUND); gc.drawLine( x + width - 1, y, x + width - 1, y + height - 1); gc.drawLine( x, y + height - 1, x + width - 1, y + height - 1); } public void drawEmptyCell(GC gc, int x, int y, int width, int height) { IGridLabelProvider labelProvider = getLabelProvider(); Color foreground = labelProvider.getForeground(null); setDefaultBackground(gc); gc.setForeground(foreground); gc.fillRectangle(x, y, width + 1, height); if (isLinesVisible()) { gc.setForeground(getLineColor()); gc.drawLine( x, y + height, x + width, y + height); gc.drawLine(x + width - 1, y, x + width - 1, y + height); } } private void drawTopLeftCell(GC gc, int x, int y, int width, int height) { int sortOrder = getContentProvider().getSortOrder(null); gc.setBackground(rowHeaderRenderer.DEFAULT_BACKGROUND); gc.fillRectangle( x, y, width - 1, height + 1); gc.setForeground(rowHeaderRenderer.DEFAULT_FOREGROUND); gc.drawLine( x + width - 1, y, x + width - 1, y + height); gc.drawLine( x, y + height - 1, x + width, y + height - 1); if (sortOrder != SWT.NONE) { int arrowWidth = GridColumnRenderer.SORT_WIDTH; Rectangle sortBounds = new Rectangle( x + width - GridColumnRenderer.ARROW_MARGIN - arrowWidth, y + GridColumnRenderer.TOP_MARGIN, arrowWidth, height); GridColumnRenderer.paintSort(gc, sortBounds, sortOrder); } } }