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