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