001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.core;
043:
044: import java.awt.AWTEvent;
045: import java.awt.Toolkit;
046: import java.awt.datatransfer.Clipboard;
047: import java.awt.datatransfer.ClipboardOwner;
048: import java.awt.datatransfer.DataFlavor;
049: import java.awt.datatransfer.FlavorEvent;
050: import java.awt.datatransfer.FlavorListener;
051: import java.awt.datatransfer.Transferable;
052: import java.awt.datatransfer.UnsupportedFlavorException;
053: import java.awt.event.AWTEventListener;
054: import java.awt.event.WindowEvent;
055: import java.io.IOException;
056: import java.lang.ref.Reference;
057: import java.lang.ref.WeakReference;
058: import java.util.Collection;
059: import java.util.logging.Level;
060: import java.util.logging.Logger;
061: import org.openide.util.Exceptions;
062: import org.openide.util.Lookup;
063: import org.openide.util.LookupEvent;
064: import org.openide.util.LookupListener;
065: import org.openide.util.RequestProcessor;
066: import org.openide.util.Utilities;
067: import org.openide.util.datatransfer.ExClipboard;
068:
069: public final class NbClipboard extends ExClipboard implements
070: LookupListener, Runnable, FlavorListener, AWTEventListener {
071: private Logger log;
072: private Clipboard systemClipboard;
073: private ExClipboard.Convertor[] convertors;
074: private Lookup.Result<ExClipboard.Convertor> result;
075: final boolean slowSystemClipboard;
076: private Transferable last;
077: private long lastWindowActivated;
078: private long lastWindowDeactivated;
079: private Reference<Object> lastWindowDeactivatedSource = new WeakReference<Object>(
080: null);
081:
082: public NbClipboard() {
083: super ("NBClipboard"); // NOI18N
084: systemClipboard = Toolkit.getDefaultToolkit()
085: .getSystemClipboard();
086: log = Logger.getLogger("org.netbeans.core.NbClipboard"); // NOI18N
087:
088: result = Lookup.getDefault().lookupResult(
089: ExClipboard.Convertor.class);
090: result.addLookupListener(this );
091:
092: systemClipboard.addFlavorListener(this );
093:
094: resultChanged(null);
095:
096: if (System.getProperty("netbeans.slow.system.clipboard.hack") != null) {
097: slowSystemClipboard = Boolean
098: .getBoolean("netbeans.slow.system.clipboard.hack"); // NOI18N
099: } else if (Utilities.isMac()) {
100: slowSystemClipboard = false;
101: } else {
102: slowSystemClipboard = true;
103: }
104:
105: if (!slowSystemClipboard) {
106: if (System.getProperty("sun.awt.datatransfer.timeout") == null) { // NOI18N
107: System.setProperty("sun.awt.datatransfer.timeout",
108: "1000"); // NOI18N
109: }
110: } else {
111: Toolkit.getDefaultToolkit().addAWTEventListener(this ,
112: AWTEvent.WINDOW_EVENT_MASK);
113: }
114: }
115:
116: protected synchronized ExClipboard.Convertor[] getConvertors() {
117: return convertors;
118: }
119:
120: public synchronized void resultChanged(LookupEvent ev) {
121: Collection<? extends ExClipboard.Convertor> c = result
122: .allInstances();
123: ExClipboard.Convertor[] temp = new ExClipboard.Convertor[c
124: .size()];
125: convertors = c.toArray(temp);
126: }
127:
128: // XXX(-ttran) on Unix (and also on Windows as we discovered recently)
129: // calling getContents() on the system clipboard is very expensive, the
130: // call can take up to 1000 milliseconds. We need to examine the clipboard
131: // contents each time the Node is activated, the method must be fast.
132: // Therefore we cache the contents of system clipboard and use the cache
133: // when someone calls getContents(). The cache is sync'ed with the system
134: // clipboard when _any_ of our Windows gets WINDOW_ACTIVATED event. It
135: // means if some other apps modify the contents of the system clipboard in
136: // the background then the change won't be propagated to us immediately.
137: // The other drawback is that if module code bypasses NBClipboard and
138: // accesses the system clipboard directly then we don't see these changes.
139: //
140: // The other problem is an AWT bug
141: //
142: // http://developer.java.sun.com/developer/bugParade/bugs/4818143.html
143: //
144: // sun.awt.datatransfer.ClipboardTransferable.getClipboardData() can hang
145: // for very long time (maxlong == eternity). We tries to avoid the hang by
146: // access the system clipboard from a separate thread. If the hang happens
147: // the thread will wait for the system clipboard forever but not the whole
148: // IDE
149:
150: private RequestProcessor.Task syncTask = new RequestProcessor(
151: "System clipboard synchronizer").create(this , true); // NOI18N
152:
153: private Transferable data;
154: private ClipboardOwner dataOwner;
155:
156: @Override
157: public void setContents(Transferable contents, ClipboardOwner owner) {
158: synchronized (this ) {
159: // XXX(-dstrupl) the following line might lead to a double converted
160: // transferable. Can be fixed as Jesse describes in #32485
161: if (log.isLoggable(Level.FINER)) {
162: log.log(Level.FINER, "setContents called with: "); // NOI18N
163: logFlavors(contents, Level.FINER, log
164: .isLoggable(Level.FINEST));
165: }
166: contents = convert(contents);
167: if (log.isLoggable(Level.FINER)) {
168: log.log(Level.FINER, "After conversion:"); // NOI18N
169: logFlavors(contents, Level.FINER, log
170: .isLoggable(Level.FINEST));
171: }
172:
173: if (slowSystemClipboard) {
174: super .setContents(contents, owner);
175: } else {
176: if (last != null)
177: transferableOwnershipLost(last);
178: last = contents;
179: }
180:
181: data = contents;
182: dataOwner = owner;
183: syncTask.schedule(0);
184: }
185: fireClipboardChange();
186: }
187:
188: @Override
189: public Transferable getContents(Object requestor) {
190: Transferable prev;
191:
192: try {
193: log.log(Level.FINE,
194: "getContents, slowSystemClipboard: {0}",
195: slowSystemClipboard); // NOI18N
196: if (slowSystemClipboard) {
197: // The purpose of lastWindowActivated+100 is to ignore calls
198: // which immediatelly follow WINDOW_ACTIVATED event.
199: // This is workaround of JDK bug described in issue 41098.
200: long curr = System.currentTimeMillis();
201: if (lastWindowActivated != 0
202: && lastWindowActivated + 100 < curr) {
203: lastWindowActivated = 0;
204:
205: syncTask.schedule(0);
206: boolean finished = syncTask.waitFinished(100);
207: log.log(Level.FINE,
208: "after syncTask wait, finished {0}",
209: finished); // NOI18N
210: } else {
211: if (log.isLoggable(Level.FINE)) {
212: log
213: .fine("no wait, last: "
214: + lastWindowActivated
215: + " now: " + curr); // NOI18N
216: }
217: }
218:
219: prev = super .getContents(requestor);
220: } else {
221: syncTask.waitFinished();
222: log.log(Level.FINE, "after syncTask clipboard wait"); // NOI18N
223: prev = systemClipboard.getContents(requestor);
224: }
225:
226: synchronized (this ) {
227: if (log.isLoggable(Level.FINE)) {
228: log.log(Level.FINE, "getContents by " + requestor); // NOI18N
229: logFlavors(prev, Level.FINE, log
230: .isLoggable(Level.FINEST));
231: }
232: if (prev == null) // if system clipboard has no contents
233: return null;
234:
235: Transferable res = convert(prev);
236: if (log.isLoggable(Level.FINE)) {
237: log.log(Level.FINE, "getContents by " + requestor); // NOI18N
238: logFlavors(res, Level.FINE, log
239: .isLoggable(Level.FINEST));
240:
241: res = new LoggableTransferable(res);
242: }
243: return res;
244: }
245: } catch (ThreadDeath ex) {
246: throw ex;
247: } catch (InterruptedException ex) {
248: Logger.getLogger(NbClipboard.class.getName()).log(
249: Level.WARNING, null, ex);
250: return null;
251: } catch (Throwable ex) {
252: Exceptions.printStackTrace(ex);
253: return null;
254: }
255: }
256:
257: public void run() {
258: Transferable cnts = null;
259: ClipboardOwner ownr = null;
260:
261: log.fine("Running update");
262:
263: synchronized (this ) {
264: if (data != null) {
265: cnts = data;
266: ownr = dataOwner;
267: }
268: data = null;
269: dataOwner = null;
270: }
271: if (cnts != null) {
272: systemClipboard.setContents(cnts, ownr);
273: if (log.isLoggable(Level.FINE)) {
274: log.log(Level.FINE, "systemClipboard updated:"); // NOI18N
275: logFlavors(cnts, Level.FINE, log
276: .isLoggable(Level.FINEST));
277: }
278: return;
279: }
280:
281: try {
282: Transferable transferable = systemClipboard
283: .getContents(this );
284: super .setContents(transferable, null);
285: if (log.isLoggable(Level.FINE)) {
286: log.log(Level.FINE, "internal clipboard updated:"); // NOI18N
287: logFlavors(transferable, Level.FINE, log
288: .isLoggable(Level.FINEST));
289: }
290: fireClipboardChange();
291: } catch (ThreadDeath ex) {
292: throw ex;
293: } catch (Throwable ignore) {
294: }
295: }
296:
297: /** For testing purposes.
298: */
299: final void waitFinished() {
300: syncTask.waitFinished();
301: }
302:
303: final void activateWindowHack(boolean reschedule) {
304: // if WINDOW_DEACTIVATED is followed immediatelly with
305: // WINDOW_ACTIVATED then it is JDK bug described in
306: // issue 41098.
307: lastWindowActivated = System.currentTimeMillis();
308: if (reschedule) {
309: syncTask.schedule(0);
310: }
311: }
312:
313: private void logFlavors(Transferable trans, Level level,
314: boolean content) {
315: if (trans == null)
316: log.log(level, " no clipboard contents");
317: else {
318: java.awt.datatransfer.DataFlavor[] arr = trans
319: .getTransferDataFlavors();
320: StringBuffer sb = new StringBuffer();
321: for (int i = 0; i < arr.length; i++) {
322: sb.append(" ").append(i).append(" = ").append(arr[i]);
323: if (content) {
324: try {
325: sb.append(" contains: ").append(
326: trans.getTransferData(arr[i]));
327: } catch (UnsupportedFlavorException ex) {
328: Exceptions.printStackTrace(ex);
329: } catch (IOException ex) {
330: Exceptions.printStackTrace(ex);
331: }
332: }
333: sb.append("\n");
334: }
335: log.log(level, sb.toString());
336: }
337: }
338:
339: public void flavorsChanged(FlavorEvent e) {
340: fireClipboardChange();
341: }
342:
343: public void eventDispatched(AWTEvent ev) {
344: if (!(ev instanceof WindowEvent))
345: return;
346:
347: if (ev.getID() == WindowEvent.WINDOW_DEACTIVATED) {
348: lastWindowDeactivated = System.currentTimeMillis();
349: lastWindowDeactivatedSource = new WeakReference<Object>(ev
350: .getSource());
351: }
352: if (ev.getID() == WindowEvent.WINDOW_ACTIVATED) {
353: if (System.currentTimeMillis() - lastWindowDeactivated < 100
354: && ev.getSource() == lastWindowDeactivatedSource
355: .get()) {
356: activateWindowHack(false);
357: }
358: if (log.isLoggable(Level.FINE)) {
359: log.log(Level.FINE,
360: "window activated scheduling update"); // NOI18N
361: }
362: syncTask.schedule(0);
363: }
364: }
365:
366: /** Transferable that logs operations on itself.
367: */
368: private final class LoggableTransferable implements Transferable {
369: private Transferable delegate;
370:
371: public LoggableTransferable(Transferable delegate) {
372: this .delegate = delegate;
373: }
374:
375: public Object getTransferData(DataFlavor flavor)
376: throws UnsupportedFlavorException, java.io.IOException {
377: log.log(Level.FINE, "Request for flavor: " + flavor); // NOI18N
378: Object res = delegate.getTransferData(flavor);
379: log.log(Level.FINE, "Returning value: " + res); // NOI18N
380: return res;
381: }
382:
383: public DataFlavor[] getTransferDataFlavors() {
384: return delegate.getTransferDataFlavors();
385: }
386:
387: public boolean isDataFlavorSupported(DataFlavor flavor) {
388: boolean res = delegate.isDataFlavorSupported(flavor);
389: log.log(Level.FINE, "isDataFlavorSupported: " + flavor
390: + " result: " + res); // NOI18N
391: return res;
392: }
393:
394: }
395: }
|