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