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 = (Point)AccessController.doPrivileged(new PrivilegedAction() {
 211                                 public Object run() {
 212                                     if (!isPointerOverTrayIcon(liveArguments.getBounds())) {
 213                                         return null;
 214                                     }
 215                                     return MouseInfo.getPointerInfo().getLocation();
 216                                 }
 217                             });
 218                         if (pointer == null) {
 219                             return;
 220                         }
 221                         show(new Point(pointer.x, pointer.y), TOOLTIP_MOUSE_CURSOR_INDENT);
 222                     }
 223                 });
 224         }
 225 
 226         public void enter() {
 227             XToolkit.schedule(starter, TOOLTIP_START_DELAY_TIME);
 228         }
 229 
 230         public void exit() {
 231             XToolkit.remove(starter);
 232             if (isVisible()) {
 233                 hide();
 234             }
 235         }
 236 
 237         private boolean isPointerOverTrayIcon(Rectangle trayRect) {
 238             Point p = MouseInfo.getPointerInfo().getLocation();
 239             return !(p.x < trayRect.x || p.x > (trayRect.x + trayRect.width) ||
 240                      p.y < trayRect.y || p.y > (trayRect.y + trayRect.height));
 241         }
 242     }
 243 
 244     @SuppressWarnings("serial") // JDK-implementation class
 245     public static class Balloon extends InfoWindow {
 246 
 247         public interface LiveArguments extends InfoWindow.LiveArguments {
 248             /** The action to be performed upon clicking the baloon. */
 249             String getActionCommand();
 250         }
 251 
 252         private final LiveArguments liveArguments;
 253         private final Object target;
 254 
 255         private final static int BALLOON_SHOW_TIME = 10000;
 256         private final static int BALLOON_TEXT_MAX_LENGTH = 256;
 257         private final static int BALLOON_WORD_LINE_MAX_LENGTH = 16;
 258         private final static int BALLOON_WORD_LINE_MAX_COUNT = 4;
 259         private final static int BALLOON_ICON_WIDTH = 32;
 260         private final static int BALLOON_ICON_HEIGHT = 32;
 261         private final static int BALLOON_TRAY_ICON_INDENT = 0;
 262         private final static Color BALLOON_CAPTION_BACKGROUND_COLOR = new Color(200, 200 ,255);
 263         private final static Font BALLOON_CAPTION_FONT = new Font(Font.DIALOG, Font.BOLD, 12);
 264 
 265         private Panel mainPanel = new Panel();
 266         private Panel captionPanel = new Panel();
 267         private Label captionLabel = new Label("");
 268         private Button closeButton = new Button("X");
 269         private Panel textPanel = new Panel();
 270         private XTrayIconPeer.IconCanvas iconCanvas = new XTrayIconPeer.IconCanvas(BALLOON_ICON_WIDTH, BALLOON_ICON_HEIGHT);
 271         private Label[] lineLabels = new Label[BALLOON_WORD_LINE_MAX_COUNT];
 272         private ActionPerformer ap = new ActionPerformer();
 273 
 274         private Image iconImage;
 275         private Image errorImage;
 276         private Image warnImage;
 277         private Image infoImage;
 278         private boolean gtkImagesLoaded;
 279 
 280         private Displayer displayer = new Displayer();
 281 
 282         public Balloon(Frame parent, Object target, LiveArguments liveArguments) {
 283             super(parent, new Color(90, 80 ,190));
 284             this.liveArguments = liveArguments;
 285             this.target = target;
 286 
 287             XTrayIconPeer.suppressWarningString(this);
 288 
 289             setCloser(new Runnable() {
 290                     public void run() {
 291                         if (textPanel != null) {
 292                             textPanel.removeAll();
 293                             textPanel.setSize(0, 0);
 294                             iconCanvas.setSize(0, 0);
 295                             XToolkit.awtLock();
 296                             try {
 297                                 displayer.isDisplayed = false;
 298                                 XToolkit.awtLockNotifyAll();
 299                             } finally {
 300                                 XToolkit.awtUnlock();
 301                             }
 302                         }
 303                     }
 304                 }, BALLOON_SHOW_TIME);
 305 
 306             add(mainPanel);
 307 
 308             captionLabel.setFont(BALLOON_CAPTION_FONT);
 309             captionLabel.addMouseListener(ap);
 310 
 311             captionPanel.setLayout(new BorderLayout());
 312             captionPanel.add(captionLabel, BorderLayout.WEST);
 313             captionPanel.add(closeButton, BorderLayout.EAST);
 314             captionPanel.setBackground(BALLOON_CAPTION_BACKGROUND_COLOR);
 315             captionPanel.addMouseListener(ap);
 316 
 317             closeButton.addActionListener(new ActionListener() {
 318                     public void actionPerformed(ActionEvent e) {
 319                         hide();
 320                     }
 321                 });
 322 
 323             mainPanel.setLayout(new BorderLayout());
 324             mainPanel.setBackground(Color.white);
 325             mainPanel.add(captionPanel, BorderLayout.NORTH);
 326             mainPanel.add(iconCanvas, BorderLayout.WEST);
 327             mainPanel.add(textPanel, BorderLayout.CENTER);
 328 
 329             iconCanvas.addMouseListener(ap);
 330 
 331             for (int i = 0; i < BALLOON_WORD_LINE_MAX_COUNT; i++) {
 332                 lineLabels[i] = new Label();
 333                 lineLabels[i].addMouseListener(ap);
 334                 lineLabels[i].setBackground(Color.white);
 335             }
 336 
 337             displayer.start();
 338         }
 339 
 340         public void display(String caption, String text, String messageType) {
 341             if (!gtkImagesLoaded) {
 342                 loadGtkImages();
 343             }
 344             displayer.display(caption, text, messageType);
 345         }
 346 
 347         private void _display(String caption, String text, String messageType) {
 348             captionLabel.setText(caption);
 349 
 350             BreakIterator iter = BreakIterator.getWordInstance();
 351             if (text != null) {
 352                 iter.setText(text);
 353                 int start = iter.first(), end;
 354                 int nLines = 0;
 355 
 356                 do {
 357                     end = iter.next();
 358 
 359                     if (end == BreakIterator.DONE ||
 360                         text.substring(start, end).length() >= 50)
 361                     {
 362                         lineLabels[nLines].setText(text.substring(start, end == BreakIterator.DONE ?
 363                                                                   iter.last() : end));
 364                         textPanel.add(lineLabels[nLines++]);
 365                         start = end;
 366                     }
 367                     if (nLines == BALLOON_WORD_LINE_MAX_COUNT) {
 368                         if (end != BreakIterator.DONE) {
 369                             lineLabels[nLines - 1].setText(
 370                                 new String(lineLabels[nLines - 1].getText() + " ..."));
 371                         }
 372                         break;
 373                     }
 374                 } while (end != BreakIterator.DONE);
 375 
 376 
 377                 textPanel.setLayout(new GridLayout(nLines, 1));
 378             }
 379 
 380             if ("ERROR".equals(messageType)) {
 381                 iconImage = errorImage;
 382             } else if ("WARNING".equals(messageType)) {
 383                 iconImage = warnImage;
 384             } else if ("INFO".equals(messageType)) {
 385                 iconImage = infoImage;
 386             } else {
 387                 iconImage = null;
 388             }
 389 
 390             if (iconImage != null) {
 391                 Dimension tpSize = textPanel.getSize();
 392                 iconCanvas.setSize(BALLOON_ICON_WIDTH, (BALLOON_ICON_HEIGHT > tpSize.height ?
 393                                                         BALLOON_ICON_HEIGHT : tpSize.height));
 394                 iconCanvas.validate();
 395             }
 396 
 397             SunToolkit.executeOnEventHandlerThread(target, new Runnable() {
 398                     public void run() {
 399                         if (liveArguments.isDisposed()) {
 400                             return;
 401                         }
 402                         Point parLoc = getParent().getLocationOnScreen();
 403                         Dimension parSize = getParent().getSize();
 404                         show(new Point(parLoc.x + parSize.width/2, parLoc.y + parSize.height/2),
 405                              BALLOON_TRAY_ICON_INDENT);
 406                         if (iconImage != null) {
 407                             iconCanvas.updateImage(iconImage); // call it after the show(..) above
 408                         }
 409                     }
 410                 });
 411         }
 412 
 413         public void dispose() {
 414             displayer.interrupt();
 415             super.dispose();
 416         }
 417 
 418         private void loadGtkImages() {
 419             if (!gtkImagesLoaded) {
 420                 errorImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty(
 421                     "gtk.icon.gtk-dialog-error.6.rtl");
 422                 warnImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty(
 423                     "gtk.icon.gtk-dialog-warning.6.rtl");
 424                 infoImage = (Image)Toolkit.getDefaultToolkit().getDesktopProperty(
 425                     "gtk.icon.gtk-dialog-info.6.rtl");
 426                 gtkImagesLoaded = true;
 427             }
 428         }
 429 
 430         private class ActionPerformer extends MouseAdapter {
 431             public void mouseClicked(MouseEvent e) {
 432                 // hide the balloon by any click
 433                 hide();
 434                 if (e.getButton() == MouseEvent.BUTTON1) {
 435                     ActionEvent aev = new ActionEvent(target, ActionEvent.ACTION_PERFORMED,
 436                                                       liveArguments.getActionCommand(),
 437                                                       e.getWhen(), e.getModifiers());
 438                     XToolkit.postEvent(XToolkit.targetToAppContext(aev.getSource()), aev);
 439                 }
 440             }
 441         }
 442 
 443         private class Displayer extends Thread {
 444             final int MAX_CONCURRENT_MSGS = 10;
 445 
 446             ArrayBlockingQueue<Message> messageQueue = new ArrayBlockingQueue<Message>(MAX_CONCURRENT_MSGS);
 447             boolean isDisplayed;
 448 
 449             Displayer() {
 450                 setDaemon(true);
 451             }
 452 
 453             public void run() {
 454                 while (true) {
 455                     Message msg = null;
 456                     try {
 457                         msg = messageQueue.take();
 458                     } catch (InterruptedException e) {
 459                         return;
 460                     }
 461 
 462                     /*
 463                      * Wait till the previous message is displayed if any
 464                      */
 465                     XToolkit.awtLock();
 466                     try {
 467                         while (isDisplayed) {
 468                             try {
 469                                 XToolkit.awtLockWait();
 470                             } catch (InterruptedException e) {
 471                                 return;
 472                             }
 473                         }
 474                         isDisplayed = true;
 475                     } finally {
 476                         XToolkit.awtUnlock();
 477                     }
 478                     _display(msg.caption, msg.text, msg.messageType);
 479                 }
 480             }
 481 
 482             void display(String caption, String text, String messageType) {
 483                 messageQueue.offer(new Message(caption, text, messageType));
 484             }
 485         }
 486 
 487         private static class Message {
 488             String caption, text, messageType;
 489 
 490             Message(String caption, String text, String messageType) {
 491                 this.caption = caption;
 492                 this.text = text;
 493                 this.messageType = messageType;
 494             }
 495         }
 496     }
 497 }
 498