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