Пример #1
0
 private void toggleStrikethrough() {
   updateStyleInSelection(
       spans ->
           StyleInfo.EMPTY.updateStrikethrough(
               !spans.styleStream().allMatch(style -> style.strikethrough.orElse(false))));
 }
Пример #2
0
 private void toggleUnderline() {
   updateStyleInSelection(
       spans ->
           StyleInfo.EMPTY.updateUnderline(
               !spans.styleStream().allMatch(style -> style.underline.orElse(false))));
 }
Пример #3
0
 private void toggleBold() {
   updateStyleInSelection(
       spans ->
           StyleInfo.EMPTY.updateBold(
               !spans.styleStream().allMatch(style -> style.bold.orElse(false))));
 }
Пример #4
0
 private void toggleItalic() {
   updateStyleInSelection(
       spans ->
           StyleInfo.EMPTY.updateItalic(
               !spans.styleStream().allMatch(style -> style.italic.orElse(false))));
 }
Пример #5
0
public class RichText extends Application {

  private static class StyleInfo {

    public static final StyleInfo EMPTY = new StyleInfo();

    public static final Codec<StyleInfo> CODEC =
        new Codec<StyleInfo>() {

          private final Codec<Color> COLOR_CODEC =
              new Codec<Color>() {

                @Override
                public String getName() {
                  return "color";
                }

                @Override
                public void encode(DataOutputStream os, Color c) throws IOException {
                  os.writeDouble(c.getRed());
                  os.writeDouble(c.getGreen());
                  os.writeDouble(c.getBlue());
                  os.writeDouble(c.getOpacity());
                }

                @Override
                public Color decode(DataInputStream is) throws IOException {
                  return Color.color(
                      is.readDouble(), is.readDouble(), is.readDouble(), is.readDouble());
                }
              };

          @Override
          public String getName() {
            return "style-info";
          }

          @Override
          public void encode(DataOutputStream os, StyleInfo s) throws IOException {
            os.writeByte(encodeBoldItalicUnderlineStrikethrough(s));
            os.writeInt(encodeOptionalUint(s.fontSize));
            encodeOptional(os, s.fontFamily, Codec.STRING_CODEC);
            encodeOptional(os, s.textColor, COLOR_CODEC);
            encodeOptional(os, s.backgroundColor, COLOR_CODEC);
          }

          @Override
          public StyleInfo decode(DataInputStream is) throws IOException {
            byte bius = is.readByte();
            Optional<Integer> fontSize = decodeOptionalUint(is.readInt());
            Optional<String> fontFamily = decodeOptional(is, Codec.STRING_CODEC);
            Optional<Color> textColor = decodeOptional(is, COLOR_CODEC);
            Optional<Color> bgrColor = decodeOptional(is, COLOR_CODEC);
            return new StyleInfo(
                bold(bius),
                italic(bius),
                underline(bius),
                strikethrough(bius),
                fontSize,
                fontFamily,
                textColor,
                bgrColor);
          }

          private int encodeBoldItalicUnderlineStrikethrough(StyleInfo s) {
            return encodeOptionalBoolean(s.bold) << 6
                | encodeOptionalBoolean(s.italic) << 4
                | encodeOptionalBoolean(s.underline) << 2
                | encodeOptionalBoolean(s.strikethrough);
          }

          private Optional<Boolean> bold(byte bius) throws IOException {
            return decodeOptionalBoolean((bius >> 6) & 3);
          }

          private Optional<Boolean> italic(byte bius) throws IOException {
            return decodeOptionalBoolean((bius >> 4) & 3);
          }

          private Optional<Boolean> underline(byte bius) throws IOException {
            return decodeOptionalBoolean((bius >> 2) & 3);
          }

          private Optional<Boolean> strikethrough(byte bius) throws IOException {
            return decodeOptionalBoolean((bius >> 0) & 3);
          }

          private int encodeOptionalBoolean(Optional<Boolean> ob) {
            return ob.map(b -> 2 + (b ? 1 : 0)).orElse(0);
          }

          private Optional<Boolean> decodeOptionalBoolean(int i) throws IOException {
            switch (i) {
              case 0:
                return Optional.empty();
              case 2:
                return Optional.of(false);
              case 3:
                return Optional.of(true);
            }
            throw new MalformedInputException(0);
          }

          private int encodeOptionalUint(Optional<Integer> oi) {
            return oi.orElse(-1);
          }

          private Optional<Integer> decodeOptionalUint(int i) {
            return (i < 0) ? Optional.empty() : Optional.of(i);
          }

          private <T> void encodeOptional(DataOutputStream os, Optional<T> ot, Codec<T> codec)
              throws IOException {
            if (ot.isPresent()) {
              os.writeBoolean(true);
              codec.encode(os, ot.get());
            } else {
              os.writeBoolean(false);
            }
          }

          private <T> Optional<T> decodeOptional(DataInputStream is, Codec<T> codec)
              throws IOException {
            return is.readBoolean() ? Optional.of(codec.decode(is)) : Optional.empty();
          }
        };

    public static StyleInfo fontSize(int fontSize) {
      return EMPTY.updateFontSize(fontSize);
    }

    public static StyleInfo fontFamily(String family) {
      return EMPTY.updateFontFamily(family);
    }

    public static StyleInfo textColor(Color color) {
      return EMPTY.updateTextColor(color);
    }

    public static StyleInfo backgroundColor(Color color) {
      return EMPTY.updateBackgroundColor(color);
    }

    private static String cssColor(Color color) {
      int red = (int) (color.getRed() * 255);
      int green = (int) (color.getGreen() * 255);
      int blue = (int) (color.getBlue() * 255);
      return "rgb(" + red + ", " + green + ", " + blue + ")";
    }

    final Optional<Boolean> bold;
    final Optional<Boolean> italic;
    final Optional<Boolean> underline;
    final Optional<Boolean> strikethrough;
    final Optional<Integer> fontSize;
    final Optional<String> fontFamily;
    final Optional<Color> textColor;
    final Optional<Color> backgroundColor;

    public StyleInfo() {
      bold = Optional.empty();
      italic = Optional.empty();
      underline = Optional.empty();
      strikethrough = Optional.empty();
      fontSize = Optional.empty();
      fontFamily = Optional.empty();
      textColor = Optional.empty();
      backgroundColor = Optional.empty();
    }

    public StyleInfo(
        Optional<Boolean> bold,
        Optional<Boolean> italic,
        Optional<Boolean> underline,
        Optional<Boolean> strikethrough,
        Optional<Integer> fontSize,
        Optional<String> fontFamily,
        Optional<Color> textColor,
        Optional<Color> backgroundColor) {
      this.bold = bold;
      this.italic = italic;
      this.underline = underline;
      this.strikethrough = strikethrough;
      this.fontSize = fontSize;
      this.fontFamily = fontFamily;
      this.textColor = textColor;
      this.backgroundColor = backgroundColor;
    }

    @Override
    public int hashCode() {
      return Objects.hash(
          bold, italic, underline, strikethrough, fontSize, fontFamily, textColor, backgroundColor);
    }

    @Override
    public boolean equals(Object other) {
      if (other instanceof StyleInfo) {
        StyleInfo that = (StyleInfo) other;
        return Objects.equals(this.bold, that.bold)
            && Objects.equals(this.italic, that.italic)
            && Objects.equals(this.underline, that.underline)
            && Objects.equals(this.strikethrough, that.strikethrough)
            && Objects.equals(this.fontSize, that.fontSize)
            && Objects.equals(this.fontFamily, that.fontFamily)
            && Objects.equals(this.textColor, that.textColor)
            && Objects.equals(this.backgroundColor, that.backgroundColor);
      } else {
        return false;
      }
    }

    public String toCss() {
      StringBuilder sb = new StringBuilder();

      if (bold.isPresent()) {
        if (bold.get()) {
          sb.append("-fx-font-weight: bold;");
        } else {
          sb.append("-fx-font-weight: normal;");
        }
      }

      if (italic.isPresent()) {
        if (italic.get()) {
          sb.append("-fx-font-style: italic;");
        } else {
          sb.append("-fx-font-style: normal;");
        }
      }

      if (underline.isPresent()) {
        if (underline.get()) {
          sb.append("-fx-underline: true;");
        } else {
          sb.append("-fx-underline: false;");
        }
      }

      if (strikethrough.isPresent()) {
        if (strikethrough.get()) {
          sb.append("-fx-strikethrough: true;");
        } else {
          sb.append("-fx-strikethrough: false;");
        }
      }

      if (fontSize.isPresent()) {
        sb.append("-fx-font-size: " + fontSize.get() + "pt;");
      }

      if (fontFamily.isPresent()) {
        sb.append("-fx-font-family: " + fontFamily.get() + ";");
      }

      if (textColor.isPresent()) {
        Color color = textColor.get();
        sb.append("-fx-fill: " + cssColor(color) + ";");
      }

      if (backgroundColor.isPresent()) {
        Color color = backgroundColor.get();
        sb.append("-fx-background-fill: " + cssColor(color) + ";");
      }

      return sb.toString();
    }

    public StyleInfo updateWith(StyleInfo mixin) {
      return new StyleInfo(
          mixin.bold.isPresent() ? mixin.bold : bold,
          mixin.italic.isPresent() ? mixin.italic : italic,
          mixin.underline.isPresent() ? mixin.underline : underline,
          mixin.strikethrough.isPresent() ? mixin.strikethrough : strikethrough,
          mixin.fontSize.isPresent() ? mixin.fontSize : fontSize,
          mixin.fontFamily.isPresent() ? mixin.fontFamily : fontFamily,
          mixin.textColor.isPresent() ? mixin.textColor : textColor,
          mixin.backgroundColor.isPresent() ? mixin.backgroundColor : backgroundColor);
    }

    public StyleInfo updateBold(boolean bold) {
      return new StyleInfo(
          Optional.of(bold),
          italic,
          underline,
          strikethrough,
          fontSize,
          fontFamily,
          textColor,
          backgroundColor);
    }

    public StyleInfo updateItalic(boolean italic) {
      return new StyleInfo(
          bold,
          Optional.of(italic),
          underline,
          strikethrough,
          fontSize,
          fontFamily,
          textColor,
          backgroundColor);
    }

    public StyleInfo updateUnderline(boolean underline) {
      return new StyleInfo(
          bold,
          italic,
          Optional.of(underline),
          strikethrough,
          fontSize,
          fontFamily,
          textColor,
          backgroundColor);
    }

    public StyleInfo updateStrikethrough(boolean strikethrough) {
      return new StyleInfo(
          bold,
          italic,
          underline,
          Optional.of(strikethrough),
          fontSize,
          fontFamily,
          textColor,
          backgroundColor);
    }

    public StyleInfo updateFontSize(int fontSize) {
      return new StyleInfo(
          bold,
          italic,
          underline,
          strikethrough,
          Optional.of(fontSize),
          fontFamily,
          textColor,
          backgroundColor);
    }

    public StyleInfo updateFontFamily(String fontFamily) {
      return new StyleInfo(
          bold,
          italic,
          underline,
          strikethrough,
          fontSize,
          Optional.of(fontFamily),
          textColor,
          backgroundColor);
    }

    public StyleInfo updateTextColor(Color textColor) {
      return new StyleInfo(
          bold,
          italic,
          underline,
          strikethrough,
          fontSize,
          fontFamily,
          Optional.of(textColor),
          backgroundColor);
    }

    public StyleInfo updateBackgroundColor(Color backgroundColor) {
      return new StyleInfo(
          bold,
          italic,
          underline,
          strikethrough,
          fontSize,
          fontFamily,
          textColor,
          Optional.of(backgroundColor));
    }
  }

  public static void main(String[] args) {
    launch(args);
  }

  private final InlineStyleTextArea<StyleInfo> area =
      new InlineStyleTextArea<StyleInfo>(
          StyleInfo.EMPTY.updateFontSize(12).updateFontFamily("Serif").updateTextColor(Color.BLACK),
          style -> style.toCss());

  {
    area.setWrapText(true);
    area.setStyleCodec(StyleInfo.CODEC);
  }

  private final SuspendableNo updatingToolbar = new SuspendableNo();

  @Override
  public void start(Stage primaryStage) {
    CheckBox wrapToggle = new CheckBox("Wrap");
    wrapToggle.setSelected(true);
    area.wrapTextProperty().bind(wrapToggle.selectedProperty());
    Button undoBtn = createButton("undo", () -> area.undo());
    Button redoBtn = createButton("redo", () -> area.redo());
    Button cutBtn = createButton("cut", () -> area.cut());
    Button copyBtn = createButton("copy", () -> area.copy());
    Button pasteBtn = createButton("paste", () -> area.paste());
    Button boldBtn = createButton("bold", () -> toggleBold());
    Button italicBtn = createButton("italic", () -> toggleItalic());
    Button underlineBtn = createButton("underline", () -> toggleUnderline());
    Button strikeBtn = createButton("strikethrough", () -> toggleStrikethrough());
    ComboBox<Integer> sizeCombo =
        new ComboBox<>(
            FXCollections.observableArrayList(
                5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20, 22, 24, 28, 32, 36, 40, 48, 56, 64,
                72));
    sizeCombo.getSelectionModel().select(Integer.valueOf(12));
    ComboBox<String> familyCombo = new ComboBox<>(FXCollections.observableList(Font.getFamilies()));
    familyCombo.getSelectionModel().select("Serif");
    ColorPicker textColorPicker = new ColorPicker(Color.BLACK);
    ColorPicker backgroundColorPicker = new ColorPicker();

    sizeCombo.setOnAction(evt -> updateFontSize(sizeCombo.getValue()));
    familyCombo.setOnAction(evt -> updateFontFamily(familyCombo.getValue()));
    textColorPicker.valueProperty().addListener((o, old, color) -> updateTextColor(color));
    backgroundColorPicker
        .valueProperty()
        .addListener((o, old, color) -> updateBackgroundColor(color));

    undoBtn.disableProperty().bind(Bindings.not(area.undoAvailableProperty()));
    redoBtn.disableProperty().bind(Bindings.not(area.redoAvailableProperty()));

    BooleanBinding selectionEmpty =
        new BooleanBinding() {
          {
            bind(area.selectionProperty());
          }

          @Override
          protected boolean computeValue() {
            return area.getSelection().getLength() == 0;
          }
        };

    cutBtn.disableProperty().bind(selectionEmpty);
    copyBtn.disableProperty().bind(selectionEmpty);

    area.beingUpdatedProperty()
        .addListener(
            (o, old, beingUpdated) -> {
              if (!beingUpdated) {
                boolean bold, italic, underline, strike;
                Integer fontSize;
                String fontFamily;
                Color textColor;
                Color backgroundColor;

                IndexRange selection = area.getSelection();
                if (selection.getLength() != 0) {
                  StyleSpans<StyleInfo> styles = area.getStyleSpans(selection);
                  bold = styles.styleStream().anyMatch(s -> s.bold.orElse(false));
                  italic = styles.styleStream().anyMatch(s -> s.italic.orElse(false));
                  underline = styles.styleStream().anyMatch(s -> s.underline.orElse(false));
                  strike = styles.styleStream().anyMatch(s -> s.strikethrough.orElse(false));
                  int[] sizes =
                      styles
                          .styleStream()
                          .mapToInt(s -> s.fontSize.orElse(-1))
                          .distinct()
                          .toArray();
                  fontSize = sizes.length == 1 ? sizes[0] : -1;
                  String[] families =
                      styles
                          .styleStream()
                          .map(s -> s.fontFamily.orElse(null))
                          .distinct()
                          .toArray(i -> new String[i]);
                  fontFamily = families.length == 1 ? families[0] : null;
                  Color[] colors =
                      styles
                          .styleStream()
                          .map(s -> s.textColor.orElse(null))
                          .distinct()
                          .toArray(i -> new Color[i]);
                  textColor = colors.length == 1 ? colors[0] : null;
                  Color[] backgrounds =
                      styles
                          .styleStream()
                          .map(s -> s.backgroundColor.orElse(null))
                          .distinct()
                          .toArray(i -> new Color[i]);
                  backgroundColor = backgrounds.length == 1 ? backgrounds[0] : null;
                } else {
                  int p = area.getCurrentParagraph();
                  int col = area.getCaretColumn();
                  StyleInfo style = area.getStyleAtPosition(p, col);
                  bold = style.bold.orElse(false);
                  italic = style.italic.orElse(false);
                  underline = style.underline.orElse(false);
                  strike = style.strikethrough.orElse(false);
                  fontSize = style.fontSize.orElse(-1);
                  fontFamily = style.fontFamily.orElse(null);
                  textColor = style.textColor.orElse(null);
                  backgroundColor = style.backgroundColor.orElse(null);
                }

                updatingToolbar.suspendWhile(
                    () -> {
                      if (bold) {
                        if (!boldBtn.getStyleClass().contains("pressed")) {
                          boldBtn.getStyleClass().add("pressed");
                        }
                      } else {
                        boldBtn.getStyleClass().remove("pressed");
                      }

                      if (italic) {
                        if (!italicBtn.getStyleClass().contains("pressed")) {
                          italicBtn.getStyleClass().add("pressed");
                        }
                      } else {
                        italicBtn.getStyleClass().remove("pressed");
                      }

                      if (underline) {
                        if (!underlineBtn.getStyleClass().contains("pressed")) {
                          underlineBtn.getStyleClass().add("pressed");
                        }
                      } else {
                        underlineBtn.getStyleClass().remove("pressed");
                      }

                      if (strike) {
                        if (!strikeBtn.getStyleClass().contains("pressed")) {
                          strikeBtn.getStyleClass().add("pressed");
                        }
                      } else {
                        strikeBtn.getStyleClass().remove("pressed");
                      }

                      if (fontSize != -1) {
                        sizeCombo.getSelectionModel().select(fontSize);
                      } else {
                        sizeCombo.getSelectionModel().clearSelection();
                      }

                      if (fontFamily != null) {
                        familyCombo.getSelectionModel().select(fontFamily);
                      } else {
                        familyCombo.getSelectionModel().clearSelection();
                      }

                      if (textColor != null) {
                        textColorPicker.setValue(textColor);
                      }

                      backgroundColorPicker.setValue(backgroundColor);
                    });
              }
            });

    HBox panel1 = new HBox(3.0);
    HBox panel2 = new HBox(3.0);
    panel1
        .getChildren()
        .addAll(
            wrapToggle,
            undoBtn,
            redoBtn,
            cutBtn,
            copyBtn,
            pasteBtn,
            boldBtn,
            italicBtn,
            underlineBtn,
            strikeBtn);
    panel2.getChildren().addAll(sizeCombo, familyCombo, textColorPicker, backgroundColorPicker);

    VBox vbox = new VBox();
    VBox.setVgrow(area, Priority.ALWAYS);
    vbox.getChildren().addAll(panel1, panel2, area);

    Scene scene = new Scene(vbox, 600, 400);
    scene.getStylesheets().add(RichText.class.getResource("rich-text.css").toExternalForm());
    primaryStage.setScene(scene);
    area.requestFocus();
    primaryStage.setTitle("Rich Text Demo");
    primaryStage.show();
  }

  private Button createButton(String styleClass, Runnable action) {
    Button button = new Button();
    button.getStyleClass().add(styleClass);
    button.setOnAction(
        (evt) -> {
          action.run();
          area.requestFocus();
        });
    button.setPrefWidth(20);
    button.setPrefHeight(20);
    return button;
  }

  private void toggleBold() {
    updateStyleInSelection(
        spans ->
            StyleInfo.EMPTY.updateBold(
                !spans.styleStream().allMatch(style -> style.bold.orElse(false))));
  }

  private void toggleItalic() {
    updateStyleInSelection(
        spans ->
            StyleInfo.EMPTY.updateItalic(
                !spans.styleStream().allMatch(style -> style.italic.orElse(false))));
  }

  private void toggleUnderline() {
    updateStyleInSelection(
        spans ->
            StyleInfo.EMPTY.updateUnderline(
                !spans.styleStream().allMatch(style -> style.underline.orElse(false))));
  }

  private void toggleStrikethrough() {
    updateStyleInSelection(
        spans ->
            StyleInfo.EMPTY.updateStrikethrough(
                !spans.styleStream().allMatch(style -> style.strikethrough.orElse(false))));
  }

  private void updateStyleInSelection(Function<StyleSpans<StyleInfo>, StyleInfo> mixinGetter) {
    IndexRange selection = area.getSelection();
    if (selection.getLength() != 0) {
      StyleSpans<StyleInfo> styles = area.getStyleSpans(selection);
      StyleInfo mixin = mixinGetter.apply(styles);
      StyleSpans<StyleInfo> newStyles = styles.mapStyles(style -> style.updateWith(mixin));
      area.setStyleSpans(selection.getStart(), newStyles);
    }
  }

  private void updateStyleInSelection(StyleInfo mixin) {
    IndexRange selection = area.getSelection();
    if (selection.getLength() != 0) {
      StyleSpans<StyleInfo> styles = area.getStyleSpans(selection);
      StyleSpans<StyleInfo> newStyles = styles.mapStyles(style -> style.updateWith(mixin));
      area.setStyleSpans(selection.getStart(), newStyles);
    }
  }

  private void updateFontSize(Integer size) {
    if (!updatingToolbar.get()) {
      updateStyleInSelection(StyleInfo.fontSize(size));
    }
  }

  private void updateFontFamily(String family) {
    if (!updatingToolbar.get()) {
      updateStyleInSelection(StyleInfo.fontFamily(family));
    }
  }

  private void updateTextColor(Color color) {
    if (!updatingToolbar.get()) {
      updateStyleInSelection(StyleInfo.textColor(color));
    }
  }

  private void updateBackgroundColor(Color color) {
    if (!updatingToolbar.get()) {
      updateStyleInSelection(StyleInfo.backgroundColor(color));
    }
  }
}