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