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