() {
+ @Override
+ public String toString(TextStyle t) {
+ return t == null ? null : t.getDisplayName();
+ }
+
+ @Override
+ public TextStyle fromString(String s) {
+ for (TextStyle t : TextStyle.values()) {
+ if (s.equals(t.getDisplayName())) {
+ return t;
+ }
+ }
+ return TextStyle.BODY;
+ }
+ };
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/Actions.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/Actions.java
new file mode 100644
index 00000000000..7192931bc09
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/Actions.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.editor;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.Locale;
+import javafx.application.Platform;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.scene.control.ButtonBar.ButtonData;
+import javafx.scene.control.ButtonType;
+import javafx.scene.control.Dialog;
+import javafx.scene.input.DataFormat;
+import javafx.scene.paint.Color;
+import javafx.stage.FileChooser;
+import javafx.stage.Window;
+import com.oracle.demo.richtext.common.Styles;
+import com.oracle.demo.richtext.common.TextStyle;
+import com.oracle.demo.richtext.util.ExceptionDialog;
+import com.oracle.demo.richtext.util.FX;
+import com.oracle.demo.richtext.util.FxAction;
+import jfx.incubator.scene.control.richtext.LineNumberDecorator;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.SelectionSegment;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.ContentChange;
+import jfx.incubator.scene.control.richtext.model.RichTextFormatHandler;
+import jfx.incubator.scene.control.richtext.model.RichTextModel;
+import jfx.incubator.scene.control.richtext.model.StyleAttribute;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+import jfx.incubator.scene.control.richtext.model.StyledTextModel;
+
+/**
+ * This is a bit of hack. JavaFX has no actions (yet), so here we are using FxActions from
+ * https://github.com/andy-goryachev/AppFramework (with permission from the author).
+ * Ideally, these actions should be created upon demand and managed by the control, because
+ * control knows when the enabled state of each action changes.
+ *
+ * This class adds a listener to the model and updates the states of all the actions.
+ * (The model does not change in this application).
+ *
+ * @author Andy Goryachev
+ */
+public class Actions {
+ // file
+ public final FxAction newDocument = new FxAction(this::newDocument);
+ public final FxAction open = new FxAction(this::open);
+ public final FxAction save = new FxAction(this::save);
+ public final FxAction saveAs = new FxAction(this::saveAs);
+ // style
+ public final FxAction bold = new FxAction(this::bold);
+ public final FxAction italic = new FxAction(this::italic);
+ public final FxAction strikeThrough = new FxAction(this::strikeThrough);
+ public final FxAction underline = new FxAction(this::underline);
+ // editing
+ public final FxAction copy = new FxAction(this::copy);
+ public final FxAction cut = new FxAction(this::cut);
+ public final FxAction paste = new FxAction(this::paste);
+ public final FxAction pasteUnformatted = new FxAction(this::pasteUnformatted);
+ public final FxAction redo = new FxAction(this::redo);
+ public final FxAction selectAll = new FxAction(this::selectAll);
+ public final FxAction undo = new FxAction(this::undo);
+ // view
+ public final FxAction highlightCurrentLine = new FxAction();
+ public final FxAction lineNumbers = new FxAction();
+ public final FxAction wrapText = new FxAction();
+
+ private final RichTextArea control;
+ private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
+ private final ReadOnlyObjectWrapper file = new ReadOnlyObjectWrapper<>();
+ private final SimpleObjectProperty styles = new SimpleObjectProperty<>();
+ private final SimpleObjectProperty textStyle = new SimpleObjectProperty<>();
+
+ public Actions(RichTextArea control) {
+ this.control = control;
+
+ // undo/redo actions
+ redo.disabledProperty().bind(control.redoableProperty().not());
+ undo.disabledProperty().bind(control.undoableProperty().not());
+
+ undo.disabledProperty().bind(Bindings.createBooleanBinding(() -> {
+ return !control.isUndoable();
+ }, control.undoableProperty()));
+
+ redo.disabledProperty().bind(Bindings.createBooleanBinding(() -> {
+ return !control.isRedoable();
+ }, control.redoableProperty()));
+
+ highlightCurrentLine.selectedProperty().bindBidirectional(control.highlightCurrentParagraphProperty());
+ wrapText.selectedProperty().bindBidirectional(control.wrapTextProperty());
+
+ lineNumbers.selectedProperty().addListener((s,p,on) -> {
+ control.setLeftDecorator(on ? new LineNumberDecorator() : null);
+ });
+
+ control.getModel().addListener(new StyledTextModel.Listener() {
+ @Override
+ public void onContentChange(ContentChange ch) {
+ handleEdit();
+ }
+ });
+
+ control.caretPositionProperty().addListener((x) -> {
+ handleCaret();
+ });
+
+ control.selectionProperty().addListener((p) -> {
+ updateSourceStyles();
+ });
+
+ styles.addListener((s,p,a) -> {
+ bold.setSelected(hasStyle(a, StyleAttributeMap.BOLD), false);
+ italic.setSelected(hasStyle(a, StyleAttributeMap.ITALIC), false);
+ strikeThrough.setSelected(hasStyle(a, StyleAttributeMap.STRIKE_THROUGH), false);
+ underline.setSelected(hasStyle(a, StyleAttributeMap.UNDERLINE), false);
+ });
+
+ updateSourceStyles();
+
+ // defaults
+ highlightCurrentLine.setSelected(true, false);
+ wrapText.setSelected(true, false);
+
+ handleEdit();
+ handleCaret();
+ setModified(false);
+ }
+
+ private boolean hasStyle(StyleAttributeMap attrs, StyleAttribute a) {
+ return attrs == null ? false : Boolean.TRUE.equals(attrs.get(a));
+ }
+
+ public final ObjectProperty textStyleProperty() {
+ return textStyle;
+ }
+
+ public final ReadOnlyBooleanProperty modifiedProperty() {
+ return modified.getReadOnlyProperty();
+ }
+
+ public final boolean isModified() {
+ return modified.get();
+ }
+
+ private void setModified(boolean on) {
+ modified.set(on);
+ }
+
+ public final ReadOnlyObjectProperty fileNameProperty() {
+ return file.getReadOnlyProperty();
+ }
+
+ public final File getFile() {
+ return file.get();
+ }
+
+ private void handleEdit() {
+ setModified(true);
+ }
+
+ private void handleCaret() {
+ boolean sel = control.hasNonEmptySelection();
+ StyleAttributeMap a = control.getActiveStyleAttributeMap();
+
+ cut.setEnabled(sel);
+ copy.setEnabled(sel);
+
+ bold.setSelected(a.getBoolean(StyleAttributeMap.BOLD), false);
+ italic.setSelected(a.getBoolean(StyleAttributeMap.ITALIC), false);
+ underline.setSelected(a.getBoolean(StyleAttributeMap.UNDERLINE), false);
+ strikeThrough.setSelected(a.getBoolean(StyleAttributeMap.STRIKE_THROUGH), false);
+ }
+
+ private void toggle(StyleAttribute attr) {
+ TextPos start = control.getAnchorPosition();
+ TextPos end = control.getCaretPosition();
+ if (start == null) {
+ return;
+ } else if (start.equals(end)) {
+ // apply to the whole paragraph
+ int ix = start.index();
+ start = TextPos.ofLeading(ix, 0);
+ end = control.getParagraphEnd(ix);
+ }
+
+ StyleAttributeMap a = control.getActiveStyleAttributeMap();
+ boolean on = !a.getBoolean(attr);
+ a = StyleAttributeMap.builder().set(attr, on).build();
+ control.applyStyle(start, end, a);
+ }
+
+ private void apply(StyleAttribute attr, T value) {
+ TextPos start = control.getAnchorPosition();
+ TextPos end = control.getCaretPosition();
+ if (start == null) {
+ return;
+ } else if (start.equals(end)) {
+ // apply to the whole paragraph
+ int ix = start.index();
+ start = TextPos.ofLeading(ix, 0);
+ end = control.getParagraphEnd(ix);
+ }
+
+ StyleAttributeMap a = control.getActiveStyleAttributeMap();
+ a = StyleAttributeMap.builder().set(attr, value).build();
+ control.applyStyle(start, end, a);
+ }
+
+ // TODO need to bind selected item in the combo
+ public void setFontSize(Integer size) {
+ apply(StyleAttributeMap.FONT_SIZE, size.doubleValue());
+ }
+
+ // TODO need to bind selected item in the combo
+ public void setFontName(String name) {
+ apply(StyleAttributeMap.FONT_FAMILY, name);
+ }
+
+ public void setTextColor(Color color) {
+ apply(StyleAttributeMap.TEXT_COLOR, color);
+ }
+
+ void newDocument() {
+ if (askToSave()) {
+ return;
+ }
+ control.setModel(new RichTextModel());
+ setModified(false);
+ }
+
+ void open() {
+ if (askToSave()) {
+ return;
+ }
+
+ FileChooser ch = new FileChooser();
+ ch.setTitle("Open File");
+ ch.getExtensionFilters().addAll(
+ filterAll(),
+ filterRich(),
+ filterRtf()
+ );
+
+ Window w = FX.getParentWindow(control);
+ File f = ch.showOpenDialog(w);
+ if (f != null) {
+ try {
+ DataFormat fmt = guessFormat(f);
+ readFile(f, fmt);
+ } catch (Exception e) {
+ new ExceptionDialog(control, e).open();
+ }
+ }
+ }
+
+ void save() {
+ File f = getFile();
+ if (f == null) {
+ f = chooseFileForSave();
+ if (f != null) {
+ return;
+ }
+ }
+
+ try {
+ writeFile(f);
+ } catch (Exception e) {
+ new ExceptionDialog(control, e).open();
+ }
+ }
+
+ boolean saveAs() {
+ File f = chooseFileForSave();
+ if (f != null) {
+ // TODO ask to overwrite if file exists
+ file.set(f);
+ try {
+ writeFile(f);
+ return true;
+ } catch(Exception e) {
+ new ExceptionDialog(control, e).open();
+ }
+ }
+ return false;
+ }
+
+ private File chooseFileForSave() {
+ File f = getFile();
+ FileChooser ch = new FileChooser();
+ if (f != null) {
+ ch.setInitialDirectory(f.getParentFile());
+ ch.setInitialFileName(f.getName());
+ }
+ ch.setTitle("Save File");
+ ch.getExtensionFilters().addAll(
+ filterRich(),
+ filterRtf(),
+ filterTxt()
+ //filterAll()
+ );
+ Window w = FX.getParentWindow(control);
+ return ch.showSaveDialog(w);
+ }
+
+ private void readFile(File f, DataFormat fmt) throws Exception {
+ try (FileInputStream in = new FileInputStream(f)) {
+ control.read(fmt, in);
+ file.set(f);
+ control.setEditable(f.canWrite());
+ setModified(false);
+ }
+ }
+
+ private void writeFile(File f) throws Exception {
+ DataFormat fmt = guessFormat(f);
+ try (FileOutputStream out = new FileOutputStream(f)) {
+ control.write(fmt, out);
+ file.set(f);
+ setModified(false);
+ }
+ }
+
+ void copy() {
+ control.copy();
+ }
+
+ void cut() {
+ control.cut();
+ }
+
+ void paste() {
+ control.paste();
+ }
+
+ void pasteUnformatted() {
+ control.pastePlainText();
+ }
+
+ void selectAll() {
+ control.selectAll();
+ }
+
+ void redo() {
+ control.redo();
+ }
+
+ void undo() {
+ control.undo();
+ }
+
+ void bold() {
+ toggleStyle(StyleAttributeMap.BOLD);
+ }
+
+ void italic() {
+ toggleStyle(StyleAttributeMap.ITALIC);
+ }
+
+ void strikeThrough() {
+ toggleStyle(StyleAttributeMap.STRIKE_THROUGH);
+ }
+
+ void underline() {
+ toggleStyle(StyleAttributeMap.UNDERLINE);
+ }
+
+ private void toggleStyle(StyleAttribute attr) {
+ TextPos start = control.getAnchorPosition();
+ TextPos end = control.getCaretPosition();
+ if (start == null) {
+ return;
+ } else if (start.equals(end)) {
+ // apply to the whole paragraph
+ int ix = start.index();
+ start = TextPos.ofLeading(ix, 0);
+ end = control.getParagraphEnd(ix);
+ }
+
+ StyleAttributeMap a = control.getActiveStyleAttributeMap();
+ boolean on = !a.getBoolean(attr);
+ a = StyleAttributeMap.builder().set(attr, on).build();
+ control.applyStyle(start, end, a);
+ updateSourceStyles();
+ }
+
+ public void setTextStyle(TextStyle st) {
+ TextPos start = control.getAnchorPosition();
+ TextPos end = control.getCaretPosition();
+ if (start == null) {
+ return;
+ } else if (start.equals(end)) {
+ TextStyle cur = Styles.guessTextStyle(control.getActiveStyleAttributeMap());
+ if (cur == st) {
+ return;
+ }
+ // apply to the whole paragraph
+ int ix = start.index();
+ start = TextPos.ofLeading(ix, 0);
+ end = control.getParagraphEnd(ix);
+ }
+
+ StyleAttributeMap a = Styles.getStyleAttributeMap(st);
+ control.applyStyle(start, end, a);
+ updateSourceStyles();
+ }
+
+ private void updateSourceStyles() {
+ StyleAttributeMap a = getSourceStyleAttrs();
+ if (a != null) {
+ styles.set(a);
+
+ TextStyle st = Styles.guessTextStyle(a);
+ textStyle.set(st);
+ }
+ }
+
+ private StyleAttributeMap getSourceStyleAttrs() {
+ SelectionSegment sel = control.getSelection();
+ if ((sel == null) || (!sel.isCollapsed())) {
+ return null;
+ }
+ return control.getActiveStyleAttributeMap();
+ }
+
+ static FileChooser.ExtensionFilter filterAll() {
+ return new FileChooser.ExtensionFilter("All Files", "*.*");
+ }
+
+ static FileChooser.ExtensionFilter filterRich() {
+ return new FileChooser.ExtensionFilter("Rich Text Files", "*.rich");
+ }
+
+ static FileChooser.ExtensionFilter filterRtf() {
+ return new FileChooser.ExtensionFilter("RTF Files", "*.rtf");
+ }
+
+ static FileChooser.ExtensionFilter filterTxt() {
+ return new FileChooser.ExtensionFilter("Text Files", "*.txt");
+ }
+
+ private static DataFormat guessFormat(File f) {
+ String name = f.getName().toLowerCase(Locale.ENGLISH);
+ if (name.endsWith(".rich")) {
+ return RichTextFormatHandler.DATA_FORMAT;
+ } else if (name.endsWith(".rtf")) {
+ return DataFormat.RTF;
+ }
+ return DataFormat.PLAIN_TEXT;
+ }
+
+ public void quit() {
+ if (askToSave()) {
+ return;
+ }
+ Platform.exit();
+ }
+
+ enum UserChoiceToSave {
+ DISCARD_CHANGES,
+ RETURN_TO_EDITING,
+ SAVE
+ }
+
+ private UserChoiceToSave askSaveChanges() {
+ Dialog d = new Dialog<>();
+ d.initOwner(FX.getParentWindow(control));
+ d.setTitle("Save Changes?");
+ d.setContentText("Do you want to save changes?");
+
+ ButtonType bSave = new ButtonType("Save", ButtonData.YES);
+ d.getDialogPane().getButtonTypes().add(bSave);
+ ButtonType bReturn = new ButtonType("Return to Editing", ButtonData.CANCEL_CLOSE);
+ d.getDialogPane().getButtonTypes().add(bReturn);
+ ButtonType bDiscard = new ButtonType("Discard Changes", ButtonData.NO);
+ d.getDialogPane().getButtonTypes().add(bDiscard);
+ d.showAndWait();
+
+ Object v = d.getResult();
+ if (v == bSave) {
+ return UserChoiceToSave.SAVE;
+ } else if (v == bDiscard) {
+ return UserChoiceToSave.DISCARD_CHANGES;
+ } else {
+ return UserChoiceToSave.RETURN_TO_EDITING;
+ }
+ }
+
+ /**
+ * Checks whether the document has been modified and if so, asks to Save, Discard or Cancel.
+ * @return true if the user chose to Cancel
+ */
+ public boolean askToSave() {
+ if (isModified()) {
+ switch(askSaveChanges()) {
+ case DISCARD_CHANGES:
+ return false;
+ case SAVE:
+ return saveAs();
+ case RETURN_TO_EDITING:
+ default:
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoApp.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoApp.java
new file mode 100644
index 00000000000..4fbcf5d1f29
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoApp.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.editor;
+
+import javafx.application.Application;
+import javafx.stage.Stage;
+import com.oracle.demo.richtext.settings.FxSettings;
+
+/**
+ * Rich Text Editor Demo Application.
+ *
+ * It's a minimal rich text editor based on the new
+ * {@link jfx.incubator.scene.control.richtext.RichTextArea} control.
+ *
+ * @author Andy Goryachev
+ */
+public class RichEditorDemoApp extends Application {
+ public static void main(String[] args) {
+ Application.launch(RichEditorDemoApp.class, args);
+ }
+
+ @Override
+ public void init() {
+ FxSettings.useDirectory(".RichEditorDemoApp");
+ }
+
+ @Override
+ public void start(Stage stage) throws Exception {
+ new RichEditorDemoWindow().show();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoPane.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoPane.java
new file mode 100644
index 00000000000..10a9375f845
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoPane.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.editor;
+
+import java.util.List;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.ToolBar;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.text.Font;
+import com.oracle.demo.richtext.common.TextStyle;
+import com.oracle.demo.richtext.editor.settings.EndKey;
+import com.oracle.demo.richtext.util.FX;
+import jfx.incubator.scene.control.input.KeyBinding;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.TextPos;
+
+/**
+ * Main Panel.
+ *
+ * @author Andy Goryachev
+ */
+public class RichEditorDemoPane extends BorderPane {
+ public final RichTextArea editor;
+ public final Actions actions;
+ private final ComboBox fontName;
+ private final ComboBox fontSize;
+ private final ColorPicker textColor;
+ private final ComboBox textStyle;
+
+ public RichEditorDemoPane() {
+ FX.name(this, "RichEditorDemoPane");
+
+ editor = new RichTextArea();
+ // custom function
+ editor.getInputMap().register(KeyBinding.shortcut(KeyCode.W), () -> {
+ System.out.println("Custom function: W key is pressed");
+ });
+
+ actions = new Actions(editor);
+ editor.setContextMenu(createContextMenu());
+
+ fontName = new ComboBox<>();
+ fontName.getItems().setAll(collectFonts());
+ fontName.setOnAction((ev) -> {
+ actions.setFontName(fontName.getSelectionModel().getSelectedItem());
+ });
+
+ fontSize = new ComboBox<>();
+ fontSize.getItems().setAll(
+ 7,
+ 8,
+ 9,
+ 10,
+ 11,
+ 12,
+ 13,
+ 14,
+ 16,
+ 18,
+ 20,
+ 22,
+ 24,
+ 28,
+ 32,
+ 36,
+ 48,
+ 72,
+ 96,
+ 128
+ );
+ fontSize.setOnAction((ev) -> {
+ actions.setFontSize(fontSize.getSelectionModel().getSelectedItem());
+ });
+
+ textColor = new ColorPicker();
+ // TODO save/restore custom colors
+ FX.tooltip(textColor, "Text Color");
+ // FIX there is no API for this! why is this a property of a skin, not the control??
+ // https://stackoverflow.com/questions/21246137/remove-text-from-colour-picker
+ textColor.setStyle("-fx-color-label-visible: false ;");
+ textColor.setOnAction((ev) -> {
+ actions.setTextColor(textColor.getValue());
+ });
+
+ textStyle = new ComboBox<>();
+ textStyle.getItems().setAll(TextStyle.values());
+ textStyle.setConverter(TextStyle.converter());
+ textStyle.setOnAction((ev) -> {
+ updateTextStyle();
+ editor.requestFocus();
+ });
+
+ setTop(createToolBar());
+ setCenter(editor);
+
+ actions.textStyleProperty().addListener((s,p,c) -> {
+ setTextStyle(c);
+ });
+
+ Settings.endKey.subscribe(this::setEndKey);
+ }
+
+ private ToolBar createToolBar() {
+ ToolBar t = new ToolBar();
+ FX.add(t, fontName);
+ FX.add(t, fontSize);
+ FX.add(t, textColor);
+ FX.space(t);
+ // TODO background
+ // TODO alignment
+ // TODO bullet
+ // TODO space left (indent left, indent right)
+ // TODO line spacing
+ FX.toggleButton(t, "𝐁", "Bold text", actions.bold);
+ FX.toggleButton(t, "𝐼", "Bold text", actions.italic);
+ FX.toggleButton(t, "S\u0336", "Strike through text", actions.strikeThrough);
+ FX.toggleButton(t, "U\u0332", "Underline text", actions.underline);
+ FX.add(t, textStyle);
+ FX.space(t);
+ FX.toggleButton(t, "W", "Wrap Text", actions.wrapText);
+ // TODO line numbers
+ return t;
+ }
+
+ private ContextMenu createContextMenu() {
+ ContextMenu m = new ContextMenu();
+ FX.item(m, "Undo", actions.undo);
+ FX.item(m, "Redo", actions.redo);
+ FX.separator(m);
+ FX.item(m, "Cut", actions.cut);
+ FX.item(m, "Copy", actions.copy);
+ FX.item(m, "Paste", actions.paste);
+ FX.item(m, "Paste and Retain Style", actions.pasteUnformatted);
+ FX.separator(m);
+ FX.item(m, "Select All", actions.selectAll);
+ FX.separator(m);
+ // TODO under "Style" submenu?
+ FX.item(m, "Bold", actions.bold);
+ FX.item(m, "Italic", actions.italic);
+ FX.item(m, "Strike Through", actions.strikeThrough);
+ FX.item(m, "Underline", actions.underline);
+ return m;
+ }
+
+ private static List collectFonts() {
+ return Font.getFamilies();
+ }
+
+ private void updateTextStyle() {
+ TextStyle st = textStyle.getSelectionModel().getSelectedItem();
+ if (st != null) {
+ actions.setTextStyle(st);
+ }
+ }
+
+ public void setTextStyle(TextStyle v) {
+ textStyle.setValue(v);
+ }
+
+ void setEndKey(EndKey v) {
+ switch(v) {
+ case END_OF_LINE:
+ editor.getInputMap().restoreDefaultFunction(RichTextArea.Tag.MOVE_TO_LINE_END);
+ break;
+ case END_OF_TEXT:
+ editor.getInputMap().registerFunction(RichTextArea.Tag.MOVE_TO_LINE_END, this::moveToEndOfText);
+ break;
+ }
+ }
+
+ // this is an illustration. we could publish the MOVE_TO_END_OF_TEXT_ON_LINE function tag
+ void moveToEndOfText() {
+ TextPos p = editor.getCaretPosition();
+ if (p != null) {
+ editor.executeDefault(RichTextArea.Tag.MOVE_TO_LINE_END);
+ TextPos p2 = editor.getCaretPosition();
+ if (p2 != null) {
+ String text = editor.getPlainText(p2.index());
+ int ix = findLastText(text, p2.charIndex());
+ if (ix > p.charIndex()) {
+ editor.select(TextPos.ofLeading(p2.index(), ix));
+ }
+ }
+ }
+ }
+
+ private static int findLastText(String text, int start) {
+ int i = start - 1;
+ while (i >= 0) {
+ char c = text.charAt(i);
+ if (!Character.isWhitespace(c)) {
+ return i + 1;
+ }
+ --i;
+ }
+ return i;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoWindow.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoWindow.java
new file mode 100644
index 00000000000..17082973165
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/RichEditorDemoWindow.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.editor;
+
+import java.io.File;
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuBar;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
+import com.oracle.demo.richtext.rta.RichTextAreaWindow;
+import com.oracle.demo.richtext.util.FX;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.TextPos;
+
+/**
+ * Rich Editor Demo window.
+ *
+ * @author Andy Goryachev
+ */
+public class RichEditorDemoWindow extends Stage {
+ public final RichEditorDemoPane pane;
+ public final Label status;
+
+ public RichEditorDemoWindow() {
+ pane = new RichEditorDemoPane();
+
+ status = new Label();
+ status.setPadding(new Insets(2, 10, 2, 10));
+
+ BorderPane bp = new BorderPane();
+ bp.setTop(createMenu());
+ bp.setCenter(pane);
+ bp.setBottom(status);
+
+ Scene scene = new Scene(bp);
+
+ setScene(scene);
+ setWidth(1200);
+ setHeight(600);
+
+ pane.editor.caretPositionProperty().addListener((x) -> {
+ updateStatus();
+ });
+ pane.actions.modifiedProperty().addListener((x) -> {
+ updateTitle();
+ });
+ pane.actions.fileNameProperty().addListener((x) -> {
+ updateTitle();
+ });
+ addEventHandler(WindowEvent.WINDOW_CLOSE_REQUEST, (ev) -> {
+ if (pane.actions.askToSave()) {
+ ev.consume();
+ }
+ });
+
+ updateStatus();
+ updateTitle();
+ }
+
+ private MenuBar createMenu() {
+ Actions actions = pane.actions;
+ MenuBar m = new MenuBar();
+ // file
+ FX.menu(m, "File");
+ FX.item(m, "New", actions.newDocument).setAccelerator(KeyCombination.keyCombination("shortcut+N"));
+ FX.item(m, "Open...", actions.open);
+ FX.separator(m);
+ FX.item(m, "Save", actions.save).setAccelerator(KeyCombination.keyCombination("shortcut+S"));
+ FX.item(m, "Save As...", actions.saveAs).setAccelerator(KeyCombination.keyCombination("shortcut+A"));
+ FX.item(m, "Quit", actions::quit);
+
+ // edit
+ FX.menu(m, "Edit");
+ FX.item(m, "Undo", actions.undo);
+ FX.item(m, "Redo", actions.redo);
+ FX.separator(m);
+ FX.item(m, "Cut", actions.cut);
+ FX.item(m, "Copy", actions.copy);
+ FX.item(m, "Paste", actions.paste);
+ FX.item(m, "Paste and Retain Style", actions.pasteUnformatted);
+
+ // format
+ FX.menu(m, "Format");
+ FX.item(m, "Bold", actions.bold).setAccelerator(KeyCombination.keyCombination("shortcut+B"));
+ FX.item(m, "Italic", actions.italic).setAccelerator(KeyCombination.keyCombination("shortcut+I"));
+ FX.item(m, "Strike Through", actions.strikeThrough);
+ FX.item(m, "Underline", actions.underline).setAccelerator(KeyCombination.keyCombination("shortcut+U"));
+
+ // view
+ FX.menu(m, "View");
+ FX.checkItem(m, "Highlight Current Paragraph", actions.highlightCurrentLine);
+ FX.checkItem(m, "Show Line Numbers", actions.lineNumbers);
+ FX.checkItem(m, "Wrap Text", actions.wrapText);
+ // TODO line spacing
+
+ // view
+ FX.menu(m, "Tools");
+ FX.item(m, "Settings", this::openSettings);
+
+ // help
+ FX.menu(m, "Help");
+ FX.item(m, "About"); // TODO
+
+ return m;
+ }
+
+ private void updateStatus() {
+ RichTextArea ed = pane.editor;
+ TextPos p = ed.getCaretPosition();
+
+ StringBuilder sb = new StringBuilder();
+
+ if (p != null) {
+ sb.append(" Line: ").append(p.index() + 1);
+ sb.append(" Column: ").append(p.offset() + 1);
+ }
+
+ status.setText(sb.toString());
+ }
+
+ private void updateTitle() {
+ File f = pane.actions.getFile();
+ boolean modified = pane.actions.isModified();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("Rich Text Editor Demo");
+ if (f != null) {
+ sb.append(" - ");
+ sb.append(f.getName());
+ }
+ if (modified) {
+ sb.append(" *");
+ }
+ setTitle(sb.toString());
+ }
+
+ void openSettings() {
+ new SettingsWindow(this).show();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/Settings.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/Settings.java
new file mode 100644
index 00000000000..041ccc39b26
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/Settings.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.editor;
+
+import javafx.beans.property.SimpleObjectProperty;
+import com.oracle.demo.richtext.editor.settings.EndKey;
+
+/**
+ * Application Settings.
+ *
+ * NOTE: these settings are not persisted across launches due to limitations of FxSettings in this project.
+ */
+public final class Settings {
+ public static final SimpleObjectProperty endKey = new SimpleObjectProperty<>(EndKey.END_OF_TEXT);
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/SettingsWindow.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/SettingsWindow.java
new file mode 100644
index 00000000000..fc55967aad4
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/SettingsWindow.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.editor;
+
+import javafx.beans.property.Property;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.ComboBox;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import com.oracle.demo.richtext.common.OptionPane;
+import com.oracle.demo.richtext.editor.settings.EndKey;
+import com.oracle.demo.richtext.util.FX;
+
+/**
+ * Rich Editor Demo Settings window.
+ *
+ * @author Andy Goryachev
+ */
+public class SettingsWindow extends Stage {
+
+ private final OptionPane op;
+
+ public SettingsWindow(Window parent) {
+
+ initOwner(parent);
+
+ op = new OptionPane();
+ op.section("Navigation");
+ op.option("End:", enumOption(EndKey.class, Settings.endKey));
+// op.option("Home:", new ComboBox());
+// op.option("Next Word:", new ComboBox());
+// op.option("Previous Word:", new ComboBox());
+
+ Scene scene = new Scene(op);
+
+ setScene(scene);
+ setWidth(700);
+ setHeight(500);
+ setTitle("Settings");
+ centerOnScreen();
+ }
+
+ private static Node enumOption(Class type, Property p) {
+ ComboBox b = new ComboBox<>();
+ b.setConverter(FX.converter());
+ E[] values = type.getEnumConstants();
+ for (E v : values) {
+ b.getItems().add(v);
+ }
+ b.getSelectionModel().select(p.getValue());
+ p.bind(b.getSelectionModel().selectedItemProperty());
+ return b;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/settings/EndKey.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/settings/EndKey.java
new file mode 100644
index 00000000000..b3be7a988e3
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/editor/settings/EndKey.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.editor.settings;
+
+import com.oracle.demo.richtext.util.HasDisplayText;
+
+/**
+ * END key behavior setting.
+ */
+public enum EndKey implements HasDisplayText {
+ END_OF_LINE,
+ END_OF_TEXT;
+
+ @Override
+ public String toDisplayString() {
+ switch (this) {
+ case END_OF_LINE:
+ return "End of Line";
+ case END_OF_TEXT:
+ return "End of Text";
+ default:
+ return "?" + this;
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/Actions.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/Actions.java
new file mode 100644
index 00000000000..408ebe6b517
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/Actions.java
@@ -0,0 +1,819 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyObjectWrapper;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ChangeListener;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.input.DataFormat;
+import com.oracle.demo.richtext.common.Styles;
+import com.oracle.demo.richtext.common.TextStyle;
+import com.oracle.demo.richtext.notebook.data.CellInfo;
+import com.oracle.demo.richtext.notebook.data.Notebook;
+import com.oracle.demo.richtext.util.FX;
+import com.oracle.demo.richtext.util.FxAction;
+import jfx.incubator.scene.control.richtext.CodeArea;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.SelectionSegment;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.ContentChange;
+import jfx.incubator.scene.control.richtext.model.StyleAttribute;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+import jfx.incubator.scene.control.richtext.model.StyledTextModel;
+
+/**
+ * This class reacts to changes in application state such as currently active cell,
+ * caret, selection, model, etc.; then updates the actions disabled and selected properties.
+ *
+ * JavaFX has no actions (yet), so here we are using FxActions from
+ * https://github.com/andy-goryachev/AppFramework (with permission from the author).
+ *
+ * @author Andy Goryachev
+ */
+public class Actions {
+ // file
+ public final FxAction newDocument = new FxAction(this::newDocument);
+ public final FxAction open = new FxAction(this::open);
+ public final FxAction save = new FxAction(this::save);
+ // style
+ public final FxAction bold = new FxAction(this::bold);
+ public final FxAction italic = new FxAction(this::italic);
+ public final FxAction strikeThrough = new FxAction(this::strikeThrough);
+ public final FxAction underline = new FxAction(this::underline);
+ // editing
+ public final FxAction copy = new FxAction(this::copy);
+ public final FxAction cut = new FxAction(this::cut);
+ public final FxAction paste = new FxAction(this::paste);
+ public final FxAction pasteUnformatted = new FxAction(this::pasteUnformatted);
+ public final FxAction redo = new FxAction(this::redo);
+ public final FxAction selectAll = new FxAction(this::selectAll);
+ public final FxAction undo = new FxAction(this::undo);
+ // cells
+ public final FxAction copyCell = new FxAction(this::copyCell);
+ public final FxAction cutCell = new FxAction(this::cutCell);
+ public final FxAction deleteCell = new FxAction(this::deleteCell);
+ public final FxAction insertCellBelow = new FxAction(this::insertCellBelow);
+ public final FxAction mergeCellAbove = new FxAction(this::mergeCellAbove);
+ public final FxAction mergeCellBelow = new FxAction(this::mergeCellBelow);
+ public final FxAction moveCellDown = new FxAction(this::moveCellDown);
+ public final FxAction moveCellUp = new FxAction(this::moveCellUp);
+ public final FxAction pasteCellBelow = new FxAction(this::pasteCellBelow);
+ public final FxAction runAndAdvance = new FxAction(this::runAndAdvance);
+ public final FxAction runAll = new FxAction(this::runAll);
+ public final FxAction splitCell = new FxAction(this::splitCell);
+
+ private enum EditorType {
+ CODE,
+ NONE,
+ OUTPUT,
+ TEXT,
+ }
+
+ private final NotebookWindow window;
+ private final DemoScriptEngine engine;
+ private final ObservableList cellPanes = FXCollections.observableArrayList();
+ private final ReadOnlyObjectWrapper activeCellPane = new ReadOnlyObjectWrapper<>();
+ private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
+ private final ReadOnlyObjectWrapper file = new ReadOnlyObjectWrapper<>();
+ private final SimpleBooleanProperty executing = new SimpleBooleanProperty();
+ private final SimpleObjectProperty editor = new SimpleObjectProperty<>();
+ private final SimpleObjectProperty editorType = new SimpleObjectProperty<>(EditorType.NONE);
+ private final SimpleObjectProperty styles = new SimpleObjectProperty<>();
+ private final SimpleObjectProperty textStyle = new SimpleObjectProperty<>();
+ private final BooleanBinding disabledStyleEditing;
+ private int sequenceNumber;
+
+ public Actions(NotebookWindow w) {
+ this.window = w;
+
+ engine = new DemoScriptEngine();
+
+ BooleanBinding disabledEditing = Bindings.createBooleanBinding(
+ () -> {
+ if (isExecuting()) {
+ return true;
+ }
+ RichTextArea r = editor.get();
+ if (r == null) {
+ return true;
+ }
+ if (r.isEditable() && (r.getModel() != null) && (r.getModel().isWritable())) {
+ return false;
+ }
+ return true;
+ },
+ editor,
+ executing
+ );
+
+ disabledStyleEditing = Bindings.createBooleanBinding(
+ () -> {
+ if (isExecuting()) {
+ return true;
+ }
+ return (editorType.get() != EditorType.TEXT);
+ },
+ editorType,
+ executing
+ );
+
+ BooleanBinding cellActionsDisabled = Bindings.createBooleanBinding(
+ () -> {
+ if(isExecuting()) {
+ return true;
+ }
+ CellPane p = getActiveCellPane();
+ return p == null;
+ },
+ activeCellPane,
+ executing
+ );
+
+ BooleanBinding runDisabled = Bindings.createBooleanBinding(
+ () -> {
+ if(isExecuting()) {
+ return true;
+ }
+ CellType p = getActiveCellType();
+ return p != CellType.CODE;
+ },
+ activeCellPane,
+ executing
+ );
+
+ SimpleBooleanProperty redoDisabled = new SimpleBooleanProperty();
+ SimpleBooleanProperty undoDisabled = new SimpleBooleanProperty();
+
+ // file actions
+ open.setDisabled(true);
+ save.setDisabled(true);
+
+ // style actions
+ bold.disabledProperty().bind(disabledStyleEditing);
+ italic.disabledProperty().bind(disabledStyleEditing);
+ strikeThrough.disabledProperty().bind(disabledStyleEditing);
+ underline.disabledProperty().bind(disabledStyleEditing);
+
+ // editing actions
+ copy.setEnabled(true); // always
+ cut.disabledProperty().bind(disabledEditing);
+ paste.disabledProperty().bind(disabledEditing);
+ pasteUnformatted.disabledProperty().bind(disabledEditing);
+ selectAll.setEnabled(true); // always
+
+ // undo/redo actions
+ redo.disabledProperty().bind(redoDisabled);
+ undo.disabledProperty().bind(undoDisabled);
+
+ // cell actions
+ copyCell.setDisabled(true);
+ cutCell.setDisabled(true);
+ deleteCell.disabledProperty().bind(cellActionsDisabled);
+ insertCellBelow.disabledProperty().bind(cellActionsDisabled);
+ mergeCellAbove.setDisabled(true);
+ mergeCellBelow.setDisabled(true);
+ moveCellDown.disabledProperty().bind(cellActionsDisabled);
+ moveCellUp.disabledProperty().bind(cellActionsDisabled);
+ pasteCellBelow.setDisabled(true);
+ runAndAdvance.disabledProperty().bind(runDisabled);
+ runAll.setDisabled(true);
+ splitCell.disabledProperty().bind(disabledEditing);
+
+ // listeners
+
+ styles.addListener((s,p,a) -> {
+ bold.setSelected(hasStyle(a, StyleAttributeMap.BOLD), false);
+ italic.setSelected(hasStyle(a, StyleAttributeMap.ITALIC), false);
+ strikeThrough.setSelected(hasStyle(a, StyleAttributeMap.STRIKE_THROUGH), false);
+ underline.setSelected(hasStyle(a, StyleAttributeMap.UNDERLINE), false);
+ });
+
+ ChangeListener focusOwnerListener = (src, old, node) -> {
+ CellPane p = FX.findParentOf(CellPane.class, node);
+ if (p == null) {
+ return;
+ }
+
+ RichTextArea r = FX.findParentOf(RichTextArea.class, node);
+ editor.set(r);
+
+ EditorType t = getEditorType(r);
+ editorType.set(t);
+ updateSourceStyles();
+ };
+
+ window.sceneProperty().addListener((src, old, cur) -> {
+ if(old != null) {
+ old.focusOwnerProperty().removeListener(focusOwnerListener);
+ }
+ if(cur != null) {
+ cur.focusOwnerProperty().addListener(focusOwnerListener);
+ }
+ });
+
+ StyledTextModel.Listener changeListener = new StyledTextModel.Listener() {
+ @Override
+ public void onContentChange(ContentChange ch) {
+ if (ch.isEdit()) {
+ handleEdit();
+ } else {
+ if (editorType.get() == EditorType.TEXT) {
+ handleEdit();
+ }
+ }
+ }
+ };
+
+ InvalidationListener selectionListener = (p) -> {
+ updateSourceStyles();
+ };
+
+ editor.addListener((src, old, ed) -> {
+ if (old != null) {
+ if (isSourceEditor(old)) {
+ old.getModel().removeListener(changeListener);
+ old.selectionProperty().removeListener(selectionListener);
+ }
+ }
+
+ redoDisabled.unbind();
+ redoDisabled.set(true);
+ undoDisabled.unbind();
+ undoDisabled.set(true);
+
+ if (ed != null) {
+ if (isSourceEditor(ed)) {
+ ed.getModel().addListener(changeListener);
+ ed.selectionProperty().addListener(selectionListener);
+ redoDisabled.bind(executing.or(ed.redoableProperty().not()));
+ undoDisabled.bind(executing.or(ed.undoableProperty().not()));
+ }
+ }
+ });
+
+ updateSourceStyles();
+
+ activeCellPane.addListener((src, prev, cur) -> {
+ if (prev != null) {
+ prev.setActive(false);
+ }
+ if (cur != null) {
+ cur.setActive(true);
+ }
+ });
+ }
+
+ private EditorType getEditorType(RichTextArea r) {
+ if (r == null) {
+ return EditorType.NONE;
+ } else if (r instanceof CodeArea) {
+ if (r.isEditable()) {
+ return EditorType.CODE;
+ } else {
+ return EditorType.OUTPUT;
+ }
+ }
+ return EditorType.TEXT;
+ }
+
+ private boolean isSourceEditor(RichTextArea r) {
+ EditorType t = getEditorType(r);
+ switch (t) {
+ case CODE:
+ case TEXT:
+ return true;
+ }
+ return false;
+ }
+
+ private void updateSourceStyles() {
+ StyleAttributeMap a = getSourceStyleAttrs();
+ if (a != null) {
+ styles.set(a);
+
+ TextStyle st = Styles.guessTextStyle(a);
+ textStyle.set(st);
+ }
+ }
+
+ public final ObjectProperty textStyleProperty() {
+ return textStyle;
+ }
+
+ private StyleAttributeMap getSourceStyleAttrs() {
+ RichTextArea r = editor.get();
+ EditorType t = getEditorType(r);
+ switch (t) {
+ case TEXT:
+ SelectionSegment sel = r.getSelection();
+ if ((sel == null) || (!sel.isCollapsed())) {
+ return null;
+ }
+ return r.getActiveStyleAttributeMap();
+ }
+ return null;
+ }
+
+ private boolean hasStyle(StyleAttributeMap attrs, StyleAttribute a) {
+ return attrs == null ? false : Boolean.TRUE.equals(attrs.get(a));
+ }
+
+ private final boolean isExecuting() {
+ return executing.get();
+ }
+
+ private void setExecuting(boolean on) {
+ executing.set(on);
+ }
+
+ public final ReadOnlyBooleanProperty modifiedProperty() {
+ return modified.getReadOnlyProperty();
+ }
+
+ public final boolean isModified() {
+ return modified.get();
+ }
+
+ private void setModified(boolean on) {
+ modified.set(on);
+ }
+
+ public final ReadOnlyObjectProperty fileNameProperty() {
+ return file.getReadOnlyProperty();
+ }
+
+ public final File getFile() {
+ return file.get();
+ }
+
+ private void handleEdit() {
+ setModified(true);
+ }
+
+ public void newDocument() {
+// if (askToSave()) {
+// return;
+// }
+ Notebook n = new Notebook();
+ n.add(new CellInfo(CellType.CODE));
+ window.setNotebook(n);
+ }
+
+ private void open() {
+// if (askToSave()) {
+// return;
+// }
+//
+// FileChooser ch = new FileChooser();
+// ch.setTitle("Open File");
+// // TODO add extensions
+// Window w = FX.getParentWindow(control);
+// File f = ch.showOpenDialog(w);
+// if (f != null) {
+// try {
+// readFile(f, RichTextFormatHandler.DATA_FORMAT);
+// } catch (Exception e) {
+// new ExceptionDialog(control, e).open();
+// }
+// }
+ }
+
+ // FIX this is too simplistic, need save() and save as...
+ private void save() {
+// File f = getFile();
+// if (f == null) {
+// FileChooser ch = new FileChooser();
+// ch.setTitle("Save File");
+// // TODO add extensions
+// Window w = FX.getParentWindow(control);
+// f = ch.showSaveDialog(w);
+// if (f == null) {
+// return;
+// }
+// }
+// try {
+// writeFile(f, RichTextFormatHandler.DATA_FORMAT);
+// } catch (Exception e) {
+// new ExceptionDialog(control, e).open();
+// }
+ }
+
+ // returns true if the user chose to Cancel
+ private boolean askToSave() {
+// if (isModified()) {
+// // alert: has been modified. do you want to save?
+// Alert alert = new Alert(AlertType.CONFIRMATION);
+// alert.initOwner(FX.getParentWindow(control));
+// alert.setTitle("Document is Modified");
+// alert.setHeaderText("Do you want to save this document?");
+// ButtonType delete = new ButtonType("Delete");
+// ButtonType cancel = new ButtonType("Cancel", ButtonData.CANCEL_CLOSE);
+// ButtonType save = new ButtonType("Save", ButtonData.APPLY);
+// alert.getButtonTypes().setAll(
+// delete,
+// cancel,
+// save
+// );
+//
+// File f = getFile();
+// // FIX format selector is not needed!
+// SavePane sp = new SavePane();
+// sp.setFile(f);
+// alert.getDialogPane().setContent(sp);
+// Optional result = alert.showAndWait();
+// if (result.isPresent()) {
+// ButtonType t = result.get();
+// if (t == delete) {
+// return false;
+// } else if (t == cancel) {
+// return true;
+// } else {
+// // save using info in the panel
+// f = sp.getFile();
+// DataFormat fmt = sp.getFileFormat();
+// // FIX
+// fmt = RichTextFormatHandler.DATA_FORMAT;
+//
+// try {
+// writeFile(f, fmt);
+// } catch (Exception e) {
+// new ExceptionDialog(control, e).open();
+// return true;
+// }
+// }
+// } else {
+// return true;
+// }
+// }
+ return false;
+ }
+
+ private void readFile(File f, DataFormat fmt) throws Exception {
+// try (FileInputStream in = new FileInputStream(f)) {
+// control.read(fmt, in);
+// file.set(f);
+// modified.set(false);
+// }
+ }
+
+ private void writeFile(File f, DataFormat fmt) throws Exception {
+// try (FileOutputStream out = new FileOutputStream(f)) {
+// control.write(fmt, out);
+// file.set(f);
+// modified.set(false);
+// }
+ }
+
+ private void runAll() {
+ // TODO
+ }
+
+ public void runAndAdvance() {
+ CellPane p = getActiveCellPane();
+ if (p != null) {
+ CellInfo cell = p.getCellInfo();
+ if (cell.isCode()) {
+ String src = cell.getSource();
+ runScript(p, src, true);
+ }
+ }
+ }
+
+ private void runScript(CellPane p, String src, boolean advance) {
+ setExecuting(true);
+ p.setExecuting();
+
+ Thread t = new Thread("executing script [" + (sequenceNumber + 1) + "]") {
+ @Override
+ public void run() {
+ Object r;
+ try {
+ r = engine.executeScript(src);
+ } catch (Throwable e) {
+ r = e;
+ }
+ handleCompletion(p, r, advance);
+ }
+ };
+ t.setPriority(Thread.MIN_PRIORITY);
+ t.start();
+ }
+
+ /** this method is called from a background thread */
+ private void handleCompletion(CellPane p, Object result, boolean advance) {
+ Platform.runLater(() -> {
+ setExecuting(false);
+ p.setResult(result, ++sequenceNumber);
+ if (advance) {
+ int ix = getActiveCellIndex();
+ ix++;
+ if (ix < cellPanes.size()) {
+ CellPane next = cellPanes.get(ix);
+ setActiveCellPane(next);
+ next.focusLater();
+ }
+ }
+ });
+ }
+
+ public final ObservableList getCellPanes() {
+ return cellPanes;
+ }
+
+ public final void setActiveCellPane(CellPane p) {
+ activeCellPane.set(p);
+ }
+
+ public final CellPane getActiveCellPane() {
+ return activeCellPane.get();
+ }
+
+ private CellType getActiveCellType() {
+ CellPane p = getActiveCellPane();
+ return (p == null ? null : p.getCellType());
+ }
+
+ private RichTextArea getSourceEditor() {
+ CellPane p = getActiveCellPane();
+ return (p == null ? null : p.getSourceEditor());
+ }
+
+ public void setNotebook(Notebook b) {
+ int sz = b == null ? 0 : b.size();
+ ArrayList ps = new ArrayList<>(sz);
+ if (b != null) {
+ for (int i = 0; i < sz; i++) {
+ CellInfo cell = b.getCell(i);
+ ps.add(new CellPane(cell));
+ }
+ }
+ cellPanes.setAll(ps);
+ setModified(false);
+ }
+
+ public void copy() {
+ whenCell((t) -> t.copy());
+ }
+
+ public void cut() {
+ whenCell((t) -> t.cut());
+ }
+
+ public void paste() {
+ whenCell((t) -> t.paste());
+ }
+
+ public void pasteUnformatted() {
+ whenCell((t) -> t.pastePlainText());
+ }
+
+ private void whenCell(Consumer c) {
+ whenCell(null, c);
+ }
+
+ private void whenCell(CellType type, Consumer c) {
+ CellPane p = getActiveCellPane();
+ if (p != null) {
+ if (type != null) {
+ if (type != p.getCellType()) {
+ return;
+ }
+ }
+ RichTextArea r = p.getSourceEditor();
+ if (r != null) {
+ c.accept(r);
+ }
+ }
+ }
+
+ public int getActiveCellIndex() {
+ CellPane p = getActiveCellPane();
+ return cellPanes.indexOf(p);
+ }
+
+ public void insertCellBelow() {
+ int ix = getActiveCellIndex();
+ if (ix < 0) {
+ ix = 0;
+ }
+ CellInfo cell = new CellInfo(CellType.CODE);
+ CellPane p = new CellPane(cell);
+ add(ix + 1, p);
+ p.focusLater();
+ }
+
+ public void moveCellDown() {
+ int ix = getActiveCellIndex();
+ if (ix >= 0) {
+ if (ix + 1 < cellPanes.size()) {
+ CellPane p = cellPanes.remove(ix);
+ add(ix + 1, p);
+ }
+ }
+ }
+
+ public void moveCellUp() {
+ int ix = getActiveCellIndex();
+ if (ix > 0) {
+ CellPane p = cellPanes.remove(ix);
+ add(ix - 1, p);
+ }
+ }
+
+ private void add(int ix, CellPane p) {
+ if (ix < cellPanes.size()) {
+ cellPanes.add(ix, p);
+ } else {
+ cellPanes.add(p);
+ }
+ }
+
+ public void deleteCell() {
+ if (cellPanes.size() > 1) {
+ int ix = getActiveCellIndex();
+ if (ix >= 0) {
+ cellPanes.remove(ix);
+ }
+ }
+ }
+
+ public void selectAll() {
+ whenCell((c) -> {
+ c.selectAll();
+ });
+ }
+
+ public void redo() {
+ whenCell((c) -> {
+ c.redo();
+ });
+ }
+
+ public void undo() {
+ whenCell((c) -> {
+ c.undo();
+ });
+ }
+
+ public void bold() {
+ toggleStyle(StyleAttributeMap.BOLD);
+ }
+
+ public void italic() {
+ toggleStyle(StyleAttributeMap.ITALIC);
+ }
+
+ public void strikeThrough() {
+ toggleStyle(StyleAttributeMap.STRIKE_THROUGH);
+ }
+
+ public void underline() {
+ toggleStyle(StyleAttributeMap.UNDERLINE);
+ }
+
+ private void toggleStyle(StyleAttribute attr) {
+ whenCell(CellType.TEXT, (c) -> {
+ TextPos start = c.getAnchorPosition();
+ TextPos end = c.getCaretPosition();
+ if (start == null) {
+ return;
+ } else if (start.equals(end)) {
+ // apply to the whole paragraph
+ int ix = start.index();
+ start = TextPos.ofLeading(ix, 0);
+ end = c.getParagraphEnd(ix);
+ }
+
+ StyleAttributeMap a = c.getActiveStyleAttributeMap();
+ boolean on = !a.getBoolean(attr);
+ a = StyleAttributeMap.builder().set(attr, on).build();
+ c.applyStyle(start, end, a);
+ updateSourceStyles();
+ });
+ }
+
+ public void setTextStyle(TextStyle st) {
+ whenCell(CellType.TEXT, (c) -> {
+ TextPos start = c.getAnchorPosition();
+ TextPos end = c.getCaretPosition();
+ if (start == null) {
+ return;
+ } else if (start.equals(end)) {
+ TextStyle cur = Styles.guessTextStyle(c.getActiveStyleAttributeMap());
+ if (cur == st) {
+ return;
+ }
+ // apply to the whole paragraph
+ int ix = start.index();
+ start = TextPos.ofLeading(ix, 0);
+ end = c.getParagraphEnd(ix);
+ }
+
+ StyleAttributeMap a = Styles.getStyleAttributeMap(st);
+ c.applyStyle(start, end, a);
+ updateSourceStyles();
+ });
+ }
+
+ public void setActiveCellType(CellType t) {
+ if (t != null) {
+ CellPane p = getActiveCellPane();
+ int ix = cellPanes.indexOf(p);
+ if (ix >= 0) {
+ CellInfo cell = p.getCellInfo();
+ if (t != cell.getCellType()) {
+ cell.setCellType(t);
+ p = new CellPane(cell);
+ cellPanes.set(ix, p);
+ p.focusLater();
+ }
+ }
+ }
+ }
+
+ private void copyCell() {
+ // TODO
+ }
+
+ private void cutCell() {
+ // TODO
+ }
+
+ private void mergeCellAbove() {
+ // TODO
+ }
+
+ private void mergeCellBelow() {
+ // TODO
+ }
+
+ private void pasteCellBelow() {
+ // TODO
+ }
+
+ private void splitCell() {
+ whenCell((c) -> {
+ int ix = getActiveCellIndex();
+ if(ix < 0) {
+ return;
+ }
+ CellPane p = cellPanes.get(ix);
+ List ps = p.split();
+ if(ps == null) {
+ return;
+ }
+ cellPanes.remove(ix);
+ cellPanes.addAll(ix, ps);
+ });
+ }
+
+ public BooleanBinding disabledStyleEditingProperty() {
+ return disabledStyleEditing;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellContainer.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellContainer.java
new file mode 100644
index 00000000000..6b268bf270a
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellContainer.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import javafx.geometry.Insets;
+import javafx.scene.layout.VBox;
+import com.oracle.demo.richtext.notebook.data.Notebook;
+
+/**
+ * Cell Container.
+ *
+ * @author Andy Goryachev
+ */
+public class CellContainer extends VBox {
+ private Notebook notebook;
+
+ public CellContainer() {
+ setSpacing(3);
+ setPadding(new Insets(3));
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellPane.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellPane.java
new file mode 100644
index 00000000000..a61a67c7e48
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellPane.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.function.Supplier;
+import javafx.application.Platform;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.css.PseudoClass;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.text.Font;
+import com.oracle.demo.richtext.codearea.JavaSyntaxDecorator;
+import com.oracle.demo.richtext.notebook.data.CellInfo;
+import com.oracle.demo.richtext.util.FX;
+import jfx.incubator.scene.control.richtext.CodeArea;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.CodeTextModel;
+
+/**
+ * Pane holds the visuals for the cell: source editor, output pane, execution label, current cell highlight.
+ *
+ * @author Andy Goryachev
+ */
+public class CellPane extends GridPane {
+ private static final PseudoClass EXECUTING = PseudoClass.getPseudoClass("executing");
+ private static final Font FONT = new Font("Iosevka Fixed SS16", 12);
+ private static final Insets OUTPUT_PADDING = new Insets(0, 0, 3, 0);
+ private final CellInfo cell;
+ private final Region codeBar;
+ private final Label execLabel;
+ private final BorderPane sourcePane;
+ private final BorderPane outputPane;
+ private final SimpleBooleanProperty active = new SimpleBooleanProperty();
+
+ // TODO the side bar and exec label turn orange when source has been edited and the old output is still present
+ // also exec label shows an asterisk *[N]
+ public CellPane(CellInfo c) {
+ super(3, 0);
+
+ this.cell = c;
+ FX.style(this, "cell-pane");
+
+ codeBar = new Region();
+ codeBar.setMinWidth(6);
+ codeBar.setMaxWidth(6);
+ FX.style(codeBar, "code-bar");
+
+ execLabel = new Label(cell.isCode() ? "[ ]:" : null);
+ execLabel.setAlignment(Pos.TOP_RIGHT);
+ execLabel.setMinWidth(50);
+ FX.style(execLabel, "exec-label");
+ // TODO bind to font property, set preferred width
+ setValignment(execLabel, VPos.TOP);
+
+ sourcePane = new BorderPane();
+ setHgrow(sourcePane, Priority.ALWAYS);
+ setVgrow(sourcePane, Priority.NEVER);
+
+ outputPane = new BorderPane();
+ FX.style(outputPane, "output-pane");
+ outputPane.setMaxHeight(200);
+ setHgrow(outputPane, Priority.ALWAYS);
+ setVgrow(outputPane, Priority.NEVER);
+ setMargin(outputPane, OUTPUT_PADDING);
+
+ int r = 0;
+ add(codeBar, 0, r, 1, 2);
+ add(execLabel, 1, r);
+ add(sourcePane, 2, r);
+ r++;
+ add(outputPane, 2, r);
+
+ updateContent();
+
+ active.addListener((s,p,v) -> {
+ FX.style(this, "active-cell", v);
+ });
+ }
+
+ private void updateContent() {
+ RichTextArea ed = createEditor();
+ sourcePane.setCenter(ed);
+ outputPane.setCenter(null);
+ ed.applyCss();
+ }
+
+ private RichTextArea createEditor() {
+ CellType t = cell.getCellType();
+ switch (t) {
+ case CODE:
+ CodeArea c = new CodeArea();
+ FX.style(c, "code-cell");
+ c.setFont(FONT);
+ c.setModel(cell.getModel());
+ c.setSyntaxDecorator(new JavaSyntaxDecorator());
+ c.setUseContentHeight(true);
+ c.setWrapText(true);
+ return c;
+ case TEXT:
+ RichTextArea r = new RichTextArea();
+ FX.style(r, "text-cell");
+ r.setModel(cell.getModel());
+ r.setUseContentHeight(true);
+ r.setWrapText(true);
+ return r;
+ }
+ return null;
+ }
+
+ public final CellInfo getCellInfo() {
+ return cell;
+ }
+
+ public final CellType getCellType() {
+ return cell.getCellType();
+ }
+
+ public void setExecuting() {
+ execLabel.setText("[*]:");
+ FX.style(execLabel, EXECUTING, true);
+
+ getSourceEditor().requestFocus();
+ outputPane.setCenter(null);
+ }
+
+ public void setResult(Object result, int execCount) {
+ String s = (execCount <= 0) ? " " : String.valueOf(execCount);
+ execLabel.setText("[" + s + "]:");
+ FX.style(execLabel, EXECUTING, false);
+
+ Node n = createResultNode(result);
+ outputPane.setCenter(n);
+ }
+
+ private Node createResultNode(Object result) {
+ if(result != null) {
+ if(result instanceof Supplier gen) {
+ Object v = gen.get();
+ if(v instanceof Node n) {
+ return n;
+ }
+ } else if(result instanceof Throwable err) {
+ StringWriter sw = new StringWriter();
+ PrintWriter wr = new PrintWriter(sw);
+ err.printStackTrace(wr);
+ String text = sw.toString();
+ return textViewer(text, true);
+ } else if(result instanceof Image im) {
+ ImageView v = new ImageView(im);
+ ScrollPane sp = new ScrollPane(v);
+ FX.style(sp, "image-result");
+ return sp;
+ } else if(result instanceof CodeTextModel m) {
+ CodeArea t = new CodeArea(m);
+ t.setMinHeight(300);
+ t.setSyntaxDecorator(new SimpleJsonDecorator());
+ t.setFont(FONT);
+ t.setWrapText(false);
+ t.setEditable(false);
+ t.setLineNumbersEnabled(true);
+ FX.style(t, "output-text");
+ return t;
+ } else {
+ String text = result.toString();
+ return textViewer(text, false);
+ }
+ }
+ return null;
+ }
+
+ private static CodeTextModel from(String text) throws IOException {
+ CodeTextModel m = new CodeTextModel();
+ m.insertText(TextPos.ZERO, text);
+ return m;
+ }
+
+ private Node textViewer(String text, boolean error) {
+ try {
+ CodeTextModel m = from(text);
+
+ CodeArea t = new CodeArea();
+ t.setFont(FONT);
+ t.setModel(m);
+ t.setUseContentHeight(true);
+ t.setWrapText(false);
+ t.setEditable(false);
+ FX.style(t, error ? "output-error" : "output-text");
+ return t;
+ } catch (IOException wontHappen) {
+ return null;
+ }
+ }
+
+ // FIX does not work!
+ public void focusLater() {
+ Platform.runLater(() -> {
+ Node n = sourcePane.getCenter();
+ if (n instanceof RichTextArea a) {
+ a.requestFocus();
+ }
+ });
+ }
+
+ public RichTextArea getSourceEditor() {
+ Node n = sourcePane.getCenter();
+ if (n instanceof RichTextArea r) {
+ return r;
+ }
+ return null;
+ }
+
+ /**
+ * Splits the cell at the current source editor caret position.
+ * TODO split into three parts when non-empty selection exists.
+ * @return the list of cells resulting from the split
+ */
+ public List split() {
+ RichTextArea r = getSourceEditor();
+ if (r != null) {
+ TextPos start = r.getAnchorPosition();
+ if (start != null) {
+ TextPos end = r.getCaretPosition();
+ if (start.equals(end)) {
+ return splitInTwo(start);
+ } else {
+ // TODO split into 3 parts?
+ }
+ }
+ }
+ return null;
+ }
+
+ private List splitInTwo(TextPos p) {
+ RichTextArea ed = getSourceEditor();
+ CellType t = getCellType();
+
+ try {
+ CellPane cell1 = new CellPane(new CellInfo(t));
+ insert(ed, TextPos.ZERO, p, cell1.getSourceEditor(), TextPos.ZERO);
+
+ CellPane cell2 = new CellPane(new CellInfo(t));
+ insert(ed, p, ed.getDocumentEnd(), cell2.getSourceEditor(), TextPos.ZERO);
+
+ return List.of(cell1, cell2);
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private void insert(RichTextArea src, TextPos start, TextPos end, RichTextArea tgt, TextPos pos) throws IOException {
+ SegmentBuffer b = new SegmentBuffer();
+ src.getModel().export(start, end, b.getStyledOutput());
+ tgt.insertText(pos, b.getStyledInput());
+ }
+
+ public void setActive(boolean on) {
+ active.set(on);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellType.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellType.java
new file mode 100644
index 00000000000..60a548b23c3
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CellType.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import javafx.util.StringConverter;
+
+/**
+ * Cell type enum.
+ *
+ * @author Andy Goryachev
+ */
+public enum CellType {
+ CODE,
+ TEXT;
+
+ public String getDisplayName() {
+ switch(this) {
+ case CODE:
+ return "Code";
+ case TEXT:
+ return "Text";
+ }
+ throw new Error("?" + this);
+ }
+
+ public static StringConverter converter() {
+ return new StringConverter() {
+ @Override
+ public String toString(CellType t) {
+ return t == null ? null : t.getDisplayName();
+ }
+
+ @Override
+ public CellType fromString(String s) {
+ for (CellType t : CellType.values()) {
+ if (s.equals(t.getDisplayName())) {
+ return t;
+ }
+ }
+ return CellType.TEXT;
+ }
+ };
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CodeCellTextModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CodeCellTextModel.java
new file mode 100644
index 00000000000..3ccffb7d9f9
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/CodeCellTextModel.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.io.IOException;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.CodeTextModel;
+import jfx.incubator.scene.control.richtext.model.ContentChange;
+import jfx.incubator.scene.control.richtext.model.StyledOutput;
+
+/**
+ * Cell Text Model.
+ *
+ * @author Andy Goryachev
+ */
+public class CodeCellTextModel extends CodeTextModel {
+ private boolean modified;
+
+ public CodeCellTextModel() {
+ addListener(new Listener() {
+ @Override
+ public void onContentChange(ContentChange ch) {
+ if (ch.isEdit()) {
+ setModified(true);
+ }
+ }
+ });
+ }
+
+ public boolean isModified() {
+ return modified;
+ }
+
+ public void setModified(boolean on) {
+ modified = on;
+ }
+
+ public void setText(String text) {
+ replace(null, TextPos.ZERO, TextPos.ZERO, text, false);
+ setModified(false);
+ }
+
+ public String getText() {
+ try {
+ StyledOutput out = StyledOutput.forPlainText();
+ TextPos end = getDocumentEnd();
+ export(TextPos.ZERO, end, out);
+ return out.toString();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/Demo.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/Demo.java
new file mode 100644
index 00000000000..c0a9092a907
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/Demo.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import com.oracle.demo.richtext.notebook.data.CellInfo;
+import com.oracle.demo.richtext.notebook.data.Notebook;
+
+/**
+ * Canned notebooks.
+ *
+ * @author Andy Goryachev
+ */
+public class Demo {
+ public static Notebook createNotebookExample() {
+ Notebook b = new Notebook();
+ {
+ CellInfo c = new CellInfo(CellType.TEXT);
+ c.setSource(
+ """
+ Notebook Interface
+
+ A notebook interface or computational notebook is a virtual notebook environment used for literate programming, a method of writing computer programs. Some notebooks are WYSIWYG environments including executable calculations embedded in formatted documents; others separate calculations and text into separate sections. Notebooks share some goals and features with spreadsheets and word processors but go beyond their limited data models.
+
+ Modular notebooks may connect to a variety of computational back ends, called "kernels". Notebook interfaces are widely used for statistics, data science, machine learning, and computer algebra.
+
+ https://en.wikipedia.org/wiki/Notebook_interface""");
+ b.add(c);
+ }
+ {
+ CellInfo c = new CellInfo(CellType.CODE);
+ c.setSource(
+ """
+ /**
+ * This code cell generates a multi-line text result.
+ */
+ int x = 5;
+ String text = "text";
+ print(x);""");
+ b.add(c);
+ }
+ {
+ CellInfo c = new CellInfo(CellType.CODE);
+ c.setSource(
+ """
+ //
+ // This code cell generates a general failure (exception)
+ //
+ double sin(double x) {
+ return Math.sin(x);
+ }
+ print(sin(x) + 5.0);""");
+ b.add(c);
+ }
+ {
+ CellInfo c = new CellInfo(CellType.CODE);
+ c.setSource(
+ """
+ //
+ // This code cell generates an image output
+ //
+ display(image);""");
+ b.add(c);
+ }
+ {
+ CellInfo c = new CellInfo(CellType.CODE);
+ c.setSource(
+ """
+ // And finally, this code cell generates a Node output.
+ // This way any complex result can be rendered: a chart, a table or a spreadsheet, a complex input form...
+ //
+ var node = new ListView(data);
+ render(node);""");
+ b.add(c);
+ }
+ {
+ CellInfo c = new CellInfo(CellType.CODE);
+ c.setSource(
+ """
+ // This example simulates a JSON output backed by an external source, such as
+ // database or remote API call.
+ json = generateJsonOutput();""");
+ b.add(c);
+ }
+ return b;
+ }
+
+ public static Notebook createSingleTextCell() {
+ Notebook b = new Notebook();
+ {
+ CellInfo c = new CellInfo(CellType.TEXT);
+ c.setSource(
+ """
+ This is a text cell.
+ Right now it is a plain text cell, but we can make it a rich text cell.
+ The only problem is that the user can change the cell type - and changing it from rich text to
+ code or any other plain text based types will remove the styles.
+ We could, of course, save the rich text until the user modifies the text, or may be even preserve
+ the style information by simply rendering the plain text paragraphs, but then what would happen if
+ the user switches back to rich text after editing? Worth the try.""");
+ b.add(c);
+ }
+ return b;
+ }
+
+ public static Notebook createSingleCodeCell() {
+ Notebook b = new Notebook();
+ {
+ CellInfo c = new CellInfo(CellType.CODE);
+ b.add(c);
+ }
+ return b;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/DemoScriptEngine.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/DemoScriptEngine.java
new file mode 100644
index 00000000000..fca2c36d0f9
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/DemoScriptEngine.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.scene.Node;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.control.ListView;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.CodeTextModel;
+
+/**
+ * A demo script engine for the notebook.
+ *
+ * @author Andy Goryachev
+ */
+public class DemoScriptEngine {
+ public DemoScriptEngine() {
+ }
+
+ /**
+ * Executes the script and returns the result.
+ * Result object can be one of the following:
+ * - a Throwable (either returned or thrown when executing the script)
+ * - a String for a text result
+ * - a Supplier that creates a Node to be inserted into the output pane
+ * @param src the source script
+ * @return the result of computation
+ */
+ public Object executeScript(String src) throws Throwable {
+ // pretent we are working
+ Thread.sleep(500);
+
+ if (src == null) {
+ return null;
+ } else if (src.contains("text")) {
+ return """
+ Multi-line execution result.
+ Line 1.
+ Line 2.
+ Line 3.
+ Completed.
+ """;
+ } else if (src.contains("json")) {
+ JsonContentWithAsyncUpdate content = new JsonContentWithAsyncUpdate(10_000_000);
+ return new CodeTextModel(content)
+ {
+ {
+ content.setUpdater((ix) -> {
+ TextPos p = TextPos.ofLeading(ix, 0);
+ int len = getPlainText(ix).length();
+ fireChangeEvent(p, p, len, 0, 0);
+ });
+ }
+ };
+ } else if (src.contains("node")) {
+ return new Supplier() {
+ @Override
+ public Node get() {
+ return new ListView(FXCollections.observableArrayList(
+ "one",
+ "two",
+ "three",
+ "four",
+ "five",
+ "six",
+ "seven",
+ "eight",
+ "nine",
+ "ten",
+ "eleven",
+ "twelve",
+ "thirteen",
+ "fourteen",
+ "fifteen",
+ "sixteen",
+ "seventeen",
+ "nineteen",
+ "twenty"
+ ));
+ }
+ };
+ } else if (src.contains("image")) {
+ return executeInFx(this::generateImage);
+ } else {
+ throw new Error("script failed");
+ }
+ }
+
+ private Image generateImage() {
+ int w = 700;
+ int h = 500;
+ Canvas c = new Canvas(w, h);
+ GraphicsContext g = c.getGraphicsContext2D();
+ g.setFill(Color.gray(1.0));
+ g.fillRect(0, 0, w, h);
+
+ g.setLineWidth(0.25);
+
+ Random rnd = new Random();
+ for(int i=0; i<128; i++) {
+ double x = rnd.nextInt(w);
+ double y = rnd.nextInt(h);
+ double r = rnd.nextInt(64);
+ int hue = rnd.nextInt(360);
+
+ g.setFill(Color.hsb(hue, 0.5, 1.0, 0.5));
+ g.fillOval(x - r, y - r, r + r, r + r);
+
+ g.setStroke(Color.hsb(hue, 0.5, 0.5, 1.0));
+ g.strokeOval(x - r, y - r, r + r, r + r);
+ }
+ return c.snapshot(null, null);
+ }
+
+ private static Object executeInFx(Supplier gen) {
+ AtomicReference result = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+
+ Platform.runLater(() -> {
+ try {
+ Object r = gen.get();
+ result.set(r);
+ } catch (Throwable e) {
+ result.set(e);
+ } finally {
+ latch.countDown();
+ }
+ });
+
+ try {
+ latch.await();
+ return result.get();
+ } catch (InterruptedException e) {
+ return e;
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/JsonContentWithAsyncUpdate.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/JsonContentWithAsyncUpdate.java
new file mode 100644
index 00000000000..ac88b8d954e
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/JsonContentWithAsyncUpdate.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.text.SimpleDateFormat;
+import java.util.HashMap;
+import java.util.HexFormat;
+import java.util.Random;
+import java.util.function.Consumer;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.util.Duration;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.BasicTextModel;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+
+/**
+ * Mock content which simulates non-instantaneous retrieval of the underlying data,
+ * as in database call or remote file system.
+ *
+ * @author Andy Goryachev
+ */
+public class JsonContentWithAsyncUpdate implements BasicTextModel.Content {
+ private final int size;
+ private final HashMap data;
+ private final Random random = new Random();
+ private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSSS");
+ private final HexFormat hex = HexFormat.of();
+ private Consumer updater;
+
+ public JsonContentWithAsyncUpdate(int size) {
+ this.size = size;
+ this.data = new HashMap<>(size);
+ }
+
+ @Override
+ public boolean isWritable() {
+ return true;
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public String getText(int index) {
+ String s = data.get(index);
+ if (s == null) {
+ queue(index);
+ return "";
+ }
+ return s;
+ }
+
+ private void queue(int index) {
+ Duration simulatedDelay = Duration.millis(200 + random.nextInt(3_000));
+ Timeline t = new Timeline();
+ t.setCycleCount(1);
+ t.getKeyFrames().add(
+ new KeyFrame(simulatedDelay, (ev) -> {
+ String s = generate(index);
+ if(!data.containsKey(index)) {
+ data.put(index, s);
+ update(index);
+ }
+ })
+ );
+ t.play();
+ }
+
+ private void update(int index) {
+ if (updater != null) {
+ updater.accept(index);
+ }
+ }
+
+ public void setUpdater(Consumer u) {
+ updater = u;
+ }
+
+ @Override
+ public int insertTextSegment(int index, int offset, String text, StyleAttributeMap attrs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void insertLineBreak(int index, int offset) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void removeRange(TextPos start, TextPos end) {
+ throw new UnsupportedOperationException();
+ }
+
+ private String bytes(int count) {
+ byte[] b = new byte[count];
+ random.nextBytes(b);
+ return hex.formatHex(b);
+ }
+
+ private String generate(int index) {
+ Random r = new Random();
+ long time = System.currentTimeMillis() - ((size - 1 - index) * 145_678L);
+ String date = dateFormat.format(time);
+ String id = bytes(8);
+ String message = bytes(1 + random.nextInt(10));
+ String payload = bytes(10 + random.nextInt(128));
+ int size = payload.length() / 2;
+
+ return
+ "{date=\"" + date + "\"" +
+ ", timestamp=" + time +
+ ", id=\"" + id + "\"" +
+ ", message-id=\"" + message + "\"" +
+ ", payload=\"" + payload + "\"" +
+ ", size=" + size +
+ "}";
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookMockupApp.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookMockupApp.java
new file mode 100644
index 00000000000..66a3c29e4a6
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookMockupApp.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import javafx.application.Application;
+import javafx.stage.Stage;
+import com.oracle.demo.richtext.settings.FxSettings;
+
+/**
+ * Interactive Notebook Skeleton Implementation.
+ * Demonstrates the use of RichTextArea/CodeArea in a notebook-like setting.
+ *
+ * @author Andy Goryachev
+ */
+public class NotebookMockupApp extends Application {
+ public static void main(String[] args) {
+ Application.launch(NotebookMockupApp.class, args);
+ }
+
+ @Override
+ public void init() {
+ FxSettings.useDirectory(".NotebookMockupApp");
+ }
+
+ @Override
+ public void start(Stage stage) throws Exception {
+ new NotebookWindow().show();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookPane.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookPane.java
new file mode 100644
index 00000000000..dde4dc3d060
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookPane.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import javafx.beans.binding.Bindings;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.ToolBar;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.BorderPane;
+import com.oracle.demo.richtext.common.TextStyle;
+import com.oracle.demo.richtext.util.FX;
+
+/**
+ * Notebook Main Panel.
+ *
+ * @author Andy Goryachev
+ */
+public class NotebookPane extends BorderPane {
+ public final CellContainer cellContainer;
+ private final Actions actions;
+ private final ComboBox cellType;
+ private final ComboBox textStyle;
+
+ public NotebookPane(Actions a) {
+ FX.name(this, "RichEditorDemoPane");
+
+ this.actions = a;
+
+ cellContainer = new CellContainer();
+ Bindings.bindContent(cellContainer.getChildren(), actions.getCellPanes());
+ //cellPane.setContextMenu(createContextMenu());
+ // this is a job for the InputMap!
+ cellContainer.addEventFilter(KeyEvent.KEY_PRESSED, this::handleContextExecute);
+
+ cellType = new ComboBox<>();
+ cellType.getItems().setAll(CellType.values());
+ cellType.setConverter(CellType.converter());
+ cellType.setOnAction((ev) -> {
+ updateActiveCellType();
+ });
+
+ textStyle = new ComboBox<>();
+ textStyle.getItems().setAll(TextStyle.values());
+ textStyle.setConverter(TextStyle.converter());
+ textStyle.setOnAction((ev) -> {
+ updateTextStyle();
+ });
+ textStyle.disableProperty().bind(actions.disabledStyleEditingProperty());
+
+ ScrollPane scroll = new ScrollPane(cellContainer);
+ scroll.setFitToWidth(true);
+
+ setTop(createToolBar());
+ setCenter(scroll);
+
+ actions.textStyleProperty().addListener((s,p,c) -> {
+ setTextStyle(c);
+ });
+ }
+
+ // TODO move to window?
+ private ToolBar createToolBar() {
+ ToolBar t = new ToolBar();
+ FX.button(t, "+", "Insert a cell below", actions.insertCellBelow);
+ FX.button(t, "Cu", "Cut this cell");
+ FX.button(t, "Co", "Copy this cell");
+ FX.button(t, "Pa", "Paste this cell from the clipboard");
+ FX.add(t, cellType);
+ FX.space(t);
+ FX.button(t, "▶", "Run this cell and advance", actions.runAndAdvance);
+ FX.button(t, "▶▶", "Run all cells", actions.runAll);
+ FX.space(t);
+ FX.toggleButton(t, "𝐁", "Bold text", actions.bold);
+ FX.toggleButton(t, "𝐼", "Bold text", actions.italic);
+ FX.toggleButton(t, "S\u0336", "Strike through text", actions.strikeThrough);
+ FX.toggleButton(t, "U\u0332", "Underline text", actions.underline);
+ FX.add(t, textStyle);
+ return t;
+ }
+
+ // TODO use this?
+ private ContextMenu createContextMenu() {
+ ContextMenu m = new ContextMenu();
+ FX.item(m, "Cut Cell");
+ FX.item(m, "Copy Cell");
+ FX.item(m, "Paste Cell Below");
+ FX.separator(m);
+ FX.item(m, "Delete Cell");
+ FX.separator(m);
+ FX.item(m, "Split Cell");
+ FX.item(m, "Merge Selected Cell");
+ FX.item(m, "Merge Cell Above");
+ FX.item(m, "Merge Cell Below");
+ FX.separator(m);
+ FX.item(m, "Undo", actions.undo);
+ FX.item(m, "Redo", actions.redo);
+ FX.separator(m);
+ FX.item(m, "Cut", actions.cut);
+ FX.item(m, "Copy", actions.copy);
+ FX.item(m, "Paste", actions.paste);
+ FX.item(m, "Paste and Retain Style", actions.pasteUnformatted);
+ FX.separator(m);
+ FX.item(m, "Select All", actions.selectAll);
+ return m;
+ }
+
+ public void setActiveCellPane(CellPane p) {
+ CellType t = (p == null ? null : p.getCellType());
+ cellType.getSelectionModel().select(t);
+ }
+
+ private void updateActiveCellType() {
+ CellType t = cellType.getSelectionModel().getSelectedItem();
+ actions.setActiveCellType(t);
+ }
+
+ private void handleContextExecute(KeyEvent ev) {
+ if (ev.getCode() == KeyCode.ENTER) {
+ if (ev.isShortcutDown()) {
+ actions.runAndAdvance();
+ }
+ }
+ }
+
+ private void updateTextStyle() {
+ TextStyle st = textStyle.getSelectionModel().getSelectedItem();
+ if (st != null) {
+ actions.setTextStyle(st);
+ }
+ }
+
+ public void setTextStyle(TextStyle v) {
+ textStyle.setValue(v);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookWindow.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookWindow.java
new file mode 100644
index 00000000000..a3daa8c7482
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/NotebookWindow.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.io.File;
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+import com.oracle.demo.richtext.notebook.data.Notebook;
+import com.oracle.demo.richtext.rta.RichTextAreaWindow;
+import com.oracle.demo.richtext.util.FX;
+
+/**
+ * Notebook Demo main window.
+ *
+ * @author Andy Goryachev
+ */
+public class NotebookWindow extends Stage {
+ private static final String TITLE = "Interactive Notebook (Mockup)";
+ private final Actions actions;
+ private final NotebookPane pane;
+ private final Label status;
+
+ public NotebookWindow() {
+ FX.name(this, "NotebookWindow");
+
+ actions = new Actions(this);
+
+ pane = new NotebookPane(actions);
+
+ status = new Label();
+ status.setPadding(new Insets(2, 10, 2, 10));
+
+ BorderPane bp = new BorderPane();
+ bp.setTop(createMenu());
+ bp.setCenter(pane);
+ bp.setBottom(status);
+
+ Scene scene = new Scene(bp);
+ scene.getStylesheets().addAll(
+ getClass().getResource("notebook.css").toExternalForm()
+ );
+ scene.focusOwnerProperty().addListener((s,p,c) -> {
+ handleFocusUpdate(c);
+ });
+
+ // TODO input map for the window: add shortcut-S for saving
+
+ setScene(scene);
+ setWidth(1200);
+ setHeight(600);
+
+ actions.modifiedProperty().addListener((x) -> {
+ updateTitle();
+ });
+ actions.fileNameProperty().addListener((x) -> {
+ updateTitle();
+ });
+ updateTitle();
+
+ setNotebook(Demo.createSingleCodeCell());
+ //setNotebook(Demo.createNotebookExample());
+ }
+
+ private MenuBar createMenu() {
+ Menu m2;
+ MenuBar b = new MenuBar();
+ // file
+ FX.menu(b, "File");
+ FX.item(b, "New", actions.newDocument);
+ FX.item(b, "Open...", actions.open);
+ m2 = FX.submenu(b, "Open Recent");
+ FX.item(m2, "Notebook Example", () -> setNotebook(Demo.createNotebookExample()));
+ FX.item(m2, "Single Text Cell", () -> setNotebook(Demo.createSingleTextCell()));
+ FX.item(m2, "Empty Code Cell", () -> setNotebook(Demo.createSingleCodeCell()));
+ FX.separator(b);
+ FX.item(b, "Save...", actions.save);
+ // TODO print?
+ FX.item(b, "Quit", () -> Platform.exit());
+
+ // edit
+ FX.menu(b, "Edit");
+ FX.item(b, "Undo", actions.undo);
+ FX.item(b, "Redo", actions.redo);
+ FX.separator(b);
+ FX.item(b, "Cut", actions.cut);
+ FX.item(b, "Copy", actions.copy);
+ FX.item(b, "Paste", actions.paste);
+ FX.item(b, "Paste and Retain Style", actions.pasteUnformatted);
+
+ // format
+ FX.menu(b, "Format");
+ FX.checkItem(b, "Bold", actions.bold);
+ FX.checkItem(b, "Italic", actions.italic);
+ FX.checkItem(b, "Strike Through", actions.strikeThrough);
+ FX.checkItem(b, "Underline", actions.underline);
+
+ // cell
+ FX.menu(b, "Cell");
+ FX.item(b, "Cut Cell", actions.cutCell);
+ FX.item(b, "Copy Cell", actions.copyCell);
+ FX.item(b, "Paste Cell Below", actions.pasteCellBelow);
+ FX.separator(b);
+ FX.item(b, "Insert Cell Below", actions.insertCellBelow);
+ FX.separator(b);
+ FX.item(b, "Move Up", actions.moveCellUp);
+ FX.item(b, "Move Down", actions.moveCellDown);
+ FX.separator(b);
+ FX.item(b, "Split Cell", actions.splitCell);
+ FX.item(b, "Merge Cell Above", actions.mergeCellAbove);
+ FX.item(b, "Merge Cell Below", actions.mergeCellBelow);
+ FX.separator(b);
+ FX.item(b, "Delete", actions.deleteCell);
+
+ // run
+ FX.menu(b, "Run");
+ FX.item(b, "Run Current Cell And Advance", actions.runAndAdvance);
+ FX.item(b, "Run All Cells", actions.runAll);
+
+ // view
+ FX.menu(b, "View");
+ FX.item(b, "Show Line Numbers");
+
+ // help
+ FX.menu(b, "Help");
+ FX.item(b, "About");
+ return b;
+ }
+
+ private void updateTitle() {
+ File f = actions.getFile();
+ boolean modified = actions.isModified();
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(TITLE);
+ if (f != null) {
+ sb.append(" - ");
+ sb.append(f.getName());
+ }
+ if (modified) {
+ sb.append(" *");
+ }
+ setTitle(sb.toString());
+ }
+
+ private void handleFocusUpdate(Node n) {
+ CellPane p = FX.findParentOf(CellPane.class, n);
+ if (p != null) {
+ actions.setActiveCellPane(p);
+ pane.setActiveCellPane(p);
+ }
+ }
+
+ public void setNotebook(Notebook b) {
+ actions.setNotebook(b);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/SegmentBuffer.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/SegmentBuffer.java
new file mode 100644
index 00000000000..ac3061c285a
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/SegmentBuffer.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import jfx.incubator.scene.control.richtext.model.StyledInput;
+import jfx.incubator.scene.control.richtext.model.StyledOutput;
+import jfx.incubator.scene.control.richtext.model.StyledSegment;
+
+/**
+ * In-memory buffer which stored {@code StyledSegment}s with associated output and input streams,
+ * for the use in export/import or transfer operations.
+ * This class and its streams are not thread safe.
+ *
+ * @author Andy Goryachev
+ */
+public class SegmentBuffer {
+ private ArrayList segments;
+ private Output output;
+
+ /**
+ * Creates the buffer with the specified initial capacity.
+ * @param initialCapacity the initial capacity
+ */
+ public SegmentBuffer(int initialCapacity) {
+ segments = new ArrayList<>(initialCapacity);
+ }
+
+ /**
+ * Creates the buffer.
+ */
+ public SegmentBuffer() {
+ this(256);
+ }
+
+ /**
+ * Returns the singleton {@code StyledOutput} instance associated with this buffer.
+ * @return the StyledOutput instance
+ */
+ public StyledOutput getStyledOutput() {
+ if(output == null) {
+ output = new Output();
+ }
+ return output;
+ }
+
+ /**
+ * Returns an array of {@code StyledSegment}s accumulated so far.
+ * @return the array of {@code StyledSegment}s
+ */
+ public StyledSegment[] getSegments() {
+ return segments.toArray(new StyledSegment[segments.size()]);
+ }
+
+ /**
+ * Returns a new instance of {@code StyledInput} which contains the segments accumulated so far.
+ * @return the instance of {@code StyledInput}
+ */
+ public StyledInput getStyledInput() {
+ return new Input(getSegments());
+ }
+
+ private class Output implements StyledOutput {
+ Output() {
+ }
+
+ @Override
+ public void consume(StyledSegment s) throws IOException {
+ segments.add(s);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ }
+
+ @Override
+ public void close() throws IOException {
+ // possibly create a boolean flag to force an IOException in append when closed
+ }
+ }
+
+ private static class Input implements StyledInput {
+ private final StyledSegment[] segments;
+ private int index;
+
+ Input(StyledSegment[] segments) {
+ this.segments = segments;
+ }
+
+ @Override
+ public StyledSegment nextSegment() {
+ if (index < segments.length) {
+ return segments[index++];
+ }
+ return null;
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/SimpleJsonDecorator.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/SimpleJsonDecorator.java
new file mode 100644
index 00000000000..a4cc1b1b8c0
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/SimpleJsonDecorator.java
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.util.ArrayList;
+import java.util.List;
+import javafx.scene.paint.Color;
+import com.oracle.demo.richtext.codearea.JavaSyntaxAnalyzer.Line;
+import com.oracle.demo.richtext.codearea.JavaSyntaxAnalyzer.Type;
+import jfx.incubator.scene.control.richtext.SyntaxDecorator;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.CodeTextModel;
+import jfx.incubator.scene.control.richtext.model.RichParagraph;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+
+/**
+ * Super simple (and therefore not always correct) syntax decorator for JSON
+ * which works one line at a time.
+ *
+ * @author Andy Goryachev
+ */
+public class SimpleJsonDecorator implements SyntaxDecorator {
+ private static final StyleAttributeMap NORMAL = mkStyle(Color.BLACK);
+ private static final StyleAttributeMap NUMBER = mkStyle(Color.MAGENTA);
+ private static final StyleAttributeMap STRING = mkStyle(Color.BLUE);
+
+ public SimpleJsonDecorator() {
+ }
+
+ @Override
+ public void handleChange(CodeTextModel m, TextPos start, TextPos end, int top, int added, int bottom) {
+ }
+
+ @Override
+ public RichParagraph createRichParagraph(CodeTextModel model, int index) {
+ String text = model.getPlainText(index);
+ List segments = new Analyzer(text).parse();
+ RichParagraph.Builder b = RichParagraph.builder();
+ for (Seg seg : segments) {
+ b.addSegment(seg.text, seg.style);
+ }
+ return b.build();
+ }
+
+ private static StyleAttributeMap mkStyle(Color c) {
+ return StyleAttributeMap.builder().setTextColor(c).build();
+ }
+
+ private static record Seg(StyleAttributeMap style, String text) {
+ }
+
+ private enum State {
+ NUMBER,
+ STRING,
+ TEXT,
+ VALUE,
+ }
+
+ private static class Analyzer {
+ private final String text;
+ private final ArrayList segments = new ArrayList<>();
+ private static final int EOF = -1;
+ private int start;
+ private int pos;
+ private State state = State.TEXT;
+
+ public Analyzer(String text) {
+ this.text = text;
+ }
+
+ private int peek(int delta) {
+ int ix = pos + delta;
+ if ((ix >= 0) && (ix < text.length())) {
+ return text.charAt(ix);
+ }
+ return EOF;
+ }
+
+ private void addSegment() {
+ StyleAttributeMap type = toStyleAttrs(state);
+ addSegment(type);
+ }
+
+ private StyleAttributeMap toStyleAttrs(State s) {
+ switch (s) {
+ case STRING:
+ return STRING;
+ case NUMBER:
+ return NUMBER;
+ case VALUE:
+ default:
+ return NORMAL;
+ }
+ }
+
+ private void addSegment(StyleAttributeMap style) {
+ if (pos > start) {
+ String s = text.substring(start, pos);
+ segments.add(new Seg(style, s));
+ start = pos;
+ }
+ }
+
+ private Error err(String text) {
+ return new Error(text + " state=" + state + " pos=" + pos);
+ }
+
+ private int parseNumber() {
+ int ix = indexOfNonNumber();
+ if (ix < 0) {
+ return 0;
+ }
+ String s = text.substring(pos, pos + ix);
+ try {
+ Double.parseDouble(s);
+ return ix;
+ } catch (NumberFormatException e) {
+ }
+ return 0;
+ }
+
+ private int indexOfNonNumber() {
+ int i = 0;
+ for (;;) {
+ int c = peek(i);
+ switch (c) {
+ case EOF:
+ return i;
+ // we'll parse integers only for now case '.':
+ case '-':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ i++;
+ continue;
+ default:
+ return i;
+ }
+ }
+ }
+
+ public List parse() {
+ start = 0;
+ for (;;) {
+ int c = peek(0);
+ switch (c) {
+ case EOF:
+ addSegment();
+ return segments;
+ case '"':
+ switch (state) {
+ case TEXT:
+ case VALUE:
+ addSegment();
+ state = State.STRING;
+ pos++;
+ break;
+ case STRING:
+ pos++;
+ addSegment();
+ state = State.TEXT;
+ break;
+ default:
+ throw err("state must be either TEXT, STRING, or VALUE");
+ }
+ break;
+ case '=':
+ state = State.VALUE;
+ break;
+ //case '.':
+ case '-':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ switch (state) {
+ case VALUE:
+ int len = parseNumber();
+ if (len > 0) {
+ addSegment();
+ state = State.NUMBER;
+ pos += len;
+ addSegment();
+ }
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ pos++;
+ }
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/TextCellTextModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/TextCellTextModel.java
new file mode 100644
index 00000000000..aebee7fd5e5
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/TextCellTextModel.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook;
+
+import java.io.IOException;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.ContentChange;
+import jfx.incubator.scene.control.richtext.model.RichTextModel;
+import jfx.incubator.scene.control.richtext.model.StyledOutput;
+
+/**
+ * RichTextModel for the text cell.
+ *
+ * @author Andy Goryachev
+ */
+public class TextCellTextModel extends RichTextModel {
+ private boolean modified;
+
+ public TextCellTextModel() {
+ addListener(new Listener() {
+ @Override
+ public void onContentChange(ContentChange ch) {
+ setModified(true);
+ }
+ });
+ }
+
+ public boolean isModified() {
+ return modified;
+ }
+
+ public void setModified(boolean on) {
+ modified = on;
+ }
+
+ public void setText(String text) {
+ replace(null, TextPos.ZERO, TextPos.ZERO, text, false);
+ setModified(false);
+ }
+
+ public String getPlainText() {
+ try {
+ StyledOutput out = StyledOutput.forPlainText();
+ TextPos end = getDocumentEnd();
+ export(TextPos.ZERO, end, out);
+ return out.toString();
+ } catch (IOException e) {
+ return null;
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/data/CellInfo.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/data/CellInfo.java
new file mode 100644
index 00000000000..fab1d8e6706
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/data/CellInfo.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook.data;
+
+import com.oracle.demo.richtext.notebook.CellType;
+import com.oracle.demo.richtext.notebook.CodeCellTextModel;
+import com.oracle.demo.richtext.notebook.TextCellTextModel;
+import jfx.incubator.scene.control.richtext.model.StyledTextModel;
+
+/**
+ * This data structure represents a cell in the notebook.
+ *
+ * @author Andy Goryachev
+ */
+public class CellInfo {
+ private CellType type;
+ private String source;
+ private CodeCellTextModel codeModel;
+ private TextCellTextModel textModel;
+
+ public CellInfo(CellType t) {
+ this.type = t;
+ }
+
+ public final CellType getCellType() {
+ return type;
+ }
+
+ public final void setCellType(CellType t) {
+ type = t;
+ }
+
+ public boolean isCode() {
+ return getCellType() == CellType.CODE;
+ }
+
+ public boolean isText() {
+ return getCellType() == CellType.TEXT;
+ }
+
+ public final StyledTextModel getModel() {
+ switch (type) {
+ case CODE:
+ if (textModel != null) {
+ if (textModel.isModified()) {
+ source = textModel.getPlainText();
+ codeModel = null;
+ }
+ }
+ if (codeModel == null) {
+ codeModel = new CodeCellTextModel();
+ codeModel.setText(source);
+ }
+ return codeModel;
+ case TEXT:
+ default:
+ if (codeModel != null) {
+ if (codeModel.isModified()) {
+ source = codeModel.getText();
+ textModel = null;
+ }
+ }
+ if (textModel == null) {
+ textModel = new TextCellTextModel();
+ textModel.setText(source);
+ }
+ return textModel;
+ }
+ }
+
+ private void handleTypeChange(CellType old, CellType type) {
+ switch (type) {
+ case CODE:
+ // TODO
+ case TEXT:
+ default:
+ break;
+ }
+ }
+
+ public String getSource() {
+ switch (type) {
+ case CODE:
+ if (codeModel != null) {
+ if (codeModel.isModified()) {
+ source = codeModel.getText();
+ codeModel.setModified(false);
+ }
+ }
+ break;
+ case TEXT:
+ default:
+ if (textModel != null) {
+ if (textModel.isModified()) {
+ source = textModel.getPlainText();
+ textModel.setModified(false);
+ }
+ }
+ break;
+ }
+ return source;
+ }
+
+ public void setSource(String text) {
+ this.source = text;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/data/Notebook.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/data/Notebook.java
new file mode 100644
index 00000000000..3a2338defcc
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/data/Notebook.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.notebook.data;
+
+import java.util.ArrayList;
+
+/**
+ * Notebook Data Object.
+ *
+ * @author Andy Goryachev
+ */
+public class Notebook {
+ private final ArrayList cells = new ArrayList<>();
+
+ public Notebook() {
+ }
+
+ public int size() {
+ return cells.size();
+ }
+
+ public CellInfo getCell(int ix) {
+ return cells.get(ix);
+ }
+
+ public void add(CellInfo cell) {
+ cells.add(cell);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/notebook.css b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/notebook.css
new file mode 100644
index 00000000000..5b248942dff
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/notebook/notebook.css
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+.active-cell .code-bar {
+ -fx-background-color: #488f48;
+ -fx-background-radius: 2;
+}
+
+.exec-label {
+ -fx-text-fill: gray;
+}
+
+.cell-pane:focus-within .exec-label {
+ -fx-text-fill: black;
+}
+
+.exec-label:executing {
+ -fx-text-fill: red;
+ -fx-font-weight: bold;
+}
+
+.output-text .content {
+ -fx-background-color:f8f8f8;
+ -fx-background-insets:0;
+}
+
+.output-error .content {
+ -fx-background-color:fff0f0;
+ -fx-background-insets:0;
+}
+
+.image-result {
+ -fx-background-color: #888888;
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/BifurcationDiagram.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/BifurcationDiagram.java
new file mode 100644
index 00000000000..eb8cd1ab612
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/BifurcationDiagram.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+
+/**
+ * Illustrate chaos arising from a simple formula.
+ *
+ * @author Andy Goryachev
+ */
+public class BifurcationDiagram {
+ private static final double min = 2.4;
+ private static final double max = 4.0;
+
+ public static Pane generate() {
+ Pane p = new Pane();
+ p.setPrefSize(600, 200);
+ p.widthProperty().addListener((x) -> update(p));
+ p.heightProperty().addListener((x) -> update(p));
+ update(p);
+ return p;
+ }
+
+ protected static void update(Pane p) {
+ double w = p.getWidth();
+ double h = p.getHeight();
+
+ if ((w < 1) || (h < 1)) {
+ return;
+ } else if (w > 600) {
+ w = 600;
+ }
+
+ Canvas c = new Canvas(w, h);
+ GraphicsContext g = c.getGraphicsContext2D();
+
+ g.setFill(Color.gray(0.9));
+ g.fillRect(0, 0, w, h);
+
+ int count = 1000;
+ int start = 500;
+ double r = 0.3;
+ g.setFill(Color.rgb(0, 0, 0, 0.2));
+
+ for (double λ = min; λ < max; λ += 0.001) {
+ double x = 0.5;
+ for (int i = 0; i < count; i++) {
+ x = λ * x * (1.0 - x);
+ if (i > start) {
+ double px = w * (λ - min) / (max - min);
+ double py = h * (1.0 - x);
+
+ g.fillOval(px - r, py - r, r + r, r + r);
+ }
+ }
+ }
+
+ p.getChildren().setAll(c);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/CssToolPane.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/CssToolPane.java
new file mode 100644
index 00000000000..d05cea7609f
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/CssToolPane.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.nio.charset.Charset;
+import java.util.Base64;
+import javafx.collections.ObservableList;
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+import javafx.scene.paint.Color;
+import javafx.stage.Window;
+import com.oracle.demo.richtext.util.FX;
+
+/**
+ * CSS Tool
+ *
+ * @author Andy Goryachev
+ */
+public class CssToolPane extends BorderPane {
+ private final TextArea cssField;
+ private static String oldStylesheet;
+
+ public CssToolPane() {
+ cssField = new TextArea();
+ cssField.setId("CssPlaygroundPaneCss");
+ cssField.setMaxWidth(Double.POSITIVE_INFINITY);
+ cssField.setMaxHeight(Double.POSITIVE_INFINITY);
+
+ Button updateButton = FX.button("Update", this::update);
+
+ // why can't I fill the width of the container with this grid pane??
+ GridPane p = new GridPane();
+ p.setPadding(new Insets(10));
+ p.setHgap(5);
+ p.setVgap(5);
+ int r = 0;
+ p.add(new Label("Custom CSS:"), 0, r);
+ r++;
+ p.add(cssField, 0, r, 3, 1);
+ r++;
+ p.add(updateButton, 2, r);
+ GridPane.setHgrow(cssField, Priority.ALWAYS);
+ GridPane.setVgrow(cssField, Priority.ALWAYS);
+
+ setCenter(p);
+ }
+
+ private void update() {
+ String css = cssField.getText();
+ applyStyleSheet(css);
+ }
+
+ private static String toCssColor(Color c) {
+ int r = toInt8(c.getRed());
+ int g = toInt8(c.getGreen());
+ int b = toInt8(c.getBlue());
+ return String.format("#%02X%02X%02X", r, g, b);
+ }
+
+ private static int toInt8(double x) {
+ int v = (int)Math.round(x * 255);
+ if (v < 0) {
+ return 0;
+ } else if (v > 255) {
+ return 255;
+ }
+ return v;
+ }
+
+ private static String encode(String s) {
+ if (s == null) {
+ return null;
+ }
+ Charset utf8 = Charset.forName("utf-8");
+ byte[] b = s.getBytes(utf8);
+ return "data:text/css;base64," + Base64.getEncoder().encodeToString(b);
+ }
+
+ private static void applyStyleSheet(String styleSheet) {
+ String ss = encode(styleSheet);
+ if (ss != null) {
+ for (Window w : Window.getWindows()) {
+ Scene scene = w.getScene();
+ if (scene != null) {
+ ObservableList sheets = scene.getStylesheets();
+ if (oldStylesheet != null) {
+ sheets.remove(oldStylesheet);
+ }
+ sheets.add(ss);
+ }
+ }
+ }
+ oldStylesheet = ss;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DataFrame.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DataFrame.java
new file mode 100644
index 00000000000..52b4ba8cb94
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DataFrame.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.util.ArrayList;
+
+/**
+ * Simple Data Frame.
+ *
+ * @author Andy Goryachev
+ */
+public class DataFrame {
+ private String[] columns;
+ private final ArrayList rows = new ArrayList();
+
+ public DataFrame() {
+ }
+
+ public static DataFrame parse(String[] lines) {
+ DataFrame f = new DataFrame();
+ for (int i = 0; i < lines.length; i++) {
+ String line = lines[i];
+ String[] ss = line.split("\\|");
+ if (i == 0) {
+ f.setColumns(ss);
+ } else {
+ f.addValues(ss);
+ }
+ }
+ return f;
+ }
+
+ public String[] getColumnNames() {
+ return columns;
+ }
+
+ public void setColumns(String[] columns) {
+ this.columns = columns;
+ }
+
+ public void addValues(String[] ss) {
+ rows.add(ss);
+ }
+
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ public String[] getRow(int ix) {
+ return rows.get(ix);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoColorSideDecorator.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoColorSideDecorator.java
new file mode 100644
index 00000000000..77ef080ec46
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoColorSideDecorator.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.scene.Node;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import jfx.incubator.scene.control.richtext.SideDecorator;
+
+/**
+ * Colorful side decorator for debugging purposes.
+ *
+ * @author Andy Goryachev
+ */
+public class DemoColorSideDecorator implements SideDecorator {
+ public DemoColorSideDecorator() {
+ }
+
+ @Override
+ public double getPrefWidth(double viewWidth) {
+ return 20.0;
+ }
+
+ @Override
+ public Node getNode(int index) {
+ int num = 36;
+ double a = 360.0 * (index % num) / num;
+ Color c = Color.hsb(a, 0.5, 1.0);
+
+ Region r = new Region();
+ r.setOpacity(1.0);
+ r.setBackground(new Background(new BackgroundFill(c, null, null)));
+ return r;
+ }
+
+ @Override
+ public Node getMeasurementNode(int index) {
+ return null;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoModel.java
new file mode 100644
index 00000000000..d68deead12d
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoModel.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+import java.util.Arrays;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.Region;
+import javafx.scene.paint.Color;
+import jfx.incubator.scene.control.richtext.model.RichTextFormatHandler;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+
+/**
+ * RichTextArea demo model.
+ *
+ * @author Andy Goryachev
+ */
+public class DemoModel extends SimpleViewOnlyStyledModel {
+ private final SimpleStringProperty textField = new SimpleStringProperty();
+
+ public DemoModel() {
+ // see RichTextAreaDemo.css
+ String ARABIC = "arabic";
+ String CODE = "code";
+ String RED = "red";
+ String GREEN = "green";
+ String GRAY = "gray";
+ String LARGE = "large";
+ String BOLD = "bold";
+ String ITALIC = "italic";
+ String STRIKETHROUGH = "strikethrough";
+ String UNDERLINE = "underline";
+
+ addWithInlineAndStyleNames("RichTextArea Control", "-fx-font-size:200%;", UNDERLINE);
+ nl(2);
+
+ addWithStyleNames("/**", RED, CODE);
+ nl();
+ addWithStyleNames(" * Syntax Highlight Demo.", RED, CODE);
+ nl();
+ addWithStyleNames(" */", RED, CODE);
+ nl();
+ addWithStyleNames("public class ", GREEN, CODE);
+ addWithStyleNames("SyntaxHighlightDemo ", CODE);
+ addWithStyleNames("extends ", GREEN, CODE);
+ addWithStyleNames("Application {", CODE);
+ nl();
+ addWithStyleNames("\tpublic static void", GREEN, CODE);
+ addWithStyleNames(" main(String[] args) {", CODE);
+ nl();
+ addWithStyleNames("\t\tApplication.launch(SyntaxHighlightDemo.", CODE);
+ addWithStyleNames("class", CODE, GREEN);
+ addWithStyleNames(", args);", CODE);
+ nl();
+ addWithStyleNames("\t}", CODE);
+ nl();
+ addWithStyleNames("}", CODE);
+ nl(2);
+ // font attributes
+ addWithStyleNames("BOLD ", BOLD);
+ addWithStyleNames("ITALIC ", ITALIC);
+ addWithStyleNames("STRIKETHROUGH ", STRIKETHROUGH);
+ addWithStyleNames("UNDERLINE ", UNDERLINE);
+ addWithStyleNames("ALL OF THEM ", BOLD, ITALIC, STRIKETHROUGH, UNDERLINE);
+ nl(2);
+ // inline nodes
+ addSegment("Inline Nodes: ");
+ addNodeSegment(() -> {
+ TextField f = new TextField();
+ f.setPrefColumnCount(20);
+ f.textProperty().bindBidirectional(textField);
+ return f;
+ });
+ addSegment(" ");
+ addNodeSegment(() -> new Button("OK"));
+ addSegment(" ");
+ nl(2);
+ addWithStyleNames("A regular Arabic verb, كَتَبَ kataba (to write).", ARABIC).nl();
+ addWithStyleNames("Emojis: [🔥🦋😀😃😄😁😆😅🤣😂🙂🙃😉😊😇]", LARGE).nl();
+ nl();
+ addWithStyleNames("Halfwidth and FullWidth Forms", UNDERLINE).nl();
+ addWithInlineStyle("ABCDEFGHIJKLMNO", "-fx-font-family:monospaced;").nl();
+ addWithInlineStyle("ABCDEFGHIJKLMNO", "-fx-font-family:monospaced;").nl();
+ addWithStyleNames(" leading and trailing whitespace ", CODE).nl();
+ nl(3);
+ addWithStyleNames("Behold various types of highlights, including overlapping highlights.", LARGE);
+ highlight(7, 7, Color.rgb(255, 255, 128, 0.7));
+ addWavyUnderline(36, 100, Color.RED);
+ highlight(46, 11, Color.rgb(255, 255, 128, 0.7));
+ highlight(50, 20, Color.rgb(0, 0, 128, 0.1));
+ nl(2);
+ addSegment("Behold various types of highlights, including overlapping highlights.");
+ highlight(7, 7, Color.rgb(255, 255, 128, 0.7));
+ addWavyUnderline(36, 100, Color.RED);
+ highlight(46, 11, Color.rgb(255, 255, 128, 0.7));
+ highlight(50, 20, Color.rgb(0, 0, 128, 0.1));
+ nl(2);
+
+ addParagraph(this::createRect);
+ nl(2);
+
+ ParagraphAttributesDemoModel.insert(this);
+
+ addImage(DemoModel.class.getResourceAsStream("animated.gif"));
+ addWithStyleNames(" Fig. 1 Embedded animated GIF image.", GRAY, ITALIC);
+ nl(2);
+
+ nl();
+ addWithInlineStyle("\t\t終 The End.", "-fx-font-size:200%;");
+ nl();
+
+ registerDataFormatHandler(RichTextFormatHandler.getInstance(), true, false, 2000);
+ }
+
+ private Region createRect() {
+ Label t = new Label() {
+ @Override
+ protected double computePrefHeight(double w) {
+ return 400;
+ }
+ };
+ t.setPrefSize(400, 200);
+ t.setMaxWidth(400);
+ t.textProperty().bind(Bindings.createObjectBinding(
+ () -> {
+ return String.format("%.1f x %.1f", t.getWidth(), t.getHeight());
+ },
+ t.widthProperty(),
+ t.heightProperty()
+ ));
+ t.setBackground(Background.fill(Color.LIGHTGRAY));
+
+ BorderPane p = new BorderPane();
+ p.setLeft(t);
+ return p;
+ }
+
+ private String word(char c, int len) {
+ char[] cs = new char[len];
+ Arrays.fill(cs, c);
+ return new String(cs);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoStyledTextModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoStyledTextModel.java
new file mode 100644
index 00000000000..585c9c6fefa
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/DemoStyledTextModel.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.text.DecimalFormat;
+import javafx.scene.Node;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+import jfx.incubator.scene.control.richtext.StyleResolver;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.RichParagraph;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+import jfx.incubator.scene.control.richtext.model.StyledTextModelViewOnlyBase;
+
+/**
+ * Demo StyledTextModel.
+ * Does not support editing events - populate the model first, then pass it to the control.
+ *
+ * @author Andy Goryachev
+ */
+public class DemoStyledTextModel extends StyledTextModelViewOnlyBase {
+ private final int size;
+ private final boolean monospaced;
+ private static final DecimalFormat format = new DecimalFormat("#,##0");
+
+ public DemoStyledTextModel(int size, boolean monospaced) {
+ this.size = size;
+ this.monospaced = monospaced;
+ }
+
+ @Override
+ public int size() {
+ return size;
+ }
+
+ @Override
+ public StyleAttributeMap getStyleAttributeMap(StyleResolver resolver, TextPos pos) {
+ return StyleAttributeMap.EMPTY;
+ }
+
+ @Override
+ public String getPlainText(int index) {
+ RichParagraph p = getParagraph(index);
+ return p.getPlainText();
+ }
+
+ private static String getText(TextFlow f) {
+ StringBuilder sb = new StringBuilder();
+ for (Node n : f.getChildrenUnmodifiable()) {
+ if (n instanceof Text t) {
+ sb.append(t.getText());
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public RichParagraph getParagraph(int ix) {
+ RichParagraph.Builder b = RichParagraph.builder();
+ String s = format.format(ix + 1);
+ String sz = format.format(size);
+ String[] css = monospaced ? new String[] { "monospaced" } : new String[0];
+
+ b.addWithInlineAndStyleNames(s, "-fx-fill:darkgreen;", css);
+ b.addWithStyleNames(" / ", css);
+ b.addWithInlineAndStyleNames(sz, "-fx-fill:black;", css);
+ if (monospaced) {
+ b.addWithStyleNames(" (monospaced)", css);
+ }
+
+ if ((ix % 10) == 9) {
+ String words = generateWords(ix);
+ b.addWithStyleNames(words, css);
+ }
+ return b.build();
+ }
+
+ private String generateWords(int ix) {
+ String s = String.valueOf(ix);
+ StringBuilder sb = new StringBuilder(128);
+ for (char c: s.toCharArray()) {
+ String digit = getDigit(c);
+ sb.append(digit);
+ }
+ return sb.toString();
+ }
+
+ private String getDigit(char c) {
+ switch (c) {
+ case '0':
+ return " zero";
+ case '1':
+ return " one";
+ case '2':
+ return " two";
+ case '3':
+ return " three";
+ case '4':
+ return " four";
+ case '5':
+ return " five";
+ case '6':
+ return " six";
+ case '7':
+ return " seven";
+ case '8':
+ return " eight";
+ default:
+ return " nine";
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ExamplesModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ExamplesModel.java
new file mode 100644
index 00000000000..1b317940a85
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ExamplesModel.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.control.TextField;
+import javafx.scene.paint.Color;
+import jfx.incubator.scene.control.richtext.StyleResolver;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.RichParagraph;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+import jfx.incubator.scene.control.richtext.model.StyledTextModelViewOnlyBase;
+
+/**
+ * This model contains code examples used in the documentation.
+ *
+ * @author Andy Goryachev
+ */
+public class ExamplesModel extends StyledTextModelViewOnlyBase {
+ /** properties in the model allow for inline controls */
+ private final SimpleStringProperty exampleProperty = new SimpleStringProperty();
+
+ public ExamplesModel() {
+ }
+
+ @Override
+ public int size() {
+ return 10;
+ }
+
+ @Override
+ public String getPlainText(int index) {
+ return getParagraph(index).getPlainText();
+ }
+
+ @Override
+ public StyleAttributeMap getStyleAttributeMap(StyleResolver resolver, TextPos pos) {
+ return null;
+ }
+
+ @Override
+ public RichParagraph getParagraph(int index) {
+ switch(index) {
+ case 0:
+ {
+ StyleAttributeMap a1 = StyleAttributeMap.builder().setBold(true).build();
+ RichParagraph.Builder b = RichParagraph.builder();
+ b.addSegment("Example: ", a1);
+ b.addSegment("spelling, highlights");
+ b.addWavyUnderline(9, 8, Color.RED);
+ b.addHighlight(19, 4, Color.rgb(255, 128, 128, 0.5));
+ b.addHighlight(20, 7, Color.rgb(128, 255, 128, 0.5));
+ return b.build();
+ }
+ case 4:
+ {
+ RichParagraph.Builder b = RichParagraph.builder();
+ b.addSegment("Input field: ");
+ // creates an embedded control bound to a property within this model
+ b.addInlineNode(() -> {
+ TextField t = new TextField();
+ t.textProperty().bindBidirectional(exampleProperty);
+ return t;
+ });
+ return b.build();
+ }
+ }
+ return RichParagraph.builder().build();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/FontOption.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/FontOption.java
new file mode 100644
index 00000000000..793741abfec
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/FontOption.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Insets;
+import javafx.scene.control.ComboBox;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.text.Font;
+import com.oracle.demo.richtext.util.FX;
+
+/**
+ * Font Option Bound to a Property.
+ *
+ * @author Andy Goryachev
+ */
+public class FontOption extends HBox {
+ private final SimpleObjectProperty property = new SimpleObjectProperty<>();
+ private final ComboBox fontField = new ComboBox<>();
+ private final ComboBox styleField = new ComboBox<>();
+ private final ComboBox sizeField = new ComboBox<>();
+
+ public FontOption(String name, boolean allowNull, ObjectProperty p) {
+ FX.name(this, name);
+ if (p != null) {
+ property.bindBidirectional(p);
+ }
+
+ FX.name(fontField, name + "_FONT");
+ fontField.getItems().setAll(collectFonts(allowNull));
+ fontField.getSelectionModel().selectedItemProperty().addListener((x) -> {
+ String fam = fontField.getSelectionModel().getSelectedItem();
+ updateStyles(fam);
+ update();
+ });
+
+ FX.name(styleField, name + "_STYLE");
+ styleField.getSelectionModel().selectedItemProperty().addListener((x) -> {
+ update();
+ });
+
+ FX.name(sizeField, name + "_SIZE");
+ sizeField.getItems().setAll(
+ 1.0,
+ 2.5,
+ 6.0,
+ 8.0,
+ 10.0,
+ 11.0,
+ 12.0,
+ 16.0,
+ 24.0,
+ 32.0,
+ 48.0,
+ 72.0,
+ 144.0,
+ 480.0
+ );
+ sizeField.getSelectionModel().selectedItemProperty().addListener((x) -> {
+ update();
+ });
+
+ getChildren().setAll(fontField, styleField, sizeField);
+ setHgrow(fontField, Priority.ALWAYS);
+ setMargin(sizeField, new Insets(0, 0, 0, 2));
+
+ setFont(property.get());
+ }
+
+ public SimpleObjectProperty getProperty() {
+ return property;
+ }
+
+ public void select(String name) {
+ fontField.getSelectionModel().select(name);
+ }
+
+ public Font getFont() {
+ String name = fontField.getSelectionModel().getSelectedItem();
+ if (name == null) {
+ return null;
+ }
+ String style = styleField.getSelectionModel().getSelectedItem();
+ if (!isBlank(style)) {
+ name = name + " " + style;
+ }
+ Double size = sizeField.getSelectionModel().getSelectedItem();
+ if (size == null) {
+ size = 12.0;
+ }
+ return new Font(name, size);
+ }
+
+ private static boolean isBlank(String s) {
+ return s == null ? true : s.trim().length() == 0;
+ }
+
+ protected void updateStyles(String family) {
+ String st = styleField.getSelectionModel().getSelectedItem();
+ if (st == null) {
+ st = "";
+ }
+
+ List ss = Font.getFontNames(family);
+ for (int i = 0; i < ss.size(); i++) {
+ String s = ss.get(i);
+ if (s.startsWith(family)) {
+ s = s.substring(family.length()).trim();
+ ss.set(i, s);
+ }
+ }
+ Collections.sort(ss);
+
+ styleField.getItems().setAll(ss);
+ int ix = ss.indexOf(st);
+ if (ix >= 0) {
+ styleField.getSelectionModel().select(ix);
+ }
+ }
+
+ protected void update() {
+ Font f = getFont();
+ property.set(f);
+ }
+
+ private void setFont(Font f) {
+ String name;
+ String style;
+ double size;
+ if (f == null) {
+ name = null;
+ style = null;
+ size = 12.0;
+ } else {
+ name = f.getFamily();
+ style = f.getStyle();
+ size = f.getSize();
+ }
+ fontField.getSelectionModel().select(name);
+ styleField.getSelectionModel().select(style);
+ sizeField.getSelectionModel().select(size);
+ }
+
+ protected List collectFonts(boolean allowNull) {
+ ArrayList rv = new ArrayList<>();
+ if (allowNull) {
+ rv.add(null);
+ }
+ rv.addAll(Font.getFamilies());
+ return rv;
+ }
+
+ public void selectSystemFont() {
+ FX.select(fontField, "System");
+ FX.select(styleField, "");
+ FX.select(sizeField, 12.0);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/InlineNodesModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/InlineNodesModel.java
new file mode 100644
index 00000000000..f484dbf9ddc
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/InlineNodesModel.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+
+/**
+ * A demo model with inline Nodes.
+ *
+ * @author Andy Goryachev
+ */
+public class InlineNodesModel extends SimpleViewOnlyStyledModel {
+ private final SimpleStringProperty textField = new SimpleStringProperty();
+
+ public InlineNodesModel() {
+ String ARABIC = "arabic";
+ String CODE = "code";
+ String RED = "red";
+ String GREEN = "green";
+ String UNDER = "underline";
+ String GRAY = "gray";
+ String LARGE = "large";
+ String ITALIC = "italic";
+
+ addWithStyleNames("Inline Nodes", UNDER, LARGE);
+ nl();
+ // trailing text
+ addNodeSegment(() -> {
+ TextField f = new TextField();
+ f.setPrefColumnCount(20);
+ f.textProperty().bindBidirectional(textField);
+ return f;
+ });
+ addWithStyleNames(" ", LARGE);
+ addNodeSegment(() -> new Button("OK"));
+ addWithStyleNames(" trailing segment.", LARGE); // FIX cannot navigate over this segment
+ nl();
+
+ // leading text
+ addWithStyleNames("Leading text", LARGE);
+ addNodeSegment(() -> {
+ TextField f = new TextField();
+ f.setPrefColumnCount(20);
+ f.textProperty().bindBidirectional(textField);
+ return f;
+ });
+ addWithStyleNames("- in between text-", LARGE);
+ addNodeSegment(() -> new Button("Find"));
+ nl();
+
+ // leading and trailing text
+ addWithStyleNames("Leading text", LARGE);
+ addNodeSegment(() -> {
+ TextField f = new TextField();
+ f.setPrefColumnCount(20);
+ f.textProperty().bindBidirectional(textField);
+ return f;
+ });
+ addWithStyleNames("- in between text-", LARGE);
+ addNodeSegment(() -> new Button("Find"));
+ addWithStyleNames(" trailing segment.", LARGE);
+ nl();
+
+ // adjacent nodes
+ addNodeSegment(() -> new Button("One"));
+ addNodeSegment(() -> new Button("Two"));
+ addNodeSegment(() -> new Button("Three"));
+ addNodeSegment(() -> new Button("Four"));
+ addNodeSegment(() -> new Button("Five"));
+ nl();
+ addWithStyleNames("", LARGE);
+ nl();
+
+ addWithStyleNames("A regular text segment for reference.", LARGE);
+ nl();
+ addWithStyleNames("The End █", LARGE);
+ nl();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/LargeTextModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/LargeTextModel.java
new file mode 100644
index 00000000000..5b1d55dcc36
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/LargeTextModel.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.util.Random;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+
+/**
+ * Large text model for debugging.
+ *
+ * @author Andy Goryachev
+ */
+public class LargeTextModel extends SimpleViewOnlyStyledModel {
+ private final String STYLE = "-fx-font-size:500%";
+ private final Random random = new Random();
+
+ public LargeTextModel(int lineCount) {
+ for (int i = 0; i < lineCount; i++) {
+ addLine(i);
+ }
+ }
+
+ private void addLine(int n) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("L").append(n).append(' ');
+ int ct;
+ if (random.nextFloat() < 0.01f) {
+ ct = 200;
+ } else {
+ ct = random.nextInt(10);
+ }
+
+ for (int i = 0; i < ct; i++) {
+ sb.append(" ").append(i);
+ int len = random.nextInt(10) + 1;
+ for (int j = 0; j < len; j++) {
+ sb.append('*');
+ }
+ }
+ addWithStyleNames(sb.toString(), STYLE);
+ nl();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ModelChoice.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ModelChoice.java
new file mode 100644
index 00000000000..456bcc69748
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ModelChoice.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.io.IOException;
+import javafx.scene.paint.Color;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.BasicTextModel;
+import jfx.incubator.scene.control.richtext.model.RichParagraph;
+import jfx.incubator.scene.control.richtext.model.RichTextModel;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+import jfx.incubator.scene.control.richtext.model.StyledInput;
+import jfx.incubator.scene.control.richtext.model.StyledTextModel;
+
+/**
+ * All the models used in the tester.
+ *
+ * @author Andy Goryachev
+ */
+public enum ModelChoice {
+ DEMO("Demo"),
+ PARAGRAPH("Paragraph Attributes"),
+ WRITING_SYSTEMS_EDITABLE("Writing Systems (Editable)"),
+ EDITABLE_STYLED("❤ Editable Rich Text Model"),
+ BILLION_LINES("2,000,000,000 Lines"),
+ NOTEBOOK("Notebook: Embedded Chart"),
+ NOTEBOOK2("Notebook: SQL Queries"),
+ EDITABLE_PLAIN("Plaintext with Syntax Highlighting"),
+ NULL("null"),
+ EXAMPLES("Examples"),
+ INLINE("Inline Nodes"),
+ MONOSPACED("Monospaced"),
+ TABS("Tabs"),
+ UNEVEN_SMALL("Uneven Small"),
+ UNEVEN_LARGE("Uneven Large"),
+ WRITING_SYSTEMS("Writing Systems"),
+ ZERO_LINES("0 Lines"),
+ ONE_LINE("1 Line"),
+ TEN_LINES("10 Lines"),
+ THOUSAND_LINES("1,000 Lines"),
+ LARGE_TEXT("Large text"),
+ LARGE_TEXT_LONG("Large Text, Long"),
+ NO_LAST_NEWLINE_SHORT("No Last Newline, Short"),
+ NO_LAST_NEWLINE_MEDIUM("No Last Newline, Medium"),
+ NO_LAST_NEWLINE_LONG("No Last Newline, Long"),
+ ;
+
+ private final String name;
+
+ ModelChoice(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ public static StyledTextModel create(ModelChoice ch) {
+ if(ch == null) {
+ return null;
+ }
+
+ switch(ch) {
+ case BILLION_LINES:
+ return new DemoStyledTextModel(2_000_000_000, false);
+ case DEMO:
+ return new DemoModel();
+ case EXAMPLES:
+ return new ExamplesModel();
+ case INLINE:
+ return new InlineNodesModel();
+ case EDITABLE_PLAIN:
+ {
+ BasicTextModel m = new BasicTextModel() {
+ private static final String DIGITS = "-fx-fill:magenta;";
+
+ @Override
+ public RichParagraph getParagraph(int index) {
+ String text = getPlainText(index);
+ RichParagraph.Builder b = RichParagraph.builder();
+ int start = 0;
+ int sz = text.length();
+ boolean num = false;
+ for (int i = 0; i < sz; i++) {
+ char c = text.charAt(i);
+ if (num != Character.isDigit(c)) {
+ if (i > start) {
+ String s = text.substring(start, i);
+ String style = num ? DIGITS : null;
+ b.addWithInlineStyle(s, style);
+ start = i;
+ }
+ num = !num;
+ }
+ }
+ if (start < sz) {
+ String s = text.substring(start);
+ String style = num ? DIGITS : null;
+ b.addWithInlineStyle(s, style);
+ }
+ return b.build();
+ }
+ };
+ return m;
+ }
+ case EDITABLE_STYLED:
+ return new RichTextModel();
+ case LARGE_TEXT:
+ return new LargeTextModel(10);
+ case LARGE_TEXT_LONG:
+ return new LargeTextModel(5_000);
+ case NO_LAST_NEWLINE_SHORT:
+ return new NoLastNewlineModel(1);
+ case NO_LAST_NEWLINE_MEDIUM:
+ return new NoLastNewlineModel(5);
+ case NO_LAST_NEWLINE_LONG:
+ return new NoLastNewlineModel(300);
+ case MONOSPACED:
+ return new DemoStyledTextModel(2_000_000_000, true);
+ case NOTEBOOK:
+ return new NotebookModel();
+ case NOTEBOOK2:
+ return new NotebookModel2();
+ case NULL:
+ return null;
+ case ONE_LINE:
+ return new DemoStyledTextModel(1, false);
+ case PARAGRAPH:
+ return new ParagraphAttributesDemoModel();
+ case TABS:
+ return tabs();
+ case TEN_LINES:
+ return new DemoStyledTextModel(10, false);
+ case THOUSAND_LINES:
+ return new DemoStyledTextModel(1_000, false);
+ case UNEVEN_SMALL:
+ return new UnevenStyledTextModel(20);
+ case UNEVEN_LARGE:
+ return new UnevenStyledTextModel(2000);
+ case WRITING_SYSTEMS:
+ return writingSystemsPlain();
+ case WRITING_SYSTEMS_EDITABLE:
+ return writingSystems();
+ case ZERO_LINES:
+ return new DemoStyledTextModel(0, false);
+ default:
+ throw new Error("?" + ch);
+ }
+ }
+
+ private static StyledTextModel writingSystemsPlain() {
+ try {
+ return SimpleViewOnlyStyledModel.of(WritingSystemsDemo.getText());
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static StyledTextModel tabs() {
+ try {
+ return SimpleViewOnlyStyledModel.of("0123456789012345678901234567890\n0\n\t1\n\t\t2\n\t\t\t3\n\t\t\t\t4\n0\n");
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ private static StyledTextModel writingSystems() {
+ StyleAttributeMap name = StyleAttributeMap.builder().
+ setFontSize(24).
+ setTextColor(Color.gray(0.5)).
+ build();
+
+ StyleAttributeMap value = StyleAttributeMap.builder().
+ setFontSize(24).
+ build();
+
+ RichTextModel m = new RichTextModel();
+ String[] ss = WritingSystemsDemo.PAIRS;
+ for (int i = 0; i < ss.length;) {
+ String s = ss[i++] + ": ";
+ append(m, s, name);
+
+ s = ss[i++];
+ append(m, s, value);
+
+ append(m, "\n", null);
+ }
+ return m;
+ }
+
+ // TODO add to StyledModel?
+ private static void append(StyledTextModel m, String text, StyleAttributeMap style) {
+ TextPos p = m.getDocumentEnd();
+ m.replace(null, p, p, StyledInput.of(text, style), false);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/MultipleStackedBoxWindow.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/MultipleStackedBoxWindow.java
new file mode 100644
index 00000000000..1ef27b441cf
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/MultipleStackedBoxWindow.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.geometry.Insets;
+import javafx.scene.Scene;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Label;
+import javafx.scene.control.Menu;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import com.oracle.demo.richtext.util.FX;
+import jfx.incubator.scene.control.richtext.LineNumberDecorator;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+
+/**
+ * Test Window that stacks multiple RichTextAreas and other components either vertically or horizontally.
+ *
+ * @author Andy Goryachev
+ */
+public class MultipleStackedBoxWindow extends Stage {
+
+ public MultipleStackedBoxWindow(boolean vertical) {
+ RichTextArea a1 = new RichTextArea(NotebookModelStacked.m1());
+ a1.setHighlightCurrentParagraph(true);
+ a1.setWrapText(true);
+ a1.setLeftDecorator(new LineNumberDecorator());
+ createPopupMenu(a1);
+
+ TextArea t1 = new TextArea("This TextArea has wrap text property set to false.");
+ t1.setPrefHeight(50);
+
+ Label t2 = new Label("Label");
+
+ RichTextArea a2 = new RichTextArea(NotebookModelStacked.m2());
+ a2.setHighlightCurrentParagraph(true);
+ a2.setWrapText(true);
+ a2.setLeftDecorator(new LineNumberDecorator());
+ createPopupMenu(a2);
+
+ PrefSizeTester tester = new PrefSizeTester();
+
+ ScrollPane sp = new ScrollPane();
+
+ if (vertical) {
+ a1.setUseContentHeight(true);
+ a2.setUseContentHeight(true);
+
+ VBox vb = new VBox(
+ a1,
+ t1,
+ a2,
+ t2,
+ tester
+ );
+ sp.setContent(vb);
+ sp.setFitToWidth(true);
+
+ setTitle("Test Vertical Stack");
+ setWidth(600);
+ setHeight(1200);
+ FX.name(this, "VerticalStack");
+ } else {
+ a1.setUseContentWidth(true);
+ a2.setUseContentWidth(true);
+
+ HBox hb = new HBox(
+ a1,
+ t1,
+ a2,
+ t2,
+ tester
+ );
+ sp.setContent(hb);
+ sp.setFitToHeight(true);
+
+ setTitle("Test Horizontal Stack");
+ setWidth(1200);
+ setHeight(600);
+ FX.name(this, "HorizontalStack");
+ }
+
+ Scene scene = new Scene(sp);
+ scene.getStylesheets().addAll(
+ RichTextAreaWindow.class.getResource("RichTextAreaDemo.css").toExternalForm()
+ );
+ setScene(scene);
+ }
+
+ protected void createPopupMenu(RichTextArea t) {
+ FX.setPopupMenu(t, () -> {
+ Menu m;
+ ContextMenu c = new ContextMenu();
+ // left side
+ m = FX.menu(c, "Left Side");
+ FX.checkItem(m, "null", t.getLeftDecorator() == null, (on) -> {
+ if (on) {
+ t.setLeftDecorator(null);
+ }
+ });
+ FX.checkItem(m, "Line Numbers", t.getLeftDecorator() instanceof LineNumberDecorator, (on) -> {
+ if (on) {
+ t.setLeftDecorator(new LineNumberDecorator());
+ }
+ });
+ FX.checkItem(m, "Colors", t.getLeftDecorator() instanceof DemoColorSideDecorator, (on) -> {
+ if (on) {
+ t.setLeftDecorator(new DemoColorSideDecorator());
+ }
+ });
+ // right side
+ m = FX.menu(c, "Right Side");
+ FX.checkItem(m, "null", t.getRightDecorator() == null, (on) -> {
+ if (on) {
+ t.setRightDecorator(null);
+ }
+ });
+ FX.checkItem(m, "Line Numbers", t.getRightDecorator() instanceof LineNumberDecorator, (on) -> {
+ if (on) {
+ t.setRightDecorator(new LineNumberDecorator());
+ }
+ });
+ FX.checkItem(m, "Colors", t.getRightDecorator() instanceof DemoColorSideDecorator, (on) -> {
+ if (on) {
+ t.setRightDecorator(new DemoColorSideDecorator());
+ }
+ });
+ // content padding
+ m = FX.menu(c, "Content Padding");
+ FX.checkItem(m, "null", t.getContentPadding() == null, (on) -> {
+ if (on) {
+ t.setContentPadding(null);
+ }
+ });
+ FX.checkItem(m, "1", new Insets(1).equals(t.getContentPadding()), (on) -> {
+ if (on) {
+ t.setContentPadding(new Insets(1));
+ }
+ });
+ FX.checkItem(m, "2", new Insets(1).equals(t.getContentPadding()), (on) -> {
+ if (on) {
+ t.setContentPadding(new Insets(2));
+ }
+ });
+ FX.checkItem(m, "10", new Insets(10).equals(t.getContentPadding()), (on) -> {
+ if (on) {
+ t.setContentPadding(new Insets(10));
+ }
+ });
+ FX.checkItem(m, "55.75", new Insets(55.75).equals(t.getContentPadding()), (on) -> {
+ if (on) {
+ t.setContentPadding(new Insets(55.75));
+ }
+ });
+
+ FX.checkItem(c, "Wrap Text", t.isWrapText(), (on) -> t.setWrapText(on));
+ return c;
+ });
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NoLastNewlineModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NoLastNewlineModel.java
new file mode 100644
index 00000000000..a3ad3d57fea
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NoLastNewlineModel.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+
+/**
+ * Test model.
+ */
+public class NoLastNewlineModel extends SimpleViewOnlyStyledModel {
+ public NoLastNewlineModel(int lineCount) {
+ for(int i=0; i OUTLINE = new StyleAttribute<>("OUTLINE", Boolean.class, true);
+
+ public NotebookModel() {
+ String GREEN = "green";
+ String GRAY = "gray";
+ String EQ = "equation";
+ String SUB = "sub";
+ String UNDER = "underline";
+
+ addWithInlineAndStyleNames("Bifurcation Diagram", "-fx-font-size:200%;", UNDER);
+ nl(2);
+ addWithStyleNames("In mathematics, particularly in dynamical systems, a ", GRAY);
+ addWithStyleNames("bifurcation diagram ", "-fx-font-weight:bold;"); // FIX does not work on mac
+ addWithStyleNames("shows the values visited or approached asymptotically (fixed points, periodic orbits, or chaotic attractors) of a system as a function of a bifurcation parameter in the system. It is usual to represent stable values with a solid line and unstable values with a dotted line, although often the unstable points are omitted. Bifurcation diagrams enable the visualization of bifurcation theory.", GRAY);
+ nl(2);
+ addWithStyleNames("An example is the bifurcation diagram of the logistic map:", GRAY);
+ nl(2);
+ addWithStyleNames(" x", EQ);
+ addWithStyleNames("n+1", EQ, SUB);
+ addWithStyleNames(" = λx", EQ);
+ addWithStyleNames("n", EQ, SUB);
+ addWithStyleNames("(1 - x", EQ);
+ addWithStyleNames("n", EQ, SUB);
+ addWithStyleNames(")", EQ);
+ setParagraphAttributes(StyleAttributeMap.of(OUTLINE, Boolean.TRUE));
+ nl(2);
+ addWithStyleNames("The bifurcation parameter λ is shown on the horizontal axis of the plot and the vertical axis shows the set of values of the logistic function visited asymptotically from almost all initial conditions.", GRAY);
+ nl(2);
+ addWithStyleNames("The bifurcation diagram shows the forking of the periods of stable orbits from 1 to 2 to 4 to 8 etc. Each of these bifurcation points is a period-doubling bifurcation. The ratio of the lengths of successive intervals between values of r for which bifurcation occurs converges to the first Feigenbaum constant.", GRAY);
+ nl(2);
+ addWithStyleNames("The diagram also shows period doublings from 3 to 6 to 12 etc., from 5 to 10 to 20 etc., and so forth.", GRAY);
+ nl();
+ addParagraph(BifurcationDiagram::generate);
+ nl(2);
+ addSegment("Source: Wikipedia");
+ nl();
+ addWithStyleNames("https://en.wikipedia.org/wiki/Bifurcation_diagram", GREEN, UNDER);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NotebookModel2.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NotebookModel2.java
new file mode 100644
index 00000000000..37afa7d4fdf
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NotebookModel2.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.control.Button;
+import javafx.scene.control.TextField;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+
+/**
+ * Mocks a Notebook Page that Provides a SQL Query Engine Interface
+ *
+ * @author Andy Goryachev
+ */
+public class NotebookModel2 extends SimpleViewOnlyStyledModel {
+ private final SimpleStringProperty query = new SimpleStringProperty();
+ private final SimpleObjectProperty result = new SimpleObjectProperty<>();
+ private static final String QUERY = "SELECT * FROM Book WHERE price > 100.00;";
+
+ public NotebookModel2() {
+ String ARABIC = "arabic";
+ String CODE = "code";
+ String RED = "red";
+ String GREEN = "green";
+ String UNDER = "underline";
+ String GRAY = "gray";
+ String LARGE = "large";
+ String EQ = "equation";
+ String SUB = "sub";
+
+ addWithInlineAndStyleNames("SQL Select", "-fx-font-size:200%;", UNDER);
+ nl(2);
+ addWithStyleNames("The SQL ", GRAY);
+ addWithInlineStyle("SELECT ", "-fx-font-weight:bold;"); // FIX does not work on mac
+ addWithStyleNames("statement returns a result set of records, from one or more tables.", GRAY);
+ nl(2);
+ addWithStyleNames("A SELECT statement retrieves zero or more rows from one or more database tables or database views. In most applications, SELECT is the most commonly used data manipulation language (DML) command. As SQL is a declarative programming language, SELECT queries specify a result set, but do not specify how to calculate it. The database translates the query into a \"query plan\" which may vary between executions, database versions and database software. This functionality is called the \"query optimizer\" as it is responsible for finding the best possible execution plan for the query, within applicable constraints.", GRAY);
+ nl(2);
+ addWithInlineStyle(QUERY, "-fx-font-weight:bold;"); // FIX does not work on mac
+ nl(2);
+ addNodeSegment(() -> {
+ TextField f = new TextField();
+ f.setPrefColumnCount(50);
+ f.textProperty().bindBidirectional(query);
+ return f;
+ });
+ addWithStyleNames(" ", GRAY);
+ addNodeSegment(() -> {
+ Button b = new Button("Run");
+ b.setOnAction((ev) -> execute());
+ return b;
+ });
+ nl(2);
+ addWithStyleNames("Result:", GRAY);
+ nl();
+ addParagraph(() -> new ResultParagraph(result));
+ nl(2);
+ addSegment("Source: Wikipedia");
+ nl();
+ addWithStyleNames("https://en.wikipedia.org/wiki/Select_(SQL)", GREEN, UNDER);
+ }
+
+ protected void execute() {
+ String q = query.get();
+ if (q == null) {
+ q = "";
+ }
+ q = q.toLowerCase();
+ if(q.equals(QUERY.toLowerCase())) {
+ result.set(generate());
+ } else {
+ result.set("This query is not supported by the demo engine.");
+ }
+ }
+
+ private String[] generate() {
+ return new String[] {
+ "Title|Author|Price",
+ "SQL Examples and Guide|J.Goodwell|145.55",
+ "The Joy of SQL|M.C.Eichler|250.00",
+ "An Introduction to SQL|Q.Adams|101.99",
+ };
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NotebookModelStacked.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NotebookModelStacked.java
new file mode 100644
index 00000000000..714c623e2e6
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/NotebookModelStacked.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.util.ArrayList;
+import java.util.function.Supplier;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.Region;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.StyleResolver;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.BasicTextModel;
+import jfx.incubator.scene.control.richtext.model.RichParagraph;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+import jfx.incubator.scene.control.richtext.model.StyledTextModel;
+
+/**
+ * Another test model.
+ *
+ * @author Andy Goryachev
+ */
+public class NotebookModelStacked extends StyledTextModel {
+ enum Type {
+ CODE,
+ COMMENT,
+ TEXTAREA,
+ }
+
+ private final ArrayList paragraphs = new ArrayList<>();
+
+ public NotebookModelStacked() {
+ paragraphs.add(m1());
+ paragraphs.add(Type.TEXTAREA);
+ paragraphs.add(m2());
+ }
+
+ public static StyledTextModel m1() {
+ return create(Type.COMMENT, "██This is\na comment cell.██p");
+ }
+
+ public static StyledTextModel m2() {
+ return create(Type.CODE, "x = 5;\nprint(x);");
+ }
+
+ public static StyledTextModel create(Type type, String text) {
+ BasicTextModel m;
+ switch(type) {
+ case CODE:
+ m = new BasicTextModel() {
+ @Override
+ public RichParagraph getParagraph(int index) {
+ String text = getPlainText(index);
+ RichParagraph.Builder b = RichParagraph.builder();
+ b.addWithInlineStyle(text, "-fx-text-fill:darkgreen; -fx-font-family:Monospace;");
+ return b.build();
+ }
+ };
+ break;
+ case COMMENT:
+ m = new BasicTextModel() {
+ @Override
+ public RichParagraph getParagraph(int index) {
+ String text = getPlainText(index);
+ RichParagraph.Builder b = RichParagraph.builder();
+ b.addWithInlineStyle(text, "-fx-text-fill:gray;");
+ return b.build();
+ }
+ };
+ break;
+ default:
+ throw new Error("?" + type);
+ }
+
+ m.insertText(TextPos.ZERO, text);
+ return m;
+ }
+
+ @Override
+ public boolean isWritable() {
+ return false;
+ }
+
+ @Override
+ public int size() {
+ return paragraphs.size();
+ }
+
+ @Override
+ public String getPlainText(int index) {
+ return "";
+ }
+
+ @Override
+ public RichParagraph getParagraph(int index) {
+ Object x = paragraphs.get(index);
+ if(x instanceof StyledTextModel m) {
+ return RichParagraph.of(() -> {
+ RichTextArea t = new RichTextArea(m);
+ t.setHighlightCurrentParagraph(true);
+ t.setMaxWidth(Double.POSITIVE_INFINITY);
+ t.setWrapText(true);
+ t.setUseContentHeight(true);
+ return t;
+ });
+ } else if(x instanceof Type type) {
+ switch(type) {
+ case TEXTAREA:
+ return RichParagraph.of(() -> {
+ TextArea t = new TextArea();
+ t.setMaxWidth(Double.POSITIVE_INFINITY);
+ t.setWrapText(true);
+ return t;
+ });
+ }
+ }
+ throw new Error("?" + x);
+ }
+
+ @Override
+ protected void removeRange(TextPos start, TextPos end) {
+ }
+
+ @Override
+ protected int insertTextSegment(int index, int offset, String text, StyleAttributeMap attrs) {
+ return 0;
+ }
+
+ @Override
+ protected void insertLineBreak(int index, int offset) {
+ }
+
+ @Override
+ protected void insertParagraph(int index, Supplier generator) {
+ }
+
+ @Override
+ public StyleAttributeMap getStyleAttributeMap(StyleResolver r, TextPos pos) {
+ return StyleAttributeMap.EMPTY;
+ }
+
+ @Override
+ protected void setParagraphStyle(int ix, StyleAttributeMap paragraphAttrs) {
+ }
+
+ @Override
+ protected void applyStyle(int ix, int start, int end, StyleAttributeMap a, boolean merge) {
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ParagraphAttributesDemoModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ParagraphAttributesDemoModel.java
new file mode 100644
index 00000000000..eaa6a5e5038
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ParagraphAttributesDemoModel.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.scene.paint.Color;
+import javafx.scene.text.TextAlignment;
+import com.oracle.demo.richtext.util.FX;
+import jfx.incubator.scene.control.richtext.model.RtfFormatHandler;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+
+/**
+ * This simple, read-only StyledModel demonstrates various paragraph attributes.
+ *
+ * @author Andy Goryachev
+ */
+public class ParagraphAttributesDemoModel extends SimpleViewOnlyStyledModel {
+ private final static StyleAttributeMap TITLE = StyleAttributeMap.builder().
+ setFontSize(24).
+ setUnderline(true).
+ build();
+ private final static StyleAttributeMap BULLET = StyleAttributeMap.builder().
+ setSpaceLeft(20).
+ setBullet("•").
+ build();
+ private final static StyleAttributeMap FIRST_LINE_INDENT = StyleAttributeMap.builder().
+ setFirstLineIndent(100).
+ build();
+
+ public ParagraphAttributesDemoModel() {
+ registerDataFormatHandler(RtfFormatHandler.getInstance(), true, false, 1000);
+ insert(this);
+ }
+
+ public static void insert(SimpleViewOnlyStyledModel m) {
+ m.addSegment("Bullet List", TITLE);
+ m.nl(2);
+ m.setParagraphAttributes(BULLET);
+ m.addSegment("This little piggy went to market,");
+ m.setParagraphAttributes(BULLET);
+ m.nl();
+ m.addSegment("This little piggy stayed home,");
+ m.setParagraphAttributes(BULLET);
+ m.nl();
+ m.addSegment("This little piggy had roast beef,");
+ m.setParagraphAttributes(BULLET);
+ m.nl();
+ m.addSegment("This little piggy had none.");
+ m.setParagraphAttributes(BULLET);
+ m.nl();
+ m.addSegment("This little piggy went ...");
+ m.setParagraphAttributes(BULLET);
+ m.nl();
+ m.addSegment("Wee, wee, wee, all the way home!");
+ m.setParagraphAttributes(BULLET);
+ m.nl(2);
+
+ m.addSegment("First Line Indent", TITLE);
+ m.nl(2);
+ m.addSegment(words(60));
+ m.setParagraphAttributes(FIRST_LINE_INDENT);
+ m.nl(2);
+
+ m.addSegment("Paragraph Attributes", TITLE);
+ m.nl(2);
+
+ m.addSegment("✓ Opaque Background Color");
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.LIGHTGREEN).
+ build());
+ m.nl();
+
+ m.addSegment("✓ Translucent Background Color");
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(FX.alpha(Color.LIGHTGREEN, 0.5)).
+ build());
+ m.nl();
+
+ // space
+
+ m.addSegment("✓ Space Above");
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setSpaceAbove(20).
+ setBackground(Color.gray(0.95, 0.5)).
+ setBullet("•").
+ build());
+ m.nl();
+
+ m.addSegment("✓ Space Below");
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setSpaceBelow(20).
+ setBackground(Color.gray(0.9, 0.5)).
+ setBullet("◦").
+ build());
+ m.nl();
+
+ m.addSegment("✓ Space Left " + words(50));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setSpaceLeft(20).
+ setBackground(Color.gray(0.85, 0.5)).
+ setBullet("∙").
+ build());
+ m.nl();
+
+ m.addSegment("✓ Space Right " + words(10));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setSpaceRight(20).
+ setBackground(Color.gray(0.8, 0.5)).
+ setBullet("‣").
+ build());
+ m.nl();
+
+ // text alignment
+
+ m.addSegment("✓ Text Alignment Left " + words(20));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.gray(0.95, 0.5)).
+ setTextAlignment(TextAlignment.LEFT).
+ build());
+ m.nl();
+
+ m.addSegment("✓ Text Alignment Right " + words(20));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.gray(0.9, 0.5)).
+ setTextAlignment(TextAlignment.RIGHT).
+ build());
+ m.nl();
+
+ m.addSegment("✓ Text Alignment Center " + words(20));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.gray(0.85, 0.5)).
+ setTextAlignment(TextAlignment.CENTER).
+ build());
+ m.nl();
+
+ m.addSegment("✓ Text Alignment Justify " + words(20));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.gray(0.8, 0.5)).
+ setTextAlignment(TextAlignment.JUSTIFY).
+ build());
+ m.nl();
+
+ // line spacing
+
+ m.addSegment("✓ Line Spacing 0 " + words(200));
+ m.highlight(50, 100, FX.alpha(Color.RED, 0.4));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.gray(0.95, 0.5)).
+ setLineSpacing(0).
+ build());
+ m.nl();
+
+ m.addSegment("✓ Line Spacing 20 " + words(200));
+ m.highlight(50, 100, FX.alpha(Color.RED, 0.4));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.gray(0.9, 0.5)).
+ setLineSpacing(20).
+ build());
+ m.nl();
+
+ m.addSegment("✓ Line Spacing 40 " + words(200));
+ m.highlight(50, 100, FX.alpha(Color.RED, 0.4));
+ m.setParagraphAttributes(StyleAttributeMap.builder().
+ setBackground(Color.gray(0.9, 0.5)).
+ setLineSpacing(40).
+ build());
+ m.nl();
+ }
+
+ private static String words(int count) {
+ String[] lorem = {
+ "Lorem",
+ "ipsum",
+ "dolor",
+ "sit",
+ "amet,",
+ "consectetur",
+ "adipiscing",
+ "elit,",
+ "sed",
+ "do",
+ "eiusmod",
+ "tempor",
+ "incididunt",
+ "ut",
+ "labore",
+ "et",
+ "dolore",
+ "magna",
+ "aliqua"
+ };
+
+ StringBuilder sb = new StringBuilder();
+ for(int i=0; i 0) {
+ sb.append(' ');
+ }
+ sb.append(lorem[i % lorem.length]);
+ }
+ sb.append(".");
+ return sb.toString();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/PrefSizeTester.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/PrefSizeTester.java
new file mode 100644
index 00000000000..6a160d5e8b2
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/PrefSizeTester.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+
+/**
+ * Debug aid.
+ *
+ * @author Andy Goryachev
+ */
+public class PrefSizeTester extends Pane {
+ private final ComboBox prefWidth;
+ private final ComboBox prefHeight;
+ private final GridPane p;
+
+ public PrefSizeTester() {
+ setBackground(Background.fill(Color.LIGHTSTEELBLUE));
+
+ prefWidth = new ComboBox();
+ prefWidth.getItems().addAll(
+ -1.0,
+ 100.0,
+ 200.0,
+ 300.0
+ );
+ prefWidth.setOnAction((ev) -> {
+ updateWidth();
+ });
+
+ prefHeight = new ComboBox();
+ prefHeight.getItems().addAll(
+ -1.0,
+ 100.0,
+ 200.0,
+ 300.0
+ );
+ prefHeight.setOnAction((ev) -> {
+ updateHeight();
+ });
+
+ p = new GridPane();
+ p.add(new Label("Pref Width:"), 0, 0);
+ p.add(prefWidth, 1, 0);
+ p.add(new Label("Pref Height:"), 0, 1);
+ p.add(prefHeight, 1, 1);
+
+ getChildren().add(p);
+ //setCenter(p);
+ }
+
+ private void updateWidth() {
+ if (prefWidth.getValue() instanceof Number n) {
+ double w = n.doubleValue();
+ p.setPrefWidth(w);
+ }
+ }
+
+ private void updateHeight() {
+ if (prefHeight.getValue() instanceof Number n) {
+ double h = n.doubleValue();
+ p.setPrefHeight(h);
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ROptionPane.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ROptionPane.java
new file mode 100644
index 00000000000..2a89e122df2
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ROptionPane.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+
+/**
+ * Option pane.
+ *
+ * @author Andy Goryachev
+ */
+public class ROptionPane extends GridPane {
+ private int row;
+ private int column;
+ private static final Insets MARGIN = new Insets(2, 4, 2, 4);
+
+ public ROptionPane() {
+ // no such thing
+ // https://stackoverflow.com/questions/20454021/how-to-set-padding-between-columns-of-a-javafx-gridpane
+ // setVGap(2);
+ }
+
+ public void label(String text) {
+ add(new Label(text));
+ }
+
+ public void option(Node n) {
+ add(n);
+ }
+
+ public void add(Node n) {
+ add(n, column, row++);
+ setMargin(n, MARGIN);
+ setFillHeight(n, Boolean.TRUE);
+ setFillWidth(n, Boolean.TRUE);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RegionCellPane.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RegionCellPane.java
new file mode 100644
index 00000000000..288a6261d94
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RegionCellPane.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.VPos;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+
+/**
+ * Content pane for TextCell that shows an arbitrary Region.
+ * The content gets resized if it cannot fit into available width.
+ *
+ * @author Andy Goryachev
+ */
+public class RegionCellPane extends Pane {
+ private final Region content;
+ private static final Insets PADDING = new Insets(1, 1, 1, 1);
+
+ public RegionCellPane(Region n) {
+ this.content = n;
+
+ getChildren().add(n);
+
+ setPadding(PADDING);
+ getStyleClass().add("region-cell");
+ }
+
+ @Override
+ protected void layoutChildren() {
+ double width = getWidth() - snappedLeftInset() - snappedRightInset();
+ double w = content.prefWidth(-1);
+ if (w < width) {
+ width = w;
+ }
+ double h = content.prefHeight(width);
+
+ double x0 = snappedLeftInset();
+ double y0 = snappedTopInset();
+ layoutInArea(
+ content,
+ x0,
+ y0,
+ width,
+ h,
+ 0,
+ null,
+ true,
+ false,
+ HPos.CENTER,
+ VPos.CENTER
+ );
+ }
+
+ @Override
+ protected double computePrefHeight(double width) {
+ return content.prefHeight(width) + snappedTopInset() + snappedBottomInset();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ResultParagraph.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ResultParagraph.java
new file mode 100644
index 00000000000..d01227e12fe
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/ResultParagraph.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.scene.Node;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.text.Text;
+import javafx.scene.text.TextFlow;
+
+/**
+ * Part of the code cell.
+ *
+ * @author Andy Goryachev
+ */
+public class ResultParagraph extends BorderPane {
+ SimpleObjectProperty result = new SimpleObjectProperty();
+
+ public ResultParagraph(SimpleObjectProperty src) {
+ result.bind(src);
+ result.addListener((s, p, c) -> {
+ update();
+ });
+ update();
+ setPrefSize(600, 200);
+ }
+
+ protected void update() {
+ Node n = getNode();
+ setCenter(n);
+ }
+
+ protected Node getNode() {
+ Object r = result.get();
+ if (r instanceof String s) {
+ Text t = new Text(s);
+ t.setStyle("-fx-fill:red;");
+
+ TextFlow f = new TextFlow();
+ f.getChildren().add(t);
+ return f;
+ } else if (r instanceof String[] ss) {
+ DataFrame f = DataFrame.parse(ss);
+ TableView t = new TableView<>();
+
+ String[] cols = f.getColumnNames();
+ for (int i=0; i c = new TableColumn<>(col);
+ int ix = i;
+ c.setCellValueFactory((d) -> {
+ String[] row = d.getValue();
+ String s = row[ix];
+ return new SimpleStringProperty(s);
+ });
+ t.getColumns().add(c);
+ }
+ for (int i = 0; i < f.getRowCount(); i++) {
+ t.getItems().add(f.getRow(i));
+ }
+ t.prefWidthProperty().bind(widthProperty());
+ return t;
+ }
+ return null;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemo.css b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemo.css
new file mode 100644
index 00000000000..30e5c19d04f
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemo.css
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+.red {
+ -fx-fill:red;
+}
+
+.green {
+ -fx-fill:#3e8c25;
+}
+
+.gray {
+ -fx-fill:gray;
+}
+
+.code {
+ -fx-font-family:Monospace;
+}
+
+.underline {
+ -fx-underline:true;
+}
+
+.monospaced {
+ -fx-font-family:Monospaced;
+}
+
+.bold {
+ -fx-font-weight: bold;
+}
+
+.italic {
+ -fx-font-family: serif;
+ -fx-font-style: italic;
+}
+
+.strikethrough {
+ -fx-strikethrough: true;
+}
+
+.arabic {
+ -fx-font-family:Tahoma;
+ -fx-font-size:200%;
+}
+
+.large {
+ -fx-font-size:200%;
+}
+
+.equation {
+ -fx-font-family:serif;
+}
+
+.sub {
+ -fx-text-origin:bottom;
+ -fx-font-size:70%;
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemoApp.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemoApp.java
new file mode 100644
index 00000000000..a91b47b9067
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemoApp.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.application.Application;
+import javafx.stage.Stage;
+import com.oracle.demo.richtext.settings.FxSettings;
+
+/**
+ * RichTextArea Demo Application.
+ *
+ * @author Andy Goryachev
+ */
+public class RichTextAreaDemoApp extends Application {
+ public static void main(String[] args) {
+ Application.launch(RichTextAreaDemoApp.class, args);
+ }
+
+ @Override
+ public void init() {
+ FxSettings.useDirectory(".RichTextAreaDemo");
+ }
+
+ @Override
+ public void start(Stage stage) throws Exception {
+ try {
+ new RichTextAreaWindow(false).show();
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemoPane.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemoPane.java
new file mode 100644
index 00000000000..f69a5109699
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaDemoPane.java
@@ -0,0 +1,762 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.nio.charset.Charset;
+import java.util.Base64;
+import java.util.List;
+import java.util.Objects;
+import javafx.application.Platform;
+import javafx.collections.ObservableList;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.scene.AccessibleAttribute;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.CheckMenuItem;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.ScrollPane.ScrollBarPolicy;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.SplitPane;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.DataFormat;
+import javafx.scene.input.KeyCode;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.scene.text.TextAlignment;
+import javafx.stage.Window;
+import javafx.util.Duration;
+import javafx.util.StringConverter;
+import com.oracle.demo.richtext.util.FX;
+import jfx.incubator.scene.control.input.KeyBinding;
+import jfx.incubator.scene.control.richtext.LineNumberDecorator;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.SideDecorator;
+import jfx.incubator.scene.control.richtext.StyleHandlerRegistry;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.ParagraphDirection;
+import jfx.incubator.scene.control.richtext.model.RichTextModel;
+import jfx.incubator.scene.control.richtext.model.StyleAttribute;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+import jfx.incubator.scene.control.richtext.model.StyledTextModel;
+import jfx.incubator.scene.control.richtext.skin.RichTextAreaSkin;
+
+/**
+ * Main Panel contains RichTextArea, split panes for quick size adjustment, and an option pane.
+ *
+ * @author Andy Goryachev
+ */
+public class RichTextAreaDemoPane extends BorderPane {
+ enum Decorator {
+ NULL,
+ LINE_NUMBERS,
+ COLORS
+ }
+
+ private static StyledTextModel globalModel;
+ public final ROptionPane op;
+ public final RichTextArea control;
+ public final ComboBox modelField;
+
+ public RichTextAreaDemoPane(boolean useContentSize) {
+ FX.name(this, "RichTextAreaDemoPane");
+
+ control = new RichTextArea() {
+ private static final StyleHandlerRegistry registry = init();
+
+ private static StyleHandlerRegistry init() {
+ // brings in the handlers from the base class
+ StyleHandlerRegistry.Builder b = StyleHandlerRegistry.builder(RichTextArea.styleHandlerRegistry);
+ // adds a handler for the new attribute
+ b.setParHandler(NotebookModel.OUTLINE, (c, cx, v) -> {
+ if (v) {
+ cx.addStyle("-fx-border-color:LIGHTPINK;");
+ cx.addStyle("-fx-border-width:1;");
+ }
+ });
+ return b.build();
+ }
+
+ @Override
+ public StyleHandlerRegistry getStyleHandlerRegistry() {
+ return registry;
+ }
+ };
+ control.setUseContentHeight(useContentSize);
+ control.setUseContentWidth(useContentSize);
+ control.setHighlightCurrentParagraph(true);
+
+ // custom functions
+ System.out.println(
+ """
+ F3: dump accessibility attributes at cursor
+ """);
+// control.getInputMap().register(KeyBinding.of(KeyCode.F2), () -> {
+// RichTextModel.dump(control.getModel(), System.out);
+// });
+ control.getInputMap().register(KeyBinding.of(KeyCode.F3), () -> {
+ dumpAccessibilityAttributes();
+ });
+
+ Node contentNode;
+ if (useContentSize) {
+ contentNode = new ScrollPane(control);
+ } else {
+ contentNode = control;
+ }
+
+ SplitPane hsplit = new SplitPane(contentNode, pane());
+ FX.name(hsplit, "hsplit");
+ hsplit.setBorder(null);
+ hsplit.setDividerPositions(1.0);
+ hsplit.setOrientation(Orientation.HORIZONTAL);
+
+ SplitPane vsplit = new SplitPane(hsplit, pane());
+ FX.name(vsplit, "vsplit");
+ vsplit.setBorder(null);
+ vsplit.setDividerPositions(1.0);
+ vsplit.setOrientation(Orientation.VERTICAL);
+
+ modelField = new ComboBox<>();
+ FX.name(modelField, "modelField");
+ modelField.getItems().setAll(ModelChoice.values());
+
+ CheckBox editable = new CheckBox("editable");
+ FX.name(editable, "editable");
+ editable.selectedProperty().bindBidirectional(control.editableProperty());
+
+ CheckBox wrapText = new CheckBox("wrap text");
+ FX.name(wrapText, "wrapText");
+ wrapText.selectedProperty().bindBidirectional(control.wrapTextProperty());
+
+ CheckBox displayCaret = new CheckBox("display caret");
+ FX.name(displayCaret, "displayCaret");
+ displayCaret.selectedProperty().bindBidirectional(control.displayCaretProperty());
+
+ CheckBox fatCaret = new CheckBox("fat caret");
+ FX.name(fatCaret, "fatCaret");
+ fatCaret.selectedProperty().addListener((s, p, on) -> {
+ Node n = control.lookup(".caret");
+ if (n != null) {
+ if (on) {
+ n.setStyle("-fx-stroke-width:2; -fx-stroke:red; -fx-effect:dropshadow(gaussian,rgba(0,0,0,.5),5,0,1,1);");
+ } else {
+ n.setStyle(null);
+ }
+ }
+ });
+
+ CheckBox fastBlink = new CheckBox("blink fast");
+ FX.name(fastBlink, "fastBlink");
+ fastBlink.selectedProperty().addListener((s,p,on) -> {
+ control.setCaretBlinkPeriod(on ? Duration.millis(200) : Duration.millis(500));
+ });
+
+ CheckBox highlightCurrentLine = new CheckBox("highlight current line");
+ FX.name(highlightCurrentLine, "highlightCurrentLine");
+ highlightCurrentLine.selectedProperty().bindBidirectional(control.highlightCurrentParagraphProperty());
+
+ Button reloadModelButton = new Button("Reload Model");
+ reloadModelButton.setOnAction((ev) -> reloadModel());
+
+ CheckBox customPopup = new CheckBox("custom popup menu");
+ FX.name(customPopup, "customPopup");
+ customPopup.selectedProperty().addListener((s, p, v) -> {
+ setCustomPopup(v);
+ });
+
+ ComboBox contentPadding = contentPaddingOption();
+
+ ComboBox leftDecorator = new ComboBox<>();
+ FX.name(leftDecorator, "leftDecorator");
+ leftDecorator.getItems().setAll(Decorator.values());
+ leftDecorator.getSelectionModel().selectedItemProperty().addListener((s,p,v) -> {
+ control.setLeftDecorator(createDecorator(v));
+ });
+
+ ComboBox rightDecorator = new ComboBox<>();
+ FX.name(rightDecorator, "rightDecorator");
+ rightDecorator.getItems().setAll(Decorator.values());
+ rightDecorator.getSelectionModel().selectedItemProperty().addListener((s,p,v) -> {
+ control.setRightDecorator(createDecorator(v));
+ });
+
+ CheckBox trackWidth = new CheckBox("use content width");
+ FX.name(trackWidth, "trackWidth");
+ trackWidth.selectedProperty().bindBidirectional(control.useContentWidthProperty());
+
+ CheckBox trackHeight = new CheckBox("use content height");
+ FX.name(trackHeight, "trackHeight");
+ trackHeight.selectedProperty().bindBidirectional(control.useContentHeightProperty());
+
+ Button appendButton = new Button("Append");
+ FX.tooltip(appendButton, "appends text to the end of the document");
+ appendButton.setOnAction((ev) -> {
+ StyleAttributeMap heading = StyleAttributeMap.builder().setBold(true).setFontSize(24).build();
+ StyleAttributeMap plain = StyleAttributeMap.builder().setFontFamily("Monospaced").build();
+ control.appendText("Heading\n", heading);
+ control.appendText("Plain monospaced text.\n", plain);
+ });
+
+ Button insertButton = new Button("Insert");
+ FX.tooltip(insertButton, "inserts text to the start of the document");
+ insertButton.setOnAction((ev) -> {
+ StyleAttributeMap heading = StyleAttributeMap.builder().setBold(true).setFontSize(24).build();
+ StyleAttributeMap plain = StyleAttributeMap.builder().setFontFamily("Monospaced").build();
+ control.insertText(TextPos.ZERO, "Plain monospaced text.\n", plain);
+ control.insertText(TextPos.ZERO, "Heading\n", heading);
+ });
+
+ Button replaceSkin = new Button("Replace Skin");
+ replaceSkin.setOnAction((ev) -> {
+ control.setSkin(new RichTextAreaSkin(control));
+ });
+
+ op = new ROptionPane();
+ op.label("Model:");
+ op.option(modelField);
+ op.option(new HBox(insertButton, appendButton));
+ op.option(editable);
+ op.option(reloadModelButton);
+ op.option(wrapText);
+ op.option(displayCaret);
+ op.option(fatCaret);
+ op.option(fastBlink);
+ op.option(highlightCurrentLine);
+ op.option(customPopup);
+ op.label("Content Padding:");
+ op.option(contentPadding);
+ op.label("Decorators:");
+ op.option(leftDecorator);
+ op.option(rightDecorator);
+ op.option(trackWidth);
+ op.option(trackHeight);
+ op.option(replaceSkin);
+
+ setCenter(vsplit);
+
+ ScrollPane sp = new ScrollPane(op);
+ sp.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
+ sp.setHbarPolicy(ScrollBarPolicy.NEVER);
+ setRight(sp);
+
+ modelField.getSelectionModel().selectFirst();
+ leftDecorator.getSelectionModel().selectFirst();
+ rightDecorator.getSelectionModel().selectFirst();
+
+ Platform.runLater(() -> {
+ // all this to make sure restore settings works correctly with second window loading the same model
+ if (globalModel == null) {
+ globalModel = createModel();
+ }
+ control.setModel(globalModel);
+
+ modelField.getSelectionModel().selectedItemProperty().addListener((s, p, c) -> {
+ updateModel();
+ });
+ });
+ }
+
+ protected SideDecorator createDecorator(Decorator d) {
+ if (d != null) {
+ switch (d) {
+ case COLORS:
+ return new DemoColorSideDecorator();
+ case LINE_NUMBERS:
+ return new LineNumberDecorator();
+ }
+ }
+ return null;
+ }
+
+ protected void updateModel() {
+ globalModel = createModel();
+ control.setModel(globalModel);
+ }
+
+ protected void reloadModel() {
+ control.setModel(null);
+ updateModel();
+ }
+
+ private StyledTextModel createModel() {
+ ModelChoice m = modelField.getSelectionModel().getSelectedItem();
+ return ModelChoice.create(m);
+ }
+
+ protected static Pane pane() {
+ Pane p = new Pane();
+ SplitPane.setResizableWithParent(p, false);
+ p.setStyle("-fx-background-color:#dddddd;");
+ return p;
+ }
+
+ public Button addButton(String name, Runnable action) {
+ Button b = new Button(name);
+ b.setOnAction((ev) -> {
+ action.run();
+ });
+
+ toolbar().add(b);
+ return b;
+ }
+
+ public TBar toolbar() {
+ if (getTop() instanceof TBar) {
+ return (TBar)getTop();
+ }
+
+ TBar t = new TBar();
+ setTop(t);
+ return t;
+ }
+
+ public Window getWindow() {
+ Scene s = getScene();
+ if (s != null) {
+ return s.getWindow();
+ }
+ return null;
+ }
+
+ public void setOptions(Node n) {
+ setRight(n);
+ }
+
+ protected String generateStylesheet(boolean fat) {
+ String s = ".rich-text-area .caret { -fx-stroke-width:" + (fat ? 2 : 1) + "; }";
+ return "data:text/css;base64," + Base64.getEncoder().encodeToString(s.getBytes(Charset.forName("utf-8")));
+ }
+
+ protected void setCustomPopup(boolean on) {
+ if (on) {
+ ContextMenu m = new ContextMenu();
+ m.getItems().add(new MenuItem("Dummy")); // otherwise no popup is shown
+ m.addEventFilter(Menu.ON_SHOWING, (ev) -> {
+ m.getItems().clear();
+ populateCustomPopupMenu(m.getItems());
+ });
+ control.setContextMenu(m);
+ } else {
+ control.setContextMenu(null);
+ }
+ }
+
+ protected void populateCustomPopupMenu(ObservableList items) {
+ boolean sel = control.hasNonEmptySelection();
+ boolean paste = true; // would be easier with Actions (findFormatForPaste() != null);
+ boolean styled = (control.getModel() instanceof RichTextModel);
+
+ items.add(new MenuItem("★ Custom Context Menu"));
+
+ items.add(new SeparatorMenuItem());
+
+ Menu m2;
+ MenuItem m;
+ CheckMenuItem cm;
+ items.add(m = new MenuItem("Undo"));
+ m.setOnAction((ev) -> control.undo());
+ m.setDisable(!control.isUndoable());
+
+ items.add(m = new MenuItem("Redo"));
+ m.setOnAction((ev) -> control.redo());
+ m.setDisable(!control.isRedoable());
+
+ items.add(new SeparatorMenuItem());
+
+ items.add(m = new MenuItem("Cut"));
+ m.setOnAction((ev) -> control.cut());
+ m.setDisable(!sel);
+
+ items.add(m = new MenuItem("Copy"));
+ m.setOnAction((ev) -> control.copy());
+ m.setDisable(!sel);
+
+ items.add(m = m2 = new Menu("Copy Special..."));
+ {
+ List fs = control.getModel().getSupportedDataFormats(true);
+ for (DataFormat f : fs) {
+ String name = f.toString();
+ m2.getItems().add(m = new MenuItem(name));
+ m.setOnAction((ev) -> control.copy(f));
+ }
+ }
+
+ items.add(m = new MenuItem("Paste"));
+ m.setOnAction((ev) -> control.paste());
+ m.setDisable(!paste);
+
+ items.add(m = m2 = new Menu("Paste Special..."));
+ m.setDisable(!paste);
+ {
+ List fs = control.getModel().getSupportedDataFormats(false);
+ for (DataFormat f : fs) {
+ if (Clipboard.getSystemClipboard().hasContent(f)) {
+ String name = f.toString();
+ m2.getItems().add(m = new MenuItem(name));
+ m2.setOnAction((ev) -> control.paste(f));
+ m2.setDisable(!paste);
+ }
+ }
+ }
+
+ items.add(m = new MenuItem("Paste and Match Style"));
+ m.setOnAction((ev) -> control.pastePlainText());
+ m.setDisable(!paste);
+
+ if (styled) {
+ StyleAttributeMap a = control.getActiveStyleAttributeMap();
+ items.add(new SeparatorMenuItem());
+
+ items.add(m = new MenuItem("Bold"));
+ m.setOnAction((ev) -> applyStyle(StyleAttributeMap.BOLD, !a.getBoolean(StyleAttributeMap.BOLD)));
+ m.setDisable(!sel);
+
+ items.add(m = new MenuItem("Italic"));
+ m.setOnAction((ev) -> applyStyle(StyleAttributeMap.ITALIC, !a.getBoolean(StyleAttributeMap.ITALIC)));
+ m.setDisable(!sel);
+
+ items.add(m = new MenuItem("Strike Through"));
+ m.setOnAction((ev) -> applyStyle(StyleAttributeMap.STRIKE_THROUGH, !a.getBoolean(StyleAttributeMap.STRIKE_THROUGH)));
+ m.setDisable(!sel);
+
+ items.add(m = new MenuItem("Underline"));
+ m.setOnAction((ev) -> applyStyle(StyleAttributeMap.UNDERLINE, !a.getBoolean(StyleAttributeMap.UNDERLINE)));
+ m.setDisable(!sel);
+
+ items.add(m2 = new Menu("Text Color"));
+ colorMenu(m2, sel, Color.BLACK);
+ colorMenu(m2, sel, Color.DARKGRAY);
+ colorMenu(m2, sel, Color.GRAY);
+ colorMenu(m2, sel, Color.LIGHTGRAY);
+ colorMenu(m2, sel, Color.GREEN);
+ colorMenu(m2, sel, Color.RED);
+ colorMenu(m2, sel, Color.BLUE);
+ colorMenu(m2, sel, null);
+
+ items.add(m2 = new Menu("Text Size"));
+ sizeMenu(m2, sel, 96);
+ sizeMenu(m2, sel, 72);
+ sizeMenu(m2, sel, 48);
+ sizeMenu(m2, sel, 36);
+ sizeMenu(m2, sel, 24);
+ sizeMenu(m2, sel, 18);
+ sizeMenu(m2, sel, 16);
+ sizeMenu(m2, sel, 14);
+ sizeMenu(m2, sel, 12);
+ sizeMenu(m2, sel, 10);
+ sizeMenu(m2, sel, 9);
+ sizeMenu(m2, sel, 8);
+ sizeMenu(m2, sel, 6);
+
+ items.add(m2 = new Menu("Font Family"));
+ fontMenu(m2, sel, "System");
+ fontMenu(m2, sel, "Serif");
+ fontMenu(m2, sel, "Sans-serif");
+ fontMenu(m2, sel, "Cursive");
+ fontMenu(m2, sel, "Fantasy");
+ fontMenu(m2, sel, "Monospaced");
+ m2.getItems().add(new SeparatorMenuItem());
+ fontMenu(m2, sel, "Arial");
+ fontMenu(m2, sel, "Courier New");
+ fontMenu(m2, sel, "Times New Roman");
+ fontMenu(m2, sel, "null");
+ }
+
+ if (styled) {
+ StyleAttributeMap a = control.getActiveStyleAttributeMap();
+ items.add(new SeparatorMenuItem());
+
+ items.add(m2 = new Menu("Alignment"));
+ alignmentMenu(m2, "Left", TextAlignment.LEFT);
+ alignmentMenu(m2, "Center", TextAlignment.CENTER);
+ alignmentMenu(m2, "Right", TextAlignment.RIGHT);
+ alignmentMenu(m2, "Justify", TextAlignment.JUSTIFY);
+
+ items.add(m2 = new Menu("Line Spacing"));
+ lineSpacingMenu(m2, 0);
+ lineSpacingMenu(m2, 1);
+ lineSpacingMenu(m2, 10);
+ lineSpacingMenu(m2, 30);
+
+ items.add(m2 = new Menu("Space"));
+ spaceMenu(m2, "All", 30, 30, 30, 30);
+ spaceMenu(m2, "Above", 30, 0, 0, 0);
+ spaceMenu(m2, "Below", 0, 0, 30, 0);
+ spaceMenu(m2, "Left", 0, 0, 0, 30);
+ spaceMenu(m2, "Right", 0, 30, 0, 0);
+ spaceMenu(m2, "None", 0, 0, 0, 0);
+
+ items.add(m2 = new Menu("Paragraph Direction"));
+ directionMenu(m2, "Left-to-Right", ParagraphDirection.LEFT_TO_RIGHT);
+ directionMenu(m2, "Right-to-Left", ParagraphDirection.RIGHT_TO_LEFT);
+ directionMenu(m2, "", null);
+
+ items.add(m2 = new Menu("Background Color"));
+ backgroundMenu(m2, "Red", Color.RED, 0.2);
+ backgroundMenu(m2, "Green", Color.GREEN, 0.2);
+ backgroundMenu(m2, "Blue", Color.BLUE, 0.2);
+ backgroundMenu(m2, "Gray", Color.GRAY, 1.0);
+ backgroundMenu(m2, "Gray 10%", Color.GRAY, 0.1);
+ backgroundMenu(m2, "Gray 20%", Color.GRAY, 0.2);
+ backgroundMenu(m2, "Yellow", Color.YELLOW, 1.0);
+
+ items.add(m2 = new Menu("Bullet"));
+ bulletMenu(m2, a, "None", null);
+ bulletMenu(m2, a, "●", "●");
+ bulletMenu(m2, a, "○", "○");
+ bulletMenu(m2, a, "♣", "♣");
+
+ items.add(m2 = new Menu("First Line Indent"));
+ firstLineIndentMenu(m2, a, 0);
+ firstLineIndentMenu(m2, a, 10);
+ firstLineIndentMenu(m2, a, 50);
+ firstLineIndentMenu(m2, a, 100);
+ }
+
+ items.add(new SeparatorMenuItem());
+
+ items.add(m = new MenuItem("Select All"));
+ m.setOnAction((ev) -> control.selectAll());
+ }
+
+ private void bulletMenu(Menu menu, StyleAttributeMap a, String name, String bullet) {
+ CheckMenuItem m = new CheckMenuItem(name);
+ menu.getItems().add(m);
+ m.setSelected(Objects.equals(bullet, a.getBullet()));
+ m.setOnAction((ev) -> {
+ applyStyle(StyleAttributeMap.BULLET, bullet);
+ });
+ }
+
+ private void firstLineIndentMenu(Menu menu, StyleAttributeMap a, int value) {
+ CheckMenuItem m = new CheckMenuItem(String.valueOf(value));
+ menu.getItems().add(m);
+ Double v = a.getFirstLineIndent();
+ if (v != null) {
+ m.setSelected(Objects.equals(value, v.intValue()));
+ }
+ m.setOnAction((ev) -> {
+ applyStyle(StyleAttributeMap.FIRST_LINE_INDENT, (double)value);
+ });
+ }
+
+ private void alignmentMenu(Menu menu, String name, TextAlignment a) {
+ MenuItem m = new MenuItem(name);
+ menu.getItems().add(m);
+ m.setOnAction((ev) -> {
+ applyStyle(StyleAttributeMap.TEXT_ALIGNMENT, a);
+ });
+ }
+
+ private void lineSpacingMenu(Menu menu, double value) {
+ MenuItem m = new MenuItem(String.valueOf(value));
+ menu.getItems().add(m);
+ m.setOnAction((ev) -> {
+ applyStyle(StyleAttributeMap.LINE_SPACING, value);
+ });
+ }
+
+ private void directionMenu(Menu menu, String text, ParagraphDirection d) {
+ MenuItem m = new MenuItem(text);
+ menu.getItems().add(m);
+ m.setOnAction((ev) -> {
+ applyStyle(StyleAttributeMap.PARAGRAPH_DIRECTION, d);
+ });
+ }
+
+ private void spaceMenu(Menu menu, String name, double top, double right, double bottom, double left) {
+ MenuItem m = new MenuItem(name);
+ menu.getItems().add(m);
+ m.setOnAction((ev) -> {
+ applyStyle(StyleAttributeMap.SPACE_ABOVE, top);
+ applyStyle(StyleAttributeMap.SPACE_BELOW, bottom);
+ applyStyle(StyleAttributeMap.SPACE_LEFT, left);
+ applyStyle(StyleAttributeMap.SPACE_RIGHT, right);
+ });
+ }
+
+ private void backgroundMenu(Menu menu, String name, Color color, double alpha) {
+ Color c = FX.alpha(color, alpha);
+ MenuItem m = new MenuItem(name);
+ menu.getItems().add(m);
+ m.setOnAction((ev) -> {
+ applyStyle(StyleAttributeMap.BACKGROUND, c);
+ });
+ }
+
+ private void fontMenu(Menu menu, boolean selected, String family) {
+ MenuItem m = new MenuItem(family);
+ m.setDisable(!selected);
+ m.setOnAction((ev) -> applyStyle(StyleAttributeMap.FONT_FAMILY, family));
+ menu.getItems().add(m);
+ }
+
+ private void sizeMenu(Menu menu, boolean selected, double size) {
+ MenuItem m = new MenuItem(String.valueOf(size));
+ m.setDisable(!selected);
+ m.setOnAction((ev) -> applyStyle(StyleAttributeMap.FONT_SIZE, size));
+ menu.getItems().add(m);
+ }
+
+ private void colorMenu(Menu menu, boolean selected, Color color) {
+ int w = 16;
+ int h = 16;
+ Canvas c = new Canvas(w, h);
+ GraphicsContext g = c.getGraphicsContext2D();
+ if (color != null) {
+ g.setFill(color);
+ g.fillRect(0, 0, w, h);
+ }
+ g.setStroke(Color.DARKGRAY);
+ g.strokeRect(0, 0, w, h);
+
+ MenuItem m = new MenuItem(null, c);
+ m.setDisable(!selected);
+ m.setOnAction((ev) -> applyStyle(StyleAttributeMap.TEXT_COLOR, color));
+ menu.getItems().add(m);
+ }
+
+ private void applyStyle(StyleAttribute a, T val) {
+ TextPos ca = control.getCaretPosition();
+ TextPos an = control.getAnchorPosition();
+ StyleAttributeMap m = StyleAttributeMap.of(a, val);
+ control.applyStyle(ca, an, m);
+ }
+
+ void dumpAccessibilityAttributes() {
+ TextPos caret = control.getCaretPosition();
+ if (caret == null) {
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ Object x;
+ x = control.queryAccessibleAttribute(AccessibleAttribute.LINE_FOR_OFFSET, caret.charIndex());
+ sb.append(x).append("\n");
+ System.out.println(sb.toString());
+ }
+
+ /** Tool Bar */
+ public static class TBar extends HBox {
+ public TBar() {
+ setFillHeight(true);
+ setAlignment(Pos.CENTER_LEFT);
+ setSpacing(2);
+ }
+
+ public T add(T n) {
+ getChildren().add(n);
+ return n;
+ }
+
+ public void addAll(Node... nodes) {
+ for (Node n : nodes) {
+ add(n);
+ }
+ }
+ }
+
+ private ComboBox contentPaddingOption() {
+ ComboBox op = new ComboBox<>();
+ FX.name(op, "contentPadding");
+ op.setConverter(new StringConverter() {
+ @Override
+ public String toString(Insets x) {
+ if (x == null) {
+ return "null";
+ }
+ return String.format(
+ "T%d, B%d, L%d, R%d",
+ (int)x.getTop(),
+ (int)x.getBottom(),
+ (int)x.getLeft(),
+ (int)x.getRight()
+ );
+ }
+
+ @Override
+ public Insets fromString(String s) {
+ return null;
+ }
+ });
+ op.getItems().setAll(
+ null,
+ new Insets(1),
+ new Insets(2),
+ new Insets(10),
+ new Insets(22.22),
+ new Insets(50),
+ new Insets(100),
+ new Insets(5, 10, 15, 20)
+ );
+
+ selectValue(op, control.getContentPadding());
+ control.contentPaddingProperty().addListener((s,p,v) -> {
+ selectValue(op, v);
+ });
+
+ op.getSelectionModel().selectedItemProperty().addListener((s,p,v) -> {
+ control.setContentPadding(v);
+ });
+
+ return op;
+ }
+
+ private void selectValue(ComboBox c, T value) {
+ int ix = -1;
+ for (int i = c.getItems().size() - 1; i >= 0; i--) {
+ T v = c.getItems().get(i);
+ if (Objects.equals(v, value)) {
+ ix = i;
+ break;
+ }
+ }
+ if (ix < 0) {
+ ix = c.getItems().size();
+ c.getItems().add(value);
+ }
+ c.getSelectionModel().select(ix);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaWindow.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaWindow.java
new file mode 100644
index 00000000000..ad591899982
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/RichTextAreaWindow.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.geometry.NodeOrientation;
+import javafx.scene.Scene;
+import javafx.scene.control.CheckMenuItem;
+import javafx.scene.control.Label;
+import javafx.scene.control.MenuBar;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import com.oracle.demo.richtext.util.FX;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.TextPos;
+
+/**
+ * Rich Text Area Demo window.
+ *
+ * @author Andy Goryachev
+ */
+public class RichTextAreaWindow extends Stage {
+ public final RichTextAreaDemoPane demoPane;
+ public final Label status;
+
+ public RichTextAreaWindow(boolean useContentSize) {
+ demoPane = new RichTextAreaDemoPane(useContentSize);
+
+ CheckMenuItem orientation = new CheckMenuItem("Orientation: RTL");
+ orientation.setOnAction((ev) -> {
+ NodeOrientation v = (orientation.isSelected()) ?
+ NodeOrientation.RIGHT_TO_LEFT :
+ NodeOrientation.LEFT_TO_RIGHT;
+ getScene().setNodeOrientation(v);
+ });
+ // TODO save orientation in settings
+
+ MenuBar mb = new MenuBar();
+ // file
+ FX.menu(mb, "_File");
+ FX.item(mb, "New Window", () -> newWindow(false));
+ FX.item(mb, "New Window, Use Content Size", () -> newWindow(true));
+ FX.separator(mb);
+ FX.item(mb, "Close Window", this::hide);
+ FX.separator(mb);
+ FX.item(mb, "Quit", Platform::exit);
+ // tools
+ FX.menu(mb, "T_ools");
+ FX.item(mb, "CSS Tool", this::openCssTool);
+ // window
+ FX.menu(mb, "_Window");
+ FX.item(mb, "Stacked Vertically", () -> openMultipeStacked(true));
+ FX.item(mb, "Stacked Horizontally", () -> openMultipeStacked(false));
+ FX.item(mb, "In a VBox", this::openInVBox);
+ FX.separator(mb);
+ FX.item(mb, orientation);
+
+ status = new Label();
+ status.setPadding(new Insets(2, 10, 2, 10));
+
+ BorderPane bp = new BorderPane();
+ bp.setTop(mb);
+ bp.setCenter(demoPane);
+ bp.setBottom(status);
+
+ Scene scene = new Scene(bp);
+ scene.getStylesheets().addAll(
+ RichTextAreaWindow.class.getResource("RichTextAreaDemo.css").toExternalForm()
+ );
+
+ setScene(scene);
+ setTitle(
+ "RichTextArea Tester FX:" +
+ System.getProperty("javafx.runtime.version") +
+ " JDK:" +
+ System.getProperty("java.version")
+ );
+ setWidth(1200);
+ setHeight(600);
+
+ demoPane.control.caretPositionProperty().addListener((x) -> updateStatus());
+ }
+
+ protected void updateStatus() {
+ RichTextArea t = demoPane.control;
+ TextPos p = t.getCaretPosition();
+
+ StringBuilder sb = new StringBuilder();
+
+ if (p != null) {
+ sb.append(" line=").append(p.index());
+ sb.append(" col=").append(p.offset());
+ sb.append(p.isLeading() ? " (leading)" : "");
+ }
+
+ status.setText(sb.toString());
+ }
+
+ protected void newWindow(boolean useContentSize) {
+ double offset = 20;
+
+ RichTextAreaWindow w = new RichTextAreaWindow(useContentSize);
+ w.setX(getX() + offset);
+ w.setY(getY() + offset);
+ w.setWidth(getWidth());
+ w.setHeight(getHeight());
+ w.show();
+ }
+
+ protected void openMultipeStacked(boolean vertical) {
+ new MultipleStackedBoxWindow(vertical).show();
+ }
+
+ protected void openInVBox() {
+ RichTextArea richTextArea = new RichTextArea();
+
+ VBox vb = new VBox();
+ vb.getChildren().add(richTextArea);
+
+ Stage w = new Stage();
+ w.setScene(new Scene(vb));
+ w.setTitle("RichTextArea in a VBox");
+ w.show();
+ }
+
+ protected void openCssTool() {
+ Stage w = new Stage();
+ w.setScene(new Scene(new CssToolPane()));
+ w.setTitle("CSS Tool");
+ w.setWidth(800);
+ w.setHeight(600);
+ w.show();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/UnevenStyledTextModel.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/UnevenStyledTextModel.java
new file mode 100644
index 00000000000..96bde3f85fd
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/UnevenStyledTextModel.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import java.util.Random;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+
+/**
+ * A test model.
+ *
+ * @author Andy Goryachev
+ */
+public class UnevenStyledTextModel extends SimpleViewOnlyStyledModel {
+ private Random r = new Random();
+
+ public UnevenStyledTextModel(int lineCount) {
+ float longLineProbability = 0.1f;
+ for (int i = 0; i < lineCount; i++) {
+ boolean large = (r.nextFloat() < longLineProbability);
+ addSegment((large ? "L." : "S.") + (i + 1));
+
+ if (large) {
+ add(1000);
+ } else {
+ add(10);
+ }
+ nl();
+ }
+ }
+
+ private void add(int count) {
+ StringBuilder sb = new StringBuilder();
+
+ for (int i = 0; i < count; i++) {
+ int len = r.nextInt(10) + 1;
+ sb.append(' ');
+ sb.append(i);
+ sb.append('.');
+
+ for (int j = 0; j < len; j++) {
+ sb.append('*');
+ }
+ }
+
+ addSegment(sb.toString());
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/UsageExamples.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/UsageExamples.java
new file mode 100644
index 00000000000..7d9d3b1fc00
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/UsageExamples.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.scene.input.KeyCode;
+import javafx.stage.Stage;
+import jfx.incubator.scene.control.input.FunctionTag;
+import jfx.incubator.scene.control.input.KeyBinding;
+import jfx.incubator.scene.control.richtext.CodeArea;
+import jfx.incubator.scene.control.richtext.LineNumberDecorator;
+import jfx.incubator.scene.control.richtext.RichTextArea;
+import jfx.incubator.scene.control.richtext.TextPos;
+import jfx.incubator.scene.control.richtext.model.SimpleViewOnlyStyledModel;
+import jfx.incubator.scene.control.richtext.model.StyleAttributeMap;
+
+/**
+ * The usage examples used in the documentation.
+ *
+ * @author Andy Goryachev
+ */
+public class UsageExamples {
+ // This example is used in the JEP and RichTextArea class javadoc.
+ void createViewOnly() {
+ SimpleViewOnlyStyledModel m = new SimpleViewOnlyStyledModel();
+ // add text segment using CSS style name (requires a stylesheet)
+ m.addWithStyleNames("RichTextArea ", "HEADER");
+ // add text segment using inline styles
+ m.addWithInlineStyle("Demo", "-fx-font-size:200%; -fx-font-weight:bold;");
+ // add newline
+ m.nl();
+
+ RichTextArea textArea = new RichTextArea(m);
+ }
+
+ void createViewOnlyAll() {
+ SimpleViewOnlyStyledModel m = new SimpleViewOnlyStyledModel();
+ // add text segment using CSS style name (requires a stylesheet)
+ m.addWithStyleNames("RichTextArea ", "HEADER");
+ // add text segment using inline styles
+ m.addWithInlineStyle("Demo ", "-fx-font-size:200%; -fx-font-weight:bold;");
+ // with inline and style names
+ m.addWithInlineAndStyleNames("... more text", "-fx-text-fill:red;", "STYLE1", "STYLE2");
+ // add newline
+ m.nl();
+
+ RichTextArea textArea = new RichTextArea(m);
+ }
+
+ static RichTextArea appendStyledText() {
+ // create styles
+ StyleAttributeMap heading = StyleAttributeMap.builder().setBold(true).setUnderline(true).setFontSize(18).build();
+ StyleAttributeMap mono = StyleAttributeMap.builder().setFontFamily("Monospaced").build();
+
+ RichTextArea textArea = new RichTextArea();
+ // build the content
+ textArea.appendText("RichTextArea\n", heading);
+ textArea.appendText("Example:\nText is ", StyleAttributeMap.EMPTY);
+ textArea.appendText("monospaced.\n", mono);
+ return textArea;
+ }
+
+ void richTextAreaExample() {
+ RichTextArea textArea = new RichTextArea();
+ // insert two paragraphs "A" and "B"
+ StyleAttributeMap bold = StyleAttributeMap.builder().setBold(true).build();
+ textArea.appendText("A\nB", bold);
+ }
+
+ private static CodeArea codeAreaExample() {
+ CodeArea codeArea = new CodeArea();
+ codeArea.setWrapText(true);
+ codeArea.setLineNumbersEnabled(true);
+ codeArea.setText("Lorem\nIpsum");
+ return codeArea;
+ }
+
+ private static final FunctionTag PRINT_TO_CONSOLE = new FunctionTag();
+
+ void customNavigation() {
+ RichTextArea richTextArea = new RichTextArea();
+
+ // creates a new key binding mapped to an external function
+ richTextArea.getInputMap().register(KeyBinding.shortcut(KeyCode.W), () -> {
+ System.out.println("console!");
+ });
+
+ // disable old key bindings
+ var old = richTextArea.getInputMap().getKeyBindingsFor(RichTextArea.Tag.PASTE_PLAIN_TEXT);
+ for (KeyBinding k : old) {
+ richTextArea.getInputMap().disableKeyBinding(k);
+ }
+ // map a new key binding
+ richTextArea.getInputMap().registerKey(KeyBinding.shortcut(KeyCode.W), RichTextArea.Tag.PASTE_PLAIN_TEXT);
+
+ // redefine a function
+ richTextArea.getInputMap().registerFunction(RichTextArea.Tag.PASTE_PLAIN_TEXT, () -> { });
+ richTextArea.pastePlainText(); // becomes a no-op
+ // revert back to the default behavior
+ richTextArea.getInputMap().restoreDefaultFunction(RichTextArea.Tag.PASTE_PLAIN_TEXT);
+
+ // sets a side decorator
+ richTextArea.setLeftDecorator(new LineNumberDecorator());
+
+ richTextArea.getInputMap().registerFunction(PRINT_TO_CONSOLE, () -> {
+ // new functionality
+ System.out.println("PRINT_TO_CONSOLE executed");
+ });
+
+ // change the functionality of an existing key binding
+ richTextArea.getInputMap().registerFunction(RichTextArea.Tag.MOVE_WORD_NEXT_START, () -> {
+ // refers to custom logic
+ TextPos p = getCustomNextWordPosition(richTextArea);
+ richTextArea.select(p);
+ });
+ }
+
+ void testGeneric() {
+ MyControl c = new MyControl();
+ c.getInputMap().registerFunction(MyControl.MY_TAG, () -> {
+ c.newFunctionImpl();
+ });
+ }
+
+ private TextPos getCustomNextWordPosition(RichTextArea richTextArea) {
+ return null;
+ }
+
+ public static class MyControl extends RichTextArea {
+ // function tag allows the user to customize key bindings
+ public static final FunctionTag MY_TAG = new FunctionTag();
+
+ public MyControl() {
+ // register custom functionality with the input map
+ getInputMap().registerFunction(MY_TAG, this::newFunctionImpl);
+ // create a key binding
+ getInputMap().registerKey(KeyBinding.shortcut(KeyCode.W), MY_TAG);
+ }
+
+ public void newFunctionImpl() {
+ // custom functionality
+ }
+ }
+
+ public static class App extends Application {
+ public App() {
+ System.out.println("test app: F1 appends at the end, F2 inserts at the start, F3 clears selection.");
+ }
+
+ @Override
+ public void start(Stage stage) throws Exception {
+ RichTextArea t = true ? appendStyledText() : codeAreaExample();
+ stage.setScene(new Scene(t));
+ t.selectionProperty().addListener((s,p,c) -> {
+ System.out.println("selection: " + c);
+ });
+ t.anchorPositionProperty().addListener((s,p,c) -> {
+ System.out.println("anchor: " + c);
+ });
+ t.caretPositionProperty().addListener((s,p,c) -> {
+ System.out.println("caret: " + c);
+ });
+ t.getInputMap().register(KeyBinding.of(KeyCode.F1), () -> {
+ t.insertText(TextPos.ZERO, "F1", StyleAttributeMap.EMPTY);
+ });
+ t.getInputMap().register(KeyBinding.of(KeyCode.F2), () -> {
+ t.insertText(TextPos.ZERO, "\n", StyleAttributeMap.EMPTY);
+ });
+ t.getInputMap().register(KeyBinding.of(KeyCode.F3), () -> {
+ t.clearSelection();
+ });
+ stage.show();
+ }
+ }
+
+ public static void main(String[] args) {
+ App.launch(App.class, args);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/WritingSystemsDemo.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/WritingSystemsDemo.java
new file mode 100644
index 00000000000..b95e3cbe76b
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/WritingSystemsDemo.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.rta;
+
+/**
+ * Sample text for testing writing systems support.
+ * See https://en.wikipedia.org/wiki/List_of_writing_systems
+ *
+ * @author Andy Goryachev
+ */
+public class WritingSystemsDemo {
+ public static final String[] PAIRS = {
+ "Arabic", "العربية",
+ "Aramaic", "Classical Syriac: ܐܪܡܝܐ, Old Aramaic: 𐤀𐤓𐤌𐤉𐤀; Imperial Aramaic: 𐡀𐡓𐡌𐡉𐡀; Jewish Babylonian Aramaic: אֲרָמִית",
+ "Akkadian", "𒀝𒅗𒁺𒌑",
+ "Armenian", "հայերէն/հայերեն",
+ "Assamese", "অসমীয়া",
+ "Awadhi", "अवधी/औधी",
+ "Azerbaijanis", "آذربایجانلیلار",
+ "Bagheli", "बघेली",
+ "Bagri", "बागड़ी, باگڑی",
+ "Bengali", "বাংলা",
+ "Bhojpuri", "𑂦𑂷𑂔𑂣𑂳𑂩𑂲",
+ "Braille", "⠃⠗⠇",
+ "Bundeli", "बुन्देली",
+ "Burmese", "မြန်မာ",
+ "Cherokee", "ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ",
+ "Chhattisgarhi", "छत्तीसगढ़ी, ଛତିଶଗଡ଼ି, ଲରିଆ",
+ "Chinese", "中文",
+ "Czech", "Čeština",
+ "Devanagari", "देवनागरी",
+ "Dhivehi", "ދިވެހި",
+ "Dhundhari", "ढूण्ढाड़ी/ઢૂણ્ઢાડ઼ી",
+ "Farsi", "فارسی",
+ "Garhwali", "गढ़वळि",
+ "Geʽez", "ግዕዝ",
+ "Greek", "Ελληνικά",
+ "Georgian", "ქართული",
+ "Gujarati", "ગુજરાતી",
+ "Harauti", "हाड़ौती, हाड़ोती",
+ "Haryanvi", "हरयाणवी",
+ "Hausa", "هَرْشٜن هَوْسَ",
+ "Hebrew", "עברית",
+ "Hindi", "हिन्दी",
+ "Inuktitut", "ᐃᓄᒃᑎᑐᑦ",
+ "Japanese", "日本語 かな カナ",
+ "Kangri", "कांगड़ी",
+ "Kannada", "ಕನ್ನಡ",
+ "Kashmiri", "كٲشُرकॉशुर𑆑𑆳𑆯𑆶𑆫𑇀",
+ "Khmer", "ខ្មែរ",
+ "Khortha", "खोरठा",
+ "Khowar", "کھووار زبان",
+ "Korean", "한국어",
+ "Kumaoni", "कुमाऊँनी",
+ "Kurdish", "Kurdî / کوردی",
+ "Magahi", "𑂧𑂏𑂯𑂲/𑂧𑂏𑂡𑂲",
+ "Maithili", "मैथिली",
+ "Malayalam", "മലയാളം",
+ "Malvi", "माळवी भाषा / માળવી ભાષા",
+ "Marathi", "मराठी",
+ "Marwari,", "मारवाड़ी",
+ "Meitei", "ꯃꯩꯇꯩꯂꯣꯟ",
+ "Mewari", "मेवाड़ी/મેવ઼ાડ઼ી",
+ "Mongolian", "ᠨᠢᠷᠤᠭᠤ",
+ "Nimadi", "निमाड़ी",
+ "Odia", "ଓଡ଼ିଆ",
+ "Pahari", "पहाड़ी پہاڑی ",
+ "Pashto", "پښتو",
+ "Punjabi", "ਪੰਜਾਬੀپن٘جابی",
+ "Rajasthani", "राजस्थानी",
+ "Russian", "Русский",
+ "Sanskrit", "संस्कृत-, संस्कृतम्",
+ "Santali", "ᱥᱟᱱᱛᱟᱲᱤ",
+ "Sindhi", "سِنڌِي • सिन्धी",
+ "Suret", "ܣܘܪܝܬ",
+ "Surgujia", "सरगुजिया",
+ "Surjapuri", "सुरजापुरी, সুরজাপুরী",
+ "Tamil", "தமிழ்",
+ "Telugu", "తెలుగు",
+ "Thaana", "ދިވެހި",
+ "Thai", "ไทย",
+ "Tibetan", "བོད་",
+ "Tulu", "ತುಳು",
+ "Turoyo", "ܛܘܪܝܐ",
+ "Ukrainian", "Українська",
+ "Urdu", "اردو",
+ "Vietnamese", "Tiếng Việt",
+ "Yiddish", "ייִדיש יידיש אידיש"
+ };
+
+ public static String getText() {
+ return getText(false);
+ }
+
+ public static String getText(boolean showUnicode) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < PAIRS.length;) {
+ String a = PAIRS[i++];
+ String b = PAIRS[i++];
+ t(sb, a, b, showUnicode);
+ }
+ return sb.toString();
+ }
+
+ private static void t(StringBuilder sb, String name, String text, boolean showUnicode) {
+ sb.append(name);
+ sb.append(": ");
+ sb.append(text);
+ if (showUnicode) {
+ sb.append(" (");
+ native2ascii(sb, text);
+ sb.append(")");
+ }
+ sb.append("\n");
+ }
+
+ private static void native2ascii(StringBuilder sb, String text) {
+ for (char c: text.toCharArray()) {
+ if (c < 0x20) {
+ escape(sb, c);
+ } else if (c > 0x7f) {
+ escape(sb, c);
+ } else {
+ sb.append(c);
+ }
+ }
+ }
+
+ private static void escape(StringBuilder sb, char c) {
+ sb.append("\\u");
+ sb.append(h(c >> 12));
+ sb.append(h(c >> 8));
+ sb.append(h(c >> 4));
+ sb.append(h(c));
+ }
+
+ private static char h(int d) {
+ return "0123456789abcdef".charAt(d & 0x000f);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/animated.gif b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/animated.gif
new file mode 100644
index 00000000000..b502ee1b7d3
Binary files /dev/null and b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/rta/animated.gif differ
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettings.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettings.java
new file mode 100644
index 00000000000..db6b1ac77c1
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettings.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+// This code borrows heavily from the following project, with permission from the author:
+// https://github.com/andy-goryachev/FxDock
+package com.oracle.demo.richtext.settings;
+
+import java.io.File;
+import java.io.IOException;
+import javafx.animation.KeyFrame;
+import javafx.animation.Timeline;
+import javafx.collections.ListChangeListener;
+import javafx.scene.Node;
+import javafx.stage.Modality;
+import javafx.stage.PopupWindow;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import javafx.util.Duration;
+
+/**
+ * This facility coordinates saving UI settings to and from persistent media.
+ * All the calls, except useProvider(), are expected to happen in an FX application thread.
+ *
+ * When using {@link FxSettingsFileProvider}, the settings file "ui-settings.properties"
+ * is placed in the specified directory in the user home.
+ *
+ * TODO handle i/o errors - set handler?
+ *
+ * @author Andy Goryachev
+ */
+public class FxSettings {
+ public static final boolean LOG = Boolean.getBoolean("FxSettings.LOG");
+ private static final Duration SAVE_DELAY = Duration.millis(100);
+ private static ISettingsProvider provider;
+ private static boolean save;
+ private static Timeline saveTimer;
+
+ /** call this in Application.init() */
+ public static synchronized void useProvider(ISettingsProvider p) {
+ if (provider != null) {
+ throw new IllegalArgumentException("provider is already set");
+ }
+
+ provider = p;
+
+ Window.getWindows().addListener((ListChangeListener.Change extends Window> ch) -> {
+ while (ch.next()) {
+ if (ch.wasAdded()) {
+ for (Window w: ch.getAddedSubList()) {
+ handleWindowOpening(w);
+ }
+ } else if (ch.wasRemoved()) {
+ for (Window w: ch.getRemoved()) {
+ handleWindowClosing(w);
+ }
+ }
+ }
+ });
+
+ try {
+ provider.load();
+ } catch (IOException e) {
+ throw new Error(e);
+ }
+
+ saveTimer = new Timeline(new KeyFrame(SAVE_DELAY, (ev) -> save()));
+ }
+
+ public static void useDirectory(String dir) {
+ File d = new File(System.getProperty("user.home"), dir);
+ useProvider(new FxSettingsFileProvider(d));
+ }
+
+ public static void setName(Window w, String name) {
+ // TODO
+ }
+
+ private static void handleWindowOpening(Window w) {
+ if (w instanceof PopupWindow) {
+ return;
+ }
+
+ if (w instanceof Stage s) {
+ if (s.getModality() != Modality.NONE) {
+ return;
+ }
+ }
+
+ restoreWindow(w);
+ }
+
+ public static void restoreWindow(Window w) {
+ WindowMonitor m = WindowMonitor.getFor(w);
+ if (m != null) {
+ FxSettingsSchema.restoreWindow(m, w);
+
+ Node p = w.getScene().getRoot();
+ FxSettingsSchema.restoreNode(p);
+ }
+ }
+
+ private static void handleWindowClosing(Window w) {
+ if (w instanceof PopupWindow) {
+ return;
+ }
+
+ storeWindow(w);
+
+ boolean last = WindowMonitor.remove(w);
+ if (last) {
+ if (saveTimer != null) {
+ saveTimer.stop();
+ save();
+ }
+ }
+ }
+
+ public static void storeWindow(Window w) {
+ WindowMonitor m = WindowMonitor.getFor(w);
+ if (m != null) {
+ FxSettingsSchema.storeWindow(m, w);
+
+ Node p = w.getScene().getRoot();
+ FxSettingsSchema.storeNode(p);
+ }
+ }
+
+ public static void set(String key, String value) {
+ if (provider != null) {
+ provider.set(key, value);
+ triggerSave();
+ }
+ }
+
+ public static String get(String key) {
+ if (provider == null) {
+ return null;
+ }
+ return provider.get(key);
+ }
+
+ public static void setStream(String key, SStream s) {
+ if (provider != null) {
+ provider.set(key, s);
+ triggerSave();
+ }
+ }
+
+ public static SStream getStream(String key) {
+ if (provider == null) {
+ return null;
+ }
+ return provider.getSStream(key);
+ }
+
+ public static void setInt(String key, int value) {
+ set(key, String.valueOf(value));
+ }
+
+ public static int getInt(String key, int defaultValue) {
+ String v = get(key);
+ if (v != null) {
+ try {
+ return Integer.parseInt(v);
+ } catch (NumberFormatException e) {
+ }
+ }
+ return defaultValue;
+ }
+
+ public static void setBoolean(String key, boolean value) {
+ set(key, String.valueOf(value));
+ }
+
+ public static Boolean getBoolean(String key) {
+ String v = get(key);
+ if (v != null) {
+ if ("true".equals(v)) {
+ return Boolean.TRUE;
+ } else if ("false".equals(v)) {
+ return Boolean.FALSE;
+ }
+ }
+ return null;
+ }
+
+ private static synchronized void triggerSave() {
+ save = true;
+ if (saveTimer != null) {
+ saveTimer.stop();
+ saveTimer.play();
+ }
+ }
+
+ private static void save() {
+ try {
+ save = false;
+ provider.save();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public static void restore(Node n) {
+ FxSettingsSchema.restoreNode(n);
+ }
+
+ public static void store(Node n) {
+ FxSettingsSchema.storeNode(n);
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettingsFileProvider.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettingsFileProvider.java
new file mode 100644
index 00000000000..a50b738963c
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettingsFileProvider.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+// This code borrows heavily from the following project, with permission from the author:
+// https://github.com/andy-goryachev/FxDock
+package com.oracle.demo.richtext.settings;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * Settings provider stores settings as a single file in the specified directory.
+ *
+ * @author Andy Goryachev
+ */
+public class FxSettingsFileProvider implements ISettingsProvider {
+ private static final char SEP = '=';
+ private static final String DIV = ",";
+ private final File file;
+ private final HashMap data = new HashMap<>();
+
+ public FxSettingsFileProvider(File dir) {
+ file = new File(dir, "ui-settings.properties");
+ }
+
+ @Override
+ public void load() throws IOException {
+ if (file.exists() && file.isFile()) {
+ Charset cs = Charset.forName("utf-8");
+ try (BufferedReader rd = new BufferedReader(new InputStreamReader(new FileInputStream(file), cs))) {
+ synchronized (data) {
+ read(rd);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void save() throws IOException {
+ if (file.getParentFile() != null) {
+ file.getParentFile().mkdirs();
+ }
+
+ Charset cs = Charset.forName("utf-8");
+ try (Writer wr = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), cs))) {
+ synchronized (data) {
+ write(wr);
+ }
+ }
+ }
+
+ private void read(BufferedReader rd) throws IOException {
+ String s;
+ while ((s = rd.readLine()) != null) {
+ int ix = s.indexOf(SEP);
+ if (ix <= 0) {
+ continue;
+ }
+ String k = s.substring(0, ix);
+ String v = s.substring(ix + 1);
+ data.put(k, v);
+ }
+ }
+
+ private void write(Writer wr) throws IOException {
+ ArrayList keys = new ArrayList<>(data.keySet());
+ Collections.sort(keys);
+
+ for (String k: keys) {
+ Object v = data.get(k);
+ wr.write(k);
+ wr.write(SEP);
+ wr.write(encode(v));
+ wr.write("\r\n");
+ }
+ }
+
+ @Override
+ public void set(String key, String value) {
+ if (FxSettings.LOG) {
+ System.out.println("FxSettingsFileProvider.set key=" + key + " value=" + value);
+ }
+ synchronized (data) {
+ if (value == null) {
+ data.remove(key);
+ } else {
+ data.put(key, value);
+ }
+ }
+ }
+
+ @Override
+ public void set(String key, SStream stream) {
+ if (FxSettings.LOG) {
+ System.out.println("FxSettingsFileProvider.set key=" + key + " stream=" + stream);
+ }
+ synchronized (data) {
+ if (stream == null) {
+ data.remove(key);
+ } else {
+ data.put(key, stream.toArray());
+ }
+ }
+ }
+
+ @Override
+ public String get(String key) {
+ Object v;
+ synchronized (data) {
+ v = data.get(key);
+ }
+
+ String s;
+ if (v instanceof String) {
+ s = (String)v;
+ } else {
+ s = null;
+ }
+
+ if (FxSettings.LOG) {
+ System.out.println("FxSettingsFileProvider.get key=" + key + " value=" + s);
+ }
+ return s;
+ }
+
+ @Override
+ public SStream getSStream(String key) {
+ SStream s;
+ synchronized (data) {
+ Object v = data.get(key);
+ if (v instanceof Object[]) {
+ s = SStream.reader((Object[])v);
+ } else if (v != null) {
+ s = parseStream(v.toString());
+ data.put(key, s.toArray());
+ } else {
+ s = null;
+ }
+ }
+
+ if (FxSettings.LOG) {
+ System.out.println("FxSettingsFileProvider.get key=" + key + " stream=" + s);
+ }
+ return s;
+ }
+
+ private static SStream parseStream(String text) {
+ String[] ss = text.split(DIV);
+ return SStream.reader(ss);
+ }
+
+ private static String encode(Object x) {
+ if (x == null) {
+ return "";
+ } else if (x instanceof Object[] items) {
+ StringBuilder sb = new StringBuilder();
+ boolean sep = false;
+ for (Object item: items) {
+ if (sep) {
+ sb.append(DIV);
+ } else {
+ sep = true;
+ }
+ sb.append(item);
+ }
+ return sb.toString();
+ } else {
+ return x.toString();
+ }
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettingsSchema.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettingsSchema.java
new file mode 100644
index 00000000000..b6c7c7d7c84
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/FxSettingsSchema.java
@@ -0,0 +1,488 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+// This code borrows heavily from the following project, with permission from the author:
+// https://github.com/andy-goryachev/FxDock
+package com.oracle.demo.richtext.settings;
+
+import javafx.beans.value.ChangeListener;
+import javafx.beans.value.ObservableValue;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.DialogPane;
+import javafx.scene.control.ListView;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.SplitPane;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.FlowPane;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.TilePane;
+import javafx.scene.layout.VBox;
+import javafx.scene.shape.Shape;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+/**
+ * Constants and methods used to persist settings.
+ *
+ * @author Andy Goryachev
+ */
+public class FxSettingsSchema {
+ private static final String PREFIX = "FX.";
+ private static final String WINDOW_NORMAL = "N";
+ private static final String WINDOW_ICONIFIED = "I";
+ private static final String WINDOW_MAXIMIZED = "M";
+ private static final String WINDOW_FULLSCREEN = "F";
+ private static final Object NAME_PROP = new Object();
+
+ public static void storeWindow(WindowMonitor m, Window w) {
+ SStream ss = SStream.writer();
+ ss.add(m.getX());
+ ss.add(m.getY());
+ ss.add(m.getWidth());
+ ss.add(m.getHeight());
+ if (w instanceof Stage s) {
+ if (s.isIconified()) {
+ ss.add(WINDOW_ICONIFIED);
+ } else if (s.isMaximized()) {
+ ss.add(WINDOW_MAXIMIZED);
+ } else if (s.isFullScreen()) {
+ ss.add(WINDOW_FULLSCREEN);
+ } else {
+ ss.add(WINDOW_NORMAL);
+ }
+ }
+ FxSettings.setStream(PREFIX + m.getID(), ss);
+ }
+
+ public static void restoreWindow(WindowMonitor m, Window win) {
+ SStream ss = FxSettings.getStream(PREFIX + m.getID());
+ if (ss == null) {
+ return;
+ }
+
+ double x = ss.nextDouble(-1);
+ double y = ss.nextDouble(-1);
+ double w = ss.nextDouble(-1);
+ double h = ss.nextDouble(-1);
+ String t = ss.nextString(WINDOW_NORMAL);
+
+ if ((w > 0) && (h > 0)) {
+ if (isValid(x, y)) {
+ win.setX(x);
+ win.setY(y);
+ }
+
+ if (win instanceof Stage s) {
+ if (s.isResizable()) {
+ s.setWidth(w);
+ s.setHeight(h);
+ }
+
+ switch (t) {
+ case WINDOW_FULLSCREEN:
+ s.setFullScreen(true);
+ break;
+ case WINDOW_MAXIMIZED:
+ s.setMaximized(true);
+ break;
+ // TODO iconified?
+ }
+ }
+ }
+ }
+
+ private static boolean isValid(double x, double y) {
+ for (Screen s: Screen.getScreens()) {
+ Rectangle2D r = s.getVisualBounds();
+ if (r.contains(x, y)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static String computeName(Node n) {
+ WindowMonitor m = WindowMonitor.getFor(n);
+ if (m == null) {
+ return null;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ if (collectNames(sb, n)) {
+ return null;
+ }
+
+ String id = m.getID();
+ return id + sb;
+ }
+
+ // returns true if Node should be ignored
+ private static boolean collectNames(StringBuilder sb, Node n) {
+ if (n instanceof MenuBar) {
+ return true;
+ } else if (n instanceof Shape) {
+ return true;
+ } else if (n instanceof ImageView) {
+ return true;
+ }
+
+ Parent p = n.getParent();
+ if (p != null) {
+ if (collectNames(sb, p)) {
+ return true;
+ }
+ }
+
+ String name = getNodeName(n);
+ if (name == null) {
+ return true;
+ }
+
+ sb.append('.');
+ sb.append(name);
+ return false;
+ }
+
+ private static String getNodeName(Node n) {
+ if (n != null) {
+ String name = getName(n);
+ if (name != null) {
+ return name;
+ }
+
+ if (n instanceof Pane) {
+ if (n instanceof AnchorPane) {
+ return "AnchorPane";
+ } else if (n instanceof BorderPane) {
+ return "BorderPane";
+ } else if (n instanceof DialogPane) {
+ return "DialogPane";
+ } else if (n instanceof FlowPane) {
+ return "FlowPane";
+ } else if (n instanceof GridPane) {
+ return "GridPane";
+ } else if (n instanceof HBox) {
+ return "HBox";
+ } else if (n instanceof StackPane) {
+ return "StackPane";
+ } else if (n instanceof TilePane) {
+ return "TilePane";
+ } else if (n instanceof VBox) {
+ return "VBox";
+ } else {
+ return "Pane";
+ }
+ } else if (n instanceof Group) {
+ return "Group";
+ } else if (n instanceof Region) {
+ return "Region";
+ }
+ }
+ return null;
+ }
+
+ public static void storeNode(Node n) {
+ if (n instanceof ListView lv) {
+ storeListView(lv);
+ return;
+ } else if (n instanceof ComboBox cb) {
+ storeComboBox(cb);
+ return;
+ } else if (n instanceof CheckBox cb) {
+ storeCheckBox(cb);
+ return;
+ } else if (n instanceof SplitPane sp) {
+ storeSplitPane(sp);
+ return;
+ } else if (n instanceof ScrollPane sp) {
+ storeNode(sp.getContent());
+ return;
+ }
+
+ if (n instanceof Parent p) {
+ for (Node ch: p.getChildrenUnmodifiable()) {
+ storeNode(ch);
+ }
+ }
+ }
+
+ public static void restoreNode(Node n) {
+ if (checkNoScene(n)) {
+ return;
+ }
+
+ if (n instanceof ListView lv) {
+ restoreListView(lv);
+ } else if (n instanceof ComboBox cb) {
+ restoreComboBox(cb);
+ } else if (n instanceof CheckBox cb) {
+ restoreCheckBox(cb);
+ } else if (n instanceof SplitPane sp) {
+ restoreSplitPane(sp);
+ } else if (n instanceof ScrollPane sp) {
+ restoreNode(sp.getContent());
+ }
+
+ if (n instanceof Parent p) {
+ for (Node ch: p.getChildrenUnmodifiable()) {
+ restoreNode(ch);
+ }
+ }
+ }
+
+ private static void storeSplitPane(SplitPane sp) {
+ double[] div = sp.getDividerPositions();
+ SStream ss = SStream.writer();
+ ss.add(div.length);
+ for (int i = 0; i < div.length; i++) {
+ ss.add(div[i]);
+ }
+ String name = computeName(sp);
+ FxSettings.setStream(PREFIX + name, ss);
+
+ for (Node ch: sp.getItems()) {
+ storeNode(ch);
+ }
+ }
+
+ private static void restoreSplitPane(SplitPane sp) {
+ for (Node ch: sp.getItems()) {
+ restoreNode(ch);
+ }
+
+ /** FIX getting smaller and smaller
+ String name = getName(m, sp);
+ SStream ss = FxSettings.getStream(PREFIX + name);
+ if (ss != null) {
+ int ct = ss.nextInt(-1);
+ if (ct > 0) {
+ for (int i = 0; i < ct; i++) {
+ double div = ss.nextDouble(-1);
+ if (div < 0) {
+ break;
+ }
+ sp.setDividerPosition(i, div);
+ }
+ }
+ }
+ */
+ }
+
+ private static void storeComboBox(ComboBox n) {
+ if (n.getSelectionModel() == null) {
+ return;
+ }
+
+ int ix = n.getSelectionModel().getSelectedIndex();
+ if (ix < 0) {
+ return;
+ }
+
+ String name = computeName(n);
+ if (name == null) {
+ return;
+ }
+
+ FxSettings.setInt(PREFIX + name, ix);
+ }
+
+ // TODO perhaps operate with selection model instead
+ private static void restoreComboBox(ComboBox n) {
+ if (n.getSelectionModel() == null) {
+ return;
+ }
+
+ if (checkNoScene(n)) {
+ return;
+ }
+
+ String name = computeName(n);
+ if (name == null) {
+ return;
+ }
+
+ int ix = FxSettings.getInt(PREFIX + name, -1);
+ if (ix < 0) {
+ return;
+ } else if (ix >= n.getItems().size()) {
+ return;
+ }
+
+ n.getSelectionModel().select(ix);
+ }
+
+ private static boolean checkNoScene(Node node) {
+ if (node == null) {
+ return true;
+ } else if (node.getScene() == null) {
+ // delay restore until node becomes a part of the scene
+ node.sceneProperty().addListener(new ChangeListener() {
+ @Override
+ public void changed(ObservableValue extends Scene> src, Scene old, Scene scene) {
+ if (scene != null) {
+ Window w = scene.getWindow();
+ if (w != null) {
+ node.sceneProperty().removeListener(this);
+ restoreNode(node);
+ }
+ }
+ }
+ });
+ return true;
+ }
+ return false;
+ }
+
+ private static void storeListView(ListView n) {
+ if (n.getSelectionModel() == null) {
+ return;
+ }
+
+ int ix = n.getSelectionModel().getSelectedIndex();
+ if (ix < 0) {
+ return;
+ }
+
+ String name = computeName(n);
+ if (name == null) {
+ return;
+ }
+
+ FxSettings.setInt(PREFIX + name, ix);
+ }
+
+ private static void restoreListView(ListView n) {
+ if (n.getSelectionModel() == null) {
+ return;
+ }
+
+ if (checkNoScene(n)) {
+ return;
+ }
+
+ String name = computeName(n);
+ if (name == null) {
+ return;
+ }
+
+ int ix = FxSettings.getInt(PREFIX + name, -1);
+ if (ix < 0) {
+ return;
+ } else if (ix >= n.getItems().size()) {
+ return;
+ }
+
+ n.getSelectionModel().select(ix);
+ }
+
+ private static void storeCheckBox(CheckBox n) {
+ String name = computeName(n);
+ if (name == null) {
+ return;
+ }
+
+ boolean sel = n.isSelected();
+ FxSettings.setBoolean(PREFIX + name, sel);
+ }
+
+ private static void restoreCheckBox(CheckBox n) {
+ if (checkNoScene(n)) {
+ return;
+ }
+
+ String name = computeName(n);
+ if (name == null) {
+ return;
+ }
+
+ Boolean sel = FxSettings.getBoolean(PREFIX + name);
+ if (sel == null) {
+ return;
+ }
+
+ n.setSelected(sel);
+ }
+
+ /** sets the name for the purposes of storing user preferences */
+ public static void setName(Node n, String name) {
+ n.getProperties().put(NAME_PROP, name);
+ }
+
+ /** sets the name for the purposes of storing user preferences */
+ public static void setName(Window w, String name) {
+ w.getProperties().put(NAME_PROP, name);
+ }
+
+ /**
+ * Returns the name for the purposes of storing user preferences,
+ * set previously by {@link #setName(Node, String)},
+ * or null.
+ */
+ public static String getName(Node n) {
+ if (n != null) {
+ Object x = n.getProperties().get(NAME_PROP);
+ if (x instanceof String s) {
+ return s;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the name for the purposes of storing user preferences,
+ * set previously by {@link #setName(Window, String)},
+ * or null.
+ */
+ public static String getName(Window w) {
+ if (w != null) {
+ Object x = w.getProperties().get(NAME_PROP);
+ if (x instanceof String s) {
+ return s;
+ }
+ }
+ return null;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/ISettingsProvider.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/ISettingsProvider.java
new file mode 100644
index 00000000000..74dd5c0d2b5
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/ISettingsProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+// This code borrows heavily from the following project, with permission from the author:
+// https://github.com/andy-goryachev/FxDock
+package com.oracle.demo.richtext.settings;
+
+import java.io.IOException;
+
+/**
+ * Defines the interface for storing and loading of settings.
+ *
+ * @author Andy Goryachev
+ */
+public interface ISettingsProvider {
+ /**
+ * Loads settings from persistent storage, if needed.
+ * @throws IOException
+ */
+ public void load() throws IOException;
+
+ /**
+ * Saves the settings to persistent media, if needed.
+ * @throws IOException
+ */
+ public void save() throws IOException;
+
+ /**
+ * Sets a key-value pair.
+ */
+ public void set(String key, String value);
+
+ /**
+ * Sets a key-value pair where value is a SStream.
+ */
+ public void set(String key, SStream s);
+
+ /**
+ * Retrieves a String value for the specific key
+ */
+ public String get(String key);
+
+ /**
+ * Retrieves a SStream value for the specific key
+ */
+ public SStream getSStream(String key);
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/SStream.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/SStream.java
new file mode 100644
index 00000000000..6f8851898fc
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/SStream.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.settings;
+
+import java.util.ArrayList;
+
+/**
+ * Represents a string property as a stream of objects.
+ *
+ * @author Andy Goryachev
+ */
+public abstract class SStream {
+
+ public abstract Object[] toArray();
+
+ private SStream() {
+ }
+
+ public static SStream writer() {
+ return new SStream() {
+ private ArrayList items = new ArrayList<>();
+
+ @Override
+ protected void addValue(Object x) {
+ items.add(x);
+ }
+
+ @Override
+ public Object[] toArray() {
+ return items.toArray();
+ }
+ };
+ }
+
+ public static SStream reader(Object[] items) {
+ return new SStream() {
+ int index;
+
+ @Override
+ protected Object nextValue() {
+ if (index >= items.length) {
+ return null;
+ }
+ return items[index++];
+ }
+
+ @Override
+ public Object[] toArray() {
+ return items;
+ }
+ };
+ }
+
+ public void add(int x) {
+ addValue(x);
+ }
+
+ public void add(double x) {
+ addValue(x);
+ }
+
+ public void add(String x) {
+ addValue(x);
+ }
+
+ protected void addValue(Object x) {
+ throw new UnsupportedOperationException();
+ }
+
+ protected Object nextValue() {
+ throw new UnsupportedOperationException();
+ }
+
+ public final String nextString(String defaultValue) {
+ Object v = nextValue();
+ if (v instanceof String s) {
+ return s;
+ }
+ return defaultValue;
+ }
+
+ public final double nextDouble(double defaultValue) {
+ Object v = nextValue();
+ if (v instanceof String s) {
+ try {
+ return Double.parseDouble(s);
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ } else if (v instanceof Double d) {
+ return d;
+ }
+ return defaultValue;
+ }
+
+ public final int nextInt(int defaultValue) {
+ Object v = nextValue();
+ if (v instanceof String s) {
+ try {
+ return Integer.parseInt(s);
+ } catch (NumberFormatException e) {
+ // ignore
+ }
+ } else if (v instanceof Integer d) {
+ return d;
+ }
+ return defaultValue;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(64);
+ sb.append("[");
+ boolean sep = false;
+ for (Object x: toArray()) {
+ if (sep) {
+ sb.append(",");
+ } else {
+ sep = true;
+ }
+ sb.append(x);
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/WindowMonitor.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/WindowMonitor.java
new file mode 100644
index 00000000000..b73f3a223bb
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/settings/WindowMonitor.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+// This code borrows heavily from the following project, with permission from the author:
+// https://github.com/andy-goryachev/FxDock
+package com.oracle.demo.richtext.settings;
+
+import java.util.HashSet;
+import java.util.WeakHashMap;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+
+/**
+ * Stage does not keep track of its normal bounds when minimized, maximized, or switched to full screen.
+ *
+ * @author Andy Goryachev
+ */
+class WindowMonitor {
+ private final String id;
+ private double x;
+ private double y;
+ private double width;
+ private double height;
+ private double x2;
+ private double y2;
+ private double w2;
+ private double h2;
+ private static final WeakHashMap monitors = new WeakHashMap<>(4);
+
+ public WindowMonitor(Window w, String id) {
+ this.id = id;
+
+ x = w.getX();
+ y = w.getY();
+ width = w.getWidth();
+ height = w.getHeight();
+
+ w.xProperty().addListener((p) -> updateX(w));
+ w.yProperty().addListener((p) -> updateY(w));
+ w.widthProperty().addListener((p) -> updateWidth(w));
+ w.heightProperty().addListener((p) -> updateHeight(w));
+
+ if (w instanceof Stage s) {
+ s.iconifiedProperty().addListener((p) -> updateIconified(s));
+ s.maximizedProperty().addListener((p) -> updateMaximized(s));
+ s.fullScreenProperty().addListener((p) -> updateFullScreen(s));
+ }
+ }
+
+ public String getID() {
+ return id;
+ }
+
+ public double getX() {
+ return x;
+ }
+
+ public double getY() {
+ return y;
+ }
+
+ public double getWidth() {
+ return width;
+ }
+
+ public double getHeight() {
+ return height;
+ }
+
+ private void updateX(Window w) {
+ x2 = x;
+ x = w.getX();
+ }
+
+ private void updateY(Window w) {
+ y2 = y;
+ y = w.getY();
+ }
+
+ private void updateWidth(Window w) {
+ w2 = width;
+ width = w.getWidth();
+ }
+
+ private void updateHeight(Window w) {
+ h2 = height;
+ height = w.getHeight();
+ }
+
+ private void updateIconified(Stage s) {
+ if (s.isIconified()) {
+ x = x2;
+ y = y2;
+ }
+ }
+
+ private void updateMaximized(Stage s) {
+ if (s.isMaximized()) {
+ x = x2;
+ y = y2;
+ }
+ }
+
+ private void updateFullScreen(Stage s) {
+ if (s.isFullScreen()) {
+ x = x2;
+ y = y2;
+ width = w2;
+ height = h2;
+ }
+ }
+
+ public static WindowMonitor getFor(Window w) {
+ if (w == null) {
+ return null;
+ }
+ WindowMonitor m = monitors.get(w);
+ if (m == null) {
+ String id = createID(w);
+ if (id == null) {
+ return null;
+ }
+ m = new WindowMonitor(w, id);
+ monitors.put(w, m);
+ }
+ return m;
+ }
+
+ public static WindowMonitor getFor(Node n) {
+ Window w = windowFor(n);
+ if (w != null) {
+ return getFor(w);
+ }
+ return null;
+ }
+
+ private static Window windowFor(Node n) {
+ Scene sc = n.getScene();
+ if (sc != null) {
+ Window w = sc.getWindow();
+ if (w != null) {
+ return w;
+ }
+ }
+ return null;
+ }
+
+ private static String createID(Window win) {
+ String prefix = FxSettingsSchema.getName(win) + ".";
+ HashSet ids = new HashSet<>();
+ for (Window w: Window.getWindows()) {
+ if (w == win) {
+ continue;
+ }
+ WindowMonitor m = monitors.get(w);
+ if (m == null) {
+ return null;
+ }
+ String id = m.getID();
+ if (id.startsWith(prefix)) {
+ ids.add(id);
+ }
+ }
+
+ for (int i = 0; i < 100_000; i++) {
+ String id = prefix + i;
+ if (!ids.contains(id)) {
+ return id;
+ }
+ }
+
+ // safeguard measure
+ throw new Error("cannot create id: too many windows?");
+ }
+
+ public static boolean remove(Window w) {
+ monitors.remove(w);
+ return monitors.size() == 0;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/ExceptionDialog.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/ExceptionDialog.java
new file mode 100644
index 00000000000..075ef05d430
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/ExceptionDialog.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.util;
+
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import javafx.scene.Node;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Priority;
+
+/**
+ * Dialog which shows the exception message and its stack trace.
+ *
+ * @author Andy Goryachev
+ */
+public class ExceptionDialog extends Alert {
+ public ExceptionDialog(Node owner, Throwable err) {
+ super(AlertType.ERROR);
+
+ setTitle("An Error Occurred");
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ err.printStackTrace(pw);
+ String text = sw.toString();
+
+ Label label = new Label("The exception stacktrace:");
+
+ TextArea textArea = new TextArea(text);
+ textArea.setEditable(false);
+ textArea.setWrapText(false);
+
+ textArea.setMaxWidth(Double.MAX_VALUE);
+ textArea.setMaxHeight(Double.MAX_VALUE);
+ GridPane.setVgrow(textArea, Priority.ALWAYS);
+ GridPane.setHgrow(textArea, Priority.ALWAYS);
+
+ GridPane expContent = new GridPane();
+ expContent.setMaxWidth(Double.MAX_VALUE);
+ expContent.add(label, 0, 0);
+ expContent.add(textArea, 0, 1);
+
+ getDialogPane().setExpandableContent(expContent);
+ }
+
+ public void open() {
+ showAndWait();
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/FX.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/FX.java
new file mode 100644
index 00000000000..c6c88835ea0
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/FX.java
@@ -0,0 +1,477 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.util;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import javafx.application.Platform;
+import javafx.css.PseudoClass;
+import javafx.event.EventHandler;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.CheckMenuItem;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Control;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.RadioMenuItem;
+import javafx.scene.control.SeparatorMenuItem;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.control.ToggleGroup;
+import javafx.scene.control.ToolBar;
+import javafx.scene.control.Tooltip;
+import javafx.scene.input.KeyCombination;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import javafx.stage.Window;
+import javafx.util.StringConverter;
+import com.oracle.demo.richtext.settings.FxSettingsSchema;
+
+/**
+ * Shortcuts and convenience methods that perhaps could be added to JavaFX.
+ *
+ * @author Andy Goryachev
+ */
+public class FX {
+ private static StringConverter converter;
+
+ public static Menu menu(MenuBar b, String text) {
+ Menu m = new Menu(text);
+ applyMnemonic(m);
+ b.getMenus().add(m);
+ return m;
+ }
+
+ public static Menu menu(ContextMenu b, String text) {
+ Menu m = new Menu(text);
+ applyMnemonic(m);
+ b.getItems().add(m);
+ return m;
+ }
+
+ public static MenuItem item(MenuBar b, String text, Runnable action) {
+ MenuItem mi = new MenuItem(text);
+ applyMnemonic(mi);
+ mi.setOnAction((ev) -> action.run());
+ lastMenu(b).getItems().add(mi);
+ return mi;
+ }
+
+ public static MenuItem item(MenuBar b, MenuItem mi) {
+ applyMnemonic(mi);
+ lastMenu(b).getItems().add(mi);
+ return mi;
+ }
+
+ public static MenuItem item(MenuBar b, String text) {
+ MenuItem mi = new MenuItem(text);
+ mi.setDisable(true);
+ applyMnemonic(mi);
+ lastMenu(b).getItems().add(mi);
+ return mi;
+ }
+
+ public static MenuItem item(MenuBar b, String text, FxAction a) {
+ MenuItem mi = new MenuItem(text);
+ applyMnemonic(mi);
+ lastMenu(b).getItems().add(mi);
+ a.attach(mi);
+ return mi;
+ }
+
+ public static CheckMenuItem checkItem(MenuBar b, String text, FxAction a) {
+ CheckMenuItem mi = new CheckMenuItem(text);
+ applyMnemonic(mi);
+ lastMenu(b).getItems().add(mi);
+ a.attach(mi);
+ return mi;
+ }
+
+ public static MenuItem item(ContextMenu cm, String text, FxAction a) {
+ MenuItem mi = new MenuItem(text);
+ applyMnemonic(mi);
+ cm.getItems().add(mi);
+ a.attach(mi);
+ return mi;
+ }
+
+ public static MenuItem item(ContextMenu cm, String text) {
+ MenuItem mi = new MenuItem(text);
+ mi.setDisable(true);
+ applyMnemonic(mi);
+ cm.getItems().add(mi);
+ return mi;
+ }
+
+ public static MenuItem item(Menu b, String text) {
+ MenuItem mi = new MenuItem(text);
+ mi.setDisable(true);
+ applyMnemonic(mi);
+ b.getItems().add(mi);
+ return mi;
+ }
+
+ public static MenuItem item(Menu b, String text, Runnable r) {
+ MenuItem mi = new MenuItem(text);
+ mi.setOnAction((ev) -> r.run());
+ applyMnemonic(mi);
+ b.getItems().add(mi);
+ return mi;
+ }
+
+ public static Menu submenu(MenuBar b, String text) {
+ Menu m = new Menu(text);
+ applyMnemonic(m);
+ lastMenu(b).getItems().add(m);
+ return m;
+ }
+
+ private static void applyMnemonic(MenuItem m) {
+ String text = m.getText();
+ if (text != null) {
+ if (text.contains("_")) {
+ m.setMnemonicParsing(true);
+ }
+ }
+ }
+
+ private static Menu lastMenu(MenuBar b) {
+ List ms = b.getMenus();
+ return ms.get(ms.size() - 1);
+ }
+
+ public static SeparatorMenuItem separator(MenuBar b) {
+ SeparatorMenuItem s = new SeparatorMenuItem();
+ lastMenu(b).getItems().add(s);
+ return s;
+ }
+
+ public static SeparatorMenuItem separator(ContextMenu m) {
+ SeparatorMenuItem s = new SeparatorMenuItem();
+ m.getItems().add(s);
+ return s;
+ }
+
+ public static RadioMenuItem radio(MenuBar b, String text, KeyCombination accelerator, ToggleGroup g) {
+ RadioMenuItem mi = new RadioMenuItem(text);
+ mi.setAccelerator(accelerator);
+ mi.setToggleGroup(g);
+ lastMenu(b).getItems().add(mi);
+ return mi;
+ }
+
+ public static CheckMenuItem checkItem(ContextMenu c, String name, boolean selected, Consumer client) {
+ CheckMenuItem m = new CheckMenuItem(name);
+ m.setSelected(selected);
+ m.setOnAction((ev) -> {
+ boolean on = m.isSelected();
+ client.accept(on);
+ });
+ c.getItems().add(m);
+ return m;
+ }
+
+ public static CheckMenuItem checkItem(Menu c, String name, boolean selected, Consumer client) {
+ CheckMenuItem m = new CheckMenuItem(name);
+ m.setSelected(selected);
+ m.setOnAction((ev) -> {
+ boolean on = m.isSelected();
+ client.accept(on);
+ });
+ c.getItems().add(m);
+ return m;
+ }
+
+ public static ToggleButton toggleButton(ToolBar t, String text, FxAction a) {
+ ToggleButton b = new ToggleButton(text);
+ a.attach(b);
+ t.getItems().add(b);
+ return b;
+ }
+
+ public static ToggleButton toggleButton(ToolBar t, String text, String tooltip, FxAction a) {
+ ToggleButton b = new ToggleButton(text);
+ b.setTooltip(new Tooltip(tooltip));
+ a.attach(b);
+ t.getItems().add(b);
+ return b;
+ }
+
+ public static ToggleButton toggleButton(ToolBar t, String text, String tooltip) {
+ ToggleButton b = new ToggleButton(text);
+ b.setTooltip(new Tooltip(tooltip));
+ b.setDisable(true);
+ t.getItems().add(b);
+ return b;
+ }
+
+ public static Button button(ToolBar t, String text, String tooltip, FxAction a) {
+ Button b = new Button(text);
+ b.setTooltip(new Tooltip(tooltip));
+ a.attach(b);
+ t.getItems().add(b);
+ return b;
+ }
+
+ public static Button button(ToolBar t, String text, String tooltip) {
+ Button b = new Button(text);
+ b.setTooltip(new Tooltip(tooltip));
+ b.setDisable(true);
+ t.getItems().add(b);
+ return b;
+ }
+
+ public static N add(ToolBar t, N child) {
+ t.getItems().add(child);
+ return child;
+ }
+
+ public static void space(ToolBar t) {
+ Pane p = new Pane();
+ p.setPrefSize(10, 10);
+ t.getItems().add(p);
+ }
+
+ public static void tooltip(Control c, String text) {
+ c.setTooltip(new Tooltip(text));
+ }
+
+ public static void add(GridPane p, Node n, int col, int row) {
+ p.getChildren().add(n);
+ GridPane.setConstraints(n, col, row);
+ }
+
+ public static void select(ComboBox cb, T value) {
+ cb.getSelectionModel().select(value);
+ }
+
+ public static void selectFirst(ComboBox cb) {
+ cb.getSelectionModel().selectFirst();
+ }
+
+ public static T getSelectedItem(ComboBox cb) {
+ return cb.getSelectionModel().getSelectedItem();
+ }
+
+ public static Window getParentWindow(Object nodeOrWindow) {
+ if (nodeOrWindow == null) {
+ return null;
+ } else if (nodeOrWindow instanceof Window w) {
+ return w;
+ } else if (nodeOrWindow instanceof Node n) {
+ Scene s = n.getScene();
+ if (s != null) {
+ return s.getWindow();
+ }
+ return null;
+ } else {
+ throw new Error("Node or Window only");
+ }
+ }
+
+ /** cascades the window relative to its owner, if any */
+ public static void cascade(Stage w) {
+ if (w != null) {
+ Window p = w.getOwner();
+ if (p != null) {
+ double x = p.getX();
+ double y = p.getY();
+ double off = 20;
+ w.setX(x + off);
+ w.setY(y + off);
+ }
+ }
+ }
+
+ /** adds a name property to the Node for the purposes of storing the preferences */
+ public static void name(Node n, String name) {
+ FxSettingsSchema.setName(n, name);
+ }
+
+ /** adds a name property to the Window for the purposes of storing the preferences */
+ public static void name(Window w, String name) {
+ FxSettingsSchema.setName(w, name);
+ }
+
+ /**
+ * attach a popup menu to a node.
+ * WARNING: sometimes, as the case is with TableView/FxTable header,
+ * the requested node gets created by the skin at some later time.
+ * In this case, additional dance must be performed, see for example
+ * FxTable.setHeaderPopupMenu()
+ */
+ // https://github.com/andy-goryachev/MP3Player/blob/8b0ff12460e19850b783b961f214eacf5e1cdaf8/src/goryachev/fx/FX.java#L1251
+ public static void setPopupMenu(Node owner, Supplier generator) {
+ if (owner == null) {
+ throw new NullPointerException("cannot attach popup menu to null");
+ }
+
+ owner.setOnContextMenuRequested((ev) -> {
+ if (generator != null) {
+ ContextMenu m = generator.get();
+ if (m != null) {
+ if (m.getItems().size() > 0) {
+ Platform.runLater(() -> {
+ // javafx does not dismiss the popup when the user
+ // clicks on the owner node
+ EventHandler li = new EventHandler() {
+ @Override
+ public void handle(MouseEvent event) {
+ m.hide();
+ owner.removeEventFilter(MouseEvent.MOUSE_PRESSED, this);
+ event.consume();
+ }
+ };
+
+ owner.addEventFilter(MouseEvent.MOUSE_PRESSED, li);
+ m.show(owner, ev.getScreenX(), ev.getScreenY());
+ });
+ ev.consume();
+ }
+ }
+ }
+ ev.consume();
+ });
+ }
+
+ /**
+ * Sets opacity (alpha) value.
+ * @param c the initial color
+ * @param opacity the opacity value
+ * @return the new Color with specified opacity
+ */
+ public static Color alpha(Color c, double opacity) {
+ double r = c.getRed();
+ double g = c.getGreen();
+ double b = c.getBlue();
+ return new Color(r, g, b, opacity);
+ }
+
+ /**
+ * Returns the node of type {@code type}, which is either the ancestor or the specified node,
+ * or the specified node itself.
+ * @param the class of Node
+ * @param type the class of Node
+ * @param n the node to look at
+ * @return the ancestor of type N, or null
+ */
+ public static N findParentOf(Class type, Node n) {
+ for (;;) {
+ if (n == null) {
+ return null;
+ } else if (type.isAssignableFrom(n.getClass())) {
+ return (N)n;
+ }
+ n = n.getParent();
+ }
+ }
+
+ /**
+ * Adds the specified style name to the Node's style list.
+ * @param n the node
+ * @param name the style name to add
+ */
+ public static void style(Node n, String name) {
+ if (n != null) {
+ n.getStyleClass().add(name);
+ }
+ }
+
+ /**
+ * Adds or removes the specified style name to the Node's style list.
+ * @param n the node
+ * @param name the style name to add
+ * @param add whether to add or remove the style
+ */
+ public static void style(Node n, String name, boolean add) {
+ if (n != null) {
+ if (add) {
+ n.getStyleClass().add(name);
+ } else {
+ n.getStyleClass().remove(name);
+ }
+ }
+ }
+
+ /**
+ * Adds or removes the specified pseudo class to the Node's style list.
+ * @param n the node
+ * @param name the style name to add
+ * @param on whether to add or remove the pseudo class
+ */
+ public static void style(Node n, PseudoClass name, boolean on) {
+ if (n != null) {
+ n.pseudoClassStateChanged(name, on);
+ }
+ }
+
+ public static Button button(String text, Runnable r) {
+ Button b = new Button(text);
+ if (r == null) {
+ b.setDisable(true);
+ } else {
+ b.setOnAction((ev) -> r.run());
+ }
+ return b;
+ }
+
+ public static StringConverter converter() {
+ if (converter == null) {
+ converter = new StringConverter<>() {
+ @Override
+ public String toString(Object v) {
+ if (v == null) {
+ return null;
+ } else if (v instanceof HasDisplayText t) {
+ return t.toDisplayString();
+ }
+ return v.toString();
+ }
+
+ @Override
+ public Object fromString(String s) {
+ // not supported
+ return null;
+ }
+ };
+ }
+ return (StringConverter)converter;
+ }
+}
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/FxAction.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/FxAction.java
new file mode 100644
index 00000000000..fabde8d6818
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/FxAction.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+// This code borrows heavily from the following project, with permission from the author:
+// https://github.com/andy-goryachev/AppFramework
+package com.oracle.demo.richtext.util;
+
+import java.util.function.Consumer;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+import javafx.scene.control.ButtonBase;
+import javafx.scene.control.CheckMenuItem;
+import javafx.scene.control.ContextMenu;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.RadioMenuItem;
+import javafx.scene.control.ToggleButton;
+
+/**
+ * An AbstractAction equivalent for FX, using method references.
+ *
+ * Usage:
+ *
+ * public final FxAction backAction = new FxAction(this::actionBack);
+ *
+ *
+ * @author Andy Goryachev
+ */
+public class FxAction implements EventHandler {
+ private final SimpleBooleanProperty selectedProperty = new SimpleBooleanProperty();
+ private final SimpleBooleanProperty disabledProperty = new SimpleBooleanProperty();
+ private Runnable onAction;
+ private Consumer onSelected;
+
+ public FxAction(Runnable onAction, Consumer onSelected, boolean enabled) {
+ this.onAction = onAction;
+ this.onSelected = onSelected;
+ setEnabled(enabled);
+
+ if (onSelected != null) {
+ selectedProperty.addListener((src, prev, cur) -> fireSelected(cur));
+ }
+ }
+
+ public FxAction(Runnable onAction, Consumer onSelected) {
+ this(onAction, onSelected, true);
+ }
+
+ public FxAction(Runnable onAction, boolean enabled) {
+ this(onAction, null, enabled);
+ }
+
+ public FxAction(Runnable onAction) {
+ this.onAction = onAction;
+ }
+
+ public FxAction() {
+ }
+
+ public void setOnAction(Runnable r) {
+ onAction = r;
+ }
+
+ protected final void invokeAction() {
+ if (onAction != null) {
+ try {
+ onAction.run();
+ } catch (Throwable e) {
+ //log.error(e);
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public void attach(ButtonBase b) {
+ b.setOnAction(this);
+ b.disableProperty().bind(disabledProperty());
+
+ if (b instanceof ToggleButton) {
+ ((ToggleButton)b).selectedProperty().bindBidirectional(selectedProperty());
+ }
+ }
+
+ public void attach(MenuItem m) {
+ m.setOnAction(this);
+ m.disableProperty().bind(disabledProperty());
+
+ if (m instanceof CheckMenuItem) {
+ ((CheckMenuItem)m).selectedProperty().bindBidirectional(selectedProperty());
+ } else if (m instanceof RadioMenuItem) {
+ ((RadioMenuItem)m).selectedProperty().bindBidirectional(selectedProperty());
+ }
+ }
+
+ public final BooleanProperty selectedProperty() {
+ return selectedProperty;
+ }
+
+ public final boolean isSelected() {
+ return selectedProperty.get();
+ }
+
+ public final void setSelected(boolean on, boolean fire) {
+ if (selectedProperty.get() != on) {
+ selectedProperty.set(on);
+ if (fire) {
+ fire();
+ }
+ }
+ }
+
+ public final BooleanProperty disabledProperty() {
+ return disabledProperty;
+ }
+
+ public final boolean isDisabled() {
+ return disabledProperty.get();
+ }
+
+ public final void setDisabled(boolean on) {
+ disabledProperty.set(on);
+ }
+
+ public final boolean isEnabled() {
+ return !isDisabled();
+ }
+
+ public final void setEnabled(boolean on) {
+ disabledProperty.set(!on);
+ }
+
+ public final void enable() {
+ setEnabled(true);
+ }
+
+ public final void disable() {
+ setEnabled(false);
+ }
+
+ /** fire onAction handler only if this action is enabled */
+ public void fire() {
+ if (isEnabled()) {
+ handle(null);
+ }
+ }
+
+ /** execute an action regardless of whether its enabled or not */
+ public void execute() {
+ try {
+ invokeAction();
+ } catch (Throwable e) {
+ //log.error(e);
+ e.printStackTrace();
+ }
+ }
+
+ protected void fireSelected(boolean on) {
+ try {
+ onSelected.accept(on);
+ } catch (Throwable e) {
+ //log.error(e);
+ e.printStackTrace();
+ }
+ }
+
+ /** override to obtain the ActionEvent */
+ @Override
+ public void handle(ActionEvent ev) {
+ if (isEnabled()) {
+ if (ev != null) {
+ if (ev.getSource() instanceof Menu) {
+ if (ev.getSource() != ev.getTarget()) {
+ // selection of a cascading child menu triggers action event for the parent
+ // for some unknown reason. ignore this.
+ return;
+ }
+ }
+
+ ev.consume();
+ }
+
+ execute();
+
+ // close popup menu, if applicable
+ if (ev != null) {
+ Object src = ev.getSource();
+ if (src instanceof Menu) {
+ ContextMenu p = ((Menu)src).getParentPopup();
+ if (p != null) {
+ p.hide();
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/HasDisplayText.java b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/HasDisplayText.java
new file mode 100644
index 00000000000..0c7176f535e
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/com/oracle/demo/richtext/util/HasDisplayText.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.oracle.demo.richtext.util;
+
+@FunctionalInterface
+public interface HasDisplayText {
+ public String toDisplayString();
+}
diff --git a/apps/samples/RichTextAreaDemo/src/module-info.java b/apps/samples/RichTextAreaDemo/src/module-info.java
new file mode 100644
index 00000000000..266c9b3716f
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/src/module-info.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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.
+ */
+
+/**
+ * RichTextArea Control demos and sample code.
+ *
+ * Incubating Feature.
+ * Will be removed in a future release.
+ *
+ * @moduleGraph
+ */
+
+module RichTextAreaDemo {
+ exports com.oracle.demo.richtext.codearea;
+ exports com.oracle.demo.richtext.editor;
+ exports com.oracle.demo.richtext.notebook;
+ exports com.oracle.demo.richtext.rta;
+
+ requires javafx.base;
+ requires javafx.controls;
+ requires javafx.graphics;
+ requires jfx.incubator.input;
+ requires jfx.incubator.richtext;
+}
diff --git a/apps/samples/RichTextAreaDemo/test/test/com/oracle/demo/richtext/codearea/TestJavaSyntaxDecorator.java b/apps/samples/RichTextAreaDemo/test/test/com/oracle/demo/richtext/codearea/TestJavaSyntaxDecorator.java
new file mode 100644
index 00000000000..239312c1d80
--- /dev/null
+++ b/apps/samples/RichTextAreaDemo/test/test/com/oracle/demo/richtext/codearea/TestJavaSyntaxDecorator.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates.
+ * All rights reserved. Use is subject to license terms.
+ *
+ * This file is available and licensed under the following license:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution.
+ * - Neither the name of Oracle Corporation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * 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 THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS 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 test.com.oracle.demo.richtext.codearea;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import com.oracle.demo.richtext.codearea.JavaSyntaxAnalyzer;
+
+/**
+ * Tests JavaSyntaxDecorator.
+ *
+ * @author Andy Goryachev
+ */
+public class TestJavaSyntaxDecorator {
+ private static final JavaSyntaxAnalyzer.Type H = JavaSyntaxAnalyzer.Type.CHARACTER;
+ private static final JavaSyntaxAnalyzer.Type C = JavaSyntaxAnalyzer.Type.COMMENT;
+ private static final JavaSyntaxAnalyzer.Type K = JavaSyntaxAnalyzer.Type.KEYWORD;
+ private static final JavaSyntaxAnalyzer.Type N = JavaSyntaxAnalyzer.Type.NUMBER;
+ private static final JavaSyntaxAnalyzer.Type T = JavaSyntaxAnalyzer.Type.OTHER;
+ private static final JavaSyntaxAnalyzer.Type S = JavaSyntaxAnalyzer.Type.STRING;
+ private static final Object NL = new Object();
+
+ @Test
+ public void specialCases() {
+ t(T, "print(x);");
+ t(K, "new", T, " StringPropertyBase(", S, "\"\"", T, ") {");
+ t(K, "import", T, " javafx.geometry.BoundingBox;");
+ t(T, "tempState.point.y = ");
+ t(T, "FX.checkItem(m, ", S, "\"1\"", T, " ", K, "new", T, " Insets(", N, "1", T, ").equals(t.getContentPadding()), (on) -> {", NL);
+ t(K, "import", T, " atry.a;");
+ }
+
+ private void someExamplesOfValidCode() {
+ // text block
+ var s = """
+ ---/** */ -- // return ;
+ """ ;
+
+ // numbers
+ double x = .1e15;
+ x = 1.5e2;
+ x = 1e3f;
+ x = 1.1f;
+ x = 5_0.1e2_3;
+ x = 1_000_000;
+ x = -1_000e-1;
+ x = +1_000e+1;
+ x = 1__1e-1_______________________________1;
+ x = 0b10100001010001011010000101000101;
+ x = 0b1010_0001_0100_0_1011_________01000010100010___1;
+ x = 0x0_000__00;
+ }
+
+ @Test
+ public void tests() {
+ // hex
+ t(N, "0x0123456789abcdefL");
+ t(N, "0x00", NL, N, "0x0123456789abcdefL");
+ t(N, "0xeFeF", NL, N, "0x0123__4567__89ab_cdefL");
+
+ // binary
+ t(N, "0b00000");
+ t(N, "0b1010101010L");
+
+ // doubles
+ t(N, "1___2e-3___6");
+ t(N, ".15e2");
+ t(N, "3.141592");
+ t(N, ".12345");
+ t(N, "1.5e2");
+ t(N, "1.5e2_2");
+ t(N, "1.5E-2");
+ t(N, "1_2.5E-2");
+ t(N, ".57E22");
+ t(N, ".75E-5");
+ t(N, "1D");
+ t(N, "1___2e-3___6d");
+ t(N, ".15e2d");
+ t(N, "3.141592d");
+ t(N, ".12345d");
+ t(N, "1.5e2d");
+ t(N, "1.5e2_2d");
+ t(N, "1.5E-2d");
+ t(N, "1_2.5E-2d");
+ t(N, ".57E22d");
+ t(N, ".75E-5d");
+ t(N, "1D", NL, N, "1d", NL, N, "1.1D", NL, N, "1.1d", NL, N, "1.2e-3d", NL, N, "1.2e-3D", NL, N, "1.2E+3d");
+
+ // floats
+ t(N, "1f");
+ t(N, "1___2e-3___6f");
+ t(N, ".15e2f");
+ t(N, "3.141592f");
+ t(N, ".12345f");
+ t(N, "1.5e2f");
+ t(N, "1.5e2_2f");
+ t(N, "1.5E-2f");
+ t(N, "1_2.5E-2f");
+ t(N, ".57E22f");
+ t(N, ".75E-5f");
+ t(N, "1F", NL, N, "1f", NL, N, "1.1F", NL, N, "1.1f", NL, N, "1.2e-3f", NL, N, "1.2e-3F", NL, N, "1.2E+3f");
+
+ // longs
+ t(N, "1L", NL, N, "1l", NL);
+ t(N, "2_2L", NL, N, "2_2l", NL);
+ t(N, "2____2L", NL, N, "2___2l", NL);
+ t(T, "-", N, "99999L", NL);
+ t(T, "5.L");
+
+ // integers
+ t(N, "1");
+ t(N, "1_0");
+ t(N, "1_000_000_000");
+ t(N, "1______000___000_____000");
+ // negative scenarios with integers
+ t(T, "_1");
+ t(T, "1_");
+ t(T, "-", N, "9999");
+
+ // text blocks
+ t(T, "String s =", S, "\"\"\" ", NL, S, " yo /* // */ */ \"\" \" ", NL, S, "a \"\"\" ", T, ";");
+
+ // strings
+ t(T, " ", S, "\"\\\"/*\\\"\"", NL);
+ t(S, "\"\\\"\\\"\\\"\"", T, " {", NL);
+ t(S, "\"abc\"", NL, T, "s = ", S, "\"\"");
+
+ // comments
+ t(T, " ", C, "/* yo", NL, C, "yo yo", NL, C, " */", T, " ");
+ t(T, " ", C, "// yo yo", NL, K, "int", T, " c;");
+ t(C, "/* // yo", NL, C, "// */", T, " ");
+
+ // chars
+ t(H, "'\\b'");
+ t(H, "'\\b'", NL);
+ t(H, "'\\u0000'", NL, H, "'\\uFf9a'", NL);
+ t(H, "'a'", NL, H, "'\\b'", NL, H, "'\\f'", NL, H, "'\\n'", NL, H, "'\\r'", NL);
+ t(H, "'\\''", NL, H, "'\\\"'", NL, H, "'\\\\'", NL);
+
+ // keywords
+ t(K, "package", T, " java.com;", NL);
+ t(K, "import", T, " java.util.ArrayList;", NL);
+ t(K, "import", T, " java.util.ArrayList;", NL, K, "import", T, " java.util.ArrayList;", NL);
+ t(K, "import", T, " com.oracle.demo");
+
+ // misc
+ t(K, "if", T, "(", S, "\"/*\"", T, " == null) {", NL);
+ t(C, "// test", NL, T, "--", NL);
+ t(T, "S_0,");
+ }
+
+ private void t(Object... items) {
+ StringBuilder sb = new StringBuilder();
+ ArrayList expected = new ArrayList<>();
+ JavaSyntaxAnalyzer.Line line = null;
+
+ // builds the input string and the expected result array
+ for (int i = 0; i < items.length; ) {
+ Object x = items[i++];
+ if (x == NL) {
+ sb.append("\n");
+ if (line == null) {
+ line = new JavaSyntaxAnalyzer.Line();
+ }
+ expected.add(line);
+ line = null;
+ } else {
+ JavaSyntaxAnalyzer.Type t = (JavaSyntaxAnalyzer.Type)x;
+ String text = (String)items[i++];
+ if (line == null) {
+ line = new JavaSyntaxAnalyzer.Line();
+ }
+ line.addSegment(t, text);
+ sb.append(text);
+ }
+ }
+
+ if (line != null) {
+ expected.add(line);
+ }
+
+ String input = sb.toString();
+ List res = new JavaSyntaxAnalyzer(input).analyze();
+ Assertions.assertArrayEquals(expected.toArray(), res.toArray());
+ }
+}
diff --git a/build.gradle b/build.gradle
index 3f79ca98512..5043bfc48be 100644
--- a/build.gradle
+++ b/build.gradle
@@ -2824,6 +2824,121 @@ project(":controls") {
//}
// END: incubator placeholder
+project(":incubator.input") {
+ project.ext.buildModule = true
+ project.ext.includeSources = true
+ project.ext.moduleRuntime = true
+ project.ext.moduleName = "jfx.incubator.input"
+ project.ext.incubating = true
+
+ sourceSets {
+ main
+ shims {
+ java {
+ compileClasspath += sourceSets.main.output
+ runtimeClasspath += sourceSets.main.output
+ }
+ }
+ test {
+ java {
+ compileClasspath += sourceSets.shims.output
+ runtimeClasspath += sourceSets.shims.output
+ }
+ }
+ }
+
+ project.ext.moduleSourcePath = defaultModuleSourcePath
+ project.ext.moduleSourcePathShim = defaultModuleSourcePathShim
+
+ commonModuleSetup(project, [
+ 'base',
+ 'graphics',
+ 'controls',
+ 'incubator.input'
+ ])
+
+ dependencies {
+ testImplementation project(":base").sourceSets.test.output
+ testImplementation project(":graphics").sourceSets.test.output
+ testImplementation project(":controls").sourceSets.test.output
+ implementation project(':base')
+ implementation project(':graphics')
+ implementation project(':controls')
+ }
+
+ test {
+ jvmArgs "-Djavafx.toolkit=test.com.sun.javafx.pgstub.StubToolkit"
+ }
+
+ def modulePath = "${project.sourceSets.main.java.getDestinationDirectory().get().getAsFile()}"
+ modulePath += File.pathSeparator + "${rootProject.projectDir}/modules/javafx.controls/build/classes/java/main"
+ modulePath += File.pathSeparator + "${rootProject.projectDir}/modules/javafx.graphics/build/classes/java/main"
+ modulePath += File.pathSeparator + "${rootProject.projectDir}/modules/javafx.base/build/classes/java/main"
+
+ addMavenPublication(project, [ 'graphics' , 'controls'])
+
+ addValidateSourceSets(project, sourceSets)
+}
+
+project(":incubator.richtext") {
+ project.ext.buildModule = true
+ project.ext.includeSources = true
+ project.ext.moduleRuntime = true
+ project.ext.moduleName = "jfx.incubator.richtext"
+ project.ext.incubating = true
+
+ sourceSets {
+ main
+ shims {
+ java {
+ compileClasspath += sourceSets.main.output
+ runtimeClasspath += sourceSets.main.output
+ }
+ }
+ test {
+ java {
+ compileClasspath += sourceSets.shims.output
+ runtimeClasspath += sourceSets.shims.output
+ }
+ }
+ }
+
+ project.ext.moduleSourcePath = defaultModuleSourcePath
+ project.ext.moduleSourcePathShim = defaultModuleSourcePathShim
+
+ commonModuleSetup(project, [
+ 'base',
+ 'graphics',
+ 'controls',
+ 'incubator.input',
+ 'incubator.richtext'
+ ])
+
+ dependencies {
+ testImplementation project(":base").sourceSets.test.output
+ testImplementation project(":graphics").sourceSets.test.output
+ testImplementation project(":controls").sourceSets.test.output
+ testImplementation project(":incubator.input").sourceSets.test.output
+ implementation project(':base')
+ implementation project(':graphics')
+ implementation project(':controls')
+ implementation project(':incubator.input')
+ }
+
+ test {
+ jvmArgs "-Djavafx.toolkit=test.com.sun.javafx.pgstub.StubToolkit"
+ }
+
+ def modulePath = "${project.sourceSets.main.java.getDestinationDirectory().get().getAsFile()}"
+ modulePath += File.pathSeparator + "${rootProject.projectDir}/modules/javafx.controls/build/classes/java/main"
+ modulePath += File.pathSeparator + "${rootProject.projectDir}/modules/javafx.graphics/build/classes/java/main"
+ modulePath += File.pathSeparator + "${rootProject.projectDir}/modules/javafx.base/build/classes/java/main"
+
+ addMavenPublication(project, [ 'graphics' , 'controls'])
+
+ addValidateSourceSets(project, sourceSets)
+}
+
project(":swing") {
// We need to skip setting compiler.options.release for this module,
@@ -3988,6 +4103,8 @@ project(":systemTests") {
// BEGIN: incubator placeholder
//'incubator.mymod',
// END: incubator placeholder
+ 'incubator.input',
+ 'incubator.richtext',
'media',
'jsobject',
'web',
@@ -4443,6 +4560,8 @@ task javadoc(type: Javadoc, dependsOn: createMSPfile) {
// BEGIN: incubator placeholder
//project(":incubator.mymod"),
// END: incubator placeholder
+ project(":incubator.input"),
+ project(":incubator.richtext"),
project(":media"),
project(":swing"),
/*project(":swt"),*/
diff --git a/doc-files/behavior/RichTextAreaBehavior.md b/doc-files/behavior/RichTextAreaBehavior.md
new file mode 100644
index 00000000000..dd8fc5ce070
--- /dev/null
+++ b/doc-files/behavior/RichTextAreaBehavior.md
@@ -0,0 +1,169 @@
+# RichTextArea Behavior
+
+## Function Tags
+
+|Function Tag |Description |
+|:-------------------------|:----------------------------------------------------------------------------|
+|BACKSPACE |Deletes the symbol before the caret
+|COPY |Copies selected text to the clipboard
+|CUT |Cuts selected text and places it to the clipboard
+|DELETE |Deletes the symbol at the caret
+|DELETE_PARAGRAPH |Deletes paragraph at the caret, or selected paragraphs
+|DELETE_PARAGRAPH_START |Deletes text from the caret to paragraph start, ignoring selection
+|DELETE_WORD_NEXT_END |Deletes empty paragraph or text to the end of the next word
+|DELETE_WORD_NEXT_START |Deletes empty paragraph or text to the start of the next word
+|DELETE_WORD_PREVIOUS |Deletes (multiple) empty paragraphs or text to the beginning of the previous word
+|DESELECT |Clears any existing selection by moving anchor to the caret position
+|ERROR_FEEDBACK |Provides audio and/or visual error feedback
+|FOCUS_NEXT |Transfer focus to the next focusable node
+|FOCUS_PREVIOUS |Transfer focus to the previous focusable node
+|INSERT_LINE_BREAK |Inserts a line break at the caret
+|INSERT_TAB |Inserts a tab symbol at the caret (if editable), or transfer focus to the next focusable node
+|MOVE_DOWN |Moves the caret one visual line down
+|MOVE_LEFT |Moves the caret one symbol to the left
+|MOVE_PARAGRAPH_DOWN |Moves the caret to the end of the current paragraph, or, if already there, to the end of the next paragraph
+|MOVE_PARAGRAPH_UP |Moves the caret to the start of the current paragraph, or, if already there, to the start of the previous paragraph
+|MOVE_RIGHT |Moves the caret one symbol to the right
+|MOVE_TO_DOCUMENT_END |Moves the caret to after the last character of the text
+|MOVE_TO_DOCUMENT_START |Moves the caret to before the first character of the text
+|MOVE_TO_LINE_END |Moves the caret to the end of the visual text line at caret
+|MOVE_TO_LINE_START |Moves the caret to the beginning of the visual text line at caret
+|MOVE_TO_PARAGRAPH_END |Moves the caret to the end of the paragraph at caret
+|MOVE_TO_PARAGRAPH_START |Moves the caret to the beginning of the paragraph at caret
+|MOVE_UP |Moves the caret one visual text line up
+|MOVE_WORD_LEFT |Moves the caret one word left (previous word if LTR, next word if RTL)
+|MOVE_WORD_NEXT_END |Moves the caret to the end of the next word
+|MOVE_WORD_NEXT_START |Moves the caret to the start of the next word, or next paragraph if at the start of an empty paragraph
+|MOVE_WORD_PREVIOUS |Moves the caret to the beginning of previous word
+|MOVE_WORD_RIGHT |Moves the caret one word right (next word if LTR, previous word if RTL)
+|PAGE_DOWN |Moves the caret one visual page down
+|PAGE_UP |Moves the caret one visual page up
+|PASTE |Pastes the clipboard content
+|PASTE_PLAIN_TEXT |Pastes the plain text clipboard content
+|REDO |If possible, redoes the last undone modification
+|SELECT_ALL |Selects all text in the document
+|SELECT_DOWN |Extends selection one visual text line down
+|SELECT_LEFT |Extends selection one symbol to the left
+|SELECT_PAGE_DOWN |Extends selection one visible page down
+|SELECT_PAGE_UP |Extends selection one visible page up
+|SELECT_PARAGRAPH |Selects the current paragraph
+|SELECT_PARAGRAPH_DOWN |Extends selection to the end of the current paragraph, or, if already there, to the end of the next paragraph
+|SELECT_PARAGRAPH_END |Extends selection to the paragraph end
+|SELECT_PARAGRAPH_START |Extends selection to the paragraph start
+|SELECT_PARAGRAPH_UP |Extends selection to the start of the current paragraph, or, if already there, to the start of the previous paragraph
+|SELECT_RIGHT |Extends selection one symbol to the right
+|SELECT_TO_DOCUMENT_END |Extends selection to the end of the document
+|SELECT_TO_DOCUMENT_START |Extends selection to the start of the document
+|SELECT_TO_LINE_END |Extends selection to the end of the visual text line at caret
+|SELECT_TO_LINE_START |Extends selection to the start of the visual text line at caret
+|SELECT_UP |Extends selection one visual text line up
+|SELECT_WORD |Selects a word at the caret position
+|SELECT_WORD_LEFT |Extends selection to the previous word (LTR) or next word (RTL)
+|SELECT_WORD_NEXT |Extends selection to the beginning of next word
+|SELECT_WORD_NEXT_END |Extends selection to the end of next word
+|SELECT_WORD_PREVIOUS |Extends selection to the previous word
+|SELECT_WORD_RIGHT |Extends selection to the next word (LTR) or previous word (RTL)
+|UNDO |If possible, undoes the last modification
+
+
+
+## Key Bindings
+
+|Key Combination |Platform |Tag |Notes |
+|:---------------------|:----------|:-------------------------|:-----------------|
+|shortcut-A | |SELECT_ALL |
+|ctrl-BACK_SLASH |linux, win |DESELECT |
+|BACKSPACE | |BACKSPACE |7
+|ctrl-BACKSPACE |linux, win |DELETE_WORD_NEXT_START |
+|option-BACKSPACE |mac |DELETE_WORD_NEXT_START |7
+|shift-BACKSPACE | |BACKSPACE |7
+|shortcut-BACKSPACE |mac |DELETE_PARAGRAPH_START |7, mac only
+|shortcut-C | |COPY |
+|COPY | |COPY |
+|CUT | |CUT |
+|shortcut-D | |DELETE_PARAGRAPH |
+|DELETE | |DELETE |8
+|option-DELETE |mac |DELETE_WORD_NEXT_END |8, option-fn-delete
+|ctrl-DELETE |linux, win |DELETE_WORD_NEXT_START |
+|DOWN | |MOVE_DOWN |
+|ctrl-DOWN |linux, win |MOVE_PARAGRAPH_DOWN |
+|ctrl-shift-DOWN |linux, win |SELECT_PARAGRAPH_DOWN |
+|option-DOWN |mac |MOVE_PARAGRAPH_DOWN |
+|option-shift-DOWN |mac |SELECT_PARAGRAPH_DOWN |
+|shift-DOWN | |SELECT_DOWN |
+|shift-shortcut-DOWN |mac |SELECT_TO_DOCUMENT_END |
+|shortcut-DOWN |mac |MOVE_TO_DOCUMENT_END |
+|END | |MOVE_TO_LINE_END |4
+|ctrl-END | |MOVE_TO_DOCUMENT_END |
+|ctrl-shift-END | |SELECT_TO_DOCUMENT_END |
+|shift-END | |SELECT_LINE_END |
+|ENTER | |INSERT_LINE_BREAK |
+|ctrl-H |linux, win |BACKSPACE |
+|HOME | |MOVE_TO_LINE_START |3
+|ctrl-HOME | |MOVE_TO_DOCUMENT_START |
+|ctrl-shift-HOME | |SELECT_TO_DOCUMENT_START |
+|shift-HOME | |SELECT_TO_LINE_START |3
+|LEFT | |MOVE_LEFT |
+|ctrl-LEFT |linux, win |MOVE_WORD_LEFT |
+|ctrl-shift-LEFT |linux, win |SELECT_WORD_LEFT |
+|option-LEFT |mac |MOVE_WORD_LEFT |
+|option-shift-LEFT |mac |SELECT_WORD_LEFT |
+|shift-LEFT | |SELECT_LEFT |
+|shift-shortcut-LEFT |mac |SELECT_TO_LINE_START |3
+|shortcut-LEFT |mac |MOVE_TO_LINE_START |3
+|PAGE_DOWN | |PAGE_DOWN |6
+|shift-PAGE_DOWN | |SELECT_PAGE_DOWN |6
+|PAGE_UP | |PAGE_UP |5
+|shift-PAGE_UP | |SELECT_PAGE_UP |5
+|PASTE | |PASTE |
+|RIGHT | |MOVE_RIGHT |
+|ctrl-RIGHT |linux, win |MOVE_WORD_RIGHT |
+|ctrl-shift-RIGHT |linux, win |SELECT_WORD_RIGHT |
+|option-RIGHT |mac |MOVE_WORD_RIGHT |
+|option-shift-RIGHT |mac |SELECT_WORD_RIGHT |
+|shift-RIGHT | |SELECT_RIGHT |
+|shift-shortcut-RIGHT |mac |SELECT_LINE_END |
+|shortcut-RIGHT |mac |MOVE_TO_LINE_END |
+|TAB | |TAB |
+|alt-ctrl-shift-TAB |linux, win |FOCUS_NEXT |
+|ctrl-TAB | |FOCUS_NEXT |
+|ctrl-shift-TAB | |FOCUS_PREVIOUS |
+|ctrl-option-shift-TAB |mac |FOCUS_NEXT |
+|shift-TAB | |FOCUS_PREVIOUS |
+|UP | |MOVE_UP |
+|ctrl-UP |linux, win |MOVE_PARAGRAPH_UP |
+|ctrl-shift-UP |linux, win |SELECT_PARAGRAPH_UP |
+|option-UP |mac |MOVE_PARAGRAPH_UP |
+|option-shift-UP |mac |SELECT_PARAGRAPH_UP |
+|shift-UP | |SELECT_UP |
+|shift-shortcut-UP | |SELECT_TO_DOCUMENT_START |
+|shortcut-UP |mac |MOVE_TO_DOCUMENT_START |
+|shortcut-V | |PASTE |
+|shift-shortcut-V | |PASTE_PLAIN_TEXT |
+|shortcut-X | |CUT |
+|ctrl-Y |win |REDO |
+|command-shift-Z |mac |REDO |
+|ctrl-shift-Z |linux |REDO |
+|shortcut-Z | |UNDO |
+
+
+### Other Mappings
+
+The following functions currently have no mapping:
+ERROR_FEEDBACK, MOVE_WORD_NEXT_END, MOVE_WORD_NEXT_START, MOVE_WORD_PREVIOUS, SELECT_WORD_NEXT, SELECT_WORD_NEXT_END, SELECT_WORD_PREVIOUS
+
+The following functions are mapped to the mouse events:
+SELECT_PARAGRAPH, SELECT_WORD
+
+
+
+### Notes:
+
+1. On macOS, `alt` is represented by the `option` key
+2. On macOS, `shortcut` is represented by the `command` key
+3. On macOS, Home is represented by the `command` + left arrow keys
+4. On macOS, End is represented by the `command` + right arrow keys
+5. On macOS, PgUp is represented by the `fn` + `up arrow` keys
+6. On macOS, PgDn is represented by the `fn` + `down arrow` keys
+7. On macOS, BACKSPACE is represented by the `delete` key
+8. On macOS, DELETE is represented by the `fn` + `delete` keys
diff --git a/modules/javafx.base/src/main/java/module-info.java b/modules/javafx.base/src/main/java/module-info.java
index 24d80b17e8e..2505b1b4f86 100644
--- a/modules/javafx.base/src/main/java/module-info.java
+++ b/modules/javafx.base/src/main/java/module-info.java
@@ -52,6 +52,8 @@
// BEGIN: incubator placeholder
//jfx.incubator.mymod,
// END: incubator placeholder
+ jfx.incubator.input,
+ jfx.incubator.richtext,
javafx.graphics,
javafx.fxml,
javafx.media,
diff --git a/modules/javafx.controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css b/modules/javafx.controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css
index 843310327bb..9fa914c5c0e 100644
--- a/modules/javafx.controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css
+++ b/modules/javafx.controls/src/main/resources/com/sun/javafx/scene/control/skin/modena/modena.css
@@ -3438,3 +3438,75 @@ is being used to size a border should also be in pixels.
.alert.warning.dialog-pane {
-fx-graphic: url("dialog-warning.png");
}
+
+/*******************************************************************************
+ * *
+ * Rich Text Area (Incubator) *
+ * *
+ ******************************************************************************/
+
+.rich-text-area {
+ -fx-text-fill: -fx-text-inner-color;
+ -fx-highlight-fill: derive(-fx-control-inner-background,-20%);
+ -fx-highlight-text-fill: -fx-text-inner-color;
+ -fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);
+ -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-cursor: text;
+ -fx-padding: 0;
+ -fx-content-padding: 4 8 4 8;
+}
+
+.rich-text-area:disabled {
+ -fx-opacity: 0.4;
+}
+
+.rich-text-area:focused {
+ -fx-highlight-fill: -fx-accent;
+ -fx-highlight-text-fill: white;
+ -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;
+ -fx-prompt-text-fill: transparent;
+}
+
+.rich-text-area .caret {
+ -fx-stroke: black;
+}
+
+.rich-text-area .caret-line {
+ -fx-stroke: null;
+ -fx-fill: derive(-fx-accent,115%);
+}
+
+.rich-text-area .selection-highlight {
+ -fx-stroke: null;
+ -fx-fill: derive(-fx-accent,70%);
+}
+
+.rich-text-area .left-side {
+ -fx-background-color: rgba(127,127,127,0.1);
+}
+
+.rich-text-area .right-side {
+ -fx-background-color: rgba(127,127,127,0.1);
+}
+
+.rich-text-area .line-number-decorator {
+ -fx-font-family: Monospace;
+ -fx-font-size: 90%;
+ -fx-min-height:1;
+ -fx-pref-height:1;
+ -fx-padding: 0 1 0 1;
+}
+
+.line-number-decorator .text {
+ -fx-bounds-type: visual;
+}
diff --git a/modules/javafx.graphics/src/main/docs/javafx/scene/doc-files/cssref.html b/modules/javafx.graphics/src/main/docs/javafx/scene/doc-files/cssref.html
index 11ae849727b..77508d9020e 100644
--- a/modules/javafx.graphics/src/main/docs/javafx/scene/doc-files/cssref.html
+++ b/modules/javafx.graphics/src/main/docs/javafx/scene/doc-files/cssref.html
@@ -446,6 +446,16 @@ Contents
+ Incubator Modules
+
+ jfx.incubator.scene.control.richtext
+
+
+
+
References
@@ -6372,6 +6382,191 @@ Substructure
chart-vertical-zero-line — Line
chart-horizontal-zero-line — Line
+
+
+
+
+
+
+ jfx.incubator.scene.control.richtext
+
+
+
+
+
+ Style class: rich-text-area
+
+ Available CSS Properties
+
+
+ CSS Property
+ Values
+ Default
+ Comments
+
+
+
+
+ -fx-caret-blink-period
+ <duration>
+ 1000 ms
+ Determines the caret blink period.
+
+
+ -fx-content-padding
+ <size>
+ | <size> <size>
+ <size> <size>
+
+ 0 0 0 0
+ Amount of padding in the content area.
+
+
+ -fx-display-caret
+ <boolean>
+ true
+ Determines whether the caret is displayed.
+
+
+ -fx-highlight-current-paragraph
+ <boolean>
+ false
+ Determines whether the current paragraph is highlighted.
+
+
+ -fx-use-content-height
+ <boolean>
+ false
+ Determines whether the preferred height is the same as the content height.
+
+
+ -fx-use-content-width
+ <boolean>
+ false
+ Determines whether the preferred width is the same as the content width.
+
+
+ -fx-wrap-text
+ <boolean>
+ false
+ Determines whether text should be wrapped.
+
+
+ Also has all properties of Control
+
+
+
+ Pseudo-classes
+
+ Available CSS Pseudo-classes
+
+
+ CSS Pseudo-class
+ Comments
+
+
+
+
+
+ Also has all pseudo‑classes of Control
+
+
+
+ Substructure
+
+ main-pane — Pane
+
+ vflow — Pane
+
+ content — Pane
+
+ caret-line — Path
+ selection-highlight — Path
+ flow — Pane
+ caret — Path
+
+ left-side — Pane
+ right-side — Pane
+
+ scroll-bar:vertical — ScrollBar
+ scroll-bar:horizontal — ScrollBar
+
+
+
+
+ The CodeArea control has all the properties of RichTextArea
+ Style class: code-area
+
+ Available CSS Properties
+
+
+ CSS Property
+ Values
+ Default
+ Comments
+
+
+
+
+ -fx-font
+ <font>
+ Monospaced 12px
+ Determines the font to use for text.
+
+
+ -fx-line-spacing
+ <number>
+ 0
+
+
+
+ -fx-tab-size
+ <integer>
+ 8
+
+
+
+ Also has all properties of RichTextArea
+
+
+
+ Pseudo-classes
+
+ Available CSS Pseudo-classes
+
+
+ CSS Pseudo-class
+ Comments
+
+
+
+
+
+ Also has all pseudo‑classes of RichTextArea
+
+
+
+
[1] CSS 2.1: http://www.w3.org/TR/CSS21/
diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextFlowHelper.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextFlowHelper.java
new file mode 100644
index 00000000000..0cea9b13a22
--- /dev/null
+++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/scene/text/TextFlowHelper.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.javafx.scene.text;
+
+import javafx.scene.text.TextFlow;
+import com.sun.javafx.util.Utils;
+
+/**
+ * Used to access internal methods of TextFlow.
+ */
+public class TextFlowHelper {
+ public interface Accessor {
+ public TextLayout getTextLayout(TextFlow f);
+ }
+
+ private static Accessor accessor;
+
+ static {
+ Utils.forceInit(TextFlow.class);
+ }
+
+ public static void setAccessor(Accessor a) {
+ if (accessor != null) {
+ throw new IllegalStateException();
+ }
+
+ accessor = a;
+ }
+
+ public static TextLayout getTextLayout(TextFlow f) {
+ return accessor.getTextLayout(f);
+ }
+}
diff --git a/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java b/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java
index b317342d081..e70165b6aee 100644
--- a/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java
+++ b/modules/javafx.graphics/src/main/java/javafx/scene/text/TextFlow.java
@@ -29,7 +29,16 @@
import java.util.Collections;
import java.util.List;
import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
+import javafx.css.CssMetaData;
+import javafx.css.Styleable;
+import javafx.css.StyleableDoubleProperty;
+import javafx.css.StyleableIntegerProperty;
+import javafx.css.StyleableObjectProperty;
+import javafx.css.StyleableProperty;
+import javafx.css.converter.EnumConverter;
+import javafx.css.converter.SizeConverter;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.NodeOrientation;
@@ -40,23 +49,15 @@
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.shape.PathElement;
-import javafx.css.StyleableDoubleProperty;
-import javafx.css.StyleableObjectProperty;
-import javafx.css.CssMetaData;
-import javafx.css.converter.EnumConverter;
-import javafx.css.converter.SizeConverter;
import com.sun.javafx.geom.BaseBounds;
import com.sun.javafx.geom.Point2D;
import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.scene.text.GlyphList;
+import com.sun.javafx.scene.text.TextFlowHelper;
import com.sun.javafx.scene.text.TextLayout;
import com.sun.javafx.scene.text.TextLayoutFactory;
import com.sun.javafx.scene.text.TextSpan;
import com.sun.javafx.tk.Toolkit;
-import javafx.beans.property.IntegerProperty;
-import javafx.css.Styleable;
-import javafx.css.StyleableIntegerProperty;
-import javafx.css.StyleableProperty;
/**
* A specialized layout for rich text.
@@ -157,6 +158,7 @@ public class TextFlow extends Pane {
private TextLayout layout;
private boolean needsContent;
private boolean inLayout;
+ static { initAccessor(); }
/**
* Creates an empty TextFlow layout.
@@ -687,4 +689,13 @@ public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object...
default: return super.queryAccessibleAttribute(attribute, parameters);
}
}
+
+ private static void initAccessor() {
+ TextFlowHelper.setAccessor(new TextFlowHelper.Accessor() {
+ @Override
+ public TextLayout getTextLayout(TextFlow f) {
+ return f.getTextLayout();
+ }
+ });
+ }
}
diff --git a/modules/javafx.graphics/src/main/java/module-info.java b/modules/javafx.graphics/src/main/java/module-info.java
index 75bf88efd87..4c31310489b 100644
--- a/modules/javafx.graphics/src/main/java/module-info.java
+++ b/modules/javafx.graphics/src/main/java/module-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -102,6 +102,8 @@
javafx.controls;
exports com.sun.javafx.scene to
javafx.controls,
+ jfx.incubator.input,
+ jfx.incubator.richtext,
javafx.media,
javafx.swing,
javafx.web;
@@ -114,9 +116,12 @@
javafx.web;
exports com.sun.javafx.scene.text to
javafx.controls,
+ jfx.incubator.richtext,
javafx.web;
exports com.sun.javafx.scene.traversal to
javafx.controls,
+ jfx.incubator.input,
+ jfx.incubator.richtext,
javafx.web;
exports com.sun.javafx.sg.prism to
javafx.media,
@@ -135,6 +140,8 @@
exports com.sun.javafx.util to
javafx.controls,
javafx.fxml,
+ jfx.incubator.input,
+ jfx.incubator.richtext,
javafx.media,
javafx.swing,
javafx.web;
diff --git a/modules/jfx.incubator.input/.classpath b/modules/jfx.incubator.input/.classpath
new file mode 100644
index 00000000000..dd2f08f78fb
--- /dev/null
+++ b/modules/jfx.incubator.input/.classpath
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/jfx.incubator.input/.project b/modules/jfx.incubator.input/.project
new file mode 100644
index 00000000000..71b58c0b0b5
--- /dev/null
+++ b/modules/jfx.incubator.input/.project
@@ -0,0 +1,17 @@
+
+
+ incubator.input
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+
+
diff --git a/modules/jfx.incubator.input/.settings/org.eclipse.core.resources.prefs b/modules/jfx.incubator.input/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 00000000000..99f26c0203a
--- /dev/null
+++ b/modules/jfx.incubator.input/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+encoding/=UTF-8
diff --git a/modules/jfx.incubator.input/README.md b/modules/jfx.incubator.input/README.md
new file mode 100644
index 00000000000..80c0c9839f2
--- /dev/null
+++ b/modules/jfx.incubator.input/README.md
@@ -0,0 +1,12 @@
+# InputMap (Incubator)
+
+This project incubates the **InputMap** proposal [0] using the **RichTextArea**/**CodeArea** controls [1]
+as test subjects.
+
+
+
+## References
+
+0. https://github.com/andy-goryachev-oracle/Test/blob/main/doc/InputMap/InputMapV3.md
+
+1. https://github.com/andy-goryachev-oracle/Test/blob/main/doc/RichTextArea/RichTextArea.md
diff --git a/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/BehaviorBase.java b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/BehaviorBase.java
new file mode 100644
index 00000000000..d2a89947947
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/BehaviorBase.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.jfx.incubator.scene.control.input;
+
+import java.util.function.BooleanSupplier;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.TraversalDirection;
+import javafx.scene.control.Control;
+import javafx.scene.input.KeyCode;
+import com.sun.javafx.PlatformUtil;
+import com.sun.jfx.incubator.scene.control.input.SkinInputMap.Stateful;
+import jfx.incubator.scene.control.input.FunctionTag;
+import jfx.incubator.scene.control.input.KeyBinding;
+
+/**
+ * This class provides convenient foundation for custom Control developers intended to simplify writing
+ * stateful behaviors.
+ *
+ * A concrete behavior implementation should do the following:
+ *
+ * provide default behavior methods (one for each function tag)
+ * implement {@link #populateSkinInputMap()} method, in which map control's function tags to
+ * the behavior methods, map key bindings to the function tags, add additional event handlers, using
+ * {@link #registerFunction(FunctionTag, Runnable)},
+ * {@link #registerKey(KeyBinding, FunctionTag)},
+ * {@link #registerKey(KeyCode, FunctionTag)},
+ * and
+ * {@code addHandler()} methods correspondingly.
+ * in the corresponding skin's {code Skin.install()}, set the skin input map to the control's input map.
+ *
+ * Example (in the actual skin class):
+ * {@code
+ * @Override
+ * public void install() {
+ * super.install();
+ * setSkinInputMap(behavior.getSkinInputMap());
+ * }
+ * }
+ *
+ * @param the type of the control
+ */
+public abstract class BehaviorBase {
+ private final C control;
+ private SkinInputMap.Stateful skinInputMap;
+
+ /**
+ * The constructor.
+ * @param c the owner Control instance
+ */
+ public BehaviorBase(C c) {
+ this.control = c;
+ }
+
+ /**
+ * In this method, which is called by {@link javafx.scene.control.Skin#install()},
+ * the child class populates the {@code SkinInputMap}
+ * by registering key mappings and event handlers.
+ *
+ * If a subclass overrides this method, it is important to call the superclass implementation.
+ */
+ protected abstract void populateSkinInputMap();
+
+ /**
+ * Returns the associated Control instance.
+ * @return the owner
+ */
+ protected final C getControl() {
+ return control;
+ }
+
+ /**
+ * Returns the skin input map associated with this behavior.
+ * @return the input map
+ */
+ public final SkinInputMap.Stateful getSkinInputMap() {
+ if (skinInputMap == null) {
+ this.skinInputMap = SkinInputMap.create();
+ populateSkinInputMap();
+ }
+ return skinInputMap;
+ }
+
+ /**
+ * Maps a function to the specified function tag.
+ *
+ * @param tag the function tag
+ * @param function the function
+ */
+ protected final void registerFunction(FunctionTag tag, Runnable function) {
+ getSkinInputMap().registerFunction(tag, function);
+ }
+
+ /**
+ * Maps a function to the specified function tag.
+ *
+ * The event which triggered execution of the function will be consumed if the function returns {@code true}.
+ *
+ * @param tag the function tag
+ * @param function the function
+ */
+ protected final void registerFunction(FunctionTag tag, BooleanSupplier function) {
+ getSkinInputMap().registerFunction(tag, function);
+ }
+
+ /**
+ * Maps a key binding to the specified function tag.
+ * A null key binding will result in no change to this input map.
+ * This method will not override a user mapping.
+ *
+ * @param k the key binding
+ * @param tag the function tag
+ */
+ protected final void registerKey(KeyBinding k, FunctionTag tag) {
+ getSkinInputMap().registerKey(k, tag);
+ }
+
+ /**
+ * Maps a key binding to the specified function tag.
+ * This method will not override a user mapping added by {@link #registerKey(KeyBinding,FunctionTag)}.
+ *
+ * @param code the key code to construct a {@link KeyBinding}
+ * @param tag the function tag
+ */
+ protected final void registerKey(KeyCode code, FunctionTag tag) {
+ getSkinInputMap().registerKey(code, tag);
+ }
+
+ /**
+ * This convenience method maps the function tag to the specified function, and at the same time
+ * maps the specified key binding to that function tag.
+ * @param tag the function tag
+ * @param k the key binding
+ * @param func the function
+ */
+ protected final void register(FunctionTag tag, KeyBinding k, Runnable func) {
+ getSkinInputMap().registerFunction(tag, func);
+ getSkinInputMap().registerKey(k, tag);
+ }
+
+ /**
+ * This convenience method maps the function tag to the specified function, and at the same time
+ * maps the specified key binding to that function tag.
+ *
+ * The event which triggered execution of the function will be consumed if the function returns {@code true}.
+ *
+ * @param tag the function tag
+ * @param k the key binding
+ * @param func the function
+ */
+ protected final void register(FunctionTag tag, KeyBinding k, BooleanSupplier func) {
+ getSkinInputMap().registerFunction(tag, func);
+ getSkinInputMap().registerKey(k, tag);
+ }
+
+ /**
+ * This convenience method maps the function tag to the specified function, and at the same time
+ * maps the specified key binding to that function tag.
+ * @param tag the function tag
+ * @param code the key code
+ * @param func the function
+ */
+ protected final void register(FunctionTag tag, KeyCode code, Runnable func) {
+ getSkinInputMap().registerFunction(tag, func);
+ getSkinInputMap().registerKey(KeyBinding.of(code), tag);
+ }
+
+ /**
+ * This convenience method registers a copy of the behavior-specific mappings from one key binding to another.
+ * The method does nothing if no behavior specific mapping can be found.
+ * @param existing the existing key binding
+ * @param newk the new key binding
+ */
+ protected final void duplicateMapping(KeyBinding existing, KeyBinding newk) {
+ getSkinInputMap().duplicateMapping(existing, newk);
+ }
+
+ /**
+ * Adds an event handler for the specified event type, in the context of this Behavior.
+ *
+ * @param the actual event type
+ * @param type the event type
+ * @param handler the event handler
+ */
+ protected final void addHandler(EventType type, EventHandler handler) {
+ getSkinInputMap().addHandler(type, handler);
+ }
+
+ /**
+ * Adds an event handler for the specific event criteria, in the context of this Behavior.
+ * This is a more specific version of {@link #addHandler(EventType,EventHandler)} method.
+ *
+ * @param the actual event type
+ * @param criteria the matching criteria
+ * @param handler the event handler
+ */
+ protected final void addHandler(EventCriteria criteria, EventHandler handler) {
+ getSkinInputMap().addHandler(criteria, handler);
+ }
+
+ /**
+ * Returns true if this method is invoked on a Linux platform.
+ * @return true on a Linux platform
+ */
+ protected final boolean isLinux() {
+ return PlatformUtil.isLinux();
+ }
+
+ /**
+ * Returns true if this method is invoked on a Mac OS platform.
+ * @return true on a Mac OS platform
+ */
+ protected final boolean isMac() {
+ return PlatformUtil.isMac();
+ }
+
+ /**
+ * Returns true if this method is invoked on a Windows platform.
+ * @return true on a Windows platform
+ */
+ protected final boolean isWindows() {
+ return PlatformUtil.isWindows();
+ }
+
+ /**
+ * Called by any of the BehaviorBase traverse methods to actually effect a
+ * traversal of the focus. The default behavior of this method is to simply
+ * traverse on the given node, passing the given direction. A
+ * subclass may override this method.
+ *
+ * @param dir The direction to traverse
+ */
+ private void traverse(TraversalDirection dir) {
+ control.requestFocusTraversal(dir);
+ }
+
+ /**
+ * Calls the focus traversal engine and indicates that traversal should
+ * go the next focusTraversable Node above the current one.
+ */
+ public final void traverseUp() {
+ traverse(TraversalDirection.UP);
+ }
+
+ /**
+ * Calls the focus traversal engine and indicates that traversal should
+ * go the next focusTraversable Node below the current one.
+ */
+ public final void traverseDown() {
+ traverse(TraversalDirection.DOWN);
+ }
+
+ /**
+ * Calls the focus traversal engine and indicates that traversal should
+ * go the next focusTraversable Node left of the current one.
+ */
+ public final void traverseLeft() {
+ traverse(TraversalDirection.LEFT);
+ }
+
+ /**
+ * Calls the focus traversal engine and indicates that traversal should
+ * go the next focusTraversable Node right of the current one.
+ */
+ public final void traverseRight() {
+ traverse(TraversalDirection.RIGHT);
+ }
+
+ /**
+ * Calls the focus traversal engine and indicates that traversal should
+ * go the next focusTraversable Node in the focus traversal cycle.
+ */
+ public final void traverseNext() {
+ traverse(TraversalDirection.NEXT);
+ }
+
+ /**
+ * Calls the focus traversal engine and indicates that traversal should
+ * go the previous focusTraversable Node in the focus traversal cycle.
+ */
+ public final void traversePrevious() {
+ traverse(TraversalDirection.PREVIOUS);
+ }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/EventCriteria.java b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/EventCriteria.java
new file mode 100644
index 00000000000..c31a5a4eb51
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/EventCriteria.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.jfx.incubator.scene.control.input;
+
+import javafx.event.Event;
+import javafx.event.EventType;
+
+/**
+ * This interface enables wider control in specifying conditional matching logic when adding skin/behavior handlers.
+ *
+ * @param the type of the event
+ */
+public interface EventCriteria {
+ /**
+ * Returns the event type for which this criteria are valid.
+ * @return the event type
+ */
+ public EventType getEventType();
+
+ /**
+ * Returns true if the specified event matches this criteria.
+ * @param ev the event
+ * @return true if match occurs
+ */
+ public boolean isEventAcceptable(T ev);
+}
diff --git a/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/EventHandlerPriority.java b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/EventHandlerPriority.java
new file mode 100644
index 00000000000..33fd579379a
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/EventHandlerPriority.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.jfx.incubator.scene.control.input;
+
+import java.util.Set;
+
+/**
+ * Codifies priority of event handler invocation.
+ */
+public enum EventHandlerPriority {
+ USER_HIGH(6000),
+ USER_KB(5000),
+ SKIN_KB(4000),
+ SKIN_HIGH(3000),
+ SKIN_LOW(2000), // not used, reserved for SkinInputMap.addHandlerLast
+ USER_LOW(1000); // not used, reserved for InputMap.addHandlerLast
+
+ /** set of priorities associated with a {@code Skin} */
+ public static final Set ALL_SKIN = Set.of(
+ SKIN_KB,
+ SKIN_HIGH,
+ SKIN_LOW
+ );
+
+ final int priority;
+
+ private EventHandlerPriority(int priority) {
+ this.priority = priority;
+ }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/InputMapHelper.java b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/InputMapHelper.java
new file mode 100644
index 00000000000..2813b7d9edd
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/InputMapHelper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.jfx.incubator.scene.control.input;
+
+import com.sun.javafx.util.Utils;
+import jfx.incubator.scene.control.input.FunctionTag;
+import jfx.incubator.scene.control.input.InputMap;
+
+/**
+ * Hides execute() methods in InputMap from the public.
+ */
+public class InputMapHelper {
+ public interface Accessor {
+ public void execute(Object source, InputMap inputMap, FunctionTag tag);
+ public void executeDefault(Object source, InputMap inputMap, FunctionTag tag);
+ public void setSkinInputMap(InputMap inputMap, SkinInputMap sm);
+ }
+
+ static {
+ Utils.forceInit(InputMap.class);
+ }
+
+ private static Accessor accessor;
+
+ public static void setAccessor(Accessor a) {
+ if (accessor != null) {
+ throw new IllegalStateException();
+ }
+ accessor = a;
+ }
+
+ public static void execute(Object source, InputMap inputMap, FunctionTag tag) {
+ accessor.execute(source, inputMap, tag);
+ }
+
+ public static void executeDefault(Object source, InputMap inputMap, FunctionTag tag) {
+ accessor.executeDefault(source, inputMap, tag);
+ }
+
+ public static void setSkinInputMap(InputMap inputMap, SkinInputMap sm) {
+ accessor.setSkinInputMap(inputMap, sm);
+ }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/KeyEventMapper.java b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/KeyEventMapper.java
new file mode 100644
index 00000000000..2eb295e716b
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/KeyEventMapper.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.jfx.incubator.scene.control.input;
+
+import javafx.event.EventType;
+import javafx.scene.input.KeyEvent;
+import jfx.incubator.scene.control.input.KeyBinding;
+
+/**
+ * Contains logic for mapping KeyBinding to a specific KeyEvent.
+ */
+public class KeyEventMapper {
+ private static final int PRESSED = 0x01;
+ private static final int RELEASED = 0x02;
+ private static final int TYPED = 0x04;
+
+ private int types;
+
+ public EventType addType(KeyBinding k) {
+ if (k.isKeyPressed()) {
+ types |= PRESSED;
+ return KeyEvent.KEY_PRESSED;
+ } else if (k.isKeyReleased()) {
+ types |= RELEASED;
+ return KeyEvent.KEY_RELEASED;
+ } else {
+ types |= TYPED;
+ return KeyEvent.KEY_TYPED;
+ }
+ }
+
+ public boolean hasKeyPressed() {
+ return (types & PRESSED) != 0;
+ }
+
+ public boolean hasKeyReleased() {
+ return (types & RELEASED) != 0;
+ }
+
+ public boolean hasKeyTyped() {
+ return (types & TYPED) != 0;
+ }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/PHList.java b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/PHList.java
new file mode 100644
index 00000000000..2da2db0246a
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/PHList.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.jfx.incubator.scene.control.input;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+
+/**
+ * Priority Handler List.
+ * Arranges event handlers according to their EventHandlerPriority.
+ */
+public class PHList {
+ /**
+ * {@code items} is a list of {@code EventHandler}s ordered from high priority to low,
+ * with each block of same priority prefixed with the priority value.
+ * Also, USER_KB and SKIN_KB require no handler pointer, so none is added.
+ * Example:
+ * [ USER_HIGH, handler1, handler2, SKIN_KB, SKIN_LOW, handler3 ]
+ */
+ private final ArrayList items = new ArrayList(4);
+
+ @Override
+ public String toString() {
+ return "PHList{items=" + items + "}";
+ }
+
+ /**
+ * Adds the specified priority (always), and the specified handler if not null.
+ * A newly added handler will be inserted after previously added handlers with the same priority.
+ * @param priority the priority
+ * @param handler the handler to add
+ */
+ public void add(EventHandlerPriority priority, EventHandler> handler) {
+ // positive: simply insert the handler there
+ // negative: insert priority and the handler if it's not null
+ int ix = findInsertionIndex(priority);
+ if (ix < 0) {
+ ix = -ix - 1;
+ insert(ix, priority);
+ // do not store the null handler
+ if (handler != null) {
+ insert(++ix, handler);
+ }
+ } else {
+ insert(ix, handler);
+ }
+ }
+
+ private void insert(int ix, Object item) {
+ if (ix < items.size()) {
+ items.add(ix, item);
+ } else {
+ items.add(item);
+ }
+ }
+
+ /**
+ * Removes all the instances of the specified handler. Returns true if the list becomes empty as a result.
+ * Returns true if the list becomes empty as a result of the removal.
+ *
+ * @param the event type
+ * @param handler the handler to remove
+ * @return true when the list becomes empty as a result
+ */
+ public boolean remove(EventHandler handler) {
+ for (int i = 0; i < items.size(); i++) {
+ Object x = items.get(i);
+ if (x == handler) {
+ items.remove(i);
+ if (isNullOrPriority(i) && isNullOrPriority(i - 1)) {
+ // remove priority
+ --i;
+ items.remove(i);
+ }
+ }
+ }
+ return items.isEmpty();
+ }
+
+ private boolean isNullOrPriority(int ix) {
+ if ((ix >= 0) && (ix < items.size())) {
+ Object x = items.get(ix);
+ return (x instanceof EventHandlerPriority);
+ }
+ return true;
+ }
+
+ /**
+ * Returns the index into {@code items}.
+ * When the list contains no elements of the given priority, the return value is
+ * negative, equals to {@code -(insertionIndex + 1)},
+ * and the caller must insert the priority value in addition to the handler.
+ *
+ * @param priority the priority
+ * @return the insertion index (positive), or -(insertionIndex + 1) (negative)
+ */
+ private int findInsertionIndex(EventHandlerPriority priority) {
+ // don't expect many handlers, so linear search is ok
+ int sz = items.size();
+ boolean found = false;
+ for (int i = 0; i < sz; i++) {
+ Object x = items.get(i);
+ if (x instanceof EventHandlerPriority p) {
+ if (p.priority == priority.priority) {
+ found = true;
+ continue;
+ } else if (p.priority < priority.priority) {
+ return found ? i : -(i + 1);
+ }
+ }
+ }
+ return found ? sz : -(sz + 1);
+ }
+
+ /**
+ * A client interface for the {@link #forEach(Client)} method.
+ * @param the event type
+ */
+ @FunctionalInterface
+ public static interface Client {
+ /**
+ * This method gets called for each handler in the order of priority.
+ * The client may signal to stop iterating by returning false from this method.
+ *
+ * @param pri the priority
+ * @param h the handler (can be null)
+ * @return true to continue the process, false to stop
+ */
+ public boolean accept(EventHandlerPriority pri, EventHandler h);
+ }
+
+ /**
+ * Invokes the {@code client} for each handler in the order of priority.
+ * @param the event type
+ * @param client the client reference
+ */
+ public void forEach(Client client) {
+ EventHandlerPriority pri = null;
+ boolean stop;
+ int sz = items.size();
+ for (int i = 0; i < sz; i++) {
+ Object x = items.get(i);
+ if (x instanceof EventHandlerPriority p) {
+ pri = p;
+ if (isNullOrPriority(i + 1)) {
+ stop = !client.accept(pri, null);
+ } else {
+ continue;
+ }
+ } else {
+ // it's a handler, cannot be null
+ stop = !client.accept(pri, (EventHandler)x);
+ }
+ if (stop) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Removes all the entries with the specified priorities.
+ * @return true if list is empty as a result
+ */
+ public boolean removeHandlers(Set priorities) {
+ boolean remove = false;
+ for (int i = 0; i < items.size();) {
+ Object x = items.get(i);
+ if (x instanceof EventHandlerPriority p) {
+ if (priorities.contains(p)) {
+ remove = true;
+ items.remove(i);
+ } else {
+ remove = false;
+ i++;
+ }
+ } else {
+ if (remove) {
+ items.remove(i);
+ } else {
+ i++;
+ }
+ }
+ }
+ return items.isEmpty();
+ }
+
+ /**
+ * An internal testing method.
+ * @param expected the expected internal structure
+ */
+ public void validateInternalState(Object... expected) {
+ if (!Arrays.equals(expected, items.toArray())) {
+ throw new RuntimeException("internal mismatch:\nitems=" + items + "\nexpected=" + List.of(expected));
+ }
+ }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/SkinInputMap.java b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/SkinInputMap.java
new file mode 100644
index 00000000000..e63de160d0c
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/com/sun/jfx/incubator/scene/control/input/SkinInputMap.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.sun.jfx.incubator.scene.control.input;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BooleanSupplier;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.control.Control;
+import javafx.scene.input.KeyCode;
+import jfx.incubator.scene.control.input.FunctionTag;
+import jfx.incubator.scene.control.input.KeyBinding;
+
+/**
+ * The Input Map for use by the Skin.
+ *
+ * Skins whose behavior encapsulates state information must use a Stateful variant obtained with
+ * the {@link #create()} factory method.
+ *
+ * Skins whose behavior requires no state, or when state is fully encapsulated by the Control itself,
+ * could use a Stateless variant obtained with the {@link #createStateless()} method.
+ */
+public abstract sealed class SkinInputMap permits SkinInputMap.Stateful, SkinInputMap.Stateless {
+ /**
+ *
KeyBinding -> FunctionTag
+ * FunctionTag -> Runnable or FunctionHandler
+ * EventType -> PHList
+ */
+ final HashMap map = new HashMap<>();
+ // TODO change to package protected once SkinInputMap is public
+ public final KeyEventMapper kmapper = new KeyEventMapper();
+
+ /**
+ * Creates a skin input map.
+ */
+ public SkinInputMap() {
+ }
+
+ /**
+ * Adds an event handler for the specified event type, in the context of this skin.
+ *
+ * @param the actual event type
+ * @param type the event type
+ * @param handler the event handler
+ */
+ public final void addHandler(EventType type, EventHandler handler) {
+ putHandler(type, EventHandlerPriority.SKIN_HIGH, handler);
+ }
+
+ /**
+ * Adds an event handler for the specific event criteria, in the context of this skin.
+ * This is a more specific version of {@link #addHandler(EventType,EventHandler)} method.
+ *
+ * @param the actual event type
+ * @param criteria the matching criteria
+ * @param handler the event handler
+ */
+ public final void addHandler(EventCriteria criteria, EventHandler handler) {
+ EventType type = criteria.getEventType();
+ putHandler(type, EventHandlerPriority.SKIN_HIGH, new EventHandler() {
+ @Override
+ public void handle(T ev) {
+ if (criteria.isEventAcceptable(ev)) {
+ handler.handle(ev);
+ }
+ }
+ });
+ }
+
+ // adds the specified handler to input map with the given priority
+ // and event type.
+ private void putHandler(EventType type, EventHandlerPriority pri, EventHandler handler) {
+ Object x = map.get(type);
+ PHList hs;
+ if (x instanceof PHList h) {
+ hs = h;
+ } else {
+ hs = new PHList();
+ map.put(type, hs);
+ }
+ hs.add(pri, handler);
+ }
+
+ /**
+ * Maps a key binding to the specified function tag.
+ *
+ * @param k the key binding
+ * @param tag the function tag
+ */
+ public final void registerKey(KeyBinding k, FunctionTag tag) {
+ map.put(k, tag);
+ kmapper.addType(k);
+ }
+
+ /**
+ * Maps a key binding to the specified function tag.
+ *
+ * @param code the key code to construct a {@link KeyBinding}
+ * @param tag the function tag
+ */
+ public final void registerKey(KeyCode code, FunctionTag tag) {
+ registerKey(KeyBinding.of(code), tag);
+ }
+
+ // TODO change to package protected once SkinInputMap is public
+ public Object resolve(KeyBinding k) {
+ return map.get(k);
+ }
+
+ /**
+ * Collects the key bindings mapped by the skin.
+ *
+ * @return a Set of key bindings
+ */
+ public final Set getKeyBindings() {
+ return collectKeyBindings(null, null);
+ }
+
+ /**
+ * Returns the set of key bindings mapped to the specified function tag.
+ * @param tag the function tag
+ * @return the set of KeyBindings
+ */
+ public final Set getKeyBindingsFor(FunctionTag tag) {
+ return collectKeyBindings(null, tag);
+ }
+
+ // TODO change to package protected once SkinInputMap is public
+ public Set collectKeyBindings(Set bindings, FunctionTag tag) {
+ if (bindings == null) {
+ bindings = new HashSet<>();
+ }
+ for (Map.Entry en : map.entrySet()) {
+ if (en.getKey() instanceof KeyBinding k) {
+ if ((tag == null) || (tag == en.getValue())) {
+ bindings.add(k);
+ }
+ }
+ }
+ return bindings;
+ }
+
+ /**
+ * This convenience method registers a copy of the behavior-specific mappings from one key binding to another.
+ * The method does nothing if no behavior specific mapping can be found.
+ * @param existing the existing key binding
+ * @param newk the new key binding
+ */
+ public final void duplicateMapping(KeyBinding existing, KeyBinding newk) {
+ Object x = map.get(existing);
+ if (x != null) {
+ map.put(newk, x);
+ }
+ }
+
+ // TODO change to package protected once SkinInputMap is public
+ public final boolean execute(Object source, FunctionTag tag) {
+ Object x = map.get(tag);
+ if (x instanceof Runnable r) {
+ r.run();
+ return true;
+ } else if (x instanceof BooleanSupplier f) {
+ return f.getAsBoolean();
+ } else if (x instanceof Stateless.FHandler h) {
+ h.handleFunction(source);
+ return true;
+ } else if (x instanceof Stateless.FHandlerConditional h) {
+ return h.handleFunction(source);
+ }
+ return false;
+ }
+
+ // TODO change to package protected once SkinInputMap is public
+ public void unbind(FunctionTag tag) {
+ Iterator> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry en = it.next();
+ if (tag == en.getValue()) {
+ // the entry must be KeyBinding -> FunctionTag
+ it.remove();
+ }
+ }
+ }
+
+ // TODO change to package protected once SkinInputMap is public
+ public void forEach(TriConsumer client) {
+ for (Map.Entry en : map.entrySet()) {
+ if (en.getKey() instanceof EventType type) {
+ PHList hs = (PHList)en.getValue();
+ hs.forEach((pri, h) -> {
+ client.accept(type, pri, h);
+ return true;
+ });
+ }
+ }
+ }
+
+ // TODO change to package protected once SkinInputMap is public
+ @FunctionalInterface
+ public static interface TriConsumer {
+ public void accept(EventType type, EventHandlerPriority pri, EventHandler h);
+ }
+
+ /**
+ * Creates the stateful SkinInputMap.
+ * @return the stateful SkinInputMap
+ */
+ public static SkinInputMap.Stateful create() {
+ return new Stateful();
+ }
+
+ /**
+ * Creates the stateless SkinInputMap.
+ * @param the type of Control
+ * @return the stateless SkinInputMap
+ */
+ public static SkinInputMap.Stateless createStateless() {
+ return new Stateless();
+ }
+
+ /** SkinInputMap for skins that maintain stateful behaviors */
+ public static final class Stateful extends SkinInputMap {
+ Stateful() {
+ }
+
+ /**
+ * Maps a function to the specified function tag.
+ *
+ * @param tag the function tag
+ * @param function the function
+ */
+ public final void registerFunction(FunctionTag tag, Runnable function) {
+ map.put(tag, function);
+ }
+
+ /**
+ * Maps a function to the specified function tag.
+ *
+ * The event which triggered execution of the function will be consumed if the function returns {@code true}.
+ *
+ * @param tag the function tag
+ * @param function the function
+ */
+ public final void registerFunction(FunctionTag tag, BooleanSupplier function) {
+ map.put(tag, function);
+ }
+
+ /**
+ * This convenience method maps the function tag to the specified function, and at the same time
+ * maps the specified key binding to that function tag.
+ * @param tag the function tag
+ * @param k the key binding
+ * @param func the function
+ */
+ public final void register(FunctionTag tag, KeyBinding k, Runnable func) {
+ registerFunction(tag, func);
+ registerKey(k, tag);
+ }
+
+ /**
+ * This convenience method maps the function tag to the specified function, and at the same time
+ * maps the specified key binding to that function tag.
+ * @param tag the function tag
+ * @param code the key code
+ * @param func the function
+ */
+ public final void register(FunctionTag tag, KeyCode code, Runnable func) {
+ registerFunction(tag, func);
+ registerKey(KeyBinding.of(code), tag);
+ }
+ }
+
+ /**
+ * SkinInputMap for skins that either encapsulate the state fully in their Controls,
+ * or don't require a state at all.
+ *
+ * @param the type of Control
+ */
+ // NOTE: The stateless skin input map adds significant complexity to the API surface while providing
+ // limited (some say non-existent) savings in terms of memory. There aren't many Controls that
+ // have a stateless behavior, which further reduces the usefulness of this class.
+ // I'd rather remove this feature altogether.
+ public static final class Stateless extends SkinInputMap {
+ /**
+ * The function handler that always consumes the corresponding event.
+ * @param the type of Control
+ */
+ public interface FHandler {
+ /**
+ * The function mapped to a key binding.
+ * @param control the instance of Control
+ */
+ public void handleFunction(C control);
+ }
+
+ /**
+ * The function handler that allows to control whether the corresponding event will get consumed.
+ * @param the type of Control
+ */
+ public interface FHandlerConditional {
+ /**
+ * The function mapped to a key binding. The return value instructs the owning InputMap
+ * to consume the triggering event or not.
+ * @param control the instance of Control
+ * @return true to consume the event, false otherwise
+ */
+ public boolean handleFunction(C control);
+ }
+
+ Stateless() {
+ }
+
+ /**
+ * Maps a function to the specified function tag.
+ *
+ * @param tag the function tag
+ * @param function the function
+ */
+ public final void registerFunction(FunctionTag tag, FHandler function) {
+ map.put(tag, function);
+ }
+
+ /**
+ * Maps a function to the specified function tag.
+ * This method allows for controlling whether the matching event will be consumed or not.
+ *
+ * @param tag the function tag
+ * @param function the function
+ */
+ public final void registerFunction(FunctionTag tag, FHandlerConditional function) {
+ map.put(tag, function);
+ }
+
+ /**
+ * This convenience method maps the function tag to the specified function, and at the same time
+ * maps the specified key binding to that function tag.
+ * @param tag the function tag
+ * @param k the key binding
+ * @param func the function
+ */
+ public final void register(FunctionTag tag, KeyBinding k, FHandler func) {
+ registerFunction(tag, func);
+ registerKey(k, tag);
+ }
+
+ /**
+ * This convenience method maps the function tag to the specified function, and at the same time
+ * maps the specified key binding to that function tag.
+ * @param tag the function tag
+ * @param code the key code
+ * @param func the function
+ */
+ public final void register(FunctionTag tag, KeyCode code, FHandler func) {
+ registerFunction(tag, func);
+ registerKey(KeyBinding.of(code), tag);
+ }
+ }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/FunctionTag.java b/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/FunctionTag.java
new file mode 100644
index 00000000000..7925231a79b
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/FunctionTag.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jfx.incubator.scene.control.input;
+
+import javafx.scene.control.Control;
+import com.sun.javafx.ModuleUtil;
+
+/**
+ * A function tag is a public identifier of a method that can be mapped to a key binding by the
+ * control's {@link InputMap}.
+ * Example
+ * Example:
+ *
+ * public class RichTextArea extends Control {
+ * public static class Tags {
+ * // Deletes the symbol before the caret.
+ * public static final FunctionTag BACKSPACE = new FunctionTag();
+ * // Copies selected text to the clipboard.
+ * public static final FunctionTag COPY = new FunctionTag();
+ * // Cuts selected text and places it to the clipboard.
+ * public static final FunctionTag CUT = new FunctionTag();
+ * ...
+ *
+ *
+ * @since 24
+ */
+public final class FunctionTag {
+ /** Constructs the function tag. */
+ public FunctionTag() {
+ }
+
+ static { ModuleUtil.incubatorWarning(); }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/InputMap.java b/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/InputMap.java
new file mode 100644
index 00000000000..7926c0d64ca
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/InputMap.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package jfx.incubator.scene.control.input;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BooleanSupplier;
+import javafx.event.Event;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.control.Control;
+import javafx.scene.input.KeyEvent;
+import com.sun.javafx.ModuleUtil;
+import com.sun.jfx.incubator.scene.control.input.EventHandlerPriority;
+import com.sun.jfx.incubator.scene.control.input.InputMapHelper;
+import com.sun.jfx.incubator.scene.control.input.KeyEventMapper;
+import com.sun.jfx.incubator.scene.control.input.PHList;
+import com.sun.jfx.incubator.scene.control.input.SkinInputMap;
+
+/**
+ * InputMap is a property of the {@link Control} class which enables customization
+ * by allowing creation of custom key mappings and event handlers.
+ *
+ * The {@code InputMap} serves as a bridge between the Control and its Skin.
+ * The {@code InputMap} provides an ordered repository of event handlers,
+ * working together with the input map managed by the skin, which
+ * guarantees the order in which handlers are invoked.
+ * It also stores key mappings with a similar guarantee that the application mappings
+ * always take precedence over mappings created by the skin,
+ * regardless of when the skin was created or replaced.
+ *
+ * The class supports the following scenarios:
+ *
+ * Mapping a key binding to a function
+ * Removing a key binding
+ * Mapping a new function to an existing key binding
+ * Retrieving the default function
+ * Ensuring that the application key mappings take priority over mappings created by the skin
+ *
+ * For key mappings, the {@code InputMap} utilizes a two-stage lookup.
+ * First, the key event is matched to a {@link FunctionTag} which identifies a function provided either by the skin
+ * or the associated behavior (the "default" function), or by the application.
+ * When such a mapping exists, the found function tag is matched to a function registered either by
+ * the application or by the skin.
+ *
+ * Additionally, the {@link #register(KeyBinding, Runnable)} method allows mapping to a function directly,
+ * bypassing the function tag.
+ *
+ * This mechanism allows for customizing the key mappings and the underlying functions independently and separately.
+ *
+ * @since 24
+ */
+public final class InputMap {
+ private static final Object NULL = new Object();
+ private final Control control;
+ /**
+ *
KeyBinding -> FunctionTag or Runnable
+ * FunctionTag -> Runnable
+ * EventType -> PHList
+ */
+ private final HashMap map = new HashMap<>();
+ private SkinInputMap skinInputMap;
+ private final KeyEventMapper kmapper = new KeyEventMapper();
+ private final EventHandler eventHandler = this::handleEvent;
+
+ static {
+ ModuleUtil.incubatorWarning();
+ initAccessor();
+ }
+
+ /**
+ * The constructor.
+ * @param control the owner control
+ */
+ public InputMap(Control control) {
+ this.control = control;
+ }
+
+ /**
+ * Adds an event handler for the specified event type.
+ * Event handlers added with this method will always be called before any handlers registered by the skin.
+ *
+ * @param the actual event type
+ * @param type the event type
+ * @param handler the event handler
+ */
+ public void addHandler(EventType type, EventHandler handler) {
+ extendHandler(type, handler, EventHandlerPriority.USER_HIGH);
+ }
+
+ /**
+ * Removes the specified handler.
+ *
+ * @param the event class
+ * @param type the event type
+ * @param handler the handler to remove
+ */
+ public void removeHandler(EventType type, EventHandler handler) {
+ Object x = map.get(type);
+ if (x instanceof PHList hs) {
+ if (hs.remove(handler)) {
+ map.remove(type);
+ control.removeEventHandler(type, eventHandler);
+ }
+ }
+ }
+
+ private void removeHandler(EventType type, EventHandlerPriority pri) {
+ Object x = map.get(type);
+ if (x instanceof PHList hs) {
+ if (hs.removeHandlers(Set.of(pri))) {
+ map.remove(type);
+ control.removeEventHandler(type, eventHandler);
+ }
+ }
+ }
+
+ private void extendHandler(EventType t, EventHandler handler, EventHandlerPriority pri) {
+ Object x = map.get(t);
+ PHList hs;
+ if (x instanceof PHList h) {
+ hs = h;
+ } else {
+ // first entry for this event type
+ hs = new PHList();
+ map.put(t, hs);
+ control.addEventHandler(t, eventHandler);
+ }
+
+ hs.add(pri, handler);
+ }
+
+ private void handleEvent(Event ev) {
+ // probably unnecessary
+ if (ev == null || ev.isConsumed()) {
+ return;
+ }
+
+ EventType> t = ev.getEventType();
+ Object x = map.get(t);
+ if (x instanceof PHList hs) {
+ hs.forEach((pri, h) -> {
+ if (h == null) {
+ handleKeyBindingEvent(ev);
+ } else {
+ h.handle(ev);
+ }
+ return !ev.isConsumed();
+ });
+ }
+ }
+
+ private void handleKeyBindingEvent(Event ev) {
+ // probably unnecessary
+ if (ev == null || ev.isConsumed()) {
+ return;
+ }
+
+ KeyBinding k = KeyBinding.from((KeyEvent)ev);
+ if (k != null) {
+ boolean consume = execute(ev.getSource(), k);
+ if (consume) {
+ ev.consume();
+ }
+ }
+ }
+
+ private boolean execute(Object source, KeyBinding k) {
+ Object x = resolve(k);
+ if (x instanceof FunctionTag tag) {
+ return execute(source, tag);
+ } else if (x instanceof BooleanSupplier h) {
+ return h.getAsBoolean();
+ } else if (x instanceof Runnable r) {
+ r.run();
+ return true;
+ }
+ return false;
+ }
+
+ // package protected to prevent unauthorized code to supply wrong instance of control (source)
+ boolean execute(Object source, FunctionTag tag) {
+ Object x = map.get(tag);
+ if (x instanceof Runnable r) {
+ r.run();
+ return true;
+ }
+
+ return executeDefault(source, tag);
+ }
+
+ // package protected to prevent unauthorized code to supply wrong instance of control (source)
+ boolean executeDefault(Object source, FunctionTag tag) {
+ if (skinInputMap != null) {
+ return skinInputMap.execute(source, tag);
+ }
+ return false;
+ }
+
+ private Object resolve(KeyBinding k) {
+ Object x = map.get(k);
+ if (x != null) {
+ return x;
+ }
+ if (skinInputMap != null) {
+ return skinInputMap.resolve(k);
+ }
+ return null;
+ }
+
+ /**
+ * Registers a function for the given key binding. This mapping will take precedence
+ * over the default mapping set by the skin.
+ *
+ * @param k the key binding
+ * @param function the function
+ */
+ public void register(KeyBinding k, Runnable function) {
+ Objects.requireNonNull(k, "key binding must not be null");
+ Objects.requireNonNull(function, "function must not be null");
+ map.put(k, function);
+ }
+
+ /**
+ * Adds (or overrides) a user-specified function under the given function tag.
+ * This function will take precedence over any default function set by the skin.
+ *
+ * @param tag the function tag
+ * @param function the function
+ */
+ public void registerFunction(FunctionTag tag, Runnable function) {
+ Objects.requireNonNull(tag, "function tag must not be null");
+ Objects.requireNonNull(function, "function must not be null");
+ map.put(tag, function);
+ }
+
+ /**
+ * Link a key binding to the specified function tag.
+ * When the key binding matches the input event, the function is executed, the event is consumed,
+ * and the process of dispatching is stopped.
+ *
+ * This method will take precedence over any default function set by the skin.
+ *
+ * @param k the key binding
+ * @param tag the function tag
+ */
+ public void registerKey(KeyBinding k, FunctionTag tag) {
+ Objects.requireNonNull(k, "KeyBinding must not be null");
+ Objects.requireNonNull(tag, "function tag must not be null");
+ map.put(k, tag);
+
+ EventType t = kmapper.addType(k);
+ extendHandler(t, null, EventHandlerPriority.USER_KB);
+ }
+
+ /**
+ * Disables the specified key binding.
+ * Calling this method will disable any mappings made with
+ * {@link #register(KeyBinding, Runnable)},
+ * {@link #registerKey(KeyBinding, FunctionTag)},
+ * or registered by the skin.
+ *
+ * @param k the key binding
+ */
+ public void disableKeyBinding(KeyBinding k) {
+ map.put(k, NULL);
+ }
+
+ /**
+ * Reverts all the key bindings set by user.
+ * This method restores key bindings set by the skin which were overwritten by the user.
+ */
+ public void resetKeyBindings() {
+ Iterator> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry me = it.next();
+ if (me.getKey() instanceof KeyBinding) {
+ it.remove();
+ }
+ }
+ }
+
+ /**
+ * Restores the specified key binding to the value set by the skin, if any.
+ *
+ * @param k the key binding
+ */
+ public void restoreDefaultKeyBinding(KeyBinding k) {
+ Object x = map.get(k);
+ if (x != null) {
+ map.remove(k);
+ }
+ }
+
+ /**
+ * Restores the specified function tag to the value set by the skin, if any.
+ *
+ * @param tag the function tag
+ */
+ public void restoreDefaultFunction(FunctionTag tag) {
+ Objects.requireNonNull(tag, "function tag must not be null");
+ map.remove(tag);
+ }
+
+ /**
+ * Collects all mapped key bindings.
+ * @return the set of key bindings
+ */
+ public Set getKeyBindings() {
+ return collectKeyBindings(null);
+ }
+
+ /**
+ * Returns the set of key bindings mapped to the specified function tag.
+ * @param tag the function tag
+ * @return the set of KeyBindings, non-null
+ */
+ public Set getKeyBindingsFor(FunctionTag tag) {
+ return collectKeyBindings(tag);
+ }
+
+ // null tag collects all bindings
+ private Set collectKeyBindings(FunctionTag tag) {
+ HashSet bindings = new HashSet<>();
+ for (Map.Entry en : map.entrySet()) {
+ if (en.getKey() instanceof KeyBinding k) {
+ if ((tag == null) || (tag == en.getValue())) {
+ bindings.add(k);
+ }
+ }
+ }
+
+ if (skinInputMap != null) {
+ skinInputMap.collectKeyBindings(bindings, tag);
+ }
+ return bindings;
+ }
+
+ /**
+ * Removes all the key bindings mapped to the specified function tag, either by the application or by the skin.
+ * This is an irreversible operation.
+ * @param tag the function tag
+ */
+ public void removeKeyBindingsFor(FunctionTag tag) {
+ if (skinInputMap != null) {
+ skinInputMap.unbind(tag);
+ }
+ Iterator> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry en = it.next();
+ if (tag == en.getValue()) {
+ // the entry must be KeyBinding -> FunctionTag
+ if (en.getKey() instanceof KeyBinding) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the skin input map, adding necessary event handlers to the control instance when required.
+ * This method must be called by the skin only from its
+ * {@link javafx.scene.control.Skin#install() Skin.install()}
+ * method.
+ *
+ * This method removes all the mappings from the previous skin input map, if any.
+ * @param m the skin input map
+ */
+ // TODO change to public once SkinInputMap is public
+ // or add getSkinInputMap() to Skin.
+ private void setSkinInputMap(SkinInputMap m) {
+ if (skinInputMap != null) {
+ // uninstall all handlers with SKIN_* priority
+ Iterator> it = map.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry en = it.next();
+ if (en.getKey() instanceof EventType t) {
+ PHList hs = (PHList)en.getValue();
+ if (hs.removeHandlers(EventHandlerPriority.ALL_SKIN)) {
+ it.remove();
+ control.removeEventHandler(t, eventHandler);
+ }
+ }
+ }
+ }
+
+ skinInputMap = m;
+
+ if (skinInputMap != null) {
+ // install skin handlers with their priority
+ skinInputMap.forEach((type, pri, h) -> {
+ extendHandler(type, h, pri);
+ });
+
+ // add key bindings listeners if needed
+ if (!kmapper.hasKeyPressed() && skinInputMap.kmapper.hasKeyPressed()) {
+ extendHandler(KeyEvent.KEY_PRESSED, null, EventHandlerPriority.SKIN_KB);
+ }
+ if (!kmapper.hasKeyReleased() && skinInputMap.kmapper.hasKeyReleased()) {
+ extendHandler(KeyEvent.KEY_RELEASED, null, EventHandlerPriority.SKIN_KB);
+ }
+ if (!kmapper.hasKeyTyped() && skinInputMap.kmapper.hasKeyTyped()) {
+ extendHandler(KeyEvent.KEY_TYPED, null, EventHandlerPriority.SKIN_KB);
+ }
+ }
+ }
+
+ private static void initAccessor() {
+ InputMapHelper.setAccessor(new InputMapHelper.Accessor() {
+ // TODO will be unnecessary after JDK-8314968
+ @Override
+ public void executeDefault(Object source, InputMap inputMap, FunctionTag tag) {
+ inputMap.executeDefault(source, tag);
+ }
+
+ // TODO will be unnecessary after JDK-8314968
+ @Override
+ public void execute(Object source, InputMap inputMap, FunctionTag tag) {
+ inputMap.execute(source, tag);
+ }
+
+ // TODO will be unnecessary once SkinInputMap is public
+ @Override
+ public void setSkinInputMap(InputMap inputMap, SkinInputMap sm) {
+ inputMap.setSkinInputMap(sm);
+ }
+ });
+ }
+}
diff --git a/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/KeyBinding.java b/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/KeyBinding.java
new file mode 100644
index 00000000000..59c9ea24735
--- /dev/null
+++ b/modules/jfx.incubator.input/src/main/java/jfx/incubator/scene/control/input/KeyBinding.java
@@ -0,0 +1,651 @@
+/*
+ * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jfx.incubator.scene.control.input;
+
+import java.util.EnumSet;
+import java.util.Objects;
+import javafx.event.EventType;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import com.sun.javafx.PlatformUtil;
+
+/**
+ * This immutable class represents a combination of keys which are used in key mappings.
+ * A key combination consists of a main key and a set of modifier keys.
+ * The main key can be specified by its {@link KeyCode key code}
+ * or key character, the latter must match values returned by {@link KeyEvent#getCharacter()}.
+ * A modifier key is {@code shift}, {@code control}, {@code alt}, {@code meta} or {@code shortcut}.
+ *
+ * This class also provides a set of convenience methods for refering to keys found on macOS platform.
+ *
+ * @since 24
+ */
+public class KeyBinding
+//implements EventCriteria