Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.control;
027
028import java.util.AbstractList;
029import java.util.ArrayList;
030import java.util.Collection;
031import java.util.Collections;
032import java.util.List;
033import javafx.beans.InvalidationListener;
034import javafx.beans.property.BooleanProperty;
035import javafx.beans.property.DoubleProperty;
036import javafx.beans.property.IntegerProperty;
037import javafx.beans.property.SimpleDoubleProperty;
038import javafx.beans.value.ChangeListener;
039import javafx.collections.ListChangeListener;
040import javafx.collections.ObservableList;
041import javafx.css.CssMetaData;
042import javafx.css.StyleConverter;
043import javafx.css.StyleableBooleanProperty;
044import javafx.css.StyleableIntegerProperty;
045import javafx.css.StyleableProperty;
046import com.sun.javafx.binding.ExpressionHelper;
047import com.sun.javafx.collections.ListListenerHelper;
048import com.sun.javafx.collections.NonIterableChange;
049import com.sun.javafx.css.converters.SizeConverter;
050import com.sun.javafx.scene.control.skin.TextAreaSkin;
051import javafx.css.Styleable;
052
053/**
054 * Text input component that allows a user to enter multiple lines of
055 * plain text. Unlike in previous releases of JavaFX, support for single line
056 * input is not available as part of the TextArea control, however this is 
057 * the sole-purpose of the {@link TextField} control. Additionally, if you want
058 * a form of rich-text editing, there is also the
059 * {@link javafx.scene.web.HTMLEditor HTMLEditor} control.
060 *
061 * <p>TextArea supports the notion of showing {@link #promptTextProperty() prompt text}
062 * to the user when there is no {@link #textProperty() text} already in the
063 * TextArea (either via the user, or set programmatically). This is a useful
064 * way of informing the user as to what is expected in the text area, without
065 * having to resort to {@link Tooltip tooltips} or on-screen {@link Label labels}.
066 *
067 * @see TextField
068 */
069public class TextArea extends TextInputControl {
070    // Text area content model
071    private static final class TextAreaContent implements Content {
072        private ExpressionHelper<String> helper = null;
073        private ArrayList<StringBuilder> paragraphs = new ArrayList<StringBuilder>();
074        private int contentLength = 0;
075        private ParagraphList paragraphList = new ParagraphList();
076        private ListListenerHelper<CharSequence> listenerHelper;
077
078        private TextAreaContent() {
079            paragraphs.add(new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY));
080            paragraphList.content = this;
081        }
082
083        @Override public String get(int start, int end) {
084            int length = end - start;
085            StringBuilder textBuilder = new StringBuilder(length);
086
087            int paragraphCount = paragraphs.size();
088
089            int paragraphIndex = 0;
090            int offset = start;
091
092            while (paragraphIndex < paragraphCount) {
093                StringBuilder paragraph = paragraphs.get(paragraphIndex);
094                int count = paragraph.length() + 1;
095
096                if (offset < count) {
097                    break;
098                }
099
100                offset -= count;
101                paragraphIndex++;
102            }
103
104            // Read characters until end is reached, appending to text builder
105            // and moving to next paragraph as needed
106            StringBuilder paragraph = paragraphs.get(paragraphIndex);
107
108            int i = 0;
109            while (i < length) {
110                if (offset == paragraph.length()
111                    && i < contentLength) {
112                    textBuilder.append('\n');
113                    paragraph = paragraphs.get(++paragraphIndex);
114                    offset = 0;
115                } else {
116                    textBuilder.append(paragraph.charAt(offset++));
117                }
118
119                i++;
120            }
121
122            return textBuilder.toString();
123        }
124
125        @Override
126        @SuppressWarnings("unchecked")
127        public void insert(int index, String text, boolean notifyListeners) {
128            if (index < 0
129                || index > contentLength) {
130                throw new IndexOutOfBoundsException();
131            }
132
133            if (text == null) {
134                throw new IllegalArgumentException();
135            }
136            text = TextInputControl.filterInput(text, false, false);
137            int length = text.length();
138            if (length > 0) {
139                // Split the text into lines
140                ArrayList<StringBuilder> lines = new ArrayList<StringBuilder>();
141
142                StringBuilder line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY);
143                for (int i = 0; i < length; i++) {
144                    char c = text.charAt(i);
145
146                    if (c == '\n') {
147                        lines.add(line);
148                        line = new StringBuilder(DEFAULT_PARAGRAPH_CAPACITY);
149                    } else {
150                        line.append(c);
151                    }
152                }
153
154                lines.add(line);
155
156                // Merge the text into the existing content
157                // Merge the text into the existing content
158                int paragraphIndex = paragraphs.size();
159                int offset = contentLength + 1;
160
161                StringBuilder paragraph = null;
162
163                do {
164                    paragraph = paragraphs.get(--paragraphIndex);
165                    offset -= paragraph.length() + 1;
166                } while (index < offset);
167
168                int start = index - offset;
169
170                int n = lines.size();
171                if (n == 1) {
172                    // The text contains only a single line; insert it into the
173                    // intersecting paragraph
174                    paragraph.insert(start, line);
175                    fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1,
176                        Collections.singletonList((CharSequence)paragraph));
177                } else {
178                    // The text contains multiple line; split the intersecting
179                    // paragraph
180                    int end = paragraph.length();
181                    CharSequence trailingText = paragraph.subSequence(start, end);
182                    paragraph.delete(start, end);
183
184                    // Append the first line to the intersecting paragraph and
185                    // append the trailing text to the last line
186                    StringBuilder first = lines.get(0);
187                    paragraph.insert(start, first);
188                    line.append(trailingText);
189                    fireParagraphListChangeEvent(paragraphIndex, paragraphIndex + 1,
190                        Collections.singletonList((CharSequence)paragraph));
191
192                    // Insert the remaining lines into the paragraph list
193                    paragraphs.addAll(paragraphIndex + 1, lines.subList(1, n));
194                    fireParagraphListChangeEvent(paragraphIndex + 1, paragraphIndex + n,
195                        Collections.EMPTY_LIST);
196                }
197
198                // Update content length
199                contentLength += length;
200                if (notifyListeners) {
201                    ExpressionHelper.fireValueChangedEvent(helper);
202                }
203            }
204        }
205
206        @Override public void delete(int start, int end, boolean notifyListeners) {
207            if (start > end) {
208                throw new IllegalArgumentException();
209            }
210
211            if (start < 0
212                || end > contentLength) {
213                throw new IndexOutOfBoundsException();
214            }
215
216            int length = end - start;
217
218            if (length > 0) {
219                // Identify the trailing paragraph index
220                int paragraphIndex = paragraphs.size();
221                int offset = contentLength + 1;
222
223                StringBuilder paragraph = null;
224
225                do {
226                    paragraph = paragraphs.get(--paragraphIndex);
227                    offset -= paragraph.length() + 1;
228                } while (end < offset);
229
230                int trailingParagraphIndex = paragraphIndex;
231                int trailingOffset = offset;
232                StringBuilder trailingParagraph = paragraph;
233
234                // Identify the leading paragraph index
235                paragraphIndex++;
236                offset += paragraph.length() + 1;
237
238                do {
239                    paragraph = paragraphs.get(--paragraphIndex);
240                    offset -= paragraph.length() + 1;
241                } while (start < offset);
242
243                int leadingParagraphIndex = paragraphIndex;
244                int leadingOffset = offset;
245                StringBuilder leadingParagraph = paragraph;
246
247                // Remove the text
248                if (leadingParagraphIndex == trailingParagraphIndex) {
249                    // The removal affects only a single paragraph
250                    leadingParagraph.delete(start - leadingOffset,
251                        end - leadingOffset);
252
253                    fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1,
254                        Collections.singletonList((CharSequence)leadingParagraph));
255                } else {
256                    // The removal spans paragraphs; remove any intervening paragraphs and
257                    // merge the leading and trailing segments
258                    CharSequence leadingSegment = leadingParagraph.subSequence(0,
259                        start - leadingOffset);
260                    int trailingSegmentLength = (start + length) - trailingOffset;
261
262                    trailingParagraph.delete(0, trailingSegmentLength);
263                    fireParagraphListChangeEvent(trailingParagraphIndex, trailingParagraphIndex + 1,
264                        Collections.singletonList((CharSequence)trailingParagraph));
265
266                    if (trailingParagraphIndex - leadingParagraphIndex > 0) {
267                        List<CharSequence> removed = new ArrayList<CharSequence>(paragraphs.subList(leadingParagraphIndex,
268                            trailingParagraphIndex));
269                        paragraphs.subList(leadingParagraphIndex,
270                            trailingParagraphIndex).clear();
271                        fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex,
272                            removed);
273                    }
274
275                    // Trailing paragraph is now at the former leading paragraph's index
276                    trailingParagraph.insert(0, leadingSegment);
277                    fireParagraphListChangeEvent(leadingParagraphIndex, leadingParagraphIndex + 1,
278                        Collections.singletonList((CharSequence)leadingParagraph));
279                }
280
281                // Update content length
282                contentLength -= length;
283                if (notifyListeners) {
284                    ExpressionHelper.fireValueChangedEvent(helper);
285                }
286            }
287        }
288
289        @Override public int length() {
290            return contentLength;
291        }
292
293        @Override public String get() {
294            return get(0, length());
295        }
296
297        @Override public void addListener(ChangeListener<? super String> changeListener) {
298            helper = ExpressionHelper.addListener(helper, this, changeListener);
299        }
300
301        @Override public void removeListener(ChangeListener<? super String> changeListener) {
302            helper = ExpressionHelper.removeListener(helper, changeListener);
303        }
304
305        @Override public String getValue() {
306            return get();
307        }
308
309        @Override public void addListener(InvalidationListener listener) {
310            helper = ExpressionHelper.addListener(helper, this, listener);
311        }
312
313        @Override public void removeListener(InvalidationListener listener) {
314            helper = ExpressionHelper.removeListener(helper, listener);
315        }
316
317        private void fireParagraphListChangeEvent(int from, int to, List<CharSequence> removed) {
318            ParagraphListChange change = new ParagraphListChange(paragraphList, from, to, removed);
319            ListListenerHelper.fireValueChangedEvent(listenerHelper, change);
320        }
321    }
322
323    // Observable list of paragraphs
324    private static final class ParagraphList extends AbstractList<CharSequence>
325            implements ObservableList<CharSequence> {
326
327        private TextAreaContent content;
328
329        @Override
330        public CharSequence get(int index) {
331            return content.paragraphs.get(index);
332        }
333
334        @Override
335        public boolean addAll(Collection<? extends CharSequence> paragraphs) {
336            throw new UnsupportedOperationException();
337        }
338
339        @Override
340        public boolean addAll(CharSequence... paragraphs) {
341            throw new UnsupportedOperationException();
342        }
343
344        @Override
345        public boolean setAll(Collection<? extends CharSequence> paragraphs) {
346            throw new UnsupportedOperationException();
347        }
348
349        @Override
350        public boolean setAll(CharSequence... paragraphs) {
351            throw new UnsupportedOperationException();
352        }
353
354        @Override
355        public int size() {
356            return content.paragraphs.size();
357        }
358
359        @Override
360        public void addListener(ListChangeListener<? super CharSequence> listener) {
361            content.listenerHelper = ListListenerHelper.addListener(content.listenerHelper, listener);
362        }
363
364        @Override
365        public void removeListener(ListChangeListener<? super CharSequence> listener) {
366            content.listenerHelper = ListListenerHelper.removeListener(content.listenerHelper, listener);
367        }
368
369        @Override
370        public boolean removeAll(CharSequence... elements) {
371            throw new UnsupportedOperationException();
372        }
373
374        @Override
375        public boolean retainAll(CharSequence... elements) {
376            throw new UnsupportedOperationException();
377        }
378
379        @Override
380        public void remove(int from, int to) {
381            throw new UnsupportedOperationException();
382        }
383
384        @Override
385        public void addListener(InvalidationListener listener) {
386            content.listenerHelper = ListListenerHelper.addListener(content.listenerHelper, listener);
387        }
388
389        @Override
390        public void removeListener(InvalidationListener listener) {
391            content.listenerHelper = ListListenerHelper.removeListener(content.listenerHelper, listener);
392        }
393    }
394
395    private static final class ParagraphListChange extends NonIterableChange<CharSequence>  {
396
397        private List<CharSequence> removed;
398
399        protected ParagraphListChange(ObservableList<CharSequence> list, int from, int to,
400            List<CharSequence> removed) {
401            super(from, to, list);
402
403            this.removed = removed;
404        }
405
406        @Override
407        public List<CharSequence> getRemoved() {
408            return removed;
409        }
410
411        @Override
412        protected int[] getPermutation() {
413            return new int[0];
414        }
415    };
416
417    /**
418     * The default value for {@link #prefColumnCount}.
419     */
420    public static final int DEFAULT_PREF_COLUMN_COUNT = 40;
421
422    /**
423     * The default value for {@link #prefRowCount}.
424     */
425    public static final int DEFAULT_PREF_ROW_COUNT = 10;
426
427    /**
428     * @treatAsPrivate implementation detail
429     * @deprecated This is an internal API that is not intended for use and will be removed in the next version
430     */
431    public static final int DEFAULT_PARAGRAPH_CAPACITY = 32;
432
433    /**
434     * Creates a {@code TextArea} with empty text content.
435     */
436    public TextArea() {
437        this("");
438    }
439
440    /**
441     * Creates a {@code TextArea} with initial text content.
442     *
443     * @param text A string for text content.
444     */
445    public TextArea(String text) {
446        super(new TextAreaContent());
447
448        getStyleClass().add("text-area");
449        setText(text);
450    }
451
452    @Override final void textUpdated() {
453        setScrollTop(0);
454        setScrollLeft(0);
455    }
456
457    /**
458     * Returns an unmodifiable list of the character sequences that back the
459     * text area's content.
460     */
461    public ObservableList<CharSequence> getParagraphs() {
462        return ((TextAreaContent)getContent()).paragraphList;
463    }
464
465
466    /***************************************************************************
467     *                                                                         *
468     * Properties                                                              *
469     *                                                                         *
470     **************************************************************************/
471
472    /**
473     * If a run of text exceeds the width of the {@code TextArea},
474     * then this variable indicates whether the text should wrap onto
475     * another line.
476     */
477    private BooleanProperty wrapText = new StyleableBooleanProperty(false) {
478        @Override public Object getBean() {
479            return TextArea.this;
480        }
481
482        @Override public String getName() {
483            return "wrapText";
484        }
485
486        @Override public CssMetaData<TextArea,Boolean> getCssMetaData() {
487            return StyleableProperties.WRAP_TEXT;
488        }
489    };
490    public final BooleanProperty wrapTextProperty() { return wrapText; }
491    public final boolean isWrapText() { return wrapText.getValue(); }
492    public final void setWrapText(boolean value) { wrapText.setValue(value); }
493
494
495    /**
496     * The preferred number of text columns. This is used for
497     * calculating the {@code TextArea}'s preferred width.
498     */
499    private IntegerProperty prefColumnCount = new StyleableIntegerProperty(DEFAULT_PREF_COLUMN_COUNT) {
500        @Override
501        public void set(int value) {
502            if (value < 0) {
503                throw new IllegalArgumentException("value cannot be negative.");
504            }
505
506            super.set(value);
507        }
508
509        @Override public CssMetaData<TextArea,Number> getCssMetaData() {
510            return StyleableProperties.PREF_COLUMN_COUNT;
511        }
512
513        @Override
514        public Object getBean() {
515            return TextArea.this;
516        }
517
518        @Override
519        public String getName() {
520            return "prefColumnCount";
521        }
522    };
523    public final IntegerProperty prefColumnCountProperty() { return prefColumnCount; }
524    public final int getPrefColumnCount() { return prefColumnCount.getValue(); }
525    public final void setPrefColumnCount(int value) { prefColumnCount.setValue(value); }
526
527
528    /**
529     * The preferred number of text rows. This is used for calculating
530     * the {@code TextArea}'s preferred height.
531     */
532    private IntegerProperty prefRowCount = new StyleableIntegerProperty(DEFAULT_PREF_ROW_COUNT) {
533        @Override
534        public void set(int value) {
535            if (value < 0) {
536                throw new IllegalArgumentException("value cannot be negative.");
537            }
538
539            super.set(value);
540        }
541
542        @Override public CssMetaData<TextArea,Number> getCssMetaData() {
543            return StyleableProperties.PREF_ROW_COUNT;
544        }
545
546        @Override
547        public Object getBean() {
548            return TextArea.this;
549        }
550
551        @Override
552        public String getName() {
553            return "prefRowCount";
554        }
555    };
556    public final IntegerProperty prefRowCountProperty() { return prefRowCount; }
557    public final int getPrefRowCount() { return prefRowCount.getValue(); }
558    public final void setPrefRowCount(int value) { prefRowCount.setValue(value); }
559
560
561    /**
562     * The number of pixels by which the content is vertically
563     * scrolled.
564     */
565    private DoubleProperty scrollTop = new SimpleDoubleProperty(this, "scrollTop", 0);
566    public final DoubleProperty scrollTopProperty() { return scrollTop; }
567    public final double getScrollTop() { return scrollTop.getValue(); }
568    public final void setScrollTop(double value) { scrollTop.setValue(value); }
569
570
571    /**
572     * The number of pixels by which the content is horizontally
573     * scrolled.
574     */
575    private DoubleProperty scrollLeft = new SimpleDoubleProperty(this, "scrollLeft", 0);
576    public final DoubleProperty scrollLeftProperty() { return scrollLeft; }
577    public final double getScrollLeft() { return scrollLeft.getValue(); }
578    public final void setScrollLeft(double value) { scrollLeft.setValue(value); }
579
580    /***************************************************************************
581     *                                                                         *
582     * Methods                                                                 *
583     *                                                                         *
584     **************************************************************************/
585
586    /** {@inheritDoc} */
587    @Override protected Skin<?> createDefaultSkin() {
588        return new TextAreaSkin(this);
589    }
590
591    /***************************************************************************
592     *                                                                         *
593     * Stylesheet Handling                                                     *
594     *                                                                         *
595     **************************************************************************/
596
597     /**
598      * @treatAsPrivate implementation detail
599      */
600    private static class StyleableProperties {
601        private static final CssMetaData<TextArea,Number> PREF_COLUMN_COUNT =
602            new CssMetaData<TextArea,Number>("-fx-pref-column-count",
603                SizeConverter.getInstance(), DEFAULT_PREF_COLUMN_COUNT) {
604
605            @Override
606            public boolean isSettable(TextArea n) {
607                return !n.prefColumnCount.isBound();
608            }
609
610            @Override
611            public StyleableProperty<Number> getStyleableProperty(TextArea n) {
612                return (StyleableProperty<Number>)n.prefColumnCountProperty();
613            }
614        };
615
616        private static final CssMetaData<TextArea,Number> PREF_ROW_COUNT =
617            new CssMetaData<TextArea,Number>("-fx-pref-row-count",
618                SizeConverter.getInstance(), DEFAULT_PREF_ROW_COUNT) {
619
620            @Override
621            public boolean isSettable(TextArea n) {
622                return !n.prefRowCount.isBound();
623            }
624
625            @Override
626            public StyleableProperty<Number> getStyleableProperty(TextArea n) {
627                return (StyleableProperty<Number>)n.prefRowCountProperty();
628            }
629        };
630
631        private static final CssMetaData<TextArea,Boolean> WRAP_TEXT =
632            new CssMetaData<TextArea,Boolean>("-fx-wrap-text",
633                StyleConverter.getBooleanConverter(), false) {
634
635            @Override
636            public boolean isSettable(TextArea n) {
637                return !n.wrapText.isBound();
638            }
639
640            @Override
641            public StyleableProperty<Boolean> getStyleableProperty(TextArea n) {
642                return (StyleableProperty<Boolean>)n.wrapTextProperty();
643            }
644        };
645
646        private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
647        static {
648            final List<CssMetaData<? extends Styleable, ?>> styleables =
649                new ArrayList<CssMetaData<? extends Styleable, ?>>(TextInputControl.getClassCssMetaData());
650            styleables.add(PREF_COLUMN_COUNT);
651            styleables.add(PREF_ROW_COUNT);
652            styleables.add(WRAP_TEXT);
653            STYLEABLES = Collections.unmodifiableList(styleables);
654        }
655    }
656
657    /**
658     * @return The CssMetaData associated with this class, which may include the
659     * CssMetaData of its super classes.
660     */
661    public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
662        return StyleableProperties.STYLEABLES;
663    }
664
665    /**
666     * {@inheritDoc}
667     */
668    @Override
669    public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
670        return getClassCssMetaData();
671    }
672}