1 /*
   2  * Copyright (c) 2005, 2010, 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 
  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                     ctrLog.fine("ConfigureNotify on parent of {0}: {1}x{2}+{3}+{4} (old: {5}+{6})",
 110                                 XTrayIconPeer.this, ce.get_width(), ce.get_height(),
 111                                 ce.get_x(), ce.get_y(), old_x, old_y);
 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                         ctrLog.fine("ConfigureNotify on parent of {0}. Skipping as intermediate resizing.",
 132                                     XTrayIconPeer.this);
 133                         return;
 134 
 135                     } else if (ce.get_height() > TRAY_ICON_HEIGHT) {
 136 
 137                         ctrLog.fine("ConfigureNotify on parent of {0}. Centering by \"Y\".",
 138                                     XTrayIconPeer.this);
 139 
 140                         XlibWrapper.XMoveResizeWindow(XToolkit.getDisplay(), eframeParentID,
 141                                                       ce.get_x(),
 142                                                       ce.get_y()+ce.get_height()/2-TRAY_ICON_HEIGHT/2,
 143                                                       TRAY_ICON_WIDTH,
 144                                                       TRAY_ICON_HEIGHT);
 145                         ex_height = ce.get_height();
 146                         ex_width = 0;
 147 
 148                     } else if (ce.get_width() > TRAY_ICON_WIDTH) {
 149 
 150                         ctrLog.fine("ConfigureNotify on parent of {0}. Centering by \"X\".",
 151                                     XTrayIconPeer.this);
 152 
 153                         XlibWrapper.XMoveResizeWindow(XToolkit.getDisplay(), eframeParentID,
 154                                                       ce.get_x()+ce.get_width()/2 - TRAY_ICON_WIDTH/2,
 155                                                       ce.get_y(),
 156                                                       TRAY_ICON_WIDTH,
 157                                                       TRAY_ICON_HEIGHT);
 158                         ex_width = ce.get_width();
 159                         ex_height = 0;
 160 
 161                     } else if (isParentWindowLocated && ce.get_x() != old_x && ce.get_y() != old_y) {
 162                         // If moving by both "X" and "Y".
 163                         // When some tray icon gets removed from the tray, a Java icon may be repositioned.
 164                         // In this case the parent window also lose centering. We have to restore it.
 165 
 166                         if (ex_height != 0) {
 167 
 168                             ctrLog.fine("ConfigureNotify on parent of {0}. Move detected. Centering by \"Y\".",
 169                                         XTrayIconPeer.this);
 170 
 171                             XlibWrapper.XMoveWindow(XToolkit.getDisplay(), eframeParentID,
 172                                                     ce.get_x(),
 173                                                     ce.get_y() + ex_height/2 - TRAY_ICON_HEIGHT/2);
 174 
 175                         } else if (ex_width != 0) {
 176 
 177                             ctrLog.fine("ConfigureNotify on parent of {0}. Move detected. Centering by \"X\".",
 178                                         XTrayIconPeer.this);
 179 
 180                             XlibWrapper.XMoveWindow(XToolkit.getDisplay(), eframeParentID,
 181                                                     ce.get_x() + ex_width/2 - TRAY_ICON_WIDTH/2,
 182                                                     ce.get_y());
 183                         } else {
 184                             ctrLog.fine("ConfigureNotify on parent of {0}. Move detected. Skipping.",
 185                                         XTrayIconPeer.this);
 186                         }
 187                     }
 188                     old_x = ce.get_x();
 189                     old_y = ce.get_y();
 190                     isParentWindowLocated = true;
 191                 }
 192             };
 193         }
 194         eframeXED = new XEventDispatcher() {
 195                 // It's executed under AWTLock.
 196                 XTrayIconPeer xtiPeer = XTrayIconPeer.this;
 197 
 198                 public void dispatchEvent(XEvent ev) {
 199                     if (isDisposed() || ev.get_type() != XConstants.ReparentNotify) {
 200                         return;
 201                     }
 202 
 203                     XReparentEvent re = ev.get_xreparent();
 204                     eframeParentID = re.get_parent();
 205 
 206                     if (eframeParentID == XToolkit.getDefaultRootWindow()) {
 207 
 208                         if (isTrayIconDisplayed) { // most likely Notification Area was removed
 209                             SunToolkit.executeOnEventHandlerThread(xtiPeer.target, new Runnable() {
 210                                     public void run() {
 211                                         SystemTray.getSystemTray().remove(xtiPeer.target);
 212                                     }
 213                                 });
 214                         }
 215                         return;
 216                     }
 217 
 218                     if (!isTrayIconDisplayed) {
 219                         addXED(eframeParentID, parentXED, XConstants.StructureNotifyMask);
 220 
 221                         isTrayIconDisplayed = true;
 222                         XToolkit.awtLockNotifyAll();
 223                     }
 224                 }
 225             };
 226 
 227         addXED(getWindow(), eframeXED, XConstants.StructureNotifyMask);
 228 
 229         XSystemTrayPeer.getPeerInstance().addTrayIcon(this); // throws AWTException
 230 
 231         // Wait till the EmbeddedFrame is reparented
 232         long start = System.currentTimeMillis();
 233         final long PERIOD = 2000L;
 234         XToolkit.awtLock();
 235         try {
 236             while (!isTrayIconDisplayed) {
 237                 try {
 238                     XToolkit.awtLockWait(PERIOD);
 239                 } catch (InterruptedException e) {
 240                     break;
 241                 }
 242                 if (System.currentTimeMillis() - start > PERIOD) {
 243                     break;
 244                 }
 245             }
 246         } finally {
 247             XToolkit.awtUnlock();
 248         }
 249 
 250         // This is unlikely to happen.
 251         if (!isTrayIconDisplayed || eframeParentID == 0 ||
 252             eframeParentID == XToolkit.getDefaultRootWindow())
 253         {
 254             throw new AWTException("TrayIcon couldn't be displayed.");
 255         }
 256 
 257         eframe.setVisible(true);
 258         updateImage();
 259 
 260         balloon = new InfoWindow.Balloon(eframe, target, this);
 261         tooltip = new InfoWindow.Tooltip(eframe, target, this);
 262 
 263         addListeners();
 264     }
 265 
 266     public void dispose() {
 267         if (SunToolkit.isDispatchThreadForAppContext(target)) {
 268             disposeOnEDT();
 269         } else {
 270             try {
 271                 SunToolkit.executeOnEDTAndWait(target, new Runnable() {
 272                         public void run() {
 273                             disposeOnEDT();
 274                         }
 275                     });
 276             } catch (InterruptedException ie) {
 277             } catch (InvocationTargetException ite) {}
 278         }
 279     }
 280 
 281     private void disposeOnEDT() {
 282         // All actions that is to be synchronized with disposal
 283         // should be executed either under AWTLock, or on EDT.
 284         // isDisposed value must be checked.
 285         XToolkit.awtLock();
 286         isDisposed = true;
 287         XToolkit.awtUnlock();
 288 
 289         removeXED(getWindow(), eframeXED);
 290         removeXED(eframeParentID, parentXED);
 291         eframe.realDispose();
 292         balloon.dispose();
 293         isTrayIconDisplayed = false;
 294         XToolkit.targetDisposedPeer(target, this);
 295     }
 296 
 297     public static void suppressWarningString(Window w) {
 298         AWTAccessor.getWindowAccessor().setTrayIconWindow(w, true);
 299     }
 300 
 301     public void setToolTip(String tooltip) {
 302         tooltipString = tooltip;
 303     }
 304 
 305     public String getTooltipString() {
 306         return tooltipString;
 307     }
 308 
 309     public void updateImage() {
 310         Runnable r = new Runnable() {
 311                 public void run() {
 312                     canvas.updateImage(target.getImage());
 313                 }
 314             };
 315 
 316         if (!SunToolkit.isDispatchThreadForAppContext(target)) {
 317             SunToolkit.executeOnEventHandlerThread(target, r);
 318         } else {
 319             r.run();
 320         }
 321     }
 322 
 323     public void displayMessage(String caption, String text, String messageType) {
 324         Point loc = getLocationOnScreen();
 325         Rectangle screen = eframe.getGraphicsConfiguration().getBounds();
 326 
 327         // Check if the tray icon is in the bounds of a screen.
 328         if (!(loc.x < screen.x || loc.x >= screen.x + screen.width ||
 329               loc.y < screen.y || loc.y >= screen.y + screen.height))
 330         {
 331             balloon.display(caption, text, messageType);
 332         }
 333     }
 334 
 335     // It's synchronized with disposal by EDT.
 336     public void showPopupMenu(int x, int y) {
 337         if (isDisposed())
 338             return;
 339 
 340         assert SunToolkit.isDispatchThreadForAppContext(target);
 341 
 342         PopupMenu newPopup = target.getPopupMenu();
 343         if (popup != newPopup) {
 344             if (popup != null) {
 345                 eframe.remove(popup);
 346             }
 347             if (newPopup != null) {
 348                 eframe.add(newPopup);
 349             }
 350             popup = newPopup;
 351         }
 352 
 353         if (popup != null) {
 354             Point loc = ((XBaseWindow)eframe.getPeer()).toLocal(new Point(x, y));
 355             popup.show(eframe, loc.x, loc.y);
 356         }
 357     }
 358 
 359 
 360     // ******************************************************************
 361     // ******************************************************************
 362 
 363 
 364     private void addXED(long window, XEventDispatcher xed, long mask) {
 365         if (window == 0) {
 366             return;
 367         }
 368         XToolkit.awtLock();
 369         try {
 370             XlibWrapper.XSelectInput(XToolkit.getDisplay(), window, mask);
 371         } finally {
 372             XToolkit.awtUnlock();
 373         }
 374         XToolkit.addEventDispatcher(window, xed);
 375     }
 376 
 377     private void removeXED(long window, XEventDispatcher xed) {
 378         if (window == 0) {
 379             return;
 380         }
 381         XToolkit.awtLock();
 382         try {
 383             XToolkit.removeEventDispatcher(window, xed);
 384         } finally {
 385             XToolkit.awtUnlock();
 386         }
 387     }
 388 
 389     // Private method for testing purposes.
 390     private Point getLocationOnScreen() {
 391         return eframe.getLocationOnScreen();
 392     }
 393 
 394     public Rectangle getBounds() {
 395         Point loc = getLocationOnScreen();
 396         return new Rectangle(loc.x, loc.y, loc.x + TRAY_ICON_WIDTH, loc.y + TRAY_ICON_HEIGHT);
 397     }
 398 
 399     void addListeners() {
 400         canvas.addMouseListener(eventProxy);
 401         canvas.addMouseMotionListener(eventProxy);
 402     }
 403 
 404     long getWindow() {
 405         return ((XEmbeddedFramePeer)eframe.getPeer()).getWindow();
 406     }
 407 
 408     public boolean isDisposed() {
 409         return isDisposed;
 410     }
 411 
 412     public String getActionCommand() {
 413         return target.getActionCommand();
 414     }
 415 
 416     static class TrayIconEventProxy implements MouseListener, MouseMotionListener {
 417         XTrayIconPeer xtiPeer;
 418 
 419         TrayIconEventProxy(XTrayIconPeer xtiPeer) {
 420             this.xtiPeer = xtiPeer;
 421         }
 422 
 423         public void handleEvent(MouseEvent e) {
 424             //prevent DRAG events from being posted with TrayIcon source(CR 6565779)
 425             if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
 426                 return;
 427             }
 428 
 429             // Event handling is synchronized with disposal by EDT.
 430             if (xtiPeer.isDisposed()) {
 431                 return;
 432             }
 433             Point coord = XBaseWindow.toOtherWindow(xtiPeer.getWindow(),
 434                                                     XToolkit.getDefaultRootWindow(),
 435                                                     e.getX(), e.getY());
 436 
 437             if (e.isPopupTrigger()) {
 438                 xtiPeer.showPopupMenu(coord.x, coord.y);
 439             }
 440 
 441             e.translatePoint(coord.x - e.getX(), coord.y - e.getY());
 442             // This is a hack in order to set non-Component source to MouseEvent
 443             // instance.
 444             // In some cases this could lead to unpredictable result (e.g. when
 445             // other class tries to cast source field to Component).
 446             // We already filter DRAG events out (CR 6565779).
 447             e.setSource(xtiPeer.target);
 448             Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(e);
 449         }
 450         public void mouseClicked(MouseEvent e) {
 451             if ((e.getClickCount() > 1 || xtiPeer.balloon.isVisible()) &&
 452                 e.getButton() == MouseEvent.BUTTON1)
 453             {
 454                 ActionEvent aev = new ActionEvent(xtiPeer.target, ActionEvent.ACTION_PERFORMED,
 455                                                   xtiPeer.target.getActionCommand(), e.getWhen(),
 456                                                   e.getModifiers());
 457                 XToolkit.postEvent(XToolkit.targetToAppContext(aev.getSource()), aev);
 458             }
 459             if (xtiPeer.balloon.isVisible()) {
 460                 xtiPeer.balloon.hide();
 461             }
 462             handleEvent(e);
 463         }
 464         public void mouseEntered(MouseEvent e) {
 465             xtiPeer.tooltip.enter();
 466             handleEvent(e);
 467         }
 468         public void mouseExited(MouseEvent e) {
 469             xtiPeer.tooltip.exit();
 470             handleEvent(e);
 471         }
 472         public void mousePressed(MouseEvent e) {
 473             handleEvent(e);
 474         }
 475         public void mouseReleased(MouseEvent e) {
 476             handleEvent(e);
 477         }
 478         public void mouseDragged(MouseEvent e) {
 479             handleEvent(e);
 480         }
 481         public void mouseMoved(MouseEvent e) {
 482             handleEvent(e);
 483         }
 484     }
 485 
 486     // ***************************************
 487     // Special embedded frame for tray icon
 488     // ***************************************
 489 
 490     private static class XTrayIconEmbeddedFrame extends XEmbeddedFrame {
 491         public XTrayIconEmbeddedFrame(){
 492             super(XToolkit.getDefaultRootWindow(), true, true);
 493         }
 494 
 495         public boolean isUndecorated() {
 496             return true;
 497         }
 498 
 499         public boolean isResizable() {
 500             return false;
 501         }
 502 
 503         // embedded frame for tray icon shouldn't be disposed by anyone except tray icon
 504         public void dispose(){
 505         }
 506 
 507         public void realDispose(){
 508             super.dispose();
 509         }
 510     };
 511 
 512     // ***************************************
 513     // Classes for painting an image on canvas
 514     // ***************************************
 515 
 516     static class TrayIconCanvas extends IconCanvas {
 517         TrayIcon target;
 518         boolean autosize;
 519 
 520         TrayIconCanvas(TrayIcon target, int width, int height) {
 521             super(width, height);
 522             this.target = target;
 523         }
 524 
 525         // Invoke on EDT.
 526         protected void repaintImage(boolean doClear) {
 527             boolean old_autosize = autosize;
 528             autosize = target.isImageAutoSize();
 529 
 530             curW = autosize ? width : image.getWidth(observer);
 531             curH = autosize ? height : image.getHeight(observer);
 532 
 533             super.repaintImage(doClear || (old_autosize != autosize));
 534         }
 535     }
 536 
 537     public static class IconCanvas extends Canvas {
 538         volatile Image image;
 539         IconObserver observer;
 540         int width, height;
 541         int curW, curH;
 542 
 543         IconCanvas(int width, int height) {
 544             this.width = curW = width;
 545             this.height = curH = height;
 546         }
 547 
 548         // Invoke on EDT.
 549         public void updateImage(Image image) {
 550             this.image = image;
 551             if (observer == null) {
 552                 observer = new IconObserver();
 553             }
 554             repaintImage(true);
 555         }
 556 
 557         // Invoke on EDT.
 558         protected void repaintImage(boolean doClear) {
 559             Graphics g = getGraphics();
 560             if (g != null) {
 561                 try {
 562                     if (isVisible()) {
 563                         if (doClear) {
 564                             update(g);
 565                         } else {
 566                             paint(g);
 567                         }
 568                     }
 569                 } finally {
 570                     g.dispose();
 571                 }
 572             }
 573         }
 574 
 575         // Invoke on EDT.
 576         public void paint(Graphics g) {
 577             if (g != null && curW > 0 && curH > 0) {
 578                 BufferedImage bufImage = new BufferedImage(curW, curH, BufferedImage.TYPE_INT_ARGB);
 579                 Graphics2D gr = bufImage.createGraphics();
 580                 if (gr != null) {
 581                     try {
 582                         gr.setColor(getBackground());
 583                         gr.fillRect(0, 0, curW, curH);
 584                         gr.drawImage(image, 0, 0, curW, curH, observer);
 585                         gr.dispose();
 586 
 587                         g.drawImage(bufImage, 0, 0, curW, curH, null);
 588                     } finally {
 589                         gr.dispose();
 590                     }
 591                 }
 592             }
 593         }
 594 
 595         class IconObserver implements ImageObserver {
 596             public boolean imageUpdate(final Image image, final int flags, int x, int y, int width, int height) {
 597                 if (image != IconCanvas.this.image || // if the image has been changed
 598                     !IconCanvas.this.isVisible())
 599                 {
 600                     return false;
 601                 }
 602                 if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS |
 603                               ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0)
 604                 {
 605                     SunToolkit.executeOnEventHandlerThread(IconCanvas.this, new Runnable() {
 606                             public void run() {
 607                                 repaintImage(false);
 608                             }
 609                         });
 610                 }
 611                 return (flags & ImageObserver.ALLBITS) == 0;
 612             }
 613         }
 614     }
 615 }