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