1 /*
2  * Copyright (c) 2005, 2013, Oracle and/or its affiliates. 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.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package sun.awt.X11;
27 
28 import java.awt.*;
29 import java.awt.event.*;
30 import java.awt.peer.TrayIconPeer;
31 import sun.awt.*;
32 import java.awt.image.*;
33 import java.text.BreakIterator;
34 import java.util.concurrent.ArrayBlockingQueue;
35 import java.security.AccessController;
36 import java.security.PrivilegedAction;
37 import java.lang.reflect.InvocationTargetException;
38 import sun.util.logging.PlatformLogger;
39 
40 public class XTrayIconPeer implements TrayIconPeer,
41        InfoWindow.Balloon.LiveArguments,
42        InfoWindow.Tooltip.LiveArguments
43 {
44     private static final PlatformLogger ctrLog = PlatformLogger.getLogger("sun.awt.X11.XTrayIconPeer.centering");
45 
46     TrayIcon target;
47     TrayIconEventProxy eventProxy;
48     XTrayIconEmbeddedFrame eframe;
49     TrayIconCanvas canvas;
50     InfoWindow.Balloon balloon;
51     InfoWindow.Tooltip tooltip;
52     PopupMenu popup;
53     String tooltipString;
54     boolean isTrayIconDisplayed;
55     long eframeParentID;
56     final XEventDispatcher parentXED, eframeXED;
57 
58     static final XEventDispatcher dummyXED = new XEventDispatcher() {
59             public void dispatchEvent(XEvent ev) {}
60         };
61 
62     volatile boolean isDisposed;
63 
64     boolean isParentWindowLocated;
65     int old_x, old_y;
66     int ex_width, ex_height;
67 
68     final static int TRAY_ICON_WIDTH = 24;
69     final static int TRAY_ICON_HEIGHT = 24;
70 
XTrayIconPeer(TrayIcon target)71     XTrayIconPeer(TrayIcon target)
72       throws AWTException
73     {
74         this.target = target;
75 
76         eventProxy = new TrayIconEventProxy(this);
77 
78         canvas = new TrayIconCanvas(target, TRAY_ICON_WIDTH, TRAY_ICON_HEIGHT);
79 
80         eframe = new XTrayIconEmbeddedFrame();
81 
82         eframe.setSize(TRAY_ICON_WIDTH, TRAY_ICON_HEIGHT);
83         eframe.add(canvas);
84 
85         // Fix for 6317038: as EmbeddedFrame is instance of Frame, it is blocked
86         // by modal dialogs, but in the case of TrayIcon it shouldn't. So we
87         // set ModalExclusion property on it.
88         AccessController.doPrivileged(new PrivilegedAction() {
89             public Object run() {
90                 eframe.setModalExclusionType(Dialog.ModalExclusionType.TOOLKIT_EXCLUDE);
91                 return null;
92             }
93         });
94 
95 
96         if (XWM.getWMID() != XWM.METACITY_WM) {
97             parentXED = dummyXED; // We don't like to leave it 'null'.
98 
99         } else {
100             parentXED = new XEventDispatcher() {
101                 // It's executed under AWTLock.
102                 public void dispatchEvent(XEvent ev) {
103                     if (isDisposed() || ev.get_type() != XConstants.ConfigureNotify) {
104                         return;
105                     }
106 
107                     XConfigureEvent ce = ev.get_xconfigure();
108 
109                     if (ctrLog.isLoggable(PlatformLogger.Level.FINE)) {
110                         ctrLog.fine("ConfigureNotify on parent of {0}: {1}x{2}+{3}+{4} (old: {5}+{6})",
111                                 XTrayIconPeer.this, ce.get_width(), ce.get_height(),
112                                 ce.get_x(), ce.get_y(), old_x, old_y);
113                     }
114 
115                     // A workaround for Gnome/Metacity (it doesn't affect the behaviour on KDE).
116                     // On Metacity the EmbeddedFrame's parent window bounds are larger
117                     // than TrayIcon size required (that is we need a square but a rectangle
118                     // is provided by the Panel Notification Area). The parent's background color
119                     // differs from the Panel's one. To hide the background we resize parent
120                     // window so that it fits the EmbeddedFrame.
121                     // However due to resizing the parent window it loses centering in the Panel.
122                     // We center it when discovering that some of its side is of size greater
123                     // than the fixed value. Centering is being done by "X" (when the parent's width
124                     // is greater) and by "Y" (when the parent's height is greater).
125 
126                     // Actually we need this workaround until we could detect taskbar color.
127 
128                     if (ce.get_height() != TRAY_ICON_HEIGHT && ce.get_width() != TRAY_ICON_WIDTH) {
129 
130                         // If both the height and the width differ from the fixed size then WM
131                         // must level at least one side to the fixed size. For some reason it may take
132                         // a few hops (even after reparenting) and we have to skip the intermediate ones.
133                         if (ctrLog.isLoggable(PlatformLogger.Level.FINE)) {
134                             ctrLog.fine("ConfigureNotify on parent of {0}. Skipping as intermediate resizing.",
135                                     XTrayIconPeer.this);
136                         }
137                         return;
138 
139                     } else if (ce.get_height() > TRAY_ICON_HEIGHT) {
140 
141                         if (ctrLog.isLoggable(PlatformLogger.Level.FINE)) {
142                             ctrLog.fine("ConfigureNotify on parent of {0}. Centering by \"Y\".",
143                                     XTrayIconPeer.this);
144                         }
145 
146                         XlibWrapper.XMoveResizeWindow(XToolkit.getDisplay(), eframeParentID,
147                                                       ce.get_x(),
148                                                       ce.get_y()+ce.get_height()/2-TRAY_ICON_HEIGHT/2,
149                                                       TRAY_ICON_WIDTH,
150                                                       TRAY_ICON_HEIGHT);
151                         ex_height = ce.get_height();
152                         ex_width = 0;
153 
154                     } else if (ce.get_width() > TRAY_ICON_WIDTH) {
155 
156                         if (ctrLog.isLoggable(PlatformLogger.Level.FINE)) {
157                             ctrLog.fine("ConfigureNotify on parent of {0}. Centering by \"X\".",
158                                     XTrayIconPeer.this);
159                         }
160 
161                         XlibWrapper.XMoveResizeWindow(XToolkit.getDisplay(), eframeParentID,
162                                                       ce.get_x()+ce.get_width()/2 - TRAY_ICON_WIDTH/2,
163                                                       ce.get_y(),
164                                                       TRAY_ICON_WIDTH,
165                                                       TRAY_ICON_HEIGHT);
166                         ex_width = ce.get_width();
167                         ex_height = 0;
168 
169                     } else if (isParentWindowLocated && ce.get_x() != old_x && ce.get_y() != old_y) {
170                         // If moving by both "X" and "Y".
171                         // When some tray icon gets removed from the tray, a Java icon may be repositioned.
172                         // In this case the parent window also lose centering. We have to restore it.
173 
174                         if (ex_height != 0) {
175 
176                             if (ctrLog.isLoggable(PlatformLogger.Level.FINE)) {
177                                 ctrLog.fine("ConfigureNotify on parent of {0}. Move detected. Centering by \"Y\".",
178                                         XTrayIconPeer.this);
179                             }
180 
181                             XlibWrapper.XMoveWindow(XToolkit.getDisplay(), eframeParentID,
182                                                     ce.get_x(),
183                                                     ce.get_y() + ex_height/2 - TRAY_ICON_HEIGHT/2);
184 
185                         } else if (ex_width != 0) {
186 
187                             if (ctrLog.isLoggable(PlatformLogger.Level.FINE)) {
188                                 ctrLog.fine("ConfigureNotify on parent of {0}. Move detected. Centering by \"X\".",
189                                         XTrayIconPeer.this);
190                             }
191 
192                             XlibWrapper.XMoveWindow(XToolkit.getDisplay(), eframeParentID,
193                                                     ce.get_x() + ex_width/2 - TRAY_ICON_WIDTH/2,
194                                                     ce.get_y());
195                         } else {
196                             if (ctrLog.isLoggable(PlatformLogger.Level.FINE)) {
197                                 ctrLog.fine("ConfigureNotify on parent of {0}. Move detected. Skipping.",
198                                         XTrayIconPeer.this);
199                             }
200                         }
201                     }
202                     old_x = ce.get_x();
203                     old_y = ce.get_y();
204                     isParentWindowLocated = true;
205                 }
206             };
207         }
208         eframeXED = new XEventDispatcher() {
209                 // It's executed under AWTLock.
210                 XTrayIconPeer xtiPeer = XTrayIconPeer.this;
211 
212                 public void dispatchEvent(XEvent ev) {
213                     if (isDisposed() || ev.get_type() != XConstants.ReparentNotify) {
214                         return;
215                     }
216 
217                     XReparentEvent re = ev.get_xreparent();
218                     eframeParentID = re.get_parent();
219 
220                     if (eframeParentID == XToolkit.getDefaultRootWindow()) {
221 
222                         if (isTrayIconDisplayed) { // most likely Notification Area was removed
223                             SunToolkit.executeOnEventHandlerThread(xtiPeer.target, new Runnable() {
224                                     public void run() {
225                                         SystemTray.getSystemTray().remove(xtiPeer.target);
226                                     }
227                                 });
228                         }
229                         return;
230                     }
231 
232                     if (!isTrayIconDisplayed) {
233                         addXED(eframeParentID, parentXED, XConstants.StructureNotifyMask);
234 
235                         isTrayIconDisplayed = true;
236                         XToolkit.awtLockNotifyAll();
237                     }
238                 }
239             };
240 
241         addXED(getWindow(), eframeXED, XConstants.StructureNotifyMask);
242 
243         XSystemTrayPeer.getPeerInstance().addTrayIcon(this); // throws AWTException
244 
245         // Wait till the EmbeddedFrame is reparented
246         long start = System.currentTimeMillis();
247         final long PERIOD = XToolkit.getTrayIconDisplayTimeout();
248         XToolkit.awtLock();
249         try {
250             while (!isTrayIconDisplayed) {
251                 try {
252                     XToolkit.awtLockWait(PERIOD);
253                 } catch (InterruptedException e) {
254                     break;
255                 }
256                 if (System.currentTimeMillis() - start > PERIOD) {
257                     break;
258                 }
259             }
260         } finally {
261             XToolkit.awtUnlock();
262         }
263 
264         // This is unlikely to happen.
265         if (!isTrayIconDisplayed || eframeParentID == 0 ||
266             eframeParentID == XToolkit.getDefaultRootWindow())
267         {
268             throw new AWTException("TrayIcon couldn't be displayed.");
269         }
270 
271         eframe.setVisible(true);
272         updateImage();
273 
274         balloon = new InfoWindow.Balloon(eframe, target, this);
275         tooltip = new InfoWindow.Tooltip(eframe, target, this);
276 
277         addListeners();
278     }
279 
dispose()280     public void dispose() {
281         if (SunToolkit.isDispatchThreadForAppContext(target)) {
282             disposeOnEDT();
283         } else {
284             try {
285                 SunToolkit.executeOnEDTAndWait(target, new Runnable() {
286                         public void run() {
287                             disposeOnEDT();
288                         }
289                     });
290             } catch (InterruptedException ie) {
291             } catch (InvocationTargetException ite) {}
292         }
293     }
294 
disposeOnEDT()295     private void disposeOnEDT() {
296         // All actions that is to be synchronized with disposal
297         // should be executed either under AWTLock, or on EDT.
298         // isDisposed value must be checked.
299         XToolkit.awtLock();
300         isDisposed = true;
301         XToolkit.awtUnlock();
302 
303         removeXED(getWindow(), eframeXED);
304         removeXED(eframeParentID, parentXED);
305         eframe.realDispose();
306         balloon.dispose();
307         isTrayIconDisplayed = false;
308         XToolkit.targetDisposedPeer(target, this);
309     }
310 
suppressWarningString(Window w)311     public static void suppressWarningString(Window w) {
312         AWTAccessor.getWindowAccessor().setTrayIconWindow(w, true);
313     }
314 
setToolTip(String tooltip)315     public void setToolTip(String tooltip) {
316         tooltipString = tooltip;
317     }
318 
getTooltipString()319     public String getTooltipString() {
320         return tooltipString;
321     }
322 
updateImage()323     public void updateImage() {
324         Runnable r = new Runnable() {
325                 public void run() {
326                     canvas.updateImage(target.getImage());
327                 }
328             };
329 
330         if (!SunToolkit.isDispatchThreadForAppContext(target)) {
331             SunToolkit.executeOnEventHandlerThread(target, r);
332         } else {
333             r.run();
334         }
335     }
336 
displayMessage(String caption, String text, String messageType)337     public void displayMessage(String caption, String text, String messageType) {
338         Point loc = getLocationOnScreen();
339         Rectangle screen = eframe.getGraphicsConfiguration().getBounds();
340 
341         // Check if the tray icon is in the bounds of a screen.
342         if (!(loc.x < screen.x || loc.x >= screen.x + screen.width ||
343               loc.y < screen.y || loc.y >= screen.y + screen.height))
344         {
345             balloon.display(caption, text, messageType);
346         }
347     }
348 
349     // It's synchronized with disposal by EDT.
showPopupMenu(int x, int y)350     public void showPopupMenu(int x, int y) {
351         if (isDisposed())
352             return;
353 
354         assert SunToolkit.isDispatchThreadForAppContext(target);
355 
356         PopupMenu newPopup = target.getPopupMenu();
357         if (popup != newPopup) {
358             if (popup != null) {
359                 eframe.remove(popup);
360             }
361             if (newPopup != null) {
362                 eframe.add(newPopup);
363             }
364             popup = newPopup;
365         }
366 
367         if (popup != null) {
368             Point loc = ((XBaseWindow)eframe.getPeer()).toLocal(new Point(x, y));
369             popup.show(eframe, loc.x, loc.y);
370         }
371     }
372 
373 
374     // ******************************************************************
375     // ******************************************************************
376 
377 
addXED(long window, XEventDispatcher xed, long mask)378     private void addXED(long window, XEventDispatcher xed, long mask) {
379         if (window == 0) {
380             return;
381         }
382         XToolkit.awtLock();
383         try {
384             XlibWrapper.XSelectInput(XToolkit.getDisplay(), window, mask);
385         } finally {
386             XToolkit.awtUnlock();
387         }
388         XToolkit.addEventDispatcher(window, xed);
389     }
390 
removeXED(long window, XEventDispatcher xed)391     private void removeXED(long window, XEventDispatcher xed) {
392         if (window == 0) {
393             return;
394         }
395         XToolkit.awtLock();
396         try {
397             XToolkit.removeEventDispatcher(window, xed);
398         } finally {
399             XToolkit.awtUnlock();
400         }
401     }
402 
403     // Private method for testing purposes.
getLocationOnScreen()404     private Point getLocationOnScreen() {
405         return eframe.getLocationOnScreen();
406     }
407 
getBounds()408     public Rectangle getBounds() {
409         Point loc = getLocationOnScreen();
410         return new Rectangle(loc.x, loc.y, loc.x + TRAY_ICON_WIDTH, loc.y + TRAY_ICON_HEIGHT);
411     }
412 
addListeners()413     void addListeners() {
414         canvas.addMouseListener(eventProxy);
415         canvas.addMouseMotionListener(eventProxy);
416     }
417 
getWindow()418     long getWindow() {
419         return ((XEmbeddedFramePeer)eframe.getPeer()).getWindow();
420     }
421 
isDisposed()422     public boolean isDisposed() {
423         return isDisposed;
424     }
425 
getActionCommand()426     public String getActionCommand() {
427         return target.getActionCommand();
428     }
429 
430     static class TrayIconEventProxy implements MouseListener, MouseMotionListener {
431         XTrayIconPeer xtiPeer;
432 
TrayIconEventProxy(XTrayIconPeer xtiPeer)433         TrayIconEventProxy(XTrayIconPeer xtiPeer) {
434             this.xtiPeer = xtiPeer;
435         }
436 
handleEvent(MouseEvent e)437         public void handleEvent(MouseEvent e) {
438             //prevent DRAG events from being posted with TrayIcon source(CR 6565779)
439             if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
440                 return;
441             }
442 
443             // Event handling is synchronized with disposal by EDT.
444             if (xtiPeer.isDisposed()) {
445                 return;
446             }
447             Point coord = XBaseWindow.toOtherWindow(xtiPeer.getWindow(),
448                                                     XToolkit.getDefaultRootWindow(),
449                                                     e.getX(), e.getY());
450 
451             if (e.isPopupTrigger()) {
452                 xtiPeer.showPopupMenu(coord.x, coord.y);
453             }
454 
455             e.translatePoint(coord.x - e.getX(), coord.y - e.getY());
456             // This is a hack in order to set non-Component source to MouseEvent
457             // instance.
458             // In some cases this could lead to unpredictable result (e.g. when
459             // other class tries to cast source field to Component).
460             // We already filter DRAG events out (CR 6565779).
461             e.setSource(xtiPeer.target);
462             XToolkit.postEvent(XToolkit.targetToAppContext(e.getSource()), e);
463         }
mouseClicked(MouseEvent e)464         public void mouseClicked(MouseEvent e) {
465             if ((e.getClickCount() > 1 || xtiPeer.balloon.isVisible()) &&
466                 e.getButton() == MouseEvent.BUTTON1)
467             {
468                 ActionEvent aev = new ActionEvent(xtiPeer.target, ActionEvent.ACTION_PERFORMED,
469                                                   xtiPeer.target.getActionCommand(), e.getWhen(),
470                                                   e.getModifiers());
471                 XToolkit.postEvent(XToolkit.targetToAppContext(aev.getSource()), aev);
472             }
473             if (xtiPeer.balloon.isVisible()) {
474                 xtiPeer.balloon.hide();
475             }
476             handleEvent(e);
477         }
mouseEntered(MouseEvent e)478         public void mouseEntered(MouseEvent e) {
479             xtiPeer.tooltip.enter();
480             handleEvent(e);
481         }
mouseExited(MouseEvent e)482         public void mouseExited(MouseEvent e) {
483             xtiPeer.tooltip.exit();
484             handleEvent(e);
485         }
mousePressed(MouseEvent e)486         public void mousePressed(MouseEvent e) {
487             handleEvent(e);
488         }
mouseReleased(MouseEvent e)489         public void mouseReleased(MouseEvent e) {
490             handleEvent(e);
491         }
mouseDragged(MouseEvent e)492         public void mouseDragged(MouseEvent e) {
493             handleEvent(e);
494         }
mouseMoved(MouseEvent e)495         public void mouseMoved(MouseEvent e) {
496             handleEvent(e);
497         }
498     }
499 
500     // ***************************************
501     // Special embedded frame for tray icon
502     // ***************************************
503 
504     private static class XTrayIconEmbeddedFrame extends XEmbeddedFrame {
XTrayIconEmbeddedFrame()505         public XTrayIconEmbeddedFrame(){
506             super(XToolkit.getDefaultRootWindow(), true, true);
507         }
508 
isUndecorated()509         public boolean isUndecorated() {
510             return true;
511         }
512 
isResizable()513         public boolean isResizable() {
514             return false;
515         }
516 
517         // embedded frame for tray icon shouldn't be disposed by anyone except tray icon
dispose()518         public void dispose(){
519         }
520 
realDispose()521         public void realDispose(){
522             super.dispose();
523         }
524     };
525 
526     // ***************************************
527     // Classes for painting an image on canvas
528     // ***************************************
529 
530     static class TrayIconCanvas extends IconCanvas {
531         TrayIcon target;
532         boolean autosize;
533 
TrayIconCanvas(TrayIcon target, int width, int height)534         TrayIconCanvas(TrayIcon target, int width, int height) {
535             super(width, height);
536             this.target = target;
537         }
538 
539         // Invoke on EDT.
repaintImage(boolean doClear)540         protected void repaintImage(boolean doClear) {
541             boolean old_autosize = autosize;
542             autosize = target.isImageAutoSize();
543 
544             curW = autosize ? width : image.getWidth(observer);
545             curH = autosize ? height : image.getHeight(observer);
546 
547             super.repaintImage(doClear || (old_autosize != autosize));
548         }
549     }
550 
551     public static class IconCanvas extends Canvas {
552         volatile Image image;
553         IconObserver observer;
554         int width, height;
555         int curW, curH;
556 
IconCanvas(int width, int height)557         IconCanvas(int width, int height) {
558             this.width = curW = width;
559             this.height = curH = height;
560         }
561 
562         // Invoke on EDT.
updateImage(Image image)563         public void updateImage(Image image) {
564             this.image = image;
565             if (observer == null) {
566                 observer = new IconObserver();
567             }
568             repaintImage(true);
569         }
570 
571         // Invoke on EDT.
repaintImage(boolean doClear)572         protected void repaintImage(boolean doClear) {
573             Graphics g = getGraphics();
574             if (g != null) {
575                 try {
576                     if (isVisible()) {
577                         if (doClear) {
578                             update(g);
579                         } else {
580                             paint(g);
581                         }
582                     }
583                 } finally {
584                     g.dispose();
585                 }
586             }
587         }
588 
589         // Invoke on EDT.
paint(Graphics g)590         public void paint(Graphics g) {
591             if (g != null && curW > 0 && curH > 0) {
592                 BufferedImage bufImage = new BufferedImage(curW, curH, BufferedImage.TYPE_INT_ARGB);
593                 Graphics2D gr = bufImage.createGraphics();
594                 if (gr != null) {
595                     try {
596                         gr.setColor(getBackground());
597                         gr.fillRect(0, 0, curW, curH);
598                         gr.drawImage(image, 0, 0, curW, curH, observer);
599                         gr.dispose();
600 
601                         g.drawImage(bufImage, 0, 0, curW, curH, null);
602                     } finally {
603                         gr.dispose();
604                     }
605                 }
606             }
607         }
608 
609         class IconObserver implements ImageObserver {
imageUpdate(final Image image, final int flags, int x, int y, int width, int height)610             public boolean imageUpdate(final Image image, final int flags, int x, int y, int width, int height) {
611                 if (image != IconCanvas.this.image || // if the image has been changed
612                     !IconCanvas.this.isVisible())
613                 {
614                     return false;
615                 }
616                 if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS |
617                               ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0)
618                 {
619                     SunToolkit.executeOnEventHandlerThread(IconCanvas.this, new Runnable() {
620                             public void run() {
621                                 repaintImage(false);
622                             }
623                         });
624                 }
625                 return (flags & ImageObserver.ALLBITS) == 0;
626             }
627         }
628     }
629 }
630