Package org.gudy.azureus2.ui.swt.views.table.impl

Source Code of org.gudy.azureus2.ui.swt.views.table.impl.TableViewSWTImpl

/*
* 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.lang.reflect.Method;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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.COConfigurationManager;
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.internat.MessageText.MessageTextListener;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.plugins.ui.tables.*;
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.UISWTViewEventListenerHolder;
import org.gudy.azureus2.ui.swt.pluginsimpl.UISWTViewImpl;
import org.gudy.azureus2.ui.swt.views.IView;
import org.gudy.azureus2.ui.swt.views.IViewExtension;
import org.gudy.azureus2.ui.swt.views.columnsetup.TableColumnSetupWindow;
import org.gudy.azureus2.ui.swt.views.table.*;
import org.gudy.azureus2.ui.swt.views.table.utils.TableColumnSWTUtils;
import org.gudy.azureus2.ui.swt.views.table.utils.TableContextMenuManager;

import com.aelitis.azureus.ui.common.table.*;
import com.aelitis.azureus.ui.common.table.impl.TableColumnManager;
import com.aelitis.azureus.ui.common.table.impl.TableViewImpl;
import com.aelitis.azureus.ui.swt.UIFunctionsManagerSWT;
import com.aelitis.azureus.ui.swt.UIFunctionsSWT;
import com.aelitis.azureus.ui.swt.imageloader.ImageLoader;
import com.aelitis.azureus.ui.swt.utils.ColorCache;

/**
* 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.
*         
* @note 4005: We set a text cell's measured width to the columns prefered width
*             instead of setting it to the actual space needed for the text.
*             We should really store the last measured width in TableCell and
*             use that.
*/
public class TableViewSWTImpl<DATASOURCETYPE>
  extends TableViewImpl<DATASOURCETYPE>
  implements ParameterListener, TableViewSWT<DATASOURCETYPE>,
  TableStructureModificationListener<DATASOURCETYPE>, ObfusticateImage,
  KeyListener, MessageTextListener
{
  protected final static boolean DRAW_VERTICAL_LINES = Constants.isWindows;

  protected static final boolean DRAW_FULL_ROW = Constants.isWindows;

  private final static LogIDs LOGID = LogIDs.GUI;

  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 int ASYOUTYPE_MODE_FIND = 0;
  private static final int ASYOUTYPE_MODE_FILTER = 1;
  private static final int ASYOUTYPE_MODE = ASYOUTYPE_MODE_FILTER;
  private static final int ASYOUTYPE_UPDATEDELAY = 300;

  private static final Color COLOR_FILTER_REGEX  = Colors.fadedYellow;
 
  protected static final boolean DEBUG_CELL_CHANGES = false;

  private static final boolean DEBUG_SELECTION = false;

  private static final boolean DEBUG_ROWCHANGE = false;

  private static final boolean OBEY_COLUMN_MINWIDTH = false;

  /** 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
   */
  protected boolean bSkipFirstColumn = true;

  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 TableOrTreeSWT table;

  private ControlEditor editor;

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

  /** Context Menu */
  private Menu menu;

  /** Link DataSource to their row in the table.
   * key = DataSource
   * value = TableRowSWT
   */
  private Map<DATASOURCETYPE, TableRowCore> mapDataSourceToRow;

  private AEMonitor listUnfilteredDatasources_mon = new AEMonitor("TableView:uds");

  private Set<DATASOURCETYPE> listUnfilteredDataSources;

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

  private List<TableRowSWT> 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;

  /** 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");

  /**
   * 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 boolean[] columnsVisible;

  private ColumnMoveListener columnMoveListener = new ColumnMoveListener();

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

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

  private boolean bReallyAddingDataSources = false;

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

  /** TabViews */
  private CTabFolder tabFolder;

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

  TableRowSWT[] visibleRows;

  protected IView[] coreTabViews = null;

  private long lCancelSelectionTriggeredOn = -1;

  private long lastSelectionTriggeredOn = -1;

  private List<TableViewSWTMenuFillListener> listenersMenuFill = new ArrayList<TableViewSWTMenuFillListener>(
      1);
 
  private ArrayList<TableRowSWTPaintListener> rowPaintListeners;

  private static AEMonitor mon_RowPaintListener = new AEMonitor( "rpl" );

  private ArrayList<TableRowMouseListener> rowMouseListeners;

  private static AEMonitor mon_RowMouseListener = new AEMonitor( "rml" );

  private TableViewSWTPanelCreator mainPanelCreator;

  private List<KeyListener> listenersKey = new ArrayList<KeyListener>(1);

  private boolean columnPaddingAdjusted = false;

  private boolean columnVisibilitiesChanged = true;

  // What type of data is stored in this table
  private final Class<?> classPluginDataSourceType;

  private AEMonitor listeners_mon = new AEMonitor("tablelisteners");

  private ArrayList<TableRowRefreshListener> refreshListeners;

  /**
   * Up to date table client area.  So far, the best places to refresh
   * this variable are in the PaintItem event and the scrollbar's events.
   * Typically table.getClientArea() is time consuming
   */
  protected Rectangle clientArea;

  private boolean isVisible;

  private boolean menuEnabled = true;

  private boolean headerVisible = true;

  /**
   * Up to date list of selected rows, so we can access rows without being on SWT Thread.
   * Guaranteed to have no nulls
   */
  private List<TableRowCore> selectedRows = new ArrayList<TableRowCore>(1);

  private List<Object> listSelectedCoreDataSources;

  private Utils.addDataSourceCallback processDataSourceQueueCallback = new Utils.addDataSourceCallback() {
    public void process() {
      processDataSourceQueue();
    }

    public void debug(String str) {
      TableViewSWTImpl.this.debug(str);
    }
  };

  // private Rectangle firstClientArea;

  private int lastHorizontalPos;
 

  // class used to keep filter stuff in a nice readable parcel
  class filter {
    Text widget = null;
   
    TimerEvent eventUpdate;
   
    String text = "";
   
    long lastFilterTime;
   
    boolean regex = false;
   
    TableViewFilterCheck<DATASOURCETYPE> checker;
   
    String nextText = "";
   
    ModifyListener widgetModifyListener;
  };
 
  filter filter;

  private boolean useTree;

  protected int headerHeight;

  private Shell shell;


  /**
   * 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(Class<?> pluginDataSourceType, String _sTableID,
      String _sPropertiesPrefix, TableColumnCore[] _basicItems,
      String _sDefaultSortOn, int _iTableStyle) {
    boolean wantTree = (_iTableStyle & SWT.CASCADE) != 0;
    _iTableStyle &= ~SWT.CASCADE;
    if (wantTree) {
      useTree = COConfigurationManager.getBooleanParameter("Table.useTree")
          && !Utils.isCarbon;
    }
    classPluginDataSourceType = pluginDataSourceType;
    sTableID = _sTableID;
    basicItems = _basicItems;
    sPropertiesPrefix = _sPropertiesPrefix;
    sDefaultSortOn = _sDefaultSortOn;
    iTableStyle = _iTableStyle | SWT.V_SCROLL | SWT.DOUBLE_BUFFERED;

    mapDataSourceToRow = new LightHashMap<DATASOURCETYPE, TableRowCore>();
    sortedRows = new ArrayList<TableRowSWT>();
    listUnfilteredDataSources = new HashSet<DATASOURCETYPE>();
  }

  /**
   * 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(Class<?> pluginDataSourceType,
      String _sTableID, String _sPropertiesPrefix,
      TableColumnCore[] _basicItems, String _sDefaultSortOn) {
    this(pluginDataSourceType, _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 (basicItems != null) {
      if (tcManager.getTableColumnCount(sTableID) != basicItems.length) {
        tcManager.addColumns(basicItems);
      }
      basicItems = null;
    }

    tableColumns = tcManager.getAllTableColumnCoreAsArray(classPluginDataSourceType,
        sTableID);

    // fixup order
    tcManager.ensureIntegrety(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) {
      tcManager.addColumns(basicItems);
    }

    tableColumns = tcManager.getAllTableColumnCoreAsArray(classPluginDataSourceType,
        sTableID);

    // fixup order
    tcManager.ensureIntegrety(sTableID);
  }

  // AbstractIView::initialize
  public void initialize(Composite composite) {
    composite.setRedraw(false);
    shell = composite.getShell();
    mainComposite = createSashForm(composite);
    mainComposite.setData( "TableView.TableID", sTableID );
    table = createTable(tableComposite);
    menu = createMenu(table);
    clientArea = table.getClientArea();
    editor = TableOrTreeUtils.createTableOrTreeEditor(table);
    editor.minimumWidth = 80;
    editor.grabHorizontal = true;
    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<String, UISWTViewEventListenerHolder> 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;
        CTabItem[] items = tabFolder.getItems();
        for (int i = 0; i < items.length; i++) {
          CTabItem tabItem = items[i];
          tabItem.getControl().setVisible(false);
        }
        form.layout();

        IView view = getActiveSubView();
        if (view instanceof IViewExtension) {
          ((IViewExtension) view).viewDeactivated();
        }

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

      public void restore(CTabFolderEvent event) {
        tabFolder.setMinimized(false);
        CTabItem selection = tabFolder.getSelection();
        if (selection != null) {
          selection.getControl().setVisible(true);
        }
        form.notifyListeners(SWT.Resize, null);

        IView view = getActiveSubView();
        if (view instanceof IViewExtension) {
          ((IViewExtension) view).viewActivated();
        }
        refreshSelectedSubView();

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

    };
    tabFolder.addCTabFolder2Listener(folderListener);

    tabFolder.addSelectionListener(new SelectionListener() {
      public void widgetSelected(SelectionEvent e) {
        // make sure its above
        try {
          ((CTabItem) e.item).getControl().setVisible(true);
          ((CTabItem) e.item).getControl().moveAbove(null);

          // TODO: Need to viewDeactivated old one
          IView view = getActiveSubView();
          if (view instanceof IViewExtension) {
            ((IViewExtension)view).viewActivated();
          }
         
        } catch (Exception t) {
        }
      }

      public void widgetDefaultSelected(SelectionEvent e) {
      }
    });

    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 = pluginViews.keySet().toArray(new String[0]);
      for (int i = 0; i < sNames.length; i++) {
        UISWTViewEventListener l = 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.NO_FOCUS);
    composite.getLayout();
    GridLayout layout = new GridLayout();
    layout.marginHeight = 0;
    layout.marginWidth = 0;
    panel.setLayout(layout);

    Object parentLayout = composite.getLayout();
    if (parentLayout == null || (parentLayout instanceof GridLayout)) {
      panel.setLayoutData(new GridData(GridData.FILL_BOTH));
    }

    return panel;
  }

  /** Creates the Table.
   *
   * @return The created Table.
   */
  public TableOrTreeSWT createTable(Composite panel) {
    table = TableOrTreeUtils.createGrid(panel, iTableStyle, useTree);
    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 TableOrTreeSWT table) {
    initializeColumnDefs();

    iTableStyle = table.getStyle();
    if ((iTableStyle & SWT.VIRTUAL) == 0) {
      throw new Error("Virtual Table Required");
    }

    table.setLinesVisible(Utils.TABLE_GRIDLINE_IS_ALTERNATING_COLOR);
    table.setMenu(menu);
    table.setData("Name", sTableID);
    table.setData("TableView", this);
   
    // On Windows, TreeItems POSTPAINT event spendsabout 7% of it's time in getFont()
    // calling the OS API.  If we set the font, it skips the API call.
    // This could be optimized further by setting the font on each table item,
    // however, it's unknown what performace hit we'd get on row creation.
    table.setFont(table.getFont());

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

    table.addPaintListener(new PaintListener() {
      public void paintControl(PaintEvent event) {
        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);
        table.removePaintListener(this);
      }
    });

    table.addListener(SWT.PaintItem, new TableViewSWT_PaintItem(this, table));

    if (Constants.isWindows) {
      TableViewSWT_EraseItem eraseItemListener = new TableViewSWT_EraseItem(this, table);
      table.addListener(SWT.EraseItem, eraseItemListener);
      table.addListener(SWT.Paint, eraseItemListener);
    }

    ScrollBar horizontalBar = table.getHorizontalBar();
    if (horizontalBar != null) {
      horizontalBar.addSelectionListener(new SelectionListener() {
        public void widgetDefaultSelected(SelectionEvent e) {
          Utils.execSWTThreadLater(0, new AERunnable() {
            public void runSupport() {
              swt_calculateClientArea();
            }
          });
          //updateColumnVisibilities();
        }

        public void widgetSelected(SelectionEvent e) {
          Utils.execSWTThreadLater(0, new AERunnable() {
            public void runSupport() {
              swt_calculateClientArea();
            }
          });
          //updateColumnVisibilities();
        }
      });
    }

    table.addListener(SWT.MeasureItem, new Listener() {
      public void handleEvent(Event event) {
        int iColumnNo = event.index;

        if (bSkipFirstColumn) {
          iColumnNo--;
        }

        if (iColumnNo >= 0 && iColumnNo < columnsOrdered.length) {
          TableColumnCore tc = columnsOrdered[iColumnNo];
          int preferredWidth = tc.getPreferredWidth();
          event.width = preferredWidth;
        }

        int defaultHeight = getRowDefaultHeight();
        if (event.height < defaultHeight) {
          event.height = defaultHeight;
        }
      }
    });

    // Deselect rows if user clicks on a blank spot (a spot with no row)
    table.addMouseListener(new MouseAdapter() {
      long lastMouseDblClkEventTime = 0;
      public void mouseDoubleClick(MouseEvent e) {
        long time = e.time & 0xFFFFFFFFL;
        long diff = time - lastMouseDblClkEventTime;
        // We fake a double click on MouseUp.. this traps 2 double clicks
        // in quick succession and ignores the 2nd.
        if (diff <= e.display.getDoubleClickTime() && diff >= 0) {
          return;
        }
        lastMouseDblClkEventTime = time;

        TableColumnCore tc = getTableColumnByOffset(e.x);
        TableCellCore cell = getTableCell(e.x, e.y);
        if (cell != null && tc != null) {
          TableCellMouseEvent event = createMouseEvent(cell, e,
              TableCellMouseEvent.EVENT_MOUSEDOUBLECLICK, false);
          if (event != null) {
            tc.invokeCellMouseListeners(event);
            cell.invokeMouseListeners(event);
            if (event.skipCoreFunctionality) {
              lCancelSelectionTriggeredOn = System.currentTimeMillis();
            }
          }
        }
      }

      long lastMouseUpEventTime = 0;
      Point lastMouseUpPos = new Point(0, 0);
      boolean mouseDown = false;
      public void mouseUp(MouseEvent e) {
        // SWT OSX Bug: two mouseup events when app not in focus and user
        // clicks on the table.  Only one mousedown, so track that and ignore
        if (!mouseDown) {
          return;
        }
        mouseDown = false;
        if (e.button == 1) {
          long time = e.time & 0xFFFFFFFFL;
          long diff = time - lastMouseUpEventTime;
          if (diff <= e.display.getDoubleClickTime() && diff >= 0
              && lastMouseUpPos.x == e.x && lastMouseUpPos.y == e.y) {
            // Fake double click because Cocoa SWT 3650 doesn't always trigger
            // DefaultSelection listener on a Tree on dblclick (works find in Table)
            runDefaultAction(e.stateMask);
            return;
          }
          lastMouseUpEventTime = time;
          lastMouseUpPos = new Point(e.x, e.y);
        }

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

      TableRowCore lastClickRow;

      public void mouseDown(MouseEvent e) {
        mouseDown = true;
        // we need to fill the selected row indexes here because the
        // dragstart event can occur before the SWT.SELECTION event and
        // our drag code needs to know the selected rows..
        TableRowSWT row = getTableRow(e.x, e.y, false);
        if (row == null) {
          setSelectedRows(new TableRowCore[0]);
        } else if (!row.isRowDisposed()) {
          selectRow(row, true);
        }

        TableColumnCore tc = getTableColumnByOffset(e.x);
        TableCellCore cell = getTableCell(e.x, e.y);

        editCell(-1, -1); // clear out current cell editor

        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, false);
          if (event != null) {
            tc.invokeCellMouseListeners(event);
            cell.invokeMouseListeners(event);
            invokeRowMouseListener(event);
            if (event.skipCoreFunctionality) {
              lCancelSelectionTriggeredOn = System.currentTimeMillis();
            }
          }
          if (tc.isInplaceEdit() && e.button == 1
              && lastClickRow == cell.getTableRowCore()) {
            editCell(getColumnNo(e.x), cell.getTableRowCore().getIndex());
          }
          if (e.button == 1) {
            lastClickRow = cell.getTableRowCore();
          }
        } else if (row != null) {
          TableRowMouseEvent event = createMouseEvent(row, e,
              TableCellMouseEvent.EVENT_MOUSEDOWN, false);
          if (event != null) {
            invokeRowMouseListener(event);
          }
        }
      }
    });

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

      int lastCursorID = 0;

      public void mouseMove(MouseEvent e) {
        lCancelSelectionTriggeredOn = -1;
        if (isDragging) {
          return;
        }
        try {
          TableCellCore cell = getTableCell(e.x, e.y);
         
          if (lastCell != null && cell != lastCell && !lastCell.isDisposed()) {
            TableCellMouseEvent event = createMouseEvent(lastCell, e,
                TableCellMouseEvent.EVENT_MOUSEEXIT, true);
            if (event != null) {
              TableColumnCore tc = ((TableColumnCore) lastCell.getTableColumn());
              if (tc != null) {
                tc.invokeCellMouseListeners(event);
              }
              lastCell.invokeMouseListeners(event);
            }
          }

          int iCursorID = 0;
          if (cell == null) {
            lastCell = null;
          } else if (cell == lastCell) {
            iCursorID = lastCursorID;
          } else {
            iCursorID = cell.getCursorID();
            lastCell = cell;
          }

          if (iCursorID < 0) {
            iCursorID = 0;
          }

          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, false);
            if (event != null) {
              TableColumnCore tc = ((TableColumnCore) cell.getTableColumn());
              if (tc.hasCellMouseMoveListener()) {
                ((TableColumnCore) cell.getTableColumn()).invokeCellMouseListeners(event);
              }
              cell.invokeMouseListeners(event);

              // listener might have changed it

              int cellCursorID = cell.getCursorID();
              if (cellCursorID != -1) {
                lastCursorID = cellCursorID;
              }
            }
          }
        } catch (Exception ex) {
          Debug.out(ex);
        }
      }
    });

    table.addSelectionListener(new SelectionListener() {
      public void widgetSelected(SelectionEvent event) {
        updateSelectedRows(table.getSelection(), true);
      }

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

    // we are sent a SWT.Settings event when the language changes and
    // when System fonts/colors change.  In both cases, invalidate
    table.addListener(SWT.Settings, new Listener() {
      public void handleEvent(Event e) {
        tableInvalidate();
      }
    });
   
    if (useTree) {
      Listener listenerExpandCollapse = new Listener() {
        public void handleEvent(Event event) {
          TableItemOrTreeItem item = TableOrTreeUtils.getEventItem(event.item);
          if (item == null) {
            return;
          }
          TableRowCore row = getRow(item);
          if (row == null || row.isRowDisposed()) {
            return;
          }
          row.setExpanded(event.type == SWT.Expand ? true : false);
          Utils.execSWTThreadLater(0, new AERunnable() {
            public void runSupport() {
              visibleRowsChanged();
            }
          });
        }
      };
      table.addListener(SWT.Expand, listenerExpandCollapse);
      table.addListener(SWT.Collapse, listenerExpandCollapse);
    }

    new TableTooltips(this, table.getComposite());

    table.addKeyListener(this);
   
    table.addDisposeListener(new DisposeListener(){
      public void widgetDisposed(DisposeEvent e) {
        if (filter != null && filter.widget != null && !filter.widget.isDisposed()) {
          filter.widget.removeKeyListener(TableViewSWTImpl.this);
          filter.widget.removeModifyListener(filter.widgetModifyListener);
        }
        Utils.disposeSWTObjects(new Object[] { sliderArea } );
      }
    });
/*
    if (Utils.isCocoa) {
      table.addListener(SWT.MouseVerticalWheel, new Listener() {
        public void handleEvent(Event event) {
          calculateClientArea();
          visibleRowsChanged();
        }
      });
    }
*/   
    ScrollBar bar = table.getVerticalBar();
    if (bar != null) {
      bar.addSelectionListener(new SelectionAdapter() {
        public void widgetSelected(SelectionEvent e) {
          Utils.execSWTThreadLater(0, new AERunnable() {
            public void runSupport() {
              // need to calc later as getClientArea isn't up to date yet
              // on Win
              swt_calculateClientArea();
              visibleRowsChanged();
            }
          });
          // Bug: Scroll is slow when table is not focus
          if (!table.isFocusControl()) {
            table.setFocus();
          }
        }
      });
    }

    table.setHeaderVisible(getHeaderVisible());
    headerHeight = table.getHeaderHeight();

    clientArea = table.getClientArea();
    //firstClientArea = table.getClientArea();
    table.addListener(SWT.Resize, new Listener() {
      public void handleEvent(Event event) {
        swt_calculateClientArea();
      }
    });

    initializeTableColumns(table);

    MessageText.addListener(this);
  }
 
  public void localeChanged(Locale old_locale, Locale new_locale) {
    Utils.execSWTThreadLater(0, new AERunnable() {
      public void runSupport() {
        if (tabViews != null && tabViews.size() > 0) {
          for (int i = 0; i < tabViews.size(); i++) {
            IView view = tabViews.get(i);
            if (view != null) {
              view.updateLanguage();
            }
          }
        }
        tableInvalidate();
        refreshTable(true);

        TableColumnOrTreeColumn[] tableColumnsSWT = table.getColumns();
        for (int i = 0; i < tableColumnsSWT.length; i++) {
          TableColumnCore column = (TableColumnCore) tableColumnsSWT[i].getData("TableColumnCore");
          if (column != null) {
            Messages.setLanguageText(tableColumnsSWT[i].getColumn(),
                column.getTitleLanguageKey());
          }
        }

      }
    });
  }


  public void keyPressed(KeyEvent event) {
    // Note: Both table key presses and txtFilter keypresses go through this
    //       method.

    Object[] listeners = listenersKey.toArray();
    for (int i = 0; i < listeners.length; i++) {
      KeyListener l = (KeyListener) listeners[i];
      l.keyPressed(event);
      if (!event.doit) {
        lCancelSelectionTriggeredOn = SystemTime.getCurrentTime();
        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 if ((event.stateMask & SWT.CONTROL) > 0) {
        runForAllRows(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 (filter == null || event.widget != filter.widget) {
            if ((table.getStyle() & SWT.MULTI) > 0) {
              selectAll();
              event.doit = false;
            }
          } else {
            filter.widget.selectAll();
            event.doit = false;
          }
          break;

        case '+': {
          if (Constants.isUnix) {
            TableColumnOrTreeColumn[] 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;
        }
        case 'f': // CTRL+F Find/Filter
          openFilterDialog();
          event.doit = false;
          break;
        case 'x': // CTRL+X: RegEx search switch
          if (filter != null && event.widget == filter.widget) {
            filter.regex = !filter.regex;
            filter.widget.setBackground(filter.regex?COLOR_FILTER_REGEX:null);
            refilter();
            return;
          }
          break;
      }

    }

    if (event.stateMask == 0) {
      if (filter != null && filter.widget == event.widget) {
        if (event.keyCode == SWT.ARROW_DOWN) {
          setFocus();
          event.doit = false;
        } else if (event.character == 13) {
          refilter();
        }
      }
    }

    if (!event.doit) {
      return;
    }

    handleSearchKeyPress(event);
  }

  public void keyReleased(KeyEvent event) {
    swt_calculateClientArea();
    visibleRowsChanged();

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

  public void setHeaderVisible(boolean visible) {
    headerVisible = visible;

    Utils.execSWTThread(new AERunnable() {
      public void runSupport() {
        if (table != null && !table.isDisposed()) {
          table.setHeaderVisible(headerVisible);
          headerHeight = table.getHeaderHeight();
        }
      }
    });
  }

  protected void swt_calculateClientArea() {
    Rectangle oldClientArea = clientArea;
    clientArea = table.getClientArea();
    ScrollBar horizontalBar = table.getHorizontalBar();
    boolean clientAreaCausedVisibilityChanged = false;
    if (horizontalBar != null) {
      int pos = horizontalBar.getSelection();
      if (pos != lastHorizontalPos) {
        lastHorizontalPos = pos;
        clientAreaCausedVisibilityChanged = true;
      }
    }
    if (oldClientArea != null
        && (oldClientArea.x != clientArea.x || oldClientArea.width != clientArea.width)) {
      clientAreaCausedVisibilityChanged = true;
    }
    if (oldClientArea != null
        && (oldClientArea.y != clientArea.y || oldClientArea.height != clientArea.height)) {
      visibleRowsChanged();
    }
    if (oldClientArea != null
        && (oldClientArea.height < table.getHeaderHeight())) {
      clientAreaCausedVisibilityChanged = true;
    }
    if (clientAreaCausedVisibilityChanged) {
      columnVisibilitiesChanged = true;
      Utils.execSWTThreadLater(50, new AERunnable() {
        public void runSupport() {
          if (columnVisibilitiesChanged) {
            refreshTable(false);
          }
        }
      });
    }
  }

  /**
   *
   *
   * @since 3.1.1.1
   */
  protected void triggerTabViewsDataSourceChanged() {
    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 = 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);
        }
      }
    }
  }

  private interface SourceReplaceListener
  {
    void sourcesChanged();

    void cleanup(Text toClean);
  }

  private SourceReplaceListener cellEditNotifier;

  private Control sliderArea;

  private boolean isDragging;

  private void editCell(final int column, final int row) {
    Utils.execSWTThread(new AERunnable() {
      public void runSupport() {
        swt_editCell(column, row);
      }
    });
  }

  private void swt_editCell(final int column, final int row) {
    Text oldInput = (Text) editor.getEditor();
    if (column >= table.getColumnCount() || row < 0
        || row >= table.getItemCount()) {
      cellEditNotifier = null;
      if (oldInput != null && !oldInput.isDisposed()) {
        editor.getEditor().dispose();
      }
      return;
    }

    TableColumnOrTreeColumn tcColumn = table.getColumn(column);
    final TableItemOrTreeItem item = table.getItem(row);

    String cellName = (String) tcColumn.getData("Name");
    final TableRowSWT rowSWT = (TableRowSWT) getRow(row);
    final TableCellSWT cell = rowSWT.getTableCellSWT(cellName);

    // reuse widget if possible, this way we'll keep the focus all the time on jumping through the rows
    final Text newInput = oldInput == null || oldInput.isDisposed() ? new Text(
        table.getComposite(), Constants.isOSX ? SWT.NONE : SWT.BORDER) : oldInput;
    final DATASOURCETYPE datasource = (DATASOURCETYPE) cell.getDataSource();
    if (cellEditNotifier != null) {
      cellEditNotifier.cleanup(newInput);
    }

    table.showItem(item);
    table.showColumn(tcColumn);

    newInput.setText(cell.getText());

    newInput.setSelection(0);
    newInput.selectAll();
    newInput.setFocus();

    class QuickEditListener
      implements ModifyListener, SelectionListener, KeyListener,
      TraverseListener, SourceReplaceListener, ControlListener
    {
      boolean resizing = true;

      public QuickEditListener(Text toAttach) {
        toAttach.addModifyListener(this);
        toAttach.addSelectionListener(this);
        toAttach.addKeyListener(this);
        toAttach.addTraverseListener(this);
        toAttach.addControlListener(this);

        cellEditNotifier = this;
      }

      public void modifyText(ModifyEvent e) {
        if (item.isDisposed()) {
          sourcesChanged();
          return;
        }
        if (((TableColumnCore) cell.getTableColumn()).inplaceValueSet(cell,
            newInput.getText(), false)) {
          newInput.setBackground(null);
        } else {
          newInput.setBackground(Colors.colorErrorBG);
        }
      }

      public void widgetDefaultSelected(SelectionEvent e) {
        if (item.isDisposed()) {
          sourcesChanged();
          newInput.traverse(SWT.TRAVERSE_RETURN);
          return;
        }
        ((TableColumnCore) cell.getTableColumn()).inplaceValueSet(cell,
            newInput.getText(), true);
        rowSWT.invalidate();
        editCell(column, row + 1);
      }

      public void widgetSelected(SelectionEvent e) {
      }

      public void keyPressed(KeyEvent e) {
        if (e.keyCode == SWT.ARROW_DOWN || e.keyCode == SWT.ARROW_UP) {
          e.doit = false;
          editCell(column, row + (e.keyCode == SWT.ARROW_DOWN ? 1 : -1));
        }
      }

      public void keyReleased(KeyEvent e) {
      }

      public void keyTraversed(TraverseEvent e) {
        if (e.detail == SWT.TRAVERSE_ESCAPE) {
          e.doit = false;
          editCell(column, -1);
        }
      }

      public void sourcesChanged() {
        if (getRow(datasource) == rowSWT || getRow(datasource) == null
            || newInput.isDisposed()) {
          return;
        }
        String newVal = newInput.getText();
        Point sel = newInput.getSelection();
        editCell(column, getRow(datasource).getIndex());
        if (newInput.isDisposed()) {
          return;
        }
        newInput.setText(newVal);
        newInput.setSelection(sel);
      }

      public void cleanup(Text oldText) {
        if (!oldText.isDisposed()) {
          oldText.removeModifyListener(this);
          oldText.removeSelectionListener(this);
          oldText.removeKeyListener(this);
          oldText.removeTraverseListener(this);
          oldText.removeControlListener(this);
        }
      }

      public void controlMoved(ControlEvent e) {
        table.showItem(item);
        if (resizing) {
          return;
        }
        resizing = true;

        Point sel = newInput.getSelection();

        TableOrTreeUtils.setEditorItem(editor, newInput, column, item);

        editor.minimumWidth = newInput.computeSize(SWT.DEFAULT, SWT.DEFAULT).x;

        Rectangle leftAlignedBounds = item.getBounds(column);
        leftAlignedBounds.width = editor.minimumWidth = newInput.computeSize(
            SWT.DEFAULT, SWT.DEFAULT).x;
        if (leftAlignedBounds.intersection(clientArea).equals(leftAlignedBounds)) {
          editor.horizontalAlignment = SWT.LEFT;
        } else {
          editor.horizontalAlignment = SWT.RIGHT;
        }

        editor.layout();

        newInput.setSelection(0);
        newInput.setSelection(sel);

        resizing = false;
      }

      public void controlResized(ControlEvent e) {
      }
    }

    QuickEditListener l = new QuickEditListener(newInput);

    l.modifyText(null);

    TableOrTreeUtils.setEditorItem(editor, newInput, column, item);
    table.deselectAll();
    table.select(table.getItem(row));
    updateSelectedRows(new TableRowCore[] { sortedRows.get(row) }, true);

    l.resizing = false;

    l.controlMoved(null);
  }

  private TableCellMouseEvent createMouseEvent(TableCellCore cell, MouseEvent e,
      int type, boolean allowOOB) {
    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;
    if (cell instanceof TableCellSWT) {
      Rectangle r = ((TableCellSWT) cell).getBounds();
      if (r == null) {
        return event;
      }
      event.x = e.x - r.x;
      if (!allowOOB && event.x < 0) {
        return null;
      }
      event.y = e.y - r.y;
      if (!allowOOB && event.y < 0) {
        return null;
      }
    }

    return event;
  }

  private TableRowMouseEvent createMouseEvent(TableRowSWT row, MouseEvent e,
      int type, boolean allowOOB) {
    TableCellMouseEvent event = new TableCellMouseEvent();
    event.row = row;
    event.eventType = type;
    event.button = e.button;
    // TODO: Change to not use SWT masks
    event.keyboardState = e.stateMask;
    event.skipCoreFunctionality = false;
    if (row != null) {
      Rectangle r = row.getBounds();
      event.x = e.x - r.x;
      if (!allowOOB && event.x < 0) {
        return null;
      }
      event.y = e.y - r.y;
      if (!allowOOB && event.y < 0) {
        return null;
      }
    }

    return event;
  }

  public void runDefaultAction(int stateMask) {
    // Don't allow mutliple run defaults in quick succession
    if (lastSelectionTriggeredOn > 0
        && System.currentTimeMillis() - lastSelectionTriggeredOn < 200) {
      return;
    }
   
    // plugin may have cancelled the default action
    if (System.currentTimeMillis() - lCancelSelectionTriggeredOn > 200) {
      lastSelectionTriggeredOn = System.currentTimeMillis();
      TableRowCore[] selectedRows = getSelectedRows();
      triggerDefaultSelectedListeners(selectedRows, stateMask);
    }
  }

  private void swt_updateColumnVisibilities(boolean doInvalidate) {
    TableColumnOrTreeColumn[] columns = table.getColumns();
    if (table.getItemCount() < 1 || columns.length == 0 || !table.isVisible()) {
      return;
    }
    columnVisibilitiesChanged = false;
    TableItemOrTreeItem topRow = table.getTopItem();
    if (topRow == null) {
      return;
    }
    for (int i = 0; i < columns.length; i++) {
      final TableColumnCore tc = (TableColumnCore) columns[i].getData("TableColumnCore");
      if (tc == null) {
        continue;
      }

      int position = tc.getPosition();
      if (position < 0 || position >= columnsVisible.length) {
        continue;
      }

      Rectangle size = topRow.getBounds(i);
      //System.out.println(sTableID + ": column " + i + ":" + tc.getName() + ": size="  + size + "; ca=" + clientArea + "; pos=" + position);
      size.intersect(clientArea);
      boolean nowVisible = !size.isEmpty();
      //System.out.println("  visible; was=" + columnsVisible[position] + "; now=" + nowVisible);
      if (columnsVisible[position] != nowVisible) {
        columnsVisible[position] = nowVisible;
        if (nowVisible && doInvalidate) {
          swt_runForVisibleRows(new TableGroupRowRunner() {
            public void run(TableRowCore row) {
              TableCellCore cell = row.getTableCellCore(tc.getName());
              if (cell != null) {
                cell.invalidate();
                cell.redraw();
              }
            }
          });
        }
      }
    }
  }

  public boolean isColumnVisible(
      org.gudy.azureus2.plugins.ui.tables.TableColumn column) {
    int position = column.getPosition();
    if (position < 0 || position >= columnsVisible.length) {
      return false;
    }
    return columnsVisible[position];

  }
 
  public boolean isUnfilteredDataSourceAdded(Object ds) {
    return listUnfilteredDataSources.contains(ds);
  }

  protected void initializeTableColumns(final TableOrTreeSWT table) {
    TableColumnOrTreeColumn[] oldColumns = table.getColumns();

    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) {
        TableColumnOrTreeColumn column = TableOrTreeUtils.getTableColumnEventItem(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();
            int newWidth = column.getWidth();
            if (OBEY_COLUMN_MINWIDTH) {
              int minWidth = tc.getMinWidth();
              if (minWidth > 0 && newWidth - padding < minWidth) {
                newWidth = minWidth + padding;
                column.setWidth(minWidth);
              }
            }
            tc.setWidth(newWidth - 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) {
      TableColumnOrTreeColumn tc = table.createNewColumn(SWT.NULL);
      //tc.setWidth(useTree ? 25 : 0);
      tc.setWidth(0);
      tc.setResizable(false);
      tc.setMoveable(false);
    }

    TableColumnCore[] tmpColumnsOrdered = new TableColumnCore[tableColumns.length];
    //Create all columns
    int columnOrderPos = 0;
    Arrays.sort(tableColumns,
        TableColumnManager.getTableColumnOrderComparator());
    for (int i = 0; i < tableColumns.length; i++) {
      int position = tableColumns[i].getPosition();
      if (position != -1 && tableColumns[i].isVisible()) {
        table.createNewColumn(SWT.NULL);
        //System.out.println(i + "] " + tableColumns[i].getName() + ";" + position);
        tmpColumnsOrdered[columnOrderPos++] = tableColumns[i];
      }
    }
    int numSWTColumns = table.getColumnCount();
    int iNewLength = numSWTColumns - (bSkipFirstColumn ? 1 : 0);
    columnsOrdered = new TableColumnCore[iNewLength];
    System.arraycopy(tmpColumnsOrdered, 0, columnsOrdered, 0, iNewLength);
    columnsVisible = new boolean[tableColumns.length];

    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).
    int swtColumnPos = (bSkipFirstColumn ? 1 : 0);
    for (int i = 0; i < tableColumns.length; i++) {
      TableColumnCore columnCore = tableColumns[i];
      int position = columnCore.getPosition();
      if (position == -1 || !columnCore.isVisible()) {
        continue;
      }

      columnsVisible[i] = false;

      String sName = columnCore.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

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

      TableColumnOrTreeColumn column = table.getColumn(swtColumnPos);
      try {
        column.setMoveable(true);
      } catch (NoSuchMethodError e) {
        // Ignore < SWT 3.1
      }
      column.setAlignment(TableColumnSWTUtils.convertColumnAlignmentToSWT(columnCore.getAlignment()));
      String iconReference = columnCore.getIconReference();
      if (iconReference != null) {
        Image image = ImageLoader.getInstance().getImage(iconReference);
        column.setImage(image);
      } else {
        Messages.setLanguageText(column.getColumn(), columnCore.getTitleLanguageKey());
      }
      if (!Constants.isUnix && !Utils.isCarbon) {
        column.setWidth(columnCore.getWidth());
      } else {
        column.setData("widthOffset", new Long(1));
        column.setWidth(columnCore.getWidth() + 1);
      }
      if (columnCore.getMinWidth() == columnCore.getMaxWidth()
          && columnCore.getMinWidth() > 0) {
        column.setResizable(false);
      }
      column.setData("TableColumnCore", columnCore);
      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);
     

      swtColumnPos++;
    }

    // Initialize the sorter after the columns have been added
    TableColumnManager tcManager = TableColumnManager.getInstance();

    String sSortColumn = tcManager.getDefaultSortColumnName(sTableID);
    if (sSortColumn == null || sSortColumn.length() == 0) {
      sSortColumn = sDefaultSortOn;
    }

    TableColumnCore tc = tcManager.getTableColumnCore(sTableID, sSortColumn);
    if (tc == null && tableColumns.length > 0) {
      tc = tableColumns[0];
    }
    sortColumn = tc;
    fixAlignment(tc, true);
    changeColumnIndicator();

    // Add move listener at the very end, so we don't get a bazillion useless
    // move triggers
    TableColumnOrTreeColumn[] columns = table.getColumns();
    for (int i = 0; i < columns.length; i++) {
      TableColumnOrTreeColumn column = columns[i];
      column.addListener(SWT.Move, columnMoveListener);
    }

    columnVisibilitiesChanged = true;
  }

  public void fixAlignment(TableColumnCore tc, boolean sorted) {
    if (Constants.isOSX) {
      if (table.isDisposed() || tc == null) {
        return;
      }
      int[] columnOrder = table.getColumnOrder();
      int i = tc.getPosition() - (bSkipFirstColumn ? 1 : 0);
      if (i < 0 || i >= columnOrder.length) {
        return;
      }
      TableColumnOrTreeColumn swtColumn = table.getColumn(columnOrder[i]);
      if (swtColumn != null) {
        if (swtColumn.getAlignment() == SWT.RIGHT && sorted) {
          swtColumn.setText("   " + swtColumn.getText() + "   ");
        } else {
          swtColumn.setText(swtColumn.getText().trim());
        }
      }
    }
  }

  /** Creates the Context Menu.
   * @param table
   *
   * @return a new Menu object
   */
  private Menu createMenu(final TableOrTreeSWT table) {
    if (!isMenuEnabled()) {
      return null;
    }
   
    final Menu menu = new Menu(shell, SWT.POP_UP);
    table.addListener(SWT.MenuDetect, new Listener() {
      public void handleEvent(Event event) {
        Point pt = event.display.map(null, table.getComposite(), new Point(event.x, event.y));
        boolean noRow = table.getItem(pt) == null;

        Rectangle clientArea = table.getClientArea();
        boolean inHeader = clientArea.y <= pt.y && pt.y < (clientArea.y + headerHeight);
        if (!noRow) {
          noRow = inHeader;
        }
       
        menu.setData("inBlankArea", (!inHeader && noRow));

        menu.setData("isHeader", new Boolean(noRow));

        int columnNo = getColumnNo(pt.x);
        menu.setData("column", columnNo < 0
            || columnNo >= table.getColumnCount() ? null
            : table.getColumn(columnNo));
      }
    });
    MenuBuildUtils.addMaintenanceListenerForMenu(menu,
        new MenuBuildUtils.MenuBuilder() {
          public void buildMenu(Menu menu, MenuEvent menuEvent) {
            Object oIsHeader = menu.getData("isHeader");
            boolean isHeader = (oIsHeader instanceof Boolean)
                ? ((Boolean) oIsHeader).booleanValue() : false;
            Object oInBlankArea = menu.getData("inBlankArea");
            boolean inBlankArea = (oInBlankArea instanceof Boolean)
                ? ((Boolean) oInBlankArea).booleanValue() : false;

            TableColumnOrTreeColumn tcColumn = (TableColumnOrTreeColumn) menu.getData("column");
           
            if (isHeader) {
              fillColumnMenu(tcColumn, inBlankArea);
            } else {
              fillMenu(menu, tcColumn);
            }

          }
        });

    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
   * @param tcColumn
   */
  private void fillMenu(final Menu menu, final TableColumnOrTreeColumn tcColumn) {
    String columnName = tcColumn == null ? null : (String) tcColumn.getData("Name");

    Object[] listeners = listenersMenuFill.toArray();
    for (int i = 0; i < listeners.length; i++) {
      TableViewSWTMenuFillListener l = (TableViewSWTMenuFillListener) listeners[i];
      l.fillMenu(columnName, menu);
    }

    boolean hasLevel1 = false;
    boolean hasLevel2 = false;
    // quick hack so we don't show plugin menus on selections of subitems
    synchronized (selectedRows) {
      for (TableRowCore row : selectedRows) {
        if (row.getParentRowCore() != null) {
          hasLevel2 = true;
        } else {
          hasLevel1 = true;
        }
      }
    }
   
    String sMenuID = hasLevel1 ? sTableID : TableManager.TABLE_TORRENT_FILES;
   
    // Add Plugin Context menus..
    boolean enable_items = table != null && table.getSelection().length > 0;

    TableContextMenuItem[] items = TableContextMenuManager.getInstance().getAllAsArray(
        sMenuID);

    // 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 (columnName == null) {
      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) {
          showColumnEditor();
        }
      });

    } else {

      MenuItem item = new MenuItem(menu, SWT.PUSH);
      Messages.setLanguageText(item, "MyTorrentsView.menu.thisColumn.toClipboard");
      item.addListener(SWT.Selection, new Listener() {
        public void handleEvent(Event e) {
          String sToClipboard = "";
          if (tcColumn == null) {
            return;
          }
          String columnName = (String) tcColumn.getData("Name");
          if (columnName == null) {
            return;
          }
          TableRowCore[] rows = getSelectedRows();
          for (TableRowCore row : rows) {
            if (row != rows[0]) {
              sToClipboard += "\n";
            }
            TableCellCore cell = row.getTableCellCore(columnName);
            if (cell != null) {
              sToClipboard += cell.getClipboardText();
            }
          }
          if (sToClipboard.length() == 0) {
            return;
          }
          new Clipboard(mainComposite.getDisplay()).setContents(new Object[] {
            sToClipboard
          }, new Transfer[] {
            TextTransfer.getInstance()
          });
        }
      });
    }
   
    if (items.length > 0 || menu_items.length > 0) {
      new org.eclipse.swt.widgets.MenuItem(menu, SWT.SEPARATOR);

      // 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)));
      }

      if (items.length > 0) {
        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, false) {
                  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());
              }
            });
      }
    }
   
    if (hasLevel1) {
    // Add Plugin Context menus..
    if (tcColumn != null) {
      TableColumnCore tc = (TableColumnCore) tcColumn.getData("TableColumnCore");
      TableContextMenuItem[] columnItems = tc.getContextMenuItems(TableColumnCore.MENU_STYLE_COLUMN_DATA);
      if (columnItems.length > 0) {
        new MenuItem(menu, SWT.SEPARATOR);
 
        MenuBuildUtils.addPluginMenuItems(getComposite(), columnItems, menu,
            true, true, new MenuBuildUtils.MenuItemPluginMenuControllerImpl(
                getSelectedDataSources(true)));
 
      }
    }

    if (filter != null) {
      final MenuItem itemFilter = new MenuItem(menu, SWT.PUSH);
      Messages.setLanguageText(itemFilter, "MyTorrentsView.menu.filter");
      itemFilter.addListener(SWT.Selection, new Listener() {
        public void handleEvent(Event event) {
          openFilterDialog();
        }
      });
    }
    }
  }

  void showColumnEditor() {
    TableRowCore focusedRow = getFocusedRow();
    if (focusedRow == null || focusedRow.isRowDisposed()) {
      focusedRow = getRow(0);
    }
    new TableColumnSetupWindow(classPluginDataSourceType, sTableID, focusedRow,
        TableStructureEventDispatcher.getInstance(sTableID)).open();
  }

  /**
   * SubMenu for column specific tasks.
   *
   * @param iColumn Column # that tasks apply to.
   */
  private void fillColumnMenu(final TableColumnOrTreeColumn tcColumn,
      boolean isBlankArea) {
    if (!isBlankArea) {
      TableColumnManager tcm = TableColumnManager.getInstance();
      TableColumnCore[] allTableColumns = tcm.getAllTableColumnCoreAsArray(
          classPluginDataSourceType, sTableID);
     
    Arrays.sort(allTableColumns,
        TableColumnManager.getTableColumnOrderComparator());

      for (final TableColumnCore tc : allTableColumns) {
        boolean visible = tc.isVisible();
        if (!visible) {
          TableColumnInfo columnInfo = tcm.getColumnInfo(classPluginDataSourceType, sTableID, tc.getName());
          if (columnInfo.getProficiency() != TableColumnInfo.PROFICIENCY_BEGINNER) {
            continue;
          }
        }
        MenuItem menuItem = new MenuItem(menu, SWT.CHECK);
        Messages.setLanguageText(menuItem, tc.getTitleLanguageKey());
        if (visible) {
          menuItem.setSelection(true);
        }
        menuItem.addListener(SWT.Selection, new Listener() {
          public void handleEvent(Event e) {
            tc.setVisible(!tc.isVisible());
            tableStructureChanged(true, null);
          }
        });
      }
    }

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

    final MenuItem itemResetColumns = new MenuItem(menu, SWT.PUSH);
    Messages.setLanguageText(itemResetColumns, "table.columns.reset");
    itemResetColumns.addListener(SWT.Selection, new Listener() {
      public void handleEvent(Event e) {
        TableColumnManager tcm = TableColumnManager.getInstance();
        String[] defaultColumnNames = tcm.getDefaultColumnNames(sTableID);
        if (defaultColumnNames != null) {
          for (TableColumnCore column : tableColumns) {
            column.setVisible(false);
          }
          int i = 0;
          for (String name : defaultColumnNames) {
            TableColumnCore column = tcm.getTableColumnCore(sTableID, name);
            if (column != null) {
              column.reset();
              column.setVisible(true);
              column.setPositionNoShift(i++);
            }
          }
          tcm.saveTableColumns(classPluginDataSourceType, sTableID);
          TableStructureEventDispatcher.getInstance(sTableID).tableStructureChanged(true, classPluginDataSourceType);
        }
      }
    });


    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) {
        showColumnEditor();
      }
    });

    if (menu != null) {
      menu.setData("column", tcColumn);
    }
   
    if (tcColumn == null) {
      return;
    }

    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, menu);
      }
    }

    final MenuItem at_item = new MenuItem(menu, SWT.CHECK);
    Messages.setLanguageText(at_item,
        "MyTorrentsView.menu.thisColumn.autoTooltip");
    at_item.addListener(SWT.Selection, new Listener() {
      public void handleEvent(Event e) {
        TableColumnOrTreeColumn tc = (TableColumnOrTreeColumn) menu.getData("column");
        TableColumnCore tcc = (TableColumnCore) tc.getData("TableColumnCore");
        tcc.setAutoTooltip(at_item.getSelection());
        tcc.invalidateCells();
      }
    });
    at_item.setSelection(((TableColumnCore) tcColumn.getData("TableColumnCore")).doesAutoTooltip());


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

      MenuBuildUtils.addPluginMenuItems(getComposite(), items, menu,
          true, true, new MenuBuildUtils.MenuItemPluginMenuControllerImpl(
              getSelectedDataSources(true)));

    }
  }

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

  public Composite getTableComposite() {
    return tableComposite;
  }
 
  public TableOrTreeSWT getTableOrTreeSWT() {
    return table;
  }

  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.getComposite().isVisible()) {
      view.refresh();
    }
  }

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

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

    triggerTableRefreshListeners();
  }

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

    // call to trigger invalidation if visibility changes
    isVisible();

    // 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 (columnVisibilitiesChanged == true) {
        swt_updateColumnVisibilities(true);
      }

      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 != null) {
          lLastSortedOn = 0;
          sortColumn.setLastSortValueChange(SystemTime.getCurrentTime());
        }
        _sortColumn(true, false, false);
      }

      long lTimeStart = SystemTime.getMonotonousTime();

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

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

      loopFactor++;
    } finally {
    }
  }

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

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

  // see common.TableView
  public void processDataSourceQueue() {
    Utils.getOffOfSWTThread(new AERunnable() {
      public void runSupport() {
        _processDataSourceQueue();
      }
    });
  }
 
  private void _processDataSourceQueue() {
    Object[] dataSourcesAdd = null;
    Object[] dataSourcesRemove = null;

    try {
      dataSourceToRow_mon.enter();
      if (dataSourcesToAdd.size() > 0) {
        if (dataSourcesToAdd.removeAll(dataSourcesToRemove) && DEBUGADDREMOVE) {
          debug("Saved time by not adding a row that was removed");
        }
        dataSourcesAdd = dataSourcesToAdd.toArray();

        dataSourcesToAdd.clear();
      }

      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;
    }

    columnVisibilitiesChanged = true;

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

  /*
  private void doPaint(final GC gc, final Rectangle dirtyArea) {
    if (getComposite() == null || getComposite().isDisposed()) {
      return;
    }

    swt_runForVisibleRows(new TableGroupRowRunner() {
      public void run(TableRowCore row) {
        if (!(row instanceof TableRowSWT)) {
          return;
        }
        TableRowSWT rowSWT = (TableRowSWT) row;
        Rectangle bounds = rowSWT.getBounds();
        if (bounds.intersects(dirtyArea)) {

          if (Constants.isWindowsVistaOrHigher) {
            Image imgBG = new Image(gc.getDevice(), bounds.width, bounds.height);
            gc.copyArea(imgBG, bounds.x, bounds.y);
            rowSWT.setBackgroundImage(imgBG);
          }

          //System.out.println("paint " + row);
          Color oldBG = (Color) row.getData("bgColor");
          Color newBG = rowSWT.getBackground();
          if (oldBG == null || !oldBG.equals(newBG)) {
            //System.out.println("redraw " + row + "; " + oldBG + ";" + newBG);
            row.invalidate();
            row.redraw();
            row.setData("bgColor", newBG);
          } else {
            rowSWT.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 = tabViews.get(i);
        if (view != null) {
          view.delete();
        }
      }
    }

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

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

    processDataSourceQueueCallback = null;

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

  // see common.TableView
  public void addDataSource(DATASOURCETYPE dataSource) {
    addDataSource(dataSource, false);
  }

  private void addDataSource(DATASOURCETYPE dataSource, boolean skipFilterCheck) {

    if (dataSource == null) {
      return;
    }

    listUnfilteredDatasources_mon.enter();
    try {
      listUnfilteredDataSources.add(dataSource);
    } finally {
      listUnfilteredDatasources_mon.exit();
    }

    if (!skipFilterCheck && filter != null
        && !filter.checker.filterCheck(dataSource, filter.text, filter.regex)) {
      return;
    }

    if (Utils.IMMEDIATE_ADDREMOVE_DELAY == 0) {
      reallyAddDataSources(new Object[] {
        dataSource
      });
      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();

        if ( dataSourcesToRemove.remove( dataSource )){
          // we're adding, override any pending removal
          if (DEBUGADDREMOVE) {
            debug("AddDS: Removed from toRemove.  Total Removals Queued: " + dataSourcesToRemove.size());
          }
        }

        if ( dataSourcesToAdd.contains(dataSource)){
          // added twice.. ensure it's not in the remove list
          if (DEBUGADDREMOVE) {
            debug("AddDS: Already There.  Total Additions Queued: " + dataSourcesToAdd.size());
          }
        } else {
          dataSourcesToAdd.add(dataSource);
          if (DEBUGADDREMOVE) {
            debug("Queued 1 dataSource to add.  Total Additions Queued: " + dataSourcesToAdd.size() + "; already=" + sortedRows.size());
          }
          refreshenProcessDataSourcesTimer();
        }

    } finally {

      dataSourceToRow_mon.exit();
    }
  }

  // see common.TableView
  public void addDataSources(final DATASOURCETYPE dataSources[]) {
    addDataSources(dataSources, false);
  }

  public void addDataSources(final DATASOURCETYPE dataSources[],
      boolean skipFilterCheck) {

    if (dataSources == null) {
      return;
    }

    listUnfilteredDatasources_mon.enter();
    try {
      listUnfilteredDataSources.addAll(Arrays.asList(dataSources));
    } finally {
      listUnfilteredDatasources_mon.exit();
    }

    if (Utils.IMMEDIATE_ADDREMOVE_DELAY == 0) {
      if (!skipFilterCheck && filter!= null) {
        for (int i = 0; i < dataSources.length; i++) {
          if (!filter.checker.filterCheck(dataSources[i], filter.text,
              filter.regex)) {
            dataSources[i] = null;
          }
        }
      }
      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++) {
        DATASOURCETYPE dataSource = dataSources[i];
        if (dataSource == null) {
          continue;
        }
        if (!skipFilterCheck
            && filter != null
            && !filter.checker.filterCheck(dataSource, filter.text,
                filter.regex)) {
          continue;
        }
        dataSourcesToRemove.remove(dataSource)// may be pending removal, override

        if (dataSourcesToAdd.contains(dataSource)){
        } else {
          count++;
          dataSourcesToAdd.add(dataSource);
        }
      }

      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 || processDataSourceQueueCallback == null) {
      // when processDataSourceQueueCallback is null, we are disposing
      return;
    }

    if (cellEditNotifier != null) {
      cellEditNotifier.sourcesChanged();
    }

    boolean processQueueImmediately = Utils.addDataSourceAggregated(processDataSourceQueueCallback);

    if (processQueueImmediately) {
      processDataSourceQueue();
    }
  }

  private void reallyAddDataSources(final Object dataSources[]) {
    // Note: We assume filterCheck has already run, and the list of dataSources
    //       all passed the filter
   
    if (mainComposite == null || table == null || mainComposite.isDisposed()
        || table.isDisposed()) {
      return;
    }

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

    // 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;
        }

        if (mapDataSourceToRow.containsKey(dataSources[i])) {
          dataSources[i] = null;
        } else {
          TableRowImpl row = new TableRowImpl(this, table, columnsOrdered,
              dataSources[i], bSkipFirstColumn);
          mapDataSourceToRow.put((DATASOURCETYPE) 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 " + dataSources.length + " rows;");
    }

    addDataSourcesToSWT(dataSources, true);
  }

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

      if (async) {
        Utils.execSWTThreadLater(0, 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;
    }

    mainComposite.getParent().setCursor(
        table.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));

    TableRowCore[] selectedRows = getSelectedRows();
     
    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
      if (!Constants.isWindows) {
        // Bug in Windows (7).  If you add 10 rows by setItemCount,
        // Windows will do some crappy shifting down of the non-row area
        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;
        }

        TableRowImpl row = (TableRowImpl) mapDataSourceToRow.get(dataSource);
        // We used to check if row already existed in sortedRows, but this
        // was always false, assuming dataSources only contains newly created
        // rows
        // ABOVE IS WRONG! It's not always false.  There's a case where
        // table is filled, cleared, filled again
        if ((row == null) || row.isRowDisposed() || sortedRows.indexOf(row) >= 0) {
        //if (row == null || row.isRowDisposed()) {
          continue;
        }
        if (sortColumn != null) {
          TableCellCore cell = row.getTableCellCore(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 = sortedRows.get(sortedRows.size() - 1);
            if (sortColumn == null || 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);
          }

          // NOTE: if the listener tries to do something like setSelected,
          // it will fail because we aren't done adding.
          // we should trigger after fillRowGaps()
          triggerListenerRowAdded(row);

          if (!bReplacedVisible
              && ((index >= iTopIndex && index <= iBottomIndex) || (index == sortedRows.size() - 1))) {
            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()) {
        // This could happen if one of the datasources was null, or
        // an error occured
        table.setItemCount(sortedRows.size());
      }
      if (sortedRows.size() == 1) {
        columnVisibilitiesChanged = true;
      }

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

      bReallyAddingDataSources = false;
      refreshenProcessDataSourcesTimer();
    }

    fillRowGaps(false);
    if (bReplacedVisible) {
      visibleRowsChanged();
    }

    if (!columnPaddingAdjusted && table.getItemCount() > 0 && bWas0Rows) {
      TableColumnOrTreeColumn[] tableColumnsSWT = table.getColumns();
      TableItemOrTreeItem item = table.getItem(0);
      // on *nix, the last column expands to fill remaining space.. let's just not touch it
      int len = Constants.isUnix ? tableColumnsSWT.length - 1
          : tableColumnsSWT.length;
      for (int i = 0; i < len; i++) {
        TableColumnCore tc = (TableColumnCore) tableColumnsSWT[i].getData("TableColumnCore");
        if (tc != null) {
          boolean foundOne = false;

          Rectangle bounds = item.getBounds(i);
          int tcWidth = tc.getWidth();
          if (tcWidth != 0 && bounds.width != 0) {
            Object oOldOfs = tableColumnsSWT[i].getData("widthOffset");
            int oldOfs = (oOldOfs instanceof Number) ? ((Number)oOldOfs).intValue() : 0;
            int ofs = tc.getWidth() - bounds.width + oldOfs;
            if (ofs > 0 && ofs != oldOfs) {
              foundOne = true;
              tableColumnsSWT[i].setResizable(true);
              tableColumnsSWT[i].setData("widthOffset", new Long(ofs + oldOfs));
            }
          }
          if (foundOne) {
            tc.triggerColumnSizeChange();
          }
        }
      }
      columnPaddingAdjusted = true;
    }
    if (bWas0Rows) {
      swt_updateColumnVisibilities(false);
    }

    setSelectedRows(selectedRows);
    if (DEBUGADDREMOVE) {
      debug("<< " + sortedRows.size());
    }

    mainComposite.getParent().setCursor(null);
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#removeDataSource(java.lang.Object)
  public void removeDataSource(final DATASOURCETYPE dataSource) {
    if (dataSource == null) {
      return;
    }

    listUnfilteredDatasources_mon.enter();
    try {
      listUnfilteredDataSources.remove(dataSource);
    } finally {
      listUnfilteredDatasources_mon.exit();
    }

    if (Utils.IMMEDIATE_ADDREMOVE_DELAY == 0) {
      reallyRemoveDataSources(new Object[]{dataSource});
      return;
    }

    try {
      dataSourceToRow_mon.enter();

      dataSourcesToAdd.remove(dataSource)// override any pending addition
      dataSourcesToRemove.add(dataSource);

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

    refreshenProcessDataSourcesTimer();
  }

  /** 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 DATASOURCETYPE[] dataSources) {
    if (dataSources == null || dataSources.length == 0) {
      return;
    }

    listUnfilteredDatasources_mon.enter();
    try {
      listUnfilteredDataSources.removeAll(Arrays.asList(dataSources));
    } finally {
      listUnfilteredDatasources_mon.exit();
    }

    if (Utils.IMMEDIATE_ADDREMOVE_DELAY == 0) {
      reallyRemoveDataSources(dataSources);
      return;
    }

    try {
      dataSourceToRow_mon.enter();

      for (int i = 0; i < dataSources.length; i++) {
        DATASOURCETYPE dataSource = dataSources[i];
        dataSourcesToAdd.remove(dataSource)// override any pending addition
        dataSourcesToRemove.add(dataSource);
      }

      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() {
        swt_reallyRemoveDataSources(dataSources, lStart);
      }
    });

    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);
        }
      }

      if (DEBUGADDREMOVE) {
        debug("<< Remove row(s), noswt");
      }
    }
  }
 
  private void swt_reallyRemoveDataSources(Object[] dataSources, long lStart) {
    if (table == null || table.isDisposed()) {
      return;
    }

    TableRowCore[] oldSelectedRows = getSelectedRows();

    mainComposite.getParent().setCursor(
        table.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));

    try {
      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<TableRowSWT> itemsToRemove = new ArrayList<TableRowSWT>();
      ArrayList<Long> swtItemsToRemove = new ArrayList<Long>();
      int iTopIndex = table.getTopIndex();
      int iBottomIndex = Utils.getTableBottomIndex(table, iTopIndex);
      boolean bRefresh = false;

      if (DEBUGADDREMOVE) {
        debug("--- Remove: vis rows " + iTopIndex + " to " + iBottomIndex);
      }

      // 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());
      }

      int numRemovedHavingSelection = 0;
      // 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) {
          if (item.isSelected()) {
            numRemovedHavingSelection++;
          }
          itemsToRemove.add(item);
          sortedRows.remove(item);
          triggerListenerRowRemoved(item);
        }
      }

      if (DEBUGADDREMOVE) {
        debug("-- Removed from map and list");
      }
      // 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);
        // refreshVisibleRows should fix up the display
        table.setItemCount(mapDataSourceToRow.size());
        // Bug in Cocoa SWT: On setItemCOunt(0), table doesn't do
        // a repaint so the rows appear to still be there.
        if (Utils.isCocoa && mapDataSourceToRow.size() == 0) {
          table.redraw();
        }
      }

      if (DEBUGADDREMOVE) {
        debug("-- Removed from SWT");
      }

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

      if (bRefresh) {
        visibleRowsChanged();
        fillRowGaps(false);
        swt_refreshVisibleRows();
        if (DEBUGADDREMOVE) {
          debug("-- Fill row gaps and refresh after remove");
        }
      }

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

      // if we removed all selected rows, select a row closest to the
      // first one
      /**  This is bad if the row was auto-removed and we select a new
       * row that the user doesn't know about, and then he does some bad
       * command to it.
      if (numRemovedHavingSelection == numSelected && numSelected >= 0
          && oldSelection.length > 0 && oldSelection[0] < table.getItemCount()
          && oldSelection[0] < sortedRows.size()) {
        oldSelectedRows = new TableRowCore[] {
          sortedRows.get(oldSelection[0])
        };
        setSelectedRows(oldSelectedRows);
        triggerSelectionListeners(getSelectedRows());
        return;
      }
      */
      if (oldSelectedRows.length > 0) {
        setSelectedRows(oldSelectedRows);
      }
    } finally {
      mainComposite.getParent().setCursor(null);
    }
  }

  // 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 (DEBUGADDREMOVE) {
          debug("removeAll (SWT)");
        }

        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");
    }
    if (parameterName == null || parameterName.equals("ReOrder Delay")) {
      reOrderDelay = configMan.getIntParameter("ReOrder Delay");
    }
    if (parameterName == null || parameterName.startsWith("Color")) {
      tableInvalidate();
    }
  }

  // ITableStructureModificationListener
  public void tableStructureChanged(final boolean columnAddedOrRemoved,
      Class forPluginDataSourceType) {
    if (forPluginDataSourceType == null
        || forPluginDataSourceType.equals(classPluginDataSourceType)) {
      Utils.execSWTThread(new AERunnable() {
        public void runSupport() {
          if (table.isDisposed()) {
            return;
          }
          _tableStructureChanged(columnAddedOrRemoved);
        }
      });
    }
  }
 
  private void _tableStructureChanged(boolean columnAddedOrRemoved) {
    triggerLifeCycleListener(TableLifeCycleListener.EVENT_DESTROYED);

    removeAllTableRows();

    if (columnAddedOrRemoved) {
      tableColumns = TableColumnManager.getInstance().getAllTableColumnCoreAsArray(
          classPluginDataSourceType, sTableID);
    }

    initializeTableColumns(table);
    refreshTable(false);

    triggerLifeCycleListener(TableLifeCycleListener.EVENT_INITIALIZED);
  }

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

  /**
   * The Columns width changed
   */
  // ITableStructureModificationListener
  public void columnSizeChanged(final TableColumnCore tableColumn) {
    Utils.execSWTThread(new AERunnable() {
      public void runSupport() {
        swt_columnSizeChanged(tableColumn);
      }
    });
  }

  public void swt_columnSizeChanged(TableColumnCore tableColumn) {
    int newWidth = tableColumn.getWidth();
    if (table == null || table.isDisposed()) {
      return;
    }

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

    if (Constants.isUnix) {
      final int fNewWidth = newWidth;
      final TableColumnOrTreeColumn 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);
  }

  // @see com.aelitis.azureus.ui.common.table.TableStructureModificationListener#cellInvalidate(com.aelitis.azureus.ui.common.table.TableColumnCore, java.lang.Object)
  public void cellInvalidate(TableColumnCore tableColumn,
      DATASOURCETYPE 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) {
        TableCellCore cell = row.getTableCellCore(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) {
        TableCellCore cell = row.getTableCellCore(sColumnName);
        if (cell != null) {
          cell.invalidate(bMustRefresh);
        }
      }
    });
    lLastSortedOn = 0;
    tableColumn.setLastSortValueChange(SystemTime.getCurrentTime());
  }

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

    runForAllRows(new TableGroupRowRunner() {
      public void run(TableRowCore row) {
        TableCellCore cell = row.getTableCellCore(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<TableRowSWT> iter = sortedRows.iterator(); iter.hasNext();) {
        TableRowCore row = iter.next();
        cells[i++] = row.getTableCellCore(sColumnName);
      }

    } finally {
      sortedRows_mon.exit();
    }

    return cells;
  }

  public org.gudy.azureus2.plugins.ui.tables.TableColumn getTableColumn(
      String sColumnName) {
    for (int i = 0; i < tableColumns.length; i++) {
      TableColumnCore tc = tableColumns[i];
      if (tc.getName().equals(sColumnName)) {
        return tc;
      }
    }
    return null;
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#getRows()
  public TableRowCore[] getRows() {
    try {
      sortedRows_mon.enter();

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

    } finally {
      sortedRows_mon.exit();
    }
  }

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

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

  // @see com.aelitis.azureus.ui.common.table.TableView#getRow(int)
  public TableRowCore getRow(int iPos) {
    try {
      sortedRows_mon.enter();

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

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

  protected TableRowCore getRowQuick(int iPos) {
    try {
      return sortedRows.get(iPos);
    } catch (Exception e) {
      return null;
    }
  }

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

  /** Warning: this method may require SWT Thread!
   *
   * TODO: Make sure callers are okay with that
   */
  protected TableRowCore getRow(TableItemOrTreeItem item) {
    if (item == null) {
      return null;
    }
    try {
      Object o = item.getData("TableRow");
      if ((o instanceof TableRowCore) && !((TableRowCore) o).isRowDisposed()) {
        return (TableRowCore) o;
      }

      if (item.getParentItem() != null) {
        TableRowCore row = getRow(item.getParentItem());
        return row.linkSubItem(item.getParentItem().indexOf(item));
      }
     
      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 = sortedRows.get(iPos);
        //System.out.print(".. associating to " + row);
        if (row != null && !row.isRowDisposed()) {
          row.setTableItem(iPos);
          //System.out.println(", now " + row);
          return row;
        }
        return null;
      }
    } 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();
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#getDataSources()
  public ArrayList<DATASOURCETYPE> getDataSources() {
    return new ArrayList<DATASOURCETYPE>(mapDataSourceToRow.keySet());
  }

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

  public List<Object> getSelectedDataSourcesList() {
    if (listSelectedCoreDataSources != null) {
      return listSelectedCoreDataSources;
    }
    synchronized (selectedRows) {
      if (table == null || table.isDisposed() || selectedRows.size() == 0) {
        return Collections.emptyList();
      }
 
      final ArrayList<Object> l = new ArrayList<Object>(
          selectedRows.size());
      for (TableRowCore row : selectedRows) {
        if (row != null && !row.isRowDisposed()) {
          Object ds = row.getDataSource(true);
          if (ds != null) {
            l.add(ds);
          }
        }
      }
      listSelectedCoreDataSources = l;
      return l;
    }
  }

  /** 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<Object> getSelectedPluginDataSourcesList() {
    synchronized (selectedRows) {
      if (table == null || table.isDisposed() || selectedRows.size() == 0) {
        return Collections.emptyList();
      }
 
      final ArrayList<Object> l = new ArrayList<Object>(selectedRows.size());
      for (TableRowCore row : selectedRows) {
        if (row != null && !row.isRowDisposed()) {
          Object ds = row.getDataSource(false);
          if (ds != null) {
            l.add(ds);
          }
        }
      }
      return l;
    }
  }

  /** Returns an array of all selected Data Sources.  Null data sources are
   * ommitted.
   *
   * @return an array containing the selected data sources
   *
   **/
  // see common.TableView
  public List<Object> getSelectedDataSources() {
    return new ArrayList<Object>(getSelectedDataSourcesList());
  }

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

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

  // @see com.aelitis.azureus.ui.common.table.TableView#getSelectedRowsSize()
  public int getSelectedRowsSize() {
    synchronized (selectedRows) {
      return selectedRows.size();
    }
  }

  /** Returns an list of all selected TableRowSWT objects.  Null data sources are
   * ommitted.
   *
   * @return an list containing the selected TableRowSWT objects
   */
  public List<TableRowCore> getSelectedRowsList() {
    synchronized (selectedRows) {
      final ArrayList<TableRowCore> l = new ArrayList<TableRowCore>(
          selectedRows.size());
      for (TableRowCore row : selectedRows) {
        if (row != null && !row.isRowDisposed()) {
          l.add(row);
        }
      }
 
      return l;
    }
  }
 
  public boolean isSelected(TableRow row) {
    synchronized (selectedRows) {
      return selectedRows.contains(row);
    }
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#getFocusedRow()
  public TableRowCore getFocusedRow() {
    synchronized (selectedRows) {
      if (selectedRows.size() == 0) {
        return null;
      }
      return selectedRows.get(0);
    }
  }

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

  public TableRowSWT[] swt_getVisibleRows() {
    if (!isVisible()) {
      return new TableRowSWT[0];
    }
   
    synchronized (this) {
      if (visibleRows == null) {
        visibleRowsChanged();
      }
     
      return visibleRows;
    }
  }

  /** 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) {
    synchronized (selectedRows) {
      if (selectedRows.size() > 0) {
        return selectedRows.get(0).getDataSource(bCoreObject);
      }
    }
    return null;
  }

  /** 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;
    }

    TableRowCore[] rows;
    synchronized (selectedRows) {
      rows = selectedRows.toArray(new TableRowCore[0]);
    }
    boolean ran = runner.run(rows);
    if (!ran) {
      for (int i = 0; i < rows.length; i++) {
        TableRowCore row = rows[i];
        runner.run(row);
      }
    }
  }

  /** 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 swt_runForVisibleRows(TableGroupRowRunner runner) {
    TableRowSWT[] rows = swt_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();

    for (int i = 0; i < rows.length; i++) {
      boolean isRowVisible = isRowVisible(rows[i]);
      runner.run(rows[i], isRowVisible);
     
      int numSubRows = rows[i].getSubItemCount();
      if (numSubRows > 0) {
        TableRowCore[] subRows = rows[i].getSubRowsWithNull();
        for (TableRowCore subRow : subRows) {
          if (subRow != null) {
            runner.run(subRow, isRowVisible && isRowVisible(subRow));
          }
        }
      }
    }
  }

  /**
   * 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<TableItemOrTreeItem> items, TableGroupRowRunner runner) {
    if (table == null || table.isDisposed()) {
      return;
    }

    final Iterator<TableItemOrTreeItem> iter = items.iterator();
    List<TableRowCore> rows_to_use = new ArrayList<TableRowCore>(items.size());
    while (iter.hasNext()) {
      TableItemOrTreeItem tableItem = iter.next();
      if (tableItem.isDisposed()) {
        continue;
      }

      TableRowSWT row = (TableRowSWT) getRow(tableItem);
      if (row != null && !row.isRowDisposed()) {
        rows_to_use.add(row);
      }
    }
    if (rows_to_use.size() > 0) {
      TableRowCore[] rows = rows_to_use.toArray(new TableRowCore[rows_to_use.size()]);
      boolean ran = runner.run(rows);
      if (!ran) {
        for (int i = 0; i < rows.length; i++) {
          TableRowCore row = rows[i];
          runner.run(row);
        }
      }
    }
  }

  // @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();
    }

    TableRowCore[] rows = getSelectedRows();
    for (TableRowCore row : rows) {
      sToClipboard += "\n";
      TableColumnCore[] visibleColumns = getVisibleColumns();
      for (int j = 0; j < visibleColumns.length; j++) {
        TableColumnCore column = visibleColumns[j];
        if (column.isVisible()) {
          if (j != 0) {
            sToClipboard += "\t";
          }
          TableCellCore cell = row.getTableCellCore(column.getName());
          if (cell != null) {
            sToClipboard += cell.getClipboardText();
          }
        }
      }
    }
    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) {
      int maskNoButton = (event.stateMask & ~SWT.BUTTON_MASK);
      if (maskNoButton != 0) {
        return;
      }
      TableColumnOrTreeColumn column = TableOrTreeUtils.getTableColumnEventItem(event.widget);
      if (column == null) {
        return;
      }
      TableColumnCore tableColumnCore = (TableColumnCore) column.getData("TableColumnCore");
      if (tableColumnCore != null) {
        sortColumnReverse(tableColumnCore);
        columnVisibilitiesChanged = true;
        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) {
      TableColumnOrTreeColumn column = TableOrTreeUtils.getTableColumnEventItem(event.widget);
      if (column == null) {
        return;
      }

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

      TableOrTreeSWT table = column.getParent();

      // Get the 'added position' of column
      // It would have been easier if event (.start, .end) contained the old
      // and new position..
      TableColumnOrTreeColumn[] tableColumns = table.getColumns();
      int iAddedPosition;
      for (iAddedPosition = 0; iAddedPosition < tableColumns.length; iAddedPosition++) {
        if (column.getColumn() == tableColumns[iAddedPosition].getColumn()) {
          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) {
            if (iNewPosition == -1) {
              iColumnOrder[0] = 0;
              iColumnOrder[1] = iAddedPosition;
              table.setColumnOrder(iColumnOrder);
              iNewPosition = 0;
            }
            //System.out.println("Moving " + tableColumnCore.getName() + " to Position " + i);
            tableColumnCore.setPositionNoShift(iNewPosition);
            tableColumnCore.saveSettings(null);
            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 < 0) {
        topIndex = itemCount - 1;
      }
      TableItemOrTreeItem 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;
    }

    TableItemOrTreeItem item = table.getItem(new Point(2, y));
    if (item == null) {
      return null;
    }
    return getRow(item);
  }

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

    TableItemOrTreeItem item = table.getItem(new Point(2, y));
    if (item == null) {
      item = table.getItem(new Point(x, y));
    }

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

    if (row == null || row.isRowDisposed()) {
      return null;
    }

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

    return row.getTableCellCore(sCellName);
  }

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

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

    TableColumnOrTreeColumn 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<DATASOURCETYPE> 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<IView> iter = tabViews.iterator(); iter.hasNext();) {
          IView view = iter.next();
          view.generateDiagnostics(writer);
        }
      } finally {
        writer.exdent();
      }

      writer.println("Columns:");
      writer.indent();
      try {
        TableColumnOrTreeColumn[] 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;
    }
    if (!Constants.isOSX) {
      bSkipFirstColumn = true;
    }
  }

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

  // from common.TableView
  public void setRowDefaultIconSize(Point size) {
    ptIconSize = size;
    if (!Constants.isOSX) {
      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, false);
  }

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

  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;

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

      boolean needsUpdate = false;

      try {
        sortedRows_mon.enter();

        if (bForceDataRefresh && sortColumn != null) {
          int i = 0;
          String sColumnID = sortColumn.getName();
          for (Iterator<TableRowSWT> iter = sortedRows.iterator(); iter.hasNext();) {
            TableRowSWT row = iter.next();
            TableCellSWT cell = row.getTableCellSWT(sColumnID);
            if (cell != null) {
              cell.refresh(true, i >= iTopIndex && i <= iBottomIndex);
            }
            i++;
          }
        }

        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 >= 0) {
                System.out.println("--- Build & Sort took " + lTimeDiff + "ms");
              }
            }
          } else {
            if (DEBUG_SORTER) {
              System.out.println("Skipping sort :)");
            }
          }
        }

        int count = sortedRows.size();
        if (iBottomIndex >= count) {
          iBottomIndex = count - 1;
        }

        for (int i = 0; i < sortedRows.size(); i++) {
          TableRowSWT row = sortedRows.get(i);
          boolean visible = i >= iTopIndex && i <= iBottomIndex;
          if (row.setTableItem(i, visible)) {
            if (visible) {
              needsUpdate = true;
            }
            iNumMoves++;
          }
        }
      } finally {
        sortedRows_mon.exit();
      }

      if (DEBUG_SORTER && iNumMoves > 0) {
        System.out.println("numMoves= " + iNumMoves + ";top=" + iTopIndex
            + ";bottom=" + iBottomIndex + ";needUpdate?" + needsUpdate);
      }

      if (needsUpdate) {
        visibleRowsChanged();
      }

      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. "
              + Debug.getCompressedStackTrace());
        }
      }
    } finally {
      sortColumn_mon.exit();
    }
  }

  protected void selectRow(final TableRowCore row, boolean trigger) {
    if (row == null || row.isRowDisposed()) {
      return;
    }
    synchronized (selectedRows) {
      if (selectedRows.contains(row)) {
        return;
      }
      selectedRows.add(row);
     
      listSelectedCoreDataSources = null;
    }
   
    if (trigger) {
      triggerSelectionListeners(new TableRowCore[] { row });
      triggerTabViewsDataSourceChanged();
    }
   
    ((TableRowSWT) row).setWidgetSelected(true);
  }

  protected void updateSelectedRows(TableItemOrTreeItem[] newSelectionArray, boolean trigger) {
    List<TableRowCore> newSelectionList = new ArrayList<TableRowCore>(1);

    //System.out.print("Selected Items: ");
    for (TableItemOrTreeItem item : newSelectionArray) {
      //System.out.print(table.indexOf(item));
      TableRowCore row = getRow(item);
      if (row != null && !row.isRowDisposed()) {
        newSelectionList.add(row);
      }// else { System.out.print("( NO ROW)"); }
      //System.out.print(", ");
    }
    //System.out.println();
    updateSelectedRows(newSelectionList.toArray(new TableRowCore[0]), trigger);
  }
 
  protected void updateSelectedRows(final TableRowCore[] newSelectionArray,
      final boolean trigger) {
    if (table.isDisposed()) {
      return;
    }

    /**
    System.out.print(newSelectionArray.length + " Selected Rows: ");
    for (TableRowCore row : newSelectionArray) {
      System.out.print(indexOf(row));
      System.out.print(", ");
    }
    System.out.println(" via " + Debug.getCompressedStackTrace(4));
    /**/

    final List<TableRowCore> oldSelectionList = new ArrayList<TableRowCore>();
    synchronized (selectedRows) {
      oldSelectionList.addAll(selectedRows);

      final TableItemOrTreeItem[] newSelectionItems = new TableItemOrTreeItem[newSelectionArray.length];
      int i = 0;
      listSelectedCoreDataSources = null;
      selectedRows.clear();
      if (newSelectionArray.length > 0) {
        for (TableRowCore row : newSelectionArray) {
          if (row != null && !row.isRowDisposed()) {
            newSelectionItems[i] = ((TableRowImpl)row).getItem();
            if (newSelectionItems[i] != null && !newSelectionItems[i].isDisposed()) {
              i++;
            }
            selectedRows.add(row);
          }
        }
        final int numItems = i;
        Utils.execSWTThread(new AERunnable() {
          public void runSupport() {
            TableItemOrTreeItem[] selection = table.getSelection();
            for (int i = 0; i < numItems; i++) {
              boolean alreadySelected = false;
              for (int j = 0; j < selection.length; j++) {
                if (selection[j] == newSelectionItems[i]) {
                  alreadySelected = true;
                  selection[j] = null;
                  break;
                }
              }
              if (!alreadySelected) {
                table.select(newSelectionItems[i]);
              }
            }
            for (int j = 0; j < selection.length; j++) {
              if (selection[j] != null) {
                table.deselect(selection[j]);
              }
            }
          }
        });
      } else {
        Utils.execSWTThread(new AERunnable() {
          public void runSupport() {
            table.deselectAll();
          }
        });
      }
    }

    Utils.getOffOfSWTThread(new AERunnable() {
      public void runSupport() {
        List<TableRowCore> listNewlySelected;
        boolean somethingChanged;
        synchronized (selectedRows) {
          List<TableRowCore> newSelectionList = new ArrayList<TableRowCore>(1);
          listNewlySelected = new ArrayList<TableRowCore>(1);

          // We'll remove items still selected from oldSelectionLeft, leaving
          // it with a list of items that need to fire the deselection event.
          for (TableRowCore row : newSelectionArray) {
            if (row == null || row.isRowDisposed()) {
              continue;
            }

            boolean existed = false;
            for (TableRowCore oldRow : oldSelectionList) {
              if (oldRow == row) {
                existed = true;
                newSelectionList.add(row);
                oldSelectionList.remove(row);
                break;
              }
            }
            if (!existed) {
              newSelectionList.add(row);
              listNewlySelected.add(row);
            }
          }

          somethingChanged = listNewlySelected.size() > 0
              || oldSelectionList.size() > 0;
          if (DEBUG_SELECTION) {
            System.out.println(somethingChanged + "] +"
                + listNewlySelected.size() + "/-" + oldSelectionList.size()
                + ";  UpdateSelectedRows via " + Debug.getCompressedStackTrace());
          }
        }

        if (trigger && somethingChanged) {
          if (listNewlySelected.size() > 0) {
            triggerSelectionListeners(listNewlySelected.toArray(new TableRowCore[0]));
          }
          if (oldSelectionList.size() > 0) {
            triggerDeselectionListeners(oldSelectionList.toArray(new TableRowCore[0]));
          }

          triggerTabViewsDataSourceChanged();
        }

      }
    });
  }

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

      TableColumnManager.getInstance().setDefaultSortColumnName(sTableID,
          sortColumn.getName());
    } else {
      sortColumn.setSortAscending(!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()
      TableColumnOrTreeColumn[] tcs = table.getColumns();
      for (int i = 0; i < tcs.length; i++) {
        String sName = (String) tcs[i].getData("Name");
        if (sName != null && sortColumn != 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
    }
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#isRowVisible(com.aelitis.azureus.ui.common.table.TableRowCore)
  public boolean isRowVisible(TableRowCore row) {
    if (row.isInPaintItem()) {
      return true;
    }
    if (visibleRows == null) {
      return false;
    }
    for (TableRowCore visibleRow : visibleRows) {
      if (row == visibleRow) {
        if (Utils.isThisThreadSWT() && !isVisible()) {
          return false;
        }
        return true;
      }
    }
    return false;
  }

  protected void visibleRowsChanged() {
    //debug("VRC " + Debug.getCompressedStackTrace());


    List<TableRowSWT> newlyVisibleRows = new ArrayList<TableRowSWT>();
    List<TableRowSWT> nowInVisibleRows;
    synchronized (this) {
      List<TableItemOrTreeItem> visibleTableItems;
      if (isVisible()) {
        visibleTableItems = Utils.getVisibleTableItems(table);
      } else {
        visibleTableItems = Collections.emptyList();
      }
      nowInVisibleRows = new ArrayList<TableRowSWT>(0);
      if (visibleRows != null) {
        nowInVisibleRows.addAll(Arrays.asList(visibleRows));
      }
      TableRowSWT[] rows = new TableRowSWT[visibleTableItems.size()];
      int pos = 0;
      for (TableItemOrTreeItem item : visibleTableItems) {
        TableRowCore row = getRow(item);
        if (row instanceof TableRowSWT) {
          rows[pos++] = (TableRowSWT) row;
          boolean removed = nowInVisibleRows.remove(row);
          if (!removed) {
            newlyVisibleRows.add((TableRowSWT) row);
          }
        }
      }
 
      if (pos < rows.length) {
        // Some were null, shrink array
        TableRowSWT[] temp = new TableRowSWT[pos];
        System.arraycopy(rows, 0, temp, 0, pos);
        visibleRows = temp;
      } else {
        visibleRows = rows;
      }
    }
   
    boolean bTableUpdate = false;

    if (DEBUG_ROWCHANGE) {
      System.out.println("visRowsChanged; shown=" + visibleRows.length + "; +"
          + newlyVisibleRows.size() + "/-" + nowInVisibleRows.size() + " via "
          + Debug.getCompressedStackTrace(8));
    }
    for (TableRowSWT row : newlyVisibleRows) {
      row.refresh(true, true);
      if (row instanceof TableRowImpl) {
        ((TableRowImpl) row).setShown(true, false);
      }
      if (Constants.isOSX) {
        bTableUpdate = true;
      }
    }

    for (TableRowSWT row : nowInVisibleRows) {
      if (row instanceof TableRowImpl) {
        ((TableRowImpl) row).setShown(false, false);
      }
    }

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

  public Image obfusticatedImage(final Image image) {
    if (table.getItemCount() == 0 || !isVisible()) {
      return image;
    }

    TableColumnOrTreeColumn[] 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 || iTopIndex < 0) {
          continue;
        }

        for (int j = iTopIndex; j <= iBottomIndex; j++) {
          TableItemOrTreeItem rowSWT = table.getItem(j);
          TableRowSWT row = (TableRowSWT) table.getItem(j).getData("TableRow");
          if (row != null && !row.isRowDisposed()) {
            TableCellSWT cell = row.getTableCellSWT(tc.getName());

            String text = cell.getObfusticatedText();

            if (text != null) {
              final Rectangle columnBounds = rowSWT.getBounds(i);
              if (columnBounds.y + columnBounds.height > clientArea.y
                  + clientArea.height) {
                columnBounds.height -= (columnBounds.y + columnBounds.height)
                    - (clientArea.y + clientArea.height);
              }
              if (columnBounds.x + columnBounds.width > clientArea.x
                  + clientArea.width) {
                columnBounds.width -= (columnBounds.x + columnBounds.width)
                    - (clientArea.x + clientArea.width);
              }
             
              Point location = Utils.getLocationRelativeToShell(table.getComposite());
             
              columnBounds.x += location.x;
              columnBounds.y += location.y;

              UIDebugGenerator.obfusticateArea(table.getDisplay(), image,
                  columnBounds, text);
            }
          }
        }

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

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

  void debug(String s) {
    AEDiagnosticsLogger diag_logger = AEDiagnostics.getLogger("table");
    diag_logger.log(SystemTime.getCurrentTime() + ":" + sTableID + ": " + 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)
  /**
   * @note bIncludeQueue can return an invalid number, such as a negative :(
   */
  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()) {
      // Used to ensure all rows have index, but I don't see a reason why.
      // Uses a lot of CPU, so kill it :)
      //ensureAllRowsHaveIndex();
      table.selectAll();
      updateSelectedRows(getRows(), true);
    }
  }

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


  // @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.getComposite(), style);
    dragSource.addDragListener(new DragSourceAdapter() {
      public void dragStart(DragSourceEvent event) {
        table.setCursor(null);
        isDragging = true;
      }
     
      public void dragFinished(DragSourceEvent event) {
        isDragging = false;
      }
    });
    table.addDisposeListener(new DisposeListener() {
      // @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
      public void widgetDisposed(DisposeEvent e) {
        if (!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.getComposite(), style);
    table.addDisposeListener(new DisposeListener() {
      // @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
      public void widgetDisposed(DisposeEvent e) {
        if (!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) {
    TableItemOrTreeItem ti = TableOrTreeUtils.getEventItem(event.item);
    if (ti != null) {
      return (TableRowCore) ti.getData("TableRow");
    }
    return null;
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#dataSourceExists(java.lang.Object)
  public boolean dataSourceExists(DATASOURCETYPE 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 TableCellCore 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, true);
  }

  // @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#getTableCellMouseOffset()
  public Point getTableCellMouseOffset(TableCellSWT tableCell) {
    if (tableCell == null) {
      return null;
    }
    Point pt = table.getDisplay().getCursorLocation();
    pt = table.toControl(pt);

    Rectangle bounds = tableCell.getBounds();
    int x = pt.x - bounds.x;
    if (x < 0 || x > bounds.width) {
      return null;
    }
    int y = pt.y - bounds.y;
    if (y < 0 || y > bounds.height) {
      return null;
    }
    return new Point(x, y);
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#getDataSourceType()
  public Class<?> getDataSourceType() {
    return classPluginDataSourceType;
  }

  public void addRefreshListener(TableRowRefreshListener listener) {
    try {
      listeners_mon.enter();

      if (refreshListeners == null) {
        refreshListeners = new ArrayList<TableRowRefreshListener>(1);
      }

      refreshListeners.add(listener);

    } finally {
      listeners_mon.exit();
    }
  }

  public void removeRefreshListener(TableRowRefreshListener listener) {
    try {
      listeners_mon.enter();

      if (refreshListeners == null) {
        return;
      }

      refreshListeners.remove(listener);

    } finally {
      listeners_mon.exit();
    }
  }

  public void invokeRefreshListeners(TableRowCore row) {
    Object[] listeners;
    try {
      listeners_mon.enter();
      if (refreshListeners == null) {
        return;
      }
      listeners = refreshListeners.toArray();

    } finally {
      listeners_mon.exit();
    }

    for (int i = 0; i < listeners.length; i++) {
      try {
        TableRowRefreshListener l = (TableRowRefreshListener) listeners[i];

        l.rowRefresh(row);

      } catch (Throwable e) {
        Debug.printStackTrace(e);
      }
    }
  }

  /** Note: Callers need to be on SWT Thread */
  public boolean isVisible() {
    boolean wasVisible = isVisible;
    isVisible = table != null && !table.isDisposed() && table.isVisible() && !shell.getMinimized();
    if (isVisible != wasVisible) {
      visibleRowsChanged();
      if (isVisible) {
        loopFactor = 0;

        IView view = getActiveSubView();
        if (view instanceof IViewExtension) {
          ((IViewExtension)view).viewActivated();
        }
      } else {
        IView view = getActiveSubView();
        if (view instanceof IViewExtension) {
          ((IViewExtension)view).viewDeactivated();
        }
      }
    }
    return isVisible;
  }

  public void showRow(final TableRowCore row) {
    Utils.execSWTThread(new AERunnable() {
      public void runSupport() {
        int index = row.getIndex();
        if (index >= 0 && index < table.getItemCount()) {
          table.showItem(table.getItem(index));
        }
      }
    });
  }

  public boolean isMenuEnabled() {
    return menuEnabled;
  }

  public void setMenuEnabled(boolean menuEnabled) {
    this.menuEnabled = menuEnabled;
  }

  private void openFilterDialog() {
    if (filter == null) {
      return;
    }
    SimpleTextEntryWindow entryWindow = new SimpleTextEntryWindow();
    entryWindow.initTexts("MyTorrentsView.dialog.setFilter.title", null,
        "MyTorrentsView.dialog.setFilter.text", new String[] {
          MessageText.getString(getTableID() + "View" + ".header")
        });
    entryWindow.setPreenteredText(filter.text, false);
    entryWindow.prompt();
    if (!entryWindow.hasSubmittedInput()) {
      return;
    }
    String message = entryWindow.getSubmittedInput();

    if (message == null) {
      message = "";
    }

    setFilterText(message);
  }

  private void handleSearchKeyPress(KeyEvent e) {
    if (filter == null || e.widget == filter.widget) {
      return;
    }

    String newText = null;

    // normal character: jump to next item with a name beginning with this character
    if (ASYOUTYPE_MODE == ASYOUTYPE_MODE_FIND) {
      if (System.currentTimeMillis() - filter.lastFilterTime > 3000)
        newText = "";
    }

    if (e.keyCode == SWT.BS) {
      if (e.stateMask == SWT.CONTROL) {
        newText = "";
      } else if (filter.nextText.length() > 0) {
        newText = filter.nextText.substring(0, filter.nextText.length() - 1);
      }
    } else if ((e.stateMask & ~SWT.SHIFT) == 0 && e.character > 32) {
      newText = filter.nextText + String.valueOf(e.character);
    }

    if (newText == null) {
      return;
    }

    if (ASYOUTYPE_MODE == ASYOUTYPE_MODE_FILTER) {
      if (filter != null && filter.widget != null && !filter.widget.isDisposed()) {
        filter.widget.setFocus();
      }
      setFilterText(newText);
    } else {
      TableCellCore[] cells = getColumnCells("name");

      //System.out.println(sLastSearch);

      Arrays.sort(cells, TableCellImpl.TEXT_COMPARATOR);
      int index = Arrays.binarySearch(cells, filter.text,
          TableCellImpl.TEXT_COMPARATOR);
      if (index < 0) {

        int iEarliest = -1;
        String s = filter.regex ? filter.text : "\\Q" + filter.text + "\\E";
        Pattern pattern = Pattern.compile(s, Pattern.CASE_INSENSITIVE);
        for (int i = 0; i < cells.length; i++) {
          Matcher m = pattern.matcher(cells[i].getText());
          if (m.find() && (m.start() < iEarliest || iEarliest == -1)) {
            iEarliest = m.start();
            index = i;
          }
        }

        if (index < 0)
          // Insertion Point (best guess)
          index = -1 * index - 1;
      }

      if (index >= 0) {
        if (index >= cells.length)
          index = cells.length - 1;
        TableRowCore row = cells[index].getTableRowCore();
        int iTableIndex = row.getIndex();
        if (iTableIndex >= 0) {
          setSelectedRows(new TableRowCore[] {
            row
          });
        }
      }
      filter.lastFilterTime = System.currentTimeMillis();
    }
    e.doit = false;
  }

  public void setFilterText(String s) {
    if (filter == null) {
      return;
    }
    filter.nextText = s;
    if (filter != null && filter.widget != null && !filter.widget.isDisposed()) {
      if (!filter.nextText.equals(filter.widget.getText())) {
        filter.widget.setText(filter.nextText);
        filter.widget.setSelection(filter.nextText.length());
      }

      if (filter.regex) {
        try {
          Pattern.compile(filter.nextText, Pattern.CASE_INSENSITIVE);
          filter.widget.setBackground(COLOR_FILTER_REGEX);
          Messages.setLanguageTooltip(filter.widget,
              "MyTorrentsView.filter.tooltip");
        } catch (Exception e) {
          filter.widget.setBackground(Colors.colorErrorBG);
          filter.widget.setToolTipText(e.getMessage());
        }
      } else {
        filter.widget.setBackground(null);
        Messages.setLanguageTooltip(filter.widget,
            "MyTorrentsView.filter.tooltip");
      }
    }

    if (filter.eventUpdate != null) {
      filter.eventUpdate.cancel();
    }
    filter.eventUpdate = SimpleTimer.addEvent("SearchUpdate",
        SystemTime.getOffsetTime(ASYOUTYPE_UPDATEDELAY),
        new TimerEventPerformer() {
          public void perform(TimerEvent event) {
            if (filter == null) {
              return;
            }
            if (filter.eventUpdate == null || filter.eventUpdate.isCancelled()) {
              filter.eventUpdate = null;
              return;
            }
            filter.eventUpdate = null;
            if (filter.nextText != null && !filter.nextText.equals(filter.text)) {
              filter.text = filter.nextText;
              filter.checker.filterSet(filter.text);
              refilter();
            }
          }
        });
  }

  public String getFilterText() {
    return filter == null ? "" : filter.text;
  }

  @SuppressWarnings("unchecked")
  public void refilter() {
    if (filter == null) {
      return;
    }
    if (filter.eventUpdate != null) {
      filter.eventUpdate.cancel();
    }
    filter.eventUpdate = null;

    listUnfilteredDatasources_mon.enter();
    try {
      DATASOURCETYPE[] unfilteredArray = (DATASOURCETYPE[]) listUnfilteredDataSources.toArray();

      Set<DATASOURCETYPE> existing = new HashSet<DATASOURCETYPE>(
          getDataSources());
      List<DATASOURCETYPE> listRemoves = new ArrayList<DATASOURCETYPE>();
      List<DATASOURCETYPE> listAdds = new ArrayList<DATASOURCETYPE>();

      for (int i = 0; i < unfilteredArray.length; i++) {
        boolean bHave = existing.contains(unfilteredArray[i]);
        boolean isOurs = filter.checker.filterCheck(
            unfilteredArray[i], filter.text, filter.regex);
        if (!isOurs) {
          if (bHave) {
            listRemoves.add(unfilteredArray[i]);
          }
        } else {
          if (!bHave) {
            listAdds.add(unfilteredArray[i]);
          }
        }
      }
      removeDataSources((DATASOURCETYPE[]) listRemoves.toArray());
      addDataSources((DATASOURCETYPE[]) listAdds.toArray(), true);

      // add back the ones removeDataSources removed
      listUnfilteredDataSources.addAll(listRemoves);
    } finally {
      listUnfilteredDatasources_mon.exit();
      processDataSourceQueue();
    }
  }

  // @see org.gudy.azureus2.ui.swt.views.table.TableViewSWT#enableFilterCheck(org.eclipse.swt.widgets.Text, org.gudy.azureus2.ui.swt.views.table.TableViewFilterCheck)
  public void enableFilterCheck(Text txtFilter,
      TableViewFilterCheck<DATASOURCETYPE> filterCheck) {
    if (filter != null) {
      if (filter.widget != null && !filter.widget.isDisposed()) {
        filter.widget.removeKeyListener(TableViewSWTImpl.this);
        filter.widget.removeModifyListener(filter.widgetModifyListener);
      }
    } else{
      filter = new filter();
    }
    filter.widget = txtFilter;
    if (txtFilter != null) {
      txtFilter.setMessage("Filter");
      txtFilter.addKeyListener(this);
 
      filter.widgetModifyListener = new ModifyListener() {
        public void modifyText(ModifyEvent e) {
          setFilterText(((Text) e.widget).getText());
        }
      };
      txtFilter.addModifyListener(filter.widgetModifyListener);
     
      if (txtFilter.getText().length() == 0) {
        txtFilter.setText(filter.text);
      } else {
        filter.text = filter.nextText = txtFilter.getText();
      }
    } else {
      filter.text = filter.nextText = "";
    }
   
    filter.checker = filterCheck;

    filter.checker.filterSet(filter.text);
    refilter();
  }

  public boolean enableSizeSlider(Composite composite, final int min, final int max) {
    try {
      if (sliderArea != null && !sliderArea.isDisposed()) {
        sliderArea.dispose();
      }
      Class<?> claTable = Class.forName("org.eclipse.swt.widgets."
          + (useTree ? "Tree" : "Table"));
      final Method method = claTable.getDeclaredMethod("setItemHeight", new Class<?>[] {
        int.class
      });
      method.setAccessible(true);

      composite.setLayout(new FormLayout());
      sliderArea = new Label(composite, SWT.NONE);
      ((Label)sliderArea).setImage(ImageLoader.getInstance().getImage("zoom"));
      sliderArea.addListener(SWT.MouseUp, new Listener() {
        public void handleEvent(Event event) {
          final Shell shell = new Shell(sliderArea.getShell(), SWT.BORDER);
          Listener l = new Listener() {
            public void handleEvent(Event event) {
              if (event.type == SWT.MouseExit) {
                Control curControl = event.display.getCursorControl();
                Point curPos = event.display.getCursorLocation();
                Point curPosRelShell = shell.toControl(curPos);
                Rectangle bounds = shell.getBounds();
                bounds.x = bounds.y = 0;
                if (!bounds.contains(curPosRelShell)) {
                  shell.dispose();
                  return;
                }
                if (curControl != null
                    && (curControl == shell || curControl.getParent() == shell)) {
                  return;
                }
              }
              shell.dispose();
            }
          };
          shell.setBackgroundMode(SWT.INHERIT_FORCE);
          shell.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_INFO_BACKGROUND));
          shell.addListener(SWT.MouseExit, l);
          shell.addListener(SWT.Deactivate, l);
          FillLayout fillLayout = new FillLayout();
          fillLayout.marginHeight = 4;
          shell.setLayout(fillLayout);
          final Scale slider = new Scale(shell, SWT.VERTICAL);
          slider.addListener(SWT.MouseExit, l);
          slider.addListener(SWT.Deactivate, l);
          slider.setMinimum(min);
          slider.setMaximum(max);
          slider.setSelection(getRowDefaultHeight());
          try {
            method.invoke(table.getComposite(), new Object[] { slider.getSelection() } );
          } catch (Throwable e1) {
          }
          slider.addSelectionListener(new SelectionListener() {
            public void widgetSelected(SelectionEvent e) {
              setRowDefaultHeight(slider.getSelection());
              try {
                method.invoke(table.getComposite(), new Object[] { slider.getSelection() } );
              } catch (Throwable e1) {
                e1.printStackTrace();
              }
              tableInvalidate();
            }
           
            public void widgetDefaultSelected(SelectionEvent e) {
            }
          });
          Point pt = sliderArea.toDisplay(event.x - 2, event.y - 5);
          int width = Constants.isOSX ? 20 : 50;
          shell.setBounds(pt.x - (width / 2), pt.y, width, 120);
          shell.open();
        }
      });
     
      sliderArea.setLayoutData(Utils.getFilledFormData());
      composite.layout();
    } catch (Throwable t) {
      return false;
    }
    return true;
  }
 
  public void disableSizeSlider() {
    Utils.disposeSWTObjects(new Object[] { sliderArea });
  }
 
  public void setEnabled(final boolean enable) {
    Utils.execSWTThread(new AERunnable() {
      public void runSupport() {
        if (!isDisposed()) {
          table.setEnabled(enable);
          /*
          if (enable) {
            Image oldImage = table.getBackgroundImage();
            table.setBackgroundImage(null);
            Utils.disposeSWTObjects(new Object[] { oldImage } );
          } else {
            final Image image = new Image(table.getDisplay(), 50, 50);
           
            GC gc = new GC(image);
            gc.setBackground(ColorCache.getColor(gc.getDevice(), 0xee, 0xee, 0xee));
            gc.fillRectangle(0, 0, 50, 50);
            gc.dispose();
            table.addDisposeListener(new DisposeListener() {
              public void widgetDisposed(DisposeEvent e) {
                Utils.disposeSWTObjects(new Object[] { image } );
              }
            });
           
            table.setBackgroundImage(image);
          }
          */
        }
      }
    });
  }
 
  public void addRowMouseListener(TableRowMouseListener listener) {
    try {
      mon_RowMouseListener.enter();

      if (rowMouseListeners == null)
        rowMouseListeners = new ArrayList<TableRowMouseListener>(1);

      rowMouseListeners.add(listener);

    } finally {
      mon_RowMouseListener.exit();
    }
  }

  public void removeRowMouseListener(TableRowMouseListener listener) {
    try {
      mon_RowMouseListener.enter();

      if (rowMouseListeners == null)
        return;

      rowMouseListeners.remove(listener);

    } finally {
      mon_RowMouseListener.exit();
    }
  }
 
  private void invokeRowMouseListener(TableRowMouseEvent event) {
    if (rowPaintListeners == null) {
      return;
    }
    ArrayList<TableRowMouseListener> listeners = new ArrayList<TableRowMouseListener>(
        rowMouseListeners);

    for (int i = 0; i < listeners.size(); i++) {
      try {
        TableRowMouseListener l = (listeners.get(i));

        l.rowMouseTrigger(event);

      } catch (Throwable e) {
        Debug.printStackTrace(e);
      }
    }
  }


  public void addRowPaintListener(TableRowSWTPaintListener listener) {
    try {
      mon_RowPaintListener.enter();

      if (rowPaintListeners == null)
        rowPaintListeners = new ArrayList<TableRowSWTPaintListener>(1);

      rowPaintListeners.add(listener);

    } finally {
      mon_RowPaintListener.exit();
    }
  }

  public void removeRowPaintListener(TableRowSWTPaintListener listener) {
    try {
      mon_RowPaintListener.enter();

      if (rowPaintListeners == null)
        return;

      rowPaintListeners.remove(listener);

    } finally {
      mon_RowPaintListener.exit();
    }
  }

  protected void invokePaintListeners(GC gc, TableRowCore row,
      TableColumnCore column, Rectangle cellArea) {
    if (rowPaintListeners == null) {
      return;
    }
    ArrayList<TableRowSWTPaintListener> listeners = new ArrayList<TableRowSWTPaintListener>(
        rowPaintListeners);

    for (int i = 0; i < listeners.size(); i++) {
      try {
        TableRowSWTPaintListener l = (listeners.get(i));

        l.rowPaint(gc, row, column, cellArea);

      } catch (Throwable e) {
        Debug.printStackTrace(e);
      }
    }
  }

  public boolean canHaveSubItems() {
    return useTree;
  }
 
  protected TableColumnCore[] getColumnsOrdered() {
    return columnsOrdered;
  }

  // @see com.aelitis.azureus.ui.common.table.TableView#setSelectedRows(com.aelitis.azureus.ui.common.table.TableRowCore[])
  public void setSelectedRows(TableRowCore[] rows) {
    updateSelectedRows(rows, true);
  }
}
TOP

Related Classes of org.gudy.azureus2.ui.swt.views.table.impl.TableViewSWTImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.