1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.text;
18
19import android.graphics.Paint;
20import android.text.style.UpdateLayout;
21import android.text.style.WrapTogetherSpan;
22
23import com.android.internal.util.ArrayUtils;
24
25import java.lang.ref.WeakReference;
26
27/**
28 * DynamicLayout is a text layout that updates itself as the text is edited.
29 * <p>This is used by widgets to control text layout. You should not need
30 * to use this class directly unless you are implementing your own widget
31 * or custom display object, or need to call
32 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
33 *  Canvas.drawText()} directly.</p>
34 */
35public class DynamicLayout extends Layout
36{
37    private static final int PRIORITY = 128;
38    private static final int BLOCK_MINIMUM_CHARACTER_LENGTH = 400;
39
40    /**
41     * Make a layout for the specified text that will be updated as
42     * the text is changed.
43     */
44    public DynamicLayout(CharSequence base,
45                         TextPaint paint,
46                         int width, Alignment align,
47                         float spacingmult, float spacingadd,
48                         boolean includepad) {
49        this(base, base, paint, width, align, spacingmult, spacingadd,
50             includepad);
51    }
52
53    /**
54     * Make a layout for the transformed text (password transformation
55     * being the primary example of a transformation)
56     * that will be updated as the base text is changed.
57     */
58    public DynamicLayout(CharSequence base, CharSequence display,
59                         TextPaint paint,
60                         int width, Alignment align,
61                         float spacingmult, float spacingadd,
62                         boolean includepad) {
63        this(base, display, paint, width, align, spacingmult, spacingadd,
64             includepad, null, 0);
65    }
66
67    /**
68     * Make a layout for the transformed text (password transformation
69     * being the primary example of a transformation)
70     * that will be updated as the base text is changed.
71     * If ellipsize is non-null, the Layout will ellipsize the text
72     * down to ellipsizedWidth.
73     */
74    public DynamicLayout(CharSequence base, CharSequence display,
75                         TextPaint paint,
76                         int width, Alignment align,
77                         float spacingmult, float spacingadd,
78                         boolean includepad,
79                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
80        this(base, display, paint, width, align, TextDirectionHeuristics.FIRSTSTRONG_LTR,
81                spacingmult, spacingadd, includepad, ellipsize, ellipsizedWidth);
82    }
83
84    /**
85     * Make a layout for the transformed text (password transformation
86     * being the primary example of a transformation)
87     * that will be updated as the base text is changed.
88     * If ellipsize is non-null, the Layout will ellipsize the text
89     * down to ellipsizedWidth.
90     * *
91     * *@hide
92     */
93    public DynamicLayout(CharSequence base, CharSequence display,
94                         TextPaint paint,
95                         int width, Alignment align, TextDirectionHeuristic textDir,
96                         float spacingmult, float spacingadd,
97                         boolean includepad,
98                         TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
99        super((ellipsize == null)
100                ? display
101                : (display instanceof Spanned)
102                    ? new SpannedEllipsizer(display)
103                    : new Ellipsizer(display),
104              paint, width, align, textDir, spacingmult, spacingadd);
105
106        mBase = base;
107        mDisplay = display;
108
109        if (ellipsize != null) {
110            mInts = new PackedIntVector(COLUMNS_ELLIPSIZE);
111            mEllipsizedWidth = ellipsizedWidth;
112            mEllipsizeAt = ellipsize;
113        } else {
114            mInts = new PackedIntVector(COLUMNS_NORMAL);
115            mEllipsizedWidth = width;
116            mEllipsizeAt = null;
117        }
118
119        mObjects = new PackedObjectVector<Directions>(1);
120
121        mIncludePad = includepad;
122
123        /*
124         * This is annoying, but we can't refer to the layout until
125         * superclass construction is finished, and the superclass
126         * constructor wants the reference to the display text.
127         *
128         * This will break if the superclass constructor ever actually
129         * cares about the content instead of just holding the reference.
130         */
131        if (ellipsize != null) {
132            Ellipsizer e = (Ellipsizer) getText();
133
134            e.mLayout = this;
135            e.mWidth = ellipsizedWidth;
136            e.mMethod = ellipsize;
137            mEllipsize = true;
138        }
139
140        // Initial state is a single line with 0 characters (0 to 0),
141        // with top at 0 and bottom at whatever is natural, and
142        // undefined ellipsis.
143
144        int[] start;
145
146        if (ellipsize != null) {
147            start = new int[COLUMNS_ELLIPSIZE];
148            start[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
149        } else {
150            start = new int[COLUMNS_NORMAL];
151        }
152
153        Directions[] dirs = new Directions[] { DIRS_ALL_LEFT_TO_RIGHT };
154
155        Paint.FontMetricsInt fm = paint.getFontMetricsInt();
156        int asc = fm.ascent;
157        int desc = fm.descent;
158
159        start[DIR] = DIR_LEFT_TO_RIGHT << DIR_SHIFT;
160        start[TOP] = 0;
161        start[DESCENT] = desc;
162        mInts.insertAt(0, start);
163
164        start[TOP] = desc - asc;
165        mInts.insertAt(1, start);
166
167        mObjects.insertAt(0, dirs);
168
169        // Update from 0 characters to whatever the real text is
170        reflow(base, 0, 0, base.length());
171
172        if (base instanceof Spannable) {
173            if (mWatcher == null)
174                mWatcher = new ChangeWatcher(this);
175
176            // Strip out any watchers for other DynamicLayouts.
177            Spannable sp = (Spannable) base;
178            ChangeWatcher[] spans = sp.getSpans(0, sp.length(), ChangeWatcher.class);
179            for (int i = 0; i < spans.length; i++)
180                sp.removeSpan(spans[i]);
181
182            sp.setSpan(mWatcher, 0, base.length(),
183                       Spannable.SPAN_INCLUSIVE_INCLUSIVE |
184                       (PRIORITY << Spannable.SPAN_PRIORITY_SHIFT));
185        }
186    }
187
188    private void reflow(CharSequence s, int where, int before, int after) {
189        if (s != mBase)
190            return;
191
192        CharSequence text = mDisplay;
193        int len = text.length();
194
195        // seek back to the start of the paragraph
196
197        int find = TextUtils.lastIndexOf(text, '\n', where - 1);
198        if (find < 0)
199            find = 0;
200        else
201            find = find + 1;
202
203        {
204            int diff = where - find;
205            before += diff;
206            after += diff;
207            where -= diff;
208        }
209
210        // seek forward to the end of the paragraph
211
212        int look = TextUtils.indexOf(text, '\n', where + after);
213        if (look < 0)
214            look = len;
215        else
216            look++; // we want the index after the \n
217
218        int change = look - (where + after);
219        before += change;
220        after += change;
221
222        // seek further out to cover anything that is forced to wrap together
223
224        if (text instanceof Spanned) {
225            Spanned sp = (Spanned) text;
226            boolean again;
227
228            do {
229                again = false;
230
231                Object[] force = sp.getSpans(where, where + after,
232                                             WrapTogetherSpan.class);
233
234                for (int i = 0; i < force.length; i++) {
235                    int st = sp.getSpanStart(force[i]);
236                    int en = sp.getSpanEnd(force[i]);
237
238                    if (st < where) {
239                        again = true;
240
241                        int diff = where - st;
242                        before += diff;
243                        after += diff;
244                        where -= diff;
245                    }
246
247                    if (en > where + after) {
248                        again = true;
249
250                        int diff = en - (where + after);
251                        before += diff;
252                        after += diff;
253                    }
254                }
255            } while (again);
256        }
257
258        // find affected region of old layout
259
260        int startline = getLineForOffset(where);
261        int startv = getLineTop(startline);
262
263        int endline = getLineForOffset(where + before);
264        if (where + after == len)
265            endline = getLineCount();
266        int endv = getLineTop(endline);
267        boolean islast = (endline == getLineCount());
268
269        // generate new layout for affected text
270
271        StaticLayout reflowed;
272
273        synchronized (sLock) {
274            reflowed = sStaticLayout;
275            sStaticLayout = null;
276        }
277
278        if (reflowed == null) {
279            reflowed = new StaticLayout(null);
280        } else {
281            reflowed.prepare();
282        }
283
284        reflowed.generate(text, where, where + after,
285                getPaint(), getWidth(), getTextDirectionHeuristic(), getSpacingMultiplier(),
286                getSpacingAdd(), false,
287                true, mEllipsizedWidth, mEllipsizeAt);
288        int n = reflowed.getLineCount();
289
290        // If the new layout has a blank line at the end, but it is not
291        // the very end of the buffer, then we already have a line that
292        // starts there, so disregard the blank line.
293
294        if (where + after != len && reflowed.getLineStart(n - 1) == where + after)
295            n--;
296
297        // remove affected lines from old layout
298        mInts.deleteAt(startline, endline - startline);
299        mObjects.deleteAt(startline, endline - startline);
300
301        // adjust offsets in layout for new height and offsets
302
303        int ht = reflowed.getLineTop(n);
304        int toppad = 0, botpad = 0;
305
306        if (mIncludePad && startline == 0) {
307            toppad = reflowed.getTopPadding();
308            mTopPadding = toppad;
309            ht -= toppad;
310        }
311        if (mIncludePad && islast) {
312            botpad = reflowed.getBottomPadding();
313            mBottomPadding = botpad;
314            ht += botpad;
315        }
316
317        mInts.adjustValuesBelow(startline, START, after - before);
318        mInts.adjustValuesBelow(startline, TOP, startv - endv + ht);
319
320        // insert new layout
321
322        int[] ints;
323
324        if (mEllipsize) {
325            ints = new int[COLUMNS_ELLIPSIZE];
326            ints[ELLIPSIS_START] = ELLIPSIS_UNDEFINED;
327        } else {
328            ints = new int[COLUMNS_NORMAL];
329        }
330
331        Directions[] objects = new Directions[1];
332
333        for (int i = 0; i < n; i++) {
334            ints[START] = reflowed.getLineStart(i) |
335                          (reflowed.getParagraphDirection(i) << DIR_SHIFT) |
336                          (reflowed.getLineContainsTab(i) ? TAB_MASK : 0);
337
338            int top = reflowed.getLineTop(i) + startv;
339            if (i > 0)
340                top -= toppad;
341            ints[TOP] = top;
342
343            int desc = reflowed.getLineDescent(i);
344            if (i == n - 1)
345                desc += botpad;
346
347            ints[DESCENT] = desc;
348            objects[0] = reflowed.getLineDirections(i);
349
350            if (mEllipsize) {
351                ints[ELLIPSIS_START] = reflowed.getEllipsisStart(i);
352                ints[ELLIPSIS_COUNT] = reflowed.getEllipsisCount(i);
353            }
354
355            mInts.insertAt(startline + i, ints);
356            mObjects.insertAt(startline + i, objects);
357        }
358
359        updateBlocks(startline, endline - 1, n);
360
361        synchronized (sLock) {
362            sStaticLayout = reflowed;
363            reflowed.finish();
364        }
365    }
366
367    /**
368     * Create the initial block structure, cutting the text into blocks of at least
369     * BLOCK_MINIMUM_CHARACTER_SIZE characters, aligned on the ends of paragraphs.
370     */
371    private void createBlocks() {
372        int offset = BLOCK_MINIMUM_CHARACTER_LENGTH;
373        mNumberOfBlocks = 0;
374        final CharSequence text = mDisplay;
375
376        while (true) {
377            offset = TextUtils.indexOf(text, '\n', offset);
378            if (offset < 0) {
379                addBlockAtOffset(text.length());
380                break;
381            } else {
382                addBlockAtOffset(offset);
383                offset += BLOCK_MINIMUM_CHARACTER_LENGTH;
384            }
385        }
386
387        // mBlockIndices and mBlockEndLines should have the same length
388        mBlockIndices = new int[mBlockEndLines.length];
389        for (int i = 0; i < mBlockEndLines.length; i++) {
390            mBlockIndices[i] = INVALID_BLOCK_INDEX;
391        }
392    }
393
394    /**
395     * Create a new block, ending at the specified character offset.
396     * A block will actually be created only if has at least one line, i.e. this offset is
397     * not on the end line of the previous block.
398     */
399    private void addBlockAtOffset(int offset) {
400        final int line = getLineForOffset(offset);
401
402        if (mBlockEndLines == null) {
403            // Initial creation of the array, no test on previous block ending line
404            mBlockEndLines = new int[ArrayUtils.idealIntArraySize(1)];
405            mBlockEndLines[mNumberOfBlocks] = line;
406            mNumberOfBlocks++;
407            return;
408        }
409
410        final int previousBlockEndLine = mBlockEndLines[mNumberOfBlocks - 1];
411        if (line > previousBlockEndLine) {
412            if (mNumberOfBlocks == mBlockEndLines.length) {
413                // Grow the array if needed
414                int[] blockEndLines = new int[ArrayUtils.idealIntArraySize(mNumberOfBlocks + 1)];
415                System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, mNumberOfBlocks);
416                mBlockEndLines = blockEndLines;
417            }
418            mBlockEndLines[mNumberOfBlocks] = line;
419            mNumberOfBlocks++;
420        }
421    }
422
423    /**
424     * This method is called every time the layout is reflowed after an edition.
425     * It updates the internal block data structure. The text is split in blocks
426     * of contiguous lines, with at least one block for the entire text.
427     * When a range of lines is edited, new blocks (from 0 to 3 depending on the
428     * overlap structure) will replace the set of overlapping blocks.
429     * Blocks are listed in order and are represented by their ending line number.
430     * An index is associated to each block (which will be used by display lists),
431     * this class simply invalidates the index of blocks overlapping a modification.
432     *
433     * This method is package private and not private so that it can be tested.
434     *
435     * @param startLine the first line of the range of modified lines
436     * @param endLine the last line of the range, possibly equal to startLine, lower
437     * than getLineCount()
438     * @param newLineCount the number of lines that will replace the range, possibly 0
439     *
440     * @hide
441     */
442    void updateBlocks(int startLine, int endLine, int newLineCount) {
443        if (mBlockEndLines == null) {
444            createBlocks();
445            return;
446        }
447
448        int firstBlock = -1;
449        int lastBlock = -1;
450        for (int i = 0; i < mNumberOfBlocks; i++) {
451            if (mBlockEndLines[i] >= startLine) {
452                firstBlock = i;
453                break;
454            }
455        }
456        for (int i = firstBlock; i < mNumberOfBlocks; i++) {
457            if (mBlockEndLines[i] >= endLine) {
458                lastBlock = i;
459                break;
460            }
461        }
462        final int lastBlockEndLine = mBlockEndLines[lastBlock];
463
464        boolean createBlockBefore = startLine > (firstBlock == 0 ? 0 :
465                mBlockEndLines[firstBlock - 1] + 1);
466        boolean createBlock = newLineCount > 0;
467        boolean createBlockAfter = endLine < mBlockEndLines[lastBlock];
468
469        int numAddedBlocks = 0;
470        if (createBlockBefore) numAddedBlocks++;
471        if (createBlock) numAddedBlocks++;
472        if (createBlockAfter) numAddedBlocks++;
473
474        final int numRemovedBlocks = lastBlock - firstBlock + 1;
475        final int newNumberOfBlocks = mNumberOfBlocks + numAddedBlocks - numRemovedBlocks;
476
477        if (newNumberOfBlocks == 0) {
478            // Even when text is empty, there is actually one line and hence one block
479            mBlockEndLines[0] = 0;
480            mBlockIndices[0] = INVALID_BLOCK_INDEX;
481            mNumberOfBlocks = 1;
482            return;
483        }
484
485        if (newNumberOfBlocks > mBlockEndLines.length) {
486            final int newSize = ArrayUtils.idealIntArraySize(newNumberOfBlocks);
487            int[] blockEndLines = new int[newSize];
488            int[] blockIndices = new int[newSize];
489            System.arraycopy(mBlockEndLines, 0, blockEndLines, 0, firstBlock);
490            System.arraycopy(mBlockIndices, 0, blockIndices, 0, firstBlock);
491            System.arraycopy(mBlockEndLines, lastBlock + 1,
492                    blockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
493            System.arraycopy(mBlockIndices, lastBlock + 1,
494                    blockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
495            mBlockEndLines = blockEndLines;
496            mBlockIndices = blockIndices;
497        } else {
498            System.arraycopy(mBlockEndLines, lastBlock + 1,
499                    mBlockEndLines, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
500            System.arraycopy(mBlockIndices, lastBlock + 1,
501                    mBlockIndices, firstBlock + numAddedBlocks, mNumberOfBlocks - lastBlock - 1);
502        }
503
504        mNumberOfBlocks = newNumberOfBlocks;
505        int newFirstChangedBlock;
506        final int deltaLines = newLineCount - (endLine - startLine + 1);
507        if (deltaLines != 0) {
508            // Display list whose index is >= mIndexFirstChangedBlock is valid
509            // but it needs to update its drawing location.
510            newFirstChangedBlock = firstBlock + numAddedBlocks;
511            for (int i = newFirstChangedBlock; i < mNumberOfBlocks; i++) {
512                mBlockEndLines[i] += deltaLines;
513            }
514        } else {
515            newFirstChangedBlock = mNumberOfBlocks;
516        }
517        mIndexFirstChangedBlock = Math.min(mIndexFirstChangedBlock, newFirstChangedBlock);
518
519        int blockIndex = firstBlock;
520        if (createBlockBefore) {
521            mBlockEndLines[blockIndex] = startLine - 1;
522            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
523            blockIndex++;
524        }
525
526        if (createBlock) {
527            mBlockEndLines[blockIndex] = startLine + newLineCount - 1;
528            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
529            blockIndex++;
530        }
531
532        if (createBlockAfter) {
533            mBlockEndLines[blockIndex] = lastBlockEndLine + deltaLines;
534            mBlockIndices[blockIndex] = INVALID_BLOCK_INDEX;
535        }
536    }
537
538    /**
539     * This package private method is used for test purposes only
540     * @hide
541     */
542    void setBlocksDataForTest(int[] blockEndLines, int[] blockIndices, int numberOfBlocks) {
543        mBlockEndLines = new int[blockEndLines.length];
544        mBlockIndices = new int[blockIndices.length];
545        System.arraycopy(blockEndLines, 0, mBlockEndLines, 0, blockEndLines.length);
546        System.arraycopy(blockIndices, 0, mBlockIndices, 0, blockIndices.length);
547        mNumberOfBlocks = numberOfBlocks;
548    }
549
550    /**
551     * @hide
552     */
553    public int[] getBlockEndLines() {
554        return mBlockEndLines;
555    }
556
557    /**
558     * @hide
559     */
560    public int[] getBlockIndices() {
561        return mBlockIndices;
562    }
563
564    /**
565     * @hide
566     */
567    public int getNumberOfBlocks() {
568        return mNumberOfBlocks;
569    }
570
571    /**
572     * @hide
573     */
574    public int getIndexFirstChangedBlock() {
575        return mIndexFirstChangedBlock;
576    }
577
578    /**
579     * @hide
580     */
581    public void setIndexFirstChangedBlock(int i) {
582        mIndexFirstChangedBlock = i;
583    }
584
585    @Override
586    public int getLineCount() {
587        return mInts.size() - 1;
588    }
589
590    @Override
591    public int getLineTop(int line) {
592        return mInts.getValue(line, TOP);
593    }
594
595    @Override
596    public int getLineDescent(int line) {
597        return mInts.getValue(line, DESCENT);
598    }
599
600    @Override
601    public int getLineStart(int line) {
602        return mInts.getValue(line, START) & START_MASK;
603    }
604
605    @Override
606    public boolean getLineContainsTab(int line) {
607        return (mInts.getValue(line, TAB) & TAB_MASK) != 0;
608    }
609
610    @Override
611    public int getParagraphDirection(int line) {
612        return mInts.getValue(line, DIR) >> DIR_SHIFT;
613    }
614
615    @Override
616    public final Directions getLineDirections(int line) {
617        return mObjects.getValue(line, 0);
618    }
619
620    @Override
621    public int getTopPadding() {
622        return mTopPadding;
623    }
624
625    @Override
626    public int getBottomPadding() {
627        return mBottomPadding;
628    }
629
630    @Override
631    public int getEllipsizedWidth() {
632        return mEllipsizedWidth;
633    }
634
635    private static class ChangeWatcher implements TextWatcher, SpanWatcher {
636        public ChangeWatcher(DynamicLayout layout) {
637            mLayout = new WeakReference<DynamicLayout>(layout);
638        }
639
640        private void reflow(CharSequence s, int where, int before, int after) {
641            DynamicLayout ml = mLayout.get();
642
643            if (ml != null)
644                ml.reflow(s, where, before, after);
645            else if (s instanceof Spannable)
646                ((Spannable) s).removeSpan(this);
647        }
648
649        public void beforeTextChanged(CharSequence s, int where, int before, int after) {
650            // Intentionally empty
651        }
652
653        public void onTextChanged(CharSequence s, int where, int before, int after) {
654            reflow(s, where, before, after);
655        }
656
657        public void afterTextChanged(Editable s) {
658            // Intentionally empty
659        }
660
661        public void onSpanAdded(Spannable s, Object o, int start, int end) {
662            if (o instanceof UpdateLayout)
663                reflow(s, start, end - start, end - start);
664        }
665
666        public void onSpanRemoved(Spannable s, Object o, int start, int end) {
667            if (o instanceof UpdateLayout)
668                reflow(s, start, end - start, end - start);
669        }
670
671        public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
672            if (o instanceof UpdateLayout) {
673                reflow(s, start, end - start, end - start);
674                reflow(s, nstart, nend - nstart, nend - nstart);
675            }
676        }
677
678        private WeakReference<DynamicLayout> mLayout;
679    }
680
681    @Override
682    public int getEllipsisStart(int line) {
683        if (mEllipsizeAt == null) {
684            return 0;
685        }
686
687        return mInts.getValue(line, ELLIPSIS_START);
688    }
689
690    @Override
691    public int getEllipsisCount(int line) {
692        if (mEllipsizeAt == null) {
693            return 0;
694        }
695
696        return mInts.getValue(line, ELLIPSIS_COUNT);
697    }
698
699    private CharSequence mBase;
700    private CharSequence mDisplay;
701    private ChangeWatcher mWatcher;
702    private boolean mIncludePad;
703    private boolean mEllipsize;
704    private int mEllipsizedWidth;
705    private TextUtils.TruncateAt mEllipsizeAt;
706
707    private PackedIntVector mInts;
708    private PackedObjectVector<Directions> mObjects;
709
710    /**
711     * Value used in mBlockIndices when a block has been created or recycled and indicating that its
712     * display list needs to be re-created.
713     * @hide
714     */
715    public static final int INVALID_BLOCK_INDEX = -1;
716    // Stores the line numbers of the last line of each block (inclusive)
717    private int[] mBlockEndLines;
718    // The indices of this block's display list in TextView's internal display list array or
719    // INVALID_BLOCK_INDEX if this block has been invalidated during an edition
720    private int[] mBlockIndices;
721    // Number of items actually currently being used in the above 2 arrays
722    private int mNumberOfBlocks;
723    // The first index of the blocks whose locations are changed
724    private int mIndexFirstChangedBlock;
725
726    private int mTopPadding, mBottomPadding;
727
728    private static StaticLayout sStaticLayout = new StaticLayout(null);
729
730    private static final Object[] sLock = new Object[0];
731
732    private static final int START = 0;
733    private static final int DIR = START;
734    private static final int TAB = START;
735    private static final int TOP = 1;
736    private static final int DESCENT = 2;
737    private static final int COLUMNS_NORMAL = 3;
738
739    private static final int ELLIPSIS_START = 3;
740    private static final int ELLIPSIS_COUNT = 4;
741    private static final int COLUMNS_ELLIPSIZE = 5;
742
743    private static final int START_MASK = 0x1FFFFFFF;
744    private static final int DIR_SHIFT  = 30;
745    private static final int TAB_MASK   = 0x20000000;
746
747    private static final int ELLIPSIS_UNDEFINED = 0x80000000;
748}
749