/***************************************************************************** * Copyright (c) 2016 Dirk Fauth. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation * *****************************************************************************/ package org.eclipse.nebula.widgets.richtext; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Map; import java.util.Set; import org.eclipse.nebula.widgets.richtext.toolbar.ToolbarButton; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.browser.BrowserFunction; /** * Configuration class that is used for general configurations of the CKEditor instance. * <p> * <b>Note:</b> This configuration class replaces the {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration}. * </p> * * @since 1.1 */ public class RichTextEditorConfiguration { /** * Key for the default language configuration. */ public static final String DEFAULT_LANGUAGE = "defaultLanguage"; /** * Key for the language configuration. */ public static final String LANGUAGE = "language"; /** * Key for toolbar groups configuration. */ public static final String TOOLBAR_GROUPS = "toolbarGroups"; /** * Key for toolbar buttons that should not be rendered. */ public static final String REMOVE_BUTTONS = "removeButtons"; /** * Key to configure whether the toolbar can be collapsed by the user. */ public static final String TOOLBAR_CAN_COLLAPSE = "toolbarCanCollapse"; /** * Key to configure whether the toolbar must start expanded when the editor is loaded. */ public static final String TOOLBAR_STARTUP_EXPANDED = "toolbarStartupExpanded"; /** * Key to configure a list of plugins that must not be loaded. */ public static final String REMOVE_PLUGINS = "removePlugins"; /** * Key to configure whether to enable the resizing feature. */ public static final String RESIZE_ENABLED = "resize_enabled"; /** * Key to configure the dimensions for which the editor resizing is enabled. Possible values are * <i>both</i>, <i>vertical</i>, and <i>horizontal</i>. */ public static final String RESIZE_DIR = "resize_dir"; /** * Key to configure the minimum editor width, in pixels, when resizing the editor interface by * using the resize handle.. */ public static final String RESIZE_MINWIDTH = "resize_minWidth"; /** * Key to configure the minimum editor height, in pixels, when resizing the editor interface by * using the resize handle.. */ public static final String RESIZE_MINHEIGHT = "resize_minHeight"; /** * Configure whether to remove the <i>paste text</i> button from the * toolbar. Default is <code>true</code>. */ private boolean removePasteText = true; /** * Configure whether to remove the <i>paste from word</i> button from the * toolbar. Default is <code>true</code>. */ private boolean removePasteFromWord = true; /** * Configure whether to remove the <i>styles</i> combo box from the toolbar. * Default is <code>true</code>. */ private boolean removeStyles = true; /** * Configure whether to remove <i>format</i> combo box from the toolbar. * Default is <code>true</code>. */ private boolean removeFormat = true; private Set<String> removedButtons = new HashSet<>(); private Browser browser; private Set<ToolbarButton> customButtons = new LinkedHashSet<>(); private Map<String, BrowserFunction> buttonCallbacks = new HashMap<>(); private Map<String, Object> options = new HashMap<>(); /** * Creates a new instance for general configurations that are added to the created CKEditor * instance at initialization. */ public RichTextEditorConfiguration() { this.options.put(DEFAULT_LANGUAGE, Locale.ENGLISH.getLanguage()); this.options.put(LANGUAGE, Locale.getDefault().getLanguage()); // remove the bottom bar that shows the applied tags this.options.put(REMOVE_PLUGINS, "elementspath"); // disable the ability to manually resize the editor this.options.put(RESIZE_ENABLED, Boolean.FALSE); // only show toolbar buttons for features that are supported by the RichTextPainter this.options.put(TOOLBAR_GROUPS, "[" + "{ name: 'clipboard', groups: [ 'clipboard', 'undo', 'find' ] }," + "{ name: 'other' }," + "'/'," + "{ name: 'paragraph', groups: [ 'list', 'indent', 'align' ] }," + "{ name: 'colors' }," + "'/'," + "{ name: 'styles' }," + "{ name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] }" + "]"); this.options.put(REMOVE_BUTTONS, getRemoveButtonConfiguration()); } /** * Creates a {@link RichTextEditorConfiguration} that is initialized with the configuration * values out of the given * {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration}. This constructor is * used for backwards compatibility only in case adopters use the old ToolbarConfiguration. * Therefore it is deprecated from the beginning. * * @param config * The {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration} that * should be used to initialized the {@link RichTextEditorConfiguration}. * @deprecated Use the no-arg constructor instead and set the values directly to the created * {@link RichTextEditorConfiguration} * @since 1.2 */ @Deprecated public RichTextEditorConfiguration(org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration config) { this(); this.removePasteText = config.removePasteText; this.removePasteFromWord = config.removePasteFromWord; this.removeStyles = config.removeStyles; this.removeFormat = config.removeFormat; this.removedButtons.addAll(config.getRemovedButtons()); setToolbarCollapsible(config.toolbarCollapsible); setToolbarInitialExpanded(config.toolbarInitialExpanded); String[] toolbarButtonConfigurations = config.getToolbarButtonConfigurations(); // set the option like this in case the method itself was overridden by subclassing String tbc = toolbarButtonConfigurations[0]; tbc = tbc.substring(tbc.indexOf("=")+1, tbc.length()-1); this.options.put(TOOLBAR_GROUPS, tbc); // set the option like this in case the method itself was overridden by subclassing String rbc = toolbarButtonConfigurations[1]; rbc = rbc.substring(rbc.indexOf("'")+1, rbc.lastIndexOf("'")); this.options.put(REMOVE_BUTTONS, rbc.trim()); this.customButtons.addAll(config.getCustomButtons()); this.buttonCallbacks.putAll(config.getButtonCallbacks()); } /** * Adds a new option to the configuration. * * @param key * The configuration option key. * @param value * The configuration option value. * * @see <a href="http://docs.ckeditor.com/#!/api/CKEDITOR.config">CKEDITOR.config</a> */ public void setOption(String key, Object value) { this.options.put(key, value); } /** * Returns a configuration option set in this {@link RichTextEditorConfiguration}. * * @param key * The configuration option key for which the value is requested. * @return The configuration option value for the given key or <code>null</code> in case there * is nothing configured for that key. */ public Object getOption(String key) { return this.options.get(key); } /** * @return An unmodifiable map that contains all configuration option values. */ public Map<String, Object> getAllOptions() { return Collections.unmodifiableMap(this.options); } // convenience methods /** * @param lang * The user interface language localization to use. If left empty, the editor will * automatically be localized to the user language. If the user language is not * supported, the language specified in the <i>defaultLanguage</i> configuration * setting is used. */ public void setLanguage(String lang) { this.options.put(LANGUAGE, lang); } /** * @param locale * The user interface language localization to use. If left empty, the editor will * automatically be localized to the user language. If the user language is not * supported, the language specified in the <i>defaultLanguage</i> configuration * setting is used. */ public void setLanguage(Locale locale) { setLanguage(locale.getLanguage()); } /** * @param lang * The language to be used if the language setting is left empty and it is not * possible to localize the editor to the user language. */ public void setDefaultLanguage(String lang) { this.options.put(DEFAULT_LANGUAGE, lang); } /** * @param locale * The language to be used if the language setting is left empty and it is not * possible to localize the editor to the user language. */ public void setDefaultLanguage(Locale locale) { setDefaultLanguage(locale.getLanguage()); } /** * Whether to enable the resizing feature. If this feature is disabled, the resize handle will * not be visible. * * @param resizable * <code>true</code> to enable the resizing feature. */ public void setResizable(boolean resizable) { this.options.put(RESIZE_ENABLED, resizable); } /** * The minimum editor size, in pixels, when resizing the editor interface by using the resize * handle. Note: It falls back to editor's actual height if it is smaller than the default * value. * * @param minWidth * the minimum editor width, in pixels * @param minHeight * the minimum editor height, in pixels */ public void setMinSize(int minWidth, int minHeight) { this.options.put(RESIZE_MINWIDTH, minWidth); this.options.put(RESIZE_MINHEIGHT, minHeight); } /** * @param direction * The dimensions for which the editor resizing is enabled. Possible values are * <code>both</code>, <code>vertical</code>, and <code>horizontal</code>. */ public void setResizeDirection(String direction) { this.options.put(RESIZE_DIR, direction); } /** * Configure if the toolbar should be collapsible. Default is <code>false</code>. * * @param toolbarCollapsible * <code>true</code> if the toolbar should be collapsible, <code>false</code> if not. */ public void setToolbarCollapsible(boolean toolbarCollapsible) { this.options.put(TOOLBAR_CAN_COLLAPSE, toolbarCollapsible); } /** * Configure if the toolbar should be initially expanded. Is only interpreted if * {@link #toolbarCollapsible} is set to <code>true</code>. Default is <code>true</code>. * * @param toolbarInitialExpanded * <code>true</code> if the toolbar should be initially expanded, <code>false</code> * if not. */ public void setToolbarInitialExpanded(boolean toolbarInitialExpanded) { this.options.put(TOOLBAR_STARTUP_EXPANDED, toolbarInitialExpanded); } /** * * @param removePasteText * <code>true</code> to remove the <i>paste text</i> button from the toolbar. */ public void setRemovePasteText(boolean removePasteText) { this.removePasteText = removePasteText; this.options.put(REMOVE_BUTTONS, getRemoveButtonConfiguration()); } /** * * @param removePasteFromWord * <code>true</code> to remove the <i>paste from word</i> button from the toolbar. */ public void setRemovePasteFromWord(boolean removePasteFromWord) { this.removePasteFromWord = removePasteFromWord; this.options.put(REMOVE_BUTTONS, getRemoveButtonConfiguration()); } /** * * @param removeStyles * <code>true</code> to remove the <i>styles</i> combo box from the toolbar. */ public void setRemoveStyles(boolean removeStyles) { this.removeStyles = removeStyles; this.options.put(REMOVE_BUTTONS, getRemoveButtonConfiguration()); } /** * * @param removeFormat * <code>true</code> to remove <i>format</i> combo box from the toolbar. */ public void setRemoveFormat(boolean removeFormat) { this.removeFormat = removeFormat; this.options.put(REMOVE_BUTTONS, getRemoveButtonConfiguration()); } /** * Adds the CKEditor default button for the given name to the toolbar. * <p> * <i>Note: This works only for buttons that have been removed using * {@link #removeDefaultToolbarButton(String[])}</i> * </p> * * @param buttonNames * The names of the CKEditor default button to add. */ public void addDefaultToolbarButton(String... buttonNames) { for (String buttonName : buttonNames) { this.removedButtons.remove(buttonName); } this.options.put(REMOVE_BUTTONS, getRemoveButtonConfiguration()); } /** * Removes the CKEditor default button for the given name from the toolbar. * * @param buttonNames * The names of the CKEditor default button to remove. */ public void removeDefaultToolbarButton(String... buttonNames) { // remember the button that should be removed for (String buttonName : buttonNames) { this.removedButtons.add(buttonName); } this.options.put(REMOVE_BUTTONS, getRemoveButtonConfiguration()); } /** * * @return The configuration which default buttons should be removed from * the toolbar. */ private String getRemoveButtonConfiguration() { // Subscript and Superscript are not supported styling options for the // Rich Text Viewer StringBuilder builder = new StringBuilder("'Subscript,Superscript"); if (removePasteText) { builder.append(",PasteText"); } if (removePasteFromWord) { builder.append(",PasteFromWord"); } if (removeStyles) { builder.append(",Styles"); } if (removeFormat) { builder.append(",Format"); } for (String removed : this.removedButtons) { builder.append(",").append(removed); } builder.append("'"); return builder.toString(); } /** * * @return The configuration for adding custom commands and buttons to the * toolbar. */ protected String getCustomButtonConfiguration() { StringBuilder builder = new StringBuilder(); for (ToolbarButton button : this.customButtons) { // add the command for the callback builder.append("CKEDITOR.instances.editor.addCommand('").append(button.getCommandName()).append("', {"); builder.append("exec: function(edt) {"); if (button.getJavascriptToExecute() == null) { builder.append("javaExecutionStarted();"); BrowserFunction function = this.buttonCallbacks.get(button.getCommandName()); builder.append(function.getName()).append("();"); builder.append("javaExecutionFinished()"); } else { builder.append(button.getJavascriptToExecute()); } builder.append("}});"); // add the button builder.append("CKEDITOR.instances.editor.ui.addButton('").append(button.getButtonName()).append("', {"); builder.append("label: '").append(button.getButtonLabel()).append("',"); builder.append("command: '").append(button.getCommandName()).append("',"); builder.append("toolbar: '").append(button.getToolbar()).append("',"); if (button.getIconURL() != null) { builder.append("icon: '").append(button.getIconURL().toString()).append("',"); } builder.append("});"); } return builder.toString(); } /** * Adds a custom button to the CKEditor toolbar. Internally creates an * anonymous {@link BrowserFunction} that executes * {@link ToolbarButton#execute()} via callback on pressing the button. * * @param button * The button to add. */ public void addToolbarButton(final ToolbarButton button) { if (this.browser != null) { // create the BrowserFunction for the callback addToolbarButton(button, new BrowserFunction(browser, button.getCommandName()) { @Override public Object function(Object[] arguments) { return button.execute(); } }); } else if (!this.customButtons.contains(button)) { this.customButtons.add(button); } } /** * Adds a custom button to the CKEditor toolbar. Executes the given * {@link BrowserFunction} via callback on pressing the button. * * @param button * The button to add. * @param function * The {@link BrowserFunction} that should be called on pressing * the button. */ public void addToolbarButton(ToolbarButton button, BrowserFunction function) { if (this.buttonCallbacks.containsKey(button.getCommandName())) { // if there is already a BrowserFunction registered for the command // name we dispose it for clean resource handling so we can register // the new one this.buttonCallbacks.get(button.getCommandName()).dispose(); } this.buttonCallbacks.put(button.getCommandName(), function); if (!this.customButtons.contains(button)) { this.customButtons.add(button); } // ensure that the added button wasn't removed before this.removedButtons.remove(button.getButtonName()); } /** * Removes the given {@link ToolbarButton} from the local list of custom * toolbar buttons. * * @param button * The {@link ToolbarButton} to remove. */ public void removeToolbarButton(ToolbarButton button) { // remove from local lists so it is not added again on reload this.customButtons.remove(button); if (this.buttonCallbacks.containsKey(button.getCommandName())) { this.buttonCallbacks.get(button.getCommandName()).dispose(); this.buttonCallbacks.remove(button.getCommandName()); } // remember the button that should be removed // I currently don't know a better way to do this this.removedButtons.add(button.getButtonName()); } /** * Adds custom buttons to the toolbar of the CKEditor based on the configurations applied in * this {@link RichTextEditorConfiguration}. */ public void customizeToolbar() { browser.evaluate(getCustomButtonConfiguration()); } /** * @return The {@link Browser} instance to which this * {@link RichTextEditorConfiguration} is connected to. */ public Browser getBrowser() { return browser; } /** * * @param browser * The {@link Browser} instance to which this * {@link RichTextEditorConfiguration} should be connected to. */ public void setBrowser(Browser browser) { this.browser = browser; // if a browser is set we ensure that the registered custom buttons // are registered and already registered BrowserFunctions are disposed for (ToolbarButton button : this.customButtons) { addToolbarButton(button); } } /** * Dispose the registered {@link BrowserFunction}s. */ public void dispose() { // dispose the registered BrowserFunctions for (BrowserFunction function : this.buttonCallbacks.values()) { function.dispose(); } } }