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