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