All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.diffplug.common.swt.dnd.StructuredDrop Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 DiffPlug
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.diffplug.common.swt.dnd;


import com.diffplug.common.collect.ImmutableList;
import com.diffplug.common.collect.ImmutableMap;
import com.diffplug.common.collect.Immutables;
import com.diffplug.common.swt.SwtMisc;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.DropTargetListener;
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.dnd.TransferData;
import org.eclipse.swt.widgets.Control;

/**
 * Typed mechanism for implementing drop listeners.
 * 
 * https://eclipse.org/articles/Article-SWT-DND/DND-in-SWT.html
 */
public class StructuredDrop {
	public enum DropMethod {
		dragEnter, dragLeave, dragOperationChanged, dragOver, drop, dropAccept
	}

	@FunctionalInterface
	public interface TypedDropHandler {
		/** If it's the result of a "paste", then DropTargetEvent will be null. */
		void onEvent(DropMethod method, @Nullable DropTargetEvent e, T value);

		default  TypedDropHandler map(Function mapper) {
			return new MappedTypedDropHandler(this, mapper);
		}
	}

	private static class MappedTypedDropHandler implements TypedDropHandler {
		TypedDropHandler delegate;
		Function mapper;

		MappedTypedDropHandler(TypedDropHandler delegate, Function mapper) {
			this.delegate = Objects.requireNonNull(delegate);
			this.mapper = Objects.requireNonNull(mapper);
		}

		@Override
		public void onEvent(DropMethod method, DropTargetEvent e, R value) {
			T mapped = mapper.apply(value);
			delegate.onEvent(method, e, mapped);
		}
	}

	private static class Handler {
		BiFunction valueGetter;
		TypedDropHandler handler;

		@SuppressWarnings("unchecked")
		public Handler(BiFunction valueGetter, TypedDropHandler handler) {
			this.valueGetter = (BiFunction) valueGetter;
			this.handler = (TypedDropHandler) handler;
		}
	}

	public class TypeMapper {
		final TypedDropHandler onEvent;

		private TypeMapper(TypedDropHandler onEvent) {
			this.onEvent = onEvent;
		}

		private  TypeMapper mapFromIfNotDuplicate(Transfer transfer, TypedDropHandler dropHandler, Function, TypeMapper> add) {
			if (hasHandlerFor(transfer)) {
				return new TypeMapper(dropHandler);
			} else {
				return add.apply(dropHandler);
			}
		}

		public  TypeMapper mapFrom(TypedTransfer transfer, Function mapper) {
			return mapFromIfNotDuplicate(transfer, onEvent.map(mapper),
					mapped -> StructuredDrop.this.add(transfer, mapped));
		}

		public TypeMapper mapFromText(Function mapper) {
			return mapFromIfNotDuplicate(TextTransfer.getInstance(), onEvent.map(mapper),
					mapped -> StructuredDrop.this.addText(mapped));
		}

		public TypeMapper> mapFromFile(Function, ? extends T> mapper) {
			return mapFromIfNotDuplicate(FileTransfer.getInstance(), onEvent.map(mapper),
					mapped -> StructuredDrop.this.addFile(mapped));
		}
	}

	private ImmutableMap.Builder builder = ImmutableMap.builder();
	private Map addedAt = new HashMap<>();
	private Listener impl;

	/** Returns true if it contains a handler for this transfer type. */
	public boolean hasHandlerFor(Transfer transfer) {
		if (builder != null) {
			return addedAt.containsKey(transfer);
		} else {
			return impl.handlers.containsKey(transfer);
		}
	}

	/** Adds a drop for the given transfer. */
	public  TypeMapper add(TTransfer transfer, BiFunction valueGetter, TypedDropHandler onEvent) {
		if (builder == null) {
			throw new IllegalStateException("Can't add new transfers after calling 'applyTo' or 'getListener'");
		}
		// check for duplicate entries 
		Exception previous = addedAt.put(transfer, new IllegalArgumentException());
		if (previous != null) {
			throw new IllegalArgumentException("Duplicate for " + transfer, previous);
		}
		// do the actual work 
		builder.put(transfer, new Handler(valueGetter, onEvent));
		return new TypeMapper<>(onEvent);
	}

	/** Adds a drop for the given filetype. */
	public  TypeMapper add(TypedTransfer transfer, TypedDropHandler onEvent) {
		TypeMapper typeMapper = add(transfer, TypedTransfer::getValue, onEvent);
		transfer.mapDrop(typeMapper);
		return typeMapper;
	}

	/** Adds the ability to drop text. */
	public TypeMapper addText(TypedDropHandler onEvent) {
		return add(TextTransfer.getInstance(), (transfer, e) -> (String) e.data, onEvent);
	}

	/** Adds the ability to drop files. */
	public TypeMapper> addFile(TypedDropHandler> onEvent) {
		return add(FileTransfer.getInstance(), (transfer, e) -> {
			if (e.data == null) {
				return ImmutableList.of();
			}
			return convertNativeToFiles((String[]) e.data);
		}, onEvent);
	}

	private static ImmutableList convertNativeToFiles(String[] paths) {
		return Arrays.stream(paths)
				.map(File::new)
				.collect(Immutables.toList(paths.length));
	}

	public Listener getListener() {
		if (impl == null) {
			impl = new Listener(builder.build());
			builder = null;
			addedAt = null;
		}
		return impl;
	}

	public void applyTo(Control control) {
		DropTarget dropTarget = new DropTarget(control, DndOp.dropAll());
		dropTarget.setTransfer(getListener().transferArray());
		dropTarget.addDropListener(getListener());
	}

	public void applyTo(Control... controls) {
		for (Control control : controls) {
			applyTo(control);
		}
	}

	/** Sets event.currentDataType to the most preferred possible. Returns the Transfer that was actually used. */
	public static Transfer preferDropTransfer(DropTargetEvent event, Transfer[] preferreds) {
		if (event.dataTypes == null) {
			return null;
		}
		for (Transfer transfer : preferreds) {
			// for each transfer
			for (TransferData data : event.dataTypes) {
				// find a supported TransferData
				if (transfer.isSupportedType(data)) {
					// set currentDataType and return the Transfer if we're successful
					event.currentDataType = data;
					return transfer;
				}
			}
		}

		// return nulls if we failed
		event.currentDataType = null;
		return null;
	}

	public static  TypedDropHandler handler(DndOp op, Consumer onValue) {
		return handler(op, (unusedEvent, value) -> onValue.accept(value));
	}

	public static  TypedDropHandler handler(DndOp op, BiConsumer onValue) {
		return new AbstractTypedDropHandler(op) {
			@Override
			protected boolean accept(T value) {
				return true;
			}

			@Override
			protected void drop(DropTargetEvent event, T value, boolean moved) {
				onValue.accept(event, value);
			}
		};
	}

	public static class Listener implements DropTargetListener {
		final ImmutableMap handlers;
		final Transfer[] transfers;

		public Listener(ImmutableMap map) {
			this.handlers = map;
			transfers = map.keySet().toArray(new Transfer[map.size()]);
		}

		public void pasteFromClipboard() {
			Clipboard clipboard = new Clipboard(SwtMisc.assertUI());
			try {
				for (Transfer transfer : transfers) {
					Object data = clipboard.getContents(transfer);
					if (data != null) {
						if (transfer instanceof FileTransfer) {
							data = convertNativeToFiles((String[]) data);
						}
						handlers.get(transfer).handler.onEvent(DropMethod.drop, null, data);
						break;
					}
				}
			} finally {
				clipboard.dispose();
			}
		}

		public Transfer[] transferArray() {
			return Arrays.copyOf(transfers, transfers.length);
		}

		@Override
		public void dragEnter(DropTargetEvent event) {
			onEvent(DropMethod.dragEnter, event);
		}

		@Override
		public void dragOver(DropTargetEvent event) {
			onEvent(DropMethod.dragOver, event);
		}

		@Override
		public void dragOperationChanged(DropTargetEvent event) {
			onEvent(DropMethod.dragOperationChanged, event);
		}

		@Override
		public void dropAccept(DropTargetEvent event) {
			onEvent(DropMethod.dropAccept, event);
		}

		@Override
		public void dragLeave(DropTargetEvent event) {
			onEvent(DropMethod.dragLeave, event);
		}

		@Override
		public void drop(DropTargetEvent event) {
			onEvent(DropMethod.drop, event);
		}

		Transfer lastPreferred;

		private void onEvent(DropMethod method, DropTargetEvent event) {
			Transfer preferred = StructuredDrop.preferDropTransfer(event, transfers);
			if (preferred == null) {
				preferred = lastPreferred;
			}
			if (preferred != null) {
				lastPreferred = preferred;
				Handler handler = handlers.get(preferred);
				Object value = handler.valueGetter.apply(preferred, event);
				handler.handler.onEvent(method, event, value);
			} else {
				event.detail = DND.DROP_NONE;
			}
		}
	}

	public abstract static class AbstractTypedDropHandler implements TypedDropHandler {
		protected final DndOp operation;

		public AbstractTypedDropHandler(DndOp operation) {
			this.operation = Objects.requireNonNull(operation);
		}

		@Override
		public void onEvent(DropMethod method, @Nullable DropTargetEvent e, T value) {
			if (method == DropMethod.dragLeave) {
				return;
			} else {
				if (value == null) {
					// this means that we don't know what the data is yet
					if (e != null) {
						operation.trySetDetail(e);
					}
				} else if (accept(value)) {
					if (e == null) {
						drop(null, value, false);
					} else if (operation.trySetDetail(e) && method == DropMethod.drop) {
						boolean moved = e.detail == DND.DROP_MOVE;
						drop(e, value, moved);
					}
				}
			}
		}

		protected abstract boolean accept(T value);

		protected abstract void drop(DropTargetEvent event, T value, boolean moved);
	}
}