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