View Javadoc

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