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 java.awt.Color;
044: import java.awt.FontMetrics;
045: import java.awt.Graphics;
046: import java.awt.Rectangle;
047:
048: import org.netbeans.modules.visualweb.api.designer.DomProvider.DomPosition;
049: import org.netbeans.modules.visualweb.api.designer.DomProvider.DomPosition.Bias;
050: import org.netbeans.modules.visualweb.api.designer.markup.MarkupService;
051: import org.netbeans.modules.visualweb.designer.DesignerPane;
052: import org.netbeans.modules.visualweb.designer.DesignerUtils;
053: import org.netbeans.modules.visualweb.designer.WebForm;
054:
055: import org.openide.ErrorManager;
056:
057: import org.w3c.dom.Element;
058: import org.w3c.dom.Node;
059: import org.w3c.dom.Text;
060:
061: /**
062: * TextBox represents a box of text characters placed within
063: * a LineBox.
064: * <p>
065: * See http://www.w3.org/TR/REC-CSS2/visuren.html
066: * <p>
067: * @author Tor Norbye
068: */
069: public final class TextBox extends CssBox {
070: public static final int UNDERLINE = 1;
071: public static final int STRIKE = 2;
072: public static final int OVERLINE = 4;
073: private int beginDomOffset = -1;
074: private int endDomOffset = -1;
075: private final int beginOffset;
076: private final int endOffset;
077: private char[] contentChars;
078: private String xhtml; // Identical to the "contentChars" char array
079: private String jspx; // Original jspx source whose entities were expanded into xhtml.
080: private FontMetrics metrics;
081: private Color fg;
082: private int decoration;
083: // private RaveText node;
084: private Text node;
085:
086: /**
087: * Create an TextBox representing the text run from
088: * position [beginNode,beginOffset] to position
089: * [endNode,endOffset], formatted using the given style.
090: *
091: * @param contentChars The string to draw text from, as an array
092: * of chars.
093: * @param beginOffset The offset into the contentChars array where the text
094: * for this inline box begins.
095: * @param endOffset The offset into the contentChars array where the text
096: * for this inline box ends.
097: * @param fg The color to draw the text with
098: * @param decoration A bit mask indicating whether underline, overline,
099: * strikethrough etc. should be drawn according to the UNDERLINE,
100: * STRIKE etc. constant fields of the class.
101: * @param metrics The font metrics (& font, via metrics.getFont()) to
102: * use to render this text.
103: */
104: public TextBox(WebForm webform, Element styleElement, Text node,
105: char[] contentChars, String xhtml, String jspx,
106: int beginOffset, int endOffset, Color fg, Color bg,
107: int decoration, FontMetrics metrics, boolean hidden) {
108: // Text isn't technically replaced, but it acts like it in many
109: // ways, e.g. it has an intrinsic size, should be treated "atomically"
110: // like an <object> or an <iframe>, etc., and it's that meaning that
111: // is used for the replaced flag
112: super (webform, styleElement, BoxType.TEXT, true, true);
113: this .contentChars = contentChars;
114: this .beginOffset = beginOffset;
115: this .endOffset = endOffset;
116: this .node = node;
117: this .decoration = decoration;
118: this .fg = fg;
119: this .bg = bg;
120: this .metrics = metrics;
121: this .xhtml = xhtml;
122: this .jspx = jspx;
123: this .hidden = hidden;
124:
125: int textWidth = DesignerUtils.getNonTabbedTextWidth(
126: contentChars, beginOffset, endOffset, metrics);
127: this .width = textWidth;
128:
129: if (metrics != null) {
130: this .height = metrics.getHeight();
131: } else {
132: ErrorManager.getDefault().log("No metrics found");
133: }
134:
135: this .contentWidth = this .width;
136: this .contentHeight = this .height;
137: }
138:
139: protected void initialize() {
140: }
141:
142: // Note that I pass in null to super's element reference because
143: // the element corresponds to the parent, not this box, yet
144: // we want to store it as our effective element such that css
145: // lookups on this text box uses this element.
146: protected void initializeDesignBean() {
147: }
148:
149: protected void initializeInvariants() {
150: }
151:
152: /** Can't set widths on text boxes themselves
153: * @todo Can min-width be set on spans etc. ?
154: */
155: protected void initializeHorizontalWidths(FormatContext context) {
156: }
157:
158: // public String toString() {
159: // return "TextBox[" + paramString() + "]";
160: // }
161:
162: protected String paramString() {
163: return "text=\""
164: + (contentChars == null ? null : getText())
165: + "\", "
166: + super .paramString()
167: + (metrics == null ? "" : ", font ascent="
168: + metrics.getAscent() + ", descent="
169: + metrics.getDescent() + ", height="
170: + metrics.getHeight() + ", font="
171: + metrics.getFont()) + ", boffset="
172: + beginOffset + ", eoffset=" + endOffset;
173: }
174:
175: // For testsuite & debugging only!
176: public String getText() {
177: return new String(contentChars, beginOffset, endOffset
178: - beginOffset);
179: }
180:
181: protected void paintBackground(Graphics g, int px, int py) {
182: if (hidden) {
183: return;
184: }
185:
186: if (bg != null) {
187: int x = getX() + px;
188: int y = getY() + py;
189: g.setColor(bg);
190: g.fillRect(x, y, width, height);
191: }
192: }
193:
194: public void paint(Graphics g, int px, int py) {
195: if (hidden) {
196: return;
197: }
198:
199: g.setFont(metrics.getFont());
200:
201: DesignerPane pane = webform.getPane();
202: // DesignerCaret caret = (pane != null) ? pane.getCaret() : null;
203: // if ((caret != null) && caret.hasSelection()) {
204: if (pane != null && pane.hasCaretSelection()) {
205: if (!paintSelectedText(g, px, py)) {
206: // No selection overlap or other failure - do normal painting
207: g.setColor(fg);
208:
209: int x = getX() + px;
210: int y = getY() + py;
211: int yadj = (y + metrics.getHeight())
212: - metrics.getDescent();
213: g.drawChars(contentChars, beginOffset, endOffset
214: - beginOffset, x, yadj);
215: paintLines(g, x, y, yadj, getWidth());
216: }
217:
218: return;
219: }
220:
221: // We deliberately don't want the background painted yet, and
222: // since we have no children to get called there's no reason to
223: // call the super.
224: //super.paint(g, px, py);
225: int x = getX() + px;
226: int y = getY() + py;
227:
228: g.setColor(fg);
229:
230: // determine the y coordinate to render the glyphs
231: int yadj = (y + metrics.getHeight()) - metrics.getDescent();
232:
233: // Draw text!
234: g.drawChars(contentChars, beginOffset, endOffset - beginOffset,
235: x, yadj);
236:
237: // render underline or strikethrough if set.
238: paintLines(g, x, y, yadj, getWidth());
239:
240: if (CssBox.paintText) {
241: g.setColor(Color.BLUE);
242: g.drawRect(getAbsoluteX(), getAbsoluteY(), width, height);
243: }
244: }
245:
246: /**
247: * Paint the text if we have a selection somewhere. Might be much
248: * slower than normal painting since each text box will do position
249: * computations to determine if it overlaps the selection.
250: * @return true iff there is overlap of this textbox and the selection -
251: * and it will handle the paint. Returns true otherwise and the caller
252: * needs to handle the paint.
253: */
254: private boolean paintSelectedText(Graphics g, int px, int py) {
255: // Some text boxes are generated (such as the image labels for
256: // images where the image is not found.) These cannot possibly
257: // contain selected text.
258: if (node == null) {
259: return false;
260: }
261: DesignerPane pane = webform.getPane();
262: // assert pane != null;
263: if (pane == null) {
264: ErrorManager.getDefault()
265: .notify(
266: ErrorManager.INFORMATIONAL,
267: new NullPointerException(
268: "WebForm has null pane, webForm="
269: + webform)); // NOI18N
270: return false;
271: }
272:
273: // DesignerCaret caret = pane.getCaret();
274: // assert (caret != null) && caret.hasSelection();
275: if (!pane.hasCaretSelection()) {
276: ErrorManager.getDefault().notify(
277: ErrorManager.INFORMATIONAL,
278: new IllegalStateException(
279: "Pane doesn't have caret selection, pane="
280: + pane)); // NOI18N
281: return false;
282: }
283:
284: // Determine if the range intersects our line box group
285: // Position sourceCaretBegin = caret.getFirstPosition();
286: // DomPosition sourceCaretBegin = caret.getFirstPosition();
287: DomPosition sourceCaretBegin = pane.getFirstPosition();
288:
289: // XXX I ought to have a cached method on the caret for obtaining the rendered
290: // location!
291: // Position caretBegin = sourceCaretBegin.getRenderedPosition();
292: // Position sourceCaretEnd = caret.getLastPosition();
293: // Position caretEnd = sourceCaretEnd.getRenderedPosition();
294: DomPosition caretBegin = sourceCaretBegin.getRenderedPosition();
295: // DomPosition sourceCaretEnd = caret.getLastPosition();
296: DomPosition sourceCaretEnd = pane.getLastPosition();
297:
298: DomPosition caretEnd = sourceCaretEnd.getRenderedPosition();
299: Node caretBeginNode = caretBegin.getNode();
300:
301: if (caretBeginNode == null) {
302: return false;
303: }
304:
305: Node caretEndNode = caretEnd.getNode();
306:
307: if (caretEndNode == null) {
308: return false;
309: }
310:
311: Text renderNode = node;
312:
313: // if (!renderNode.isRendered()) {
314: // renderNode = renderNode.getRendered();
315: // }
316: // if (!MarkupService.isRenderedNode(renderNode)) {
317: if (!webform.isRenderedNode(renderNode)) {
318: renderNode = MarkupService
319: .getRenderedTextForText(renderNode);
320: }
321:
322: // int r1 =
323: // Position.compareBoundaryPoints(caretBeginNode, caretBegin.getOffset(), renderNode,
324: // endOffset);
325: // int r2 =
326: // Position.compareBoundaryPoints(caretEndNode, caretEnd.getOffset(), renderNode,
327: // beginOffset);
328: int r1 = webform.compareBoundaryPoints(caretBeginNode,
329: caretBegin.getOffset(), renderNode, endOffset);
330: int r2 = webform.compareBoundaryPoints(caretEndNode, caretEnd
331: .getOffset(), renderNode, beginOffset);
332:
333: if (!((r1 >= 0) && (r2 <= 0))) {
334: // No overlap - just do normal painting
335: return false;
336: }
337:
338: // Compute the before, during and after sections of the selection.
339: // We will paint selection from selStartOffset to selEndOffset,
340: // and non-selection from beginOffset to selStartOffset and
341: // from selEndOffset to endOffset.
342: int selStartOffset;
343: int selEndOffset;
344:
345: if (caretBeginNode == renderNode) {
346: selStartOffset = caretBegin.getOffset();
347:
348: if (selStartOffset < beginOffset) {
349: selStartOffset = beginOffset;
350: }
351: } else {
352: selStartOffset = beginOffset; // somewhere before this node
353: }
354:
355: if (caretEndNode == renderNode) {
356: selEndOffset = caretEnd.getOffset();
357:
358: if (selEndOffset > endOffset) {
359: selEndOffset = endOffset;
360: }
361: } else {
362: selEndOffset = endOffset; // somewhere after this node
363: }
364:
365: int x = getX() + px;
366: int y = getY() + py;
367:
368: // determine the y coordinate to render the glyphs
369: int yadj = (y + metrics.getHeight()) - metrics.getDescent();
370:
371: // Paint region BEFORE selection (might be empty)
372: if (selStartOffset > beginOffset) {
373: g.setColor(fg);
374: g.drawChars(contentChars, beginOffset, selStartOffset
375: - beginOffset, x, yadj);
376:
377: int w = DesignerUtils.getNonTabbedTextWidth(contentChars,
378: beginOffset, selStartOffset, metrics);
379: paintLines(g, x, y, yadj, w);
380: x += w;
381: }
382:
383: // Paint selection region: selStartOffset to selEndOffset
384: if (selEndOffset > selStartOffset) {
385: g.setColor(pane.getSelectedTextColor());
386: g.drawChars(contentChars, selStartOffset, selEndOffset
387: - selStartOffset, x, yadj);
388:
389: int w = DesignerUtils.getNonTabbedTextWidth(contentChars,
390: selStartOffset, selEndOffset, metrics);
391: paintLines(g, x, y, yadj, w);
392: x += w;
393: }
394:
395: // Paint region AFTER selection (might be empty)
396: if (selEndOffset < endOffset) {
397: g.setColor(fg);
398: g.drawChars(contentChars, selEndOffset, endOffset
399: - selEndOffset, x, yadj);
400:
401: int w = DesignerUtils.getNonTabbedTextWidth(contentChars,
402: selEndOffset, endOffset, metrics);
403: paintLines(g, x, y, yadj, w);
404: x += w; // not strictly necessary, we're done with x
405: }
406:
407: return true;
408: }
409:
410: /** Paint underline, strike through and/or overline as appropriate */
411: private void paintLines(Graphics g, int x, int y, int yBaseline,
412: int width) {
413: // render underline or strikethrough if set.
414: if ((decoration & UNDERLINE) != 0) {
415: int yTmp = yBaseline;
416: yTmp += 1;
417: g.drawLine(x, yTmp, x + width, yTmp);
418: }
419:
420: if ((decoration & STRIKE) != 0) {
421: int yTmp = yBaseline;
422:
423: // move y coordinate above baseline
424: yTmp -= (int) (metrics.getAscent() * 0.4f);
425: g.drawLine(x, yTmp, x + width, yTmp);
426: }
427:
428: if ((decoration & OVERLINE) != 0) {
429: g.drawLine(x, y, x + width, y);
430: }
431: }
432:
433: public FontMetrics getMetrics() {
434: return metrics;
435: }
436:
437: // Intended for testsuite
438: public boolean isUnderline() {
439: return (decoration & UNDERLINE) != 0;
440: }
441:
442: // Intended for testsuite
443: public boolean isStrikeThrough() {
444: return (decoration & STRIKE) != 0;
445: }
446:
447: // Intended for testsuite
448: public boolean isOverline() {
449: return (decoration & OVERLINE) != 0;
450: }
451:
452: // Intended for testsuite
453: public Color getTextColor() {
454: return fg;
455: }
456:
457: public int getIntrinsicWidth() {
458: return width;
459: }
460:
461: public int getIntrinsicHeight() {
462: return height;
463: }
464:
465: public int getBaseline() {
466: // TODO - half leading?
467: return metrics.getHeight() - metrics.getDescent();
468: }
469:
470: /** Return the first position in the document of this text node */
471: // public Position getFirstPosition() {
472: public DomPosition getFirstPosition() {
473: if (node != null) {
474: // return new Position(node, getDomStartOffset(), Bias.FORWARD);
475: // return Position.create(node, getDomStartOffset(), Bias.FORWARD);
476: return webform.createDomPosition(node, getDomStartOffset(),
477: Bias.FORWARD);
478: } else {
479: // return Position.NONE;
480: return DomPosition.NONE;
481: }
482: }
483:
484: /** Return the last position in the document of this text node */
485: // public Position getLastPosition() {
486: public DomPosition getLastPosition() {
487: if (node != null) {
488: // return new Position(node, getDomEndOffset(), Bias.BACKWARD);
489: // return Position.create(node, getDomEndOffset(), Bias.BACKWARD);
490: return webform.createDomPosition(node, getDomEndOffset(),
491: Bias.BACKWARD);
492: } else {
493: return DomPosition.NONE;
494: }
495: }
496:
497: /** TODO: rename to computePosition */
498: // public Position computePosition(int px) {
499: public DomPosition computePosition(int px) {
500: if (node == null) {
501: // return Position.NONE;
502: return DomPosition.NONE;
503: }
504:
505: // XXX what about ' ' ?
506: int offset =
507: // DesignerUtils.getNonTabbedTextOffset(contentChars, beginOffset,
508: getNonTabbedTextOffset(contentChars, beginOffset, endOffset
509: - beginOffset, metrics, 0, px);
510:
511: // Pick the nearest offset. Alternatively enhance the above method
512: // to return it (perhaps based on a flag, since it's also used
513: // to compute how much we can fit which has to be more conservative.
514: if (offset < endOffset) {
515: // Distance up to the character that contains the x position
516: int textWidth = DesignerUtils.getNonTabbedTextWidth(
517: contentChars, beginOffset, offset, metrics);
518:
519: // Width of the character that contains the x position
520: int charWidth = DesignerUtils.getNonTabbedTextWidth(
521: contentChars, offset, offset + 1, metrics);
522:
523: // px points to a distance between textWidth and textWidth+charWidth
524: // so px-textWidth should be between 0 and charWidth
525: if ((px - textWidth) > (charWidth / 2)) {
526: offset++;
527: }
528: }
529:
530: // In the presence of entities we have to compute the position in the original
531: // jspx dom
532: if (jspx != xhtml && jspx.indexOf('&') != -1) {
533: // <markup_separation>
534: // offset = MarkupServiceProvider.getDefault().getUnexpandedOffset(jspx, offset);
535: // ====
536: // offset = InSyncService.getProvider().getUnexpandedOffset(jspx, offset);
537: offset = webform.getDomProviderService()
538: .getUnexpandedOffset(jspx, offset);
539: // </markup_separation>
540: }
541:
542: // return new Position(node, offset, Bias.FORWARD); // XXX set bias depending on how it compares to end offset?
543: // return Position.create(node, offset, Bias.FORWARD); // XXX set bias depending on how it compares to end offset?
544: return webform.createDomPosition(node, offset, Bias.FORWARD); // XXX set bias depending on how it compares to end offset?
545: }
546:
547: // XXX Moved from DesignerUtils.
548: /**
549: * Unlike the methods in javax.swing.text I'm returning the text offset itself, not the distance
550: * from the passed in segment/string offset
551: */
552: private static final int getNonTabbedTextOffset(char[] s,
553: int txtOffset, int len, FontMetrics metrics, int x0, int x) {
554: if (x0 >= x) {
555: // x before x0, return.
556: return txtOffset;
557: }
558:
559: int currX = x0;
560: int nextX = currX;
561:
562: // s may be a shared segment, so it is copied prior to calling
563: // the tab expander
564: int n = txtOffset + len;
565: final boolean round = true;
566:
567: for (int i = txtOffset; i < n; i++) {
568: char c = s[i];
569:
570: // TODO if there are successive spaces, ignore them
571: // TODO count a newline as a space!
572: if ((c == '\t') || (c == '\n')) {
573: nextX += metrics.charWidth(' ');
574: } else {
575: nextX += metrics.charWidth(c);
576: }
577:
578: if ((x >= currX) && (x < nextX)) {
579: // found the hit position... return the appropriate side
580: if ((round == false) || ((x - currX) < (nextX - x))) {
581: return i;
582: } else {
583: return i + 1;
584: }
585: }
586:
587: currX = nextX;
588: }
589:
590: return txtOffset;
591: }
592:
593: /** Return the bounding box of the character at the given position */
594: // public Rectangle getBoundingBox(Position pos) {
595: public Rectangle getBoundingBox(DomPosition pos) {
596: // This is not always true, because for example if you're pointing to
597: // a paragraph in the position, we adjust the visual location search to
598: // refer to the first text node child in that paragraph instead
599: // assert pos.getNode() == node;
600: // XXX what about ' ' ?
601: int htmlpos;
602: if (jspx == xhtml || jspx.indexOf('&') == -1) {
603: htmlpos = pos.getOffset();
604: } else {
605: // <markup_separation>
606: // htmlpos = MarkupServiceProvider.getDefault().
607: // getExpandedOffset(jspx, pos.getOffset());
608: // ====
609: // htmlpos = InSyncService.getProvider().getExpandedOffset(jspx, pos.getOffset());
610: htmlpos = webform.getDomProviderService()
611: .getExpandedOffset(jspx, pos.getOffset());
612: // </markup_separation>
613: }
614:
615: if (htmlpos > contentChars.length) {
616: // Race condition; after we've modified a document node (in Document.insertString
617: // for example, we immediately update the caret, and it may be painted
618: // due to paint requests, before we've updated the view hiearchy (which
619: // is always delayed one event loop iteration for various reasons, some
620: // described in Document.handleEvent and some in DndHandler, if I recall
621: // correctly
622: contentChars = pos.getNode().getNodeValue().toCharArray();
623: }
624:
625: int offset = DesignerUtils.getNonTabbedTextWidth(contentChars,
626: beginOffset, htmlpos, metrics);
627: int textWidth = 1; // can't compute position for last char in a text box
628:
629: if (pos.getOffset() < contentChars.length) {
630: textWidth = DesignerUtils.getNonTabbedTextWidth(
631: contentChars, htmlpos, htmlpos, metrics);
632: }
633:
634: return new Rectangle(getAbsoluteX() + offset, getAbsoluteY(),
635: textWidth, getHeight());
636: }
637:
638: /** Return the node this text box is associated with */
639: public Text getNode() {
640: return node;
641: }
642:
643: /* Used only by the unit tests?
644: public int getLength() {
645: return endOffset-beginOffset;
646: }
647: */
648:
649: /** Return the beginning offset within the text node that this box represents */
650: public int getDomStartOffset() {
651: if (beginDomOffset == -1) {
652: if (jspx == xhtml || jspx.indexOf('&') == -1) {
653: beginDomOffset = beginOffset;
654: } else {
655: // <markup_separation>
656: // beginDomOffset = MarkupServiceProvider.getDefault().
657: // getUnexpandedOffset(jspx, beginOffset);
658: // ====
659: // beginDomOffset = InSyncService.getProvider().getUnexpandedOffset(jspx, beginOffset);
660: beginDomOffset = webform.getDomProviderService()
661: .getUnexpandedOffset(jspx, beginOffset);
662: // </markup_separation>
663: }
664: }
665:
666: return beginDomOffset;
667: }
668:
669: /** Return the ending offset within the text node that this box represents */
670: public int getDomEndOffset() {
671: if (endDomOffset == -1) {
672: if (jspx == xhtml || jspx.indexOf('&') == -1) {
673: endDomOffset = endOffset;
674: } else {
675: // <markup_separation>
676: // endDomOffset = MarkupServiceProvider.getDefault().
677: // getUnexpandedOffset(jspx, endOffset);
678: // ====
679: // endDomOffset = InSyncService.getProvider().getUnexpandedOffset(jspx, endOffset);
680: endDomOffset = webform.getDomProviderService()
681: .getUnexpandedOffset(jspx, endOffset);
682: // </markup_separation>
683: }
684: }
685:
686: return endDomOffset;
687: }
688:
689: /**
690: * Given the position, return the previous position within this textbox if available
691: * or Position.NONE if not.
692: */
693: // public Position getPrev(Position pos) {
694: public DomPosition getPrev(DomPosition pos) {
695: if (node == null) {
696: // return Position.NONE;
697: return DomPosition.NONE;
698: }
699:
700: if (pos.getOffset() > getDomStartOffset()) {
701: int offset;
702: if (jspx == xhtml || jspx.indexOf('&') == -1) {
703: offset = pos.getOffset();
704: } else {
705: // <markup_separation>
706: // offset = MarkupServiceProvider.getDefault().
707: // getExpandedOffset(jspx, pos.getOffset());
708: // ====
709: // offset = InSyncService.getProvider().getExpandedOffset(jspx, pos.getOffset());
710: offset = webform.getDomProviderService()
711: .getExpandedOffset(jspx, pos.getOffset());
712: // </markup_separation>
713: }
714:
715: if (isCharacterPair(offset - 2)) {
716: offset -= 2;
717: } else {
718: offset -= 1;
719: }
720:
721: if (jspx != xhtml && jspx.indexOf('&') != -1) {
722: // <markup_separation>
723: // offset = MarkupServiceProvider.getDefault().
724: // getUnexpandedOffset(jspx, offset);
725: // ====
726: // offset = InSyncService.getProvider().getUnexpandedOffset(jspx, offset);
727: offset = webform.getDomProviderService()
728: .getUnexpandedOffset(jspx, offset);
729: // </markup_separation>
730: }
731:
732: // return new Position(node, offset, Bias.BACKWARD);
733: // return Position.create(node, offset, Bias.BACKWARD);
734: return webform.createDomPosition(node, offset,
735: Bias.BACKWARD);
736: } else {
737: // return Position.NONE;
738: return DomPosition.NONE;
739: }
740: }
741:
742: /**
743: * Given the position, return the next position within this textbox if available
744: * or Position.NONE if not.
745: */
746: // public Position getNext(Position pos) {
747: public DomPosition getNext(DomPosition pos) {
748: if (node == null) {
749: // return Position.NONE;
750: return DomPosition.NONE;
751: }
752:
753: if (pos.getOffset() < getDomEndOffset()) {
754: int offset;
755: if (jspx == xhtml || jspx.indexOf('&') == -1) {
756: offset = pos.getOffset();
757: } else {
758: // <markup_separation>
759: // offset = MarkupServiceProvider.getDefault().
760: // getExpandedOffset(jspx, pos.getOffset());
761: // ====
762: // offset = InSyncService.getProvider().getExpandedOffset(jspx, pos.getOffset());
763: offset = webform.getDomProviderService()
764: .getExpandedOffset(jspx, pos.getOffset());
765: // </markup_separation>
766: }
767:
768: if (isCharacterPair(offset)) {
769: offset += 2;
770: } else {
771: offset += 1;
772: }
773:
774: if (jspx != xhtml && jspx.indexOf('&') != -1) {
775: // <markup_separation>
776: // offset = MarkupServiceProvider.getDefault().
777: // getUnexpandedOffset(jspx, offset);
778: // ====
779: // offset = InSyncService.getProvider().getUnexpandedOffset(jspx, offset);
780: offset = webform.getDomProviderService()
781: .getUnexpandedOffset(jspx, offset);
782: // </markup_separation>
783: }
784:
785: // return new Position(node, offset, Bias.FORWARD);
786: // return Position.create(node, offset, Bias.FORWARD);
787: return webform
788: .createDomPosition(node, offset, Bias.FORWARD);
789: } else {
790: // return Position.NONE;
791: return DomPosition.NONE;
792: }
793: }
794:
795: /** Return true iff the character at the given offset is the beginning
796: * of a 2-char "unit" which should be deleted, caret'ed over, etc. as a single
797: * item. If offset is outside the valid range for this text area, it will return
798: * false.
799: */
800: private boolean isCharacterPair(int offset) {
801: // See if this character is "attached" to the previous character -
802: // if so delete the pair
803: if ((offset >= 0) && (offset < (endOffset - 1))) {
804: char c0 = contentChars[offset];
805: char c1 = contentChars[offset + 1];
806:
807: // D800-DBFF: Unicode Low Surrogate Area Range
808: if ((c0 >= '\uD800') && (c0 <= '\uDBFF') && (
809: // DC00-DFFF: Unicode High Surrogate Area Range
810: c1 >= '\uDC00') && (c1 <= '\uDFFF')) {
811: // Yes!
812: return true;
813: }
814: }
815:
816: return false;
817: }
818: }
|