diff --git a/CHANGELOG.md b/CHANGELOG.md index c276a9ffc..051ef61be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Change Log +## [v0.10.4](https://github.com/FXMisc/RichTextFX/tree/v0.10.4) (2020-02-19) +[Full Changelog](https://github.com/FXMisc/RichTextFX/compare/v0.10.3...v0.10.4) + +**Implemented enhancements:** + +- Added convenience method to UndoUtils to create a NO OP undo manager [\#881](https://github.com/FXMisc/RichTextFX/issues/881) +- Added styled text fields [\#894](https://github.com/FXMisc/RichTextFX/pull/894), [\#895](https://github.com/FXMisc/RichTextFX/pull/895), [\#896](https://github.com/FXMisc/RichTextFX/pull/896) & [\#897](https://github.com/FXMisc/RichTextFX/pull/897) +- Added place holder to GenericStyledArea [\#899](https://github.com/FXMisc/RichTextFX/pull/899) [\#900](https://github.com/FXMisc/RichTextFX/pull/900) +- Added prompt text to StyledTextField [\#899](https://github.com/FXMisc/RichTextFX/pull/899) +- Added Automatic-Module-Name +- Bumped to Flowless 0.6.1 + +**Fixed bugs:** + +- Bug: Fixed paragraph graphic creation if index is -1 [\#882](https://github.com/FXMisc/RichTextFX/pull/882) +- Bug: Fixed Java 9 code [\#887](https://github.com/FXMisc/RichTextFX/pull/887/files) and [\#888](https://github.com/FXMisc/RichTextFX/pull/888) +- Bug: Fixed memory leak in ParagraphText [\#893](https://github.com/FXMisc/RichTextFX/pull/893) + ## [v0.10.3](https://github.com/FXMisc/RichTextFX/tree/v0.10.3) (2019-11-27) [Full Changelog](https://github.com/FXMisc/RichTextFX/compare/v0.10.2...v0.10.3) diff --git a/README.md b/README.md index f41b2007f..bd4a47e30 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Table of Contents * [Stable](#stable-release) * [Snapshot](#snapshot-releases) * API Documentation (Javadoc) - * [0.10.3](http://fxmisc.github.io/richtext/javadoc/0.10.3/index.html?org/fxmisc/richtext/package-summary.html) + * [0.10.4](http://fxmisc.github.io/richtext/javadoc/0.10.4/index.html?org/fxmisc/richtext/package-summary.html) * [License](#license) * [Contributing](./CONTRIBUTING.md) @@ -145,31 +145,31 @@ Download ### Stable release -Current stable release is 0.10.3 which is a multi-release JAR that is compatible with Java 9 and UP without the need for `add-exports` or `add-opens` JVM arguments. +Current stable release is 0.10.4 which is a multi-release JAR that is compatible with Java 9 and UP without the need for `add-exports` or `add-opens` JVM arguments. #### Maven coordinates | Group ID | Artifact ID | Version | | :-----------------: | :---------: | :-----: | -| org.fxmisc.richtext | richtextfx | 0.10.3 | +| org.fxmisc.richtext | richtextfx | 0.10.4 | #### Gradle example ```groovy dependencies { - compile group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.10.3' + compile group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.10.4' } ``` #### Sbt example ```scala -libraryDependencies += "org.fxmisc.richtext" % "richtextfx" % "0.10.3" +libraryDependencies += "org.fxmisc.richtext" % "richtextfx" % "0.10.4" ``` #### Manual download -Download [the JAR file](https://github.com/TomasMikula/RichTextFX/releases/download/v0.10.3/richtextfx-0.10.3.jar) or [the fat JAR file (including dependencies)](https://github.com/TomasMikula/RichTextFX/releases/download/v0.10.3/richtextfx-fat-0.10.3.jar) and place it on your classpath. +Download [the JAR file](https://github.com/TomasMikula/RichTextFX/releases/download/v0.10.4/richtextfx-0.10.4.jar) or [the fat JAR file (including dependencies)](https://github.com/TomasMikula/RichTextFX/releases/download/v0.10.4/richtextfx-fat-0.10.4.jar) and place it on your classpath. ### Snapshot releases diff --git a/richtextfx/build.gradle b/richtextfx/build.gradle index 46c12cb96..8b36e879c 100644 --- a/richtextfx/build.gradle +++ b/richtextfx/build.gradle @@ -33,7 +33,7 @@ sourceSets { dependencies { compile group: 'org.reactfx', name: 'reactfx', version: '2.0-M5' compile group: 'org.fxmisc.undo', name: 'undofx', version: '2.1.0' - compile group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6' + compile group: 'org.fxmisc.flowless', name: 'flowless', version: '0.6.1' compile group: 'org.fxmisc.wellbehaved', name: 'wellbehavedfx', version: '0.3.3' java9Implementation files(sourceSets.main.output.classesDirs) { builtBy compileJava } @@ -75,6 +75,7 @@ jar { 'Specification-Version': project.specificationVersion, 'Implementation-Title': 'RichTextFX', 'Implementation-Version': project.version, + 'Automatic-Module-Name': 'org.fxmisc.richtext', 'Multi-Release': 'true') } } @@ -248,7 +249,7 @@ uploadArchives.onlyIf { doUploadArchives } task fatJar(type: Jar, dependsOn: classes) { appendix = 'fat' - manifest.attributes( 'Multi-Release': 'true' ) + manifest.attributes( 'Automatic-Module-Name': 'org.fxmisc.richtext', 'Multi-Release': 'true' ) into 'META-INF/versions/9', { from sourceSets.java9.output } from sourceSets.main.output from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index c7cf5585f..488fa3511 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -18,6 +18,7 @@ import javafx.application.Platform; import javafx.beans.NamedArg; +import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.ObjectProperty; @@ -53,7 +54,6 @@ import javafx.scene.shape.PathElement; import javafx.scene.text.TextFlow; -import javafx.application.Platform; import org.fxmisc.flowless.Cell; import org.fxmisc.flowless.VirtualFlow; import org.fxmisc.flowless.VirtualFlowHit; @@ -346,9 +346,12 @@ protected void invalidated() { // undo manager private UndoManager undoManager; @Override public UndoManager getUndoManager() { return undoManager; } + /** + * @param undoManager may be null in which case a no op undo manager will be set. + */ @Override public void setUndoManager(UndoManager undoManager) { this.undoManager.close(); - this.undoManager = undoManager; + this.undoManager = undoManager != null ? undoManager : UndoUtils.noOpUndoManager(); } private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null); @@ -368,6 +371,14 @@ public Node getParagraphGraphic( int parNdx ) { return getCell(parNdx).getGraphic(); } + /** + * This Node is shown to the user, centered over the area, when the area has no text content. + */ + private ObjectProperty placeHolderProp = new SimpleObjectProperty<>(this, "placeHolder", null); + public final ObjectProperty placeholderProperty() { return placeHolderProp; } + public final void setPlaceholder(Node value) { placeHolderProp.set(value); } + public final Node getPlaceholder() { return placeHolderProp.get(); } + private ObjectProperty contextMenu = new SimpleObjectProperty<>(null); @Override public final ObjectProperty contextMenuObjectProperty() { return contextMenu; } // Don't remove as FXMLLoader doesn't recognise default methods ! @@ -784,6 +795,49 @@ public GenericStyledArea( .subscribe(evt -> Event.fireEvent(this, evt)); new GenericStyledAreaBehavior(this); + + // Setup place holder visibility & placement + final Val showPlaceholder = Val.create + ( + () -> getLength() == 0 && ! isFocused(), + lengthProperty(), focusedProperty() + ); + + placeHolderProp.addListener( (ob,ov,newNode) -> displayPlaceHolder( showPlaceholder.getValue(), newNode ) ); + showPlaceholder.addListener( (ob,ov,show) -> displayPlaceHolder( show, getPlaceholder() ) ); + } + + private Node placeholder; + + private void displayPlaceHolder( boolean show, Node newNode ) + { + if ( placeholder != null && (! show || newNode != placeholder) ) + { + placeholder.layoutXProperty().unbind(); + placeholder.layoutYProperty().unbind(); + getChildren().remove( placeholder ); + placeholder = null; + setClip( null ); + } + if ( newNode != null && show && newNode != placeholder ) + { + configurePlaceholder( newNode ); + getChildren().add( newNode ); + placeholder = newNode; + } + } + + protected void configurePlaceholder( Node placeholder ) + { + placeholder.layoutYProperty().bind( Bindings.createDoubleBinding( () -> + (getHeight() - placeholder.getLayoutBounds().getHeight()) / 2, + heightProperty(), placeholder.layoutBoundsProperty() ) + ); + + placeholder.layoutXProperty().bind( Bindings.createDoubleBinding( () -> + (getWidth() - placeholder.getLayoutBounds().getWidth()) / 2, + widthProperty(), placeholder.layoutBoundsProperty() ) + ); } /* ********************************************************************** * @@ -955,7 +1009,7 @@ public IndexRange getParagraphSelection(Selection selection, int paragraph) { int startPar = selection.getStartParagraphIndex(); int endPar = selection.getEndParagraphIndex(); - if(paragraph < startPar || paragraph > endPar) { + if(selection.getLength() == 0 || paragraph < startPar || paragraph > endPar) { return EMPTY_RANGE; } @@ -1395,6 +1449,11 @@ protected void layoutChildren() { followCaretRequested = false; paging = false; }); + + Node holder = placeholder; + if (holder != null && holder.isResizable() && holder.isManaged()) { + holder.autosize(); + } } /* ********************************************************************** * diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextField.java b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextField.java new file mode 100644 index 000000000..120e2ab2b --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextField.java @@ -0,0 +1,29 @@ +package org.fxmisc.richtext; + +import org.fxmisc.richtext.model.SimpleEditableStyledDocument; + +import javafx.scene.text.TextAlignment; +import javafx.scene.text.TextFlow; + +/** + * A TextField that uses inline CSS, i.e. setStyle(String), to define the styles of text segments. + *

Use CSS Style Class ".styled-text-field" for styling the control. + * @author Jurgen + */ +public class InlineCssTextField extends StyledTextField +{ + public InlineCssTextField() { + super( "", TextFlow::setStyle, "", TextExt::setStyle, new SimpleEditableStyledDocument<>("", "") ); + } + + public InlineCssTextField( String text ) { + this(); replaceText( text ); + getUndoManager().forgetHistory(); + getUndoManager().mark(); + } + + @Override + protected void changeAlignment( TextAlignment txtAlign ) { + setParagraphStyle( 0, "-fx-text-alignment: "+ txtAlign ); + } +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java index 562e50d2e..3b4c7b35d 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java @@ -105,7 +105,7 @@ public final ObservableMap, SelectionPath> selectionsPrope graphic = Val.combine( graphicFactory, this.index, - (f, i) -> f != null ? f.apply(i) : null); + (f, i) -> f != null && i > -1 ? f.apply(i) : null); graphic.addListener((obs, oldG, newG) -> { if(oldG != null) { getChildren().remove(oldG); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index 755b5e364..4e2667bba 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -70,7 +70,7 @@ public ObjectProperty highlightTextFillProperty() { return highlightTextFill; } - private final Paragraph paragraph; + private Paragraph paragraph; private final CustomCssShapeHelper backgroundShapeHelper; private final CustomCssShapeHelper borderShapeHelper; @@ -221,11 +221,18 @@ public ObjectProperty highlightTextFillProperty() { } void dispose() { - // this removes listeners (in selections and carets listeners) and avoids memory leaks - selections.removeListener( selectionPathListener ); - carets.removeListener( caretNodeListener ); - selections.clear(); carets.clear(); + selections.clear(); + // The above must be before the below to prevent any memory leaks. + // Then remove listeners to also avoid memory leaks. + selections.removeListener( selectionPathListener ); + carets.removeListener( caretNodeListener ); + + getChildren().stream().filter( n -> n instanceof TextExt ).map( n -> (TextExt) n ) + .forEach( t -> JavaFXCompatibility.Text_selectionFillProperty(t).unbind() ); + + getChildren().clear(); + paragraph = null; } public Paragraph getParagraph() { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextField.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextField.java new file mode 100644 index 000000000..a0580839f --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextField.java @@ -0,0 +1,66 @@ +package org.fxmisc.richtext; + +import java.util.Collection; +import java.util.Collections; + +import org.fxmisc.richtext.model.SimpleEditableStyledDocument; + +import javafx.scene.text.TextAlignment; + +/** + * A TextField that uses style classes, i.e. getStyleClass().add(String), to define the styles of text segments. + *

Use CSS Style Class ".styled-text-field" for styling the control. + * @author Jurgen + */ +public class StyleClassedTextField extends StyledTextField, Collection> +{ + public StyleClassedTextField() { + super( + Collections.emptyList(), + (paragraph, styleClasses) -> paragraph.getStyleClass().addAll(styleClasses), + Collections.emptyList(), + (text, styleClasses) -> text.getStyleClass().addAll(styleClasses), + new SimpleEditableStyledDocument<>( Collections.emptyList(), Collections.emptyList() ) + ); + } + + public StyleClassedTextField( String text ) { + this(); replaceText( text ); + getUndoManager().forgetHistory(); + getUndoManager().mark(); + } + + /** + * Convenient method to append text together with a single style class. + */ + public void append( String text, String styleClass ) { + insert( getLength(), text, styleClass ); + } + + /** + * Convenient method to insert text together with a single style class. + */ + public void insert( int position, String text, String styleClass ) { + replace( position, position, text, Collections.singleton( styleClass ) ); + } + + /** + * Convenient method to replace text together with a single style class. + */ + public void replace( int start, int end, String text, String styleClass ) { + replace( start, end, text, Collections.singleton( styleClass ) ); + } + + /** + * Convenient method to assign a single style class. + */ + public void setStyleClass( int from, int to, String styleClass ) { + setStyle( from, to, Collections.singletonList( styleClass ) ); + } + + @Override + protected void changeAlignment( TextAlignment txtAlign ) { + // Set to style class as defined in "styled-text-field-caspian.css" AND "styled-text-field-modena.css" + setParagraphStyle( 0, Collections.singletonList( txtAlign.toString().toLowerCase() ) ); + } +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextField.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextField.java new file mode 100644 index 000000000..88fe096d6 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextField.java @@ -0,0 +1,287 @@ +package org.fxmisc.richtext; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.regex.Pattern; + +import org.fxmisc.richtext.model.EditableStyledDocument; + +import javafx.application.Application; +import javafx.beans.NamedArg; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.css.CssMetaData; +import javafx.css.StyleConverter; +import javafx.css.Styleable; +import javafx.css.StyleableObjectProperty; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.AccessibleRole; +import javafx.scene.Group; +import javafx.scene.Node; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Text; +import javafx.scene.text.TextAlignment; +import javafx.scene.text.TextFlow; + +/** + * A text field whose segment generic has been specified to be a {@link String}. How the text + * will be styled is not yet specified in this class, but use {@link StyleClassedTextField} for a style class + * approach to styling the text and {@link InlineCssTextField} for an inline css approach to styling the text. + * + *

Use CSS Style Class ".styled-text-field" for styling the control.

+ * + * @param type of paragraph style + * @param type of style that can be applied to text. + * + * @author Jurgen + */ +public abstract class StyledTextField extends StyledTextArea +{ + private final static List> CSS_META_DATA_LIST; + + private final static CssMetaData TEXT_ALIGNMENT = new CustomCssMetaData<>( + "-fx-alignment", (StyleConverter) StyleConverter.getEnumConverter(TextAlignment.class), + TextAlignment.LEFT, s -> (StyleableObjectProperty) s.alignmentProperty() + ); + + private final static CssMetaData PROMPT_TEXT_FILL = new CustomCssMetaData<>( + "-fx-prompt-text-fill", (StyleConverter) StyleConverter.getPaintConverter(), + Color.GRAY, s -> (StyleableObjectProperty) s.promptTextFillProperty() + ); + + private final static Pattern VERTICAL_WHITESPACE = Pattern.compile( "\\v+" ); + private final static String STYLE_SHEET; + private final static double HEIGHT; + static { + List> styleables = new ArrayList<>(GenericStyledArea.getClassCssMetaData()); + styleables.add( PROMPT_TEXT_FILL ); styleables.add( TEXT_ALIGNMENT ); + CSS_META_DATA_LIST = Collections.unmodifiableList(styleables); + + String globalCSS = System.getProperty( "javafx.userAgentStylesheetUrl" ); // JavaFX preference! + if ( globalCSS == null ) globalCSS = Application.getUserAgentStylesheet(); + if ( globalCSS == null ) globalCSS = Application.STYLESHEET_MODENA; + globalCSS = "styled-text-field-"+ globalCSS.toLowerCase() +".css"; + STYLE_SHEET = StyledTextField.class.getResource( globalCSS ).toExternalForm(); + + // Ugly hack to get a TextFields default height :( + // as it differs between Caspian, Modena, etc. + TextField tf = new TextField( "GetHeight" ); + new Scene(tf); tf.applyCss(); tf.layout(); + HEIGHT = tf.getHeight(); + } + + private boolean selectAll = true; + private StyleableObjectProperty textAlignment; + private StyleableObjectProperty promptFillProp; + + + public StyledTextField(@NamedArg("initialParagraphStyle") PS initialParagraphStyle, + @NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle, + @NamedArg("initialTextStyle") S initialTextStyle, + @NamedArg("applyStyle") BiConsumer applyStyle, + @NamedArg("document") EditableStyledDocument document) + { + super( initialParagraphStyle, applyParagraphStyle, initialTextStyle, applyStyle, document, true ); + + getStylesheets().add( STYLE_SHEET ); + getStyleClass().setAll( "styled-text-field" ); + + setAccessibleRole( AccessibleRole.TEXT_FIELD ); + setPrefSize( 135, HEIGHT ); + + addEventFilter( KeyEvent.KEY_PRESSED, KE -> { + if ( KE.getCode() == KeyCode.ENTER ) { + fireEvent( new ActionEvent( this, null ) ); + KE.consume(); + } + else if ( KE.getCode() == KeyCode.TAB ) { + traverse( this.getParent(), this, KE.isShiftDown() ? -1 : +1 ); + KE.consume(); + } + }); + + addEventFilter( MouseEvent.MOUSE_PRESSED, ME -> selectAll = isFocused() ); + + focusedProperty().addListener( (ob,was,focused) -> { + if ( ! was && focused && selectAll ) { + selectRange( getLength(), 0 ); + } + else if ( ! focused && was ) { + moveTo( 0 ); requestFollowCaret(); + } + selectAll = true; + }); + + super.setWrapText( false ); + wrapTextProperty().addListener( (ob,ov,wrap) -> { + if ( wrap ) { // veto any changes + wrapTextProperty().unbind(); + super.setWrapText(false); + } + }); + } + + /* + * There's no public API to move the focus forward or backward + * without explicitly knowing the node. So here's a basic local + * implementation to accomplish that. + */ + private Node traverse( Parent p, Node from, int dir ) + { + if ( p == null ) return null; + + List nodeList = p.getChildrenUnmodifiable(); + int len = nodeList.size(); + int neighbor = -1; + + if ( from != null ) while ( ++neighbor < len && nodeList.get(neighbor) != from ); + else if ( dir == 1 ) neighbor = -1; + else neighbor = len; + + for ( neighbor += dir; neighbor > -1 && neighbor < len; neighbor += dir ) { + + Node target = nodeList.get( neighbor ); + + if ( target instanceof Pane || target instanceof Group ) { + target = traverse( (Parent) target, null, dir ); // down + if ( target != null ) return target; + } + else if ( target.isVisible() && ! target.isDisabled() && target.isFocusTraversable() ) { + target.requestFocus(); + return target; + } + } + + return traverse( p.getParent(), p, dir ); // up + } + + /** + * Specifies how the text should be aligned when there is empty space within the TextField. + * To configure via CSS use {@code -fx-alignment:} and values from {@link javafx.scene.text.TextAlignment}. + */ + public final ObjectProperty alignmentProperty() { + if (textAlignment == null) { + textAlignment = new CustomStyleableProperty<>( TextAlignment.LEFT, "textAlignment", this, TEXT_ALIGNMENT ); + textAlignment.addListener( (ob,ov,alignment) -> changeAlignment( alignment ) ); + } + return textAlignment; + } + public final TextAlignment getAlignment() { return textAlignment == null ? TextAlignment.LEFT : textAlignment.getValue(); } + public final void setAlignment( TextAlignment value ) { alignmentProperty().setValue( value ); } + protected abstract void changeAlignment( TextAlignment txtAlign ); + + /** + * The action handler associated with this text field, or {@code null} if no action handler is assigned. + * The action handler is normally called when the user types the ENTER key. + */ + private ObjectProperty> onAction = new ObjectPropertyBase>() { + @Override + protected void invalidated() { + setEventHandler(ActionEvent.ACTION, get()); + } + + @Override + public Object getBean() { + return StyledTextField.this; + } + + @Override + public String getName() { + return "onAction"; + } + }; + public final ObjectProperty> onActionProperty() { return onAction; } + public final EventHandler getOnAction() { return onActionProperty().get(); } + public final void setOnAction(EventHandler value) { onActionProperty().set(value); } + + + /** + * The prompt text to display or null if no prompt text is to be displayed. + *

The Text will be aligned according to the text fields alignment setting and have a default + * text fill of GRAY unless you have changed it by any means, e.g. with CSS "-fx-prompt-text-fill" + */ + public final ObjectProperty promptTextProperty() { return placeholderProperty(); } + public final Text getPromptText() { return getPlaceholder() instanceof Text ? (Text) getPlaceholder() : null; } + public final void setPromptText( Text value ) { setPlaceholder( value ); } + @Override protected void configurePlaceholder( Node placeholder ) + { + placeholder.layoutYProperty().bind( Bindings.createDoubleBinding( () -> + (getHeight() - placeholder.getLayoutBounds().getHeight()) / 2 + Math.abs( placeholder.getLayoutBounds().getMinY() ), + heightProperty(), placeholder.layoutBoundsProperty() ) + ); + + placeholder.layoutXProperty().bind( Bindings.createDoubleBinding( () -> calcHorizontalPos(), + widthProperty(), placeholder.layoutBoundsProperty(), paddingProperty(), alignmentProperty() ) + ); + + if ( placeholder instanceof Text && ((Text) placeholder).getFill() == Color.BLACK ) { + ((Text) placeholder).fillProperty().bind( promptTextFillProperty() ); + } + } + + private final ObjectProperty promptTextFillProperty() { + if ( promptFillProp == null ) { + promptFillProp = new CustomStyleableProperty<>( Color.GRAY, "promptFill", this, PROMPT_TEXT_FILL ); + } + return promptFillProp; + } + + private double calcHorizontalPos() + { + double leftPad = getPadding().getLeft(); + double rightPad = getPadding().getRight(); + double promptWidth = getPlaceholder().getLayoutBounds().getWidth(); + TextAlignment alignment = getAlignment(); + double alignmentPadding = leftPad; + + if ( alignment == TextAlignment.RIGHT ) alignmentPadding = rightPad; + else if ( alignment == TextAlignment.CENTER ) alignmentPadding = 0; + + if ( promptWidth < (getWidth() - alignmentPadding) ) setClip( null ); + else setClip( new Rectangle( getWidth(), getHeight() ) ); + + switch ( alignment ) + { + case CENTER : return (getWidth() - promptWidth) / 2; + case RIGHT : return getWidth() - rightPad - promptWidth; + default : return leftPad; + } + } + + @Override + public void replaceText( int start, int end, String text ) + { + super.replaceText( start, end, VERTICAL_WHITESPACE.matcher( text ).replaceAll( " " ) ); + } + + public void setText( String text ) + { + replaceText( text ); + } + + /** This is a no op for text fields and therefore marked as deprecated. */ + @Override @Deprecated public void setWrapText( boolean value ) {} + /** This always returns false for styled text fields. */ + @Override public boolean isWrapText() { return false; } + + + @Override public List> getCssMetaData() { + return CSS_META_DATA_LIST; + } + public static List> getClassCssMetaData() { + return CSS_META_DATA_LIST; + } +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/package-info.java b/richtextfx/src/main/java/org/fxmisc/richtext/package-info.java index 6a80240c2..ec6e38348 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/package-info.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/package-info.java @@ -10,6 +10,11 @@ * {@link org.fxmisc.richtext.StyleClassedTextArea} and {@link org.fxmisc.richtext.InlineCssTextArea}. * For those looking to use a base for a code editor, see {@link org.fxmisc.richtext.CodeArea}. *

+ *

+ * For text fields there is {@link org.fxmisc.richtext.StyledTextField} using {@link java.lang.String}-only segments, + * and styling them are also already supported in the two most common ways via + * {@link org.fxmisc.richtext.StyleClassedTextField} and {@link org.fxmisc.richtext.InlineCssTextField}. + *

* * @see org.fxmisc.richtext.model.EditableStyledDocument * @see org.fxmisc.richtext.model.TwoDimensional diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/util/UndoUtils.java b/richtextfx/src/main/java/org/fxmisc/richtext/util/UndoUtils.java index 982209541..7b6470a43 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/util/UndoUtils.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/util/UndoUtils.java @@ -7,7 +7,9 @@ import org.fxmisc.richtext.model.TextChange; import org.fxmisc.undo.UndoManager; import org.fxmisc.undo.UndoManagerFactory; -import org.reactfx.EventStream; +import org.reactfx.value.Val; + +import javafx.beans.value.ObservableBooleanValue; import java.time.Duration; import java.util.List; @@ -35,6 +37,37 @@ public static UndoManager defaultUndoManager(GenericStyledArea alwaysFalse = Val.constant(false); + + @Override public boolean undo() { return false; } + @Override public boolean redo() { return false; } + @Override public Val undoAvailableProperty() { return alwaysFalse; } + @Override public boolean isUndoAvailable() { return false; } + @Override public Val redoAvailableProperty() { return alwaysFalse; } + @Override public boolean isRedoAvailable() { return false; } + @Override public boolean isPerformingAction() { return false; } + @Override public boolean isAtMarkedPosition() { return false; } + + // not sure whether these may throw NPEs at some point + @Override public Val nextUndoProperty() { return null; } + @Override public Val nextRedoProperty() { return null; } + @Override public ObservableBooleanValue performingActionProperty() { return null; } + @Override public UndoPosition getCurrentPosition() { return null; } + @Override public ObservableBooleanValue atMarkedPositionProperty() { return null; } + + // ignore these + @Override public void preventMerge() { } + @Override public void forgetHistory() { } + @Override public void close() { } + }; + } + /* ********************************************************************** * * * * UndoManager Factory Methods * diff --git a/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowExt.java b/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowExt.java index 7b0b0b41a..cb545eb8a 100644 --- a/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowExt.java +++ b/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowExt.java @@ -26,7 +26,7 @@ class TextFlowExt extends TextFlow { private TextFlowLayout textLayout() { if ( layout == null ) { - layout = new TextFlowLayout( this, getManagedChildren() ); + layout = new TextFlowLayout( this ); } return layout; } @@ -43,9 +43,9 @@ int getLineStartPosition(int charIdx) { int getLineEndPosition(int charIdx) { TwoLevelNavigator navigator = textLayout().getTwoLevelNavigator(); - int currentLineIndex = navigator.offsetToPosition(charIdx, Forward).getMajor(); - int minor = (currentLineIndex == getLineCount() - 1) ? 0 : -1; - return navigator.position(currentLineIndex + 1, minor).toOffset(); + int currentLineIndex = navigator.offsetToPosition(charIdx, Forward).getMajor() + 1; + int minor = (currentLineIndex == getLineCount()) ? 0 : -1; + return navigator.position(currentLineIndex, minor).toOffset(); } int getLineOfCharacter(int charIdx) { diff --git a/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowLayout.java b/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowLayout.java index 6ae507a1b..78246b5b0 100644 --- a/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowLayout.java +++ b/richtextfx/src/main/java9/org/fxmisc/richtext/TextFlowLayout.java @@ -21,14 +21,12 @@ class TextFlowLayout { private TextFlow flow; - private List children; private List lineMetrics = new ArrayList<>(); private int lineCount = -1; - TextFlowLayout( TextFlow tf, List managedChildren ) { + TextFlowLayout( TextFlow tf ) { tf.getChildren().addListener( (Observable ob) -> lineCount = -1 ); tf.widthProperty().addListener( (Observable ob) -> lineCount = -1 ); - children = managedChildren; flow = tf; } @@ -68,12 +66,13 @@ TwoLevelNavigator getTwoLevelNavigator() { int getLineCount() { if ( lineCount > -1 ) return lineCount; - + + lineCount = 0; lineMetrics.clear(); double totLines = 0.0, prevMinY = 1.0, prevMaxY = -1.0; int totCharSoFar = 0; - for ( Node n : children ) { + for ( Node n : flow.getChildren() ) if ( n.isManaged() ) { Bounds nodeBounds = n.getBoundsInParent(); int length = (n instanceof Text) ? ((Text) n).getText().length() : 1; diff --git a/richtextfx/src/main/resources/org/fxmisc/richtext/styled-text-field-caspian.css b/richtextfx/src/main/resources/org/fxmisc/richtext/styled-text-field-caspian.css new file mode 100644 index 000000000..fc1530320 --- /dev/null +++ b/richtextfx/src/main/resources/org/fxmisc/richtext/styled-text-field-caspian.css @@ -0,0 +1,38 @@ +.styled-text-field +{ + -fx-cursor: text; + -fx-text-fill: -fx-text-inner-color; + -fx-background-color: -fx-shadow-highlight-color, -fx-text-box-border, -fx-control-inner-background; + -fx-background-insets: 0, 1, 2; + -fx-background-radius: 3, 2, 2; + -fx-padding: 3 5 4 5; +} +.styled-text-field:focused +{ + -fx-background-color: -fx-focus-color, -fx-text-box-border, -fx-control-inner-background; + -fx-background-insets: -0.4, 1, 2; + -fx-background-radius: 3.4, 2, 2; +} +.styled-text-field:disabled +{ + -fx-opacity: -fx-disabled-opacity; +} +.main-selection +{ + -fx-fill: #0093ff; +} + +// Text alignment classes for StyleClassedTextField + +.center +{ + -fx-text-alignment: center; +} +.left +{ + -fx-text-alignment: left; +} +.right +{ + -fx-text-alignment: right; +} diff --git a/richtextfx/src/main/resources/org/fxmisc/richtext/styled-text-field-modena.css b/richtextfx/src/main/resources/org/fxmisc/richtext/styled-text-field-modena.css new file mode 100644 index 000000000..af1acb3f3 --- /dev/null +++ b/richtextfx/src/main/resources/org/fxmisc/richtext/styled-text-field-modena.css @@ -0,0 +1,40 @@ +.styled-text-field +{ + -fx-cursor: text; + -fx-text-fill: -fx-text-inner-color; + -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%), -fx-text-box-border), + linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -fx-control-inner-background); + -fx-background-insets: 0, 1; + -fx-background-radius: 3, 2; + -fx-padding: 4 7 4 7; +} +.styled-text-field:focused +{ + -fx-background-color: -fx-focus-color, -fx-control-inner-background, -fx-faint-focus-color, + linear-gradient(from 0px 0px to 0px 5px, derive(-fx-control-inner-background, -9%), -fx-control-inner-background); + -fx-background-insets: -0.2, 1, -1.4, 3; + -fx-background-radius: 3, 2, 4, 0; +} +.styled-text-field:disabled +{ + -fx-opacity: 0.4; +} +.main-selection +{ + -fx-fill: #0096C9; +} + +// Text alignment classes for StyleClassedTextField + +.center +{ + -fx-text-alignment: center; +} +.left +{ + -fx-text-alignment: left; +} +.right +{ + -fx-text-alignment: right; +}