/******************************************************************************* * 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.supportedmcu; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.MultiStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubProgressMonitor; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.TableEditor; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.forms.events.HyperlinkEvent; import org.eclipse.ui.forms.events.IHyperlinkListener; import org.eclipse.ui.forms.widgets.Hyperlink; import org.eclipse.ui.ide.IDE; import de.innot.avreclipse.AVRPlugin; import de.innot.avreclipse.core.IMCUProvider; import de.innot.avreclipse.core.toolinfo.Datasheets; import de.innot.avreclipse.core.toolinfo.MCUNames; import de.innot.avreclipse.core.util.AVRMCUidConverter; import de.innot.avreclipse.util.URLDownloadException; import de.innot.avreclipse.util.URLDownloadManager; /** * This is an extended ColumnLabelProvider that handles URL hyperlinks. * <p> * This Class needs two {@link IMCUProvider}s, one for the Label text and one for the URL. As * implemented in the View these are {@link MCUNames} and {@link Datasheets} respectively. * </p> * <p> * As TableViewers do not support custom controls or actually anything clickable, this class is * implemented by adding TableEditors on top of the TableItems in this Column. The * {@link #updateColumn(TableViewer, TableViewerColumn)} method needs to be called to set up the * TableEditors. This method may only be called after the table has been filled with values (after * the TableViewer.setInput(model)) method has been called. * </p> * <p> * The TableEditors are not used as Editors, but contain an Hyperlink control each, which can be * clicked to download and open the URL from the given linkprovider. * </p> * * @author Thomas Holland * @since 2.2 */ public class URLColumnLabelProvider extends ColumnLabelProvider implements ISelectionChangedListener { /** The MCUProvider that provides the text to be shown in the cell */ private final IMCUProvider fNameProvider; /** The IMCUProvider that provides the url to be opene */ private final IMCUProvider fLinkProvider; private TableViewer fTableViewer; /** The last TableEditor selected. Required to de-select */ private TableEditor fLastEditor; /** * List of all TableEditors of this column. Required to update the TableEditors manually on a * {@link SelectionChangedEvent} */ private final Map<TableItem, TableEditor> fTableEditors = new HashMap<TableItem, TableEditor>(); /** The text color for links not yet downloaded. Value: SWT.COLOR_DARK_BLUE */ private static Color LINK_COLOR = PlatformUI .getWorkbench() .getDisplay() .getSystemColor( SWT.COLOR_DARK_BLUE); /** The text color for links already in the cache. Value: SWT.COLOR_MAGETA */ private static Color LINK_IN_CACHE_COLOR = PlatformUI .getWorkbench() .getDisplay() .getSystemColor( SWT.COLOR_MAGENTA); /** The text color for malformed links. Value: SWT.COLOR_RED */ private static Color LINK_MALFORMED_COLOR = PlatformUI .getWorkbench() .getDisplay() .getSystemColor( SWT.COLOR_RED); /** * @param nameprovider * The <code>IMCUProvider</code> that returns a User readable name for a given MCU * id * @param linkProvider * The <code>IMCUProvider</code> that returns the URL (as <code>String</code>) * for the datasheet for the given MCU id */ public URLColumnLabelProvider(IMCUProvider nameprovider, IMCUProvider linkProvider) { fNameProvider = nameprovider; fLinkProvider = linkProvider; } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.ColumnLabelProvider#getText(java.lang.Object) */ @Override public String getText(Object element) { // returns the name of the given MCU id String mcuid = (String) element; String info = fNameProvider.getMCUInfo(mcuid); // If MCUNames is used as a provider, info will never be null. But this // might change... return info != null ? info : "n/a"; } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.BaseLabelProvider#dispose() */ @Override public void dispose() { // Not sure if this is really necessary, as this ColumnLabelProvider // will only be disposed when the whole View (incl. the TableViewer) is // closed. if (fTableViewer != null) fTableViewer.removeSelectionChangedListener(this); } /** * Set up this column for URL table cells. * <p> * This needs to be called <strong>after</strong> the table has been filled with rows. It will * add Hyperlink Widgets on top of all cells in the column, that actually contain URLs. This is * done via TableEditors for those cells. * </p> * <p> * This also adds itself as <code>SelectionChangeListener</code> and as * <code>FocusListener</code> to the given TableViewer. * </p> * <p> * Both parameters need to refer to the same column as this ColumnLabelProvider. Passing other * TableViewers or TableViewerColumns will result in undefined results. * </p> * * @param tableviewer * The TableViewer which contains this Column. * @param viewercolumn * The TableViewerColumn for this ColumnLabelProvider */ public void updateColumn(TableViewer tableviewer, TableViewerColumn viewercolumn) { // get the table from the Column and find the index of the given column // this is needed later on for the TableEditor fTableViewer = tableviewer; TableColumn column = viewercolumn.getColumn(); Table table = column.getParent(); int index = getColumnIndex(column); // Now go through all TableItems (=Rows) of the Table. // For each TableItem a new TableEditor with a Hyperlink Control is // generated (if the MCU id of the row has a Datasheet associated with // it). TableItem[] allitems = table.getItems(); for (TableItem item : allitems) { // get the mcuid for this row String mcuname = item.getText(); String mcuid = AVRMCUidConverter.name2id(mcuname); // Test if there is a datasheet available. If yes, add a TableEditor // with a Hyperlink in it. If no, do nothing (will show the text // from #getText()) if (fLinkProvider.hasMCU(mcuid)) { final URL url; final Hyperlink link = new Hyperlink(table, SWT.NONE); link.setText(mcuname); try { // Create an URL object for the Datasheet URL and set the // Hyperlink Control to look like a Browser link. url = new URL(fLinkProvider.getMCUInfo(mcuid)); link.setUnderlined(true); link.setHref(url); link.setToolTipText(url.toExternalForm()); // Simlate standard Browser behaviour if (URLDownloadManager.inCache(url)) { link.setData(LINK_IN_CACHE_COLOR); } else { link.setData(LINK_COLOR); } } catch (MalformedURLException e1) { // unlikely, as this should be covered by the Datasheet // Preferences. Nevertheless I leave this here, if a user // tries to mess with the datasheet property files. link.setUnderlined(false); link.setData(LINK_MALFORMED_COLOR); link.setToolTipText("Malformed Datasheet URL: " + fLinkProvider.getMCUInfo(mcuid)); } // The HyperlinkListener is taking care of opening the URL when // clicked. link.addHyperlinkListener(new IHyperlinkListener() { /* * (non-Javadoc) * * @see org.eclipse.ui.forms.events.IHyperlinkListener#linkActivated(org.eclipse.ui.forms.events.HyperlinkEvent) */ public void linkActivated(HyperlinkEvent event) { URL url = (URL) event.getHref(); if (url != null) { // Start the (downloading and) opening of the // references URL openURL(url); } } public void linkEntered(HyperlinkEvent event) { // Do nothing } public void linkExited(HyperlinkEvent event) { // Do nothing } }); // finally create a TableEditor for our Hyperlink and keep a // reference to it, so manual layout() method calls can be made // when the Table selection has changed. TableEditor editor = new TableEditor(table); editor.grabHorizontal = true; editor.setEditor(link, item, index); fTableEditors.put(item, editor); // finally set the colors (not selected, not focused) setEditorColors(editor, false, false); } } // Sometimes the TableEditors are a bit off when opening the viewer. // Re-layout the TableEditors in the background Display display = table.getDisplay(); display.asyncExec(updateEditors); // Add this to the table PostSelectionChangeListener fTableViewer.addSelectionChangedListener(this); // Now add a FocusListener to set the colors of the selected TableEditor // whenever the focus changes for the Table fTableViewer.getTable().addFocusListener(new FocusListener() { /* * (non-Javadoc) * * @see org.eclipse.swt.events.FocusListener#focusGained(org.eclipse.swt.events.FocusEvent) */ public void focusGained(FocusEvent e) { Table table = (Table) e.getSource(); int index = table.getSelectionIndex(); if (index != -1) { // some item is selected. Get it, find the associated // TableEditor (if any), and change the colors of the // associated Hyperlink. TableItem selected = table.getItem(index); TableEditor editor = fTableEditors.get(selected); if (editor != null) { setEditorColors(editor, true, true); } } } /* * (non-Javadoc) * * @see org.eclipse.swt.events.FocusListener#focusLost(org.eclipse.swt.events.FocusEvent) */ public void focusLost(FocusEvent e) { Table table = (Table) e.getSource(); int index = table.getSelectionIndex(); if (index != -1) { // some item is selected. Get it, find the associated // TableEditor (if any), and change the colors of the // associated Hyperlink. TableItem selected = table.getItem(index); TableEditor editor = fTableEditors.get(selected); if (editor != null) { setEditorColors(editor, true, false); } } } }); } /** * Sets the colors of a Hyperlink control (via the associated TableEditor). * <p> * A (Windows) SWT Table Cell can have three color states. These three states are covered in * this method: * </p> * <ul> * <li> * <p> * Item selected: <code>true</code>, Table has Focus: <code>true</code><br> * Background: <code>SWT.COLOR_LIST_SELECTION</code><br> * Foreground: <code>SWT.COLOR_LIST_SELECTION_TEXT</code> * </p> * </li> * <li> * <p> * Item selected: <code>true</code>, Table has Focus: <code>false</code><br> * Background: <code>SWT.COLOR_WIDGET_BACKGROUND</code><br> * Foreground: Link color provided by the Hyperlink Control * </p> * </li> * <li> * <p> * Item selected: <code>false</code>, Table has Focus: not required<br> * Background: <code>SWT.COLOR_LIST_BACKGROUND</code><br> * Foreground: Link color provided by the Hyperlink Control * </p> * </li> * </ul> * * @param editor * TableEdior to set the colors for. * @param isselected * <code>true</code> if the editor is in a currently selected table row. * @param hasfocus * <code>true</code> if the table has the focus. Not required if isselected is * <code>false</code> */ private void setEditorColors(TableEditor editor, boolean isselected, boolean hasfocus) { final Color background, foreground; final Control link = editor.getEditor(); final Display display = link.getDisplay(); if (isselected) { if (hasfocus) { background = display.getSystemColor(SWT.COLOR_LIST_SELECTION); foreground = display.getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT); } else { // no focus background = display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND); foreground = (Color) link.getData(); } } else { // not selected background = display.getSystemColor(SWT.COLOR_LIST_BACKGROUND); foreground = (Color) link.getData(); } link.setBackground(background); link.setForeground(foreground); } /** * Gets the index of the given TableColumn in the table * * @param column * @return int with Table column index */ private static int getColumnIndex(TableColumn column) { Table table = column.getParent(); TableColumn[] allcolumns = table.getColumns(); int index = -1; for (int i = 0; i < allcolumns.length; i++) { if (allcolumns[i] == column) { index = i; break; } } return index; } /* * (non-Javadoc) * * @see org.eclipse.jface.viewers.ISelectionChangedListener#selectionChanged(org.eclipse.jface.viewers.SelectionChangedEvent) */ public void selectionChanged(SelectionChangedEvent event) { // Stupid - at least on Windows an external Selection change via the // TableViewer.setSelection() method does not cause a Selection event in // the underlying Table, so we have to with the SelectionChangeEvent of // the TableViewer. // When the selection has changed, first a previously selected // TableEditor (which still has its "selected" colors) needs to be // changed to the unselected colors. // // Then all TableEditors of this column need to recalculate their // layout, // because the (programmatic) selection change might change the visible // part of the Table, which the TableEditors won't notice (again stupid) TableViewer source = (TableViewer) event.getSource(); Table table = source.getTable(); TableItem item = table.getItem(table.getSelectionIndex()); // Restore the previously selected link if (fLastEditor != null) { setEditorColors(fLastEditor, false, false); } TableEditor editor = fTableEditors.get(item); if (editor != null) { setEditorColors(editor, true, item.getParent().isFocusControl()); fLastEditor = editor; } // Update all Editors. This is called twice, because at - least on // windows - clicking on a partially visible row will cause the table to // scroll *after* the selection has been made (which -again- the // TableEditors will not be aware of, as this partial scroll is without // Scroll Events. // The 0,5 sec value is just a guess. It works on my PentiumM 1,6 GHz // Laptop for a redraw after a partial scroll without much lag. // The TableEditor uses 1,5 sec in a similar situation (for a resize), // but that caused the update to lag far behind the partial scroll // (making the TableEditor hang behind the other Columns. // However, if this value is to short, it will cause the TableEditor to // be off by one row. Display display = item.getDisplay(); display.syncExec(updateEditors); display.timerExec(500, updateEditors); } /** * A small Runnable that will call {@link TableEditor#layout()} on all TableEditors of the * column */ private final Runnable updateEditors = new Runnable() { public void run() { Collection<TableEditor> alleditors = fTableEditors .values(); for (TableEditor e : alleditors) { e.layout(); } } }; /** * Load and Display the given URL. * <p> * The File from the URL is first downloaded via the {@link URLDownloadManager} and then opened * using the default Editor registered for this filetype. * </p> * <p> * The download and the opening of the file is done in a Job, so this method returns immediatly. * </p> * <p> * If a download of the same URL is still in progress, this method does nothing to avoid * multiple parallel downloads of the same file by nervous users. </p * * @param urlstring * A String with an URL. */ private void openURL(final URL url) { final Display display = PlatformUI.getWorkbench().getDisplay(); // Test if a download of this file is already in progress. // If yes: do nothing and return, assuming that the user has clicked // on the url twice accidentally if (URLDownloadManager.isDownloading(url)) { return; } // The actual download is done in this Job. // For any Exception during the download an ErrorDialog is displayed // with the cause(s) // The Job also returns an IStatus result, but by the time this is // returned, the openURL() method has long finished and there is // no one there to actually read this message :-) Job loadandopenJob = new Job("Download and Open") { @Override protected IStatus run(final IProgressMonitor monitor) { try { monitor.beginTask("Download " + url.toExternalForm(), 100); // Download the file and... final File file = URLDownloadManager.download(url, new SubProgressMonitor( monitor, 95)); // ...open the file in an editor. monitor.subTask("Opening Editor for " + file.getName()); if (display == null || display.isDisposed()) { return new Status(Status.ERROR, AVRPlugin.PLUGIN_ID, "Cannot open Editor: no Display found", null); } openFileInEditor(file); monitor.worked(5); } catch (URLDownloadException ude) { final URLDownloadException exc = ude; // ErrorDialog for all Exceptions, in an // Display.syncExec() to run in the UI Thread. display.syncExec(new Runnable() { public void run() { Shell shell = display.getActiveShell(); String title = "Download Failed"; String message = "The requested file could not be downloaded\nFile: " + url.getPath() + "\nHost: " + url.getHost(); String reason = exc.getMessage(); MultiStatus status = new MultiStatus(AVRPlugin.PLUGIN_ID, 0, reason, null); Throwable cause = exc.getCause(); // in case there are multiple root causes // (unlikely, but who knows?) while (cause != null) { status.add(new Status(Status.ERROR, AVRPlugin.PLUGIN_ID, cause .getClass().getSimpleName(), cause)); cause = cause.getCause(); } ErrorDialog.openError(shell, title, message, status, Status.ERROR); AVRPlugin.getDefault().log(status); } }); // fDisplay.asyncExec } finally { monitor.done(); } return Status.OK_STATUS; } // run }; // new Job() // set some options and start the Job. loadandopenJob.setUser(true); loadandopenJob.setPriority(Job.LONG); loadandopenJob.schedule(); return; } /** * Opens the given file with the standard editor. * <p> * An ErrorDialog is shown when the opening of the file fails. * </p> * * @param file * <code>java.io.File</code> with the file to open * @return */ private IStatus openFileInEditor(final File file) { final Display display = PlatformUI.getWorkbench().getDisplay(); // Because this is called from a Job (which is not running in the UI // Thread, the opening is delegated to a Display.syncExec() display.syncExec(new Runnable() { public void run() { IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(file.toString())); if (!fileStore.fetchInfo().isDirectory() && fileStore.fetchInfo().exists()) { IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow() .getActivePage(); try { IDE.openEditorOnFileStore(page, fileStore); } catch (PartInitException e) { IStatus status = new Status(Status.ERROR, AVRPlugin.PLUGIN_ID, "Could not open " + file.toString(), e); Shell shell = display.getActiveShell(); String title = "Can't open File"; String message = "The File " + file.toString() + " could not be opened"; ErrorDialog.openError(shell, title, message, status); AVRPlugin.getDefault().log(status); } } } }); return Status.OK_STATUS; } }