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 }