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