sysperfana/perfinvestigator/com.nokia.carbide.cpp.pi.call/src/com/nokia/carbide/cpp/pi/call/CallVisualiser.java
author Toni Pulkkinen <ext-toni.p.pulkkinen@nokia.com>
Wed, 23 Jun 2010 15:05:09 +0300
changeset 12 ae255c9aa552
parent 5 844b047e260d
permissions -rw-r--r--
Performance Investigator Carbide extension 2.4.0

/*
 * Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). 
 * All rights reserved.
 * This component and the accompanying materials are made available
 * under the terms of the License "Eclipse Public License v1.0"
 * which accompanies this distribution, and is available
 * at the URL "http://www.eclipse.org/legal/epl-v10.html".
 *
 * Initial Contributors:
 * Nokia Corporation - initial contribution.
 *
 * Contributors:
 *
 * Description: 
 *
 */

/**
 * 
 */
package com.nokia.carbide.cpp.pi.call;

//import java.awt.Color;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.MouseEvent;
import java.text.DecimalFormat;
import java.util.Vector;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionManager;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.SubMenuManager;
import org.eclipse.jface.dialogs.IPageChangedListener;
import org.eclipse.jface.dialogs.PageChangedEvent;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.IActionBars;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.ide.IIDEActionConstants;

import com.nokia.carbide.cpp.internal.pi.interfaces.ISaveSamples;
import com.nokia.carbide.cpp.internal.pi.interfaces.ISaveTable;
import com.nokia.carbide.cpp.internal.pi.save.SaveTableWizard;
import com.nokia.carbide.cpp.internal.pi.visual.GenericTable;
import com.nokia.carbide.cpp.pi.editors.PIPageEditor;
import com.nokia.carbide.cpp.pi.util.SourceLookup;


public class CallVisualiser extends GenericTable
{
	private static final long serialVersionUID = 1L;

	// table column IDs in hopes it's quicker than comparing characters
	protected static final int COLUMN_ID_IS_CALLED       = 100;
	protected static final int COLUMN_ID_IS_CALLER       = 101;
	protected static final int COLUMN_ID_RECURSIVE_CALL  = 102;
	protected static final int COLUMN_ID_CALLER_PERCENT  = 103;
	protected static final int COLUMN_ID_CALLEE_PERCENT  = 104;
	protected static final int COLUMN_ID_IS_CALLED_COUNT = 105;
	protected static final int COLUMN_ID_IS_CALLER_COUNT = 106;
	
	// table column headings
	protected static final String COLUMN_HEAD_IS_CALLED       = Messages.getString("CallVisualiser.isCalled"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_IS_CALLER       = Messages.getString("CallVisualiser.isCaller"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_RECURSIVE_CALL  = Messages.getString("CallVisualiser.recursiveCaller"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_CALLER_PERCENT  = Messages.getString("CallVisualiser.percentOfCalls"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_CALLEE_PERCENT  = Messages.getString("CallVisualiser.percentOfCalls"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_IS_CALLED_COUNT = Messages.getString("CallVisualiser.calledSamples"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_IS_CALLER_COUNT = Messages.getString("CallVisualiser.callerSamples"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_IS_CALLED_COUNT2 = Messages.getString("CallVisualiser.calls"); //$NON-NLS-1$
	protected static final String COLUMN_HEAD_IS_CALLER_COUNT2 = Messages.getString("CallVisualiser.calls"); //$NON-NLS-1$

	// table column widths
	protected static final int COLUMN_WIDTH_IS_CALLED       = 70;
	protected static final int COLUMN_WIDTH_IS_CALLER       = 70;
	protected static final int COLUMN_WIDTH_RECURSIVE_CALL  = 105;
	protected static final int COLUMN_WIDTH_CALLER_PERCENT  = 98;
	protected static final int COLUMN_WIDTH_CALLEE_PERCENT  = 98;
	protected static final int COLUMN_WIDTH_IS_CALLED_COUNT = 113;
	protected static final int COLUMN_WIDTH_IS_CALLER_COUNT = 85;
	protected static final int COLUMN_WIDTH_IS_CALLED_COUNT2 = 65;
	protected static final int COLUMN_WIDTH_IS_CALLER_COUNT2 = 65;
	
	// SashForm to hold the tables and their titles
	private SashForm sashForm;
	
	// Table viewers and their tables
	private TableViewer callerTableViewer;
	private Table callerTable;
	
	private TableViewer currentFunctionTableViewer;
	private Table       currentFunctionTable;
	private TableColumn  currentFunctionDefaultColumn;

	private TableViewer calleeTableViewer;
	private Table calleeTable;

	// context menus
	private Menu callerMenu;
	private Menu currentFunctionMenu;
	private Menu calleeMenu;
	
	private Table currentMenuTable;

	private SashForm parent;

	// this display's editor and page
	private PIPageEditor pageEditor;
	private int pageIndex;
	
	// trace associated with this display
	private GfcTrace myTrace;
	
	/** the editor page this is sitting on */
	private Composite curPage;
	protected boolean isPageActive;
	protected boolean needsRefresh;
	
	//start and end of the timeframe at the last selection event
	private int curStart;
	private int curEnd;
	
	// lists of functions, function callers, and function callees
	private GfcFunctionItem[]  functionArray;
	private CallerCalleeItem[] callerList;
	private CallerCalleeItem[] calleeList;
	
	// menu items
	protected Action copyAction;
	protected Action copyTableAction;
	protected Action functionCopyFunctionAction;
	protected Action saveTableAction;
	protected Action functionSaveFunctionAction;
	
	protected final static int SAMPLES_AT_ONE_TIME = 1000;
	private Job setTimeframeJob = null;
	
	// class to pass sample data to the save wizard
    public class SaveSampleString implements ISaveSamples {
    	int startIndex = 0;
    	
    	public SaveSampleString() {
		}

    	public String getData() {
    		return getData(SAMPLES_AT_ONE_TIME);
		}

		public String getData(int size) {
			String returnString = getSampleString(this.startIndex, this.startIndex + size);
    		if (returnString == null)
    			this.startIndex = 0;
    		else
    			this.startIndex += size;
			return returnString;
		}

		public int getIndex() {
			return this.startIndex;
		}

		public void clear() {
			this.startIndex = 0;
		}
    }

	/*
	 * return the call samples selected in the interval 
	 */
	protected String getSampleString(int startIndex, int endIndex)
	{
		int startTime = (int) (PIPageEditor.currentPageEditor().getStartTime() * 1000.0 + 0.0005);
		int endTime   = (int) (PIPageEditor.currentPageEditor().getEndTime()   * 1000.0 + 0.0005);
		
		// check if we have returned everything
		if (startIndex > (endTime - startTime))
			return null;
		
		GfcTrace trace = this.myTrace;
		Vector samples = trace.samples;
	
		String returnString = ""; //$NON-NLS-1$
		
		if (startIndex == 0)
			returnString = Messages.getString("CallVisualiser.callHeading"); //$NON-NLS-1$

		for (int i = startTime + startIndex; i < endTime && i < startTime + endIndex; i++) {
			GfcSample sample = (GfcSample) samples.get(i);

			returnString +=   sample.sampleSynchTime + ",0x" //$NON-NLS-1$
							+ Long.toHexString(sample.linkRegister)
							+ ",\"" //$NON-NLS-1$
							+ (sample.getCallerFunctionItt() != null
								? sample.getCallerFunctionItt().getFunctionName()
								: sample.getCallerFunctionSym().getFunctionName())
							+ "\"," //$NON-NLS-1$
							+ (sample.getCallerFunctionItt() != null
								? sample.getCallerFunctionItt().getFunctionBinary().getBinaryName()
								: sample.getCallerFunctionSym().getFunctionBinary().getBinaryName())
							+ ",0x" //$NON-NLS-1$
							+ Long.toHexString(sample.programCounter)
							+ ",\"" //$NON-NLS-1$
							+ (sample.getCurrentFunctionItt() != null
								? sample.getCurrentFunctionItt().getFunctionName()
								: sample.getCurrentFunctionSym().getFunctionName())
							+ "\"," //$NON-NLS-1$
							+ (sample.getCurrentFunctionItt() != null
								? sample.getCurrentFunctionItt().getFunctionBinary().getBinaryName()
								: sample.getCurrentFunctionSym().getFunctionBinary().getBinaryName())
							+ "\n"; //$NON-NLS-1$
		}

		return returnString;
	}

	protected MenuItem getSaveSamplesItem(Menu menu, boolean enabled) {
	    MenuItem saveSamplesItem = new MenuItem(menu, SWT.PUSH);

		saveSamplesItem.setText(Messages.getString("CallVisualiser.saveAllSamplesForInterval")); //$NON-NLS-1$
		saveSamplesItem.setEnabled(enabled);
		
		if (enabled) {
			saveSamplesItem.addSelectionListener(new SelectionAdapter() { 
				public void widgetSelected(SelectionEvent e) {
					action("saveSamples"); //$NON-NLS-1$
				}
			});
		}
	
		return saveSamplesItem;
	}

	public CallVisualiser(PIPageEditor pageEditor, int pageIndex, SashForm parent, GfcTrace trace, Composite curPage)
	{
		this.pageEditor = pageEditor;
		this.pageIndex  = pageIndex;
		this.parent     = parent;
		this.myTrace    = trace;
		this.curPage = curPage;
		isPageActive = true;
		needsRefresh = false;
		
		// let the trace know about the CallVisualiser so that unit tests can find it
		trace.setCallVisualiser(this);

		// create the 3 table viewers: caller functions, selected function, callee functions
		createTableViewers(parent);
		
		createSetTimeframeJob();
		createPageListeners();
	}


	public void createTableViewers(SashForm parent)
	{
		if (parent == null)
			return;

		/*
		 * Create a SashForm with three labeled tables:
		 * 
		 * 1. Functions called by
		 * 2. Functions (one checkbox selectable at a time)
		 * 3. Functions called
		 */

		// the 3 tables will be in a sashForm
		if (parent.getOrientation() != SWT.VERTICAL)
		{
			// put a SashForm in the SashForm
			this.sashForm = new SashForm(parent, SWT.VERTICAL);
			this.sashForm.SASH_WIDTH = 5; // 5 pixel wide sash
		} else
			this.sashForm = parent;
		
		createCallerTableViewer(sashForm);
		createCurrentFunctionTableViewer(sashForm);
		createCalleeTableViewer(sashForm);

		createDefaultActions(true);
	}
	
	private void createCurrentFunctionTableViewer(SashForm sashForm)
	{
		/*
		 * Functions (one checkbox selectable at a time)
		 *
		 * 		selected checkbox
		 *		percent load
		 *  	function name
		 *		function start address
		 *		binary containing function
		 *		sample count
		 */
		Label label;
		Composite holder;
		Table table;
		TableColumn column;
		
		// middle functionTable and title
		holder = new Composite(sashForm, SWT.NONE);
		GridLayout gridLayout = new GridLayout(1, true);
		gridLayout.marginWidth = 0;
		gridLayout.marginHeight = 0;
		holder.setLayout(gridLayout);

		label = new Label(holder, SWT.CENTER | SWT.BORDER);
		label.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_YELLOW));
		label.setFont(PIPageEditor.helvetica_10);
		label.setText(Messages.getString("CallVisualiser.selectFunction")); //$NON-NLS-1$
		label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		this.currentFunctionTableViewer = new TableViewer(holder,
  				SWT.BORDER | SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION);
		this.currentFunctionTable = currentFunctionTableViewer.getTable();
		
		// add the label provider and content provider
		this.currentFunctionTableViewer.setLabelProvider(new SharedLabelProvider(this.currentFunctionTable));
		this.currentFunctionTableViewer.setContentProvider(new ArrayContentProvider());
		this.currentFunctionTableViewer.setSorter(new SharedSorter());
		
		table = this.currentFunctionTable;
		table.setRedraw(false);

		// give the table a heading for use in copying and exported
		table.setData(Messages.getString("CallVisualiser.selectedFunction")); //$NON-NLS-1$
		
		// create the other table entries when a row in this table is selected
		table.addSelectionListener(new SelectionListener() {

			public void widgetSelected(SelectionEvent event) {

				if (!(event.item instanceof TableItem))
					return;
				
	       		// set the other tables based on this selection
	       		if (!(event.item.getData() instanceof GfcFunctionItem))
	       			return;
	       		
	       		updateCallerCalleeTables((GfcFunctionItem)event.item.getData());
			}

			public void widgetDefaultSelected(SelectionEvent e) {
				widgetSelected(e);
			}});
		
		// data associated with the TableViewer will note which columns contain hex values
		// Keep this in the order in which columns have been created
		boolean[] isHex = {false, false, false, true, false, false, false};
		this.currentFunctionTable.setData("isHex", isHex); //$NON-NLS-1$

		// is called percent column
		column = new TableColumn(table, SWT.RIGHT);
		column.setText(COLUMN_HEAD_IS_CALLED);
		column.setWidth(COLUMN_WIDTH_IS_CALLED);
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLED));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		// is caller percent column
		column = new TableColumn(table, SWT.RIGHT);
		column.setText(COLUMN_HEAD_IS_CALLER);
		column.setWidth(COLUMN_WIDTH_IS_CALLER);
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLER));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		// function name column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_FUNCTION);
		column.setWidth(COLUMN_WIDTH_FUNCTION_NAME);
		column.setData(Integer.valueOf(COLUMN_ID_FUNCTION));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		// function start address column
		column = new TableColumn(table, SWT.CENTER);
		column.setText(COLUMN_HEAD_START_ADDR);
		column.setWidth(COLUMN_WIDTH_START_ADDRESS);
		column.setData(Integer.valueOf(COLUMN_ID_START_ADDR));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		// binary containing function column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_IN_BINARY);
		column.setWidth(COLUMN_WIDTH_IN_BINARY);
		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		// path to binary containing function column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_IN_BINARY_PATH);
		column.setWidth(COLUMN_WIDTH_IN_BINARY_PATH);
		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY_PATH));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		// sample count column
		column = new TableColumn(table, SWT.CENTER);
		column.setText(COLUMN_HEAD_IS_CALLED_COUNT);
		column.setWidth(COLUMN_WIDTH_IS_CALLED_COUNT);
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLED_COUNT));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		// set the default sort column to COLUMN_ID_IS_CALLED_COUNT
		currentFunctionDefaultColumn = column;

		// sample count column
		column = new TableColumn(table, SWT.CENTER);
		column.setText(COLUMN_HEAD_IS_CALLER_COUNT);
		column.setWidth(COLUMN_WIDTH_IS_CALLER_COUNT);
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLER_COUNT));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CheckboxColumnSelectionHandler());

		table.setLayoutData(new GridData(GridData.FILL_BOTH));
		table.setHeaderVisible(true);
		table.setLinesVisible(true);
		table.setRedraw(true);

		// listen for mouse clicks: to select a row, pop up a menu, etc.
		table.addMouseListener(new CurrentFunctionMouseListener());

		table.addFocusListener(new FocusListener() {
			IAction oldCopyAction = null;

			public void focusGained(org.eclipse.swt.events.FocusEvent arg0) {
				IActionBars bars = PIPageEditor.getActionBars();
				
				// modify what is executed when Copy is called from the Edit menu
				oldCopyAction = bars.getGlobalActionHandler(ActionFactory.COPY.getId());

				bars.setGlobalActionHandler(ActionFactory.COPY.getId(), copyAction);

				copyAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);
				bars.updateActionBars();
				
				// add to the Edit menu
		        IMenuManager editMenuManager = bars.getMenuManager().findMenuUsingPath(IIDEActionConstants.M_EDIT);

		        if (editMenuManager instanceof SubMenuManager)
		        {
		        	IContributionManager editManager = ((SubMenuManager)editMenuManager).getParent();
		        	ActionContributionItem item;

					editMenuManager.remove("PICopyFunction"); //$NON-NLS-1$
					functionCopyFunctionAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);
		        	item = new ActionContributionItem(functionCopyFunctionAction);
		        	item.setVisible(true);
		        	editManager.prependToGroup(IIDEActionConstants.CUT_EXT, item);

					editMenuManager.remove("PICopyTable"); //$NON-NLS-1$
					copyTableAction.setEnabled(currentFunctionTable.getItemCount() > 0);
		        	item = new ActionContributionItem(copyTableAction);
		        	item.setVisible(true);
		        	editManager.prependToGroup(IIDEActionConstants.CUT_EXT, item);
		        }
				
				// add to the File menu
		        IMenuManager fileMenuManager = bars.getMenuManager().findMenuUsingPath(IIDEActionConstants.M_FILE);

		        if (fileMenuManager instanceof SubMenuManager)
		        {
		        	IContributionManager fileManager = ((SubMenuManager)fileMenuManager).getParent();
		        	ActionContributionItem item;

					fileMenuManager.remove("PISaveTable"); //$NON-NLS-1$
					saveTableAction.setEnabled(currentFunctionTable.getItemCount() > 0);
		        	item = new ActionContributionItem(saveTableAction);
		        	item.setVisible(true);
		        	fileManager.insertAfter("saveAll", item); //$NON-NLS-1$

					fileMenuManager.remove("PISaveFunction"); //$NON-NLS-1$
					functionSaveFunctionAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);
		        	item = new ActionContributionItem(functionSaveFunctionAction);
		        	item.setVisible(true);
		        	fileManager.insertAfter("PISaveTable", item); //$NON-NLS-1$
		        }
		}

			public void focusLost(org.eclipse.swt.events.FocusEvent arg0) {
				IActionBars bars = PIPageEditor.getActionBars();
				bars.setGlobalActionHandler(ActionFactory.COPY.getId(), oldCopyAction);
				bars.updateActionBars();

				SubMenuManager editMenuManager = (SubMenuManager) PIPageEditor.getMenuManager().find(IIDEActionConstants.M_EDIT);
				editMenuManager.remove("PICopyTable"); //$NON-NLS-1$
				editMenuManager.remove("PICopyFunction"); //$NON-NLS-1$

				SubMenuManager fileMenuManager = (SubMenuManager) PIPageEditor.getMenuManager().find(IIDEActionConstants.M_FILE);
				fileMenuManager.remove("PISaveTable"); //$NON-NLS-1$
				fileMenuManager.remove("PISaveFunction"); //$NON-NLS-1$
			}
		});
	}
	
	private void createCallerTableViewer(SashForm sashForm)
	{
		/*
		 * Functions called by
		 * 
		 *		percent of calls
		 *		total percent
		 *		caller percent
		 *  	function name
		 *		function start address
		 *		binary containing function
		 *		sample count
		 */
		Label label;
		Composite holder;
		Table table;
		TableColumn column;
		
		// top functionTable and title
		holder = new Composite(sashForm, SWT.NONE);
//		holder.setLayout(new FillLayout(SWT.VERTICAL));
		GridLayout gridLayout = new GridLayout(1, true);
		gridLayout.marginWidth = 0;
		gridLayout.marginHeight = 0;
		holder.setLayout(gridLayout);

		label = new Label(holder, SWT.CENTER | SWT.BORDER);
		label.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_CYAN));
		label.setFont(PIPageEditor.helvetica_10);
		label.setText(Messages.getString("CallVisualiser.callingSelectedFunction")); //$NON-NLS-1$
		label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		this.callerTableViewer = new TableViewer(holder,
  				SWT.BORDER | SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION);
		this.callerTable = callerTableViewer.getTable();
		
		// add the label provider and content provider
		this.callerTableViewer.setLabelProvider(new SharedLabelProvider(this.callerTable));
		this.callerTableViewer.setContentProvider(new ArrayContentProvider());
		this.callerTableViewer.setSorter(new SharedSorter());

		table = this.callerTable;
		table.setRedraw(false);

		// give the table a heading for possible use in copying and exported
		table.setData(Messages.getString("CallVisualiser.callerFunctions")); //$NON-NLS-1$
		
		// data associated with the TableViewer will note which columns contain hex values
		// Keep this in the order in which columns have been created
		boolean[] isHex = {false, false, false, true, false, false, false};
		this.callerTable.setData("isHex", isHex); //$NON-NLS-1$

		// percent of calls column
		column = new TableColumn(table, SWT.RIGHT);
		column.setText(COLUMN_HEAD_CALLER_PERCENT);
		column.setWidth(COLUMN_WIDTH_CALLER_PERCENT);
		column.setData(Integer.valueOf(COLUMN_ID_CALLER_PERCENT));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledByColumnSelectionHandler());

		// sample count column
		column = new TableColumn(table, SWT.CENTER);
		column.setText(COLUMN_HEAD_IS_CALLER_COUNT2);
		column.setWidth(COLUMN_WIDTH_IS_CALLER_COUNT2);
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLER_COUNT));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledByColumnSelectionHandler());

		// function name column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_FUNCTION);
		column.setWidth(COLUMN_WIDTH_FUNCTION_NAME);
		column.setData(Integer.valueOf(COLUMN_ID_FUNCTION));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledByColumnSelectionHandler());

		// function start address column
		column = new TableColumn(table, SWT.CENTER);
		column.setText(COLUMN_HEAD_START_ADDR);
		column.setWidth(COLUMN_WIDTH_START_ADDRESS);
		column.setData(Integer.valueOf(COLUMN_ID_START_ADDR));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledByColumnSelectionHandler());

		// binary containing function column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_IN_BINARY);
		column.setWidth(COLUMN_WIDTH_IN_BINARY);
		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledByColumnSelectionHandler());

		// path to binary containing function column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_IN_BINARY_PATH);
		column.setWidth(COLUMN_WIDTH_IN_BINARY_PATH);
		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY_PATH));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledByColumnSelectionHandler());

		// is caller percent column
		column = new TableColumn(table, SWT.RIGHT);
		column.setText(COLUMN_HEAD_IS_CALLER);
		column.setWidth(COLUMN_WIDTH_IS_CALLER);
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLER));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledByColumnSelectionHandler());

		table.setLayoutData(new GridData(GridData.FILL_BOTH));
//		table.setLayout(new FillLayout());
		table.setHeaderVisible(true);
		table.setLinesVisible(true);
		table.setRedraw(true);
		
		// when a row is selected, allow the user to double-click or right click and make that function the selected function
		table.addSelectionListener(new SelectionAdapter() {

			public void widgetDefaultSelected(SelectionEvent se) {
				TableItem item = (TableItem)se.item;
				Table table = (Table)se.widget;
				CallerCalleeItem callerCalleeItem = (CallerCalleeItem)item.getData();
				
				// deselect this line
				table.deselectAll();
				
				// choose this row's function
				chooseNewFunction(callerCalleeItem);
			}
		});
		
		table.addMouseListener(new CallerMouseListener());
		
		table.addFocusListener(new FocusListener() {
			IAction oldCopyAction = null;

			public void focusGained(org.eclipse.swt.events.FocusEvent arg0) {
				IActionBars bars = PIPageEditor.getActionBars();
				
				// modify what is executed when Copy is called from the Edit menu
				oldCopyAction = bars.getGlobalActionHandler(ActionFactory.COPY.getId());

				bars.setGlobalActionHandler(ActionFactory.COPY.getId(), copyAction);

				copyAction.setEnabled(callerTable.getSelectionCount() > 0);
				bars.updateActionBars();
				
				// add to the Edit menu
		        IMenuManager editMenuManager = bars.getMenuManager().findMenuUsingPath(IIDEActionConstants.M_EDIT);

		        if (editMenuManager instanceof SubMenuManager)
		        {
		        	IContributionManager editManager = ((SubMenuManager)editMenuManager).getParent();
		        	ActionContributionItem item;

					editMenuManager.remove("PICopyTable"); //$NON-NLS-1$
					copyTableAction.setEnabled(callerTable.getItemCount() > 0);
		        	item = new ActionContributionItem(copyTableAction);
		        	item.setVisible(true);
		        	editManager.prependToGroup(IIDEActionConstants.CUT_EXT, item);
		        }
				
				// add to the File menu
		        IMenuManager fileMenuManager = bars.getMenuManager().findMenuUsingPath(IIDEActionConstants.M_FILE);

		        if (fileMenuManager instanceof SubMenuManager)
		        {
		        	IContributionManager fileManager = ((SubMenuManager)fileMenuManager).getParent();
		        	ActionContributionItem item;

					fileMenuManager.remove("PISaveTable"); //$NON-NLS-1$
					saveTableAction.setEnabled(callerTable.getItemCount() > 0);
		        	item = new ActionContributionItem(saveTableAction);
		        	item.setVisible(true);
		        	fileManager.insertAfter("saveAll", item); //$NON-NLS-1$
		        }
			}

			public void focusLost(org.eclipse.swt.events.FocusEvent arg0) {
				IActionBars bars = PIPageEditor.getActionBars();
				bars.setGlobalActionHandler(ActionFactory.COPY.getId(), oldCopyAction);
				bars.updateActionBars();

				SubMenuManager editMenuManager = (SubMenuManager) PIPageEditor.getMenuManager().find(IIDEActionConstants.M_EDIT);
				editMenuManager.remove("PICopyTable"); //$NON-NLS-1$

				SubMenuManager fileMenuManager = (SubMenuManager) PIPageEditor.getMenuManager().find(IIDEActionConstants.M_FILE);
				fileMenuManager.remove("PISaveTable"); //$NON-NLS-1$
			}
		});
	}
	
	private void createCalleeTableViewer(SashForm sashForm)
	{
		/*
		 * Functions called
		 * 
		 *		percent of calls
		 *		total percent
		 *		caller percent
		 *  	function name
		 *		function start address
		 *		binary containing function
		 *		sample count
		 */
		Label label;
		Composite holder;
		Table table;
		TableColumn column;
		
		// bottom functionTable and title
		holder = new Composite(sashForm, SWT.NONE);
//		holder.setLayout(new FillLayout(SWT.VERTICAL));
		GridLayout gridLayout = new GridLayout(1, true);
		gridLayout.marginWidth = 0;
		gridLayout.marginHeight = 0;
		holder.setLayout(gridLayout);

		label = new Label(holder, SWT.CENTER | SWT.BORDER);
		label.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_CYAN));
		label.setFont(PIPageEditor.helvetica_10);
		label.setText(Messages.getString("CallVisualiser.calledBySelectedFunction")); //$NON-NLS-1$
		label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		this.calleeTableViewer = new TableViewer(holder,
  				SWT.BORDER | SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION);
		this.calleeTable = calleeTableViewer.getTable();
		
		// add the label provider and content provider
		this.calleeTableViewer.setLabelProvider(new SharedLabelProvider(this.calleeTable));
		this.calleeTableViewer.setContentProvider(new ArrayContentProvider());
		this.calleeTableViewer.setSorter(new SharedSorter());

		table = this.calleeTable;
		table.setRedraw(false);

		// give the table a heading for possible use in copying and exported
		table.setData(Messages.getString("CallVisualiser.calleeFunctions")); //$NON-NLS-1$
		
		// data associated with the TableViewer will note which columns contain hex values
		// Keep this in the order in which columns have been created
		boolean[] isHex = {false, false, false, true, false, false, false};
		this.calleeTable.setData("isHex", isHex); //$NON-NLS-1$
		
		// percent of calls column
		column = new TableColumn(table, SWT.RIGHT);
		column.setText(COLUMN_HEAD_CALLEE_PERCENT);
		column.setWidth(COLUMN_WIDTH_CALLEE_PERCENT);
		column.setData(Integer.valueOf(COLUMN_ID_CALLEE_PERCENT));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledColumnSelectionHandler());

		// sample count column
		column = new TableColumn(table, SWT.CENTER);
		column.setText(COLUMN_HEAD_IS_CALLED_COUNT2);
		column.setWidth(COLUMN_WIDTH_IS_CALLED_COUNT2);
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLED_COUNT));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledColumnSelectionHandler());

		// function name column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_FUNCTION);
		column.setWidth(COLUMN_WIDTH_FUNCTION_NAME);
		column.setData(Integer.valueOf(COLUMN_ID_FUNCTION));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledColumnSelectionHandler());

		// function start address column
		column = new TableColumn(table, SWT.CENTER);
		column.setText(COLUMN_HEAD_START_ADDR);
		column.setWidth(COLUMN_WIDTH_START_ADDRESS);
//		column.setData(Integer.valueOf(COLUMN_ID_START_ADDR3));
		column.setData(Integer.valueOf(COLUMN_ID_START_ADDR));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledColumnSelectionHandler());

		// binary containing function column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_IN_BINARY);
		column.setWidth(COLUMN_WIDTH_IN_BINARY);
//		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY3));
		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledColumnSelectionHandler());

		// path to binary containing function column
		column = new TableColumn(table, SWT.LEFT);
		column.setText(COLUMN_HEAD_IN_BINARY_PATH);
		column.setWidth(COLUMN_WIDTH_IN_BINARY_PATH);
//		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY_PATH3));
		column.setData(Integer.valueOf(COLUMN_ID_IN_BINARY_PATH));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledColumnSelectionHandler());

		// percent of calls column
		column = new TableColumn(table, SWT.RIGHT);
		column.setText(COLUMN_HEAD_IS_CALLED);
		column.setWidth(COLUMN_WIDTH_IS_CALLED);
//		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLED3));
		column.setData(Integer.valueOf(COLUMN_ID_IS_CALLED));
		column.setMoveable(true);
		column.setResizable(true);
		column.addSelectionListener(new CalledColumnSelectionHandler());

		table.setLayoutData(new GridData(GridData.FILL_BOTH));
//		table.setLayout(new FillLayout());
		table.setHeaderVisible(true);
		table.setLinesVisible(true);
		table.setRedraw(true);
		
		// when a row is selected, allow the user to double-click or right click and make that function the selected function
		table.addSelectionListener(new SelectionAdapter() {

			public void widgetDefaultSelected(SelectionEvent se) {
				TableItem item = (TableItem)se.item;
				Table table = (Table)se.widget;
				CallerCalleeItem callerCallee = (CallerCalleeItem)item.getData();
				
				// deselect this row
				table.deselectAll();
				
				// choose this row's function
				chooseNewFunction(callerCallee);
			}
		});
		
		table.addMouseListener(new CalleeMouseListener());
		
		table.addFocusListener(new FocusListener() {
			IAction oldCopyAction = null;

			public void focusGained(org.eclipse.swt.events.FocusEvent arg0) {
				IActionBars bars = PIPageEditor.getActionBars();
				
				// modify what is executed when Copy is called from the Edit menu
				oldCopyAction = bars.getGlobalActionHandler(ActionFactory.COPY.getId());

				bars.setGlobalActionHandler(ActionFactory.COPY.getId(), copyAction);

				copyAction.setEnabled(calleeTable.getSelectionCount() > 0);
				bars.updateActionBars();
				
				// add to the Edit menu
		        IMenuManager editMenuManager = bars.getMenuManager().findMenuUsingPath(IIDEActionConstants.M_EDIT);

		        if (editMenuManager instanceof SubMenuManager)
		        {
		        	IContributionManager editManager = ((SubMenuManager)editMenuManager).getParent();
		        	ActionContributionItem item;

					editMenuManager.remove("PICopyTable"); //$NON-NLS-1$
					copyTableAction.setEnabled(calleeTable.getItemCount() > 0);
		        	item = new ActionContributionItem(copyTableAction);
		        	item.setVisible(true);
		        	editManager.prependToGroup(IIDEActionConstants.CUT_EXT, item);
		        }
				
				// add to the File menu
		        IMenuManager fileMenuManager = bars.getMenuManager().findMenuUsingPath(IIDEActionConstants.M_FILE);

		        if (fileMenuManager instanceof SubMenuManager)
		        {
		        	IContributionManager fileManager = ((SubMenuManager)fileMenuManager).getParent();
		        	ActionContributionItem item;

					fileMenuManager.remove("PISaveTable"); //$NON-NLS-1$
					saveTableAction.setEnabled(calleeTable.getItemCount() > 0);
		        	item = new ActionContributionItem(saveTableAction);
		        	item.setVisible(true);
		        	fileManager.insertAfter("saveAll", item); //$NON-NLS-1$
		        }
			}

			public void focusLost(org.eclipse.swt.events.FocusEvent arg0) {
				IActionBars bars = PIPageEditor.getActionBars();
				bars.setGlobalActionHandler(ActionFactory.COPY.getId(), oldCopyAction);
				bars.updateActionBars();

				SubMenuManager editMenuManager = (SubMenuManager) PIPageEditor.getMenuManager().find(IIDEActionConstants.M_EDIT);
				editMenuManager.remove("PICopyTable"); //$NON-NLS-1$

				SubMenuManager fileMenuManager = (SubMenuManager) PIPageEditor.getMenuManager().find(IIDEActionConstants.M_FILE);
				fileMenuManager.remove("PISaveTable"); //$NON-NLS-1$
			}
		});
	}
	
	private void chooseNewFunction(CallerCalleeItem callerCalleeItem)
	{
		// find the correct line in the function table and select it
		int count = currentFunctionTable.getItemCount();

		for (int i = 0; i < count; i++) {
			GfcFunctionItem functionItem = (GfcFunctionItem)(currentFunctionTable.getItem(i).getData());
			if (   functionItem.name.equals(callerCalleeItem.item.name)
				&& functionItem.dllName.equals(callerCalleeItem.item.dllName)) {
				currentFunctionTable.select(i);
				currentFunctionTable.showSelection();
				updateCallerCalleeTables(functionItem);
				return;
			}
		}
	}

	public void updateCallerCalleeTables(GfcFunctionItem item)
	{
		Table table;
		TableColumn sortByColumn;

   		setCallerListToFunctionsThatCallThisFunction(item);
    	setCalleeListToFunctionsThisFunctionCalls(item);
    	
    	callerTableViewer.setInput(callerList);

    	if ((callerList != null) && (callerList.length > 0)) {
    		SharedSorter sorter = ((SharedSorter) callerTableViewer.getSorter());
    		int columnID = sorter.getColumnID();
    		
    		if (columnID == -1) {
    			columnID = COLUMN_ID_CALLER_PERCENT;
    		} else {
    			sorter.setSortAscending(!sorter.getSortAscending());
    		}
			sorter.doSort(columnID);
 	
	    	sortByColumn = null;
	    	table = callerTableViewer.getTable();
			for (int i = 0; i < table.getColumnCount(); i++) {
				if (table.getColumn(i).getData() instanceof Integer) {
					if (((Integer)table.getColumn(i).getData()) == columnID) {
						sortByColumn = table.getColumn(i);
						break;
					}
				}
			}
	
			if (sortByColumn != null) {
				table.setSortColumn(sortByColumn);
				table.setSortDirection(sorter.getSortAscending() ? SWT.UP : SWT.DOWN);
			}
	    	callerTableViewer.refresh();
    	} else {
    		callerTableViewer.getTable().setSortColumn(null);
    	}

    	calleeTableViewer.setInput(calleeList);

    	if ((calleeList != null) && (calleeList.length > 0)) {
    		SharedSorter sorter = ((SharedSorter) calleeTableViewer.getSorter());
    		int columnID = sorter.getColumnID();
    		
    		if (columnID == -1) {
    			columnID = COLUMN_ID_CALLEE_PERCENT;
    		} else {
    			sorter.setSortAscending(!sorter.getSortAscending());
    		}
			sorter.doSort(columnID);
	
	    	sortByColumn = null;
	    	table = calleeTableViewer.getTable();
			for (int i = 0; i < table.getColumnCount(); i++) {
				if (table.getColumn(i).getData() instanceof Integer) {
					if (((Integer)table.getColumn(i).getData()) == columnID) {
						sortByColumn = table.getColumn(i);
						break;
					}
				}
			}
	
			if (sortByColumn != null) {
				table.setSortColumn(sortByColumn);
				table.setSortDirection(sorter.getSortAscending() ? SWT.UP : SWT.DOWN);
			}
			calleeTableViewer.refresh();
    	} else {
    		calleeTableViewer.getTable().setSortColumn(null);
    	}
	}
	
	private class CallerMouseListener implements MouseListener
	{
		public void mouseDoubleClick(org.eclipse.swt.events.MouseEvent me) {}

		public void mouseDown(org.eclipse.swt.events.MouseEvent me) {}

		public void mouseUp(org.eclipse.swt.events.MouseEvent me) {
			copyAction.setEnabled(callerTable.getSelectionCount() > 0);
			copyTableAction.setEnabled(callerTable.getItemCount() > 0);

			// only look for button 3 (right click)
			if (me.button != MouseEvent.BUTTON3)
				return;

			// make the caller table the menu table
			currentMenuTable = callerTable;

			if (callerMenu != null) {
				callerMenu.dispose();
			}

			callerMenu = new Menu(callerTable.getShell(), SWT.POP_UP); 
			
			MenuItem showCallInfoItem = new MenuItem(callerMenu, SWT.PUSH);
			showCallInfoItem.setText(Messages.getString("CallVisualiser.showCallInfo")); //$NON-NLS-1$
			showCallInfoItem.addSelectionListener(new SelectionAdapter() { 
				public void widgetSelected(SelectionEvent se) {
				    // based on the table's selected item, update the tables
					TableItem[] selections = callerTable.getSelection();
					callerTable.deselectAll();
					
					if (selections.length != 0)
						chooseNewFunction((CallerCalleeItem)selections[0].getData());
				}
			});
			
			new MenuItem(callerMenu, SWT.SEPARATOR);
			
			MenuItem sourceLookupItem = new MenuItem(callerMenu, SWT.PUSH);
			sourceLookupItem.setText(Messages.getString("CallVisualiser.sourcelookup"));	//$NON-NLS-1$
			sourceLookupItem.addSelectionListener(new SelectionListener() {

				public void widgetDefaultSelected(SelectionEvent arg0) {
				}

				public void widgetSelected(SelectionEvent arg0) {
					doSourceLookup(callerTable);
				}
				
			});

			if (callerTable.getSelectionCount() == 0) {
				showCallInfoItem.setEnabled(false);
				sourceLookupItem.setEnabled(false);
			}

			// add copy, copy all, and save all
			new MenuItem(callerMenu, SWT.SEPARATOR);
			getCopyItem(callerMenu, callerTable.getSelectionCount() > 0);
			getCopyTableItem(callerMenu, callerTable.getItemCount() > 0);
			copyAction.setEnabled(callerTable.getSelectionCount() > 0);
			copyTableAction.setEnabled(callerTable.getItemCount() > 0);

			new MenuItem(callerMenu, SWT.SEPARATOR);
			getSaveTableItem(callerMenu, callerTable.getItemCount() > 0);
			saveTableAction.setEnabled(callerTable.getItemCount() > 0);
			
			// save samples
			int startTime = (int) (PIPageEditor.currentPageEditor().getStartTime() * 1000.0f);
			int endTime   = (int) (PIPageEditor.currentPageEditor().getEndTime()   * 1000.0f);

			if ((startTime == -1) || (endTime   == -1) || (startTime == endTime))
				getSaveSamplesItem(callerMenu, false); //$NON-NLS-1$
			else
				getSaveSamplesItem(callerMenu, true); //$NON-NLS-1$
			
			callerMenu.setLocation(callerTable.getParent().toDisplay(me.x, me.y));
			callerMenu.setVisible(true);
			callerTable.setMenu(callerMenu);
		}
	}
	
	private class CurrentFunctionMouseListener implements MouseListener
	{
		public void mouseDoubleClick(org.eclipse.swt.events.MouseEvent me) {}

		public void mouseDown(org.eclipse.swt.events.MouseEvent me) {}

		public void mouseUp(org.eclipse.swt.events.MouseEvent me) {
			copyAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);
			copyTableAction.setEnabled(currentFunctionTable.getItemCount() > 0);
			functionCopyFunctionAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);

			// only look for button 3 (right click)
			if (me.button != MouseEvent.BUTTON3)
				return;

			// make the current function table the menu table
			currentMenuTable = currentFunctionTable;

			if (currentFunctionMenu != null) {
				currentFunctionMenu.dispose();
			}

			currentFunctionMenu = new Menu(currentFunctionTable.getShell(), SWT.POP_UP); 
			
//			new MenuItem(callerMenu, SWT.SEPARATOR);
			
			MenuItem sourceLookupItem = new MenuItem(currentFunctionMenu, SWT.PUSH);
			sourceLookupItem.setText(Messages.getString("CallVisualiser.sourcelookup"));	//$NON-NLS-1$
			sourceLookupItem.addSelectionListener(new SelectionListener() {

				public void widgetDefaultSelected(SelectionEvent arg0) {
				}

				public void widgetSelected(SelectionEvent arg0) {
					doSourceLookup(currentFunctionTable);
				}
				
			});
			
			if (currentFunctionTable.getSelectionCount() == 0) {
				sourceLookupItem.setEnabled(false);
			}

			// add copy, copy all, copy caller/callee info, save all, save caller/callee info
			new MenuItem(currentFunctionMenu, SWT.SEPARATOR);
			getCopyItem(currentFunctionMenu, currentFunctionTable.getSelectionCount() > 0);
			getCopyTableItem(currentFunctionMenu, currentFunctionTable.getItemCount() > 0);
			getCopyFunctionItem(currentFunctionMenu, currentFunctionTable.getSelectionCount() > 0);
			copyAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);
			copyTableAction.setEnabled(currentFunctionTable.getItemCount() > 0);
			functionCopyFunctionAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);

			new MenuItem(currentFunctionMenu, SWT.SEPARATOR);
			getSaveTableItem(currentFunctionMenu, currentFunctionTable.getItemCount() > 0);
			getSaveFunctionItem(currentFunctionMenu, currentFunctionTable.getSelectionCount() > 0);
			saveTableAction.setEnabled(currentFunctionTable.getItemCount() > 0);
			functionSaveFunctionAction.setEnabled(currentFunctionTable.getSelectionCount() > 0);
			
			// save samples
			int startTime = (int) (PIPageEditor.currentPageEditor().getStartTime() * 1000.0f);
			int endTime   = (int) (PIPageEditor.currentPageEditor().getEndTime()   * 1000.0f);

			if ((startTime == -1) || (endTime   == -1) || (startTime == endTime))
				getSaveSamplesItem(currentFunctionMenu, false); //$NON-NLS-1$
			else
				getSaveSamplesItem(currentFunctionMenu, true); //$NON-NLS-1$

			currentFunctionMenu.setLocation(currentFunctionTable.getParent().toDisplay(me.x, me.y));
			currentFunctionMenu.setVisible(true);
			currentFunctionTable.setMenu(currentFunctionMenu);
		}
	}

	private class CalleeMouseListener implements MouseListener
	{
		public void mouseDoubleClick(org.eclipse.swt.events.MouseEvent me) {}

		public void mouseDown(org.eclipse.swt.events.MouseEvent me) {}

		public void mouseUp(org.eclipse.swt.events.MouseEvent me) {
			copyAction.setEnabled(calleeTable.getSelectionCount() > 0);
			copyTableAction.setEnabled(calleeTable.getItemCount() > 0);

			// only look for button 3 (right click)
			if (me.button != MouseEvent.BUTTON3)
				return;

			// make the callee table the menu table
			currentMenuTable = calleeTable;

			if (calleeMenu != null) {
				calleeMenu.dispose();
			}
			
			calleeMenu = new Menu(calleeTable.getShell(), SWT.POP_UP); 
			
			MenuItem showCallInfoItem = new MenuItem(calleeMenu, SWT.PUSH);
			showCallInfoItem.setText(Messages.getString("CallVisualiser.showCallInfo")); //$NON-NLS-1$
			showCallInfoItem.addSelectionListener(new SelectionAdapter() { 
				public void widgetSelected(SelectionEvent se) {
				    // based on the table's selected item, update the tables
					TableItem[] selections = calleeTable.getSelection();
					calleeTable.deselectAll();
					
					if (selections.length != 0)
						chooseNewFunction((CallerCalleeItem)selections[0].getData());
				}
			});
			
			new MenuItem(calleeMenu, SWT.SEPARATOR);
			
			MenuItem sourceLookupItem = new MenuItem(calleeMenu, SWT.PUSH);
			sourceLookupItem.setText(Messages.getString("CallVisualiser.sourcelookup"));	//$NON-NLS-1$
			sourceLookupItem.addSelectionListener(new SelectionListener() {

				public void widgetDefaultSelected(SelectionEvent arg0) {
				}

				public void widgetSelected(SelectionEvent arg0) {
					doSourceLookup(calleeTable);
				}
				
			});
			
			if (calleeTable.getSelectionCount() == 0) {
				showCallInfoItem.setEnabled(false);
				sourceLookupItem.setEnabled(false);
			}

			// add copy, copy all, and save all
			new MenuItem(calleeMenu, SWT.SEPARATOR);
			getCopyItem(calleeMenu, calleeTable.getSelectionCount() > 0);
			getCopyTableItem(calleeMenu, calleeTable.getItemCount() > 0);
			copyAction.setEnabled(calleeTable.getSelectionCount() > 0);
			copyTableAction.setEnabled(calleeTable.getItemCount() > 0);

			new MenuItem(calleeMenu, SWT.SEPARATOR);
			getSaveTableItem(calleeMenu, calleeTable.getItemCount() > 0);
			saveTableAction.setEnabled(calleeTable.getItemCount() > 0);
			
			// save samples
			int startTime = (int) (PIPageEditor.currentPageEditor().getStartTime() * 1000.0f);
			int endTime   = (int) (PIPageEditor.currentPageEditor().getEndTime()   * 1000.0f);

			if ((startTime == -1) || (endTime   == -1) || (startTime == endTime))
				getSaveSamplesItem(calleeMenu, false); //$NON-NLS-1$
			else
				getSaveSamplesItem(calleeMenu, true); //$NON-NLS-1$

			calleeMenu.setLocation(calleeTable.getParent().toDisplay(me.x, me.y));
			calleeMenu.setVisible(true);
			calleeTable.setMenu(calleeMenu);
		}
	}
	
	protected void createDefaultActions(boolean copyFunction)
	{
		copyAction = new Action(Messages.getString("CallVisualiser.0")) { //$NON-NLS-1$
			public void run() {
				action("copy"); //$NON-NLS-1$
			}
		};
		copyAction.setEnabled(false);

		copyTableAction = new Action(Messages.getString("CallVisualiser.1")) { //$NON-NLS-1$
			public void run() {
				action("copyTable"); //$NON-NLS-1$
			}
		};
		copyTableAction.setEnabled(true);
		copyTableAction.setId("PICopyTable"); //$NON-NLS-1$
		copyTableAction.setText(Messages.getString("CallVisualiser.CopyTable")); //$NON-NLS-1$

		saveTableAction = new Action(Messages.getString("CallVisualiser.2")) { //$NON-NLS-1$
			public void run() {
				action("saveTable"); //$NON-NLS-1$
			}
		};
		saveTableAction.setEnabled(true);
		saveTableAction.setId("PISaveTable"); //$NON-NLS-1$
		saveTableAction.setText(Messages.getString("CallVisualiser.SaveTable")); //$NON-NLS-1$

		functionCopyFunctionAction = new Action(Messages.getString("CallVisualiser.3")) { //$NON-NLS-1$
			public void run() {
				action("copyFunction"); //$NON-NLS-1$
			}
		};
		functionCopyFunctionAction.setEnabled(true);
		functionCopyFunctionAction.setId("PICopyFunction"); //$NON-NLS-1$
		functionCopyFunctionAction.setText(Messages.getString("CallVisualiser.CopyDataForFunction")); //$NON-NLS-1$

		functionSaveFunctionAction = new Action(Messages.getString("CallVisualiser.4")) { //$NON-NLS-1$
			public void run() {
				action("saveFunction"); //$NON-NLS-1$
			}
		};
		functionSaveFunctionAction.setEnabled(true);
		functionSaveFunctionAction.setId("PISaveFunction"); //$NON-NLS-1$
		functionSaveFunctionAction.setText(Messages.getString("CallVisualiser.SaveDataForFunction")); //$NON-NLS-1$
	}

	private class SharedLabelProvider extends LabelProvider implements ITableLabelProvider {
		
		Table table;

		public SharedLabelProvider(Table table) {
			super();
			this.table = table;
		}

		public String getColumnText(Object element, int columnIndex) {
			if (   !(element instanceof GfcFunctionItem)
				&& !(element instanceof CallerCalleeItem))
				return ""; //$NON-NLS-1$

			int columnId = ((Integer) this.table.getColumn(columnIndex).getData()).intValue();
			
			GfcFunctionItem item = null;
			
			if (element instanceof GfcFunctionItem)
				item = (GfcFunctionItem)element;
			else
				item = ((CallerCalleeItem)element).item;

			switch (columnId)
			{
				case COLUMN_ID_CALLEE_PERCENT:
				case COLUMN_ID_CALLER_PERCENT:
				{
					double percent;
					if (element instanceof CallerCalleeItem)
						percent = ((CallerCalleeItem)element).percent;
					else
						percent = 0.0;
						
					// Percent load string
					return (new DecimalFormat(Messages.getString("CallVisualiser.shortDecimalFormat"))).format(percent); //$NON-NLS-1$
				}
				case COLUMN_ID_FUNCTION:
				{
					// Function
					return item.name;
				}
				case COLUMN_ID_START_ADDR:
				{
					// Function start
					return Long.toHexString(item.address);
				}
				case COLUMN_ID_IN_BINARY:
				{
					// Binary
					String binary = item.dllName;
					int index = binary.lastIndexOf('\\');
					if (index == -1)
						return binary;
					else
						return binary.substring(index + 1);
				}
				case COLUMN_ID_IN_BINARY_PATH:
				{
					// Path
					String binary = item.dllName;
					int index = binary.lastIndexOf('\\');
					if (index == -1)
						return ""; //$NON-NLS-1$
					else
						return binary.substring(0, index);
				}
				case COLUMN_ID_IS_CALLED:
				{
					// Percent load string
					return (new DecimalFormat(Messages.getString("CallVisualiser.decimalFormat"))).format(myTrace.getAbsoluteTraditionalPercentageFor(item)); //$NON-NLS-1$
				}
				case COLUMN_ID_IS_CALLER:
				{
					// Percent load string
					return (new DecimalFormat(Messages.getString("CallVisualiser.decimalFormat"))).format(myTrace.getAbsoluteCallerPercentageFor(item)); //$NON-NLS-1$
				}
				case COLUMN_ID_RECURSIVE_CALL:
				{
					// Percent load string
					return (new DecimalFormat(Messages.getString("CallVisualiser.decimalFormat"))).format(myTrace.getRecursiveCallerPrecentageFor(item)); //$NON-NLS-1$
				}
				case COLUMN_ID_IS_CALLED_COUNT:
				{
					// Sample count
					int samples;
					if (element instanceof CallerCalleeItem)
						samples = ((CallerCalleeItem)element).samples;
					else
						samples = item.isCalledCount();
					return String.valueOf(samples);
				}
				case COLUMN_ID_IS_CALLER_COUNT:
				{
					// Sample count
					int samples;
					if (element instanceof CallerCalleeItem)
						samples = ((CallerCalleeItem)element).samples;
					else
						samples = item.isCallerCount();
					return String.valueOf(samples);
				}
				default:
				{
					break;
				}
			}
			// should never get here
			return ""; //$NON-NLS-1$
		}

		public Image getColumnImage(Object element, int columnIndex) {
			return null;
		}
	}

	public void selectFunction(String functionName)
	{
	}
  
	public void setStartAndEnd(int start, int end)
	{
		if (this.myTrace == null)
			return;
		
		this.curStart = start;
		this.curEnd = end;
		needsRefresh = true;

		if (isPageActive){
			setTimeframeJob.cancel();
			setTimeframeJob.schedule();
		}
	}

	private static class CallerCalleeItem {
	    GfcFunctionItem item;
	    double percent;
	    int samples;
	}
	
	public void setCallerListToFunctionsThatCallThisFunction(GfcFunctionItem function)
	{
		if (function == null) {
			this.callerList = null;
			return;
		}

		GfcFunctionItem[] list = function.getCallerList();
	    Double[] perc = function.getCallerPercentages();
	
	    this.callerList = new CallerCalleeItem[list.length];
	    
	    for (int i = 0; i < list.length; i++)
	    {
	    	this.callerList[i] = new CallerCalleeItem();
	    	this.callerList[i].item    = list[i];
	    	this.callerList[i].percent = perc[i];
	    	this.callerList[i].samples = (int) (perc[i] * function.isCalledCount() + 0.5) / 100;
	    }
	}

	public void setCalleeListToFunctionsThisFunctionCalls(GfcFunctionItem function)
	{
		if (function == null) {
			this.calleeList = null;
			return;
		}

	    GfcFunctionItem[] list = function.getCalleeList();
	    Double[] perc = function.getCalleePercentages();
	
	    this.calleeList = new CallerCalleeItem[list.length];
	    
	    for (int i = 0; i < list.length; i++)
	    {
	    	this.calleeList[i] = new CallerCalleeItem();
	    	this.calleeList[i].item    = list[i];
	    	this.calleeList[i].percent = perc[i];
	    	this.calleeList[i].samples = (int) (perc[i] * function.isCallerCount() + 0.5) / 100;
	    }
	}

	public void action(String actionString) {
		if (actionString.equals("copy")) //$NON-NLS-1$
	    {
	    	actionCopyOrSave(true, currentMenuTable, CHECKBOX_NONE, false, "\t", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
	        return;
	    }
	    else if (actionString.equals("copyTable")) //$NON-NLS-1$
	    {
	    	actionCopyOrSave(true, currentMenuTable, CHECKBOX_NONE, true, "\t", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
	        return;
	    }
	    else if (actionString.equals("copyFunction")) //$NON-NLS-1$
	    {
	    	actionCopyOrSaveFunction(true, "\t"); //$NON-NLS-1$
	        return;
	    }
	    else if (actionString.equals("saveTable")) //$NON-NLS-1$
	    {
	    	actionCopyOrSave(false, currentMenuTable, CHECKBOX_NONE, true, ",", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
	        return;
	    }
	    else if (actionString.equals("saveFunction")) //$NON-NLS-1$
	    {
	    	actionCopyOrSaveFunction(false, ","); //$NON-NLS-1$
	        return;
	    }
	    else if (actionString.equals("saveSamples")) //$NON-NLS-1$
	    {
	    	SaveSampleString saveSampleString = new SaveSampleString();
	    	actionSaveSamples(saveSampleString); //$NON-NLS-1$
	        return;
	    }
	    else if (actionString.equals("saveTableTest")) //$NON-NLS-1$
	    {
			// copy save file contents to the clipboard for easy viewing
	        Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
			SaveTableString getString = new SaveTableString(currentMenuTable, CHECKBOX_NONE, ",", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
	        String copyString = getString.getData();
			StringSelection contents = new StringSelection(copyString);
	        cb.setContents(contents, contents);
	        return;
	    }
	    else if (actionString.equals("saveFunctionTest")) //$NON-NLS-1$
	    {
			// copy save file contents to the clipboard for easy viewing
	        Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
	        SaveFunctionString getString = new SaveFunctionString(","); //$NON-NLS-1$
	        String copyString = getString.getData();
			StringSelection contents = new StringSelection(copyString);
	        cb.setContents(contents, contents);
	        return;
	    }
	}
	
	/*
	 * TableViewer sorter for the called-by and called function tableviewers
	 */
	private class SharedSorter extends ViewerSorter {
		// sort direction
		private boolean sortAscending;
		
		// last column sorted
		private int column = -1;
		
		/* 
		 * decide on which column to sort by, and the sort ordering
		 */
		public void doSort(int column) {
			// ignore the column passed in and use the id set by the column selection handler
			if (column == this.column) {
				// sort in other order
				sortAscending = !sortAscending;
			} else {
				switch (column) {
					case COLUMN_ID_FUNCTION:
					case COLUMN_ID_START_ADDR:
					case COLUMN_ID_IN_BINARY:
					case COLUMN_ID_IN_BINARY_PATH:
					{
		            	// sort in ascending order
		            	sortAscending = true;
		                break;
					}
					case COLUMN_ID_IS_CALLED:
					case COLUMN_ID_IS_CALLER:
					case COLUMN_ID_RECURSIVE_CALL:
					case COLUMN_ID_CALLER_PERCENT:
					case COLUMN_ID_CALLEE_PERCENT:
					case COLUMN_ID_IS_CALLED_COUNT:
					case COLUMN_ID_IS_CALLER_COUNT:
					{
		            	// sort in descending order
		            	sortAscending = false;
		                break;
					}
					default:
					{
						// ignore the column
						return;
					}
				}
				this.column = column;
			}
		}
		
		/*
		 * compare two items from a table column
		 */
		public int compare(Viewer viewer, Object e1, Object e2) {
			int comparison = 0;
			
			GfcFunctionItem item1 = null;
			GfcFunctionItem item2 = null;
			CallerCalleeItem ccItem1 = null;
			CallerCalleeItem ccItem2 = null;
			
			if (e1 instanceof GfcFunctionItem) {
				item1 = (GfcFunctionItem) e1;
				item2 = (GfcFunctionItem) e2;
			} else {
				ccItem1 = (CallerCalleeItem)e1;
				ccItem2 = (CallerCalleeItem)e2;
				item1 =   ccItem1.item;
				item2 =   ccItem2.item;
			}

			switch (column) {
				case COLUMN_ID_CALLER_PERCENT:
				case COLUMN_ID_CALLEE_PERCENT:
				{
					double percent1;
					double percent2;
					if (e1 instanceof GfcFunctionItem) {
						percent1 = 0.0;
						percent2 = 0.0;
					} else {
						percent1 = ccItem1.percent;
						percent2 = ccItem2.percent;
					}
					comparison = percent1 > percent2 ? 1 : -1;
					break;
				}
				case COLUMN_ID_FUNCTION:
				{
					comparison = this.getComparator().compare(item1.name, item2.name);
					break;
				}
				case COLUMN_ID_START_ADDR:
				{
					comparison = (item1.address > item2.address) ? 1 : -1; 
					break;
				}
				case COLUMN_ID_IN_BINARY:
				{
					int index;
					String name1 = item1.dllName;
					index = name1.lastIndexOf('\\');
					if (index != -1)
						name1 = name1.substring(index);

					String name2 = item2.dllName;
					index = name2.lastIndexOf('\\');
					if (index != -1)
						name2 = name2.substring(index);

					comparison = this.getComparator().compare(name1, name2);
					break;
				}
				case COLUMN_ID_IN_BINARY_PATH:
				{
					int index;
					String name1 = item1.dllName;
					index = name1.lastIndexOf('\\');
					if (index == -1)
						name1 = ""; //$NON-NLS-1$
					else
						name1 = name1.substring(0, index);

					String name2 = item2.dllName;
					index = name2.lastIndexOf('\\');
					if (index == -1)
						name2 = ""; //$NON-NLS-1$
					else
						name2 = name2.substring(0, index);

					comparison = this.getComparator().compare(name1, name2);
					break;
				}
				case COLUMN_ID_IS_CALLED:
				{
					// actual sample count used as a proxy for the percentage
					comparison = item1.isCalledCount() - item2.isCalledCount();
					break;
				}
				case COLUMN_ID_IS_CALLER:
				{
					// actual sample count used as a proxy for the percentage
					comparison = item1.isCallerCount() - item2.isCallerCount();
					break;
				}
				case COLUMN_ID_RECURSIVE_CALL:
				{
					comparison = myTrace.getRecursiveCallerPrecentageFor(item1) >
								 myTrace.getRecursiveCallerPrecentageFor(item2) ? 1 : -1;
					break;
				}
				case COLUMN_ID_IS_CALLED_COUNT:
				{
					if (e1 instanceof GfcFunctionItem) {
						comparison = item1.isCalledCount() - item2.isCalledCount();
					} else {
						comparison = ccItem1.samples - ccItem2.samples;
					}
					break;
				}
				case COLUMN_ID_IS_CALLER_COUNT:
				{
					if (e1 instanceof GfcFunctionItem) {
						comparison = item1.isCallerCount() - item2.isCallerCount();
					} else {
						comparison = ccItem1.samples - ccItem2.samples;
					}
					break;
				}
				default:
				{
					break;
				}
			}

			// for descending order, reverse the sense of the compare
			if (!sortAscending)
				comparison = -comparison;
			return comparison;
		}
		
		public boolean getSortAscending() {
			return sortAscending;
		}
		
		public void setSortAscending(boolean sortAscending) {
			this.sortAscending = sortAscending;
		}
		
		public int getColumnID() {
			return column;
		}
	}
	
	public void sortAndRefresh(TableViewer tableViewer, TableColumn tableColumn)
	{
		Table table = tableViewer.getTable();
    	int columnID = ((Integer) tableColumn.getData()).intValue();
    	
    	if (table.getItemCount() == 0)
    		return;
		
		// sort by selected columnID
    	((SharedSorter) tableViewer.getSorter()).doSort(columnID);
		table.setSortColumn(tableColumn);
		table.setSortDirection(((SharedSorter) tableViewer.getSorter()).getSortAscending() ? SWT.UP : SWT.DOWN);
		tableViewer.refresh();
	}

	private class CalledByColumnSelectionHandler extends SelectionAdapter
	{
		public void widgetSelected(SelectionEvent e)
        {
        	if (!(e.widget instanceof TableColumn))
        		return;
        	
        	sortAndRefresh(callerTableViewer, (TableColumn) e.widget);
       }
	}

	private class CalledColumnSelectionHandler extends SelectionAdapter
	{
		public void widgetSelected(SelectionEvent e)
        {
        	if (!(e.widget instanceof TableColumn))
        		return;

        	sortAndRefresh(calleeTableViewer, (TableColumn) e.widget);
        }
	}

	private class CheckboxColumnSelectionHandler extends SelectionAdapter
	{
		public void widgetSelected(SelectionEvent e)
        {
        	if (!(e.widget instanceof TableColumn))
        		return;

        	sortAndRefresh(currentFunctionTableViewer, (TableColumn) e.widget);
        }
	}
	
	public PIPageEditor getPageEditor()
	{
		return this.pageEditor;
	}
	
	public int getPageIndex()
	{
		return this.pageIndex;
	}
	
	private MenuItem getCopyFunctionItem(Menu menu, boolean enabled) {
	    MenuItem copyFunctionItem = new MenuItem(menu, SWT.PUSH);
	    
		copyFunctionItem.setText(Messages.getString("CallVisualiser.CopyDataForFunction")); //$NON-NLS-1$
		copyFunctionItem.setEnabled(enabled);
		
		if (enabled) {
			copyFunctionItem.addSelectionListener(new SelectionAdapter() { 
				public void widgetSelected(SelectionEvent e) {
				    action("copyFunction"); //$NON-NLS-1$
				}
			});
		}
		
		return copyFunctionItem;
	}
	
	private MenuItem getSaveFunctionItem(Menu menu, boolean enabled) {
	    MenuItem saveFunctionItem = new MenuItem(menu, SWT.PUSH);
	    
		saveFunctionItem.setText(Messages.getString("CallVisualiser.SaveDataForFunction")); //$NON-NLS-1$
		saveFunctionItem.setEnabled(enabled);
		
		if (enabled) {
			saveFunctionItem.addSelectionListener(new SelectionAdapter() { 
				public void widgetSelected(SelectionEvent e) {
				    action("saveFunction"); //$NON-NLS-1$
				}
			});
		}
		
		return saveFunctionItem;
	}

	private void doSourceLookup(Table mytable) {
		// source look up for selected items
		if ((mytable.getItemCount() == 0) || (mytable.getSelectionCount() == 0))
			return;

		TableItem selectedItem = mytable.getSelection()[0];
		Object selectedData = selectedItem.getData();
		String functionName = null;
		String binaryName = null;
		if (selectedData instanceof GfcFunctionItem) {
			GfcFunctionItem functionItem = (GfcFunctionItem) selectedData;
			functionName = functionItem.name;
			binaryName = functionItem.dllName;
		} else if (selectedData instanceof CallerCalleeItem) {
			CallerCalleeItem callerCalleeItem = (CallerCalleeItem) selectedData;
			functionName = callerCalleeItem.item.name;
			binaryName = callerCalleeItem.item.dllName;
		}
		if (functionName != null && binaryName != null)
		{
			SourceLookup.getInstance().lookupAndopenEditorWithHighlight(functionName , binaryName);
		}

	}
	
	// class to pass the function caller/callee data to the save wizard
	private class SaveFunctionString implements ISaveTable {
		private String separator;
		
		public SaveFunctionString(String separator) {
			this.separator = separator;
		}

		public String getData() {
			return copyFunction(separator);
		}
	}
	
    private void actionCopyOrSaveFunction(boolean doCopy, String separator)
    {
    	// one function must be selected
    	if (currentFunctionTable.getSelectionCount() != 1)
    		return;
 
		// copy one function's caller and callee data to the clipboard or save to a file
		if (doCopy) {
			// change the clipboard contents
	        Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
			String copyString = copyFunction(separator);
			StringSelection contents = new StringSelection(copyString);
	        cb.setContents(contents, contents);
	    } else {
			SaveFunctionString getString = new SaveFunctionString(separator);
			WizardDialog dialog;
			Wizard w = new SaveTableWizard(getString);
			dialog = new WizardDialog(PlatformUI.getWorkbench().getDisplay().getActiveShell(), w);
	    	dialog.open();
	    }
    }

    private String copyFunction(String separator)
	{	
    	// one function must be selected
    	if (currentFunctionTable.getSelectionCount() != 1)
			return ""; //$NON-NLS-1$
		
		String copyString = ""; //$NON-NLS-1$

		// create the multiple table heading line (e.g., "Selected Function     Callers")
		// space them out based on how many columns are in their table
		if (currentFunctionTable.getData() instanceof String) {
			copyString += (String) (currentFunctionTable.getData());
		}
		for (int j = 0; j < currentFunctionTable.getColumnCount(); j++) {
			copyString += separator;
		}

		if (callerTable.getData() instanceof String) {
			copyString += (String) (callerTable.getData());
		}
		for (int j = 0; j < callerTable.getColumnCount(); j++) {
			copyString += separator;
		}

		if (calleeTable.getData() instanceof String) {
			copyString += (String) (calleeTable.getData());
		}
		for (int j = 0; j < calleeTable.getColumnCount(); j++) {
			copyString += separator;
		}

		copyString += "\n"; //$NON-NLS-1$
		
		// create the multiple table column headings
		copyString += copyHeading(currentFunctionTable, CHECKBOX_NONE, separator, separator); //$NON-NLS-1$
		copyString += copyHeading(callerTable, CHECKBOX_NONE, separator, separator); //$NON-NLS-1$
		copyString += copyHeading(calleeTable, CHECKBOX_NONE, separator, "\n"); //$NON-NLS-1$
		
		// determine the row, column count, and column ordering in each table
		// NOTE: the first table in the copy will contain the function, and it only its
		// one selected line will be shown
		int rowCount0      = 1;
		int columnCount0   = currentFunctionTable.getColumnCount();
		int[] columnOrder0 = currentFunctionTable.getColumnOrder();
		boolean[] isHex0   = (boolean[]) currentFunctionTable.getData("isHex"); //$NON-NLS-1$
		String emptyRow0 = ""; //$NON-NLS-1$

		int rowCount1      = callerTable.getItemCount();
		int columnCount1   = callerTable.getColumnCount();
		int[] columnOrder1 = callerTable.getColumnOrder();
		boolean[] isHex1   = (boolean[]) callerTable.getData("isHex"); //$NON-NLS-1$
		String emptyRow1 = ""; //$NON-NLS-1$

		int rowCount2      = calleeTable.getItemCount();
		int columnCount2   = calleeTable.getColumnCount();
		int[] columnOrder2 = calleeTable.getColumnOrder();
		boolean[] isHex2   = (boolean[]) calleeTable.getData("isHex"); //$NON-NLS-1$
		String emptyRow2 = ""; //$NON-NLS-1$

		// determine the number of multiple table rows (max of any table's rows) 
		int rowCount = rowCount0 >= rowCount1 ? rowCount0 : rowCount1;
		rowCount = rowCount2 > rowCount ? rowCount2 : rowCount;
		
		// generate empty row strings, to speed things up
		if (rowCount0 < rowCount) {
			for (int j = 0; j < columnCount0 - 1; j++) {
				emptyRow0 += separator;
			}
		}

		if (rowCount1 < rowCount) {
			for (int j = 0; j < columnCount1; j++) {
				emptyRow1 += separator;
			}
		}

		if (rowCount2 < rowCount) {
			for (int j = 0; j < columnCount2; j++) {
				emptyRow2 += separator;
			}
		}

		// generate the rows
		for (int i = 0; i < rowCount; i++) {
			if (i < 1) {
				copyString += copyRow(isHex0, currentFunctionTable.getItem(currentFunctionTable.getSelectionIndex()),
										CHECKBOX_NONE, columnCount0, columnOrder0, separator);
			} else {
				copyString += emptyRow0;
			}
			
			if (i < rowCount1) {
				copyString += separator + copyRow(isHex1, callerTable.getItem(i), CHECKBOX_NONE, columnCount1, columnOrder1, separator);
			} else {
				// NOTE: if this is the last table, or the 3rd table has nothing but empty
				// rows left, we may not need to fill in these fields
				copyString += emptyRow1;
			}
			
			if (i < rowCount2) {
				copyString += separator + copyRow(isHex2, calleeTable.getItem(i), CHECKBOX_NONE, columnCount2, columnOrder2, separator);
			} else {
				// NOTE: we may not need to fill in the empty fields of the last table
				copyString += emptyRow2;
			}
			
			copyString += "\n"; //$NON-NLS-1$
		}
		
		return copyString;
	}

	private void createSetTimeframeJob() {
		//this functionality used to be in the setStartEnd() method
		//but now the long-running calculations are in a background job
		//and the short-running refresh of table viewers is done in the UI thread
		
		setTimeframeJob = new Job("Updating function hierarchy..."){
			@Override
			protected IStatus run(IProgressMonitor monitor) {
				//run the time consuming calculations in the background
				if (myTrace == null){
					return Status.CANCEL_STATUS;
				}				
				
				needsRefresh = false;
			    myTrace.parseEntries(curStart, curEnd);			    
			    functionArray = myTrace.getEntriesSorted(GfcTrace.SORT_BY_TOTAL_LOAD);
			    
			    return Status.OK_STATUS;
			}
		};
		
		setTimeframeJob.addJobChangeListener(new JobChangeAdapter() {
			public void done(IJobChangeEvent event) {
				if (event.getResult().isOK()) {
					Display.getDefault().syncExec( new Runnable() {
						public void run () {
							
							if (!currentFunctionTableViewer.getControl().isDisposed()){
								
								//updating the table viewers has to be done in the UI thread (this operation doesn't take long)
							    currentFunctionTableViewer.setInput(functionArray);
								
							    updateCallerCalleeTables(null);
							    
							    Table table = currentFunctionTableViewer.getTable();
							    
							    if (table.getItemCount() == 0)
							    	return;
							    
							    if (table.getSortColumn() == null) {
							    	table.setSortColumn(currentFunctionDefaultColumn);
							    	table.setSortDirection(SWT.UP);
							    } else {
							    	// use the user's preferred sort column, if any
							    	boolean sortAscending = !((SharedSorter) currentFunctionTableViewer.getSorter()).getSortAscending();
									((SharedSorter) currentFunctionTableViewer.getSorter()).setSortAscending(sortAscending);
							    }
							    
							    sortAndRefresh(currentFunctionTableViewer, table.getSortColumn());
								
							}
						}
					});

				} else {
					//unsuccessful operation: we still need to refresh
					needsRefresh = true;
				}
			}
		});
		
		setTimeframeJob.setUser(true); //show a progress dialog to the user
		
		currentFunctionTableViewer.getControl().addDisposeListener(new DisposeListener(){
			public void widgetDisposed(DisposeEvent e) {
				setTimeframeJob.cancel();
			}
		});
	}
    
    private void createPageListeners() {
    	final IPageChangedListener pageChangeListener = new IPageChangedListener(){

			/* (non-Javadoc)
			 * @see org.eclipse.jface.dialogs.IPageChangedListener#pageChanged(org.eclipse.jface.dialogs.PageChangedEvent)
			 */
			public void pageChanged(PageChangedEvent event) {
				isPageActive = (event.getSelectedPage() == CallVisualiser.this.curPage);//compare on reference
				if (isPageActive && needsRefresh){ 
					// if this ProfileVisualiser is the page being activated, 
					// check if the time frame selection needs updating
					setTimeframeJob.cancel();
					setTimeframeJob.schedule();
				}
				
			}
			
		};
    	final IPartListener partListener = new IPartListener(){

			public void partActivated(IWorkbenchPart part) {
				if (part instanceof PIPageEditor){
					PIPageEditor editor = (PIPageEditor) part;
					isPageActive = (editor.getActivePage() == pageIndex);
					if (isPageActive && needsRefresh){ 
						setTimeframeJob.cancel();
						setTimeframeJob.schedule();
					}
				}
			}

			public void partBroughtToTop(IWorkbenchPart part) {
				// nothing to do
			}

			public void partClosed(IWorkbenchPart part) {
				if (part instanceof PIPageEditor){
					//remove listeners
					PIPageEditor editor = (PIPageEditor) part;
					editor.removePageChangedListener(pageChangeListener);
					editor.getSite().getPage().removePartListener(this);
				}
			}

			public void partDeactivated(IWorkbenchPart part) {
				// nothing to do
			}

			public void partOpened(IWorkbenchPart part) {
				// nothing to do
			}
    		
    	};
    	
    	
    	
		PIPageEditor.currentPageEditor().addPageChangedListener(pageChangeListener);
		PIPageEditor.currentPageEditor().getSite().getPage().addPartListener(partListener);
    	
	}
	
    // added to give JUnit tests access
	public TableViewer getCallerViewer() {
		return this.callerTableViewer;
	}
	
    // added to give JUnit tests access
	public TableViewer getCurrentFunctionViewer() {
		return this.currentFunctionTableViewer;
	}
	
    // added to give JUnit tests access
	public TableViewer getCalleeViewer() {
		return this.calleeTableViewer;
	}
	
    // added to give JUnit tests access
	public void setCurrentMenuTable(Table currentMenuTable) {
		this.currentMenuTable = currentMenuTable;
	}

}