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
26package sun.awt.X11;
27
28import java.awt.BorderLayout;
29import java.awt.Button;
30import java.awt.Color;
31import java.awt.Component;
32import java.awt.Container;
33import java.awt.Dimension;
34import java.awt.Font;
35import java.awt.Frame;
36import java.awt.GridLayout;
37import java.awt.Image;
38import java.awt.Insets;
39import java.awt.Label;
40import java.awt.MouseInfo;
41import java.awt.Panel;
42import java.awt.Point;
43import java.awt.Rectangle;
44import java.awt.Toolkit;
45import java.awt.Window;
46import java.awt.event.ActionEvent;
47import java.awt.event.ActionListener;
48import java.awt.event.MouseAdapter;
49import java.awt.event.MouseEvent;
50import java.security.AccessController;
51import java.security.PrivilegedAction;
52import java.text.BreakIterator;
53import java.util.concurrent.ArrayBlockingQueue;
54
55import 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
61public 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
522