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