com.gluonhq.richtextarea.RichTextArea Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of rich-text-area Show documentation
Show all versions of rich-text-area Show documentation
Rich text control for JavaFX
The newest version!
/*
* Copyright (c) 2022, 2024, Gluon
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.gluonhq.richtextarea;
import com.gluonhq.emoji.EmojiSkinTone;
import com.gluonhq.richtextarea.action.ActionFactory;
import com.gluonhq.richtextarea.model.Decoration;
import com.gluonhq.richtextarea.model.Document;
import com.gluonhq.richtextarea.model.ParagraphDecoration;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.SkinBase;
import javafx.scene.input.DataFormat;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* The RichTextArea control is a text input component that allows a user to enter multiple lines of
* rich text and other non-text objects like images or hyperlinks.
*
* Internally the data model is based on a {@link com.gluonhq.richtextarea.model.PieceTable} implementation.
* Pieces hold text and decorations that can be applied to style the text (like font or color) and
* the paragraph with such text (like paragraph alignment). Unlimited undo/redo operations are allowed.
*
* A {@link com.gluonhq.richtextarea.model.DecorationModel} is used to represent text and paragraph decorations for
* a given segment of content.
*
* A list of {@link com.gluonhq.richtextarea.model.DecorationModel} and the full text forms the {@link Document}, which
* is the model that ultimately the control renders.
*
*/
public class RichTextArea extends Control {
public static final String STYLE_CLASS = "rich-text-area";
public static final DataFormat RTA_DATA_FORMAT;
static {
DataFormat dataFormat = DataFormat.lookupMimeType("text/rich-text-area");
RTA_DATA_FORMAT = dataFormat == null ? new DataFormat("text/rich-text-area") : dataFormat;
}
private static final PseudoClass PSEUDO_CLASS_READONLY = PseudoClass.getPseudoClass("readonly");
private final ActionFactory actionFactory = new ActionFactory(this);
public RichTextArea() {
getStyleClass().add(STYLE_CLASS);
}
// Properties
// documentProperty
/**
* The {@link Document document} is the model that holds the full text and decorations that are being
* displayed by the control.
*
* By default, this property is set via {@link ActionFactory#newDocument()} or {@link ActionFactory#open(Document)},
* and gets updated only via {@link ActionFactory#save()}, unless {@link #autoSaveProperty()} is enabled, in which
* the document gets updated after every change.
*/
// documentProperty
final ReadOnlyObjectWrapper documentProperty = new ReadOnlyObjectWrapper<>(this, "document", new Document());
public final ReadOnlyObjectProperty documentProperty() {
return documentProperty.getReadOnlyProperty();
}
public final Document getDocument() {
return documentProperty.get();
}
// autoSaveProperty
/**
* Property that allows saving every change done into the {@link Document document}.
* By default, it is disabled, and it is recommended to use the {@link ActionFactory#save()} action
* instead, on user's demand: If auto save is enabled, there might be some impact on performance.
*
* @return if auto saving is enabled or not
*/
public final BooleanProperty autoSaveProperty() {
return autoSaveProperty;
}
public final boolean isAutoSave() {
return autoSaveProperty.get();
}
public final void setAutoSave(boolean value) {
autoSaveProperty.set(value);
}
private final BooleanProperty autoSaveProperty = new SimpleBooleanProperty(this, "autoSave");
// modifiedProperty
/**
* Indicates if the current {@link Document document} has unsaved changes or not.
*
* Unless {@link #autoSaveProperty()} is enabled, after any change of the document being edited with the control
* this property will be set to true, and will enable the {@link ActionFactory#save()} action.
*
* @return if the document is modified or not
*/
public final ReadOnlyBooleanProperty modifiedProperty() {
return modifiedProperty.getReadOnlyProperty();
}
public final boolean isModified() {
return modifiedProperty.get();
}
final ReadOnlyBooleanWrapper modifiedProperty = new ReadOnlyBooleanWrapper(this, "modified");
// editableProperty
/**
* Indicates if the {@link Document document} is editable or not.
*
* By default, it is set to true.
*
* @return if the document is editable or not
*/
public final BooleanProperty editableProperty() {
return editableProperty;
}
public final boolean isEditable() {
return editableProperty.get();
}
public final void setEditable(boolean value) {
editableProperty.set(value);
}
private final BooleanProperty editableProperty = new SimpleBooleanProperty(this, "editable", true) {
@Override
protected void invalidated() {
pseudoClassStateChanged(PSEUDO_CLASS_READONLY, !get());
}
};
// selectionProperty
/**
* Contains the {@link Selection selection} of some fragment of the document, if any.
*
* @return the existing selection, if any
*/
public final ReadOnlyObjectProperty selectionProperty() {
return selectionProperty.getReadOnlyProperty();
}
public final Selection getSelection() {
return selectionProperty.get();
}
final ReadOnlyObjectWrapper selectionProperty = new ReadOnlyObjectWrapper<>(this, "selection", Selection.UNDEFINED);
// textLengthProperty
/**
* Returns the current length of the {@link Document document}, and it gets updated after every change,
* even if the document has not been saved yet.
*
* @return the current length of the document
*/
public final ReadOnlyIntegerProperty textLengthProperty() {
return textLengthProperty.getReadOnlyProperty();
}
public final int getTextLength() {
return textLengthProperty.get();
}
final ReadOnlyIntegerWrapper textLengthProperty = new ReadOnlyIntegerWrapper(this, "textLength");
// contentAreaWidthProperty
/**
* Defines a width constraint for the content area of the rich text control,
* in user space coordinates, where text can be added.
* The width is measured in pixels (and not glyph or character count).
* If the value is {@code <= 0}, the content area extends to the whole viewport of control
* and will change whenever the viewport gets resized.
* If the value is {@code > 0}, the content area is exactly set to this value,
* and the control will provide a horizontal scrollbar if needed.
*
* In any case, text will be line wrapped as needed to satisfy this constraint.
*
* @defaultValue 0
*
* @return the width of the content area
*/
public final DoubleProperty contentAreaWidthProperty() {
return contentAreaWidthProperty;
}
public final double getContentAreaWidth() {
return contentAreaWidthProperty.get();
}
public final void setContentAreaWidth(double value) {
contentAreaWidthProperty.set(value);
}
private final DoubleProperty contentAreaWidthProperty = new SimpleDoubleProperty(this, "contentAreaWidth", 0d);
// paragraphGraphicFactoryProperty
/**
* A paragraph can be decorated with a node to the left. This property allows adding the graphic for
* numbered or bulleted lists.
*
* Once the {@link ParagraphDecoration#getIndentationLevel() indentation level} and the
* {@link ParagraphDecoration.GraphicType graphic type} for a given paragraph are set,
* for instance with the {@link com.gluonhq.richtextarea.action.ParagraphDecorateAction},
* the {@link BiFunction} allows defining the node that will be used for the graphic with
* such indentation level and type of list.
*
* Numbered lists should use a {@link javafx.scene.control.Label} as node. The text of the label should contain,
* at least, an {@code "#"} as a wildcard for the index of paragraph, which will be automatically determined
* by the control.
*
* By default, the control provides a {@link DefaultParagraphGraphicFactory factory}.
*
* @return a factory to define the graphic decoration for each paragraph, if any
*/
public final ObjectProperty> paragraphGraphicFactoryProperty() {
return paragraphGraphicFactoryProperty;
}
public final BiFunction getParagraphGraphicFactory() {
return paragraphGraphicFactoryProperty.get();
}
public final void setParagraphGraphicFactory(BiFunction value) {
paragraphGraphicFactoryProperty.set(value);
}
private final ObjectProperty> paragraphGraphicFactoryProperty =
new SimpleObjectProperty<>(this, "paragraphGraphicFactory", DefaultParagraphGraphicFactory.getFactory());
// linkCallbackFactoryProperty
/**
* Allows setting a consumer that accepts a valid URL string, for a given node.
*
* Typically, this can be applied to {@link javafx.scene.text.Text} or {@link javafx.scene.image.ImageView} nodes
* with a link to a given URL.
*
* A default {@link DefaultLinkCallbackFactory factory} allows opening this link in
* the browser.
*
* @return a factory to process links to URL strings
*/
public final ObjectProperty>> linkCallbackFactoryProperty() {
return linkCallbackFactoryProperty;
}
public final Function> getLinkCallbackFactory() {
return linkCallbackFactoryProperty.get();
}
public final void setLinkCallbackFactory(Function> value) {
linkCallbackFactoryProperty.set(value);
}
private final ObjectProperty>> linkCallbackFactoryProperty =
new SimpleObjectProperty<>(this, "linkCallbackFactory", DefaultLinkCallbackFactory.getFactory());
/**
* Defines the action to be performed when enter is pressed. If no action is set,
* a new line will be added by default.
*/
private final ObjectProperty> onAction = new SimpleObjectProperty<>(this, "onAction");
public final ObjectProperty> onActionProperty() {
return onAction;
}
public final EventHandler getOnAction() {
return onAction.get();
}
public final void setOnAction(EventHandler value) {
onAction.set(value);
}
/**
* Defines the preferred skin tone that will be used
*/
private final ObjectProperty skinToneProperty = new SimpleObjectProperty<>(this, "skinTone", EmojiSkinTone.NO_SKIN_TONE);
public final ObjectProperty skinToneProperty() {
return skinToneProperty;
}
public final EmojiSkinTone getSkinTone() {
return skinToneProperty.get();
}
public final void setSkinTone(EmojiSkinTone value) {
skinToneProperty.set(value);
}
/**
* The current position of the caret within the text.
*/
final ReadOnlyIntegerWrapper caretPosition = new ReadOnlyIntegerWrapper(this, "caretPosition", 0);
public final int getCaretPosition() { return caretPosition.get(); }
public final ReadOnlyIntegerProperty caretPositionProperty() { return caretPosition.getReadOnlyProperty(); }
/**
* The current decoration at the caret.
*/
final ReadOnlyObjectWrapper decorationAtCaret = new ReadOnlyObjectWrapper<>(this, "decorationAtCaret");
public final ReadOnlyObjectProperty decorationAtCaretProperty() {
return decorationAtCaret.getReadOnlyProperty();
}
public final Decoration getDecorationAtCaret() {
return decorationAtCaret.get();
}
/**
* The paragraph decoration at the current paragraph.
*/
final ReadOnlyObjectWrapper decorationAtParagraph = new ReadOnlyObjectWrapper<>(this, "decorationAtParagraph");
public final ReadOnlyObjectProperty decorationAtParagraphProperty() {
return decorationAtParagraph.getReadOnlyProperty();
}
public final ParagraphDecoration getDecorationAtParagraph() {
return decorationAtParagraph.get();
}
/**
* Defines if tables can be inserted into the document or not. By default, it is allowed
*/
private final BooleanProperty tableAllowedProperty = new SimpleBooleanProperty(this, "tableAllowed", true);
public final BooleanProperty tableAllowedProperty() {
return tableAllowedProperty;
}
public final boolean isTableAllowed() {
return tableAllowedProperty.get();
}
public final void setTableAllowed(boolean value) {
tableAllowedProperty.set(value);
}
/**
* The prompt text to display in the {@code Document}. If set to null or an empty string, no
* prompt text is displayed.
*
* Prompt text changes are not handled by the command manager and therefore cannot be undone/redone.
*
* @defaultValue An empty String
*/
private final StringProperty promptText = new SimpleStringProperty(this, "promptText", "") {
@Override protected void invalidated() {
// Strip out newlines
String txt = get();
if (txt != null && txt.contains("\n")) {
txt = txt.replace("\n", "");
set(txt);
}
}
};
public final StringProperty promptTextProperty() { return promptText; }
public final String getPromptText() { return promptText.get(); }
public final void setPromptText(String value) { promptText.set(value); }
// public methods
/**
* The action factory that can be used from toolBars, menus or context menus to
* apply given actions, like {@link ActionFactory#save()}, {@link ActionFactory#copy()}
* or {@link ActionFactory#undo()}, to the {@link Document document}.
*
* @return the action factory
*/
public final ActionFactory getActionFactory() {
return actionFactory;
}
/**
* {@inheritDoc}
*/
@Override
protected SkinBase createDefaultSkin() {
return new RichTextAreaSkin(this);
}
/**
* {@inheritDoc}
*/
@Override public String getUserAgentStylesheet() {
return getClass().getResource("rich-text-area.css").toExternalForm();
}
}