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