Home | History | Annotate | Download | only in impl
      1 //  2016 and later: Unicode, Inc. and others.
      2 // License & terms of use: http://www.unicode.org/copyright.html#License
      3 /*
      4  *******************************************************************************
      5  * Copyright (C) 1996-2010, International Business Machines Corporation and    *
      6  * others. All Rights Reserved.                                                *
      7  *******************************************************************************
      8  */
      9 package com.ibm.icu.dev.demo.impl;
     10 import java.awt.AWTEventMulticaster;
     11 import java.awt.Canvas;
     12 import java.awt.Color;
     13 import java.awt.Cursor;
     14 import java.awt.Dimension;
     15 import java.awt.Font;
     16 import java.awt.FontMetrics;
     17 import java.awt.Graphics;
     18 import java.awt.Image;
     19 import java.awt.Point;
     20 import java.awt.datatransfer.Clipboard;
     21 import java.awt.datatransfer.DataFlavor;
     22 import java.awt.datatransfer.StringSelection;
     23 import java.awt.datatransfer.Transferable;
     24 import java.awt.event.ActionEvent;
     25 import java.awt.event.ActionListener;
     26 import java.awt.event.FocusEvent;
     27 import java.awt.event.FocusListener;
     28 import java.awt.event.InputEvent;
     29 import java.awt.event.KeyEvent;
     30 import java.awt.event.KeyListener;
     31 import java.awt.event.MouseEvent;
     32 import java.awt.event.MouseListener;
     33 import java.awt.event.MouseMotionListener;
     34 import java.awt.event.TextEvent;
     35 import java.awt.event.TextListener;
     36 import java.text.BreakIterator;
     37 
     38 // LIU: Changed from final to non-final
     39 public class DumbTextComponent extends Canvas
     40   implements KeyListener, MouseListener, MouseMotionListener, FocusListener
     41 {
     42 
     43     /**
     44      * For serialization
     45      */
     46     private static final long serialVersionUID = 8265547730738652151L;
     47 
     48 //    private transient static final String copyright =
     49 //      "Copyright \u00A9 1998, Mark Davis. All Rights Reserved.";
     50     private transient static boolean DEBUG = false;
     51 
     52     private String contents = "";
     53     private Selection selection = new Selection();
     54     private int activeStart = -1;
     55     private boolean editable = true;
     56 
     57     private transient Selection tempSelection = new Selection();
     58     private transient boolean focus;
     59     private transient BreakIterator lineBreaker = BreakIterator.getLineInstance();
     60     private transient BreakIterator wordBreaker = BreakIterator.getWordInstance();
     61     private transient BreakIterator charBreaker = BreakIterator.getCharacterInstance();
     62     private transient int lineAscent;
     63     private transient int lineHeight;
     64     private transient int lineLeading;
     65     private transient int lastHeight = 10;
     66     private transient int lastWidth = 50;
     67     private static final int MAX_LINES = 200; // LIU: Use symbolic name
     68     private transient int[] lineStarts = new int[MAX_LINES]; // LIU
     69     private transient int lineCount = 1;
     70 
     71     private transient boolean valid = false;
     72     private transient FontMetrics fm;
     73     private transient boolean redoLines = true;
     74     private transient boolean doubleClick = false;
     75     private transient TextListener textListener;
     76     private transient ActionListener selectionListener;
     77     private transient Image cacheImage;
     78     private transient Dimension mySize;
     79     private transient int xInset = 5;
     80     private transient int yInset = 5;
     81     private transient Point startPoint = new Point();
     82     private transient Point endPoint = new Point();
     83     private transient Point caretPoint = new Point();
     84     private transient Point activePoint = new Point();
     85 
     86     //private transient static String clipBoard;
     87 
     88     private static final char CR = '\015'; // LIU
     89 
     90     // ============================================
     91 
     92     public DumbTextComponent() {
     93         addMouseListener(this);
     94         addMouseMotionListener(this);
     95         addKeyListener(this);
     96         addFocusListener(this);
     97         setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
     98 
     99     }
    100 
    101 // ================ Events ====================
    102 
    103     // public boolean isFocusTraversable() { return true; }
    104 
    105     public void addActionListener(ActionListener l) {
    106         selectionListener = AWTEventMulticaster.add(selectionListener, l);
    107     }
    108 
    109     public void removeActionListener(ActionListener l) {
    110         selectionListener = AWTEventMulticaster.remove(selectionListener, l);
    111     }
    112 
    113     public void addTextListener(TextListener l) {
    114         textListener = AWTEventMulticaster.add(textListener, l);
    115     }
    116 
    117     public void removeTextListener(TextListener l) {
    118         textListener = AWTEventMulticaster.remove(textListener, l);
    119     }
    120 
    121     private transient boolean pressed;
    122 
    123     public void mousePressed(MouseEvent e) {
    124         if (DEBUG) System.out.println("mousePressed");
    125         if (pressed) {
    126             select(e,false);
    127         } else {
    128             doubleClick = e.getClickCount() > 1;
    129             requestFocus();
    130             select(e, true);
    131             pressed = true;
    132         }
    133     }
    134 
    135     public void mouseDragged(MouseEvent e) {
    136         if (DEBUG) System.out.println("mouseDragged");
    137         select(e, false);
    138     }
    139 
    140     public void mouseReleased(MouseEvent e) {
    141         if (DEBUG) System.out.println("mouseReleased");
    142         pressed = false;
    143     }
    144 
    145     public void mouseEntered(MouseEvent e) {
    146         //if (pressed) select(e, false);
    147     }
    148 
    149     public void mouseExited(MouseEvent e){
    150         //if (pressed) select(e, false);
    151     }
    152 
    153     public void mouseClicked(MouseEvent e) {}
    154     public void mouseMoved(MouseEvent e) {}
    155 
    156 
    157     public void focusGained(FocusEvent e) {
    158         if (DEBUG) System.out.println("focusGained");
    159         focus = true;
    160         valid = false;
    161         repaint(16);
    162     }
    163     public void focusLost(FocusEvent e) {
    164         if (DEBUG) System.out.println("focusLost");
    165         focus = false;
    166         valid = false;
    167         repaint(16);
    168     }
    169 
    170     public void select(MouseEvent e, boolean first) {
    171         setKeyStart(-1);
    172         point2Offset(e.getPoint(), tempSelection);
    173         if (first) {
    174             if ((e.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
    175                 tempSelection.anchor = tempSelection.caret;
    176             }
    177         }
    178         // fix words
    179         if (doubleClick) {
    180             tempSelection.expand(wordBreaker);
    181         }
    182         select(tempSelection);
    183     }
    184 
    185     public void keyPressed(KeyEvent e) {
    186         int code = e.getKeyCode();
    187         if (DEBUG) System.out.println("keyPressed "
    188           + hex((char)code) + ", " + hex((char)e.getModifiers()));
    189         int start = selection.getStart();
    190         int end = selection.getEnd();
    191         boolean shift = (e.getModifiers() & InputEvent.SHIFT_MASK) != 0;
    192         boolean ctrl = (e.getModifiers() & InputEvent.CTRL_MASK) != 0;
    193 
    194         switch (code) {
    195         case KeyEvent.VK_Q:
    196             if (!ctrl || !editable) break;
    197             setKeyStart(-1);
    198             fixHex();
    199             break;
    200         case KeyEvent.VK_V:
    201             if (!ctrl) break;
    202             if (!editable) {
    203                 this.getToolkit().beep();
    204             } else {
    205                 paste();
    206             }
    207             break;
    208         case KeyEvent.VK_C:
    209             if (!ctrl) break;
    210             copy();
    211             break;
    212         case KeyEvent.VK_X:
    213             if (!ctrl) break;
    214             if (!editable) {
    215                 this.getToolkit().beep();
    216             } else {
    217                 copy();
    218                 insertText("");
    219             }
    220             break;
    221         case KeyEvent.VK_A:
    222             if (!ctrl) break;
    223             setKeyStart(-1);
    224             select(Integer.MAX_VALUE, 0, false);
    225             break;
    226         case KeyEvent.VK_RIGHT:
    227             setKeyStart(-1);
    228             tempSelection.set(selection);
    229             tempSelection.nextBound(ctrl ? wordBreaker : charBreaker, +1, shift);
    230             select(tempSelection);
    231             break;
    232         case KeyEvent.VK_LEFT:
    233             setKeyStart(-1);
    234             tempSelection.set(selection);
    235             tempSelection.nextBound(ctrl ? wordBreaker : charBreaker, -1, shift);
    236             select(tempSelection);
    237             break;
    238         case KeyEvent.VK_UP: // LIU: Add support for up arrow
    239             setKeyStart(-1);
    240             tempSelection.set(selection);
    241             tempSelection.caret = lineDelta(tempSelection.caret, -1);
    242             if (!shift) {
    243                 tempSelection.anchor = tempSelection.caret;
    244             }
    245             select(tempSelection);
    246             break;
    247         case KeyEvent.VK_DOWN: // LIU: Add support for down arrow
    248             setKeyStart(-1);
    249             tempSelection.set(selection);
    250             tempSelection.caret = lineDelta(tempSelection.caret, +1);
    251             if (!shift) {
    252                 tempSelection.anchor = tempSelection.caret;
    253             }
    254             select(tempSelection);
    255             break;
    256         case KeyEvent.VK_DELETE: // LIU: Add delete key support
    257             if (!editable) break;
    258             setKeyStart(-1);
    259             if (contents.length() == 0) break;
    260             start = selection.getStart();
    261             end = selection.getEnd();
    262             if (start == end) {
    263                 ++end;
    264                 if (end > contents.length()) {
    265                     getToolkit().beep();
    266                     return;
    267                 }
    268             }
    269             replaceRange("", start, end);
    270             break;
    271         }
    272     }
    273 
    274     void copy() {
    275         Clipboard cb = this.getToolkit().getSystemClipboard();
    276         StringSelection ss = new StringSelection(
    277             contents.substring(selection.getStart(), selection.getEnd()));
    278         cb.setContents(ss, ss);
    279     }
    280 
    281     void paste () {
    282         Clipboard cb = this.getToolkit().getSystemClipboard();
    283         Transferable t = cb.getContents(this);
    284         if (t == null) {
    285             this.getToolkit().beep();
    286             return;
    287         }
    288         try {
    289             String temp = (String) t.getTransferData(DataFlavor.stringFlavor);
    290             insertText(temp);
    291         } catch (Exception e) {
    292             this.getToolkit().beep();
    293         }
    294     }
    295 
    296     /**
    297      * LIU: Given an offset into contents, moves up or down by lines,
    298      * according to lineStarts[].
    299      * @param off the offset into contents
    300      * @param delta how many lines to move up (< 0) or down (> 0)
    301      * @return the new offset into contents
    302      */
    303     private int lineDelta(int off, int delta) {
    304         int line = findLine(off, false);
    305         int posInLine = off - lineStarts[line];
    306         // System.out.println("off=" + off + " at " + line + ":" + posInLine);
    307         line += delta;
    308         if (line < 0) {
    309             line = posInLine = 0;
    310         } else if (line >= lineCount) {
    311             return contents.length();
    312         }
    313         off = lineStarts[line] + posInLine;
    314         if (off >= lineStarts[line+1]) {
    315             off = lineStarts[line+1] - 1;
    316         }
    317         return off;
    318     }
    319 
    320     public void keyReleased(KeyEvent e) {
    321         int code = e.getKeyCode();
    322         if (DEBUG) System.out.println("keyReleased "
    323           + hex((char)code) + ", " + hex((char)e.getModifiers()));
    324     }
    325 
    326     public void keyTyped(KeyEvent e) {
    327         char ch = e.getKeyChar();
    328         if (DEBUG) System.out.println("keyTyped "
    329           + hex((char)ch) + ", " + hex((char)e.getModifiers()));
    330         if ((e.getModifiers() & InputEvent.CTRL_MASK) != 0) return;
    331         int start, end;
    332         switch (ch) {
    333         case KeyEvent.CHAR_UNDEFINED:
    334             break;
    335         case KeyEvent.VK_BACK_SPACE:
    336             //setKeyStart(-1);
    337             if (!editable) break;
    338             if (contents.length() == 0) break;
    339             start = selection.getStart();
    340             end = selection.getEnd();
    341             if (start == end) {
    342                 --start;
    343                 if (start < 0) {
    344                     getToolkit().beep(); // LIU: Add audio feedback of NOP
    345                     return;
    346                 }
    347             }
    348             replaceRange("", start, end);
    349             break;
    350         case KeyEvent.VK_DELETE:
    351             //setKeyStart(-1);
    352             if (!editable) break;
    353             if (contents.length() == 0) break;
    354             start = selection.getStart();
    355             end = selection.getEnd();
    356             if (start == end) {
    357                 ++end;
    358                 if (end > contents.length()) {
    359                     getToolkit().beep(); // LIU: Add audio feedback of NOP
    360                     return;
    361                 }
    362             }
    363             replaceRange("", start, end);
    364             break;
    365         default:
    366             if (!editable) break;
    367             // LIU: Dispatch to subclass API
    368             handleKeyTyped(e);
    369             break;
    370         }
    371     }
    372 
    373     // LIU: Subclass API for handling of key typing
    374     protected void handleKeyTyped(KeyEvent e) {
    375         insertText(String.valueOf(e.getKeyChar()));
    376     }
    377 
    378     protected void setKeyStart(int keyStart) {
    379         if (activeStart != keyStart) {
    380             activeStart = keyStart;
    381             repaint(10);
    382         }
    383     }
    384 
    385     protected void validateKeyStart() {
    386         if (activeStart > selection.getStart()) {
    387             activeStart = selection.getStart();
    388             repaint(10);
    389         }
    390     }
    391 
    392     protected int getKeyStart() {
    393         return activeStart;
    394     }
    395 
    396 // ===================== Control ======================
    397 
    398     public synchronized void setEditable(boolean b) {
    399         editable = b;
    400     }
    401 
    402     public boolean isEditable() {
    403         return editable;
    404     }
    405 
    406     public void select(Selection newSelection) {
    407         newSelection.pin(contents);
    408         if (!selection.equals(newSelection)) {
    409             selection.set(newSelection);
    410             if (selectionListener != null) {
    411                 selectionListener.actionPerformed(
    412                   new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
    413                     "Selection Changed", 0));
    414             }
    415             repaint(10);
    416             valid = false;
    417         }
    418     }
    419 
    420     public void select(int start, int end) {
    421         select(start, end, false);
    422     }
    423 
    424     public void select(int start, int end, boolean clickAfter) {
    425         tempSelection.set(start, end, clickAfter);
    426         select(tempSelection);
    427     }
    428 
    429     public int getSelectionStart() {
    430         return selection.getStart();
    431     }
    432 
    433     public int getSelectionEnd() {
    434         return selection.getEnd();
    435     }
    436 
    437     public void setBounds(int x, int y, int w, int h) {
    438         super.setBounds(x,y,w,h);
    439         redoLines = true;
    440     }
    441 
    442     public Dimension getPreferredSize() {
    443         return new Dimension(lastWidth,lastHeight);
    444     }
    445 
    446     public Dimension getMaximumSize() {
    447         return new Dimension(lastWidth,lastHeight);
    448     }
    449 
    450     public Dimension getMinimumSize() {
    451         return new Dimension(lastHeight,lastHeight);
    452     }
    453 
    454     public void setText(String text) {
    455         setText2(text);
    456         select(tempSelection.set(selection).pin(contents));
    457     }
    458 
    459     public void setText2(String text) {
    460         contents = text;
    461         charBreaker.setText(text);
    462         wordBreaker.setText(text);
    463         lineBreaker.setText(text);
    464         redoLines = true;
    465         if (textListener != null)
    466             textListener.textValueChanged(
    467               new TextEvent(this, TextEvent.TEXT_VALUE_CHANGED));
    468         repaint(16);
    469     }
    470 
    471     public void insertText(String text) {
    472         if (activeStart == -1) activeStart = selection.getStart();
    473         replaceRange(text, selection.getStart(), selection.getEnd());
    474     }
    475 
    476     public void replaceRange(String s, int start, int end) {
    477         setText2(contents.substring(0,start) + s
    478           + contents.substring(end));
    479         select(tempSelection.set(selection).
    480           fixAfterReplace(start, end, s.length()));
    481         validateKeyStart();
    482     }
    483 
    484     public String getText() {
    485         return contents;
    486     }
    487 
    488     public void setFont(Font font) {
    489         super.setFont(font);
    490         redoLines = true;
    491         repaint(16);
    492     }
    493 
    494     // ================== Graphics ======================
    495 
    496     public void update(Graphics g) {
    497         if (DEBUG) System.out.println("update");
    498         paint(g);
    499     }
    500 
    501     public void paint(Graphics g) {
    502         mySize = getSize();
    503         if (cacheImage == null
    504           || cacheImage.getHeight(this) != mySize.height
    505           || cacheImage.getWidth(this) != mySize.width) {
    506             cacheImage = createImage(mySize.width, mySize.height);
    507             valid = false;
    508         }
    509         if (!valid || redoLines) {
    510             if (DEBUG) System.out.println("painting");
    511             paint2(cacheImage.getGraphics());
    512             valid = true;
    513         }
    514         //getToolkit().sync();
    515         if (DEBUG) System.out.println("copying");
    516         g.drawImage(cacheImage,
    517           0, 0, mySize.width, mySize.height,
    518           0, 0, mySize.width, mySize.height,
    519           this);
    520     }
    521 
    522     public void paint2(Graphics g) {
    523         g.clearRect(0, 0, mySize.width, mySize.height);
    524         if (DEBUG) System.out.println("print");
    525         if (focus) g.setColor(Color.black);
    526         else g.setColor(Color.gray);
    527         g.drawRect(0,0,mySize.width-1,mySize.height-1);
    528         g.setClip(1,1,
    529           mySize.width-2,mySize.height-2);
    530         g.setColor(Color.black);
    531         g.setFont(getFont());
    532         fm = g.getFontMetrics();
    533         lineAscent = fm.getAscent();
    534         lineLeading = fm.getLeading();
    535         lineHeight = lineAscent + fm.getDescent() + lineLeading;
    536         int y = yInset + lineAscent;
    537         String lastSubstring = "";
    538         if (redoLines) fixLineStarts(mySize.width-xInset-xInset);
    539         for (int i = 0; i < lineCount; y += lineHeight, ++i) {
    540             // LIU: Don't display terminating ^M characters
    541             int lim = lineStarts[i+1];
    542             if (lim > 0 && contents.length() > 0 &&
    543                 contents.charAt(lim-1) == CR) --lim;
    544             lastSubstring = contents.substring(lineStarts[i],lim);
    545             g.drawString(lastSubstring, xInset, y);
    546         }
    547         drawSelection(g, lastSubstring);
    548         lastHeight = y + yInset - lineHeight + yInset;
    549         lastWidth = mySize.width-xInset-xInset;
    550     }
    551 
    552     void paintRect(Graphics g, int x, int y, int w, int h) {
    553         if (focus) {
    554             g.fillRect(x, y, w, h);
    555         } else {
    556             g.drawRect(x, y, w-1, h-1);
    557         }
    558     }
    559 
    560     public void drawSelection(Graphics g, String lastSubstring) {
    561         g.setXORMode(Color.black);
    562         if (activeStart != -1) {
    563             offset2Point(activeStart, false, activePoint);
    564             g.setColor(Color.magenta);
    565             int line = activePoint.x - 1;
    566             g.fillRect(line, activePoint.y, 1, lineHeight);
    567         }
    568         if (selection.isCaret()) {
    569             offset2Point(selection.caret, selection.clickAfter, caretPoint);
    570         } else {
    571             if (focus) g.setColor(Color.blue);
    572             else g.setColor(Color.yellow);
    573             offset2Point(selection.getStart(), true, startPoint);
    574             offset2Point(selection.getEnd(), false, endPoint);
    575             if (selection.getStart() == selection.caret)
    576                 caretPoint.setLocation(startPoint);
    577             else caretPoint.setLocation(endPoint);
    578             if (startPoint.y == endPoint.y) {
    579                 paintRect(g, startPoint.x, startPoint.y,
    580                   Math.max(1,endPoint.x-startPoint.x), lineHeight);
    581             } else {
    582                 paintRect(g, startPoint.x, startPoint.y,
    583                   (mySize.width-xInset)-startPoint.x, lineHeight);
    584                 if (startPoint.y + lineHeight < endPoint.y)
    585                   paintRect(g, xInset, startPoint.y + lineHeight,
    586                   (mySize.width-xInset)-xInset, endPoint.y - startPoint.y - lineHeight);
    587                 paintRect(g, xInset, endPoint.y, endPoint.x-xInset, lineHeight);
    588             }
    589         }
    590         if (focus || selection.isCaret()) {
    591             if (focus) g.setColor(Color.green);
    592             else g.setColor(Color.red);
    593             int line = caretPoint.x - (selection.clickAfter ? 0 : 1);
    594             g.fillRect(line, caretPoint.y, 1, lineHeight);
    595             int w = lineHeight/12 + 1;
    596             int braces = line - (selection.clickAfter ? -1 : w);
    597             g.fillRect(braces, caretPoint.y, w, 1);
    598             g.fillRect(braces, caretPoint.y + lineHeight - 1, w, 1);
    599         }
    600     }
    601 
    602     public Point offset2Point(int off, boolean start, Point p) {
    603         int line = findLine(off, start);
    604         int width = 0;
    605         try {
    606             width = fm.stringWidth(
    607               contents.substring(lineStarts[line], off));
    608         } catch (Exception e) {
    609             System.out.println(e);
    610         }
    611         p.x = width + xInset;
    612         if (p.x > mySize.width - xInset)
    613             p.x = mySize.width - xInset;
    614         p.y = lineHeight * line + yInset;
    615         return p;
    616     }
    617 
    618     private int findLine(int off, boolean start) {
    619         // if it is start, then go to the next line!
    620         if (start) ++off;
    621         for (int i = 1; i < lineCount; ++i) {
    622             // LIU: This was <= ; changed to < to make caret after
    623             // final CR in line appear at START of next line.
    624             if (off < lineStarts[i]) return i-1;
    625         }
    626         // LIU: Check for special case; after CR at end of the last line
    627         if (off == lineStarts[lineCount] &&
    628             off > 0 && contents.length() > 0 && contents.charAt(off-1) == CR) {
    629             return lineCount;
    630         }
    631         return lineCount-1;
    632     }
    633 
    634     // offsets on any line will go from start,true to end,false
    635     // excluding start,false and end,true
    636     public Selection point2Offset(Point p, Selection o) {
    637         if (p.y < yInset) {
    638             o.caret = 0;
    639             o.clickAfter = true;
    640             return o;
    641         }
    642         int line = (p.y - yInset)/lineHeight;
    643         if (line >= lineCount) {
    644             o.caret = contents.length();
    645             o.clickAfter = false;
    646             return o;
    647         }
    648         int target = p.x - xInset;
    649         if (target <= 0) {
    650             o.caret = lineStarts[line];
    651             o.clickAfter = true;
    652             return o;
    653         }
    654         int lowGuess = lineStarts[line];
    655         int lowWidth = 0;
    656         int highGuess = lineStarts[line+1];
    657         int highWidth = fm.stringWidth(contents.substring(lineStarts[line],highGuess));
    658         if (target >= highWidth) {
    659             o.caret = lineStarts[line+1];
    660             o.clickAfter = false;
    661             return o;
    662         }
    663         while (lowGuess < highGuess - 1) {
    664             int guess = (lowGuess + highGuess)/2;
    665             int width = fm.stringWidth(contents.substring(lineStarts[line],guess));
    666             if (width <= target) {
    667                 lowGuess = guess;
    668                 lowWidth = width;
    669                 if (width == target) break;
    670             } else {
    671                 highGuess = guess;
    672                 highWidth = width;
    673             }
    674         }
    675         // at end, either lowWidth < target < width(low+1), or lowWidth = target
    676         int highBound = charBreaker.following(lowGuess);
    677         int lowBound = charBreaker.previous();
    678         // we are now at character boundaries
    679         if (lowBound != lowGuess)
    680             lowWidth = fm.stringWidth(contents.substring(lineStarts[line],lowBound));
    681         if (highBound != highGuess)
    682             highWidth = fm.stringWidth(contents.substring(lineStarts[line],highBound));
    683         // we now have the right widths
    684         if (target - lowWidth < highWidth - target) {
    685             o.caret = lowBound;
    686             o.clickAfter = true;
    687         } else {
    688             o.caret = highBound;
    689             o.clickAfter = false;
    690         }
    691         // we now have the closest!
    692         return o;
    693     }
    694 
    695     private void fixLineStarts(int width) {
    696         lineCount = 1;
    697         lineStarts[0] = 0;
    698         if (contents.length() == 0) {
    699             lineStarts[1] = 0;
    700             return;
    701         }
    702         int end = 0;
    703         // LIU: Add check for MAX_LINES
    704         for (int start = 0; start < contents.length() && lineCount < MAX_LINES;
    705              start = end) {
    706             end = nextLine(fm, start, width);
    707             lineStarts[lineCount++] = end;
    708             if (end == start) { // LIU: Assertion
    709                 throw new RuntimeException("nextLine broken");
    710             }
    711         }
    712         --lineCount;
    713         redoLines = false;
    714     }
    715 
    716     // LIU: Enhanced to wrap long lines.  Bug with return of start fixed.
    717     public int nextLine(FontMetrics fMtr, int start, int width) {
    718         int len = contents.length();
    719         for (int i = start; i < len; ++i) {
    720             // check for line separator
    721             char ch = (contents.charAt(i));
    722             if (ch >= 0x000A && ch <= 0x000D || ch == 0x2028 || ch == 0x2029) {
    723                 len = i + 1;
    724                 if (ch == 0x000D && i+1 < len && contents.charAt(i+1) == 0x000A) // crlf
    725                     ++len; // grab extra char
    726                 break;
    727             }
    728         }
    729         String subject = contents.substring(start,len);
    730         if (visibleWidth(fMtr, subject) <= width)
    731           return len;
    732 
    733         // LIU: Remainder of this method rewritten to accomodate lines
    734         // longer than the component width by first trying to break
    735         // into lines; then words; finally chars.
    736         int n = findFittingBreak(fMtr, subject, width, lineBreaker);
    737         if (n == 0) {
    738             n = findFittingBreak(fMtr, subject, width, wordBreaker);
    739         }
    740         if (n == 0) {
    741             n = findFittingBreak(fMtr, subject, width, charBreaker);
    742         }
    743         return n > 0 ? start + n : len;
    744     }
    745 
    746     /**
    747      * LIU: Finds the longest substring that fits a given width
    748      * composed of subunits returned by a BreakIterator.  If the smallest
    749      * subunit is too long, returns 0.
    750      * @param fMtr metrics to use
    751      * @param line the string to be fix into width
    752      * @param width line.substring(0, result) must be <= width
    753      * @param breaker the BreakIterator that will be used to find subunits
    754      * @return maximum characters, at boundaries returned by breaker,
    755      * that fit into width, or zero on failure
    756      */
    757     private int findFittingBreak(FontMetrics fMtr, String line, int width,
    758                                  BreakIterator breaker) {
    759         breaker.setText(line);
    760         int last = breaker.first();
    761         int end = breaker.next();
    762         while (end != BreakIterator.DONE &&
    763                visibleWidth(fMtr, line.substring(0, end)) <= width) {
    764             last = end;
    765             end = breaker.next();
    766         }
    767         return last;
    768     }
    769 
    770     public int visibleWidth(FontMetrics fMtr, String s) {
    771         int i;
    772         for (i = s.length()-1; i >= 0; --i) {
    773             char ch = s.charAt(i);
    774             if (!(ch == ' ' || ch >= 0x000A && ch <= 0x000D || ch == 0x2028 || ch == 0x2029))
    775                 return fMtr.stringWidth(s.substring(0,i+1));
    776         }
    777         return 0;
    778     }
    779 
    780 // =============== Utility ====================
    781 
    782     private void fixHex() {
    783         if (selection.getEnd() == 0) return;
    784         int store = 0;
    785         int places = 1;
    786         int count = 0;
    787         int min = Math.min(8,selection.getEnd());
    788         for (int i = 0; i < min; ++i) {
    789             char ch = contents.charAt(selection.getEnd()-1-i);
    790             int value = Character.getNumericValue(ch);
    791             if (value < 0 || value > 15) break;
    792             store += places * value;
    793             ++count;
    794             places *= 16;
    795         }
    796         String add = "";
    797         int bottom = store & 0xFFFF;
    798         if (store >= 0xD8000000 && store < 0xDC000000
    799           && bottom >= 0xDC00 && bottom < 0xE000) { // surrogates
    800             add = "" + (char)(store >> 16) + (char)bottom;
    801         } else if (store > 0xFFFF && store <= 0x10FFFF) {
    802             store -= 0x10000;
    803             add = "" + (char)(((store >> 10) & 0x3FF) + 0xD800)
    804               + (char)((store & 0x3FF) + 0xDC00);
    805 
    806         } else if (count >= 4) {
    807             count = 4;
    808             add = ""+(char)(store & 0xFFFF);
    809         } else {
    810             count = 1;
    811             char ch = contents.charAt(selection.getEnd()-1);
    812             add = hex(ch);
    813             if (ch >= 0xDC00 && ch <= 0xDFFF && selection.getEnd() > 1) {
    814                 ch = contents.charAt(selection.getEnd()-2);
    815                 if (ch >= 0xD800 && ch <= 0xDBFF) {
    816                     count = 2;
    817                     add = hex(ch) + add;
    818                 }
    819             }
    820         }
    821         replaceRange(add, selection.getEnd()-count, selection.getEnd());
    822     }
    823 
    824     public static String hex(char ch) {
    825         String result = Integer.toString(ch,16).toUpperCase();
    826         result = "0000".substring(result.length(),4) + result;
    827         return result;
    828     }
    829 }
    830