001/*
002 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved.
003 * 
004 * http://www.izforge.com/izpack/
005 * http://developer.berlios.de/projects/izpack/
006 * 
007 * Copyright 1997,2002 Elmar Grom
008 * 
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *     http://www.apache.org/licenses/LICENSE-2.0
014 *     
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 */
021
022package com.izforge.izpack.util;
023
024import java.awt.Color;
025import java.awt.Dimension;
026import java.awt.Font;
027import java.awt.FontMetrics;
028import java.awt.Graphics;
029import java.util.Vector;
030
031import javax.swing.JComponent;
032
033/*---------------------------------------------------------------------------*/
034/**
035 * <BR>
036 * <code>MultiLineLabel</code> may be used in place of javax.swing.JLabel. <BR>
037 * <BR>
038 * This class implements a component that is capable of displaying multiple lines of text. Line
039 * breaks are inserted automatically whenever a line of text extends beyond the predefined maximum
040 * line length. Line breaks will only be inserted between words, except where a single word is
041 * longer than the maximum line length. Line breaks may be forced at any location in the text by
042 * inserting a newline (\n). White space that is not valuable (i.e. is placed at the beginning of a
043 * new line or at the very beginning or end of the text) is removed. <br>
044 * <br>
045 * <b>Note:</b> you can set the maximum width of the label either through one of the constructors
046 * or you can call <code>setMaxWidth()</code> explicitly. If this is not set,
047 * <code>MultiLineLabel</code> will derive its width from the parent component.
048 * 
049 * @version 0.0.1 / 05-15-97
050 * @version 1.0 / 04-13-02
051 * @author Elmar Grom
052 */
053/*---------------------------------------------------------------------------*
054 * Reviving some old code here that was written before there was swing.
055 * The original was written to work with awt. I had to do some masaging to
056 * make it a JComponent and I hope it behaves like a reasonably good mannered
057 * swing component.
058 *---------------------------------------------------------------------------*/
059public class MultiLineLabel extends JComponent
060{
061
062    /**
063     * 
064     */
065    private static final long serialVersionUID = 4051045255031894837L;
066
067    public static final int LEFT = 0; // alignment constants
068
069    public static final int CENTER = 1;
070
071    public static final int RIGHT = 2;
072
073    public static final int DEFAULT_MARGIN = 10;
074
075    public static final int DEFAULT_ALIGN = LEFT;
076
077    public static final int LEAST_ALLOWED = 200; // default setting for
078
079    // maxAllowed
080
081    private static final int FOUND = 0; // constants for string search.
082
083    private static final int NOT_FOUND = 1;
084
085    private static final int NOT_DONE = 0;
086
087    private static final int DONE = 1;
088
089    private static final char[] WHITE_SPACE = { ' ', '\n', '\t'};
090
091    private static final char[] SPACES = { ' ', '\t'};
092
093    private static final char NEW_LINE = '\n';
094
095    protected Vector line = new Vector();// text lines to display
096
097    protected String labelText; // text lines to display
098
099    protected int numLines; // the number of lines
100
101    protected int marginHeight; // top and bottom margins
102
103    protected int marginWidth; // left and right margins
104
105    protected int lineHeight; // total height of the font
106
107    protected int lineAscent; // font height above the baseline
108
109    protected int lineDescent; // font hight below the baseline
110
111    protected int[] lineWidth; // width of each line
112
113    protected int maxWidth; // width of the widest line
114
115    private int maxAllowed = LEAST_ALLOWED; // max width allowed to use
116
117    private boolean maxAllowedSet = false; // signals if the max allowed width
118
119    // has been explicitly set
120
121    protected int alignment = LEFT; // default text alignment
122
123    /*-------------------------------------------------------------------*/
124    /**
125     * Constructor
126     * 
127     * @param text the text to be displayed
128     * @param horMargin the horizontal margin for the label
129     * @param vertMargin the vertical margin for the label
130     * @param maxWidth the maximum allowed width of the text
131     * @param justify the text alignment for the label
132     */
133    /*-------------------------------------------------------------------*
134     * <detailed description / implementation details if applicable>
135     *-------------------------------------------------------------------*/
136    public MultiLineLabel(String text, int horMargin, int vertMargin, int maxWidth, int justify)
137    {
138        this.labelText = text;
139        this.marginWidth = horMargin;
140        this.marginHeight = vertMargin;
141        this.maxAllowed = maxWidth;
142        this.maxAllowedSet = true;
143        this.alignment = justify;
144    }
145
146    /*-------------------------------------------------------------------*/
147    /**
148     * Constructor using default max-width and alignment.
149     * 
150     * @param label the text to be displayed
151     * @param marginWidth the horizontal margin for the label
152     * @param marginHeight the vertical margin for the label
153     */
154    /*-------------------------------------------------------------------*
155     * <detailed description / implementation details if applicable>
156     *-------------------------------------------------------------------*/
157    public MultiLineLabel(String label, int marginWidth, int marginHeight)
158    {
159        this.labelText = label;
160        this.marginWidth = marginWidth;
161        this.marginHeight = marginHeight;
162    }
163
164    /*-------------------------------------------------------------------*/
165    /**
166     * Constructor using default max-width, and margin.
167     * 
168     * @param label the text to be displayed
169     * @param alignment the text alignment for the label
170     */
171    /*-------------------------------------------------------------------*
172     * <detailed description / implementation details if applicable>
173     *-------------------------------------------------------------------*/
174    public MultiLineLabel(String label, int alignment)
175    {
176        this.labelText = label;
177        this.alignment = alignment;
178    }
179
180    /*-------------------------------------------------------------------*/
181    /**
182     * Constructor using default max-width, alignment, and margin.
183     * 
184     * @param label the text to be displayed
185     */
186    /*-------------------------------------------------------------------*
187     * <detailed description / implementation details if applicable>
188     *-------------------------------------------------------------------*/
189    public MultiLineLabel(String label)
190    {
191        this.labelText = label;
192    }
193
194    /*-------------------------------------------------------------------*/
195    /**
196     * This method searches the target string for occurences of any of the characters in the source
197     * string. The return value is the position of the first hit. Based on the mode parameter the
198     * hit position is either the position where any of the source characters first was found or the
199     * first position where none of the source characters where found.
200     * 
201     * 
202     * @return position of the first occurence
203     * @param target the text to be searched
204     * @param start the start position for the search
205     * @param source the list of characters to be searched for
206     * @param mode the search mode FOUND = reports first found NOT_FOUND = reports first not found
207     */
208    /*-------------------------------------------------------------------*
209     * <detailed description / implementation details if applicable>
210     *-------------------------------------------------------------------*/
211    int getPosition(String target, int start, char[] source, int mode)
212    {
213        int status;
214        int position;
215        int scan;
216        int targetEnd;
217        int sourceLength;
218        char temp;
219
220        targetEnd = (target.length() - 1);
221        sourceLength = source.length;
222        position = start;
223
224        if (mode == FOUND)
225        {
226            status = NOT_DONE;
227            while (status != DONE)
228            {
229                position++;
230                if (!(position < targetEnd)) // end of string reached, the
231                // next
232                { // statement would cause a runtime error
233                    return (targetEnd);
234                }
235                temp = target.charAt(position);
236                for (scan = 0; scan < sourceLength; scan++) // walk through the
237                // source
238                { // string and compare each char
239                    if (source[scan] == temp)
240                    {
241                        status = DONE;
242                    }
243                }
244            }
245            return (position);
246        }
247        else if (mode == NOT_FOUND)
248        {
249            status = NOT_DONE;
250            while (status != DONE)
251            {
252                position++;
253                if (!(position < targetEnd)) // end of string reached, the
254                // next
255                { // statement would cause a runtime error
256                    return (targetEnd);
257                }
258                temp = target.charAt(position);
259                status = DONE;
260                for (scan = 0; scan < sourceLength; scan++) // walk through the
261                // source
262                { // string and compare each char
263                    if (source[scan] == temp)
264                    {
265                        status = NOT_DONE;
266                    }
267                }
268            }
269            return (position);
270        }
271        return (0);
272    }
273
274    /*-------------------------------------------------------------------*/
275    /**
276     * This method scans the input string until the max allowed width is reached. The return value
277     * indicates the position just before this happens.
278     * 
279     * 
280     * @return position character position just before the string is too long
281     * @param word word to break
282     */
283    /*-------------------------------------------------------------------*
284     * <detailed description / implementation details if applicable>
285     *-------------------------------------------------------------------*/
286    int breakWord(String word, FontMetrics fm)
287    {
288        int width;
289        int currentPos;
290        int endPos;
291
292        width = 0;
293        currentPos = 0;
294        endPos = word.length() - 1;
295
296        // make sure we don't end up with a negative position
297        if (endPos <= 0) { return (currentPos); }
298        // seek the position where the word first is longer than allowed
299        while ((width < maxAllowed) && (currentPos < endPos))
300        {
301            currentPos++;
302            width = fm.stringWidth(labelText.substring(0, currentPos));
303        }
304        // adjust to get the chatacter just before (this should make it a bit
305        // shorter than allowed!)
306        if (currentPos != endPos)
307        {
308            currentPos--;
309        }
310        return (currentPos);
311    }
312
313    /*-------------------------------------------------------------------*/
314    /**
315     * This method breaks the label text up into multiple lines of text. Line breaks are established
316     * based on the maximum available space. A new line is started whenever a line break is
317     * encountered, even if the permissible length is not yet reached. Words are broken only if a
318     * single word happens to be longer than one line.
319     */
320    /*-------------------------------------------------------------------*/
321    private void divideLabel()
322    {
323        int width;
324        int startPos;
325        int currentPos;
326        int lastPos;
327        int endPos;
328
329        line.clear();
330        FontMetrics fm = this.getFontMetrics(this.getFont());
331
332        startPos = 0;
333        currentPos = startPos;
334        lastPos = currentPos;
335        endPos = (labelText.length() - 1);
336
337        while (currentPos < endPos)
338        {
339            width = 0;
340            // ----------------------------------------------------------------
341            // find the first substring that occupies more than the granted
342            // space.
343            // Break at the end of the string or a line break
344            // ----------------------------------------------------------------
345            while ((width < maxAllowed) && (currentPos < endPos)
346                    && (labelText.charAt(currentPos) != NEW_LINE))
347            {
348                lastPos = currentPos;
349                currentPos = getPosition(labelText, currentPos, WHITE_SPACE, FOUND);
350                width = fm.stringWidth(labelText.substring(startPos, currentPos));
351            }
352            // ----------------------------------------------------------------
353            // if we have a line break we want to copy everything up to
354            // currentPos
355            // ----------------------------------------------------------------
356            if (labelText.charAt(currentPos) == NEW_LINE)
357            {
358                lastPos = currentPos;
359            }
360            // ----------------------------------------------------------------
361            // if we are at the end of the string we want to copy everything up
362            // to
363            // the last character. Since there seems to be a problem to get the
364            // last
365            // character if the substring definition ends at the very last
366            // character
367            // we have to call a different substring function than normal.
368            // ----------------------------------------------------------------
369            if (currentPos == endPos && width <= maxAllowed)
370            {
371                lastPos = currentPos;
372                String s = labelText.substring(startPos);
373                line.addElement(s);
374            }
375            // ----------------------------------------------------------------
376            // in all other cases copy the substring that we have found to fit
377            // and
378            // add it as a new line of text to the line vector.
379            // ----------------------------------------------------------------
380            else
381            {
382                // ------------------------------------------------------------
383                // make sure it's not a single word. If so we must break it at
384                // the
385                // proper location.
386                // ------------------------------------------------------------
387                if (lastPos == startPos)
388                {
389                    lastPos = startPos + breakWord(labelText.substring(startPos, currentPos), fm);
390                }
391                String s = labelText.substring(startPos, lastPos);
392                line.addElement(s);
393            }
394
395            // ----------------------------------------------------------------
396            // seek for the end of the white space to cut out any unnecessary
397            // spaces
398            // and tabs and set the new start condition.
399            // ----------------------------------------------------------------
400            startPos = getPosition(labelText, lastPos, SPACES, NOT_FOUND);
401            currentPos = startPos;
402        }
403
404        numLines = line.size();
405        lineWidth = new int[numLines];
406    }
407
408    /*-------------------------------------------------------------------*/
409    /**
410     * This method finds the font size, each line width and the widest line.
411     * 
412     */
413    /*-------------------------------------------------------------------*/
414    protected void measure()
415    {
416        if (!maxAllowedSet)
417        {
418            maxAllowed = getParent().getSize().width;
419        }
420
421        // return if width is too small
422        if (maxAllowed < (20)) { return; }
423
424        FontMetrics fm = this.getFontMetrics(this.getFont());
425
426        // return if no font metrics available
427        if (fm == null) { return; }
428
429        divideLabel();
430
431        this.lineHeight = fm.getHeight();
432        this.lineDescent = fm.getDescent();
433        this.maxWidth = 0;
434
435        for (int i = 0; i < numLines; i++)
436        {
437            this.lineWidth[i] = fm.stringWidth((String) this.line.elementAt(i));
438            if (this.lineWidth[i] > this.maxWidth)
439            {
440                this.maxWidth = this.lineWidth[i];
441            }
442        }
443    }
444
445    /*-------------------------------------------------------------------*/
446    /**
447     * This method draws the label.
448     * 
449     * @param graphics the device context
450     */
451    /*-------------------------------------------------------------------*/
452    public void paint(Graphics graphics)
453    {
454        int x;
455        int y;
456
457        measure();
458        Dimension d = this.getSize();
459
460        y = lineAscent + (d.height - (numLines * lineHeight)) / 2;
461
462        for (int i = 0; i < numLines; i++)
463        {
464            y += lineHeight;
465            switch (alignment)
466            {
467            case LEFT:
468                x = marginWidth;
469                break;
470            case CENTER:
471                x = (d.width - lineWidth[i]) / 2;
472                break;
473            case RIGHT:
474                x = d.width - marginWidth - lineWidth[i];
475                break;
476            default:
477                x = (d.width - lineWidth[i]) / 2;
478            }
479            graphics.drawString((String) line.elementAt(i), x, y);
480        }
481    }
482
483    /*-------------------------------------------------------------------*/
484    /**
485     * This method may be used to set the label text
486     * 
487     * @param labelText the text to be displayed
488     */
489    /*-------------------------------------------------------------------*/
490    public void setText(String labelText)
491    {
492        this.labelText = labelText;
493        repaint();
494    }
495
496    /*-------------------------------------------------------------------*/
497    /**
498     * This method may be used to set the font that should be used to draw the label
499     * 
500     * @param font font to be used within the label
501     */
502    /*-------------------------------------------------------------------*/
503    public void setFont(Font font)
504    {
505        super.setFont(font);
506        repaint();
507    }
508
509    /*-------------------------------------------------------------------*/
510    /**
511     * This method may be used to set the color in which the text should be drawn
512     * 
513     * @param color the text color
514     */
515    /*-------------------------------------------------------------------*/
516    public void setColor(Color color)
517    {
518        super.setForeground(color);
519        repaint();
520    }
521
522    /*-------------------------------------------------------------------*/
523    /**
524     * This method may be used to set the text alignment for the label
525     * 
526     * @param alignment the alignment, possible values are LEFT, CENTER, RIGHT
527     */
528    /*-------------------------------------------------------------------*/
529    public void setJustify(int alignment)
530    {
531        this.alignment = alignment;
532        repaint();
533    }
534
535    /*-------------------------------------------------------------------*/
536    /**
537     * This method may be used to set the max allowed line width
538     * 
539     * @param width the max allowed line width in pixels
540     */
541    /*-------------------------------------------------------------------*/
542    public void setMaxWidth(int width)
543    {
544        this.maxAllowed = width;
545        this.maxAllowedSet = true;
546        repaint();
547    }
548
549    /*-------------------------------------------------------------------*/
550    /**
551     * This method may be used to set the horizontal margin
552     * 
553     * @param margin the margin to the left and to the right of the label
554     */
555    /*-------------------------------------------------------------------*/
556    public void setMarginWidth(int margin)
557    {
558        this.marginWidth = margin;
559        repaint();
560    }
561
562    /*-------------------------------------------------------------------*/
563    /**
564     * This method may be used to set the vertical margin for the label
565     * 
566     * @param margin the margin on the top and bottom of the label
567     */
568    /*-------------------------------------------------------------------*/
569    public void setMarginHeight(int margin)
570    {
571        this.marginHeight = margin;
572        repaint();
573    }
574
575    /*-------------------------------------------------------------------*/
576    /**
577     * Moves and resizes this component. The new location of the top-left corner is specified by
578     * <code>x</code> and <code>y</code>, and the new size is specified by <code>width</code>
579     * and <code>height</code>.
580     * 
581     * @param x The new x-coordinate of this component.
582     * @param y The new y-coordinate of this component.
583     * @param width The new width of this component.
584     * @param height The new height of this component.
585     */
586    /*-------------------------------------------------------------------*/
587    public void setBounds(int x, int y, int width, int height)
588    {
589        super.setBounds(x, y, width, height);
590        this.maxAllowed = width;
591        this.maxAllowedSet = true;
592    }
593
594    /*-------------------------------------------------------------------*/
595    /**
596     * This method may be used to retrieve the text alignment for the label
597     * 
598     * @return alignment the text alignment currently in use for the label
599     */
600    /*-------------------------------------------------------------------*/
601    public int getAlignment()
602    {
603        return (this.alignment);
604    }
605
606    /*-------------------------------------------------------------------*/
607    /**
608     * This method may be used to retrieve the horizontal margin for the label
609     * 
610     * @return marginWidth the margin currently in use to the left and right of the label
611     */
612    /*-------------------------------------------------------------------*/
613    public int getMarginWidth()
614    {
615        return (this.marginWidth);
616    }
617
618    /*-------------------------------------------------------------------*/
619    /**
620     * This method may be used to retrieve the vertical margin for the label
621     * 
622     * @return marginHeight the margin currently in use on the top and bottom of the label
623     */
624    /*-------------------------------------------------------------------*/
625    public int getMarginHeight()
626    {
627        return (this.marginHeight);
628    }
629
630    /*-------------------------------------------------------------------*/
631    /**
632     * This method is typically used by the layout manager, it reports the necessary space to
633     * display the label comfortably.
634     */
635    /*-------------------------------------------------------------------*/
636    public Dimension getPreferredSize()
637    {
638        measure();
639        return (new Dimension(maxAllowed, (numLines * (lineHeight + lineAscent + lineDescent))
640                + (2 * marginHeight)));
641    }
642
643    /*-------------------------------------------------------------------*/
644    /**
645     * This method is typically used by the layout manager, it reports the absolute minimum space
646     * required to display the entire label.
647     * 
648     */
649    /*-------------------------------------------------------------------*/
650    public Dimension getMinimumSize()
651    {
652        measure();
653        return (new Dimension(maxAllowed, (numLines * (lineHeight + lineAscent + lineDescent))
654                + (2 * marginHeight)));
655    }
656
657    /*-------------------------------------------------------------------*/
658    /**
659     * This method is called by the system after this object is first created.
660     * 
661     */
662    /*-------------------------------------------------------------------*/
663    public void addNotify()
664    {
665        super.addNotify(); // invoke the superclass
666    }
667}
668/*---------------------------------------------------------------------------*/