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