001/*
002 *  jDTAUS Core Utilities
003 *  Copyright (C) 2005 Christian Schulte
004 *  <cs@schulte.it>
005 *
006 *  This library is free software; you can redistribute it and/or
007 *  modify it under the terms of the GNU Lesser General Public
008 *  License as published by the Free Software Foundation; either
009 *  version 2.1 of the License, or any later version.
010 *
011 *  This library is distributed in the hope that it will be useful,
012 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 *  Lesser General Public License for more details.
015 *
016 *  You should have received a copy of the GNU Lesser General Public
017 *  License along with this library; if not, write to the Free Software
018 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
019 *
020 */
021package org.jdtaus.core.text.util;
022
023import java.awt.Component;
024import java.awt.Dimension;
025import java.awt.GraphicsEnvironment;
026import java.awt.GridBagConstraints;
027import java.awt.GridBagLayout;
028import java.awt.HeadlessException;
029import java.awt.Insets;
030import java.beans.PropertyChangeEvent;
031import java.beans.PropertyChangeListener;
032import java.lang.reflect.InvocationTargetException;
033import java.util.Locale;
034import javax.swing.JDialog;
035import javax.swing.JLabel;
036import javax.swing.JOptionPane;
037import javax.swing.JPanel;
038import javax.swing.SwingConstants;
039import javax.swing.SwingUtilities;
040import javax.swing.plaf.basic.BasicHTML;
041import javax.swing.text.View;
042import org.jdtaus.core.container.ContainerFactory;
043import org.jdtaus.core.logging.spi.Logger;
044import org.jdtaus.core.text.MessageEvent;
045import org.jdtaus.core.text.MessageListener;
046
047/**
048 * {@code MessageListener} displaying messages using Swing's {@code JOptionPane}.
049 * <p>This implementation displays a dialog for each {@code MessageEvent} this {@code MessageListener} is notified
050 * about. Since a {@code MessageEvent} can hold multiple messages a maximum number of messages to display per event
051 * can be specified by property {@code maximumMessages} (defaults to 25).</p>
052 *
053 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
054 * @version $JDTAUS: SwingMessagePane.java 8786 2012-12-03 02:09:05Z schulte $
055 *
056 * @see #onMessage(MessageEvent)
057 */
058public final class SwingMessagePane implements MessageListener
059{
060    //--Dependencies------------------------------------------------------------
061
062// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
063    // This section is managed by jdtaus-container-mojo.
064
065    /**
066     * Gets the configured <code>Logger</code> implementation.
067     *
068     * @return The configured <code>Logger</code> implementation.
069     */
070    private Logger getLogger()
071    {
072        return (Logger) ContainerFactory.getContainer().
073            getDependency( this, "Logger" );
074
075    }
076
077    /**
078     * Gets the configured <code>Locale</code> implementation.
079     *
080     * @return The configured <code>Locale</code> implementation.
081     */
082    private Locale getLocale()
083    {
084        return (Locale) ContainerFactory.getContainer().
085            getDependency( this, "Locale" );
086
087    }
088
089// </editor-fold>//GEN-END:jdtausDependencies
090
091    //------------------------------------------------------------Dependencies--
092    //--Properties--------------------------------------------------------------
093
094// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausProperties
095    // This section is managed by jdtaus-container-mojo.
096
097    /**
098     * Gets the value of property <code>defaultResizable</code>.
099     *
100     * @return Default resizability of the message pane.
101     */
102    private java.lang.Boolean isDefaultResizable()
103    {
104        return (java.lang.Boolean) ContainerFactory.getContainer().
105            getProperty( this, "defaultResizable" );
106
107    }
108
109    /**
110     * Gets the value of property <code>defaultMaximumMessages</code>.
111     *
112     * @return Default maximum number of messages displayed per event.
113     */
114    private java.lang.Integer getDefaultMaximumMessages()
115    {
116        return (java.lang.Integer) ContainerFactory.getContainer().
117            getProperty( this, "defaultMaximumMessages" );
118
119    }
120
121    /**
122     * Gets the value of property <code>defaultColumns</code>.
123     *
124     * @return Default number of columns the preferred width of the message pane is computed with.
125     */
126    private java.lang.Integer getDefaultColumns()
127    {
128        return (java.lang.Integer) ContainerFactory.getContainer().
129            getProperty( this, "defaultColumns" );
130
131    }
132
133// </editor-fold>//GEN-END:jdtausProperties
134
135    //--------------------------------------------------------------Properties--
136    //--MessageListener---------------------------------------------------------
137
138    /**
139     * {@inheritDoc}
140     * <p>This method uses Swing's {@link JOptionPane} to display messages given by the event. It will block the event
141     * dispatch thread until the user confirms the message pane.</p>
142     *
143     * @param event The event holding messages.
144     */
145    public void onMessage( final MessageEvent event )
146    {
147        if ( event != null )
148        {
149            try
150            {
151                final Runnable r = new Runnable()
152                {
153
154                    public void run()
155                    {
156                        final JPanel panel = new JPanel();
157                        panel.setLayout( new GridBagLayout() );
158
159                        for ( int i = 0, s0 = event.getMessages().length; i < s0 && i < getMaximumMessages(); i++ )
160                        {
161                            String text = event.getMessages()[i].getText( getLocale() );
162
163                            if ( !BasicHTML.isHTMLString( text ) )
164                            {
165                                text = "<html>" + HtmlEntities.escapeHtml( text ) + "</html>";
166                            }
167
168                            final JLabel label = new HtmlLabel( getColumns() );
169                            label.setText( text );
170
171                            final GridBagConstraints c = new GridBagConstraints();
172                            c.anchor = GridBagConstraints.NORTHWEST;
173                            c.fill = GridBagConstraints.BOTH;
174                            c.gridheight = 1;
175                            c.gridwidth = 1;
176                            c.gridx = 0;
177                            c.gridy = i;
178                            c.weightx = 1.0D;
179
180                            panel.add( label, c );
181
182                            if ( !( i + 1 < s0 && i + 1 < getMaximumMessages() ) )
183                            {
184                                final JPanel p = new JPanel();
185                                c.anchor = GridBagConstraints.SOUTH;
186                                c.weighty = 1.0D;
187                                c.gridy = i + 1;
188                                panel.add( p, c );
189                            }
190                        }
191
192                        JOptionPane optionPane = null;
193                        JDialog dialog = null;
194
195                        switch ( event.getType() )
196                        {
197                            case MessageEvent.INFORMATION:
198                                optionPane = new JOptionPane();
199                                optionPane.setMessage( panel );
200                                optionPane.setMessageType( JOptionPane.INFORMATION_MESSAGE );
201                                dialog = optionPane.createDialog( getParent(), getInformationMessage( getLocale() ) );
202                                dialog.setResizable( isResizable() );
203                                dialog.setVisible( true );
204                                dialog.dispose();
205                                break;
206
207                            case MessageEvent.NOTIFICATION:
208                                optionPane = new JOptionPane();
209                                optionPane.setMessage( panel );
210                                optionPane.setMessageType( JOptionPane.INFORMATION_MESSAGE );
211                                dialog = optionPane.createDialog( getParent(), getNotificationMessage( getLocale() ) );
212                                dialog.setResizable( isResizable() );
213                                dialog.setVisible( true );
214                                dialog.dispose();
215                                break;
216
217                            case MessageEvent.WARNING:
218                                optionPane = new JOptionPane();
219                                optionPane.setMessage( panel );
220                                optionPane.setMessageType( JOptionPane.WARNING_MESSAGE );
221                                dialog = optionPane.createDialog( getParent(), getWarningMessage( getLocale() ) );
222                                dialog.setResizable( isResizable() );
223                                dialog.setVisible( true );
224                                dialog.dispose();
225                                break;
226
227                            case MessageEvent.ERROR:
228                                optionPane = new JOptionPane();
229                                optionPane.setMessage( panel );
230                                optionPane.setMessageType( JOptionPane.ERROR_MESSAGE );
231                                dialog = optionPane.createDialog( getParent(), getErrorMessage( getLocale() ) );
232                                dialog.setResizable( isResizable() );
233                                dialog.setVisible( true );
234                                dialog.dispose();
235                                break;
236
237                            default:
238                                getLogger().warn( getUnknownMessageEventTypeMessage(
239                                    getLocale(), new Integer( event.getType() ) ) );
240
241                        }
242                    }
243
244                };
245
246                if ( SwingUtilities.isEventDispatchThread() )
247                {
248                    r.run();
249                }
250                else
251                {
252                    SwingUtilities.invokeAndWait( r );
253                }
254            }
255            catch ( final InterruptedException e )
256            {
257                this.getLogger().error( e );
258            }
259            catch ( final InvocationTargetException e )
260            {
261                this.getLogger().error( e );
262            }
263        }
264    }
265
266    //---------------------------------------------------------MessageListener--
267    //--SwingMessagePane--------------------------------------------------------
268
269    /** The current parent component to use when displaying messages. */
270    private Component parent;
271
272    /** Maximum number of messages displayed per event. */
273    private Integer maximumMessages;
274
275    /** Number of columns the preferred width of the message pane is computed with. */
276    private Integer columns;
277
278    /** Flag indicating the message pane is resizable. */
279    private Boolean resizable;
280
281    /**
282     * Creates a new {@code SwingMessagePane} instance taking the parent component to use when displaying messages.
283     *
284     * @param parent The parent component to use when displaying messages.
285     *
286     * @throws NullPointerException if {@code parent} is {@code null}.
287     * @throws HeadlessException if this class is used in an environment not providing a keyboard, display, or mouse.
288     */
289    public SwingMessagePane( final Component parent )
290    {
291        this( parent, Integer.MIN_VALUE, Integer.MIN_VALUE, false );
292    }
293
294    /**
295     * Creates a new {@code SwingMessagePane} instance taking the parent component to use when displaying messages and
296     * the maximum number of messages displayed per event.
297     *
298     * @param parent The parent component to use when displaying messages.
299     * @param maximumMessages Maximum number of messages displayed per event.
300     *
301     * @throws NullPointerException if {@code parent} is {@code null}.
302     * @throws HeadlessException if this class is used in an environment not providing a keyboard, display, or mouse.
303     */
304    public SwingMessagePane( final Component parent, final int maximumMessages )
305    {
306        this( parent, maximumMessages, Integer.MIN_VALUE, false );
307    }
308
309    /**
310     * Creates a new {@code SwingMessagePane} instance taking the parent component to use when displaying messages, the
311     * maximum number of messages displayed per event and a number of columns to compute the preferred width of the
312     * message pane with.
313     *
314     * @param parent The parent component to use when displaying messages.
315     * @param maximumMessages Maximum number of messages displayed per event.
316     * @param columns Number of columns the preferred width of the message pane should be computed with.
317     *
318     * @throws NullPointerException if {@code parent} is {@code null}.
319     * @throws HeadlessException if this class is used in an environment not providing a keyboard, display, or mouse.
320     *
321     * @since 1.10
322     */
323    public SwingMessagePane( final Component parent, final int maximumMessages, final int columns )
324    {
325        this( parent, maximumMessages, columns, false );
326    }
327
328    /**
329     * Creates a new {@code SwingMessagePane} instance taking the parent component to use when displaying messages, the
330     * maximum number of messages displayed per event, a number of columns to compute the preferred width of the
331     * message pane with and a flag indicating the message pane is resizable.
332     *
333     * @param parent The parent component to use when displaying messages.
334     * @param maximumMessages Maximum number of messages displayed per event.
335     * @param columns Number of columns the preferred width of the message pane should be computed with.
336     * @param resizable {@code true}, if the message pane should be resizable; {@code false} if the message pane should
337     * have a fixed size.
338     *
339     * @throws NullPointerException if {@code parent} is {@code null}.
340     * @throws HeadlessException if this class is used in an environment not providing a keyboard, display, or mouse.
341     *
342     * @since 1.11
343     */
344    public SwingMessagePane( final Component parent, final int maximumMessages, final int columns,
345                             final boolean resizable )
346    {
347        if ( parent == null )
348        {
349            throw new NullPointerException( "parent" );
350        }
351
352        if ( GraphicsEnvironment.isHeadless() )
353        {
354            throw new HeadlessException();
355        }
356
357        this.parent = parent;
358
359        if ( maximumMessages > 0 )
360        {
361            this.maximumMessages = new Integer( maximumMessages );
362        }
363        if ( columns > 0 )
364        {
365            this.columns = new Integer( columns );
366        }
367
368        this.resizable = Boolean.valueOf( resizable );
369    }
370
371    /**
372     * Gets the parent component for any message displays.
373     *
374     * @return the parent component for any message displays.
375     */
376    public Component getParent()
377    {
378        return this.parent;
379    }
380
381    /**
382     * Sets the parent component used by any message displays.
383     *
384     * @param parent the parent component used by any message displays.
385     *
386     * @throws NullPointerException if {@code parent} is {@code null}.
387     */
388    public void setParent( final Component parent )
389    {
390        if ( parent == null )
391        {
392            throw new NullPointerException( "parent" );
393        }
394
395        this.parent = parent;
396    }
397
398    /**
399     * Gets the maximum number of messages displayed per event.
400     *
401     * @return the maximum number of messages displayed per event.
402     */
403    public int getMaximumMessages()
404    {
405        if ( this.maximumMessages == null )
406        {
407            this.maximumMessages = this.getDefaultMaximumMessages();
408        }
409
410        return this.maximumMessages.intValue();
411    }
412
413    /**
414     * Gets the number of columns the preferred width of the message pane is computed with.
415     *
416     * @return The number of columns the preferred width of the message pane is computed with.
417     *
418     * @since 1.10
419     */
420    public int getColumns()
421    {
422        if ( this.columns == null )
423        {
424            this.columns = this.getDefaultColumns();
425        }
426
427        return this.columns.intValue();
428    }
429
430    /**
431     * Gets a flag indicating the message pane is resizable.
432     *
433     * @return {@code true}, if the message pane is resizable; {@code false} if the message pane is not resizable.
434     *
435     * @since 1.11
436     */
437    public boolean isResizable()
438    {
439        if ( this.resizable == null )
440        {
441            this.resizable = this.isDefaultResizable();
442        }
443
444        return this.resizable.booleanValue();
445    }
446
447    //--------------------------------------------------------SwingMessagePane--
448    //--Messages----------------------------------------------------------------
449
450// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
451    // This section is managed by jdtaus-container-mojo.
452
453    /**
454     * Gets the text of message <code>unknownMessageEventType</code>.
455     * <blockquote><pre>Meldung unbekannten Typs {0,number} ignoriert.</pre></blockquote>
456     * <blockquote><pre>Ignored message event of unknown type {0,number}.</pre></blockquote>
457     *
458     * @param locale The locale of the message instance to return.
459     * @param unknownEventType The unknown event type.
460     *
461     * @return Message stating that an unknown message event got ignored.
462     */
463    private String getUnknownMessageEventTypeMessage( final Locale locale,
464            final java.lang.Number unknownEventType )
465    {
466        return ContainerFactory.getContainer().
467            getMessage( this, "unknownMessageEventType", locale,
468                new Object[]
469                {
470                    unknownEventType
471                });
472
473    }
474
475    /**
476     * Gets the text of message <code>information</code>.
477     * <blockquote><pre>Information</pre></blockquote>
478     * <blockquote><pre>Information</pre></blockquote>
479     *
480     * @param locale The locale of the message instance to return.
481     *
482     * @return Information title.
483     */
484    private String getInformationMessage( final Locale locale )
485    {
486        return ContainerFactory.getContainer().
487            getMessage( this, "information", locale, null );
488
489    }
490
491    /**
492     * Gets the text of message <code>notification</code>.
493     * <blockquote><pre>Hinweis</pre></blockquote>
494     * <blockquote><pre>Notification</pre></blockquote>
495     *
496     * @param locale The locale of the message instance to return.
497     *
498     * @return Notification title.
499     */
500    private String getNotificationMessage( final Locale locale )
501    {
502        return ContainerFactory.getContainer().
503            getMessage( this, "notification", locale, null );
504
505    }
506
507    /**
508     * Gets the text of message <code>warning</code>.
509     * <blockquote><pre>Warnung</pre></blockquote>
510     * <blockquote><pre>Warning</pre></blockquote>
511     *
512     * @param locale The locale of the message instance to return.
513     *
514     * @return Warning title.
515     */
516    private String getWarningMessage( final Locale locale )
517    {
518        return ContainerFactory.getContainer().
519            getMessage( this, "warning", locale, null );
520
521    }
522
523    /**
524     * Gets the text of message <code>error</code>.
525     * <blockquote><pre>Fehler</pre></blockquote>
526     * <blockquote><pre>Error</pre></blockquote>
527     *
528     * @param locale The locale of the message instance to return.
529     *
530     * @return Error title.
531     */
532    private String getErrorMessage( final Locale locale )
533    {
534        return ContainerFactory.getContainer().
535            getMessage( this, "error", locale, null );
536
537    }
538
539// </editor-fold>//GEN-END:jdtausMessages
540
541    //----------------------------------------------------------------Messages--
542}
543
544/**
545 * Extension to {@code JLabel} adding support for a preferred width based on a number of columns.
546 *
547 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
548 * @version $JDTAUS: SwingMessagePane.java 8786 2012-12-03 02:09:05Z schulte $
549 */
550class HtmlLabel extends JLabel
551{
552
553    /** Serial version UID for backwards compatibility with 1.10.x classes. */
554    private static final long serialVersionUID = -3022796716512336911L;
555
556    /** The number of columns the preferred width of the label is computed with. */
557    private int columns;
558
559    /**
560     * Creates a new {@code HtmlLabel} instance taking a number of columns to compute the preferred width of the label
561     * with.
562     *
563     * @param columns The number of columns to compute the preferred width of the label with.
564     */
565    HtmlLabel( final int columns )
566    {
567        super();
568        this.columns = columns;
569        this.setVerticalAlignment( SwingConstants.TOP );
570        this.setVerticalTextPosition( SwingConstants.TOP );
571
572        if ( columns > 0 )
573        {
574            this.addPropertyChangeListener( new PropertyChangeListener()
575            {
576
577                public void propertyChange( final PropertyChangeEvent evt )
578                {
579                    if ( ( "text".equals( evt.getPropertyName() )
580                           || "font".equals( evt.getPropertyName() )
581                           || "border".equals( evt.getPropertyName() ) )
582                         && getText() != null
583                         && BasicHTML.isHTMLString( getText() ) )
584                    {
585                        setPreferredSize( computePreferredSize() );
586                    }
587                }
588
589            } );
590        }
591    }
592
593    private Dimension computePreferredSize()
594    {
595        Dimension preferredSize = null;
596        final Insets insets = this.getInsets();
597        final int width = this.columns * this.getFontMetrics( this.getFont() ).charWidth( 'W' )
598                          + ( insets != null ? insets.left + insets.right : 0 );
599
600        final JLabel label = new JLabel( this.getText() );
601        final Object html = label.getClientProperty( BasicHTML.propertyKey );
602
603        if ( html instanceof View )
604        {
605            final View view = (View) html;
606            view.setSize( width, 0 );
607            preferredSize = new Dimension( (int) view.getPreferredSpan( View.X_AXIS ),
608                                           (int) view.getPreferredSpan( View.Y_AXIS ) );
609
610        }
611
612        return preferredSize;
613    }
614
615}