Skip to content

Commit

Permalink
Attach and detach control from viewmodel, including test (#330)
Browse files Browse the repository at this point in the history
  • Loading branch information
jperedadnr authored May 23, 2024
1 parent 85f6bac commit 5ff7c3d
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 57 deletions.
124 changes: 73 additions & 51 deletions rta/src/main/java/com/gluonhq/richtextarea/RichTextAreaSkin.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
Expand Down Expand Up @@ -361,7 +365,53 @@ protected void invalidated() {
private int nonTextNodesCount;
AtomicInteger nonTextNodes = new AtomicInteger();

// attachedProperty
private final BooleanProperty attachedProperty = new SimpleBooleanProperty() {
@Override
protected void invalidated() {
if (promptVisibleBinding == null) {
promptVisibleBinding = Bindings.createBooleanBinding(
() -> {
Point2D point2D = caretOriginProperty.get();
boolean visible = viewModel.getTextLength() == 0 && viewModel.getCaretPosition() == 0 &&
point2D.getX() > DEFAULT_POINT_2D.getX() && point2D.getY() > DEFAULT_POINT_2D.getY();
if (visible) {
updatePromptNodeLocation();
}
return visible;
},
viewModel.caretPositionProperty(), viewModel.textLengthProperty(), caretOriginProperty);
}

if (get()) {
// bind control properties to viewModel properties, to forward the changes of the later
getSkinnable().textLengthProperty.bind(viewModel.textLengthProperty());
getSkinnable().modifiedProperty.bind(viewModel.savedProperty().not());
getSkinnable().selectionProperty.bind(viewModel.selectionProperty());
getSkinnable().decorationAtCaret.bind(viewModel.decorationAtCaretProperty());
getSkinnable().decorationAtParagraph.bind(viewModel.decorationAtParagraphProperty());
caretPositionProperty.bind(viewModel.caretPositionProperty());
promptNode.visibleProperty().bind(promptVisibleBinding);
promptNode.fontProperty().bind(promptFontBinding);
} else {
// unbind control properties from viewModel properties, to avoid forwarding
// the internal changes of the latter, while it performs an action
getSkinnable().textLengthProperty.unbind();
getSkinnable().modifiedProperty.unbind();
getSkinnable().selectionProperty.unbind();
getSkinnable().decorationAtCaret.unbind();
getSkinnable().decorationAtParagraph.unbind();
caretPositionProperty.unbind();
promptNode.visibleProperty().unbind();
promptNode.fontProperty().unbind();
}
}
};

private final ChangeListener<Document> documentChangeListener = (obs, ov, nv) -> {
if (!attachedProperty.get()) {
return;
}
if (ov == null && nv != null) {
// new/open
dispose();
Expand All @@ -373,11 +423,26 @@ protected void invalidated() {
}
};

private ObjectBinding<Font> promptFontBinding;
final ObjectProperty<Point2D> caretOriginProperty = new SimpleObjectProperty<>(this, "caretOrigin", DEFAULT_POINT_2D);

private final ObjectBinding<Font> promptFontBinding = Bindings.createObjectBinding(this::getPromptNodeFont,
viewModel.decorationAtCaretProperty(), viewModel.decorationAtParagraphProperty());
private BooleanBinding promptVisibleBinding;

private final ChangeListener<Number> caretChangeListener;
private final ChangeListener<Number> internalCaretChangeListener;
private final IntegerProperty caretPositionProperty = new SimpleIntegerProperty() {
@Override
protected void invalidated() {
int caret = get();
int externalCaret = caret;
if (caret > -1) {
String text = viewModel.getTextBuffer().getText(0, caret);
externalCaret = text.length();
}
getSkinnable().caretPosition.set(externalCaret);
viewModel.getParagraphWithCaret()
.ifPresent(paragraph -> Platform.runLater(paragraphListView::scrollIfNeeded));
}
};

private final InvalidationListener focusListener;
private final EventHandler<DragEvent> dndHandler = this::dndListener;
Expand All @@ -387,8 +452,6 @@ protected void invalidated() {

private final ResourceBundle resources;

final ObjectProperty<Point2D> caretOriginProperty = new SimpleObjectProperty<>(this, "caretOrigin", DEFAULT_POINT_2D);

private class RichVirtualFlow extends VirtualFlow<ListCell<Paragraph>> {

RichVirtualFlow(RichTextArea control) {
Expand Down Expand Up @@ -600,17 +663,6 @@ protected RichTextAreaSkin(final RichTextArea control) {

tableAllowedListener = (obs, ov, nv) -> viewModel.setTableAllowed(nv);
skinToneChangeListener = (obs, ov, nv) -> refreshTextFlow();
caretChangeListener = (obs, ov, nv) -> viewModel.getParagraphWithCaret()
.ifPresent(paragraph -> Platform.runLater(paragraphListView::scrollIfNeeded));
internalCaretChangeListener = (obs, ov, nv) -> {
int caret = nv.intValue();
int externalCaret = caret;
if (caret > -1) {
String text = viewModel.getTextBuffer().getText(0, caret);
externalCaret = text.length();
}
getSkinnable().caretPosition.set(externalCaret);
};

focusListener = o -> paragraphListView.updateLayout();

Expand All @@ -628,6 +680,8 @@ protected RichTextAreaSkin(final RichTextArea control) {
// set prompt text
promptNode = new Text();
setupPromptNode();

viewModel.attachedProperty().subscribe((b0, b) -> attachedProperty.set(b));
setup(control.getDocument());
}

Expand All @@ -639,23 +693,14 @@ protected RichTextAreaSkin(final RichTextArea control) {
@Override
public void dispose() {
viewModel.clearSelection();
viewModel.caretPositionProperty().removeListener(caretChangeListener);
viewModel.caretPositionProperty().removeListener(internalCaretChangeListener);
viewModel.removeChangeListener(textChangeListener);
viewModel.documentProperty().removeListener(documentChangeListener);
viewModel.autoSaveProperty().unbind();
lastValidCaretPosition = -1;
promptNode.textProperty().unbind();
promptNode.fillProperty().unbind();
promptNode.visibleProperty().unbind();
promptNode.fontProperty().unbind();
getSkinnable().editableProperty().removeListener(this::editableChangeListener);
getSkinnable().tableAllowedProperty().removeListener(tableAllowedListener);
getSkinnable().textLengthProperty.unbind();
getSkinnable().modifiedProperty.unbind();
getSkinnable().selectionProperty.unbind();
getSkinnable().decorationAtCaret.unbind();
getSkinnable().decorationAtParagraph.unbind();
getSkinnable().setOnKeyPressed(null);
getSkinnable().setOnKeyTyped(null);
getSkinnable().widthProperty().removeListener(controlPrefWidthListener);
Expand All @@ -667,6 +712,7 @@ public void dispose() {
tableContextMenuItems = null;
editableContextMenuItems = null;
nonEditableContextMenuItems = null;
attachedProperty.set(false);
}

public RichTextAreaViewModel getViewModel() {
Expand All @@ -687,8 +733,7 @@ private void setup(Document document) {
if (document == null) {
return;
}
viewModel.caretPositionProperty().addListener(caretChangeListener);
viewModel.caretPositionProperty().addListener(internalCaretChangeListener);
attachedProperty.set(false);
viewModel.setTextBuffer(new PieceTable(document));
lastValidCaretPosition = viewModel.getTextBuffer().getInternalPosition(document.getCaretPosition());
viewModel.setCaretPosition(lastValidCaretPosition);
Expand All @@ -699,30 +744,6 @@ private void setup(Document document) {
viewModel.autoSaveProperty().bind(getSkinnable().autoSaveProperty());
promptNode.textProperty().bind(getSkinnable().promptTextProperty());
promptNode.fillProperty().bind(promptTextFillProperty());
if (promptVisibleBinding == null) {
promptVisibleBinding = Bindings.createBooleanBinding(
() -> {
Point2D point2D = caretOriginProperty.get();
boolean visible = viewModel.getTextLength() == 0 && viewModel.getCaretPosition() == 0 &&
point2D.getX() > DEFAULT_POINT_2D.getX() && point2D.getY() > DEFAULT_POINT_2D.getY();
if (visible) {
updatePromptNodeLocation();
}
return visible;
},
viewModel.caretPositionProperty(), viewModel.textLengthProperty(), caretOriginProperty);
}
promptNode.visibleProperty().bind(promptVisibleBinding);
if (promptFontBinding == null) {
promptFontBinding = Bindings.createObjectBinding(this::getPromptNodeFont,
viewModel.decorationAtCaretProperty(), viewModel.decorationAtParagraphProperty());
}
promptNode.fontProperty().bind(promptFontBinding);
getSkinnable().textLengthProperty.bind(viewModel.textLengthProperty());
getSkinnable().modifiedProperty.bind(viewModel.savedProperty().not());
getSkinnable().selectionProperty.bind(viewModel.selectionProperty());
getSkinnable().decorationAtCaret.bind(viewModel.decorationAtCaretProperty());
getSkinnable().decorationAtParagraph.bind(viewModel.decorationAtParagraphProperty());
getSkinnable().setOnContextMenuRequested(contextMenuEventEventHandler);
getSkinnable().editableProperty().addListener(this::editableChangeListener);
getSkinnable().tableAllowedProperty().addListener(tableAllowedListener);
Expand All @@ -736,6 +757,7 @@ private void setup(Document document) {
refreshTextFlow();
requestLayout();
editableChangeListener(null); // sets up all related listeners
attachedProperty.set(true);
}

private void setupPromptNode() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Gluon
* 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
Expand Down Expand Up @@ -32,22 +32,30 @@ public abstract class AbstractCommand<T> {
protected abstract void doUndo(T context);
protected abstract void doRedo(T context);

protected void storeContext(T context){}
protected void restoreContext(T context){}
protected void storeContext(T context) {}
protected void restoreContext(T context) {}
protected void attachContext(T context) {}
protected void detachContext(T context) {}

public final void execute(T context) {
detachContext(context);
storeContext(context);
doRedo(context);
attachContext(context);
}

public final void undo(T context) {
detachContext(context);
doUndo(context);
restoreContext(context);
attachContext(context);
}

public final void redo(T context) {
detachContext(context);
restoreContext(context);
doRedo(context);
attachContext(context);
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Gluon
* 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
Expand Down Expand Up @@ -54,6 +54,16 @@ protected void restoreContext( RichTextAreaViewModel viewModel ) {
viewModel.setSelection(selection);
}

@Override
protected void attachContext(RichTextAreaViewModel viewModel) {
Objects.requireNonNull(viewModel).attach();
}

@Override
protected void detachContext(RichTextAreaViewModel viewModel) {
Objects.requireNonNull(viewModel).detach();
}

@Override
public String toString() {
return getClass().getSimpleName() + " { " +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, 2023, Gluon
* 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
Expand Down Expand Up @@ -55,7 +55,6 @@
import javafx.scene.image.Image;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;

import java.text.BreakIterator;
import java.util.ArrayList;
Expand Down Expand Up @@ -307,6 +306,20 @@ public final boolean isSaved() {
return savedProperty.get();
}

// attachedProperty
private final ReadOnlyBooleanWrapper attachedProperty = new ReadOnlyBooleanWrapper(this, "attached", true);
public final ReadOnlyBooleanProperty attachedProperty() {
return attachedProperty.getReadOnlyProperty();
}

final void attach() {
attachedProperty.set(true);
}

final void detach() {
attachedProperty.set(false);
}

public RichTextAreaViewModel(BiFunction<Double, Boolean, Integer> getNextRowPosition, Function<Boolean, Integer> getNextTableCellPosition) {
this.getNextRowPosition = Objects.requireNonNull(getNextRowPosition);
this.getNextTableCellPosition = Objects.requireNonNull(getNextTableCellPosition);
Expand Down
Loading

0 comments on commit 5ff7c3d

Please sign in to comment.