/*
 * Created on 2004/Apr/18
 *
 * Copyright (C) 2004, 2005, 2006 Aelitis SAS, All rights Reserved
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details ( see the LICENSE file ).
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * AELITIS, SAS au capital de 46,603.30 euros,
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 */
package org.gudy.azureus2.ui.swt.views.table.impl;

import java.util.*;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.*;
import org.eclipse.swt.dnd.*;
import org.eclipse.swt.events.*;
import org.eclipse.swt.graphics.*;
import org.eclipse.swt.layout.*;
import org.eclipse.swt.widgets.*;

import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.config.impl.ConfigurationManager;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.logging.*;
import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.core3.util.Timer;
import org.gudy.azureus2.pluginsimpl.local.ui.tables.TableContextMenuItemImpl;
import org.gudy.azureus2.ui.common.util.MenuItemManager;
import org.gudy.azureus2.ui.swt.*;
import org.gudy.azureus2.ui.swt.debug.ObfusticateImage;
import org.gudy.azureus2.ui.swt.debug.UIDebugGenerator;
import org.gudy.azureus2.ui.swt.mainwindow.Colors;
import org.gudy.azureus2.ui.swt.plugins.UISWTViewEventListener;
import org.gudy.azureus2.ui.swt.pluginsimpl.UISWTInstanceImpl;
import org.gudy.azureus2.ui.swt.pluginsimpl.UISWTViewImpl;
import org.gudy.azureus2.ui.swt.shells.GCStringPrinter;
import org.gudy.azureus2.ui.swt.views.IView;
import org.gudy.azureus2.ui.swt.views.table.*;
import org.gudy.azureus2.ui.swt.views.table.utils.*;
import org.gudy.azureus2.ui.swt.views.utils.VerticalAligner;

import com.aelitis.azureus.ui.common.table.*;
import com.aelitis.azureus.ui.common.table.impl.TableViewImpl;
import com.aelitis.azureus.ui.swt.UIFunctionsManagerSWT;
import com.aelitis.azureus.ui.swt.UIFunctionsSWT;

import org.gudy.azureus2.plugins.ui.tables.TableCellMouseEvent;
import org.gudy.azureus2.plugins.ui.tables.TableContextMenuItem;

/** 
 * An IView with a sortable table.  Handles composite/menu/table creation
 * and management.
 *
 * @author Olivier (Original PeersView/MyTorrentsView/etc code)
 * @author TuxPaper
 *         2004/Apr/20: Remove need for tableItemToObject
 *         2005/Oct/07: Virtual Table
 *         2005/Nov/16: Moved TableSorter into TableView
 *         
 * @note From TableSorter.java:<br>
 *   <li>2004/Apr/20: Remove need for tableItemToObject (store object in tableItem.setData)
 *   <li>2004/May/11: Use Comparable instead of SortableItem
 *   <li>2004/May/14: moved from org.gudy.azureus2.ui.swt.utils
 *   <li>2005/Oct/10: v2307 : Sort SWT.VIRTUAL Tables, Column Indicator
 *   
 * @future TableView should be split into two.  One for non SWT functions, and
 *          the other extending the first, with extra SWT stuff. 
 *
 * @future dataSourcesToRemove should be removed after a certain amount of time
 *          has passed.  Currently, dataSourcesToRemove is processed every
 *          refresh IF the table is visible, or it is processed when we collect
 *          20 items to remove. 
 */
public class TableViewSWTImpl
	extends TableViewImpl
	implements ParameterListener, TableViewSWT,
	TableStructureModificationListener, ObfusticateImage
	
{
	private final static LogIDs LOGID = LogIDs.GUI;

	/** Virtual Tables still a work in progress */
	// Non-Virtual tables scroll faster with they keyboard
	// Virtual tables don't flicker when updating a cell (Windows)
	private final static boolean DISABLEVIRTUAL = SWT.getVersion() < 3138;

	private final static boolean COLUMN_CLICK_DELAY = Constants.isOSX
			&& SWT.getVersion() >= 3221 && SWT.getVersion() <= 3222;

	private static final boolean DEBUG_SORTER = false;

	// Shorter name for ConfigManager, easier to read code
	private static final ConfigurationManager configMan = ConfigurationManager.getInstance();

	private static final String CFG_SORTDIRECTION = "config.style.table.defaultSortOrder";

	private static final long IMMEDIATE_ADDREMOVE_DELAY = 150;

	private static final long IMMEDIATE_ADDREMOVE_MAXDELAY = 2000;

	private static final long BREAKOFF_ADDTOMAP = 1000;

	private static final long BREAKOFF_ADDROWSTOSWT = 800;

	/** TableID (from {@link org.gudy.azureus2.plugins.ui.tables.TableManager}) 
	 * of the table this class is
	 * handling.  Config settings are stored with the prefix of 
	 * "Table.<i>TableID</i>"
	 */
	protected String sTableID;

	/** Prefix for retrieving text from the properties file (MessageText)
	 * Typically <i>TableID</i> + "View"
	 */
	protected String sPropertiesPrefix;

	/** Column name to sort on if user hasn't chosen one yet 
	 */
	protected String sDefaultSortOn;

	/** 1st column gap problem (Eclipse Bug 43910).  Set to true when table is 
	 * using TableItem.setImage 
	 */
	private boolean bSkipFirstColumn;

	private Point ptIconSize = null;

	/** Basic (pre-defined) Column Definitions */
	private TableColumnCore[] basicItems;

	/** All Column Definitions.  The array is not necessarily in column order */
	private TableColumnCore[] tableColumns;

	/** Composite for IView implementation */
	private Composite mainComposite;

	/** Composite that stores the table (sometimes the same as mainComposite) */
	private Composite tableComposite;

	/** Table for SortableTable implementation */
	private Table table;

	/** SWT style options for the creation of the Table */
	protected int iTableStyle;

	/** Whether the Table is Virtual */
	private boolean bTableVirtual;

	/** Context Menu */
	private Menu menu;

	/** Context Menu specific to the column the mouse was on */
	private Menu menuThisColumn;

	/** Link DataSource to their row in the table.
	 * key = DataSource
	 * value = TableRowSWT
	 */
	private Map mapDataSourceToRow;

	private AEMonitor dataSourceToRow_mon = new AEMonitor("TableView:OTSI");

	private List sortedRows;

	private AEMonitor sortedRows_mon = new AEMonitor("TableView:sR");

	private AEMonitor sortColumn_mon = new AEMonitor("TableView:sC");

	/** Sorting functions */
	protected TableColumnCore sortColumn;

	/** TimeStamp of when last sorted all the rows was */
	private long lLastSortedOn;

	/* position of mouse in table.  Used for context menu. */
	private int iMouseX = -1;

	/** For updating GUI.  
	 * Some UI objects get updating every X cycles (user configurable) 
	 */
	protected int loopFactor;

	/** How often graphic cells get updated
	 */
	protected int graphicsUpdate = configMan.getIntParameter("Graphics Update");

	protected int reOrderDelay = configMan.getIntParameter("ReOrder Delay");

	/** Check Column Widths every 10 seconds on Pre 3.0RC1 on OSX if view is active.  
	 * Other OSes can capture column width changes automatically */
	private int checkColumnWidthsEvery = (Constants.isOSX && SWT.getVersion() < 3054)
			? 10000 / configMan.getIntParameter("GUI Refresh") : 0;

	/**
	 * Cache of selected table items to bypass insufficient drawing on Mac OS X
	 */
	//private ArrayList oldSelectedItems;
	/** We need to remember the order of the columns at the time we added them
	 * in case the user drags the columns around.
	 */
	private TableColumnCore[] columnsOrdered;

	private ColumnMoveListener columnMoveListener = new ColumnMoveListener();

	/** Queue added datasources and add them on refresh */
	private List dataSourcesToAdd = new ArrayList(4);

	/** Queue removed datasources and add them on refresh */
	private List dataSourcesToRemove = new ArrayList(4);

	private Timer timerProcessDataSources = new Timer("Process Data Sources");

	private TimerEvent timerEventProcessDS;

	private boolean bReallyAddingDataSources = false;

	/** TabViews */
	public boolean bEnableTabViews = false;

	/** TabViews */
	private CTabFolder tabFolder;

	/** TabViews */
	private ArrayList tabViews = new ArrayList(1);

	private int lastTopIndex = 0;

	private int lastBottomIndex = -1;

	protected IView[] coreTabViews = null;

	private long lCancelSelectionTriggeredOn = -1;

	// XXX Remove after column selection is no longered triggered on column resize (OSX)
	private long lLastColumnResizeOn = -1;

	private List listenersMenuFill = new ArrayList();

	private TableViewSWTPanelCreator mainPanelCreator;

	private List listenersKey = new ArrayList();
	
	private boolean columnPaddingAdjusted = false;

	/**
	 * Main Initializer
	 * @param _sTableID Which table to handle (see 
	 *                   {@link org.gudy.azureus2.plugins.ui.tables.TableManager}).
	 *                   Config settings are stored with the prefix of  
	 *                   "Table.<i>TableID</i>"
	 * @param _sPropertiesPrefix Prefix for retrieving text from the properties
	 *                            file (MessageText).  Typically 
	 *                            <i>TableID</i> + "View"
	 * @param _basicItems Column Definitions
	 * @param _sDefaultSortOn Column name to sort on if user hasn't chosen one yet
	 * @param _iTableStyle SWT style constants used when creating the table
	 */
	public TableViewSWTImpl(String _sTableID, String _sPropertiesPrefix,
			TableColumnCore[] _basicItems, String _sDefaultSortOn, int _iTableStyle) {
		sTableID = _sTableID;
		basicItems = _basicItems;
		sPropertiesPrefix = _sPropertiesPrefix;
		sDefaultSortOn = _sDefaultSortOn;
		iTableStyle = _iTableStyle | SWT.V_SCROLL;
		if (DISABLEVIRTUAL)
			iTableStyle &= ~(SWT.VIRTUAL);
		bTableVirtual = (iTableStyle & SWT.VIRTUAL) != 0;

		mapDataSourceToRow = new HashMap();
		sortedRows = new ArrayList();
	}

	/**
	 * Main Initializer. Table Style will be SWT.SINGLE | SWT.FULL_SELECTION
	 *
	 * @param _sTableID Which table to handle (see 
	 *                   {@link org.gudy.azureus2.plugins.ui.tables.TableManager}
	 *                   ).  Config settings are stored with the prefix of 
	 *                   "Table.<i>TableID</i>"
	 * @param _sPropertiesPrefix Prefix for retrieving text from the properties
	 *                            file (MessageText).  
	 *                            Typically <i>TableID</i> + "View"
	 * @param _basicItems Column Definitions
	 * @param _sDefaultSortOn Column name to sort on if user hasn't chosen one
	 *                         yet
	 */
	public TableViewSWTImpl(String _sTableID, String _sPropertiesPrefix,
			TableColumnCore[] _basicItems, String _sDefaultSortOn) {
		this(_sTableID, _sPropertiesPrefix, _basicItems, _sDefaultSortOn,
				SWT.SINGLE | SWT.FULL_SELECTION | SWT.VIRTUAL);
	}

	private void initializeColumnDefs() {
		// XXX Adding Columns only has to be done once per TableID.  
		// Doing it more than once won't harm anything, but it's a waste.
		TableColumnManager tcManager = TableColumnManager.getInstance();
		if (tcManager.getTableColumnCount(sTableID) != basicItems.length) {
			for (int i = 0; i < basicItems.length; i++) {
				tcManager.addColumn(basicItems[i]);
			}
		}

		// fixup order
		tcManager.ensureIntegrety(sTableID);

		tableColumns = tcManager.getAllTableColumnCoreAsArray(sTableID);
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#setColumnList(com.aelitis.azureus.ui.common.table.TableColumnCore[], java.lang.String)
	// XXX This isn't right
	public void setColumnList(TableColumnCore[] columns,
			String defaultSortColumnID, boolean defaultSortOrder,
			boolean titleIsMinWidth) {
		// XXX Adding Columns only has to be done once per TableID.  
		// Doing it more than once won't harm anything, but it's a waste.
		TableColumnManager tcManager = TableColumnManager.getInstance();
		if (tcManager.getTableColumnCount(sTableID) != columns.length) {
			for (int i = 0; i < columns.length; i++) {
				columns[i].setTableID(sTableID);
				tcManager.addColumn(columns[i]);
			}
		}

		// fixup order
		tcManager.ensureIntegrety(sTableID);

		tableColumns = tcManager.getAllTableColumnCoreAsArray(sTableID);
	}

	// AbstractIView::initialize
	public void initialize(Composite composite) {
		composite.setRedraw(false);
		mainComposite = createSashForm(composite);
		menu = createMenu();
		table = createTable(tableComposite);
		initializeTable(table);

		triggerLifeCycleListener(TableLifeCycleListener.EVENT_INITIALIZED);

		configMan.addParameterListener("Graphics Update", this);
		configMan.addParameterListener("ReOrder Delay", this);
		Colors.getInstance().addColorsChangedListener(this);

		// So all TableView objects of the same TableID have the same columns,
		// and column widths, etc
		TableStructureEventDispatcher.getInstance(sTableID).addListener(this);
		composite.setRedraw(true);
	}

	private Composite createSashForm(final Composite composite) {
		if (!bEnableTabViews) {
			tableComposite = createMainPanel(composite);
			return tableComposite;
		}

		int iNumViews = coreTabViews == null ? 0 : coreTabViews.length;

		UIFunctionsSWT uiFunctions = UIFunctionsManagerSWT.getUIFunctionsSWT();
		Map pluginViews = null;
		if (uiFunctions != null) {
			UISWTInstanceImpl pluginUI = uiFunctions.getSWTPluginInstanceImpl();

			if (pluginUI != null) {
				pluginViews = pluginUI.getViewListeners(sTableID);
				if (pluginViews != null)
					iNumViews += pluginViews.size();
			}
		}

		if (iNumViews == 0) {
			tableComposite = createMainPanel(composite);
			return tableComposite;
		}

		FormData formData;

		final Composite form = new Composite(composite, SWT.NONE);
		FormLayout flayout = new FormLayout();
		flayout.marginHeight = 0;
		flayout.marginWidth = 0;
		form.setLayout(flayout);
		GridData gridData;
		gridData = new GridData(GridData.FILL_BOTH);
		form.setLayoutData(gridData);

		// Create them in reverse order, so we can have the table auto-grow, and
		// set the tabFolder's height manually

		final int TABHEIGHT = 20;
		tabFolder = new CTabFolder(form, SWT.TOP | SWT.BORDER);
		tabFolder.setMinimizeVisible(true);
		tabFolder.setTabHeight(TABHEIGHT);
		final int iFolderHeightAdj = tabFolder.computeSize(SWT.DEFAULT, 0).y;

		final Sash sash = new Sash(form, SWT.HORIZONTAL);

		tableComposite = createMainPanel(form);
		Composite cFixLayout = tableComposite;
		while (cFixLayout != null && cFixLayout.getParent() != form) {
			cFixLayout = cFixLayout.getParent();
		}
		if (cFixLayout == null) {
			cFixLayout = tableComposite;
		}
		GridLayout layout = new GridLayout();
		layout.numColumns = 1;
		layout.horizontalSpacing = 0;
		layout.verticalSpacing = 0;
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		cFixLayout.setLayout(layout);

		// FormData for Folder
		formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		formData.right = new FormAttachment(100, 0);
		formData.bottom = new FormAttachment(100, 0);
		int iSplitAt = configMan.getIntParameter(sPropertiesPrefix + ".SplitAt",
				3000);
		// Was stored at whole
		if (iSplitAt < 100)
			iSplitAt *= 100;

		double pct = iSplitAt / 10000.0;
		if (pct < 0.03)
			pct = 0.03;
		else if (pct > 0.97)
			pct = 0.97;

		// height will be set on first resize call
		sash.setData("PCT", new Double(pct));
		tabFolder.setLayoutData(formData);
		final FormData tabFolderData = formData;

		// FormData for Sash
		formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		formData.right = new FormAttachment(100, 0);
		formData.bottom = new FormAttachment(tabFolder);
		formData.height = 5;
		sash.setLayoutData(formData);

		// FormData for table Composite
		formData = new FormData();
		formData.left = new FormAttachment(0, 0);
		formData.right = new FormAttachment(100, 0);
		formData.top = new FormAttachment(0, 0);
		formData.bottom = new FormAttachment(sash);
		cFixLayout.setLayoutData(formData);

		// Listeners to size the folder
		sash.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				final boolean FASTDRAG = true;

				if (FASTDRAG && e.detail == SWT.DRAG)
					return;

				if (tabFolder.getMinimized()) {
					tabFolder.setMinimized(false);
					refreshSelectedSubView();
					configMan.setParameter(sPropertiesPrefix + ".subViews.minimized",
							false);
				}

				Rectangle area = form.getClientArea();
				tabFolderData.height = area.height - e.y - e.height - iFolderHeightAdj;
				form.layout();

				Double l = new Double((double) tabFolder.getBounds().height
						/ form.getBounds().height);
				sash.setData("PCT", l);
				if (e.detail != SWT.DRAG)
					configMan.setParameter(sPropertiesPrefix + ".SplitAt",
							(int) (l.doubleValue() * 10000));
			}
		});

		final CTabFolder2Adapter folderListener = new CTabFolder2Adapter() {
			public void minimize(CTabFolderEvent event) {
				tabFolder.setMinimized(true);
				tabFolderData.height = iFolderHeightAdj;
				form.layout();

				configMan.setParameter(sPropertiesPrefix + ".subViews.minimized", true);
			}

			public void restore(CTabFolderEvent event) {
				tabFolder.setMinimized(false);
				form.notifyListeners(SWT.Resize, null);

				refreshSelectedSubView();

				configMan.setParameter(sPropertiesPrefix + ".subViews.minimized", false);
			}
		};
		tabFolder.addCTabFolder2Listener(folderListener);

		tabFolder.addMouseListener(new MouseAdapter() {
			public void mouseDown(MouseEvent e) {
				if (tabFolder.getMinimized()) {
					folderListener.restore(null);
					// If the user clicked down on the restore button, and we restore
					// before the CTabFolder does, CTabFolder will minimize us again
					// There's no way that I know of to determine if the mouse is 
					// on that button!

					// one of these will tell tabFolder to cancel
					e.button = 0;
					tabFolder.notifyListeners(SWT.MouseExit, null);
				}
			}
		});

		form.addListener(SWT.Resize, new Listener() {
			public void handleEvent(Event e) {
				if (tabFolder.getMinimized())
					return;

				Double l = (Double) sash.getData("PCT");
				if (l != null) {
					tabFolderData.height = (int) (form.getBounds().height * l.doubleValue())
							- iFolderHeightAdj;
					form.layout();
				}
			}
		});

		if (coreTabViews != null)
			for (int i = 0; i < coreTabViews.length; i++)
				addTabView(coreTabViews[i]);

		// Call plugin listeners
		if (pluginViews != null) {
			String[] sNames = (String[]) pluginViews.keySet().toArray(new String[0]);
			for (int i = 0; i < sNames.length; i++) {
				UISWTViewEventListener l = (UISWTViewEventListener) pluginViews.get(sNames[i]);
				if (l != null) {
					try {
						UISWTViewImpl view = new UISWTViewImpl(sTableID, sNames[i], l);
						addTabView(view);
					} catch (Exception e) {
						// skip, plugin probably specifically asked to not be added
					}
				}
			}
		}

		if (configMan.getBooleanParameter(
				sPropertiesPrefix + ".subViews.minimized", false)) {
			tabFolder.setMinimized(true);
			tabFolderData.height = iFolderHeightAdj;
		} else {
			tabFolder.setMinimized(false);
		}

		tabFolder.setSelection(0);

		return form;
	}

	/** Creates a composite within the specified composite and sets its layout
	 * to a default FillLayout().
	 *
	 * @param composite to create your Composite under
	 * @return The newly created composite
	 */
	public Composite createMainPanel(Composite composite) {
		TableViewSWTPanelCreator mainPanelCreator = getMainPanelCreator();
		if (mainPanelCreator != null) {
			return mainPanelCreator.createTableViewPanel(composite);
		}
		Composite panel = new Composite(composite, SWT.NULL);
		GridLayout layout = new GridLayout();
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		panel.setLayout(layout);

		return panel;
	}

	/** Creates the Table.
	 *
	 * @return The created Table.
	 */
	public Table createTable(Composite panel) {
		table = new Table(panel, iTableStyle);
		table.setLayoutData(new GridData(GridData.FILL_BOTH));

		return table;
	}

	/** Sets up the sorter, columns, and context menu.
	 *
	 * @param table Table to be initialized
	 */
	public void initializeTable(final Table table) {
		initializeColumnDefs();

		iTableStyle = table.getStyle();
		bTableVirtual = (iTableStyle & SWT.VIRTUAL) != 0;

		table.setLinesVisible(Utils.TABLE_GRIDLINE_IS_ALTERNATING_COLOR);
		table.setMenu(menu);
		table.setData("Name", sTableID);
		table.setData("TableView", this);

		// Setup table
		// -----------

		// XXX On linux (an other OSes?), changing the column indicator doesn't 
		//     work until the table is shown.  Since SWT.Show doesn't trigger,
		//     use the first paint trigger.
		if (!Utils.SWT32_TABLEPAINT) {
			table.addPaintListener(new PaintListener() {
				boolean first = true;

				public void paintControl(PaintEvent event) {
					if (first) {
						changeColumnIndicator();
						// This fixes the scrollbar not being long enough on Win2k
						// There may be other methods to get it to refresh right, but
						// layout(true, true) didn't work.
						table.setRedraw(false);
						table.setRedraw(true);
						first = false;
					}
					if (event.width == 0 || event.height == 0)
						return;
					doPaint(event.gc);
					visibleRowsChanged();
				}
			});
		}

		if (Utils.SWT32_TABLEPAINT) {
			// SWT 3.2 only.  Code Ok -- Only called in SWT 3.2 mode
			table.addListener(SWT.PaintItem, new Listener() {
				public void handleEvent(Event event) {
					paintItem(event);
				}
			});
			table.addListener(SWT.EraseItem, new Listener() {
				public void handleEvent(Event event) {
				}
			});
		}

		table.addListener(SWT.MeasureItem, new Listener() {
			public void handleEvent(Event event) {
				int defaultHeight = getRowDefaultHeight();
				if (event.height < defaultHeight) {
					event.height = defaultHeight;
				}
			}
		});

		// Deselect rows if user clicks on a black spot (a spot with no row)
		table.addMouseListener(new MouseAdapter() {
			public void mouseDoubleClick(MouseEvent e) {
				TableColumnCore tc = getTableColumnByOffset(e.x);
				TableCellSWT cell = getTableCell(e.x, e.y);
				if (cell != null && tc != null) {
					TableCellMouseEvent event = createMouseEvent(cell, e,
							TableCellMouseEvent.EVENT_MOUSEDOUBLECLICK);
					if (event != null) {
						tc.invokeCellMouseListeners(event);
						cell.invokeMouseListeners(event);
						if (event.skipCoreFunctionality) {
							lCancelSelectionTriggeredOn = System.currentTimeMillis();
						}
					}
				}
			}

			public void mouseUp(MouseEvent e) {
				TableColumnCore tc = getTableColumnByOffset(e.x);
				TableCellSWT cell = getTableCell(e.x, e.y);
				if (cell != null && tc != null) {
					TableCellMouseEvent event = createMouseEvent(cell, e,
							TableCellMouseEvent.EVENT_MOUSEUP);
					if (event != null) {
						tc.invokeCellMouseListeners(event);
						cell.invokeMouseListeners(event);
						if (event.skipCoreFunctionality) {
							lCancelSelectionTriggeredOn = System.currentTimeMillis();
						}
					}
				}
			}

			public void mouseDown(MouseEvent e) {
				TableColumnCore tc = getTableColumnByOffset(e.x);
				TableCellSWT cell = getTableCell(e.x, e.y);
				if (cell != null && tc != null) {
					if (e.button == 2 && e.stateMask == SWT.CONTROL) {
						((TableCellImpl) cell).bDebug = !((TableCellImpl) cell).bDebug;
						System.out.println("Set debug for " + cell + " to "
								+ ((TableCellImpl) cell).bDebug);
					}
					TableCellMouseEvent event = createMouseEvent(cell, e,
							TableCellMouseEvent.EVENT_MOUSEDOWN);
					if (event != null) {
						tc.invokeCellMouseListeners(event);
						cell.invokeMouseListeners(event);
						if (event.skipCoreFunctionality) {
							lCancelSelectionTriggeredOn = System.currentTimeMillis();
						}
					}
				}

				iMouseX = e.x;
				try {
					if (table.getItemCount() <= 0)
						return;

					// skip if outside client area (ie. scrollbars)
					Rectangle rTableArea = table.getClientArea();
					//System.out.println("Mouse="+iMouseX+"x"+e.y+";TableArea="+rTableArea);
					Point pMousePosition = new Point(e.x, e.y);
					if (rTableArea.contains(pMousePosition)) {
						int[] columnOrder = table.getColumnOrder();
						if (columnOrder.length == 0) {
							return;
						}
						TableItem ti = table.getItem(table.getItemCount() - 1);
						Rectangle cellBounds = ti.getBounds(columnOrder[columnOrder.length - 1]);
						// OSX returns 0 size if the cell is not on screen (sometimes? all the time?)
						if (cellBounds.width <= 0 || cellBounds.height <= 0)
							return;
						//System.out.println("cellbounds="+cellBounds);
						if (e.x > cellBounds.x + cellBounds.width
								|| e.y > cellBounds.y + cellBounds.height) {
							table.deselectAll();
						}
						/*        // This doesn't work because of OS inconsistencies when table is scrolled
						 // Re-enable once SWT fixes the problem
						 // Bug 103934: Table.getItem(Point) uses incorrect calculation on Motif
						 //             Fixed 20050718 SWT 3.2M1 (3201) & SWT 3.1.1 (3139)
						 // TODO: Get Build IDs and use this code (if it works)
						 TableItem ti = table.getItem(pMousePosition);
						 if (ti == null)
						 table.deselectAll();
						 */
					}
				} catch (Exception ex) {
					System.out.println("MouseDownError");
					Debug.printStackTrace(ex);
				}
			}
		});

		table.addMouseMoveListener(new MouseMoveListener() {
			TableCellSWT lastCell = null;

			int lastCursorID = -1;

			public void mouseMove(MouseEvent e) {
				try {
					iMouseX = e.x;

					TableCellSWT cell = getTableCell(e.x, e.y);
					int iCursorID = -1;
					if (cell == null) {
						lastCell = null;
					} else if (cell != lastCell) {
						iCursorID = cell.getCursorID();
						lastCell = cell;
					}

					if (iCursorID != lastCursorID) {
						lastCursorID = iCursorID;

						if (iCursorID >= 0) {
							table.setCursor(table.getDisplay().getSystemCursor(iCursorID));
						} else {
							table.setCursor(null);
						}
					}

					if (cell != null) {
						TableCellMouseEvent event = createMouseEvent(cell, e,
								TableCellMouseEvent.EVENT_MOUSEMOVE);
						if (event != null) {
  						TableColumnCore tc = ((TableColumnCore) cell.getTableColumn());
  						if (tc.hasCellMouseMoveListener()) {
  							((TableColumnCore) cell.getTableColumn()).invokeCellMouseListeners(event);
  						}
  						cell.invokeMouseListeners(event);
						}
					}
				} catch (Exception ex) {
					Debug.out(ex);
				}
			}
		});

		table.addSelectionListener(new SelectionListener() {
			public void widgetSelected(SelectionEvent event) {
				triggerSelectionListeners(new TableRowCore[] {
					getRow((TableItem) event.item)
				});

				if (tabViews == null || tabViews.size() == 0)
					return;

				// Set Data Object for all tabs.  Tabs of PluginView are sent the plugin
				// Peer object, while Tabs of IView are sent the core PEPeer object.

				// TODO: Send all datasources
				Object[] dataSourcesCore = getSelectedDataSources(true);
				Object[] dataSourcesPlugin = null;

				for (int i = 0; i < tabViews.size(); i++) {
					IView view = (IView) tabViews.get(i);
					if (view != null) {
						if (view instanceof UISWTViewImpl) {
							if (dataSourcesPlugin == null)
								dataSourcesPlugin = getSelectedDataSources(false);

							((UISWTViewImpl) view).dataSourceChanged(dataSourcesPlugin.length == 0
									? null : dataSourcesPlugin);
						} else {
							view.dataSourceChanged(dataSourcesCore.length == 0 ? null
									: dataSourcesCore);
						}
					}
				}
			}

			public void widgetDefaultSelected(SelectionEvent e) {
				if (lCancelSelectionTriggeredOn > 0
						&& System.currentTimeMillis() - lCancelSelectionTriggeredOn < 200) {
					e.doit = false;
					lCancelSelectionTriggeredOn = -1;
				} else {
					runDefaultAction();
				}
			}
		});

		// we are sent a SWT.Settings event when the language changes and
		// when System fonts/colors change.  In both cases, invalidate
		if (SWT.getVersion() > 3200) {
			table.addListener(SWT.Settings, new Listener() {
				public void handleEvent(Event e) {
					tableInvalidate();
				}
			});
		}

		new TableTooltips(this, table);

		table.addKeyListener(new KeyListener() {
			public void keyPressed(KeyEvent event) {
				Object[] listeners = listenersKey.toArray();
				for (int i = 0; i < listeners.length; i++) {
					KeyListener l = (KeyListener) listeners[i];
					l.keyPressed(event);
					if (!event.doit) {
						return;
					}
				}

				if (event.keyCode == SWT.F5) {
					if ((event.stateMask & SWT.SHIFT) > 0) {
						runForSelectedRows(new TableGroupRowRunner() {
							public void run(TableRowCore row) {
								row.invalidate();
								row.refresh(true);
							}
						});
					} else {
						sortColumn(true);
					}
					event.doit = false;
					return;
				}

				int key = event.character;
				if (key <= 26 && key > 0)
					key += 'a' - 1;

				if (event.stateMask == SWT.MOD1) {
					switch (key) {
						case 'a': // CTRL+A select all Torrents
							if ((table.getStyle() & SWT.MULTI) > 0) {
								selectAll();
								event.doit = false;
							}
							break;

						case '+': {
							if (Constants.isUnix) {
								TableColumn[] tableColumnsSWT = table.getColumns();
								for (int i = 0; i < tableColumnsSWT.length; i++) {
									TableColumnCore tc = (TableColumnCore) tableColumnsSWT[i].getData("TableColumnCore");
									if (tc != null) {
										int w = tc.getPreferredWidth();
										if (w <= 0) {
											w = tc.getMinWidth();
											if (w <= 0) {
												w = 100;
											}
										}
										tc.setWidth(w);
									}
								}
								event.doit = false;
							}
							break;
						}
					}

					if (!event.doit)
						return;
				}
			}

			public void keyReleased(KeyEvent event) {
				Object[] listeners = listenersKey.toArray();
				for (int i = 0; i < listeners.length; i++) {
					KeyListener l = (KeyListener) listeners[i];
					l.keyReleased(event);
					if (!event.doit) {
						return;
					}
				}
			}
		});

		ScrollBar bar = table.getVerticalBar();
		if (bar != null) {
			bar.addSelectionListener(new SelectionAdapter() {
				public void widgetSelected(SelectionEvent e) {
					// Bug: Scroll is slow when table is not focus
					if (!table.isFocusControl()) {
						table.setFocus();
					}
				}
			});
		}

		table.setHeaderVisible(true);

		initializeTableColumns(table);
	}

	private TableCellMouseEvent createMouseEvent(TableCellSWT cell, MouseEvent e,
			int type) {
		TableCellMouseEvent event = new TableCellMouseEvent();
		event.cell = cell;
		if (cell != null) {
			event.row = cell.getTableRow();
		}
		event.eventType = type;
		event.button = e.button;
		// TODO: Change to not use SWT masks
		event.keyboardState = e.stateMask;
		event.skipCoreFunctionality = false;
		Rectangle r = cell.getBounds();
		event.x = e.x - r.x + VerticalAligner.getTableAdjustHorizontallyBy(table);
		if (event.x < 0) {
			return null;
		}
		event.y = e.y - r.y + VerticalAligner.getTableAdjustVerticalBy(table);
		if (event.y < 0) {
			return null;
		}

		return event;
	}

	/**
	 * @param event
	 */
	protected void paintItem(Event event) {
		TableItem item = (TableItem) event.item;
		if (item == null || item.isDisposed()) {
			return;
		}

		TableRowSWT row = (TableRowSWT) getRow(item);
		if (row == null) {
			return;
		}

		// SWT 3.2 only.  Code Ok -- Only called in SWT 3.2 mode
		Rectangle cellBounds = item.getBounds(event.index);

		cellBounds.x += 3;
		cellBounds.width -= 6;

		try {
			// SWT 3.2 only.  Code Ok -- Only called in SWT 3.2 mode
			int iColumnNo = event.index;

			if (item.getImage(iColumnNo) != null) {
				cellBounds.x += 18;
				cellBounds.width -= 18;
			}

			if (cellBounds.width <= 0 || cellBounds.height <= 0) {
				return;
			}

			if (bSkipFirstColumn) {
				if (iColumnNo == 0) {
					return;
				}
				iColumnNo--;
			}

			if (iColumnNo >= columnsOrdered.length) {
				System.out.println(iColumnNo + " >= " + columnsOrdered.length);
				return;
			}

			TableCellSWT cell = row.getTableCellSWT(columnsOrdered[iColumnNo].getName());

			if (!cell.isUpToDate()) {
				//System.out.println("R " + table.indexOf(item));
				cell.refresh(true, true);
				return;
			}

			//System.out.println("PS " + table.indexOf(item) + ";" + cellBounds);
			GCStringPrinter.printString(event.gc, cell.getText(), cellBounds, true,
					true,
					CoreTableColumn.getSWTAlign(columnsOrdered[iColumnNo].getAlignment()));

			if (cell.needsPainting()) {
				cell.doPaint(event.gc);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public void runDefaultAction() {
		// plugin may have cancelled the default action

		if (lCancelSelectionTriggeredOn > 0
				&& System.currentTimeMillis() - lCancelSelectionTriggeredOn < 200) {
			lCancelSelectionTriggeredOn = -1;
		} else {
			TableRowCore[] selectedRows = getSelectedRows();
			triggerDefaultSelectedListeners(selectedRows);
		}
	}

	protected void initializeTableColumns(final Table table) {
		TableColumn[] oldColumns = table.getColumns();

		if (SWT.getVersion() >= 3100)
			for (int i = 0; i < oldColumns.length; i++)
				oldColumns[i].removeListener(SWT.Move, columnMoveListener);

		for (int i = oldColumns.length - 1; i >= 0; i--)
			oldColumns[i].dispose();

		columnPaddingAdjusted = false;

		// Pre 3.0RC1 SWT on OSX doesn't call this!! :(
		ControlListener resizeListener = new ControlAdapter() {
			// Bug: getClientArea() eventually calls back to controlResized,
			//      creating a loop until a stack overflow
			private boolean bInFunction = false;

			public void controlResized(ControlEvent e) {
				TableColumn column = (TableColumn) e.widget;
				if (column == null || column.isDisposed() || bInFunction)
					return;

				try {
					bInFunction = true;

					TableColumnCore tc = (TableColumnCore) column.getData("TableColumnCore");
					if (tc != null) {
						Long lPadding = (Long) column.getData("widthOffset");
						int padding = (lPadding == null) ? 0 : lPadding.intValue();
						tc.setWidth(column.getWidth() - padding);
					}

					int columnNumber = table.indexOf(column);
					locationChanged(columnNumber);
				} finally {
					bInFunction = false;
				}
			}
		};

		// Add 1 to position because we make a non resizable 0-sized 1st column
		// to fix the 1st column gap problem (Eclipse Bug 43910)

		// SWT does not set 0 column width as expected in OS X; see bug 43910
		// this will be removed when a SWT-provided solution is available to satisfy all platforms with identation issue
		bSkipFirstColumn = bSkipFirstColumn && !Constants.isOSX;

		if (bSkipFirstColumn) {
			TableColumn tc = new TableColumn(table, SWT.NULL);
			tc.setWidth(0);
			tc.setResizable(false);
		}

		TableColumnCore[] tmpColumnsOrdered = new TableColumnCore[tableColumns.length];
		//Create all columns
		for (int i = 0; i < tableColumns.length; i++) {
			int position = tableColumns[i].getPosition();
			if (position != -1) {
				new TableColumn(table, SWT.NULL);
				tmpColumnsOrdered[position] = tableColumns[i];
			}
		}
		int numSWTColumns = table.getColumnCount();
		int iNewLength = numSWTColumns - (bSkipFirstColumn ? 1 : 0);
		columnsOrdered = new TableColumnCore[iNewLength];
		System.arraycopy(tmpColumnsOrdered, 0, columnsOrdered, 0, iNewLength);

		ColumnSelectionListener columnSelectionListener = new ColumnSelectionListener();

		//Assign length and titles
		//We can only do it after ALL columns are created, as position (order)
		//may not be in the natural order (if the user re-order the columns).
		for (int i = 0; i < tableColumns.length; i++) {
			int position = tableColumns[i].getPosition();
			if (position == -1)
				continue;

			String sName = tableColumns[i].getName();
			// +1 for Eclipse Bug 43910 (see above)
			// user has reported a problem here with index-out-of-bounds - not sure why
			// but putting in a preventative check so that hopefully the view still opens
			// so they can fix it

			int adjusted_position = position + (bSkipFirstColumn ? 1 : 0);

			if (adjusted_position >= numSWTColumns) {
				Debug.out("Incorrect table column setup, skipping column '" + sName
						+ "', position=" + adjusted_position + ";numCols=" + numSWTColumns);
				continue;
			}

			TableColumn column = table.getColumn(adjusted_position);
			try {
				column.setMoveable(true);
			} catch (NoSuchMethodError e) {
				// Ignore < SWT 3.1
			}
			column.setAlignment(CoreTableColumn.getSWTAlign(tableColumns[i].getAlignment()));
			Messages.setLanguageText(column, tableColumns[i].getTitleLanguageKey());
			column.setWidth(tableColumns[i].getWidth());
			if (tableColumns[i].getMinWidth() == tableColumns[i].getMaxWidth()
					&& tableColumns[i].getMinWidth() > 0) {
				column.setResizable(false);
			}
			column.setData("TableColumnCore", tableColumns[i]);
			column.setData("configName", "Table." + sTableID + "." + sName);
			column.setData("Name", sName);

			column.addControlListener(resizeListener);
			// At the time of writing this SWT (3.0RC1) on OSX doesn't call the 
			// selection listener for tables
			column.addListener(SWT.Selection, columnSelectionListener);
		}

		// Initialize the sorter after the columns have been added
		String sSortColumn = configMan.getStringParameter(sTableID + ".sortColumn",
				sDefaultSortOn);
		int iSortDirection = configMan.getIntParameter(CFG_SORTDIRECTION);
		boolean bSortAscending = configMan.getBooleanParameter(sTableID
				+ ".sortAsc", iSortDirection == 1 ? false : true);

		TableColumnManager tcManager = TableColumnManager.getInstance();
		TableColumnCore tc = tcManager.getTableColumnCore(sTableID, sSortColumn);
		if (tc == null) {
			tc = tableColumns[0];
		}
		sortColumn = tc;
		sortColumn.setSortAscending(bSortAscending);
		changeColumnIndicator();

		// Add move listener at the very end, so we don't get a bazillion useless 
		// move triggers
		if (SWT.getVersion() >= 3100) {
			Listener columnResizeListener = (!COLUMN_CLICK_DELAY) ? null
					: new Listener() {
						public void handleEvent(Event event) {
							lLastColumnResizeOn = System.currentTimeMillis();
						}
					};

			for (int i = 0; i < tableColumns.length; i++) {
				int position = tableColumns[i].getPosition();
				if (position == -1)
					continue;

				int adjusted_position = position + (bSkipFirstColumn ? 1 : 0);
				if (adjusted_position >= numSWTColumns)
					continue;

				TableColumn column = table.getColumn(adjusted_position);
				column.addListener(SWT.Move, columnMoveListener);
				if (COLUMN_CLICK_DELAY)
					column.addListener(SWT.Resize, columnResizeListener);
			}
		}
	}

	/** Creates the Context Menu.
	 *
	 * @return a new Menu object
	 */
	public Menu createMenu() {
		final Menu menu = new Menu(tableComposite.getShell(), SWT.POP_UP);
		MenuBuildUtils.addMaintenanceListenerForMenu(menu,
				new MenuBuildUtils.MenuBuilder() {
					public void buildMenu(Menu menu) {
						fillMenu(menu);
						addThisColumnSubMenu(getColumnNo(iMouseX));
					}
				});

		return menu;
	}

	/** Fill the Context Menu with items.  Called when menu is about to be shown.
	 *
	 * By default, a "Edit Columns" menu and a Column specific menu is set up.
	 *
	 * @param menu Menu to fill
	 */
	public void fillMenu(Menu menu) {
		Object[] listeners = listenersMenuFill.toArray();
		for (int i = 0; i < listeners.length; i++) {
			TableViewSWTMenuFillListener l = (TableViewSWTMenuFillListener) listeners[i];
			l.fillMenu(menu);
		}

		final MenuItem itemChangeTable = new MenuItem(menu, SWT.PUSH);
		Messages.setLanguageText(itemChangeTable,
				"MyTorrentsView.menu.editTableColumns");
		Utils.setMenuItemImage(itemChangeTable, "columns");

		itemChangeTable.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event e) {
				new TableColumnEditorWindow(table.getShell(), tableColumns,
						TableStructureEventDispatcher.getInstance(sTableID));
			}
		});

		menuThisColumn = new Menu(tableComposite.getShell(), SWT.DROP_DOWN);
		final MenuItem itemThisColumn = new MenuItem(menu, SWT.CASCADE);
		itemThisColumn.setMenu(menuThisColumn);

		// Add Plugin Context menus..
		boolean enable_items = table != null && table.getSelection().length > 0;

		TableContextMenuItem[] items = TableContextMenuManager.getInstance().getAllAsArray(
				sTableID);
		
		// We'll add download-context specific menu items - if the table is download specific.
		// We need a better way to determine this...
		org.gudy.azureus2.plugins.ui.menus.MenuItem[] menu_items = null;
		if ("MySeeders".equals(sTableID) || "MyTorrents".equals(sTableID)) {
			menu_items = MenuItemManager.getInstance().getAllAsArray("download_context");
		}
		else {
			menu_items = MenuItemManager.getInstance().getAllAsArray((String)null);
		}
		if (items.length > 0 || menu_items.length > 0) {
			new org.eclipse.swt.widgets.MenuItem(menu, SWT.SEPARATOR);
			MenuBuildUtils.addPluginMenuItems(getComposite(), items, menu, true,
					enable_items, new MenuBuildUtils.PluginMenuController() {
						public Listener makeSelectionListener(
								final org.gudy.azureus2.plugins.ui.menus.MenuItem plugin_menu_item) {
							return new TableSelectedRowsListener(TableViewSWTImpl.this) {
								public boolean run(TableRowCore[] rows) {
									if (rows.length != 0) {
										((TableContextMenuItemImpl) plugin_menu_item).invokeListenersMulti(rows);
									}
									return true;
								}
							};
						}

						public void notifyFillListeners(
								org.gudy.azureus2.plugins.ui.menus.MenuItem menu_item) {
							((TableContextMenuItemImpl) menu_item).invokeMenuWillBeShownListeners(getSelectedRows());
						}
					});
			
			// Add download context menu items.
			if (menu_items != null) {
				// getSelectedDataSources(false) returns us plugin items.
    			MenuBuildUtils.addPluginMenuItems(getComposite(), menu_items, menu, true, true,
						new MenuBuildUtils.MenuItemPluginMenuControllerImpl(getSelectedDataSources(false))
    			);				
			}
		}
	}

	/**
	 * SubMenu for column specific tasks. 
	 *
	 * @param iColumn Column # that tasks apply to.
	 */
	private void addThisColumnSubMenu(int iColumn) {
		MenuItem item;

		if (menuThisColumn == null || menuThisColumn.isDisposed())
			return;

		// Dispose of the old items
		MenuItem[] oldItems = menuThisColumn.getItems();
		for (int i = 0; i < oldItems.length; i++) {
			oldItems[i].dispose();
		}

		item = menuThisColumn.getParentItem();
		if (iColumn == -1) {
			item.setEnabled(false);
			item.setText(MessageText.getString("GenericText.column"));
			return;
		}

		item.setEnabled(true);

		menu.setData("ColumnNo", new Long(iColumn));

		TableColumn tcColumn = table.getColumn(iColumn);
		item.setText("'" + tcColumn.getText() + "' "
				+ MessageText.getString("GenericText.column"));

		String sColumnName = (String) tcColumn.getData("Name");
		if (sColumnName != null) {
			Object[] listeners = listenersMenuFill.toArray();
			for (int i = 0; i < listeners.length; i++) {
				TableViewSWTMenuFillListener l = (TableViewSWTMenuFillListener) listeners[i];
				l.addThisColumnSubMenu(sColumnName, menuThisColumn);
			}
		}

		if (menuThisColumn.getItemCount() > 0) {
			new MenuItem(menuThisColumn, SWT.SEPARATOR);
		}

		item = new MenuItem(menuThisColumn, SWT.PUSH);
		Messages.setLanguageText(item, "MyTorrentsView.menu.thisColumn.sort");
		item.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event e) {
				int iColumn = ((Long) menu.getData("ColumnNo")).intValue();
				table.getColumn(iColumn).notifyListeners(SWT.Selection, new Event());
			}
		});

		item = new MenuItem(menuThisColumn, SWT.PUSH);
		Messages.setLanguageText(item, "MyTorrentsView.menu.thisColumn.remove");
		item.setEnabled(false);

		item = new MenuItem(menuThisColumn, SWT.PUSH);
		Messages.setLanguageText(item, "MyTorrentsView.menu.thisColumn.toClipboard");
		item.addListener(SWT.Selection, new Listener() {
			public void handleEvent(Event e) {
				String sToClipboard = "";
				int iColumn = ((Long) menu.getData("ColumnNo")).intValue();
				TableItem[] tis = table.getSelection();
				for (int i = 0; i < tis.length; i++) {
					if (i != 0)
						sToClipboard += "\n";
					sToClipboard += tis[i].getText(iColumn);
				}
				new Clipboard(mainComposite.getDisplay()).setContents(new Object[] {
					sToClipboard
				}, new Transfer[] {
					TextTransfer.getInstance()
				});
			}
		});

		// Add Plugin Context menus..
		TableColumnCore tc = (TableColumnCore) tcColumn.getData("TableColumnCore");
		TableContextMenuItem[] items = tc.getContextMenuItems();
		if (items.length > 0) {
			new MenuItem(menuThisColumn, SWT.SEPARATOR);

			for (int i = 0; i < items.length; i++) {
				final TableContextMenuItemImpl contextMenuItem = (TableContextMenuItemImpl) items[i];
				final MenuItem menuItem = new MenuItem(menuThisColumn, SWT.PUSH);

				Messages.setLanguageText(menuItem, contextMenuItem.getResourceKey());
				menuItem.addListener(SWT.Selection,
						new TableSelectedRowsListener(this) {
							public boolean run(TableRowCore[] rows) {
								contextMenuItem.invokeListenersMulti(rows);
								return true;
							}
						});
			}
		}
	}

	/** IView.getComposite()
	 * @return the composite for this TableView
	 */
	public Composite getComposite() {
		return mainComposite;
	}

	public Composite getTableComposite() {
		return tableComposite;
	}

	public IView getActiveSubView() {
		if (!bEnableTabViews || tabFolder == null || tabFolder.isDisposed()
				|| tabFolder.getMinimized())
			return null;

		CTabItem item = tabFolder.getSelection();
		if (item != null) {
			return (IView) item.getData("IView");
		}

		return null;
	}

	public void refreshSelectedSubView() {
		IView view = getActiveSubView();
		if (view != null)
			view.refresh();
	}

	// see common.TableView
	public void refreshTable(final boolean bForceSort) {
		Utils.execSWTThread(new AERunnable() {
			public void runSupport() {
				_refreshTable(bForceSort);

				if (bEnableTabViews && tabFolder != null && !tabFolder.isDisposed()
						&& !tabFolder.getMinimized())
					refreshSelectedSubView();
			}
		});

		triggerTableRefreshListeners();
	}

	private void _refreshTable(boolean bForceSort) {
		// don't refresh while there's no table
		if (table == null)
			return;

		// XXX Try/Finally used to be there for monitor.enter/exit, however
		//     this doesn't stop re-entry from the same thread while already in
		//     process.. need a bAlreadyRefreshing variable instead
		try {
			if (getComposite() == null || getComposite().isDisposed())
				return;

			if (checkColumnWidthsEvery != 0
					&& (loopFactor % checkColumnWidthsEvery) == 0) {
				TableColumn[] tableColumnsSWT = table.getColumns();
				for (int i = 0; i < tableColumnsSWT.length; i++) {
					TableColumnCore tc = (TableColumnCore) tableColumnsSWT[i].getData("TableColumnCore");
					if (tc != null && tc.getWidth() != tableColumnsSWT[i].getWidth()) {
						tc.setWidth(tableColumnsSWT[i].getWidth());

						int columnNumber = table.indexOf(tableColumnsSWT[i]);
						locationChanged(columnNumber);
					}
				}
			}

			long lTimeStart = System.currentTimeMillis();

			final boolean bDoGraphics = (loopFactor % graphicsUpdate) == 0;
			final boolean bWillSort = bForceSort || (reOrderDelay != 0)
					&& ((loopFactor % reOrderDelay) == 0);
			//System.out.println("Refresh.. WillSort? " + bWillSort);

			if (bWillSort) {
				if (bForceSort) {
					sortColumn.setLastSortValueChange(SystemTime.getCurrentTime());
				}
				sortColumn(true);
			}

			lTimeStart = System.currentTimeMillis();

			//Refresh all visible items in table...
			runForAllRows(new TableGroupRowVisibilityRunner() {
				public void run(TableRowCore row, boolean bVisible) {
					row.refresh(bDoGraphics, bVisible);
				}
			});

			if (DEBUGADDREMOVE) {
				long lTimeDiff = (System.currentTimeMillis() - lTimeStart);
				if (lTimeDiff > 500)
					debug(lTimeDiff + "ms to refresh rows");
			}

			loopFactor++;
		} finally {
		}
	}

	private void refreshVisibleRows() {
		if (getComposite() == null || getComposite().isDisposed())
			return;

		runForVisibleRows(new TableGroupRowRunner() {
			public void run(TableRowCore row) {
				row.setAlternatingBGColor(true);
				row.refresh(false, true);
			}
		});
	}

	// see common.TableView
	public void processDataSourceQueue() {
		Object[] dataSourcesAdd = null;
		Object[] dataSourcesRemove = null;

		try {
			dataSourceToRow_mon.enter();
			if (dataSourcesToAdd.size() > 0) {
				dataSourcesAdd = dataSourcesToAdd.toArray();
				dataSourcesToAdd.clear();

				// remove the ones we are going to add then delete
				if (dataSourcesToRemove.size() > 0) {
					for (int i = 0; i < dataSourcesAdd.length; i++)
						if (dataSourcesToRemove.contains(dataSourcesAdd[i])) {
							dataSourcesToRemove.remove(dataSourcesAdd[i]);
							dataSourcesAdd[i] = null;
							if (DEBUGADDREMOVE)
								debug("Saved time by not adding a row that was removed");
						}
				}
			}

			if (dataSourcesToRemove.size() > 0) {
				dataSourcesRemove = dataSourcesToRemove.toArray();
				if (DEBUGADDREMOVE && dataSourcesRemove.length > 1)
					debug("Streamlining removing " + dataSourcesRemove.length + " rows");
				dataSourcesToRemove.clear();
			}
		} finally {
			dataSourceToRow_mon.exit();
		}

		if (dataSourcesAdd != null && dataSourcesAdd.length > 0) {
			reallyAddDataSources(dataSourcesAdd);
			if (DEBUGADDREMOVE && dataSourcesAdd.length > 1)
				debug("Streamlined adding " + dataSourcesAdd.length + " rows");
		}

		if (dataSourcesRemove != null && dataSourcesRemove.length > 0) {
			reallyRemoveDataSources(dataSourcesRemove);
		}
	}

	private void locationChanged(final int iStartColumn) {
		if (getComposite() == null || getComposite().isDisposed())
			return;

		runForAllRows(new TableGroupRowRunner() {
			public void run(TableRowCore row) {
				row.locationChanged(iStartColumn);
			}
		});
	}

	private void doPaint(final GC gc) {
		if (getComposite() == null || getComposite().isDisposed())
			return;

		runForVisibleRows(new TableGroupRowRunner() {
			public void run(TableRowCore row) {
				((TableRowSWT) row).doPaint(gc, true);
			}
		});
	}

	/** IView.delete: This method is called when the view is destroyed.
	 * Each color instanciated, images and such things should be disposed.
	 * The caller is the GUI thread.
	 */
	public void delete() {
		triggerLifeCycleListener(TableLifeCycleListener.EVENT_DESTROYED);

		if (tabViews != null && tabViews.size() > 0) {
			for (int i = 0; i < tabViews.size(); i++) {
				IView view = (IView) tabViews.get(i);
				if (view != null)
					view.delete();
			}
		}

		TableStructureEventDispatcher.getInstance(sTableID).removeListener(this);
		TableColumnManager tcManager = TableColumnManager.getInstance();
		if (tcManager != null) {
			tcManager.saveTableColumns(sTableID);
		}

		if (table != null && !table.isDisposed())
			table.dispose();
		removeAllTableRows();
		configMan.removeParameterListener("ReOrder Delay", this);
		configMan.removeParameterListener("Graphics Update", this);
		Colors.getInstance().removeColorsChangedListener(this);

		if (timerProcessDataSources != null) {
			timerProcessDataSources.destroy();
			timerProcessDataSources = null;
		}

		//oldSelectedItems =  null;
		Composite comp = getComposite();
		if (comp != null && !comp.isDisposed())
			comp.dispose();
	}

	/* (non-Javadoc)
	 * @see org.gudy.azureus2.ui.swt.views.AbstractIView#updateLanguage()
	 */
	public void updateLanguage() {
		if (tabViews != null && tabViews.size() > 0) {
			for (int i = 0; i < tabViews.size(); i++) {
				IView view = (IView) tabViews.get(i);
				if (view != null)
					view.updateLanguage();
			}
		}
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#addDataSource(java.lang.Object, boolean)
	public void addDataSource(Object datasource, boolean immediate) {
		addDataSource(datasource);
	}

	// see common.TableView
	public void addDataSource(Object dataSource) {
		addDataSources(new Object[] {
			dataSource
		});
	}

	// see common.TableView
	public void addDataSources(final Object dataSources[]) {

		if (dataSources == null)
			return;

		if (IMMEDIATE_ADDREMOVE_DELAY == 0) {
			reallyAddDataSources(dataSources);
			return;
		}

		// In order to save time, we cache entries to be added and process them
		// in a refresh cycle.  This is a huge benefit to tables that have
		// many rows being added and removed in rapid succession

		try {
			dataSourceToRow_mon.enter();

			int count = 0;

			for (int i = 0; i < dataSources.length; i++) {
				if (dataSources[i] != null
						&& !dataSourcesToAdd.contains(dataSources[i])) {
					count++;
					dataSourcesToAdd.add(dataSources[i]);
				}
			}

			if (DEBUGADDREMOVE)
				debug("Queued " + count + " of " + dataSources.length
						+ " dataSources to add.  Total Queued: " + dataSourcesToAdd.size());

		} finally {

			dataSourceToRow_mon.exit();
		}

		refreshenProcessDataSourcesTimer();
	}

	private void refreshenProcessDataSourcesTimer() {
		if (bReallyAddingDataSources || timerProcessDataSources == null) {
			// when timerProcessDataSources is null, we are disposing
			return;
		}

		synchronized (timerProcessDataSources) {
			if (timerEventProcessDS != null && !timerEventProcessDS.hasRun()) {
				// Push timer forward, unless we've pushed it forward for over x seconds
				long now = SystemTime.getCurrentTime();
				if (now - timerEventProcessDS.getCreatedTime() < IMMEDIATE_ADDREMOVE_MAXDELAY) {
					long lNextTime = now + IMMEDIATE_ADDREMOVE_DELAY;
					timerProcessDataSources.adjustAllBy(lNextTime
							- timerEventProcessDS.getWhen());
				} else {
					timerEventProcessDS.cancel();
					timerEventProcessDS = null;
					if (DEBUGADDREMOVE) {
						debug("Over immediate delay limit, processing queue now");
					}

					processDataSourceQueue();
				}
			} else {
				timerEventProcessDS = timerProcessDataSources.addEvent(
						SystemTime.getCurrentTime() + IMMEDIATE_ADDREMOVE_DELAY,
						new TimerEventPerformer() {
							public void perform(TimerEvent event) {
								if (DEBUGADDREMOVE && timerEventProcessDS != null) {
									debug("processDataSourceQueue after "
											+ (SystemTime.getCurrentTime() - timerEventProcessDS.getCreatedTime())
											+ "ms");
								}

								timerEventProcessDS = null;

								processDataSourceQueue();
							}
						});
			}
		}
	}

	private void reallyAddDataSources(final Object dataSources[]) {

		if (mainComposite == null || table == null || mainComposite.isDisposed()
				|| table.isDisposed())
			return;

		bReallyAddingDataSources = true;
		if (DEBUGADDREMOVE)
			debug(">>" + " Add " + dataSources.length + " rows;");

		Object[] remainingDataSources = null;
		Object[] doneDataSources = dataSources;

		// Create row, and add to map immediately
		try {
			dataSourceToRow_mon.enter();

			long lStartTime = SystemTime.getCurrentTime();

			for (int i = 0; i < dataSources.length; i++) {
				if (dataSources[i] == null)
					continue;

				// Break off and add the rows to the UI if we've taken too long to
				// create them
				if (SystemTime.getCurrentTime() - lStartTime > BREAKOFF_ADDTOMAP) {
					int iNewSize = dataSources.length - i;
					if (DEBUGADDREMOVE) {
						debug("Breaking off adding datasources to map after " + i
								+ " took " + (SystemTime.getCurrentTime() - lStartTime)
								+ "ms; # remaining: " + iNewSize);
					}
					remainingDataSources = new Object[iNewSize];
					doneDataSources = new Object[i];
					System.arraycopy(dataSources, i, remainingDataSources, 0, iNewSize);
					System.arraycopy(dataSources, 0, doneDataSources, 0, i);
					break;
				}

				if (mapDataSourceToRow.containsKey(dataSources[i])) {
					dataSources[i] = null;
				} else {
					TableRowImpl row = new TableRowImpl(this, table, sTableID,
							columnsOrdered, dataSources[i], bSkipFirstColumn);
					mapDataSourceToRow.put(dataSources[i], row);
				}
			}
		} catch (Exception e) {
			Logger.log(new LogEvent(LOGID, "Error while added row to Table "
					+ sTableID, e));
		} finally {
			dataSourceToRow_mon.exit();
		}

		if (DEBUGADDREMOVE)
			debug("--" + " Add " + doneDataSources.length + " rows;");

		if (remainingDataSources == null) {
			addDataSourcesToSWT(doneDataSources, true);
		} else {
			final Object[] fDoneDataSources = doneDataSources;
			final Object[] fRemainingDataSources = remainingDataSources;
			// wrap both calls in a SWT thread so that continuation of adding 
			// remaining datasources will be on SWT thread.  OSX has horrible handling
			// of switching to SWT thread.
			Utils.execSWTThread(new AERunnable() {
				public void runSupport() {
					addDataSourcesToSWT(fDoneDataSources, false);
					reallyAddDataSources(fRemainingDataSources);
				}
			}, false);
		}
	}

	private void addDataSourcesToSWT(final Object dataSources[], boolean async) {
		try {
			if (DEBUGADDREMOVE)
				debug("--" + " Add " + dataSources.length + " rows to SWT "
						+ (async ? " async " : " NOW"));

			if (async) {
				table.getDisplay().asyncExec(new AERunnable() {
					public void runSupport() {
						_addDataSourcesToSWT(dataSources);
					}
				});
			} else {
				Utils.execSWTThread(new AERunnable() {
					public void runSupport() {
						_addDataSourcesToSWT(dataSources);
					}
				}, false);
			}
		} catch (Exception e) {
			bReallyAddingDataSources = false;
			e.printStackTrace();
		}
	}

	private void _addDataSourcesToSWT(final Object dataSources[]) {
		if (table == null || table.isDisposed()) {
			bReallyAddingDataSources = false;
			return;
		}

		boolean bBrokeEarly = false;
		boolean bReplacedVisible = false;
		boolean bWas0Rows = table.getItemCount() == 0;
		try {
			dataSourceToRow_mon.enter();
			sortedRows_mon.enter();

			if (DEBUGADDREMOVE)
				debug("--" + " Add " + dataSources.length + " rows to SWT");

			// purposefully not included in time check 
			table.setItemCount(sortedRows.size() + dataSources.length);

			long lStartTime = SystemTime.getCurrentTime();

			int iTopIndex = table.getTopIndex();
			int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);

			// add to sortedRows list in best position.  
			// We need to be in the SWT thread because the rowSorter may end up
			// calling SWT objects.
			for (int i = 0; i < dataSources.length; i++) {
				Object dataSource = dataSources[i];
				if (dataSource == null)
					continue;

				// If we've been processing on the SWT thread for too long,
				// break off and allow SWT a breather to update.
				if (SystemTime.getCurrentTime() - lStartTime > BREAKOFF_ADDROWSTOSWT) {
					int iNewSize = dataSources.length - i;
					if (DEBUGADDREMOVE) {
						debug("Breaking off adding datasources to SWT after " + i
								+ " took " + (SystemTime.getCurrentTime() - lStartTime)
								+ "ms; # remaining: " + iNewSize);
					}
					Object[] remainingDataSources = new Object[iNewSize];
					System.arraycopy(dataSources, i, remainingDataSources, 0, iNewSize);
					addDataSourcesToSWT(remainingDataSources, true);
					bBrokeEarly = true;
					break;
				}

				TableRowImpl row = (TableRowImpl) mapDataSourceToRow.get(dataSource);
				if (row == null || row.getIndex() >= 0)
					continue;
				if (sortColumn != null) {
					TableCellSWT cell = row.getTableCellSWT(sortColumn.getName());
					if (cell != null) {
						try {
							cell.invalidate();
							cell.refresh(true);
						} catch (Exception e) {
							Logger.log(new LogEvent(LOGID,
									"Minor error adding a row to table " + sTableID, e));
						}
					}
				}

				try {
					int index = 0;
					if (sortedRows.size() > 0) {
						// If we are >= to the last item, then just add it to the end
						// instead of relying on binarySearch, which may return an item
						// in the middle that also is equal.
						TableRowSWT lastRow = (TableRowSWT) sortedRows.get(sortedRows.size() - 1);
						if (sortColumn.compare(row, lastRow) >= 0) {
							index = sortedRows.size();
							sortedRows.add(row);
							if (DEBUGADDREMOVE)
								debug("Adding new row to bottom");
						} else {
							index = Collections.binarySearch(sortedRows, row, sortColumn);
							if (index < 0)
								index = -1 * index - 1; // best guess

							if (index > sortedRows.size())
								index = sortedRows.size();

							if (DEBUGADDREMOVE)
								debug("Adding new row at position " + index + " of "
										+ (sortedRows.size() - 1));
							sortedRows.add(index, row);
						}
					} else {
						if (DEBUGADDREMOVE)
							debug("Adding new row to bottom (1st Entry)");
						index = sortedRows.size();
						sortedRows.add(row);
					}

					triggerListenerRowAdded(row);

					if (!bReplacedVisible && index >= iTopIndex && index <= iBottomIndex) {
						bReplacedVisible = true;
					}

					// XXX Don't set table item here, it will mess up selected rows
					//     handling (which is handled in fillRowGaps called later on)
					//row.setTableItem(index);
					row.setIconSize(ptIconSize);
				} catch (Exception e) {
					Logger.log(new LogEvent(LOGID, "Error adding a row to table "
							+ sTableID, e));
					try {
						if (!sortedRows.contains(row))
							sortedRows.add(row);
					} catch (Exception e2) {
						Debug.out(e2);
					}
				}
			} // for dataSources
			
			if (DEBUGADDREMOVE) {
				debug("Adding took "+ (SystemTime.getCurrentTime() - lStartTime) + "ms");
			}

			// Sanity Check: Make sure # of rows in table and in array match
			if (table.getItemCount() > sortedRows.size() && !bBrokeEarly) {
				// This could happen if one of the datasources was null, or
				// an error occured
				table.setItemCount(sortedRows.size());
			}

		} catch (Exception e) {
			Logger.log(new LogEvent(LOGID, "Error while adding row to Table "
					+ sTableID, e));
		} finally {
			sortedRows_mon.exit();
			dataSourceToRow_mon.exit();

			if (!bBrokeEarly) {
				bReallyAddingDataSources = false;
				refreshenProcessDataSourcesTimer();
			}
		}

		if (!bBrokeEarly || bReplacedVisible) {
			lastTopIndex = 0;
			lastBottomIndex = -1;
			fillRowGaps(false);
			visibleRowsChanged();
		}

		if (!columnPaddingAdjusted && table.getItemCount() > 0 && bWas0Rows) {
			TableColumn[] tableColumnsSWT = table.getColumns();
			TableItem item = table.getItem(0);
			for (int i = 0; i < tableColumnsSWT.length; i++) {
				TableColumnCore tc = (TableColumnCore) tableColumnsSWT[i].getData("TableColumnCore");
				if (tc != null) {
					Rectangle bounds = item.getBounds(i);
					int tcWidth = tc.getWidth();
					if (tcWidth != 0 && bounds.width != 0) {
  					int ofs = tc.getWidth() - bounds.width;
  					if (ofs > 0) {
  						tableColumnsSWT[i].setResizable(true);
  						tableColumnsSWT[i].setData("widthOffset", new Long(ofs));
  						tc.triggerColumnSizeChange();
  					}
					}
				}
			}
			columnPaddingAdjusted = true;
		}
		if (DEBUGADDREMOVE)
			debug("<< " + sortedRows.size());
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#removeDataSource(java.lang.Object, boolean)
	public void removeDataSource(Object dataSource, boolean immediate) {
		removeDataSources(new Object[] {
			dataSource
		});
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#removeDataSource(java.lang.Object)
	public void removeDataSource(final Object dataSource) {
		removeDataSources(new Object[] {
			dataSource
		});
	}

	/** Remove the specified dataSource from the table.
	 *
	 * @param dataSources data sources to be removed
	 * @param bImmediate Remove immediately, or queue and remove at next refresh
	 */
	public void removeDataSources(final Object[] dataSources) {
		if (dataSources == null) {
			return;
		}

		if (IMMEDIATE_ADDREMOVE_DELAY == 0) {
			reallyAddDataSources(dataSources);
			return;
		}

		try {
			dataSourceToRow_mon.enter();

			for (int i = 0; i < dataSources.length; i++)
				dataSourcesToRemove.add(dataSources[i]);

			if (DEBUGADDREMOVE)
				debug("Queued " + dataSources.length
						+ " dataSources to remove.  Total Queued: "
						+ dataSourcesToRemove.size());
		} finally {
			dataSourceToRow_mon.exit();
		}

		refreshenProcessDataSourcesTimer();
	}

	private void reallyRemoveDataSources(final Object[] dataSources) {

		if (DEBUGADDREMOVE)
			debug(">> Remove rows");

		final long lStart = SystemTime.getCurrentTime();

		boolean ok = Utils.execSWTThread(new AERunnable() {
			public void runSupport() {
				if (table == null || table.isDisposed()) {
					return;
				}

				StringBuffer sbWillRemove = null;
				if (DEBUGADDREMOVE) {
					debug(">>> Remove rows.  Start w/" + mapDataSourceToRow.size()
							+ "ds; tc=" + table.getItemCount() + ";"
							+ (SystemTime.getCurrentTime() - lStart) + "ms wait");

					sbWillRemove = new StringBuffer("Will soon remove row #");
				}

				ArrayList itemsToRemove = new ArrayList();
				ArrayList swtItemsToRemove = new ArrayList();
				int iTopIndex = table.getTopIndex();
				int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);
				boolean bRefresh = false;

				// pass one: get the SWT indexes of the items we are going to remove
				//           This will re-link them if they lost their link
				for (int i = 0; i < dataSources.length; i++) {
					if (dataSources[i] == null)
						continue;

					TableRowSWT item = (TableRowSWT) mapDataSourceToRow.get(dataSources[i]);
					if (item != null) {
						// use sortedRows position instead of item.getIndex(), because
						// getIndex may have a wrong value (unless we fillRowGaps() which
						// is more time consuming and we do afterwards anyway)
						int index = sortedRows.indexOf(item);
						if (!bRefresh) {
							bRefresh = index >= iTopIndex || index <= iBottomIndex;
						}
						if (DEBUGADDREMOVE) {
							if (i != 0) {
								sbWillRemove.append(", ");
							}
							sbWillRemove.append(index);
						}
						if (index >= 0) {
							swtItemsToRemove.add(new Long(index));
						}
					}
				}

				if (DEBUGADDREMOVE) {
					debug(sbWillRemove.toString());
					debug("#swtItemsToRemove=" + swtItemsToRemove.size());
				}

				// pass 2: remove from map and list, add removed to seperate list
				for (int i = 0; i < dataSources.length; i++) {
					if (dataSources[i] == null)
						continue;

					// Must remove from map before deleted from gui
					TableRowSWT item = (TableRowSWT) mapDataSourceToRow.remove(dataSources[i]);
					if (item != null) {
						itemsToRemove.add(item);
						sortedRows.remove(item);
						triggerListenerRowRemoved(item);
					}
				}

				// Remove the rows from SWT first.  On SWT 3.2, this currently has 
				// zero perf gain, and a small perf gain on Windows.  However, in the
				// future it may be optimized.
				if (swtItemsToRemove.size() > 0) {
					int[] swtRowsToRemove = new int[swtItemsToRemove.size()];
					for (int i = 0; i < swtItemsToRemove.size(); i++) {
						swtRowsToRemove[i] = ((Long) swtItemsToRemove.get(i)).intValue();
					}
					table.remove(swtRowsToRemove);
				}

				// Finally, delete the rows
				for (Iterator iter = itemsToRemove.iterator(); iter.hasNext();) {
					TableRowCore row = (TableRowCore) iter.next();
					row.delete();
				}

				if (bRefresh) {
					refreshVisibleRows();
				}

				if (DEBUGADDREMOVE)
					debug("<< Remove " + itemsToRemove.size() + " rows. now "
							+ mapDataSourceToRow.size() + "ds; tc=" + table.getItemCount());
			}
		});

		if (!ok) {
			// execRunnable will only fail if we are closing
			for (int i = 0; i < dataSources.length; i++) {
				if (dataSources[i] == null)
					continue;

				TableRowSWT item = (TableRowSWT) mapDataSourceToRow.get(dataSources[i]);
				mapDataSourceToRow.remove(dataSources[i]);
				if (item != null) {
					sortedRows.remove(item);
					item.delete();
				}
			}

			fillRowGaps(false);
			if (DEBUGADDREMOVE)
				debug("<< Remove 1 row, noswt");
		}
	}

	// from common.TableView
	public void removeAllTableRows() {
		long lTimeStart = System.currentTimeMillis();

		final TableRowCore[] rows = getRows();

		try {
			dataSourceToRow_mon.enter();
			sortedRows_mon.enter();

			mapDataSourceToRow.clear();
			sortedRows.clear();

			dataSourcesToAdd.clear();
			dataSourcesToRemove.clear();

			if (DEBUGADDREMOVE)
				debug("removeAll");

		} finally {

			sortedRows_mon.exit();
			dataSourceToRow_mon.exit();
		}

		Utils.execSWTThread(new AERunnable() {
			public void runSupport() {
				if (table != null && !table.isDisposed())
					table.removeAll();

				// Image Disposal handled by each cell

				for (int i = 0; i < rows.length; i++)
					rows[i].delete();
			}
		});

		if (DEBUGADDREMOVE) {
			long lTimeDiff = (System.currentTimeMillis() - lTimeStart);
			if (lTimeDiff > 10)
				debug("RemovaAll took " + lTimeDiff + "ms");
		}
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getTableID()
	public String getTableID() {
		return sTableID;
	}

	/* ParameterListener Implementation */

	public void parameterChanged(String parameterName) {
		if (parameterName == null || parameterName.equals("Graphics Update")) {
			graphicsUpdate = configMan.getIntParameter("Graphics Update");
			return;
		}
		if (parameterName == null || parameterName.equals("ReOrder Delay")) {
			reOrderDelay = configMan.getIntParameter("ReOrder Delay");
			return;
		}
		if (parameterName == null || parameterName.startsWith("Color")) {
			tableInvalidate();
		}
	}

	// ITableStructureModificationListener
	public void tableStructureChanged() {
		triggerLifeCycleListener(TableLifeCycleListener.EVENT_DESTROYED);

		removeAllTableRows();
		initializeTableColumns(table);
		refreshTable(false);

		triggerLifeCycleListener(TableLifeCycleListener.EVENT_INITIALIZED);
	}

	// ITableStructureModificationListener
	public void columnOrderChanged(int[] positions) {
		try {
			table.setColumnOrder(positions);
		} catch (NoSuchMethodError e) {
			// Pre SWT 3.1
			// This shouldn't really happen, since this function only gets triggered
			// from SWT >= 3.1
			tableStructureChanged();
		}
	}

	/** 
	 * The Columns width changed
	 */
	// ITableStructureModificationListener
	public void columnSizeChanged(TableColumnCore tableColumn) {
		int newWidth = tableColumn.getWidth();
		if (table == null || table.isDisposed())
			return;

		TableColumn column = null;
		TableColumn[] tableColumnsSWT = table.getColumns();
		for (int i = 0; i < tableColumnsSWT.length; i++) {
			if (tableColumnsSWT[i].getData("TableColumnCore") == tableColumn) {
				column = tableColumnsSWT[i];
				break;
			}
		}
		Long lOfs = (Long) column.getData("widthOffset");
		if (lOfs != null) {
			newWidth += lOfs.intValue();
		}
		if (column == null || column.isDisposed()
				|| (column.getWidth() == newWidth))
			return;

		if (Constants.isUnix) {
			final int fNewWidth = newWidth;
			final TableColumn fTableColumn = column;
			column.getDisplay().asyncExec(new AERunnable() {
				public void runSupport() {
					if (!fTableColumn.isDisposed()) {
						fTableColumn.setWidth(fNewWidth);
					}
				}
			});
		} else {
			column.setWidth(newWidth);
		}
	}

	// ITableStructureModificationListener
	// TableView
	public void columnInvalidate(TableColumnCore tableColumn) {
		// We are being called from a plugin (probably), so we must refresh
		columnInvalidate(tableColumn, true);
	}
	
	public void cellInvalidate(TableColumnCore tableColumn, Object data_source) {
		cellInvalidate(tableColumn, data_source, true);
	}

	public void columnRefresh(TableColumnCore tableColumn) {
		final String sColumnName = tableColumn.getName();
		runForAllRows(new TableGroupRowVisibilityRunner() {
			public void run(TableRowCore row, boolean bVisible) {
				TableCellSWT cell = ((TableRowSWT) row).getTableCellSWT(sColumnName);
				if (cell != null)
					cell.refresh(true, bVisible);
			}
		});
	}

	/**
	 * Invalidate and refresh whole table
	 */
	public void tableInvalidate() {
		runForAllRows(new TableGroupRowVisibilityRunner() {
			public void run(TableRowCore row, boolean bVisible) {
				row.invalidate();
				row.refresh(true, bVisible);
			}
		});
	}

	// see common.TableView
	public void columnInvalidate(final String sColumnName) {
		TableColumnCore tc = TableColumnManager.getInstance().getTableColumnCore(
				sTableID, sColumnName);
		if (tc != null)
			columnInvalidate(tc, tc.getType() == TableColumnCore.TYPE_TEXT_ONLY);
	}

	public void columnInvalidate(TableColumnCore tableColumn,
			final boolean bMustRefresh) {
		final String sColumnName = tableColumn.getName();

		runForAllRows(new TableGroupRowRunner() {
			public void run(TableRowCore row) {
				TableCellSWT cell = ((TableRowSWT) row).getTableCellSWT(sColumnName);
				if (cell != null)
					cell.invalidate(bMustRefresh);
			}
		});
	}
	
	public void cellInvalidate(TableColumnCore tableColumn,
			final Object data_source, final boolean bMustRefresh) {
		final String sColumnName = tableColumn.getName();

		runForAllRows(new TableGroupRowRunner() {
			public void run(TableRowCore row) {
				TableCellSWT cell = ((TableRowSWT) row).getTableCellSWT(sColumnName);
				if (cell != null && cell.getDataSource() != null && cell.getDataSource().equals(data_source)) {
					cell.invalidate(bMustRefresh);
				}
			}
		});		
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getColumnCells(java.lang.String)
	public TableCellCore[] getColumnCells(String sColumnName) {
		TableCellCore[] cells = new TableCellCore[sortedRows.size()];

		try {
			sortedRows_mon.enter();

			int i = 0;
			for (Iterator iter = sortedRows.iterator(); iter.hasNext();) {
				TableRowCore row = (TableRowCore) iter.next();
				cells[i++] = row.getTableCellCore(sColumnName);
			}

		} finally {
			sortedRows_mon.exit();
		}

		return cells;
	}

	// see common.TableView
	public TableRowCore[] getRows() {
		try {
			sortedRows_mon.enter();

			return (TableRowCore[]) sortedRows.toArray(new TableRowCore[0]);

		} finally {
			sortedRows_mon.exit();
		}
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getRow(java.lang.Object)
	public TableRowCore getRow(Object dataSource) {
		return (TableRowCore) mapDataSourceToRow.get(dataSource);
	}

	// @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#getRowSWT(java.lang.Object)
	public TableRowSWT getRowSWT(Object dataSource) {
		return (TableRowSWT) mapDataSourceToRow.get(dataSource);
	}

	private TableRowCore getRow(int iPos) {
		try {
			sortedRows_mon.enter();

			if (iPos >= 0 && iPos < sortedRows.size()) {
				TableRowCore row = (TableRowCore) sortedRows.get(iPos);

				if (row.getIndex() != iPos) {
					row.setTableItem(iPos);
				}
				return row;
			}
		} finally {
			sortedRows_mon.exit();
		}
		return null;
	}

	public int indexOf(TableRowCore row) {
		int i = ((TableRowImpl) row).getRealIndex();
		if (i == -1) {
			i = sortedRows.indexOf(row);
			if (i >= 0) {
				row.setTableItem(i);
			}
		}
		return i;
	}

	private TableRowCore getRow(TableItem item) {
		try {
			Object o = item.getData("TableRow");
			if (o instanceof TableRowCore) {
				return (TableRowCore) o;
			} else {
				int iPos = table.indexOf(item);
				//System.out.println(iPos + " has no table row.. associating. " + Debug.getCompressedStackTrace(4));
				if (iPos >= 0 && iPos < sortedRows.size()) {
					TableRowSWT row = (TableRowSWT) sortedRows.get(iPos);
					//System.out.print(".. associating to " + row);
					if (row != null) {
						row.setTableItem(iPos);
					}
					//System.out.println(", now " + row);
					return row;
				}
			}
		} catch (Exception e) {
			Debug.out(e);
		}
		return null;
	}

	public int getRowCount() {
		// don't use sortedRows here, it's not always up to date 
		return mapDataSourceToRow.size();
	}

	public Object[] getDataSources() {
		return mapDataSourceToRow.keySet().toArray();
	}

	/* various selected rows functions */
	/***********************************/

	public List getSelectedDataSourcesList() {
		return getSelectedDataSourcesList(true);
	}

	/** Returns an array of all selected Data Sources.  Null data sources are
	 * ommitted.
	 *
	 * @return an array containing the selected data sources
	 * 
	 * @TODO TuxPaper: Virtual row not created when using getSelection?
	 *                  computePossibleActions isn't being calculated right
	 *                  because of non-created rows when select user selects all
	 */
	public List getSelectedDataSourcesList(final boolean bCoreDataSource) {
		final ArrayList l = new ArrayList();
		if (table == null || table.isDisposed()) {
			return l;
		}
		Utils.execSWTThread(new AERunnable() {
			public void runSupport() {
				if (table.isDisposed()) {
					return;
				}
				TableItem[] tis = table.getSelection();
				for (int i = 0; i < tis.length; i++) {
					TableRowSWT row = (TableRowSWT) getRow(tis[i]);
					if (row == null) {
						fillRowGaps(false);

						// Try again
						row = (TableRowSWT) getRow(tis[i]);
						if (row == null)
							System.out.println("XXX Boo, row still null "
									+ table.indexOf(tis[i]) + ";sd=" + tis[i].getData("SD") + ";"
									+ Debug.getCompressedStackTrace());
					}
					if (row != null && row.getDataSource(true) != null)
						l.add(row.getDataSource(bCoreDataSource));
				}
			}
		}, false);
		return l;
	}

	/** Returns an array of all selected Data Sources.  Null data sources are
	 * ommitted.
	 *
	 * @param a the array into which the selected data sources are to be stored, 
	 *          if the size is the big enough; otherwise, a new array of the same 
	 *          runtime type is allocated for this purpose.
	 *
	 * @return an array containing the selected data sources
	 */
	public Object[] getSelectedDataSources(Object[] a) {
		return getSelectedDataSourcesList().toArray(a);
	}

	// see common.TableView
	public Object[] getSelectedDataSources() {
		return getSelectedDataSourcesList().toArray();
	}

	// see common.TableView
	public Object[] getSelectedDataSources(boolean bCoreDataSource) {
		return getSelectedDataSourcesList(bCoreDataSource).toArray();
	}

	/** @see com.aelitis.azureus.ui.common.table.TableView#getSelectedRows() */
	public TableRowCore[] getSelectedRows() {
		return (TableRowCore[]) getSelectedRowsList().toArray(new TableRowCore[0]);
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getSelectedRowsSize()
	public int getSelectedRowsSize() {
		if (table != null && !table.isDisposed()) {
			return table.getSelectionCount();
		}
		return 0;
	}

	public TableRowSWT[] getSelectedRowsSWT() {
		return (TableRowSWT[]) getSelectedRowsList().toArray(new TableRowSWT[0]);
	}

	/** Returns an list of all selected TableRowSWT objects.  Null data sources are
	 * ommitted.
	 *
	 * @return an list containing the selected TableRowSWT objects
	 */
	public List getSelectedRowsList() {
		ArrayList l = new ArrayList();
		if (table != null && !table.isDisposed()) {
			TableItem[] tis = table.getSelection();
			for (int i = 0; i < tis.length; i++) {
				TableRowSWT row = (TableRowSWT) getRow(tis[i]);
				if (row != null && row.getDataSource(true) != null)
					l.add(row);
			}
		}
		return l;
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getFocusedRow()
	public TableRowCore getFocusedRow() {
		TableRowSWT[] selectedRows = getSelectedRowsSWT();
		if (selectedRows.length == 0) {
			return null;
		}
		return selectedRows[0];
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getFirstSelectedDataSource()
	public Object getFirstSelectedDataSource() {
		return getFirstSelectedDataSource(true);
	}

	public TableRowSWT[] getVisibleRows() {
		if (table == null || table.isDisposed())
			return new TableRowSWT[0];

		int iTopIndex = table.getTopIndex();
		int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);

		int size = iBottomIndex - iTopIndex + 1;
		if (size <= 0)
			return new TableRowSWT[0];

		TableRowSWT[] rows = new TableRowSWT[size];
		int pos = 0;
		for (int i = iTopIndex; i <= iBottomIndex; i++) {
			TableItem item = table.getItem(i);
			if (item != null && !item.isDisposed()) {
				TableRowSWT row = (TableRowSWT) getRow(item);
				if (row != null) {
					rows[pos++] = row;
				}
			}
		}

		if (pos <= rows.length) {
			// Some were null, shrink array
			TableRowSWT[] temp = new TableRowSWT[pos];
			System.arraycopy(rows, 0, temp, 0, pos);
			return temp;
		}

		return rows;
	}

	/** Returns the first selected data sources.
	 *
	 * @return the first selected data source, or null if no data source is 
	 *         selected
	 */
	public Object getFirstSelectedDataSource(boolean bCoreObject) {
		if (table == null || table.isDisposed() || table.getSelectionCount() == 0)
			return null;

		TableRowCore row = getRow(table.getSelection()[0]);
		if (row == null)
			return null;
		return row.getDataSource(bCoreObject);
	}

	/** For each row source that the user has selected, run the code
	 * provided by the specified parameter.
	 *
	 * @param runner Code to run for each selected row/datasource
	 */
	public void runForSelectedRows(TableGroupRowRunner runner) {
		if (table == null || table.isDisposed())
			return;

		TableItem[] tis = table.getSelection();
		List rows_to_use = null;
		if (runner.supportsMultipleRows()) {
			rows_to_use = new ArrayList(tis.length);
		}
		for (int i = 0; i < tis.length; i++) {
			TableRowSWT row = (TableRowSWT) getRow(tis[i]);
			if (row != null)
				if (rows_to_use != null) {
					rows_to_use.add(row);
				} else {
					runner.run(row);
				}
		}
		if (rows_to_use != null) {
			runner.run((TableRowSWT[]) rows_to_use.toArray(new TableRowSWT[rows_to_use.size()]));
		}
	}

	/** For each visible row source, run the code provided by the specified 
	 * parameter.
	 *
	 * @param runner Code to run for each selected row/datasource
	 */
	public void runForVisibleRows(TableGroupRowRunner runner) {
		TableRowSWT[] rows = getVisibleRows();
		if (runner.run(rows)) {
			return;
		}

		for (int i = 0; i < rows.length; i++)
			runner.run(rows[i]);
	}

	// see common.tableview
	public void runForAllRows(TableGroupRowVisibilityRunner runner) {
		if (table == null || table.isDisposed()) {
			return;
		}

		// put to array instead of synchronised iterator, so that runner can remove
		TableRowCore[] rows = getRows();
		int iTopIndex = table.getTopIndex();
		int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);

		for (int i = 0; i < rows.length; i++) {
			runner.run(rows[i], i >= iTopIndex && i <= iBottomIndex);
		}
	}

	/**
	 * Runs a specified task for a list of table items that the table contains
	 * @param items A list of TableItems that are part of the table view
	 * @param runner A task
	 */
	public void runForTableItems(List items, TableGroupRowRunner runner) {
		if (table == null || table.isDisposed())
			return;

		final Iterator iter = items.iterator();
		List rows_to_use = null;
		if (runner.supportsMultipleRows()) {
			rows_to_use = new ArrayList(items.size());
		}
		while (iter.hasNext()) {
			TableItem tableItem = (TableItem) iter.next();
			if (tableItem.isDisposed())
				continue;

			TableRowSWT row = (TableRowSWT) getRow(tableItem);
			if (row != null) {
				if (rows_to_use != null) {
					rows_to_use.add(row);
				} else {
					runner.run(row);
				}
			}
		}
		if (rows_to_use != null) {
			runner.run((TableRowSWT[]) rows_to_use.toArray(new TableRowSWT[rows_to_use.size()]));
		}
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#clipboardSelected()
	public void clipboardSelected() {
		String sToClipboard = "";
		for (int j = 0; j < table.getColumnCount(); j++) {
			if (j != 0)
				sToClipboard += "\t";
			sToClipboard += table.getColumn(j).getText();
		}

		TableItem[] tis = table.getSelection();
		for (int i = 0; i < tis.length; i++) {
			sToClipboard += "\n";
			for (int j = 0; j < table.getColumnCount(); j++) {
				if (j != 0)
					sToClipboard += "\t";
				sToClipboard += tis[i].getText(j);
			}
		}
		new Clipboard(getComposite().getDisplay()).setContents(new Object[] {
			sToClipboard
		}, new Transfer[] {
			TextTransfer.getInstance()
		});
	}

	/** Handle sorting of a column based on clicking the Table Header */
	private class ColumnSelectionListener
		implements Listener
	{
		/** Process a Table Header click
		 * @param event event information
		 */
		public void handleEvent(final Event event) {
			if (COLUMN_CLICK_DELAY) {
				// temporary for OSX.. resizing column triggers selection, so cancel
				// if a resize was recent. 
				final Timer timer = new Timer("Column Selection Wait");
				timer.addEvent(System.currentTimeMillis() + 85,
						new TimerEventPerformer() {
							public void perform(TimerEvent timerEvent) {
								Utils.execSWTThread(new AERunnable() {
									public void runSupport() {
										if (lLastColumnResizeOn == -1
												|| System.currentTimeMillis() - lLastColumnResizeOn > 220)
											reallyHandleEvent(event);
									}
								});
								timer.destroy();
							}
						});
			} else {
				reallyHandleEvent(event);
			}
		}

		private void reallyHandleEvent(Event event) {
			TableColumn column = (TableColumn) event.widget;
			if (column == null)
				return;
			TableColumnCore tableColumnCore = (TableColumnCore) column.getData("TableColumnCore");
			if (tableColumnCore != null) {
				sortColumnReverse(tableColumnCore);
				refreshTable(true);
			}
		}
	}

	/**
	 * Handle movement of a column based on user dragging the Column Header.
	 * SWT >= 3.1
	 */
	private class ColumnMoveListener
		implements Listener
	{
		public void handleEvent(Event event) {
			TableColumn column = (TableColumn) event.widget;
			if (column == null)
				return;

			TableColumnCore tableColumnCore = (TableColumnCore) column.getData("TableColumnCore");
			if (tableColumnCore == null)
				return;

			Table table = column.getParent();

			// Get the 'added position' of column
			// It would have been easier if event (.start, .end) contained the old
			// and new position..
			TableColumn[] tableColumns = table.getColumns();
			int iAddedPosition;
			for (iAddedPosition = 0; iAddedPosition < tableColumns.length; iAddedPosition++) {
				if (column == tableColumns[iAddedPosition])
					break;
			}
			if (iAddedPosition >= tableColumns.length)
				return;

			// Find out position in the order list
			int iColumnOrder[];
			try {
				iColumnOrder = table.getColumnOrder();
			} catch (NoSuchMethodError e) {
				// Ignore < SWT 3.1
				return;
			}
			for (int i = 0; i < iColumnOrder.length; i++) {
				if (iColumnOrder[i] == iAddedPosition) {
					int iNewPosition = i - (bSkipFirstColumn ? 1 : 0);
					if (tableColumnCore.getPosition() != iNewPosition) {
						//System.out.println("Moving " + tableColumnCore.getName() + " to Position " + i);
						tableColumnCore.setPositionNoShift(iNewPosition);
						tableColumnCore.saveSettings();
						TableStructureEventDispatcher.getInstance(sTableID).columnOrderChanged(
								iColumnOrder);
					}
					break;
				}
			}
		}
	}

	private int getColumnNo(int iMouseX) {
		int iColumn = -1;
		int itemCount = table.getItemCount();
		if (table.getItemCount() > 0) {
			//Using  table.getTopIndex() instead of 0, cause
			//the first row has no bounds when it's not visible under OS X.
			int topIndex = table.getTopIndex();
			if (topIndex >= itemCount) {
				topIndex = itemCount - 1;
			}
			TableItem ti = table.getItem(topIndex);
			if (ti.isDisposed()) {
				return -1;
			}
			for (int i = bSkipFirstColumn ? 1 : 0; i < table.getColumnCount(); i++) {
				// M8 Fixes SWT GTK Bug 51777:
				//  "TableItem.getBounds(int) returns the wrong values when table scrolled"
				Rectangle cellBounds = ti.getBounds(i);
				//System.out.println("i="+i+";Mouse.x="+iMouseX+";cellbounds="+cellBounds);
				if (iMouseX >= cellBounds.x
						&& iMouseX < cellBounds.x + cellBounds.width
						&& cellBounds.width > 0) {
					iColumn = i;
					break;
				}
			}
		}
		return iColumn;
	}

	public TableRowCore getRow(int x, int y) {
		int iColumn = getColumnNo(x);
		if (iColumn < 0)
			return null;

		TableItem item = table.getItem(new Point(2, y));
		return (TableRowCore) getRow(item);
	}

	public TableCellSWT getTableCell(int x, int y) {
		int iColumn = getColumnNo(x);
		if (iColumn < 0)
			return null;

		TableItem item = table.getItem(new Point(2, y));
		if (item == null)
			return null;
		TableRowSWT row = (TableRowSWT) getRow(item);

		if (row == null) {
			return null;
		}

		TableColumn tcColumn = table.getColumn(iColumn);
		String sCellName = (String) tcColumn.getData("Name");
		if (sCellName == null)
			return null;

		return row.getTableCellSWT(sCellName);
	}

	public TableRowSWT getTableRow(int x, int y) {
		TableItem item = table.getItem(new Point(2, y));
		if (item == null)
			return null;
		return (TableRowSWT) getRow(item);
	}

	private TableColumnCore getTableColumnByOffset(int x) {
		int iColumn = getColumnNo(x);
		if (iColumn < 0)
			return null;

		TableColumn column = table.getColumn(iColumn);
		return (TableColumnCore) column.getData("TableColumnCore");
	}

	// @see org.gudy.azureus2.core3.util.AEDiagnosticsEvidenceGenerator#generate(org.gudy.azureus2.core3.util.IndentWriter)
	public void generate(IndentWriter writer) {
		writer.println("Diagnostics for " + this + " (" + sTableID + ")");

		try {
			dataSourceToRow_mon.enter();
 
			writer.println("DataSources scheduled to Add/Remove: "
					+ dataSourcesToAdd.size() + "/" + dataSourcesToRemove.size());

			writer.println("TableView: " + mapDataSourceToRow.size() + " datasources");
			Iterator it = mapDataSourceToRow.keySet().iterator();

			while (it.hasNext()) {

				Object key = it.next();

				writer.println("  " + key + " -> " + mapDataSourceToRow.get(key));
			}

			writer.println("# of SubViews: " + tabViews.size());
			writer.indent();
			try {
				for (Iterator iter = tabViews.iterator(); iter.hasNext();) {
					IView view = (IView) iter.next();
					view.generateDiagnostics(writer);
				}
			} finally {
				writer.exdent();
			}
			
			writer.println("Columns:");
			writer.indent();
			try {
  			TableColumn[] tableColumnsSWT = table.getColumns();
  			for (int i = 0; i < tableColumnsSWT.length; i++) {
  				final TableColumnCore tc = (TableColumnCore) tableColumnsSWT[i].getData("TableColumnCore");
  				if (tc != null) {
  					writer.println(tc.getName() + ";w=" + tc.getWidth() + ";w-offset="
								+ tableColumnsSWT[i].getData("widthOffset"));
  				}
  			}
			} catch (Throwable t) {
			} finally {
				writer.exdent();
			}

		} finally {

			dataSourceToRow_mon.exit();
		}
	}

	public boolean getSkipFirstColumn() {
		return bSkipFirstColumn;
	}

	// see common.TableView
	public void setRowDefaultHeight(int iHeight) {
		if (ptIconSize == null)
			ptIconSize = new Point(1, iHeight);
		else
			ptIconSize.y = iHeight;
		bSkipFirstColumn = true;
	}

	public int getRowDefaultHeight() {
		if (ptIconSize == null)
			return 0;
		return ptIconSize.y;
	}

	// from common.TableView
	public void setRowDefaultIconSize(Point size) {
		ptIconSize = size;
		bSkipFirstColumn = true;
	}

	// TabViews Functions
	public void addTabView(IView view) {
		if (view == null || tabFolder == null)
			return;

		CTabItem item = new CTabItem(tabFolder, SWT.NULL);
		item.setData("IView", view);
		Messages.setLanguageText(item, view.getData());
		view.initialize(tabFolder);
		item.setControl(view.getComposite());
		tabViews.add(view);
	}

	private void fillRowGaps(boolean bForceDataRefresh) {
		_sortColumn(bForceDataRefresh, true, true);
	}

	private void sortColumn(boolean bForceDataRefresh) {
		_sortColumn(bForceDataRefresh, false, true);
	}

	private void _sortColumn(boolean bForceDataRefresh, boolean bFillGapsOnly,
			boolean bFollowSelected) {
		if (table == null || table.isDisposed()) {
			return;
		}

		try {
			sortColumn_mon.enter();

			long lTimeStart;
			if (DEBUG_SORTER) {
				//System.out.println(">>> Sort.. ");
				lTimeStart = System.currentTimeMillis();
			}

			int iNumMoves = 0;

			// This actually gets the focus, assuming the focus is selected
			int iFocusIndex = table.getSelectionIndex();
			TableRowCore focusedRow = (iFocusIndex == -1) ? null
					: getRow(iFocusIndex);

			int iTopIndex = table.getTopIndex();
			int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);
			boolean allSelectedRowsVisible = true;

			int[] selectedRowIndices = table.getSelectionIndices();
			TableRowCore[] selectedRows = new TableRowCore[selectedRowIndices.length];
			for (int i = 0; i < selectedRowIndices.length; i++) {
				int index = selectedRowIndices[i];
				selectedRows[i] = getRow(index);
				if (allSelectedRowsVisible
						&& (index < iTopIndex || index > iBottomIndex)) {
					allSelectedRowsVisible = false;
				}
				//System.out.println("Selected: " + selectedRowIndices[i] + ";" + selectedRows[i]);
			}

			try {
				sortedRows_mon.enter();

				if (bForceDataRefresh && sortColumn != null) {
					String sColumnID = sortColumn.getName();
					for (Iterator iter = sortedRows.iterator(); iter.hasNext();) {
						TableRowSWT row = (TableRowSWT) iter.next();
						TableCellSWT cell = row.getTableCellSWT(sColumnID);
						if (cell != null) {
							cell.refresh(true,false,false);
						}
					}
				}

				if (!bFillGapsOnly) {
					if (sortColumn != null
							&& sortColumn.getLastSortValueChange() > lLastSortedOn) {
						lLastSortedOn = SystemTime.getCurrentTime();
						Collections.sort(sortedRows, sortColumn);
						if (DEBUG_SORTER) {
							long lTimeDiff = (System.currentTimeMillis() - lTimeStart);
							if (lTimeDiff > 150)
								System.out.println("--- Build & Sort took " + lTimeDiff + "ms");
						}
					} else {
						if (DEBUG_SORTER) {
							System.out.println("Skipping sort :)");
						}
					}
				}

				if (bTableVirtual && allSelectedRowsVisible) {
					int count = sortedRows.size();
					if (iBottomIndex >= count) {
						iBottomIndex = count - 1;
					}
					for (int i = iTopIndex; i <= iBottomIndex; i++) {
						TableRowSWT row = (TableRowSWT) sortedRows.get(i);
						if (row.setTableItem(i)) {
							iNumMoves++;
						}
					}

					// visibleRowsChanged() will setTableItem for the rest
				} else {
					for (int i = 0; i < sortedRows.size(); i++) {
						TableRowSWT row = (TableRowSWT) sortedRows.get(i);
						if (row.setTableItem(i)) {
							iNumMoves++;
						}
					}
				}
			} finally {
				sortedRows_mon.exit();
			}

			// move cursor to selected row
			/** SWT/Windows Bug:
			 * When we set selection, the first index is the focus row.
			 * This works visually, however, if you press shift-up or shift-down,
			 * it uses an older selection index.
			 * 
			 * ie. User selects row #10
			 *     Programmically change selection to Row #15 only
			 *     Shift-down
			 *     Rows 10 through 26 will be selected
			 *     
			 * This is Eclipse bug #77106, and is marked WONTFIX 
			 */
			if (focusedRow != null) {
				int pos = 1;
				int numSame = 0;
				int[] newSelectedRowIndices = new int[selectedRows.length];
				Arrays.sort(selectedRowIndices);
				for (int i = 0; i < selectedRows.length; i++) {
					if (selectedRows[i] == null) {
						continue;
					}
					int index = selectedRows[i].getIndex();
					int iNewPos = (selectedRows[i] == focusedRow) ? 0 : pos++;
					newSelectedRowIndices[iNewPos] = index;
					if (Arrays.binarySearch(selectedRowIndices, index) >= 0) {
						numSame++;
					}
				}

				if (numSame < selectedRows.length) {
					// XXX setSelection calls showSelection().  We don't want the table
					//     to jump all over.  Quick fix is to reset topIndex, but
					//     there might be a better way
					iTopIndex = 0;
					if (!bFollowSelected) {
						table.setRedraw(false);
						iTopIndex = table.getTopIndex();
					}
					table.setSelection(newSelectedRowIndices);
					if (!bFollowSelected) {
						table.setTopIndex(iTopIndex);
						table.setRedraw(true);
					}
				}
			}

			if (DEBUG_SORTER) {
				long lTimeDiff = (System.currentTimeMillis() - lTimeStart);
				if (lTimeDiff >= 500)
					System.out.println("<<< Sort & Assign took " + lTimeDiff + "ms with "
							+ iNumMoves + " rows (of " + sortedRows.size() + ") moved. "
							+ focusedRow + ";" + Debug.getCompressedStackTrace());
			}
		} finally {
			sortColumn_mon.exit();
		}
	}

	public void sortColumnReverse(TableColumnCore sorter) {
		boolean bSameColumn = sortColumn.equals(sorter);
		if (!bSameColumn) {
			sortColumn = sorter;
			int iSortDirection = configMan.getIntParameter(CFG_SORTDIRECTION);
			if (iSortDirection == 0)
				sortColumn.setSortAscending(true);
			else if (iSortDirection == 1)
				sortColumn.setSortAscending(false);
			else
				sortColumn.setSortAscending(!sortColumn.isSortAscending());

			configMan.setParameter(sTableID + ".sortAsc",
					sortColumn.isSortAscending());
			configMan.setParameter(sTableID + ".sortColumn", sortColumn.getName());
		} else {
			sortColumn.setSortAscending(!sortColumn.isSortAscending());
			configMan.setParameter(sTableID + ".sortAsc",
					sortColumn.isSortAscending());
		}

		changeColumnIndicator();
		sortColumn(!bSameColumn);
	}

	private void changeColumnIndicator() {
		if (table == null || table.isDisposed())
			return;

		try {
			// can't use TableColumnCore.getPosition, because user may have moved
			// columns around, messing up the SWT column indexes.  
			// We can either use search columnsOrdered, or search table.getColumns()
			TableColumn[] tcs = table.getColumns();
			for (int i = 0; i < tcs.length; i++) {
				String sName = (String) tcs[i].getData("Name");
				if (sName != null && sName.equals(sortColumn.getName())) {
					table.setSortDirection(sortColumn.isSortAscending() ? SWT.UP
							: SWT.DOWN);
					table.setSortColumn(tcs[i]);
					return;
				}
			}

			table.setSortColumn(null);
		} catch (NoSuchMethodError e) {
			// sWT < 3.2 doesn't have column indicaters
		}
	}

	private void visibleRowsChanged() {
		if (Utils.SWT32_TABLEPAINT) {
			return;
		}

		if (!table.isVisible()) {
			lastTopIndex = 0;
			lastBottomIndex = -1;
			return;
		}
		//debug("VRC " + Debug.getCompressedStackTrace());

		boolean bTableUpdate = false;
		int iTopIndex = table.getTopIndex();
		int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);

		if (lastTopIndex != iTopIndex) {
			int tmpIndex = lastTopIndex;
			lastTopIndex = iTopIndex;

			if (iTopIndex < tmpIndex) {
				if (tmpIndex > iBottomIndex + 1 && iBottomIndex >= 0)
					tmpIndex = iBottomIndex + 1;

				//debug("Refresh top rows " + iTopIndex + " to " + (tmpIndex - 1));
				try {
					sortedRows_mon.enter();
					for (int i = iTopIndex; i < tmpIndex && i < sortedRows.size(); i++) {
						TableRowSWT row = (TableRowSWT) getRow(i);
						if (row != null) {
							row.refresh(true, true);
							row.setAlternatingBGColor(true);
							if (Constants.isOSX) {
								bTableUpdate = true;
							}
						}
					}
				} finally {
					sortedRows_mon.exit();
				}

				// A refresh might have triggered a row height resize, so
				// bottom index needs updating
				iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);
			} else {
				//System.out.println("Made T.Invisible " + (tmpIndex) + " to " + (iTopIndex - 1));
			}
		}

		if (lastBottomIndex != iBottomIndex) {
			int tmpIndex = lastBottomIndex;
			lastBottomIndex = iBottomIndex;

			if (tmpIndex < iTopIndex - 1)
				tmpIndex = iTopIndex - 1;

			if (tmpIndex <= iBottomIndex) {
				//debug("Refresh bottom rows " + (tmpIndex + 1) + " to " + iBottomIndex);
				try {
					sortedRows_mon.enter();
					for (int i = tmpIndex + 1; i <= iBottomIndex && i < sortedRows.size(); i++) {
						TableRowSWT row = (TableRowSWT) getRow(i);
						if (row != null) {
							row.refresh(true, true);
							row.setAlternatingBGColor(true);
							if (Constants.isOSX) {
								bTableUpdate = true;
							}
						}
					}
				} finally {
					sortedRows_mon.exit();
				}
			} else {
				//System.out.println("Made B.Invisible " + (tmpIndex) + " to " + (iBottomIndex + 1));
			}
		}

		if (bTableUpdate) {
			table.update();
		}
	}

	public Image obfusticatedImage(final Image image, Point shellOffset) {
		if (table.getItemCount() == 0) {
			return image;
		}
		Rectangle tableArea = table.getClientArea();

		TableColumn[] tableColumnsSWT = table.getColumns();
		for (int i = 0; i < tableColumnsSWT.length; i++) {
			final TableColumnCore tc = (TableColumnCore) tableColumnsSWT[i].getData("TableColumnCore");

			if (tc != null && tc.isObfusticated()) {
				int iTopIndex = table.getTopIndex();
				int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);

				int size = iBottomIndex - iTopIndex + 1;
				if (size <= 0)
					continue;

				for (int j = iTopIndex; j <= iBottomIndex; j++) {
					TableItem rowSWT = table.getItem(j);
					TableRowSWT row = (TableRowSWT) table.getItem(j).getData("TableRow");
					if (row != null) {
						TableCellSWT cell = row.getTableCellSWT(tc.getName());
						final Rectangle columnBounds = rowSWT.getBounds(i);
						if (columnBounds.y + columnBounds.height > tableArea.y
								+ tableArea.height) {
							columnBounds.height -= (columnBounds.y + columnBounds.height)
									- (tableArea.y + tableArea.height);
						}
						if (columnBounds.x + columnBounds.width > tableArea.x
								+ tableArea.width) {
							columnBounds.width -= (columnBounds.x + columnBounds.width)
									- (tableArea.x + tableArea.width);
						}

						final Point offset = table.toDisplay(columnBounds.x, columnBounds.y);

						columnBounds.x = offset.x - shellOffset.x;
						columnBounds.y = offset.y - shellOffset.y;

						String text = cell.getObfusticatedText();

						if (text != null) {
							UIDebugGenerator.obfusticateArea(table.getDisplay(), image,
									columnBounds, text);
						}
					}
				}

				//UIDebugGenerator.offusticateArea(image, columnBounds);
			}
		}

		IView view = getActiveSubView();
		if (view instanceof ObfusticateImage) {
			try {
				((ObfusticateImage) view).obfusticatedImage(image, shellOffset);
			} catch (Exception e) {
				Debug.out("Obfusticating " + view, e);
			}
		}
		return image;
	}

	void debug(String s) {
		AEDiagnosticsLogger diag_logger = AEDiagnostics.getLogger("table");
		diag_logger.log(s);

		System.out.println(SystemTime.getCurrentTime() + ": " + sTableID + ": " + s);
	}

	// from common.TableView
	public boolean isEnableTabViews() {
		return bEnableTabViews;
	}

	// from common.TableView
	public void setEnableTabViews(boolean enableTabViews) {
		bEnableTabViews = enableTabViews;
	}

	// from common.TableView
	public IView[] getCoreTabViews() {
		return coreTabViews;
	}

	// @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#setCoreTabViews(org.gudy.azureus2.ui.swt.views.IView[])
	public void setCoreTabViews(IView[] coreTabViews) {
		this.coreTabViews = coreTabViews;
	}

	public void addMenuFillListener(TableViewSWTMenuFillListener l) {
		listenersMenuFill.add(l);
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#isDisposed()
	public boolean isDisposed() {
		return mainComposite == null || mainComposite.isDisposed() || table == null
				|| table.isDisposed();
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#size(boolean)
	public int size(boolean bIncludeQueue) {
		int size = sortedRows.size();

		if (bIncludeQueue) {
			if (dataSourcesToAdd != null) {
				size += dataSourcesToAdd.size();
			}
			if (dataSourcesToRemove != null) {
				size += dataSourcesToRemove.size();
			}
		}
		return size;
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getPropertiesPrefix()
	public String getPropertiesPrefix() {
		return sPropertiesPrefix;
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#setFocus()
	public void setFocus() {
		if (table != null && !table.isDisposed()) {
			table.setFocus();
		}
	}

	// @see org.gudy.azureus2.ui.swt.views.TableViewSWT#addKeyListener(org.eclipse.swt.events.KeyListener)
	public void addKeyListener(KeyListener listener) {
		if (listenersKey.contains(listener)) {
			return;
		}

		listenersKey.add(listener);
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#removeKeyListener(org.eclipse.swt.events.KeyListener)
	public void removeKeyListener(KeyListener listener) {
		listenersKey.remove(listener);
	}

	// @see org.gudy.azureus2.ui.swt.views.TableViewSWT#getSortColumn()
	public TableColumnCore getSortColumn() {
		return sortColumn;
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#selectAll()
	public void selectAll() {
		if (table != null && !table.isDisposed()) {
			ensureAllRowsHaveIndex();
			table.selectAll();
		}
	}

	/**
	 * 
	 *
	 * @since 3.0.0.7
	 */
	private void ensureAllRowsHaveIndex() {
		for (int i = 0; i < sortedRows.size(); i++) {
			TableRowSWT row = (TableRowSWT) sortedRows.get(i);
			row.setTableItem(i);
		}
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#setSelectedRows(com.aelitis.azureus.ui.common.table.TableRowCore[])
	public void setSelectedRows(TableRowCore[] rows) {
		table.deselectAll();
		for (int i = 0; i < rows.length; i++) {
			TableRowCore row = rows[i];
			if (row.getIndex() == -1) {
				int j = sortedRows.indexOf(row);
				if (j == -1) {
					System.err.println("BOO");
				} else {
					row.setTableItem(j);
				}
			}
			row.setSelected(true);
		}
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#isTableFocus()
	public boolean isTableFocus() {
		return table.isFocusControl();
	}

	// @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#createDragSource(int)
	public DragSource createDragSource(int style) {
		final DragSource dragSource = new DragSource(table, style);
		table.addDisposeListener(new DisposeListener() {
			// @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
			public void widgetDisposed(DisposeEvent e) {
				if (dragSource != null && !dragSource.isDisposed()) {
					dragSource.dispose();
				}
			}
		});
		return dragSource;
	}

	// @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#createDropTarget(int)
	public DropTarget createDropTarget(int style) {
		final DropTarget dropTarget = new DropTarget(table, style);
		table.addDisposeListener(new DisposeListener() {
			// @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
			public void widgetDisposed(DisposeEvent e) {
				if (dropTarget != null && !dropTarget.isDisposed()) {
					dropTarget.dispose();
				}
			}
		});
		return dropTarget;
	}

	// @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#indexOf(org.eclipse.swt.widgets.Widget)
	public TableRowCore getRow(DropTargetEvent event) {
		if (event.item instanceof TableItem) {
			TableItem ti = (TableItem) event.item;
			return (TableRowCore) ti.getData("TableRow");
		}
		return null;
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#dataSourceExists(java.lang.Object)
	public boolean dataSourceExists(Object dataSource) {
		return mapDataSourceToRow.containsKey(dataSource)
				|| dataSourcesToAdd.contains(dataSource);
	}

	// @see com.aelitis.azureus.ui.common.table.TableView#getVisibleColumns()
	public TableColumnCore[] getVisibleColumns() {
		return tableColumns;
	}

	/**
	 * @return
	 */
	protected TableViewSWTPanelCreator getMainPanelCreator() {
		return mainPanelCreator;
	}

	// @see org.gudy.azureus2.ui.swt.views.TableViewSWT#setMainPanelCreator(org.gudy.azureus2.ui.swt.views.TableViewMainPanelCreator)
	public void setMainPanelCreator(TableViewSWTPanelCreator mainPanelCreator) {
		this.mainPanelCreator = mainPanelCreator;
	}

	public TableCellSWT getTableCellWithCursor() {
		Point pt = table.getDisplay().getCursorLocation();
		pt = table.toControl(pt);
		return getTableCell(pt.x, pt.y);
	}

	// @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#getTableRowWithCursor()
	public TableRowCore getTableRowWithCursor() {
		Point pt = table.getDisplay().getCursorLocation();
		pt = table.toControl(pt);
		return getTableRow(pt.x, pt.y);
	}
}
