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 			g.translate(x+fWidth,y+fHeight);
187 			((Graphics2D)g).rotate(-NINETY_DEGREES);
188 			g.drawString(fLabel, kBufferSpace, -fDescent);
189 			((Graphics2D)g).rotate(NINETY_DEGREES);
190 			g.translate(-(x+fWidth),-(y+fHeight));
191 		} 
192 		else if (fRotation == ROTATE_RIGHT) {
193 			g.translate(x,y);
194 			((Graphics2D)g).rotate(NINETY_DEGREES);
195 			g.drawString(fLabel, kBufferSpace, -fDescent);
196 			((Graphics2D)g).rotate(-NINETY_DEGREES);
197 			g.translate(-x,-y);
198 		} 
199 	
200 	}
201     
202     /***
203      * Returns the icon's width.
204      *
205      * @return an int specifying the fixed width of the icon.
206      */
207     public int getIconWidth() {
208 		return fWidth;
209 	}
210 	
211     /***
212      * Returns the icon's height.
213      *
214      * @return an int specifying the fixed height of the icon.
215      */
216     public int getIconHeight() {
217 		return fHeight;
218 	}
219 	
220 	/*** 
221 		 verifyRotation
222 		
223 		returns the best rotation for the string (ROTATE_NONE, ROTATE_LEFT, ROTATE_RIGHT)
224 		
225 		This is public static so you can use it to test a string without creating a VTextIcon
226 		
227 	 from http://www.unicode.org/unicode/reports/tr9/tr9-3.html
228 	 When setting text using the Arabic script in vertical lines, 
229 	 it is more common to employ a horizontal baseline that 
230 	 is rotated by 90¡ counterclockwise so that the characters 
231 	 are ordered from top to bottom. Latin text and numbers 
232 	 may be rotated 90¡ clockwise so that the characters 
233 	 are also ordered from top to bottom.
234 	 
235 		Rotation rules
236 	 	- Roman can rotate left, right, or none - default right (counterclockwise)
237 		- CJK can't rotate
238 		- Arabic must rotate - default left (clockwise)
239 	 
240 	 from the online edition of _The Unicode Standard, Version 3.0_, file ch10.pdf page 4
241 	 Ideographs are found in three blocks of the Unicode Standard...
242 	 U+4E00-U+9FFF, U+3400-U+4DFF, U+F900-U+FAFF
243 	 
244 	 Hiragana is U+3040-U+309F, katakana is U+30A0-U+30FF
245 	 
246 	 from http://www.unicode.org/unicode/faq/writingdirections.html
247 	 East Asian scripts are frequently written in vertical lines 
248 	 which run from top-to-bottom and are arrange columns either 
249 	 from left-to-right (Mongolian) or right-to-left (other scripts). 
250 	 Most characters use the same shape and orientation when displayed 
251 	 horizontally or vertically, but many punctuation characters 
252 	 will change their shape when displayed vertically.
253 
254 	 Letters and words from other scripts are generally rotated through 
255 	 ninety degree angles so that they, too, will read from top to bottom. 
256 	 That is, letters from left-to-right scripts will be rotated clockwise 
257 	 and letters from right-to-left scripts counterclockwise, both 
258 	 through ninety degree angles.
259 
260 	Unlike the bidirectional case, the choice of vertical layout 
261 	is usually treated as a formatting style; therefore, 
262 	the Unicode Standard does not define default rendering behavior 
263 	for vertical text nor provide directionality controls designed to override such behavior
264 
265 	 */
266 	public static int verifyRotation(String label, int rotateHint) {
267 		boolean hasCJK = false;
268 		boolean hasMustRotate = false; // Arabic, etc
269 		
270 		int len = label.length();
271 		char data[] = new char[len];
272 		char ch;
273 		label.getChars(0, len, data, 0);
274 		for (int i = 0; i < len; i++) {
275 			ch = data[i];
276 			if ((ch >= '\u4E00' && ch <= '\u9FFF') ||
277 				(ch >= '\u3400' && ch <= '\u4DFF') ||
278 				(ch >= '\uF900' && ch <= '\uFAFF') ||
279 				(ch >= '\u3040' && ch <= '\u309F') ||
280 				(ch >= '\u30A0' && ch <= '\u30FF') )
281 			   hasCJK = true;
282 			if ((ch >= '\u0590' && ch <= '\u05FF') || // Hebrew
283 				(ch >= '\u0600' && ch <= '\u06FF') || // Arabic
284 				(ch >= '\u0700' && ch <= '\u074F') ) // Syriac
285 			   hasMustRotate = true;
286 		}
287 		// If you mix Arabic with Chinese, you're on your own
288 		if (hasCJK)
289 			return DEFAULT_CJK;
290 		
291 		int legal = hasMustRotate ? LEGAL_MUST_ROTATE : LEGAL_ROMAN;
292 		if ((rotateHint & legal) > 0)
293 			return rotateHint;
294 		
295 		// The hint wasn't legal, or it was zero
296 		return hasMustRotate ? DEFAULT_MUST_ROTATE : DEFAULT_ROMAN;
297 	}
298 	
299 	// The small kana characters and Japanese punctuation that draw in the top right quadrant:
300 	// small a, i, u, e, o, tsu, ya, yu, yo, wa  (katakana only) ka ke
301 	static final String sDrawsInTopRight = 
302 		"\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308E" + // hiragana 
303 		"\u30A1\u30A3\u30A5\u30A7\u30A9\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6"; // katakana
304 	static final String sDrawsInFarTopRight = "\u3001\u3002"; // comma, full stop
305 	
306 	static final int DEFAULT_CJK = ROTATE_NONE;
307 	static final int LEGAL_ROMAN = ROTATE_NONE | ROTATE_LEFT | ROTATE_RIGHT;
308 	static final int DEFAULT_ROMAN = ROTATE_RIGHT; 
309 	static final int LEGAL_MUST_ROTATE = ROTATE_LEFT | ROTATE_RIGHT;
310 	static final int DEFAULT_MUST_ROTATE = ROTATE_LEFT;
311 
312 	static final double NINETY_DEGREES = Math.toRadians(90.0);
313 	static final int kBufferSpace = 5;
314 }