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