Carron
Mitglied
[SWT] Table sortieren, editiern, filtern, drucken
Hallo auch
Im Rahmen privater und beruflicher Projekte habe ich vor knapp anderthalb Jahren mit der Java-Programmierung begonnen.
Innerhalb eines solchen Erwuchs der Wunsch eine normale SWT-Table mit zusätzlichen Funktionen auszustatten.
Da ich für dieses und eine Vielzahl anderer Probleme in diesem Forum stets eine gute Anlaufstelle gefunden habe, möchte ich natürlich auch etwas zurückgeben und hier quasi die bisherigen Ergebnisse meiner Bemühungen (in Sachen Tabelle) auch anderen zur Verfügung stellen.
Die SorTable selbst bietet neben den grundsätzlichen Funktionalitäten folgende Ergänzungen (zu einem Großteil zusammengesetzt aus Snippets anderer fleißiger Helfer):
Der angesprochene separate SWTTablePrinter dient zum Ausdrucken einer beliebigen SWT-Table und kann damit auch unabhängig von der SorTable genutzt werden.
Es kann eine Spalte festgelegt werden (meist eine Beschreibungsspalte oder eine andere mit möglicherweise großem Umfang), deren Inhalte - sofern durch das gewählte Papierformat und die zu druckenden Inhalte erforderlich - mit Zeilenumbrüchen ausgestattet werden, bzw. die bereits enthaltenen übernommen werden.
Darüber hinaus kann eine Überschrift auf der ersten Seite ausgegeben werden und sowohl für diese Überschrift als auch den Tabellenkopf und die Tabelleninhalte benutzerdefinierte Schriftarten verwendet werden.
Anbei noch eine kleine Test-Klasse, die - bis auf das Hintergrundbild - alle enthaltenen Funktionen vorstellen und deren Nutzung verdeutlichen soll.
Über Kommentare/Fragen/Anregungen würde ich mich freuen. Vor allem für Möglichkeiten an der Performance zu schrauben bin ich immer zu begeistern. Sei es hinsichtlich Rechenzeit oder Speichernutzung.
SorTable
SWTTablePrinter
SorTableTester
Hallo auch
Im Rahmen privater und beruflicher Projekte habe ich vor knapp anderthalb Jahren mit der Java-Programmierung begonnen.
Innerhalb eines solchen Erwuchs der Wunsch eine normale SWT-Table mit zusätzlichen Funktionen auszustatten.
Da ich für dieses und eine Vielzahl anderer Probleme in diesem Forum stets eine gute Anlaufstelle gefunden habe, möchte ich natürlich auch etwas zurückgeben und hier quasi die bisherigen Ergebnisse meiner Bemühungen (in Sachen Tabelle) auch anderen zur Verfügung stellen.
Die SorTable selbst bietet neben den grundsätzlichen Funktionalitäten folgende Ergänzungen (zu einem Großteil zusammengesetzt aus Snippets anderer fleißiger Helfer):
- Sortieren
- standardmäßig als Zeichenketten,
- aber auch als einfache Integer-Werte,
- Double-Werte (auch mit Einheiten)
- oder Datums-Werte (wobei diese in der aktuellen Implementierung nur deutsche Schreibweisen beachten)
- Editieren
(wobei hier sowohl einzelne Spalten als editierbar ausgewählt werden können als auch die ganze Tabelle) - Filtern
(in einem extra Feld eingegebene Begriffe werden als Suchfilter auf die Tabelleninhalte angewandt) - Drucken
(dafür wird der separate SWTTablePrinter benutzt, welcher sich auch für jede andere SWT-Table nutzen lässt) - ein Hintergrundbild auf die volle Größe der Tabelle skalieren
- ausgewählte Einträge in die Zwischenablage kopieren
Der angesprochene separate SWTTablePrinter dient zum Ausdrucken einer beliebigen SWT-Table und kann damit auch unabhängig von der SorTable genutzt werden.
Es kann eine Spalte festgelegt werden (meist eine Beschreibungsspalte oder eine andere mit möglicherweise großem Umfang), deren Inhalte - sofern durch das gewählte Papierformat und die zu druckenden Inhalte erforderlich - mit Zeilenumbrüchen ausgestattet werden, bzw. die bereits enthaltenen übernommen werden.
Darüber hinaus kann eine Überschrift auf der ersten Seite ausgegeben werden und sowohl für diese Überschrift als auch den Tabellenkopf und die Tabelleninhalte benutzerdefinierte Schriftarten verwendet werden.
Anbei noch eine kleine Test-Klasse, die - bis auf das Hintergrundbild - alle enthaltenen Funktionen vorstellen und deren Nutzung verdeutlichen soll.
Über Kommentare/Fragen/Anregungen würde ich mich freuen. Vor allem für Möglichkeiten an der Performance zu schrauben bin ich immer zu begeistern. Sei es hinsichtlich Rechenzeit oder Speichernutzung.
SorTable
Java:
package org.hermeneutix.utilities.swt;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.TableEditor;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.printing.PrintDialog;
import org.eclipse.swt.printing.PrinterData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;
/**
* Capsuled SWT-{@link Table} with a few additional features.
* <p>
* <ul>
* <li>Sorting</li>
* <ul>
* <li>available sorting algorithms can be set for each column by using
* <code>setSortingMethod()</code> in combination with one of the preset
* compare modes (<code>COMPARE_TEXT</code>, <code>COMPARE_INT</code>,
* <code>COMPARE_DOUBLE</code>, <code>COMPARE_DATETIME</code>) </li>
* <li><code>COMPARE_DATETIME</code> is set for the german date format.</li>
* <li>after inserting all column the sortability needs to be activated with
* <code>enableSorting(true)</code>. Columns added after this activation are
* not available for sorting till a new <code>enableSorting(true)</code> call
* occurs.</li>
* </ul>
* <li>Filtering</li>
* <ul>
* <li>after initializing the table itself you can generate a {@link Text} with
* <code>createSearchField()</code> which serves as a filter input for the
* table values</li>
* <li>the background colors of the single table items are lost each time the
* <code>resetvalues()</code> call (which occurs on each change of the
* filter). By using <code>addColorListener()</code> you can implement a
* method for recoloring all items after that happens.</li>
* </ul>
* <li>Editing</li>
* <ul>
* <li><code>setEditable()</code> enables the possibilty to change the values
* in the selected columns</li>
* </ul>
* <li>Scalable Background</li>
* <li>Copy to Clipboard</li>
* <li>Printing</li>
* </ul>
*
* @author Carsten Englert
*/
public final class SorTable extends Composite {
/**
* Text of the popup menu offering to copy the selected items to clipboard.
*/
private static final String COPY_TEXT = "Copy Selection";
/**
* Sorting method regarding the compared values as <code>String</code>s.
*/
public static final int COMPARE_TEXT = 1;
/**
* Sorting method for <code>Integer</code> values.
*/
public static final int COMPARE_INT = 2;
/**
* Sorting method for <code>Double</code> values.<br>
* Every character which is neither a number, a dot or a comma is ignored.
*/
public static final int COMPARE_DOUBLE = 4;
/**
* Sorting method for date strings in one of the following forms:
* <code>DD</code>, <code>DD:MM</code>, <code>DD:MM:YYYY</code>,
* <code>DD:MM:YYYY_hh:mm</code>, <code>DD:MM_hh:mm</code><br>
* Every character that is no number is considered to be a separator.<br>
* The single values are interpreted as <code>Integer</code>s.
*/
public static final int COMPARE_DATETIME = 8;
/**
* Default sorting method to set use if no custom mode is selected for the
* column to sort.<br>
* Current setting: <code>COMPARE_TEXT</code>
*/
public static final int COMPARE_DEFAULT = COMPARE_TEXT;
private static final String REGEX_DATETIME_DD = "[\\D]*[\\d]{2}[\\D]*";
private static final SimpleDateFormat FORMAT_DATETIME_DD = new SimpleDateFormat(
"dd");
private static final String REGEX_DATETIME_DDMM = "[\\D]*[\\d]{2}"
+ "[\\D]+[\\d]{2}[\\D]*";
private static final SimpleDateFormat FORMAT_DATETIME_DDMM = new SimpleDateFormat(
"dd MM");
private static final String REGEX_DATETIME_DDMMYYYY = "[\\D]*[\\d]{2}"
+ "[\\D]+[\\d]{2}" + "[\\D]+[\\d]{4}[\\D]*";
private static final SimpleDateFormat FORMAT_DATETIME_DDMMYYYY = new SimpleDateFormat(
"dd MM yyyy");
private static final String REGEX_DATETIME_DDMM_HHMM = "[\\D]*[\\d]{2}"
+ "[\\D]+[\\d]{2}" + "[\\D]+[\\d]{2}" + "[\\D]+[\\d]{2}[\\D]*";
private static final SimpleDateFormat FORMAT_DATETIME_DDMM_HHMM = new SimpleDateFormat(
"dd MM HH mm");
private static final String REGEX_DATETIME_DDMMYYYY_HHMM = "[\\D]*[\\d]{2}"
+ "[\\D]+[\\d]{2}" + "[\\D]+[\\d]{4}" + "[\\D]+[\\d]{2}"
+ "[\\D]+[\\d]{2}[\\D]*";
private static final SimpleDateFormat FORMAT_DATETIME_DDMMYYYY_HHMM = new SimpleDateFormat(
"dd MM yyyy HH mm");
/**
* Regular expression representing all characters to be treated as
* separators in <code>COMPARE_DATETIME</code> mode.<br>
* Current setting: all non-numeric characters
*/
private static final String REGEX_DATETIME_SEPARATOR = "[\\D]+";
/**
* Regular expression representing all characters to be ignored in
* <code>COMPARE_DOUBLE</code> mode.<br>
* Current setting: all non-numeric characters except "-" and "."
*/
private static final String REGEX_DOUBLE_IGNORE = "[^\\d\\.\\-]+";
/**
* Contained {@link Table}, because it is impossible to extend it directly.
*/
final Table contentTable;
/**
* Stored sorting methods for each column
*/
private final Map<TableColumn, Comparator<TableItem>> comparators;
/**
* Registered listeners, which are activated when the colors of the table
* entries are lost once again. (each time the method
* <code>resetValues()</code> is invoked)
*/
private final List<Listener> colorListeners = new ArrayList<Listener>();
/**
* Because it is impossible to set single {@link TableItem}s invisible they
* need to be disposed in order to apply the search filter.<br>
* To be able to restore the previous values they have to be stored
* separatly in this list.
*/
List<String[]> values = null;
/**
* Created {@link Text} for filtering the table values.
*/
Text searchField = null;
/**
* Last descending sorted column to change to an ascending order on the next
* selection of the same column.
*/
TableColumn lastDescendingColumn = null;
/**
* image to scale on background (not created here -> not disposed here)
*/
private Image backgroundImage = null;
/**
* Scale the background image to the whole table size?
*/
boolean backgroundScaled = false;
/**
* {@link TableEditor} responsible for the manipulating of single values.
*/
TableEditor editor = null;
/**
* Constructor generates a new {@link SorTable} with visible headers and
* lines, but without any columns or entries.
*
* @param parent
* parent {@link Composite}
* @param style
* SWT style constants to apply
*/
public SorTable(final Composite parent, final int style) {
super(parent, SWT.NONE);
// initialize contained table
this.contentTable = new Table(this, style);
this.comparators = new HashMap<TableColumn, Comparator<TableItem>>();
// set default visibility settings
setHeaderVisible(true);
setLinesVisible(true);
// create popup menu for clipboard usage
final Menu popup = new Menu(getShell(), SWT.POP_UP);
final MenuItem copyItem = new MenuItem(popup, SWT.PUSH);
copyItem.setText(COPY_TEXT);
copyItem.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent event) {
// copy selected items to clipboard
copySelectionToClipboard();
}
});
this.contentTable.setMenu(popup);
addControlListener(new ControlAdapter() {
@Override
public void controlResized(final ControlEvent event) {
// manage correct width of columns
controlColumnSize();
if (SorTable.this.backgroundScaled) {
// recalculate background for new size
scaleBackground();
}
}
});
}
/**
* Generates a new {@link SorTable} with visible headers and lines, creates
* columns according to the specified headers, inserts the given contents
* and enables the sortability regarding the <code>COMPARE_DEFAULT</code>
* value. Other sorting methods (<code>COMPARE_TEXT</code>,
* <code>COMPARE_INT</code>, <code>COMPARE_DOUBLE</code>,
* <code>COMPARE_DATETIME</code>) need to be set separatly by calling
* <code>setSortingMethod()</code> for each targeted column.
*
* @param parent
* parent {@link Composite}
* @param style
* SWT style constants to apply
* @param header
* column topics to set
* @param initialValues
* contents to insert after initialization
* @return created {@link SorTable} instance
*/
public static SorTable createSorTable(final Composite parent,
final int style, final String[] header,
final String[][] initialValues) {
// invoke constructor
final SorTable created = new SorTable(parent, style);
// disable redraw to increase performance
created.setRedraw(false);
// generate all columns
for (String singleColumn : header) {
created.addColumn(SWT.NONE).setText(singleColumn);
}
// activate sortability
created.enableSorting(true);
if (initialValues != null) {
// insert initial values
created.resetValues(initialValues);
}
// manage width of columns
created.controlColumnSize();
// reactivate redraw
created.setRedraw(true);
return created;
}
/**
* Adds a new column without header.
*
* @param style
* SWT style constants to apply
* @return created column
*/
public TableColumn addColumn(final int style) {
return new TableColumn(this.contentTable, style);
}
/**
* Adds a new column with the specified header.
*
* @param style
* SWT style constants to apply
* @param topic
* column header to set
* @return created column
*/
public TableColumn addColumn(final int style, final String topic) {
final TableColumn column = addColumn(style);
column.setText(topic);
return column;
}
/**
* Adds a new column on the specified position without header.
*
* @param style
* SWT style constants to apply
* @param index
* where to insert the generated column
* @return created column
*/
public TableColumn addColumn(final int style, final int index) {
return new TableColumn(this.contentTable, style, index);
}
/**
* Adds a new column on the specified position with the given header.
*
* @param style
* SWT style constants to apply
* @param index
* where to insert the generated column
* @param topic
* column header to set
* @return created column
*/
public TableColumn addColumn(final int style, final int index,
final String topic) {
final TableColumn column = addColumn(style, index);
column.setText(topic);
return column;
}
/**
* Inserts a new entry with the given values on the end of the table.
*
* @param style
* SWT style constants to apply
* @param itemValues
* values to set
* @return created item
*/
public TableItem addItem(final int style, final String[] itemValues) {
return (addItem(style, this.contentTable.getItemCount(), itemValues));
}
/**
* Inserts a new entry with the given values on the specified position.
*
* @param style
* SWT style constants to apply
* @param index
* position where to insert the new table item
* @param itemValues
* values to set
* @return created item
*/
public TableItem addItem(final int style, final int index,
final String[] itemValues) {
final TableItem created = new TableItem(this.contentTable, style, index);
// copy values to display
final String[] text = new String[itemValues.length];
System.arraycopy(itemValues, 0, text, 0, itemValues.length);
created.setText(text);
if (!this.contentTable.getHeaderVisible()) {
/*
* user is unable to set the column width on its own: do it
* automatically
*/
for (int i = 1; i < this.contentTable.getColumnCount(); i++) {
this.contentTable.getColumn(i - 1).pack();
}
controlColumnSize();
}
if (this.values != null) {
// filtering is in use
synchronized (this.values) {
// store values for restoration
this.values.add(index, text);
}
/*
* if the item is disposed by anyone except the filter, the
* restoration should not bring this entry back
*/
created.addDisposeListener(new DisposeListener() {
public void widgetDisposed(DisposeEvent ev) {
if (SorTable.this.values != null) {
synchronized (SorTable.this.values) {
// remove associated values from restoration list
SorTable.this.values.remove(text);
}
}
}
});
}
return (created);
}
/**
* Inserts a new entry with the given values on the specified position by
* regarding the current sortation.
*
* @param style
* SWT style constants to apply
* @param itemValues
* values to set
* @return created item
*/
public TableItem insertItem(final int style, final String[] itemValues) {
// disable redraw to increase performance
this.contentTable.setRedraw(false);
// insert an item on the end of the table
final TableItem checkItem = addItem(style, itemValues);
// which column is currently selected for the sorting?
final TableColumn sortColumn = this.contentTable.getSortColumn();
TableItem insertedItem = checkItem;
if (sortColumn != null) {
// get items with the (temporary) entry
final TableItem[] items = this.contentTable.getItems();
// get the responsible comparator
final Comparator<TableItem> comparator = this.comparators
.get(sortColumn);
// sort items including the recently added one
Arrays.sort(items, comparator);
// where is the correct position of the item?
int index = Arrays.binarySearch(items, checkItem, comparator);
// what is the current sort direction?
final boolean descending = (this.lastDescendingColumn == sortColumn);
// only bother if it should not be at the end
if (descending || (index < (items.length - 1))) {
/*
* the inserted item should be treated as the largest of
* (possible) surrounding equals
*/
if (descending) {
// descending sort direction: larger = up
while (index > 0) {
if (comparator.compare(items[index - 1], items[index]) != 0) {
// compared item is larger: not further
break;
}
index--;
}
} else {
// ascending sort direction: larger = down
while (index < (items.length - 1)) {
if (comparator.compare(items[index], items[index + 1]) != 0) {
// compared item is larger: not further
break;
}
index++;
}
}
// remove the temporary item at the end
checkItem.dispose();
// insert new item on the correct position
insertedItem = addItem(style, index, itemValues);
}
}
// handle width of columns
controlColumnSize();
// reactivate redraw
this.contentTable.setRedraw(true);
return insertedItem;
}
/**
* Select the item at the specified position.
*
* @param index
* position of the item to select
*/
public void select(final int index) {
this.contentTable.select(index);
}
/**
* Select the items at the specified indizes
*
* @param indizes
* positions of the items to select
*/
public void select(final int[] indizes) {
this.contentTable.select(indizes);
}
/**
* Select all current entries of the table.
*/
public void selectAll() {
this.contentTable.selectAll();
}
/**
* Select the item at the specified position.
*
* @param index
* position of the item to select
*/
public void setSelection(final int index) {
this.contentTable.setSelection(index);
}
/**
* Select the items at the specified indizes
*
* @param indizes
* positions of the items to select
*/
public void setSelection(final int[] indizes) {
this.contentTable.setSelection(indizes);
}
/**
* Select the specified item
*
* @param target
* item to select
*/
public void setSelection(final TableItem target) {
this.contentTable.setSelection(target);
}
/**
* Select the specified items
*
* @param targets
* items to select
*/
public void setSelection(final TableItem[] targets) {
this.contentTable.setSelection(targets);
}
/**
* Set the visibility of the column headers.
*
* @param flag
* visible or not?
*/
public void setHeaderVisible(final boolean flag) {
this.contentTable.setHeaderVisible(flag);
}
/**
* Set the line visiblity.
*
* @param flag
* visible or not?
*/
public void setLinesVisible(final boolean flag) {
this.contentTable.setLinesVisible(flag);
}
/**
* Scroll visible part of the table to the specified item index.
*
* @param index
* position of the targeted table item
*/
public void setTopIndex(final int index) {
this.contentTable.setTopIndex(index);
}
/**
* Scroll visible part of the table to the specified item.
*
* @param target
* targeted table item
*/
public void setItemOnTop(final TableItem target) {
setTopIndex(Math.max(0, Arrays.asList(this.contentTable.getItems())
.indexOf(target)));
}
@Override
public void setBackground(final Color background) {
// avoid setting the background color of the composite behind the table
this.contentTable.setBackground(background);
}
/**
* @param index
* position of the requested column
* @return column on the specified position
*/
public TableColumn getColumn(final int index) {
return this.contentTable.getColumn(index);
}
/**
* @return all contained columns
*/
public TableColumn[] getColumns() {
return this.contentTable.getColumns();
}
/**
* @return number of contained columns
*/
public int getColumnCount() {
return this.contentTable.getColumnCount();
}
/**
* @param targetColumn
* targeted column to request
* @return position of the column
*/
public int getColumnIndex(final TableColumn targetColumn) {
return Arrays.asList(getColumns()).indexOf(targetColumn);
}
/**
* @return all currently displayed entries
*/
public TableItem[] getItems() {
return this.contentTable.getItems();
}
/**
* @param index
* position of the requested entry
* @return table entry on the specified position
*/
public TableItem getItem(final int index) {
return this.contentTable.getItem(index);
}
/**
* @return all currently selected entries
*/
public TableItem[] getSelection() {
return this.contentTable.getSelection();
}
/**
* @return position of the (first) selected entry
*/
public int getSelectionIndex() {
return this.contentTable.getSelectionIndex();
}
@Override
public void setFont(final Font value) {
// avoid setting the font of the composite behind the table
this.contentTable.setFont(value);
}
@Override
public void setBackgroundImage(final Image image) {
// avoid setting the background image of the composite behind the table
if (!this.backgroundScaled) {
this.backgroundImage = null;
this.contentTable.setBackgroundImage(image);
} else {
// scaling was enabled earlier
this.backgroundImage = image;
scaleBackground();
}
}
/**
* Sets the background image to the given value and enables/disables the
* scaling to the full size of the table.
*
* @param image
* background image to set
* @param scaled
* scale image to fill the whole background at once
*/
public void setBackgroundImage(final Image image, final boolean scaled) {
this.backgroundScaled = scaled;
setBackgroundImage(image);
}
/**
* Scale stored background image to fill the whole background of the table
* at once
*/
void scaleBackground() {
if (this.backgroundImage == null) {
// you are wasting my time...
return;
}
// what size are we talking about?
final Rectangle clientArea = this.contentTable.getClientArea();
if ((clientArea.width == 0) || (clientArea.height == 0)) {
// no need to try
return;
}
final Image oldImage = this.contentTable.getBackgroundImage();
if ((oldImage != null) && (oldImage != this.backgroundImage)
&& (!oldImage.isDisposed())) {
// clean up old scaled background image
oldImage.dispose();
}
// what image sould be scaled here?
final ImageData sourceData = this.backgroundImage.getImageData();
// calculate scaling factor
final double scale = Math.max(((double) clientArea.width)
/ sourceData.width, ((double) clientArea.height)
/ sourceData.height);
// execute scaling
final ImageData fittingData = sourceData.scaledTo(
(int) (sourceData.width * scale),
(int) (sourceData.height * scale));
// display the fitting image
this.contentTable.setBackgroundImage(new Image(getDisplay(),
fittingData));
}
/**
* Reset the displayed contents to the given values.
*
* @param newValues
* entries to display
*/
public void resetValues(final String[][] newValues) {
// deactivate redraw to increase performance
setRedraw(false);
final boolean filterActive = (this.values != null);
if (filterActive) {
// avoid multiple operations of the registered dispose listeners
this.values = null;
}
// remove all old entries
final TableItem[] oldItems = this.contentTable.getItems();
for (TableItem singleItem : oldItems) {
singleItem.dispose();
}
if (filterActive) {
// reset filter functionality
this.values = new ArrayList<String[]>();
}
if (newValues == null) {
// no new entries to display
this.contentTable.setItemCount(0);
} else {
// insert new entries
for (String[] singleEntry : newValues) {
addItem(SWT.NONE, singleEntry);
}
// manage width of columns
controlColumnSize();
}
if (!this.colorListeners.isEmpty()) {
final Event event = new Event();
event.widget = this;
event.doit = true;
// tell them to reapply the cell colors
for (Listener singleColorListener : this.colorListeners) {
singleColorListener.handleEvent(event);
}
}
setRedraw(true);
}
/**
* Register a listener, which is activated each time the colors of the table
* entries are lost. (each time the method <code>resetValues()</code> is
* invoked)
*
* @param target
* listener to register
*/
public void addColorListener(final Listener target) {
this.colorListeners.add(target);
}
/**
* Remove a listener, waiting for the loosage of cell colors.
*
* @param target
* listener to remove from registration
*/
public boolean removeColorListener(final Listener target) {
return this.colorListeners.remove(target);
}
/**
* Set the compare mode used when the specified column is selected to sort
* the table contents.<br>
* Available sorting methods are accessible by the representing constants (<code>COMPARE_TEXT</code>,
* <code>COMPARE_INT</code>, <code>COMPARE_DOUBLE</code>,
* <code>COMPARE_DATETIME</code>).
*
* @param targetColumn
* targeted column to be treated differently in sorting
* @param sortMethod
* how to compare the values of the targeted column in two
* different entries?
*/
public void setSortingMethod(final TableColumn targetColumn,
final int sortMethod) {
Comparator<TableItem> comparator;
// create the choosen comparator for the targeted column
switch (sortMethod) {
case (COMPARE_TEXT):
comparator = new Comparator<TableItem>() {
public int compare(final TableItem itemOne,
final TableItem itemTwo) {
return compareText(targetColumn, itemOne, itemTwo);
}
};
break;
case (COMPARE_INT):
comparator = new Comparator<TableItem>() {
public int compare(final TableItem itemOne,
final TableItem itemTwo) {
return compareInt(targetColumn, itemOne, itemTwo);
}
};
break;
case (COMPARE_DOUBLE):
comparator = new Comparator<TableItem>() {
public int compare(final TableItem itemOne,
final TableItem itemTwo) {
return compareDouble(targetColumn, itemOne, itemTwo);
}
};
break;
case (COMPARE_DATETIME):
comparator = new Comparator<TableItem>() {
public int compare(final TableItem itemOne,
final TableItem itemTwo) {
return compareDateTime(targetColumn, itemOne, itemTwo);
}
};
break;
default:
// invalid compare mode
throw new IllegalArgumentException();
}
// store created comparator for the targeted column
this.comparators.put(targetColumn, comparator);
}
/**
* Compares the values of the given column in the two specified entries as
* <code>String</code>s.
*
* @param targetColumn
* selected column
* @param itemOne
* first entry to compare
* @param itemTwo
* second entry to compare
* @return first entry is smaller, bigger or equals second entry
*/
int compareText(final TableColumn targetColumn, final TableItem itemOne,
final TableItem itemTwo) {
// get current position of the target column
final int columnIndex = getColumnIndex(targetColumn);
// compare values of the specified column in both entries
int result = itemOne.getText(columnIndex).compareToIgnoreCase(
itemTwo.getText(columnIndex));
if (SorTable.this.lastDescendingColumn == targetColumn) {
/*
* column was already sorted in descending order: invert sort
* direction
*/
result *= -1;
}
return result;
}
/**
* Compares the values of the given column in the two specified entries as
* <code>Integer</code>s.
*
* @param targetColumn
* selected column
* @param itemOne
* first entry to compare
* @param itemTwo
* second entry to compare
* @return first entry is smaller, bigger or equals second entry
*/
int compareInt(final TableColumn targetColumn, final TableItem itemOne,
final TableItem itemTwo) {
int result;
// get current position of the target column
final int columnIndex = getColumnIndex(targetColumn);
// compare values of the specified column in both entries
try {
// parse integer values
final Integer valueOne = Integer.valueOf(itemOne
.getText(columnIndex));
final Integer valueTwo = Integer.valueOf(itemTwo
.getText(columnIndex));
// compare successfully parsed values
result = valueOne.compareTo(valueTwo);
if (SorTable.this.lastDescendingColumn == targetColumn) {
/*
* column was already sorted in descending order: invert sort
* direction
*/
result *= -1;
}
} catch (NumberFormatException ex) {
// at least one value cannot be interpreted as an integer
result = compareText(targetColumn, itemOne, itemTwo);
}
return result;
}
/**
* Compares the values of the given column in the two specified entries as
* <code>Double</code>s.
*
* @param targetColumn
* selected column
* @param itemOne
* first entry to compare
* @param itemTwo
* second entry to compare
* @return first entry is smaller, bigger or equals second entry
*/
int compareDouble(final TableColumn targetColumn, final TableItem itemOne,
final TableItem itemTwo) {
int result;
// get current position of the target column
final int columnIndex = getColumnIndex(targetColumn);
// compare values of the specified column in both entries
try {
final String nothing = "";
// parse double values
final Double valueOne = Double.valueOf(itemOne.getText(columnIndex)
.replaceAll(REGEX_DOUBLE_IGNORE, nothing));
final Double valueTwo = Double.valueOf(itemTwo.getText(columnIndex)
.replaceAll(REGEX_DOUBLE_IGNORE, nothing));
// compare successfully parsed values
result = valueOne.compareTo(valueTwo);
if (SorTable.this.lastDescendingColumn == targetColumn) {
/*
* column was already sorted in descending order: invert sort
* direction
*/
result *= -1;
}
} catch (NumberFormatException ex) {
// at least one value cannot be interpreted as a double
result = compareText(targetColumn, itemOne, itemTwo);
}
return result;
}
/**
* Compares the values of the given column in the two specified entries as
* date strings in one of the following forms: <code>DD.</code>,
* <code>DD.MM.</code>, <code>DD.MM.YYYY</code>,
* <code>DD.MM.YYYY_hh:mm</code>, <code>DD.MM_hh:mm</code><br>
* Every character that is no number is considered to be a separator.<br>
* The single values are interpreted as <code>Integer</code>s.
*
* @param targetColumn
* selected column
* @param itemOne
* first entry to compare
* @param itemTwo
* second entry to compare
* @return first entry is smaller, bigger or equals second entry
*/
int compareDateTime(TableColumn targetColumn, TableItem itemOne,
TableItem itemTwo) {
int result;
// get current position of the target column
final int columnIndex = getColumnIndex(targetColumn);
try {
// read contained dates and compare them
result = parseDate(itemOne.getText(columnIndex)).compareTo(
parseDate(itemTwo.getText(columnIndex)));
if (SorTable.this.lastDescendingColumn == targetColumn) {
/*
* column was already sorted in descending order: invert sort
* direction
*/
result *= -1;
}
} catch (final ParseException ex) {
/*
* at least one of the contained strings do not fit on any of the
* given date formats
*/
result = compareText(targetColumn, itemOne, itemTwo);
}
return result;
}
/**
* Reads the given <code>String</code> expecting one of the given date
* formats:<br>
* <code>DD.</code>, <code>DD.MM.</code>, <code>DD.MM.YYYY</code>,
* <code>DD.MM.YYYY_hh:mm</code> or <code>DD.MM._hh:mm</code></br> All
* non-numeric characters are treated to be separators. Empty segments are
* ignored.
*
* @param dateString
* date <code>String</code> to parse
* @return contained date value
* @throws ParseException
* failed to parse given format or <code>String</code> did not
* match any allowed pattern
*/
private Date parseDate(final String dateString) throws ParseException {
// replace separators with whitespaces to use trim()
final String cleanValue = dateString.replaceAll(
REGEX_DATETIME_SEPARATOR, " ").trim();
// get the Formatter for the matching pattern
SimpleDateFormat formatter;
if (Pattern.matches(REGEX_DATETIME_DD, cleanValue)) {
formatter = FORMAT_DATETIME_DD;
} else if (Pattern.matches(REGEX_DATETIME_DDMM, cleanValue)) {
formatter = FORMAT_DATETIME_DDMM;
} else if (Pattern.matches(REGEX_DATETIME_DDMMYYYY, cleanValue)) {
formatter = FORMAT_DATETIME_DDMMYYYY;
} else if (Pattern.matches(REGEX_DATETIME_DDMM_HHMM, cleanValue)) {
formatter = FORMAT_DATETIME_DDMM_HHMM;
} else if (Pattern.matches(REGEX_DATETIME_DDMMYYYY_HHMM, cleanValue)) {
formatter = FORMAT_DATETIME_DDMMYYYY_HHMM;
} else {
// does not fit on any given pattern
throw new ParseException(dateString, 0);
}
synchronized (formatter) {
// parse the contained date
return formatter.parse(cleanValue);
}
}
/**
* Activates/Deactivates the sortability of the currently contained columns.
*
* @param aFlag
* activate or deactivate?
*/
public void enableSorting(final boolean aFlag) {
// remove sort column highlighting
this.contentTable.setSortColumn(null);
for (final TableColumn singleColumn : this.contentTable.getColumns()) {
if (aFlag && (!singleColumn.isListening(SWT.Selection))) {
// activate sortability if it is not already sortable
singleColumn.addListener(SWT.Selection, new Listener() {
public void handleEvent(final Event ev) {
// execute sorting by the selected column
sortTable(singleColumn);
}
});
}
if (!aFlag) {
// remove all listeners to deactivate sortability
for (Listener singleListener : singleColumn
.getListeners(SWT.Selection)) {
singleColumn.removeListener(SWT.Selection, singleListener);
}
}
}
controlColumnSize();
}
/**
* Sort the currently contained table values by the specified column<br>
* In case this column was the last sorted one, the sort direction is
* inverted.
*
* @param targetColumn
* column to sort the values by
*/
public void sortTable(final TableColumn targetColumn) {
// executing sorting
sortByColumn(targetColumn);
// show which column was selected for sorting
this.contentTable.setSortColumn(targetColumn);
// determine sort direction
int sortDirection;
if (this.lastDescendingColumn == targetColumn) {
/*
* column was already sorted (descending); sort direction is
* inverted (ascending)
*/
sortDirection = SWT.UP;
// next time the column should be sorted descending again
this.lastDescendingColumn = null;
} else {
// sort direction by default: descending
sortDirection = SWT.DOWN;
// remember descending sorted column
this.lastDescendingColumn = targetColumn;
}
// show sort direction
this.contentTable.setSortDirection(sortDirection);
}
/**
* Sort the table values referring to the entries in the given column.
*
* @param targetColumn
* column to regard the entries in
*/
private void sortByColumn(final TableColumn targetColumn) {
// ensure a sorting method is selected for the given column
if (!this.comparators.containsKey(targetColumn)) {
// no sorting method stored: use default
setSortingMethod(targetColumn, COMPARE_DEFAULT);
}
// is a search filter in use?
final boolean filterInUse = (this.searchField != null)
&& (this.searchField.getText() != null)
&& (this.searchField.getText().length() > 0);
// deactivate redraw to increase performance
setRedraw(false);
if (filterInUse && (this.values != null)) {
// sort all entries (not only the filtered values)
resetValues(this.values.toArray(new String[this.values.size()][]));
}
final TableItem[] items = getItems();
// sort talbe entries in the array
Arrays.sort(items, this.comparators.get(targetColumn));
/*
* display the changed entry order
*/
final int columnCount = getColumnCount();
final String[] itemValues = new String[columnCount];
final Color[] backgrounds = new Color[columnCount];
final Color[] foregrounds = new Color[columnCount];
for (int i = 0; i < items.length; i++) {
final TableItem targetItem = items[items.length - 1 - i];
// collect values of the single entry
for (int j = 0; j < columnCount; j++) {
itemValues[j] = targetItem.getText(j);
}
// remember cell colors
for (int j = 0; !filterInUse && j < columnCount; j++) {
backgrounds[j] = targetItem.getBackground(j);
foregrounds[j] = targetItem.getForeground(j);
}
// remove old entry from view
targetItem.dispose();
// create new entry with the collected values on the right position
final TableItem newItem = addItem(SWT.NONE, i, itemValues);
// restore cell colors
for (int j = 0; !filterInUse && j < columnCount; j++) {
newItem.setBackground(j, backgrounds[j]);
newItem.setForeground(j, foregrounds[j]);
}
}
if (filterInUse) {
// reapply the previously used filter on the sorted table
applyFilter();
}
// reactivate visual representation
setRedraw(true);
}
/**
* Generate a {@link Text} which serves as a filter input for the table
* values.<br>
* The background colors of the single table items are lost each time the
* filter changes.
* <p>
* It is impossible to create a second search field for the same table.
*
* @param parent
* parent {@link Composite}
* @param style
* SWT style constants to apply
* @return created search field
*/
public Text createSearchField(Composite parent, int style) {
if (this.searchField != null) {
// another search field already exists
throw new IllegalStateException();
}
final int columnCount = this.contentTable.getColumnCount();
// initialize fallback structure
this.values = new ArrayList<String[]>();
synchronized (this.values) {
// copy current state
for (TableItem singleItem : this.contentTable.getItems()) {
final String[] itemValues = new String[columnCount];
for (int i = 0; i < columnCount; i++) {
itemValues[i] = singleItem.getText(i);
}
this.values.add(itemValues);
/*
* an explicite dispose()-call on an entry should remove the
* entry permanently and not only until the filter changes
*/
singleItem.addDisposeListener(new DisposeListener() {
public void widgetDisposed(final DisposeEvent ev) {
if (SorTable.this.values != null) {
// filter still active
synchronized (SorTable.this.values) {
// permanently remove the entry
SorTable.this.values.remove(itemValues);
}
}
}
});
}
}
// create the search filter
this.searchField = new Text(parent, style);
this.searchField.addModifyListener(new ModifyListener() {
public void modifyText(final ModifyEvent ev) {
synchronized (this) {
// apply the currently inserted filter
applyFilter();
}
}
});
this.searchField.addDisposeListener(new DisposeListener() {
public void widgetDisposed(final DisposeEvent ev) {
// deactivate sortability
SorTable.this.values = null;
SorTable.this.searchField = null;
}
});
return this.searchField;
}
/**
* Apply the currently inserted filter on all table entries (not only the
* currently visible ones).
*/
void applyFilter() {
// deactivate redraw to increase performance
setRedraw(false);
// restore all entries
resetValues(this.values.toArray(new String[this.values.size()][]));
final String filter = this.searchField.getText();
if ((filter != null) && (filter.length() > 0)) {
// a filter is set: apply on all entries
for (TableItem singleItem : getItems()) {
if (!filterMatchesItem(singleItem, filter)) {
/*
* single entry does not match the specified filter; its
* removal should not affect the stored list of entries
*/
for (Listener singleListener : singleItem
.getListeners(SWT.Dispose)) {
singleItem.removeListener(SWT.Dispose, singleListener);
}
// remove unfitting entry from view
singleItem.dispose();
}
}
}
// reactivate redraw
setRedraw(true);
layout();
}
/**
* Check if the given table entry matches the specified filter. A
* <code>-</code> in front of a single term inverts its result.
*
* @param item
* table entry to check
* @param searchFilter
* search filter to apply (whitespace separated terms)
* @return given entry matches the specified filter
*/
boolean filterMatchesItem(final TableItem item, final String searchFilter) {
// check aech of the whitespace separated items
for (String singleTerm : searchFilter.split("[\\s]")) {
if (!termMatchesItem(item, singleTerm)) {
// entry does not match this term
return false;
}
}
// entry matches the whole filter
return true;
}
/**
* Check if the given table entry matches the specified filter term. A
* <code>-</code> in front inverts its result.
*
* @param item
* table entry to check
* @param searchTerm
* filter term to apply
* @return <code>TRUE</code> : term contained OR term not contained and a
* <code>-</code> in front<br>
* <code>FALSE</code> : term not contained OR term contained and a
* <code>-</code> in front
*/
private boolean termMatchesItem(final TableItem item,
final String searchTerm) {
final boolean noMatch = searchTerm.startsWith("-");
String term = searchTerm.toLowerCase();
if (noMatch) {
// ignore invert character (-)
term = term.substring(1);
}
if (term.length() == 0) {
// no term to apply
return true;
}
// look for the term in all columns
for (int i = 0; i < this.contentTable.getColumnCount(); i++) {
if (item.getText(i).toLowerCase().contains(term)) {
// term found in this column
return (!noMatch);
}
}
// term is not contained in the given entry
return noMatch;
}
/**
* Enables/Disables the possibility to manipulate single table entries.
*
* @param flag
* activate or deactivate editability?
* @param editableColumns
* which columns should be available for manipulation? (no
* selection equals all)<br>
* if the flag is <code>FALSE</code> the editability is
* deactivated for alle columns.
*/
public void setEditable(final boolean flag, final int... editableColumns) {
if (this.editor != null) {
// remove old editor and all MouseDown listeners
final Listener[] listeners = this.contentTable
.getListeners(SWT.MouseDown);
for (Listener singleMouseDownListener : listeners) {
this.contentTable.removeListener(SWT.MouseDown,
singleMouseDownListener);
}
this.editor.dispose();
}
if (!flag) {
// deactivation finished
this.editor = null;
return;
}
// create a new TableEditor
this.editor = new TableEditor(this.contentTable);
this.editor.horizontalAlignment = SWT.LEFT;
this.editor.grabHorizontal = true;
// handle mouse clicks over table
this.contentTable.addListener(SWT.MouseDown, new Listener() {
public void handleEvent(final Event mouseDownEvent) {
// where was it?
final Point clickPosition = new Point(mouseDownEvent.x,
mouseDownEvent.y);
final TableItem item = SorTable.this.contentTable
.getItem(clickPosition);
if (item != null) {
// click occured over an entry
editItem(item, clickPosition, editableColumns);
}
}
private void editItem(final TableItem item,
final Point clickPosition, final int[] editableColumns) {
// check if the click was on an editable column
for (int i = 0; i < getColumnCount(); i++) {
// where is the single cell?
final Rectangle cellPosition = item.getBounds(i);
if (cellPosition.contains(clickPosition)) {
// selected cell found
editCell(item, i, editableColumns);
break;
}
}
}
void editCell(final TableItem item, final int columnIndex,
final int[] editableColumns) {
if ((editableColumns != null) && (editableColumns.length > 0)) {
for (int j = 0; j < editableColumns.length; j++) {
if (editableColumns[j] == columnIndex) {
/*
* selected column is enabled for manipulation
*/
break;
}
if ((j + 1) == editableColumns.length) {
/*
* selected column is not enabled for manipulation
*/
return;
}
}
}
/*
* create manipulation input
*/
final int column = columnIndex;
final Text text = new Text(SorTable.this.contentTable, SWT.NONE);
text.setFont(SorTable.this.contentTable.getFont());
text.addListener(SWT.FocusOut, new Listener() {
// manipulation field lost the focus
public void handleEvent(final Event ev) {
final String newValue = text.getText();
if (!newValue.equals(item.getText(column))) {
// store changed value
item.setText(column, newValue);
reinsertChangedItem(item);
}
// remove maniulation field
text.dispose();
}
});
text.addListener(SWT.Traverse, new Listener() {
// manipulation field was traversed (e.g. TAB)
public void handleEvent(final Event ev) {
TableItem newItem = null;
if (ev.detail != SWT.TRAVERSE_ESCAPE) {
final String newValue = text.getText();
if (newValue.equals(item.getText(column))) {
newItem = item;
} else {
// store changed value
item.setText(column, newValue);
newItem = reinsertChangedItem(item);
}
}
// remove maniulation field
text.dispose();
if (newItem != null) {
final int index = SorTable.this.contentTable
.indexOf(newItem);
switch (ev.detail) {
case SWT.TRAVERSE_ARROW_NEXT:
if (ev.keyCode == SWT.ARROW_DOWN) {
ev.doit = (index + 2) > SorTable.this.contentTable
.getItemCount();
if (!ev.doit) {
editCell(getItem(index + 1),
columnIndex, editableColumns);
}
break;
} // else { FALL THROUGH }
case SWT.TRAVERSE_TAB_NEXT:
ev.doit = !traverseNext(newItem, columnIndex,
editableColumns);
break;
case SWT.TRAVERSE_ARROW_PREVIOUS:
if (ev.keyCode == SWT.ARROW_UP) {
ev.doit = (index < 1);
if (!ev.doit) {
editCell(getItem(index - 1),
columnIndex, editableColumns);
}
break;
} // else { FALL THROUGH }
case SWT.TRAVERSE_TAB_PREVIOUS:
ev.doit = !traversePrevious(newItem,
columnIndex, editableColumns);
break;
}
}
}
});
// register manipulation field
SorTable.this.editor.setEditor(text, item, column);
// preset field with current value
text.setText(item.getText(column));
// select value and request focus
text.selectAll();
text.setFocus();
}
boolean traverseNext(final TableItem item, final int columnIndex,
final int[] editableColumns) {
int nextColumn;
if ((editableColumns == null) || (editableColumns.length == 0)) {
nextColumn = columnIndex + 1;
} else {
nextColumn = getColumnCount();
for (int singleColumn : editableColumns) {
if (singleColumn > columnIndex) {
nextColumn = Math.min(nextColumn, singleColumn);
}
}
}
if (nextColumn < getColumnCount()) {
editCell(item, nextColumn, editableColumns);
} else {
final TableItem[] items = getItems();
final int itemIndex = Arrays.asList(items).indexOf(item);
if (itemIndex < (items.length - 1)) {
int firstColumn;
if ((editableColumns == null)
|| (editableColumns.length == 0)) {
firstColumn = 0;
} else {
firstColumn = editableColumns[0];
for (int singleColumn : editableColumns) {
firstColumn = Math.min(firstColumn,
singleColumn);
}
}
editCell(items[itemIndex + 1], firstColumn,
editableColumns);
} else {
return false;
}
}
return true;
}
boolean traversePrevious(final TableItem item,
final int columnIndex, final int[] editableColumns) {
int previousColumn;
if ((editableColumns == null) || (editableColumns.length == 0)) {
previousColumn = columnIndex - 1;
} else {
previousColumn = -1;
for (int singleColumn : editableColumns) {
if (singleColumn < columnIndex) {
previousColumn = Math.max(previousColumn,
singleColumn);
}
}
}
if (previousColumn > -1) {
editCell(item, previousColumn, editableColumns);
} else {
final TableItem[] items = getItems();
final int itemIndex = Arrays.asList(items).indexOf(item);
if (itemIndex > 0) {
int lastColumn;
if ((editableColumns == null)
|| (editableColumns.length == 0)) {
lastColumn = getColumnCount() - 1;
} else {
lastColumn = 0;
for (int singleColumn : editableColumns) {
lastColumn = Math.max(lastColumn, singleColumn);
}
}
editCell(items[itemIndex - 1], lastColumn,
editableColumns);
} else {
return false;
}
}
return true;
}
});
}
/**
* Removes the given entry from the table and inserts a new one with the
* same values at the same position in the same colors, to ensure the change
* of an entrys values is stored properly for a possible filter usage.
* <p>
* The current filter is reapplied on the table (including the new entry)
* and might remove the created entry from view.
* </p>
*
* @param item
* entry to remove and recreate
* @return reinserted entry<br>
* returns <code>NULL</code> if the created entry does not match
* the current filter</br>
*/
TableItem reinsertChangedItem(final TableItem item) {
setRedraw(false);
// get current entry position
final int index = this.contentTable.indexOf(item);
// collect changed values
final String[] values = new String[getColumnCount()];
// remember colors
final Color[] backgrounds = new Color[values.length];
final Color[] foregrounds = new Color[values.length];
for (int i = 0; i < values.length; i++) {
values[i] = item.getText(i);
backgrounds[i] = item.getBackground(i);
foregrounds[i] = item.getForeground(i);
}
// remove old entry from view
item.dispose();
TableItem changedItem = addItem(SWT.NONE, index, values);
// restore cell colors
for (int i = 0; i < values.length; i++) {
changedItem.setBackground(i, backgrounds[i]);
changedItem.setForeground(i, foregrounds[i]);
}
if ((SorTable.this.searchField != null)
&& (SorTable.this.searchField.getText() != null)
&& (SorTable.this.searchField.getText().length() > 0)) {
/*
* reapply the previously used filter (might not fit anymore on the
* changed item)
*/
final int itemCount = this.contentTable.getItemCount();
applyFilter();
if (itemCount == this.contentTable.getItemCount()) {
// filter did not remove changed item
changedItem = this.contentTable.getItem(index);
} else {
// filter removed changed item
changedItem = null;
}
}
// reactivate visual representation
setRedraw(true);
return changedItem;
}
/**
* Manage the column widths and expand the last one to fill the available
* space
*/
void controlColumnSize() {
// deactivate redraw to increase performance
this.contentTable.setRedraw(false);
final Rectangle area = getClientArea();
// calculate the width available for the table entries
int width = area.width
- SorTable.this.contentTable.computeTrim(0, 0, 0, 0).width;
final Point oldSize = this.contentTable.getSize();
final TableColumn lastColumn = SorTable.this.contentTable
.getColumn(getColumnCount() - 1);
// calculate how much space is left for the last column
for (TableColumn singleColumn : this.contentTable.getColumns()) {
if (singleColumn != lastColumn) {
width -= singleColumn.getWidth();
}
}
if (oldSize.x > area.width) {
/*
* table is getting smaller: first decrease column width to fit
*/
lastColumn.pack();
if (lastColumn.getWidth() < width) {
lastColumn.setWidth(width);
}
this.contentTable.setSize(area.width, area.height);
} else {
/*
* table is getting bigger: first increase table width to fit
*/
this.contentTable.setSize(area.width, area.height);
lastColumn.pack();
if (lastColumn.getWidth() < width) {
lastColumn.setWidth(width);
}
}
this.contentTable.setRedraw(true);
}
/**
* All currently displayed table entries in a two-dimensional
* <code>String</code> array. (regarding active filter and sorting)
*
* @return current table entries
*/
public String[][] getValues() {
final TableItem[] items = getItems();
final int columnCount = getColumnCount();
final String[][] values = new String[items.length][];
for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
// copy all entries
final String[] singleItem = new String[columnCount];
for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) {
// get each cell value
singleItem[columnIndex] = items[itemIndex].getText(columnIndex);
}
values[itemIndex] = singleItem;
}
return values;
}
/**
* Copy the currently selected table entries to the clipboard.<br>
* Each entry takes up one line and the single column values are separated
* by tabs.
*/
public void copySelectionToClipboard() {
final TableItem[] selection = this.contentTable.getSelection();
if ((selection == null) || (selection.length == 0)) {
// no selection: nothing to do
return;
}
final String newLine = System.getProperty("line.separator");
final char tab = '\t';
final int columnCount = getColumnCount();
final StringBuffer valueCollection = new StringBuffer();
for (int i = 0; i < selection.length; i++) {
for (int j = 0; j < columnCount; j++) {
valueCollection.append(selection[i].getText(j));
// separate columns by tabs
if ((j + 1) < columnCount) {
valueCollection.append(tab);
}
}
// insert line separator
if ((i + 1) < selection.length) {
valueCollection.append(newLine);
}
}
// copy collected values to clipboard
new Clipboard(getDisplay()).setContents(new Object[] { valueCollection
.toString() }, new Transfer[] { TextTransfer.getInstance() });
}
/**
* Print the current table state (regarding filter and sorting).<br>
* The user selects the general print setting in a {@link PrintDialog}.
* <p>
* All but the printer parameter are optional and do not need to be set.
*
* @param printData
* {@link PrintData} to use
* @param topic
* topic to display on top of the first page (optional)
* @param jobTitle
* print job description (optional)
* @param wrapColumn
* index of the column to be able to wrap its content on multiple
* lines if neccessary (optional)
* @param tableFont
* {@link Font} to use for all table entries (optional)
* @param topicFont
* {@link Font} to use for the topic (optional)
*/
public void print(final PrinterData printData, final String topic,
final String jobTitle, final int wrapColumn, final FontData[] tableFont,
final FontData[] headerFont, final FontData[] topicFont) {
new SWTTablePrinter(this.contentTable).print(topic, jobTitle,
printData, wrapColumn, tableFont, headerFont, topicFont);
}
}
SWTTablePrinter
Java:
package org.hermeneutix.utilities.swt;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.printing.Printer;
import org.eclipse.swt.printing.PrinterData;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
/**
* Offers the ability to print a single SWT-{@link Table} by regarding custom
* {@link Font}s and {@link Printer} settings as well as the current header and
* line visiblity.<br>
* The page number is displayed on the bottom and a topic can be added on the
* top of the first page.<br>
* The width of each column equals the maximum width of the contained values.
* One column can be selected as the <code>wrapColumn</code>. The values of
* this column are wrapped in multiple lines if the needed width is bigger than
* the space that is left.
* <p>
* The printing fails if the minimum width of all columns exceeds the available
* space depending on the chosen page format or the <code>wrapColumn</code>
* generates an entry longer than one page.
*
* @author Carsten Englert
*/
public final class SWTTablePrinter {
/**
* topic of the message box displaying an occured error
*/
private static final String ERROR_TOPIC = "Error";
/**
* first part of the displayed error message if the <code>wrapColumn</code>
* generated an entry longer than one page
*/
private static final String ERROR_ENTRYLENGTH1 = "Entry in the ";
/**
* second part of the displayed error message if the <code>wrapColumn</code>
* generated an entry longer than one page
*/
private static final String ERROR_ENTRYLENGTH2 = ". column "
+ "exceeds the height of one page in the chosen format.\n"
+ "Printing Aborted.";
/**
* displayed error message if the minimum width of all columns exceeds the
* available space depending on the chosen page format
*/
private static final String ERROR_TABLEWIDTH = "Calculated "
+ "table width exceeds the chosen page format.\n"
+ "Print Job Canceled.";
/**
* {@link Font} to use for the topic if no other one is selected
*/
private static final String DEFAULT_TOPIC_FONT_NAME = "Times New Roman";
/**
* font size to use for the topic if no other {@link Font} is selected
*/
private static final int DEFAULT_TOPIC_FONT_SIZE = 16;
/**
* font style to set for the topic if no other {@link Font} is selected
*/
private static final int DEFAULT_TOPIC_FONT_STYLE = SWT.BOLD;
/**
* {@link Font} to use for the table headers if no other one is selected
*/
private static final String DEFAULT_HEADER_FONT_NAME = "MS Times New Roman";
/**
* font size to use for the table headers if no other {@link Font} is
* selected
*/
private static final int DEFAULT_HEADER_FONT_SIZE = 11;
/**
* font style to set for the table headers of no other {@link Font} is
* selected
*/
private static final int DEFAULT_HEADER_FONT_STYLE = SWT.BOLD;
/**
* {@link Font} to use for the table contents if no other one is selected
*/
private static final String DEFAULT_TABLE_FONT_NAME = "MS Times New Roman";
/**
* font size to use for the table contents if no other {@link Font} is
* selected
*/
private static final int DEFAULT_TABLE_FONT_SIZE = 10;
/**
* font style to set for the table contents of no other {@link Font} is
* selected
*/
private static final int DEFAULT_TABLE_FONT_STYLE = SWT.NONE;
final Table printSource;
private Printer printer;
GC gc;
Font tableFont, headerFont;
private int headerOffset, leftMargin, rightMargin, topMargin, bottomMargin,
bottomEnd, endX, lineHeight, horizontalSpacing, verticalSpacing,
posY, tableTop, wrapXstart, wrapXend, pageCount;
private int[] columnPosX;
private boolean headersVisible, linesVisible;
private String[] headTitles;
private Image headerImage, footerImage;
/**
* Constructor stores the targeted SWT-{@link Table}.
*
* @param printSource
* SWT-{@link Table} to print on <code>print()</code> call
*/
public SWTTablePrinter(final Table printSource) {
this.printSource = printSource;
}
/**
* Calculates the print area in the chosen page format.<br>
* Might be used to determine the target size for the header and/or footer.</br>
*
* @param printData
* selected printer and page format
* @return calculated print area
*/
public static Rectangle calculatePrintArea(final PrinterData printData) {
final SWTTablePrinter instance = new SWTTablePrinter(null);
// initialize needed printer and GC
instance.printer = new Printer(printData);
instance.gc = new GC(instance.printer);
// calculate page format and print area
instance.calculatePageFormat();
// clean up resources
instance.gc.dispose();
instance.printer.dispose();
// return the calculated print area
return new Rectangle(instance.leftMargin, instance.topMargin,
instance.rightMargin - instance.leftMargin,
instance.bottomMargin - instance.topMargin);
}
/**
* Prints the SWT-{@link Table} on the specified {@link Printer} by
* wrapping the values in the column on the specified
* <code>wrapColumn</code>-Index if neccessary. The width of each column
* equals the maximum width of the contained values. If the calculated width
* exceeds the available space, the <code>wrapColumn</code> is set to the
* available space and enabled to fill multiple lines where needed.
* <p>
* In this case the given topic-<code>String</code> is transfered to an
* image and printed (in the specified topicFont) on top of the first page.
* </p>
*
* @param topic
* topic to print on the top of the first page (optional)
* @param jobTitle
* name of the print job (optional)
* @param printData
* targeted printer regarding the selected options and page
* format
* @param wrapColumn
* index of the column to be wrapped in case of need
* @param tableEntryFont
* {@link Font} to use for the table contents (optional)
* @param tableHeaderFont
* {@link Font} to use for the table headers (optional)
* @param topicFont
* {@link Font} to use for the table headers (optional)
*/
public void print(final String topic, final String jobTitle,
final PrinterData printData, final int wrapColumn,
final FontData[] tableEntryFont, final FontData[] tableHeaderFont,
final FontData[] topicFont) {
print(topic, null, 0, null, jobTitle, printData, wrapColumn,
tableEntryFont, tableHeaderFont, topicFont);
}
/**
* Prints the SWT-{@link Table} on the specified {@link Printer} by
* wrapping the values in the column on the specified
* <code>wrapColumn</code>-Index if neccessary. The width of each column
* equals the maximum width of the contained values. If the calculated width
* exceeds the available space, the <code>wrapColumn</code> is set to the
* available space and enabled to fill multiple lines where needed.
* <p>
* In this case the given topic-<code>String</code> is transfered to an
* image and printed (in the specified topicFont) on top of the first page.
* The given footer image is applied to the bottom of the last page.
* </p>
*
* @param topic
* topic to print on the top of the first page (optional)
* @param topicOffset
* vertical position correction for the topic (optional)<br>
* case = 0 : top edge of the topic is the top edge of the print
* area (regarding top margin)</br> case < 0 : top edge of the
* topic is over the top edge of the print area (in the top
* margin)<br>
* case > 0 : top edge of the topic is below the top edge of the
* print area (additional top margin on first site)</br>
* @param footer
* footer to print on the bottom of the last page (optional)
* @param jobTitle
* name of the print job (optional)
* @param printData
* targeted printer regarding the selected options and page
* format
* @param wrapColumn
* index of the column to be wrapped in case of need
* @param tableEntryFont
* {@link Font} to use for the table contents (optional)
* @param tableHeaderFont
* {@link Font} to use for the table headers (optional)
*/
public void print(final String topic, final int topicOffset,
final ImageData footer, final String jobTitle,
final PrinterData printData, final int wrapColumn,
final FontData[] tableEntryFont, final FontData[] tableHeaderFont) {
print(topic, null, topicOffset, footer, jobTitle, printData,
wrapColumn, tableEntryFont, tableHeaderFont, null);
}
/**
* Prints the SWT-{@link Table} on the specified {@link Printer} by
* wrapping the values in the column on the specified
* <code>wrapColumn</code>-Index if neccessary. The width of each column
* equals the maximum width of the contained values. If the calculated width
* exceeds the available space, the <code>wrapColumn</code> is set to the
* available space and enabled to fill multiple lines where needed.
* <p>
* In this case the given images for header and footer are applied to the
* first (header) and the last (footer) page.
* </p>
*
* @param header
* header to print on the top of the first page (optional)
* @param headerOffset
* vertical position correction for the header (optional)<br>
* case = 0 : top edge of the header is the top edge of the print
* area (regarding top margin)</br> case < 0 : top edge of the
* header is over the top edge of the print area (in the top
* margin)<br>
* case > 0 : top edge of the header is below the top edge of the
* print area (additional top margin on first site)</br>
* @param footer
* footer to print on the bottom of the last page (optional)
* @param jobTitle
* name of the print job (optional)
* @param printData
* targeted printer regarding the selected options and page
* format
* @param wrapColumn
* index of the column to be wrapped in case of need
* @param tableEntryFont
* {@link Font} to use for the table contents (optional)
* @param tableHeaderFont
* {@link Font} to use for the table headers (optional)
*/
public void print(final ImageData header, final int headerOffset,
final ImageData footer, final String jobTitle,
final PrinterData printData, final int wrapColumn,
final FontData[] tableEntryFont, final FontData[] tableHeaderFont) {
print(null, header, headerOffset, footer, jobTitle, printData,
wrapColumn, tableEntryFont, tableHeaderFont, null);
}
/**
* Prints the SWT-{@link Table} on the specified {@link Printer} by
* wrapping the values in the column on the specified
* <code>wrapColumn</code>-Index if neccessary. The width of each column
* equals the maximum width of the contained values. If the calculated width
* exceeds the available space, the <code>wrapColumn</code> is set to the
* available space and enabled to fill multiple lines where needed.
* <p>
* Combines the three possible <code>print()</code> calls:
* <ul>
* <li>if the topic <code>String</code> is set, it's transfered to an
* image and stored as the header. That's why the given header image is
* ignored, when the topic parameter is not <code>null</code>.</li>
* <li>the header (either the transfered topic or the given image) is
* printed on top of the first page</li>
* <li>the footer is printed on the bottom of the last page</li>
* </ul>
* </p>
*
* @param topic
* topic to print on the top of the first page (optional)<br>
* if a topic is set, the header is ignored</br>
* @param header
* header to print on the top of the first page (optional)
* @param headerOffset
* vertical position correction for the header (optional)<br>
* case = 0 : top edge of the header is the top edge of the print
* area (regarding top margin)</br> case < 0 : top edge of the
* header is over the top edge of the print area (in the top
* margin)<br>
* case > 0 : top edge of the header is below the top edge of the
* print area (additional top margin on first site)</br>
* @param footer
* footer to print on the bottom of the last page (optional)
* @param jobTitle
* name of the print job (optional)
* @param printData
* targeted printer regarding the selected options and page
* format
* @param wrapColumn
* index of the column to be wrapped in case of need
* @param tableEntryFont
* {@link Font} to use for the table contents (optional)
* @param tableHeaderFont
* {@link Font} to use for the table headers (optional)
* @param topicFont
* {@link Font} to use for the table headers (optional) <br>
* if a header is set, this {@link Font} is ignored</br>
*/
private void print(final String topic, final ImageData header,
final int headerOffset, final ImageData footer,
final String jobTitle, final PrinterData printData,
final int wrapColumn, final FontData[] tableEntryFont,
final FontData[] tableHeaderFont, final FontData[] topicFont) {
// store selected offset
this.headerOffset = headerOffset;
this.printer = new Printer(printData);
if (tableEntryFont == null) {
// no font selected, use the default
this.tableFont = new Font(this.printer, DEFAULT_TABLE_FONT_NAME,
DEFAULT_TABLE_FONT_SIZE, DEFAULT_TABLE_FONT_STYLE);
} else {
// transfer selected font to new context
this.tableFont = new Font(this.printer, tableEntryFont);
}
if (tableHeaderFont == null) {
// no font selected, use the default
this.headerFont = new Font(this.printer, DEFAULT_HEADER_FONT_NAME,
DEFAULT_HEADER_FONT_SIZE, DEFAULT_HEADER_FONT_STYLE);
} else {
// transfer selected font to new context
this.headerFont = new Font(this.printer, tableHeaderFont);
}
this.gc = new GC(this.printer);
// regard chosen font for the table contents
this.gc.setFont(this.tableFont);
// prepare printing
calculatePageFormat();
if (topic != null) {
// transfer topic String to header Image
this.headerImage = createTopicHeader(topic, topicFont);
} else if (header == null) {
this.headerImage = null;
} else {
this.headerImage = new Image(this.printer, header);
}
if (footer == null) {
this.footerImage = null;
} else {
this.footerImage = new Image(this.printer, footer);
}
// regard header visiblity of the targeted SWT-Table
this.headersVisible = this.printSource.getHeaderVisible();
// regard line visiblity of the targeted SWT-Table
this.linesVisible = this.printSource.getLinesVisible();
final TableColumn[] columns = this.printSource.getColumns();
// store table headers
this.headTitles = new String[columns.length];
for (int i = 0; i < columns.length; i++) {
this.headTitles[i] = columns[i].getText();
if (this.headTitles[i] == null) {
// replace invalid topic value
this.headTitles[i] = "";
}
}
// collect all values to print
final TableItem[] items = this.printSource.getItems();
final String[][] allLines = new String[this.printSource.getItemCount()][];
for (int i = 0; i < allLines.length; i++) {
final String[] line = new String[columns.length];
for (int j = 0; j < columns.length; j++) {
line[j] = items[i].getText(j);
if (line[j] == null) {
// replace invalid item value
line[j] = "";
}
}
allLines[i] = line;
}
// execute the printing in an extra thread to release the GUI
new Thread() {
@Override
public void run() {
printWithoutSourceAccess(allLines, jobTitle, wrapColumn);
}
}.start();
}
/**
* Executes the printing without referring to the SWT-Table
*
* @param allLines
* table entries to print
* @param jobTitle
* print job title to set
* @param wrapColumn
* index of the column to wrapped on multiple lines
*/
void printWithoutSourceAccess(final String[][] allLines,
final String jobTitle, final int wrapColumn) {
if (!calculateColumnPositions(allLines, wrapColumn)) {
// error occured, abort
return;
}
// finished initial calculations; start printing
this.printer.startJob(jobTitle);
this.pageCount = 0;
newPage(this.headersVisible);
// print all entries
for (String[] singleLine : allLines) {
if (!printLine(singleLine, wrapColumn)) {
// error occured: cancel printing
break;
}
}
endPage(true);
this.printer.endJob();
// clean system resources
disposeResources();
}
/**
* Calculates margins and the line height regarding the chosen table
* {@link Font}.
*/
private void calculatePageFormat() {
// get page format
final Rectangle clientArea = this.printer.getClientArea();
// calculate margins
final Rectangle trim = this.printer.computeTrim(0, 0, 0, 0);
// regard resolution
final Point dpi = this.printer.getDPI();
// left border
this.leftMargin = dpi.x + trim.x;
// maximum expansion to the right
this.rightMargin = clientArea.width - dpi.x + trim.x + trim.width;
// top border
this.topMargin = dpi.y + trim.y;
// height of one page
this.bottomEnd = clientArea.height;
// maximum expansion to the bottom
this.bottomMargin = this.bottomEnd - dpi.y + trim.y + trim.height;
// calculate height of one line
this.lineHeight = (int) (1.2 * this.gc.getFontMetrics().getHeight());
// calculate spacing between single columns
this.verticalSpacing = 3 * this.gc.getFontMetrics()
.getAverageCharWidth();
// calculate spacing between single lines
this.horizontalSpacing = this.lineHeight / 5;
}
/**
* Transfers the specified text with the given font in an image, which can
* be used as the header image on top of the first page.
*
* @param text
* text to transfer into image
* @param topicFont
* font to use for the transfered text
* @return created image
*/
private Image createTopicHeader(final String text,
final FontData[] topicFont) {
// remember wrap settings
final int tempWrapStart = this.wrapXstart;
final int tempWrapEnd = this.wrapXend;
// set topic wrap space
this.wrapXstart = this.leftMargin;
this.wrapXend = this.rightMargin;
Font font;
if (topicFont == null) {
// no font selected, use the default
font = new Font(this.printer, DEFAULT_TOPIC_FONT_NAME,
DEFAULT_TOPIC_FONT_SIZE, DEFAULT_TOPIC_FONT_STYLE);
} else {
// transfer selected font to new context
font = new Font(this.printer, topicFont);
}
// use printer GC for calculations
this.gc.setFont(font);
// calculate height of one topic line
final int topicHeight = this.gc.getFontMetrics().getHeight();
// fit topic in print area (width)
final String[] wrappedTopic = handleWrap(text).split("[\n]");
// reset wrap column settings
this.wrapXstart = tempWrapStart;
this.wrapXend = tempWrapEnd;
this.gc.setFont(this.tableFont);
// calculate height including spacing underneath
final int height = wrappedTopic.length * topicHeight + topicHeight / 2;
// create Image and generate GC for draw operations
final Image topicHeader = new Image(this.printer, this.rightMargin
- this.leftMargin, height);
final GC headerGC = new GC(topicHeader);
headerGC.setFont(font);
// print fitting topic
for (int i = 0; i < wrappedTopic.length; i++) {
headerGC.drawString(wrappedTopic[i], 0, i * topicHeight);
}
// clean up
headerGC.dispose();
font.dispose();
// finished
return topicHeader;
}
/**
* Calculates the width and position of the columns regarding all entries to
* print. Stores the area for the wrap column.
*
* @param allLines
* table entries to print
* @param wrapColumn
* index of the column to be wrapped into multiple lines
* @return all columns fit on the chosen page format (width)
*/
private boolean calculateColumnPositions(final String[][] allLines,
final int wrapColumn) {
// calculate preferred width of all columns
final int[] maxColumnWidth = new int[this.headTitles.length];
if (this.headersVisible) {
this.gc.setFont(this.headerFont);
// regard table headers
for (int i = 0; i < maxColumnWidth.length; i++) {
maxColumnWidth[i] = this.gc.stringExtent(this.headTitles[i]).x;
}
this.gc.setFont(this.tableFont);
} else {
// initializie minimum width of 0
for (int i = 0; i < maxColumnWidth.length; i++) {
maxColumnWidth[i] = 0;
}
}
// regard all entries
for (String[] singleLine : allLines) {
// regard minimum width of each cell
for (int i = 0; i < maxColumnWidth.length; i++) {
maxColumnWidth[i] = Math.max(maxColumnWidth[i], this.gc
.stringExtent(singleLine[i]).x);
}
}
// do not forget spacing between columns
for (int i = 0; i < maxColumnWidth.length; i++) {
maxColumnWidth[i] += this.verticalSpacing;
}
// summarize the width of all columns without wrap
int minWidth = 0;
for (int i = 0; i < maxColumnWidth.length; i++) {
// ignore wrapColumn at this point
if (wrapColumn != i) {
minWidth += maxColumnWidth[i];
}
}
// how much space is left for the wrapColumn?
final int wrapWidth = this.rightMargin - this.leftMargin
- this.verticalSpacing - minWidth;
// wrap column need more space then available
boolean wrapEnabled = (wrapColumn > -1)
&& (wrapColumn < maxColumnWidth.length);
if ((wrapWidth < 0)
|| (wrapEnabled && (wrapWidth <= maxColumnWidth[wrapColumn]) && (wrapWidth < this.verticalSpacing))) {
// the other columns need too much space
showError(ERROR_TABLEWIDTH);
return false;
}
// calculat X-coordinates for the beginning of each column
this.columnPosX = new int[maxColumnWidth.length];
// first column starts at the left margin
this.columnPosX[0] = this.leftMargin;
if (wrapEnabled) {
// all columns before and the wrapColumn itself
for (int i = 0; i < wrapColumn; i++) {
this.columnPosX[i + 1] = this.columnPosX[i] + maxColumnWidth[i];
}
// all columns behind the wrapColumn
if ((this.columnPosX.length - 1) > wrapColumn) {
// last columns ends at the right margin
this.columnPosX[this.columnPosX.length - 1] = this.rightMargin
- maxColumnWidth[maxColumnWidth.length - 1];
for (int i = (maxColumnWidth.length - 2); i > wrapColumn; i--) {
// previous column in front of its follower
this.columnPosX[i] = this.columnPosX[i + 1]
- maxColumnWidth[i];
}
}
} else {
// calculate position regarding column sizes
for (int i = 0; i < (this.columnPosX.length - 1); i++) {
this.columnPosX[i + 1] = this.columnPosX[i] + maxColumnWidth[i];
}
}
this.wrapXend -= this.verticalSpacing;
if (wrapEnabled) {
// determine the wrap area
this.wrapXstart = this.columnPosX[wrapColumn];
if ((wrapColumn + 1) < this.columnPosX.length) {
this.wrapXend = this.columnPosX[wrapColumn + 1]
- (2 * this.verticalSpacing / 3);
} else {
// wrapColumn is the last column
this.wrapXend = this.rightMargin;
}
// last column ends at right margin
this.endX = this.rightMargin;
} else {
this.wrapXstart = 0;
this.wrapXend = 0;
// calculate end of the last column
this.endX = this.columnPosX[this.columnPosX.length - 1]
+ maxColumnWidth[maxColumnWidth.length - 1];
}
return true;
}
/**
* Prints one line containing the specified values.
*
* @param entries
* values of the line to print
* @param wrapColumn
* index of the column to wrap
*
* @return printing successful
*/
private boolean printLine(final String[] entries, final int wrapColumn) {
String[] wrapEntry = null;
int entryHeight = this.lineHeight;
if ((wrapColumn > -1) && (wrapColumn < entries.length)) {
// insert line separators where needed
wrapEntry = handleWrap(entries[wrapColumn]).split("[\n]");
entryHeight *= wrapEntry.length;
}
// calculate where the entry would end
int newY = this.posY + entryHeight;
if (newY > this.bottomMargin) {
// the line to print exceeds the page format: end this page
endPage(false);
// start a new page
newPage(this.headersVisible);
// calculate where the entry would end
newY = this.posY + entryHeight;
if (newY > this.bottomMargin) {
// the line to print exceeds the whole page: abort
showError(ERROR_ENTRYLENGTH1 + (wrapColumn + 1)
+ ERROR_ENTRYLENGTH2);
return false;
}
}
// entry fits on the current page: print it
for (int i = 0; i < entries.length; i++) {
if ((wrapEntry != null) && (i == wrapColumn)) {
for (int j = 0; j < wrapEntry.length; j++) {
// draw on multiple lines
this.gc.drawString(wrapEntry[j], this.columnPosX[i],
this.posY + (j * this.lineHeight));
}
} else {
// ignore possible line separators in non-wrap-columns
this.gc.drawString(entries[i], this.columnPosX[i], this.posY);
}
}
if (this.linesVisible) {
// only if this is not the first line in a headless table
if (this.posY != this.tableTop) {
// draw the horizontal line over the printed table entry
this.gc.drawLine(this.leftMargin, this.posY
- this.horizontalSpacing - 1, this.endX, this.posY
- this.horizontalSpacing - 1);
}
// add an additional spacing below the printed entry
newY += this.horizontalSpacing;
}
// store new vertical position for the next entry
this.posY = newY;
return true;
}
/**
* Inserts - if neccessary - line separators in the given entry to ensure
* that the entry fits in the column to wrap.
*
* @param entry
* single entry in the column to wrap
*
* @return fitting entry
*/
private String handleWrap(final String entry) {
if (this.gc.stringExtent(entry).x <= (this.wrapXend - this.wrapXstart)) {
// entry fits in one line
return entry;
}
int wrapIndex = this.wrapXstart;
final StringBuffer textBuffer = new StringBuffer();
StringBuffer wordBuffer = new StringBuffer();
char singleChar;
for (int i = 0; i < (entry.length() + 1); i++) {
if (i == entry.length()) {
singleChar = ' ';
} else {
singleChar = entry.charAt(i);
}
if (Character.isWhitespace(singleChar)) {
// end of word reached
final String word = wordBuffer.toString();
// calculate needed space for this word
final int wordWidth = this.gc.stringExtent(word).x;
if (((wrapIndex + wordWidth) > this.wrapXend)
&& (wrapIndex != this.wrapXstart)) {
/*
* required width exceeds the space that is left: new line
*/
textBuffer.append('\n');
wrapIndex = this.wrapXstart;
}
if ((wrapIndex + wordWidth) < this.wrapXend) {
// add the fitting word
wrapIndex += wordWidth;
textBuffer.append(word);
} else {
char wordChar;
// whole wrapColumn is not big enough for this word
for (int j = 0; j < word.length(); j++) {
if (wrapIndex > this.wrapXend) {
// character does not fit: new line
textBuffer.append('\n');
wrapIndex = this.wrapXstart;
}
// add each single character
wordChar = word.charAt(j);
textBuffer.append(wordChar);
wrapIndex += this.gc.getAdvanceWidth(wordChar);
}
}
if ((singleChar == 0x0a) || (singleChar == 0x0d)) {
textBuffer.append('\n');
} else if ((singleChar == ' ') && (i < entry.length())) {
textBuffer.append(singleChar);
}
// reset word buffer
wordBuffer = new StringBuffer();
} else {
wordBuffer.append(singleChar);
}
}
return textBuffer.toString();
}
/**
* Request new page on printer, increase page counter by one and add topic -
* if available - on top and table headers - according to their visibility -
* underneath.
*
* @param printTableHeader
* print the column topics
*/
private void newPage(final boolean printTableHeader) {
// request new page on printer
this.printer.startPage();
// increase page counter
this.pageCount++;
// begin on top
this.posY = this.topMargin;
// insert header
if ((this.pageCount == 1) && (this.headerImage != null)) {
this.posY += this.headerOffset;
this.gc.drawImage(this.headerImage, this.leftMargin, this.posY);
// calculate top position for the table header
this.posY = Math.max(this.topMargin, this.posY
+ this.headerImage.getImageData().height);
}
// store horizontal begin of the table for the vertical lines
this.tableTop = this.posY;
if (printTableHeader) {
this.gc.setFont(this.headerFont);
// add additional spacing under the headers
this.posY += this.gc.getFontMetrics().getHeight()
+ this.horizontalSpacing;
if (this.posY > this.bottomMargin) {
/*
* header is so big, there is not even enough space for the
* column topics
*/
this.gc.setFont(this.tableFont);
// end this page and start a new one without header image
endPage(false);
newPage(false);
// set the table header font again
this.gc.setFont(this.headerFont);
// recalculate the vertical position after the table header
this.posY += this.gc.getFontMetrics().getHeight()
+ this.horizontalSpacing;
}
for (int i = 0; i < this.headTitles.length; i++) {
// print each table header
this.gc.drawString(this.headTitles[i], this.columnPosX[i],
this.tableTop);
}
// draw vertical line underneath
this.gc.drawLine(this.leftMargin, this.posY, this.endX, this.posY);
// add more spacing
this.posY += (3 * this.horizontalSpacing) / 2;
// reset table entry font
this.gc.setFont(this.tableFont);
}
}
/**
* Finishes the current page by drawing the vertical column lines, inserting
* the page count on the bottom and telling the printer to end this page.
*
* @param printFooter
* print the footer image (if available) before closing this page
*/
private void endPage(final boolean printFooter) {
if (printFooter && (this.footerImage != null)) {
if ((this.posY + this.footerImage.getImageData().height) > this.bottomMargin) {
/*
* footer does not fit on the bottom of this page, end it and
* start a new one, just for the footer
*/
endPage(false);
newPage(false);
}
this.gc.drawImage(this.footerImage, this.leftMargin,
this.bottomMargin - this.footerImage.getImageData().height);
}
if (this.tableTop != this.posY) {
int posX;
// draw each vertical line between columns
for (int i = 1; i < this.columnPosX.length; i++) {
posX = this.columnPosX[i] - (this.verticalSpacing / 3);
this.gc.drawLine(posX, this.tableTop, posX, this.posY);
}
}
final String pageNumber = Integer.toString(this.pageCount);
// calculate size of the page count string
final int width = this.gc.stringExtent(pageNumber).x;
// get horizontal mid for the page count
final int pageX = (this.rightMargin - ((this.rightMargin - this.leftMargin) / 2))
- (width / 2);
// get the vertical mid in the bottom margin for the page count
final int pageY = this.bottomEnd
- ((this.bottomEnd - this.bottomMargin + this.lineHeight) / 2);
// draw the page count
this.gc.drawString(pageNumber, pageX, pageY);
// tell printer to end current page
this.printer.endPage();
}
/**
* Disposes all system resources like {@link Image}s and {@link Font}s as
* well as the used {@link GC} and {@link Printer}.
* <p>
* Does not affect given parameter values. Only self-allocated resources are
* disposed.
* </p>
*/
private void disposeResources() {
if (this.headerImage != null) {
this.headerImage.dispose();
}
if (this.footerImage != null) {
this.footerImage.dispose();
}
this.tableFont.dispose();
this.headerFont.dispose();
this.gc.dispose();
this.printer.dispose();
}
/**
* Generates a {@link MessageBox} to display the occured error by refering
* to the specified error message.
*
* @param message
* error message to display
*/
private void showError(final String message) {
// clean up
disposeResources();
Display.getDefault().asyncExec(new Runnable() {
public void run() {
// get super ordinated shell for message box
final Shell parentShell;
if (SWTTablePrinter.this.printSource.isDisposed()) {
// no access to origin shell, create a new one
parentShell = new Shell();
} else {
// get current shell
parentShell = SWTTablePrinter.this.printSource.getShell();
}
// generate message box
final MessageBox box = new MessageBox(parentShell, SWT.OK
| SWT.APPLICATION_MODAL | SWT.ICON_ERROR);
// set message box topic and message
box.setText(ERROR_TOPIC);
box.setMessage(message);
// display message box
box.open();
}
});
}
}
SorTableTester
Java:
package org.hermeneutix.utilities.swt;
import java.util.Calendar;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.printing.PrintDialog;
import org.eclipse.swt.printing.PrinterData;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
public class SorTableTester {
// date format: dd.
private static final String DATE_FORMAT1 = "%1$td.";
// date format: dd.mm.
private static final String DATE_FORMAT2 = "%1$td.%1$tm.";
// date format: dd.mm.YYYY
private static final String DATE_FORMAT3 = "%1$td.%1$tm.%1$tY";
// date format: dd.mm. HH:MM
private static final String DATETIME_FORMAT1 = "%1$td.%1$tm. %1$tH:%1$tM";
// date format: dd.mm.YYYY HH:MM
private static final String DATETIME_FORMAT2 = "%1$td.%1$tm.%1$tY %1$tH:%1$tM";
// number of items to display (try higher count to check out performance)
private static final int ITEM_COUNT = 10;
// calendar instance for the date-time columns
private final static Calendar date = Calendar.getInstance();
public static void main(String[] args) {
// create the shell to test the SorTable in
final Shell shell = new Shell();
shell.setLayout(new GridLayout(1, true));
// prepare line for the print button and the search field
final Composite topLine = new Composite(shell, SWT.NONE);
topLine.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
topLine.setLayout(new GridLayout(4, true));
// create the SorTable itself
final SorTable table = createSorTable(shell);
// insert a button in the top line to print the table contents
final Button printButton = new Button(topLine, SWT.PUSH);
printButton.setText("Print");
printButton.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(final SelectionEvent ev) {
// offer possibility to set general print settings
final PrinterData data = new PrintDialog(shell, SWT.BORDER)
.open();
// was the dialog canceled?
if (data != null) {
// dialog finished successfully
table.print(data, "Printing Example",
"Printing Example - SorTableTester", 3,
// start printing with default fonts
null, null, null);
}
}
});
new Label(topLine, SWT.NONE).setText("Filter: ");
// create a text field for filtering the table contents
table.createSearchField(topLine, SWT.BORDER).setLayoutData(
new GridData(GridData.HORIZONTAL_ALIGN_FILL));
final Button insertButton = new Button(topLine, SWT.PUSH);
insertButton.setText("Insert an item regarding sorting");
insertButton.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(final SelectionEvent ev) {
shell.getDisplay().asyncExec(new Runnable() {
public void run() {
final TableItem created = table.insertItem(SWT.NONE,
createRandomEntry(table.getItems().length));
applyColors(created);
table.setSelection(created);
}
});
}
});
// display shell
shell.setSize(800, 600);
shell.open();
// SWT-Shell-Loop
final Display display = shell.getDisplay();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
private static SorTable createSorTable(final Composite parent) {
// create an array of table headers
final String[] header = new String[9];
for (int column = 0; column < 9; column++) {
header[column] = "Column" + (1 + column);
}
// create an array of sample values
final String[][] values = new String[ITEM_COUNT][];
for (int i = 0; i < ITEM_COUNT; i++) {
values[i] = createRandomEntry(i);
}
// create the table itself
final SorTable table = SorTable.createSorTable(parent,
SWT.FULL_SELECTION | SWT.MULTI, header, values);
table.setLayoutData(new GridData(GridData.FILL_BOTH));
/*
* after creation of a table with visible headers, the columns need
* separate setting of width
*/
for (TableColumn singleColumn : table.getColumns()) {
singleColumn.pack();
}
// enable compare mode for simple numbers
table.setSortingMethod(table.getColumn(0), SorTable.COMPARE_INT);
// enable compare mode for decimal numbers
table.setSortingMethod(table.getColumn(1), SorTable.COMPARE_DOUBLE);
// enable compare mode for decimal numbers
table.setSortingMethod(table.getColumn(2), SorTable.COMPARE_DOUBLE);
// by default the contained values are compared as texts
// table.setSortingMethod(table.getColumn(3), SorTable.COMPARE_TEXT);
// enable compare mode for date formats
table.setSortingMethod(table.getColumn(4), SorTable.COMPARE_DATETIME);
table.setSortingMethod(table.getColumn(5), SorTable.COMPARE_DATETIME);
table.setSortingMethod(table.getColumn(6), SorTable.COMPARE_DATETIME);
table.setSortingMethod(table.getColumn(7), SorTable.COMPARE_DATETIME);
table.setSortingMethod(table.getColumn(8), SorTable.COMPARE_DATETIME);
/*
* enabled editing of three columns (equivalent to
* table.setEditable(true, new int[] {1,2,3}))
*/
table.setEditable(true, 1, 2, 3);
/*
* highlight first column items
*/
table.addColorListener(new Listener() {
public void handleEvent(final Event event) {
for (TableItem singleItem : table.getItems()) {
applyColors(singleItem);
}
}
});
// initial coloring
for (TableItem singleItem : table.getItems()) {
applyColors(singleItem);
}
return table;
}
static void applyColors(final TableItem target) {
if (Integer.parseInt(target.getText(0)) < 500) {
target.setBackground(0, target.getDisplay()
.getSystemColor(SWT.COLOR_CYAN));
target.setForeground(target.getDisplay()
.getSystemColor(SWT.COLOR_BLUE));
}
}
static String[] createRandomEntry(final int index) {
String[] entry = new String[9];
// first column: simple int values
entry[0] = Integer.toString((int) (Math.random() * 1000));
// second column: simple decimal number
entry[1] = Double.toString(Math.random() * 10);
/*
* third column: (possible negative) decimal number with additional unit
* text
*/
entry[2] = Math.round((Math.random() - 0.5) * 100) + "."
+ ((int) (Math.random() * 1000)) + " °C";
// fourth column: text column
entry[3] = "Entry" + (1 + index) + " long enough to force "
+ "a wrap in this column when printed on an A3 format\n" +
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
// change the displayed date
date.add(Calendar.MINUTE, (int) ((Math.random() - 0.5) * 1000000));
// fifth column: date in format dd.
entry[4] = String.format(DATE_FORMAT1, date);
// sixth column: date in format dd.mm.
entry[5] = String.format(DATE_FORMAT2, date);
// seventh column: date in format dd.mm.YYYY
entry[6] = String.format(DATE_FORMAT3, date);
// eigth column: date in format dd.mm. HH:MM
entry[7] = String.format(DATETIME_FORMAT1, date);
// nineth column: date in format dd.mm.YYYY HH:MM
entry[8] = String.format(DATETIME_FORMAT2, date);
return entry;
}
}
Zuletzt bearbeitet: