I want to be notified if a string is copied to the system clipboard. When a new string is copied from the same source application, the FlavorListener won't get an event. To get informed when another string is copied, i read the string from the clipboard, convert it to a SrtingSelection, which is able to take the ownership, and put it back to the clipboard. Now I got informed twice, once the StringSelection lost ownership and once it takes it back. Is there a way to check for the ownership directly, instead of storing the string and check it equals the new one?
Here is my code so far:
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.FlavorEvent;
import java.awt.datatransfer.FlavorListener;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws Exception {
// The clipboard
final Clipboard cb = Toolkit.getDefaultToolkit().getSystemClipboard();
// read clipboard and take ownership to get the FlavorListener notified
// when the content has changed but the owner has not
processClipboard(cb);
cb.addFlavorListener(new FlavorListener() {
#Override
public void flavorsChanged(FlavorEvent e) {
processClipboard(cb);
}
});
// keep thread for testing
Thread.sleep(100000L);
}
public static void processClipboard(Clipboard cb) {
// gets the content of clipboard
Transferable trans = cb.getContents(null);
if (trans.isDataFlavorSupported(DataFlavor.stringFlavor)) {
try {
// cast to string
String s = (String) trans
.getTransferData(DataFlavor.stringFlavor);
System.out.println(s);
// only StringSelection can take ownership, i think
StringSelection ss = new StringSelection(s);
// set content, take ownership
cb.setContents(ss, ss);
} catch (UnsupportedFlavorException e2) {
e2.printStackTrace();
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
}
I hope you understand my bad english :-(
The previous answer is close to be working.
The real cure is to instead just monitor the event of ownership change. By the monitor's occupying the clipboard as owner when monitoring the clipboard, so when any application changes the clipboard, the ownership would change, so this would reliably indicate the clipboard content change. However, this approach must have sufficient wait to work, (200 ms was found to be working) after an ownership change event before accessing the clipboard and re-occupying the clipboard.
This solution was provided and proved to be working by marc weber at
http://www.coderanch.com/t/377833/java/java/listen-clipboard
I have verified for my purpose. If needed, I can post the solution here.
Yu
To avoid double notification remove the flavor listener before setting the new clipboard content and add the listener again after setting clipboard content.
public class NewClass implements FlavorListener, ClipboardOwner{
private Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
public NewClass() {
System.out.println("NewClass constructor");
clip.setContents(clip.getContents(null), this);
clip.addFlavorListener(this);
try {
Thread.sleep(100000L);
}
catch (InterruptedException e) {
}
}
#Override
public void flavorsChanged(FlavorEvent e) {
System.out.println("ClipBoard Changed!!!");
clip.removeFlavorListener(this);
clip.setContents(clip.getContents(null), this);
clip.addFlavorListener(this);
}
#Override
public void lostOwnership(Clipboard arg0, Transferable arg1) {
System.out.println("ownership losted");
}
}
I think this would work :)
import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.io.IOException;
import java.util.Observable;
import java.util.Observer;
import javax.swing.JFrame;
public final class ClipboardMonitor extends Observable implements ClipboardOwner {
private static ClipboardMonitor monitor = null;
public ClipboardMonitor() {
gainOwnership();
}
private void gainOwnership() {
Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
try {
Transferable content = clip.getContents(null);
DataFlavor[] f = content.getTransferDataFlavors();
boolean imageDetected = false;
for (int i = 0; i < f.length; i++) {
// System.out.println("Name: " + f[i].getHumanPresentableName());
// System.out.println("MimeType: " + f[i].getMimeType());
// System.out.println("PrimaryType: " + f[i].getPrimaryType());
// System.out.println("SubType: " + f[i].getSubType());
if (f[i].equals(DataFlavor.imageFlavor)) {
imageDetected = true;
break;
}
}
if (imageDetected) {
System.out.println("Image content detected");
Transferable t = new Transferable() {
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { DataFlavor.stringFlavor };
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
return false;
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
return "dummy text instead of snapshot image";
}
};
clip.setContents(t, this);
} else {
clip.setContents(content, this);
}
setChanged();
notifyObservers(content);
} catch (IllegalArgumentException istateexception) {
istateexception.printStackTrace();
} catch (Exception ioexception) {
ioexception.printStackTrace();
}
}
private int getCurrentEventModifiers() {
int modifiers = 0;
AWTEvent currentEvent = EventQueue.getCurrentEvent();
if (currentEvent instanceof InputEvent) {
modifiers = ((InputEvent) currentEvent).getModifiers();
} else
if (currentEvent instanceof ActionEvent) {
modifiers = ((ActionEvent) currentEvent).getModifiers();
}
return modifiers;
}
public void lostOwnership(Clipboard clipboard, Transferable contents) {
System.out.println("Ownership lost ...");
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(200);
gainOwnership();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
public void flushClipboard() {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(""), null);
}
public static final ClipboardMonitor getMonitor() {
if (monitor == null)
monitor = new ClipboardMonitor();
return (monitor);
}
public static void main(String[] args) {
JFrame f = new JFrame();
ClipboardMonitor monitor = ClipboardMonitor.getMonitor();
monitor.addObserver(new Observer() {
public void update(Observable o, Object arg) {
System.out.println("Clipboard has been regained!");
}
});
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(500, 100);
f.setVisible(true);
}
}
Related
In the following java application, I use TwitterStream to gather tweets using sample function. I need to start and stop the stream whenever user wants, but I get the following exception:
java.util.concurrent.RejectedExecutionException: Task twitter4j.StatusStreamBase$1#74e75335 rejected from java.util.concurrent.ThreadPoolExecutor#5117b235[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 2]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
at twitter4j.DispatcherImpl.invokeLater(DispatcherImpl.java:58)
at twitter4j.StatusStreamBase.handleNextElement(StatusStreamBase.java:80)
at twitter4j.StatusStreamImpl.next(StatusStreamImpl.java:56)
at twitter4j.TwitterStreamImpl$TwitterStreamConsumer.run(TwitterStreamImpl.java:568)
When the user presses "Crawl" or "Stop Crawling", the method actionPerformed is correctly called. However, if the user presses Crawl and then presses Stop and then again presses Crawl, I get the error above
I have several classes, but the principal ones are the followings:
The first one creates the interface and comunicates with the crawler class.
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JTextArea;
public class StackOv extends JFrame implements ActionListener{
private JTextArea usersSaved;
private boolean alreadyCrawling;
private boolean stopReceived;
private Stream stream;
private JButton Crawl;
private JButton stopCrawl;
private Mongo m;
public StackOv(){
this.stopReceived = false;
this.alreadyCrawling = false;
setLayout(new FlowLayout(FlowLayout.CENTER));
Crawl = new JButton("Crawl");
Crawl.setActionCommand("Crawl");
Crawl.addActionListener(this);
stopCrawl = new JButton("Stop Crawling");
stopCrawl.setActionCommand("Stop Crawling");
stopCrawl.addActionListener(this);
m = new Mongo(); //instance of class that uses MongoDB
/*
*
*bla bla bla create the rest of the interface as you wish
*add(button)
*add(button)
*etc...
*/
}
public void setOut(String out){
usersSaved.setText(out);
}
public void setOffAlreadyCrawling(){
this.alreadyCrawling = false;
}
#Override
public void actionPerformed(ActionEvent e){
if(e.getActionCommand().equals("Stop Crawling") && !this.stopReceived){
this.stopReceived = true;
stream.setStop();
}
else if(e.getActionCommand().equals("Crawl") && !alreadyCrawling){
if(stream != null && stream.isAlive()){
stream.interrupt();
}
alreadyCrawling = true;
stream = new Stream(m, this);
//independently of using one of the following two calls, I get the same exception above
stream.execute1();
//stream.start();
this.stopReceived = false;
}
}
public void main(String[] args){
StackOv so = new StackOv();
so.setSize(800, 800);
so.setVisible(true);
}
}
The following class is the crawler class, that shutdown twitterStream when stopCrawl is true or when twitterStream has sampled a number of tweets over the maximum limit.
import java.awt.TextArea;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import twitter4j.FilterQuery;
import twitter4j.StallWarning;
import twitter4j.Status;
import twitter4j.StatusDeletionNotice;
import twitter4j.StatusListener;
import twitter4j.Twitter;
import twitter4j.TwitterException;
import twitter4j.TwitterFactory;
import twitter4j.TwitterStream;
import twitter4j.TwitterStreamFactory;
public class Stream extends Thread{
private Crawler cr;
private TwitterStream twitterStream;
private int maxTweets;
private int usersSaved;
private Mongo database;
private CreateIndex ci;
private TwitterSearch twitterSearch;
private static boolean stopCrawl;
public Stream(Mongo database, TwitterSearch twitterSearch){
Stream.stopCrawl = false;
this.database = database;
this.cr = new Crawler(database);
this.twitterStream = new TwitterStreamFactory(DefaultConfiguration.getConfiguration()).getInstance();
this.maxTweets = 1000;
ci = new CreateIndex(database);
this.twitterSearch = twitterSearch;
}
public void setStop(){
Stream.stopCrawl = true;
}
public void execute() throws TwitterException {
final List<Status> statuses = new ArrayList<Status>();
StatusListener listener = new StatusListener() {
public void onStatus(Status status) {
statuses.add(status);
System.out.println(statuses.size() + ":" + status.getText());
int usersIndexed = cr.retrieve(status.getUser());
usersSaved = database.countDocuments();
twitterSearch.setOut("usersSaved: "+usersSaved);
if(usersIndexed > maxTweets || Stream.stopCrawl){
//ci.load();
ci.load(); //this call creates my index
twitterSearch.setOut("INDEX CREATED");
System.out.println("shutdown...");
twitterSearch.setOffAlreadyCrawling();
twitterStream.shutdown();
}
}
public void onDeletionNotice(StatusDeletionNotice statusDeletionNotice) {
}
public void onTrackLimitationNotice(int numberOfLimitedStatuses) {
}
public void onScrubGeo(long userId, long upToStatusId) {
}
public void onException(Exception ex) {
ex.printStackTrace();
}
#Override
public void onStallWarning(StallWarning arg0) {
// TODO Auto-generated method stub
}
};
twitterStream.addListener(listener);
twitterStream.sample("en");
}
public void execute1(){
try{
this.execute();
}catch(TwitterException e){
e.printStackTrace();
}
}
public void run(){
try {
this.execute();
} catch (TwitterException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
When your thread is shut down/closed, it prevents it from being "restarted", as with other java IO classes. In other words, once you close it, you can't really start it up again. I'm pretty sure somewhere in either the Twitter code or your code, your thread is being stopped. To prevent this from happening, here's a code snippet that may work: http://pastebin.com/APByKuiY
Also, try this stack overflow thingy: What could be the cause of RejectedExecutionException.
JTextPane text;
text.setText("somewords <img src=\"file:///C:/filepath/fire.png\" text=\"[fire1]\" title=\"[fire2]\" alt=\"[fire3]\" style=\"width:11px;height:11px;\"> otherwords");
Gives me this , which is as expected. But when I highlight it and copy paste it, I get "somewords otherwords". The same thing done inside Firefox when copied would paste "somewords [fire3] otherwords" (it substitutes alt text for image). Is there any way to replicate this behavior where the alt text is copied, or any other indication that a picture was copied? I'm guessing it is not a built in feature, so what I probably need to know is what should be overloaded to mimic this behavior.
Its for an output/chat window so its important that when the users quote it it includes the images (like emotes would)
Update: Successfully overrode the copyAction method... now what?
// (should) allow copying of alt text in place of images
class CustomEditorKit extends HTMLEditorKit {
Action[] modifiedactions;
CustomEditorKit() {
int whereat=-1;
modifiedactions=super.getActions();
for(int k=0;k<super.getActions().length;k++) {
if(super.getActions()[k] instanceof CopyAction) //find where they keep the copyaction
{
whereat=k;
modifiedactions[whereat]=new CustomCopyAction(); //and replace it with a different one
}
}
}
#Override
public Action[] getActions() {
return modifiedactions; //returns the modified version instead of defaultActions
}
public static class CustomCopyAction extends TextAction {
public CustomCopyAction() {
super(copyAction);
}
#Override
public void actionPerformed(ActionEvent e) { //need to change this to substitute images with text, preferably their alt text.
JTextComponent target = getTextComponent(e);
//target.getText() gives full body of html, unbounded by selection area
if (target != null) {
target.copy(); //a confusing and seemingly never ending labyrinth of classes and methods
}
}
}
}
The only way I can think of accomplishing this is by writing your own TransferHandler, and overriding the getSourceActions and exportToClipboard methods.
You can convert the HTML to plain text yourself, rather than letting Swing use the getSelectedText method of JTextPane, by recursively converting each Element of the HTML Document, customizing the conversion in the case where the Element has a NameAttribute of IMG and also has an ALT attribute.
Here's what I came up with:
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.awt.EventQueue;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.TransferHandler;
import javax.swing.text.AttributeSet;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.BadLocationException;
import javax.swing.text.html.HTML;
public class HTMLCopier
extends TransferHandler {
private static final long serialVersionUID = 1;
private final Collection<DataFlavor> flavors;
HTMLCopier() {
Collection<DataFlavor> flavorList = new LinkedHashSet<>();
Collections.addAll(flavorList,
new DataFlavor(String.class, null),
DataFlavor.stringFlavor);
String[] mimeTypes = {
"text/html", "text/plain"
};
Class<?>[] textClasses = {
Reader.class, String.class, CharBuffer.class, char[].class
};
Class<?>[] byteClasses = {
InputStream.class, ByteBuffer.class, byte[].class
};
String[] charsets = {
Charset.defaultCharset().name(),
StandardCharsets.UTF_8.name(),
StandardCharsets.UTF_16.name(),
StandardCharsets.UTF_16BE.name(),
StandardCharsets.UTF_16LE.name(),
StandardCharsets.ISO_8859_1.name(),
"windows-1252",
StandardCharsets.US_ASCII.name(),
};
try {
flavorList.add(new DataFlavor(
DataFlavor.javaJVMLocalObjectMimeType +
"; class=" + String.class.getName()));
for (String mimeType : mimeTypes) {
for (Class<?> textClass : textClasses) {
flavorList.add(new DataFlavor(String.format(
"%s; class=\"%s\"",
mimeType, textClass.getName())));
}
for (String charset : charsets) {
for (Class<?> byteClass : byteClasses) {
flavorList.add(new DataFlavor(String.format(
"%s; charset=%s; class=\"%s\"",
mimeType, charset, byteClass.getName())));
}
}
}
for (String mimeType : mimeTypes) {
flavorList.add(new DataFlavor(String.format(
"%s; charset=unicode; class=\"%s\"",
mimeType, InputStream.class.getName())));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
this.flavors = Collections.unmodifiableCollection(flavorList);
}
#Override
public int getSourceActions(JComponent component) {
return COPY_OR_MOVE;
}
#Override
public void exportToClipboard(JComponent component,
Clipboard clipboard,
int action) {
JTextPane pane = (JTextPane) component;
Document doc = pane.getDocument();
int start = pane.getSelectionStart();
int end = pane.getSelectionEnd();
final String html;
final String plainText;
try {
StringWriter writer = new StringWriter(end - start);
pane.getEditorKit().write(writer, doc, start, end - start);
html = writer.toString();
StringBuilder plainTextBuilder = new StringBuilder();
appendTextContent(doc.getDefaultRootElement(), start, end,
plainTextBuilder);
plainText = plainTextBuilder.toString();
} catch (BadLocationException | IOException e) {
throw new RuntimeException(e);
}
Transferable contents = new Transferable() {
#Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return flavors.contains(flavor);
}
#Override
public DataFlavor[] getTransferDataFlavors() {
return flavors.toArray(new DataFlavor[0]);
}
#Override
public Object getTransferData(DataFlavor flavor)
throws UnsupportedFlavorException,
IOException {
String data;
if (flavor.isMimeTypeEqual("text/html")) {
data = html;
} else {
data = plainText;
}
Class<?> dataClass = flavor.getRepresentationClass();
if (dataClass.equals(char[].class)) {
return data.toCharArray();
}
if (flavor.isRepresentationClassReader()) {
return new StringReader(data);
}
if (flavor.isRepresentationClassCharBuffer()) {
return CharBuffer.wrap(data);
}
if (flavor.isRepresentationClassByteBuffer()) {
String charset = flavor.getParameter("charset");
return Charset.forName(charset).encode(data);
}
if (flavor.isRepresentationClassInputStream()) {
String charset = flavor.getParameter("charset");
return new ByteArrayInputStream(
data.getBytes(charset));
}
if (dataClass.equals(byte[].class)) {
String charset = flavor.getParameter("charset");
return data.getBytes(charset);
}
return data;
}
};
clipboard.setContents(contents, null);
if (action == MOVE) {
pane.replaceSelection("");
}
}
private void appendTextContent(Element element,
int textStart,
int textEnd,
StringBuilder content)
throws BadLocationException {
int start = element.getStartOffset();
int end = element.getEndOffset();
if (end < textStart || start >= textEnd) {
return;
}
start = Math.max(start, textStart);
end = Math.min(end, textEnd);
AttributeSet attr = element.getAttributes();
Object tag = attr.getAttribute(AttributeSet.NameAttribute);
if (tag.equals(HTML.Tag.HEAD) ||
tag.equals(HTML.Tag.TITLE) ||
tag.equals(HTML.Tag.COMMENT) ||
tag.equals(HTML.Tag.SCRIPT)) {
return;
}
if (tag.equals(HTML.Tag.INPUT) ||
tag.equals(HTML.Tag.TEXTAREA) ||
tag.equals(HTML.Tag.SELECT)) {
// Swing doesn't provide a way to read input values
// dynamically (as far as I know; I could be wrong).
return;
}
if (tag.equals(HTML.Tag.IMG)) {
Object altText = attr.getAttribute(HTML.Attribute.ALT);
if (altText != null) {
content.append(altText);
}
return;
}
if (tag.equals(HTML.Tag.CONTENT)) {
content.append(
element.getDocument().getText(start, end - start));
return;
}
int count = element.getElementCount();
for (int i = 0; i < count; i++) {
appendTextContent(element.getElement(i), textStart, textEnd,
content);
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JTextPane text = new JTextPane();
text.setContentType("text/html");
text.setEditable(false);
text.setText("somewords <img src=\"file:///C:/filepath/fire.png\" text=\"[fire1]\" title=\"[fire2]\" alt=\"[fire3]\" style=\"width:11px;height:11px;\"> otherwords");
text.setTransferHandler(new HTMLCopier());
JFrame window = new JFrame("HTML Copier");
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.getContentPane().add(new JScrollPane(text));
window.pack();
window.setLocationByPlatform(true);
window.setVisible(true);
text.selectAll();
text.copy();
}
});
}
}
Edit: Updated code to properly place only highlighted text on clipboard.
JTextPane provides method setEditorKit(EditorKit). I think you'll find your solution by providing a custom EditorKit.
You can override the copy and cut actions in a DefaultEditorKit, then pass it to JTextPane.
http://docs.oracle.com/javase/7/docs/api/javax/swing/text/DefaultEditorKit.html#copyAction
Or Java 8 introduces HTMLEditorKit that, if compatible with JTextPane, may provide the behavior you want.
https://docs.oracle.com/javase/8/docs/api/javax/swing/text/html/HTMLEditorKit.html
I want to have a button that, when clicked, opens the default browser and points to a URI.
I have some code that works in my testing, when run in Netbeans 7.x, but it fails when deployed as a JAR. My Java app runs on Linux only.
Anyone see the problem or know of another solution?
This code is in my main form:
linkBtnTest = new LinkButton(
new URI("http://www.example.com/some-page"),
"Click here for blah blah blah");
linkBtnTest.init();
} catch (Exception ex) {
Logger.log(ex);
}
Accessibility.increaseFontSize(linkBtnTest,
ApplicationContext.get().getFontIncreaseSize());
TestPanel.add(linkBtnTest);
Here's the class. I didn't write this code and I'm open to other suggestions:
package com.example.client;
import java.awt.Desktop;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.URI;
import javax.swing.JButton;
public class LinkButton extends JButton
implements ActionListener {
/** The target or href of this link. */
private URI target;
final static private String defaultText = "<HTML>Click the <FONT color=\"#000099\"><U>link</U></FONT>"
+ " to go to the example website.</HTML>";
public LinkButton(URI target, String text) {
super(text);
this.target = target;
//this.setText(text);
this.setToolTipText(target.toString());
}
public LinkButton(URI target) {
this( target, target.toString() );
}
public void init() {
this.addActionListener(this);
}
public void actionPerformed(ActionEvent e) {
open(target);
}
class OpenUrlAction implements ActionListener {
#Override public void actionPerformed(ActionEvent e) {
open(target);
}
}
private static void open(URI uri) {
if (Desktop.isDesktopSupported()) {
try {
Desktop.getDesktop().browse(uri);
} catch (IOException e) { /* TODO: error handling */ }
} else { /* TODO: error handling */ }
}
}
}
I've implemented a set of draggable tabs, following the form of this example:
How to implement draggable tab using Java Swing?
Everything appears to work as I desire, however,when I drag outside of the main panel, the desktop will become a valid drop target (the resulting drop is accepted and marked successful).
Is there a way to intercept this drop to react to dropping outside of our root pane? It's simple enough to detect, but it's not clear to me how to actually capture the drop before the outside world does.
By the time DragSourceListener's dragDropEnd is called, the drop is already executed and there doesn't appear to be a good way to end dragging in dragOver/Exit/Whatever.
Gee, it'd be nice if something like this worked:
#Override
public void dragOver(DragSourceDragEvent dragEvent)
{
DragEnabledTabTransferData data = getTabTransferData(dragEvent);
DragSourceContext dragSourceContext = dragEvent.getDragSourceContext();
if (data == null)
{
dragSourceContext.setCursor(DragSource.DefaultMoveNoDrop);
return;
}
if (!data.getTabbedPane().getRootPane().getBounds().contains(dragEvent.getLocation()))
{
dragSourceContext.dragDropEnd(new DragSourceDropEvent(dragSourceContext, 999, true));
}
}
Instead the drag continues dragging along. I do, however get a dragDropEnd for my troubles.
Any ideas? I'd be pretty sad to hear that the only solution would be to have some hidden maximized global pane that acted only as a drop target to capture out-of-window events.
Here is a working example. If you drag a tab out to, say, the desktop in Linux, it'll try to cast the transfer data into a Serializable and not be happy. The drag over I was playing with is commented with "This is where I'd assume we'd be able to intercept stuff" if you want to jump straight to what I'd pointed to above.
/** "Simple" example of DnD tabbed panes. Sourced from Eugene Yokota:
* http:stackoverflow.com/questions/60269/how-to-implement-draggable-tab-using-java-swing */
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import javax.swing.*;
public class DnDTabbedPane extends JTabbedPane {
private static final String NAME = "TabTransferData";
private final DataFlavor FLAVOR = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType, NAME);
public DnDTabbedPane() {
super();
final DragSourceListener dsl = new DragSourceListener() {
public void dragEnter(DragSourceDragEvent e) {
e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
}
public void dragExit(DragSourceEvent e) {
e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
}
/**
* This is where I'd assume we'd be able to intercept stuff
* so drops don't happen where we don't want them to.
*/
public void dragOver(DragSourceDragEvent e) {
TabTransferData data = getTabTransferData(e);
if (data == null) {
e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
return;
}
//This is where I ended up robokilling the drag via hackery
e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
}
public void dragDropEnd(DragSourceDropEvent e) {}
public void dropActionChanged(DragSourceDragEvent e) {}
};
final DragGestureListener dgl = new DragGestureListener() {
public void dragGestureRecognized(DragGestureEvent e) {
Point tabPt = e.getDragOrigin();
int dragTabIndex = indexAtLocation(tabPt.x, tabPt.y);
if (dragTabIndex < 0) {
return;
}
e.startDrag(DragSource.DefaultMoveDrop,new TabTransferable(DnDTabbedPane.this, dragTabIndex), dsl);
}
};
new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, new CDropTargetListener(), true);
new DragSource().createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_COPY_OR_MOVE, dgl);
}
private TabTransferData getTabTransferData(DropTargetDropEvent a_event) {
try {
return (TabTransferData) a_event.getTransferable().getTransferData(FLAVOR);
} catch (Exception e) {}
return null;
}
private TabTransferData getTabTransferData(DropTargetDragEvent a_event) {
try {
return (TabTransferData) a_event.getTransferable().getTransferData(FLAVOR);
} catch (Exception e) {}
return null;
}
private TabTransferData getTabTransferData(DragSourceDragEvent a_event) {
try {
return (TabTransferData) a_event.getDragSourceContext().getTransferable().getTransferData(FLAVOR);
} catch (Exception e) {}
return null;
}
class TabTransferable implements Transferable {
private TabTransferData m_data = null;
private DataFlavor[] flavors = {FLAVOR};
public TabTransferable(DnDTabbedPane a_tabbedPane, int a_tabIndex) {
m_data = new TabTransferData(DnDTabbedPane.this, a_tabIndex);
}
public Object getTransferData(DataFlavor flavor) {
return m_data;
}
public DataFlavor[] getTransferDataFlavors() {
return flavors;
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
return flavor.getHumanPresentableName().equals(NAME);
}
}
class TabTransferData {
DnDTabbedPane m_tabbedPane = null;
int m_tabIndex = -1;
public TabTransferData(DnDTabbedPane a_tabbedPane, int a_tabIndex) {
m_tabbedPane = a_tabbedPane;
m_tabIndex = a_tabIndex;
}
}
class CDropTargetListener implements DropTargetListener {
public void dragEnter(DropTargetDragEvent e) {
if (isDragAcceptable(e)) {
e.acceptDrag(e.getDropAction());
} else {
e.rejectDrag();
}
}
public void drop(DropTargetDropEvent a_event) {
if (isDropAcceptable(a_event)) {
convertTab(getTabTransferData(a_event),
getTargetTabIndex(a_event.getLocation()));
a_event.dropComplete(true);
} else {
a_event.dropComplete(false);
}
}
private boolean isTransferableGood(Transferable t, DataFlavor flavor)
{
return t == null || t.isDataFlavorSupported(flavor);
}
private boolean isDataGood(TabTransferData data)
{
if (DnDTabbedPane.this == data.m_tabbedPane && data.m_tabIndex >= 0) {
return true;
}
return false;
}
public boolean isDragAcceptable(DropTargetDragEvent e) {
Transferable t = e.getTransferable();
if (!isTransferableGood(t, e.getCurrentDataFlavors()[0])) {
return false;
}
return isDataGood(getTabTransferData(e));
}
public boolean isDropAcceptable(DropTargetDropEvent e) {
Transferable t = e.getTransferable();
if (!isTransferableGood(t, e.getCurrentDataFlavors()[0])) {
return false;
}
return isDataGood(getTabTransferData(e));
}
public void dragExit(DropTargetEvent e) {}
public void dropActionChanged(DropTargetDragEvent e) {}
public void dragOver(final DropTargetDragEvent e) {}
}
private int getTargetTabIndex(Point a_point) {
for (int i = 0; i < getTabCount(); i++) {
Rectangle r = getBoundsAt(i);
r.setRect(r.x - r.width / 2, r.y, r.width, r.height);
if (r.contains(a_point)) {
return i;
}
}
return -1;
}
private void convertTab(TabTransferData a_data, int a_targetIndex) {
DnDTabbedPane source = a_data.m_tabbedPane;
int sourceIndex = a_data.m_tabIndex;
if (sourceIndex < 0) {
return;
}
Component cmp = source.getComponentAt(sourceIndex);
String str = source.getTitleAt(sourceIndex);
if (a_targetIndex < 0 || sourceIndex == a_targetIndex) {
return;
}
source.remove(sourceIndex);
if (a_targetIndex == getTabCount()) {
addTab(str, cmp);
} else if (sourceIndex > a_targetIndex) {
insertTab(str, null, cmp, null, a_targetIndex);
} else {
insertTab(str, null, cmp, null, a_targetIndex - 1);
}
}
public static void main(String[] args)
{
JFrame window = new JFrame();
DnDTabbedPane tabbedPane = new DnDTabbedPane();
for(int i=0; i< 5; i++)
{
tabbedPane.addTab("I'm tab "+i, new JLabel("I'm tab "+i));
}
window.add(tabbedPane);
window.setSize(400, 200);
window.setVisible(true);
}
}
Thus far, the best I can do is call something to this effect when we hop out of the parent.
Component rootPane = SwingUtilities.getRoot(component);
Rectangle bounds = rootPane.getBounds();
if (!bounds.contains(location))
{
Robot robot = null;
try
{
robot = new Robot();
} catch (AWTException e)
{
return;
}
robot.keyPress(KeyEvent.VK_ESCAPE);
robot.keyRelease(KeyEvent.VK_ESCAPE);
}
It's a total hack, and doesn't solve my issue. I'd like to intercept the final drop event, see if it was outside of the frame and spawn the tab in its own JFrame.
If I was using the NetBeans, MyDoggy, or Eclipse frameworks, I guess this would all be magically handled for me. Alas.
There is no Way to Cancel the Drag directly. see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4502185
I would prefer to show the User that Drop on Desktop is not allowed, by changing the Cursor.
Your DragSourceListener dsl has in the dragOver method a DragSourceDragEvent which tells you
that the target action is NONE over the Desktop.
Change to this:
public void dragOver(DragSourceDragEvent e) {
TabTransferData data = getTabTransferData(e);
if( data == null || e.getTargetActions() == DnDConstants.ACTION_NONE ) {
e.getDragSourceContext().setCursor( DragSource.DefaultMoveNoDrop );
return;
}
e.getDragSourceContext().setCursor( DragSource.DefaultMoveDrop);
}
If you really want to Cancel, than you have to use your ESC solution or something like that:
try {
new Robot().mouseRelease( InputEvent.BUTTON1_MASK ); // if Button1 was the only Button to start a Drag
} catch( AWTException e1 ) {
}
As confirmed by #oliholz, there just isn't a way to do it without having to force a cancel via a keystroke.
However, for my needs of creating a tear-off tab, I found that creating a floating pane that was, itself, a drop target listener felt like the cleanest solution:
package com.amish.whatever;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JLabel;
import javax.swing.JWindow;
import javax.swing.Timer;
public class TearAwayTab extends JWindow {
MousePoller mousePoller = new MousePoller();
public TearAwayTab() {
this.add(new JLabel("FLONT"));
this.pack();
new DropTarget(this, DnDConstants.ACTION_COPY_OR_MOVE, new EasyDropTarget(), true);
this.setVisible(false);
}
private void center(Point location)
{
Point center = new Point();
center.setLocation(location.x-this.getWidth()/2, location.y-this.getHeight()/2);
TearAwayTab.this.setLocation(center);
}
public void attach(Point location)
{
center(location);
mousePoller.start();
this.setVisible(true);
}
public void detach()
{
mousePoller.stop();
this.setVisible(false);
}
private int DELAY = 10;
private class MousePoller extends Timer{
public MousePoller(){
super(DELAY, new ActionListener() {
private Point lastPoint = MouseInfo.getPointerInfo().getLocation();
#Override
public void actionPerformed(ActionEvent e) {
Point point = MouseInfo.getPointerInfo().getLocation();
if (!point.equals(lastPoint)) {
center(point);
}
lastPoint = point;
}
});
}
}
private class EasyDropTarget implements DropTargetListener
{
#Override
public void dragEnter(DropTargetDragEvent dtde) {
dtde.acceptDrag(dtde.getDropAction());
}
#Override
public void dragOver(DropTargetDragEvent dtde) {}
#Override
public void dropActionChanged(DropTargetDragEvent dtde) {}
#Override
public void dragExit(DropTargetEvent dte) {}
#Override
public void drop(DropTargetDropEvent dtde) {
dtde.dropComplete(true);
detach();
System.out.println("DROP Intercepted");
}
}
}
The bit with the MousePoller works around scrubbing the mouse too fast for mouse listeners to reliably update the location. I'd tried with a motion listener and was able to escape the bounds of the floater quite easily.
Back in the first example, I now include the tear away tab as a private member of the tabbed pane, and call attach and detach when exiting or entering my drop areas:
final DragSourceListener dsl = new DragSourceListener() {
public void dragEnter(DragSourceDragEvent e) {
e.getDragSourceContext().setCursor(DragSource.DefaultMoveDrop);
Rectangle bounds = SwingUtilities.getRoot(DnDTabbedPane.this).getBounds();
if(bounds.contains(e.getLocation())){
tearTab.detach();
}
}
public void dragExit(DragSourceEvent e) {
e.getDragSourceContext().setCursor(DragSource.DefaultMoveNoDrop);
tearTab.attach(e.getLocation());
}
...
This also has the added benefit of preserving the DnD operation in the case of dragging out, and then back in.
Thanks for the input. If you have any other ideas/comments, I'm all ears.
This doesn't directly relate to tabs, but one way to stop drags from being able to be dragged to the desktop is to wrap whatever you're dragging in a custom wrapper class. Then, when you make your TransferHandler, make a DataFlavor localFlavor = new ActivationDataFlavor(YourWrapperClass.class, DataFlavor.javaJVMLocalObjectMimeType, "description"); Next, override the createTransferable method to have new DataHandler(passedInComponent, localFlavor.getMimeType()); and return a new Transferable in which you've overridden all the methods to only have your localFlavor. Finally, in the importData method, make sure to import your data as your localFlavor type. This will prevent dragging to the deaktop as the flavor you defined is local to the JVM.
I'm writing an eclipse-plugin which creating a new Console. Please see my source code:
CliConsoleFactory.java
import java.io.IOException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.ConsolePlugin;
import org.eclipse.ui.console.IConsole;
import org.eclipse.ui.console.IConsoleConstants;
import org.eclipse.ui.console.IConsoleFactory;
import org.eclipse.ui.console.IConsoleView;
import org.eclipse.ui.console.IOConsoleOutputStream;
public class CliConsoleFactory implements IConsoleFactory {
private static final String ENTER_KEY = "\r\n";
private static final String CLI_PROMPT = "CLI> ";
private IConsoleView m_consoleView = null;
#Override
public void openConsole() {
IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
try {
m_consoleView = (IConsoleView) page.showView(IConsoleConstants.ID_CONSOLE_VIEW);
} catch (PartInitException e1) {
e1.printStackTrace();
}
if (m_consoleView == null) {
return;
}
final MyIOConsole myConsole = new MyIOConsole("CLI", null);
final IDocument document = myConsole.getDocument();
document.addDocumentListener(new IDocumentListener() {
#Override
public void documentChanged(DocumentEvent event) {
if (ENTER_KEY.equals(event.getText())) {
// Print the Prompt
writeToConsole(myConsole, CLI_PROMPT);
}
}
#Override
public void documentAboutToBeChanged(DocumentEvent event) {
}
});
ConsolePlugin.getDefault().getConsoleManager().addConsoles(new IConsole[] { myConsole });
m_consoleView.display(myConsole);
writeToConsole(myConsole, CLI_PROMPT);
}
private void writeToConsole(final MyIOConsole myConsole, String msg) {
IOConsoleOutputStream stream = myConsole.newOutputStream();
stream.setActivateOnWrite(true);
try {
stream.write(msg);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
MyIOConsole.java
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.ui.console.IOConsole;
public class MyIOConsole extends IOConsole {
public MyIOConsole(String name, ImageDescriptor imageDescriptor) {
super(name, imageDescriptor);
}
}
It works great. When I enter to a new line, the Prompt is "CLI> ", but the Caret position is not okie, it is at the first position of the line instead of the last position. I want to make the Caret move to the last position. Who know please help me.!!!
To gain access to the caret position, you will need to implement a console viewer.
This is the setup I have for my custom console,
public class MyConsole extends IOConsole
{
....
#Override
public IPageBookViewPage createPage(IConsoleView view) {
return new MyConsolePage(this, view);
}
}
public class MyConsolePage extends TextConsolePage
{
....
#Override
protected TextConsoleViewer createViewer(Composite parent) {
return new MyConsoleViewer(parent, (MyConsole) this.getConsole());
}
}
public class MyConsoleViewer extends TextConsoleViewer
{
//This class gives you access to setting the caret position
//by getting the styled text widget and then using setCaretOffset
}
There are multiple ways of getting the styled text widget depending on which method you are overriding. I also created my own Console history class which kept track of the caret offset since I needed additional functionality of using the up and down arrow keys to navigate through previously entered commands.
The best way to implement the MyConsoleViewer is to use Eclipse's vast source code that sets a perfect example. I practically reused all of this class org.eclipse.ui.internal.console.IOConsoleViewer. It even shows examples of setting the caret.
Hope this still helps as your question was a while ago.