/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community 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
*
* http://www.osedu.org/licenses/ECL-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 tufts.vue;
import tufts.Util;
import static tufts.Util.*;
import tufts.vue.gui.GUI;
import static tufts.vue.gui.GUI.dragName;
import tufts.vue.NodeTool.NodeModeTool;
import java.awt.dnd.*;
import java.awt.datatransfer.*;
import java.awt.geom.Point2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.Shape;
import java.awt.Point;
import java.awt.Image;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.regex.*;
import java.io.File;
import java.io.FileInputStream;
import static java.awt.dnd.DnDConstants.*;
import java.net.*;
/**
* Handle the dropping of drags mediated by host operating system onto the map.
*
* We currently handling the dropping of File lists, LWComponent lists,
* Resource lists, and text (a String).
*
* @version $Revision: 1.135 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $
*/
public class MapDropTarget
implements java.awt.dnd.DropTargetListener
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MapDropTarget.class);
private static final boolean DropImagesAsNodes = true;
private static final int DROP_FILE_LIST = 1;
private static final int DROP_NODE_LIST = 2;
private static final int DROP_RESOURCE_LIST = 3;
private static final int DROP_TEXT = 4;
private static final int DROP_ONTOLOGY_TYPE = 5;
private static final int DROP_GENERAL_HANDLER = 6;
public static final int ALL_DROP_TYPES =
DnDConstants.ACTION_COPY // 0x1
| DnDConstants.ACTION_MOVE // 0x2
| DnDConstants.ACTION_LINK // 0x40000000
;
public static final int ACCEPTABLE_DROP_TYPES =
DnDConstants.ACTION_COPY // 0x1
| DnDConstants.ACTION_MOVE // 0x2
| DnDConstants.ACTION_LINK // 0x40000000
;
//public static final DataFlavor URLDataFlavor = GUI.makeDataFlavor(java.net.URL.class);
//public static final DataFlavor URLDataFlavor = GUI.makeDataFlavor("application/x-java-url");
// Calls to acceptDrag / rejectDrag do absolutely nothing as far as I can tell
// (at last on MacOSX). The cursor certianly doesn't change, which is what we
// want (to make the "copy" cursor show). Apparently the drag & drop code gets
// a major overhaul in Java 1.6 (Mustang), and I can see why.
// [ OLD COMMENT ] Do NOT include MOVE, or dragging a URL from the IE address bar
// becomes a denied drag option! Even dragged text from IE becomes disabled. FYI,
// also, "move" doesn't appear to actually ever mean delete original source.
// Putting this in makes certial special windows files "available" for drag (e.g., a
// desktop "hard" reference link), yet there are never any data-flavors available to
// process it, so we might as well indicate that we can't accept it.
private boolean CenterNodesOnDrop = true;
private final MapViewer mViewer;
// /** for Windows, track the last dropAction we got during dragOver, as it can somehow be reverted by
// * the time we get the drop event */
// private int dropActionOverride = 0;
//private LWComponent DropHit;
private DropHandler mActiveHandler;
public MapDropTarget(MapViewer viewer) {
mViewer = viewer;
}
public static final String DROP_REJECT = "DROP_REJECT";
public static final String DROP_ACCEPT_NODE = "DROP_ACCEPT_NODE";
public static final String DROP_ACCEPT_DATA = "DROP_ACCEPT_DATA";
public static final String DROP_RESOURCE_RESET = "DROP_RESOURCE_RESET";
public static class DropIndication {
private static final DropIndication REJECTED = new DropIndication();
static DropIndication last;
public final Object type;
/** This is the action we want to ACCEPT: it may be different from the
* user-indicated drop action, and may not even be one of the availble source
* actions, tho we can accept any action we like, and the drag/drop cursor
* should be set appropriately */
public final int acceptedAction; // e.g. link or copy
/** This accepted hit target, which will depend on the drop action and drop target */
public final LWComponent hit;
public DropIndication(Object t, int action, LWComponent target) {
type = t;
acceptedAction = action;
hit = target;
last = this;
}
public static DropIndication rejected() { return REJECTED; }
// a rejected drag
private DropIndication() {
type = DROP_REJECT;
acceptedAction = ACTION_NONE;
hit = null;
last = this;
}
boolean isAccepted() {
return type != DROP_REJECT;
}
// boolean isResourceRelink() {
// return dropType == DROP_ACCEPT_NEW_NODE && acceptedAction == ACTION_LINK;
// }
/** @return true if type and hit are the same */
boolean isSame(DropIndication di) {
return di.type == type && di.hit == hit;
}
java.awt.Color getColor() {
if (type == DROP_RESOURCE_RESET)
return VueConstants.COLOR_INDICATION_ALTERNATE;
// else if (type == DROP_ACCEPT_DATA)
// return java.awt.Color.red;
else
return VueConstants.COLOR_INDICATION;
}
@Override public String toString() {
return "Indication[" + type + "; " + dropName(acceptedAction) + "; hit=" + LWComponent.tag(hit) + "]";
}
}
/** DropTargetListener */
public void dragEnter(DropTargetDragEvent e)
{
final Transferable transfer = e.getTransferable();
if (transfer.isDataFlavorSupported(DropHandler.DataFlavor))
mActiveHandler = extractData(transfer, DropHandler.DataFlavor, DropHandler.class); // will be null if not found
final DropIndication di = getIndication(e);
if (DEBUG.DND) out("dragEnter: " + dragName(e) + "; handler=" + mActiveHandler + " " + di);
e.acceptDrag(di.acceptedAction);
}
/** DropTargetListener */
public void dragOver(DropTargetDragEvent e)
{
final DropIndication di = getIndication(e);
if (DEBUG.DND) out("dragOver: " + dragName(e) + " " + di);
if (di.isAccepted()) {
e.acceptDrag(di.acceptedAction);
mViewer.setIndicated(di);
} else {
mViewer.clearIndicated();
e.rejectDrag();
}
}
/** DropTargetListener */
public void dropActionChanged(DropTargetDragEvent e) {
if (DEBUG.DND) out("dropActionChanged: " + dragName(e));
// Just re-use our dragOver code:
// (e.g., in case action type has changed and we want to change indication color)
dragOver(e);
}
/** DropTargetListener */
public void dragExit(DropTargetEvent e) {
if (DEBUG.DND) out("dragExit: " + e);
}
/** DropTargetListener */
public void drop(DropTargetDropEvent e)
{
try
{
GUI.activateWaitCursor();
/* UnsupportedOperation (tring to discover key's being held down ourselves) try {
System.out.println("caps state="+mViewer.getToolkit()
.getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
} catch (Exception ex) { System.err.println(ex); }*/
final DropIndication di = getIndication(e);
if (DEBUG.DND) out(TERM_GREEN + "\nDROP: " + Util.tag(e)
+ "\n\t sourceActions: " + dropName(e.getSourceActions())
+ "\n\t dropAction: " + dropName(e.getDropAction())
+ "\n\t dropAccept: " + dropName(di.acceptedAction)
// + (Util.isWindowsPlatform() ?
// "\n\tdropActionOverride: " + dropName(dropActionOverride) : "")
+ "\n\t location: " + e.getLocation()
+ TERM_CLEAR
);
e.acceptDrop(di.acceptedAction);
// Scan thru the data-flavors, looking for a useful mime-type
boolean success =
processTransferable(e.getTransferable(), e);
if (DEBUG.DND) out(TERM_CYAN + "processTransferable: success=" + success + TERM_CLEAR);
e.dropComplete(success);
mViewer.clearIndicated();
}
finally
{
GUI.clearWaitCursor();
}
}
private static final Object POSSIBLE_RESOURCE = new Object();
private DropIndication getIndication(DropTargetDragEvent e) {
return getIndication(e, e.getSourceActions(), e.getDropAction(), dropToMapLocation(e.getLocation()));
}
private DropIndication getIndication(DropTargetDropEvent e) {
return getIndication(e, e.getSourceActions(), e.getDropAction(), dropToMapLocation(e.getLocation()));
}
private DropIndication getIndication
(final DropTargetEvent e,
final int sourceAbleActions,
final int dropAction,
final Point2D.Float mapLoc)
{
int dropAccept = dropAction;
if (dropAction == ACTION_MOVE && (sourceAbleActions & ACTION_COPY) != 0) {
dropAccept = ACTION_COPY; // will show '+' cursor
}
else if (dropAction == ACTION_NONE) {
// NOTE: The user cannot currently generate ACTION_LINK when dragging an image from the
// Safari web browser content window (tho you can when dragging from the address bar).
// Changing the action in any way via user modifier keys leaves us only with
// ACTION_NONE, so we always override that here to mean ACTION_LINK, assuming the
// special case was intended. SMF 2008-05-07
dropAccept = ACTION_LINK;
}
final PickContext pc = mViewer.getPickContext(mapLoc.x, mapLoc.y);
pc.dropping = POSSIBLE_RESOURCE; // most lenient targeting if unknown
final LWComponent hit = LWTraversal.PointPick.pick(pc);
LWComponent dropHit = null;
//boolean acceptThisDrop = true;
if (mActiveHandler != null) {
return mActiveHandler.getIndication(hit, dropAccept);
// final int handlerAction = mActiveHandler.acceptedDropAction(hit, dropAccept);
// if (handlerAction > 0) {
// dropHit = hit;
// dropAccept = handlerAction;
// } else
// acceptThisDrop = false;
} else {
if (hit instanceof LWImage) {
final LWImage image = (LWImage) hit;
if (image.isNodeIcon()) {
dropHit = hit.getParent();
}
else if (image.hasImageError() || dropAccept == ACTION_LINK) {
dropHit = hit;
dropAccept = ACTION_LINK;
}
} else if (hit instanceof LWLink) {
dropHit = hit;
// drop-action is "link" for setting the resource -- not to be confused
// with the fact that his happens to be a LWLink object. LWLink's
// can't have children, so the only allowable action is a resource-set.
dropAccept = ACTION_LINK;
} else if (hit != null) {
if (hit.supportsChildren() && hit != mViewer.getFocal()) {
// above: we can always drop into focals that support children
if (hit instanceof LWSlide)
; // disable slide icon dropping for now
// (mapToLocalLocation doesn't seem to be working in this case)
else if (hit instanceof LWGroup)
; // don't allow drops into groups sitting on maps (when not the focal)
else
dropHit = hit;
}
// This case not currently needed, as our LWImage case above was only supportsChildre() == false
// object, and anytime the hit results in null, we automatically go to the focal anyway below.
// else {
// if (hit.getAncestorOfType(LWSlide.class) == mViewer.getFocal()) {
// // make sure we can always drop onto slides, even if "hit" a non-parenting
// // This should probably be even more generic, but need to reconcile/merge
// // this code with MapDropTarget.processTransferrable, which calls us.
// hit = mViewer.getFocal();
// }
}
// DropHit currently must be null if we want our old hairy code below to
// properly create new objects in the focal.
// if (DropHit == null)
// DropHit = mViewer.getFocal();
}
final DropIndication report;
Object type = DROP_ACCEPT_NODE;
if (dropAccept == ACTION_LINK) {
if ((hit instanceof LWNode || hit instanceof LWImage) && hit.hasResource()) {
type = DROP_RESOURCE_RESET;
} else {
// better to allow the link icon as user feedback that the option is being attempted
//dropAccept = ACTION_COPY; // disallow the link action if it won't work
}
}
if (type != null)
report = new DropIndication(type, dropAccept, hit);
else
report = new DropIndication();
// if (DEBUG.DND) out(report + " sourceAbleActions=" + dropName(sourceActions));
// if (DEBUG.DND) out(Util.tag(e) + "; sourceActions=" + dropName(sourceActions)
// + "; drop=" + dropName(dropAction)
// + "; accept=" + dropName(dropAccept)
// + "; accepting=" + acceptThisDrop
// //+ "; hit=" + hit
// + "; target=[" + (dropHit == null ? null : dropHit.getDiagnosticLabel()) + "]"
// );
return report;
}
public static class DropContext {
public final Transferable transfer;
public final Point2D.Float location; // map location of the drop
public final MapViewer viewer; // we dropped into this component
public /*final*/ LWComponent hit; // we dropped into this component
public /*final*/ LWContainer hitParent; // we dropped into this component, and it can take children
public final boolean isLinkAction; // user kbd modifiers down produced LINK drop action
public List items; // convience reference to items if it is a List
public final String text; // only one of items+list or text
private float nextX;
private float nextY;
public List select = new java.util.ArrayList(); // what to select
DropContext(Transferable t,
Point2D.Float mapLocation,
MapViewer viewer,
List items,
String text,
LWComponent hit,
boolean isLinkAction
//,LWComponent.Producer producer
)
{
this.transfer = t;
this.location = mapLocation;
this.viewer = viewer;
this.items = items;
this.text = text;
this.hit = hit;
//this.producer = producer;
// if (items instanceof java.util.List)
// list = (List) items;
// else
// list = null;
if (hit != null && hit.supportsChildren())
hitParent = (LWContainer) hit;
else
hitParent = null;
this.isLinkAction = isLinkAction;
if (mapLocation != null) {
nextX = mapLocation.x;
nextY = mapLocation.y;
}
if (DEBUG.DND) System.out.println( "DropContext: loc: " + Util.fmt(mapLocation)
+ "\n hit: " + hit
+ "\n hitParent: " + hitParent
);
}
Point2D nextDropLocation() {
Point2D p = new Point.Float(nextX, nextY);
// todo: either track height of last item created on drop,
// so can adjust for actual height, or just "make-column"
// on the whole drop after it's done.
nextX += 15;
nextY += 15;
return p;
}
/**
* Track top-level nodes created and added to map as we processed the drop.
* Note that nodes are created and added to the map as the drop is processed in
* case we fail we can get some partial results. This lets us set the selection
* to everything that was dropped at the end.
*/
void add(LWComponent c) {
//added.add(c);
select.add(c);
}
}
private static final Object DATA_FAILURE = new Object();
/** Extract data and auto-cast to the desired type. If a type-mistmatch, reporting a warning and return null. */
// Altho tho the class information is normally stored in the flavor via it's represenation,
// we can't make use of generics to auto-cast and auto-report any errors w/out the 3rd explicit class object (type) argument.
public static <A> A extractData(Transferable transfer, DataFlavor flavor, Class<A> clazz) {
final Object data = extractData(transfer, flavor);
if (clazz.isInstance(data)) {
return clazz.cast(data);
} else {
Log.warn("Transfer data expecting type " + clazz + "; found: " + Util.tags(data));
return null;
}
}
public static Object extractData(Transferable transfer, DataFlavor flavor)
{
Log.info("extractData " + flavor);
Object data = DATA_FAILURE;
try {
data = transfer.getTransferData(flavor);
} catch (UnsupportedFlavorException ex) {
Util.printStackTrace(ex, "TRANSFER: Transfer lied about supporting flavor "
+ "\"" + flavor.getHumanPresentableName() + "\" "
+ flavor
);
} catch (java.io.IOException ex) {
Util.printStackTrace(ex, "TRANSFER: data no longer available");
}
return data;
}
private static final Pattern HTML_Fragment
= Pattern.compile(".*<!--StartFragment-->(.*)<!--EndFragment-->",
Pattern.MULTILINE|Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
private static final Pattern IMG_Tag
= Pattern.compile(".*<img\\s+.*\\bsrc=\"([^\"]*)",
Pattern.MULTILINE|Pattern.DOTALL|Pattern.CASE_INSENSITIVE);
/** @return the string matched by the first group in the given Pattern, or null if no match */
private static final String extractText(Pattern pattern, String text) {
final Matcher m = pattern.matcher(text);
String s = null;
if (m.lookingAt())
return m.group(1);
else
return null;
}
/**
* Process any transferrable: @param e can be null if don't have a drop event
* (e.g., could use to process clipboard contents as well as drop events)
* A sucessful result will be newly created items on the map.
* @return true if succeeded
*/
public boolean processTransferable(Transferable transfer, DropTargetDropEvent e)
{
Point dropLocation = null;
Point2D.Float mapLocation = null;
//Point2D.Float focalLocation = null;
int dropAction = DnDConstants.ACTION_COPY; // default action, in case no DropTargetDropEvent
// On current JVM's on Mac and PC, default action for dragging a desktop item is
// MOVE, and holding CTRL down (both platforms) changes action to COPY.
// However, when dragging a link from Internet Explorer on Win2k, or Safari on
// OS X 10.4.2, CTRL doesn't change drop action at all, SHIFT changes drop
// action to NONE, and only CTRL-SHIFT changes action to LINK, which fortunately
// is at least the same on the mac: CTRL-SHIFT gets you LINK.
//
// Note: In both Safari & IE6/W2K, dragging an image from within a web page will
// NOT allow ACTION_LINK, so we can't change a resource that way on the mac.
// Also note that Safari will give you the real URL of the image, where as at
// least as of IE 6 on Win2k, it will only give you the image file from your
// cache. (IE does also give you HTML snippets as data transfer options, with
// the IMG tag, but it gives you no base URL to add to the relative locations
// usually named in IMG tags!)
// Also: Dragging a URL from Safari address bar CLAIMS to support COPY & LINK
// source actions, but drop action is fixed at COPY can never ba changed to LINK
// no matter what modifier keys you hold down (MacOSX 10.4, JVM 1.5.0_06-93)
if (e != null) {
dropLocation = e.getLocation();
dropAction = DropIndication.last.acceptedAction;
// if (dropActionOverride != 0) // TODO: handle above once in setting dropAccept
// dropAction = dropActionOverride;
// else
// dropAction = DropAccept;
//dropAction = e.getDropAction();
mapLocation = dropToMapLocation(dropLocation);
//focalLocation = dropToFocalLocation(dropLocation);
//if (DEBUG.DND) out(TERM_GREEN + "processTransferable: " + GUI.dropName(e) + TERM_CLEAR
if (DEBUG.DND) out("processTransferable: " + Util.tag(e)
+ "\n\t description: " + GUI.dropName(e)
+ "\n\t dropAction: " + dropName(e.getDropAction())
+ "\n\t dropAccept: " + DropIndication.last
//+ "\n\t dropAccept: " + dropName(DropAccept)
// + (Util.isWindowsPlatform() ?
// "\n\tdropActionOverride: " + dropName(dropActionOverride) : "")
+ "\n\t dropScreenLoc: " + Util.fmt(dropLocation)
+ "\n\t dropMapLoc: " + Util.fmt(mapLocation)
//+ "\n\t dropFocalLoc: " + Util.fmt(focalLocation)
);
} else {
if (DEBUG.DND) out("processTransferable: (no drop event) transfer=" + transfer);
}
final boolean isLinkAction = (dropAction == ACTION_LINK);
LWComponent dropTarget = null;
Point2D.Float hitLocation = null;
if (dropLocation != null) {
//dropTarget = pickDropTarget(mapLocation, isLinkAction);
dropTarget = DropIndication.last.hit;
if (DEBUG.DND) out("dropTarget=" + dropTarget + " in " + mViewer);
if (dropTarget != null) {
if (!dropTarget.supportsChildren() && !isLinkAction) {
// this SHOULD be preventing drops onto MapSlides, but dropTarget is coming
// back null because pickDropTarget is checking supportsChildren itself (and
// returning null) -- we might want to have a special DROP_DENIED return value
// from pickDropTarget, or change semantics to return NULL only when denied,
// and return the actual map/focal we want to hit/added to, but that value
// thread down through the DropContext to tons of code below that we need to
// check. In any case, code down below denying based on the focal being
// non-map when there's no target found handles this for now.
if (DEBUG.DND) out("dropTarget: doesn't support children: " + dropTarget);
return false;
}
hitLocation = mapToLocalLocation(mapLocation, dropTarget);
if (DEBUG.DND) out("dropTarget hit location: " + Util.fmt(hitLocation));
} else {
// drop target is null
if (mViewer.getFocal() instanceof LWMap == false) {
// this prevents drops on MapSlides, and off-slide when real slides are the focal
if (DEBUG.DND) out("warning: drop to non-map focal " + mViewer.getFocal());
hitLocation = mapToLocalLocation(mapLocation, mViewer.getFocal());
//if (DEBUG.DND) out("null dropTarget: default drop denied to non-map focal");
//return false;
}
}
/*
// handle via traversal picking code:
if (dropTarget instanceof LWImage) { // todo: does LWComponent accept drop events...
if (DEBUG.DND) out("dropHit=" + dropTarget + " (ignored)");
dropTarget = null;
} else
if (DEBUG.DND) out("dropHit=" + dropTarget);
*/
} else {
// if no drop location (e.g., we did a "Paste") then assume where
// they last clicked.
if (mViewer != null) {
dropLocation = mViewer.getLastMousePressPoint();
mapLocation = dropToFocalLocation(dropLocation);
}
}
if (hitLocation == null)
hitLocation = mapLocation;
DataFlavor foundFlavor = null;
Object foundData = null;
//LWComponent.Producer foundProducer = null;
DropHandler foundHandler = null;
String dropText = null;
List dropItems = null;
int dropType = 0;
if (DEBUG.DND && DEBUG.META) dumpFlavors(transfer);
// BTW, we could wait till after we check for all the local flavors which always take precedence
// before we bother to scan for these.
final DataFlavor[] dataFlavors = transfer.getTransferDataFlavors();
final DataFlavor URLFlavor = findFlavor(dataFlavors, "application/x-java-url", java.net.URL.class);
final DataFlavor HTMLTextFlavor = findFlavor(dataFlavors, "text/html", java.lang.String.class);
// DataFlavor URLDataFlavor = null;
// try {
// URLDataFlavor = new DataFlavor("application/x-java-url; class=java.net.URL");
// if (transfer.isDataFlavorSupported(URLDataFlavor))
// Log.info("GENERIC URL DATA FLAVOR SUPPORTED");
// } catch (Throwable t) {
// Util.printStackTrace(t);
// }
URL found_HTTP_URL = null;
// The fanciest we can ultimately do: search for text/html type that has
// <!--StartFragment-->..., and pull <img src=RealImageSource> out, which is
// especially handy for Wikipedia, and we'd stop trying to process
// wiki/Image:Ship.jpg crap, which is actually an HTML page. Can also pull out
// title="foo" from <href> tag or alt="foo" from <img> tag.
// AND, we can always scan the unicode string for a second line of text for a
// title (firefox puts title info here).
// And actually, if on Windows, prioritize text/html over local file list (espec
// if size==1), == generically scanning for <!--StartFragment--> will pull out
// an IMG tag also, allowing us to get the real URL, as opposed to a damn local
// cache file...
// TODO: ALWAYS PRE-EXTRACT ACTUAL URL OBJECTS from URLFlavor, as well as any
// <img src=...> found in any fragment, as well as the unicode string flavor,
// including any second line with title info, so we can compare/contrast and
// make use of as needed below, as the logic is going to get pretty ad-hoc
// hairy...
// Also: split out the native types we can just check first w/out any
// of the ad-hoc mess. Split out into methods that take and populate
// a drop-context, returning true if suceeded.
if (HTMLTextFlavor != null) {
// The MAIN reason we want to attempt the fragment is in case the stock
// incoming URL is in fact a file reference to a local browser cache file,
// which we're really not interested in. Consider only using the fragment if
// the stock URL in fact points to a local file, when the fragment points to
// an HTTP url, as sometimes the fragment is actually less useful data than
// what's in the stock URL (e.g., google news images, tho there's another
// special decoding opportunity there -- they appear to embed the original
// source image as "imgurl=" in the query, tho w/out "http" at the front).
// A notable reverse case is Wikipedia, where often the stock URL actually
// points to an HTML page even tho it looks like an image link, but the
// fragment points to the real uploaded image.
final String htmlText = extractData(transfer, HTMLTextFlavor, String.class);
if (htmlText != null) {
//Log.debug("FOUND HTML TEXT [" + htmlText + "]");
final String fragment = extractText(HTML_Fragment, htmlText);
if (fragment != null) {
Log.debug("FOUND HTML FRAGMENT [" + fragment + "]");
final String imgSrc = extractText(IMG_Tag, fragment);
if (imgSrc != null) {
Log.debug("FOUND IMG SRC=[" + imgSrc + "]");
if (imgSrc != null && imgSrc.toLowerCase().startsWith("http")) {
URL url = null;
try {
url = new java.net.URL(imgSrc);
} catch (Throwable t) {
Log.debug("invalid URL: " + imgSrc + "; " + t);
}
found_HTTP_URL = url;
}
}
}
}
}
try {
if (URLFlavor != null && found_HTTP_URL == null) {
URL url = null;
try {
url = extractData(transfer, URLFlavor, URL.class);
} catch (Throwable t) {
Log.warn("failure extracting " + URLFlavor, t);
}
if (url != null) {
if ("http".equals(url.getProtocol())) {
// we especially don't want file: URL's, as then we might
// try and process what is actually an entire list of locally
// dropped files as a single URL drop.
found_HTTP_URL = url;
Log.debug("FOUND HTTP URL FLAVOR/DATA: " + URLFlavor + "; URL=" + url);
}
}
}
// We want to repeatedly do the casts below for each case
// to make sure the data type we got is what we expected.
// (Can be a problem if somebody creates a bad Transferable)
if (transfer.isDataFlavorSupported(DropHandler.DataFlavor)) {
foundFlavor = DropHandler.DataFlavor;
foundHandler = extractData(transfer, foundFlavor, DropHandler.class);
dropType = DROP_GENERAL_HANDLER;
}
else if (transfer.isDataFlavorSupported(edu.tufts.vue.ontology.ui.TypeList.DataFlavor)
&& (dropAction == DnDConstants.ACTION_LINK))
{
dropType = DROP_ONTOLOGY_TYPE;
foundData = extractData(transfer, edu.tufts.vue.ontology.ui.TypeList.DataFlavor);
}
else if (transfer.isDataFlavorSupported(LWComponent.DataFlavor)) {
foundFlavor = LWComponent.DataFlavor;
foundData = extractData(transfer, foundFlavor);
dropType = DROP_NODE_LIST;
dropItems = (List) foundData;
}
else if (transfer.isDataFlavorSupported(Resource.DataFlavor)) {
foundFlavor = Resource.DataFlavor;
foundData = extractData(transfer, foundFlavor);
if (foundData == null)
throw new IllegalStateException("null resource found");
dropType = DROP_RESOURCE_LIST;
if (foundData instanceof List)
dropItems = (List) foundData;
else
dropItems = Collections.singletonList(foundData);
}
else if (found_HTTP_URL != null && !found_HTTP_URL.getHost().equals("images.google.com")) {
// don't use fragment URL if standard URL was from google image light-tray, as
// the fragment <img src=...> in this case is a reference to the internal google
// image icon stored at google, and we want the original image source...
dropType = DROP_TEXT;
final String http_url = found_HTTP_URL.toString();
if (transfer.isDataFlavorSupported(DataFlavor.stringFlavor)) {
final String txt = extractData(transfer, DataFlavor.stringFlavor, String.class);
// If the found URL is the same as unicode string, but unicode string
// is longer but same at head, it may contain a newline with title info
// that can be parsed on processDroppedText (better: generically extract
// a "title" during this process and set/pass on in the drop context)
if (txt.length() > http_url.length() && txt.startsWith(http_url)) {
foundData = txt;
dropText = txt;
foundFlavor = DataFlavor.stringFlavor;
}
}
if (dropText == null) {
foundFlavor = URLFlavor;
foundData = found_HTTP_URL;
dropText = http_url;
}
} else if (transfer.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
foundFlavor = DataFlavor.javaFileListFlavor;
foundData = extractData(transfer, foundFlavor);
dropType = DROP_FILE_LIST;
dropItems = (List) foundData;
} else if (transfer.isDataFlavorSupported(DataFlavor.stringFlavor)) {
foundFlavor = DataFlavor.stringFlavor;
foundData = extractData(transfer, foundFlavor);
dropType = DROP_TEXT;
dropText = (String) foundData;
} else {
if (DEBUG.Enabled) {
System.out.println("TRANSFER: found no supported dataFlavors");
dumpFlavors(transfer);
}
return false;
}
} catch (ClassCastException ex) {
Util.printStackTrace(ex, "TRANSFER: Transfer data did not match expected type:"
+ "\n\tflavor=" + foundFlavor
+ "\n\t type=" + foundData.getClass());
return false;
} catch (Throwable t) {
Util.printStackTrace(t, "TRANSFER: data extraction failure");
return false;
}
if (foundData == DATA_FAILURE)
return false;
if (DEBUG.Enabled) {
String size = "";
Object firstInBag = null;
String bagEntry0 = "";
if (foundData instanceof Collection) {
Collection bag = (Collection) foundData;
size = " (Collection size " + bag.size() + ")";
if (bag.size() > 0) {
final Object o = bag.iterator().next();
firstInBag = o;
bagEntry0 = "\n\tdata[0]: " + Util.tags(firstInBag);
}
}
Log.debug(TERM_CYAN + "\nTRANSFER: Found a supported DataFlavor among the " + dataFlavors.length + " available;"
+ "\n\t flavor: " + foundFlavor
+ "\n\tdataTag: " + Util.tag(foundData) + size
+ (DEBUG.META ? ("\n\tdataRaw: [" + foundData + "]") : "")
+ bagEntry0
+ TERM_CLEAR
);
}
DropContext drop =
new DropContext(transfer,
hitLocation,
mViewer,
dropItems,
dropText,
dropTarget,
isLinkAction
//,foundProducer
);
boolean success = false;
if (dropItems != null && dropItems.size() > 1)
;//CenterNodesOnDrop = false; // turned off 2008-09-30 for data-drops to be on-center
// TODO: fix for non-centered lists or fix code to handle centering them
else
CenterNodesOnDrop = true;
try {
switch (dropType) {
case DROP_GENERAL_HANDLER:
//success = processDroppedHandler(drop, foundHandler);
// handlers now process their own completion if they think they'll succeed:
return processDroppedHandler(drop, foundHandler);
case DROP_FILE_LIST:
success = processDroppedFileList(drop);
break;
case DROP_NODE_LIST:
success = processDroppedNodes(drop);
break;
case DROP_RESOURCE_LIST:
success = processDroppedResourceList(drop);
break;
case DROP_TEXT:
success = processDroppedText(drop);
break;
case DROP_ONTOLOGY_TYPE:
success = processDroppedOntologyType(drop,foundData);
break;
default:
// should never happen
throw new Error("unknown drop type " + dropType);
}
completeDrop(drop);
} catch (Throwable t) {
Util.printStackTrace(t, "drop processing failed");
}
// if (foundProducer != null) {
// try {
// foundProducer.postProcessNodes();
// } catch (Throwable t) {
// Log.error("Exception in postProcess for with node producer " + foundProducer, t);
// }
// }
// Even if we had an exception during processing,
// mark the drop in case there were partial results for Undo.
mViewer.getMap().getUndoManager().mark("Drop");
return success;
}
public static void completeDrop(DropContext drop) {
if (drop.items != null && drop.items.size() > 0) {
// Must make sure the selection is owned
// by this map before we try and change it.
// TODO: SlideViewer currently not handling this properly...
drop.viewer.grabVueApplicationFocus("drop", null);
}
if (drop.select != null && drop.select.size() > 0) {
drop.viewer.selectionSet(drop.select); // VUE-978: selection focal should also be set
}
// if (drop.select.size() > 0) {
// // Must make sure the selection is owned
// // by this map before we try and change it.
// // TODO: SlideViewer currently not handling this properly...
// mViewer.grabVueApplicationFocus("drop", null);
// mViewer.selectionSet(drop.select); // VUE-978: selection focal should also be set
// }
}
protected boolean processDroppedText(DropContext drop)
{
if (DEBUG.DND) out("processDroppedText");
// Attempt to make a URL of any string dropped -- if fails, just treat as
// regular pasted text. todo: if newlines in middle of string, don't do this,
// or possibly attempt to split into list of multiple URL's (tho only if *every*
// line succeeds as a URL -- prob too hairy to bother)
String[] rows = drop.text.split("\n");
URL foundURL = null;
Map properties = new HashMap();
if (rows.length < 3) {
foundURL = makeURL(rows[0]);
if (rows.length > 1) {
// Current version of Mozilla (at least on Windows XP, as of 2004-02-22)
// includes the HTML <title> as second row of text.
// TODO: pass this on to the Resource factory or actually handle
// this directly in the resource factory.
properties.put("title", rows[1]);
}
}
// TODO: foundURL may need to be decoded once to see query (e.g., 2008 Yahoo image search lightbox)
// why did we skip this if it was a link action? this is now breaking the dropping of light-box images
// entirely when we use the link action -- maybe it was for resource replacement? test with
// OSID's that have queries inthe URL's. -- SMF 2009-12-09
if (foundURL != null && foundURL.getQuery() != null /*&& !drop.isLinkAction*/) {
// if this URL is from a common search engine, we can find
// the original source for the image instead of the search
// engine's context page for the image.
foundURL = decodeSearchEngineLightBoxURL(foundURL, properties);
}
if (foundURL != null) {
boolean processed = true;
boolean overwriteResource = drop.isLinkAction;
if (drop.hit != null) {
if (overwriteResource) {
// TODO: master slides in slide-viewer are "hit", thus we can set a resource on them this way!
drop.hit.setResource(foundURL.toString());
// TODO: clean this up: resource should load meta-data on CREATION.
((URLResource)drop.hit.getResource()).scanForMetaDataAsync(drop.hit);
} else if (drop.hitParent != null) {
drop.hitParent.dropChild(createNodeAndResource(drop, null, foundURL.toString(), properties, drop.location));
} else {
processed = false;
}
}
if (drop.hit == null || !processed)
createNodeAndResource(drop, null, foundURL.toString(), properties, drop.location);
} else {
// create a text node
drop.add(createTextNode(drop.text, drop.location));
}
return true;
}
// todo: move to GUI package?
public abstract static class DropHandler {
public static final java.awt.datatransfer.DataFlavor DataFlavor =
tufts.vue.gui.GUI.makeDataFlavor(DropHandler.class);
public abstract boolean handleDrop(DropContext drop);
public abstract DropIndication getIndication(LWComponent target, int requestAction);
// protected void handlePostDrop() {
// if (drop.added.size() > 0) {
// // Must make sure the selection is owned
// // by this map before we try and change it.
// // TODO: SlideViewer currently not handling this properly...
// mViewer.grabVueApplicationFocus("drop", null);
// //VUE.getSelection().setTo(drop.added);
// mViewer.selectionSet(drop.added); // VUE-978: selection focal should also be set
// }
// }
}
private boolean processDroppedHandler(DropContext drop, DropHandler handler)
{
if (DEBUG.Enabled) out("processDroppedHandler: " + Util.tags(handler));
VUE.activateWaitCursor(); // todo: the existing drag/drop cursor seems to be interfering with this
boolean success = false;
try {
success = handler.handleDrop(drop);
// if (success) {
// // now handled by the drop-handler itself:
// //if (drop.items != null && drop.items.size() > 0)
// // addNodesToMap(drop);
// }
} catch (Throwable t) {
Log.error("dropHandler failed: " + Util.tags(handler), t);
}
finally {
VUE.clearWaitCursor();
}
return success;
}
public static void addNodesToMap(DropContext drop)
{
if (DEBUG.Enabled) Log.debug("addNodesToMap: " + Util.tags(drop.items));
if (drop.hitParent != null && !(drop.hitParent instanceof LWMap)) {
// todo: refactor: hitParent never accounted for layers -- this
// is probably for dropping onto slides -- can we get rid of using
// hitParent entirely?
drop.hitParent.addChildren(drop.items, LWComponent.ADD_DROP);
} else {
drop.viewer.getDropFocal().addChildren(drop.items, LWComponent.ADD_DROP);
}
}
private boolean processDroppedNodes(DropContext drop)
{
if (DEBUG.DND) out("processDroppedNodes");
// now add them to the map
// We'd like to always to the set center, in case hitParent isn't something
// that is going to auto-layout the new children
if (CenterNodesOnDrop)
setCenterAt(drop.items, drop.location);
else
setLocation(drop.items, drop.location);
addNodesToMap(drop);
drop.select.addAll(drop.items);
return true;
}
private boolean processDroppedResourceList(DropContext drop)
{
if (DEBUG.DND) out("processDroppedResourceList");
if (drop.items.size() == 1 && drop.hit != null && drop.isLinkAction) {
// Only one item is in the list, and we've hit a component, and
// it's a link-action drop: replace the hit component resource
drop.hit.setResource((Resource)drop.items.get(0));
} else {
Iterator i = drop.items.iterator();
while (i.hasNext()) {
Resource resource = (Resource) i.next();
// System.out.println("Following resource has been dropped"+ resource);
if (drop.hitParent != null && !drop.isLinkAction) {
// create new node children of the hit node
//drop.hitParent.addChild(createNode(drop, resource, null));
drop.hitParent.dropChild(createNode(drop, resource, drop.nextDropLocation()));
} else {
createNode(drop, resource, drop.nextDropLocation());
}
}
}
return true;
}
private boolean processDroppedOntologyType(DropContext drop,Object foundData)
{
if (DEBUG.DND) out("processDroppedType");
edu.tufts.vue.metadata.VueMetadataElement ele = new edu.tufts.vue.metadata.VueMetadataElement();
ele.setObject(foundData);
drop.hit.getMetadataList().getMetadata().add(ele);
edu.tufts.vue.metadata.ui.OntologicalMembershipPane.getGlobal().refresh();
return true;
}
private boolean processDroppedFileList(DropContext drop)
{
if (DEBUG.DND) out("processDroppedFileList");
for (Object o : drop.items) {
try {
processDroppedFile((File) o, drop);
} catch (Throwable t) {
Log.error("processing dropped file " + Util.tags(o), t);
}
}
return true;
}
private void processDroppedFile(File file, DropContext drop)
{
String resourceSpec = file.getPath();
String path = file.getPath();
Map props = new HashMap();
if ((DEBUG.IO || DEBUG.DND) && !file.exists()) examineBadFile(file);
if (path.toLowerCase().endsWith(".url")) {
// Search a windows .url file (an internet shortcut)
// for the actual web reference.
String url = convertWindowsURLShortCutToURL(file);
if (url != null) {
resourceSpec = url;
// We compute the resource name here as it's coming from a short-cut
// that we extract a URL from, in which case we want to use the name of
// the original shortcut, and not compute the resource title from it's
// source URL.
String resourceName;
if (file.getName().length() > 4)
resourceName = file.getName().substring(0, file.getName().length() - 4);
else
resourceName = file.getName();
props.put("title", resourceName);
}
} else if (path.endsWith(".textClipping") || path.endsWith(".webloc") || path.endsWith(".fileloc")) {
// TODO: we can handle Mac .fileloc's if we check multiple data-flavors: the initial LIST
// flavor gives us the .fileloc, which we could even pull a name from if we want, and in
// any case, a later STRING data-flavor actually gives us the source of the link!
// SAME APPLIES TO .webloc files... AND .textClipping files
// Of course, if they drop multple ones of these, we're screwed, as only the last
// one gets translated for us in the later string data-flavor. Oh well -- at least
// we can handle the single case if we want.
if (drop.transfer.isDataFlavorSupported(DataFlavor.stringFlavor)) {
// Unforunately, this only works if there's ONE ITEM IN THE LIST,
// or more accurately, the last item in the list, or perhaps even
// more accurately, the item that also shows up as the application/x-java-url.
// Which is why ultimately we'll want to put all this type of processing
// in a full-and-fancy filing OSID to end all filing OSID's, that'll
// handle windows .url shortcuts, the above mac cases plus mac aliases, etc, etc.
String unicodeString;
try {
unicodeString = (String) drop.transfer.getTransferData(DataFlavor.stringFlavor);
if (DEBUG.Enabled) out("*** GOT MAC REDIRECT DATA [" + unicodeString + "]");
} catch (Exception ex) {
ex.printStackTrace();
}
// for textClipping, the string data is raw dropped text, for webloc,
// it's URL be we already have text->URL in dropped text code below,
// and same for .fileloc... REFACTOR THIS MESS.
//resourceSpec = unicodeString;
//resourceName =
// NOW WE NEED TO JUMP DOWN TO HANDLING DROPPED TEXT...
}
}
//if (debug) System.out.println("\t" + file.getClass().getName() + " " + file);
//if (hitComponent != null && fileList.size() == 1) {
if (drop.hit != null) {
// TODO: CONSOLODATE THE SET-RESOURCE CODE FROM ALL THE PROCESSING SUB-ROUTINES
if (drop.isLinkAction || drop.hit instanceof LWLink) { // hack for now: if a link, just always set resource...
drop.hit.setResource(resourceSpec);
} else if (drop.hitParent != null) {
drop.hitParent.dropChild(createNodeAndResource(drop, file, resourceSpec, props, drop.nextDropLocation()));
// Why were we leaving out the location here? Oh: when hitParent could only be a node (auto-layout), that made sense
//drop.hitNode.addChild(createNodeAndResource(drop, resourceSpec, props, null));
}
} else {
createNodeAndResource(drop, file, resourceSpec, props, drop.nextDropLocation());
}
}
private LWComponent createNodeAndResource(DropContext drop, File file, String resourceSpec, Map properties, Point2D where)
{
//URLResource resource = new URLResource(resourceSpec);
final Resource resource;
if (file != null)
resource = mViewer.getMap().getResourceFactory().get(file);
else
resource = mViewer.getMap().getResourceFactory().get(resourceSpec);
if (DEBUG.DND) out("createNodeAndResource " + resourceSpec + " " + properties + " where=" + where);
LWComponent c = createNode(drop, resource, properties, where, true);
//EditorManager.targetAndApplyCurrentProperties(c);
// TODO: get this so that one call is triggering the async stuff for both
// meta-data and image loading. Maybe the resource can will load the image...,
// yeah, probably. Tho we also need activate separate animation threads that
// are going to wait on the data loading threads...
// Establish an undo for the node creation, and then one for the title update,
// so you can just undo to get the http title back if you want. Will need to
// make undo for at least this case or maybe all cases to treat the stopping of
// thread as an undo action itself also so you can stop it in the middle?
// How to handle group drops tho? There will be a ton of threads.
// I guess it would have to be all or nothing, and somehow group
// ALL the loading threads under a single undo thread mark?
// TODO: if an image, let async image loader do this so we don't
// have two threads both pulling data from the same URL!
// Could wait to start this till end of all drop processing
// and pull it from drop.added
//resource.scanForMetaDataAsync(c, true);
return c;
}
private LWComponent createNode(DropContext drop, Resource resource, Point2D where) {
return createNode(drop, resource, Collections.EMPTY_MAP, where, false);
}
private static int MaxNodeTitleLen = VueResources.getInt("node.title.maxDefaultChars", 50);
private LWComponent createNode(DropContext drop,
Resource resource,
Map properties,
Point2D where,
boolean newResource)
{
if (DEBUG.DND) Log.debug(drop + "; createNode " + resource + " " + properties + " where=" + where);
if (properties == null)
properties = Collections.EMPTY_MAP;
final boolean dropImagesAsNodes =
DropImagesAsNodes
&& !drop.isLinkAction
&& !(drop.hitParent instanceof LWSlide)
&& !(drop.hit == null && mViewer.getFocal() instanceof LWSlide)
;
// TODO: above conditions a mess: see commented experimental code at end of file on how
// we're going to clean this up
LWComponent node;
String displayName = (String) properties.get("title");
if (displayName == null)
displayName = makeNodeTitle(resource);
String shortName = displayName;
if (shortName.length() > MaxNodeTitleLen)
shortName = shortName.substring(0,MaxNodeTitleLen) + "...";
LWImage lwImage = null;
/*
MapResource mapResource = null;
if (resource instanceof MapResource) { // todo: fix Resource so no more of this kind of hacking
mapResource = (MapResource) resource;
}
*/
/*
* To accomodate for the MapDisplay->Image Size preference, I needed to do this so that
* when Image Size is set to Off Images aren't added to the node. MK
*
* SMF: no longer meaningful -- we're removing the image size preference.
*/
//if (resource.isImage() && LWImage.getMaxRenderSize() > 0) {
// if (resource.isImage()) {
// if (DEBUG.DND || DEBUG.IMAGE) Log.debug(drop + "; IMAGE DROP " + resource + " " + properties);
// //node = new LWImage(resource, viewer.getMap().getUndoManager());
// lwImage = new LWImage();
// String ws = (String) properties.get("width");
// String hs = (String) properties.get("height");
// if (ws != null && hs != null) {
// int w = Integer.parseInt(ws);
// int h = Integer.parseInt(hs);
// lwImage.suggestSize(w, h);
// resource.setProperty("image.width", ws);
// resource.setProperty("image.height", hs);
// /*
// if (mapResource != null) {
// mapResource.setProperty("image.width", ws);
// mapResource.setProperty("image.height", hs);
// }
// */
// } else {
// // give it some kind of size so the center-on-drop code can at least do something
// // todo: this causes off-standard image sizes to be created as the image
// // ends up being shaped via ConstrainToAspect instead of setMaxDimension,
// // tho I also note the sizes this produces tend to be more pleasing/balanced.
// //lwImage.setSize(LWImage.DefaultMaxDimension, LWImage.DefaultMaxDimension);
// lwImage.suggestSize(LWImage.DefaultMaxDimension, LWImage.DefaultMaxDimension);
// }
// //lwImage.setLabel(displayName);
// }
// if (lwImage == null || dropImagesAsNodes) {
// if (false && where == null && lwImage != null) {
// // don't wrap image if we're about to drop it into something else
// node = lwImage;
// } else {
// shortName = Util.formatLines(shortName, VueResources.getInt("dataNode.labelLength"));
// node = NodeModeTool.createNewNode(shortName);
// node.setResource(resource); // doing this first would let the image know it's a node-icon,
// // but this code will auto-add the image now!
// if (lwImage != null)
// ((LWNode)node).addChild(lwImage);
// node.setResource(resource); //
// }
// } else {
// // we're dropping the image raw (either on map or into something else)
// node = lwImage;
// }
if (resource.isImage()) {
int suggestWidth = -1, suggestHeight = -1;
if (DEBUG.DND || DEBUG.IMAGE) Log.debug(drop + "; IMAGE DROP " + resource + " " + properties);
String ws = (String) properties.get("width");
String hs = (String) properties.get("height");
if (ws != null && hs != null) {
suggestWidth = Integer.parseInt(ws);
suggestHeight = Integer.parseInt(hs);
if (suggestWidth > 0 && suggestHeight > 0) {
resource.setProperty(Resource.IMAGE_WIDTH, ws);
resource.setProperty(Resource.IMAGE_HEIGHT, hs);
}
// else {
// suggestWidth = suggestHeight = -1;
// }
}
}
if (dropImagesAsNodes) {
shortName = Util.formatLines(shortName, VueResources.getInt("dataNode.labelLength"));
node = NodeModeTool.createNewNode(shortName);
node.setResource(resource); // this will force the creation of an image-icon on the node from the resource
// if (lwImage != null)
// ((LWNode)node).addChild(lwImage);
// node.setResource(resource);
} else {
// // we're dropping the image raw (either on map or into something else)
// lwImage = new LWImage();
// // if (suggestWidth > 0) {
// // //-----------------------------------------------------------------------------
// // // do we still need this? Won't the image pull this itself from the Resource?
// // //-----------------------------------------------------------------------------
// // lwImage.suggestSize(suggestWidth, suggestHeight);
// // }
// lwImage.setResourceForDrop(resource, mViewer.getMap().getUndoManager()); // NEW
lwImage = LWImage.create(resource);
if (resource != null)
lwImage.setLabel(makeNodeTitle(resource));
node = lwImage;
}
// if "where" is null, the caller is adding this to another
// existing node, so we don't add it to the map here
// TODO: this is a confusing side-effect!
// TODO: merge all hitParent v.s. not swtiches above into a unified hanlder case
// FYI, this is now overdone! We always provide the location now,
// so on the slideviewer for master slide, where the slide is "hittable",
// and looks like the parent, it's added to map first needlessly, then
// reparented to where it needs to go.
//-----------------------------------------------------------------------------
// TODO: REDESIGN SO WE DON'T DO THIS HERE
//-----------------------------------------------------------------------------
if (where != null)
addNodeToFocal(node, where);
//-----------------------------------------------------------------------------
// if (lwImage != null) {
// lwImage.setResourceAndTitle(resource, mViewer.getMap().getUndoManager());
// } else if (newResource) {
// // if image, it will do this at end of loading
// ((URLResource)resource).scanForMetaDataAsync(node, true);
// }
//if (where != null) addNodeToFocal(node, where);
drop.add(node);
return node;
}
private LWComponent createTextNode(String text, Point2D where)
{
return addNodeToFocal(NodeModeTool.createTextNode(text), where);
}
private LWComponent addNodeToFocal(final LWComponent node, Point2D where)
{
if (DEBUG.DND) Log.debug("addNodeToFocal: " + node + "; where=" + where + "; centerAt=" + CenterNodesOnDrop);
// todo: if we're adding an LWImage, and it isn't loaded / it's LWComponent size
// isn't known yet, setCenterAt will have no discernable effect (because size is 0x0)
if (CenterNodesOnDrop)
node.setCenterAt(where);
else
node.setLocation(where);
mViewer.getFocal().dropChild(node);
// We don't want to push out any nodes if a node was dropped into another node.
// Checking the new parent for isTopLevel ought to work for this, but the
// current design is weak in that the node is first added to the layer (always
// top-level), THEN added to the drop target, so we're going to need a
// completely drop-code redesign to make this work.
//Log.debug("node parent: " + node.getParent() + "; isTopLevel=" + node.getParent().isTopLevel());
//if (node.getParent().isTopLevel())
// makeRoomFor(node);
// JAN 2010: DISABLING auto-push when dropping onto the map.
// See form post: https://vue-forums.uit.tufts.edu/posts/list/620.page
// makeRoomFor(node);
return node;
}
/**
* for nodes dropped directly into the layer (not another node in the layer), if it
* looks "crowded", push out all the other nodes on the layer to make more room;
*/
public static void makeRoomFor(final LWComponent node) {
// We add this as a cleanup task, so that all nodes created by this drop
// have already been added to the map before we start trying to make any
// new room
node.addCleanupTask(new Runnable() { public void run() {
if (!node.getParent().isTopLevel()) {
// todo: a design where we don't need this check here -- see addNodeToFocal
if (DEBUG.Enabled) Log.debug("ignoring push for non-top0level final parent of " + node);
return;
}
final LWMap.Layer layer = node.getLayer();
if (layer != null && layer.getChildren() != null) {
final Rectangle2D.Float clearRegion = Util.grow(node.getMapBounds(), 24);
for (LWComponent n : layer.getChildren()) {
if (n == node || n.getParent() != layer)
continue;
if (clearRegion.intersects(n.getMapBounds())) {
Actions.PushOut.act(node);
break;
}
}
}
}});
}
//-----------------------------------------------------------------------------
// support & debug code below
//-----------------------------------------------------------------------------
private static String dropName(int dropAction) {
return GUI.dropName(dropAction);
}
// debug
private void dumpFlavors(Transferable transfer) {
// dumpFlavors(transfer.getTransferDataFlavors());
// }
// private void dumpFlavors(DataFlavor[] dataFlavors)
// {
final DataFlavor[] dataFlavors = transfer.getTransferDataFlavors();
Log.debug("TRANSFERABLE: " + transfer + " has " + dataFlavors.length + " dataFlavors:");
for (int i = 0; i < dataFlavors.length; i++) {
DataFlavor flavor = dataFlavors[i];
String name = flavor.getHumanPresentableName();
if (flavor.getMimeType().toString().startsWith(name + ";"))
name = "";
else
name = "\"" + name + "\"";
System.out.format("flavor %2d %-16s %s", i, name, flavor.getMimeType());
//System.out.println("\tflavor:" + flavor);
try {
Object data = transfer.getTransferData(flavor);
System.out.println(" [" + data + "]");
if (DEBUG.META) {
if (flavor.getHumanPresentableName().equals("text/uri-list"))
readTextFlavor(flavor, transfer);
}
} catch (Exception ex) {
System.out.println("\tEXCEPTION: getTransferData: " + ex);
}
}
}
private DataFlavor findFlavor(DataFlavor[] dataFlavors, String mimeType, Class repClass)
{
for (DataFlavor flavor : dataFlavors) {
//System.out.println("MT " + Util.tags(flavor.getMimeType()) + " REPCLASS " + Util.tags(flavor.getRepresentationClass()));
if (flavor.isMimeTypeEqual(mimeType) && flavor.getRepresentationClass() == repClass)
return flavor;
}
return null;
}
/** attempt to make a URL from a string: return null if malformed */
private static URL makeURL(String s) {
try {
return new URL(s);
} catch (MalformedURLException ex) {
return null;
}
}
/**
* URL's dragged from the image search page of most search engines include query
* fields that allow us to locate the original source of the image, as well as
* width and height
*
* @param url a URL that at least know has a query
* @param properties a map to put found properties into (e.g., width, height)
*/
private static URL decodeSearchEngineLightBoxURL(final URL url, Map properties)
{
final String query = url.getQuery();
// special case for google image search:
if (DEBUG.IMAGE || DEBUG.IO || DEBUG.DND) Log.debug("DECODE QUERY: host " + url.getHost() + " query " + url.getQuery());
Map data = VueUtil.getQueryData(query);
//if (DEBUG.DND && DEBUG.META) {
if (DEBUG.DND) {
String[] pairs = query.split("&");
for (int i = 0; i < pairs.length; i++) {
System.out.println("\tquery pair " + pairs[i]);
}
System.out.println("data " + data);
}
final String host = url.getHost();
final String s = url.toString();
final String urlWithoutQuery;
final int questionMarkIndex = s.indexOf('?');
if (questionMarkIndex > 0)
urlWithoutQuery = s.substring(0, questionMarkIndex);
else
urlWithoutQuery = s;
String imageURL = (String) data.get("imgurl"); // google & yahoo
if (imageURL == null)
imageURL = (String) data.get("image_url"); // Lycos & Mamma(who are they?)
// note: as of Aug 2005, excite gives us no option
if (imageURL == null && host.endsWith(".msn.com") || host.endsWith(".live.com"))
imageURL = (String) data.get("iu"); // MSN search / Live Search
if (imageURL == null && host.endsWith(".netscape.com"))
imageURL = (String) data.get("img"); // Netscape search
if (imageURL == null && host.endsWith(".ask.com"))
imageURL = (String) data.get("u"); // ask jeeves, but only from their context page
URL redirectURL = null;
if (imageURL == null && host.endsWith(".flickr.com")) {
// TODO: can try this trick with any URL: strip off the query and see if what's
// left is an image url (e.g., file has an image extension, or could even
// just try grabbing content to see if it has an image type: that would
// be most reliable and generic version)
try {
return new URL(urlWithoutQuery);
} catch (Throwable t) {
return url;
}
}
if (imageURL == null)
imageURL = (String) data.get("url");
// TODO: ask.com now has multiple levels of indirection of query pair sets
// to get through...
// Attempt a default
if (imageURL != null
&& ("www.google.com".equals(host)
|| "images.google.com".equals(host)
|| "search.live.com".equals(host) // microsoft
|| "images.search.yahoo.com".equals(host)
|| "rds.yahoo.com".equals(host) // old
|| "search.lycos.com".equals(host)
|| "tm.ask.com".equals(host)
|| "search.msn.com".equals(host)
|| "search.netscape.com".equals(host)
|| host.endsWith("mamma.com")
)
) {
imageURL = Util.decodeURL(imageURL);
//if (imageURL.indexOf('%') >= 0)
//VueUtil.decodeURL(imageURL); // double-encoded (Ask Jeeves) -- need get query data AGAIN and get "imgsrc"
//-------------------------------------------------------
// %25 is % (percent), %2520 is an apparently often over-encoded
// %20, that we can (and need) to bring back down to %20
//imageURL = imageURL.replaceAll("%2520", "%20");
//-------------------------------------------------------
//imageURL = imageURL.replaceFirst("%3A", ":");
//imageURL = imageURL.replaceAll("%2F", "/");
//-------------------------------------------------------
if (DEBUG.IMAGE || DEBUG.IO || DEBUG.DND) Log.debug("redirect to image search url " + imageURL);
if (imageURL.indexOf(':') < 0)
imageURL = "http://" + imageURL;
redirectURL = makeURL(imageURL);
if (redirectURL == null && !imageURL.startsWith("http://"))
redirectURL = makeURL("http://" + imageURL);
if (DEBUG.IMAGE || DEBUG.IO || DEBUG.DND) Log.debug("redirect got URL " + redirectURL);
if (url != null) {
String w = (String) data.get("w"); // Google & Yahoo
String h = (String) data.get("h");
if (w == null || h == null) {
w = (String) data.get("wd"); // MSN search
h = (String) data.get("ht");
if (w == null || h == null) {
w = (String) data.get("image_width"); // Lycos
h = (String) data.get("image_height");
if (w == null || h == null) {
w = (String) data.get("width"); // Mamma
h = (String) data.get("height");
}
}
}
if (w != null && h != null && properties != null) {
properties.put("width", w);
properties.put("height", h);
}
}
}
if (redirectURL == null)
return url;
else
return redirectURL;
}
private static final Pattern URL_Line = Pattern.compile(".*^URL=([^\r\n]+).*", Pattern.MULTILINE|Pattern.DOTALL);
public static String convertWindowsURLShortCutToURL(File file)
{
String url = null;
try {
if (DEBUG.DND) Log.debug("Searching for URL in: " + file);
FileInputStream is = new FileInputStream(file);
byte[] buf = new byte[2048]; // if not in first 2048, don't bother
int len = is.read(buf);
is.close();
String str = new String(buf, 0, len);
if (DEBUG.DND) System.out.println("*** size="+str.length() +"["+str+"]");
Matcher m = URL_Line.matcher(str);
if (m.lookingAt()) {
url = m.group(1);
if (url != null)
url = url.trim();
if (DEBUG.DND) System.out.println("*** FOUND URL ["+url+"]");
int i = url.indexOf("|/");
if (i > -1) {
// odd: have found "file:///D|/dir/file.html" example
// where '|' is where ':' should be -- still works
// for Windows 2000 as a shortcut, but NOT using
// Windows 2000 url DLL, so VUE can't open it.
url = url.substring(0,i) + ":" + url.substring(i+1);
Log.debug("PATCHED URL ["+url+"]");
}
// if this is a file:/// url to a local html page,
// AND we can determine that we're on another computer
// accessing this file via the network (can we?)
// then we should not covert this shortcut.
// Okay, this is good enough for now, tho it also
// won't end up converting a bad shortcut, and
// ideally that wouldn't be our decision.
// [this is not worth it]
/*
URL u = new URL(url);
if (u.getProtocol().equals("file")) {
File f = new File(u.getFile());
if (!f.exists()) {
url = null;
System.out.println("*** BAD FILE ["+f+"]");
}
}
*/
}
} catch (Exception e) {
Log.debug(e);
}
return url;
}
private Point2D.Float dropToFocalLocation(Point p)
{
return dropToFocalLocation(p.x, p.y);
}
private Point2D.Float mapToLocalLocation(Point2D.Float mapLocation, LWComponent local)
{
return (Point2D.Float) local.transformMapToZeroPoint(mapLocation, new Point2D.Float());
}
private Point2D.Float dropToFocalLocation(int x, int y)
{
final Point2D.Float mapLoc = (Point2D.Float) mViewer.screenToFocalPoint(x, y);
//if (DEBUG.DND) out("dropToMapLocation " + x + "," + y + " = " + mapLoc);
return mapLoc;
}
private Point2D.Float dropToMapLocation(Point p)
{
final Point2D.Float mapLoc = mViewer.screenToMapPoint(p.x, p.y);
//if (DEBUG.DND) out("dropToMapLocation " + x + "," + y + " = " + mapLoc);
return mapLoc;
}
// TODO: this should be here: move to URLResource.java
static String makeNodeTitle(Resource resource)
{
if (resource.getTitle() != null)
return resource.getTitle();
String title = resource.getProperty("title");
if (title != null)
return title;
String spec = resource.getSpec();
String name = Util.decodeURL(spec); // in case any %xx notations
int slashIdx = name.lastIndexOf('/'); //TODO: fileSeparator? test on PC
if (slashIdx == name.length() - 1) {
// last char is '/'
return name;
} else {
if (slashIdx > 0) {
name = name.substring(slashIdx+1);
// trim off extension if there is one
int dotIdx = name.lastIndexOf('.');
if (dotIdx > 0)
name = name.substring(0, dotIdx);
name = name.replace('_', ' ');
name = name.replace('.', ' ');
name = name.replace('-', ' ');
name = Util.upperCaseWords(name);
}
}
//if (DEBUG.DND) out("MADE TITLE[" + name + "]");
return name;
}
/**
* Given a collection of LWComponent's, center them as a group at the given map location.
*/
public static void setCenterAt(Collection<LWComponent> nodes, Point2D.Float mapLocation)
{
if (DEBUG.DND) Log.debug("setCenterAt " + mapLocation + "; " + Util.tags(nodes));
java.awt.geom.Rectangle2D.Float bounds = LWMap.getBounds(nodes.iterator());
//java.awt.geom.Rectangle2D.Float bounds = LWMap.getLocalBounds(nodes);
float dx = mapLocation.x - (bounds.x + bounds.width/2);
float dy = mapLocation.y - (bounds.y + bounds.height/2);
translate(nodes, dx, dy);
}
/**
* Given a collection of LWComponent's, place the upper left hand corner of the group at the given location.
*/
public static void setLocation(List<LWComponent> nodes, Point2D.Float mapLocation)
{
if (nodes.size() == 1) {
if (nodes.get(0).getParent() == null)
nodes.get(0).setLocation(mapLocation);
} else {
java.awt.geom.Rectangle2D.Float bounds = LWMap.getBounds(nodes.iterator());
float dx = mapLocation.x - bounds.x;
float dy = mapLocation.y - bounds.y;
translate(nodes, dx, dy);
}
}
private static void translate(Collection<LWComponent>nodes, float dx, float dy)
{
for (LWComponent c : nodes) {
// If parent and some child both in selection and you drag (normally
// only the parent get's selected), the child will have it's
// location updated by the parent, so only set the location
// on the orphans.
if (c.getParent() == null)
c.translate(dx, dy);
}
}
private void out(String s) {
Log.debug(s);
// final String name;
// if (mViewer.getFocal() != null)
// name = mViewer.getFocal().getLabel();
// else
// name = mViewer.toString();
// Log.debug(String.format("(%s): %s", name, s));
//System.out.println("MapDropTarget(" + name + ") " + s);
}
private String readTextFlavor(DataFlavor flavor, Transferable transfer)
{
java.io.Reader reader = null;
String value = null;
try {
reader = flavor.getReaderForText(transfer);
//if (DEBUG.DND && DEBUG.META) System.out.println("\treader=" + reader);
char buf[] = new char[512];
int got = reader.read(buf);
value = new String(buf, 0, got);
if (DEBUG.DND && DEBUG.META) System.out.println("\t" + Util.tags(value));
if (reader.read() != -1)
System.out.println("[there was more data in the reader]");
} catch (Exception e) {
System.err.println("readTextFlavor: " + e);
}
return value;
}
// TODO: to cleanup the dropping onto hit/hitParent v.s. dropping into the focal in
// all the below code, have a single drop.hit that is allowed to take on the value
// of the focal (e.g., the LWMap, or a master slide, or any slide in the slide
// viewer). Always assume you may need to set the coords on new children, and do
// that, and if whatever the new children are added to wants to re-lay them out,
// fine. Then all you need to do is be able to distinguish between something you're
// allowed to set a resource on... I supposed a boolean LWComponent.takesResource()
// could tell us this (off for LWSlide, LWMap, LWGroup, LWText?), tho before doing
// that, if we manage a fully dynamic property system, that might handle it for us.
// private static class DropData {
// final Transferable transfer;
// DataFlavor flavor;
// Object data;
// URL mainURL;
// URL iconURL;
// URL contextURL; // referrer page for search-engine light-tray results that provide them
// URL searchURL; // search engine this was found at
// List list;
// String text;
// DropData(Transferable t) {
// transfer = t;
// }
// // void select(DataFlavor pickFlavor) {
// // this.flavor = pickFlavor;
// // this.data = extractData(transfer, pickFlavor);
// // }
// }
// private static final Collection<DropHandler> DropHandlers = new java.util.ArrayList();
// private abstract static class DropHandler<T> {
// final DataFlavor flavor;
// T data;
// DropHandler(DataFlavor f) {
// flavor = f;
// DropHandlers.add(this); // not threadsafe
// }
// // override for anything more complicated
// boolean accept(DropContext drop) {
// return drop.transfer.isDataFlavorSupported(flavor);
// }
// void processDrop(DropContext drop) {
// Object data = extractData(drop.transfer, flavor);
// process(drop, data); // just set data in drop context?
// }
// abstract void process(DropContext drop, Object data);
// }
// static {
// // may want to change impl to just adding anon inner class impls to a list...
// // how handle priorities?
// new DropHandler(edu.tufts.vue.ontology.ui.TypeList.DataFlavor) {
// // boolean accept(DropContext drop) {
// // return drop.dropAction == DnDConstants.ACTION_LINK && super.accept(drop);
// // }
// void process(DropContext drop, Object data) {
// //private boolean processDroppedOntologyType(DropContext drop,Object foundData)
// edu.tufts.vue.metadata.VueMetadataElement ele = new edu.tufts.vue.metadata.VueMetadataElement();
// ele.setObject(data);
// drop.hit.getMetadataList().getMetadata().add(ele);
// }
// };
// new DropHandler(LWComponent.DataFlavor) {
// //private boolean processDroppedNodes(DropContext drop)
// void process(DropContext drop, Object data) {
// final List<LWComponent> items = (List) data;
// // now add them to the map
// // Always to the set center, in case hitParent isn't something
// // that is going to auto-layout the new children
// setCenterAt(items, drop.location);
// if (drop.hitParent != null) {
// drop.hitParent.addChildren(items);
// } else {
// drop.viewer.getFocal().addChildren(items);
// }
// drop.added.addAll(items);
// }
// };
// // // createNode needs to be made static / moved to this drop handler
// // new DropHandler(Resource.DataFlavor) {
// // //private boolean processDroppedResourceList(DropContext drop)
// // void process(DropContext drop, Object data)
// // {
// // final List<Resource> items = (List) data;
// // if (items.size() == 1 && drop.hit != null && drop.isLinkAction) {
// // // Only one item is in the list, and we've hit a component, and
// // // it's a link-action drop: replace the hit component resource
// // drop.hit.setResource(items.get(0));
// // } else {
// // for (Resource resource : items) {
// // if (drop.hitParent != null && !drop.isLinkAction) {
// // // create new node children of the hit node
// // //drop.hitParent.addChild(createNode(drop, resource, null));
// // drop.hitParent.addChild(createNode(drop, resource, drop.nextDropLocation()));
// // } else {
// // createNode(drop, resource, drop.nextDropLocation());
// // }
// // }
// // }
// // } // end process
// // };
// }
private static void examineBadFile(File file) {
examineBadFile(file, true);
}
private static void examineBadFile(File file, boolean descend) {
// note: on at least mac, file names can exist that are not actually accessable:
// BAD-CHARACTER[x].jpg
// The file works fine if the "not-equals" special char is changed to an 'x', but not as-is.
// Actually, I can't even save this source file with that character in it w/out changing
// to UTF-8 or mac-roman encoding. This is a java bug (mac platform, maybe other platforms).
// Note: The bad character in this example URL encodes to "%E2%89%A0". Mac URL open doesn't
// handle it either tho, but it can be visibly seen in the meta-data window and content summary window.
// This is as of java version "1.6.0_17", which happened to just update to my mac today, 2009-12-04. --SMF
// Addendum: See http://bugs.sun.com/bugdatabase/view%5Fbug.do?bug%5Fid=4733494
// Has been known since 2002! And was declared "Not a Defect" !
// The reasoning being only characters that are supported by the current locale language
// are supported. What crap.
// Also see: http://stackoverflow.com/questions/1545625/java-cant-open-a-file-with-surrogate-unicode-values-in-the-filename
// Apparently, there's no workaround w/out writing native code.
Log.warn("BAD DROPPED FILE:"
+ "\n\t file: " + file
+ "\n\t name: " + Util.tags(file.getName())
+ "\n\t exists: false"
+ "\n\t canRead: " + file.canRead());
URI uri = null;
String nameUTF = null;
String nameMac = null;
//String nameUNI = null;
File fileUTF = null;
File fileMac = null;
File uriUTF = null;
File uriMac = null;
try {
uri = file.toURI();
nameUTF = java.net.URLEncoder.encode(file.getName(), "UTF-8");
nameMac = java.net.URLEncoder.encode(file.getName(), "MacRoman");
//nameUNI = java.nio.charset.Charset.defaultCharset().decode(file.getName().getBytes());
fileUTF = new File(file.getParent(), nameUTF);
fileMac = new File(file.getParent(), nameMac);
URI utf, mac;
utf = new URI("file://" + file.getParent() + File.separator + nameUTF);
mac = new URI("file://" + file.getParent() + File.separator + nameMac);
Log.debug("URIUTF " + utf);
Log.debug("URIMac " + mac);
uriUTF = new File(utf);
uriMac = new File(mac);
URL url = new URL(utf.toString());
Log.debug("URL: " + url);
URLConnection c = url.openConnection(); // this fails as well
Log.debug("URL-CONTENT: " + Util.tags(c.getContent()));
} catch (Throwable t) {
Log.error("meta-debug", t);
}
Log.warn("BAD DROPPED FILE ANALYSIS:"
+ "\n\t toURI: " + uri
+ "\n\t asUTF8: " + Util.tags(nameUTF)
+ "\n\tasMacRoman: " + Util.tags(nameMac)
+ "\n\t fileUTF: " + fileUTF
+ "\n\t existsUTF: " + fileUTF.exists()
+ "\n\t fileMac: " + fileMac
+ "\n\t existsMac: " + fileMac.exists()
+ "\n\t uriUTF: " + uriUTF
+ "\n\t existsUTF: " + uriUTF.exists()
+ "\n\t uriMac: " + uriMac
+ "\n\t existsMac: " + uriMac.exists()
+ "\n\t(probably contains unicode character(s) unhandled by java: this is a java bug)");
// try {
// FileInputStream fin = new FileInputStream(file);
// Log.debug("AVAILABLE: " + fin.available());
// } catch (Throwable t) {
// Log.error("FIN", t);
// }
// if (descend) {
// File[] all = file.getParentFile().listFiles();
// for (File f : all) {
// if (f.equals(file)) {
// Log.debug("FOUND MATCH IN PARENT " + Util.tags(f));
// // Even the File object obtained from the parent is bad!!!
// examineBadFile(f, false);
// }
// }
// //Util.dump(all);
// }
}
private static final String MIME_TYPE_MAC_URLN = "application/x-mac-ostype-75726c6e";
// 75726c6e="URLN" -- mac uses this type for a flavor containing the title of a web document
// this existed in 1.3, but apparently went away in 1.4.
}