Exemple #1
0
 /**
  * set / update the text of the displayLabels. these are the Week column headers above the days on
  * the Calendar part of the <code>CDateTime</code>.
  */
 private void updateDaysOfWeek() {
   if (dayPanel != null) {
     Calendar tmpcal = cdt.getCalendarInstance();
     tmpcal.set(Calendar.DAY_OF_WEEK, tmpcal.getFirstDayOfWeek());
     Locale locale = cdt.getLocale();
     boolean ltr =
         (ComponentOrientation.getOrientation(locale).isLeftToRight()
             && !locale.getLanguage().equals("zh")); // $NON-NLS-1$
     BreakIterator iterator = BreakIterator.getCharacterInstance(locale);
     for (int x = 0; x < dayLabels.length; x++) {
       String str = getFormattedDate("E", tmpcal.getTime()); // $NON-NLS-1$
       if (dayLabels[x].getData(CDT.Key.Compact, Boolean.class)) {
         iterator.setText(str);
         int start, end;
         if (ltr) {
           start = iterator.first();
           end = iterator.next();
         } else {
           end = iterator.last();
           start = iterator.previous();
         }
         dayLabels[x].setText(str.substring(start, end));
       } else {
         dayLabels[x].setText(str);
       }
       tmpcal.add(Calendar.DAY_OF_WEEK, 1);
     }
   }
 }
  @Override
  protected BoundaryScanner get(String fieldName, SolrParams params) {
    // construct Locale
    String language = params.getFieldParam(fieldName, HighlightParams.BS_LANGUAGE);
    String country = params.getFieldParam(fieldName, HighlightParams.BS_COUNTRY);
    if (country != null && language == null) {
      throw new SolrException(
          ErrorCode.BAD_REQUEST,
          HighlightParams.BS_LANGUAGE
              + " parameter cannot be null when you specify "
              + HighlightParams.BS_COUNTRY);
    }
    Locale locale = null;
    if (language != null) {
      locale = country == null ? new Locale(language) : new Locale(language, country);
    } else {
      locale = Locale.ROOT;
    }

    // construct BreakIterator
    String type =
        params.getFieldParam(fieldName, HighlightParams.BS_TYPE, "WORD").toLowerCase(Locale.ROOT);
    BreakIterator bi = null;
    if (type.equals("character")) {
      bi = BreakIterator.getCharacterInstance(locale);
    } else if (type.equals("word")) {
      bi = BreakIterator.getWordInstance(locale);
    } else if (type.equals("line")) {
      bi = BreakIterator.getLineInstance(locale);
    } else if (type.equals("sentence")) {
      bi = BreakIterator.getSentenceInstance(locale);
    } else
      throw new SolrException(
          ErrorCode.BAD_REQUEST, type + " is invalid for parameter " + HighlightParams.BS_TYPE);

    return new org.apache.lucene.search.vectorhighlight.BreakIteratorBoundaryScanner(bi);
  }
// LIU: Changed from final to non-final
public class DumbTextComponent extends Canvas
    implements KeyListener, MouseListener, MouseMotionListener, FocusListener {

  /** For serialization */
  private static final long serialVersionUID = 8265547730738652151L;

  //    private transient static final String copyright =
  //      "Copyright \u00A9 1998, Mark Davis. All Rights Reserved.";
  private static transient boolean DEBUG = false;

  private String contents = "";
  private Selection selection = new Selection();
  private int activeStart = -1;
  private boolean editable = true;

  private transient Selection tempSelection = new Selection();
  private transient boolean focus;
  private transient BreakIterator lineBreaker = BreakIterator.getLineInstance();
  private transient BreakIterator wordBreaker = BreakIterator.getWordInstance();
  private transient BreakIterator charBreaker = BreakIterator.getCharacterInstance();
  private transient int lineAscent;
  private transient int lineHeight;
  private transient int lineLeading;
  private transient int lastHeight = 10;
  private transient int lastWidth = 50;
  private static final int MAX_LINES = 200; // LIU: Use symbolic name
  private transient int[] lineStarts = new int[MAX_LINES]; // LIU
  private transient int lineCount = 1;

  private transient boolean valid = false;
  private transient FontMetrics fm;
  private transient boolean redoLines = true;
  private transient boolean doubleClick = false;
  private transient TextListener textListener;
  private transient ActionListener selectionListener;
  private transient Image cacheImage;
  private transient Dimension mySize;
  private transient int xInset = 5;
  private transient int yInset = 5;
  private transient Point startPoint = new Point();
  private transient Point endPoint = new Point();
  private transient Point caretPoint = new Point();
  private transient Point activePoint = new Point();

  // private transient static String clipBoard;

  private static final char CR = '\015'; // LIU

  // ============================================

  public DumbTextComponent() {
    addMouseListener(this);
    addMouseMotionListener(this);
    addKeyListener(this);
    addFocusListener(this);
    setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
  }

  // ================ Events ====================

  // public boolean isFocusTraversable() { return true; }

  public void addActionListener(ActionListener l) {
    selectionListener = AWTEventMulticaster.add(selectionListener, l);
  }

  public void removeActionListener(ActionListener l) {
    selectionListener = AWTEventMulticaster.remove(selectionListener, l);
  }

  public void addTextListener(TextListener l) {
    textListener = AWTEventMulticaster.add(textListener, l);
  }

  public void removeTextListener(TextListener l) {
    textListener = AWTEventMulticaster.remove(textListener, l);
  }

  private transient boolean pressed;

  public void mousePressed(MouseEvent e) {
    if (DEBUG) System.out.println("mousePressed");
    if (pressed) {
      select(e, false);
    } else {
      doubleClick = e.getClickCount() > 1;
      requestFocus();
      select(e, true);
      pressed = true;
    }
  }

  public void mouseDragged(MouseEvent e) {
    if (DEBUG) System.out.println("mouseDragged");
    select(e, false);
  }

  public void mouseReleased(MouseEvent e) {
    if (DEBUG) System.out.println("mouseReleased");
    pressed = false;
  }

  public void mouseEntered(MouseEvent e) {
    // if (pressed) select(e, false);
  }

  public void mouseExited(MouseEvent e) {
    // if (pressed) select(e, false);
  }

  public void mouseClicked(MouseEvent e) {}

  public void mouseMoved(MouseEvent e) {}

  public void focusGained(FocusEvent e) {
    if (DEBUG) System.out.println("focusGained");
    focus = true;
    valid = false;
    repaint(16);
  }

  public void focusLost(FocusEvent e) {
    if (DEBUG) System.out.println("focusLost");
    focus = false;
    valid = false;
    repaint(16);
  }

  public void select(MouseEvent e, boolean first) {
    setKeyStart(-1);
    point2Offset(e.getPoint(), tempSelection);
    if (first) {
      if ((e.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
        tempSelection.anchor = tempSelection.caret;
      }
    }
    // fix words
    if (doubleClick) {
      tempSelection.expand(wordBreaker);
    }
    select(tempSelection);
  }

  public void keyPressed(KeyEvent e) {
    int code = e.getKeyCode();
    if (DEBUG)
      System.out.println("keyPressed " + hex((char) code) + ", " + hex((char) e.getModifiers()));
    int start = selection.getStart();
    int end = selection.getEnd();
    boolean shift = (e.getModifiers() & InputEvent.SHIFT_MASK) != 0;
    boolean ctrl = (e.getModifiers() & InputEvent.CTRL_MASK) != 0;

    switch (code) {
      case KeyEvent.VK_Q:
        if (!ctrl || !editable) break;
        setKeyStart(-1);
        fixHex();
        break;
      case KeyEvent.VK_V:
        if (!ctrl) break;
        if (!editable) {
          this.getToolkit().beep();
        } else {
          paste();
        }
        break;
      case KeyEvent.VK_C:
        if (!ctrl) break;
        copy();
        break;
      case KeyEvent.VK_X:
        if (!ctrl) break;
        if (!editable) {
          this.getToolkit().beep();
        } else {
          copy();
          insertText("");
        }
        break;
      case KeyEvent.VK_A:
        if (!ctrl) break;
        setKeyStart(-1);
        select(Integer.MAX_VALUE, 0, false);
        break;
      case KeyEvent.VK_RIGHT:
        setKeyStart(-1);
        tempSelection.set(selection);
        tempSelection.nextBound(ctrl ? wordBreaker : charBreaker, +1, shift);
        select(tempSelection);
        break;
      case KeyEvent.VK_LEFT:
        setKeyStart(-1);
        tempSelection.set(selection);
        tempSelection.nextBound(ctrl ? wordBreaker : charBreaker, -1, shift);
        select(tempSelection);
        break;
      case KeyEvent.VK_UP: // LIU: Add support for up arrow
        setKeyStart(-1);
        tempSelection.set(selection);
        tempSelection.caret = lineDelta(tempSelection.caret, -1);
        if (!shift) {
          tempSelection.anchor = tempSelection.caret;
        }
        select(tempSelection);
        break;
      case KeyEvent.VK_DOWN: // LIU: Add support for down arrow
        setKeyStart(-1);
        tempSelection.set(selection);
        tempSelection.caret = lineDelta(tempSelection.caret, +1);
        if (!shift) {
          tempSelection.anchor = tempSelection.caret;
        }
        select(tempSelection);
        break;
      case KeyEvent.VK_DELETE: // LIU: Add delete key support
        if (!editable) break;
        setKeyStart(-1);
        if (contents.length() == 0) break;
        start = selection.getStart();
        end = selection.getEnd();
        if (start == end) {
          ++end;
          if (end > contents.length()) {
            getToolkit().beep();
            return;
          }
        }
        replaceRange("", start, end);
        break;
    }
  }

  void copy() {
    Clipboard cb = this.getToolkit().getSystemClipboard();
    StringSelection ss =
        new StringSelection(contents.substring(selection.getStart(), selection.getEnd()));
    cb.setContents(ss, ss);
  }

  void paste() {
    Clipboard cb = this.getToolkit().getSystemClipboard();
    Transferable t = cb.getContents(this);
    if (t == null) {
      this.getToolkit().beep();
      return;
    }
    try {
      String temp = (String) t.getTransferData(DataFlavor.stringFlavor);
      insertText(temp);
    } catch (Exception e) {
      this.getToolkit().beep();
    }
  }

  /**
   * LIU: Given an offset into contents, moves up or down by lines, according to lineStarts[].
   *
   * @param off the offset into contents
   * @param delta how many lines to move up (< 0) or down (> 0)
   * @return the new offset into contents
   */
  private int lineDelta(int off, int delta) {
    int line = findLine(off, false);
    int posInLine = off - lineStarts[line];
    // System.out.println("off=" + off + " at " + line + ":" + posInLine);
    line += delta;
    if (line < 0) {
      line = posInLine = 0;
    } else if (line >= lineCount) {
      return contents.length();
    }
    off = lineStarts[line] + posInLine;
    if (off >= lineStarts[line + 1]) {
      off = lineStarts[line + 1] - 1;
    }
    return off;
  }

  public void keyReleased(KeyEvent e) {
    int code = e.getKeyCode();
    if (DEBUG)
      System.out.println("keyReleased " + hex((char) code) + ", " + hex((char) e.getModifiers()));
  }

  public void keyTyped(KeyEvent e) {
    char ch = e.getKeyChar();
    if (DEBUG)
      System.out.println("keyTyped " + hex((char) ch) + ", " + hex((char) e.getModifiers()));
    if ((e.getModifiers() & InputEvent.CTRL_MASK) != 0) return;
    int start, end;
    switch (ch) {
      case KeyEvent.CHAR_UNDEFINED:
        break;
      case KeyEvent.VK_BACK_SPACE:
        // setKeyStart(-1);
        if (!editable) break;
        if (contents.length() == 0) break;
        start = selection.getStart();
        end = selection.getEnd();
        if (start == end) {
          --start;
          if (start < 0) {
            getToolkit().beep(); // LIU: Add audio feedback of NOP
            return;
          }
        }
        replaceRange("", start, end);
        break;
      case KeyEvent.VK_DELETE:
        // setKeyStart(-1);
        if (!editable) break;
        if (contents.length() == 0) break;
        start = selection.getStart();
        end = selection.getEnd();
        if (start == end) {
          ++end;
          if (end > contents.length()) {
            getToolkit().beep(); // LIU: Add audio feedback of NOP
            return;
          }
        }
        replaceRange("", start, end);
        break;
      default:
        if (!editable) break;
        // LIU: Dispatch to subclass API
        handleKeyTyped(e);
        break;
    }
  }

  // LIU: Subclass API for handling of key typing
  protected void handleKeyTyped(KeyEvent e) {
    insertText(String.valueOf(e.getKeyChar()));
  }

  protected void setKeyStart(int keyStart) {
    if (activeStart != keyStart) {
      activeStart = keyStart;
      repaint(10);
    }
  }

  protected void validateKeyStart() {
    if (activeStart > selection.getStart()) {
      activeStart = selection.getStart();
      repaint(10);
    }
  }

  protected int getKeyStart() {
    return activeStart;
  }

  // ===================== Control ======================

  public synchronized void setEditable(boolean b) {
    editable = b;
  }

  public boolean isEditable() {
    return editable;
  }

  public void select(Selection newSelection) {
    newSelection.pin(contents);
    if (!selection.equals(newSelection)) {
      selection.set(newSelection);
      if (selectionListener != null) {
        selectionListener.actionPerformed(
            new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "Selection Changed", 0));
      }
      repaint(10);
      valid = false;
    }
  }

  public void select(int start, int end) {
    select(start, end, false);
  }

  public void select(int start, int end, boolean clickAfter) {
    tempSelection.set(start, end, clickAfter);
    select(tempSelection);
  }

  public int getSelectionStart() {
    return selection.getStart();
  }

  public int getSelectionEnd() {
    return selection.getEnd();
  }

  public void setBounds(int x, int y, int w, int h) {
    super.setBounds(x, y, w, h);
    redoLines = true;
  }

  public Dimension getPreferredSize() {
    return new Dimension(lastWidth, lastHeight);
  }

  public Dimension getMaximumSize() {
    return new Dimension(lastWidth, lastHeight);
  }

  public Dimension getMinimumSize() {
    return new Dimension(lastHeight, lastHeight);
  }

  public void setText(String text) {
    setText2(text);
    select(tempSelection.set(selection).pin(contents));
  }

  public void setText2(String text) {
    contents = text;
    charBreaker.setText(text);
    wordBreaker.setText(text);
    lineBreaker.setText(text);
    redoLines = true;
    if (textListener != null)
      textListener.textValueChanged(new TextEvent(this, TextEvent.TEXT_VALUE_CHANGED));
    repaint(16);
  }

  public void insertText(String text) {
    if (activeStart == -1) activeStart = selection.getStart();
    replaceRange(text, selection.getStart(), selection.getEnd());
  }

  public void replaceRange(String s, int start, int end) {
    setText2(contents.substring(0, start) + s + contents.substring(end));
    select(tempSelection.set(selection).fixAfterReplace(start, end, s.length()));
    validateKeyStart();
  }

  public String getText() {
    return contents;
  }

  public void setFont(Font font) {
    super.setFont(font);
    redoLines = true;
    repaint(16);
  }

  // ================== Graphics ======================

  public void update(Graphics g) {
    if (DEBUG) System.out.println("update");
    paint(g);
  }

  public void paint(Graphics g) {
    mySize = getSize();
    if (cacheImage == null
        || cacheImage.getHeight(this) != mySize.height
        || cacheImage.getWidth(this) != mySize.width) {
      cacheImage = createImage(mySize.width, mySize.height);
      valid = false;
    }
    if (!valid || redoLines) {
      if (DEBUG) System.out.println("painting");
      paint2(cacheImage.getGraphics());
      valid = true;
    }
    // getToolkit().sync();
    if (DEBUG) System.out.println("copying");
    g.drawImage(
        cacheImage, 0, 0, mySize.width, mySize.height, 0, 0, mySize.width, mySize.height, this);
  }

  public void paint2(Graphics g) {
    g.clearRect(0, 0, mySize.width, mySize.height);
    if (DEBUG) System.out.println("print");
    if (focus) g.setColor(Color.black);
    else g.setColor(Color.gray);
    g.drawRect(0, 0, mySize.width - 1, mySize.height - 1);
    g.setClip(1, 1, mySize.width - 2, mySize.height - 2);
    g.setColor(Color.black);
    g.setFont(getFont());
    fm = g.getFontMetrics();
    lineAscent = fm.getAscent();
    lineLeading = fm.getLeading();
    lineHeight = lineAscent + fm.getDescent() + lineLeading;
    int y = yInset + lineAscent;
    String lastSubstring = "";
    if (redoLines) fixLineStarts(mySize.width - xInset - xInset);
    for (int i = 0; i < lineCount; y += lineHeight, ++i) {
      // LIU: Don't display terminating ^M characters
      int lim = lineStarts[i + 1];
      if (lim > 0 && contents.length() > 0 && contents.charAt(lim - 1) == CR) --lim;
      lastSubstring = contents.substring(lineStarts[i], lim);
      g.drawString(lastSubstring, xInset, y);
    }
    drawSelection(g, lastSubstring);
    lastHeight = y + yInset - lineHeight + yInset;
    lastWidth = mySize.width - xInset - xInset;
  }

  void paintRect(Graphics g, int x, int y, int w, int h) {
    if (focus) {
      g.fillRect(x, y, w, h);
    } else {
      g.drawRect(x, y, w - 1, h - 1);
    }
  }

  public void drawSelection(Graphics g, String lastSubstring) {
    g.setXORMode(Color.black);
    if (activeStart != -1) {
      offset2Point(activeStart, false, activePoint);
      g.setColor(Color.magenta);
      int line = activePoint.x - 1;
      g.fillRect(line, activePoint.y, 1, lineHeight);
    }
    if (selection.isCaret()) {
      offset2Point(selection.caret, selection.clickAfter, caretPoint);
    } else {
      if (focus) g.setColor(Color.blue);
      else g.setColor(Color.yellow);
      offset2Point(selection.getStart(), true, startPoint);
      offset2Point(selection.getEnd(), false, endPoint);
      if (selection.getStart() == selection.caret) caretPoint.setLocation(startPoint);
      else caretPoint.setLocation(endPoint);
      if (startPoint.y == endPoint.y) {
        paintRect(
            g, startPoint.x, startPoint.y, Math.max(1, endPoint.x - startPoint.x), lineHeight);
      } else {
        paintRect(
            g, startPoint.x, startPoint.y, (mySize.width - xInset) - startPoint.x, lineHeight);
        if (startPoint.y + lineHeight < endPoint.y)
          paintRect(
              g,
              xInset,
              startPoint.y + lineHeight,
              (mySize.width - xInset) - xInset,
              endPoint.y - startPoint.y - lineHeight);
        paintRect(g, xInset, endPoint.y, endPoint.x - xInset, lineHeight);
      }
    }
    if (focus || selection.isCaret()) {
      if (focus) g.setColor(Color.green);
      else g.setColor(Color.red);
      int line = caretPoint.x - (selection.clickAfter ? 0 : 1);
      g.fillRect(line, caretPoint.y, 1, lineHeight);
      int w = lineHeight / 12 + 1;
      int braces = line - (selection.clickAfter ? -1 : w);
      g.fillRect(braces, caretPoint.y, w, 1);
      g.fillRect(braces, caretPoint.y + lineHeight - 1, w, 1);
    }
  }

  public Point offset2Point(int off, boolean start, Point p) {
    int line = findLine(off, start);
    int width = 0;
    try {
      width = fm.stringWidth(contents.substring(lineStarts[line], off));
    } catch (Exception e) {
      System.out.println(e);
    }
    p.x = width + xInset;
    if (p.x > mySize.width - xInset) p.x = mySize.width - xInset;
    p.y = lineHeight * line + yInset;
    return p;
  }

  private int findLine(int off, boolean start) {
    // if it is start, then go to the next line!
    if (start) ++off;
    for (int i = 1; i < lineCount; ++i) {
      // LIU: This was <= ; changed to < to make caret after
      // final CR in line appear at START of next line.
      if (off < lineStarts[i]) return i - 1;
    }
    // LIU: Check for special case; after CR at end of the last line
    if (off == lineStarts[lineCount]
        && off > 0
        && contents.length() > 0
        && contents.charAt(off - 1) == CR) {
      return lineCount;
    }
    return lineCount - 1;
  }

  // offsets on any line will go from start,true to end,false
  // excluding start,false and end,true
  public Selection point2Offset(Point p, Selection o) {
    if (p.y < yInset) {
      o.caret = 0;
      o.clickAfter = true;
      return o;
    }
    int line = (p.y - yInset) / lineHeight;
    if (line >= lineCount) {
      o.caret = contents.length();
      o.clickAfter = false;
      return o;
    }
    int target = p.x - xInset;
    if (target <= 0) {
      o.caret = lineStarts[line];
      o.clickAfter = true;
      return o;
    }
    int lowGuess = lineStarts[line];
    int lowWidth = 0;
    int highGuess = lineStarts[line + 1];
    int highWidth = fm.stringWidth(contents.substring(lineStarts[line], highGuess));
    if (target >= highWidth) {
      o.caret = lineStarts[line + 1];
      o.clickAfter = false;
      return o;
    }
    while (lowGuess < highGuess - 1) {
      int guess = (lowGuess + highGuess) / 2;
      int width = fm.stringWidth(contents.substring(lineStarts[line], guess));
      if (width <= target) {
        lowGuess = guess;
        lowWidth = width;
        if (width == target) break;
      } else {
        highGuess = guess;
        highWidth = width;
      }
    }
    // at end, either lowWidth < target < width(low+1), or lowWidth = target
    int highBound = charBreaker.following(lowGuess);
    int lowBound = charBreaker.previous();
    // we are now at character boundaries
    if (lowBound != lowGuess)
      lowWidth = fm.stringWidth(contents.substring(lineStarts[line], lowBound));
    if (highBound != highGuess)
      highWidth = fm.stringWidth(contents.substring(lineStarts[line], highBound));
    // we now have the right widths
    if (target - lowWidth < highWidth - target) {
      o.caret = lowBound;
      o.clickAfter = true;
    } else {
      o.caret = highBound;
      o.clickAfter = false;
    }
    // we now have the closest!
    return o;
  }

  private void fixLineStarts(int width) {
    lineCount = 1;
    lineStarts[0] = 0;
    if (contents.length() == 0) {
      lineStarts[1] = 0;
      return;
    }
    int end = 0;
    // LIU: Add check for MAX_LINES
    for (int start = 0; start < contents.length() && lineCount < MAX_LINES; start = end) {
      end = nextLine(fm, start, width);
      lineStarts[lineCount++] = end;
      if (end == start) { // LIU: Assertion
        throw new RuntimeException("nextLine broken");
      }
    }
    --lineCount;
    redoLines = false;
  }

  // LIU: Enhanced to wrap long lines.  Bug with return of start fixed.
  public int nextLine(FontMetrics fMtr, int start, int width) {
    int len = contents.length();
    for (int i = start; i < len; ++i) {
      // check for line separator
      char ch = (contents.charAt(i));
      if (ch >= 0x000A && ch <= 0x000D || ch == 0x2028 || ch == 0x2029) {
        len = i + 1;
        if (ch == 0x000D && i + 1 < len && contents.charAt(i + 1) == 0x000A) // crlf
        ++len; // grab extra char
        break;
      }
    }
    String subject = contents.substring(start, len);
    if (visibleWidth(fMtr, subject) <= width) return len;

    // LIU: Remainder of this method rewritten to accomodate lines
    // longer than the component width by first trying to break
    // into lines; then words; finally chars.
    int n = findFittingBreak(fMtr, subject, width, lineBreaker);
    if (n == 0) {
      n = findFittingBreak(fMtr, subject, width, wordBreaker);
    }
    if (n == 0) {
      n = findFittingBreak(fMtr, subject, width, charBreaker);
    }
    return n > 0 ? start + n : len;
  }

  /**
   * LIU: Finds the longest substring that fits a given width composed of subunits returned by a
   * BreakIterator. If the smallest subunit is too long, returns 0.
   *
   * @param fMtr metrics to use
   * @param line the string to be fix into width
   * @param width line.substring(0, result) must be <= width
   * @param breaker the BreakIterator that will be used to find subunits
   * @return maximum characters, at boundaries returned by breaker, that fit into width, or zero on
   *     failure
   */
  private int findFittingBreak(FontMetrics fMtr, String line, int width, BreakIterator breaker) {
    breaker.setText(line);
    int last = breaker.first();
    int end = breaker.next();
    while (end != BreakIterator.DONE && visibleWidth(fMtr, line.substring(0, end)) <= width) {
      last = end;
      end = breaker.next();
    }
    return last;
  }

  public int visibleWidth(FontMetrics fMtr, String s) {
    int i;
    for (i = s.length() - 1; i >= 0; --i) {
      char ch = s.charAt(i);
      if (!(ch == ' ' || ch >= 0x000A && ch <= 0x000D || ch == 0x2028 || ch == 0x2029))
        return fMtr.stringWidth(s.substring(0, i + 1));
    }
    return 0;
  }

  // =============== Utility ====================

  private void fixHex() {
    if (selection.getEnd() == 0) return;
    int store = 0;
    int places = 1;
    int count = 0;
    int min = Math.min(8, selection.getEnd());
    for (int i = 0; i < min; ++i) {
      char ch = contents.charAt(selection.getEnd() - 1 - i);
      int value = Character.getNumericValue(ch);
      if (value < 0 || value > 15) break;
      store += places * value;
      ++count;
      places *= 16;
    }
    String add = "";
    int bottom = store & 0xFFFF;
    if (store >= 0xD8000000
        && store < 0xDC000000
        && bottom >= 0xDC00
        && bottom < 0xE000) { // surrogates
      add = "" + (char) (store >> 16) + (char) bottom;
    } else if (store > 0xFFFF && store <= 0x10FFFF) {
      store -= 0x10000;
      add = "" + (char) (((store >> 10) & 0x3FF) + 0xD800) + (char) ((store & 0x3FF) + 0xDC00);

    } else if (count >= 4) {
      count = 4;
      add = "" + (char) (store & 0xFFFF);
    } else {
      count = 1;
      char ch = contents.charAt(selection.getEnd() - 1);
      add = hex(ch);
      if (ch >= 0xDC00 && ch <= 0xDFFF && selection.getEnd() > 1) {
        ch = contents.charAt(selection.getEnd() - 2);
        if (ch >= 0xD800 && ch <= 0xDBFF) {
          count = 2;
          add = hex(ch) + add;
        }
      }
    }
    replaceRange(add, selection.getEnd() - count, selection.getEnd());
  }

  public static String hex(char ch) {
    String result = Integer.toString(ch, 16).toUpperCase();
    result = "0000".substring(result.length(), 4) + result;
    return result;
  }
}