001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.visualweb.css2;
042:
043: import org.netbeans.modules.visualweb.api.designer.cssengine.CssProvider;
044: import org.netbeans.modules.visualweb.api.designer.cssengine.CssValue;
045: import java.awt.Color;
046: import java.awt.Component;
047: import java.awt.FontMetrics;
048: import java.awt.Graphics;
049: import java.net.MalformedURLException;
050: import java.net.URL;
051:
052: import javax.swing.Icon;
053: import javax.swing.ImageIcon;
054:
055: import org.w3c.dom.Element;
056:
057: import org.netbeans.modules.visualweb.designer.WebForm;
058: import org.netbeans.modules.visualweb.designer.html.HtmlAttribute;
059: import org.netbeans.modules.visualweb.designer.html.HtmlTag;
060: import org.netbeans.modules.visualweb.api.designer.cssengine.XhtmlCss;
061:
062: /**
063: * ListBox represents a box that contains a numbered or lettered or
064: * un-numbered list. Most of the list-painting code was derived from
065: * Swing's StyleSheet.ListPainter class, with the following modifications:
066: * <ul>
067: * <li> Changed over to our own DOM/CSS lookup
068: * <li> Changed circle and square code to use size 5 instead of 8 to
069: * match what Mozilla & Safari are doing
070: * <li> Support lower-latin and upper-latin (same as lower-alpha and
071: * upper alpha)
072: * <li> Alignment computation changed to use our own layout based
073: * linebox alignment
074: * </ul>
075: * @todo Support list-style-type: decimal-leading-zero
076: * @todo Support list-style-position
077: * @todo Support list-style
078: * @todo I don't seem to handle empty list items well (<li></li>);
079: * they don't currently take up a whole font-height line
080: *
081: * @author Tor Norbye
082: */
083: public class ListBox extends ContainerBox {
084: private static final int SHAPE_SIZE = 5; // Swing had 8, but
085:
086: /* list of roman numerals */
087: private static final char[][] romanChars = { { 'i', 'v' },
088: { 'x', 'l' }, { 'c', 'd' }, { 'm', '?' }, };
089: private boolean checkedForStart;
090: private int start;
091: // private Value type;
092: private CssValue cssType;
093: Icon img = null;
094: private int bulletgap = 5;
095:
096: /**
097: * Create a ListBox for the given string
098: *
099: * @param webform The <code>WebForm</code>
100: * @param element The element this string box is associated with
101: * @param boxType Type of box.
102: * @param string The string to manage
103: * @param width The width to use for the box
104: * @param height The height to use for the box
105: */
106: public ListBox(WebForm webform, Element element, BoxType boxType,
107: boolean inline, boolean replaced) {
108: super (webform, element, boxType, inline, replaced);
109:
110: // Get the image to use as a list bullet
111: img = getListStyleImage(webform, element);
112:
113: // Get the type of bullet to use in the list
114: if (img == null) {
115: // type = CssLookup.getValue(element, XhtmlCss.LIST_STYLE_TYPE_INDEX);
116: cssType = CssProvider.getEngineService()
117: .getComputedValueForElement(element,
118: XhtmlCss.LIST_STYLE_TYPE_INDEX);
119: }
120:
121: start = 1;
122: }
123:
124: public void paint(Graphics g, int px, int py) {
125: super .paint(g, px, py);
126:
127: px += getX();
128: py += getY();
129:
130: // Box model quirk: my coordinate system is based on the visual
131: // extents of the boxes - e.g. location and size of the border
132: // edge. Because of this, when visually traversing the hierarchy,
133: // I need to add in the margins.
134: px += leftMargin;
135: py += effectiveTopMargin;
136:
137: int n = getBoxCount();
138:
139: for (int i = 0; i < n; i++) {
140: CssBox child = getBox(i);
141:
142: if (!child.hidden) {
143: paintBullet(g,
144: (float) (px + child.getX() + child.leftMargin),
145: (float) (py + child.getY()), // margins?
146: (float) child.getWidth(), (float) child
147: .getHeight(), this , i);
148: }
149: }
150: }
151:
152: // public String toString() {
153: // return "ListBox[" + paramString() + "]";
154: // }
155:
156: // both Mozilla and Safari seems to use roughly 5 pixels
157: private static ImageIcon getListStyleImage(WebForm webform,
158: Element element) {
159: // Value value = CssLookup.getValue(element, XhtmlCss.LIST_STYLE_IMAGE_INDEX);
160: CssValue cssValue = CssProvider.getEngineService()
161: .getComputedValueForElement(element,
162: XhtmlCss.LIST_STYLE_IMAGE_INDEX);
163:
164: // if (value == CssValueConstants.NONE_VALUE) {
165: if (CssProvider.getValueService().isNoneValue(cssValue)) {
166: return null;
167: }
168:
169: // String urlString = value.getStringValue();
170: String urlString = cssValue.getStringValue();
171:
172: // XXX This is wrong. I should get the -stylesheet- URL.
173: // And what about linked style sheets?
174: // URL reference = webform.getMarkup().getBase();
175: URL reference = webform.getBaseUrl();
176: URL url = null;
177:
178: try {
179: url = new URL(reference, urlString);
180: } catch (MalformedURLException e) {
181: e.printStackTrace();
182:
183: return null;
184: }
185:
186: ImageIcon image = new ImageIcon(url);
187:
188: return image;
189: }
190:
191: /**
192: * Returns a string that represents the value
193: * of the HTML.Attribute.TYPE attribute.
194: * If this attributes is not defined, then
195: * then the type defaults to "disc" unless
196: * the tag is on Ordered list. In the case
197: * of the latter, the default type is "decimal".
198: */
199: private CssValue getChildType(CssBox childBox) {
200: Element child = childBox.getElement();
201: // Value childtype = CssLookup.getValue(child, XhtmlCss.LIST_STYLE_TYPE_INDEX);
202: CssValue cssChildType = CssProvider.getEngineService()
203: .getComputedValueForElement(child,
204: XhtmlCss.LIST_STYLE_TYPE_INDEX);
205:
206: // if (childtype == null) {
207: // if (type == null) {
208: if (cssChildType == null) {
209: if (cssType == null) {
210: // Parent view.
211: CssBox parent = childBox.getParent();
212:
213: if (parent.tag == HtmlTag.OL) {
214: // childtype = CssValueConstants.DECIMAL_VALUE;
215: cssChildType = CssProvider.getValueService()
216: .getDecimalCssValueConstant();
217: } else {
218: // childtype = CssValueConstants.DISC_VALUE;
219: cssChildType = CssProvider.getValueService()
220: .getDiscCssValueConstant();
221: }
222: } else {
223: // childtype = type;
224: cssChildType = cssType;
225: }
226: }
227:
228: // return childtype;
229: return cssChildType;
230: }
231:
232: /**
233: * Obtains the starting index from <code>parent</code>.
234: */
235: private void getStart(CssBox parent) {
236: checkedForStart = true;
237:
238: Element element = parent.getElement();
239:
240: if (element != null) {
241: String startValue = element
242: .getAttribute(HtmlAttribute.START);
243:
244: if ((startValue != null) && (startValue.length() > 0)) {
245: try {
246: start = Integer.parseInt(startValue);
247: } catch (NumberFormatException nfe) {
248: // User has entered a bogus start attribute in the markup
249: // so ignore it.
250: start = 1;
251: }
252: }
253: }
254: }
255:
256: /**
257: * Returns an integer that should be used to render the child at
258: * <code>childIndex</code> with. The retValue will usually be
259: * <code>childIndex</code> + 1, unless <code>parentBox</code>
260: * has some child boxes that do not represent LI's, or one of the views
261: * has a HtmlAttribute.START specified.
262: */
263: private int getRenderIndex(CssBox parentBox, int childIndex) {
264: if (!checkedForStart) {
265: getStart(parentBox);
266: }
267:
268: int retIndex = childIndex;
269:
270: for (int counter = childIndex; counter >= 0; counter--) {
271: CssBox cs = parentBox.getBox(counter);
272:
273: if (cs.tag != HtmlTag.LI) {
274: retIndex--;
275: } else {
276: String value = cs.getElement().getAttribute(
277: HtmlAttribute.VALUE);
278:
279: if ((value != null) && (value.length() > 0)) {
280: try {
281: int iValue = Integer.parseInt((String) value);
282:
283: return retIndex - counter + iValue;
284: } catch (NumberFormatException nfe) {
285: // The user has entered a bogus value, so ignore it
286: continue;
287: }
288: }
289: }
290: }
291:
292: return retIndex + start;
293: }
294:
295: /**
296: * Paints the CSS list decoration according to the
297: * attributes given.
298: *
299: * @param g the rendering surface.
300: * @param x the x coordinate of the list item allocation
301: * @param y the y coordinate of the list item allocation
302: * @param w the width of the list item allocation
303: * @param h the height of the list item allocation
304: * @param s the allocated area to paint into.
305: * @param item which list item is being painted. This
306: * is a number greater than or equal to 0.
307: */
308: public void paintBullet(Graphics g, float x, float y, float w,
309: float h, CssBox parentBox, int item) {
310: CssBox cv = parentBox.getBox(item);
311:
312: // Only draw something if the View is a list item. This won't
313: // be the case for comments.
314: if (cv.tag != HtmlTag.LI) { // XXX I should use CSS "display: list-item" instead for this!
315:
316: return;
317: }
318:
319: // How the list indicator is aligned is not specified, it is
320: // left up to the UA. IE and NS differ on this behavior.
321: // This is closer to NS where we align to the first line of text.
322: // If the child is not text we draw the indicator at the
323: // origin (0).
324: float align = 0;
325: LineBox lineBox = findFirstLineBox(cv);
326:
327: if (lineBox != null) {
328: h = lineBox.getHeight();
329: y = lineBox.getAbsoluteY();
330: align = 0.5f;
331: }
332:
333: if (img != null) {
334: drawIcon(g, (int) x, (int) y, (int) h, align, webform
335: .getPane());
336:
337: return;
338: }
339:
340: // Value childtype = getChildType(cv);
341: CssValue cssChildType = getChildType(cv);
342:
343: /* TODO - is this necessary? I believe I'll still have the
344: List item font in the graphics context since list painter
345: is called right after the item itself is painted.
346: Font font = ((StyledDocument)cv.getDocument()).
347: getFont(cv.getElement());
348: if (font != null) {
349: g.setFont(font);
350: }
351: */
352: // if ((childtype == CssValueConstants.SQUARE_VALUE) ||
353: // (childtype == CssValueConstants.CIRCLE_VALUE) ||
354: // (childtype == CssValueConstants.DISC_VALUE)) {
355: if (CssProvider.getValueService().isSquareValue(cssChildType)
356: || CssProvider.getValueService().isCircleValue(
357: cssChildType)
358: || CssProvider.getValueService().isDiscValue(
359: cssChildType)) {
360: // drawShape(g, childtype, (int)x, (int)y, (int)h, align);
361: drawShape(g, cssChildType, (int) x, (int) y, (int) h, align);
362: // XXX Needless, handled by above
363: // } else if (childtype == CssValueConstants.CIRCLE_VALUE) {
364: // drawShape(g, childtype, (int)x, (int)y, (int)h, align);
365: // } else if (childtype == CssValueConstants.DECIMAL_VALUE) {
366: } else if (CssProvider.getValueService().isDecimalValue(
367: cssChildType)) {
368: drawLetter(g, '1', (int) x, (int) y, (int) h, align,
369: getRenderIndex(parentBox, item));
370: // } else if ((childtype == CssValueConstants.LOWER_LATIN_VALUE) ||
371: // (childtype == CssValueConstants.LOWER_ALPHA_VALUE)) {
372: } else if (CssProvider.getValueService().isLowerLatinValue(
373: cssChildType)
374: || CssProvider.getValueService().isLowerAlphaValue(
375: cssChildType)) {
376: drawLetter(g, 'a', (int) x, (int) y, (int) h, align,
377: getRenderIndex(parentBox, item));
378: // } else if ((childtype == CssValueConstants.UPPER_LATIN_VALUE) ||
379: // (childtype == CssValueConstants.UPPER_ALPHA_VALUE)) {
380: } else if (CssProvider.getValueService().isUpperLatinValue(
381: cssChildType)
382: || CssProvider.getValueService().isUpperAlphaValue(
383: cssChildType)) {
384: drawLetter(g, 'A', (int) x, (int) y, (int) h, align,
385: getRenderIndex(parentBox, item));
386: // } else if (childtype == CssValueConstants.LOWER_ROMAN_VALUE) {
387: } else if (CssProvider.getValueService().isLowerRomanValue(
388: cssChildType)) {
389: drawLetter(g, 'i', (int) x, (int) y, (int) h, align,
390: getRenderIndex(parentBox, item));
391: // } else if (childtype == CssValueConstants.UPPER_ROMAN_VALUE) {
392: } else if (CssProvider.getValueService().isUpperRomanValue(
393: cssChildType)) {
394: drawLetter(g, 'I', (int) x, (int) y, (int) h, align,
395: getRenderIndex(parentBox, item));
396: }
397: }
398:
399: /** Find first linebox within the given box IF the linebox is at
400: * the top! In other words, it only returns a LineBox if it's the
401: * first normal flow child (but of course it can be the first child
402: * of a first child of a first child ...) */
403: private LineBox findFirstLineBox(CssBox box) {
404: if (box instanceof LineBox) {
405: return (LineBox) box;
406: }
407:
408: CssBox first = box.getFirstNormalBox();
409:
410: if (first == null) {
411: return null;
412: }
413:
414: return findFirstLineBox(first);
415: }
416:
417: /**
418: * Draws the bullet icon specified by the list-style-image argument.
419: *
420: * @param g the graphics context
421: * @param ax x coordinate to place the bullet
422: * @param ay y coordinate to place the bullet
423: * @param ah height of the container the bullet is placed in
424: * @param align preferred alignment factor for the child view
425: */
426: void drawIcon(Graphics g, int ax, int ay, int ah, float align,
427: Component c) {
428: // Align to bottom of icon.
429: g.setColor(Color.black);
430:
431: int x = ax - img.getIconWidth() - bulletgap;
432: int y = Math.max(ay, (ay + (int) (align * ah))
433: - img.getIconHeight());
434:
435: img.paintIcon(c, g, x, y);
436: }
437:
438: /**
439: * Draws the graphical bullet item specified by the type argument.
440: *
441: * @param g the graphics context
442: * @param type type of bullet to draw (circle, square, disc)
443: * @param ax x coordinate to place the bullet
444: * @param ay y coordinate to place the bullet
445: * @param ah height of the container the bullet is placed in
446: * @param align preferred alignment factor for the child view
447: */
448: void drawShape(Graphics g, CssValue cssType, int ax, int ay,
449: int ah, float align) {
450: // Align to bottom of shape.
451: // Color fg = CssLookup.getColor(getElement(), XhtmlCss.COLOR_INDEX);
452: Color fg = CssProvider.getValueService().getColorForElement(
453: getElement(), XhtmlCss.COLOR_INDEX);
454:
455: if (fg == null) {
456: fg = Color.black;
457: }
458:
459: g.setColor(fg);
460:
461: int x = ax - bulletgap - SHAPE_SIZE;
462: int y = Math.max(ay, (ay + (int) (align * ah))
463: - (SHAPE_SIZE / 2));
464:
465: // if (type == CssValueConstants.SQUARE_VALUE) {
466: if (CssProvider.getValueService().isSquareValue(cssType)) {
467: //g.drawRect(x, y, SHAPE_SIZE, SHAPE_SIZE);
468: // Mozilla and Safari use solid squares
469: g.fillRect(x, y, SHAPE_SIZE, SHAPE_SIZE);
470: // } else if (type == CssValueConstants.CIRCLE_VALUE) {
471: } else if (CssProvider.getValueService().isCircleValue(cssType)) {
472: g.drawOval(x, y, SHAPE_SIZE, SHAPE_SIZE);
473: } else {
474: g.fillOval(x, y, SHAPE_SIZE, SHAPE_SIZE);
475: }
476: }
477:
478: /**
479: * Draws the letter or number for an ordered list.
480: *
481: * @param g the graphics context
482: * @param letter type of ordered list to draw
483: * @param ax x coordinate to place the bullet
484: * @param ay y coordinate to place the bullet
485: * @param ah height of the container the bullet is placed in
486: * @param index position of the list item in the list
487: */
488: void drawLetter(Graphics g, char letter, int ax, int ay, int ah,
489: float align, int index) {
490: // Color fg = CssLookup.getColor(getElement(), XhtmlCss.COLOR_INDEX);
491: Color fg = CssProvider.getValueService().getColorForElement(
492: getElement(), XhtmlCss.COLOR_INDEX);
493:
494: if (fg == null) {
495: fg = Color.black;
496: }
497:
498: g.setColor(fg);
499:
500: String str = formatItemNum(index, letter) + ".";
501: FontMetrics fm = g.getFontMetrics();
502: int stringwidth = fm.stringWidth(str);
503: int x = ax - stringwidth - bulletgap;
504: int y = Math.max(ay + fm.getAscent(), ay + (int) (ah * align));
505: g.drawString(str, x, y);
506: }
507:
508: /**
509: * Converts the item number into the ordered list number
510: * (i.e. 1 2 3, i ii iii, a b c, etc.
511: *
512: * @param itemNum number to format
513: * @param type type of ordered list
514: */
515: String formatItemNum(int itemNum, char type) {
516: //String numStyle = "1";
517:
518: boolean uppercase = false;
519:
520: String formattedNum;
521:
522: switch (type) {
523: case '1':
524: default:
525: formattedNum = String.valueOf(itemNum);
526:
527: break;
528:
529: case 'A':
530: uppercase = true;
531:
532: // fall through
533: case 'a':
534: formattedNum = formatAlphaNumerals(itemNum);
535:
536: break;
537:
538: case 'I':
539: uppercase = true;
540:
541: // fall through
542: case 'i':
543: formattedNum = formatRomanNumerals(itemNum);
544: }
545:
546: if (uppercase) {
547: formattedNum = formattedNum.toUpperCase();
548: }
549:
550: return formattedNum;
551: }
552:
553: /**
554: * Converts the item number into an alphabetic character
555: *
556: * @param itemNum number to format
557: */
558: String formatAlphaNumerals(int itemNum) {
559: String result = "";
560:
561: if (itemNum > 26) {
562: result = formatAlphaNumerals(itemNum / 26)
563: + formatAlphaNumerals(itemNum % 26);
564: } else {
565: // -1 because item is 1 based.
566: result = String.valueOf((char) (('a' + itemNum) - 1));
567: }
568:
569: return result;
570: }
571:
572: /**
573: * Converts the item number into a roman numeral
574: *
575: * @param num number to format
576: */
577: private static String formatRomanNumerals(int num) {
578: return formatRomanNumerals(0, num);
579: }
580:
581: /**
582: * Converts the item number into a roman numeral
583: *
584: * @param num number to format
585: */
586: private static String formatRomanNumerals(int level, int num) {
587: if (num < 10) {
588: return formatRomanDigit(level, num);
589: } else {
590: return formatRomanNumerals(level + 1, num / 10)
591: + formatRomanDigit(level, num % 10);
592: }
593: }
594:
595: /**
596: * Converts the item number into a roman numeral
597: *
598: * @param level position
599: * @param num digit to format
600: */
601: private static String formatRomanDigit(int level, int digit) {
602: // TODO When on 1.5, use StringBuilder.
603: StringBuffer result = new StringBuffer();
604:
605: if (digit == 9) {
606: result.append(romanChars[level][0]);
607: result.append(romanChars[level + 1][0]);
608:
609: return result.toString();
610: } else if (digit == 4) {
611: result.append(romanChars[level][0]);
612: result.append(romanChars[level][1]);
613:
614: return result.toString();
615: } else if (digit >= 5) {
616: result.append(romanChars[level][1]);
617: digit -= 5;
618: }
619:
620: for (int i = 0; i < digit; i++) {
621: result.append(romanChars[level][0]);
622: }
623:
624: return result.toString();
625: }
626: }
|