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