blob: f6d89011e42b54e3592634b4bfb8ac563507b27a [file] [log] [blame]
/*
* Copyright (C) 2005 David Orme <djo@coconut-palm-software.com>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* David Orme - Initial API and implementation
*/
package org.eclipse.jface.examples.databinding.compositetable;
import java.lang.reflect.Constructor;
import java.util.Iterator;
import java.util.LinkedList;
import org.eclipse.jface.examples.databinding.compositetable.internal.EmptyTablePlaceholder;
import org.eclipse.jface.examples.databinding.compositetable.internal.ISelectableRegionControl;
import org.eclipse.jface.examples.databinding.compositetable.internal.TableRow;
import org.eclipse.jface.examples.databinding.compositetable.reflect.DuckType;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Slider;
import org.eclipse.swt.widgets.Widget;
/** (non-API)
* Class InternalCompositeTable. This is the run-time CompositeTableControl. It gets its prototype
* row and (optional) header objects from its SWT parent, then uses them to implement an SWT
* virtual table control.
*
* @author djo
*/
public class InternalCompositeTable extends Composite implements Listener {
// The internal UI controls that make up this control.
private Composite sliderHolder = null;
private Composite controlHolder = null;
private Slider slider = null;
private EmptyTablePlaceholder emptyTablePlaceholder = null;
// My parent CompositeTable
private CompositeTable parent;
// Property fields
private int maxRowsVisible;
private int numRowsInDisplay;
private int numRowsInCollection;
private int topRow;
private int currentRow;
private int currentColumn;
// The visible/invisible row objects and bookeeping info about them
private int currentVisibleTopRow = 0;
private int numRowsVisible = 0;
private LinkedList rows = new LinkedList();
private LinkedList spareRows = new LinkedList();
// The prototype header/row objects and Constructors so we can duplicate them
private Constructor headerConstructor;
private Constructor rowConstructor;
private Control headerControl;
private Control myHeader = null;
private Control rowControl;
/**
* Constructor InternalCompositeTable. The usual SWT constructor. The same style bits are
* allowed here as are allowed on Composite.
*
* @param parentControl The SWT parent.
* @param style Style bits.
*/
public InternalCompositeTable(Composite parentControl, int style) {
super(parentControl, style);
initialize();
this.parent = (CompositeTable) parentControl;
setBackground(parentControl.getBackground());
controlHolder.addListener(SWT.MouseWheel, this);
maxRowsVisible = parent.getMaxRowsVisible();
numRowsInCollection = parent.getNumRowsInCollection();
topRow = parent.getTopRow();
headerConstructor = parent.getHeaderConstructor();
rowConstructor = parent.getRowConstructor();
headerControl = parent.getHeaderControl();
rowControl = parent.getRowControl();
currentVisibleTopRow = topRow;
showHeader();
updateVisibleRows();
if (numRowsVisible < 1) {
createEmptyTablePlaceholer();
}
}
public void setBackground(Color color) {
super.setBackground(color);
controlHolder.setBackground(color);
}
/**
* Initialize the overall table UI.
*/
private void initialize() {
GridLayout gl = new GridLayout();
gl.numColumns = 2;
gl.verticalSpacing = 0;
gl.marginWidth = 0;
gl.marginHeight = 0;
gl.horizontalSpacing = 0;
this.setLayout(gl);
createControlHolder();
createSliderHolder();
}
/**
* Initialize the controlHolder, which is the holder Composite for the header object (if
* applicable) and the row objects.
*/
private void createControlHolder() {
GridData gridData = new org.eclipse.swt.layout.GridData();
gridData.horizontalAlignment = org.eclipse.swt.layout.GridData.FILL;
gridData.grabExcessHorizontalSpace = true;
gridData.grabExcessVerticalSpace = true;
gridData.verticalAlignment = org.eclipse.swt.layout.GridData.FILL;
controlHolder = new Composite(this, SWT.NONE);
controlHolder.setLayoutData(gridData);
controlHolder.setLayout(new Layout() {
protected Point computeSize(Composite composite, int wHint, int hHint, boolean flushCache) {
if (rowControl != null) {
int height = 0;
int width = 0;
if (headerControl != null) {
Point headerSize = headerControl.getSize();
width = headerSize.x;
height = headerSize.y;
}
Point rowSize = rowControl.getSize();
height += rowSize.y * 2;
if (width < rowSize.x) {
width = rowSize.x;
}
return new Point(height, width);
}
return new Point(50, 50);
}
protected void layout(Composite composite, boolean flushCache) {
layoutControlHolder();
}
});
}
/**
* Initialize the sliderHolder and slider. The SliderHolder is a Composite that is
* responsible for showing and hiding the vertical slider upon request.
*/
private void createSliderHolder() {
GridData gd = getSliderGridData();
sliderHolder = new Composite(this, SWT.NONE);
slider = new Slider(sliderHolder, SWT.VERTICAL);
slider.addSelectionListener(sliderSelectionListener);
sliderHolder.setLayout(new FillLayout());
sliderHolder.setLayoutData(gd);
sliderHolder.setTabList(new Control[] {});
}
// Slider utility methods ---------------------------------------------------------------------
/**
* Returns a GridData for the SliderHolder appropriate for if the slider is visible or not.
*
* @return A GridData with a widthHint of 0 if the slider is not visible or with a widthHint
* of SWT.DEFAULT otherwise.
*/
private GridData getSliderGridData() {
GridData gd = new org.eclipse.swt.layout.GridData();
gd.grabExcessVerticalSpace = true;
gd.verticalAlignment = org.eclipse.swt.layout.GridData.FILL;
gd.verticalSpan = 1;
if (!sliderVisible) {
gd.widthHint = 0;
}
gd.horizontalAlignment = org.eclipse.swt.layout.GridData.CENTER;
return gd;
}
private boolean sliderVisible = false;
/**
* Sets if the slider is visible or not.
*
* @param visible true if the slider should be visible; false otherwise.
*/
public void setSliderVisible(boolean visible) {
this.sliderVisible = visible;
sliderHolder.setLayoutData(getSliderGridData());
Display.getCurrent().asyncExec(new Runnable() {
public void run() {
sliderHolder.getParent().layout(true);
sliderHolder.layout(true);
Point sliderHolderSize = sliderHolder.getSize();
slider.setBounds(0, 0, sliderHolderSize.x, sliderHolderSize.y);
}
});
}
/**
* Returns if the slider is visible.
*
* @return true if the slider is visible; false otherwise.
*/
public boolean isSliderVisible() {
return sliderVisible;
}
/* (non-Javadoc)
* @see org.eclipse.swt.widgets.Widget#dispose()
*/
public void dispose() {
disposeRows(rows);
disposeRows(spareRows);
super.dispose();
}
/**
* Disposes all the row objects in the specified LinkedList.
*
* @param rowsCollection The collection containing TableRow objects to dispose.
*/
private void disposeRows(LinkedList rowsCollection) {
for (Iterator rowsIter = rowsCollection.iterator(); rowsIter.hasNext();) {
TableRow row = (TableRow) rowsIter.next();
row.dispose();
}
}
// Row object layout --------------------------------------------------------------------------
/**
* Layout the child controls within the controlHolder Composite.
*/
protected void layoutControlHolder() {
if (myHeader != null)
layoutChild(myHeader);
for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) {
TableRow row = (TableRow) rowsIter.next();
layoutChild(row.getRowControl());
}
updateVisibleRows();
}
/**
* Layout a particular row or header control (child control of the controlHolder).
* If the child control has a layout manager, we delegate to that layout manager.
* Otherwise, we use the built in table layout manager.
*
* @param child The row or header control to layout.
* @return height of child
*/
private int layoutChild(Control child) {
if (child instanceof Composite) {
Composite composite = (Composite) child;
if (composite.getLayout() == null) {
return parent.layoutHeaderOrRow(composite);
}
composite.layout(true);
return composite.getSize().y;
}
return child.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).y;
}
// Table control layout -- utility methods ----------------------------------------------------
/**
* Construct a header or row object on demand. Logs an error and returns null on failure.
*
* @param parent The SWT parent.
* @param constructor The header or row object's constructor.
* @return The constructed control or null if none could be constructed.
*/
private Control createInternalControl(Composite parent, Constructor constructor) {
Control result = null;
try {
result = (Control) constructor.newInstance(new Object[] {parent, new Integer(SWT.NULL)});
} catch (Exception e) {
throw new IllegalArgumentException("Unable to construct control"); //$NON-NLS-1$
}
return result;
}
/**
* If the header control hasn't been created yet, create and show it.
*/
private void showHeader() {
if (myHeader == null && headerConstructor != null) {
myHeader = createInternalControl(controlHolder, headerConstructor);
if (myHeader instanceof Composite) {
Composite headerComp = (Composite) myHeader;
if (headerComp.getLayout() == null) {
headerComp.addPaintListener(headerPaintListener);
}
}
layoutChild(myHeader);
}
}
// Table control layout -- main refresh algorithm ---------------------------------------------
/**
* Main refresh algorithm entry point. This method refreshes everything in the table:
*
* <ul>
* <li>Makes sure the correct number of rows are visible
* <li>Makes sure each row has been refreshed with data from the underlying model
* </ul>
*/
void updateVisibleRows() {
// If we don't have our prototype row object yet, bail out
if (rowControl == null) {
return;
}
// Figure out how many rows we can stack vertically
int clientAreaHeight = controlHolder.getSize().y;
if (clientAreaHeight <= 0) {
return;
}
int topPosition = 0;
int headerHeight = 0;
if (myHeader != null) {
headerHeight = headerControl.getSize().y + 3;
clientAreaHeight -= headerHeight;
topPosition += headerHeight;
}
numRowsInDisplay = clientAreaHeight / rowControl.getSize().y;
// Make sure we have something to lay out to begin with
if (numRowsInCollection > 0) {
numRowsVisible = numRowsInDisplay;
disposeEmptyTablePlaceholder();
int displayableRows = numRowsInCollection - topRow;
if (numRowsVisible > displayableRows) {
numRowsVisible = displayableRows;
}
if (numRowsVisible > maxRowsVisible) {
numRowsVisible = maxRowsVisible;
}
if (numRowsVisible < 1) {
numRowsVisible = 1;
}
// Scroll the view so that the right number of row
// objects are showing and they have the right data
if (rows.size() - Math.abs(currentVisibleTopRow - topRow) > 0) {
if (currentRow >= numRowsVisible) {
deleteRowAt(0);
++currentVisibleTopRow;
++topRow;
--currentRow;
}
scrollTop();
fixNumberOfRows();
} else {
currentVisibleTopRow = topRow;
// The order of number fixing/refresh is important in order to
// minimize the number of screen redraw operations
if (rows.size() > numRowsVisible) {
fixNumberOfRows();
refreshAllRows();
} else {
refreshAllRows();
fixNumberOfRows();
}
}
} else {
numRowsVisible = 0;
topRow=0;
currentRow=0;
currentColumn=0;
currentVisibleTopRow = 0;
numRowsVisible = 0;
if (emptyTablePlaceholder == null) {
fixNumberOfRows();
createEmptyTablePlaceholer();
}
}
// Show, hide, reset the scroll bar
if (numRowsVisible < numRowsInCollection) {
int extra = numRowsInCollection - numRowsVisible;
int pageIncrement = numRowsVisible;
if (pageIncrement > extra)
pageIncrement = extra;
slider.setMaximum(numRowsInCollection);
slider.setMinimum(0);
slider.setIncrement(1);
slider.setPageIncrement(pageIncrement);
slider.setThumb(numRowsInCollection - (numRowsInCollection - numRowsVisible));
slider.setSelection(topRow);
if (!isSliderVisible()) {
setSliderVisible(true);
}
} else {
setSliderVisible(false);
}
// Lay out the header and rows correctly in the display
int width = controlHolder.getSize().x;
// First, the header...
if (myHeader != null) {
myHeader.setBounds(0, 0, width, headerHeight);
}
// Make sure we have rows to lay out...
if (numRowsInCollection < 1) {
return;
}
// Now the rows.
int rowHeight = 50;
rowHeight = rowControl.getSize().y;
for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) {
TableRow row = (TableRow) rowsIter.next();
Control rowControl = row.getRowControl();
rowControl.setBounds(0, topPosition, width, rowHeight);
layoutChild(rowControl);
topPosition += rowHeight;
}
}
/**
* Utility method: Makes sure that the currently visible top row is the same as the top row
* specified in the TopRow property.
*/
private void scrollTop() {
while (currentVisibleTopRow < topRow) {
deleteRowAt(0);
++currentVisibleTopRow;
}
while (currentVisibleTopRow > topRow) {
--currentVisibleTopRow;
insertRowAt(0);
}
}
/**
* Utility method: Makes sure that the number of rows that are visible correspond with
* what should be visible given the table control's size, where it is scrolled, and
* the number of rows in the underlying collection.
*/
private void fixNumberOfRows() {
int numRows = rows.size();
while (numRows > numRowsVisible) {
deleteRowAt(numRows-1);
numRows = rows.size();
}
while (numRows < numRowsVisible) {
insertRowAt(numRows);
numRows = rows.size();
}
}
/**
* Fire the refresh event on all visible rows.
*/
void refreshAllRows() {
int row=0;
for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) {
TableRow rowControl = (TableRow) rowsIter.next();
fireRefreshEvent(topRow + row, rowControl.getRowControl());
++row;
}
}
/**
* Insert a new row object at the specified 0-based position relatve to the topmost row.
*
* @param position The 0-based position relative to the topmost row.
*/
private void insertRowAt(int position) {
TableRow newRow = getNewRow();
if (position > rows.size()) {
position = rows.size();
}
rows.add(position, newRow);
fireRefreshEvent(currentVisibleTopRow + position, newRow.getRowControl());
}
/**
* Delete the row at the specified 0-based position relative to the topmost row.
*
* @param position The 0-based position relative to the topmost row.
*/
private void deleteRowAt(int position) {
TableRow row = (TableRow) rows.remove(position);
row.setVisible(false);
spareRows.addLast(row);
}
/**
* Utility method: Creates a new row object or recycles one that had been previously
* created but was no longer needed.
*
* @return The new row object.
*/
private TableRow getNewRow() {
if (spareRows.size() > 0) {
TableRow recycledRow = (TableRow) spareRows.removeFirst();
recycledRow.setVisible(true);
return recycledRow;
}
TableRow newRow = new TableRow(this, createInternalControl(controlHolder, rowConstructor));
fireRowConstructionEvent(newRow.getRowControl());
if (newRow.getRowControl() instanceof Composite) {
Composite rowComp = (Composite) newRow.getRowControl();
if (rowComp.getLayout() == null) {
rowComp.setBackground(getBackground());
rowComp.addPaintListener(rowPaintListener);
}
}
return newRow;
}
// Property getters/setters --------------------------------------------------------------
/*
* These are internal API.
* <p>
* Plese refer to the JavaDoc on CompositeTable for detailed description of these property methods.
*/
/** (non-API)
* Method getHeaderControl. Return the prototype control being used as a header.
*
* @return The header control
*/
public Control getHeaderControl() {
return headerControl;
}
/**
* Method setMaxRowsVisible. Sets the maximum number of rows that will be permitted
* in the table at once. For example, setting this property to 1 will have the effect of
* creating a single editing area with a scroll bar on the right allowing the user to scroll
* through all rows using either the mouse or the PgUp/PgDn keys. The default value is
* Integer.MAX_VALUE.
*
* @param maxRowsVisible the maximum number of rows that are permitted to be visible at one time, regardless
* of the control's size.
*/
public void setMaxRowsVisible(int maxRowsVisible) {
this.maxRowsVisible = maxRowsVisible;
updateVisibleRows();
}
/**
* Method getNumRowsVisible. Returns the actual number of rows that are currently visible.
* Normally CompositeTable displays as many rows as will fit vertically given the control's
* size. This value can be clamped to a maximum using the MaxRowsVisible property.
*
* @return the actual number of rows that are currently visible.
*/
public int getNumRowsVisible() {
return numRowsVisible;
}
/**
* Method setNumRowsInCollection. Sets the number of rows in the data structure that is
* being edited.
*
* @param numRowsInCollection the number of rows represented by the underlying data structure.
*/
public void setNumRowsInCollection(int numRowsInCollection) {
this.topRow = 0;
if (currentRow > 0) {
currentRow = 0;
}
this.numRowsInCollection = numRowsInCollection;
updateVisibleRows();
refreshAllRows();
}
/**
* Method setTopRow. Set the number of the line that is being displayed in the top row
* of the CompositeTable editor (0-based). If the new top row is not equal to the current
* top row, the table will automatically be scrolled to the new position. This number must
* be greater than 0 and less than NumRowsInCollection.
*
* @param topRow the line number of the new top row.
*/
public void setTopRow(int topRow) {
fireRowDepartEvent();
this.topRow = topRow;
updateVisibleRows();
fireRowArriveEvent();
}
/**
* Method getTopRow. Return the number of the line that is being displayed in the top row
* of the CompositeTable editor (0-based).
*
* @return the number of the top line.
*/
public int getTopRow() {
return topRow;
}
/**
* Method getSelection. Returns the currently-selected (column, row) pair where the row
* specifies the offset from the top of the table window. In order to get the current
* row in the underlying data structure, use getSelection().y + getCurrentRow().
*
* @return the currently-selected (column, row) pair where the row specifies the offset
* from the top of the table window.
*/
public Point getSelection() {
return new Point(currentColumn, currentRow);
}
/**
* Method setSelection. Sets the currently-selected (column, row) pair where the row
* specifies the offset from the top of the table window. In order to get the current
* row in the underlying data structure, use getSelection().y + getCurrentRow().
*
* @param column the column to select
* @param row the row to select
*/
public void setSelection(int column, int row) {
if (row == currentRow)
internalSetSelection(column, row, false);
else {
if (fireRequestRowChangeEvent())
internalSetSelection(column, row, true);
}
}
/**
* Method setWeights. Indicates that the column weights were just set and we should re-layout
* the control holder object.
*/
public void setWeights() {
layoutControlHolder();
}
// Refresh Event API --------------------------------------------------------------------------
/**
* Adds the specified listener to the set of listeners that will be notified when a row
* refresh event occurs.
*
* @param listener the listener to add.
*/
public void addRefreshContentProvider(IRowContentProvider listener) {
parent.contentProviders.add(listener);
}
/**
* Remove the specified listener from the set of listeners that will be notified when a
* row refresh event occurs.
*
* @param listener the listener to remove.
*/
public void removeRefreshContentProvider(IRowContentProvider listener) {
parent.contentProviders.remove(listener);
}
private void fireRefreshEvent(int positionInCollection, Control rowControl) {
if (numRowsInCollection < 1) {
return;
}
for (Iterator refreshListenersIter = parent.contentProviders.iterator(); refreshListenersIter.hasNext();) {
IRowContentProvider listener = (IRowContentProvider) refreshListenersIter.next();
listener.refresh(parent, positionInCollection, rowControl);
}
}
// Empty table placeholder --------------------------------------------------------------------
private void createEmptyTablePlaceholer() {
emptyTablePlaceholder = new EmptyTablePlaceholder(controlHolder, SWT.NULL);
if (rowControl != null)
emptyTablePlaceholder.setBackground(rowControl.getBackground());
emptyTablePlaceholder.setMessage(parent.getInsertHint());
}
private void disposeEmptyTablePlaceholder() {
if (emptyTablePlaceholder != null) {
emptyTablePlaceholder.dispose();
emptyTablePlaceholder = null;
}
}
// Event Handling -----------------------------------------------------------------------------
private boolean needToRequestRC=true;
/**
* Handle a keyPressed event on any row control.
*
* @param sender The row that is sending the event
* @param e the actual KeyEvent
*/
public void keyPressed(TableRow sender, KeyEvent e) {
if ((e.stateMask & SWT.CONTROL) != 0) {
switch (e.keyCode) {
case SWT.HOME:
if (topRow <= 0) {
return;
}
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(e.widget);
// If the focus is already in the top visible row, we will need to explicitly
// fire an arrive event.
boolean needToArrive = true;
if (currentRow > 0) {
needToArrive = false;
}
setTopRow(0);
if (needToArrive) {
internalSetSelection(currentColumn, 0, true);
} else {
internalSetSelection(currentColumn, 0, false);
}
return;
case SWT.END:
if (topRow + numRowsVisible < numRowsInCollection) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(e.widget);
// If the focus is already in the last visible row, we will need to explicitly
// fire an arrive event.
needToArrive = true;
if (currentRow < numRowsVisible-1) {
needToArrive = false;
}
setTopRow(numRowsInCollection - numRowsVisible);
if (needToArrive) {
internalSetSelection(currentColumn, numRowsVisible-1, true);
} else {
internalSetSelection(currentColumn, numRowsVisible-1, false);
}
}
return;
case SWT.DEL:
if (fireDeleteEvent()) {
// We know the object is gone if we made it here, so now refresh the display...
--numRowsInCollection;
// If we deleted the last row in the list
if (currentRow >= numRowsVisible-1) {
// If that wasn't the last row in the collection, move the focus
if (numRowsInCollection > 0) {
// If we're only displaying one row, scroll first
if (currentRow < 1) {
needToRequestRC = false;
deleteRowAt(currentRow);
setTopRow(topRow-1);
internalSetSelection(currentColumn, currentRow, true);
} else {
needToRequestRC = false;
internalSetSelection(currentColumn, currentRow-1, false);
Display.getCurrent().asyncExec(new Runnable() {
public void run() {
deleteRowAt(currentRow+1);
updateVisibleRows();
}
});
}
} else {
// Otherwise, show the placeholder object and give it focus
deleteRowAt(currentRow);
--numRowsVisible;
createEmptyTablePlaceholer();
emptyTablePlaceholder.setFocus();
}
} else {
// else, keep the focus where it was
deleteRowAt(currentRow);
updateVisibleRows();
internalSetSelection(currentColumn, currentRow, true);
}
}
return;
default:
return;
}
}
switch (e.keyCode) {
case SWT.INSERT:
// If no insertHandler has been registered, bail out
if (parent.insertHandlers.size() < 1) {
return;
}
// Make sure we can leave the current row
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
// Insert the new object
int newRowPosition = fireInsertEvent();
if (newRowPosition < 0) {
// This should never happen, but...
return;
}
disposeEmptyTablePlaceholder();
// If the current widget has a selection, deselect it
deselect(e.widget);
// If the new row is in the visible space, refresh it
if (topRow <= newRowPosition &&
numRowsVisible > newRowPosition - topRow) {
insertRowAt(newRowPosition - topRow);
++numRowsInCollection;
updateVisibleRows();
int newRowNumber = newRowPosition - topRow;
if (newRowNumber != currentRow) {
internalSetSelection(currentColumn, newRowNumber, false);
} else {
internalSetSelection(currentColumn, newRowNumber, true);
}
return;
}
// else...
++numRowsInCollection;
// If the new row is above us, scroll up to it
if (newRowPosition < topRow + currentRow) {
setTopRow(newRowPosition);
Display.getDefault().asyncExec(new Runnable() {
public void run() {
updateVisibleRows();
if (currentRow != 0) {
internalSetSelection(currentColumn, 0, false);
} else {
internalSetSelection(currentColumn, 0, true);
}
}
});
} else {
// If we're appending
if (numRowsInDisplay > numRowsVisible) {
updateVisibleRows();
int newRowNumber = newRowPosition - topRow;
if (newRowNumber != currentRow) {
internalSetSelection(currentColumn, newRowNumber, false);
} else {
internalSetSelection(currentColumn, newRowNumber, true);
}
} else {
// It's somewhere in the middle below us; scroll down to it
setTopRow(newRowPosition-numRowsVisible+1);
int newRowNumber = numRowsVisible-1;
if (newRowNumber != currentRow) {
internalSetSelection(currentColumn, newRowNumber, false);
} else {
internalSetSelection(currentColumn, newRowNumber, true);
}
}
}
return;
case SWT.ARROW_UP:
if (maxRowsVisible <= 1)
return;
if (currentRow > 0) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(e.widget);
internalSetSelection(currentColumn, currentRow-1, false);
return;
}
if (topRow > 0) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(e.widget);
setTopRow(topRow - 1);
internalSetSelection(currentColumn, currentRow, true);
return;
}
return;
case SWT.ARROW_DOWN:
if (maxRowsVisible <= 1)
return;
if (currentRow < numRowsVisible-1) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(e.widget);
internalSetSelection(currentColumn, currentRow+1, false);
return;
}
if (topRow + numRowsVisible < numRowsInCollection) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(e.widget);
setTopRow(topRow + 1);
internalSetSelection(currentColumn, currentRow, true);
return;
}
return;
case SWT.PAGE_UP:
if (topRow > 0) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
int newTopRow = topRow - numRowsInDisplay;
if (newTopRow < 0) {
newTopRow = 0;
}
setTopRow(newTopRow);
internalSetSelection(currentColumn, currentRow, true);
}
return;
case SWT.PAGE_DOWN:
if (topRow + numRowsVisible < numRowsInCollection) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
int newTopRow = topRow + numRowsVisible;
if (newTopRow >= numRowsInCollection - 1) {
newTopRow = numRowsInCollection - 1;
}
setTopRow(newTopRow);
if (currentRow < numRowsVisible) {
internalSetSelection(currentColumn, currentRow, true);
} else {
internalSetSelection(currentColumn, numRowsVisible-1, true);
}
}
return;
}
}
/**
* Handle the keyTraversed event on any child control in the table.
*
* @param sender The row sending the event.
* @param e The SWT TraverseEvent
*/
public void keyTraversed(TableRow sender, TraverseEvent e) {
if (e.detail == SWT.TRAVERSE_TAB_NEXT) {
if (currentColumn >= sender.getNumColumns() - 1) {
e.detail = SWT.TRAVERSE_NONE;
handleNextRowNavigation();
}
} else if (e.detail == SWT.TRAVERSE_TAB_PREVIOUS) {
if (currentColumn == 0) {
e.detail = SWT.TRAVERSE_NONE;
handlePreviousRowNavigation(sender);
}
} else if (e.detail == SWT.TRAVERSE_RETURN) {
e.detail = SWT.TRAVERSE_NONE;
if (currentColumn >= sender.getNumColumns() - 1) {
handleNextRowNavigation();
} else {
deferredSetFocus(getControl(currentColumn+1, currentRow), false);
}
}
}
/**
* The SelectionListener for the table's slider control.
*/
private SelectionListener sliderSelectionListener = new SelectionListener() {
public void widgetSelected(SelectionEvent e) {
if (slider.getSelection() == topRow) {
return;
}
if (!fireRequestRowChangeEvent()) {
slider.setSelection(topRow);
return;
}
deselect(getControl(currentColumn, currentRow));
setTopRow(slider.getSelection());
deferredSetFocus(getControl(currentColumn, currentRow), true);
}
public void widgetDefaultSelected(SelectionEvent e) {
widgetSelected(e);
}
};
/**
* Scroll wheel event handling.
*/
public void handleEvent(Event event) {
if (event.count > 0) {
if (topRow > 0) {
if (!fireRequestRowChangeEvent()) {
return;
}
deselect(getControl(currentColumn, currentRow));
setTopRow(topRow - 1);
deferredSetFocus(getControl(currentColumn, currentRow), true);
}
} else {
if (topRow < numRowsInCollection - numRowsVisible) {
if (!fireRequestRowChangeEvent()) {
return;
}
deselect(getControl(currentColumn, currentRow));
setTopRow(topRow + 1);
deferredSetFocus(getControl(currentColumn, currentRow), true);
}
}
}
/**
* Handle focusLost events on any child control. This is not currently used.
*
* @param sender The row containing the sending control.
* @param e The SWT FocusEvent.
*/
public void focusLost(TableRow sender, FocusEvent e) {
}
/**
* Handle focusGained events on any child control.
*
* @param sender The row containing the sending control.
* @param e The SWT FocusEvent.
*/
public void focusGained(TableRow sender, FocusEvent e) {
boolean rowChanged = false;
if (getRowNumber(sender) != currentRow) {
if (needToRequestRC) {
if (!fireRequestRowChangeEvent()) {
// Go back if we're not allowed to be here
deferredSetFocus(getControl(currentColumn, currentRow), false);
}
} else {
needToRequestRC = true;
}
rowChanged = true;
}
currentRow = getRowNumber(sender);
currentColumn = sender.getColumnNumber((Control)e.widget);
if (rowChanged)
fireRowArriveEvent();
}
private PaintListener headerPaintListener = new PaintListener() {
public void paintControl(PaintEvent e) {
if (parent.gridLinesOn) {
drawGridLines(e, true);
}
}
};
private PaintListener rowPaintListener = new PaintListener() {
public void paintControl(PaintEvent e) {
if (parent.gridLinesOn) {
drawGridLines(e, false);
}
}
};
private void drawGridLines(PaintEvent e, boolean isHeader) {
Color oldColor = e.gc.getForeground();
try {
// Get the colors we need
Display display = Display.getCurrent();
Color lineColor = display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW);
Color secondaryColor = display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW);
Color hilightColor = display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW);
if (!isHeader) {
lineColor = display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW);
}
// Get the control
Control toPaint = (Control) e.widget;
Point controlSize = toPaint.getSize();
// Draw the bottom line(s)
e.gc.setForeground(lineColor);
e.gc.drawLine(0, controlSize.y-1, controlSize.x, controlSize.y-1);
if (isHeader) {
e.gc.setForeground(secondaryColor);
e.gc.drawLine(0, controlSize.y-2, controlSize.x, controlSize.y-2);
e.gc.setForeground(hilightColor);
e.gc.drawLine(0, 1, controlSize.x, 1);
}
// Now draw lines around the child controls, if there are any
if (toPaint instanceof Composite) {
Composite row = (Composite) toPaint;
Control[] children = row.getChildren();
for (int i = 0; i < children.length; i++) {
Rectangle childBounds = children[i].getBounds();
// Paint the beginning lines
if (isHeader) {
e.gc.setForeground(hilightColor);
e.gc.drawLine(childBounds.x-2, 1, childBounds.x-2, controlSize.y-2);
}
// Paint the ending lines
e.gc.setForeground(lineColor);
int lineLeft = childBounds.x + childBounds.width+1;
e.gc.drawLine(lineLeft, 0, lineLeft, controlSize.y);
if (isHeader) {
e.gc.setForeground(secondaryColor);
e.gc.drawLine(lineLeft-1, 0, lineLeft-1, controlSize.y-1);
}
}
}
} finally {
e.gc.setForeground(oldColor);
}
}
// Event Firing -------------------------------------------------------------------------------
/**
* Fire the row construction event
*
* @param newControl The new row's SWT control
*/
private void fireRowConstructionEvent(Control newControl) {
for (Iterator rowConstructionListenersIter = parent.rowConstructionListeners.iterator(); rowConstructionListenersIter.hasNext();) {
IRowConstructionListener listener = (IRowConstructionListener) rowConstructionListenersIter.next();
listener.rowConstructed(newControl);
}
}
/**
* Indicate to listeners that the focus is arriving on the specified row
*/
private void fireRowArriveEvent() {
if (rows.size() < 1) {
return;
}
for (Iterator rowChangeListenersIter = parent.rowFocusListeners.iterator(); rowChangeListenersIter.hasNext();) {
IRowFocusListener listener = (IRowFocusListener) rowChangeListenersIter.next();
listener.arrive(parent, topRow+currentRow, currentRow().getRowControl());
}
}
/**
* Request permission from all listeners to leave the current row.
*
* @return true if all listeners permit the row change; false otherwise.
*/
private boolean fireRequestRowChangeEvent() {
if (rows.size() < 1) {
return true;
}
if (currentRow > rows.size()-1) {
// (if the other row is already gone)
return true;
}
for (Iterator rowChangeListenersIter = parent.rowFocusListeners.iterator(); rowChangeListenersIter.hasNext();) {
IRowFocusListener listener = (IRowFocusListener) rowChangeListenersIter.next();
if (!listener.requestRowChange(parent, topRow+currentRow, currentRow().getRowControl())) {
return false;
}
}
fireRowDepartEvent();
return true;
}
/**
* Indicate to listeners that the focus is about to leave the current row.
*/
private void fireRowDepartEvent() {
if (rows.size() < 1) {
return;
}
for (Iterator rowChangeListenersIter = parent.rowFocusListeners.iterator(); rowChangeListenersIter.hasNext();) {
IRowFocusListener listener = (IRowFocusListener) rowChangeListenersIter.next();
listener.depart(parent, topRow+currentRow, currentRow().getRowControl());
}
}
/**
* Request deletion of the current row from the underlying data structure.
*
* @return true if the deletion was successful; false otherwise.
*/
private boolean fireDeleteEvent() {
if (parent.deleteHandlers.size() < 1) {
return false;
}
int absoluteRow = topRow + currentRow;
for (Iterator deleteHandlersIter = parent.deleteHandlers.iterator(); deleteHandlersIter.hasNext();) {
IDeleteHandler handler = (IDeleteHandler) deleteHandlersIter.next();
if (!handler.canDelete(absoluteRow)) {
return false;
}
}
for (Iterator deleteHandlersIter = parent.deleteHandlers.iterator(); deleteHandlersIter.hasNext();) {
IDeleteHandler handler = (IDeleteHandler) deleteHandlersIter.next();
handler.deleteRow(absoluteRow);
}
return true;
}
/**
* Request that the model insert a new row into itself.
*
* @return The 0-based offset of the new row from the start of the collection or -1 if a
* new row could not be inserted.
*/
private int fireInsertEvent() {
if (parent.insertHandlers.size() < 1) {
return -1;
}
for (Iterator insertHandlersIter = parent.insertHandlers.iterator(); insertHandlersIter.hasNext();) {
IInsertHandler handler = (IInsertHandler) insertHandlersIter.next();
int resultRow = handler.insert(topRow+currentRow);
if (resultRow >= 0) {
return resultRow;
}
}
return -1;
}
// Event Handling, utility methods ------------------------------------------------------------
/**
* Set the widget's selection to an empty selection.
*
* @param widget The widget to deselect
*/
private void deselect(Widget widget) {
if (DuckType.instanceOf(ISelectableRegionControl.class, widget)) {
ISelectableRegionControl control = (ISelectableRegionControl) DuckType.implement(ISelectableRegionControl.class, widget);
control.setSelection(0, 0);
}
}
/**
* Try to go to the next row in the collection.
*/
private void handleNextRowNavigation() {
if (currentRow < numRowsVisible-1) {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(getControl(currentColumn, currentRow));
deferredSetFocus(getControl(0, currentRow+1), false);
} else {
if (topRow + numRowsVisible >= numRowsInCollection) {
// We're at the end; don't go anywhere
return;
}
// We have to scroll forwards
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(getControl(currentColumn, currentRow));
setTopRow(topRow+1);
deferredSetFocus(getControl(0, currentRow), true);
}
}
/**
* Try to go to the previous row in the collection.
*
* @param row The current table row.
*/
private void handlePreviousRowNavigation(TableRow row) {
if (currentRow == 0) {
if (topRow == 0) {
// We're at the beginning of the table; don't go anywhere
return;
}
// We have to scroll backwards
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(getControl(currentColumn, currentRow));
setTopRow(topRow-1);
deferredSetFocus(getControl(row.getNumColumns()-1, 0), true);
} else {
if (!fireRequestRowChangeEvent()) {
return;
}
needToRequestRC = false;
deselect(getControl(currentColumn, currentRow));
deferredSetFocus(getControl(row.getNumColumns()-1, currentRow-1), false);
}
}
/**
* Gets the current TableRow.
*
* @return the current TableRow
*/
private TableRow currentRow() {
if (currentRow > rows.size()-1) {
return null;
}
return (TableRow) rows.get(currentRow);
}
/**
* Returns the SWT control corresponding to the current row.
*
* @return the current row control.
*/
public Control getCurrentRowControl() {
TableRow currentRow = currentRow();
if (currentRow == null) {
return null;
}
return currentRow().getRowControl();
}
/**
* Returns the TableRow by the specified 0-based offset from the top visible row.
*
* @param rowNumber 0-based offset of the requested fow starting from the top visible row.
* @return The corresponding TableRow or null if there is none.
*/
private TableRow getRowByNumber(int rowNumber) {
if (rowNumber > rows.size()-1) {
return null;
}
return (TableRow) rows.get(rowNumber);
}
/**
* Return the SWT control at (column, row), where row is a 0-based number starting
* from the top visible row.
*
* @param column the 0-based column.
* @param row the 0-based row starting from the top visible row.
* @return the SWT control at (column, row)
*/
private Control getControl(int column, int row) {
TableRow rowObject = getRowByNumber(row);
if (rowObject == null) {
throw new IndexOutOfBoundsException("Request for a nonexistent row"); //$NON-NLS-1$
}
Control result = rowObject.getColumnControl(column);
return result;
}
/**
* Return the 0-based row number corresponding to a particular TableRow object.
*
* @param row The TableRow to translate to row coordinates.
* @return the 0-based row number or -1 if the specified TableRow is not visible.
*/
private int getRowNumber(TableRow row) {
int rowNumber = 0;
for (Iterator rowIter = rows.iterator(); rowIter.hasNext();) {
TableRow candidate = (TableRow) rowIter.next();
if (candidate == row) {
return rowNumber;
}
++rowNumber;
}
return -1;
}
/**
* Set the focus to the specified (column, row). If rowChange is true, fire a
* row change event, otherwise be silent.
*
* @param column The 0-based column to focus
* @param row The 0-based row to focus
* @param rowChange true if a row change event should be fired; false otherwise.
*/
private void internalSetSelection(int column, int row, boolean rowChange) {
Control toFocus = getControl(column, row);
deferredSetFocus(toFocus, rowChange);
}
/**
* Set the focus to the specified control after allowing all pending events to complete
* first. If rowChange is true, fire a row arrive event after the focus has been set.
*
* @param toFocus The SWT Control to focus
* @param rowChange true if the rowArrive event should be fired; false otherwise.
*/
private void deferredSetFocus(final Control toFocus, final boolean rowChange) {
Display.getCurrent().asyncExec(new Runnable() {
public void run() {
toFocus.setFocus();
if (rowChange) {
fireRowArriveEvent();
}
}
});
}
}