8000 Add support for embedded images by jperedadnr · Pull Request #91 · gluonhq/rich-text-area · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add support for embedded images #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 35 additions & 7 deletions src/main/java/com/gluonhq/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import com.gluonhq.richtext.action.Action;
import com.gluonhq.richtext.RichTextArea;
import com.gluonhq.richtext.FaceModel;
import com.gluonhq.richtext.model.DecorationModel;
import com.gluonhq.richtext.model.FaceModel;
import com.gluonhq.richtext.model.ImageDecoration;
import com.gluonhq.richtext.model.TextDecoration;
import javafx.application.Application;
import javafx.geometry.Orientation;
Expand All @@ -16,14 +18,17 @@
import javafx.scene.text.Font;
import javafx.scene.text.FontPosture;
import javafx.scene.text.FontWeight;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.kordamp.ikonli.Ikon;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.lineawesome.LineAwesomeSolid;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.LogManager;
Expand All @@ -43,18 +48,22 @@ public class Main extends Application {
}
}

private final List<FaceModel.Decoration> decorations = List.of(
new FaceModel.Decoration(0, 12, TextDecoration.builder().presets().build()),
new FaceModel.Decoration(12, 3, TextDecoration.builder().presets().fontWeight(FontWeight.BOLD).build()),
new FaceModel.Decoration(15, 11, TextDecoration.builder().presets().fontPosture(FontPosture.ITALIC).build()),
new FaceModel.Decoration(26, 15, TextDecoration.builder().presets().foreground(Color.RED).build())
private final List<DecorationModel> decorations = List.of(
new DecorationModel(0, 12, TextDecoration.builder().presets().build()),
new DecorationModel(12, 3, TextDecoration.builder().presets().fontWeight(FontWeight.BOLD).build()),
new DecorationModel(15, 1, new ImageDecoration(Main.class.getResource("gluon_logo-150x150@2x.png").toURI().toString(), 32, 32)),
new DecorationModel(16, 11, TextDecoration.builder().presets().fontPosture(FontPosture.ITALIC).build()),
new DecorationModel(27, 15, TextDecoration.builder().presets().foreground(Color.RED).build())
);

private final FaceModel faceModel = new FaceModel("Simple text one two three\nExtra line text", decorations, 41);
private final FaceModel faceModel = new FaceModel("Simple text one\u200b two three\nExtra line text", decorations, 42);

private final Label textLengthLabel = new Label();
private final RichTextArea editor = new RichTextArea();

public Main() throws URISyntaxException {
}

@Override
public void start(Stage stage) {

Expand Down Expand Up @@ -111,6 +120,8 @@ public Double fromString(String s) {
actionButton(LineAwesomeSolid.UNDO, editor.getActionFactory().undo()),
actionButton(LineAwesomeSolid.REDO, editor.getActionFactory().redo()),
new Separator(Orientation.VERTICAL),
actionImage(LineAwesomeSolid.IMAGE),
new Separator(Orientation.VERTICAL),
fontFamilies,
fontSize,
actionButton(LineAwesomeSolid.BOLD, editor.getActionFactory().decorate(TextDecoration.builder().fontWeight(FontWeight.BOLD).build())),
Expand Down Expand Up @@ -167,6 +178,23 @@ private Button actionButton(Ikon ikon, Action action) {
return button;
}

private Button actionImage(Ikon ikon) {
Button button = new Button();
FontIcon icon = new FontIcon(ikon);
icon.setIconSize(20);
button.setGraphic(icon);
button.setOnAction(e -> {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image", "*.png", ".jpeg", ".gif"));
File file = fileChooser.showOpenDialog(button.getScene().getWindow());
if (file != null) {
String url = file.toURI().toString();
editor.getActionFactory().decorate(new ImageDecoration(url)).execute(e);
}
});
return button;
}

private MenuItem actionMenuItem(String text, Ikon ikon, Action action) {
FontIcon icon = new FontIcon(ikon);
icon.setIconSize(16);
Expand Down
68 changes: 0 additions & 68 deletions src/main/java/com/gluonhq/richtext/FaceModel.java

This file was deleted.

1 change: 1 addition & 0 deletions src/main/java/com/gluonhq/richtext/RichTextArea.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.gluonhq.richtext;

import com.gluonhq.richtext.action.ActionFactory;
import com.gluonhq.richtext.model.FaceModel;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
Expand Down
84 changes: 66 additions & 18 deletions src/main/java/com/gluonhq/richtext/RichTextAreaSkin.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.gluonhq.richtext;

import com.gluonhq.richtext.model.FaceModel;
import com.gluonhq.richtext.model.ImageDecoration;
import com.gluonhq.richtext.model.PieceTable;
import com.gluonhq.richtext.model.TextBuffer;
import com.gluonhq.richtext.model.TextDecoration;
Expand Down Expand Up @@ -29,6 +31,8 @@
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
Expand All @@ -48,7 +52,6 @@
import javafx.util.Duration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -118,7 +121,9 @@ interface ActionBuilder extends Function<KeyEvent, ActionCmd>{}
);

private final Map<Integer, Font> fontCache = new ConcurrentHashMap<>();
private final SmartTimer fontCacheEvictionTimer = new SmartTimer( this::evictUnusedFonts, 1000, 60000);
private final Map<String, Image> imageCache = new ConcurrentHashMap<>();
private final SmartTimer fontCacheEvictionTimer = new SmartTimer(this::evictUnusedFonts, 1000, 60000);
private final SmartTimer imageCacheEvictionTimer = new SmartTimer(this::evictUnusedImages, 1000, 60000);

private final Consumer<TextBuffer.Event> textChangeListener = e -> refreshTextFlow();
private final ChangeListener<Boolean> focusChangeListener;
Expand All @@ -136,14 +141,14 @@ interface ActionBuilder extends Function<KeyEvent, ActionCmd>{}
private final DoubleBinding prefHeightBinding;
private final ChangeListener<Number> textFlowPrefWidthListener = (obs, ov, nv) -> {
refreshTextFlow();
updateSelection(viewModel.getSelection());
updateCaretPosition(viewModel.getCaretPosition());
requestLayout();
};
private double textFlowLayoutX = 0d, textFlowLayoutY = 0d;
private final ChangeListener<Insets> insetsChangeListener = (obs, ov, nv) -> {
textFlowLayoutX = nv.getLeft();
textFlowLayoutY = nv.getTop();
};
private int nonTextNodesCount;

protected RichTextAreaSkin(final RichTextArea control) {
super(control);
Expand Down Expand Up @@ -264,28 +269,38 @@ private void setup(FaceModel faceModel) {
// For now rebuilding the whole text flow
private void refreshTextFlow() {
fontCacheEvictionTimer.pause();
imageCacheEvictionTimer.pause();
try {
var fragments = new ArrayList<Text>();
var fragments = new ArrayList<Node>();
var backgroundIndexRanges = new ArrayList<IndexRangeColor>();
var length = new AtomicInteger();
var nonTextNodes = new AtomicInteger();
viewModel.walkFragments((text, decoration) -> {
final Text textNode = buildText(text, decoration);
fragments.add(textNode);

if (decoration.getBackground() != Color.TRANSPARENT) {
final IndexRangeColor indexRangeColor = new IndexRangeColor(
length.get(),
length.get() + textNode.getText().length(),
decoration.getBackground()
);
backgroundIndexRanges.add(indexRangeColor);
if (decoration instanceof TextDecoration) {
final Text textNode = buildText(text, (TextDecoration) decoration);
fragments.add(textNode);
Color background = ((TextDecoration) decoration).getBackground();
if (background != Color.TRANSPARENT) {
backgroundIndexRanges.add(new IndexRangeColor(
length.get(), length.get() + text.length(), background));
}
length.addAndGet(text.length());
} else if (decoration instanceof ImageDecoration) {
fragments.add(buildImage((ImageDecoration) decoration));
length.incrementAndGet();
nonTextNodes.incrementAndGet();
}
length.addAndGet(textNode.getText().length());
});
textFlow.getChildren().setAll(fragments);
addBackgroundPathsToLayers(backgroundIndexRanges);
if (nonTextNodesCount != nonTextNodes.get()) {
requestLayout();
nonTextNodesCount = nonTextNodes.get();
}
getSkinnable().requestFocus();
} finally {
fontCacheEvictionTimer.start();
imageCacheEvictionTimer.start();
}
}

Expand Down Expand Up @@ -320,7 +335,7 @@ private Text buildText(String content, TextDecoration decoration ) {
decoration.getFontPosture(),
decoration.getFontSize());

Font font = fontCache.computeIfAbsent( hash,
Font font = fontCache.computeIfAbsent(hash,
h -> Font.font(
decoration.getFontFamily(),
decoration.getFontWeight(),
Expand All @@ -331,18 +346,45 @@ private Text buildText(String content, TextDecoration decoration ) {
return text;
}

private ImageView buildImage(ImageDecoration imageDecoration) {
Image image = imageCache.computeIfAbsent(imageDecoration.getUrl(), Image::new);
final ImageView imageView = new ImageView(image);
// TODO Create resizable ImageView
if (imageDecoration.getWidth() > -1 && imageDecoration.getHeight() > -1) {
imageView.setFitWidth(imageDecoration.getWidth());
imageView.setFitHeight(imageDecoration.getHeight());
}
// TODO Clip imageView if wider than contentArea
if (imageDecoration.getLink() != null) {
// TODO Add action to open link on mouseClick
}
return imageView;
}

private void evictUnusedFonts() {
Set<Font> usedFonts = textFlow.getChildren()
.stream()
.filter(Text.class::isInstance)
.map( t -> ((Text)t).getFont())
.map(t -> ((Text) t).getFont())
.filter(Objects::nonNull)
.collect(Collectors.toSet());
List<Font> cachedFonts = new ArrayList<>(fontCache.values());
cachedFonts.removeAll(usedFonts);
fontCache.values().removeAll(cachedFonts);
}

private void evictUnusedImages() {
Set<Image> usedImages = textFlow.getChildren()
.stream()
.filter(ImageView.class::isInstance)
.map(t -> ((ImageView) t).getImage())
.filter(Objects::nonNull)
.collect(Collectors.toSet());
List<Image> cachedImages = new ArrayList<>(imageCache.values());
cachedImages.removeAll(usedImages);
imageCache.values().removeAll(cachedImages);
}

private void editableChangeListener(Observable o) {
boolean editable = getSkinnable().isEditable();
viewModel.setEditable(editable);
Expand Down Expand Up @@ -391,6 +433,12 @@ private void setCaretVisibility(boolean on) {
}
}

private void requestLayout() {
updateSelection(viewModel.getSelection());
updateCaretPosition(viewModel.getCaretPosition());
getSkinnable().requestLayout();
}

private int dragStart = -1;

private void mousePressedListener(MouseEvent e) {
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/com/gluonhq/richtext/action/ActionFactory.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.gluonhq.richtext.action;

import com.gluonhq.richtext.RichTextArea;
import com.gluonhq.richtext.model.Decoration;
import com.gluonhq.richtext.model.ImageDecoration;
import com.gluonhq.richtext.model.TextDecoration;
import com.gluonhq.richtext.viewmodel.ActionCmdFactory;

Expand Down Expand Up @@ -53,7 +55,14 @@ public Action paste() {
return paste;
}

public Action decorate(TextDecoration decoration) {
return new BasicAction(control, action -> ACTION_CMD_FACTORY.decorateText(decoration));
public Action decorate(Decoration decoration) {
return new BasicAction(control, action -> {
if (decoration instanceof TextDecoration) {
return ACTION_CMD_FACTORY.decorateText((TextDecoration) decoration);
} else if (decoration instanceof ImageDecoration) {
return ACTION_CMD_FACTORY.decorateImage((ImageDecoration) decoration);
}
throw new IllegalArgumentException("Decoration type not supported: " + decoration);
});
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/gluonhq/richtext/action/BasicAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.gluonhq.richtext.RichTextAreaSkin;
import com.gluonhq.richtext.viewmodel.ActionCmd;
import com.gluonhq.richtext.viewmodel.RichTextAreaViewModel;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.binding.BooleanBinding;
Expand Down Expand Up @@ -57,7 +58,7 @@ private ActionCmd getActionCmd() {

@Override
public void execute(ActionEvent event) {
getActionCmd().apply(viewModel);
Platform.runLater(() -> getActionCmd().apply(viewModel));
}

@Override
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/gluonhq/richtext/model/Decoration.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gluonhq.richtext.model;

public interface Decoration {

}
Loading
0