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 }