/*
*
* Copyright 2012 Chad Preisler
*
* This file is part of jriaffeBlog.
*
* jriaffeBlog is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* jriaffeBlog is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with jriaffeBlog. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.jriaffe.blog.panels;
import jriaffe.client.NotificationCenter;
import org.jriaffe.blog.domain.Blog;
import org.jriaffe.blog.service.ImageImporterService;
import org.jriaffe.blog.service.ImportImageInfo;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.im.InputContext;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
/**
* This class was mostly copied from the JDK. I added the ability to drop image files
* into the JEditPane. This class needs some cleaning up. It assumes that the JTextComponent
* is has an HTMLEditorKit.
*
* I'm not happy about copying the code from the JDK but I didn't see that I had a choice because
* of the way Swing implemented the TextTransferHandler.
*/
class TextTransferHandler extends TransferHandler {
private JTextComponent exportComp;
private Blog blog;
private boolean shouldRemove;
private int p0;
private int p1;
/**
* Whether or not this is a drop using
* <code>DropMode.INSERT</code>.
*/
private boolean modeBetween = false;
/**
* Whether or not this is a drop.
*/
private boolean isDrop = false;
/**
* The drop action.
*/
private int dropAction = MOVE;
/**
* The drop bias.
*/
private Position.Bias dropBias;
/**
* Try to find a flavor that can be used to import a Transferable.
* The set of usable flavors are tried in the following order:
* <ol>
* <li>First, an attempt is made to find a flavor matching the content type
* of the EditorKit for the component.
* <li>Second, an attempt to find a text/plain flavor is made.
* <li>Third, an attempt to find a flavor representing a String reference
* in the same VM is made.
* <li>Lastly, DataFlavor.stringFlavor is searched for.
* </ol>
* @param flavors
* @param c
* @return
*/
protected DataFlavor getImportFlavor(DataFlavor[] flavors, JTextComponent c) {
DataFlavor plainFlavor = null;
DataFlavor refFlavor = null;
DataFlavor stringFlavor = null;
DataFlavor imageFlavor = null;
for (int i = 0; i < flavors.length; i++) {
String mime = flavors[i].getMimeType();
System.out.println("Flavor Mimetype is " + mime);
if (mime.startsWith(((JEditorPane) c).getEditorKit().getContentType())) {
return flavors[i];
} else if (plainFlavor == null && mime.startsWith("text/plain")) {
plainFlavor = flavors[i];
} else if (refFlavor == null && mime.startsWith("application/x-java-jvm-local-objectref")
&& flavors[i].getRepresentationClass() == java.lang.String.class) {
refFlavor = flavors[i];
} else if (stringFlavor == null && flavors[i].equals(DataFlavor.stringFlavor)) {
stringFlavor = flavors[i];
} else if (imageFlavor == null && mime.startsWith("application/x-java-file-list; class=java.util.List")) {
imageFlavor = flavors[i];
break;
} else if (imageFlavor == null && mime.startsWith("text/uri-list; class=java.io.Reader")) {
imageFlavor = flavors[i];
break;
}
}
if (imageFlavor != null) {
return imageFlavor;
} else if (plainFlavor != null) {
return plainFlavor;
} else if (refFlavor != null) {
return refFlavor;
} else if (stringFlavor != null) {
return stringFlavor;
}
return null;
}
/**
* Import the given stream data into the text component.
*/
protected void handleReaderImport(Reader in, JTextComponent c, boolean useRead)
throws BadLocationException, IOException {
if (useRead) {
int startPosition = c.getSelectionStart();
int endPosition = c.getSelectionEnd();
int length = endPosition - startPosition;
EditorKit kit = c.getUI().getEditorKit(c);
Document doc = c.getDocument();
if (length > 0) {
doc.remove(startPosition, length);
}
kit.read(in, doc, startPosition);
} else {
char[] buff = new char[1024];
int nch;
boolean lastWasCR = false;
int last;
StringBuffer sbuff = null;
// Read in a block at a time, mapping \r\n to \n, as well as single
// \r to \n.
while ((nch = in.read(buff, 0, buff.length)) != -1) {
if (sbuff == null) {
sbuff = new StringBuffer(nch);
}
last = 0;
for (int counter = 0; counter < nch; counter++) {
switch (buff[counter]) {
case '\r':
if (lastWasCR) {
if (counter == 0) {
sbuff.append('\n');
} else {
buff[counter - 1] = '\n';
}
} else {
lastWasCR = true;
}
break;
case '\n':
if (lastWasCR) {
if (counter > (last + 1)) {
sbuff.append(buff, last, counter - last - 1);
}
// else nothing to do, can skip \r, next write will
// write \n
lastWasCR = false;
last = counter;
}
break;
default:
if (lastWasCR) {
if (counter == 0) {
sbuff.append('\n');
} else {
buff[counter - 1] = '\n';
}
lastWasCR = false;
}
break;
}
}
if (last < nch) {
if (lastWasCR) {
if (last < (nch - 1)) {
sbuff.append(buff, last, nch - last - 1);
}
} else {
sbuff.append(buff, last, nch - last);
}
}
}
if (lastWasCR) {
sbuff.append('\n');
}
c.replaceSelection(sbuff != null ? sbuff.toString() : "");
}
}
// --- TransferHandler methods ------------------------------------
/**
* This is the type of transfer actions supported by the source. Some models are
* not mutable, so a transfer operation of COPY only should
* be advertised in that case.
*
* @param c The component holding the data to be transfered. This
* argument is provided to enable sharing of TransferHandlers by
* multiple components.
* @return This is implemented to return NONE if the component is a JPasswordField
* since exporting data via user gestures is not allowed. If the text component is
* editable, COPY_OR_MOVE is returned, otherwise just COPY is allowed.
*/
public int getSourceActions(JComponent c) {
if (c instanceof JPasswordField &&
c.getClientProperty("JPasswordField.cutCopyAllowed") !=
Boolean.TRUE) {
return NONE;
}
return ((JTextComponent) c).isEditable() ? COPY_OR_MOVE : COPY;
}
/**
* Create a Transferable to use as the source for a data transfer.
*
* @param comp The component holding the data to be transfered. This
* argument is provided to enable sharing of TransferHandlers by
* multiple components.
* @return The representation of the data to be transfered.
*
*/
protected Transferable createTransferable(JComponent comp) {
exportComp = (JTextComponent)comp;
shouldRemove = true;
p0 = exportComp.getSelectionStart();
p1 = exportComp.getSelectionEnd();
return (p0 != p1) ? (new EditPaneTransferable((JEditorPane)exportComp, p0, p1)) : null;
}
/**
* This method is called after data has been exported. This method should remove
* the data that was transfered if the action was MOVE.
*
* @param source The component that was the source of the data.
* @param data The data that was transferred or possibly null
* if the action is <code>NONE</code>.
* @param action The actual action that was performed.
*/
protected void exportDone(JComponent source, Transferable data, int action) {
// only remove the text if shouldRemove has not been set to
// false by importData and only if the action is a move
if (shouldRemove && action == MOVE) {
EditPaneTransferable t = (EditPaneTransferable)data;
t.removeText();
t.removeText();
}
exportComp = null;
}
public boolean importData(TransferSupport support) {
isDrop = support.isDrop();
if (isDrop) {
modeBetween =
((JTextComponent) support.getComponent()).getDropMode() == DropMode.INSERT;
dropBias = ((JTextComponent.DropLocation) support.getDropLocation()).getBias();
dropAction = support.getDropAction();
}
try {
return super.importData(support);
} finally {
isDrop = false;
modeBetween = false;
dropBias = null;
dropAction = MOVE;
}
}
/**
* This method causes a transfer to a component from a clipboard or a
* DND drop operation. The Transferable represents the data to be
* imported into the component.
*
* @param comp The component to receive the transfer. This
* argument is provided to enable sharing of TransferHandlers by
* multiple components.
* @param t The data to import
* @return true if the data was inserted into the component, false otherwise.
*/
public boolean importData(JComponent comp, Transferable t) {
JTextComponent c = (JTextComponent) comp;
int pos = modeBetween
? (c.getDropLocation()).getIndex()
: c.getCaretPosition();
// if we are importing to the same component that we exported from
// then don't actually do anything if the drop location is inside
// the drag location and set shouldRemove to false so that exportDone
// knows not to remove any data
if (dropAction == MOVE && c == exportComp && pos >= p0 && pos <= p1) {
shouldRemove = false;
return true;
}
boolean imported = false;
DataFlavor importFlavor = getImportFlavor(t.getTransferDataFlavors(), c);
if (importFlavor != null) {
try {
boolean useRead = false;
if (comp instanceof JEditorPane) {
JEditorPane ep = (JEditorPane) comp;
if (!ep.getContentType().startsWith("text/plain") &&
importFlavor.getMimeType().startsWith(ep.getContentType())) {
useRead = true;
}
}
InputContext ic = c.getInputContext();
if (ic != null) {
ic.endComposition();
}
if (modeBetween) {
Caret caret = c.getCaret();
if (caret instanceof DefaultCaret) {
((DefaultCaret) caret).setDot(pos, dropBias);
} else {
c.setCaretPosition(pos);
}
}
if (importFlavor.getMimeType().startsWith("application/x-java-file-list; class=java.util.List")) {
List<File> files = (List<File>)t.getTransferData(importFlavor);
notifyFileImportService(files, c, blog);
} else if(importFlavor.getMimeType().startsWith("text/uri-list; class=java.io.Reader")) {
BufferedReader reader = new BufferedReader((InputStreamReader)t.getTransferData(importFlavor));
String line = reader.readLine();
List<File> files = new ArrayList<File> ();
while (line != null) {
if(line.startsWith("file://")) {
try {
files.add(new File(new URI(line)));
} catch (URISyntaxException e) {
//shouldn't really happen but if it does print the line and try the next line.
e.printStackTrace();
}
}
line = reader.readLine();
}
notifyFileImportService(files, c, blog);
} else {
Reader r = importFlavor.getReaderForText(t);
handleReaderImport(r, c, useRead);
}
if (isDrop) {
c.requestFocus();
Caret caret = c.getCaret();
if (caret instanceof DefaultCaret) {
int newPos = caret.getDot();
Position.Bias newBias = ((DefaultCaret) caret).getDotBias();
((DefaultCaret) caret).setDot(pos, dropBias);
((DefaultCaret) caret).moveDot(newPos, newBias);
} else {
c.select(pos, c.getCaretPosition());
}
}
imported = true;
} catch (UnsupportedFlavorException ufe) {
} catch (BadLocationException ble) {
} catch (IOException ioe) {
}
}
return imported;
}
private void notifyFileImportService(List<File>files, JTextComponent comp, Blog blog) {
ImportImageInfo info = new ImportImageInfo();
info.setFiles(files);
info.setCaretPos(comp.getCaret().getDot());
info.setComponent((JEditorPane)comp);
info.setOutputDir(blog.getOutputDir());
NotificationCenter.getDefaultNotificationCenter().postNotification(ImageImporterService.START_IMPORT, info);
}
/**
* This method indicates if a component would accept an import of the given
* set of data flavors prior to actually attempting to import it.
*
* @param comp The component to receive the transfer. This
* argument is provided to enable sharing of TransferHandlers by
* multiple components.
* @param flavors The data formats available
* @return true if the data can be inserted into the component, false otherwise.
*/
public boolean canImport(JComponent comp, DataFlavor[] flavors) {
JTextComponent c = (JTextComponent) comp;
if (!(c.isEditable() && c.isEnabled())) {
return false;
}
return (getImportFlavor(flavors, c) != null);
}
}