crashanalysis/crashanalyser/com.nokia.s60tools.crashanalyser/src/com/nokia/s60tools/crashanalyser/ui/viewers/CallStackTableViewer.java
author Matti Laitinen <matti.t.laitinen@nokia.com>
Thu, 11 Feb 2010 15:06:45 +0200
changeset 0 5ad7ad99af01
child 4 615035072f7e
permissions -rw-r--r--
Initial version of CrashAnalyser under EPL

/*
* Copyright (c) 2008 Nokia Corporation and/or its subsidiary(-ies). 
* All rights reserved.
* This component and the accompanying materials are made available
* under the terms of "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.s60tools.crashanalyser.ui.viewers;

import org.eclipse.jface.action.*;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.TableViewerColumn;
import org.eclipse.swt.widgets.*;
import org.eclipse.swt.SWT;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.ui.*;
import org.eclipse.ui.editors.text.*;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.menus.*;
import sun.awt.windows.*;
import com.nokia.s60tools.crashanalyser.model.*;
import com.nokia.s60tools.crashanalyser.resources.*;
import com.nokia.s60tools.crashanalyser.containers.*;
import com.nokia.s60tools.crashanalyser.ui.console.CrashAnalyserEditorConsole;
import com.nokia.s60tools.util.sourcecode.*;
import java.util.*;
import java.net.URI;
import java.io.*;
import java.awt.datatransfer.*;
import java.awt.Toolkit;

/**
 * Table viewer for call stack table. Call stack table is used in 
 * SummaryPage.java (Crash Data page in Crash Visualiser editor) 
 *
 */
public class CallStackTableViewer extends CrashAnalyserTableViewer {
	Action actionCopySelectionToClipboardAsRichText;
	Action actionCopySelectionToClipboardAsPlainText;
	Action actionCopyStackToClipboardAsRichText;
	Action actionCopyStackToClipboardAsPlainText;
	Action actionOpenSourceFile;
	Action actionDoubleClick;
	Table tableControl;
	
	public final static int COLUMN_ADDRESS = 0;
	public final static int COLUMN_SYMBOL = 1;
	public final static int COLUMN_VALUE = 2;
	public final static int COLUMN_OFFSET = 3;
	public final static int COLUMN_OBJECT = 4;
	public final static int COLUMN_TEXT = 5;
	
	MenuManager subMenuSdk;

	/**
	 * Constructor
	 * @param table table where this viewer is used
	 */
	public CallStackTableViewer(Table table) {
		super(table);
		
		tableControl = table;

		doCreateActions();
		doCreateContextMenu();
		
		TableViewerColumn column = new TableViewerColumn(this, SWT.NONE);
		column.setLabelProvider(labelProvider);
		column.getColumn().setText("");

		column = new TableViewerColumn(this, SWT.NONE);
		column.setLabelProvider(labelProvider);
		column.getColumn().setText("Symbol");
		
		column = new TableViewerColumn(this, SWT.NONE);
		column.setLabelProvider(labelProvider);
		column.getColumn().setText("Address");

		column = new TableViewerColumn(this, SWT.NONE);
		column.setLabelProvider(labelProvider);
		column.getColumn().setText("Offset");

		column = new TableViewerColumn(this, SWT.NONE);
		column.setLabelProvider(labelProvider);
		column.getColumn().setText("Object");

		column = new TableViewerColumn(this, SWT.NONE);
		column.setLabelProvider(labelProvider);
		column.getColumn().setText("Data");
		
		addDoubleClickListener(new IDoubleClickListener() {
			public void doubleClick(DoubleClickEvent event) {
				actionDoubleClick.run();
			}
		});			
	}
	
	/**
	 * Creates pop-up menu actions
	 */
	private void doCreateActions() {
		actionCopySelectionToClipboardAsRichText = new Action("as Rich Text") {
			public void run() {
				copyToClipboard(tableControl, true, true);
				}
			};

		actionCopyStackToClipboardAsRichText = new Action("as Rich Text") {
			public void run() {
				copyToClipboard(tableControl, false, true);
				}
			};

		actionCopySelectionToClipboardAsPlainText = new Action("as Plain Text") {
			public void run() {
				copyToClipboard(tableControl, true, false);
				}
			};

		actionCopyStackToClipboardAsPlainText = new Action("as Plain Text") {
			public void run() {
				copyToClipboard(tableControl, false, false);
				}
			};

		actionOpenSourceFile = new Action("Open Source File") {
			public void run() {
				openSourceFile(tableControl);
			}
		};
		
		actionDoubleClick = new Action() {
			public void run() {
				openSourceFile(tableControl);
			}
		};
	}
	
	/**
	 * Setting the focus in opened file there where the method name occurs,
	 * must call after file is opened and only if opening was successful
	 * @param location
	 * @throws CoreException
	 */
	private static void setFocusToLineWhereMethodIs(
			final SourceFileLocation location) throws CoreException {
		//Runnable to open new file
		final IWorkspaceRunnable runSetFocus = new IWorkspaceRunnable() {
			public void run(IProgressMonitor monitor) throws CoreException {
				// do the actual work in here

				try {
					//Setting focus to correct line
					IWorkbenchPage activePage = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
					IEditorPart activeEditor = activePage.getActiveEditor();

					if(activeEditor != null && activeEditor instanceof TextEditor){
						
						// This is actually an instance of 'org.eclipse.cdt.internal.ui.editor.CEditor' 
						// that extends org.eclipse.ui.editors.text.TextEditor
						TextEditor editor = (TextEditor) activeEditor;
						IDocument doc =  getDocument(editor);
						if(doc != null){

							String text = doc.get();
							int methodOffset = location.getMethodOffset();
							if(methodOffset == SourceFileLocation.OFFSET_NOT_FOUND){
								// Removing parameters and getting new offset.
								String methodNameWithOutParams = location.getMethodName();
								methodNameWithOutParams = methodNameWithOutParams.substring(0, methodNameWithOutParams.indexOf("(")); //$NON-NLS-1$
								methodOffset = text.indexOf(methodNameWithOutParams);

								if(methodOffset == SourceFileLocation.OFFSET_NOT_FOUND){
									// Removing possible namespace and getting new offset.
									String separator = "::"; //$NON-NLS-1$
									int separatorLocation = methodNameWithOutParams.lastIndexOf(separator);
									if(separatorLocation > 0){
										methodNameWithOutParams = methodNameWithOutParams.substring(separatorLocation + separator.length());
										methodOffset = text.indexOf(methodNameWithOutParams);
									}
								}
							}
							
							editor.setHighlightRange(methodOffset, 0, true);
						}							
					}					

				} catch (Exception e) {
					e.printStackTrace();
					Status status = new Status(IStatus.ERROR,
							"com.nokia.s60tools.crashanalyser", 0, e //$NON-NLS-1$
									.getMessage(), e);

					throw new CoreException(status);
				} 

			}
		};
		
		
		ResourcesPlugin.getWorkspace().run(runSetFocus, null, IWorkspace.AVOID_UPDATE, null);
	}

	/**
	 * Returns the document interface for the currently active document 
	 * in the given editor.
	 * @param editor Editor to ask currently active document from. 
	 * @return Document interface if found, otherwise <code>null</code>.
	 */
	private static IDocument getDocument(TextEditor editor) {
		
		TextFileDocumentProvider  documentProvider = (TextFileDocumentProvider) editor.getDocumentProvider();
		if(documentProvider != null){
			return  documentProvider.getDocument(editor.getEditorInput());
			}								
		return null;
	}		
	
	/**
	 * Shows a message box with given message
	 * @param message
	 */
	private static void showMessage(String message, Shell shell) {
		MessageDialog.openInformation(
			shell,
			"Crash Analyser",
			message);
	}
	
	/**
	 * Tries to open source file for the selected line in the call stack table
	 * @param tableControl
	 */
	static void openSourceFile(Table tableControl) {
		try {
			if (tableControl.getSelection() != null &&
				tableControl.getSelection().length == 1) {
				StackEntry stackEntry = 
					(StackEntry)tableControl.getSelection()[0].getData();
				
				// we need code segment in order to be able to open source code,
				// if we don't find code segment, notify user
				if (stackEntry == null ||
					"".equals(stackEntry.getCodeSegmentName())) {
					showMessage("CODE SEGMENT NOT FOUND\n\nThe selected row does not contain information from which Code Segment (e.g. dll, exe) its address can be found. Source file cannot be opened.", tableControl.getShell());
					return;
				}
				
				String currentSdkName = SourceSdkManager.getCurrentSkdName();
				// user must first choose an active SDK. If it has not been chosen, notify user
				if (currentSdkName == null || "".equals(currentSdkName)) {
					showMessage("ACTIVE SDK NOT SELECTED.\n\n Please select Active SDK first by right-clicking in the Call Stack table.", tableControl.getShell());
					return;
				}
				
				String epocroot = SourceSdkManager.getEpocroot(currentSdkName);
				// user might have e.g. selected long ago an sdk which is not available anymore
				if (epocroot == null) {
					showMessage("INVALID ACTIVE SDK.\n\n Please select a valid Active SDK by right-clicking in the Call Stack table.", tableControl.getShell());
					return;
				}
				File epoc = new File(epocroot);
				
				// epocroot for the selected SDK if not valid, notify user
				if (!epoc.isDirectory() || !epoc.exists()) {
					String message = "INVALID EPOCROOT.\n\nThe EPOCROOT (" +
					 epocroot +
					 ") of selected Active SDK ("+
					 currentSdkName +
					 ") is not valid. Please check your SDK settings from Window > Preferences > Carbide.c++ > SDK Preferences, or select a different Active SDK by right-clicking in the Call Stack table.";
					 showMessage(message, tableControl.getShell());
					return;
				}
				// currentSdkName is e.g. 'SDK_Name armv5 urel', we need to get out, build and variant
				int lastSpace = currentSdkName.lastIndexOf(" ");
				int secondToLastSpace = currentSdkName.lastIndexOf(" ", lastSpace-1);
				
				String build = currentSdkName.substring(lastSpace+1, currentSdkName.length()); //e.g. urel
				String variant = currentSdkName.substring(secondToLastSpace+1, lastSpace);  // e.g. armv5
				
				String methodNameAsItsInMapFile = tableControl.getSelection()[0].getText(COLUMN_SYMBOL); 
				String dllName = stackEntry.getCodeSegmentName(); 

				ISourceFinder finder = SourceFinderFactory.createMapSourceFinder(CrashAnalyserEditorConsole.getInstance());

				SourceFileLocation location = 
					finder.findSourceFileByMethodName(methodNameAsItsInMapFile, dllName, variant, build, epocroot);

				File file = new File(location.getSourceFileLocation());
				// we could not find the source file, notify user
				if(file == null || !file.exists()){
					showMessage("SOURCE FILE CANNOT BE FOUND\n\n" +
								file.getName() +
								" was not found from " +
								FileOperations.getFolder(location.getSourceFileLocation()) +
								". Please make sure you have selected a correct Active SDK by right-clicking in the Call Stack table. It is also possible that you don't have all source files for your Active SDK present. Source file cannot be opened.", 
								tableControl.getShell());
					return;
				}
				
				//Create URI to open file
				String uriStr = location.getSourceFileLocation().replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
				uriStr = "file://" + uriStr; //$NON-NLS-1$
				final URI srcURI = new URI(uriStr);
				
				//Find default editor for that file
				IEditorRegistry reg = PlatformUI.getWorkbench().getEditorRegistry();
				
				IEditorDescriptor editor = reg.getDefaultEditor(file.getName());
				//We open editor by it's ID
				final String editorId = editor.getId();
				
				IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
				try {
					IEditorPart part = IDE.openEditor(page, srcURI, editorId, true);
					if(part != null){
						//Set focus to correct line
						setFocusToLineWhereMethodIs(location);
					}
				} catch (PartInitException e) {
					e.printStackTrace();
				}
			} 
		}catch (CannotFoundFileException ex) {
			String newLine = System.getProperty("line.separator");
			showMessage(ex.getMessage() + newLine + newLine + "Please check that you have selected correct Active SDK, by right-clicking in the Call Stack table.", tableControl.getShell());
		} catch (Exception e) {
			e.printStackTrace();
			showMessage("Unable to find source file. Please check your SDK properties.", tableControl.getShell());
		}
	}

	/**
	 * Copies data from table into clipboard 
	 * @param table table from which data is copied from
	 * @param selection if true, only the selected rows are copied, if false, all rows are copied
	 * @param richText if true, data is copied in rich-text format, if false, data is copied in plain text format
	 */
	static void copyToClipboard(Table table, boolean selection, boolean richText) {
		TableItem[] items = null;
		// copy only selected rows
		if (selection)
			items = table.getSelection();
		// copy all rows
		else
			items = table.getItems();
		
		if (items != null && items.length > 0) {
			// rich-text format
			if (richText) {
				try {
					String data = HtmlFormatter.formatStackForClipboard(items, table.getColumnCount());
					byte[] bytes = convertToHTMLFormat(data);
					PEBClip clip = new PEBClip();
					clip.setData( WDataTransferer.CF_HTML, bytes);
				} catch (Exception e) {
				}
			// plain text format
			} else {
				String separator = System.getProperty("line.separator");
				String data = "";
				for (int i = 0; i < items.length; i++) {
					TableItem item = items[i];
					data += String.format("%-10s  %-10s  %-6s  %-6s  %s    %s", 
											item.getText(COLUMN_ADDRESS),
											item.getText(COLUMN_VALUE),
											item.getText(COLUMN_TEXT),
											item.getText(COLUMN_OFFSET),
											item.getText(COLUMN_OBJECT),
											item.getText(COLUMN_SYMBOL)) +
											separator;
				}
				StringSelection stringSelection = new StringSelection(data);
				Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
				clipboard.setContents(stringSelection, null);
			}
		}
	}
	
	/**
	 * surrounds the html with this envelope, ready for
	 * windows clipboard (jdk support can be made better)
	 * <pre>
	 * Version:1.0
	 * StartHTML:00000000000
	 * EndHTML:00000000000
	 * StartFragment:00000000000
	 * EndFragment:00000000000
	 * &lt;!--StartFragment--&gt;
	 * ...
	 * &lt;!-- EndFragment-- &gt;
	 * </pre>
	 * We have to return a byte array 'cause in Windows the html needs to be utf-8
	 * encoded. And because we have to calculate char-offsets, we encode it here.
	 * @param htmlText
	 * @return byte[]
	 */
	static byte[] convertToHTMLFormat(String htmlText) {
		try {
			String sep = "\r\n";
			String header = "Version:1.0"+ sep +
										 "StartHTML:00000000000"+ sep +
										 "EndHTML:00000000000"+ sep +
										 "StartFragment:00000000000"+ sep +
										 "EndFragment:00000000000" + sep;
			
			String html = "<!--StartFragment-->\r\n" + htmlText + "<!--EndFragment-->\r\n";
	
			byte[] bHtml = html.getBytes("UTF-8");// encode first 'cause it may grow
	
			int headerLen = header.length();
			int htmlLen = bHtml.length;
	
			StringBuffer buf = new  StringBuffer(header);
			setValue( buf, "StartHTML", headerLen-1);
			setValue( buf, "EndHTML", headerLen + htmlLen-1);
			setValue( buf, "StartFragment", headerLen-1);
			setValue( buf, "EndFragment", headerLen + htmlLen-1);
			byte[] bHeader = buf.toString().getBytes("UTF-8");// should stay the same (no nonASCII chars in header)
	
			byte result[] = new byte[headerLen + htmlLen ];
			System.arraycopy(bHeader, 0, result, 0, bHeader.length);
			System.arraycopy(bHtml, 0, result, bHeader.length, bHtml.length);
	
			return result;
		} catch (Exception e) {
			return null;
		}
	}

	/**
	 * Replaces name+":00000000000" with name+":xxxxxxxxxxx" where xxx... is the '0' padded value.
	 * Value can't be to long, since maxint can be displayed with 11 digits. If value is below zero
	 * there is enough place (10 for the digits 1 for sign).<br>
	 * If the search is not found nothing is done.
	 * @param src
	 * @param name
	 * @param value
	 */
	private static void setValue( StringBuffer src, String name, int value){
		int val = value;
		String search = name+":00000000000";
		int pos = src.indexOf(search);
		if (pos ==-1) return;// not found, do nothing

		boolean belowZero = val<0;
		if (belowZero) val = -val;

		src.replace(pos+search.length()-(val+"").length(), pos+search.length(), val+"");
		if (belowZero) src.setCharAt(pos+name.length()+1,'-'); // +1 'cause of ':' in "SearchMe:"
	}
	
	/**
	 * creates context menu accordingly to what is selected in the table.
	 */
	private void doCreateContextMenu() {
		
		// Active SDK menu
		subMenuSdk = new MenuManager("Active SDK");	
		subMenuSdk.setRemoveAllWhenShown(true);
		subMenuSdk.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager manager) {
				Map<String, String> sdks = SourceSdkManager.getAllSdks();
				String currentSdk = SourceSdkManager.getCurrentSkdName();
				// if there are sdks
				if (sdks != null && !sdks.isEmpty()) {
					String[] sdkNames = sdks.keySet().toArray(new String[sdks.size()]);
					java.util.Arrays.sort(sdkNames);
					// go through all found sdks and and them to pop-up menu
					for (int i = 0; i < sdkNames.length; i++) {
						String sdk = sdkNames[i];
						CommandContributionItemParameter p = 
							new CommandContributionItemParameter(PlatformUI.getWorkbench().getActiveWorkbenchWindow(),
																 null,
																 "com.nokia.s60tools.crashanalyser.commands.SdkSelection",
																 CommandContributionItem.STYLE_PUSH);
						p.label = sdk;
						// if this sdk is selected as Active sdk, draw an circular image to 
						// this menu item to indicate that this is the active sdk
						if (sdk.equalsIgnoreCase(currentSdk)) {
							p.icon = ImageResourceManager.getImageDescriptor((ImageKeys.SELECTED_SDK));
						}
						CommandContributionItem item = new CommandContributionItem(p);	    
				   	    subMenuSdk.add(item);
					}
				}
			}
		});

   	    final MenuManager subMenuSelection = new MenuManager("Copy Selection to Clipboard");
		subMenuSelection.add(actionCopySelectionToClipboardAsPlainText);
		subMenuSelection.add(actionCopySelectionToClipboardAsRichText);

		final MenuManager subMenuFull = new MenuManager("Copy Whole Stack to Clipboard");
		subMenuFull.add(actionCopyStackToClipboardAsPlainText);
		subMenuFull.add(actionCopyStackToClipboardAsRichText);

		MenuManager manager = new MenuManager("#PopupMenu");
		Menu menu = manager.createContextMenu(getControl());
		getControl().setMenu(menu);
		
		manager.setRemoveAllWhenShown(true);
		manager.addMenuListener(new IMenuListener() {
			public void menuAboutToShow(IMenuManager manager) {
				Table table = (Table)getControl();
				if (table.getSelection() != null) {
					// add 'Open source file' and 'Active SDK' menu items to pop-up menu
					// only if exactly one row is selected in table and if that
					// row's symbol column is not empty
					if (table.getSelection().length == 1 && 
						!"".equals(table.getSelection()[0].getText(COLUMN_SYMBOL))) {
						manager.add(actionOpenSourceFile);
						manager.add(subMenuSdk);
						manager.add(new Separator());
					}
					manager.add(subMenuSelection);
					manager.add(subMenuFull);
				}
			}
		});
	}
}