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