blob: c9b9a1abfd3a231ea1bb8cda304d67620d071826 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2005-2007 Sun Microsystems, Inc. All Rights Reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package sun.awt.X11;
27
28import java.awt.*;
29import java.awt.event.*;
30import java.awt.peer.TrayIconPeer;
31import sun.awt.*;
32import java.awt.image.*;
33import java.text.BreakIterator;
34import java.util.Vector;
35import java.lang.reflect.Field;
36import java.util.logging.Logger;
37import java.util.logging.Level;
38import java.util.AbstractQueue;
39import java.util.concurrent.ArrayBlockingQueue;
40import java.security.AccessController;
41import java.security.PrivilegedAction;
42import java.lang.reflect.InvocationTargetException;
43
44public class XTrayIconPeer implements TrayIconPeer {
45 private static final Logger ctrLog = Logger.getLogger("sun.awt.X11.XTrayIconPeer.centering");
46
47 TrayIcon target;
48 TrayIconEventProxy eventProxy;
49 XTrayIconEmbeddedFrame eframe;
50 TrayIconCanvas canvas;
51 Balloon balloon;
52 Tooltip tooltip;
53 PopupMenu popup;
54 String tooltipString;
55 boolean isTrayIconDisplayed;
56 long eframeParentID;
57 final XEventDispatcher parentXED, eframeXED;
58
59 static final XEventDispatcher dummyXED = new XEventDispatcher() {
60 public void dispatchEvent(XEvent ev) {}
61 };
62
63 volatile boolean isDisposed;
64
65 boolean isParentWindowLocated;
66 int old_x, old_y;
67 int ex_width, ex_height;
68
69 final static int TRAY_ICON_WIDTH = 24;
70 final static int TRAY_ICON_HEIGHT = 24;
71
72 XTrayIconPeer(TrayIcon target)
73 throws AWTException
74 {
75 this.target = target;
76
77 eventProxy = new TrayIconEventProxy(this);
78
79 canvas = new TrayIconCanvas(target, TRAY_ICON_WIDTH, TRAY_ICON_HEIGHT);
80
81 eframe = new XTrayIconEmbeddedFrame();
82
83 eframe.setSize(TRAY_ICON_WIDTH, TRAY_ICON_HEIGHT);
84 eframe.add(canvas);
85
86 // Fix for 6317038: as EmbeddedFrame is instance of Frame, it is blocked
87 // by modal dialogs, but in the case of TrayIcon it shouldn't. So we
88 // set ModalExclusion property on it.
89 AccessController.doPrivileged(new PrivilegedAction() {
90 public Object run() {
91 eframe.setModalExclusionType(Dialog.ModalExclusionType.TOOLKIT_EXCLUDE);
92 return null;
93 }
94 });
95
96
97 if (XWM.getWMID() != XWM.METACITY_WM) {
98 parentXED = dummyXED; // We don't like to leave it 'null'.
99
100 } else {
101 parentXED = new XEventDispatcher() {
102 // It's executed under AWTLock.
103 public void dispatchEvent(XEvent ev) {
104 if (isDisposed() || ev.get_type() != XlibWrapper.ConfigureNotify) {
105 return;
106 }
107
108 XConfigureEvent ce = ev.get_xconfigure();
109
110 ctrLog.log(Level.FINE, "ConfigureNotify on parent of {0}: {1}x{2}+{3}+{4} (old: {5}+{6})",
111 new Object[] { XTrayIconPeer.this, ce.get_width(), ce.get_height(),
112 ce.get_x(), ce.get_y(), old_x, old_y });
113
114 // A workaround for Gnome/Metacity (it doesn't affect the behaviour on KDE).
115 // On Metacity the EmbeddedFrame's parent window bounds are larger
116 // than TrayIcon size required (that is we need a square but a rectangle
117 // is provided by the Panel Notification Area). The parent's background color
118 // differs from the Panel's one. To hide the background we resize parent
119 // window so that it fits the EmbeddedFrame.
120 // However due to resizing the parent window it loses centering in the Panel.
121 // We center it when discovering that some of its side is of size greater
122 // than the fixed value. Centering is being done by "X" (when the parent's width
123 // is greater) and by "Y" (when the parent's height is greater).
124
125 // Actually we need this workaround until we could detect taskbar color.
126
127 if (ce.get_height() != TRAY_ICON_HEIGHT && ce.get_width() != TRAY_ICON_WIDTH) {
128
129 // If both the height and the width differ from the fixed size then WM
130 // must level at least one side to the fixed size. For some reason it may take
131 // a few hops (even after reparenting) and we have to skip the intermediate ones.
132 ctrLog.log(Level.FINE, "ConfigureNotify on parent of {0}. Skipping as intermediate resizing.",
133 XTrayIconPeer.this);
134 return;
135
136 } else if (ce.get_height() > TRAY_ICON_HEIGHT) {
137
138 ctrLog.log(Level.FINE, "ConfigureNotify on parent of {0}. Centering by \"Y\".",
139 XTrayIconPeer.this);
140
141 XlibWrapper.XMoveResizeWindow(XToolkit.getDisplay(), eframeParentID,
142 ce.get_x(),
143 ce.get_y()+ce.get_height()/2-TRAY_ICON_HEIGHT/2,
144 TRAY_ICON_WIDTH,
145 TRAY_ICON_HEIGHT);
146 ex_height = ce.get_height();
147 ex_width = 0;
148
149 } else if (ce.get_width() > TRAY_ICON_WIDTH) {
150
151 ctrLog.log(Level.FINE, "ConfigureNotify on parent of {0}. Centering by \"X\".",
152 XTrayIconPeer.this);
153
154 XlibWrapper.XMoveResizeWindow(XToolkit.getDisplay(), eframeParentID,
155 ce.get_x()+ce.get_width()/2 - TRAY_ICON_WIDTH/2,
156 ce.get_y(),
157 TRAY_ICON_WIDTH,
158 TRAY_ICON_HEIGHT);
159 ex_width = ce.get_width();
160 ex_height = 0;
161
162 } else if (isParentWindowLocated && ce.get_x() != old_x && ce.get_y() != old_y) {
163 // If moving by both "X" and "Y".
164 // When some tray icon gets removed from the tray, a Java icon may be repositioned.
165 // In this case the parent window also lose centering. We have to restore it.
166
167 if (ex_height != 0) {
168
169 ctrLog.log(Level.FINE, "ConfigureNotify on parent of {0}. Move detected. Centering by \"Y\".",
170 XTrayIconPeer.this);
171
172 XlibWrapper.XMoveWindow(XToolkit.getDisplay(), eframeParentID,
173 ce.get_x(),
174 ce.get_y() + ex_height/2 - TRAY_ICON_HEIGHT/2);
175
176 } else if (ex_width != 0) {
177
178 ctrLog.log(Level.FINE, "ConfigureNotify on parent of {0}. Move detected. Centering by \"X\".",
179 XTrayIconPeer.this);
180
181 XlibWrapper.XMoveWindow(XToolkit.getDisplay(), eframeParentID,
182 ce.get_x() + ex_width/2 - TRAY_ICON_WIDTH/2,
183 ce.get_y());
184 } else {
185 ctrLog.log(Level.FINE, "ConfigureNotify on parent of {0}. Move detected. Skipping.",
186 XTrayIconPeer.this);
187 }
188 }
189 old_x = ce.get_x();
190 old_y = ce.get_y();
191 isParentWindowLocated = true;
192 }
193 };
194 }
195 eframeXED = new XEventDispatcher() {
196 // It's executed under AWTLock.
197 XTrayIconPeer xtiPeer = XTrayIconPeer.this;
198
199 public void dispatchEvent(XEvent ev) {
200 if (isDisposed() || ev.get_type() != XlibWrapper.ReparentNotify) {
201 return;
202 }
203
204 XReparentEvent re = ev.get_xreparent();
205 eframeParentID = re.get_parent();
206
207 if (eframeParentID == XToolkit.getDefaultRootWindow()) {
208
209 if (isTrayIconDisplayed) { // most likely Notification Area was removed
210 SunToolkit.executeOnEventHandlerThread(xtiPeer.target, new Runnable() {
211 public void run() {
212 SystemTray.getSystemTray().remove(xtiPeer.target);
213 }
214 });
215 }
216 return;
217 }
218
219 if (!isTrayIconDisplayed) {
220 addXED(eframeParentID, parentXED, XlibWrapper.StructureNotifyMask);
221
222 isTrayIconDisplayed = true;
223 XToolkit.awtLockNotifyAll();
224 }
225 }
226 };
227
228 addXED(getWindow(), eframeXED, XlibWrapper.StructureNotifyMask);
229
230 XSystemTrayPeer.getPeerInstance().addTrayIcon(this); // throws AWTException
231
232 // Wait till the EmbeddedFrame is reparented
233 long start = System.currentTimeMillis();
234 final long PERIOD = 2000L;
235 XToolkit.awtLock();
236 try {
237 while (!isTrayIconDisplayed) {
238 try {
239 XToolkit.awtLockWait(PERIOD);
240 } catch (InterruptedException e) {
241 break;
242 }
243 if (System.currentTimeMillis() - start > PERIOD) {
244 break;
245 }
246 }
247 } finally {
248 XToolkit.awtUnlock();
249 }
250
251 // This is unlikely to happen.
252 if (!isTrayIconDisplayed || eframeParentID == 0 ||
253 eframeParentID == XToolkit.getDefaultRootWindow())
254 {
255 throw new AWTException("TrayIcon couldn't be displayed.");
256 }
257
258 eframe.setVisible(true);
259 updateImage();
260
261 balloon = new Balloon(this, eframe);
262 tooltip = new Tooltip(this, eframe);
263
264 addListeners();
265 }
266
267 public void dispose() {
268 if (SunToolkit.isDispatchThreadForAppContext(target)) {
269 disposeOnEDT();
270 } else {
271 try {
272 SunToolkit.executeOnEDTAndWait(target, new Runnable() {
273 public void run() {
274 disposeOnEDT();
275 }
276 });
277 } catch (InterruptedException ie) {
278 } catch (InvocationTargetException ite) {}
279 }
280 }
281
282 private void disposeOnEDT() {
283 // All actions that is to be synchronized with disposal
284 // should be executed either under AWTLock, or on EDT.
285 // isDisposed value must be checked.
286 XToolkit.awtLock();
287 isDisposed = true;
288 XToolkit.awtUnlock();
289
290 removeXED(getWindow(), eframeXED);
291 removeXED(eframeParentID, parentXED);
292 eframe.realDispose();
293 balloon.dispose();
294 isTrayIconDisplayed = false;
295 XToolkit.targetDisposedPeer(target, this);
296 }
297
298 public static void suppressWarningString(Window w) {
299 WindowAccessor.setTrayIconWindow(w, true);
300 }
301
302 public void setToolTip(String tooltip) {
303 tooltipString = tooltip;
304 }
305
306 public void updateImage() {
307 Runnable r = new Runnable() {
308 public void run() {
309 canvas.updateImage(target.getImage());
310 }
311 };
312
313 if (!SunToolkit.isDispatchThreadForAppContext(target)) {
314 SunToolkit.executeOnEventHandlerThread(target, r);
315 } else {
316 r.run();
317 }
318 }
319
320 public void displayMessage(String caption, String text, String messageType) {
321 Point loc = getLocationOnScreen();
322 Rectangle screen = eframe.getGraphicsConfiguration().getBounds();
323
324 // Check if the tray icon is in the bounds of a screen.
325 if (!(loc.x < screen.x || loc.x >= screen.x + screen.width ||
326 loc.y < screen.y || loc.y >= screen.y + screen.height))
327 {
328 balloon.display(caption, text, messageType);
329 }
330 }
331
332 // It's synchronized with disposal by EDT.
333 public void showPopupMenu(int x, int y) {
334 if (isDisposed())
335 return;
336
337 assert SunToolkit.isDispatchThreadForAppContext(target);
338
339 PopupMenu newPopup = target.getPopupMenu();
340 if (popup != newPopup) {
341 if (popup != null) {
342 eframe.remove(popup);
343 }
344 if (newPopup != null) {
345 eframe.add(newPopup);
346 }
347 popup = newPopup;
348 }
349
350 if (popup != null) {
351 Point loc = ((XBaseWindow)eframe.getPeer()).toLocal(new Point(x, y));
352 popup.show(eframe, loc.x, loc.y);
353 }
354 }
355
356
357 // ******************************************************************
358 // ******************************************************************
359
360
361 private void addXED(long window, XEventDispatcher xed, long mask) {
362 if (window == 0) {
363 return;
364 }
365 XToolkit.awtLock();
366 try {
367 XlibWrapper.XSelectInput(XToolkit.getDisplay(), window, mask);
368 } finally {
369 XToolkit.awtUnlock();
370 }
371 XToolkit.addEventDispatcher(window, xed);
372 }
373
374 private void removeXED(long window, XEventDispatcher xed) {
375 if (window == 0) {
376 return;
377 }
378 XToolkit.awtLock();
379 try {
380 XToolkit.removeEventDispatcher(window, xed);
381 } finally {
382 XToolkit.awtUnlock();
383 }
384 }
385
386 // Private method for testing purposes.
387 private Point getLocationOnScreen() {
388 return eframe.getLocationOnScreen();
389 }
390
391 private Rectangle getBounds() {
392 Point loc = getLocationOnScreen();
393 return new Rectangle(loc.x, loc.y, loc.x + TRAY_ICON_WIDTH, loc.y + TRAY_ICON_HEIGHT);
394 }
395
396 void addListeners() {
397 canvas.addMouseListener(eventProxy);
398 canvas.addMouseMotionListener(eventProxy);
399 }
400
401 long getWindow() {
402 return ((XEmbeddedFramePeer)eframe.getPeer()).getWindow();
403 }
404
405 boolean isDisposed() {
406 return isDisposed;
407 }
408
409 static class TrayIconEventProxy implements MouseListener, MouseMotionListener {
410 XTrayIconPeer xtiPeer;
411
412 TrayIconEventProxy(XTrayIconPeer xtiPeer) {
413 this.xtiPeer = xtiPeer;
414 }
415
416 public void handleEvent(MouseEvent e) {
417 //prevent DRAG events from being posted with TrayIcon source(CR 6565779)
418 if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
419 return;
420 }
421
422 // Event handling is synchronized with disposal by EDT.
423 if (xtiPeer.isDisposed()) {
424 return;
425 }
426 Point coord = XBaseWindow.toOtherWindow(xtiPeer.getWindow(),
427 XToolkit.getDefaultRootWindow(),
428 e.getX(), e.getY());
429
430 if (e.isPopupTrigger()) {
431 xtiPeer.showPopupMenu(coord.x, coord.y);
432 }
433
434 e.translatePoint(coord.x - e.getX(), coord.y - e.getY());
435 // This is a hack in order to set non-Component source to MouseEvent
436 // instance.
437 // In some cases this could lead to unpredictable result (e.g. when
438 // other class tries to cast source field to Component).
439 // We already filter DRAG events out (CR 6565779).
440 e.setSource(xtiPeer.target);
441 Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(e);
442 }
443 public void mouseClicked(MouseEvent e) {
444 if ((e.getClickCount() > 1 || xtiPeer.balloon.isVisible()) &&
445 e.getButton() == MouseEvent.BUTTON1)
446 {
447 ActionEvent aev = new ActionEvent(xtiPeer.target, ActionEvent.ACTION_PERFORMED,
448 xtiPeer.target.getActionCommand(), e.getWhen(),
449 e.getModifiers());
450 Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(aev);
451 }
452 if (xtiPeer.balloon.isVisible()) {
453 xtiPeer.balloon.hide();
454 }
455 handleEvent(e);
456 }
457 public void mouseEntered(MouseEvent e) {
458 xtiPeer.tooltip.enter();
459 handleEvent(e);
460 }
461 public void mouseExited(MouseEvent e) {
462 xtiPeer.tooltip.exit();
463 handleEvent(e);
464 }
465 public void mousePressed(MouseEvent e) {
466 handleEvent(e);
467 }
468 public void mouseReleased(MouseEvent e) {
469 handleEvent(e);
470 }
471 public void mouseDragged(MouseEvent e) {
472 handleEvent(e);
473 }
474 public void mouseMoved(MouseEvent e) {
475 handleEvent(e);
476 }
477 }
478
479 static boolean isTrayIconStuffWindow(Window w) {
480 return (w instanceof Tooltip) ||
481 (w instanceof Balloon) ||
482 (w instanceof XTrayIconEmbeddedFrame);
483 }
484
485 // ***************************************
486 // Special embedded frame for tray icon
487 // ***************************************
488
489 private static class XTrayIconEmbeddedFrame extends XEmbeddedFrame {
490 public XTrayIconEmbeddedFrame(){
491 super(XToolkit.getDefaultRootWindow(), true, true);
492 }
493
494 public boolean isUndecorated() {
495 return true;
496 }
497
498 public boolean isResizable() {
499 return false;
500 }
501
502 // embedded frame for tray icon shouldn't be disposed by anyone except tray icon
503 public void dispose(){
504 }
505
506 public void realDispose(){
507 super.dispose();
508 }
509 };
510
511 // ***************************************
512 // Classes for painting an image on canvas
513 // ***************************************
514
515 static class TrayIconCanvas extends IconCanvas {
516 TrayIcon target;
517 boolean autosize;
518
519 TrayIconCanvas(TrayIcon target, int width, int height) {
520 super(width, height);
521 this.target = target;
522 }
523
524 // Invoke on EDT.
525 protected void repaintImage(boolean doClear) {
526 boolean old_autosize = autosize;
527 autosize = target.isImageAutoSize();
528
529 curW = autosize ? width : image.getWidth(observer);
530 curH = autosize ? height : image.getHeight(observer);
531
532 super.repaintImage(doClear || (old_autosize != autosize));
533 }
534 }
535
536 static class IconCanvas extends Canvas {
537 volatile Image image;
538 IconObserver observer;
539 int width, height;
540 int curW, curH;
541
542 IconCanvas(int width, int height) {
543 this.width = curW = width;
544 this.height = curH = height;
545 }
546
547 // Invoke on EDT.
548 public void updateImage(Image image) {
549 this.image = image;
550 if (observer == null) {
551 observer = new IconObserver();
552 }
553 repaintImage(true);
554 }
555
556 // Invoke on EDT.
557 protected void repaintImage(boolean doClear) {
558 Graphics g = getGraphics();
559 if (g != null) {
560 try {
561 if (isVisible()) {
562 if (doClear) {
563 update(g);
564 } else {
565 paint(g);
566 }
567 }
568 } finally {
569 g.dispose();
570 }
571 }
572 }
573
574 // Invoke on EDT.
575 public void paint(Graphics g) {
576 if (g != null && curW > 0 && curH > 0) {
577 BufferedImage bufImage = new BufferedImage(curW, curH, BufferedImage.TYPE_INT_ARGB);
578 Graphics2D gr = bufImage.createGraphics();
579 if (gr != null) {
580 try {
581 gr.setColor(getBackground());
582 gr.fillRect(0, 0, curW, curH);
583 gr.drawImage(image, 0, 0, curW, curH, observer);
584 gr.dispose();
585
586 g.drawImage(bufImage, 0, 0, curW, curH, null);
587 } finally {
588 gr.dispose();
589 }
590 }
591 }
592 }
593
594 class IconObserver implements ImageObserver {
595 public boolean imageUpdate(final Image image, final int flags, int x, int y, int width, int height) {
596 if (image != IconCanvas.this.image || // if the image has been changed
597 !IconCanvas.this.isVisible())
598 {
599 return false;
600 }
601 if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS |
602 ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0)
603 {
604 SunToolkit.executeOnEventHandlerThread(IconCanvas.this, new Runnable() {
605 public void run() {
606 repaintImage(false);
607 }
608 });
609 }
610 return (flags & ImageObserver.ALLBITS) == 0;
611 }
612 }
613 }
614
615 // ***************************************
616 // Classes for toolitp and balloon windows
617 // ***************************************
618
619 static class Tooltip extends InfoWindow {
620 XTrayIconPeer xtiPeer;
621 Label textLabel = new Label("");
622 Runnable starter = new Runnable() {
623 public void run() {
624 display();
625 }};
626
627 final static int TOOLTIP_SHOW_TIME = 10000;
628 final static int TOOLTIP_START_DELAY_TIME = 1000;
629 final static int TOOLTIP_MAX_LENGTH = 64;
630 final static int TOOLTIP_MOUSE_CURSOR_INDENT = 5;
631 final static Color TOOLTIP_BACKGROUND_COLOR = new Color(255, 255, 220);
632 final static Font TOOLTIP_TEXT_FONT = XWindow.defaultFont;
633
634 Tooltip(XTrayIconPeer xtiPeer, Frame parent) {
635 super(parent, Color.black);
636 this.xtiPeer = xtiPeer;
637
638 suppressWarningString(this);
639
640 setCloser(null, TOOLTIP_SHOW_TIME);
641 textLabel.setBackground(TOOLTIP_BACKGROUND_COLOR);
642 textLabel.setFont(TOOLTIP_TEXT_FONT);
643 add(textLabel);
644 }
645
646 /*
647 * WARNING: this method is executed on Toolkit thread!
648 */
649 void display() {
650 String tip = xtiPeer.tooltipString;
651 if (tip == null) {
652 return;
653 } else if (tip.length() > TOOLTIP_MAX_LENGTH) {
654 textLabel.setText(tip.substring(0, TOOLTIP_MAX_LENGTH));
655 } else {
656 textLabel.setText(tip);
657 }
658
659 // Execute on EDT to avoid deadlock (see 6280857).
660 SunToolkit.executeOnEventHandlerThread(xtiPeer.target, new Runnable() {
661 public void run() {
662 if (xtiPeer.isDisposed()) {
663 return;
664 }
665 Point pointer = (Point)AccessController.doPrivileged(new PrivilegedAction() {
666 public Object run() {
667 if (!isPointerOverTrayIcon(xtiPeer.getBounds())) {
668 return null;
669 }
670 return MouseInfo.getPointerInfo().getLocation();
671 }
672 });
673 if (pointer == null) {
674 return;
675 }
676 show(new Point(pointer.x, pointer.y), TOOLTIP_MOUSE_CURSOR_INDENT);
677 }
678 });
679 }
680
681 void enter() {
682 XToolkit.schedule(starter, TOOLTIP_START_DELAY_TIME);
683 }
684
685 void exit() {
686 XToolkit.remove(starter);
687 if (isVisible()) {
688 hide();
689 }
690 }
691
692 boolean isPointerOverTrayIcon(Rectangle trayRect) {
693 Point p = MouseInfo.getPointerInfo().getLocation();
694 return !(p.x < trayRect.x || p.x > (trayRect.x + trayRect.width) ||
695 p.y < trayRect.y || p.y > (trayRect.y + trayRect.height));
696 }
697 }
698
699 static class Balloon extends InfoWindow {
700 final static int BALLOON_SHOW_TIME = 10000;
701 final static int BALLOON_TEXT_MAX_LENGTH = 256;
702 final static int BALLOON_WORD_LINE_MAX_LENGTH = 16;
703 final static int BALLOON_WORD_LINE_MAX_COUNT = 4;
704 final static int BALLOON_ICON_WIDTH = 32;
705 final static int BALLOON_ICON_HEIGHT = 32;
706 final static int BALLOON_TRAY_ICON_INDENT = 0;
707 final static Color BALLOON_CAPTION_BACKGROUND_COLOR = new Color(200, 200 ,255);
708 final static Font BALLOON_CAPTION_FONT = new Font(Font.DIALOG, Font.BOLD, 12);
709
710 XTrayIconPeer xtiPeer;
711 Panel mainPanel = new Panel();
712 Panel captionPanel = new Panel();
713 Label captionLabel = new Label("");
714 Button closeButton = new Button("X");
715 Panel textPanel = new Panel();
716 IconCanvas iconCanvas = new IconCanvas(BALLOON_ICON_WIDTH, BALLOON_ICON_HEIGHT);
717 Label[] lineLabels = new Label[BALLOON_WORD_LINE_MAX_COUNT];
718 ActionPerformer ap = new ActionPerformer();
719
720 Image iconImage;
721 Image errorImage;
722 Image warnImage;
723 Image infoImage;
724 boolean gtkImagesLoaded;
725
726 Displayer displayer = new Displayer();
727
728 Balloon(final XTrayIconPeer xtiPeer, Frame parent) {
729 super(parent, new Color(90, 80 ,190));
730 this.xtiPeer = xtiPeer;
731
732 suppressWarningString(this);
733
734 setCloser(new Runnable() {
735 public void run() {
736 if (textPanel != null) {
737 textPanel.removeAll();
738 textPanel.setSize(0, 0);
739 iconCanvas.setSize(0, 0);
740 XToolkit.awtLock();
741 try {
742 displayer.isDisplayed = false;
743 XToolkit.awtLockNotifyAll();
744 } finally {
745 XToolkit.awtUnlock();
746 }
747 }
748 }
749 }, BALLOON_SHOW_TIME);
750
751 add(mainPanel);
752
753 captionLabel.setFont(BALLOON_CAPTION_FONT);
754 captionLabel.addMouseListener(ap);
755
756 captionPanel.setLayout(new BorderLayout());
757 captionPanel.add(captionLabel, BorderLayout.WEST);
758 captionPanel.add(closeButton, BorderLayout.EAST);
759 captionPanel.setBackground(BALLOON_CAPTION_BACKGROUND_COLOR);
760 captionPanel.addMouseListener(ap);
761
762 closeButton.addActionListener(new ActionListener() {
763 public void actionPerformed(ActionEvent e) {
764 hide();
765 }
766 });
767
768 mainPanel.setLayout(new BorderLayout());
769 mainPanel.setBackground(Color.white);
770 mainPanel.add(captionPanel, BorderLayout.NORTH);
771 mainPanel.add(iconCanvas, BorderLayout.WEST);
772 mainPanel.add(textPanel, BorderLayout.CENTER);
773
774 iconCanvas.addMouseListener(ap);
775
776 for (int i = 0; i < BALLOON_WORD_LINE_MAX_COUNT; i++) {
777 lineLabels[i] = new Label();
778 lineLabels[i].addMouseListener(ap);
779 lineLabels[i].setBackground(Color.white);
780 }
781
782 displayer.start();
783 }
784
785 void display(String caption, String text, String messageType) {
786 if (!gtkImagesLoaded) {
787 loadGtkImages();
788 }
789 displayer.display(caption, text, messageType);
790 }
791
792 private void _display(String caption, String text, String messageType) {
793 captionLabel.setText(caption);
794
795 BreakIterator iter = BreakIterator.getWordInstance();
796 if (text != null) {
797 iter.setText(text);
798 int start = iter.first(), end;
799 int nLines = 0;
800
801 do {
802 end = iter.next();
803
804 if (end == BreakIterator.DONE ||
805 text.substring(start, end).length() >= 50)
806 {
807 lineLabels[nLines].setText(text.substring(start, end == BreakIterator.DONE ?
808 iter.last() : end));
809 textPanel.add(lineLabels[nLines++]);
810 start = end;
811 }
812 if (nLines == BALLOON_WORD_LINE_MAX_COUNT) {
813 if (end != BreakIterator.DONE) {
814 lineLabels[nLines - 1].setText(
815 new String(lineLabels[nLines - 1].getText() + " ..."));
816 }
817 break;
818 }
819 } while (end != BreakIterator.DONE);
820
821
822 textPanel.setLayout(new GridLayout(nLines, 1));
823 }
824
825 if ("ERROR".equals(messageType)) {
826 iconImage = errorImage;
827 } else if ("WARNING".equals(messageType)) {
828 iconImage = warnImage;
829 } else if ("INFO".equals(messageType)) {
830 iconImage = infoImage;
831 } else {
832 iconImage = null;
833 }
834
835 if (iconImage != null) {
836 Dimension tpSize = textPanel.getSize();
837 iconCanvas.setSize(BALLOON_ICON_WIDTH, (BALLOON_ICON_HEIGHT > tpSize.height ?
838 BALLOON_ICON_HEIGHT : tpSize.height));
839 }
840
841 SunToolkit.executeOnEventHandlerThread(xtiPeer.target, new Runnable() {
842 public void run() {
843 if (xtiPeer.isDisposed()) {
844 return;
845 }
846 Point parLoc = getParent().getLocationOnScreen();
847 Dimension parSize = getParent().getSize();
848 show(new Point(parLoc.x + parSize.width/2, parLoc.y + parSize.height/2),
849 BALLOON_TRAY_ICON_INDENT);
850 if (iconImage != null) {
851 iconCanvas.updateImage(iconImage); // call it after the show(..) above
852 }
853 }
854 });
855 }
856
857 public void dispose() {
858 displayer.interrupt();
859 super.dispose();
860 }
861
862 void loadGtkImages() {
863 if (!gtkImagesLoaded) {
864 errorImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty(
865 "gtk.icon.gtk-dialog-error.6.rtl");
866 warnImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty(
867 "gtk.icon.gtk-dialog-warning.6.rtl");
868 infoImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty(
869 "gtk.icon.gtk-dialog-info.6.rtl");
870 gtkImagesLoaded = true;
871 }
872 }
873
874 class ActionPerformer extends MouseAdapter {
875 public void mouseClicked(MouseEvent e) {
876 // hide the balloon by any click
877 hide();
878 if (e.getButton() == MouseEvent.BUTTON1) {
879 ActionEvent aev = new ActionEvent(xtiPeer.target, ActionEvent.ACTION_PERFORMED,
880 xtiPeer.target.getActionCommand(),
881 e.getWhen(), e.getModifiers());
882 Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(aev);
883 }
884 }
885 }
886
887 class Displayer extends Thread {
888 final int MAX_CONCURRENT_MSGS = 10;
889
890 ArrayBlockingQueue<Message> messageQueue = new ArrayBlockingQueue<Message>(MAX_CONCURRENT_MSGS);
891 boolean isDisplayed;
892
893 Displayer() {
894 setDaemon(true);
895 }
896
897 public void run() {
898 while (true) {
899 Message msg = null;
900 try {
901 msg = (Message)messageQueue.take();
902 } catch (InterruptedException e) {
903 return;
904 }
905
906 /*
907 * Wait till the previous message is displayed if any
908 */
909 XToolkit.awtLock();
910 try {
911 while (isDisplayed) {
912 try {
913 XToolkit.awtLockWait();
914 } catch (InterruptedException e) {
915 return;
916 }
917 }
918 isDisplayed = true;
919 } finally {
920 XToolkit.awtUnlock();
921 }
922 _display(msg.caption, msg.text, msg.messageType);
923 }
924 }
925
926 void display(String caption, String text, String messageType) {
927 messageQueue.offer(new Message(caption, text, messageType));
928 }
929 }
930
931 class Message {
932 String caption, text, messageType;
933
934 Message(String caption, String text, String messageType) {
935 this.caption = caption;
936 this.text = text;
937 this.messageType = messageType;
938 }
939 }
940 }
941
942 static class InfoWindow extends Window {
943 Container container;
944 Closer closer;
945
946 InfoWindow(Frame parent, Color borderColor) {
947 super(parent);
948 container = new Container() {
949 public Insets getInsets() {
950 return new Insets(1, 1, 1, 1);
951 }
952 };
953 setLayout(new BorderLayout());
954 setBackground(borderColor);
955 add(container, BorderLayout.CENTER);
956 container.setLayout(new BorderLayout());
957
958 closer = new Closer();
959 }
960
961 public Component add(Component c) {
962 container.add(c, BorderLayout.CENTER);
963 return c;
964 }
965
966 void setCloser(Runnable action, int time) {
967 closer.set(action, time);
968 }
969
970 // Must be executed on EDT.
971 protected void show(Point corner, int indent) {
972 assert SunToolkit.isDispatchThreadForAppContext(InfoWindow.this);
973
974 pack();
975
976 Dimension size = getSize();
977 // TODO: When 6356322 is fixed we should get screen bounds in
978 // this way: eframe.getGraphicsConfiguration().getBounds().
979 Dimension scrSize = Toolkit.getDefaultToolkit().getScreenSize();
980
981 if (corner.x < scrSize.width/2 && corner.y < scrSize.height/2) { // 1st square
982 setLocation(corner.x + indent, corner.y + indent);
983
984 } else if (corner.x >= scrSize.width/2 && corner.y < scrSize.height/2) { // 2nd square
985 setLocation(corner.x - indent - size.width, corner.y + indent);
986
987 } else if (corner.x < scrSize.width/2 && corner.y >= scrSize.height/2) { // 3rd square
988 setLocation(corner.x + indent, corner.y - indent - size.height);
989
990 } else if (corner.x >= scrSize.width/2 && corner.y >= scrSize.height/2) { // 4th square
991 setLocation(corner.x - indent - size.width, corner.y - indent - size.height);
992 }
993
994 InfoWindow.super.show();
995 InfoWindow.this.closer.schedule();
996 }
997
998 public void hide() {
999 closer.close();
1000 }
1001
1002 class Closer implements Runnable {
1003 Runnable action;
1004 int time;
1005
1006 public void run() {
1007 doClose();
1008 }
1009
1010 void set(Runnable action, int time) {
1011 this.action = action;
1012 this.time = time;
1013 }
1014
1015 void schedule() {
1016 XToolkit.schedule(this, time);
1017 }
1018
1019 void close() {
1020 XToolkit.remove(this);
1021 doClose();
1022 }
1023
1024 // WARNING: this method may be executed on Toolkit thread.
1025 private void doClose() {
1026 SunToolkit.executeOnEventHandlerThread(InfoWindow.this, new Runnable() {
1027 public void run() {
1028 InfoWindow.super.hide();
1029 invalidate();
1030 if (action != null) {
1031 action.run();
1032 }
1033 }
1034 });
1035 }
1036 }
1037 }
1038}