| /******************************************************************************* |
| * Copyright (c) 2016 Manumitting Technologies Inc 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: |
| * Manumitting Technologies Inc - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.ui.intro.quicklinks; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.PrintWriter; |
| import java.io.UnsupportedEncodingException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLEncoder; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Base64; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.function.Supplier; |
| import java.util.regex.Pattern; |
| import java.util.stream.Collectors; |
| import java.util.stream.Stream; |
| |
| import org.eclipse.core.commands.CommandManager; |
| import org.eclipse.core.commands.ParameterizedCommand; |
| import org.eclipse.core.commands.SerializationException; |
| import org.eclipse.core.commands.common.NotDefinedException; |
| import org.eclipse.core.runtime.FileLocator; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IExtension; |
| import org.eclipse.core.runtime.IExtensionPoint; |
| import org.eclipse.core.runtime.IExtensionRegistry; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.jface.resource.ImageDescriptor; |
| import org.eclipse.jface.viewers.ArrayContentProvider; |
| import org.eclipse.jface.viewers.TableViewer; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.graphics.Image; |
| import org.eclipse.swt.graphics.ImageData; |
| import org.eclipse.swt.graphics.ImageLoader; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.commands.ICommandImageService; |
| import org.eclipse.ui.forms.widgets.FormToolkit; |
| import org.eclipse.ui.forms.widgets.Section; |
| import org.eclipse.ui.internal.intro.impl.model.AbstractIntroPartImplementation; |
| import org.eclipse.ui.internal.intro.impl.model.IntroTheme; |
| import org.eclipse.ui.internal.intro.impl.util.Log; |
| import org.eclipse.ui.internal.menus.MenuHelper; |
| import org.eclipse.ui.intro.config.IIntroContentProvider; |
| import org.eclipse.ui.intro.config.IIntroContentProviderSite; |
| import org.eclipse.ui.services.IServiceLocator; |
| import org.osgi.framework.Bundle; |
| import org.osgi.framework.FrameworkUtil; |
| |
| /** |
| * An Intro content provider that populates a list of frequently-used commands |
| * from an extension point. The appearance of these quicklinks is normally taken |
| * from the command metadata, including the image icon, but can be tailored. |
| * These tailorings can be made optional depending on the current theme. |
| * |
| * This implementation is still experimental and subject to change. Feedback |
| * welcome as a <a href="http://eclip.se/9f">bug report on the Eclipse Bugzilla |
| * against Platform/User Assistance</a>. |
| */ |
| @SuppressWarnings("restriction") |
| public class QuicklinksViewer implements IIntroContentProvider { |
| |
| /** Represents the importance of an element */ |
| enum Importance { |
| HIGH("high", 0), MEDIUM("medium", 1), LOW("low", 2); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| |
| String id; |
| int level; |
| |
| Importance(String text, int importance) { |
| this.id = text; |
| this.level = importance; |
| } |
| |
| public static Importance forId(String id) { |
| for (Importance i : values()) { |
| if (i.id.equals(id)) { |
| return i; |
| } |
| } |
| return LOW; |
| } |
| } |
| |
| /** Model holding the relevant attributes of a Quicklink element */ |
| class Quicklink implements Comparable<Quicklink> { |
| String commandSpec; |
| String url; |
| String label; |
| String description; |
| String iconUrl; |
| Importance importance = Importance.MEDIUM; |
| long rank; |
| String resolution; |
| |
| public Quicklink() { |
| } |
| |
| @Override |
| public int compareTo(Quicklink b) { |
| int impA = this.importance.level; |
| int impB = b.importance.level; |
| if (impA != impB) { |
| return impA - impB; |
| } |
| long diff = this.rank - b.rank; |
| if (diff > 0) { |
| return 1; |
| } |
| if (diff < 0) { |
| return -1; |
| } |
| return 0; |
| } |
| } |
| |
| /** |
| * Responsible for retrieving Quicklinks and applying any icon overrides |
| */ |
| class ModelReader implements Supplier<List<Quicklink>> { |
| private static final String QL_EXT_PT = "org.eclipse.ui.intro.quicklinks"; //$NON-NLS-1$ |
| private static final String ELMT_COMMAND = "command"; //$NON-NLS-1$ |
| private static final String ATT_ID = "id"; //$NON-NLS-1$ |
| private static final String ELMT_URL = "url"; //$NON-NLS-1$ |
| private static final String ATT_LOCATION = "location"; //$NON-NLS-1$ |
| |
| private static final String ELMT_OVERRIDE = "override"; //$NON-NLS-1$ |
| private static final String ATT_COMMANDID = "command"; //$NON-NLS-1$ |
| private static final String ATT_THEME = "theme"; //$NON-NLS-1$ |
| |
| private static final String ATT_LABEL = "label"; //$NON-NLS-1$ |
| private static final String ATT_DESCRIPTION = "description"; //$NON-NLS-1$ |
| private static final String ATT_ICON = "icon"; //$NON-NLS-1$ |
| private static final String ATT_IMPORTANCE = "importance"; //$NON-NLS-1$ |
| private static final String ATT_RESOLUTION = "resolution"; //$NON-NLS-1$ |
| |
| /** commandSpec/url → quicklink */ |
| private Map<String, Quicklink> quicklinks = new LinkedHashMap<>(); |
| /** bundle symbolic name → bundle id */ |
| private Map<String, Long> bundleIds; |
| private Bundle[] bundles; |
| |
| /** |
| * Return the list of configured {@link Quicklink} that can be found. |
| * |
| * @return |
| */ |
| public List<Quicklink> get() { |
| IExtension extensions[] = getExtensions(QL_EXT_PT); |
| |
| // Process definitions from the product bundle first |
| Bundle productBundle = Platform.getProduct().getDefiningBundle(); |
| if(productBundle != null) { |
| for (IExtension ext : extensions) { |
| if (productBundle.getSymbolicName().equals(ext.getNamespaceIdentifier())) { |
| for (IConfigurationElement ce : ext.getConfigurationElements()) { |
| processDefinition(ce); |
| } |
| } |
| } |
| } |
| |
| for (IExtension ext : extensions) { |
| if (productBundle == null || !productBundle.getSymbolicName().equals(ext.getNamespaceIdentifier())) { |
| for (IConfigurationElement ce : ext.getConfigurationElements()) { |
| processDefinition(ce); |
| } |
| } |
| } |
| |
| // Now process all command overrides |
| for (IExtension ext : extensions) { |
| for (IConfigurationElement ce : ext.getConfigurationElements()) { |
| if (!ELMT_OVERRIDE.equals(ce.getName())) { |
| continue; |
| } |
| String theme = ce.getAttribute(ATT_THEME); |
| String commandSpecPattern = ce.getAttribute(ATT_COMMANDID); |
| String icon = ce.getAttribute(ATT_ICON); |
| if (theme != null && icon != null && Objects.equals(theme, getCurrentThemeId()) |
| && commandSpecPattern != null) { |
| findMatchingQuicklinks(commandSpecPattern) |
| .forEach(ql -> ql.iconUrl = getImageURL(ce, ATT_ICON)); |
| } |
| } |
| } |
| return new ArrayList<>(quicklinks.values()); |
| } |
| |
| private void processDefinition(IConfigurationElement ce) { |
| if (!ELMT_COMMAND.equals(ce.getName()) && !ELMT_URL.equals(ce.getName())) { |
| return; |
| } |
| |
| String key = null; |
| Quicklink ql = new Quicklink(); |
| if (ELMT_COMMAND.equals(ce.getName())) { |
| key = ce.getAttribute(ATT_ID); |
| if (key == null) { |
| Log.warning(NLS.bind("Skipping '{0}': missing {1}", ce.getName(), ATT_ID)); //$NON-NLS-1$ |
| return; |
| } |
| ql.commandSpec = key; |
| ql.label = ce.getAttribute(ATT_LABEL); |
| ql.description = ce.getAttribute(ATT_DESCRIPTION); |
| ql.iconUrl = getImageURL(ce, ATT_ICON); |
| } else if (ELMT_URL.equals(ce.getName())) { |
| key = ce.getAttribute(ATT_LOCATION); |
| if (key == null) { |
| Log.warning(NLS.bind("Skipping '{0}': missing {1}", ELMT_URL, ATT_LOCATION)); //$NON-NLS-1$ |
| return; |
| } |
| ql.url = key; |
| ql.label = ce.getAttribute(ATT_LABEL); |
| ql.description = ce.getAttribute(ATT_DESCRIPTION); |
| ql.iconUrl = getImageURL(ce, ATT_ICON); |
| } |
| ql.rank = getRank(ce.getContributor().getName()); |
| if (ce.getAttribute(ATT_IMPORTANCE) != null) { |
| ql.importance = Importance.forId(ce.getAttribute(ATT_IMPORTANCE)); |
| } |
| if (ce.getAttribute(ATT_RESOLUTION) != null) { |
| ql.resolution = ce.getAttribute(ATT_RESOLUTION); |
| } |
| // discard if already seen |
| quicklinks.putIfAbsent(key, ql); |
| } |
| |
| /** |
| * Find {@link Quicklink}s whose {@code commandSpec} matches the simple |
| * wildcard pattern in {@code commandSpecPattern} |
| * |
| * @param commandSpecPattern |
| * a simple wildcard pattern supporting *, ? |
| * @return the set of matching Quicklinks |
| */ |
| private Stream<Quicklink> findMatchingQuicklinks(String commandSpecPattern) { |
| // transform simple wildcards into regexp |
| String regexp = commandSpecPattern.replace(".", "\\.").replace("(", "\\(").replace(")", "\\)") //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ |
| .replace("*", ".*"); //$NON-NLS-1$ //$NON-NLS-2$ |
| final Pattern pattern = Pattern.compile(regexp); |
| return quicklinks.values().stream().filter( |
| ql -> ql.commandSpec != null && (commandSpecPattern.equals(ql.commandSpec) |
| || pattern.matcher(ql.commandSpec).matches())); |
| } |
| |
| private IExtension[] getExtensions(String extPtId) { |
| IExtensionRegistry registry = locator.getService(IExtensionRegistry.class); |
| IExtensionPoint extPt = registry.getExtensionPoint(extPtId); |
| return extPt == null ? new IExtension[0] : extPt.getExtensions(); |
| } |
| |
| |
| private long getRank(String bundleSymbolicName) { |
| if (bundleIds == null) { |
| Bundle bundle = FrameworkUtil.getBundle(getClass()); |
| bundleIds = new HashMap<>(); |
| bundles = bundle.getBundleContext().getBundles(); |
| } |
| return bundleIds.computeIfAbsent(bundleSymbolicName, bsn -> { |
| for (Bundle b : bundles) { |
| if (bsn.equals(b.getSymbolicName()) |
| && (b.getState() & (Bundle.INSTALLED | Bundle.UNINSTALLED)) == 0) { |
| return b.getBundleId(); |
| } |
| } |
| return Long.MAX_VALUE; |
| }); |
| } |
| |
| /** |
| * @return URL to image, suitable for using in an external browser; may |
| * be a <code>data:</code> URL; may be null |
| */ |
| private String getImageURL(IConfigurationElement ce, String attr) { |
| String iconURL = MenuHelper.getIconURI(ce, attr); |
| if (iconURL != null) { |
| return asBrowserURL(iconURL); |
| } |
| return null; |
| } |
| } |
| |
| /** Source: http://stackoverflow.com/a/417184 */ |
| private static final int MAX_URL_LENGTH = 2083; |
| |
| private IIntroContentProviderSite site; |
| private IServiceLocator locator; |
| private CommandManager manager; |
| private ICommandImageService images; |
| private Supplier<List<Quicklink>> model; |
| |
| public void init(IIntroContentProviderSite site) { |
| this.site = site; |
| // IIntroContentProviderSite should provide services. |
| if (site instanceof IServiceLocator) { |
| this.locator = (IServiceLocator) site; |
| } else if (site instanceof AbstractIntroPartImplementation) { |
| this.locator = ((AbstractIntroPartImplementation) site).getIntroPart().getIntroSite(); |
| } else { |
| this.locator = PlatformUI.getWorkbench(); |
| } |
| manager = locator.getService(CommandManager.class); |
| images = locator.getService(ICommandImageService.class); |
| |
| model = new ModelReader(); |
| } |
| |
| /** |
| * Find the current Welcome/Intro identifier |
| * |
| * @return the current identifier or {@code null} if no theme |
| */ |
| protected String getCurrentThemeId() { |
| if (site instanceof AbstractIntroPartImplementation) { |
| IntroTheme theme = ((AbstractIntroPartImplementation) site).getModel().getTheme(); |
| return theme.getId(); |
| } |
| return null; |
| } |
| |
| public void createContent(String id, PrintWriter out) { |
| // Content is already embedded within a <div id="..."> |
| getQuicklinks().forEach(ql -> { |
| try { |
| // ah how lovely to embed HTML in code |
| String urlEncodedCommand = asEmbeddedURL(ql); |
| out.append("<a class='content-link'"); //$NON-NLS-1$ |
| if (ql.commandSpec != null) { |
| out.append(" id='").append(asCSSId(ql.commandSpec)).append("' "); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| out.append(" href='"); //$NON-NLS-1$ |
| out.append(urlEncodedCommand); |
| out.append("'>"); //$NON-NLS-1$ |
| if (ql.iconUrl != null) { |
| out.append("<img class='background-image' src='").append(ql.iconUrl).append("'>"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| out.append("\n<div class='link-extra-div'></div>\n"); // UNKNOWN //$NON-NLS-1$ |
| out.append("<span class='link-label'>"); //$NON-NLS-1$ |
| out.append(ql.label); |
| out.append("</span>"); //$NON-NLS-1$ |
| if (ql.description != null) { |
| out.append("\n<p><span class='text'>"); //$NON-NLS-1$ |
| out.append(ql.description); |
| out.append("</span></p>"); //$NON-NLS-1$ |
| } |
| out.append("</a>"); //$NON-NLS-1$ |
| } catch (UnsupportedEncodingException e) { |
| e.printStackTrace(); |
| } |
| }); |
| } |
| |
| private String asEmbeddedURL(Quicklink ql) throws UnsupportedEncodingException { |
| if (ql.url != null) { |
| return ql.url; |
| } |
| String encoded = URLEncoder.encode(ql.commandSpec, "UTF-8"); //$NON-NLS-1$ |
| if (ql.resolution != null) { |
| encoded += "&standby=" + ql.resolution; //$NON-NLS-1$ |
| } |
| return "http://org.eclipse.ui.intro/execute?command=" + encoded; //$NON-NLS-1$ |
| } |
| |
| /** |
| * Transform the Eclipse Command identifier (with dots) to a CSS-compatible |
| * class |
| */ |
| private String asCSSId(String commandSpec) { |
| int indexOf = commandSpec.indexOf('('); |
| if (indexOf > 0) { |
| commandSpec = commandSpec.substring(0, indexOf); |
| } |
| return commandSpec.replace('.', '_'); |
| } |
| |
| /** |
| * Rewrite or possible extract the icon at the given URL to a stable URL |
| * that can be embedded in HTML and rendered in a browser. May create |
| * temporary files that will be cleaned up on exit. |
| * |
| * @param iconURL |
| * @return stable URL |
| */ |
| private String asBrowserURL(String iconURL) { |
| if (iconURL.startsWith("file:") || iconURL.startsWith("http:")) { //$NON-NLS-1$ //$NON-NLS-2$ |
| return iconURL; |
| } |
| try { |
| URL original = new URL(iconURL); |
| URL toLocal = FileLocator.toFileURL(original); |
| if (!toLocal.sameFile(original)) { |
| return toLocal.toString(); |
| } |
| } catch (IOException e1) { |
| /* ignore */ |
| } |
| |
| // extract content |
| try { |
| return asDataURL(ImageDescriptor.createFromURL(new URL(iconURL))); |
| } catch (MalformedURLException e) { |
| // should probably log this |
| return iconURL; |
| } |
| } |
| |
| /** |
| * Write out the image as a data: URL if possible or to the file-system. |
| * |
| * @param descriptor |
| * @return URL with the resulting image |
| */ |
| private String asDataURL(ImageDescriptor descriptor) { |
| if (descriptor == null) { |
| return null; |
| } |
| ImageData data = descriptor.getImageData(); |
| if (data == null) { |
| return null; |
| } |
| ImageLoader loader = new ImageLoader(); |
| loader.data = new ImageData[] { data }; |
| |
| ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| loader.save(output, SWT.IMAGE_PNG); |
| if (output.size() * 4 / 3 < MAX_URL_LENGTH) { |
| // You'd think there was a more efficient way to do this... |
| return "data:image/png;base64," + Base64.getEncoder().encodeToString(output.toByteArray()); //$NON-NLS-1$ |
| } |
| try { |
| File tempFile = File.createTempFile("qlink", "png"); //$NON-NLS-1$ //$NON-NLS-2$ |
| FileOutputStream fos = new FileOutputStream(tempFile); |
| fos.write(output.toByteArray()); |
| fos.close(); |
| tempFile.deleteOnExit(); |
| return tempFile.toURI().toString(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| return null; |
| } |
| } |
| |
| public void createContent(String id, Composite parent, FormToolkit toolkit) { |
| Section section = toolkit.createSection(parent, Section.EXPANDED); |
| TableViewer tableViewer = new TableViewer(toolkit.createTable(section, SWT.FULL_SELECTION)); |
| tableViewer.setLabelProvider(new URLLabelProvider() { |
| @Override |
| public String getText(Object element) { |
| if (element instanceof Quicklink) { |
| return ((Quicklink) element).label; |
| } |
| return super.getText(element); |
| } |
| |
| @Override |
| public Image getImage(Object element) { |
| if (element instanceof Quicklink) { |
| return super.getImage(((Quicklink) element).iconUrl); |
| } |
| return super.getImage(element); |
| } |
| }); |
| tableViewer.setContentProvider(new ArrayContentProvider()); |
| tableViewer.setInput(getQuicklinks().toArray()); |
| } |
| |
| private List<Quicklink> getQuicklinks() { |
| List<Quicklink> links = model.get(); |
| if (links.isEmpty()) { |
| links = generateDefaultQuicklinks(); |
| } |
| return links.stream().filter(this::populateQuicklink).sorted(Quicklink::compareTo) |
| .collect(Collectors.toList()); |
| } |
| |
| /** |
| * Attempt to populate common fields given other information. For commands, |
| * we look up information in the ICommandService and ICommandImageService. |
| * Return false if this quicklink cannot be found and should not be shown. |
| * |
| * @return true if should be included |
| */ |
| private boolean populateQuicklink(Quicklink ql) { |
| if (ql.commandSpec == null) { |
| // non-commmands are fine |
| return true; |
| } |
| try { |
| ParameterizedCommand pc = manager.deserialize(ql.commandSpec); |
| if (!pc.getCommand().isDefined()) { |
| // not an error: just not found |
| return false; |
| } |
| if (ql.label == null) { |
| ql.label = pc.getCommand().getName(); |
| } |
| if (ql.description == null) { |
| ql.description = pc.getCommand().getDescription(); |
| } |
| if (ql.iconUrl == null && images != null) { |
| ImageDescriptor descriptor = images.getImageDescriptor(pc.getId()); |
| if (descriptor != null) { |
| String iconUrl = MenuHelper.getImageUrl(descriptor); |
| ql.iconUrl = iconUrl != null ? asBrowserURL(iconUrl) : asDataURL(descriptor); |
| } |
| } |
| return true; |
| } catch (NotDefinedException | SerializationException e) { |
| // exclude commands that don't exist or are mis-fashioned |
| return false; |
| } |
| } |
| |
| /** Simplify creating a quicklink for a command */ |
| private Quicklink forCommand(String commandSpec) { |
| Quicklink ql = new Quicklink(); |
| ql.commandSpec = commandSpec; |
| return ql; |
| } |
| |
| /** Simplify creating a quicklink for a command */ |
| private Quicklink forCommand(String commandSpec, Importance importance) { |
| Quicklink ql = new Quicklink(); |
| ql.commandSpec = commandSpec; |
| ql.importance = importance; |
| return ql; |
| } |
| |
| /** |
| * Return the default commands to be shown if there is no other content |
| * available |
| */ |
| private List<Quicklink> generateDefaultQuicklinks() { |
| return Arrays.asList(forCommand("org.eclipse.oomph.setup.ui.questionnaire", Importance.HIGH), //$NON-NLS-1$ |
| forCommand("org.eclipse.ui.cheatsheets.openCheatSheet"), //$NON-NLS-1$ |
| forCommand("org.eclipse.ui.newWizard"), //$NON-NLS-1$ |
| forCommand("org.eclipse.ui.file.import"), //$NON-NLS-1$ |
| forCommand("org.eclipse.epp.mpc.ui.command.showMarketplaceWizard"), //$NON-NLS-1$ |
| forCommand("org.eclipse.ui.edit.text.openLocalFile", Importance.LOW)); //$NON-NLS-1$ |
| } |
| |
| public void dispose() { |
| } |
| } |