de.innot.avreclipse.ui.views.supportedmcu.URLColumnLabelProvider.java Source code

Java tutorial

Introduction

Here is the source code for de.innot.avreclipse.ui.views.supportedmcu.URLColumnLabelProvider.java

Source

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