/*******************************************************************************
* Copyright (c) 2008, 2011 Thomas Holland (thomas@innot.de) and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Thomas Holland - initial API and implementation
*******************************************************************************/
package de.innot.avreclipse.ui.views.avrdevice;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.cdt.core.model.ICElement;
import org.eclipse.cdt.core.model.ICProject;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectNature;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.viewers.ComboViewer;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CTabFolder;
import org.eclipse.swt.custom.CTabItem;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.ISelectionListener;
import org.eclipse.ui.IViewSite;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE;
import org.eclipse.ui.part.ViewPart;
import de.innot.avreclipse.PluginIDs;
import de.innot.avreclipse.core.properties.AVRProjectProperties;
import de.innot.avreclipse.core.properties.ProjectPropertyManager;
import de.innot.avreclipse.core.util.AVRMCUidConverter;
import de.innot.avreclipse.devicedescription.ICategory;
import de.innot.avreclipse.devicedescription.IDeviceDescription;
import de.innot.avreclipse.devicedescription.IDeviceDescriptionProvider;
import de.innot.avreclipse.devicedescription.IProviderChangeListener;
import de.innot.avreclipse.devicedescription.avrio.AVRiohDeviceDescriptionProvider;
/**
* This is the main part of the AVR Device Explorer View.
*
* @author Thomas Holland
* @version 1.1
*/
public class AVRDeviceView extends ViewPart {
// The parent Composite of this Viewer
private Composite fViewParent;
private Composite fTop;
private ComboViewer fCombo;
private Composite fSourcesComposite;
private final List<Label> fSourcesLabels = new ArrayList<Label>(0);
private final List<Text> fSourcesTexts = new ArrayList<Text>(0);
private CTabFolder fTabFolder;
private final List<CTabItem> fTabs = new ArrayList<CTabItem>(0);
private Map<String, List<TreeColumn>> fTreeColumns;
private IMemento fMemento;
private IDeviceDescriptionProvider dmprovider = null;
private IProviderChangeListener fProviderChangeListener;
private ISelectionListener fWorkbenchSelectionListener;
/**
* The constructor.
*
* Nothing done here.
*/
public AVRDeviceView() {
}
/*
* (non-Javadoc) Method declared on IViewPart.
*/
@Override
public void init(IViewSite site, IMemento memento) throws PartInitException {
// Initialize the SuperClass and store the passed memento for use by
// the individual methods.
super.init(site, memento);
fMemento = memento;
}
@Override
public void saveState(IMemento memento) {
// Save the current state of the viewer
super.saveState(memento);
// TODO: Save the Column Layout for each category
}
/**
* Create, layout and initialize the controls of this viewer
*/
@Override
public void createPartControl(Composite parent) {
fViewParent = parent;
// All listeners that are need to unregistered on dispose()
fProviderChangeListener = new ProviderChangeListener();
fWorkbenchSelectionListener = new WorkbenchSelectionListener();
// Get the default AVRiohDeviceDescriptionProvider
// TODO: once more than one DeviceDescriptionProvider exist,
// this has to be changed.
dmprovider = AVRiohDeviceDescriptionProvider.getDefault();
if (dmprovider != null)
// setup ourself as a change listener for the
// DeviceDescriptionProvider
dmprovider.addProviderChangeListener(fProviderChangeListener); // ProviderChangeListener
fViewParent.setLayout(new GridLayout());
// TODO: TreeColumn Map is dependent on devicemodelprovider
fTreeColumns = new HashMap<String, List<TreeColumn>>();
// Layout the top part, which consists of the MCU Type selection Combo
// and a composite of the sources Labels and Texts
// The Combo gets 33% of the space, the sources composite the other 67%
GridLayout gl = new GridLayout(3, true);
gl.marginHeight = 0;
gl.marginWidth = 0;
fTop = new Composite(fViewParent, SWT.NONE);
fTop.setLayout(gl);
fTop.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
fCombo = new ComboViewer(fTop, SWT.READ_ONLY | SWT.DROP_DOWN);
fCombo.getControl().setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false));
fCombo.setContentProvider(new DeviceListContentProvider());
fCombo.setLabelProvider(new ComboLabelProvider());
// register the combo as a Selection Provider
getSite().setSelectionProvider(fCombo);
fSourcesComposite = new Composite(fTop, SWT.NONE);
fSourcesComposite.setLayout(new RowLayout());
fSourcesComposite.setLayoutData(new GridData(SWT.FILL, SWT.NONE, true, false, 2, 1));
new Label(fSourcesComposite, SWT.NONE).setText("source:");
// The bottom part consists of a TabFolder Control which will take all
// space not used by the top part.
fTabFolder = new CTabFolder(fViewParent, SWT.BORDER);
fTabFolder.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
fCombo.addSelectionChangedListener(new ComboSelectionChangedListener());
fTabFolder.addSelectionListener(new TabFolderSelectionListener());
// This will -in turn- cause all the data sub-widgets to be
// initialized and displayed
providerChanged();
// Activate the Workbench selection listener
getSite().getWorkbenchWindow().getSelectionService()
.addPostSelectionListener(fWorkbenchSelectionListener);
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.part.WorkbenchPart#setFocus()
*/
@Override
public void setFocus() {
// Passing the focus request to the treeviewer of the selected tab
CTabItem item = fTabFolder.getSelection();
if (item != null) {
TreeViewer tv = (TreeViewer) item.getData();
tv.getControl().setFocus();
}
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.part.WorkbenchPart#dispose()
*/
@Override
public void dispose() {
// remove the listeners from their objects
dmprovider.removeProviderChangeListener(fProviderChangeListener);
getSite().getWorkbenchWindow().getSelectionService()
.removePostSelectionListener(fWorkbenchSelectionListener);
super.dispose();
}
/**
* Update the controls which make up the sources composite.
* <p>
* The sources are shown in a "breadcrumb" fashion, with a ">>" between the names of the source
* files.
* </p>
* <p>
* This will handle arbitrary numbers of sources. While this is not really required today with
* maximum two sources, it is ready in case the structure of the include files is changed.
* </p
*/
private void updateSourcelist(Composite parent, IDeviceDescription device) {
Text txt = null;
Label lbl = null;
List<String> sources = device.getSourcesList();
// Strategy: iterate over all elements of the sources list, and try to
// get a Text control from the existing text controls. If this fails
// with an Exception a new Text control is created and added to the
// internal list fSourcesTexts. After each Text control a Label with
// ">>" is added, but the last of these labels is hidden again.
//
// After the list has been iterated, any Text and Label Controls that
// are left over in the list will be disposed.
int txtcounter = 0;
for (String path : sources) {
try {
txt = fSourcesTexts.get(txtcounter);
lbl = fSourcesLabels.get(txtcounter);
} catch (IndexOutOfBoundsException ioobe) {
txt = new Text(parent, SWT.NONE);
txt.setEditable(false);
// make it look like a link (grey background, dark blue color
txt.setBackground(parent.getBackground());
txt.setForeground(parent.getDisplay().getSystemColor(SWT.COLOR_DARK_BLUE));
txt.setCursor(parent.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
Listener srclistener = new SourceSelectionMouseHandler();
txt.addListener(SWT.MouseEnter, srclistener);
txt.addListener(SWT.MouseExit, srclistener);
txt.addListener(SWT.MouseUp, srclistener);
fSourcesTexts.add(txt);
// create a ">>" label
lbl = new Label(parent, SWT.NONE);
lbl.setText(">>");
fSourcesLabels.add(lbl);
}
txt.setText(path);
lbl.setVisible(true);
txtcounter++;
}
// hide the last label
fSourcesLabels.get(txtcounter - 1).setVisible(false);
// dispose and remove any remaining Texts / Labels
// The loop will end with an Exception once the last list element has
// been removed and we try to read the same index again.
try {
while (true) {
txt = fSourcesTexts.get(txtcounter);
txt.dispose();
fSourcesTexts.remove(txtcounter); // all remaining items
// are moved up
lbl = fSourcesLabels.get(txtcounter);
lbl.dispose();
fSourcesLabels.remove(txtcounter);
}
} catch (IndexOutOfBoundsException ioobe) {
// do nothing, as this exception is expected
}
// Redraw the parent
parent.pack(true);
}
/**
* Update all tabs.
* <p>
* One tab for each Category of the device is created. Each tab also gets a
* <code>TreeViewer</code> associated to it, accessible via the <code>CTabItem.getData()</code>
* method.
* </p>
* <p>
* If a new tab has the same name as the previously selected tab, it will be selected as well
* </p>
*/
private void updateTabs(CTabFolder parent, IDeviceDescription device) {
TreeViewer tv = null;
CTabItem cti = null;
String activetabname = null;
// Remember the name of the active tab
if (parent.getSelection() != null) {
activetabname = parent.getSelection().getText();
}
List<ICategory> categories = device.getCategories();
// Strategy: iterate over all elements of the category list, and try to
// get a CTabItem control from the existing CTabItems. If this fails
// with an Exception a new CTabItem control is created and added to the
// internal list fTabs. Also a TreeViewer is created and linked to the
// tab.
// After the list has been iterated, any CTabItems that
// are left over in the list will be disposed.
int cticounter = 0;
for (ICategory cat : categories) {
try {
cti = fTabs.get(cticounter);
} catch (IndexOutOfBoundsException ioobe) {
// Tab did not exist: create a new CTabItem and an associated
// TreeViewer
cti = new CTabItem(parent, SWT.NONE);
tv = createTreeView(parent, cat);
cti.setData(tv);
cti.setControl(tv.getControl());
fTabs.add(cti);
}
cti.setText(cat.getName());
tv = (TreeViewer) cti.getData();
updateTreeView(tv, cat);
tv.setInput(cat);
// Check if this tab should be made active
if (cat.getName().equals(activetabname)) {
parent.setSelection(cti);
}
cticounter++;
}
// dispose and remove any remaining CTabItems and associated TreeViewers
// The loop will end with an Exception once the last list element has
// been removed and we try to read the same index again.
try {
while (true) {
cti = fTabs.get(cticounter);
cti.getControl().dispose();
cti.dispose();
fTabs.remove(cticounter); // all remaining items are moved up
}
} catch (IndexOutOfBoundsException ioobe) {
// do nothing, as this exception is expected
}
// If no Tab is active then activate the first one
if (parent.getSelectionIndex() == -1) {
parent.setSelection(0);
}
}
/**
* Create a new TreeViewer for the given Category.
*
* It is up to the caller to dispose the <code>TreeViewer</code> when it is no longer needed.
*
* @return A newly created TreeViewer.
*/
private TreeViewer createTreeView(Composite parent, ICategory category) {
TreeViewer tv = new TreeViewer(parent);
tv.setContentProvider(new DeviceModelContentProvider());
tv.setLabelProvider(new EntryLabelProvider());
tv.getTree().setLayoutData(new GridData(GridData.FILL_BOTH));
tv.getTree().setHeaderVisible(true);
tv.getTree().setLinesVisible(true);
// TODO: add an Action on the header for sorting
tv.setInput(category);
return tv;
}
/**
* Updates a TreeViewer to a new Category.
* <p>
* If the Category with this name does not yet exist the required <code>TreeColumns</code> will
* be created and initialized to their default values. If a Category with this name has already
* existed (even from a previous device>, then the <code>TreeColumns</code> will be reused.
* Therefore any changes by the user are preserved.
* </p>
*
* @param parent
* @param cat
* The Category which the parent will use as Content
*/
private void updateTreeView(TreeViewer parent, ICategory cat) {
List<TreeColumn> columnlist;
String catname = cat.getName();
// have a somewhat reasonable layout regardless of font size.
// Yes, I know that height is not the same as width, but with
// proportional fonts its the next best thing.
FontData curfd[] = parent.getTree().getFont().getFontData();
int approxfontwidth = curfd[0].getHeight();
// See if a column layout for this Category already exists
columnlist = fTreeColumns.get(catname);
if (columnlist == null) {
// No: create the columns for this category name
columnlist = new ArrayList<TreeColumn>(cat.getColumnCount());
// TODO: pass more information from the Category, like alignment,
// icon etc.
String[] labels = cat.getColumnLabels();
int[] widths = cat.getColumnDefaultWidths();
for (int i = 0; i < labels.length; i++) {
TreeColumn tc = new TreeColumn(parent.getTree(), SWT.LEFT);
tc.setWidth(widths[i] * approxfontwidth);
tc.setResizable(true);
tc.setMoveable(true);
tc.setText(labels[i]);
columnlist.add(tc);
}
fTreeColumns.put(catname, columnlist);
} else {
// Yes:
// TODO: restore the TreeColumns Layout from the memento
}
}
/**
* Show an Error message.
*
* @param message
*/
private void showMessage(String message) {
MessageDialog.openError(fViewParent.getShell(), "AVR Device Explorer Message", message);
}
// called whenever the provider has changed
private void providerChanged() {
// pass the current AVRiohDeviceDescriptionProvider to the
// DeviceListContentProvider
fCombo.setInput(dmprovider);
String show = null;
if (fMemento != null)
show = fMemento.getString("combovalue");
if (show == null || "".equals(show)) {
show = (String) fCombo.getElementAt(0);
}
if (show != null) {
// This next step will cause a SelectionChangeEvent which in turn
// will load the sources, tabs and treeviewers
fCombo.setSelection(new StructuredSelection(show), true);
}
}
// When a different MCU Type is selected do the following:
// - get the new device from the provider
// - update the sources text elements
// - Update the tabs of the TabFolder (which will update the treeviewers)
// - Set the focus to the TreeViewer of the active tab.
private class ComboSelectionChangedListener implements ISelectionChangedListener {
public void selectionChanged(SelectionChangedEvent event) {
String devicename = (String) ((StructuredSelection) event.getSelection())
.getFirstElement();
if (devicename == null) {
// The new provider does not know the previously selected mcu
if (fMemento != null) {
String oldmcu = fMemento.getString("combovalue");
if (oldmcu != null) {
String msg = MessageFormat.format("The previously selected mcu [{0}] is not known", oldmcu);
showMessage(msg);
}
}
return;
}
if (fMemento != null) {
// persist the selected mcu
fMemento.putString("combovalue", devicename);
}
String deviceid = AVRMCUidConverter.name2id(devicename);
IDeviceDescription device = dmprovider.getDeviceDescription(deviceid);
if (device == null) {
showMessage(dmprovider.getErrorMessage());
} else {
updateSourcelist(fSourcesComposite, device);
updateTabs(fTabFolder, device);
// setFocus();
}
}
}
// When a different tab is selected do the following:
// - refresh the associated TreeViewer
// - set the focus to this tabs TreeViewer
private static class TabFolderSelectionListener implements SelectionListener {
public void widgetDefaultSelected(SelectionEvent e) {
// not called for a CTabFolder
}
public void widgetSelected(SelectionEvent e) {
CTabItem ti = (CTabItem) e.item;
((TreeViewer) ti.getData()).refresh();
((TreeViewer) ti.getData()).getControl().setFocus();
}
}
/**
* Handle Mouse Events for the source file text widgets.
*
* Three events are handled:
* <ul>
* <li><code>MouseEnter</code>: Change background color to show the user that this widget can be
* clicked on.</li>
* <li><code>MouseExit</code>: Restore the background color.</li>
* <li><code>MouseUp</code>: Open the sourcefile associated with the selected widget in an
* Editor.</li>
* </ul>
*/
private class SourceSelectionMouseHandler implements Listener {
public void handleEvent(Event event) {
Text txt = (Text) event.widget;
switch (event.type) {
case SWT.MouseEnter:
// change background color to some lighter color
txt.setBackground(txt.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
break;
case SWT.MouseExit:
// change background color back to normal
txt.setBackground(txt.getParent().getBackground());
break;
case SWT.MouseUp:
// Open the selected source file
// get the basepath from the AVRiohDeviceDescriptionProvider,
// append the content of the selected text widget, get a
// IFileStore for this file and open an Editor for this
// file in the active Workbench page.
// No error checking as this file should exist
IPath srcfile = dmprovider.getBasePath();
srcfile = srcfile.append(txt.getText());
IFileStore fileStore = EFS.getLocalFileSystem().getStore(srcfile);
if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) {
IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
.getActivePage();
try {
IDE.openEditorOnFileStore(page, fileStore);
// TODO: if any row in the treeview has been selected,
// try to scroll the Editor to the associated
// definition.
} catch (PartInitException e) {
// what can cause this?
e.printStackTrace();
}
}
break;
} // switch
} // handleevent
}
/**
* Handle Provider Change Events.
* <p>
* This is called from the current DeviceDescriptionProvider whenever its internal data has
* changed. For example, when the user changes the path to the source data.
* </p>
*/
private class ProviderChangeListener implements IProviderChangeListener {
public void providerChange() {
// We don't know which Threat this comes from.
// Assume its not the SWT Threat and act accordingly
fViewParent.getDisplay().asyncExec(new Runnable() {
public void run() {
providerChanged();
}
}); // Runnable
}
}
/**
* Handle Selection Change Events.
* <p>
* This is called by the workbench selection services to inform this viewer, that something has
* been selected on the workbench. If something with an AVR MCU type has been selected
* (Project), then the viewer will show the description of the associated mcu.
* </p>
*/
private class WorkbenchSelectionListener implements ISelectionListener {
public void selectionChanged(IWorkbenchPart part, final ISelection selection) {
// we ignore our own selections
if (part == AVRDeviceView.this) {
return;
}
// To minimize the GUI impact run the rest of this method in a Job
Job selectionjob = new Job("AVR Device Explorer") {
@Override
protected IStatus run(IProgressMonitor monitor) {
String newid = null;
try {
monitor.beginTask("Selection Change", 3);
Set<String> devicelist = dmprovider.getMCUList();
if (devicelist == null) {
// The provider has no mcus -> return immediately
return Status.OK_STATUS;
}
monitor.worked(1);
// see if the selection is something that has an avr mcu
// id
if (selection instanceof IStructuredSelection) {
// First: Projects
IStructuredSelection ss = (IStructuredSelection) selection;
newid = getMCUId(ss);
} else if (selection instanceof ITextSelection) {
// Second: Selected Text
String text = ((ITextSelection) selection).getText();
// Test if the text is an MCU name/id
if (devicelist.contains(AVRMCUidConverter.name2id(text))) {
// yes: use it
newid = AVRMCUidConverter.name2id(text);
}
}
monitor.worked(1);
if (newid != null) {
// Test if it is a valid mcu id. Only set valid mcu
// ids
// as otherwise an error message would be displayed
if (!devicelist.contains(AVRMCUidConverter.name2id(newid))) {
return Status.OK_STATUS;
}
IDeviceDescription device = dmprovider.getDeviceDescription(newid);
if (device == null) {
// do nothing
return Status.OK_STATUS;
}
// Next cause a SelectionChange Event in the UI
// Thread, which handles the rest of the change.
// (Only if the control still exists)
final IStructuredSelection newselection = new StructuredSelection(
AVRMCUidConverter.id2name(newid));
if ((fViewParent != null) && (!fViewParent.isDisposed())) {
fViewParent.getDisplay().asyncExec(new Runnable() {
public void run() {
if (fCombo != null && !fCombo.getControl().isDisposed()) {
fCombo.setSelection(newselection, true);
}
}
}); // Runnable
}
}
monitor.worked(1);
} catch (IOException e) {
// could not get a MCU list from the provider
// nothing to be done about this but to fail silently.
} finally {
monitor.done();
}
return Status.OK_STATUS;
}
};
selectionjob.setSystem(true);
selectionjob.setPriority(Job.SHORT);
selectionjob.schedule();
}
/**
* Get the mcu id from the given structured selection.
* <p>
* If the first element of the selection is an AVR project, the mcu type is taken from the
* properties of the active build configuration.
* </p>
* <p>
* If the selection did not contain a valid mcu type <code>null</code> is returned
*
* @param selection
* <code>IStructuredSelection</code> from the Eclipse Selection Services
* @return String with the mcu id or <code>null</null> if no mcu id was found.
*/
private String getMCUId(IStructuredSelection selection) {
Object item = selection.getFirstElement();
if (item == null) {
return null;
}
IProject project = null;
// See if the given is an IProject (directly or via IAdaptable)
if (item instanceof IProject) {
project = (IProject) item;
} else if (item instanceof IResource) {
project = ((IResource) item).getProject();
} else if (item instanceof IAdaptable) {
IAdaptable adaptable = (IAdaptable) item;
project = (IProject) adaptable.getAdapter(IProject.class);
if (project == null) {
// Try ICProject -> IProject
ICProject cproject = (ICProject) adaptable.getAdapter(ICProject.class);
if (cproject == null) {
// Try ICElement -> ICProject -> IProject
ICElement celement = (ICElement) adaptable.getAdapter(ICElement.class);
if (celement != null) {
cproject = celement.getCProject();
}
}
if (cproject != null) {
project = cproject.getProject();
}
}
}
if (project != null) {
try {
IProjectNature nature = project.getNature(PluginIDs.NATURE_ID);
if (nature != null) {
// This is an AVR Project
// Get the AVR properties for the active build
// configuration and fetch the mcu id from it.
ProjectPropertyManager projprops = ProjectPropertyManager
.getPropertyManager(project);
AVRProjectProperties props = projprops.getActiveProperties();
return props.getMCUId();
}
} catch (CoreException e) {
return null;
}
} else if (item instanceof String) {
String mcuname = (String) item;
String mcuid = AVRMCUidConverter.name2id(mcuname);
return mcuid;
}
// Selection does not contain a mcuid
return null;
}
}
}