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