View Javadoc

1   /*
2    *  soapUI, copyright (C) 2004-2009 eviware.com 
3    *
4    *  soapUI is free software; you can redistribute it and/or modify it under the 
5    *  terms of version 2.1 of the GNU Lesser General Public License as published by 
6    *  the Free Software Foundation.
7    *
8    *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
9    *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
10   *  See the GNU Lesser General Public License for more details at gnu.org.
11   */
12  
13  package com.eviware.soapui.support.components;
14  
15  import java.awt.Component;
16  import java.awt.FontMetrics;
17  import java.awt.Graphics;
18  import java.awt.Graphics2D;
19  import java.beans.PropertyChangeEvent;
20  import java.beans.PropertyChangeListener;
21  
22  import javax.swing.Icon;
23  
24  /***
25   * VTextIcon is an Icon implementation which draws a short string vertically.
26   * It's useful for JTabbedPanes with LEFT or RIGHT tabs but can be used in any
27   * component which supports Icons, such as JLabel or JButton
28   * 
29   * You can provide a hint to indicate whether to rotate the string to the left
30   * or right, or not at all, and it checks to make sure that the rotation is
31   * legal for the given string (for example, Chinese/Japanese/Korean scripts have
32   * special rules when drawn vertically and should never be rotated)
33   */
34  public class VTextIcon implements Icon, PropertyChangeListener
35  {
36  	String fLabel;
37  	String[] fCharStrings; // for efficiency, break the fLabel into one-char
38  									// strings to be passed to drawString
39  	int[] fCharWidths; // Roman characters should be centered when not rotated
40  								// (Japanese fonts are monospaced)
41  	int[] fPosition; // Japanese half-height characters need to be shifted when
42  							// drawn vertically
43  	int fWidth, fHeight, fCharHeight, fDescent; // Cached for speed
44  	int fRotation;
45  	Component fComponent;
46  
47  	static final int POSITION_NORMAL = 0;
48  	static final int POSITION_TOP_RIGHT = 1;
49  	static final int POSITION_FAR_TOP_RIGHT = 2;
50  
51  	public static final int ROTATE_DEFAULT = 0x00;
52  	public static final int ROTATE_NONE = 0x01;
53  	public static final int ROTATE_LEFT = 0x02;
54  	public static final int ROTATE_RIGHT = 0x04;
55  
56  	/***
57  	 * Creates a <code>VTextIcon</code> for the specified <code>component</code>
58  	 * with the specified <code>label</code>. It sets the orientation to the
59  	 * default for the string
60  	 * 
61  	 * @see #verifyRotation
62  	 */
63  	public VTextIcon( Component component, String label )
64  	{
65  		this( component, label, ROTATE_DEFAULT );
66  	}
67  
68  	/***
69  	 * Creates a <code>VTextIcon</code> for the specified <code>component</code>
70  	 * with the specified <code>label</code>. It sets the orientation to the
71  	 * provided value if it's legal for the string
72  	 * 
73  	 * @see #verifyRotation
74  	 */
75  	public VTextIcon( Component component, String label, int rotateHint )
76  	{
77  		fComponent = component;
78  		fLabel = label;
79  		fRotation = verifyRotation( label, rotateHint );
80  		calcDimensions();
81  		fComponent.addPropertyChangeListener( this );
82  	}
83  
84  	/***
85  	 * sets the label to the given string, updating the orientation as needed and
86  	 * invalidating the layout if the size changes
87  	 * 
88  	 * @see #verifyRotation
89  	 */
90  	public void setLabel( String label )
91  	{
92  		fLabel = label;
93  		fRotation = verifyRotation( label, fRotation ); // Make sure the current
94  																		// rotation is still legal
95  		recalcDimensions();
96  	}
97  
98  	/***
99  	 * Checks for changes to the font on the fComponent so that it can invalidate
100 	 * the layout if the size changes
101 	 */
102 	public void propertyChange( PropertyChangeEvent e )
103 	{
104 		String prop = e.getPropertyName();
105 		if( "font".equals( prop ) )
106 		{
107 			recalcDimensions();
108 		}
109 	}
110 
111 	/***
112 	 * Calculates the dimensions. If they've changed, invalidates the component
113 	 */
114 	void recalcDimensions()
115 	{
116 		int wOld = getIconWidth();
117 		int hOld = getIconHeight();
118 		calcDimensions();
119 		if( wOld != getIconWidth() || hOld != getIconHeight() )
120 			fComponent.invalidate();
121 	}
122 
123 	void calcDimensions()
124 	{
125 		FontMetrics fm = fComponent.getFontMetrics( fComponent.getFont() );
126 		fCharHeight = fm.getAscent() + fm.getDescent();
127 		fDescent = fm.getDescent();
128 		if( fRotation == ROTATE_NONE )
129 		{
130 			int len = fLabel.length();
131 			char data[] = new char[len];
132 			fLabel.getChars( 0, len, data, 0 );
133 			// if not rotated, width is that of the widest char in the string
134 			fWidth = 0;
135 			// we need an array of one-char strings for drawString
136 			fCharStrings = new String[len];
137 			fCharWidths = new int[len];
138 			fPosition = new int[len];
139 			char ch;
140 			for( int i = 0; i < len; i++ )
141 			{
142 				ch = data[i];
143 				fCharWidths[i] = fm.charWidth( ch );
144 				if( fCharWidths[i] > fWidth )
145 					fWidth = fCharWidths[i];
146 				fCharStrings[i] = new String( data, i, 1 );
147 				// small kana and punctuation
148 				if( sDrawsInTopRight.indexOf( ch ) >= 0 ) // if ch is in
149 																		// sDrawsInTopRight
150 					fPosition[i] = POSITION_TOP_RIGHT;
151 				else if( sDrawsInFarTopRight.indexOf( ch ) >= 0 )
152 					fPosition[i] = POSITION_FAR_TOP_RIGHT;
153 				else
154 					fPosition[i] = POSITION_NORMAL;
155 			}
156 			// and height is the font height * the char count, + one extra leading
157 			// at the bottom
158 			fHeight = fCharHeight * len + fDescent;
159 		}
160 		else
161 		{
162 			// if rotated, width is the height of the string
163 			fWidth = fCharHeight;
164 			// and height is the width, plus some buffer space
165 			fHeight = fm.stringWidth( fLabel ) + 2 * kBufferSpace;
166 		}
167 	}
168 
169 	/***
170 	 * Draw the icon at the specified location. Icon implementations may use the
171 	 * Component argument to get properties useful for painting, e.g. the
172 	 * foreground or background color.
173 	 */
174 	public void paintIcon( Component c, Graphics g, int x, int y )
175 	{
176 		// We don't insist that it be on the same Component
177 
178 		g.setColor( fComponent.getForeground() );
179 		g.setFont( fComponent.getFont() );
180 		if( fRotation == ROTATE_NONE )
181 		{
182 			int yPos = y + fCharHeight;
183 			for( int i = 0; i < fCharStrings.length; i++ )
184 			{
185 				// Special rules for Japanese - "half-height" characters (like ya,
186 				// yu, yo in combinations)
187 				// should draw in the top-right quadrant when drawn vertically
188 				// - they draw in the bottom-left normally
189 				int tweak;
190 				switch( fPosition[i] )
191 				{
192 				case POSITION_NORMAL :
193 					// Roman fonts should be centered. Japanese fonts are always
194 					// monospaced.
195 					g.drawString( fCharStrings[i], x + ( ( fWidth - fCharWidths[i] ) / 2 ), yPos );
196 					break;
197 				case POSITION_TOP_RIGHT :
198 					tweak = fCharHeight / 3; // Should be 2, but they aren't actually
199 														// half-height
200 					g.drawString( fCharStrings[i], x + ( tweak / 2 ), yPos - tweak );
201 					break;
202 				case POSITION_FAR_TOP_RIGHT :
203 					tweak = fCharHeight - fCharHeight / 3;
204 					g.drawString( fCharStrings[i], x + ( tweak / 2 ), yPos - tweak );
205 					break;
206 				}
207 				yPos += fCharHeight;
208 			}
209 		}
210 		else if( fRotation == ROTATE_LEFT )
211 		{
212 
213 			g.translate( x + fWidth, y + fHeight );
214 			( ( Graphics2D )g ).rotate( -NINETY_DEGREES );
215 			g.setColor( fComponent.getBackground() );
216 			g.fillRect( 1, 1, fWidth - 2, fHeight - 2 );
217 			g.setColor( fComponent.getForeground() );
218 			g.drawString( fLabel, kBufferSpace, -fDescent );
219 			g.setColor( fComponent.getBackground() );
220 			( ( Graphics2D )g ).rotate( NINETY_DEGREES );
221 			g.translate( -( x + fWidth ), -( y + fHeight ) );
222 		}
223 		else if( fRotation == ROTATE_RIGHT )
224 		{
225 			g.translate( x, y );
226 			( ( Graphics2D )g ).rotate( NINETY_DEGREES );
227 			g.setColor( fComponent.getBackground() );
228 			g.fillRect( 0, 0, fWidth, fHeight );
229 			g.setColor( fComponent.getForeground() );
230 			g.drawString( fLabel, kBufferSpace, -fDescent );
231 			g.setColor( fComponent.getBackground() );
232 			( ( Graphics2D )g ).rotate( -NINETY_DEGREES );
233 			g.translate( -x, -y );
234 		}
235 
236 	}
237 
238 	/***
239 	 * Returns the icon's width.
240 	 * 
241 	 * @return an int specifying the fixed width of the icon.
242 	 */
243 	public int getIconWidth()
244 	{
245 		return fWidth;
246 	}
247 
248 	/***
249 	 * Returns the icon's height.
250 	 * 
251 	 * @return an int specifying the fixed height of the icon.
252 	 */
253 	public int getIconHeight()
254 	{
255 		return fHeight;
256 	}
257 
258 	/***
259 	 * verifyRotation
260 	 * 
261 	 * returns the best rotation for the string (ROTATE_NONE, ROTATE_LEFT,
262 	 * ROTATE_RIGHT)
263 	 * 
264 	 * This is public static so you can use it to test a string without creating
265 	 * a VTextIcon
266 	 * 
267 	 * from http://www.unicode.org/unicode/reports/tr9/tr9-3.html When setting
268 	 * text using the Arabic script in vertical lines, it is more common to
269 	 * employ a horizontal baseline that is rotated by 90¡ counterclockwise so
270 	 * that the characters are ordered from top to bottom. Latin text and numbers
271 	 * may be rotated 90¡ clockwise so that the characters are also ordered from
272 	 * top to bottom.
273 	 * 
274 	 * Rotation rules - Roman can rotate left, right, or none - default right
275 	 * (counterclockwise) - CJK can't rotate - Arabic must rotate - default left
276 	 * (clockwise)
277 	 * 
278 	 * from the online edition of _The Unicode Standard, Version 3.0_, file
279 	 * ch10.pdf page 4 Ideographs are found in three blocks of the Unicode
280 	 * Standard... U+4E00-U+9FFF, U+3400-U+4DFF, U+F900-U+FAFF
281 	 * 
282 	 * Hiragana is U+3040-U+309F, katakana is U+30A0-U+30FF
283 	 * 
284 	 * from http://www.unicode.org/unicode/faq/writingdirections.html East Asian
285 	 * scripts are frequently written in vertical lines which run from
286 	 * top-to-bottom and are arrange columns either from left-to-right
287 	 * (Mongolian) or right-to-left (other scripts). Most characters use the same
288 	 * shape and orientation when displayed horizontally or vertically, but many
289 	 * punctuation characters will change their shape when displayed vertically.
290 	 * 
291 	 * Letters and words from other scripts are generally rotated through ninety
292 	 * degree angles so that they, too, will read from top to bottom. That is,
293 	 * letters from left-to-right scripts will be rotated clockwise and letters
294 	 * from right-to-left scripts counterclockwise, both through ninety degree
295 	 * angles.
296 	 * 
297 	 * Unlike the bidirectional case, the choice of vertical layout is usually
298 	 * treated as a formatting style; therefore, the Unicode Standard does not
299 	 * define default rendering behavior for vertical text nor provide
300 	 * directionality controls designed to override such behavior
301 	 */
302 	public static int verifyRotation( String label, int rotateHint )
303 	{
304 		boolean hasCJK = false;
305 		boolean hasMustRotate = false; // Arabic, etc
306 
307 		int len = label.length();
308 		char data[] = new char[len];
309 		char ch;
310 		label.getChars( 0, len, data, 0 );
311 		for( int i = 0; i < len; i++ )
312 		{
313 			ch = data[i];
314 			if( ( ch >= '\u4E00' && ch <= '\u9FFF' ) || ( ch >= '\u3400' && ch <= '\u4DFF' )
315 					|| ( ch >= '\uF900' && ch <= '\uFAFF' ) || ( ch >= '\u3040' && ch <= '\u309F' )
316 					|| ( ch >= '\u30A0' && ch <= '\u30FF' ) )
317 				hasCJK = true;
318 			if( ( ch >= '\u0590' && ch <= '\u05FF' ) || // Hebrew
319 					( ch >= '\u0600' && ch <= '\u06FF' ) || // Arabic
320 					( ch >= '\u0700' && ch <= '\u074F' ) ) // Syriac
321 				hasMustRotate = true;
322 		}
323 		// If you mix Arabic with Chinese, you're on your own
324 		if( hasCJK )
325 			return DEFAULT_CJK;
326 
327 		int legal = hasMustRotate ? LEGAL_MUST_ROTATE : LEGAL_ROMAN;
328 		if( ( rotateHint & legal ) > 0 )
329 			return rotateHint;
330 
331 		// The hint wasn't legal, or it was zero
332 		return hasMustRotate ? DEFAULT_MUST_ROTATE : DEFAULT_ROMAN;
333 	}
334 
335 	// The small kana characters and Japanese punctuation that draw in the top
336 	// right quadrant:
337 	// small a, i, u, e, o, tsu, ya, yu, yo, wa (katakana only) ka ke
338 	static final String sDrawsInTopRight = "\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308E" + // hiragana
339 			"\u30A1\u30A3\u30A5\u30A7\u30A9\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6"; // katakana
340 	static final String sDrawsInFarTopRight = "\u3001\u3002"; // comma, full stop
341 
342 	static final int DEFAULT_CJK = ROTATE_NONE;
343 	static final int LEGAL_ROMAN = ROTATE_NONE | ROTATE_LEFT | ROTATE_RIGHT;
344 	static final int DEFAULT_ROMAN = ROTATE_RIGHT;
345 	static final int LEGAL_MUST_ROTATE = ROTATE_LEFT | ROTATE_RIGHT;
346 	static final int DEFAULT_MUST_ROTATE = ROTATE_LEFT;
347 
348 	static final double NINETY_DEGREES = Math.toRadians( 90.0 );
349 	static final int kBufferSpace = 5;
350 }