/* * Copyright (c) 2010, 2018, 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 javafx.scene.web; import java.util.ResourceBundle; import com.sun.javafx.application.PlatformImpl; import com.sun.javafx.scene.ParentHelper; import com.sun.javafx.scene.traversal.Algorithm; import com.sun.javafx.scene.traversal.Direction; import com.sun.javafx.scene.traversal.ParentTraversalEngine; import com.sun.javafx.scene.traversal.TraversalContext; import javafx.css.PseudoClass; import javafx.geometry.Orientation; import org.w3c.dom.html.HTMLDocument; import org.w3c.dom.html.HTMLElement; import javafx.application.ConditionalFeature; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.css.StyleableProperty; import javafx.geometry.NodeOrientation; import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Separator; import javafx.scene.control.TextInputControl; import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToolBar; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.util.Callback; import com.sun.javafx.scene.control.skin.FXVK; import com.sun.javafx.scene.web.behavior.HTMLEditorBehavior; import com.sun.webkit.dom.HTMLDocumentImpl; import com.sun.webkit.WebPage; import com.sun.javafx.webkit.Accessor; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.HashMap; import java.util.Locale; import java.util.Map; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.layout.*; import javafx.collections.ListChangeListener; import static javafx.geometry.NodeOrientation.*; import javafx.print.PrinterJob; import static javafx.scene.web.HTMLEditorSkin.Command.*; /** * HTML editor skin. * * @see HTMLEditor * @since 9 */ public class HTMLEditorSkin extends SkinBase { /*************************************************************************** * * * Private fields * * * **************************************************************************/ private GridPane gridPane; private ToolBar toolbar1; private ToolBar toolbar2; private Button cutButton; private Button copyButton; private Button pasteButton; // private Button undoButton; // private Button redoButton; private Button insertHorizontalRuleButton; private ToggleGroup alignmentToggleGroup; private ToggleButton alignLeftButton; private ToggleButton alignCenterButton; private ToggleButton alignRightButton; private ToggleButton alignJustifyButton; private ToggleButton bulletsButton; private ToggleButton numbersButton; private Button indentButton; private Button outdentButton; private ComboBox formatComboBox; private Map formatStyleMap; private Map styleFormatMap; private ComboBox fontFamilyComboBox; private ComboBox fontSizeComboBox; private Map fontSizeMap; private Map sizeFontMap; private ToggleButton boldButton; private ToggleButton italicButton; private ToggleButton underlineButton; private ToggleButton strikethroughButton; private ColorPicker fgColorButton; private ColorPicker bgColorButton; private WebView webView; private WebPage webPage; private ParentTraversalEngine engine; private boolean resetToolbarState = false; private String cachedHTMLText = ""; private ResourceBundle resources; private boolean enableAtomicityCheck = false; private int atomicityCount = 0; private boolean isFirstRun = true; private static final int FONT_FAMILY_MENUBUTTON_WIDTH = 150; private static final int FONT_FAMILY_MENU_WIDTH = 100; private static final int FONT_SIZE_MENUBUTTON_WIDTH = 80; /*************************************************************************** * * * Static fields * * * **************************************************************************/ private static final Color DEFAULT_BG_COLOR = Color.WHITE; private static final Color DEFAULT_FG_COLOR = Color.BLACK; private static final String FORMAT_PARAGRAPH = "

"; private static final String FORMAT_HEADING_1 = "

"; private static final String FORMAT_HEADING_2 = "

"; private static final String FORMAT_HEADING_3 = "

"; private static final String FORMAT_HEADING_4 = "

"; private static final String FORMAT_HEADING_5 = "

"; private static final String FORMAT_HEADING_6 = "
"; private static final String SIZE_XX_SMALL = "1"; private static final String SIZE_X_SMALL = "2"; private static final String SIZE_SMALL = "3"; private static final String SIZE_MEDIUM = "4"; private static final String SIZE_LARGE = "5"; private static final String SIZE_X_LARGE = "6"; private static final String SIZE_XX_LARGE = "7"; // As per RT-16330: default format -> bold/size mappings are as follows: private static final String[][] DEFAULT_FORMAT_MAPPINGS = { { FORMAT_PARAGRAPH, "", SIZE_SMALL }, { FORMAT_HEADING_1, BOLD.getCommand(), SIZE_X_LARGE }, { FORMAT_HEADING_2, BOLD.getCommand(), SIZE_LARGE }, { FORMAT_HEADING_3, BOLD.getCommand(), SIZE_MEDIUM }, { FORMAT_HEADING_4, BOLD.getCommand(), SIZE_SMALL }, { FORMAT_HEADING_5, BOLD.getCommand(), SIZE_X_SMALL }, { FORMAT_HEADING_6, BOLD.getCommand(), SIZE_XX_SMALL }, }; private static PseudoClass CONTAINS_FOCUS_PSEUDOCLASS_STATE = PseudoClass.getPseudoClass("contains-focus"); /*************************************************************************** * * * Static Methods * * * **************************************************************************/ /*************************************************************************** * * * Listeners * * * **************************************************************************/ private ListChangeListener itemsListener = c -> { while (c.next()) { if (c.getRemovedSize() > 0) { for (Node n : c.getList()) { if (n instanceof WebView) { // RT-28611 webView removed - set associated webPage to null webPage.dispose(); } } } } }; /*************************************************************************** * * * Constructors * * * **************************************************************************/ /** * Creates a new HTMLEditorSkin instance, installing the necessary child * nodes into the Control {@link Control#getChildren() children} list, as * well as the necessary input mappings for handling key, mouse, etc events. * * @param control The control that this skin should be installed onto. */ public HTMLEditorSkin(HTMLEditor control) { super(control); // install default input map for the HTMLEditor control HTMLEditorBehavior behavior = new HTMLEditorBehavior(control); // htmlEditor.setInputMap(behavior.getInputMap()); getChildren().clear(); gridPane = new GridPane(); gridPane.getStyleClass().add("grid"); getChildren().addAll(gridPane); toolbar1 = new ToolBar(); toolbar1.getStyleClass().add("top-toolbar"); gridPane.add(toolbar1, 0, 0); toolbar2 = new ToolBar(); toolbar2.getStyleClass().add("bottom-toolbar"); gridPane.add(toolbar2, 0, 1); // populateToolbars(); webView = new WebView(); gridPane.add(webView, 0, 2); ColumnConstraints column = new ColumnConstraints(); column.setHgrow(Priority.ALWAYS); gridPane.getColumnConstraints().add(column); webPage = Accessor.getPageFor(webView.getEngine()); webView.addEventHandler(MouseEvent.MOUSE_RELEASED, event2 -> { Platform.runLater(new Runnable() { @Override public void run() { enableAtomicityCheck = true; updateToolbarState(true); enableAtomicityCheck = false; } }); }); webView.addEventHandler(KeyEvent.KEY_PRESSED, event -> { applyTextFormatting(); if (event.getCode() == KeyCode.CONTROL || event.getCode() == KeyCode.META) { return; } if (event.getCode() == KeyCode.TAB && !event.isControlDown()) { if (!event.isShiftDown()) { /* ** if we are in either Bullet or Numbers mode then the ** TAB key tells us to indent again. */ if (getCommandState(BULLETS.getCommand()) || getCommandState(NUMBERS.getCommand())) { executeCommand(INDENT.getCommand(), null); } else { executeCommand(INSERT_TAB.getCommand(), null); } } else { /* ** if we are in either Bullet or Numbers mode then the ** Shift-TAB key tells us to outdent. */ if (getCommandState(BULLETS.getCommand()) || getCommandState(NUMBERS.getCommand())) { executeCommand(OUTDENT.getCommand(), null); } } return; } // Work around for bug that sends events from ColorPicker to this Scene if ((fgColorButton != null && fgColorButton.isShowing()) || (bgColorButton != null && bgColorButton.isShowing())) { return; } Platform.runLater(() -> { if (webPage.getClientSelectedText().isEmpty()) { if (event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN || event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.HOME || event.getCode() == KeyCode.END) { updateToolbarState(true); } else if (event.isControlDown() || event.isMetaDown()) { if (event.getCode() == KeyCode.B) { performCommand(BOLD); } else if (event.getCode() == KeyCode.I) { performCommand(ITALIC); } else if (event.getCode() == KeyCode.U) { performCommand(UNDERLINE); } updateToolbarState(true); } else { resetToolbarState = event.getCode() == KeyCode.ENTER; if (resetToolbarState) { if (getCommandState(BOLD.getCommand()) != boldButton.selectedProperty().getValue()) { executeCommand(BOLD.getCommand(), boldButton.selectedProperty().getValue().toString()); } } updateToolbarState(false); } resetToolbarState = false; } else if (event.isShiftDown() && (event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN || event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.HOME || event.getCode() == KeyCode.END)) { updateToolbarState(true); } else if ((event.isControlDown() || event.isMetaDown()) && event.getCode() == KeyCode.A) { updateToolbarState(true); } }); }); webView.addEventHandler(KeyEvent.KEY_RELEASED, event -> { if (event.getCode() == KeyCode.CONTROL || event.getCode() == KeyCode.META) { return; } // Work around for bug that sends events from ColorPicker to this Scene if ((fgColorButton != null && fgColorButton.isShowing()) || (bgColorButton != null && bgColorButton.isShowing())) { return; } Platform.runLater(() -> { if (webPage.getClientSelectedText().isEmpty()) { if (event.getCode() == KeyCode.UP || event.getCode() == KeyCode.DOWN || event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.HOME || event.getCode() == KeyCode.END) { updateToolbarState(true); } else if (event.isControlDown() || event.isMetaDown()) { if (event.getCode() == KeyCode.B) { performCommand(BOLD); } else if (event.getCode() == KeyCode.I) { performCommand(ITALIC); } else if (event.getCode() == KeyCode.U) { performCommand(UNDERLINE); } updateToolbarState(true); } else { resetToolbarState = event.getCode() == KeyCode.ENTER; if (!resetToolbarState) { updateToolbarState(false); } } resetToolbarState = false; } }); }); getSkinnable().focusedProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(new Runnable() { @Override public void run() { if (newValue) { webView.requestFocus(); } } }); }); webView.focusedProperty().addListener((observable, oldValue, newValue) -> { // disabling as a fix for RT-30081 // if (newValue) { // webPage.dispatchFocusEvent(new WCFocusEvent(WCFocusEvent.FOCUS_GAINED, WCFocusEvent.FORWARD)); // enableToolbar(true); // } else { // webPage.dispatchFocusEvent(new WCFocusEvent(WCFocusEvent.FOCUS_LOST, WCFocusEvent.FORWARD)); // enableToolbar(false); // } pseudoClassStateChanged(CONTAINS_FOCUS_PSEUDOCLASS_STATE, newValue); Platform.runLater(new Runnable() { @Override public void run() { updateToolbarState(true); if (PlatformImpl.isSupported(ConditionalFeature.VIRTUAL_KEYBOARD)) { Scene scene = getSkinnable().getScene(); if (newValue) { FXVK.attach(webView); } else if (scene == null || scene.getWindow() == null || !scene.getWindow().isFocused() || !(scene.getFocusOwner() instanceof TextInputControl /*|| getScene().getFocusOwner() instanceof WebView*/)) { FXVK.detach(); } } } }); }); webView.getEngine().getLoadWorker().workDoneProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { webView.requestLayout(); }); double totalWork = webView.getEngine().getLoadWorker().getTotalWork(); if (newValue.doubleValue() == totalWork) { cachedHTMLText = null; Platform.runLater(() -> { setDesignMode("on"); setContentEditable(true); updateToolbarState(true); updateNodeOrientation(); }); } }); enableToolbar(true); setHTMLText(cachedHTMLText); engine = new ParentTraversalEngine(getSkinnable(), new Algorithm() { @Override public Node select(Node owner, Direction dir, TraversalContext context) { return cutButton; } @Override public Node selectFirst(TraversalContext context) { return cutButton; } @Override public Node selectLast(TraversalContext context) { return cutButton; } }); ParentHelper.setTraversalEngine(getSkinnable(), engine); webView.setFocusTraversable(true); gridPane.getChildren().addListener(itemsListener); } /*************************************************************************** * * * Public API * * * **************************************************************************/ /** * Special-case handling for certain commands. Over time this may be extended * to handle additional commands. The current list of supported commands is: * *
    *
  • BOLD
  • *
  • ITALIC
  • *
  • UNDERLINE
  • *
* @param command the command */ public void performCommand(final Command command) { switch (command) { case BOLD: boldButton.fire(); break; case ITALIC: italicButton.setSelected(!italicButton.isSelected()); break; case UNDERLINE: underlineButton.setSelected(!underlineButton.isSelected()); break; } } /** {@inheritDoc} */ @Override protected void layoutChildren(final double x, final double y, final double w, final double h) { if (isFirstRun) { populateToolbars(); isFirstRun = false; } super.layoutChildren(x,y,w,h); double toolbarWidth = Math.max(toolbar1.prefWidth(-1), toolbar2.prefWidth(-1)); toolbar1.setMinWidth(toolbarWidth); toolbar1.setPrefWidth(toolbarWidth); toolbar2.setMinWidth(toolbarWidth); toolbar2.setPrefWidth(toolbarWidth); } /*************************************************************************** * * * Private Implementation * * * **************************************************************************/ final String getHTMLText() { // RT17203 setHTMLText is asynchronous. We use the cached version of // the html text until the page finishes loading. return cachedHTMLText != null ? cachedHTMLText : webPage.getHtml(webPage.getMainFrame()); } final void setHTMLText(String htmlText) { cachedHTMLText = htmlText; webPage.load(webPage.getMainFrame(), htmlText, "text/html"); Platform.runLater(() -> { updateToolbarState(true); }); } private void populateToolbars() { resources = ResourceBundle.getBundle(HTMLEditorSkin.class.getName()); // Toolbar 1 cutButton = addButton(toolbar1, resources.getString("cutIcon"), resources.getString("cut"), CUT.getCommand(), "html-editor-cut"); copyButton = addButton(toolbar1, resources.getString("copyIcon"), resources.getString("copy"), COPY.getCommand(), "html-editor-copy"); pasteButton = addButton(toolbar1, resources.getString("pasteIcon"), resources.getString("paste"), PASTE.getCommand(), "html-editor-paste"); toolbar1.getItems().add(new Separator(Orientation.VERTICAL)); // undoButton = addButton(toolbar1, "undoIcon", resources.getString("undo"), UNDO.getCommand()); // redoButton = addButton(toolbar1, "redoIcon", resources.getString("redo"), REDO.getCommand());// // toolbar1.getItems().add(new Separator()); alignmentToggleGroup = new ToggleGroup(); alignLeftButton = addToggleButton(toolbar1, alignmentToggleGroup, resources.getString("alignLeftIcon"), resources.getString("alignLeft"), ALIGN_LEFT.getCommand(), "html-editor-align-left"); alignCenterButton = addToggleButton(toolbar1, alignmentToggleGroup, resources.getString("alignCenterIcon"), resources.getString("alignCenter"), ALIGN_CENTER.getCommand(), "html-editor-align-center"); alignRightButton = addToggleButton(toolbar1, alignmentToggleGroup, resources.getString("alignRightIcon"), resources.getString("alignRight"), ALIGN_RIGHT.getCommand(), "html-editor-align-right"); alignJustifyButton = addToggleButton(toolbar1, alignmentToggleGroup, resources.getString("alignJustifyIcon"), resources.getString("alignJustify"), ALIGN_JUSTIFY.getCommand(), "html-editor-align-justify"); toolbar1.getItems().add(new Separator(Orientation.VERTICAL)); outdentButton = addButton(toolbar1, resources.getString("outdentIcon"), resources.getString("outdent"), OUTDENT.getCommand(), "html-editor-outdent"); if (outdentButton.getGraphic() != null) outdentButton.getGraphic().setNodeOrientation(NodeOrientation.INHERIT); indentButton = addButton(toolbar1, resources.getString("indentIcon"), resources.getString("indent"), INDENT.getCommand(), "html-editor-indent"); if (indentButton.getGraphic() != null) indentButton.getGraphic().setNodeOrientation(NodeOrientation.INHERIT); toolbar1.getItems().add(new Separator(Orientation.VERTICAL)); ToggleGroup listStyleToggleGroup = new ToggleGroup(); bulletsButton = addToggleButton(toolbar1, listStyleToggleGroup, resources.getString("bulletsIcon"), resources.getString("bullets"), BULLETS.getCommand(), "html-editor-bullets"); if (bulletsButton.getGraphic() != null) bulletsButton.getGraphic().setNodeOrientation(NodeOrientation.INHERIT); numbersButton = addToggleButton(toolbar1, listStyleToggleGroup, resources.getString("numbersIcon"), resources.getString("numbers"), NUMBERS.getCommand(), "html-editor-numbers"); toolbar1.getItems().add(new Separator(Orientation.VERTICAL)); //toolbar1.getItems().add(new Separator()); // Toolbar 2 formatComboBox = new ComboBox(); formatComboBox.getStyleClass().add("font-menu-button"); formatComboBox.setFocusTraversable(false); formatComboBox.setMinWidth(Region.USE_PREF_SIZE); toolbar2.getItems().add(formatComboBox); formatStyleMap = new HashMap(); styleFormatMap = new HashMap(); createFormatMenuItem(FORMAT_PARAGRAPH, resources.getString("paragraph")); Platform.runLater(() -> { formatComboBox.setValue(resources.getString("paragraph")); }); createFormatMenuItem(FORMAT_HEADING_1, resources.getString("heading1")); createFormatMenuItem(FORMAT_HEADING_2, resources.getString("heading2")); createFormatMenuItem(FORMAT_HEADING_3, resources.getString("heading3")); createFormatMenuItem(FORMAT_HEADING_4, resources.getString("heading4")); createFormatMenuItem(FORMAT_HEADING_5, resources.getString("heading5")); createFormatMenuItem(FORMAT_HEADING_6, resources.getString("heading6")); // formatComboBox.setCellFactory(new Callback, ListCell>() { // @Override public ListCell call(ListView param) { // final ListCell cell = new ListCell() { // @Override public void updateItem(String item, boolean empty) { // super.updateItem(item, empty); // if (item != null) { // setText(item); // } // } // }; // return cell; // } // }); formatComboBox.setTooltip(new Tooltip(resources.getString("format"))); formatComboBox.valueProperty().addListener((observable, oldValue, newValue) -> { if (newValue == null) { formatComboBox.setValue(null); } else { String formatValue = formatStyleMap.get(newValue); executeCommand(FORMAT.getCommand(), formatValue); updateToolbarState(false); // RT-16330 match the new font format with the required weight and size for (int i = 0; i < DEFAULT_FORMAT_MAPPINGS.length; i++) { String[] mapping = DEFAULT_FORMAT_MAPPINGS[i]; if (mapping[0].equalsIgnoreCase(formatValue)) { executeCommand(FONT_SIZE.getCommand(), mapping[2]); updateToolbarState(false); break; } } } }); fontFamilyComboBox = new ComboBox(); fontFamilyComboBox.getStyleClass().add("font-menu-button"); fontFamilyComboBox.setMinWidth(FONT_FAMILY_MENUBUTTON_WIDTH); fontFamilyComboBox.setPrefWidth(FONT_FAMILY_MENUBUTTON_WIDTH); fontFamilyComboBox.setMaxWidth(FONT_FAMILY_MENUBUTTON_WIDTH); fontFamilyComboBox.setFocusTraversable(false); fontFamilyComboBox.setTooltip(new Tooltip(resources.getString("fontFamily"))); toolbar2.getItems().add(fontFamilyComboBox); // Fix for RT-32906, where all rows were being put through the cell factory // so that they could be measured. Because we have a fixed width for the // button this is unnecessary and so we tell the ComboBox to not measure // any rows. fontFamilyComboBox.getProperties().put("comboBoxRowsToMeasureWidth", 0); fontFamilyComboBox.setCellFactory(new Callback, ListCell>() { @Override public ListCell call(ListView param) { final ListCell cell = new ListCell() { @Override public void updateItem(String item, boolean empty) { super.updateItem(item, empty); if (item != null) { setText(item); setFont(new Font(item, 12)); } } }; cell.setMinWidth(FONT_FAMILY_MENU_WIDTH); cell.setPrefWidth(FONT_FAMILY_MENU_WIDTH); cell.setMaxWidth(FONT_FAMILY_MENU_WIDTH); return cell; } }); Platform.runLater(() -> { final ObservableList fonts = FXCollections.observableArrayList(Font.getFamilies()); fonts.add(0, ""); for (String fontFamily : fonts) { fontFamilyComboBox.setValue(""); fontFamilyComboBox.setItems(fonts); } }); fontFamilyComboBox.valueProperty().addListener((observable, oldValue, newValue) -> { executeCommand(FONT_FAMILY.getCommand(), ("".equals(newValue)) ? "''" : newValue); }); fontSizeComboBox = new ComboBox(); fontSizeComboBox.getStyleClass().add("font-menu-button"); fontSizeComboBox.setFocusTraversable(false); toolbar2.getItems().add(fontSizeComboBox); fontSizeMap = new HashMap(); sizeFontMap = new HashMap(); createFontSizeMenuItem(SIZE_XX_SMALL, resources.getString("extraExtraSmall")); createFontSizeMenuItem(SIZE_X_SMALL, resources.getString("extraSmall")); createFontSizeMenuItem(SIZE_SMALL, resources.getString("small")); Platform.runLater(() -> { fontSizeComboBox.setValue(resources.getString("small")); }); createFontSizeMenuItem(SIZE_MEDIUM, resources.getString("medium")); createFontSizeMenuItem(SIZE_LARGE, resources.getString("large")); createFontSizeMenuItem(SIZE_X_LARGE, resources.getString("extraLarge")); createFontSizeMenuItem(SIZE_XX_LARGE, resources.getString("extraExtraLarge")); fontSizeComboBox.setTooltip(new Tooltip(resources.getString("fontSize"))); fontSizeComboBox.setCellFactory(new Callback, ListCell>() { @Override public ListCell call(ListView param) { final ListCell cell = new ListCell() { @Override public void updateItem(String item, boolean empty) { super.updateItem(item, empty); if (item != null) { setText(item); // Remove trailing non-digits to get the size (don't assume there's a space). String size = item.replaceFirst("[^0-9.].*$", ""); setFont(new Font((String)fontFamilyComboBox.getValue(), Double.valueOf(size))); } } }; return cell; } }); fontSizeComboBox.valueProperty().addListener((observable, oldValue, newValue) -> { Object fontSizeValue = getCommandValue(FONT_SIZE.getCommand()); if (!newValue.equals(fontSizeValue)) { executeCommand(FONT_SIZE.getCommand(), fontSizeMap.get(newValue)); } }); toolbar2.getItems().add(new Separator(Orientation.VERTICAL)); boldButton = addToggleButton(toolbar2, null, resources.getString("boldIcon"), resources.getString("bold"), BOLD.getCommand(), "html-editor-bold"); boldButton.setOnAction(event1 -> { // Only use the bold button for paragraphs. We don't // want to turn bold off for headings. if ("

".equals(formatStyleMap.get(formatComboBox.getValue()))) { executeCommand(BOLD.getCommand(), boldButton.selectedProperty().getValue().toString()); } }); italicButton = addToggleButton(toolbar2, null, resources.getString("italicIcon"), resources.getString("italic"), ITALIC.getCommand(), "html-editor-italic"); underlineButton = addToggleButton(toolbar2, null, resources.getString("underlineIcon"), resources.getString("underline"), UNDERLINE.getCommand(), "html-editor-underline"); strikethroughButton = addToggleButton(toolbar2, null, resources.getString("strikethroughIcon"), resources.getString("strikethrough"), STRIKETHROUGH.getCommand(), "html-editor-strike"); toolbar2.getItems().add(new Separator(Orientation.VERTICAL)); insertHorizontalRuleButton = addButton(toolbar2, resources.getString("insertHorizontalRuleIcon"), resources.getString("insertHorizontalRule"), INSERT_HORIZONTAL_RULE.getCommand(), "html-editor-hr"); // We override setOnAction to insert a new line. This fixes RT-16453 insertHorizontalRuleButton.setOnAction(event -> { executeCommand(INSERT_NEW_LINE.getCommand(), null); executeCommand(INSERT_HORIZONTAL_RULE.getCommand(), null); updateToolbarState(false); }); fgColorButton = new ColorPicker(); fgColorButton.getStyleClass().add("html-editor-foreground"); fgColorButton.setFocusTraversable(false); toolbar1.getItems().add(fgColorButton); // JDK-8115747: Icon URLs are now specified in CSS. // fgColorButton.applyCss(); // ColorPickerSkin fgColorPickerSkin = (ColorPickerSkin) fgColorButton.getSkin(); // String fgIcon = AccessController.doPrivileged((PrivilegedAction) () -> HTMLEditorSkin.class.getResource(resources.getString("foregroundColorIcon")).toString()); // ((StyleableProperty)fgColorPickerSkin.imageUrlProperty()).applyStyle(null,fgIcon); fgColorButton.setValue(DEFAULT_FG_COLOR); fgColorButton.setTooltip(new Tooltip(resources.getString("foregroundColor"))); fgColorButton.setOnAction(ev1 -> { Color newValue = fgColorButton.getValue(); if (newValue != null) { executeCommand(FOREGROUND_COLOR.getCommand(), colorValueToHex(newValue)); fgColorButton.hide(); } }); bgColorButton = new ColorPicker(); bgColorButton.getStyleClass().add("html-editor-background"); bgColorButton.setFocusTraversable(false); toolbar1.getItems().add(bgColorButton); // JDK-8115747: Icon URLs are now specified in CSS. // bgColorButton.applyCss(); // ColorPickerSkin bgColorPickerSkin = (ColorPickerSkin) bgColorButton.getSkin(); // String bgIcon = AccessController.doPrivileged((PrivilegedAction) () -> HTMLEditorSkin.class.getResource(resources.getString("backgroundColorIcon")).toString()); // ((StyleableProperty)bgColorPickerSkin.imageUrlProperty()).applyStyle(null,bgIcon); bgColorButton.setValue(DEFAULT_BG_COLOR); bgColorButton.setTooltip(new Tooltip(resources.getString("backgroundColor"))); bgColorButton.setOnAction(ev -> { Color newValue = bgColorButton.getValue(); if (newValue != null) { executeCommand(BACKGROUND_COLOR.getCommand(), colorValueToHex(newValue)); bgColorButton.hide(); } }); } private String colorValueToHex(Color c) { return String.format((Locale)null, "#%02x%02x%02x", Math.round(c.getRed() * 255), Math.round(c.getGreen() * 255), Math.round(c.getBlue() * 255)); } private Button addButton(ToolBar toolbar, final String iconName, String tooltipText, final String command, final String styleClass) { Button button = new Button(); button.setFocusTraversable(false); button.getStyleClass().add(styleClass); toolbar.getItems().add(button); Image icon = AccessController.doPrivileged((PrivilegedAction) () -> new Image(HTMLEditorSkin.class.getResource(iconName).toString())); // button.setGraphic(new ImageView(icon)); ((StyleableProperty)button.graphicProperty()).applyStyle(null, new ImageView(icon)); button.setTooltip(new Tooltip(tooltipText)); button.setOnAction(event -> { executeCommand(command, null); updateToolbarState(false); }); return button; } private ToggleButton addToggleButton(ToolBar toolbar, ToggleGroup toggleGroup, final String iconName, String tooltipText, final String command, final String styleClass) { ToggleButton toggleButton = new ToggleButton(); toggleButton.setUserData(command); toggleButton.setFocusTraversable(false); toggleButton.getStyleClass().add(styleClass); toolbar.getItems().add(toggleButton); if (toggleGroup != null) { toggleButton.setToggleGroup(toggleGroup); } Image icon = AccessController.doPrivileged((PrivilegedAction) () -> new Image(HTMLEditorSkin.class.getResource(iconName).toString())); ((StyleableProperty)toggleButton.graphicProperty()).applyStyle(null, new ImageView(icon)); // toggleButton.setGraphic(new ImageView(icon)); toggleButton.setTooltip(new Tooltip(tooltipText)); if (!BOLD.getCommand().equals(command)) { toggleButton.selectedProperty().addListener((observable, oldValue, newValue) -> { if (getCommandState(command) != newValue.booleanValue()) { executeCommand(command, null); } }); } return toggleButton; } private void createFormatMenuItem(String formatValue, String label) { formatComboBox.getItems().add(label); formatStyleMap.put(label, formatValue); styleFormatMap.put(formatValue, label); } private void createFontSizeMenuItem(String fontSizeValue, String label) { fontSizeComboBox.getItems().add(label); fontSizeMap.put(label, fontSizeValue); sizeFontMap.put(fontSizeValue, label); } private void updateNodeOrientation() { NodeOrientation orientation = getSkinnable().getEffectiveNodeOrientation(); HTMLDocument htmlDocument = (HTMLDocument)webPage.getDocument(webPage.getMainFrame()); HTMLElement htmlDocumentElement = (HTMLElement)htmlDocument.getDocumentElement(); if (htmlDocumentElement.getAttribute("dir") == null) { htmlDocumentElement.setAttribute("dir", (orientation == RIGHT_TO_LEFT) ? "rtl" : "ltr"); } } private void updateToolbarState(final boolean updateAlignment) { if (!webView.isFocused()) { return; } atomicityCount++; // These command aways return true. copyButton.setDisable(!isCommandEnabled(CUT.getCommand())); cutButton.setDisable(!isCommandEnabled(COPY.getCommand())); pasteButton.setDisable(!isCommandEnabled(PASTE.getCommand())); // undoButton.setDisable(!isCommandEnabled(UNDO.getCommand())); // redoButton.setDisable(!isCommandEnabled(REDO.getCommand())); // undoButton.setDisable(!isCommandEnabled(FORMAT.getCommand())); // redoButton.setDisable(!isCommandEnabled(FORMAT.getCommand())); insertHorizontalRuleButton.setDisable(!isCommandEnabled(INSERT_HORIZONTAL_RULE.getCommand())); if (updateAlignment) { alignLeftButton.setDisable(!isCommandEnabled(ALIGN_LEFT.getCommand())); alignLeftButton.setSelected(getCommandState(ALIGN_LEFT.getCommand())); alignCenterButton.setDisable(!isCommandEnabled(ALIGN_CENTER.getCommand())); alignCenterButton.setSelected(getCommandState(ALIGN_CENTER.getCommand())); alignRightButton.setDisable(!isCommandEnabled(ALIGN_RIGHT.getCommand())); alignRightButton.setSelected(getCommandState(ALIGN_RIGHT.getCommand())); alignJustifyButton.setDisable(!isCommandEnabled(ALIGN_JUSTIFY.getCommand())); alignJustifyButton.setSelected(getCommandState(ALIGN_JUSTIFY.getCommand())); } else { if (alignmentToggleGroup.getSelectedToggle() != null) { String command = alignmentToggleGroup.getSelectedToggle().getUserData().toString(); if (isCommandEnabled(command) && !getCommandState(command) ) { executeCommand(command, null); } } } if (alignmentToggleGroup.getSelectedToggle() == null) { alignmentToggleGroup.selectToggle(alignLeftButton); } bulletsButton.setDisable(!isCommandEnabled(BULLETS.getCommand())); bulletsButton.setSelected(getCommandState(BULLETS.getCommand())); numbersButton.setDisable(!isCommandEnabled(NUMBERS.getCommand())); numbersButton.setSelected(getCommandState(NUMBERS.getCommand())); indentButton.setDisable(!isCommandEnabled(INDENT.getCommand())); outdentButton.setDisable(!isCommandEnabled(OUTDENT.getCommand())); formatComboBox.setDisable(!isCommandEnabled(FORMAT.getCommand())); String formatValue = getCommandValue(FORMAT.getCommand()); if (formatValue != null) { String htmlTag = "<" + formatValue + ">"; String comboFormatValue = styleFormatMap.get(htmlTag); String format = formatComboBox.getValue(); // if the format value is then we assume that we're dealing with a paragraph, // which seems to correspond with the HTML output we receive. if ((resetToolbarState || htmlTag.equals("<>") || htmlTag.equalsIgnoreCase("

") || htmlTag.equalsIgnoreCase("
"))) { formatComboBox.setValue(resources.getString("paragraph")); } else if (format != null && ! format.equalsIgnoreCase(comboFormatValue)) { formatComboBox.setValue(comboFormatValue); } } fontFamilyComboBox.setDisable(!isCommandEnabled(FONT_FAMILY.getCommand())); final String fontFamilyValue = getCommandValue(FONT_FAMILY.getCommand()); if (fontFamilyValue != null) { String fontFamilyStr = fontFamilyValue; // stripping out apostrophe characters, which are appended to either // end of the font face name when the font face has one or more spaces. if (fontFamilyStr.startsWith("'")) { fontFamilyStr = fontFamilyStr.substring(1); } if (fontFamilyStr.endsWith("'")) { fontFamilyStr = fontFamilyStr.substring(0,fontFamilyStr.length() - 1); } Object selectedFont = fontFamilyComboBox.getValue(); if (selectedFont instanceof String) { if (!selectedFont.equals(fontFamilyStr)) { ObservableList fontFamilyItems = fontFamilyComboBox.getItems(); String selectedComboFont = null; for (String comboFontFamilyValue : fontFamilyItems) { if (comboFontFamilyValue.equals(fontFamilyStr)) { selectedComboFont = comboFontFamilyValue; break; } // Note: By default, 'Dialog' is the font returned from webview. // For presidio, we're just mapping to the default font. if (comboFontFamilyValue.equals("") && fontFamilyStr.equals("Dialog")) { selectedComboFont = comboFontFamilyValue; break; } } if (selectedComboFont != null) { fontFamilyComboBox.setValue(selectedComboFont); } } } } fontSizeComboBox.setDisable(!isCommandEnabled(FONT_SIZE.getCommand())); String fontSizeValue = getCommandValue(FONT_SIZE.getCommand()); // added test for fontSizeValue == null to combat RT-28847 if (resetToolbarState && fontSizeValue == null) { fontSizeComboBox.setValue(sizeFontMap.get(SIZE_SMALL)); } else { if (fontSizeValue != null) { if (!fontSizeComboBox.getValue().equals(sizeFontMap.get(fontSizeValue))) { fontSizeComboBox.setValue(sizeFontMap.get(fontSizeValue)); } } else { /* ** these is no font size set in webview, ** let's just use the default.... */ if ((fontSizeComboBox.getValue() == null) || !fontSizeComboBox.getValue().equals(sizeFontMap.get(SIZE_SMALL))) { fontSizeComboBox.setValue(sizeFontMap.get(SIZE_SMALL)); } } } boldButton.setDisable(!isCommandEnabled(BOLD.getCommand())); boldButton.setSelected(getCommandState(BOLD.getCommand())); italicButton.setDisable(!isCommandEnabled(ITALIC.getCommand())); italicButton.setSelected(getCommandState(ITALIC.getCommand())); underlineButton.setDisable(!isCommandEnabled(UNDERLINE.getCommand())); underlineButton.setSelected(getCommandState(UNDERLINE.getCommand())); strikethroughButton.setDisable(!isCommandEnabled(STRIKETHROUGH.getCommand())); strikethroughButton.setSelected(getCommandState(STRIKETHROUGH.getCommand())); fgColorButton.setDisable(!isCommandEnabled(FOREGROUND_COLOR.getCommand())); String foregroundColorValue = getCommandValue(FOREGROUND_COLOR.getCommand()); if (foregroundColorValue != null) { fgColorButton.setValue(getColor(foregroundColorValue)); } bgColorButton.setDisable(!isCommandEnabled(BACKGROUND_COLOR.getCommand())); String backgroundColorValue = getCommandValue(BACKGROUND_COLOR.getCommand()); if (backgroundColorValue != null) { bgColorButton.setValue(getColor(backgroundColorValue)); } atomicityCount = atomicityCount == 0 ? 0 : --atomicityCount; } private void enableToolbar(final boolean enable) { Platform.runLater(() -> { // Make sure buttons have been created to avoid NPE if (copyButton == null) return; /* ** if we're to enable, we still only enable ** the cut/copy/paste buttons that make sense */ if (enable) { copyButton.setDisable(!isCommandEnabled(COPY.getCommand())); cutButton.setDisable(!isCommandEnabled(CUT.getCommand())); pasteButton.setDisable(!isCommandEnabled(PASTE.getCommand())); } else { copyButton.setDisable(true); cutButton.setDisable(true); pasteButton.setDisable(true); } // undoButton.setDisable(!enable); // redoButton.setDisable(!enable); insertHorizontalRuleButton.setDisable(!enable); alignLeftButton.setDisable(!enable); alignCenterButton.setDisable(!enable); alignRightButton.setDisable(!enable); alignJustifyButton.setDisable(!enable); bulletsButton.setDisable(!enable); numbersButton.setDisable(!enable); indentButton.setDisable(!enable); outdentButton.setDisable(!enable); formatComboBox.setDisable(!enable); fontFamilyComboBox.setDisable(!enable); fontSizeComboBox.setDisable(!enable); boldButton.setDisable(!enable); italicButton.setDisable(!enable); underlineButton.setDisable(!enable); strikethroughButton.setDisable(!enable); fgColorButton.setDisable(!enable); bgColorButton.setDisable(!enable); }); } private boolean executeCommand(String command, String value) { // The mentions of atomicity throughout this class relate back to RT-39941, // refer to that jira issue for more context. if (!enableAtomicityCheck || (enableAtomicityCheck && atomicityCount == 0)) { return webPage.executeCommand(command, value); } return false; } private boolean isCommandEnabled(String command) { return webPage.queryCommandEnabled(command); } private void setContentEditable(boolean b) { HTMLDocument htmlDocument = (HTMLDocument)webPage.getDocument(webPage.getMainFrame()); HTMLElement htmlDocumentElement = (HTMLElement)htmlDocument.getDocumentElement(); HTMLElement htmlBodyElement = (HTMLElement)htmlDocumentElement.getElementsByTagName("body").item(0); htmlBodyElement.setAttribute("contenteditable", Boolean.toString(b)); } private void setDesignMode(String mode) { HTMLDocumentImpl htmlDocumentImpl = (HTMLDocumentImpl)webPage.getDocument(webPage.getMainFrame()); htmlDocumentImpl.setDesignMode(mode); } private boolean getCommandState(String command) { return webPage.queryCommandState(command); } private String getCommandValue(String command) { return webPage.queryCommandValue(command); } private Color getColor(String value) { Color color = Color.web(value); /* The default background color for WebView, according to the HTML * standard is rgba=#00000000 (transparent). The canvas background is expected * to be white. */ if (color.equals(Color.TRANSPARENT)) { color = Color.WHITE; } return color; } private void applyTextFormatting() { if (getCommandState(BULLETS.getCommand()) || getCommandState(NUMBERS.getCommand())) { return; } if (webPage.getClientCommittedTextLength() == 0) { String format = formatStyleMap.get(formatComboBox.getValue()); String font = fontFamilyComboBox.getValue().toString(); executeCommand(FORMAT.getCommand(), format); executeCommand(FONT_FAMILY.getCommand(), font); } } void print(PrinterJob job) { webView.getEngine().print(job); } /*************************************************************************** * * * Support Classes * * * **************************************************************************/ /** * Represents commands that can be passed into the HTMLEditor web engine. */ public enum Command { CUT("cut"), COPY("copy"), PASTE("paste"), UNDO("undo"), REDO("redo"), INSERT_HORIZONTAL_RULE("inserthorizontalrule"), ALIGN_LEFT("justifyleft"), ALIGN_CENTER("justifycenter"), ALIGN_RIGHT("justifyright"), ALIGN_JUSTIFY("justifyfull"), BULLETS("insertUnorderedList"), NUMBERS("insertOrderedList"), INDENT("indent"), OUTDENT("outdent"), FORMAT("formatblock"), FONT_FAMILY("fontname"), FONT_SIZE("fontsize"), BOLD("bold"), ITALIC("italic"), UNDERLINE("underline"), STRIKETHROUGH("strikethrough"), FOREGROUND_COLOR("forecolor"), BACKGROUND_COLOR("backcolor"), INSERT_NEW_LINE("insertnewline"), INSERT_TAB("inserttab"); private final String command; Command(String command) { this.command = command; } public String getCommand() { return command; } } }