| /* |
| * 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 boolean fittingVertically; |
| 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(); |
| int clientAreaHeight; |
| |
| // 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(); |
| fittingVertically = parent.isFittingVertically(); |
| 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, true); |
| for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) { |
| TableRow row = (TableRow) rowsIter.next(); |
| layoutChild(row.getRowControl(), false); |
| } |
| 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. |
| * @param isHeader TODO |
| * @return height of child |
| */ |
| private int layoutChild(Control child, boolean isHeader) { |
| if (child instanceof Composite) { |
| Composite composite = (Composite) child; |
| if (composite.getLayout() == null) { |
| return parent.layoutHeaderOrRow(composite, isHeader); |
| } |
| 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, true); |
| } |
| } |
| |
| // 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; |
| } |
| |
| 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 / getRowHeight(clientAreaHeight); |
| |
| // 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 = getRowHeight(clientAreaHeight); |
| |
| for (Iterator rowsIter = rows.iterator(); rowsIter.hasNext();) { |
| TableRow row = (TableRow) rowsIter.next(); |
| Control rowControl = row.getRowControl(); |
| rowControl.setBounds(0, topPosition, width, rowHeight); |
| layoutChild(rowControl, false); |
| topPosition += rowHeight; |
| } |
| } |
| |
| int getRowHeight(int clientAreaHeight) { |
| int rowControlHeight = rowControl.getSize().y; |
| if (maxRowsVisible == Integer.MAX_VALUE) { |
| return rowControlHeight; |
| } |
| if (fittingVertically) { |
| int fitControlHeight = clientAreaHeight / maxRowsVisible; |
| if (fitControlHeight < rowControlHeight) { |
| return rowControlHeight; |
| } |
| return fitControlHeight; |
| } |
| return rowControlHeight; |
| } |
| |
| /** |
| * 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; |
| } |
| Control newControl = createInternalControl(controlHolder, rowConstructor); |
| fireRowConstructionEvent(newControl); |
| TableRow newRow = new TableRow(this, newControl); |
| 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(); |
| } |
| |
| /** |
| * @param fittingVertically |
| */ |
| public void setFittingVertically(boolean fittingVertically) { |
| this.fittingVertically = fittingVertically; |
| 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.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.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.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(); |
| } |
| } |
| }); |
| } |
| |
| } |