[SWT] Table sortieren, editiern, drucken

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):
  • 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:
Nach einer Runde Bugfixing und ein paar kleinen Erweiterungen, habe ich mir erlaubt den Inhalt dieses Threads wieder auf den neusten Stand zu bringen.

In der SorTable gibt es jetzt die Möglichkeit via addColorListener() auf das Verwerfen der Eintragsfärbung zu reagieren und quasi seine eigenen Farben wieder anzuwenden.
Darüber hinaus hat der SWTTablePrinter jetzt noch die Möglichkeit statt des Überschriften-Strings eine Kopfzeile (für die erste Seite) und eine Fußzeile (für die letzte Seite) in Image-Form entgegen zu nehmen.
So ist es möglich einfache Protokolle aus der Tabellenform mit einem (zum Beispiel firmeneigenen) Header zu versehen und bei Bedarf eine/mehrere Unterschriftenzeilen in die Fußzeile der letzten Seite zu legen.
Diese neuen Druckoptionen reicht die SorTable jetzt auch weiter, so dass es zwei neue print()-Methoden gibt.

Ansonsten sind durch den Dialog hier im Forum und ein wenig eigenes Testen noch hier und da kleine Fehler verschwunden.


Vielleicht hilft es ja mal jemandem :)
Bis dahin gibt es ja vielleicht noch Hinweise für Performance-Verbesserungen oder mögliche sinnvolle Erweiterungen, die ich einbauen könnte ;)


Viele Grüße
Carron


PS: ich hab das Ganze auch nochmal mit deutschen Kommentaren, falls das jemand haben wollen würde, müsste ich nur die firmeninternen Dinge entfernen und könnte das quasi auch liefern (
 
Zuletzt bearbeitet:
Zurück