001: /*
002: * soapUI, copyright (C) 2004-2007 eviware.com
003: *
004: * soapUI is free software; you can redistribute it and/or modify it under the
005: * terms of version 2.1 of the GNU Lesser General Public License as published by
006: * the Free Software Foundation.
007: *
008: * soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
009: * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
010: * See the GNU Lesser General Public License for more details at gnu.org.
011: */
012:
013: package com.eviware.soapui.support.components;
014:
015: import java.awt.Component;
016: import java.awt.FontMetrics;
017: import java.awt.Graphics;
018: import java.awt.Graphics2D;
019: import java.beans.PropertyChangeEvent;
020: import java.beans.PropertyChangeListener;
021:
022: import javax.swing.Icon;
023:
024: /**
025: VTextIcon is an Icon implementation which draws a short string vertically.
026: It's useful for JTabbedPanes with LEFT or RIGHT tabs but can be used in any
027: component which supports Icons, such as JLabel or JButton
028:
029: You can provide a hint to indicate whether to rotate the string
030: to the left or right, or not at all, and it checks to make sure
031: that the rotation is legal for the given string
032: (for example, Chinese/Japanese/Korean scripts have special rules when
033: drawn vertically and should never be rotated)
034: */
035: public class VTextIcon implements Icon, PropertyChangeListener {
036: String fLabel;
037: String[] fCharStrings; // for efficiency, break the fLabel into one-char strings to be passed to drawString
038: int[] fCharWidths; // Roman characters should be centered when not rotated (Japanese fonts are monospaced)
039: int[] fPosition; // Japanese half-height characters need to be shifted when drawn vertically
040: int fWidth, fHeight, fCharHeight, fDescent; // Cached for speed
041: int fRotation;
042: Component fComponent;
043:
044: static final int POSITION_NORMAL = 0;
045: static final int POSITION_TOP_RIGHT = 1;
046: static final int POSITION_FAR_TOP_RIGHT = 2;
047:
048: public static final int ROTATE_DEFAULT = 0x00;
049: public static final int ROTATE_NONE = 0x01;
050: public static final int ROTATE_LEFT = 0x02;
051: public static final int ROTATE_RIGHT = 0x04;
052:
053: /**
054: * Creates a <code>VTextIcon</code> for the specified <code>component</code>
055: * with the specified <code>label</code>.
056: * It sets the orientation to the default for the string
057: * @see #verifyRotation
058: */
059: public VTextIcon(Component component, String label) {
060: this (component, label, ROTATE_DEFAULT);
061: }
062:
063: /**
064: * Creates a <code>VTextIcon</code> for the specified <code>component</code>
065: * with the specified <code>label</code>.
066: * It sets the orientation to the provided value if it's legal for the string
067: * @see #verifyRotation
068: */
069: public VTextIcon(Component component, String label, int rotateHint) {
070: fComponent = component;
071: fLabel = label;
072: fRotation = verifyRotation(label, rotateHint);
073: calcDimensions();
074: fComponent.addPropertyChangeListener(this );
075: }
076:
077: /**
078: * sets the label to the given string, updating the orientation as needed
079: * and invalidating the layout if the size changes
080: * @see #verifyRotation
081: */
082: public void setLabel(String label) {
083: fLabel = label;
084: fRotation = verifyRotation(label, fRotation); // Make sure the current rotation is still legal
085: recalcDimensions();
086: }
087:
088: /**
089: * Checks for changes to the font on the fComponent
090: * so that it can invalidate the layout if the size changes
091: */
092: public void propertyChange(PropertyChangeEvent e) {
093: String prop = e.getPropertyName();
094: if ("font".equals(prop)) {
095: recalcDimensions();
096: }
097: }
098:
099: /**
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
113: .getFontMetrics(fComponent.getFont());
114: fCharHeight = fm.getAscent() + fm.getDescent();
115: fDescent = fm.getDescent();
116: if (fRotation == ROTATE_NONE) {
117: int len = fLabel.length();
118: char data[] = new char[len];
119: fLabel.getChars(0, len, data, 0);
120: // if not rotated, width is that of the widest char in the string
121: fWidth = 0;
122: // we need an array of one-char strings for drawString
123: fCharStrings = new String[len];
124: fCharWidths = new int[len];
125: fPosition = new int[len];
126: char ch;
127: for (int i = 0; i < len; i++) {
128: ch = data[i];
129: fCharWidths[i] = fm.charWidth(ch);
130: if (fCharWidths[i] > fWidth)
131: fWidth = fCharWidths[i];
132: fCharStrings[i] = new String(data, i, 1);
133: // small kana and punctuation
134: if (sDrawsInTopRight.indexOf(ch) >= 0) // if ch is in sDrawsInTopRight
135: fPosition[i] = POSITION_TOP_RIGHT;
136: else if (sDrawsInFarTopRight.indexOf(ch) >= 0)
137: fPosition[i] = POSITION_FAR_TOP_RIGHT;
138: else
139: fPosition[i] = POSITION_NORMAL;
140: }
141: // and height is the font height * the char count, + one extra leading at the bottom
142: fHeight = fCharHeight * len + fDescent;
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
172: + ((fWidth - fCharWidths[i]) / 2), yPos);
173: break;
174: case POSITION_TOP_RIGHT:
175: tweak = fCharHeight / 3; // Should be 2, but they aren't actually half-height
176: g.drawString(fCharStrings[i], x + (tweak / 2), yPos
177: - tweak);
178: break;
179: case POSITION_FAR_TOP_RIGHT:
180: tweak = fCharHeight - fCharHeight / 3;
181: g.drawString(fCharStrings[i], x + (tweak / 2), yPos
182: - tweak);
183: break;
184: }
185: yPos += fCharHeight;
186: }
187: } else if (fRotation == ROTATE_LEFT) {
188: g.translate(x + fWidth, y + fHeight);
189: ((Graphics2D) g).rotate(-NINETY_DEGREES);
190: g.drawString(fLabel, kBufferSpace, -fDescent);
191: ((Graphics2D) g).rotate(NINETY_DEGREES);
192: g.translate(-(x + fWidth), -(y + fHeight));
193: } else if (fRotation == ROTATE_RIGHT) {
194: g.translate(x, y);
195: ((Graphics2D) g).rotate(NINETY_DEGREES);
196: g.drawString(fLabel, kBufferSpace, -fDescent);
197: ((Graphics2D) g).rotate(-NINETY_DEGREES);
198: g.translate(-x, -y);
199: }
200:
201: }
202:
203: /**
204: * Returns the icon's width.
205: *
206: * @return an int specifying the fixed width of the icon.
207: */
208: public int getIconWidth() {
209: return fWidth;
210: }
211:
212: /**
213: * Returns the icon's height.
214: *
215: * @return an int specifying the fixed height of the icon.
216: */
217: public int getIconHeight() {
218: return fHeight;
219: }
220:
221: /**
222: verifyRotation
223:
224: returns the best rotation for the string (ROTATE_NONE, ROTATE_LEFT, ROTATE_RIGHT)
225:
226: This is public static so you can use it to test a string without creating a VTextIcon
227:
228: from http://www.unicode.org/unicode/reports/tr9/tr9-3.html
229: When setting text using the Arabic script in vertical lines,
230: it is more common to employ a horizontal baseline that
231: is rotated by 90¡ counterclockwise so that the characters
232: are ordered from top to bottom. Latin text and numbers
233: may be rotated 90¡ clockwise so that the characters
234: are also ordered from top to bottom.
235:
236: Rotation rules
237: - Roman can rotate left, right, or none - default right (counterclockwise)
238: - CJK can't rotate
239: - Arabic must rotate - default left (clockwise)
240:
241: from the online edition of _The Unicode Standard, Version 3.0_, file ch10.pdf page 4
242: Ideographs are found in three blocks of the Unicode Standard...
243: U+4E00-U+9FFF, U+3400-U+4DFF, U+F900-U+FAFF
244:
245: Hiragana is U+3040-U+309F, katakana is U+30A0-U+30FF
246:
247: from http://www.unicode.org/unicode/faq/writingdirections.html
248: East Asian scripts are frequently written in vertical lines
249: which run from top-to-bottom and are arrange columns either
250: from left-to-right (Mongolian) or right-to-left (other scripts).
251: Most characters use the same shape and orientation when displayed
252: horizontally or vertically, but many punctuation characters
253: will change their shape when displayed vertically.
254:
255: Letters and words from other scripts are generally rotated through
256: ninety degree angles so that they, too, will read from top to bottom.
257: That is, letters from left-to-right scripts will be rotated clockwise
258: and letters from right-to-left scripts counterclockwise, both
259: through ninety degree angles.
260:
261: Unlike the bidirectional case, the choice of vertical layout
262: is usually treated as a formatting style; therefore,
263: the Unicode Standard does not define default rendering behavior
264: for vertical text nor provide directionality controls designed to override such behavior
265:
266: */
267: public static int verifyRotation(String label, int rotateHint) {
268: boolean hasCJK = false;
269: boolean hasMustRotate = false; // Arabic, etc
270:
271: int len = label.length();
272: char data[] = new char[len];
273: char ch;
274: label.getChars(0, len, data, 0);
275: for (int i = 0; i < len; i++) {
276: ch = data[i];
277: if ((ch >= '\u4E00' && ch <= '\u9FFF')
278: || (ch >= '\u3400' && ch <= '\u4DFF')
279: || (ch >= '\uF900' && ch <= '\uFAFF')
280: || (ch >= '\u3040' && ch <= '\u309F')
281: || (ch >= '\u30A0' && ch <= '\u30FF'))
282: hasCJK = true;
283: if ((ch >= '\u0590' && ch <= '\u05FF') || // Hebrew
284: (ch >= '\u0600' && ch <= '\u06FF') || // Arabic
285: (ch >= '\u0700' && ch <= '\u074F')) // Syriac
286: hasMustRotate = true;
287: }
288: // If you mix Arabic with Chinese, you're on your own
289: if (hasCJK)
290: return DEFAULT_CJK;
291:
292: int legal = hasMustRotate ? LEGAL_MUST_ROTATE : LEGAL_ROMAN;
293: if ((rotateHint & legal) > 0)
294: return rotateHint;
295:
296: // The hint wasn't legal, or it was zero
297: return hasMustRotate ? DEFAULT_MUST_ROTATE : DEFAULT_ROMAN;
298: }
299:
300: // The small kana characters and Japanese punctuation that draw in the top right quadrant:
301: // small a, i, u, e, o, tsu, ya, yu, yo, wa (katakana only) ka ke
302: static final String sDrawsInTopRight = "\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308E"
303: + // hiragana
304: "\u30A1\u30A3\u30A5\u30A7\u30A9\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6"; // katakana
305: static final String sDrawsInFarTopRight = "\u3001\u3002"; // comma, full stop
306:
307: static final int DEFAULT_CJK = ROTATE_NONE;
308: static final int LEGAL_ROMAN = ROTATE_NONE | ROTATE_LEFT
309: | ROTATE_RIGHT;
310: static final int DEFAULT_ROMAN = ROTATE_RIGHT;
311: static final int LEGAL_MUST_ROTATE = ROTATE_LEFT | ROTATE_RIGHT;
312: static final int DEFAULT_MUST_ROTATE = ROTATE_LEFT;
313:
314: static final double NINETY_DEGREES = Math.toRadians(90.0);
315: static final int kBufferSpace = 5;
316: }
|