1 /*
   2  * Copyright 2005-2008 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 
  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.logging.Logger;
  35 import java.util.logging.Level;
  36 import java.util.concurrent.ArrayBlockingQueue;
  37 import java.security.AccessController;
  38 import java.security.PrivilegedAction;
  39 import java.lang.reflect.InvocationTargetException;
  40 
  41 public class XTrayIconPeer implements TrayIconPeer,
  42        InfoWindow.Balloon.LiveArguments,
  43        InfoWindow.Tooltip.LiveArguments
  44 {
  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     InfoWindow.Balloon balloon;
  52     InfoWindow.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() != XConstants.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() != XConstants.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, XConstants.StructureNotifyMask);
 221 
 222                         isTrayIconDisplayed = true;
 223                         XToolkit.awtLockNotifyAll();
 224                     }
 225                 }
 226             };
 227 
 228         addXED(getWindow(), eframeXED, XConstants.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 InfoWindow.Balloon(eframe, target, this);
 262         tooltip = new InfoWindow.Tooltip(eframe, target, this);
 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 String getTooltipString() {
 307         return tooltipString;
 308     }
 309 
 310     public void updateImage() {
 311         Runnable r = new Runnable() {
 312                 public void run() {
 313                     canvas.updateImage(target.getImage());
 314                 }
 315             };
 316 
 317         if (!SunToolkit.isDispatchThreadForAppContext(target)) {
 318             SunToolkit.executeOnEventHandlerThread(target, r);
 319         } else {
 320             r.run();
 321         }
 322     }
 323 
 324     public void displayMessage(String caption, String text, String messageType) {
 325         Point loc = getLocationOnScreen();
 326         Rectangle screen = eframe.getGraphicsConfiguration().getBounds();
 327 
 328         // Check if the tray icon is in the bounds of a screen.
 329         if (!(loc.x < screen.x || loc.x >= screen.x + screen.width ||
 330               loc.y < screen.y || loc.y >= screen.y + screen.height))
 331         {
 332             balloon.display(caption, text, messageType);
 333         }
 334     }
 335 
 336     // It's synchronized with disposal by EDT.
 337     public void showPopupMenu(int x, int y) {
 338         if (isDisposed())
 339             return;
 340 
 341         assert SunToolkit.isDispatchThreadForAppContext(target);
 342 
 343         PopupMenu newPopup = target.getPopupMenu();
 344         if (popup != newPopup) {
 345             if (popup != null) {
 346                 eframe.remove(popup);
 347             }
 348             if (newPopup != null) {
 349                 eframe.add(newPopup);
 350             }
 351             popup = newPopup;
 352         }
 353 
 354         if (popup != null) {
 355             Point loc = ((XBaseWindow)eframe.getPeer()).toLocal(new Point(x, y));
 356             popup.show(eframe, loc.x, loc.y);
 357         }
 358     }
 359 
 360 
 361     // ******************************************************************
 362     // ******************************************************************
 363 
 364 
 365     private void addXED(long window, XEventDispatcher xed, long mask) {
 366         if (window == 0) {
 367             return;
 368         }
 369         XToolkit.awtLock();
 370         try {
 371             XlibWrapper.XSelectInput(XToolkit.getDisplay(), window, mask);
 372         } finally {
 373             XToolkit.awtUnlock();
 374         }
 375         XToolkit.addEventDispatcher(window, xed);
 376     }
 377 
 378     private void removeXED(long window, XEventDispatcher xed) {
 379         if (window == 0) {
 380             return;
 381         }
 382         XToolkit.awtLock();
 383         try {
 384             XToolkit.removeEventDispatcher(window, xed);
 385         } finally {
 386             XToolkit.awtUnlock();
 387         }
 388     }
 389 
 390     // Private method for testing purposes.
 391     private Point getLocationOnScreen() {
 392         return eframe.getLocationOnScreen();
 393     }
 394 
 395     public Rectangle getBounds() {
 396         Point loc = getLocationOnScreen();
 397         return new Rectangle(loc.x, loc.y, loc.x + TRAY_ICON_WIDTH, loc.y + TRAY_ICON_HEIGHT);
 398     }
 399 
 400     void addListeners() {
 401         canvas.addMouseListener(eventProxy);
 402         canvas.addMouseMotionListener(eventProxy);
 403     }
 404 
 405     long getWindow() {
 406         return ((XEmbeddedFramePeer)eframe.getPeer()).getWindow();
 407     }
 408 
 409     public boolean isDisposed() {
 410         return isDisposed;
 411     }
 412 
 413     public String getActionCommand() {
 414         return target.getActionCommand();
 415     }
 416 
 417     static class TrayIconEventProxy implements MouseListener, MouseMotionListener {
 418         XTrayIconPeer xtiPeer;
 419 
 420         TrayIconEventProxy(XTrayIconPeer xtiPeer) {
 421             this.xtiPeer = xtiPeer;
 422         }
 423 
 424         public void handleEvent(MouseEvent e) {
 425             //prevent DRAG events from being posted with TrayIcon source(CR 6565779)
 426             if (e.getID() == MouseEvent.MOUSE_DRAGGED) {
 427                 return;
 428             }
 429 
 430             // Event handling is synchronized with disposal by EDT.
 431             if (xtiPeer.isDisposed()) {
 432                 return;
 433             }
 434             Point coord = XBaseWindow.toOtherWindow(xtiPeer.getWindow(),
 435                                                     XToolkit.getDefaultRootWindow(),
 436                                                     e.getX(), e.getY());
 437 
 438             if (e.isPopupTrigger()) {
 439                 xtiPeer.showPopupMenu(coord.x, coord.y);
 440             }
 441 
 442             e.translatePoint(coord.x - e.getX(), coord.y - e.getY());
 443             // This is a hack in order to set non-Component source to MouseEvent
 444             // instance.
 445             // In some cases this could lead to unpredictable result (e.g. when
 446             // other class tries to cast source field to Component).
 447             // We already filter DRAG events out (CR 6565779).
 448             e.setSource(xtiPeer.target);
 449             Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(e);
 450         }
 451         public void mouseClicked(MouseEvent e) {
 452             if ((e.getClickCount() > 1 || xtiPeer.balloon.isVisible()) &&
 453                 e.getButton() == MouseEvent.BUTTON1)
 454             {
 455                 ActionEvent aev = new ActionEvent(xtiPeer.target, ActionEvent.ACTION_PERFORMED,
 456                                                   xtiPeer.target.getActionCommand(), e.getWhen(),
 457                                                   e.getModifiers());
 458                 Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(aev);
 459             }
 460             if (xtiPeer.balloon.isVisible()) {
 461                 xtiPeer.balloon.hide();
 462             }
 463             handleEvent(e);
 464         }
 465         public void mouseEntered(MouseEvent e) {
 466             xtiPeer.tooltip.enter();
 467             handleEvent(e);
 468         }
 469         public void mouseExited(MouseEvent e) {
 470             xtiPeer.tooltip.exit();
 471             handleEvent(e);
 472         }
 473         public void mousePressed(MouseEvent e) {
 474             handleEvent(e);
 475         }
 476         public void mouseReleased(MouseEvent e) {
 477             handleEvent(e);
 478         }
 479         public void mouseDragged(MouseEvent e) {
 480             handleEvent(e);
 481         }
 482         public void mouseMoved(MouseEvent e) {
 483             handleEvent(e);
 484         }
 485     }
 486 
 487     static boolean isTrayIconStuffWindow(Window w) {
 488         return (w instanceof InfoWindow.Tooltip) ||
 489                (w instanceof InfoWindow.Balloon) ||
 490                (w instanceof XTrayIconEmbeddedFrame);
 491     }
 492 
 493     // ***************************************
 494     // Special embedded frame for tray icon
 495     // ***************************************
 496 
 497     private static class XTrayIconEmbeddedFrame extends XEmbeddedFrame {
 498         public XTrayIconEmbeddedFrame(){
 499             super(XToolkit.getDefaultRootWindow(), true, true);
 500         }
 501 
 502         public boolean isUndecorated() {
 503             return true;
 504         }
 505 
 506         public boolean isResizable() {
 507             return false;
 508         }
 509 
 510         // embedded frame for tray icon shouldn't be disposed by anyone except tray icon
 511         public void dispose(){
 512         }
 513 
 514         public void realDispose(){
 515             super.dispose();
 516         }
 517     };
 518 
 519     // ***************************************
 520     // Classes for painting an image on canvas
 521     // ***************************************
 522 
 523     static class TrayIconCanvas extends IconCanvas {
 524         TrayIcon target;
 525         boolean autosize;
 526 
 527         TrayIconCanvas(TrayIcon target, int width, int height) {
 528             super(width, height);
 529             this.target = target;
 530         }
 531 
 532         // Invoke on EDT.
 533         protected void repaintImage(boolean doClear) {
 534             boolean old_autosize = autosize;
 535             autosize = target.isImageAutoSize();
 536 
 537             curW = autosize ? width : image.getWidth(observer);
 538             curH = autosize ? height : image.getHeight(observer);
 539 
 540             super.repaintImage(doClear || (old_autosize != autosize));
 541         }
 542     }
 543 
 544     public static class IconCanvas extends Canvas {
 545         volatile Image image;
 546         IconObserver observer;
 547         int width, height;
 548         int curW, curH;
 549 
 550         IconCanvas(int width, int height) {
 551             this.width = curW = width;
 552             this.height = curH = height;
 553         }
 554 
 555         // Invoke on EDT.
 556         public void updateImage(Image image) {
 557             this.image = image;
 558             if (observer == null) {
 559                 observer = new IconObserver();
 560             }
 561             repaintImage(true);
 562         }
 563 
 564         // Invoke on EDT.
 565         protected void repaintImage(boolean doClear) {
 566             Graphics g = getGraphics();
 567             if (g != null) {
 568                 try {
 569                     if (isVisible()) {
 570                         if (doClear) {
 571                             update(g);
 572                         } else {
 573                             paint(g);
 574                         }
 575                     }
 576                 } finally {
 577                     g.dispose();
 578                 }
 579             }
 580         }
 581 
 582         // Invoke on EDT.
 583         public void paint(Graphics g) {
 584             if (g != null && curW > 0 && curH > 0) {
 585                 BufferedImage bufImage = new BufferedImage(curW, curH, BufferedImage.TYPE_INT_ARGB);
 586                 Graphics2D gr = bufImage.createGraphics();
 587                 if (gr != null) {
 588                     try {
 589                         gr.setColor(getBackground());
 590                         gr.fillRect(0, 0, curW, curH);
 591                         gr.drawImage(image, 0, 0, curW, curH, observer);
 592                         gr.dispose();
 593 
 594                         g.drawImage(bufImage, 0, 0, curW, curH, null);
 595                     } finally {
 596                         gr.dispose();
 597                     }
 598                 }
 599             }
 600         }
 601 
 602         class IconObserver implements ImageObserver {
 603             public boolean imageUpdate(final Image image, final int flags, int x, int y, int width, int height) {
 604                 if (image != IconCanvas.this.image || // if the image has been changed
 605                     !IconCanvas.this.isVisible())
 606                 {
 607                     return false;
 608                 }
 609                 if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS |
 610                               ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0)
 611                 {
 612                     SunToolkit.executeOnEventHandlerThread(IconCanvas.this, new Runnable() {
 613                             public void run() {
 614                                 repaintImage(false);
 615                             }
 616                         });
 617                 }
 618                 return (flags & ImageObserver.ALLBITS) == 0;
 619             }
 620         }
 621     }
 622 }