1 /* 2 * Copyright (c) 2009, 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 sun.misc.ManagedLocalsThread; 33 34 import java.awt.image.*; 35 import java.text.BreakIterator; 36 import java.util.concurrent.ArrayBlockingQueue; 37 import java.security.AccessController; 38 import java.security.PrivilegedAction; 39 import java.lang.reflect.InvocationTargetException; 40 41 /** 42 * An utility window class. This is a base class for Tooltip and Balloon. 43 */ 44 @SuppressWarnings("serial") // JDK-implementation class 45 public abstract class InfoWindow extends Window { 46 private Container container; 47 private Closer closer; 48 49 protected InfoWindow(Frame parent, Color borderColor) { 50 super(parent); 51 setType(Window.Type.POPUP); 52 container = new Container() { 53 @Override 54 public Insets getInsets() { 55 return new Insets(1, 1, 1, 1); 56 } 57 }; 58 setLayout(new BorderLayout()); 59 setBackground(borderColor); 60 add(container, BorderLayout.CENTER); 61 container.setLayout(new BorderLayout()); 62 63 closer = new Closer(); 64 } 65 66 public Component add(Component c) { 67 container.add(c, BorderLayout.CENTER); 68 return c; 69 } 70 71 protected void setCloser(Runnable action, int time) { 72 closer.set(action, time); 73 } 74 75 // Must be executed on EDT. 76 @SuppressWarnings("deprecation") 77 protected void show(Point corner, int indent) { 78 assert SunToolkit.isDispatchThreadForAppContext(this); 79 80 pack(); 81 82 Dimension size = getSize(); 83 // TODO: When 6356322 is fixed we should get screen bounds in 84 // this way: eframe.getGraphicsConfiguration().getBounds(). 85 Dimension scrSize = Toolkit.getDefaultToolkit().getScreenSize(); 86 87 if (corner.x < scrSize.width/2 && corner.y < scrSize.height/2) { // 1st square 88 setLocation(corner.x + indent, corner.y + indent); 89 90 } else if (corner.x >= scrSize.width/2 && corner.y < scrSize.height/2) { // 2nd square 91 setLocation(corner.x - indent - size.width, corner.y + indent); 92 93 } else if (corner.x < scrSize.width/2 && corner.y >= scrSize.height/2) { // 3rd square 94 setLocation(corner.x + indent, corner.y - indent - size.height); 95 96 } else if (corner.x >= scrSize.width/2 && corner.y >= scrSize.height/2) { // 4th square 97 setLocation(corner.x - indent - size.width, corner.y - indent - size.height); 98 } 99 100 super.show(); 101 closer.schedule(); 102 } 103 104 @SuppressWarnings("deprecation") 105 public void hide() { 106 closer.close(); 107 } 108 109 private class Closer implements Runnable { 110 Runnable action; 111 int time; 112 113 public void run() { 114 doClose(); 115 } 116 117 void set(Runnable action, int time) { 118 this.action = action; 119 this.time = time; 120 } 121 122 void schedule() { 123 XToolkit.schedule(this, time); 124 } 125 126 void close() { 127 XToolkit.remove(this); 128 doClose(); 129 } 130 131 // WARNING: this method may be executed on Toolkit thread. 132 @SuppressWarnings("deprecation") 133 private void doClose() { 134 SunToolkit.executeOnEventHandlerThread(InfoWindow.this, new Runnable() { 135 public void run() { 136 InfoWindow.super.hide(); 137 invalidate(); 138 if (action != null) { 139 action.run(); 140 } 141 } 142 }); 143 } 144 } 145 146 147 private interface LiveArguments { 148 /** Whether the target of the InfoWindow is disposed. */ 149 boolean isDisposed(); 150 151 /** The bounds of the target of the InfoWindow. */ 152 Rectangle getBounds(); 153 } 154 155 @SuppressWarnings("serial") // JDK-implementation class 156 public static class Tooltip extends InfoWindow { 157 158 public interface LiveArguments extends InfoWindow.LiveArguments { 159 /** The tooltip to be displayed. */ 160 String getTooltipString(); 161 } 162 163 private final Object target; 164 private final LiveArguments liveArguments; 165 166 private final Label textLabel = new Label(""); 167 private final Runnable starter = new Runnable() { 168 public void run() { 169 display(); 170 }}; 171 172 private static final int TOOLTIP_SHOW_TIME = 10000; 173 private static final int TOOLTIP_START_DELAY_TIME = 1000; 174 private static final int TOOLTIP_MAX_LENGTH = 64; 175 private static final int TOOLTIP_MOUSE_CURSOR_INDENT = 5; 176 private static final Color TOOLTIP_BACKGROUND_COLOR = new Color(255, 255, 220); 177 private static final Font TOOLTIP_TEXT_FONT = XWindow.getDefaultFont(); 178 179 public Tooltip(Frame parent, Object target, 180 LiveArguments liveArguments) 181 { 182 super(parent, Color.black); 183 184 this.target = target; 185 this.liveArguments = liveArguments; 186 187 XTrayIconPeer.suppressWarningString(this); 188 189 setCloser(null, TOOLTIP_SHOW_TIME); 190 textLabel.setBackground(TOOLTIP_BACKGROUND_COLOR); 191 textLabel.setFont(TOOLTIP_TEXT_FONT); 192 add(textLabel); 193 } 194 195 /* 196 * WARNING: this method is executed on Toolkit thread! 197 */ 198 private void display() { 199 // Execute on EDT to avoid deadlock (see 6280857). 200 SunToolkit.executeOnEventHandlerThread(target, new Runnable() { 201 public void run() { 202 if (liveArguments.isDisposed()) { 203 return; 204 } 205 206 String tooltipString = liveArguments.getTooltipString(); 207 if (tooltipString == null) { 208 return; 209 } else if (tooltipString.length() > TOOLTIP_MAX_LENGTH) { 210 textLabel.setText(tooltipString.substring(0, TOOLTIP_MAX_LENGTH)); 211 } else { 212 textLabel.setText(tooltipString); 213 } 214 215 Point pointer = AccessController.doPrivileged( 216 new PrivilegedAction<Point>() { 217 public Point run() { 218 if (!isPointerOverTrayIcon(liveArguments.getBounds())) { 219 return null; 220 } 221 return MouseInfo.getPointerInfo().getLocation(); 222 } 223 }); 224 if (pointer == null) { 225 return; 226 } 227 show(new Point(pointer.x, pointer.y), TOOLTIP_MOUSE_CURSOR_INDENT); 228 } 229 }); 230 } 231 232 public void enter() { 233 XToolkit.schedule(starter, TOOLTIP_START_DELAY_TIME); 234 } 235 236 public void exit() { 237 XToolkit.remove(starter); 238 if (isVisible()) { 239 hide(); 240 } 241 } 242 243 private boolean isPointerOverTrayIcon(Rectangle trayRect) { 244 Point p = MouseInfo.getPointerInfo().getLocation(); 245 return !(p.x < trayRect.x || p.x > (trayRect.x + trayRect.width) || 246 p.y < trayRect.y || p.y > (trayRect.y + trayRect.height)); 247 } 248 } 249 250 @SuppressWarnings("serial") // JDK-implementation class 251 public static class Balloon extends InfoWindow { 252 253 public interface LiveArguments extends InfoWindow.LiveArguments { 254 /** The action to be performed upon clicking the baloon. */ 255 String getActionCommand(); 256 } 257 258 private final LiveArguments liveArguments; 259 private final Object target; 260 261 private static final int BALLOON_SHOW_TIME = 10000; 262 private static final int BALLOON_TEXT_MAX_LENGTH = 256; 263 private static final int BALLOON_WORD_LINE_MAX_LENGTH = 16; 264 private static final int BALLOON_WORD_LINE_MAX_COUNT = 4; 265 private static final int BALLOON_ICON_WIDTH = 32; 266 private static final int BALLOON_ICON_HEIGHT = 32; 267 private static final int BALLOON_TRAY_ICON_INDENT = 0; 268 private static final Color BALLOON_CAPTION_BACKGROUND_COLOR = new Color(200, 200 ,255); 269 private static final Font BALLOON_CAPTION_FONT = new Font(Font.DIALOG, Font.BOLD, 12); 270 271 private Panel mainPanel = new Panel(); 272 private Panel captionPanel = new Panel(); 273 private Label captionLabel = new Label(""); 274 private Button closeButton = new Button("X"); 275 private Panel textPanel = new Panel(); 276 private XTrayIconPeer.IconCanvas iconCanvas = new XTrayIconPeer.IconCanvas(BALLOON_ICON_WIDTH, BALLOON_ICON_HEIGHT); 277 private Label[] lineLabels = new Label[BALLOON_WORD_LINE_MAX_COUNT]; 278 private ActionPerformer ap = new ActionPerformer(); 279 280 private Image iconImage; 281 private Image errorImage; 282 private Image warnImage; 283 private Image infoImage; 284 private boolean gtkImagesLoaded; 285 286 private Displayer displayer = new Displayer(); 287 288 public Balloon(Frame parent, Object target, LiveArguments liveArguments) { 289 super(parent, new Color(90, 80 ,190)); 290 this.liveArguments = liveArguments; 291 this.target = target; 292 293 XTrayIconPeer.suppressWarningString(this); 294 295 setCloser(new Runnable() { 296 public void run() { 297 if (textPanel != null) { 298 textPanel.removeAll(); 299 textPanel.setSize(0, 0); 300 iconCanvas.setSize(0, 0); 301 XToolkit.awtLock(); 302 try { 303 displayer.isDisplayed = false; 304 XToolkit.awtLockNotifyAll(); 305 } finally { 306 XToolkit.awtUnlock(); 307 } 308 } 309 } 310 }, BALLOON_SHOW_TIME); 311 312 add(mainPanel); 313 314 captionLabel.setFont(BALLOON_CAPTION_FONT); 315 captionLabel.addMouseListener(ap); 316 317 captionPanel.setLayout(new BorderLayout()); 318 captionPanel.add(captionLabel, BorderLayout.WEST); 319 captionPanel.add(closeButton, BorderLayout.EAST); 320 captionPanel.setBackground(BALLOON_CAPTION_BACKGROUND_COLOR); 321 captionPanel.addMouseListener(ap); 322 323 closeButton.addActionListener(new ActionListener() { 324 public void actionPerformed(ActionEvent e) { 325 hide(); 326 } 327 }); 328 329 mainPanel.setLayout(new BorderLayout()); 330 mainPanel.setBackground(Color.white); 331 mainPanel.add(captionPanel, BorderLayout.NORTH); 332 mainPanel.add(iconCanvas, BorderLayout.WEST); 333 mainPanel.add(textPanel, BorderLayout.CENTER); 334 335 iconCanvas.addMouseListener(ap); 336 337 for (int i = 0; i < BALLOON_WORD_LINE_MAX_COUNT; i++) { 338 lineLabels[i] = new Label(); 339 lineLabels[i].addMouseListener(ap); 340 lineLabels[i].setBackground(Color.white); 341 } 342 343 displayer.thread.start(); 344 } 345 346 public void display(String caption, String text, String messageType) { 347 if (!gtkImagesLoaded) { 348 loadGtkImages(); 349 } 350 displayer.display(caption, text, messageType); 351 } 352 353 private void _display(String caption, String text, String messageType) { 354 captionLabel.setText(caption); 355 356 BreakIterator iter = BreakIterator.getWordInstance(); 357 if (text != null) { 358 iter.setText(text); 359 int start = iter.first(), end; 360 int nLines = 0; 361 362 do { 363 end = iter.next(); 364 365 if (end == BreakIterator.DONE || 366 text.substring(start, end).length() >= 50) 367 { 368 lineLabels[nLines].setText(text.substring(start, end == BreakIterator.DONE ? 369 iter.last() : end)); 370 textPanel.add(lineLabels[nLines++]); 371 start = end; 372 } 373 if (nLines == BALLOON_WORD_LINE_MAX_COUNT) { 374 if (end != BreakIterator.DONE) { 375 lineLabels[nLines - 1].setText( 376 new String(lineLabels[nLines - 1].getText() + " ...")); 377 } 378 break; 379 } 380 } while (end != BreakIterator.DONE); 381 382 383 textPanel.setLayout(new GridLayout(nLines, 1)); 384 } 385 386 if ("ERROR".equals(messageType)) { 387 iconImage = errorImage; 388 } else if ("WARNING".equals(messageType)) { 389 iconImage = warnImage; 390 } else if ("INFO".equals(messageType)) { 391 iconImage = infoImage; 392 } else { 393 iconImage = null; 394 } 395 396 if (iconImage != null) { 397 Dimension tpSize = textPanel.getSize(); 398 iconCanvas.setSize(BALLOON_ICON_WIDTH, (BALLOON_ICON_HEIGHT > tpSize.height ? 399 BALLOON_ICON_HEIGHT : tpSize.height)); 400 iconCanvas.validate(); 401 } 402 403 SunToolkit.executeOnEventHandlerThread(target, new Runnable() { 404 public void run() { 405 if (liveArguments.isDisposed()) { 406 return; 407 } 408 Point parLoc = getParent().getLocationOnScreen(); 409 Dimension parSize = getParent().getSize(); 410 show(new Point(parLoc.x + parSize.width/2, parLoc.y + parSize.height/2), 411 BALLOON_TRAY_ICON_INDENT); 412 if (iconImage != null) { 413 iconCanvas.updateImage(iconImage); // call it after the show(..) above 414 } 415 } 416 }); 417 } 418 419 public void dispose() { 420 displayer.thread.interrupt(); 421 super.dispose(); 422 } 423 424 private void loadGtkImages() { 425 if (!gtkImagesLoaded) { 426 errorImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty( 427 "gtk.icon.gtk-dialog-error.6.rtl"); 428 warnImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty( 429 "gtk.icon.gtk-dialog-warning.6.rtl"); 430 infoImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty( 431 "gtk.icon.gtk-dialog-info.6.rtl"); 432 gtkImagesLoaded = true; 433 } 434 } 435 436 private class ActionPerformer extends MouseAdapter { 437 public void mouseClicked(MouseEvent e) { 438 // hide the balloon by any click 439 hide(); 440 if (e.getButton() == MouseEvent.BUTTON1) { 441 ActionEvent aev = new ActionEvent(target, ActionEvent.ACTION_PERFORMED, 442 liveArguments.getActionCommand(), 443 e.getWhen(), e.getModifiers()); 444 XToolkit.postEvent(XToolkit.targetToAppContext(aev.getSource()), aev); 445 } 446 } 447 } 448 449 private class Displayer implements Runnable { 450 final int MAX_CONCURRENT_MSGS = 10; 451 452 ArrayBlockingQueue<Message> messageQueue = new ArrayBlockingQueue<Message>(MAX_CONCURRENT_MSGS); 453 boolean isDisplayed; 454 final Thread thread; 455 456 Displayer() { 457 this.thread = new ManagedLocalsThread(this); 458 this.thread.setDaemon(true); 459 } 460 461 @Override 462 public void run() { 463 while (true) { 464 Message msg = null; 465 try { 466 msg = messageQueue.take(); 467 } catch (InterruptedException e) { 468 return; 469 } 470 471 /* 472 * Wait till the previous message is displayed if any 473 */ 474 XToolkit.awtLock(); 475 try { 476 while (isDisplayed) { 477 try { 478 XToolkit.awtLockWait(); 479 } catch (InterruptedException e) { 480 return; 481 } 482 } 483 isDisplayed = true; 484 } finally { 485 XToolkit.awtUnlock(); 486 } 487 _display(msg.caption, msg.text, msg.messageType); 488 } 489 } 490 491 void display(String caption, String text, String messageType) { 492 messageQueue.offer(new Message(caption, text, messageType)); 493 } 494 } 495 496 private static class Message { 497 String caption, text, messageType; 498 499 Message(String caption, String text, String messageType) { 500 this.caption = caption; 501 this.text = text; 502 this.messageType = messageType; 503 } 504 } 505 } 506 } 507