/*****************************************************************************
* Copyright (c) 2015, 2016 CEA LIST.
*
* 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.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.nebula.widgets.richtext.toolbar.JavaCallbackListener;
import org.eclipse.nebula.widgets.richtext.toolbar.ToolbarButton;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.browser.BrowserFunction;
import org.eclipse.swt.browser.ProgressEvent;
import org.eclipse.swt.browser.ProgressListener;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
/**
* Rich Text Editor control that wraps a {@link Browser} with enabled Javascript that shows a simple
* HTML template containing a ckeditor as rich text editor.
*
* <p>
* The following style bits are supported:
* <ul>
* <li>{@link SWT#RESIZE} - specify if the resize function of ckeditor is enabled (mostly used for
* embedded usage)</li>
* <li>{@link SWT#MIN} - specify if the configured minimum dimensions should be applied to the
* resize function of ckeditor</li>
* <li>{@link SWT#EMBEDDED} - specify if the rich text editor is used in embedded mode (e.g. as a
* cell editor of a JFace viewer)</li>
* </ul>
* Additionally the SWT Browser style bits {@link SWT#MOZILLA} or {@link SWT#WEBKIT} can be set to
* specify the native browser that should be used for rendering
* </p>
*
* @see <a href="http://ckeditor.com/">http://ckeditor.com/</a>
*/
public class RichTextEditor extends Composite {
private Double CKEDITOR_ALT;
private Double CKEDITOR_CTRL;
private Double CKEDITOR_SHIFT;
private boolean editorLoaded = false;
private String initialValue = null;
private boolean initialSetFocus = false;
protected Rectangle resizedBounds = null;
private final Browser browser;
private final List<BrowserFunction> browserFunctions = new ArrayList<>();
private final ListenerList modifyListener = new ListenerList(ListenerList.IDENTITY);
private final ListenerList keyListener = new ListenerList(ListenerList.IDENTITY);
private final ListenerList focusListener = new ListenerList(ListenerList.IDENTITY);
private final ListenerList javaCallbackListener = new ListenerList(ListenerList.IDENTITY);
private final RichTextEditorConfiguration editorConfig;
private Shell embeddedShell;
private Point mouseDragPosition;
private boolean handleFocusChanges = true;
/**
* Key of the system property to specify a fixed directory to unpack the ckeditor resources to.
* If a system property for that key is registered and the rich text control is deployed within
* a JAR, the resources will be unpacked into the specified directory. If no value is registered
* for that key and the rich text control is deployed in a JAR, the resources will be unpacked
* into a temporary directory, that gets deleted when the runtime is shutdown. If the rich text
* control is not deployed within a JAR but as part of an Eclipse application, the bundle will
* be unpacked automatically. In this case this system property won't get interpreted.
*/
public static final String JAR_UNPACK_LOCATION_PROPERTY = "org.eclipse.nebula.widgets.richtext.jar.unpackdir";
private static URL templateURL;
static {
locateTemplateURL();
}
/**
* Creates a {@link RichTextEditor} that wraps a {@link Browser} using the style bit
* {@link SWT#NONE} and the default {@link RichTextEditorConfiguration}.
*
* @param parent
* the parent composite where this rich text editor should be added to
*/
public RichTextEditor(Composite parent) {
this(parent, (RichTextEditorConfiguration) null, SWT.NONE);
}
/**
* Creates a {@link RichTextEditor} that wraps a {@link Browser} using the style bit
* {@link SWT#NONE} and creates a {@link RichTextEditorConfiguration} out of the given
* {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration}.
*
* @param parent
* the parent composite where this rich text editor should be added to
* @param toolbarConfig
* the {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration} to
* use or <code>null</code> for using the default
* {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration}
* @deprecated use constructors that take a {@link RichTextEditorConfiguration}
*/
@Deprecated
public RichTextEditor(Composite parent, org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration toolbarConfig) {
this(parent, toolbarConfig, SWT.NONE);
}
/**
* Creates a {@link RichTextEditor} that wraps a {@link Browser} using the style bit
* {@link SWT#NONE} and the given {@link RichTextEditorConfiguration}.
*
* @param parent
* the parent composite where this rich text editor should be added to
* @param editorConfig
* the {@link RichTextEditorConfiguration} to use or <code>null</code> for using the default
* {@link RichTextEditorConfiguration}
*/
public RichTextEditor(Composite parent, RichTextEditorConfiguration editorConfig) {
this(parent, editorConfig, SWT.NONE);
}
/**
* Creates a {@link RichTextEditor} that wraps a {@link Browser} using the given style bit
* and the default {@link RichTextEditorConfiguration}.
*
* @param parent
* the parent composite where this rich text editor should be added to
* @param style
* the style of widget to construct, see {@link Browser} for further style bit
* information
*/
public RichTextEditor(Composite parent, int style) {
this(parent, (RichTextEditorConfiguration) null, style);
}
/**
* Creates a {@link RichTextEditor} that wraps a {@link Browser} using the given style bit and
* creates a {@link RichTextEditorConfiguration} out of the given {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration}.
*
* @param parent
* the parent composite where this rich text editor should be added to
* @param toolbarConfig
* the {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration} to use or <code>null</code> for using the default
* {@link org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration}
* @param style
* the style of widget to construct, see {@link Browser} for further style bit
* information
*
* @see Browser
* @deprecated use constructors that take a {@link RichTextEditorConfiguration}
*/
@Deprecated
public RichTextEditor(Composite parent, org.eclipse.nebula.widgets.richtext.toolbar.ToolbarConfiguration toolbarConfig, int style) {
this(parent, toolbarConfig != null ? new RichTextEditorConfiguration(toolbarConfig) : null, style);
}
/**
* Creates a {@link RichTextEditor} that wraps a {@link Browser} using the given style bit
* and the given {@link RichTextEditorConfiguration}.
*
* @param parent
* the parent composite where this rich text editor should be added to
* @param editorConfig
* the {@link RichTextEditorConfiguration} to use or <code>null</code> for using the default
* {@link RichTextEditorConfiguration}
* @param style
* the style of widget to construct, see {@link Browser} for further style bit
* information
*
* @see Browser
*/
public RichTextEditor(Composite parent, RichTextEditorConfiguration editorConfig, int style) {
super(parent, style);
setLayout(new FillLayout());
final boolean resizable = (getStyle() & SWT.RESIZE) != 0;
final boolean embedded = (getStyle() & SWT.EMBEDDED) != 0;
if (embedded) {
this.embeddedShell = new Shell(parent.getShell(), SWT.MODELESS);
this.embeddedShell.setLayout(new FillLayout());
}
// remove styles that are not relevant for the browser
int browserStyle = style & ~SWT.RESIZE & ~SWT.MIN & ~SWT.EMBEDDED;
this.browser = new Browser(!embedded ? this : this.embeddedShell, browserStyle);
this.browser.setJavascriptEnabled(true);
// init editor configuration
if (editorConfig == null) {
this.editorConfig = new RichTextEditorConfiguration();
}
else {
this.editorConfig = editorConfig;
}
this.editorConfig.setBrowser(this.browser);
// if SWT.RESIZE is set, we update the configuration
if (resizable) {
boolean specifyMin = ((getStyle() & SWT.MIN) != 0);
int minWidth = specifyMin ? getMinimumWidth() : 0;
int minHeight = specifyMin ? getMinimumHeight() : 0;
this.editorConfig.setResizable(resizable);
this.editorConfig.setMinSize(minWidth, minHeight);
this.editorConfig.setResizeDirection("both");
}
this.browser.setUrl(templateURL.toString());
this.browserFunctions.add(new ModifyFunction(browser, "textModified"));
this.browserFunctions.add(new KeyPressedFunction(browser, "keyPressed"));
this.browserFunctions.add(new KeyReleasedFunction(browser, "keyReleased"));
this.browserFunctions.add(new FocusInFunction(browser, "focusIn"));
this.browserFunctions.add(new FocusOutFunction(browser, "focusOut"));
this.browserFunctions.add(new JavaExecutionStartedFunction(browser, "javaExecutionStarted"));
this.browserFunctions.add(new JavaExecutionFinishedFunction(browser, "javaExecutionFinished"));
this.browserFunctions.add(new BrowserFunction(browser, "customizeToolbar") {
@Override
public Object function(Object[] arguments) {
RichTextEditor.this.editorConfig.customizeToolbar();
return super.function(arguments);
}
});
this.browserFunctions.add(new BrowserFunction(browser, "getAllOptions") {
@Override
public Object function(Object[] arguments) {
// transform the configuration options map into an Object array
// necessary as map is not a supported return value
Map<String, Object> options = RichTextEditor.this.editorConfig.getAllOptions();
Object[] result = new Object[options.size()*2];
int i = 0;
for (Map.Entry<String, Object> entry : options.entrySet()) {
result[i++] = entry.getKey();
result[i++] = entry.getValue() != null ? entry.getValue() : "";
}
return result;
}
});
this.browser.addProgressListener(new ProgressListener() {
@Override
public void completed(ProgressEvent event) {
browser.evaluate("initEditor();");
CKEDITOR_ALT = (Double) browser.evaluate("return getCKEditorALT()");
CKEDITOR_CTRL = (Double) browser.evaluate("return getCKEditorCTRL()");
CKEDITOR_SHIFT = (Double) browser.evaluate("return getCKEditorSHIFT()");
editorLoaded = true;
// only add this function for resizable inline editing
if (resizable && embedded) {
// register the callback to resize the browser if the
// ckeditor is resized
browserFunctions.add(new BrowserFunction(browser, "updateDimensions") {
@Override
public Object function(Object[] arguments) {
// width and height +2 because the editor is 1 pixel
// smaller than the browser container on every side
setInlineContainerBounds(
getBounds().x,
getBounds().y,
((Double) arguments[0]).intValue() + 2,
((Double) arguments[1]).intValue() + 2);
// also repaint the parent control to avoid
// rendering glitches while resizing
if (getParent() != null) {
getParent().redraw();
getParent().update();
}
return super.function(arguments);
}
});
browser.evaluate("enableResizeCallback();");
if (embedded) {
browserFunctions.add(new BrowserFunction(browser, "activateShellDragMode") {
@Override
public Object function(Object[] arguments) {
mouseDragPosition = new Point(((Double) arguments[0]).intValue(), ((Double) arguments[1]).intValue());
return super.function(arguments);
};
});
browserFunctions.add(new BrowserFunction(browser, "deactivateShellDragMode") {
@Override
public Object function(Object[] arguments) {
mouseDragPosition = null;
return super.function(arguments);
};
});
browserFunctions.add(new BrowserFunction(browser, "moveShell") {
@Override
public Object function(Object[] arguments) {
if (mouseDragPosition != null) {
Point cursorLocation = Display.getDefault().getCursorLocation();
embeddedShell.setLocation(cursorLocation.x - mouseDragPosition.x, cursorLocation.y - mouseDragPosition.y);
}
return super.function(arguments);
};
});
browser.evaluate("addMoveAnchor();");
}
}
if (initialValue != null) {
setText(initialValue);
}
if (initialSetFocus) {
setFocus();
}
}
@Override
public void changed(ProgressEvent event) {
}
});
}
@Override
public void dispose() {
// dispose the editor configuration
editorConfig.dispose();
// dispose the registered BrowserFunctions
for (BrowserFunction function : browserFunctions) {
function.dispose();
}
// dispose the Browser
browser.dispose();
// dispose the embedded shell if we are in embedded mode
if (this.embeddedShell != null && !this.embeddedShell.isDisposed()) {
this.embeddedShell.dispose();
}
// call super to ensure the resources of this composite are released
super.dispose();
}
@Override
public void setVisible(boolean visible) {
if (this.embeddedShell != null && !this.embeddedShell.isDisposed()) {
this.embeddedShell.setVisible(visible);
}
super.setVisible(visible);
}
/**
* @return The text that is currently set in the editing area. Contains HTML tags for
* formatting.
*/
public String getText() {
if (browser != null && !browser.isDisposed()) {
Object result = browser.evaluate("return getText()");
return result != null ? result.toString() : null;
}
return null;
}
/**
* Set text to the editing area. Can contain HTML tags for styling.
*
* @param text
* The text to set to the editing area.
*/
public void setText(String text) {
if (editorLoaded) {
text = text.replace("\n", "\\n").replace("\r", "\\r");
browser.evaluate("setText('" + text + "')");
}
else {
initialValue = text;
}
}
/**
* Insert text content into the currently selected position in the editor in WYSIWYG mode. The
* styles of the selected element will be applied to the inserted text. Spaces around the text
* will be left untouched.
*
* @param text
* Text to be inserted into the editor.
*/
public void insertText(String text) {
browser.evaluate("insertText('" + text + "')");
}
/**
* Inserts HTML code into the currently selected position in the editor in WYSIWYG mode.
*
* @param html
* HTML code to be inserted into the editor.
*/
public void insertHTML(String html) {
browser.evaluate("insertHTML('" + html + "')");
}
/**
* Returns the current selected text without any markup tags.
*
* @return The current selected text without any markup tags.
*/
public String getSelectedText() {
return "" + browser.evaluate("return getSelectedText();");
}
/**
* Returns the current selected text containing the markup tags for styling.
* <p>
* <i>Note: It will not contain the parent tag.</i>
* </p>
*
* @return The current selected text containing any markup styling tags.
*/
public String getSelectedHTML() {
Object html = browser.evaluate("return getSelectedHTML()");
return html != null ? html.toString() : "";
}
/**
* Returns the editable state.
*
* @return whether or not the receiver is editable
*
*/
public boolean isEditable() {
Object result = browser.evaluate("return isEditable()");
return result != null ? !Boolean.valueOf(result.toString()) : true;
}
/**
* Update the toolbar. Typically used if buttons where added or removed at runtime.
*/
public void updateToolbar() {
editorConfig.customizeToolbar();
browser.evaluate("updateToolbar();");
}
/**
* Update the editor. Basically it will destroy and recreate the CKEditor.
* Needed to be used if a basic configuration is changed, e.g. the language.
*
* @since 1.1
*/
public void updateEditor() {
browser.evaluate("updateEditor();");
}
/**
* Sets the editable state.
*
* @param editable
* the new editable state
*/
public void setEditable(boolean editable) {
browser.evaluate("setReadOnly(" + !editable + ")");
}
/**
* This method returns the {@link RichTextEditorConfiguration} that is used to configure this
* {@link RichTextEditor}. It can be used to change some configurations at runtime.
* <p>
* <b>Note:</b> After configuration values have been changed it is necessary to call
* {@link #updateEditor()} so the configurations are applied.
* </p>
*
* @return The {@link RichTextEditorConfiguration} used to configure this
* {@link RichTextEditor}.
*
* @since 1.1
*/
public RichTextEditorConfiguration getEditorConfiguration() {
return this.editorConfig;
}
/**
* Sets the user interface language localization to use. Only the language part of the
* {@link Locale} will be used. This method triggers an immediate update of the editor instance.
*
* @param locale
* The user interface language localization to use.
* @since 1.1
*/
public void setLanguage(Locale locale) {
setLanguage(locale, true);
}
/**
*
* @param locale
* The user interface language localization to use.
* @param update
* <code>true</code> if the editor should be updated immediately, <code>false</code>
* if the update should not be executed. In that case {@link #updateEditor()} needs
* to be executed explicitly.
* @since 1.1
*/
public void setLanguage(Locale locale, boolean update) {
setLanguage(locale.getLanguage(), update);
}
/**
* Sets the user interface language localization to use. This method triggers an immediate
* update of the editor instance.
*
* @param language
* The user interface language localization to use.
* @since 1.1
*/
public void setLanguage(String language) {
setLanguage(language, true);
}
/**
*
* @param language
* The user interface language localization to use.
* @param update
* <code>true</code> if the editor should be updated immediately, <code>false</code>
* if the update should not be executed. In that case {@link #updateEditor()} needs
* to be executed explicitly.
* @since 1.1
*/
public void setLanguage(String language, boolean update) {
this.editorConfig.setLanguage(language);
if (update) {
updateEditor();
}
}
/**
* Adds the given {@link ToolbarButton} to the toolbar of the editor.
*
* @param button
* The button to add.
*
* @see RichTextEditorConfiguration#addToolbarButton(ToolbarButton)
*/
public void addToolbarButton(ToolbarButton button) {
editorConfig.addToolbarButton(button);
}
/**
* Adds the given {@link ToolbarButton} to the toolbar of the editor. Uses the given
* {@link BrowserFunction} as callback for the button.
*
* @param button
* The button to add.
* @param function
* The function to use as callback.
*
* @see RichTextEditorConfiguration#addToolbarButton(ToolbarButton, BrowserFunction)
*/
public void addToolbarButton(ToolbarButton button, BrowserFunction function) {
editorConfig.addToolbarButton(button, function);
}
/**
* Removes the given {@link ToolbarButton} from the toolbar of the editor.
*
* @param button
* The button to remove.
*
* @see RichTextEditorConfiguration#removeToolbarButton(ToolbarButton)
*/
public void removeToolbarButton(ToolbarButton button) {
editorConfig.removeToolbarButton(button);
}
@Override
public boolean setFocus() {
if (editorLoaded) {
browser.evaluate("setFocus();");
Object result = browser.evaluate("return hasFocus();");
return result != null ? Boolean.valueOf(result.toString()) : false;
}
else {
initialSetFocus = true;
return true;
}
}
@Override
public boolean forceFocus() {
return setFocus();
}
@Override
public boolean isFocusControl() {
if (editorLoaded) {
Object result = browser.evaluate("return hasFocus();");
return result != null ? Boolean.valueOf(result.toString()) : false;
}
return false;
}
@Override
public void addFocusListener(FocusListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.focusListener.add(listener);
}
@Override
public void removeFocusListener(FocusListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.focusListener.remove(listener);
}
/**
* Notify the registered {@link FocusListener} that the editor gained focus.
*
* @param event
* The event to fire.
*/
public void notifyFocusGained(final FocusEvent event) {
checkWidget();
// do not handle focus events, e.g. in case a Java callback execution is running
if (event == null || !this.handleFocusChanges) {
return;
}
if (event.display != null) {
event.display.asyncExec(new Runnable() {
@Override
public void run() {
doNotifyFocusGained(event);
}
});
}
else {
// no display in the event, fire the events synchronously
doNotifyFocusGained(event);
}
}
private void doNotifyFocusGained(FocusEvent event) {
Object[] listeners = this.focusListener.getListeners();
for (int i = 0; i < listeners.length; ++i) {
((FocusListener) listeners[i]).focusGained(event);
}
}
/**
* Notify the registered {@link FocusListener} that the editor lost focus.
*
* @param event
* The event to fire.
*/
public void notifyFocusLost(final FocusEvent event) {
checkWidget();
// do not handle focus events, e.g. in case a Java callback execution is running
if (event == null || !this.handleFocusChanges) {
return;
}
if (event.display != null) {
event.display.asyncExec(new Runnable() {
@Override
public void run() {
doNotifyFocusLost(event);
}
});
}
else {
// no display in the event, fire the events synchronously
doNotifyFocusLost(event);
}
}
private void doNotifyFocusLost(FocusEvent event) {
Object[] listeners = this.focusListener.getListeners();
for (int i = 0; i < listeners.length; ++i) {
((FocusListener) listeners[i]).focusLost(event);
}
}
@Override
public void setBounds(Rectangle rect) {
setBounds(rect.x, rect.y, rect.width, rect.height);
}
@Override
public void setBounds(int x, int y, int width, int height) {
int newX = x;
int newY = y;
int newWidth = width;
int newHeight = height;
if (resizedBounds != null) {
newX = resizedBounds.x;
newY = resizedBounds.y;
newWidth = resizedBounds.width;
newHeight = resizedBounds.height;
}
else if ((getStyle() & SWT.EMBEDDED) != 0) {
// ensure min size when opened inline
newHeight = Math.max(height, getMinimumHeight());
newWidth = Math.max(width, getMinimumWidth());
}
if (this.embeddedShell != null) {
Point shellLocation = super.toDisplay(newX, newY);
this.embeddedShell.setBounds(shellLocation.x, shellLocation.y, newWidth, newHeight);
this.embeddedShell.setVisible(true);
}
else {
super.setBounds(newX, newY, newWidth, newHeight);
}
}
/**
* Used in embedded mode to support manual resizing of the editor. Executed via callback on
* ckeditor resize.
*
* @param x
* the new x coordinate for the receiver
* @param y
* the new y coordinate for the receiver
* @param width
* the new width for the receiver
* @param height
* the new height for the receiver
*/
void setInlineContainerBounds(int x, int y, int width, int height) {
this.resizedBounds = new Rectangle(x, y, width, height);
if (this.embeddedShell != null) {
Point shellLocation = this.embeddedShell.getLocation();
this.embeddedShell.setBounds(shellLocation.x, shellLocation.y, width + 2, height + 2);
}
else {
super.setBounds(x, y, width, height);
}
}
/**
* Returns the minimum height that should be used for initially open the editor in embedded
* mode. It is also used to specify the resize minimum height if the editor was created using
* the style bit {@link SWT#MIN}. Using the default {@link RichTextEditorConfiguration} this is
* 150 for the toolbar and 50 for showing one row in the editor area.
*
* @return The minimum height to use for initially open the editor in embedded mode and for
* editor resize minimum in case the editor was created with {@link SWT#MIN}
*/
protected int getMinimumHeight() {
return 200;
}
/**
* Returns the minimum width that should be used for initially open the editor in embedded mode.
* It is also used to specify the resize minimum width if the editor was created using the style
* bit {@link SWT#MIN}. Using the default {@link RichTextEditorConfiguration} this is 370 for
* showing the default options in three lines of the toolbar.
*
* @return The minimum width to use for initially open the editor in embedded mode and for
* editor resize minimum in case the editor was created with {@link SWT#MIN}
*/
protected int getMinimumWidth() {
return 370;
}
/**
* Executes the specified script in the internal {@link Browser}. Can be used to execute
* Javascript directly in the browser from a listener if necessary.
*
* @param script
* the script with javascript commands
* @return <code>true</code> if the operation was successful and <code>false</code> otherwise
* @see Browser#execute(String)
*/
public boolean executeJavascript(String script) {
return this.browser.execute(script);
}
/**
* Evaluates the specified script in the internal {@link Browser} and returns the result. Can be
* used to evaluate Javascript directly in the browser from a listener if necessary.
*
* @param script
* the script with javascript commands
* @return the return value, if any, of executing the script
* @see Browser#evaluate(String)
*/
public Object evaluateJavascript(String script) {
return this.browser.evaluate(script);
}
/**
* @return <code>true</code> if focus changes are handled, <code>false</code> if not
*/
public boolean isHandleFocusChanges() {
return handleFocusChanges;
}
/**
* Configure whether focus changes should be handled or not. A typical use case for disabling
* the focus handling is for example to open a dialog from a Java callback via custom toolbar
* button.
*
* @param handleFocusChanges
* <code>true</code> if focus changes should be handled, <code>false</code> if not
*/
public void setHandleFocusChanges(boolean handleFocusChanges) {
this.handleFocusChanges = handleFocusChanges;
}
@Override
public void addKeyListener(KeyListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.keyListener.add(listener);
}
@Override
public void removeKeyListener(KeyListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.keyListener.remove(listener);
}
/**
* Notify the registered {@link KeyListener} that a key was pressed.
*
* @param event
* The event to fire.
*/
public void notifyKeyPressed(final KeyEvent event) {
checkWidget();
if (event == null) {
return;
}
if (event.display != null) {
event.display.asyncExec(new Runnable() {
@Override
public void run() {
doNotifyKeyPressed(event);
}
});
}
else {
// no display in the event, fire the events synchronously
doNotifyKeyPressed(event);
}
}
private void doNotifyKeyPressed(KeyEvent event) {
Object[] listeners = this.keyListener.getListeners();
for (int i = 0; i < listeners.length; ++i) {
((KeyListener) listeners[i]).keyPressed(event);
}
}
/**
* Notify the registered {@link KeyListener} that a key was released.
*
* @param event
* The event to fire.
*/
public void notifyKeyReleased(final KeyEvent event) {
checkWidget();
if (event == null) {
return;
}
if (event.display != null) {
event.display.asyncExec(new Runnable() {
@Override
public void run() {
doNotifyKeyReleased(event);
}
});
}
else {
// no display in the event, fire the events synchronously
doNotifyKeyReleased(event);
}
}
private void doNotifyKeyReleased(KeyEvent event) {
Object[] listeners = this.keyListener.getListeners();
for (int i = 0; i < listeners.length; ++i) {
((KeyListener) listeners[i]).keyReleased(event);
}
}
/**
* Adds the listener to the collection of listeners who will be notified when the receiver's
* text is modified, by sending it one of the messages defined in the
* <code>ModifyListener</code> interface.
*
* @param listener
* the listener which should be notified
*
* @exception IllegalArgumentException
* <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException
* <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created
* the receiver</li>
* </ul>
*
* @see ModifyListener
* @see #removeModifyListener
*/
public void addModifyListener(ModifyListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.modifyListener.add(listener);
}
/**
* Removes the listener from the collection of listeners who will be notified when the
* receiver's text is modified.
*
* @param listener
* the listener which should no longer be notified
*
* @exception IllegalArgumentException
* <ul>
* <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
* </ul>
* @exception SWTException
* <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created
* the receiver</li>
* </ul>
*
* @see ModifyListener
* @see #addModifyListener
*/
public void removeModifyListener(ModifyListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.modifyListener.remove(listener);
}
/**
* Notifies all of the receiver's listeners when the receiver's text is modified.
*
* @param eventType
* the type of event which has occurred
* @param event
* the event data
*
* @exception SWTException
* <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created
* the receiver</li>
* </ul>
*
* @see #addModifyListener(ModifyListener)
* @see #removeModifyListener(ModifyListener)
*/
public void notifyModifyListeners(final ModifyEvent event) {
checkWidget();
if (event == null) {
return;
}
if (event.display != null) {
event.display.asyncExec(new Runnable() {
@Override
public void run() {
doNotifyModifyText(event);
}
});
}
else {
// no display in the event, fire the events synchronously
doNotifyModifyText(event);
}
}
private void doNotifyModifyText(ModifyEvent event) {
Object[] listeners = this.modifyListener.getListeners();
for (int i = 0; i < listeners.length; ++i) {
((ModifyListener) listeners[i]).modifyText(event);
}
}
/**
* Creates a SWT {@link KeyEvent} out of the given informations.
*
* @param keyCode
* The keyCode sent by ckeditor.
* @param modifier
* The modifier value sent by ckeditor.
* @return The {@link KeyEvent} containing the tranformed key event information.
*/
private KeyEvent createKeyEvent(Double keyCode, Double modifier) {
Event event = new Event();
event.display = this.getDisplay();
event.widget = this;
event.keyCode = keyCode.intValue();
Double modifierOnly = modifier - keyCode;
if (modifierOnly != 0) {
if (modifierOnly.equals(CKEDITOR_ALT + CKEDITOR_CTRL + CKEDITOR_SHIFT)) {
event.stateMask = SWT.MOD3 | SWT.MOD1 | SWT.MOD2;
} else if (modifierOnly.equals(CKEDITOR_ALT + CKEDITOR_CTRL)) {
event.stateMask = SWT.MOD3 | SWT.MOD1;
} else if (modifierOnly.equals(CKEDITOR_ALT + CKEDITOR_SHIFT)) {
event.stateMask = SWT.MOD3 | SWT.MOD2;
} else if (modifierOnly.equals(CKEDITOR_CTRL + CKEDITOR_SHIFT)) {
event.stateMask = SWT.MOD1 | SWT.MOD2;
} else if (modifierOnly.equals(CKEDITOR_ALT)) {
event.stateMask = SWT.MOD3;
} else if (modifierOnly.equals(CKEDITOR_CTRL)) {
event.stateMask = SWT.MOD1;
} else if (modifierOnly.equals(CKEDITOR_SHIFT)) {
event.stateMask = SWT.MOD2;
}
}
// transform function keys
switch (event.keyCode) {
case 33:
event.keyCode = SWT.PAGE_UP;
break;
case 34:
event.keyCode = SWT.PAGE_DOWN;
break;
case 35:
event.keyCode = SWT.END;
break;
case 36:
event.keyCode = SWT.HOME;
break;
case 37:
event.keyCode = SWT.ARROW_LEFT;
break;
case 38:
event.keyCode = SWT.ARROW_UP;
break;
case 39:
event.keyCode = SWT.ARROW_RIGHT;
break;
case 40:
event.keyCode = SWT.ARROW_DOWN;
break;
case 45:
event.keyCode = SWT.INSERT;
break;
case 46:
event.keyCode = SWT.DEL;
break;
case 96:
event.keyCode = SWT.KEYPAD_0;
event.character = '0';
break;
case 97:
event.keyCode = SWT.KEYPAD_1;
event.character = '1';
break;
case 98:
event.keyCode = SWT.KEYPAD_2;
event.character = '2';
break;
case 99:
event.keyCode = SWT.KEYPAD_3;
event.character = '3';
break;
case 100:
event.keyCode = SWT.KEYPAD_4;
event.character = '4';
break;
case 101:
event.keyCode = SWT.KEYPAD_5;
event.character = '5';
break;
case 102:
event.keyCode = SWT.KEYPAD_6;
event.character = '6';
break;
case 103:
event.keyCode = SWT.KEYPAD_7;
event.character = '7';
break;
case 104:
event.keyCode = SWT.KEYPAD_8;
event.character = '8';
break;
case 105:
event.keyCode = SWT.KEYPAD_9;
event.character = '9';
break;
case 106:
event.keyCode = SWT.KEYPAD_MULTIPLY;
event.character = '*';
break;
case 107:
event.keyCode = SWT.KEYPAD_ADD;
event.character = '+';
break;
case 109:
event.keyCode = SWT.KEYPAD_SUBTRACT;
event.character = '-';
break;
case 110:
event.keyCode = SWT.KEYPAD_DECIMAL;
event.character = ',';
break;
case 111:
event.keyCode = SWT.KEYPAD_DIVIDE;
event.character = '/';
break;
case 112:
event.keyCode = SWT.F1;
break;
case 113:
event.keyCode = SWT.F2;
break;
case 114:
event.keyCode = SWT.F3;
break;
case 115:
event.keyCode = SWT.F4;
break;
case 116:
event.keyCode = SWT.F5;
break;
case 117:
event.keyCode = SWT.F6;
break;
case 118:
event.keyCode = SWT.F7;
break;
case 119:
event.keyCode = SWT.F8;
break;
case 120:
event.keyCode = SWT.F9;
break;
case 121:
event.keyCode = SWT.F10;
break;
case 122:
event.keyCode = SWT.F11;
break;
case 123:
event.keyCode = SWT.F12;
break;
}
// character
String keyCharString = SWTKeySupport.getKeyFormatterForPlatform().format(event.keyCode);
if (keyCharString.length() == 1) {
char keyChar = keyCharString.charAt(0);
if (Character.isUpperCase(keyChar) && Character.isAlphabetic(keyChar)) {
if (!((event.stateMask & SWT.MOD2) == SWT.MOD2)) {
event.character = Character.toLowerCase(keyChar);
} else {
event.character = keyChar;
}
} else if (Character.isDigit(keyChar) && !((event.stateMask & SWT.MOD2) == SWT.MOD2) && !((event.stateMask & SWT.MOD3) == SWT.MOD3)) {
event.character = keyChar;
}
}
return new KeyEvent(event);
}
/**
* Add a {@link JavaCallbackListener} that is triggered on executing a Java callback via custom
* {@link ToolbarButton}.
*
* @param listener
* The {@link JavaCallbackListener} to add.
*/
public void addJavaCallbackListener(JavaCallbackListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.javaCallbackListener.add(listener);
}
/**
* Remove a {@link JavaCallbackListener} that is triggered on executing a Java callback via
* custom {@link ToolbarButton}.
*
* @param listener
* The {@link JavaCallbackListener} to remove.
*/
public void removeJavaCallbackListener(JavaCallbackListener listener) {
checkWidget();
if (listener == null)
SWT.error(SWT.ERROR_NULL_ARGUMENT);
this.javaCallbackListener.remove(listener);
}
private void doNotifyJavaExecutionStarted() {
Object[] listeners = this.javaCallbackListener.getListeners();
for (int i = 0; i < listeners.length; ++i) {
((JavaCallbackListener) listeners[i]).javaExecutionStarted();
}
}
private void doNotifyJavaExecutionFinished() {
Object[] listeners = this.javaCallbackListener.getListeners();
for (int i = 0; i < listeners.length; ++i) {
((JavaCallbackListener) listeners[i]).javaExecutionFinished();
}
}
private FocusEvent createFocusEvent() {
Event event = new Event();
event.display = this.getDisplay();
event.widget = this;
return new FocusEvent(event);
}
private ModifyEvent createModifyEvent() {
Event event = new Event();
event.display = this.getDisplay();
event.widget = this;
return new ModifyEvent(event);
}
/**
* Callback function that is called via Javascript if a change event occurs in the editor part.
* Is registered for the Javascript function name <i>textModified</i>.
*/
class ModifyFunction extends BrowserFunction {
public ModifyFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
notifyModifyListeners(createModifyEvent());
return super.function(arguments);
}
}
/**
* Callback function that is called via Javascript if a keydown event occurs in the editor part.
* Is registered for the Javascript function name <i>keyPressed</i>.
*/
class KeyPressedFunction extends BrowserFunction {
public KeyPressedFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
Double keyCode = (Double) arguments[0];
Double modifier = (Double) arguments[1];
notifyKeyPressed(createKeyEvent(keyCode, modifier));
return super.function(arguments);
}
}
/**
* Callback function that is called via Javascript if a keyup event occurs in the editor part.
* Is registered for the Javascript function name <i>keyReleased</i>.
*/
class KeyReleasedFunction extends BrowserFunction {
public KeyReleasedFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
Double keyCode = (Double) arguments[0];
Double modifier = (Double) arguments[1];
notifyKeyReleased(createKeyEvent(keyCode, modifier));
return super.function(arguments);
}
}
/**
* Callback function that is called via Javascript if the editor gains focus. Is registered for
* the Javascript function name <i>focusIn</i>.
*/
class FocusInFunction extends BrowserFunction {
public FocusInFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
notifyFocusGained(createFocusEvent());
return super.function(arguments);
}
}
/**
* Callback function that is called via Javascript on blur in the editor part. Is registered for
* the Javascript function name <i>focusOut</i>.
*/
class FocusOutFunction extends BrowserFunction {
public FocusOutFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
notifyFocusLost(createFocusEvent());
return super.function(arguments);
}
}
/**
* Callback function that is called via Javascript before a java callback is triggered via
* custom toolbar button. Is registered for the Javascript function name
* <i>javaExecutionStarted</i>.
*/
class JavaExecutionStartedFunction extends BrowserFunction {
public JavaExecutionStartedFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
doNotifyJavaExecutionStarted();
return super.function(arguments);
}
}
/**
* Callback function that is called via Javascript after a java callback is triggered via custom
* toolbar button. Is registered for the Javascript function name <i>javaExecutionFinished</i>.
*/
class JavaExecutionFinishedFunction extends BrowserFunction {
public JavaExecutionFinishedFunction(Browser browser, String name) {
super(browser, name);
}
@Override
public Object function(Object[] arguments) {
doNotifyJavaExecutionFinished();
return super.function(arguments);
}
}
private static void locateTemplateURL() {
templateURL = RichTextEditor.class.getResource("resources/template.html");
// if we are in an OSGi context, we need to convert the bundle URL to a file URL
Bundle bundle = FrameworkUtil.getBundle(RichTextEditor.class);
if (bundle != null) {
try {
templateURL = FileLocator.toFileURL(templateURL);
} catch (IOException e) {
e.printStackTrace();
}
}
else if (templateURL.toString().startsWith("jar")) {
BusyIndicator.showWhile(Display.getDefault(), new Runnable() {
@Override
public void run() {
URL jarURL = RichTextEditor.class.getProtectionDomain().getCodeSource().getLocation();
File jarFileReference = null;
if (jarURL.getProtocol().equals("file")) {
try {
String decodedPath = URLDecoder.decode(jarURL.getPath(), "UTF-8");
jarFileReference = new File(decodedPath);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
else {
// temporary download of jar file
// necessary to be able to unzip the resources
try {
final Path jar = Files.createTempFile("richtext", ".jar");
Files.copy(jarURL.openStream(), jar, StandardCopyOption.REPLACE_EXISTING);
jarFileReference = jar.toFile();
// delete the temporary file
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
Files.delete(jar);
} catch (IOException e) {
e.printStackTrace();
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
if (jarFileReference != null) {
try (JarFile jarFile = new JarFile(jarFileReference)) {
String unpackDirectory = System.getProperty(JAR_UNPACK_LOCATION_PROPERTY);
// create the directory to unzip to
final java.nio.file.Path tempDir = (unpackDirectory == null)
? Files.createTempDirectory("richtext") : Files.createDirectories(Paths.get(unpackDirectory));
// only register the hook to delete the temp directory after shutdown if
// a temporary directory was used
if (unpackDirectory == null) {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.startsWith("org/eclipse/nebula/widgets/richtext/resources")) {
File file = new File(tempDir.toAbsolutePath() + File.separator + name);
if (!file.exists()) {
if (entry.isDirectory()) {
file.mkdirs();
}
else {
try (InputStream is = jarFile.getInputStream(entry);
OutputStream os = new FileOutputStream(file)) {
while (is.available() > 0) {
os.write(is.read());
}
}
}
}
// found the template.html in the jar entries, so remember the
// URL for further usage
if (name.endsWith("template.html")) {
templateURL = file.toURI().toURL();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
}
}
}