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 }