| /******************************************************************************* |
| * Copyright (c) 2011, 2018 The Eclipse Foundation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * The Eclipse Foundation - initial API and implementation |
| * Yatta Solutions - public API (bug 432803), drag&drop (bug 433333) |
| *******************************************************************************/ |
| package org.eclipse.epp.internal.mpc.ui.wizards; |
| |
| import java.lang.reflect.Field; |
| |
| 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.epp.internal.mpc.ui.MarketplaceClientUi; |
| import org.eclipse.epp.internal.mpc.ui.MarketplaceClientUiPlugin; |
| import org.eclipse.epp.mpc.ui.MarketplaceUrlHandler; |
| import org.eclipse.epp.mpc.ui.MarketplaceUrlHandler.SolutionInstallationInfo; |
| import org.eclipse.jface.util.Util; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.SWTException; |
| import org.eclipse.swt.dnd.DND; |
| import org.eclipse.swt.dnd.DropTarget; |
| import org.eclipse.swt.dnd.DropTargetAdapter; |
| import org.eclipse.swt.dnd.DropTargetEvent; |
| import org.eclipse.swt.dnd.DropTargetListener; |
| import org.eclipse.swt.dnd.Transfer; |
| import org.eclipse.swt.dnd.TransferData; |
| import org.eclipse.swt.dnd.URLTransfer; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Control; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.ui.IPageListener; |
| import org.eclipse.ui.IPartListener2; |
| import org.eclipse.ui.IPartService; |
| import org.eclipse.ui.IPerspectiveDescriptor; |
| import org.eclipse.ui.IPerspectiveListener; |
| import org.eclipse.ui.IStartup; |
| import org.eclipse.ui.IWindowListener; |
| import org.eclipse.ui.IWorkbench; |
| import org.eclipse.ui.IWorkbenchPage; |
| import org.eclipse.ui.IWorkbenchPartReference; |
| import org.eclipse.ui.IWorkbenchWindow; |
| import org.eclipse.ui.PlatformUI; |
| import org.eclipse.ui.progress.UIJob; |
| |
| /** |
| * @author Benjamin Muskalla |
| * @author Carsten Reckord |
| */ |
| public class MarketplaceDropAdapter implements IStartup { |
| |
| private static final int[] PREFERRED_DROP_OPERATIONS = { DND.DROP_DEFAULT, DND.DROP_COPY, DND.DROP_MOVE, |
| DND.DROP_LINK }; |
| |
| private static final int DROP_OPERATIONS = DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK | DND.DROP_DEFAULT; |
| |
| private final DropTargetAdapter dropListener = new MarketplaceDropTargetListener(); |
| |
| private final WorkbenchListener workbenchListener = new WorkbenchListener(); |
| |
| private Transfer[] transferAgents; |
| |
| @Override |
| public void earlyStartup() { |
| UIJob registerJob = new UIJob(Display.getDefault(), Messages.MarketplaceDropAdapter_0) { |
| { |
| setPriority(Job.SHORT); |
| setSystem(true); |
| } |
| |
| @Override |
| public IStatus runInUIThread(IProgressMonitor monitor) { |
| IWorkbench workbench = PlatformUI.getWorkbench(); |
| workbench.addWindowListener(workbenchListener); |
| IWorkbenchWindow[] workbenchWindows = workbench |
| .getWorkbenchWindows(); |
| for (IWorkbenchWindow window : workbenchWindows) { |
| workbenchListener.hookWindow(window); |
| } |
| return Status.OK_STATUS; |
| } |
| |
| }; |
| registerJob.schedule(); |
| } |
| |
| public void installDropTarget(final Shell shell) { |
| hookUrlTransfer(shell, dropListener); |
| } |
| |
| private DropTarget hookUrlTransfer(final Shell shell, DropTargetAdapter dropListener) { |
| DropTarget target = findDropTarget(shell); |
| if (target != null) { |
| //target exists, get it and check proper registration |
| registerWithExistingTarget(target); |
| } else { |
| target = new DropTarget(shell, DROP_OPERATIONS); |
| if (transferAgents == null) { |
| transferAgents = new Transfer[] { URLTransfer.getInstance() }; |
| } |
| target.setTransfer(transferAgents); |
| } |
| registerDropListener(target, dropListener); |
| |
| Control[] children = shell.getChildren(); |
| for (Control child : children) { |
| hookRecursive(child, dropListener); |
| } |
| return target; |
| } |
| |
| private void registerDropListener(DropTarget target, DropTargetListener dropListener) { |
| target.removeDropListener(dropListener); |
| target.addDropListener(dropListener); |
| } |
| |
| private void hookRecursive(Control child, DropTargetListener dropListener) { |
| DropTarget childTarget = findDropTarget(child); |
| if (childTarget != null) { |
| registerWithExistingTarget(childTarget); |
| registerDropListener(childTarget, dropListener); |
| } |
| if (child instanceof Composite) { |
| Composite composite = (Composite) child; |
| // Bug 485245 - avoid UI freezes for deeply nested widget trees |
| composite.getDisplay().asyncExec(() -> { |
| Control[] children = composite.getChildren(); |
| for (Control control : children) { |
| hookRecursive(control, dropListener); |
| } |
| }); |
| } |
| } |
| |
| private void registerWithExistingTarget(DropTarget target) { |
| Transfer[] transfers = target.getTransfer(); |
| boolean exists = false; |
| if (transfers != null) { |
| for (Transfer transfer : transfers) { |
| if (transfer instanceof URLTransfer) { |
| exists = true; |
| break; |
| } |
| } |
| if (!exists) { |
| Transfer[] newTransfers = new Transfer[transfers.length + 1]; |
| System.arraycopy(transfers, 0, newTransfers, 0, transfers.length); |
| newTransfers[transfers.length] = URLTransfer.getInstance(); |
| target.setTransfer(newTransfers); |
| } |
| } |
| } |
| |
| private DropTarget findDropTarget(Control control) { |
| if (control.isDisposed()) { |
| return null; |
| } |
| Object object = control.getData(DND.DROP_TARGET_KEY); |
| if (object instanceof DropTarget) { |
| return (DropTarget) object; |
| } |
| return null; |
| } |
| |
| protected void proceedInstallation(String url) { |
| SolutionInstallationInfo info = MarketplaceUrlHandler.createSolutionInstallInfo(url); |
| if (info != null) { |
| MarketplaceUrlHandler.triggerInstall(info); |
| } |
| } |
| |
| protected void proceedFavorites(String url) { |
| MarketplaceUrlHandler.triggerFavorites(url); |
| } |
| |
| protected boolean acceptSolutionUrl(final String url) { |
| return MarketplaceUrlHandler.isPotentialSolution(url); |
| } |
| |
| protected boolean acceptFavoritesListUrl(final String url) { |
| return MarketplaceUrlHandler.isPotentialFavoritesList(url); |
| } |
| |
| private class MarketplaceDropTargetListener extends DropTargetAdapter { |
| |
| @Override |
| public void dragEnter(DropTargetEvent e) { |
| updateDragDetails(e); |
| } |
| |
| @Override |
| public void dragOver(DropTargetEvent e) { |
| updateDragDetails(e); |
| } |
| |
| @Override |
| public void dragLeave(DropTargetEvent e) { |
| if (e.detail == DND.DROP_NONE) { |
| setDropOperation(e); |
| } |
| } |
| |
| @Override |
| public void dropAccept(DropTargetEvent e) { |
| updateDragDetails(e); |
| } |
| |
| @Override |
| public void dragOperationChanged(DropTargetEvent e) { |
| updateDragDetails(e); |
| } |
| |
| private void setDropOperation(DropTargetEvent e) { |
| int allowedOperations = e.operations; |
| for (int op : PREFERRED_DROP_OPERATIONS) { |
| if ((allowedOperations & op) != 0) { |
| traceDropOperation(op); |
| e.detail = op; |
| return; |
| } |
| } |
| e.detail = allowedOperations; |
| } |
| |
| private void updateDragDetails(DropTargetEvent e) { |
| if (dropTargetIsValid(e, false)) { |
| setDropOperation(e); |
| } |
| } |
| |
| private boolean dropTargetIsValid(DropTargetEvent e, boolean isDrop) { |
| if (URLTransfer.getInstance().isSupportedType(e.currentDataType)) { |
| //on Windows, we get the URL already during drag operations... |
| //FIXME find a way to check the URL early on other platforms, too... |
| if (isDrop || Util.isWindows()) { |
| if (e.data == null && !extractEventData(e)) { |
| traceMissingEventData(e); |
| //... but if we don't, it's no problem, unless this is already |
| //the final drop event |
| return !isDrop; |
| } |
| final String url = getUrl(e.data); |
| if (acceptSolutionUrl(url)) { |
| return true; |
| } else if (acceptFavoritesListUrl(url)) { |
| return true; |
| } else { |
| traceInvalidEventData(e); |
| return false; |
| } |
| } |
| return true; |
| } |
| traceUnsupportedDataType(e); |
| return false; |
| } |
| |
| private boolean extractEventData(DropTargetEvent e) { |
| TransferData transferData = e.currentDataType; |
| if (transferData != null) { |
| Object data = URLTransfer.getInstance().nativeToJava(transferData); |
| if (data != null && getUrl(data) != null) { |
| e.data = data; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public void drop(DropTargetEvent event) { |
| if (!URLTransfer.getInstance().isSupportedType(event.currentDataType)) { |
| traceUnsupportedDataType(event); |
| //ignore |
| return; |
| } |
| if (event.data == null) { |
| traceMissingEventData(event); |
| //reject |
| event.detail = DND.DROP_NONE; |
| return; |
| } |
| if (!dropTargetIsValid(event, true)) { |
| //reject |
| event.detail = DND.DROP_NONE; |
| return; |
| } |
| final String url = getUrl(event.data); |
| if (acceptSolutionUrl(url)) { |
| //http://marketplace.eclipse.org/marketplace-client-intro?mpc_install=1640500 |
| DropTarget source = (DropTarget) event.getSource(); |
| Display display = source.getDisplay(); |
| display.asyncExec(() -> proceedInstallation(url)); |
| } else if (acceptFavoritesListUrl(url)) { |
| //https://marketplace.eclipse.org/user/xxx/favorites |
| DropTarget source = (DropTarget) event.getSource(); |
| Display display = source.getDisplay(); |
| display.asyncExec(() -> proceedFavorites(url)); |
| } else { |
| traceInvalidEventData(event); |
| } |
| } |
| |
| private void traceDropOperation(int op) { |
| if (MarketplaceClientUiPlugin.DEBUG) { |
| MarketplaceClientUiPlugin.trace(MarketplaceClientUiPlugin.DROP_ADAPTER_DEBUG_OPTION, |
| "Updating drop event: Setting drop operation to {0}", op); //$NON-NLS-1$ |
| } |
| } |
| |
| private void traceInvalidEventData(DropTargetEvent event) { |
| if (MarketplaceClientUiPlugin.DEBUG) { |
| MarketplaceClientUiPlugin.trace(MarketplaceClientUiPlugin.DROP_ADAPTER_DEBUG_OPTION, |
| "Drop event: Data is not a solution url: {0}", event.data, new Throwable()); //$NON-NLS-1$ |
| } |
| } |
| |
| private void traceMissingEventData(DropTargetEvent event) { |
| if (MarketplaceClientUiPlugin.DEBUG) { |
| MarketplaceClientUiPlugin.trace(MarketplaceClientUiPlugin.DROP_ADAPTER_DEBUG_OPTION, |
| "Missing drop event data {0}", event.data, new Throwable()); //$NON-NLS-1$ |
| } |
| } |
| |
| private void traceUnsupportedDataType(DropTargetEvent event) { |
| if (MarketplaceClientUiPlugin.DEBUG) { |
| MarketplaceClientUiPlugin.trace(MarketplaceClientUiPlugin.DROP_ADAPTER_DEBUG_OPTION, |
| "Unsupported drop data type {0}", traceTransferData(event.currentDataType), new Throwable()); //$NON-NLS-1$ |
| } |
| } |
| |
| private Object traceTransferData(TransferData data) { |
| if (MarketplaceClientUiPlugin.DEBUG) { |
| return new TransferDataTraceFormatter(data); |
| } |
| return null; |
| } |
| |
| private String getUrl(Object eventData) { |
| if (eventData == null) { |
| return null; |
| } |
| if (eventData == null || !(eventData instanceof String)) { |
| return null; |
| } |
| // Depending on the form the link and browser/os, |
| // we get the url twice in the data separated by new lines |
| String[] dataLines = ((String) eventData).split(System.getProperty("line.separator")); //$NON-NLS-1$ |
| String url = dataLines[0]; |
| return url; |
| } |
| } |
| |
| private static final class TransferDataTraceFormatter { |
| private static final Field TYPE_FIELD; |
| |
| static { |
| Field typeField = null; |
| try { |
| typeField = TransferData.class.getDeclaredField("type"); //$NON-NLS-1$ |
| typeField.setAccessible(true); |
| } catch (Exception e) { |
| } |
| TYPE_FIELD = typeField; |
| } |
| |
| private final TransferData transferData; |
| |
| public TransferDataTraceFormatter(TransferData transferData) { |
| this.transferData = transferData; |
| } |
| |
| @Override |
| public String toString() { |
| if (transferData == null) { |
| return null; |
| } |
| return "TransferData[type=" + getType() + "]"; //$NON-NLS-1$//$NON-NLS-2$ |
| } |
| |
| private String getType() { |
| if (TYPE_FIELD == null) { |
| return "<unknown>"; //$NON-NLS-1$ |
| } |
| try { |
| Object type = TYPE_FIELD.get(transferData); |
| return type == null ? null : type.toString(); |
| } catch (IllegalArgumentException e) { |
| return "<unknown:" + transferData.getClass() + ">"; //$NON-NLS-1$ //$NON-NLS-2$ |
| } catch (IllegalAccessException e) { |
| return "<inaccessible>"; //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private class WorkbenchListener implements IPartListener2, IPageListener, IPerspectiveListener, IWindowListener { |
| |
| @Override |
| public void perspectiveActivated(IWorkbenchPage page, IPerspectiveDescriptor perspective) { |
| pageChanged(page); |
| } |
| |
| @Override |
| public void perspectiveChanged(IWorkbenchPage page, IPerspectiveDescriptor perspective, String changeId) { |
| } |
| |
| @Override |
| public void pageActivated(IWorkbenchPage page) { |
| pageChanged(page); |
| } |
| |
| @Override |
| public void pageClosed(IWorkbenchPage page) { |
| } |
| |
| @Override |
| public void pageOpened(IWorkbenchPage page) { |
| pageChanged(page); |
| } |
| |
| private void pageChanged(IWorkbenchPage page) { |
| if (page == null) { |
| return; |
| } |
| IWorkbenchWindow workbenchWindow = page.getWorkbenchWindow(); |
| windowChanged(workbenchWindow); |
| } |
| |
| @Override |
| public void windowActivated(IWorkbenchWindow window) { |
| windowChanged(window); |
| } |
| |
| private void windowChanged(IWorkbenchWindow window) { |
| if (window == null) { |
| return; |
| } |
| Shell shell = window.getShell(); |
| runUpdate(shell); |
| } |
| |
| @Override |
| public void windowDeactivated(IWorkbenchWindow window) { |
| } |
| |
| @Override |
| public void windowClosed(IWorkbenchWindow window) { |
| } |
| |
| @Override |
| public void windowOpened(IWorkbenchWindow window) { |
| hookWindow(window); |
| } |
| |
| public void hookWindow(IWorkbenchWindow window) { |
| if (window == null) { |
| return; |
| } |
| window.addPageListener(this); |
| window.addPerspectiveListener(this); |
| IPartService partService = window.getService(IPartService.class); |
| partService.addPartListener(this); |
| windowChanged(window); |
| } |
| |
| @Override |
| public void partOpened(IWorkbenchPartReference partRef) { |
| partUpdate(partRef); |
| } |
| |
| @Override |
| public void partActivated(IWorkbenchPartReference partRef) { |
| partUpdate(partRef); |
| } |
| |
| @Override |
| public void partBroughtToTop(IWorkbenchPartReference partRef) { |
| partUpdate(partRef); |
| } |
| |
| @Override |
| public void partVisible(IWorkbenchPartReference partRef) { |
| } |
| |
| @Override |
| public void partClosed(IWorkbenchPartReference partRef) { |
| partUpdate(partRef); |
| } |
| |
| @Override |
| public void partDeactivated(IWorkbenchPartReference partRef) { |
| partUpdate(partRef); |
| } |
| |
| @Override |
| public void partHidden(IWorkbenchPartReference partRef) { |
| partUpdate(partRef); |
| } |
| |
| @Override |
| public void partInputChanged(IWorkbenchPartReference partRef) { |
| } |
| |
| private void partUpdate(IWorkbenchPartReference partRef) { |
| if (partRef == null) { |
| return; |
| } |
| IWorkbenchPage page = partRef.getPage(); |
| pageChanged(page); |
| } |
| |
| private void runUpdate(final Shell shell) { |
| if (shell == null || shell.isDisposed()) { |
| return; |
| } |
| Display display = shell.getDisplay(); |
| if (display == null || display.isDisposed()) { |
| return; |
| } |
| try { |
| display.asyncExec(() -> { |
| if (!shell.isDisposed()) { |
| installDropTarget(shell); |
| } |
| }); |
| } catch (SWTException ex) { |
| if (ex.code == SWT.ERROR_DEVICE_DISPOSED) { |
| //ignore |
| return; |
| } |
| MarketplaceClientUi.error(ex); |
| } catch (RuntimeException ex) { |
| MarketplaceClientUi.error(ex); |
| } |
| } |
| } |
| } |