001: /*
002: * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
003: * for visualizing and manipulating spatial features with geometry and attributes.
004: *
005: * Copyright (C) 2003 Vivid Solutions
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: *
021: * For more information, contact:
022: *
023: * Vivid Solutions
024: * Suite #1A
025: * 2328 Government Street
026: * Victoria BC V8T 5G5
027: * Canada
028: *
029: * (250)385-6040
030: * www.vividsolutions.com
031: */
032:
033: package com.vividsolutions.jump.workbench.ui;
034:
035: import java.awt.Component;
036: import java.awt.FontMetrics;
037: import java.awt.Graphics;
038: import java.awt.Graphics2D;
039: import java.awt.RenderingHints;
040: import java.beans.PropertyChangeEvent;
041: import java.beans.PropertyChangeListener;
042:
043: import javax.swing.Icon;
044:
045: /**
046: * VTextIcon is an Icon implementation which draws a short string vertically.
047: * It's useful for JTabbedPanes with LEFT or RIGHT tabs but can be used in any
048: * component which supports Icons, such as JLabel or JButton
049: * <P>
050: * You can provide a hint to indicate whether to rotate the string
051: * to the left or right, or not at all, and it checks to make sure
052: * that the rotation is legal for the given string
053: * (for example, Chinese/Japanese/Korean scripts have special rules when
054: * drawn vertically and should never be rotated)
055: *
056: * Rucker, Lee Ann. "Vertical Text in Java." March 2002.
057: * Available from http://www.macdevcenter.com/pub/a/mac/2002/03/22/vertical_text.html.
058: * Internet; accessed 29 October 2002.
059: *
060: * Licence: "Anybody can use the code for any purpose; I don't want any
061: * compensation, nor do I accept any liability."
062: */
063: public class VTextIcon implements Icon, PropertyChangeListener {
064: static final int POSITION_NORMAL = 0;
065: static final int POSITION_TOP_RIGHT = 1;
066: static final int POSITION_FAR_TOP_RIGHT = 2;
067: public static final int ROTATE_DEFAULT = 0x00;
068: public static final int ROTATE_NONE = 0x01;
069: public static final int ROTATE_LEFT = 0x02;
070: public static final int ROTATE_RIGHT = 0x04;
071:
072: // The small kana characters and Japanese punctuation that draw in the top right quadrant:
073: // small a, i, u, e, o, tsu, ya, yu, yo, wa (katakana only) ka ke
074: static final String sDrawsInTopRight = "\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308E"
075: + // hiragana
076: "\u30A1\u30A3\u30A5\u30A7\u30A9\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6"; // katakana
077: static final String sDrawsInFarTopRight = "\u3001\u3002"; // comma, full stop
078: static final int DEFAULT_CJK = ROTATE_NONE;
079: static final int LEGAL_ROMAN = ROTATE_NONE | ROTATE_LEFT
080: | ROTATE_RIGHT;
081: static final int DEFAULT_ROMAN = ROTATE_RIGHT;
082: static final int LEGAL_MUST_ROTATE = ROTATE_LEFT | ROTATE_RIGHT;
083: static final int DEFAULT_MUST_ROTATE = ROTATE_LEFT;
084: static final double NINETY_DEGREES = Math.toRadians(90.0);
085: static final int kBufferSpace = 5;
086: String fLabel;
087: String[] fCharStrings; // for efficiency, break the fLabel into one-char strings to be passed to drawString
088: int[] fCharWidths; // Roman characters should be centered when not rotated (Japanese fonts are monospaced)
089: int[] fPosition; // Japanese half-height characters need to be shifted when drawn vertically
090: int fWidth; // Cached for speed
091: int fHeight; // Cached for speed
092: int fCharHeight; // Cached for speed
093: int fDescent; // Cached for speed
094: int fRotation;
095: Component fComponent;
096:
097: /**
098: * Creates a <code>VTextIcon</code> for the specified <code>component</code>
099: * with the specified <code>label</code>.
100: * It sets the orientation to the default for the string
101: * @see #verifyRotation
102: */
103: public VTextIcon(Component component, String label) {
104: this (component, label, ROTATE_DEFAULT);
105: }
106:
107: /**
108: * Creates a <code>VTextIcon</code> for the specified <code>component</code>
109: * with the specified <code>label</code>.
110: * It sets the orientation to the provided value if it's legal for the string
111: * @see #verifyRotation
112: */
113: public VTextIcon(Component component, String label, int rotateHint) {
114: fComponent = component;
115: fLabel = label;
116: fRotation = verifyRotation(label, rotateHint);
117: calcDimensions();
118: fComponent.addPropertyChangeListener(this );
119: }
120:
121: /**
122: * sets the label to the given string, updating the orientation as needed
123: * and invalidating the layout if the size changes
124: * @see #verifyRotation
125: */
126: public void setLabel(String label) {
127: fLabel = label;
128: fRotation = verifyRotation(label, fRotation); // Make sure the current rotation is still legal
129: recalcDimensions();
130: }
131:
132: /**
133: * Checks for changes to the font on the fComponent
134: * so that it can invalidate the layout if the size changes
135: */
136: public void propertyChange(PropertyChangeEvent e) {
137: String prop = e.getPropertyName();
138:
139: if ("font".equals(prop)) {
140: recalcDimensions();
141: }
142: }
143:
144: /**
145: * Calculates the dimensions. If they've changed,
146: * invalidates the component
147: */
148: void recalcDimensions() {
149: int wOld = getIconWidth();
150: int hOld = getIconHeight();
151: calcDimensions();
152:
153: if ((wOld != getIconWidth()) || (hOld != getIconHeight())) {
154: fComponent.invalidate();
155: }
156: }
157:
158: void calcDimensions() {
159: FontMetrics fm = fComponent
160: .getFontMetrics(fComponent.getFont());
161: fCharHeight = fm.getAscent() + fm.getDescent();
162: fDescent = fm.getDescent();
163:
164: if (fRotation == ROTATE_NONE) {
165: int len = fLabel.length();
166: char[] data = new char[len];
167: fLabel.getChars(0, len, data, 0);
168:
169: // if not rotated, width is that of the widest char in the string
170: fWidth = 0;
171:
172: // we need an array of one-char strings for drawString
173: fCharStrings = new String[len];
174: fCharWidths = new int[len];
175: fPosition = new int[len];
176:
177: char ch;
178:
179: for (int i = 0; i < len; i++) {
180: ch = data[i];
181: fCharWidths[i] = fm.charWidth(ch);
182:
183: if (fCharWidths[i] > fWidth) {
184: fWidth = fCharWidths[i];
185: }
186:
187: fCharStrings[i] = new String(data, i, 1);
188:
189: // small kana and punctuation
190: if (sDrawsInTopRight.indexOf(ch) >= 0) { // if ch is in sDrawsInTopRight
191: fPosition[i] = POSITION_TOP_RIGHT;
192: } else if (sDrawsInFarTopRight.indexOf(ch) >= 0) {
193: fPosition[i] = POSITION_FAR_TOP_RIGHT;
194: } else {
195: fPosition[i] = POSITION_NORMAL;
196: }
197: }
198:
199: // and height is the font height * the char count, + one extra leading at the bottom
200: fHeight = (fCharHeight * len) + fDescent;
201: } else {
202: // if rotated, width is the height of the string
203: fWidth = fCharHeight;
204:
205: // and height is the width, plus some buffer space
206: fHeight = fm.stringWidth(fLabel) + (2 * kBufferSpace);
207: }
208: }
209:
210: /**
211: * Draw the icon at the specified location. Icon implementations
212: * may use the Component argument to get properties useful for
213: * painting, e.g. the foreground or background color.
214: */
215: public void paintIcon(Component c, Graphics g, int x, int y) {
216: Graphics2D g2 = (Graphics2D) g;
217: g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
218: RenderingHints.VALUE_ANTIALIAS_ON);
219:
220: // We don't insist that it be on the same Component
221: g.setColor(c.getForeground());
222: g.setFont(c.getFont());
223:
224: if (fRotation == ROTATE_NONE) {
225: int yPos = y + fCharHeight;
226:
227: for (int i = 0; i < fCharStrings.length; i++) {
228: // Special rules for Japanese - "half-height" characters (like ya, yu, yo in combinations)
229: // should draw in the top-right quadrant when drawn vertically
230: // - they draw in the bottom-left normally
231: int tweak;
232:
233: switch (fPosition[i]) {
234: case POSITION_NORMAL:
235:
236: // Roman fonts should be centered. Japanese fonts are always monospaced.
237: g.drawString(fCharStrings[i], x
238: + ((fWidth - fCharWidths[i]) / 2), yPos);
239:
240: break;
241:
242: case POSITION_TOP_RIGHT:
243: tweak = fCharHeight / 3; // Should be 2, but they aren't actually half-height
244: g.drawString(fCharStrings[i], x + (tweak / 2), yPos
245: - tweak);
246:
247: break;
248:
249: case POSITION_FAR_TOP_RIGHT:
250: tweak = fCharHeight - (fCharHeight / 3);
251: g.drawString(fCharStrings[i], x + (tweak / 2), yPos
252: - tweak);
253:
254: break;
255: }
256:
257: yPos += fCharHeight;
258: }
259: } else if (fRotation == ROTATE_LEFT) {
260: g.translate(x + fWidth, y + fHeight);
261: ((Graphics2D) g).rotate(-NINETY_DEGREES);
262: g.drawString(fLabel, kBufferSpace, -fDescent);
263: ((Graphics2D) g).rotate(NINETY_DEGREES);
264: g.translate(-(x + fWidth), -(y + fHeight));
265: } else if (fRotation == ROTATE_RIGHT) {
266: g.translate(x, y);
267: ((Graphics2D) g).rotate(NINETY_DEGREES);
268: g.drawString(fLabel, kBufferSpace, -fDescent);
269: ((Graphics2D) g).rotate(-NINETY_DEGREES);
270: g.translate(-x, -y);
271: }
272: }
273:
274: /**
275: * Returns the icon's width.
276: *
277: * @return an int specifying the fixed width of the icon.
278: */
279: public int getIconWidth() {
280: return fWidth;
281: }
282:
283: /**
284: * Returns the icon's height.
285: *
286: * @return an int specifying the fixed height of the icon.
287: */
288: public int getIconHeight() {
289: return fHeight;
290: }
291:
292: /**
293: verifyRotation
294:
295: returns the best rotation for the string (ROTATE_NONE, ROTATE_LEFT, ROTATE_RIGHT)
296:
297: This is public static so you can use it to test a string without creating a VTextIcon
298:
299: from http://www.unicode.org/unicode/reports/tr9/tr9-3.html
300: When setting text using the Arabic script in vertical lines,
301: it is more common to employ a horizontal baseline that
302: is rotated by 90¡ counterclockwise so that the characters
303: are ordered from top to bottom. Latin text and numbers
304: may be rotated 90¡ clockwise so that the characters
305: are also ordered from top to bottom.
306:
307: Rotation rules
308: - Roman can rotate left, right, or none - default right (counterclockwise)
309: - CJK can't rotate
310: - Arabic must rotate - default left (clockwise)
311:
312: from the online edition of _The Unicode Standard, Version 3.0_, file ch10.pdf page 4
313: Ideographs are found in three blocks of the Unicode Standard...
314: U+4E00-U+9FFF, U+3400-U+4DFF, U+F900-U+FAFF
315:
316: Hiragana is U+3040-U+309F, katakana is U+30A0-U+30FF
317:
318: from http://www.unicode.org/unicode/faq/writingdirections.html
319: East Asian scripts are frequently written in vertical lines
320: which run from top-to-bottom and are arrange columns either
321: from left-to-right (Mongolian) or right-to-left (other scripts).
322: Most characters use the same shape and orientation when displayed
323: horizontally or vertically, but many punctuation characters
324: will change their shape when displayed vertically.
325:
326: Letters and words from other scripts are generally rotated through
327: ninety degree angles so that they, too, will read from top to bottom.
328: That is, letters from left-to-right scripts will be rotated clockwise
329: and letters from right-to-left scripts counterclockwise, both
330: through ninety degree angles.
331:
332: Unlike the bidirectional case, the choice of vertical layout
333: is usually treated as a formatting style; therefore,
334: the Unicode Standard does not define default rendering behavior
335: for vertical text nor provide directionality controls designed to override such behavior
336:
337: */
338: public static int verifyRotation(String label, int rotateHint) {
339: boolean hasCJK = false;
340: boolean hasMustRotate = false; // Arabic, etc
341:
342: int len = label.length();
343: char[] data = new char[len];
344: char ch;
345: label.getChars(0, len, data, 0);
346:
347: for (int i = 0; i < len; i++) {
348: ch = data[i];
349:
350: if (((ch >= '\u4E00') && (ch <= '\u9FFF'))
351: || ((ch >= '\u3400') && (ch <= '\u4DFF'))
352: || ((ch >= '\uF900') && (ch <= '\uFAFF'))
353: || ((ch >= '\u3040') && (ch <= '\u309F'))
354: || ((ch >= '\u30A0') && (ch <= '\u30FF'))) {
355: hasCJK = true;
356: }
357:
358: if (((ch >= '\u0590') && (ch <= '\u05FF')) || // Hebrew
359: ((ch >= '\u0600') && (ch <= '\u06FF')) || // Arabic
360: ((ch >= '\u0700') && (ch <= '\u074F'))) { // Syriac
361: hasMustRotate = true;
362: }
363: }
364:
365: // If you mix Arabic with Chinese, you're on your own
366: if (hasCJK) {
367: return DEFAULT_CJK;
368: }
369:
370: int legal = hasMustRotate ? LEGAL_MUST_ROTATE : LEGAL_ROMAN;
371:
372: if ((rotateHint & legal) > 0) {
373: return rotateHint;
374: }
375:
376: // The hint wasn't legal, or it was zero
377: return hasMustRotate ? DEFAULT_MUST_ROTATE : DEFAULT_ROMAN;
378: }
379: }
|