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.ComponentPeer; 31 import java.awt.peer.TrayIconPeer; 32 import sun.awt.*; 33 import java.awt.image.*; 34 import java.security.AccessController; 35 import java.security.PrivilegedAction; 36 import java.lang.reflect.InvocationTargetException; 37 import sun.util.logging.PlatformLogger; 38 39 public class XTrayIconPeer implements TrayIconPeer, 40 InfoWindow.Balloon.LiveArguments, 41 InfoWindow.Tooltip.LiveArguments 42 { 43 private static final PlatformLogger ctrLog = PlatformLogger.getLogger("sun.awt.X11.XTrayIconPeer.centering"); 44 45 TrayIcon target; 46 TrayIconEventProxy eventProxy; 47 XTrayIconEmbeddedFrame eframe; 48 TrayIconCanvas canvas; 49 InfoWindow.Balloon balloon; 50 InfoWindow.Tooltip tooltip; 51 PopupMenu popup; 52 String tooltipString; 53 boolean isTrayIconDisplayed; 54 long eframeParentID; 55 final XEventDispatcher parentXED, eframeXED; 56 57 static final XEventDispatcher dummyXED = new XEventDispatcher() { 58 public void dispatchEvent(XEvent ev) {} 59 }; 60 61 volatile boolean isDisposed; 62 63 boolean isParentWindowLocated; 64 int old_x, old_y; 65 int ex_width, ex_height; 66 67 static final int TRAY_ICON_WIDTH = 24; 68 static final int TRAY_ICON_HEIGHT = 24; 69 70 XTrayIconPeer(TrayIcon target) 71 throws AWTException 72 { 73 this.target = target; 74 75 eventProxy = new TrayIconEventProxy(this); 76 77 canvas = new TrayIconCanvas(target, TRAY_ICON_WIDTH, TRAY_ICON_HEIGHT); 78 79 eframe = new XTrayIconEmbeddedFrame(); 80 81 eframe.setSize(TRAY_ICON_WIDTH, TRAY_ICON_HEIGHT); 82 eframe.add(canvas); 83 84 // Fix for 6317038: as EmbeddedFrame is instance of Frame, it is blocked 85 // by modal dialogs, but in the case of TrayIcon it shouldn't. So we 86 // set ModalExclusion property on it. 87 AccessController.doPrivileged(new PrivilegedAction<Object>() { 88 public Object run() { 89 eframe.setModalExclusionType(Dialog.ModalExclusionType.TOOLKIT_EXCLUDE); 90 return null; 91 } 92 }); 93 94 95 if (XWM.getWMID() != XWM.METACITY_WM) { 96 parentXED = dummyXED; // We don't like to leave it 'null'. 97 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 eframe.realDispose(); 305 balloon.dispose(); 306 isTrayIconDisplayed = false; 307 XToolkit.targetDisposedPeer(target, this); 308 } 309 310 public static void suppressWarningString(Window w) { 311 AWTAccessor.getWindowAccessor().setTrayIconWindow(w, true); 312 } 313 314 public void setToolTip(String tooltip) { 315 tooltipString = tooltip; 316 } 317 318 public String getTooltipString() { 319 return tooltipString; 320 } 321 322 public void updateImage() { 323 Runnable r = new Runnable() { 324 public void run() { 325 canvas.updateImage(target.getImage()); 326 } 327 }; 328 329 if (!SunToolkit.isDispatchThreadForAppContext(target)) { 330 SunToolkit.executeOnEventHandlerThread(target, r); 331 } else { 332 r.run(); 333 } 334 } 335 336 public void displayMessage(String caption, String text, String messageType) { 337 Point loc = getLocationOnScreen(); 338 Rectangle screen = eframe.getGraphicsConfiguration().getBounds(); 339 340 // Check if the tray icon is in the bounds of a screen. 341 if (!(loc.x < screen.x || loc.x >= screen.x + screen.width || 342 loc.y < screen.y || loc.y >= screen.y + screen.height)) 343 { 344 balloon.display(caption, text, messageType); 345 } 346 } 347 348 // It's synchronized with disposal by EDT. 349 public void showPopupMenu(int x, int y) { 350 if (isDisposed()) 351 return; 352 353 assert SunToolkit.isDispatchThreadForAppContext(target); 354 355 PopupMenu newPopup = target.getPopupMenu(); 356 if (popup != newPopup) { 357 if (popup != null) { 358 eframe.remove(popup); 359 } 360 if (newPopup != null) { 361 eframe.add(newPopup); 362 } 363 popup = newPopup; 364 } 365 366 if (popup != null) { 367 final XBaseWindow peer = AWTAccessor.getComponentAccessor() 368 .getPeer(eframe); 369 Point loc = peer.toLocal(new Point(x, y)); 370 popup.show(eframe, loc.x, loc.y); 371 } 372 } 373 374 375 // ****************************************************************** 376 // ****************************************************************** 377 378 379 private void addXED(long window, XEventDispatcher xed, long mask) { 380 if (window == 0) { 381 return; 382 } 383 XToolkit.awtLock(); 384 try { 385 XlibWrapper.XSelectInput(XToolkit.getDisplay(), window, mask); 386 } finally { 387 XToolkit.awtUnlock(); 388 } 389 XToolkit.addEventDispatcher(window, xed); 390 } 391 392 private void removeXED(long window, XEventDispatcher xed) { 393 if (window == 0) { 394 return; 395 } 396 XToolkit.awtLock(); 397 try { 398 XToolkit.removeEventDispatcher(window, xed); 399 } finally { 400 XToolkit.awtUnlock(); 401 } 402 } 403 404 // Private method for testing purposes. 405 private Point getLocationOnScreen() { 406 return eframe.getLocationOnScreen(); 407 } 408 409 public Rectangle getBounds() { 410 Point loc = getLocationOnScreen(); 411 return new Rectangle(loc.x, loc.y, loc.x + TRAY_ICON_WIDTH, loc.y + TRAY_ICON_HEIGHT); 412 } 413 414 void addListeners() { 415 canvas.addMouseListener(eventProxy); 416 canvas.addMouseMotionListener(eventProxy); 417 eframe.addMouseListener(eventProxy); 418 } 419 420 long getWindow() { 421 return AWTAccessor.getComponentAccessor() 422 .<XEmbeddedFramePeer>getPeer(eframe).getWindow(); 423 } 424 425 public boolean isDisposed() { 426 return isDisposed; 427 } 428 429 public String getActionCommand() { 430 return target.getActionCommand(); 431 } 432 433 static class TrayIconEventProxy implements MouseListener, MouseMotionListener { 434 XTrayIconPeer xtiPeer; 435 436 TrayIconEventProxy(XTrayIconPeer xtiPeer) { 437 this.xtiPeer = xtiPeer; 438 } 439 440 public void handleEvent(MouseEvent e) { 441 //prevent DRAG events from being posted with TrayIcon source(CR 6565779) 442 if (e.getID() == MouseEvent.MOUSE_DRAGGED) { 443 return; 444 } 445 446 // Event handling is synchronized with disposal by EDT. 447 if (xtiPeer.isDisposed()) { 448 return; 449 } 450 Point coord = XBaseWindow.toOtherWindow(xtiPeer.getWindow(), 451 XToolkit.getDefaultRootWindow(), 452 e.getX(), e.getY()); 453 454 if (e.isPopupTrigger()) { 455 xtiPeer.showPopupMenu(coord.x, coord.y); 456 } 457 458 e.translatePoint(coord.x - e.getX(), coord.y - e.getY()); 459 // This is a hack in order to set non-Component source to MouseEvent 460 // instance. 461 // In some cases this could lead to unpredictable result (e.g. when 462 // other class tries to cast source field to Component). 463 // We already filter DRAG events out (CR 6565779). 464 e.setSource(xtiPeer.target); 465 XToolkit.postEvent(XToolkit.targetToAppContext(e.getSource()), e); 466 } 467 public void mouseClicked(MouseEvent e) { 468 if ((e.getClickCount() > 1 || xtiPeer.balloon.isVisible()) && 469 e.getButton() == MouseEvent.BUTTON1) 470 { 471 ActionEvent aev = new ActionEvent(xtiPeer.target, ActionEvent.ACTION_PERFORMED, 472 xtiPeer.target.getActionCommand(), e.getWhen(), 473 e.getModifiers()); 474 XToolkit.postEvent(XToolkit.targetToAppContext(aev.getSource()), aev); 475 } 476 if (xtiPeer.balloon.isVisible()) { 477 xtiPeer.balloon.hide(); 478 } 479 handleEvent(e); 480 } 481 public void mouseEntered(MouseEvent e) { 482 xtiPeer.tooltip.enter(); 483 handleEvent(e); 484 } 485 public void mouseExited(MouseEvent e) { 486 xtiPeer.tooltip.exit(); 487 handleEvent(e); 488 } 489 public void mousePressed(MouseEvent e) { 490 handleEvent(e); 491 } 492 public void mouseReleased(MouseEvent e) { 493 handleEvent(e); 494 } 495 public void mouseDragged(MouseEvent e) { 496 handleEvent(e); 497 } 498 public void mouseMoved(MouseEvent e) { 499 handleEvent(e); 500 } 501 } 502 503 // *************************************** 504 // Special embedded frame for tray icon 505 // *************************************** 506 507 @SuppressWarnings("serial") // JDK-implementation class 508 private static class XTrayIconEmbeddedFrame extends XEmbeddedFrame { 509 public XTrayIconEmbeddedFrame(){ 510 super(XToolkit.getDefaultRootWindow(), true, true); 511 } 512 513 public boolean isUndecorated() { 514 return true; 515 } 516 517 public boolean isResizable() { 518 return false; 519 } 520 521 // embedded frame for tray icon shouldn't be disposed by anyone except tray icon 522 public void dispose(){ 523 } 524 525 public void realDispose(){ 526 super.dispose(); 527 } 528 }; 529 530 // *************************************** 531 // Classes for painting an image on canvas 532 // *************************************** 533 534 @SuppressWarnings("serial") // JDK-implementation class 535 static class TrayIconCanvas extends IconCanvas { 536 TrayIcon target; 537 boolean autosize; 538 539 TrayIconCanvas(TrayIcon target, int width, int height) { 540 super(width, height); 541 this.target = target; 542 } 543 544 // Invoke on EDT. 545 protected void repaintImage(boolean doClear) { 546 boolean old_autosize = autosize; 547 autosize = target.isImageAutoSize(); 548 549 curW = autosize ? width : image.getWidth(observer); 550 curH = autosize ? height : image.getHeight(observer); 551 552 super.repaintImage(doClear || (old_autosize != autosize)); 553 } 554 } 555 556 @SuppressWarnings("serial") // JDK-implementation class 557 public static class IconCanvas extends Canvas { 558 volatile Image image; 559 IconObserver observer; 560 int width, height; 561 int curW, curH; 562 563 IconCanvas(int width, int height) { 564 this.width = curW = width; 565 this.height = curH = height; 566 } 567 568 // Invoke on EDT. 569 public void updateImage(Image image) { 570 this.image = image; 571 if (observer == null) { 572 observer = new IconObserver(); 573 } 574 repaintImage(true); 575 } 576 577 // Invoke on EDT. 578 protected void repaintImage(boolean doClear) { 579 Graphics g = getGraphics(); 580 if (g != null) { 581 try { 582 if (isVisible()) { 583 if (doClear) { 584 update(g); 585 } else { 586 paint(g); 587 } 588 } 589 } finally { 590 g.dispose(); 591 } 592 } 593 } 594 595 // Invoke on EDT. 596 public void paint(Graphics g) { 597 if (g != null && curW > 0 && curH > 0) { 598 BufferedImage bufImage = new BufferedImage(curW, curH, BufferedImage.TYPE_INT_ARGB); 599 Graphics2D gr = bufImage.createGraphics(); 600 if (gr != null) { 601 try { 602 gr.setColor(getBackground()); 603 gr.fillRect(0, 0, curW, curH); 604 gr.drawImage(image, 0, 0, curW, curH, observer); 605 gr.dispose(); 606 607 g.drawImage(bufImage, 0, 0, curW, curH, null); 608 } finally { 609 gr.dispose(); 610 } 611 } 612 } 613 } 614 615 class IconObserver implements ImageObserver { 616 public boolean imageUpdate(final Image image, final int flags, int x, int y, int width, int height) { 617 if (image != IconCanvas.this.image || // if the image has been changed 618 !IconCanvas.this.isVisible()) 619 { 620 return false; 621 } 622 if ((flags & (ImageObserver.FRAMEBITS | ImageObserver.ALLBITS | 623 ImageObserver.WIDTH | ImageObserver.HEIGHT)) != 0) 624 { 625 SunToolkit.executeOnEventHandlerThread(IconCanvas.this, new Runnable() { 626 public void run() { 627 repaintImage(false); 628 } 629 }); 630 } 631 return (flags & ImageObserver.ALLBITS) == 0; 632 } 633 } 634 } 635 }