diff options
| author | Dean Roberts | 2013-09-06 17:36:55 +0000 |
|---|---|---|
| committer | Paul Webster | 2013-09-06 17:36:55 +0000 |
| commit | 9a610cb443fa4f4b5e239ab4b1775e3eb3d93834 (patch) | |
| tree | 214c52b628086415bf7d7188e5d97c87f46fc0fe | |
| parent | 3e6eb9686cacb557a6e5fa8dc6b11152ec93bc5c (diff) | |
| download | org.eclipse.e4.ui-9a610cb443fa4f4b5e239ab4b1775e3eb3d93834.tar.gz org.eclipse.e4.ui-9a610cb443fa4f4b5e239ab4b1775e3eb3d93834.tar.xz org.eclipse.e4.ui-9a610cb443fa4f4b5e239ab4b1775e3eb3d93834.zip | |
Bug 343854 - Web Application to Workbench ExamplesI20131126-2200
Transfer Orion examples from CVS.
54 files changed, 19433 insertions, 0 deletions
diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.classpath b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.classpath new file mode 100644 index 00000000..2d1a4302 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.project b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.project new file mode 100644 index 00000000..73e5a2b7 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.project @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>org.eclipse.e4.examples.webintegration.orion.editor.plugin</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.ManifestBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ <buildCommand>
+ <name>org.eclipse.pde.SchemaBuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.pde.PluginNature</nature>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.settings/org.eclipse.jdt.core.prefs b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..17d7d23f --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Fri May 06 09:49:44 EDT 2011
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/META-INF/MANIFEST.MF b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/META-INF/MANIFEST.MF new file mode 100644 index 00000000..23c7d4cb --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/META-INF/MANIFEST.MF @@ -0,0 +1,15 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Orion Editor Plugin +Bundle-SymbolicName: org.eclipse.e4.examples.webintegration.orion.editor.plugin; singleton:=true +Bundle-Version: 1.0.0.qualifier +Bundle-Vendor: Eclipse.org +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime, + org.eclipse.jface.text, + org.eclipse.ui.editors, + org.eclipse.ui.ide;bundle-version="3.7.0", + org.eclipse.core.resources;bundle-version="3.7.100", + org.mortbay.jetty.util;bundle-version="6.1.23" +Bundle-RequiredExecutionEnvironment: J2SE-1.5 +Bundle-ActivationPolicy: lazy diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/build.properties b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/build.properties new file mode 100644 index 00000000..c662e2b2 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/build.properties @@ -0,0 +1,8 @@ +source.. = src/ +output.. = bin/ +bin.includes = plugin.xml,\ + META-INF/,\ + .,\ + icons/,\ + orion/,\ + editorService/ diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/editorService/embeddededitor.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/editorService/embeddededitor.js new file mode 100644 index 00000000..d593cc1a --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/editorService/embeddededitor.js @@ -0,0 +1,253 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +/*global eclipse:true orion:true dojo window editorServiceHandler editor:true */ +/*jslint devel:true*/ + +/** + * This file demonstrates one way in which a web application could be structured + * to easily allow different behavior depending on whether the application is hosted + * in a browser or in an Eclipse Workbench + **/ + +/** + * This object encapsulates the set of actions an editor may perform that would + * behave differently depending on where the web application is hosted. + * + * The web application implementor may install a set of functions in this object + * that may differ for browser hosting vs. Eclipse Workbench hosting. + * + * Not all functions need to be hooked. Also, some functions may only be hooked for + * one type of hosting scenerio. + * + * This object would be created by the web application developer as they are defining + * the separation of responsibilities for their web application. + * + * This object is global because it needs to be referenced by the Eclipse Workbench + **/ +var editorService = { + DIRTY_CHANGED : 1, + dirtyChanged: function(dirty) {}, // Called by the editor when its dirty state changes + + GET_CONTENT_NAME : 2, + getContentName: function() {}, // Called to get the current content name. A file name for example + + GET_INITIAL_CONTENT : 3, + getInitialContent: function() {}, // Called to get the initial contents for the editor + + SAVE : 4, + save: function(editor) {}, // Called to persist the contents of the editor + + STATUS_CHANGED : 5, + statusChanged: function(message, isError) {} // Called by the editor to report status line changes +}; + +var editor; + +function initEmbeddedEditor(){ + var editorDomNode = dojo.byId("editor"); + + var textViewFactory = function() { + return new orion.textview.TextView({ + parent: editorDomNode, + stylesheet: ["../../orion/textview/textview.css", "../../orion/textview/rulers.css", "../../examples/textview/textstyler.css", "../../examples/editor/htmlStyles.css"], + tabSize: 4 + }); + }; + + var contentAssistFactory = function(editor) { + var contentAssist = new orion.editor.ContentAssist(editor, "contentassist"); + contentAssist.addProvider(new orion.editor.CssContentAssistProvider(), "css", "\\.css$"); + contentAssist.addProvider(new orion.editor.JavaScriptContentAssistProvider(), "js", "\\.js$"); + return contentAssist; + }; + + // Canned highlighters for js, java, and css. Grammar-based highlighter for html + var syntaxHighlighter = { + styler: null, + + highlight: function(fileName, textView) { + if (this.styler) { + this.styler.destroy(); + this.styler = null; + } + if (fileName) { + var splits = fileName.split("."); + var extension = splits.pop().toLowerCase(); + if (splits.length > 0) { + switch(extension) { + case "js": + this.styler = new examples.textview.TextStyler(textView, "js"); + break; + case "java": + this.styler = new examples.textview.TextStyler(textView, "java"); + break; + case "css": + this.styler = new examples.textview.TextStyler(textView, "css"); + break; + case "html": + this.styler = new orion.editor.TextMateStyler(textView, orion.editor.HtmlGrammar.grammar); + break; + } + } + } + } + }; + + var annotationFactory = new orion.editor.AnnotationFactory(); + + var keyBindingFactory = function(editor, keyModeStack, undoStack, contentAssist) { + + // Create keybindings for generic editing + var genericBindings = new orion.editor.TextActions(editor, undoStack); + keyModeStack.push(genericBindings); + + // create keybindings for source editing + var codeBindings = new orion.editor.SourceCodeActions(editor, undoStack, contentAssist); + keyModeStack.push(codeBindings); + + // save binding + editor.getTextView().setKeyBinding(new orion.textview.KeyBinding("s", true), "save"); + editor.getTextView().setAction("save", function(){ + // The save function is called through the editorService allowing Eclipse and Browser hosted instances to behave differently + editorService.save(editor); + return true; + }); + + // speaking of save... + dojo.byId("save").onclick = function() {editorService.save(editor);}; + + }; + + editor = new orion.editor.Editor({ + textViewFactory: textViewFactory, + undoStackFactory: new orion.editor.UndoFactory(), + annotationFactory: annotationFactory, + lineNumberRulerFactory: new orion.editor.LineNumberRulerFactory(), + contentAssistFactory: contentAssistFactory, + keyBindingFactory: keyBindingFactory, + statusReporter: editorService.statusChanged, + domNode: editorDomNode + }); + + dojo.connect(editor, "onDirtyChange", this, editorService.dirtyChanged); // Hooks the onDirtyChange event listener through the editorService + + editor.installTextView(); + + // Set editor input by calling through editorService + editor.onInputChange(editorService.getContentName(), null, editorService.getInitialContent()); + + // Set the syntax highlighter + syntaxHighlighter.highlight(editorService.getContentName(), editor.getTextView()); +} // end of initEmbeddedEditor + +// Created embedded editor +dojo.addOnLoad(function() { + + // Install functions for servicing browser hosted applications + function installBrowserHooks() { + + // Register a getContentName implementation + editorService.getContentName = function() { + return "sample.js"; + }; + + // Register a getInitialContent implementation + editorService.getInitialContent = function() { + return "var foo = function() {window.alert('bar');}; //Initial text. Try editing it"; + }; + + // Register a save implementation. + editorService.save = function(editor) { + window.alert(editor.getContents()); + + // Mark editor as saved + editor.onInputChange(null, null, null, true); + }; + + // Register an implementation to display status changes reported by the editor + editorService.statusChanged = function(message, isError) { + var status; + if (isError) { + status = "ERROR: " + message; + } else { + status = message; + } + + var dirtyIndicator = ""; + if (editor.isDirty()) { + dirtyIndicator = "*"; + } + dojo.byId("status").innerHTML = dirtyIndicator + status; + }; + + // Prevent the browser tab/window from closing with unsaved changes. + // Not needed when running in a workbench since the editor lifecycle code takes + // care of this + window.onbeforeunload = function() { + if (editor.isDirty()) { + return "There are unsaved changes."; + } + }; + } + + // Install functions for servicing Eclipse Workbench hosted applications + function installWorkbenchHooks() { + // Register a function that will be called by the editor when the editor's dirty state changes + editorService.dirtyChanged = function(dirty) { + // This is a function created in Eclipse and registered with the page. + editorServiceHandler(editorService.DIRTY_CHANGED, dirty); + }; + + // Register a getContentName implementation + editorService.getContentName = function() { + // This is a function created in Eclipse and registered with the page. + return editorServiceHandler(editorService.GET_CONTENT_NAME); + }; + + // Register an implementation that can return initial content for the editor + editorService.getInitialContent = function() { + // This is a function created in Eclipse and registered with the page. + return editorServiceHandler(editorService.GET_INITIAL_CONTENT); + }; + + // Register an implementation that should run when the editors status changes. + editorService.statusChanged = function(message, isError) { + // This is a function created in Eclipse and registered with the page. + editorServiceHandler(editorService.STATUS_CHANGED, message); + }; + + // Register an implementation that can save the editors contents. + editorService.save = function() { + // This is a function created in Eclipse and registered with the page. + var result = editorServiceHandler(editorService.SAVE, editor.getContents()); + if (result) { + editor.onInputChange(null, null, null, true); + } + return result; + }; + } + + // Return true if the page is hosted in an Eclipse Workbench, false if hosted in a browser + function isHostedInWorkbench() { + // Check if Eclipse has registered the "EditorServiceHandler" BrowserFunction + return typeof editorServiceHandler=== 'function'; + } + + // Install the appropriate editorService for the current hosting environment + if (isHostedInWorkbench()) { + installWorkbenchHooks(); + } else { + installBrowserHooks(); + } + + // Initialize the editor + initEmbeddedEditor(); +});
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/embeddededitor.css b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/embeddededitor.css new file mode 100644 index 00000000..393ee55f --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/embeddededitor.css @@ -0,0 +1,29 @@ +@import "/org.dojotoolkit/dojo/resources/dojo.css"; +@import "/org.dojotoolkit/dijit/themes/nihilo/nihilo.css"; + +a { + cursor: hand; + text-decoration: none; + color: #ffffff; +} + +a:hover { + cursor: hand; + text-decoration: underline; + color: #ffffff; +} + +.contentassist { + display: none; + background-color: #ffffff; + padding: 2px; + position: fixed; + top: 100px; + left: 100px; + border: 1px solid #cccccc; + z-index:10; +} + +.contentassist .selected { + background-color: #dddddd; +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/embeddededitor.html b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/embeddededitor.html new file mode 100644 index 00000000..b61c0fa1 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/embeddededitor.html @@ -0,0 +1,55 @@ +<!doctype html> +<html style="height: 100%"> + <head> + <meta name="copyright" content="Copyright (c) IBM Corporation and others 2010." > + <meta http-equiv="Content-Language" content="en-us"> + <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> + <title>Embedded Orion Editor</title> + <link rel="stylesheet" type="text/css" href="embeddededitor.css" /> + + <script type="text/javascript"> + var djConfig = { + isDebug:false, + parseOnLoad:true + }; + </script> + <script> + // temporary + var __originalDefine = window.define; + </script> + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/dojo/1.6/dojo/dojo.xd.js"></script> + <script> + // temporary + window.define = __originalDefine; + </script> + + <!-- Web Editor --> + <script src="../../orion/textview/keyBinding.js"></script> + <script src="../../orion/textview/textModel.js"></script> + <script src="../../orion/textview/textView.js"></script> + <script src="../../orion/textview/rulers.js"></script> + <script src="../../orion/textview/undoStack.js"></script> + <script src="../../examples/textview/textStyler.js"></script> + <script src="../../orion/editor/editorFeatures.js"></script> + <script src="../../orion/editor/contentAssist.js"></script> + <script src="../../orion/editor/htmlGrammar.js"></script> + <script src="../../orion/editor/textMateStyler.js"></script> + <script src="../../orion/editor/webContentAssist.js"></script> + <script src="../../orion/editor/editor.js"></script> + <script src="../../../editorService/embeddededitor.js"></script> + </head> + + <body class="nihilo" style="margin: 0px; width: 100%; height: 100%"> + <div id="logo" style="height: 28px; background: #404040; text-align: left;"> + <img src="images/skinnyheaderlogo.png" alt="Orion"> + <span id="bar" style="height: 28px; vertical-align: top; color: #FFFFFF"> + <span id="status" style="height: 28px; vertical-align: top; color: #FFFFFF"></span> + <div id="actions" style="height: 28px; padding-right: 8px; vertical-align: top; float: right; color: #FFFFFF"> + <a id="save">Save</a> + </div> + </span> + </div> + <div id="editor" style="width: 100%; height: 90%"></div> + <div id="contentassist" class="contentassist"></div> +</body> +</html> diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/htmlStyles.css b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/htmlStyles.css new file mode 100644 index 00000000..a0b22a90 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/htmlStyles.css @@ -0,0 +1,26 @@ +/* Styling for html syntax highlighting */ +.entity-name-tag { + color: #3f7f7f; +} + +.entity-other-attribute-name { + color: #7f007f; +} + +.punctuation-definition-comment { + color: #3f5fbf; +} + +.comment { + color: #3f5fbf +} + +.string-quoted { + color: #2a00ff; + font-style: italic; +} + +.invalid { + color: red; + font-weight: bold; +}
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/images/skinnyheaderlogo.png b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/images/skinnyheaderlogo.png Binary files differnew file mode 100644 index 00000000..23f206fa --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/editor/images/skinnyheaderlogo.png diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.css b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.css new file mode 100644 index 00000000..f4f6ed0f --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.css @@ -0,0 +1,8 @@ +.button { + border: 2px dotted orange; + padding: 0 2 0 2 +} + +.parentElement { + border: 1px solid teal; +}
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.html b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.html new file mode 100644 index 00000000..4f3dda91 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> +<meta name="copyright" content="Copyright (c) IBM Corporation and others 2010."/> +<meta http-equiv="PRAGMA" content="NO-CACHE"/> +<meta http-equiv="Expires" content="-1"/> +<title>Orion TextView Demo</title> +<link rel="stylesheet" type="text/css" href="demo.css" /> +<script type="text/javascript" src="/requirejs/require.js"></script> +<script type="text/javascript"> + require({ + baseUrl: '', + paths: { + orion: '/orion', + examples: '/examples', + tests: '/js-tests', + } + }); + require(["demo"]); +</script> +</head> +<body> + +<h1>Orion Text View Demo</h1> + +<table width="100%"> +<tr> +<th>View</th> +<th>Console</th> +</tr> +<tr> +<td width="100%"> +<div id='divParent' class='parentElement' style='width:100%;height:650px;'> +Create the view by clicking one of the buttons at the bottom. +</div> +</td> +<td> +<div id='consoleParent' class='parentElement' style='width:300px;height:650px;'> +<iframe id='console' frameBorder="0" style="width:100%;height:100%;"></iframe> +</div> +</td> +</tr> +</table> +<span id='createJavaSample' class="button">Java file</span> +<span id='createJavaScriptSample' class="button">JavaScript file</span> +<span id='createPlainTextSample' class="button">Plain Text</span> +<span id='createBidiTextSample' class="button">Bidi Text</span> +<span id='clearLog' class="button">ClearConsole</span> +<span id='test' class="button">Test</span> +Performance tests: +<select id="performanceTestSelect"> + <option value="test_pageDown">Page Down</option> + <option value="test_pageUp">Page Up</option> + <option value="test_lineDown">Line Down</option> + <option value="test_lineUp">Line Up</option> + <option value="test_selectPageDown">Select Page Down</option> + <option value="test_selectPageUp">Select Page Up</option> + <option value="test_selectLineDown">Select Line Down</option> + <option value="test_selectLineUp">Select Line Up</option> + <option value="test_getLocationAtOffset">Location at Offset</option> + <option value="test_getOffsetAtLocation">Offset at Location</option> + <option value="test_getLocationAtOffsetStyled">Location at Offset[styles]</option> + <option value="test_getOffsetAtLocationStyled">Offset at Location[styles]</option> +</select> +<span id='performanceTest' class="button">Run</span> +</body> +</html> diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.js new file mode 100644 index 00000000..759516c8 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/demo.js @@ -0,0 +1,199 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ + + /*globals window define document navigator setTimeout XMLHttpRequest PerformanceTest */ + + +function log (text) { + var console = window.document.getElementById('console'); + if (!console) { return; } + for (var n = 1; n < arguments.length; n++) { + text += " "; + text += arguments[n]; + } + + var document = console.contentWindow.document; + var t = document.createTextNode(text); + document.body.appendChild(t); + var br = document.createElement("br"); + document.body.appendChild(br); + if (!console.scroll) { + console.scroll = true; + setTimeout(function() { + document.body.lastChild.scrollIntoView(false); + console.scroll = false; + }, 0); + } +} + + define(["orion/textview/keyBinding", + "orion/textview/textModel", + "orion/textview/textView", + "orion/textview/rulers", + "orion/textview/undoStack", + "examples/textview/textStyler", + "tests/textview/test-performance"], + +function(mKeyBinding, mTextModel, mTextView, mRulers, mUndoStack, mTextStyler) { + var view = null; + var styler = null; + var isMac = navigator.platform.indexOf("Mac") !== -1; + + function clearLog () { + var console = window.document.getElementById('console'); + if (!console) { return; } + var document = console.contentWindow.document; + var body = document.body; + while (body.hasChildNodes()) { body.removeChild(body.lastChild); } + } + + function getFile(file) { + try { + var objXml = new XMLHttpRequest(); + objXml.open("GET",file,false); + objXml.send(null); + return objXml.responseText; + } catch (e) { + return null; + } + } + + function checkView() { + if (view) { return; } + var stylesheets = [ + "/orion/textview/textview.css", + "/orion/textview/rulers.css", + "/examples/textview/textstyler.css" + ]; + var options = { + parent: "divParent", + model: new mTextModel.TextModel(), + stylesheet: stylesheets, + tabSize: 4 + }; + view = new mTextView.TextView(options); + + /* Undo stack */ + var undoStack = new mUndoStack.UndoStack(view, 200); + view.setKeyBinding(new mKeyBinding.KeyBinding('z', true), "undo"); + view.setAction("undo", function() { + undoStack.undo(); + return true; + }); + view.setKeyBinding(isMac ? new mKeyBinding.KeyBinding('z', true, true) : new mKeyBinding.KeyBinding('y', true), "redo"); + view.setAction("redo", function() { + undoStack.redo(); + return true; + }); + + /* Example: Adding a keyBinding and action*/ + view.setKeyBinding(new mKeyBinding.KeyBinding('s', true), "save"); + view.setAction("save", function() { + log("*****************SAVE"); + return true; + }); + + /* Adding the Rulers */ + var breakpoint = { + html: "<img src='images/brkp_obj.gif'></img>", + style: {styleClass: "ruler_annotation_breakpoint"}, + overviewStyle: {styleClass: "ruler_annotation_breakpoint_overview"} + }; + var todo = { + html: "<img src='images/todo.gif'></img>", + style: {styleClass: "ruler_annotation_todo"}, + overviewStyle: {styleClass: "ruler_annotation_todo_overview"} + }; + var annotation = new mRulers.AnnotationRuler("left", {styleClass: "ruler_annotation"}, breakpoint); + annotation.onDblClick = function(lineIndex, e) { + if (lineIndex === undefined) { return; } + annotation.setAnnotation(lineIndex, annotation.getAnnotation(lineIndex) !== undefined ? undefined : e.ctrlKey ? todo : breakpoint); + }; + var lines = new mRulers.LineNumberRuler("left", {styleClass: "ruler_lines"}, {styleClass: "ruler_lines_odd"}, {styleClass: "ruler_lines_even"}); + lines.onDblClick = annotation.onDblClick; + var overview = new mRulers.OverviewRuler("right", {styleClass: "ruler_overview"}, annotation); + view.addRuler(annotation); + view.addRuler(lines); + view.addRuler(overview); + } + + function createJavaSample() { + checkView(); + var file = getFile("text.txt"); + if (styler) { + styler.destroy(); + styler = null; + } + styler = new mTextStyler.TextStyler(view, "java"); + view.setText(file); + } + + function createJavaScriptSample() { + checkView(); + var file = getFile("/orion/textview/textview.js"); + if (styler) { + styler.destroy(); + styler = null; + } + styler = new mTextStyler.TextStyler(view, "js"); + view.setText(file); + } + + function createPlainTextSample() { + checkView(); + var lineCount = 50000; + var lines = []; + for(var i = 0; i < lineCount; i++) { + lines.push("This is the line of text number "+i); + } + if (styler) { + styler.destroy(); + styler = null; + } + view.setText(lines.join("\r\n")); + } + + function createBidiTextSample() { + checkView(); + var lines = []; + lines.push("Hello \u0644\u0645\u0646\u0647"); + if (styler) { + styler.destroy(); + styler = null; + } + view.setText(lines.join("\r\n")); + } + + function test() { + } + + function performanceTest() { + checkView(); + if (styler) { + styler.destroy(); + styler = null; + } + /* Note: PerformanceTest is not using require js */ + var test = new PerformanceTest(view); + var select = document.getElementById("performanceTestSelect"); + test[select.value](); + } + + /* Adding events */ + document.getElementById("createJavaSample").onclick = createJavaSample; + document.getElementById("createJavaScriptSample").onclick = createJavaScriptSample; + document.getElementById("createPlainTextSample").onclick = createPlainTextSample; + document.getElementById("createBidiTextSample").onclick = createBidiTextSample; + document.getElementById("clearLog").onclick = clearLog; + document.getElementById("test").onclick = test; + document.getElementById("performanceTest").onclick = performanceTest; + + });
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/brkp_obj.gif b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/brkp_obj.gif Binary files differnew file mode 100644 index 00000000..a831fe72 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/brkp_obj.gif diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/todo.gif b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/todo.gif Binary files differnew file mode 100644 index 00000000..0bbc98a7 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/todo.gif diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/white_space.png b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/white_space.png Binary files differnew file mode 100644 index 00000000..5722d5b8 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/white_space.png diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/white_tab.png b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/white_tab.png Binary files differnew file mode 100644 index 00000000..47c570e7 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/images/white_tab.png diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/text.txt b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/text.txt new file mode 100644 index 00000000..61429ef6 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/text.txt @@ -0,0 +1,7951 @@ +/******************************************************************************* + * Copyright (c) 2000, 2005 IBM Corporation and others. + * 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: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.custom; + + +import java.util.*; + +import org.eclipse.swt.*; +import org.eclipse.swt.accessibility.*; +import org.eclipse.swt.dnd.*; +import org.eclipse.swt.events.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.*; +import org.eclipse.swt.printing.*; +import org.eclipse.swt.widgets.*; + +/** + * A StyledText is an editable user interface object that displays lines + * of text. The following style attributes can be defined for the text: + * <ul> + * <li>foreground color + * <li>background color + * <li>font style (bold, italic, bold-italic, regular) + * <li>underline + * <li>strikeout + * </ul> + * <p> + * In addition to text style attributes, the background color of a line may + * be specified. + * </p> + * <p> + * There are two ways to use this widget when specifying text style information. + * You may use the API that is defined for StyledText or you may define your own + * LineStyleListener. If you define your own listener, you will be responsible + * for maintaining the text style information for the widget. IMPORTANT: You may + * not define your own listener and use the StyledText API. The following + * StyledText API is not supported if you have defined a LineStyleListener: + * <ul> + * <li>getStyleRangeAtOffset(int) + * <li>getStyleRanges() + * <li>replaceStyleRanges(int,int,StyleRange[]) + * <li>setStyleRange(StyleRange) + * <li>setStyleRanges(StyleRange[]) + * </ul> + * </p> + * <p> + * There are two ways to use this widget when specifying line background colors. + * You may use the API that is defined for StyledText or you may define your own + * LineBackgroundListener. If you define your own listener, you will be responsible + * for maintaining the line background color information for the widget. + * IMPORTANT: You may not define your own listener and use the StyledText API. + * The following StyledText API is not supported if you have defined a + * LineBackgroundListener: + * <ul> + * <li>getLineBackground(int) + * <li>setLineBackground(int,int,Color) + * </ul> + * </p> + * <p> + * The content implementation for this widget may also be user-defined. To do so, + * you must implement the StyledTextContent interface and use the StyledText API + * setContent(StyledTextContent) to initialize the widget. + * </p> + * <p> + * IMPORTANT: This class is <em>not</em> intended to be subclassed. + * </p> + * <dl> + * <dt><b>Styles:</b><dd>FULL_SELECTION, MULTI, READ_ONLY, SINGLE, WRAP + * <dt><b>Events:</b><dd>ExtendedModify, LineGetBackground, LineGetSegments, LineGetStyle, Modify, Selection, Verify, VerifyKey + * </dl> + */ +public class StyledText extends Canvas { + static final char TAB = '\t'; + static final String PlatformLineDelimiter = System.getProperty("line.separator"); + static final int BIDI_CARET_WIDTH = 3; + static final int DEFAULT_WIDTH = 64; + static final int DEFAULT_HEIGHT = 64; + static final int V_SCROLL_RATE = 50; + static final int H_SCROLL_RATE = 10; + + static final int ExtendedModify = 3000; + static final int LineGetBackground = 3001; + static final int LineGetStyle = 3002; + static final int TextChanging = 3003; + static final int TextSet = 3004; + static final int VerifyKey = 3005; + static final int TextChanged = 3006; + static final int LineGetSegments = 3007; + + Color selectionBackground; // selection background color + Color selectionForeground; // selection foreground color + StyledTextContent logicalContent; // native content (default or user specified) + StyledTextContent content; // line wrapping content, same as logicalContent if word wrap is off + DisplayRenderer renderer; + Listener listener; + TextChangeListener textChangeListener; // listener for TextChanging, TextChanged and TextSet events from StyledTextContent + DefaultLineStyler defaultLineStyler;// used for setStyles API when no LineStyleListener is registered + LineCache lineCache; + boolean userLineStyle = false; // true=widget is using a user defined line style listener for line styles. false=widget is using the default line styler to store line styles + boolean userLineBackground = false; // true=widget is using a user defined line background listener for line backgrounds. false=widget is using the default line styler to store line backgrounds + int verticalScrollOffset = 0; // pixel based + int horizontalScrollOffset = 0; // pixel based + int topIndex = 0; // top visible line + int lastPaintTopIndex = -1; + int topOffset = 0; // offset of first character in top line + int clientAreaHeight = 0; // the client area height. Needed to calculate content width for new + // visible lines during Resize callback + int clientAreaWidth = 0; // the client area width. Needed during Resize callback to determine + // if line wrap needs to be recalculated + int lineHeight; // line height=font height + int tabLength = 4; // number of characters in a tab + int leftMargin; + int topMargin; + int rightMargin; + int bottomMargin; + Cursor ibeamCursor; + int columnX; // keep track of the horizontal caret position + // when changing lines/pages. Fixes bug 5935 + int caretOffset = 0; + Point selection = new Point(0, 0); // x and y are start and end caret offsets of selection + Point clipboardSelection; // x and y are start and end caret offsets of previous selection + int selectionAnchor; // position of selection anchor. 0 based offset from beginning of text + Point doubleClickSelection; // selection after last mouse double click + boolean editable = true; + boolean wordWrap = false; + boolean doubleClickEnabled = true; // see getDoubleClickEnabled + boolean overwrite = false; // insert/overwrite edit mode + int textLimit = -1; // limits the number of characters the user can type in the widget. Unlimited by default. + Hashtable keyActionMap = new Hashtable(); + Color background = null; // workaround for bug 4791 + Color foreground = null; // + Clipboard clipboard; + boolean mouseDown = false; + boolean mouseDoubleClick = false; // true=a double click ocurred. Don't do mouse swipe selection. + int autoScrollDirection = SWT.NULL; // the direction of autoscrolling (up, down, right, left) + int autoScrollDistance = 0; + int lastTextChangeStart; // cache data of the + int lastTextChangeNewLineCount; // last text changing + int lastTextChangeNewCharCount; // event for use in the + int lastTextChangeReplaceLineCount; // text changed handler + int lastTextChangeReplaceCharCount; + boolean isMirrored; + boolean bidiColoring = false; // apply the BIDI algorithm on text segments of the same color + Image leftCaretBitmap = null; + Image rightCaretBitmap = null; + int caretDirection = SWT.NULL; + boolean advancing = true; + Caret defaultCaret = null; + boolean updateCaretDirection = true; + + final static boolean IS_CARBON, IS_GTK, IS_MOTIF; + final static boolean DOUBLE_BUFFER; + static { + String platform = SWT.getPlatform(); + IS_CARBON = "carbon".equals(platform); + IS_GTK = "gtk".equals(platform); + IS_MOTIF = "motif".equals(platform); + DOUBLE_BUFFER = !IS_CARBON; + } + + /** + * The Printing class implements printing of a range of text. + * An instance of <class>Printing </class> is returned in the + * StyledText#print(Printer) API. The run() method may be + * invoked from any thread. + */ + static class Printing implements Runnable { + final static int LEFT = 0; // left aligned header/footer segment + final static int CENTER = 1; // centered header/footer segment + final static int RIGHT = 2; // right aligned header/footer segment + + StyledText parent; + Printer printer; + PrintRenderer renderer; + StyledTextPrintOptions printOptions; + StyledTextContent printerContent; // copy of the widget content + Rectangle clientArea; // client area to print on + Font printerFont; + FontData displayFontData; + Hashtable printerColors; // printer color cache for line backgrounds and style + Hashtable lineBackgrounds = new Hashtable(); // cached line backgrounds + Hashtable lineStyles = new Hashtable(); // cached line styles + Hashtable bidiSegments = new Hashtable(); // cached bidi segments when running on a bidi platform + GC gc; // printer GC + int pageWidth; // width of a printer page in pixels + int startPage; // first page to print + int endPage; // last page to print + int pageSize; // number of lines on a page + int startLine; // first (wrapped) line to print + int endLine; // last (wrapped) line to print + boolean singleLine; // widget single line mode + Point selection = null; // selected text + boolean mirrored; //indicates the printing gc should be mirrored + + /** + * Creates an instance of <class>Printing</class>. + * Copies the widget content and rendering data that needs + * to be requested from listeners. + * </p> + * @param parent StyledText widget to print. + * @param printer printer device to print on. + * @param printOptions print options + */ + Printing(StyledText parent, Printer printer, StyledTextPrintOptions printOptions) { + PrinterData data = printer.getPrinterData(); + + this.parent = parent; + this.printer = printer; + this.printOptions = printOptions; + this.mirrored = (parent.getStyle() & SWT.MIRRORED) != 0; + singleLine = parent.isSingleLine(); + startPage = 1; + endPage = Integer.MAX_VALUE; + if (data.scope == PrinterData.PAGE_RANGE) { + startPage = data.startPage; + endPage = data.endPage; + if (endPage < startPage) { + int temp = endPage; + endPage = startPage; + startPage = temp; + } + } + else + if (data.scope == PrinterData.SELECTION) { + selection = parent.getSelectionRange(); + } + + displayFontData = parent.getFont().getFontData()[0]; + copyContent(parent.getContent()); + cacheLineData(printerContent); + } + /** + * Caches the bidi segments of the given line. + * </p> + * @param lineOffset offset of the line to cache bidi segments for. + * Relative to the start of the document. + * @param line line to cache bidi segments for. + */ + void cacheBidiSegments(int lineOffset, String line) { + int[] segments = parent.getBidiSegments(lineOffset, line); + + if (segments != null) { + bidiSegments.put(new Integer(lineOffset), segments); + } + } + /** + * Caches the line background color of the given line. + * </p> + * @param lineOffset offset of the line to cache the background + * color for. Relative to the start of the document. + * @param line line to cache the background color for + */ + void cacheLineBackground(int lineOffset, String line) { + StyledTextEvent event = parent.getLineBackgroundData(lineOffset, line); + + if (event != null) { + lineBackgrounds.put(new Integer(lineOffset), event); + } + } + /** + * Caches all line data that needs to be requested from a listener. + * </p> + * @param printerContent <class>StyledTextContent</class> to request + * line data for. + */ + void cacheLineData(StyledTextContent printerContent) { + for (int i = 0; i < printerContent.getLineCount(); i++) { + int lineOffset = printerContent.getOffsetAtLine(i); + String line = printerContent.getLine(i); + + if (printOptions.printLineBackground) { + cacheLineBackground(lineOffset, line); + } + if (printOptions.printTextBackground || + printOptions.printTextForeground || + printOptions.printTextFontStyle) { + cacheLineStyle(lineOffset, line); + } + if (parent.isBidi()) { + cacheBidiSegments(lineOffset, line); + } + } + } + /** + * Caches all line styles of the given line. + * </p> + * @param lineOffset offset of the line to cache the styles for. + * Relative to the start of the document. + * @param line line to cache the styles for. + */ + void cacheLineStyle(int lineOffset, String line) { + StyledTextEvent event = parent.getLineStyleData(lineOffset, line); + + if (event != null) { + StyleRange[] styles = event.styles; + for (int i = 0; i < styles.length; i++) { + StyleRange styleCopy = null; + if (!printOptions.printTextBackground && styles[i].background != null) { + styleCopy = (StyleRange) styles[i].clone(); + styleCopy.background = null; + } + if (!printOptions.printTextForeground && styles[i].foreground != null) { + if (styleCopy == null) { + styleCopy = (StyleRange) styles[i].clone(); + } + styleCopy.foreground = null; + } + if (!printOptions.printTextFontStyle && styles[i].fontStyle != SWT.NORMAL) { + if (styleCopy == null) { + styleCopy = (StyleRange) styles[i].clone(); + } + styleCopy.fontStyle = SWT.NORMAL; + } + if (styleCopy != null) { + styles[i] = styleCopy; + } + } + lineStyles.put(new Integer(lineOffset), event); + } + } + /** + * Copies the text of the specified <class>StyledTextContent</class>. + * </p> + * @param original the <class>StyledTextContent</class> to copy. + */ + void copyContent(StyledTextContent original) { + int insertOffset = 0; + + printerContent = new DefaultContent(); + for (int i = 0; i < original.getLineCount(); i++) { + int insertEndOffset; + if (i < original.getLineCount() - 1) { + insertEndOffset = original.getOffsetAtLine(i + 1); + } + else { + insertEndOffset = original.getCharCount(); + } + printerContent.replaceTextRange(insertOffset, 0, original.getTextRange(insertOffset, insertEndOffset - insertOffset)); + insertOffset = insertEndOffset; + } + } + /** + * Replaces all display colors in the cached line backgrounds and + * line styles with printer colors. + */ + void createPrinterColors() { + Enumeration values = lineBackgrounds.elements(); + printerColors = new Hashtable(); + while (values.hasMoreElements()) { + StyledTextEvent event = (StyledTextEvent) values.nextElement(); + event.lineBackground = getPrinterColor(event.lineBackground); + } + + values = lineStyles.elements(); + while (values.hasMoreElements()) { + StyledTextEvent event = (StyledTextEvent) values.nextElement(); + for (int i = 0; i < event.styles.length; i++) { + StyleRange style = event.styles[i]; + Color printerBackground = getPrinterColor(style.background); + Color printerForeground = getPrinterColor(style.foreground); + + if (printerBackground != style.background || + printerForeground != style.foreground) { + style = (StyleRange) style.clone(); + style.background = printerBackground; + style.foreground = printerForeground; + event.styles[i] = style; + } + } + } + } + /** + * Disposes of the resources and the <class>PrintRenderer</class>. + */ + void dispose() { + if (printerColors != null) { + Enumeration colors = printerColors.elements(); + + while (colors.hasMoreElements()) { + Color color = (Color) colors.nextElement(); + color.dispose(); + } + printerColors = null; + } + if (gc != null) { + gc.dispose(); + gc = null; + } + if (printerFont != null) { + printerFont.dispose(); + printerFont = null; + } + if (renderer != null) { + renderer.dispose(); + renderer = null; + } + } + /** + * Finish printing the indicated page. + * + * @param page page that was printed + */ + void endPage(int page) { + printDecoration(page, false); + printer.endPage(); + } + /** + * Creates a <class>PrintRenderer</class> and calculate the line range + * to print. + */ + void initializeRenderer() { + Rectangle trim = printer.computeTrim(0, 0, 0, 0); + Point dpi = printer.getDPI(); + + printerFont = new Font(printer, displayFontData.getName(), displayFontData.getHeight(), SWT.NORMAL); + clientArea = printer.getClientArea(); + pageWidth = clientArea.width; + // one inch margin around text + clientArea.x = dpi.x + trim.x; + clientArea.y = dpi.y + trim.y; + clientArea.width -= (clientArea.x + trim.width); + clientArea.height -= (clientArea.y + trim.height); + + // make the orientation of the printer gc match the control + int style = mirrored ? SWT.RIGHT_TO_LEFT : SWT.LEFT_TO_RIGHT; + gc = new GC(printer, style); + gc.setFont(printerFont); + renderer = new PrintRenderer( + printer, printerFont, gc, printerContent, + lineBackgrounds, lineStyles, bidiSegments, + parent.tabLength, clientArea); + if (printOptions.header != null) { + int lineHeight = renderer.getLineHeight(); + clientArea.y += lineHeight * 2; + clientArea.height -= lineHeight * 2; + } + if (printOptions.footer != null) { + clientArea.height -= renderer.getLineHeight() * 2; + } + pageSize = clientArea.height / renderer.getLineHeight(); + StyledTextContent content = renderer.getContent(); + startLine = 0; + if (singleLine) { + endLine = 0; + } + else { + endLine = content.getLineCount() - 1; + } + PrinterData data = printer.getPrinterData(); + if (data.scope == PrinterData.PAGE_RANGE) { + startLine = (startPage - 1) * pageSize; + } + else + if (data.scope == PrinterData.SELECTION) { + startLine = content.getLineAtOffset(selection.x); + if (selection.y > 0) { + endLine = content.getLineAtOffset(selection.x + selection.y - 1); + } + else { + endLine = startLine - 1; + } + } + } + /** + * Returns the printer color for the given display color. + * </p> + * @param color display color + * @return color create on the printer with the same RGB values + * as the display color. + */ + Color getPrinterColor(Color color) { + Color printerColor = null; + + if (color != null) { + printerColor = (Color) printerColors.get(color); + if (printerColor == null) { + printerColor = new Color(printer, color.getRGB()); + printerColors.put(color, printerColor); + } + } + return printerColor; + } + /** + * Prints the lines in the specified page range. + */ + void print() { + StyledTextContent content = renderer.getContent(); + Color background = gc.getBackground(); + Color foreground = gc.getForeground(); + int lineHeight = renderer.getLineHeight(); + int paintY = clientArea.y; + int page = startPage; + + for (int i = startLine; i <= endLine && page <= endPage; i++, paintY += lineHeight) { + String line = content.getLine(i); + + if (paintY == clientArea.y) { + startPage(page); + } + renderer.drawLine( + line, i, paintY, gc, background, foreground, true); + if (paintY + lineHeight * 2 > clientArea.y + clientArea.height) { + // close full page + endPage(page); + paintY = clientArea.y - lineHeight; + page++; + } + } + if (paintY > clientArea.y) { + // close partial page + endPage(page); + } + } + /** + * Print header or footer decorations. + * + * @param page page number to print, if specified in the StyledTextPrintOptions header or footer. + * @param header true = print the header, false = print the footer + */ + void printDecoration(int page, boolean header) { + int lastSegmentIndex = 0; + final int SegmentCount = 3; + String text; + + if (header) { + text = printOptions.header; + } + else { + text = printOptions.footer; + } + if (text == null) { + return; + } + for (int i = 0; i < SegmentCount; i++) { + int segmentIndex = text.indexOf(StyledTextPrintOptions.SEPARATOR, lastSegmentIndex); + String segment; + + if (segmentIndex == -1) { + segment = text.substring(lastSegmentIndex); + printDecorationSegment(segment, i, page, header); + break; + } + else { + segment = text.substring(lastSegmentIndex, segmentIndex); + printDecorationSegment(segment, i, page, header); + lastSegmentIndex = segmentIndex + StyledTextPrintOptions.SEPARATOR.length(); + } + } + } + /** + * Print one segment of a header or footer decoration. + * Headers and footers have three different segments. + * One each for left aligned, centered, and right aligned text. + * + * @param segment decoration segment to print + * @param alignment alignment of the segment. 0=left, 1=center, 2=right + * @param page page number to print, if specified in the decoration segment. + * @param header true = print the header, false = print the footer + */ + void printDecorationSegment(String segment, int alignment, int page, boolean header) { + int pageIndex = segment.indexOf(StyledTextPrintOptions.PAGE_TAG); + + if (pageIndex != -1) { + final int PageTagLength = StyledTextPrintOptions.PAGE_TAG.length(); + StringBuffer buffer = new StringBuffer(segment.substring (0, pageIndex)); + buffer.append (page); + buffer.append (segment.substring(pageIndex + PageTagLength)); + segment = buffer.toString(); + } + if (segment.length() > 0) { + int segmentWidth; + int drawX = 0; + int drawY = 0; + TextLayout layout = new TextLayout(printer); + layout.setText(segment); + layout.setFont(printerFont); + segmentWidth = layout.getLineBounds(0).width; + if (header) { + drawY = clientArea.y - renderer.getLineHeight() * 2; + } + else { + drawY = clientArea.y + clientArea.height + renderer.getLineHeight(); + } + if (alignment == LEFT) { + drawX = clientArea.x; + } + else + if (alignment == CENTER) { + drawX = (pageWidth - segmentWidth) / 2; + } + else + if (alignment == RIGHT) { + drawX = clientArea.x + clientArea.width - segmentWidth; + } + layout.draw(gc, drawX, drawY); + layout.dispose(); + } + } + /** + * Starts a print job and prints the pages specified in the constructor. + */ + public void run() { + String jobName = printOptions.jobName; + + if (jobName == null) { + jobName = "Printing"; + } + if (printer.startJob(jobName)) { + createPrinterColors(); + initializeRenderer(); + print(); + dispose(); + printer.endJob(); + } + } + /** + * Start printing a new page. + * + * @param page page number to be started + */ + void startPage(int page) { + printer.startPage(); + printDecoration(page, true); + } + } + /** + * The <code>RTFWriter</code> class is used to write widget content as + * rich text. The implementation complies with the RTF specification + * version 1.5. + * <p> + * toString() is guaranteed to return a valid RTF string only after + * close() has been called. + * </p> + * <p> + * Whole and partial lines and line breaks can be written. Lines will be + * formatted using the styles queried from the LineStyleListener, if + * set, or those set directly in the widget. All styles are applied to + * the RTF stream like they are rendered by the widget. In addition, the + * widget font name and size is used for the whole text. + * </p> + */ + class RTFWriter extends TextWriter { + static final int DEFAULT_FOREGROUND = 0; + static final int DEFAULT_BACKGROUND = 1; + Vector colorTable = new Vector(); + boolean WriteUnicode; + + /** + * Creates a RTF writer that writes content starting at offset "start" + * in the document. <code>start</code> and <code>length</code>can be set to specify partial + * lines. + * <p> + * + * @param start start offset of content to write, 0 based from + * beginning of document + * @param length length of content to write + */ + public RTFWriter(int start, int length) { + super(start, length); + colorTable.addElement(getForeground()); + colorTable.addElement(getBackground()); + setUnicode(); + } + /** + * Closes the RTF writer. Once closed no more content can be written. + * <b>NOTE:</b> <code>toString()</code> does not return a valid RTF string until + * <code>close()</code> has been called. + */ + public void close() { + if (!isClosed()) { + writeHeader(); + write("\n}}\0"); + super.close(); + } + } + /** + * Returns the index of the specified color in the RTF color table. + * <p> + * + * @param color the color + * @param defaultIndex return value if color is null + * @return the index of the specified color in the RTF color table + * or "defaultIndex" if "color" is null. + */ + int getColorIndex(Color color, int defaultIndex) { + int index; + + if (color == null) { + index = defaultIndex; + } + else { + index = colorTable.indexOf(color); + if (index == -1) { + index = colorTable.size(); + colorTable.addElement(color); + } + } + return index; + } + /** + * Determines if Unicode RTF should be written. + * Don't write Unicode RTF on Windows 95/98/ME or NT. + */ + void setUnicode() { + final String Win95 = "windows 95"; + final String Win98 = "windows 98"; + final String WinME = "windows me"; + final String WinNT = "windows nt"; + String osName = System.getProperty("os.name").toLowerCase(); + String osVersion = System.getProperty("os.version"); + int majorVersion = 0; + + if (osName.startsWith(WinNT) && osVersion != null) { + int majorIndex = osVersion.indexOf('.'); + if (majorIndex != -1) { + osVersion = osVersion.substring(0, majorIndex); + try { + majorVersion = Integer.parseInt(osVersion); + } + catch (NumberFormatException exception) { + // ignore exception. version number remains unknown. + // will write without Unicode + } + } + } + if (!osName.startsWith(Win95) && + !osName.startsWith(Win98) && + !osName.startsWith(WinME) && + (!osName.startsWith(WinNT) || majorVersion > 4)) { + WriteUnicode = true; + } + else { + WriteUnicode = false; + } + } + /** + * Appends the specified segment of "string" to the RTF data. + * Copy from <code>start</code> up to, but excluding, <code>end</code>. + * <p> + * + * @param string string to copy a segment from. Must not contain + * line breaks. Line breaks should be written using writeLineDelimiter() + * @param start start offset of segment. 0 based. + * @param end end offset of segment + */ + void write(String string, int start, int end) { + for (int index = start; index < end; index++) { + char ch = string.charAt(index); + if (ch > 0xFF && WriteUnicode) { + // write the sub string from the last escaped character + // to the current one. Fixes bug 21698. + if (index > start) { + write(string.substring(start, index)); + } + write("\\u"); + write(Integer.toString((short) ch)); + write(' '); // control word delimiter + start = index + 1; + } + else + if (ch == '}' || ch == '{' || ch == '\\') { + // write the sub string from the last escaped character + // to the current one. Fixes bug 21698. + if (index > start) { + write(string.substring(start, index)); + } + write('\\'); + write(ch); + start = index + 1; + } + } + // write from the last escaped character to the end. + // Fixes bug 21698. + if (start < end) { + write(string.substring(start, end)); + } + } + /** + * Writes the RTF header including font table and color table. + */ + void writeHeader() { + StringBuffer header = new StringBuffer(); + FontData fontData = getFont().getFontData()[0]; + header.append("{\\rtf1\\ansi"); + // specify code page, necessary for copy to work in bidi + // systems that don't support Unicode RTF. + String cpg = System.getProperty("file.encoding").toLowerCase(); + if (cpg.startsWith("cp") || cpg.startsWith("ms")) { + cpg = cpg.substring(2, cpg.length()); + header.append("\\ansicpg"); + header.append(cpg); + } + header.append("\\uc0\\deff0{\\fonttbl{\\f0\\fnil "); + header.append(fontData.getName()); + header.append(";}}\n{\\colortbl"); + for (int i = 0; i < colorTable.size(); i++) { + Color color = (Color) colorTable.elementAt(i); + header.append("\\red"); + header.append(color.getRed()); + header.append("\\green"); + header.append(color.getGreen()); + header.append("\\blue"); + header.append(color.getBlue()); + header.append(";"); + } + // some RTF readers ignore the deff0 font tag. Explicitly + // set the font for the whole document to work around this. + header.append("}\n{\\f0\\fs"); + // font size is specified in half points + header.append(fontData.getHeight() * 2); + header.append(" "); + write(header.toString(), 0); + } + /** + * Appends the specified line text to the RTF data. Lines will be formatted + * using the styles queried from the LineStyleListener, if set, or those set + * directly in the widget. + * <p> + * + * @param line line text to write as RTF. Must not contain line breaks + * Line breaks should be written using writeLineDelimiter() + * @param lineOffset offset of the line. 0 based from the start of the + * widget document. Any text occurring before the start offset or after the + * end offset specified during object creation is ignored. + * @exception SWTException <ul> + * <li>ERROR_IO when the writer is closed.</li> + * </ul> + */ + public void writeLine(String line, int lineOffset) { + StyleRange[] styles = new StyleRange[0]; + Color lineBackground = null; + StyledTextEvent event; + + if (isClosed()) { + SWT.error(SWT.ERROR_IO); + } + event = renderer.getLineStyleData(lineOffset, line); + if (event != null) { + styles = event.styles; + } + event = renderer.getLineBackgroundData(lineOffset, line); + if (event != null) { + lineBackground = event.lineBackground; + } + if (lineBackground == null) { + lineBackground = getBackground(); + } + writeStyledLine(line, lineOffset, styles, lineBackground); + } + /** + * Appends the specified line delmimiter to the RTF data. + * <p> + * + * @param lineDelimiter line delimiter to write as RTF. + * @exception SWTException <ul> + * <li>ERROR_IO when the writer is closed.</li> + * </ul> + */ + public void writeLineDelimiter(String lineDelimiter) { + if (isClosed()) { + SWT.error(SWT.ERROR_IO); + } + write(lineDelimiter, 0, lineDelimiter.length()); + write("\\par "); + } + /** + * Appends the specified line text to the RTF data. + * Use the colors and font styles specified in "styles" and "lineBackground". + * Formatting is written to reflect the text rendering by the text widget. + * Style background colors take precedence over the line background color. + * Background colors are written using the \highlight tag (vs. the \cb tag). + * <p> + * + * @param line line text to write as RTF. Must not contain line breaks + * Line breaks should be written using writeLineDelimiter() + * @param lineOffset offset of the line. 0 based from the start of the + * widget document. Any text occurring before the start offset or after the + * end offset specified during object creation is ignored. + * @param styles styles to use for formatting. Must not be null. + * @param lineBackground line background color to use for formatting. + * May be null. + */ + void writeStyledLine(String line, int lineOffset, StyleRange[] styles, Color lineBackground) { + int lineLength = line.length(); + int lineIndex; + int copyEnd; + int startOffset = getStart(); + int endOffset = startOffset + super.getCharCount(); + int lineEndOffset = Math.min(lineLength, endOffset - lineOffset); + int writeOffset = startOffset - lineOffset; + + if (writeOffset >= line.length()) { + return; // whole line is outside write range + } + else + if (writeOffset > 0) { + lineIndex = writeOffset; // line starts before RTF write start + } + else { + lineIndex = 0; + } + if (lineBackground != null) { + write("{\\highlight"); + write(getColorIndex(lineBackground, DEFAULT_BACKGROUND)); + write(" "); + } + for (int i = 0; i < styles.length; i++) { + StyleRange style = styles[i]; + int start = style.start - lineOffset; + int end = start + style.length; + int colorIndex; + // skip over partial first line + if (end < writeOffset) { + continue; + } + // style starts beyond line end or RTF write end + if (start >= lineEndOffset) { + break; + } + // write any unstyled text + if (lineIndex < start) { + // copy to start of style + // style starting betond end of write range or end of line + // is guarded against above. + write(line, lineIndex, start); + lineIndex = start; + } + // write styled text + colorIndex = getColorIndex(style.background, DEFAULT_BACKGROUND); + write("{\\cf"); + write(getColorIndex(style.foreground, DEFAULT_FOREGROUND)); + if (colorIndex != DEFAULT_BACKGROUND) { + write("\\highlight"); + write(colorIndex); + } + if ((style.fontStyle & SWT.BOLD) != 0) { + write("\\b"); + } + if ((style.fontStyle & SWT.ITALIC) != 0) { + write("\\i"); + } + if (style.underline) { + write("\\ul"); + } + if (style.strikeout) { + write("\\strike"); + } + write(" "); + // copy to end of style or end of write range or end of line + copyEnd = Math.min(end, lineEndOffset); + // guard against invalid styles and let style processing continue + copyEnd = Math.max(copyEnd, lineIndex); + write(line, lineIndex, copyEnd); + if ((style.fontStyle & SWT.BOLD) != 0) { + write("\\b0"); + } + if ((style.fontStyle & SWT.ITALIC) != 0) { + write("\\i0"); + } + if (style.underline) { + write("\\ul0"); + } + if (style.strikeout) { + write("\\strike0"); + } + write("}"); + lineIndex = copyEnd; + } + // write unstyled text at the end of the line + if (lineIndex < lineEndOffset) { + write(line, lineIndex, lineEndOffset); + } + if (lineBackground != null) { + write("}"); + } + } + } + /** + * The <code>TextWriter</code> class is used to write widget content to + * a string. Whole and partial lines and line breaks can be written. To write + * partial lines, specify the start and length of the desired segment + * during object creation. + * <p> + * </b>NOTE:</b> <code>toString()</code> is guaranteed to return a valid string only after close() + * has been called. + */ + class TextWriter { + private StringBuffer buffer; + private int startOffset; // offset of first character that will be written + private int endOffset; // offset of last character that will be written. + // 0 based from the beginning of the widget text. + private boolean isClosed = false; + + /** + * Creates a writer that writes content starting at offset "start" + * in the document. <code>start</code> and <code>length</code> can be set to specify partial lines. + * <p> + * + * @param start start offset of content to write, 0 based from beginning of document + * @param length length of content to write + */ + public TextWriter(int start, int length) { + buffer = new StringBuffer(length); + startOffset = start; + endOffset = start + length; + } + /** + * Closes the writer. Once closed no more content can be written. + * <b>NOTE:</b> <code>toString()</code> is not guaranteed to return a valid string unless + * the writer is closed. + */ + public void close() { + if (!isClosed) { + isClosed = true; + } + } + /** + * Returns the number of characters to write. + * @return the integer number of characters to write + */ + public int getCharCount() { + return endOffset - startOffset; + } + /** + * Returns the offset where writing starts. 0 based from the start of + * the widget text. Used to write partial lines. + * @return the integer offset where writing starts + */ + public int getStart() { + return startOffset; + } + /** + * Returns whether the writer is closed. + * @return a boolean specifying whether or not the writer is closed + */ + public boolean isClosed() { + return isClosed; + } + /** + * Returns the string. <code>close()</code> must be called before <code>toString()</code> + * is guaranteed to return a valid string. + * + * @return the string + */ + public String toString() { + return buffer.toString(); + } + /** + * Appends the given string to the data. + */ + void write(String string) { + buffer.append(string); + } + /** + * Inserts the given string to the data at the specified offset. + * Do nothing if "offset" is < 0 or > getCharCount() + * <p> + * + * @param string text to insert + * @param offset offset in the existing data to insert "string" at. + */ + void write(String string, int offset) { + if (offset < 0 || offset > buffer.length()) { + return; + } + buffer.insert(offset, string); + } + /** + * Appends the given int to the data. + */ + void write(int i) { + buffer.append(i); + } + /** + * Appends the given character to the data. + */ + void write(char i) { + buffer.append(i); + } + /** + * Appends the specified line text to the data. + * <p> + * + * @param line line text to write. Must not contain line breaks + * Line breaks should be written using writeLineDelimiter() + * @param lineOffset offset of the line. 0 based from the start of the + * widget document. Any text occurring before the start offset or after the + * end offset specified during object creation is ignored. + * @exception SWTException <ul> + * <li>ERROR_IO when the writer is closed.</li> + * </ul> + */ + public void writeLine(String line, int lineOffset) { + int lineLength = line.length(); + int lineIndex; + int copyEnd; + int writeOffset = startOffset - lineOffset; + + if (isClosed) { + SWT.error(SWT.ERROR_IO); + } + if (writeOffset >= lineLength) { + return; // whole line is outside write range + } + else + if (writeOffset > 0) { + lineIndex = writeOffset; // line starts before write start + } + else { + lineIndex = 0; + } + copyEnd = Math.min(lineLength, endOffset - lineOffset); + if (lineIndex < copyEnd) { + write(line.substring(lineIndex, copyEnd)); + } + } + /** + * Appends the specified line delmimiter to the data. + * <p> + * + * @param lineDelimiter line delimiter to write + * @exception SWTException <ul> + * <li>ERROR_IO when the writer is closed.</li> + * </ul> + */ + public void writeLineDelimiter(String lineDelimiter) { + if (isClosed) { + SWT.error(SWT.ERROR_IO); + } + write(lineDelimiter); + } + } + /** + * LineCache provides an interface to calculate and invalidate + * line based data. + * Implementors need to return a line width in <code>getWidth</code>. + */ + interface LineCache { + /** + * Calculates the lines in the specified range. + * <p> + * + * @param startLine first line to calculate + * @param lineCount number of lines to calculate + */ + public void calculate(int startLine, int lineCount); + /** + * Returns a width that will be used by the <code>StyledText</code> + * widget to size a horizontal scroll bar. + * <p> + * + * @return the line width + */ + public int getWidth(); + /** + * Resets the lines in the specified range. + * This method is called in <code>StyledText.redraw()</code> + * and allows implementors to call redraw themselves during reset. + * <p> + * + * @param startLine the first line to reset + * @param lineCount the number of lines to reset + * @param calculateMaxWidth true=implementors should retain a + * valid width even if it is affected by the reset operation. + * false=the width may be set to 0 + */ + public void redrawReset(int startLine, int lineCount, boolean calculateMaxWidth); + /** + * Resets the lines in the specified range. + * <p> + * + * @param startLine the first line to reset + * @param lineCount the number of lines to reset + * @param calculateMaxWidth true=implementors should retain a + * valid width even if it is affected by the reset operation. + * false=the width may be set to 0 + */ + public void reset(int startLine, int lineCount, boolean calculateMaxWidth); + /** + * Called when a text change occurred. + * <p> + * + * @param startOffset the start offset of the text change + * @param newLineCount the number of inserted lines + * @param replaceLineCount the number of deleted lines + * @param newCharCount the number of new characters + * @param replaceCharCount the number of deleted characters + */ + public void textChanged(int startOffset, int newLineCount, int replaceLineCount, int newCharCount, int replaceCharCount); + } + /** + * Keeps track of line widths and the longest line in the + * StyledText document. + * Line widths are calculated when requested by a call to + * <code>calculate</code> and cached until reset by a call + * to <code>redrawReset</code> or <code>reset</code>. + */ + class ContentWidthCache implements LineCache { + StyledText parent; // parent widget, used to create a GC for line measuring + int[] lineWidth; // width in pixel of each line in the document, -1 for unknown width + StyledTextContent content; // content to use for line width calculation + int lineCount; // number of lines in lineWidth array + int maxWidth; // maximum line width of all measured lines + int maxWidthLineIndex; // index of the widest line + + /** + * Creates a new <code>ContentWidthCache</code> and allocates space + * for the given number of lines. + * <p> + * + * @param parent the StyledText widget used to create a GC for + * line measuring + * @param content a StyledTextContent containing the initial number + * of lines to allocate space for + */ + public ContentWidthCache(StyledText parent, StyledTextContent content) { + this.parent = parent; + this.content = content; + this.lineCount = content.getLineCount(); + lineWidth = new int[lineCount]; + reset(0, lineCount, false); + } + /** + * Calculates the width of each line in the given range if it has + * not been calculated yet. + * If any line in the given range is wider than the currently widest + * line, the maximum line width is updated, + * <p> + * + * @param startLine first line to calculate the line width of + * @param lineCount number of lines to calculate the line width for + */ + public void calculate(int startLine, int lineCount) { + int caretWidth = 0; + int endLine = startLine + lineCount; + + if (startLine < 0 || endLine > lineWidth.length) { + return; + } + caretWidth = getCaretWidth(); + for (int i = startLine; i < endLine; i++) { + if (lineWidth[i] == -1) { + String line = content.getLine(i); + int lineOffset = content.getOffsetAtLine(i); + lineWidth[i] = contentWidth(line, lineOffset) + caretWidth; + } + if (lineWidth[i] > maxWidth) { + maxWidth = lineWidth[i]; + maxWidthLineIndex = i; + } + } + } + /** + * Calculates the width of the visible lines in the specified + * range. + * <p> + * + * @param startLine the first changed line + * @param newLineCount the number of inserted lines + */ + void calculateVisible(int startLine, int newLineCount) { + int topIndex = parent.getTopIndex(); + int bottomLine = Math.min(getPartialBottomIndex(), startLine + newLineCount); + + startLine = Math.max(startLine, topIndex); + calculate(startLine, bottomLine - startLine + 1); + } + /** + * Measures the width of the given line. + * <p> + * + * @param line the line to measure + * @param lineOffset start offset of the line to measure, relative + * to the start of the document + * @return the width of the given line + */ + int contentWidth(String line, int lineOffset) { + TextLayout layout = renderer.getTextLayout(line, lineOffset); + Rectangle rect = layout.getLineBounds(0); + renderer.disposeTextLayout(layout); + return rect.x + rect.width + leftMargin + rightMargin; + } + /** + * Grows the <code>lineWidth</code> array to accomodate new line width + * information. + * <p> + * + * @param numLines the number of elements to increase the array by + */ + void expandLines(int numLines) { + int size = lineWidth.length; + if (size - lineCount >= numLines) { + return; + } + int[] newLines = new int[Math.max(size * 2, size + numLines)]; + System.arraycopy(lineWidth, 0, newLines, 0, size); + lineWidth = newLines; + reset(size, lineWidth.length - size, false); + } + /** + * Returns the width of the longest measured line. + * <p> + * + * @return the width of the longest measured line. + */ + public int getWidth() { + return maxWidth; + } + /** + * Updates the line width array to reflect inserted or deleted lines. + * <p> + * + * @param startLine the starting line of the change that took place + * @param delta the number of lines in the change, > 0 indicates lines inserted, + * < 0 indicates lines deleted + */ + void linesChanged(int startLine, int delta) { + boolean inserting = delta > 0; + + if (delta == 0) { + return; + } + if (inserting) { + // shift the lines down to make room for new lines + expandLines(delta); + for (int i = lineCount - 1; i >= startLine; i--) { + lineWidth[i + delta] = lineWidth[i]; + } + // reset the new lines + for (int i = startLine + 1; i <= startLine + delta && i < lineWidth.length; i++) { + lineWidth[i] = -1; + } + // have new lines been inserted above the longest line? + if (maxWidthLineIndex >= startLine) { + maxWidthLineIndex += delta; + } + } + else { + // shift up the lines + for (int i = startLine - delta; i < lineCount; i++) { + lineWidth[i+delta] = lineWidth[i]; + } + // has the longest line been removed? + if (maxWidthLineIndex > startLine && maxWidthLineIndex <= startLine - delta) { + maxWidth = 0; + maxWidthLineIndex = -1; + } + else + if (maxWidthLineIndex >= startLine - delta) { + maxWidthLineIndex += delta; + } + } + lineCount += delta; + } + /** + * Resets the line width of the lines in the specified range. + * <p> + * + * @param startLine the first line to reset + * @param lineCount the number of lines to reset + * @param calculateMaxWidth true=if the widest line is being + * reset the maximum width of all remaining cached lines is + * calculated. false=the maximum width is set to 0 if the + * widest line is being reset. + */ + public void redrawReset(int startLine, int lineCount, boolean calculateMaxWidth) { + reset(startLine, lineCount, calculateMaxWidth); + } + /** + * Resets the line width of the lines in the specified range. + * <p> + * + * @param startLine the first line to reset + * @param lineCount the number of lines to reset + * @param calculateMaxWidth true=if the widest line is being + * reset the maximum width of all remaining cached lines is + * calculated. false=the maximum width is set to 0 if the + * widest line is being reset. + */ + public void reset(int startLine, int lineCount, boolean calculateMaxWidth) { + int endLine = startLine + lineCount; + + if (startLine < 0 || endLine > lineWidth.length) { + return; + } + for (int i = startLine; i < endLine; i++) { + lineWidth[i] = -1; + } + // if the longest line is one of the reset lines, the maximum line + // width is no longer valid + if (maxWidthLineIndex >= startLine && maxWidthLineIndex < endLine) { + maxWidth = 0; + maxWidthLineIndex = -1; + if (calculateMaxWidth) { + for (int i = 0; i < lineCount; i++) { + if (lineWidth[i] > maxWidth) { + maxWidth = lineWidth[i]; + maxWidthLineIndex = i; + } + } + } + } + } + /** + * Updates the line width array to reflect a text change. + * Lines affected by the text change will be reset. + * <p> + * + * @param startOffset the start offset of the text change + * @param newLineCount the number of inserted lines + * @param replaceLineCount the number of deleted lines + * @param newCharCount the number of new characters + * @param replaceCharCount the number of deleted characters + */ + public void textChanged(int startOffset, int newLineCount, int replaceLineCount, int newCharCount, int replaceCharCount) { + int startLine = parent.getLineAtOffset(startOffset); + boolean removedMaxLine = (maxWidthLineIndex > startLine && maxWidthLineIndex <= startLine + replaceLineCount); + // entire text deleted? + if (startLine == 0 && replaceLineCount == lineCount) { + lineCount = newLineCount; + lineWidth = new int[lineCount]; + reset(0, lineCount, false); + maxWidth = 0; + } + else { + linesChanged(startLine, -replaceLineCount); + linesChanged(startLine, newLineCount); + lineWidth[startLine] = -1; + } + // only calculate the visible lines. otherwise measurements of changed lines + // outside the visible area may subsequently change again without the + // lines ever being visible. + calculateVisible(startLine, newLineCount); + // maxWidthLineIndex will be -1 (i.e., unknown line width) if the widget has + // not been visible yet and the changed lines have therefore not been + // calculated above. + if (removedMaxLine || + (maxWidthLineIndex != -1 && lineWidth[maxWidthLineIndex] < maxWidth)) { + // longest line has been removed or changed and is now shorter. + // need to recalculate maximum content width for all lines + maxWidth = 0; + for (int i = 0; i < lineCount; i++) { + if (lineWidth[i] > maxWidth) { + maxWidth = lineWidth[i]; + maxWidthLineIndex = i; + } + } + } + } + } + /** + * Updates the line wrapping of the content. + * The line wrapping must always be in a consistent state. + * Therefore, when <code>reset</code> or <code>redrawReset</code> + * is called, the line wrapping is recalculated immediately + * instead of in <code>calculate</code>. + */ + class WordWrapCache implements LineCache { + StyledText parent; + WrappedContent visualContent; + + /** + * Creates a new <code>WordWrapCache</code> and calculates an initial + * line wrapping. + * <p> + * + * @param parent the StyledText widget to wrap content in. + * @param content the content provider that does the actual line wrapping. + */ + public WordWrapCache(StyledText parent, WrappedContent content) { + this.parent = parent; + visualContent = content; + visualContent.wrapLines(); + } + /** + * Do nothing. Lines are wrapped immediately after reset. + * <p> + * + * @param startLine first line to calculate + * @param lineCount number of lines to calculate + */ + public void calculate(int startLine, int lineCount) { + } + /** + * Returns the client area width. Lines are wrapped so there + * is no horizontal scroll bar. + * <p> + * + * @return the line width + */ + public int getWidth() { + return parent.getClientArea().width; + } + /** + * Wraps the lines in the specified range. + * This method is called in <code>StyledText.redraw()</code>. + * A redraw is therefore not necessary. + * <p> + * + * @param startLine the first line to reset + * @param lineCount the number of lines to reset + * @param calculateMaxWidth true=implementors should retain a + * valid width even if it is affected by the reset operation. + * false=the width may be set to 0 + */ + public void redrawReset(int startLine, int lineCount, boolean calculateMaxWidth) { + if (lineCount == visualContent.getLineCount()) { + // do a full rewrap if all lines are reset + visualContent.wrapLines(); + } + else { + visualContent.reset(startLine, lineCount); + } + } + /** + * Rewraps the lines in the specified range and redraws + * the widget if the line wrapping has changed. + * <p> + * + * @param startLine the first line to reset + * @param lineCount the number of lines to reset + * @param calculateMaxWidth true=implementors should retain a + * valid width even if it is affected by the reset operation. + * false=the width may be set to 0 + */ + public void reset(int startLine, int lineCount, boolean calculateMaxWidth) { + int itemCount = getPartialBottomIndex() - topIndex + 1; + int[] oldLineOffsets = new int[itemCount]; + + for (int i = 0; i < itemCount; i++) { + oldLineOffsets[i] = visualContent.getOffsetAtLine(i + topIndex); + } + redrawReset(startLine, lineCount, calculateMaxWidth); + // check for cases which will require a full redraw + if (getPartialBottomIndex() - topIndex + 1 != itemCount) { + // number of visible lines has changed + parent.internalRedraw(); + } + else { + for (int i = 0; i < itemCount; i++) { + if (visualContent.getOffsetAtLine(i + topIndex) != oldLineOffsets[i]) { + // wrapping of one of the visible lines has changed + parent.internalRedraw(); + break; + } + } + } + } + /** + * Passes the text change notification to the line wrap content. + * <p> + * + * @param startOffset the start offset of the text change + * @param newLineCount the number of inserted lines + * @param replaceLineCount the number of deleted lines + * @param newCharCount the number of new characters + * @param replaceCharCount the number of deleted characters + */ + public void textChanged(int startOffset, int newLineCount, int replaceLineCount, int newCharCount, int replaceCharCount) { + int startLine = visualContent.getLineAtOffset(startOffset); + visualContent.textChanged(startOffset, newLineCount, replaceLineCount, newCharCount, replaceCharCount); + + // if we are wrapping then it is possible for a deletion on the last + // line of text to shorten the total text length by a line. If this + // occurs then the startIndex must be adjusted such that a redraw will + // be performed if a visible region is affected. fixes bug 42947. + if (wordWrap) { + int lineCount = content.getLineCount(); + if (startLine >= lineCount) startLine = lineCount - 1; + } + if (startLine <= getPartialBottomIndex()) { + // only redraw if the text change affects text inside or above + // the visible lines. if it is below the visible lines it will + // not affect the word wrapping. fixes bug 14047. + parent.internalRedraw(); + } + } + } + +/** + * Constructs a new instance of this class given its parent + * and a style value describing its behavior and appearance. + * <p> + * The style value is either one of the style constants defined in + * class <code>SWT</code> which is applicable to instances of this + * class, or must be built by <em>bitwise OR</em>'ing together + * (that is, using the <code>int</code> "|" operator) two or more + * of those <code>SWT</code> style constants. The class description + * lists the style constants that are applicable to the class. + * Style bits are also inherited from superclasses. + * </p> + * + * @param parent a widget which will be the parent of the new instance (cannot be null) + * @param style the style of widget to construct + * + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT - if the parent is null</li> + * </ul> + * @exception SWTException <ul> + * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li> + * </ul> + * + * @see SWT#FULL_SELECTION + * @see SWT#MULTI + * @see SWT#READ_ONLY + * @see SWT#SINGLE + * @see SWT#WRAP + * @see #getStyle + */ +public StyledText(Composite parent, int style) { + super(parent, checkStyle(style | SWT.NO_REDRAW_RESIZE | SWT.NO_BACKGROUND)); + // set the bg/fg in the OS to ensure that these are the same as StyledText, necessary + // for ensuring that the bg/fg the IME box uses is the same as what StyledText uses + super.setForeground(getForeground()); + super.setBackground(getBackground()); + Display display = getDisplay(); + isMirrored = (super.getStyle() & SWT.MIRRORED) != 0; + if ((style & SWT.READ_ONLY) != 0) { + setEditable(false); + } + leftMargin = rightMargin = isBidiCaret() ? BIDI_CARET_WIDTH - 1: 0; + if ((style & SWT.SINGLE) != 0 && (style & SWT.BORDER) != 0) { + leftMargin = topMargin = rightMargin = bottomMargin = 2; + } + clipboard = new Clipboard(display); + installDefaultContent(); + initializeRenderer(); + if ((style & SWT.WRAP) != 0) { + setWordWrap(true); + } + else { + lineCache = new ContentWidthCache(this, content); + } + defaultCaret = new Caret(this, SWT.NULL); + if (isBidiCaret()) { + createCaretBitmaps(); + Runnable runnable = new Runnable() { + public void run() { + int direction = BidiUtil.getKeyboardLanguage() == BidiUtil.KEYBOARD_BIDI ? SWT.RIGHT : SWT.LEFT; + if (direction == caretDirection) return; + if (getCaret() != defaultCaret) return; + int lineIndex = getCaretLine(); + String line = content.getLine(lineIndex); + int lineOffset = content.getOffsetAtLine(lineIndex); + int offsetInLine = caretOffset - lineOffset; + int newCaretX = getXAtOffset(line, lineIndex, offsetInLine); + setCaretLocation(newCaretX, getCaretLine(), direction); + } + }; + BidiUtil.addLanguageListener(handle, runnable); + } + setCaret(defaultCaret); + calculateScrollBars(); + createKeyBindings(); + ibeamCursor = new Cursor(display, SWT.CURSOR_IBEAM); + setCursor(ibeamCursor); + installListeners(); + installDefaultLineStyler(); + initializeAccessible(); +} +/** + * Adds an extended modify listener. An ExtendedModify event is sent by the + * widget when the widget text has changed. + * <p> + * + * @param extendedModifyListener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void addExtendedModifyListener(ExtendedModifyListener extendedModifyListener) { + checkWidget(); + if (extendedModifyListener == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + StyledTextListener typedListener = new StyledTextListener(extendedModifyListener); + addListener(ExtendedModify, typedListener); +} +/** + * Maps a key to an action. + * One action can be associated with N keys. However, each key can only + * have one action (key:action is N:1 relation). + * <p> + * + * @param key a key code defined in SWT.java or a character. + * Optionally ORd with a state mask. Preferred state masks are one or more of + * SWT.MOD1, SWT.MOD2, SWT.MOD3, since these masks account for modifier platform + * differences. However, there may be cases where using the specific state masks + * (i.e., SWT.CTRL, SWT.SHIFT, SWT.ALT, SWT.COMMAND) makes sense. + * @param action one of the predefined actions defined in ST.java. + * Use SWT.NULL to remove a key binding. + * @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> + */ +public void setKeyBinding(int key, int action) { + checkWidget(); + + int keyValue = key & SWT.KEY_MASK; + int modifierValue = key & SWT.MODIFIER_MASK; + char keyChar = (char)keyValue; + + if (Compatibility.isLetter(keyChar)) { + // make the keybinding case insensitive by adding it + // in its upper and lower case form + char ch = Character.toUpperCase(keyChar); + int newKey = ch | modifierValue; + if (action == SWT.NULL) { + keyActionMap.remove(new Integer(newKey)); + } + else { + keyActionMap.put(new Integer(newKey), new Integer(action)); + } + ch = Character.toLowerCase(keyChar); + newKey = ch | modifierValue; + if (action == SWT.NULL) { + keyActionMap.remove(new Integer(newKey)); + } + else { + keyActionMap.put(new Integer(newKey), new Integer(action)); + } + } else { + if (action == SWT.NULL) { + keyActionMap.remove(new Integer(key)); + } + else { + keyActionMap.put(new Integer(key), new Integer(action)); + } + } + +} +/** + * Adds a bidirectional segment listener. A BidiSegmentEvent is sent + * whenever a line of text is measured or rendered. The user can + * specify text ranges in the line that should be treated as if they + * had a different direction than the surrounding text. + * This may be used when adjacent segments of right-to-left text should + * not be reordered relative to each other. + * E.g., Multiple Java string literals in a right-to-left language + * should generally remain in logical order to each other, that is, the + * way they are stored. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + * @see BidiSegmentEvent + * @since 2.0 + */ +public void addBidiSegmentListener(BidiSegmentListener listener) { + checkWidget(); + if (listener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + StyledTextListener typedListener = new StyledTextListener(listener); + addListener(LineGetSegments, typedListener); +} +/** + * Adds a line background listener. A LineGetBackground event is sent by the + * widget to determine the background color for a line. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void addLineBackgroundListener(LineBackgroundListener listener) { + checkWidget(); + if (listener == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + if (!userLineBackground) { + removeLineBackgroundListener(defaultLineStyler); + defaultLineStyler.setLineBackground(0, logicalContent.getLineCount(), null); + userLineBackground = true; + } + StyledTextListener typedListener = new StyledTextListener(listener); + addListener(LineGetBackground, typedListener); +} +/** + * Adds a line style listener. A LineGetStyle event is sent by the widget to + * determine the styles for a line. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void addLineStyleListener(LineStyleListener listener) { + checkWidget(); + if (listener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + if (!userLineStyle) { + removeLineStyleListener(defaultLineStyler); + defaultLineStyler.setStyleRange(null); + userLineStyle = true; + } + StyledTextListener typedListener = new StyledTextListener(listener); + addListener(LineGetStyle, typedListener); +} +/** + * Adds a modify listener. A Modify event is sent by the widget when the widget text + * has changed. + * <p> + * + * @param modifyListener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void addModifyListener(ModifyListener modifyListener) { + checkWidget(); + if (modifyListener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + TypedListener typedListener = new TypedListener(modifyListener); + addListener(SWT.Modify, typedListener); +} +/** + * Adds a selection listener. A Selection event is sent by the widget when the + * selection has changed. + * <p> + * When <code>widgetSelected</code> is called, the event x amd y fields contain + * the start and end caret indices of the selection. + * <code>widgetDefaultSelected</code> is not called for StyledTexts. + * </p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void addSelectionListener(SelectionListener listener) { + checkWidget(); + if (listener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + TypedListener typedListener = new TypedListener(listener); + addListener(SWT.Selection, typedListener); +} +/** + * Adds a verify key listener. A VerifyKey event is sent by the widget when a key + * is pressed. The widget ignores the key press if the listener sets the doit field + * of the event to false. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void addVerifyKeyListener(VerifyKeyListener listener) { + checkWidget(); + if (listener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + StyledTextListener typedListener = new StyledTextListener(listener); + addListener(VerifyKey, typedListener); +} +/** + * Adds a verify listener. A Verify event is sent by the widget when the widget text + * is about to change. The listener can set the event text and the doit field to + * change the text that is set in the widget or to force the widget to ignore the + * text change. + * <p> + * + * @param verifyListener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void addVerifyListener(VerifyListener verifyListener) { + checkWidget(); + if (verifyListener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + TypedListener typedListener = new TypedListener(verifyListener); + addListener(SWT.Verify, typedListener); +} +/** + * Appends a string to the text at the end of the widget. + * <p> + * + * @param string the string to be appended + * @see #replaceTextRange(int,int,String) + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void append(String string) { + checkWidget(); + if (string == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + int lastChar = Math.max(getCharCount(), 0); + replaceTextRange(lastChar, 0, string); +} +/** + * Calculates the width of the widest visible line. + */ +void calculateContentWidth() { + lineCache = getLineCache(content); + lineCache.calculate(topIndex, getPartialBottomIndex() - topIndex + 1); +} +/** + * Calculates the scroll bars + */ +void calculateScrollBars() { + ScrollBar horizontalBar = getHorizontalBar(); + ScrollBar verticalBar = getVerticalBar(); + + setScrollBars(); + if (verticalBar != null) { + verticalBar.setIncrement(getVerticalIncrement()); + } + if (horizontalBar != null) { + horizontalBar.setIncrement(getHorizontalIncrement()); + } +} +/** + * Calculates the top index based on the current vertical scroll offset. + * The top index is the index of the topmost fully visible line or the + * topmost partially visible line if no line is fully visible. + * The top index starts at 0. + */ +void calculateTopIndex() { + int oldTopIndex = topIndex; + int verticalIncrement = getVerticalIncrement(); + int clientAreaHeight = getClientArea().height; + + if (verticalIncrement == 0) { + return; + } + topIndex = Compatibility.ceil(verticalScrollOffset, verticalIncrement); + // Set top index to partially visible top line if no line is fully + // visible but at least some of the widget client area is visible. + // Fixes bug 15088. + if (topIndex > 0) { + if (clientAreaHeight > 0) { + int bottomPixel = verticalScrollOffset + clientAreaHeight; + int fullLineTopPixel = topIndex * verticalIncrement; + int fullLineVisibleHeight = bottomPixel - fullLineTopPixel; + // set top index to partially visible line if no line fully fits in + // client area or if space is available but not used (the latter should + // never happen because we use claimBottomFreeSpace) + if (fullLineVisibleHeight < verticalIncrement) { + topIndex--; + } + } + else + if (topIndex >= content.getLineCount()) { + topIndex = content.getLineCount() - 1; + } + } + if (topIndex != oldTopIndex) { + topOffset = content.getOffsetAtLine(topIndex); + lineCache.calculate(topIndex, getPartialBottomIndex() - topIndex + 1); + setHorizontalScrollBar(); + } +} +/** + * Hides the scroll bars if widget is created in single line mode. + */ +static int checkStyle(int style) { + if ((style & SWT.SINGLE) != 0) { + style &= ~(SWT.H_SCROLL | SWT.V_SCROLL | SWT.WRAP | SWT.MULTI); + } else { + style |= SWT.MULTI; + if ((style & SWT.WRAP) != 0) { + style &= ~SWT.H_SCROLL; + } + } + return style; +} +/** + * Scrolls down the text to use new space made available by a resize or by + * deleted lines. + */ +void claimBottomFreeSpace() { + int newVerticalOffset = Math.max(0, content.getLineCount() * lineHeight - getClientArea().height); + + if (newVerticalOffset < verticalScrollOffset) { + // Scroll up so that empty lines below last text line are used. + // Fixes 1GEYJM0 + setVerticalScrollOffset(newVerticalOffset, true); + } +} +/** + * Scrolls text to the right to use new space made available by a resize. + */ +void claimRightFreeSpace() { + int newHorizontalOffset = Math.max(0, lineCache.getWidth() - (getClientArea().width - leftMargin - rightMargin)); + + if (newHorizontalOffset < horizontalScrollOffset) { + // item is no longer drawn past the right border of the client area + // align the right end of the item with the right border of the + // client area (window is scrolled right). + scrollHorizontalBar(newHorizontalOffset - horizontalScrollOffset); + } +} +/** + * Clears the widget margin. + * + * @param gc GC to render on + * @param background background color to use for clearing the margin + * @param clientArea widget client area dimensions + */ +void clearMargin(GC gc, Color background, Rectangle clientArea, int y) { + // clear the margin background + gc.setBackground(background); + if (topMargin > 0) { + gc.fillRectangle(0, -y, clientArea.width, topMargin); + } + if (bottomMargin > 0) { + gc.fillRectangle(0, clientArea.height - bottomMargin - y, clientArea.width, bottomMargin); + } + if (leftMargin > 0) { + gc.fillRectangle(0, -y, leftMargin, clientArea.height); + } + if (rightMargin > 0) { + gc.fillRectangle(clientArea.width - rightMargin, -y, rightMargin, clientArea.height); + } +} +/** + * Removes the widget selection. + * <p> + * + * @param sendEvent a Selection event is sent when set to true and when the selection is actually reset. + */ +void clearSelection(boolean sendEvent) { + int selectionStart = selection.x; + int selectionEnd = selection.y; + int length = content.getCharCount(); + + resetSelection(); + // redraw old selection, if any + if (selectionEnd - selectionStart > 0) { + // called internally to remove selection after text is removed + // therefore make sure redraw range is valid. + int redrawStart = Math.min(selectionStart, length); + int redrawEnd = Math.min(selectionEnd, length); + if (redrawEnd - redrawStart > 0) { + internalRedrawRange(redrawStart, redrawEnd - redrawStart, true); + } + if (sendEvent) { + sendSelectionEvent(); + } + } +} +public Point computeSize (int wHint, int hHint, boolean changed) { + checkWidget(); + int count, width, height; + boolean singleLine = (getStyle() & SWT.SINGLE) != 0; + + if (singleLine) { + count = 1; + } else { + count = content.getLineCount(); + } + if (wHint != SWT.DEFAULT) { + width = wHint; + } + else { + width = DEFAULT_WIDTH; + } + if (wHint == SWT.DEFAULT) { + LineCache computeLineCache = lineCache; + if (wordWrap) { + // set non-wrapping content width calculator. Ensures ideal line width + // that does not required wrapping. Fixes bug 31195. + computeLineCache = new ContentWidthCache(this, logicalContent); + if (!singleLine) { + count = logicalContent.getLineCount(); + } + } + // Only calculate what can actually be displayed. + // Do this because measuring each text line is a + // time-consuming process. + int visibleCount = Math.min (count, getDisplay().getBounds().height / lineHeight); + computeLineCache.calculate(0, visibleCount); + width = computeLineCache.getWidth() + leftMargin + rightMargin; + } + else + if (wordWrap && !singleLine) { + // calculate to wrap to width hint. Fixes bug 20377. + // don't wrap live content. Fixes bug 38344. + WrappedContent wrappedContent = new WrappedContent(renderer, logicalContent); + wrappedContent.wrapLines(width); + count = wrappedContent.getLineCount(); + } + if (hHint != SWT.DEFAULT) { + height = hHint; + } + else { + height = count * lineHeight + topMargin + bottomMargin; + } + // Use default values if no text is defined. + if (width == 0) { + width = DEFAULT_WIDTH; + } + if (height == 0) { + if (singleLine) { + height = lineHeight; + } + else { + height = DEFAULT_HEIGHT; + } + } + Rectangle rect = computeTrim(0, 0, width, height); + return new Point (rect.width, rect.height); +} +/** + * Copies the selected text to the <code>DND.CLIPBOARD</code> clipboard. + * The text will be put on the clipboard in plain text format and RTF format. + * The <code>DND.CLIPBOARD</code> clipboard is used for data that is + * transferred by keyboard accelerator (such as Ctrl+C/Ctrl+V) or + * by menu action. + * + * <p> + * + * @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> + */ +public void copy() { + checkWidget(); + copy(DND.CLIPBOARD); +} + +/** + * Copies the selected text to the specified clipboard. The text will be put in the + * clipboard in plain text format and RTF format. + * + * <p>The clipboardType is one of the clipboard constants defined in class + * <code>DND</code>. The <code>DND.CLIPBOARD</code> clipboard is + * used for data that is transferred by keyboard accelerator (such as Ctrl+C/Ctrl+V) + * or by menu action. The <code>DND.SELECTION_CLIPBOARD</code> + * clipboard is used for data that is transferred by selecting text and pasting + * with the middle mouse button.</p> + * + * @param clipboardType indicates the type of clipboard + * + * @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> + * + * @since 3.1 + */ +public void copy(int clipboardType) { + checkWidget(); + if (clipboardType != DND.CLIPBOARD && + clipboardType != DND.SELECTION_CLIPBOARD) return; + int length = selection.y - selection.x; + if (length > 0) { + try { + setClipboardContent(selection.x, length, clipboardType); + } + catch (SWTError error) { + // Copy to clipboard failed. This happens when another application + // is accessing the clipboard while we copy. Ignore the error. + // Fixes 1GDQAVN + // Rethrow all other errors. Fixes bug 17578. + if (error.code != DND.ERROR_CANNOT_SET_CLIPBOARD) { + throw error; + } + } + } +} +/** + * Returns a string that uses only the line delimiter specified by the + * StyledTextContent implementation. + * Returns only the first line if the widget has the SWT.SINGLE style. + * <p> + * + * @param text the text that may have line delimiters that don't + * match the model line delimiter. Possible line delimiters + * are CR ('\r'), LF ('\n'), CR/LF ("\r\n") + * @return the converted text that only uses the line delimiter + * specified by the model. Returns only the first line if the widget + * has the SWT.SINGLE style. + */ +String getModelDelimitedText(String text) { + StringBuffer convertedText; + String delimiter = getLineDelimiter(); + int length = text.length(); + int crIndex = 0; + int lfIndex = 0; + int i = 0; + + if (length == 0) { + return text; + } + convertedText = new StringBuffer(length); + while (i < length) { + if (crIndex != -1) { + crIndex = text.indexOf(SWT.CR, i); + } + if (lfIndex != -1) { + lfIndex = text.indexOf(SWT.LF, i); + } + if (lfIndex == -1 && crIndex == -1) { // no more line breaks? + break; + } + else // CR occurs before LF or no LF present? + if ((crIndex < lfIndex && crIndex != -1) || lfIndex == -1) { + convertedText.append(text.substring(i, crIndex)); + if (lfIndex == crIndex + 1) { // CR/LF combination? + i = lfIndex + 1; + } + else { + i = crIndex + 1; + } + } + else { // LF occurs before CR! + convertedText.append(text.substring(i, lfIndex)); + i = lfIndex + 1; + } + if (isSingleLine()) { + break; + } + convertedText.append(delimiter); + } + // copy remaining text if any and if not in single line mode or no + // text copied thus far (because there only is one line) + if (i < length && (!isSingleLine() || convertedText.length() == 0)) { + convertedText.append(text.substring(i)); + } + return convertedText.toString(); +} +/** + * Creates default key bindings. + */ +void createKeyBindings() { + int nextKey = isMirrored() ? SWT.ARROW_LEFT : SWT.ARROW_RIGHT; + int previousKey = isMirrored() ? SWT.ARROW_RIGHT : SWT.ARROW_LEFT; + + // Navigation + setKeyBinding(SWT.ARROW_UP, ST.LINE_UP); + setKeyBinding(SWT.ARROW_DOWN, ST.LINE_DOWN); + setKeyBinding(SWT.HOME, ST.LINE_START); + setKeyBinding(SWT.END, ST.LINE_END); + setKeyBinding(SWT.PAGE_UP, ST.PAGE_UP); + setKeyBinding(SWT.PAGE_DOWN, ST.PAGE_DOWN); + setKeyBinding(SWT.HOME | SWT.MOD1, ST.TEXT_START); + setKeyBinding(SWT.END | SWT.MOD1, ST.TEXT_END); + setKeyBinding(SWT.PAGE_UP | SWT.MOD1, ST.WINDOW_START); + setKeyBinding(SWT.PAGE_DOWN | SWT.MOD1, ST.WINDOW_END); + setKeyBinding(nextKey, ST.COLUMN_NEXT); + setKeyBinding(previousKey, ST.COLUMN_PREVIOUS); + setKeyBinding(nextKey | SWT.MOD1, ST.WORD_NEXT); + setKeyBinding(previousKey | SWT.MOD1, ST.WORD_PREVIOUS); + + // Selection + setKeyBinding(SWT.ARROW_UP | SWT.MOD2, ST.SELECT_LINE_UP); + setKeyBinding(SWT.ARROW_DOWN | SWT.MOD2, ST.SELECT_LINE_DOWN); + setKeyBinding(SWT.HOME | SWT.MOD2, ST.SELECT_LINE_START); + setKeyBinding(SWT.END | SWT.MOD2, ST.SELECT_LINE_END); + setKeyBinding(SWT.PAGE_UP | SWT.MOD2, ST.SELECT_PAGE_UP); + setKeyBinding(SWT.PAGE_DOWN | SWT.MOD2, ST.SELECT_PAGE_DOWN); + setKeyBinding(SWT.HOME | SWT.MOD1 | SWT.MOD2, ST.SELECT_TEXT_START); + setKeyBinding(SWT.END | SWT.MOD1 | SWT.MOD2, ST.SELECT_TEXT_END); + setKeyBinding(SWT.PAGE_UP | SWT.MOD1 | SWT.MOD2, ST.SELECT_WINDOW_START); + setKeyBinding(SWT.PAGE_DOWN | SWT.MOD1 | SWT.MOD2, ST.SELECT_WINDOW_END); + setKeyBinding(nextKey | SWT.MOD2, ST.SELECT_COLUMN_NEXT); + setKeyBinding(previousKey | SWT.MOD2, ST.SELECT_COLUMN_PREVIOUS); + setKeyBinding(nextKey | SWT.MOD1 | SWT.MOD2, ST.SELECT_WORD_NEXT); + setKeyBinding(previousKey | SWT.MOD1 | SWT.MOD2, ST.SELECT_WORD_PREVIOUS); + + // Modification + // Cut, Copy, Paste + setKeyBinding('X' | SWT.MOD1, ST.CUT); + setKeyBinding('C' | SWT.MOD1, ST.COPY); + setKeyBinding('V' | SWT.MOD1, ST.PASTE); + // Cut, Copy, Paste Wordstar style + setKeyBinding(SWT.DEL | SWT.MOD2, ST.CUT); + setKeyBinding(SWT.INSERT | SWT.MOD1, ST.COPY); + setKeyBinding(SWT.INSERT | SWT.MOD2, ST.PASTE); + setKeyBinding(SWT.BS | SWT.MOD2, ST.DELETE_PREVIOUS); + + setKeyBinding(SWT.BS, ST.DELETE_PREVIOUS); + setKeyBinding(SWT.DEL, ST.DELETE_NEXT); + setKeyBinding(SWT.BS | SWT.MOD1, ST.DELETE_WORD_PREVIOUS); + setKeyBinding(SWT.DEL | SWT.MOD1, ST.DELETE_WORD_NEXT); + + // Miscellaneous + setKeyBinding(SWT.INSERT, ST.TOGGLE_OVERWRITE); +} +/** + * Create the bitmaps to use for the caret in bidi mode. This + * method only needs to be called upon widget creation and when the + * font changes (the caret bitmap height needs to match font height). + */ +void createCaretBitmaps() { + int caretWidth = BIDI_CARET_WIDTH; + Display display = getDisplay(); + if (leftCaretBitmap != null) { + if (defaultCaret != null && leftCaretBitmap.equals(defaultCaret.getImage())) { + defaultCaret.setImage(null); + } + leftCaretBitmap.dispose(); + } + leftCaretBitmap = new Image(display, caretWidth, lineHeight); + GC gc = new GC (leftCaretBitmap); + gc.setBackground(display.getSystemColor(SWT.COLOR_BLACK)); + gc.fillRectangle(0, 0, caretWidth, lineHeight); + gc.setForeground(display.getSystemColor(SWT.COLOR_WHITE)); + gc.drawLine(0,0,0,lineHeight); + gc.drawLine(0,0,caretWidth-1,0); + gc.drawLine(0,1,1,1); + gc.dispose(); + + if (rightCaretBitmap != null) { + if (defaultCaret != null && rightCaretBitmap.equals(defaultCaret.getImage())) { + defaultCaret.setImage(null); + } + rightCaretBitmap.dispose(); + } + rightCaretBitmap = new Image(display, caretWidth, lineHeight); + gc = new GC (rightCaretBitmap); + gc.setBackground(display.getSystemColor(SWT.COLOR_BLACK)); + gc.fillRectangle(0, 0, caretWidth, lineHeight); + gc.setForeground(display.getSystemColor(SWT.COLOR_WHITE)); + gc.drawLine(caretWidth-1,0,caretWidth-1,lineHeight); + gc.drawLine(0,0,caretWidth-1,0); + gc.drawLine(caretWidth-1,1,1,1); + gc.dispose(); +} +/** + * Moves the selected text to the clipboard. The text will be put in the + * clipboard in plain text format and RTF format. + * <p> + * + * @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> + */ +public void cut(){ + checkWidget(); + int length = selection.y - selection.x; + + if (length > 0) { + try { + setClipboardContent(selection.x, length, DND.CLIPBOARD); + } + catch (SWTError error) { + // Copy to clipboard failed. This happens when another application + // is accessing the clipboard while we copy. Ignore the error. + // Fixes 1GDQAVN + // Rethrow all other errors. Fixes bug 17578. + if (error.code != DND.ERROR_CANNOT_SET_CLIPBOARD) { + throw error; + } + // Abort cut operation if copy to clipboard fails. + // Fixes bug 21030. + return; + } + doDelete(); + } +} +/** + * A mouse move event has occurred. See if we should start autoscrolling. If + * the move position is outside of the client area, initiate autoscrolling. + * Otherwise, we've moved back into the widget so end autoscrolling. + */ +void doAutoScroll(Event event) { + Rectangle area = getClientArea(); + + if (event.y > area.height) { + doAutoScroll(SWT.DOWN, event.y - area.height); + } + else + if (event.y < 0) { + doAutoScroll(SWT.UP, -event.y); + } + else + if (event.x < leftMargin && !wordWrap) { + doAutoScroll(ST.COLUMN_PREVIOUS, leftMargin - event.x); + } + else + if (event.x > area.width - leftMargin - rightMargin && !wordWrap) { + doAutoScroll(ST.COLUMN_NEXT, event.x - (area.width - leftMargin - rightMargin)); + } + else { + endAutoScroll(); + } +} +/** + * Initiates autoscrolling. + * <p> + * + * @param direction SWT.UP, SWT.DOWN, SWT.COLUMN_NEXT, SWT.COLUMN_PREVIOUS + */ +void doAutoScroll(int direction, int distance) { + Runnable timer = null; + + autoScrollDistance = distance; + + // If we're already autoscrolling in the given direction do nothing + if (autoScrollDirection == direction) { + return; + } + + final Display display = getDisplay(); + // Set a timer that will simulate the user pressing and holding + // down a cursor key (i.e., arrowUp, arrowDown). + if (direction == SWT.UP) { + timer = new Runnable() { + public void run() { + if (autoScrollDirection == SWT.UP) { + int lines = (autoScrollDistance / getLineHeight()) + 1; + doSelectionPageUp(lines); + display.timerExec(V_SCROLL_RATE, this); + } + } + }; + autoScrollDirection = direction; + display.timerExec(V_SCROLL_RATE, timer); + } else if (direction == SWT.DOWN) { + timer = new Runnable() { + public void run() { + if (autoScrollDirection == SWT.DOWN) { + int lines = (autoScrollDistance / getLineHeight()) + 1; + doSelectionPageDown(lines); + display.timerExec(V_SCROLL_RATE, this); + } + } + }; + autoScrollDirection = direction; + display.timerExec(V_SCROLL_RATE, timer); + } else if (direction == ST.COLUMN_NEXT) { + timer = new Runnable() { + public void run() { + if (autoScrollDirection == ST.COLUMN_NEXT) { + doVisualNext(); + setMouseWordSelectionAnchor(); + doMouseSelection(); + display.timerExec(H_SCROLL_RATE, this); + } + } + }; + autoScrollDirection = direction; + display.timerExec(H_SCROLL_RATE, timer); + } else if (direction == ST.COLUMN_PREVIOUS) { + timer = new Runnable() { + public void run() { + if (autoScrollDirection == ST.COLUMN_PREVIOUS) { + doVisualPrevious(); + setMouseWordSelectionAnchor(); + doMouseSelection(); + display.timerExec(H_SCROLL_RATE, this); + } + } + }; + autoScrollDirection = direction; + display.timerExec(H_SCROLL_RATE, timer); + } +} +/** + * Deletes the previous character. Delete the selected text if any. + * Move the caret in front of the deleted text. + */ +void doBackspace() { + Event event = new Event(); + event.text = ""; + if (selection.x != selection.y) { + event.start = selection.x; + event.end = selection.y; + sendKeyEvent(event); + } + else + if (caretOffset > 0) { + int line = content.getLineAtOffset(caretOffset); + int lineOffset = content.getOffsetAtLine(line); + + if (caretOffset == lineOffset) { + lineOffset = content.getOffsetAtLine(line - 1); + event.start = lineOffset + content.getLine(line - 1).length(); + event.end = caretOffset; + } + else { + String lineText = content.getLine(line); + TextLayout layout = renderer.getTextLayout(lineText, lineOffset); + int start = layout.getPreviousOffset(caretOffset - lineOffset, SWT.MOVEMENT_CHAR); + renderer.disposeTextLayout(layout); + event.start = start + lineOffset; + event.end = caretOffset; + } + sendKeyEvent(event); + } +} +/** + * Replaces the selection with the character or insert the character at the + * current caret position if no selection exists. + * If a carriage return was typed replace it with the line break character + * used by the widget on this platform. + * <p> + * + * @param key the character typed by the user + */ +void doContent(char key) { + Event event; + + if (textLimit > 0 && + content.getCharCount() - (selection.y - selection.x) >= textLimit) { + return; + } + event = new Event(); + event.start = selection.x; + event.end = selection.y; + // replace a CR line break with the widget line break + // CR does not make sense on Windows since most (all?) applications + // don't recognize CR as a line break. + if (key == SWT.CR || key == SWT.LF) { + if (!isSingleLine()) { + event.text = getLineDelimiter(); + } + } + // no selection and overwrite mode is on and the typed key is not a + // tab character (tabs are always inserted without overwriting)? + else + if (selection.x == selection.y && overwrite && key != TAB) { + int lineIndex = content.getLineAtOffset(event.end); + int lineOffset = content.getOffsetAtLine(lineIndex); + String line = content.getLine(lineIndex); + // replace character at caret offset if the caret is not at the + // end of the line + if (event.end < lineOffset + line.length()) { + event.end++; + } + event.text = new String(new char[] {key}); + } + else { + event.text = new String(new char[] {key}); + } + if (event.text != null) { + sendKeyEvent(event); + } +} +/** + * Moves the caret after the last character of the widget content. + */ +void doContentEnd() { + // place caret at end of first line if receiver is in single + // line mode. fixes 4820. + if (isSingleLine()) { + doLineEnd(); + } + else { + int length = content.getCharCount(); + if (caretOffset < length) { + caretOffset = length; + showCaret(); + } + } +} +/** + * Moves the caret in front of the first character of the widget content. + */ +void doContentStart() { + if (caretOffset > 0) { + caretOffset = 0; + showCaret(); + } +} +/** + * Moves the caret to the start of the selection if a selection exists. + * Otherwise, if no selection exists move the cursor according to the + * cursor selection rules. + * <p> + * + * @see #doSelectionCursorPrevious + */ +void doCursorPrevious() { + advancing = false; + if (selection.y - selection.x > 0) { + int caretLine; + + caretOffset = selection.x; + caretLine = getCaretLine(); + showCaret(caretLine); + } + else { + doSelectionCursorPrevious(); + } +} +/** + * Moves the caret to the end of the selection if a selection exists. + * Otherwise, if no selection exists move the cursor according to the + * cursor selection rules. + * <p> + * + * @see #doSelectionCursorNext + */ +void doCursorNext() { + advancing = true; + if (selection.y - selection.x > 0) { + int caretLine; + + caretOffset = selection.y; + caretLine = getCaretLine(); + showCaret(caretLine); + } + else { + doSelectionCursorNext(); + } +} +/** + * Deletes the next character. Delete the selected text if any. + */ +void doDelete() { + Event event = new Event(); + event.text = ""; + if (selection.x != selection.y) { + event.start = selection.x; + event.end = selection.y; + sendKeyEvent(event); + } + else + if (caretOffset < content.getCharCount()) { + int line = content.getLineAtOffset(caretOffset); + int lineOffset = content.getOffsetAtLine(line); + int lineLength = content.getLine(line).length(); + + if (caretOffset == lineOffset + lineLength) { + event.start = caretOffset; + event.end = content.getOffsetAtLine(line + 1); + } + else { + event.start = caretOffset; + event.end = getClusterNext(caretOffset, line); + } + sendKeyEvent(event); + } +} +/** + * Deletes the next word. + */ +void doDeleteWordNext() { + if (selection.x != selection.y) { + // if a selection exists, treat the as if + // only the delete key was pressed + doDelete(); + } else { + Event event = new Event(); + event.text = ""; + event.start = caretOffset; + event.end = getWordEnd(caretOffset); + sendKeyEvent(event); + } +} +/** + * Deletes the previous word. + */ +void doDeleteWordPrevious() { + if (selection.x != selection.y) { + // if a selection exists, treat as if + // only the backspace key was pressed + doBackspace(); + } else { + Event event = new Event(); + event.text = ""; + event.start = getWordStart(caretOffset); + event.end = caretOffset; + sendKeyEvent(event); + } +} +/** + * Moves the caret one line down and to the same character offset relative + * to the beginning of the line. Move the caret to the end of the new line + * if the new line is shorter than the character offset. + * + * @return index of the new line relative to the first line in the document + */ +int doLineDown() { + if (isSingleLine()) { + return 0; + } + // allow line down action only if receiver is not in single line mode. + // fixes 4820. + int caretLine = getCaretLine(); + if (caretLine < content.getLineCount() - 1) { + caretLine++; + caretOffset = getOffsetAtMouseLocation(columnX, caretLine); + } + return caretLine; +} +/** + * Moves the caret to the end of the line. + */ +void doLineEnd() { + int caretLine = getCaretLine(); + int lineOffset = content.getOffsetAtLine(caretLine); + int lineLength = content.getLine(caretLine).length(); + int lineEndOffset = lineOffset + lineLength; + + if (caretOffset < lineEndOffset) { + caretOffset = lineEndOffset; + showCaret(); + } +} +/** + * Moves the caret to the beginning of the line. + */ +void doLineStart() { + int caretLine = getCaretLine(); + int lineOffset = content.getOffsetAtLine(caretLine); + if (caretOffset > lineOffset) { + caretOffset = lineOffset; + showCaret(caretLine); + } +} +/** + * Moves the caret one line up and to the same character offset relative + * to the beginning of the line. Move the caret to the end of the new line + * if the new line is shorter than the character offset. + * + * @return index of the new line relative to the first line in the document + */ +int doLineUp() { + int caretLine = getCaretLine(); + if (caretLine > 0) { + caretLine--; + caretOffset = getOffsetAtMouseLocation(columnX, caretLine); + } + return caretLine; +} +/** + * Moves the caret to the specified location. + * <p> + * + * @param x x location of the new caret position + * @param y y location of the new caret position + * @param select the location change is a selection operation. + * include the line delimiter in the selection + */ +void doMouseLocationChange(int x, int y, boolean select) { + int line = (y + verticalScrollOffset) / lineHeight; + int lineCount = content.getLineCount(); + int newCaretOffset; + int newCaretLine; + boolean oldAdvancing = advancing; + + updateCaretDirection = true; + if (line > lineCount - 1) { + line = lineCount - 1; + } + // allow caret to be placed below first line only if receiver is + // not in single line mode. fixes 4820. + if (line < 0 || (isSingleLine() && line > 0)) { + return; + } + newCaretOffset = getOffsetAtMouseLocation(x, line); + + if (mouseDoubleClick) { + // double click word select the previous/next word. fixes bug 15610 + newCaretOffset = doMouseWordSelect(x, newCaretOffset, line); + } + newCaretLine = content.getLineAtOffset(newCaretOffset); + // Is the mouse within the left client area border or on + // a different line? If not the autoscroll selection + // could be incorrectly reset. Fixes 1GKM3XS + if (y >= 0 && y < getClientArea().height && + (x >= 0 && x < getClientArea().width || wordWrap || + newCaretLine != content.getLineAtOffset(caretOffset))) { + if (newCaretOffset != caretOffset || advancing != oldAdvancing) { + caretOffset = newCaretOffset; + if (select) { + doMouseSelection(); + } + showCaret(); + } + } + if (!select) { + caretOffset = newCaretOffset; + clearSelection(true); + } +} +/** + * Updates the selection based on the caret position + */ +void doMouseSelection() { + if (caretOffset <= selection.x || + (caretOffset > selection.x && + caretOffset < selection.y && selectionAnchor == selection.x)) { + doSelection(ST.COLUMN_PREVIOUS); + } + else { + doSelection(ST.COLUMN_NEXT); + } +} +/** + * Returns the offset of the word at the specified offset. + * If the current selection extends from high index to low index + * (i.e., right to left, or caret is at left border of selecton on + * non-bidi platforms) the start offset of the word preceeding the + * selection is returned. If the current selection extends from + * low index to high index the end offset of the word following + * the selection is returned. + * + * @param x mouse x location + * @param newCaretOffset caret offset of the mouse cursor location + * @param line line index of the mouse cursor location + */ +int doMouseWordSelect(int x, int newCaretOffset, int line) { + int wordOffset; + + // flip selection anchor based on word selection direction from + // base double click. Always do this here (and don't rely on doAutoScroll) + // because auto scroll only does not cover all possible mouse selections + // (e.g., mouse x < 0 && mouse y > caret line y) + if (newCaretOffset < selectionAnchor && selectionAnchor == selection.x) { + selectionAnchor = doubleClickSelection.y; + } + else + if (newCaretOffset > selectionAnchor && selectionAnchor == selection.y) { + selectionAnchor = doubleClickSelection.x; + } + if (x >= 0 && x < getClientArea().width) { + // find the previous/next word + if (caretOffset == selection.x) { + wordOffset = getWordStart(newCaretOffset); + } + else { + wordOffset = getWordEndNoSpaces(newCaretOffset); + } + // mouse word select only on same line mouse cursor is on + if (content.getLineAtOffset(wordOffset) == line) { + newCaretOffset = wordOffset; + } + } + return newCaretOffset; +} +/** + * Scrolls one page down so that the last line (truncated or whole) + * of the current page becomes the fully visible top line. + * The caret is scrolled the same number of lines so that its location + * relative to the top line remains the same. The exception is the end + * of the text where a full page scroll is not possible. In this case + * the caret is moved after the last character. + * <p> + * + * @param select whether or not to select the page + */ +void doPageDown(boolean select, int lines) { + int lineCount = content.getLineCount(); + int oldColumnX = columnX; + int oldHScrollOffset = horizontalScrollOffset; + int caretLine; + + // do nothing if in single line mode. fixes 5673 + if (isSingleLine()) { + return; + } + caretLine = getCaretLine(); + if (caretLine < lineCount - 1) { + int verticalMaximum = lineCount * getVerticalIncrement(); + int pageSize = getClientArea().height; + int scrollLines = Math.min(lineCount - caretLine - 1, lines); + int scrollOffset; + + // ensure that scrollLines never gets negative and at leat one + // line is scrolled. fixes bug 5602. + scrollLines = Math.max(1, scrollLines); + caretLine += scrollLines; + caretOffset = getOffsetAtMouseLocation(columnX, caretLine); + if (select) { + doSelection(ST.COLUMN_NEXT); + } + // scroll one page down or to the bottom + scrollOffset = verticalScrollOffset + scrollLines * getVerticalIncrement(); + if (scrollOffset + pageSize > verticalMaximum) { + scrollOffset = verticalMaximum - pageSize; + } + if (scrollOffset > verticalScrollOffset) { + setVerticalScrollOffset(scrollOffset, true); + } + } + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(caretLine); + // restore the original horizontal caret position + int hScrollChange = oldHScrollOffset - horizontalScrollOffset; + columnX = oldColumnX + hScrollChange; +} +/** + * Moves the cursor to the end of the last fully visible line. + */ +void doPageEnd() { + // go to end of line if in single line mode. fixes 5673 + if (isSingleLine()) { + doLineEnd(); + } + else { + int line = getBottomIndex(); + int bottomCaretOffset = content.getOffsetAtLine(line) + content.getLine(line).length(); + + if (caretOffset < bottomCaretOffset) { + caretOffset = bottomCaretOffset; + showCaret(); + } + } +} +/** + * Moves the cursor to the beginning of the first fully visible line. + */ +void doPageStart() { + int topCaretOffset = content.getOffsetAtLine(topIndex); + + if (caretOffset > topCaretOffset) { + caretOffset = topCaretOffset; + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(topIndex); + } +} +/** + * Scrolls one page up so that the first line (truncated or whole) + * of the current page becomes the fully visible last line. + * The caret is scrolled the same number of lines so that its location + * relative to the top line remains the same. The exception is the beginning + * of the text where a full page scroll is not possible. In this case the + * caret is moved in front of the first character. + */ +void doPageUp(boolean select, int lines) { + int oldColumnX = columnX; + int oldHScrollOffset = horizontalScrollOffset; + int caretLine = getCaretLine(); + + if (caretLine > 0) { + int scrollLines = Math.max(1, Math.min(caretLine, lines)); + int scrollOffset; + + caretLine -= scrollLines; + caretOffset = getOffsetAtMouseLocation(columnX, caretLine); + if (select) { + doSelection(ST.COLUMN_PREVIOUS); + } + // scroll one page up or to the top + scrollOffset = Math.max(0, verticalScrollOffset - scrollLines * getVerticalIncrement()); + if (scrollOffset < verticalScrollOffset) { + setVerticalScrollOffset(scrollOffset, true); + } + } + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(caretLine); + // restore the original horizontal caret position + int hScrollChange = oldHScrollOffset - horizontalScrollOffset; + columnX = oldColumnX + hScrollChange; +} +/** + * Updates the selection to extend to the current caret position. + */ +void doSelection(int direction) { + int redrawStart = -1; + int redrawEnd = -1; + + if (selectionAnchor == -1) { + selectionAnchor = selection.x; + } + if (direction == ST.COLUMN_PREVIOUS) { + if (caretOffset < selection.x) { + // grow selection + redrawEnd = selection.x; + redrawStart = selection.x = caretOffset; + // check if selection has reversed direction + if (selection.y != selectionAnchor) { + redrawEnd = selection.y; + selection.y = selectionAnchor; + } + } + else // test whether selection actually changed. Fixes 1G71EO1 + if (selectionAnchor == selection.x && caretOffset < selection.y) { + // caret moved towards selection anchor (left side of selection). + // shrink selection + redrawEnd = selection.y; + redrawStart = selection.y = caretOffset; + } + } + else { + if (caretOffset > selection.y) { + // grow selection + redrawStart = selection.y; + redrawEnd = selection.y = caretOffset; + // check if selection has reversed direction + if (selection.x != selectionAnchor) { + redrawStart = selection.x; + selection.x = selectionAnchor; + } + } + else // test whether selection actually changed. Fixes 1G71EO1 + if (selectionAnchor == selection.y && caretOffset > selection.x) { + // caret moved towards selection anchor (right side of selection). + // shrink selection + redrawStart = selection.x; + redrawEnd = selection.x = caretOffset; + } + } + if (redrawStart != -1 && redrawEnd != -1) { + internalRedrawRange(redrawStart, redrawEnd - redrawStart, true); + sendSelectionEvent(); + } +} +/** + * Moves the caret to the next character or to the beginning of the + * next line if the cursor is at the end of a line. + */ +void doSelectionCursorNext() { + int caretLine = getCaretLine(); + int lineOffset = content.getOffsetAtLine(caretLine); + int offsetInLine = caretOffset - lineOffset; + advancing = true; + if (offsetInLine < content.getLine(caretLine).length()) { + caretOffset = getClusterNext(caretOffset, caretLine); + showCaret(); + } + else + if (caretLine < content.getLineCount() - 1 && !isSingleLine()) { + // only go to next line if not in single line mode. fixes 5673 + caretLine++; + caretOffset = content.getOffsetAtLine(caretLine); + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(caretLine); + } +} +/** + * Moves the caret to the previous character or to the end of the previous + * line if the cursor is at the beginning of a line. + */ +void doSelectionCursorPrevious() { + int caretLine = getCaretLine(); + int lineOffset = content.getOffsetAtLine(caretLine); + int offsetInLine = caretOffset - lineOffset; + advancing = false; + if (offsetInLine > 0) { + caretOffset = getClusterPrevious(caretOffset, caretLine); + showCaret(caretLine); + } + else + if (caretLine > 0) { + caretLine--; + lineOffset = content.getOffsetAtLine(caretLine); + caretOffset = lineOffset + content.getLine(caretLine).length(); + showCaret(); + } +} +/** + * Moves the caret one line down and to the same character offset relative + * to the beginning of the line. Moves the caret to the end of the new line + * if the new line is shorter than the character offset. + * Moves the caret to the end of the text if the caret already is on the + * last line. + * Adjusts the selection according to the caret change. This can either add + * to or subtract from the old selection, depending on the previous selection + * direction. + */ +void doSelectionLineDown() { + int oldColumnX; + int caretLine; + int lineStartOffset; + + if (isSingleLine()) { + return; + } + caretLine = getCaretLine(); + lineStartOffset = content.getOffsetAtLine(caretLine); + // reset columnX on selection + oldColumnX = columnX = getXAtOffset( + content.getLine(caretLine), caretLine, caretOffset - lineStartOffset); + if (caretLine == content.getLineCount() - 1) { + caretOffset = content.getCharCount(); + } + else { + caretLine = doLineDown(); + } + setMouseWordSelectionAnchor(); + // select first and then scroll to reduce flash when key + // repeat scrolls lots of lines + doSelection(ST.COLUMN_NEXT); + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(caretLine); + // save the original horizontal caret position + columnX = oldColumnX; +} +/** + * Moves the caret one line up and to the same character offset relative + * to the beginning of the line. Moves the caret to the end of the new line + * if the new line is shorter than the character offset. + * Moves the caret to the beginning of the document if it is already on the + * first line. + * Adjusts the selection according to the caret change. This can either add + * to or subtract from the old selection, depending on the previous selection + * direction. + */ +void doSelectionLineUp() { + int oldColumnX; + int caretLine = getCaretLine(); + int lineStartOffset = content.getOffsetAtLine(caretLine); + + // reset columnX on selection + oldColumnX = columnX = getXAtOffset( + content.getLine(caretLine), caretLine, caretOffset - lineStartOffset); + if (caretLine == 0) { + caretOffset = 0; + } + else { + caretLine = doLineUp(); + } + setMouseWordSelectionAnchor(); + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(caretLine); + doSelection(ST.COLUMN_PREVIOUS); + // save the original horizontal caret position + columnX = oldColumnX; +} +/** + * Scrolls one page down so that the last line (truncated or whole) + * of the current page becomes the fully visible top line. + * The caret is scrolled the same number of lines so that its location + * relative to the top line remains the same. The exception is the end + * of the text where a full page scroll is not possible. In this case + * the caret is moved after the last character. + * <p> + * Adjusts the selection according to the caret change. This can either add + * to or subtract from the old selection, depending on the previous selection + * direction. + * </p> + */ +void doSelectionPageDown(int lines) { + int oldColumnX; + int caretLine = getCaretLine(); + int lineStartOffset = content.getOffsetAtLine(caretLine); + + // reset columnX on selection + oldColumnX = columnX = getXAtOffset( + content.getLine(caretLine), caretLine, caretOffset - lineStartOffset); + doPageDown(true, lines); + columnX = oldColumnX; +} +/** + * Scrolls one page up so that the first line (truncated or whole) + * of the current page becomes the fully visible last line. + * The caret is scrolled the same number of lines so that its location + * relative to the top line remains the same. The exception is the beginning + * of the text where a full page scroll is not possible. In this case the + * caret is moved in front of the first character. + * <p> + * Adjusts the selection according to the caret change. This can either add + * to or subtract from the old selection, depending on the previous selection + * direction. + * </p> + */ +void doSelectionPageUp(int lines) { + int oldColumnX; + int caretLine = getCaretLine(); + int lineStartOffset = content.getOffsetAtLine(caretLine); + + // reset columnX on selection + oldColumnX = columnX = getXAtOffset( + content.getLine(caretLine), caretLine, caretOffset - lineStartOffset); + doPageUp(true, lines); + columnX = oldColumnX; +} +/** + * Moves the caret to the end of the next word . + */ +void doSelectionWordNext() { + int newCaretOffset = getWordEnd(caretOffset); + // Force symmetrical movement for word next and previous. Fixes 14536 + advancing = false; + // don't change caret position if in single line mode and the cursor + // would be on a different line. fixes 5673 + if (!isSingleLine() || + content.getLineAtOffset(caretOffset) == content.getLineAtOffset(newCaretOffset)) { + caretOffset = newCaretOffset; + showCaret(); + } +} +/** + * Moves the caret to the start of the previous word. + */ +void doSelectionWordPrevious() { + int caretLine; + advancing = false; + caretOffset = getWordStart(caretOffset); + caretLine = content.getLineAtOffset(caretOffset); + // word previous always comes from bottom line. when + // wrapping lines, stay on bottom line when on line boundary + if (wordWrap && caretLine < content.getLineCount() - 1 && + caretOffset == content.getOffsetAtLine(caretLine + 1)) { + caretLine++; + } + showCaret(caretLine); +} +/** + * Moves the caret one character to the left. Do not go to the previous line. + * When in a bidi locale and at a R2L character the caret is moved to the + * beginning of the R2L segment (visually right) and then one character to the + * left (visually left because it's now in a L2R segment). + */ +void doVisualPrevious() { + caretOffset = getClusterPrevious(caretOffset, getCaretLine()); + showCaret(); +} +/** + * Moves the caret one character to the right. Do not go to the next line. + * When in a bidi locale and at a R2L character the caret is moved to the + * end of the R2L segment (visually left) and then one character to the + * right (visually right because it's now in a L2R segment). + */ +void doVisualNext() { + caretOffset = getClusterNext(caretOffset, getCaretLine()); + showCaret(); +} +/** + * Moves the caret to the end of the next word. + * If a selection exists, move the caret to the end of the selection + * and remove the selection. + */ +void doWordNext() { + if (selection.y - selection.x > 0) { + int caretLine; + + caretOffset = selection.y; + caretLine = getCaretLine(); + showCaret(caretLine); + } + else { + doSelectionWordNext(); + } +} +/** + * Moves the caret to the start of the previous word. + * If a selection exists, move the caret to the start of the selection + * and remove the selection. + */ +void doWordPrevious() { + if (selection.y - selection.x > 0) { + int caretLine; + + caretOffset = selection.x; + caretLine = getCaretLine(); + showCaret(caretLine); + } + else { + doSelectionWordPrevious(); + } +} +/** + * Draws the specified rectangle. + * Draw directly without invalidating the affected area when clearBackground is + * false. + * <p> + * + * @param x the x position + * @param y the y position + * @param width the width + * @param height the height + * @param clearBackground true=clear the background by invalidating the requested + * redraw area, false=draw the foreground directly without invalidating the + * redraw area. + */ +void draw(int x, int y, int width, int height, boolean clearBackground) { + if (clearBackground) { + redraw(x + leftMargin, y + topMargin, width, height, true); + } + else { + int startLine = (y + verticalScrollOffset) / lineHeight; + int endY = y + height; + int paintYFromTopLine = (startLine - topIndex) * lineHeight; + int topLineOffset = (topIndex * lineHeight - verticalScrollOffset); + int paintY = paintYFromTopLine + topLineOffset + topMargin; // adjust y position for pixel based scrolling + int lineCount = content.getLineCount(); + Color background = getBackground(); + Color foreground = getForeground(); + GC gc = getGC(); + + if (isSingleLine()) { + lineCount = 1; + } + for (int i = startLine; paintY < endY && i < lineCount; i++, paintY += lineHeight) { + String line = content.getLine(i); + renderer.drawLine(line, i, paintY, gc, background, foreground, clearBackground); + } + gc.dispose(); + } +} +/** + * Ends the autoscroll process. + */ +void endAutoScroll() { + autoScrollDirection = SWT.NULL; +} +public Color getBackground() { + checkWidget(); + if (background == null) { + return getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND); + } + return background; +} +/** + * Returns the baseline, in pixels. + * + * @return baseline the baseline + * @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> + * @since 3.0 + */ +public int getBaseline() { + checkWidget(); + return renderer.getBaseline(); +} +/** + * Gets the BIDI coloring mode. When true the BIDI text display + * algorithm is applied to segments of text that are the same + * color. + * + * @return the current coloring mode + * @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> + * <p> + * @deprecated use BidiSegmentListener instead. + * </p> + */ +public boolean getBidiColoring() { + checkWidget(); + return bidiColoring; +} +/** + * Returns the index of the last fully visible line. + * <p> + * + * @return index of the last fully visible line. + */ +int getBottomIndex() { + int lineCount = 1; + + if (lineHeight != 0) { + // calculate the number of lines that are fully visible + int partialTopLineHeight = topIndex * lineHeight - verticalScrollOffset; + lineCount = (getClientArea().height - partialTopLineHeight) / lineHeight; + } + return Math.min(content.getLineCount() - 1, topIndex + Math.max(0, lineCount - 1)); +} +/** + * Returns the caret position relative to the start of the text. + * <p> + * + * @return the caret position relative to the start of the text. + * @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> + */ +public int getCaretOffset() { + checkWidget(); + + return caretOffset; +} +/** + * Returns the caret offset at the given x location in the line. + * The caret offset is the offset of the character where the caret will be + * placed when a mouse click occurs. The caret offset will be the offset of + * the character after the clicked one if the mouse click occurs at the second + * half of a character. + * Doesn't properly handle ligatures and other context dependent characters + * unless the current locale is a bidi locale. + * Ligatures are handled properly as long as they don't occur at lineXOffset. + * <p> + * + * @param line text of the line to calculate the offset in + * @param lineOffset offset of the first character in the line. + * 0 based from the beginning of the document. + * @param lineXOffset x location in the line + * @return caret offset at the x location relative to the start of the line. + */ +int getOffsetAtX(String line, int lineOffset, int lineXOffset) { + int x = lineXOffset - leftMargin + horizontalScrollOffset; + TextLayout layout = renderer.getTextLayout(line, lineOffset); + int[] trailing = new int[1]; + int offsetInLine = layout.getOffset(x, 0, trailing); + advancing = false; + if (trailing[0] != 0) { + int lineLength = line.length(); + if (offsetInLine + trailing[0] >= lineLength) { + offsetInLine = lineLength; + advancing = true; + } else { + int level; + int offset = offsetInLine; + while (offset > 0 && Character.isDigit(line.charAt(offset))) offset--; + if (offset == 0 && Character.isDigit(line.charAt(offset))) { + level = isMirrored() ? 1 : 0; + } else { + level = layout.getLevel(offset) & 0x1; + } + offsetInLine += trailing[0]; + int trailingLevel = layout.getLevel(offsetInLine) & 0x1; + advancing = (level ^ trailingLevel) != 0; + } + } + renderer.disposeTextLayout(layout); + return offsetInLine; +} +/** + * Returns the caret width. + * <p> + * + * @return the caret width, 0 if caret is null. + */ +int getCaretWidth() { + Caret caret = getCaret(); + if (caret == null) return 0; + return caret.getSize().x; +} +Object getClipboardContent(int clipboardType) { + TextTransfer plainTextTransfer = TextTransfer.getInstance(); + return clipboard.getContents(plainTextTransfer, clipboardType); +} +int getClusterNext(int offset, int lineIndex) { + String line = content.getLine(lineIndex); + int lineOffset = content.getOffsetAtLine(lineIndex); + TextLayout layout = renderer.getTextLayout(line, lineOffset); + offset -= lineOffset; + offset = layout.getNextOffset(offset, SWT.MOVEMENT_CLUSTER); + offset += lineOffset; + renderer.disposeTextLayout(layout); + return offset; +} +int getClusterPrevious(int offset, int lineIndex) { + String line = content.getLine(lineIndex); + int lineOffset = content.getOffsetAtLine(lineIndex); + TextLayout layout = renderer.getTextLayout(line, lineOffset); + offset -= lineOffset; + offset = layout.getPreviousOffset(offset, SWT.MOVEMENT_CLUSTER); + offset += lineOffset; + renderer.disposeTextLayout(layout); + return offset; +} +/** + * Returns the content implementation that is used for text storage + * or null if no user defined content implementation has been set. + * <p> + * + * @return content implementation that is used for text storage or null + * if no user defined content implementation has been set. + * @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> + */ +public StyledTextContent getContent() { + checkWidget(); + + return logicalContent; +} +/** + * Returns whether the widget implements double click mouse behavior. + * <p> + * + * @return true if double clicking a word selects the word, false if double clicks + * have the same effect as regular mouse clicks + * @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> + */ +public boolean getDoubleClickEnabled() { + checkWidget(); + return doubleClickEnabled; +} +/** + * Returns whether the widget content can be edited. + * <p> + * + * @return true if content can be edited, false otherwise + * @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> + */ +public boolean getEditable() { + checkWidget(); + return editable; +} +public Color getForeground() { + checkWidget(); + if (foreground == null) { + return getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND); + } + return foreground; +} +/** + * Return a GC to use for rendering and update the cached font style to + * represent the current style. + * <p> + * + * @return GC. + */ +GC getGC() { + return new GC(this); +} +/** + * Returns the horizontal scroll increment. + * <p> + * + * @return horizontal scroll increment. + */ +int getHorizontalIncrement() { + GC gc = getGC(); + int increment = gc.getFontMetrics().getAverageCharWidth(); + + gc.dispose(); + return increment; +} +/** + * Returns the horizontal scroll offset relative to the start of the line. + * <p> + * + * @return horizontal scroll offset relative to the start of the line, + * measured in character increments starting at 0, if > 0 the content is scrolled + * @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> + */ +public int getHorizontalIndex() { + checkWidget(); + return horizontalScrollOffset / getHorizontalIncrement(); +} +/** + * Returns the horizontal scroll offset relative to the start of the line. + * <p> + * + * @return the horizontal scroll offset relative to the start of the line, + * measured in pixel starting at 0, if > 0 the content is scrolled. + * @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> + */ +public int getHorizontalPixel() { + checkWidget(); + return horizontalScrollOffset; +} +/** + * Returns the action assigned to the key. + * Returns SWT.NULL if there is no action associated with the key. + * <p> + * + * @param key a key code defined in SWT.java or a character. + * Optionally ORd with a state mask. Preferred state masks are one or more of + * SWT.MOD1, SWT.MOD2, SWT.MOD3, since these masks account for modifier platform + * differences. However, there may be cases where using the specific state masks + * (i.e., SWT.CTRL, SWT.SHIFT, SWT.ALT, SWT.COMMAND) makes sense. + * @return one of the predefined actions defined in ST.java or SWT.NULL + * if there is no action associated with the key. + * @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> + */ +public int getKeyBinding(int key) { + checkWidget(); + Integer action = (Integer) keyActionMap.get(new Integer(key)); + int intAction; + + if (action == null) { + intAction = SWT.NULL; + } + else { + intAction = action.intValue(); + } + return intAction; +} +/** + * Gets the number of characters. + * <p> + * + * @return number of characters in the widget + * @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> + */ +public int getCharCount() { + checkWidget(); + return content.getCharCount(); +} +/** + * Returns the background color of the line at the given index. + * Returns null if a LineBackgroundListener has been set or if no background + * color has been specified for the line. Should not be called if a + * LineBackgroundListener has been set since the listener maintains the + * line background colors. + * + * @param index the index of the line + * @return the background color of the line at the given index. + * + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT when the index is invalid</li> + * </ul> + */ +public Color getLineBackground(int index) { + checkWidget(); + Color lineBackground = null; + + if (index < 0 || index > logicalContent.getLineCount()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + if (!userLineBackground) { + lineBackground = defaultLineStyler.getLineBackground(index); + } + return lineBackground; +} +/** + * Returns the line background data for the given line or null if + * there is none. + * <p> + * @param lineOffset offset of the line start relative to the start + * of the content. + * @param line line to get line background data for + * @return line background data for the given line. + */ +StyledTextEvent getLineBackgroundData(int lineOffset, String line) { + return sendLineEvent(LineGetBackground, lineOffset, line); +} +/** + * Gets the number of text lines. + * <p> + * + * @return the number of lines in the widget + * @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> + */ +public int getLineCount() { + checkWidget(); + return getLineAtOffset(getCharCount()) + 1; +} +/** + * Returns the number of lines that can be completely displayed in the + * widget client area. + * <p> + * + * @return number of lines that can be completely displayed in the widget + * client area. + */ +int getLineCountWhole() { + int lineCount; + + if (lineHeight != 0) { + lineCount = getClientArea().height / lineHeight; + } + else { + lineCount = 1; + } + return lineCount; +} +/** + * Returns the line at the specified offset in the text + * where 0 <= offset <= getCharCount() so that getLineAtOffset(getCharCount()) + * returns the line of the insert location. + * + * @param offset offset relative to the start of the content. + * 0 <= offset <= getCharCount() + * @return line at the specified offset in the text + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when the offset is outside the valid range (< 0 or > getCharCount())</li> + * </ul> + */ +public int getLineAtOffset(int offset) { + checkWidget(); + + if (offset < 0 || offset > getCharCount()) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + return logicalContent.getLineAtOffset(offset); +} +/** + * Returns the line delimiter used for entering new lines by key down + * or paste operation. + * <p> + * + * @return line delimiter used for entering new lines by key down + * or paste operation. + * @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> + */ +public String getLineDelimiter() { + checkWidget(); + return content.getLineDelimiter(); +} +/** + * Returns a StyledTextEvent that can be used to request data such + * as styles and background color for a line. + * The specified line may be a visual (wrapped) line if in word + * wrap mode. The returned object will always be for a logical + * (unwrapped) line. + * <p> + * + * @param lineOffset offset of the line. This may be the offset of + * a visual line if the widget is in word wrap mode. + * @param line line text. This may be the text of a visualline if + * the widget is in word wrap mode. + * @return StyledTextEvent that can be used to request line data + * for the given line. + */ +StyledTextEvent sendLineEvent(int eventType, int lineOffset, String line) { + StyledTextEvent event = null; + + if (isListening(eventType)) { + event = new StyledTextEvent(logicalContent); + if (wordWrap) { + // if word wrap is on, the line offset and text may be visual (wrapped) + int lineIndex = logicalContent.getLineAtOffset(lineOffset); + + event.detail = logicalContent.getOffsetAtLine(lineIndex); + event.text = logicalContent.getLine(lineIndex); + } + else { + event.detail = lineOffset; + event.text = line; + } + notifyListeners(eventType, event); + } + return event; +} +/** + * Returns the line height. + * <p> + * + * @return line height in pixel. + * @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> + */ +public int getLineHeight() { + checkWidget(); + return lineHeight; +} +/** + * Returns a LineCache implementation. Depending on whether or not + * word wrap is on this may be a line wrapping or line width + * calculating implementaiton. + * <p> + * + * @param content StyledTextContent to create the LineCache on. + * @return a LineCache implementation + */ +LineCache getLineCache(StyledTextContent content) { + LineCache lineCache; + + if (wordWrap) { + lineCache = new WordWrapCache(this, (WrappedContent) content); + } + else { + lineCache = new ContentWidthCache(this, content); + } + return lineCache; +} +/** + * Returns the line style data for the given line or null if there is + * none. If there is a LineStyleListener but it does not set any styles, + * the StyledTextEvent.styles field will be initialized to an empty + * array. + * <p> + * + * @param lineOffset offset of the line start relative to the start of + * the content. + * @param line line to get line styles for + * @return line style data for the given line. Styles may start before + * line start and end after line end + */ +StyledTextEvent getLineStyleData(int lineOffset, String line) { + return sendLineEvent(LineGetStyle, lineOffset, line); +} +/** + * Returns the x, y location of the upper left corner of the character + * bounding box at the specified offset in the text. The point is + * relative to the upper left corner of the widget client area. + * <p> + * + * @param offset offset relative to the start of the content. + * 0 <= offset <= getCharCount() + * @return x, y location of the upper left corner of the character + * bounding box at the specified offset in the text. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when the offset is outside the valid range (< 0 or > getCharCount())</li> + * </ul> + */ +public Point getLocationAtOffset(int offset) { + checkWidget(); + if (offset < 0 || offset > getCharCount()) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + int line = content.getLineAtOffset(offset); + int lineOffset = content.getOffsetAtLine(line); + String lineContent = content.getLine(line); + int x = getXAtOffset(lineContent, line, offset - lineOffset); + int y = line * lineHeight - verticalScrollOffset; + + return new Point(x, y); +} +/** + * Returns the character offset of the first character of the given line. + * <p> + * + * @param lineIndex index of the line, 0 based relative to the first + * line in the content. 0 <= lineIndex < getLineCount(), except + * lineIndex may always be 0 + * @return offset offset of the first character of the line, relative to + * the beginning of the document. The first character of the document is + * at offset 0. + * When there are not any lines, getOffsetAtLine(0) is a valid call that + * answers 0. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when the offset is outside the valid range (< 0 or > getCharCount())</li> + * </ul> + * @since 2.0 + */ +public int getOffsetAtLine(int lineIndex) { + checkWidget(); + + if (lineIndex < 0 || + (lineIndex > 0 && lineIndex >= logicalContent.getLineCount())) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + return logicalContent.getOffsetAtLine(lineIndex); +} +/** + * Returns the offset of the character at the given location relative + * to the first character in the document. + * The return value reflects the character offset that the caret will + * be placed at if a mouse click occurred at the specified location. + * If the x coordinate of the location is beyond the center of a character + * the returned offset will be behind the character. + * <p> + * + * @param point the origin of character bounding box relative to + * the origin of the widget client area. + * @return offset of the character at the given location relative + * to the first character in the document. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when point is null</li> + * <li>ERROR_INVALID_ARGUMENT when there is no character at the specified location</li> + * </ul> + */ +public int getOffsetAtLocation(Point point) { + checkWidget(); + TextLayout layout; + int line; + int lineOffset; + int offsetInLine; + String lineText; + + if (point == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + // is y above first line or is x before first column? + if (point.y + verticalScrollOffset < 0 || point.x + horizontalScrollOffset < 0) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + line = (getTopPixel() + point.y) / lineHeight; + // does the referenced line exist? + if (line >= content.getLineCount()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + lineText = content.getLine(line); + lineOffset = content.getOffsetAtLine(line); + + int x = point.x - leftMargin + horizontalScrollOffset; + layout = renderer.getTextLayout(lineText, lineOffset); + Rectangle rect = layout.getLineBounds(0); + if (x > rect.x + rect.width) { + renderer.disposeTextLayout(layout); + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + int[] trailing = new int[1]; + offsetInLine = layout.getOffset(x, 0, trailing); + if (offsetInLine != lineText.length() - 1) { + offsetInLine = Math.min(lineText.length(), offsetInLine + trailing[0]); + } + renderer.disposeTextLayout(layout); + return lineOffset + offsetInLine; +} +/** + * Returns the offset at the specified x location in the specified line. + * <p> + * + * @param x x location of the mouse location + * @param line line the mouse location is in + * @return the offset at the specified x location in the specified line, + * relative to the beginning of the document + */ +int getOffsetAtMouseLocation(int x, int line) { + String lineText = content.getLine(line); + int lineOffset = content.getOffsetAtLine(line); + return getOffsetAtX(lineText, lineOffset, x) + lineOffset; +} +/** + * Return the orientation of the receiver. + * + * @return the orientation style + * + * @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> + * + * @since 2.1.2 + */ +public int getOrientation () { + checkWidget(); + return isMirrored() ? SWT.RIGHT_TO_LEFT : SWT.LEFT_TO_RIGHT; +} +/** + * Returns the index of the last partially visible line. + * + * @return index of the last partially visible line. + */ +int getPartialBottomIndex() { + int partialLineCount = Compatibility.ceil(getClientArea().height, lineHeight); + return Math.min(content.getLineCount(), topIndex + partialLineCount) - 1; +} +/** + * Returns the content in the specified range using the platform line + * delimiter to separate lines. + * <p> + * + * @param writer the TextWriter to write line text into + * @return the content in the specified range using the platform line + * delimiter to separate lines as written by the specified TextWriter. + */ +String getPlatformDelimitedText(TextWriter writer) { + int end = writer.getStart() + writer.getCharCount(); + int startLine = logicalContent.getLineAtOffset(writer.getStart()); + int endLine = logicalContent.getLineAtOffset(end); + String endLineText = logicalContent.getLine(endLine); + int endLineOffset = logicalContent.getOffsetAtLine(endLine); + + for (int i = startLine; i <= endLine; i++) { + writer.writeLine(logicalContent.getLine(i), logicalContent.getOffsetAtLine(i)); + if (i < endLine) { + writer.writeLineDelimiter(PlatformLineDelimiter); + } + } + if (end > endLineOffset + endLineText.length()) { + writer.writeLineDelimiter(PlatformLineDelimiter); + } + writer.close(); + return writer.toString(); +} +/** + * Returns the selection. + * <p> + * Text selections are specified in terms of caret positions. In a text + * widget that contains N characters, there are N+1 caret positions, + * ranging from 0..N + * <p> + * + * @return start and end of the selection, x is the offset of the first + * selected character, y is the offset after the last selected character. + * The selection values returned are visual (i.e., x will always always be + * <= y). To determine if a selection is right-to-left (RtoL) vs. left-to-right + * (LtoR), compare the caretOffset to the start and end of the selection + * (e.g., caretOffset == start of selection implies that the selection is RtoL). + * @see #getSelectionRange + * @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> + */ +public Point getSelection() { + checkWidget(); + return new Point(selection.x, selection.y); +} +/** + * Returns the selection. + * <p> + * + * @return start and length of the selection, x is the offset of the + * first selected character, relative to the first character of the + * widget content. y is the length of the selection. + * The selection values returned are visual (i.e., length will always always be + * positive). To determine if a selection is right-to-left (RtoL) vs. left-to-right + * (LtoR), compare the caretOffset to the start and end of the selection + * (e.g., caretOffset == start of selection implies that the selection is RtoL). + * @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> + */ +public Point getSelectionRange() { + checkWidget(); + return new Point(selection.x, selection.y - selection.x); +} +/** + * Returns the receiver's selection background color. + * + * @return the selection background color + * + * @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> + * @since 2.1 + */ +public Color getSelectionBackground() { + checkWidget(); + if (selectionBackground == null) { + return getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION); + } + return selectionBackground; +} +/** + * Gets the number of selected characters. + * <p> + * + * @return the number of selected characters. + * @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> + */ +public int getSelectionCount() { + checkWidget(); + return getSelectionRange().y; +} +/** + * Returns the receiver's selection foreground color. + * + * @return the selection foreground color + * + * @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> + * @since 2.1 + */ +public Color getSelectionForeground() { + checkWidget(); + if (selectionForeground == null) { + return getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT); + } + return selectionForeground; +} +/** + * Returns the selected text. + * <p> + * + * @return selected text, or an empty String if there is no selection. + * @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> + */ +public String getSelectionText() { + checkWidget(); + return content.getTextRange(selection.x, selection.y - selection.x); +} + +public int getStyle() { + int style = super.getStyle(); + style &= ~(SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT | SWT.MIRRORED); + if (isMirrored()) { + style |= SWT.RIGHT_TO_LEFT | SWT.MIRRORED; + } else { + style |= SWT.LEFT_TO_RIGHT; + } + return style; +} + +/** + * Returns the text segments that should be treated as if they + * had a different direction than the surrounding text. + * <p> + * + * @param lineOffset offset of the first character in the line. + * 0 based from the beginning of the document. + * @param line text of the line to specify bidi segments for + * @return text segments that should be treated as if they had a + * different direction than the surrounding text. Only the start + * index of a segment is specified, relative to the start of the + * line. Always starts with 0 and ends with the line length. + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT - if the segment indices returned + * by the listener do not start with 0, are not in ascending order, + * exceed the line length or have duplicates</li> + * </ul> + */ +int [] getBidiSegments(int lineOffset, String line) { + if (!isListening(LineGetSegments)) { + return getBidiSegmentsCompatibility(line, lineOffset); + } + StyledTextEvent event = sendLineEvent(LineGetSegments, lineOffset, line); + int lineLength = line.length(); + int[] segments; + if (event == null || event.segments == null || event.segments.length == 0) { + segments = new int[] {0, lineLength}; + } + else { + int segmentCount = event.segments.length; + + // test segment index consistency + if (event.segments[0] != 0) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + for (int i = 1; i < segmentCount; i++) { + if (event.segments[i] <= event.segments[i - 1] || event.segments[i] > lineLength) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + } + // ensure that last segment index is line end offset + if (event.segments[segmentCount - 1] != lineLength) { + segments = new int[segmentCount + 1]; + System.arraycopy(event.segments, 0, segments, 0, segmentCount); + segments[segmentCount] = lineLength; + } + else { + segments = event.segments; + } + } + return segments; +} +/** + * @see #getBidiSegments + * Supports deprecated setBidiColoring API. Remove when API is removed. + */ +int [] getBidiSegmentsCompatibility(String line, int lineOffset) { + StyledTextEvent event; + StyleRange [] styles = new StyleRange [0]; + int lineLength = line.length(); + if (!bidiColoring) { + return new int[] {0, lineLength}; + } + event = renderer.getLineStyleData(lineOffset, line); + if (event != null) { + styles = event.styles; + } + if (styles.length == 0) { + return new int[] {0, lineLength}; + } + int k=0, count = 1; + while (k < styles.length && styles[k].start == 0 && styles[k].length == lineLength) { + k++; + } + int[] offsets = new int[(styles.length - k) * 2 + 2]; + for (int i = k; i < styles.length; i++) { + StyleRange style = styles[i]; + int styleLineStart = Math.max(style.start - lineOffset, 0); + int styleLineEnd = Math.max(style.start + style.length - lineOffset, styleLineStart); + styleLineEnd = Math.min (styleLineEnd, line.length ()); + if (i > 0 && count > 1 && + ((styleLineStart >= offsets[count-2] && styleLineStart <= offsets[count-1]) || + (styleLineEnd >= offsets[count-2] && styleLineEnd <= offsets[count-1])) && + style.similarTo(styles[i-1])) { + offsets[count-2] = Math.min(offsets[count-2], styleLineStart); + offsets[count-1] = Math.max(offsets[count-1], styleLineEnd); + } else { + if (styleLineStart > offsets[count - 1]) { + offsets[count] = styleLineStart; + count++; + } + offsets[count] = styleLineEnd; + count++; + } + } + // add offset for last non-colored segment in line, if any + if (lineLength > offsets[count-1]) { + offsets [count] = lineLength; + count++; + } + if (count == offsets.length) { + return offsets; + } + int [] result = new int [count]; + System.arraycopy (offsets, 0, result, 0, count); + return result; +} +/** + * Returns the style range at the given offset. + * Returns null if a LineStyleListener has been set or if a style is not set + * for the offset. + * Should not be called if a LineStyleListener has been set since the + * listener maintains the styles. + * <p> + * + * @param offset the offset to return the style for. + * 0 <= offset < getCharCount() must be true. + * @return a StyleRange with start == offset and length == 1, indicating + * the style at the given offset. null if a LineStyleListener has been set + * or if a style is not set for the given offset. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT when the offset is invalid</li> + * </ul> + */ +public StyleRange getStyleRangeAtOffset(int offset) { + checkWidget(); + if (offset < 0 || offset >= getCharCount()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + if (!userLineStyle) { + return defaultLineStyler.getStyleRangeAtOffset(offset); + } + return null; +} +/** + * Returns the styles. + * Returns an empty array if a LineStyleListener has been set. + * Should not be called if a LineStyleListener has been set since the + * listener maintains the styles. + * <p> + * + * @return the styles or an empty array if a LineStyleListener has been set. + * + * @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> + */ +public StyleRange [] getStyleRanges() { + checkWidget(); + StyleRange styles[]; + + if (!userLineStyle) { + styles = defaultLineStyler.getStyleRanges(); + } + else { + styles = new StyleRange[0]; + } + return styles; +} +/** + * Returns the styles for the given text range. + * Returns an empty array if a LineStyleListener has been set. + * Should not be called if a LineStyleListener has been set since the + * listener maintains the styles. + * + * @param start the start offset of the style ranges to return + * @param length the number of style ranges to return + * + * @return the styles or an empty array if a LineStyleListener has + * been set. The returned styles will reflect the given range. The first + * returned <code>StyleRange</code> will have a starting offset >= start + * and the last returned <code>StyleRange</code> will have an ending + * offset <= start + length - 1 + * + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when start and/or end are outside the widget content</li> + * </ul> + * + * @since 3.0 + */ +public StyleRange [] getStyleRanges(int start, int length) { + checkWidget(); + int contentLength = getCharCount(); + int end = start + length; + if (start > end || start < 0 || end > contentLength) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + StyleRange styles[]; + + if (!userLineStyle) { + styles = defaultLineStyler.getStyleRangesFor(start, length); + if (styles == null) return new StyleRange[0]; + // adjust the first and last style to reflect the specified + // range, clone these styles since the returned styles are the + // styles cached by the widget + if (styles.length == 1) { + StyleRange style = styles[0]; + if (style.start < start) { + StyleRange newStyle = (StyleRange)styles[0].clone(); + newStyle.length = newStyle.length - (start - newStyle.start); + newStyle.start = start; + styles[0] = newStyle; + } + if (style.start + style.length > (start + length)) { + StyleRange newStyle = (StyleRange)styles[0].clone(); + newStyle.length = start + length - newStyle.start; + styles[0] = newStyle; + } + } else if (styles.length > 1) { + StyleRange style = styles[0]; + if (style.start < start) { + StyleRange newStyle = (StyleRange)styles[0].clone(); + newStyle.length = newStyle.length - (start - newStyle.start); + newStyle.start = start; + styles[0] = newStyle; + } + style = styles[styles.length - 1]; + if (style.start + style.length > (start + length)) { + StyleRange newStyle = (StyleRange)styles[styles.length - 1].clone(); + newStyle.length = start + length - newStyle.start; + styles[styles.length - 1] = newStyle; + } + } + } + else { + styles = new StyleRange[0]; + } + return styles; +} +/** + * Returns the tab width measured in characters. + * + * @return tab width measured in characters + * @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> + */ +public int getTabs() { + checkWidget(); + return tabLength; +} +/** + * Returns a copy of the widget content. + * <p> + * + * @return copy of the widget content + * @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> + */ +public String getText() { + checkWidget(); + return content.getTextRange(0, getCharCount()); +} +/** + * Returns the widget content between the two offsets. + * <p> + * + * @param start offset of the first character in the returned String + * @param end offset of the last character in the returned String + * @return widget content starting at start and ending at end + * @see #getTextRange(int,int) + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when start and/or end are outside the widget content</li> + * </ul> + */ +public String getText(int start, int end) { + checkWidget(); + int contentLength = getCharCount(); + + if (start < 0 || start >= contentLength || end < 0 || end >= contentLength || start > end) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + return content.getTextRange(start, end - start + 1); +} +/** + * Returns the smallest bounding rectangle that includes the characters between two offsets. + * <p> + * + * @param start offset of the first character included in the bounding box + * @param end offset of the last character included in the bounding box + * @return bounding box of the text between start and end + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when start and/or end are outside the widget content</li> + * </ul> + * @since 3.1 + */ +public Rectangle getTextBounds(int start, int end) { + checkWidget(); + int contentLength = getCharCount(); + if (start < 0 || start >= contentLength || end < 0 || end >= contentLength || start > end) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + int lineStart = content.getLineAtOffset(start); + int lineEnd = content.getLineAtOffset(end); + Rectangle rect; + int y = lineStart * lineHeight; + int height = (lineEnd + 1) * lineHeight - y; + int left = 0x7fffffff, right = 0; + for (int i = lineStart; i <= lineEnd; i++) { + int lineOffset = content.getOffsetAtLine(i); + String line = content.getLine(i); + TextLayout layout = renderer.getTextLayout(line, lineOffset); + if (i == lineStart && i == lineEnd) { + rect = layout.getBounds(start - lineOffset, end - lineOffset); + } else if (i == lineStart) { + rect = layout.getBounds(start - lineOffset, line.length()); + } else if (i == lineEnd) { + rect = layout.getBounds(0, end - lineOffset); + } else { + rect = layout.getLineBounds(0); + } + left = Math.min (left, rect.x); + right = Math.max (right, rect.x + rect.width); + renderer.disposeTextLayout(layout); + } + rect = new Rectangle (left, y, right-left, height); + rect.x += leftMargin - horizontalScrollOffset; + rect.y -= verticalScrollOffset; + return rect; +} +/** + * Returns the widget content starting at start for length characters. + * <p> + * + * @param start offset of the first character in the returned String + * @param length number of characters to return + * @return widget content starting at start and extending length characters. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when start and/or length are outside the widget content</li> + * </ul> + */ +public String getTextRange(int start, int length) { + checkWidget(); + int contentLength = getCharCount(); + int end = start + length; + + if (start > end || start < 0 || end > contentLength) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + return content.getTextRange(start, length); +} +/** + * Returns the maximum number of characters that the receiver is capable of holding. + * + * @return the text limit + * + * @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> + */ +public int getTextLimit() { + checkWidget(); + + return textLimit; +} +/** + * Gets the top index. The top index is the index of the fully visible line that + * is currently at the top of the widget or the topmost partially visible line if + * no line is fully visible. + * The top index changes when the widget is scrolled. Indexing is zero based. + * <p> + * + * @return the index of the top line + * @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> + */ +public int getTopIndex() { + checkWidget(); + int logicalTopIndex = topIndex; + + if (wordWrap) { + int visualLineOffset = content.getOffsetAtLine(topIndex); + logicalTopIndex = logicalContent.getLineAtOffset(visualLineOffset); + } + return logicalTopIndex; +} +/** + * Gets the top pixel. The top pixel is the pixel position of the line that is + * currently at the top of the widget.The text widget can be scrolled by pixels + * by dragging the scroll thumb so that a partial line may be displayed at the top + * the widget. The top pixel changes when the widget is scrolled. The top pixel + * does not include the widget trimming. + * <p> + * + * @return pixel position of the top line + * @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> + */ +public int getTopPixel() { + checkWidget(); + return verticalScrollOffset; +} +/** + * Returns the vertical scroll increment. + * <p> + * + * @return vertical scroll increment. + */ +int getVerticalIncrement() { + return lineHeight; +} +int getCaretDirection() { + if (!isBidiCaret()) return SWT.DEFAULT; + if (!updateCaretDirection && caretDirection != SWT.NULL) return caretDirection; + updateCaretDirection = false; + int caretLine = getCaretLine(); + int lineOffset = content.getOffsetAtLine(caretLine); + String line = content.getLine(caretLine); + int offset = caretOffset - lineOffset; + int lineLength = line.length(); + if (lineLength == 0) return isMirrored() ? SWT.RIGHT : SWT.LEFT; + if (advancing && offset > 0) offset--; + if (offset == lineLength && offset > 0) offset--; + while (offset > 0 && Character.isDigit(line.charAt(offset))) offset--; + if (offset == 0 && Character.isDigit(line.charAt(offset))) { + return isMirrored() ? SWT.RIGHT : SWT.LEFT; + } + TextLayout layout = renderer.getTextLayout(line, lineOffset); + int level = layout.getLevel(offset); + renderer.disposeTextLayout(layout); + return ((level & 1) != 0) ? SWT.RIGHT : SWT.LEFT; +} +/** + * Returns the index of the line the caret is on. + * When in word wrap mode and at the end of one wrapped line/ + * beginning of the continuing wrapped line the caret offset + * is not sufficient to determine the caret line. + * + * @return the index of the line the caret is on. + */ +int getCaretLine() { + int caretLine = content.getLineAtOffset(caretOffset); + int leftColumnX = leftMargin; + if (wordWrap && columnX <= leftColumnX && + caretLine < content.getLineCount() - 1 && + caretOffset == content.getOffsetAtLine(caretLine + 1)) { + caretLine++; + } + return caretLine; +} +/** + * Returns the offset of the character after the word at the specified + * offset. + * <p> + * There are two classes of words formed by a sequence of characters: + * <ul> + * <li>from 0-9 and A-z (ASCII 48-57 and 65-122) + * <li>every other character except line breaks + * </ul> + * </p> + * <p> + * Space characters ' ' (ASCII 20) are special as they are treated as + * part of the word leading up to the space character. Line breaks are + * treated as one word. + * </p> + */ +int getWordEnd(int offset) { + int line = logicalContent.getLineAtOffset(offset); + int lineOffset = logicalContent.getOffsetAtLine(line); + String lineText = logicalContent.getLine(line); + int lineLength = lineText.length(); + + if (offset >= getCharCount()) { + return offset; + } + if (offset == lineOffset + lineLength) { + line++; + offset = logicalContent.getOffsetAtLine(line); + } + else { + TextLayout layout = renderer.getTextLayout(lineText, lineOffset); + offset -= lineOffset; + offset = layout.getNextOffset(offset, SWT.MOVEMENT_WORD); + offset += lineOffset; + renderer.disposeTextLayout(layout); + } + return offset; +} +/** + * Returns the offset of the character after the word at the specified + * offset. + * <p> + * There are two classes of words formed by a sequence of characters: + * <ul> + * <li>from 0-9 and A-z (ASCII 48-57 and 65-122) + * <li>every other character except line breaks + * </ul> + * </p> + * <p> + * Spaces are ignored and do not represent a word. Line breaks are treated + * as one word. + * </p> + */ +int getWordEndNoSpaces(int offset) { + int line = logicalContent.getLineAtOffset(offset); + int lineOffset = logicalContent.getOffsetAtLine(line); + String lineText = logicalContent.getLine(line); + int lineLength = lineText.length(); + + if (offset >= getCharCount()) { + return offset; + } + if (offset == lineOffset + lineLength) { + line++; + offset = logicalContent.getOffsetAtLine(line); + } + else { + offset -= lineOffset; + char ch = lineText.charAt(offset); + boolean letterOrDigit = Compatibility.isLetterOrDigit(ch); + + while (offset < lineLength - 1 && Compatibility.isLetterOrDigit(ch) == letterOrDigit && !Compatibility.isSpaceChar(ch)) { + offset++; + ch = lineText.charAt(offset); + } + if (offset == lineLength - 1 && Compatibility.isLetterOrDigit(ch) == letterOrDigit && !Compatibility.isSpaceChar(ch)) { + offset++; + } + offset += lineOffset; + } + return offset; +} +/** + * Returns the start offset of the word at the specified offset. + * There are two classes of words formed by a sequence of characters: + * <p> + * <ul> + * <li>from 0-9 and A-z (ASCII 48-57 and 65-122) + * <li>every other character except line breaks + * </ul> + * </p> + * <p> + * Space characters ' ' (ASCII 20) are special as they are treated as + * part of the word leading up to the space character. Line breaks are treated + * as one word. + * </p> + */ +int getWordStart(int offset) { + int line = logicalContent.getLineAtOffset(offset); + int lineOffset = logicalContent.getOffsetAtLine(line); + String lineText = logicalContent.getLine(line); + + if (offset <= 0) { + return offset; + } + if (offset == lineOffset) { + line--; + lineText = logicalContent.getLine(line); + offset = logicalContent.getOffsetAtLine(line) + lineText.length(); + } + else { + TextLayout layout = renderer.getTextLayout(lineText, lineOffset); + offset -= lineOffset; + offset = layout.getPreviousOffset(offset, SWT.MOVEMENT_WORD); + offset += lineOffset; + renderer.disposeTextLayout(layout); + } + return offset; +} +/** + * Returns whether the widget wraps lines. + * <p> + * + * @return true if widget wraps lines, false otherwise + * @since 2.0 + */ +public boolean getWordWrap() { + checkWidget(); + return wordWrap; +} +/** + * Returns the x location of the character at the give offset in the line. + * <b>NOTE:</b> Does not return correct values for true italic fonts (vs. slanted fonts). + * <p> + * + * @return x location of the character at the given offset in the line. + */ +int getXAtOffset(String line, int lineIndex, int offsetInLine) { + int x = 0; + int lineLength = line.length(); + if (lineIndex < content.getLineCount() - 1) { + int endLineOffset = content.getOffsetAtLine(lineIndex + 1) - 1; + if (lineLength < offsetInLine && offsetInLine <= endLineOffset) { + offsetInLine = lineLength; + } + } + if (lineLength != 0 && offsetInLine <= lineLength) { + int lineOffset = content.getOffsetAtLine(lineIndex); + TextLayout layout = renderer.getTextLayout(line, lineOffset); + if (!advancing || offsetInLine == 0) { + x = layout.getLocation(offsetInLine, false).x; + } else { + x = layout.getLocation(offsetInLine - 1, true).x; + } + renderer.disposeTextLayout(layout); + } + return x + leftMargin - horizontalScrollOffset; +} +/** + * Inserts a string. The old selection is replaced with the new text. + * <p> + * + * @param string the string + * @see #replaceTextRange(int,int,String) + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when string is null</li> + * </ul> + */ +public void insert(String string) { + checkWidget(); + if (string == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + Point sel = getSelectionRange(); + replaceTextRange(sel.x, sel.y, string); +} +/** + * Creates content change listeners and set the default content model. + */ +void installDefaultContent() { + textChangeListener = new TextChangeListener() { + public void textChanging(TextChangingEvent event) { + handleTextChanging(event); + } + public void textChanged(TextChangedEvent event) { + handleTextChanged(event); + } + public void textSet(TextChangedEvent event) { + handleTextSet(event); + } + }; + logicalContent = content = new DefaultContent(); + content.addTextChangeListener(textChangeListener); +} +/** + * Creates a default line style listener. + * Used to store line background colors and styles. + * Removed when the user sets a LineStyleListener. + * <p> + * + * @see #addLineStyleListener + */ +void installDefaultLineStyler() { + defaultLineStyler = new DefaultLineStyler(logicalContent); + StyledTextListener typedListener = new StyledTextListener(defaultLineStyler); + if (!userLineStyle) { + addListener(LineGetStyle, typedListener); + } + if (!userLineBackground) { + addListener(LineGetBackground, typedListener); + } +} +/** + * Adds event listeners + */ +void installListeners() { + ScrollBar verticalBar = getVerticalBar(); + ScrollBar horizontalBar = getHorizontalBar(); + + listener = new Listener() { + public void handleEvent(Event event) { + switch (event.type) { + case SWT.Dispose: handleDispose(event); break; + case SWT.KeyDown: handleKeyDown(event); break; + case SWT.KeyUp: handleKeyUp(event); break; + case SWT.MouseDown: handleMouseDown(event); break; + case SWT.MouseUp: handleMouseUp(event); break; + case SWT.MouseDoubleClick: handleMouseDoubleClick(event); break; + case SWT.MouseMove: handleMouseMove(event); break; + case SWT.Paint: handlePaint(event); break; + case SWT.Resize: handleResize(event); break; + case SWT.Traverse: handleTraverse(event); break; + } + } + }; + addListener(SWT.Dispose, listener); + addListener(SWT.KeyDown, listener); + addListener(SWT.KeyUp, listener); + addListener(SWT.MouseDown, listener); + addListener(SWT.MouseUp, listener); + addListener(SWT.MouseDoubleClick, listener); + addListener(SWT.MouseMove, listener); + addListener(SWT.Paint, listener); + addListener(SWT.Resize, listener); + addListener(SWT.Traverse, listener); + if (verticalBar != null) { + verticalBar.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event event) { + handleVerticalScroll(event); + } + }); + } + if (horizontalBar != null) { + horizontalBar.addListener(SWT.Selection, new Listener() { + public void handleEvent(Event event) { + handleHorizontalScroll(event); + } + }); + } +} +StyledTextContent internalGetContent() { + return content; +} +int internalGetHorizontalPixel() { + return horizontalScrollOffset; +} +Point internalGetSelection() { + return selection; +} +boolean internalGetWordWrap() { + return wordWrap; +} +/** + * Used by WordWrapCache to bypass StyledText.redraw which does + * an unwanted cache reset. + */ +void internalRedraw() { + super.redraw(); +} +/** + * Redraws the specified text range. + * <p> + * + * @param start offset of the first character to redraw + * @param length number of characters to redraw + * @param clearBackground true if the background should be cleared as + * part of the redraw operation. If true, the entire redraw range will + * be cleared before anything is redrawn. If the redraw range includes + * the last character of a line (i.e., the entire line is redrawn) the + * line is cleared all the way to the right border of the widget. + * The redraw operation will be faster and smoother if clearBackground is + * set to false. Whether or not the flag can be set to false depends on + * the type of change that has taken place. If font styles or background + * colors for the redraw range have changed, clearBackground should be + * set to true. If only foreground colors have changed for the redraw + * range, clearBackground can be set to false. + */ +void internalRedrawRange(int start, int length, boolean clearBackground) { + int end = start + length; + int firstLine = content.getLineAtOffset(start); + int lastLine = content.getLineAtOffset(end); + int offsetInFirstLine; + int partialBottomIndex = getPartialBottomIndex(); + int partialTopIndex = verticalScrollOffset / lineHeight; + // do nothing if redraw range is completely invisible + if (firstLine > partialBottomIndex || lastLine < partialTopIndex) { + return; + } + // only redraw visible lines + if (partialTopIndex > firstLine) { + firstLine = partialTopIndex; + offsetInFirstLine = 0; + } + else { + offsetInFirstLine = start - content.getOffsetAtLine(firstLine); + } + if (partialBottomIndex + 1 < lastLine) { + lastLine = partialBottomIndex + 1; // + 1 to redraw whole bottom line, including line break + end = content.getOffsetAtLine(lastLine); + } + redrawLines(firstLine, offsetInFirstLine, lastLine, end, clearBackground); + + // redraw entire center lines if redraw range includes more than two lines + if (lastLine - firstLine > 1) { + Rectangle clientArea = getClientArea(); + int redrawStopY = lastLine * lineHeight - verticalScrollOffset; + int redrawY = (firstLine + 1) * lineHeight - verticalScrollOffset; + draw(0, redrawY, clientArea.width, redrawStopY - redrawY, clearBackground); + } +} +/** + * Returns the widget text with style information encoded using RTF format + * specification version 1.5. + * + * @return the widget text with style information encoded using RTF format + * @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> + */ +String getRtf(){ + checkWidget(); + RTFWriter rtfWriter = new RTFWriter(0, getCharCount()); + return getPlatformDelimitedText(rtfWriter); +} +/** + * Frees resources. + */ +void handleDispose(Event event) { + removeListener(SWT.Dispose, listener); + notifyListeners(SWT.Dispose, event); + event.type = SWT.None; + + clipboard.dispose(); + ibeamCursor.dispose(); + if (renderer != null) { + renderer.dispose(); + renderer = null; + } + if (content != null) { + content.removeTextChangeListener(textChangeListener); + content = null; + } + if (defaultCaret != null) { + defaultCaret.dispose(); + defaultCaret = null; + } + if (leftCaretBitmap != null) { + leftCaretBitmap.dispose(); + leftCaretBitmap = null; + } + if (rightCaretBitmap != null) { + rightCaretBitmap.dispose(); + rightCaretBitmap = null; + } + if (defaultLineStyler != null) { + defaultLineStyler.release(); + defaultLineStyler = null; + } + if (isBidiCaret()) { + BidiUtil.removeLanguageListener(handle); + } + selectionBackground = null; + selectionForeground = null; + logicalContent = null; + textChangeListener = null; + lineCache = null; + ibeamCursor = null; + selection = null; + doubleClickSelection = null; + keyActionMap = null; + background = null; + foreground = null; + clipboard = null; +} +/** + * Scrolls the widget horizontally. + */ +void handleHorizontalScroll(Event event) { + int scrollPixel = getHorizontalBar().getSelection() - horizontalScrollOffset; + scrollHorizontal(scrollPixel); +} +/** + * If an action has been registered for the key stroke execute the action. + * Otherwise, if a character has been entered treat it as new content. + * <p> + * + * @param event keyboard event + */ +void handleKey(Event event) { + int action; + advancing = true; + if (event.keyCode != 0) { + // special key pressed (e.g., F1) + action = getKeyBinding(event.keyCode | event.stateMask); + } + else { + // character key pressed + action = getKeyBinding(event.character | event.stateMask); + if (action == SWT.NULL) { + // see if we have a control character + if ((event.stateMask & SWT.CTRL) != 0 && (event.character >= 0) && event.character <= 31) { + // get the character from the CTRL+char sequence, the control + // key subtracts 64 from the value of the key that it modifies + int c = event.character + 64; + action = getKeyBinding(c | event.stateMask); + } + } + } + if (action == SWT.NULL) { + boolean ignore = false; + + if (IS_CARBON) { + // Ignore accelerator key combinations (we do not want to + // insert a character in the text in this instance). Do not + // ignore COMMAND+ALT combinations since that key sequence + // produces characters on the mac. + ignore = (event.stateMask ^ SWT.COMMAND) == 0 || + (event.stateMask ^ (SWT.COMMAND | SWT.SHIFT)) == 0; + } else if (IS_MOTIF) { + // Ignore accelerator key combinations (we do not want to + // insert a character in the text in this instance). Do not + // ignore ALT combinations since this key sequence + // produces characters on motif. + ignore = (event.stateMask ^ SWT.CTRL) == 0 || + (event.stateMask ^ (SWT.CTRL | SWT.SHIFT)) == 0; + } else { + // Ignore accelerator key combinations (we do not want to + // insert a character in the text in this instance). Don't + // ignore CTRL+ALT combinations since that is the Alt Gr + // key on some keyboards. See bug 20953. + ignore = (event.stateMask ^ SWT.ALT) == 0 || + (event.stateMask ^ SWT.CTRL) == 0 || + (event.stateMask ^ (SWT.ALT | SWT.SHIFT)) == 0 || + (event.stateMask ^ (SWT.CTRL | SWT.SHIFT)) == 0; + } + // -ignore anything below SPACE except for line delimiter keys and tab. + // -ignore DEL + if (!ignore && event.character > 31 && event.character != SWT.DEL || + event.character == SWT.CR || event.character == SWT.LF || + event.character == TAB) { + doContent(event.character); + } + } + else { + invokeAction(action); + } +} +/** + * If a VerifyKey listener exists, verify that the key that was entered + * should be processed. + * <p> + * + * @param event keyboard event + */ +void handleKeyDown(Event event) { + if (clipboardSelection == null) { + clipboardSelection = new Point(selection.x, selection.y); + } + + Event verifyEvent = new Event(); + verifyEvent.character = event.character; + verifyEvent.keyCode = event.keyCode; + verifyEvent.stateMask = event.stateMask; + verifyEvent.doit = true; + notifyListeners(VerifyKey, verifyEvent); + if (verifyEvent.doit) { + handleKey(event); + } +} +/** + * Update the Selection Clipboard. + * <p> + * + * @param event keyboard event + */ +void handleKeyUp(Event event) { + if (clipboardSelection != null) { + if (clipboardSelection.x != selection.x || clipboardSelection.y != selection.y) { + try { + if (selection.y - selection.x > 0) { + setClipboardContent(selection.x, selection.y - selection.x, DND.SELECTION_CLIPBOARD); + } + } + catch (SWTError error) { + // Copy to clipboard failed. This happens when another application + // is accessing the clipboard while we copy. Ignore the error. + // Fixes 1GDQAVN + // Rethrow all other errors. Fixes bug 17578. + if (error.code != DND.ERROR_CANNOT_SET_CLIPBOARD) { + throw error; + } + } + } + } + clipboardSelection = null; +} +/** + * Updates the caret location and selection if mouse button 1 has been + * pressed. + */ +void handleMouseDoubleClick(Event event) { + if (event.button != 1 || !doubleClickEnabled) { + return; + } + event.y -= topMargin; + mouseDoubleClick = true; + caretOffset = getWordStart(caretOffset); + resetSelection(); + caretOffset = getWordEndNoSpaces(caretOffset); + showCaret(); + doMouseSelection(); + doubleClickSelection = new Point(selection.x, selection.y); +} +/** + * Updates the caret location and selection if mouse button 1 has been + * pressed. + */ +void handleMouseDown(Event event) { + mouseDown = true; + mouseDoubleClick = false; + if (event.button == 2) { + String text = (String)getClipboardContent(DND.SELECTION_CLIPBOARD); + if (text != null && text.length() > 0) { + // position cursor + int x = event.x; + int y = event.y - topMargin; + doMouseLocationChange(x, y, false); + // insert text + Event e = new Event(); + e.start = selection.x; + e.end = selection.y; + e.text = getModelDelimitedText(text); + sendKeyEvent(e); + } + } + if ((event.button != 1) || (IS_CARBON && (event.stateMask & SWT.MOD4) != 0)) { + return; + } + boolean select = (event.stateMask & SWT.MOD2) != 0; + event.y -= topMargin; + doMouseLocationChange(event.x, event.y, select); +} +/** + * Updates the caret location and selection if mouse button 1 is pressed + * during the mouse move. + */ +void handleMouseMove(Event event) { + if (!mouseDown) return; + if ((event.stateMask & SWT.BUTTON1) == 0) { + return; + } + event.y -= topMargin; + doMouseLocationChange(event.x, event.y, true); + update(); + doAutoScroll(event); +} +/** + * Autoscrolling ends when the mouse button is released. + */ +void handleMouseUp(Event event) { + mouseDown = false; + mouseDoubleClick = false; + event.y -= topMargin; + endAutoScroll(); + if (event.button == 1) { + try { + if (selection.y - selection.x > 0) { + setClipboardContent(selection.x, selection.y - selection.x, DND.SELECTION_CLIPBOARD); + } + } + catch (SWTError error) { + // Copy to clipboard failed. This happens when another application + // is accessing the clipboard while we copy. Ignore the error. + // Fixes 1GDQAVN + // Rethrow all other errors. Fixes bug 17578. + if (error.code != DND.ERROR_CANNOT_SET_CLIPBOARD) { + throw error; + } + } + } +} +/** + * Renders the invalidated area specified in the paint event. + * <p> + * + * @param event paint event + */ +void handlePaint(Event event) { + // Check if there is work to do + if (event.height == 0) return; + int startLine = Math.max(0, (event.y - topMargin + verticalScrollOffset) / lineHeight); + int paintYFromTopLine = (startLine - topIndex) * lineHeight; + int topLineOffset = topIndex * lineHeight - verticalScrollOffset; + int startY = paintYFromTopLine + topLineOffset + topMargin; // adjust y position for pixel based scrolling and top margin + int renderHeight = event.y + event.height - startY; + performPaint(event.gc, startLine, startY, renderHeight); +} +/** + * Recalculates the scroll bars. Rewraps all lines when in word + * wrap mode. + * <p> + * + * @param event resize event + */ +void handleResize(Event event) { + int oldHeight = clientAreaHeight; + int oldWidth = clientAreaWidth; + + Rectangle clientArea = getClientArea(); + clientAreaHeight = clientArea.height; + clientAreaWidth = clientArea.width; + /* Redraw the old or new right/bottom margin if needed */ + if (oldWidth != clientAreaWidth) { + if (rightMargin > 0) { + int x = (oldWidth < clientAreaWidth ? oldWidth : clientAreaWidth)- rightMargin; + redraw(x, 0, rightMargin, oldHeight, false); + } + } + if (oldHeight != clientAreaHeight) { + if (bottomMargin > 0) { + int y = (oldHeight < clientAreaHeight ? oldHeight : clientAreaHeight)- bottomMargin; + redraw(0, y, oldWidth, bottomMargin, false); + } + } + if (wordWrap) { + if (oldWidth != clientAreaWidth) { + wordWrapResize(oldWidth); + } + } + else + if (clientAreaHeight > oldHeight) { + int lineCount = content.getLineCount(); + int oldBottomIndex = topIndex + oldHeight / lineHeight; + int newItemCount = Compatibility.ceil(clientAreaHeight - oldHeight, lineHeight); + + oldBottomIndex = Math.min(oldBottomIndex, lineCount); + newItemCount = Math.min(newItemCount, lineCount - oldBottomIndex); + lineCache.calculate(oldBottomIndex, newItemCount); + } + setScrollBars(); + claimBottomFreeSpace(); + claimRightFreeSpace(); + if (oldHeight != clientAreaHeight) { + calculateTopIndex(); + } +} +/** + * Updates the caret position and selection and the scroll bars to reflect + * the content change. + * <p> + */ +void handleTextChanged(TextChangedEvent event) { + lineCache.textChanged(lastTextChangeStart, + lastTextChangeNewLineCount, + lastTextChangeReplaceLineCount, + lastTextChangeNewCharCount, + lastTextChangeReplaceCharCount); + setScrollBars(); + // update selection/caret location after styles have been changed. + // otherwise any text measuring could be incorrect + // + // also, this needs to be done after all scrolling. Otherwise, + // selection redraw would be flushed during scroll which is wrong. + // in some cases new text would be drawn in scroll source area even + // though the intent is to scroll it. + // fixes 1GB93QT + updateSelection( + lastTextChangeStart, + lastTextChangeReplaceCharCount, + lastTextChangeNewCharCount); + + if (lastTextChangeReplaceLineCount > 0) { + // Only check for unused space when lines are deleted. + // Fixes 1GFL4LY + // Scroll up so that empty lines below last text line are used. + // Fixes 1GEYJM0 + claimBottomFreeSpace(); + } + if (lastTextChangeReplaceCharCount > 0) { + // fixes bug 8273 + claimRightFreeSpace(); + } + // do direct drawing if the text change is confined to a single line. + // optimization and fixes bug 13999. see also handleTextChanging. + if (lastTextChangeNewLineCount == 0 && lastTextChangeReplaceLineCount == 0) { + int startLine = content.getLineAtOffset(lastTextChangeStart); + int startY = startLine * lineHeight - verticalScrollOffset + topMargin; + + if (DOUBLE_BUFFER) { + GC gc = getGC(); + Caret caret = getCaret(); + boolean caretVisible = false; + + if (caret != null) { + caretVisible = caret.getVisible(); + caret.setVisible(false); + } + performPaint(gc, startLine, startY, lineHeight); + if (caret != null) { + caret.setVisible(caretVisible); + } + gc.dispose(); + } else { + redraw(0, startY, getClientArea().width, lineHeight, false); + update(); + } + } +} +/** + * Updates the screen to reflect a pending content change. + * <p> + * + * @param event.start the start offset of the change + * @param event.newText text that is going to be inserted or empty String + * if no text will be inserted + * @param event.replaceCharCount length of text that is going to be replaced + * @param event.newCharCount length of text that is going to be inserted + * @param event.replaceLineCount number of lines that are going to be replaced + * @param event.newLineCount number of new lines that are going to be inserted + */ +void handleTextChanging(TextChangingEvent event) { + int firstLine; + int textChangeY; + boolean isMultiLineChange = event.replaceLineCount > 0 || event.newLineCount > 0; + + if (event.replaceCharCount < 0) { + event.start += event.replaceCharCount; + event.replaceCharCount *= -1; + } + lastTextChangeStart = event.start; + lastTextChangeNewLineCount = event.newLineCount; + lastTextChangeNewCharCount = event.newCharCount; + lastTextChangeReplaceLineCount = event.replaceLineCount; + lastTextChangeReplaceCharCount = event.replaceCharCount; + firstLine = content.getLineAtOffset(event.start); + textChangeY = firstLine * lineHeight - verticalScrollOffset + topMargin; + if (isMultiLineChange) { + redrawMultiLineChange(textChangeY, event.newLineCount, event.replaceLineCount); + } + // notify default line styler about text change + if (defaultLineStyler != null) { + defaultLineStyler.textChanging(event); + } + + // Update the caret offset if it is greater than the length of the content. + // This is necessary since style range API may be called between the + // handleTextChanging and handleTextChanged events and this API sets the + // caretOffset. + int newEndOfText = content.getCharCount() - event.replaceCharCount + event.newCharCount; + if (caretOffset > newEndOfText) caretOffset = newEndOfText; +} +/** + * Called when the widget content is set programatically, overwriting + * the old content. Resets the caret position, selection and scroll offsets. + * Recalculates the content width and scroll bars. Redraws the widget. + * <p> + * + * @param event text change event. + */ +void handleTextSet(TextChangedEvent event) { + reset(); +} +/** + * Called when a traversal key is pressed. + * Allow tab next traversal to occur when the widget is in single + * line mode or in multi line and non-editable mode . + * When in editable multi line mode we want to prevent the tab + * traversal and receive the tab key event instead. + * <p> + * + * @param event the event + */ +void handleTraverse(Event event) { + switch (event.detail) { + case SWT.TRAVERSE_ESCAPE: + case SWT.TRAVERSE_PAGE_NEXT: + case SWT.TRAVERSE_PAGE_PREVIOUS: + event.doit = true; + break; + case SWT.TRAVERSE_RETURN: + case SWT.TRAVERSE_TAB_NEXT: + case SWT.TRAVERSE_TAB_PREVIOUS: + if ((getStyle() & SWT.SINGLE) != 0) { + event.doit = true; + } else { + if (!editable || (event.stateMask & SWT.MODIFIER_MASK) != 0) { + event.doit = true; + } + } + break; + } +} +/** + * Scrolls the widget vertically. + */ +void handleVerticalScroll(Event event) { + setVerticalScrollOffset(getVerticalBar().getSelection(), false); +} +/** + * Add accessibility support for the widget. + */ +void initializeAccessible() { + final Accessible accessible = getAccessible(); + accessible.addAccessibleListener(new AccessibleAdapter() { + public void getHelp(AccessibleEvent e) { + e.result = getToolTipText(); + } + }); + accessible.addAccessibleTextListener(new AccessibleTextAdapter() { + public void getCaretOffset(AccessibleTextEvent e) { + e.offset = StyledText.this.getCaretOffset(); + } + public void getSelectionRange(AccessibleTextEvent e) { + Point selection = StyledText.this.getSelectionRange(); + e.offset = selection.x; + e.length = selection.y; + } + }); + accessible.addAccessibleControlListener(new AccessibleControlAdapter() { + public void getRole(AccessibleControlEvent e) { + e.detail = ACC.ROLE_TEXT; + } + public void getState(AccessibleControlEvent e) { + int state = 0; + if (isEnabled()) state |= ACC.STATE_FOCUSABLE; + if (isFocusControl()) state |= ACC.STATE_FOCUSED; + if (!isVisible()) state |= ACC.STATE_INVISIBLE; + if (!getEditable()) state |= ACC.STATE_READONLY; + e.detail = state; + } + public void getValue(AccessibleControlEvent e) { + e.result = StyledText.this.getText(); + } + }); + addListener(SWT.FocusIn, new Listener() { + public void handleEvent(Event event) { + accessible.setFocus(ACC.CHILDID_SELF); + } + }); +} +/** + * Initializes the fonts used to render font styles. + * Presently only regular and bold fonts are supported. + */ +void initializeRenderer() { + if (renderer != null) { + renderer.dispose(); + } + renderer = new DisplayRenderer(getDisplay(), getFont(), this, tabLength); + lineHeight = renderer.getLineHeight(); + if (wordWrap) { + content = new WrappedContent(renderer, logicalContent); + } +} +/** + * Executes the action. + * <p> + * + * @param action one of the actions defined in ST.java + */ +public void invokeAction(int action) { + int oldColumnX, oldHScrollOffset, hScrollChange; + int caretLine; + + checkWidget(); + updateCaretDirection = true; + switch (action) { + // Navigation + case ST.LINE_UP: + caretLine = doLineUp(); + oldColumnX = columnX; + oldHScrollOffset = horizontalScrollOffset; + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(caretLine); + // restore the original horizontal caret position + hScrollChange = oldHScrollOffset - horizontalScrollOffset; + columnX = oldColumnX + hScrollChange; + clearSelection(true); + break; + case ST.LINE_DOWN: + caretLine = doLineDown(); + oldColumnX = columnX; + oldHScrollOffset = horizontalScrollOffset; + // explicitly go to the calculated caret line. may be different + // from content.getLineAtOffset(caretOffset) when in word wrap mode + showCaret(caretLine); + // restore the original horizontal caret position + hScrollChange = oldHScrollOffset - horizontalScrollOffset; + columnX = oldColumnX + hScrollChange; + clearSelection(true); + break; + case ST.LINE_START: + doLineStart(); + clearSelection(true); + break; + case ST.LINE_END: + doLineEnd(); + clearSelection(true); + break; + case ST.COLUMN_PREVIOUS: + doCursorPrevious(); + clearSelection(true); + break; + case ST.COLUMN_NEXT: + doCursorNext(); + clearSelection(true); + break; + case ST.PAGE_UP: + doPageUp(false, getLineCountWhole()); + clearSelection(true); + break; + case ST.PAGE_DOWN: + doPageDown(false, getLineCountWhole()); + clearSelection(true); + break; + case ST.WORD_PREVIOUS: + doWordPrevious(); + clearSelection(true); + break; + case ST.WORD_NEXT: + doWordNext(); + clearSelection(true); + break; + case ST.TEXT_START: + doContentStart(); + clearSelection(true); + break; + case ST.TEXT_END: + doContentEnd(); + clearSelection(true); + break; + case ST.WINDOW_START: + doPageStart(); + clearSelection(true); + break; + case ST.WINDOW_END: + doPageEnd(); + clearSelection(true); + break; + // Selection + case ST.SELECT_LINE_UP: + doSelectionLineUp(); + break; + case ST.SELECT_ALL: + selectAll(); + break; + case ST.SELECT_LINE_DOWN: + doSelectionLineDown(); + break; + case ST.SELECT_LINE_START: + doLineStart(); + doSelection(ST.COLUMN_PREVIOUS); + break; + case ST.SELECT_LINE_END: + doLineEnd(); + doSelection(ST.COLUMN_NEXT); + break; + case ST.SELECT_COLUMN_PREVIOUS: + doSelectionCursorPrevious(); + doSelection(ST.COLUMN_PREVIOUS); + break; + case ST.SELECT_COLUMN_NEXT: + doSelectionCursorNext(); + doSelection(ST.COLUMN_NEXT); + break; + case ST.SELECT_PAGE_UP: + doSelectionPageUp(getLineCountWhole()); + break; + case ST.SELECT_PAGE_DOWN: + doSelectionPageDown(getLineCountWhole()); + break; + case ST.SELECT_WORD_PREVIOUS: + doSelectionWordPrevious(); + doSelection(ST.COLUMN_PREVIOUS); + break; + case ST.SELECT_WORD_NEXT: + doSelectionWordNext(); + doSelection(ST.COLUMN_NEXT); + break; + case ST.SELECT_TEXT_START: + doContentStart(); + doSelection(ST.COLUMN_PREVIOUS); + break; + case ST.SELECT_TEXT_END: + doContentEnd(); + doSelection(ST.COLUMN_NEXT); + break; + case ST.SELECT_WINDOW_START: + doPageStart(); + doSelection(ST.COLUMN_PREVIOUS); + break; + case ST.SELECT_WINDOW_END: + doPageEnd(); + doSelection(ST.COLUMN_NEXT); + break; + // Modification + case ST.CUT: + cut(); + break; + case ST.COPY: + copy(); + break; + case ST.PASTE: + paste(); + break; + case ST.DELETE_PREVIOUS: + doBackspace(); + break; + case ST.DELETE_NEXT: + doDelete(); + break; + case ST.DELETE_WORD_PREVIOUS: + doDeleteWordPrevious(); + break; + case ST.DELETE_WORD_NEXT: + doDeleteWordNext(); + break; + // Miscellaneous + case ST.TOGGLE_OVERWRITE: + overwrite = !overwrite; // toggle insert/overwrite mode + break; + } +} +/** + * Temporary until SWT provides this + */ +boolean isBidi() { + return IS_GTK || BidiUtil.isBidiPlatform() || isMirrored; +} +/** + * Returns whether the given offset is inside a multi byte line delimiter. + * Example: + * "Line1\r\n" isLineDelimiter(5) == false but isLineDelimiter(6) == true + * + * @return true if the given offset is inside a multi byte line delimiter. + * false if the given offset is before or after a line delimiter. + */ +boolean isLineDelimiter(int offset) { + int line = content.getLineAtOffset(offset); + int lineOffset = content.getOffsetAtLine(line); + int offsetInLine = offset - lineOffset; + // offsetInLine will be greater than line length if the line + // delimiter is longer than one character and the offset is set + // in between parts of the line delimiter. + return offsetInLine > content.getLine(line).length(); +} +/** + * Returns whether the widget is mirrored (right oriented/right to left + * writing order). + * + * @return isMirrored true=the widget is right oriented, false=the widget + * is left oriented + */ +boolean isMirrored() { + return isMirrored; +} +/** + * Returns whether or not the given lines are visible. + * <p> + * + * @return true if any of the lines is visible + * false if none of the lines is visible + */ +boolean isAreaVisible(int firstLine, int lastLine) { + int partialBottomIndex = getPartialBottomIndex(); + int partialTopIndex = verticalScrollOffset / lineHeight; + boolean notVisible = firstLine > partialBottomIndex || lastLine < partialTopIndex; + return !notVisible; +} +/** + * Returns whether the widget can have only one line. + * <p> + * + * @return true if widget can have only one line, false if widget can have + * multiple lines + */ +boolean isSingleLine() { + return (getStyle() & SWT.SINGLE) != 0; +} +/** + * Sends the specified verify event, replace/insert text as defined by + * the event and send a modify event. + * <p> + * + * @param event the text change event. + * <ul> + * <li>event.start - the replace start offset</li> + * <li>event.end - the replace end offset</li> + * <li>event.text - the new text</li> + * </ul> + * @param updateCaret whether or not he caret should be set behind + * the new text + */ +void modifyContent(Event event, boolean updateCaret) { + event.doit = true; + notifyListeners(SWT.Verify, event); + if (event.doit) { + StyledTextEvent styledTextEvent = null; + int replacedLength = event.end - event.start; + if (isListening(ExtendedModify)) { + styledTextEvent = new StyledTextEvent(logicalContent); + styledTextEvent.start = event.start; + styledTextEvent.end = event.start + event.text.length(); + styledTextEvent.text = content.getTextRange(event.start, replacedLength); + } + if (updateCaret) { + //Fix advancing flag for delete/backspace key on direction boundary + if (event.text.length() == 0) { + int lineIndex = content.getLineAtOffset(event.start); + int lineOffset = content.getOffsetAtLine(lineIndex); + String lineText = content.getLine(lineIndex); + TextLayout layout = renderer.getTextLayout(lineText, lineOffset); + int levelStart = layout.getLevel(event.start - lineOffset); + int lineIndexEnd = content.getLineAtOffset(event.end); + if (lineIndex != lineIndexEnd) { + renderer.disposeTextLayout(layout); + lineOffset = content.getOffsetAtLine(lineIndexEnd); + lineText = content.getLine(lineIndexEnd); + layout = renderer.getTextLayout(lineText, lineOffset); + } + int levelEnd = layout.getLevel(event.end - lineOffset); + renderer.disposeTextLayout(layout); + advancing = levelStart != levelEnd; + } + } + content.replaceTextRange(event.start, replacedLength, event.text); + // set the caret position prior to sending the modify event. + // fixes 1GBB8NJ + if (updateCaret) { + // always update the caret location. fixes 1G8FODP + internalSetSelection(event.start + event.text.length(), 0, true); + showCaret(); + } + sendModifyEvent(event); + if (isListening(ExtendedModify)) { + notifyListeners(ExtendedModify, styledTextEvent); + } + } +} +/** + * Replaces the selection with the text on the <code>DND.CLIPBOARD</code> + * clipboard or, if there is no selection, inserts the text at the current + * caret offset. If the widget has the SWT.SINGLE style and the + * clipboard text contains more than one line, only the first line without + * line delimiters is inserted in the widget. + * <p> + * + * @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> + */ +public void paste(){ + checkWidget(); + String text; + text = (String) getClipboardContent(DND.CLIPBOARD); + if (text != null && text.length() > 0) { + Event event = new Event(); + event.start = selection.x; + event.end = selection.y; + event.text = getModelDelimitedText(text); + sendKeyEvent(event); + } +} +/** + * Render the specified area. Broken out as its own method to support + * direct drawing. + * <p> + * + * @param gc GC to render on + * @param startLine first line to render + * @param startY y pixel location to start rendering at + * @param renderHeight renderHeight widget area that needs to be filled with lines + */ +void performPaint(GC gc,int startLine,int startY, int renderHeight) { + Rectangle clientArea = getClientArea(); + Color background = getBackground(); + + // Check if there is work to do. We never want to try and create + // an Image with 0 width or 0 height. + if (clientArea.width == 0) { + return; + } + if (renderHeight > 0) { + // renderHeight will be negative when only top margin needs redrawing + Color foreground = getForeground(); + int lineCount = content.getLineCount(); + int gcStyle = isMirrored() ? SWT.RIGHT_TO_LEFT : SWT.LEFT_TO_RIGHT; + if (isSingleLine()) { + lineCount = 1; + } + int paintY, paintHeight; + Image lineBuffer; + GC lineGC; + boolean doubleBuffer = DOUBLE_BUFFER && lastPaintTopIndex == topIndex; + lastPaintTopIndex = topIndex; + if (doubleBuffer) { + paintY = 0; + paintHeight = renderHeight; + lineBuffer = new Image(getDisplay(), clientArea.width, renderHeight); + lineGC = new GC(lineBuffer, gcStyle); + lineGC.setFont(getFont()); + lineGC.setForeground(foreground); + lineGC.setBackground(background); + } else { + paintY = startY; + paintHeight = startY + renderHeight; + lineBuffer = null; + lineGC = gc; + } + for (int i = startLine; paintY < paintHeight && i < lineCount; i++, paintY += lineHeight) { + String line = content.getLine(i); + renderer.drawLine(line, i, paintY, lineGC, background, foreground, true); + } + if (paintY < paintHeight) { + lineGC.setBackground(background); + lineGC.fillRectangle(0, paintY, clientArea.width, paintHeight - paintY); + } + if (doubleBuffer) { + clearMargin(lineGC, background, clientArea, startY); + gc.drawImage(lineBuffer, 0, startY); + lineGC.dispose(); + lineBuffer.dispose(); + } + } + clearMargin(gc, background, clientArea, 0); +} +/** + * Prints the widget's text to the default printer. + * + * @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> + */ +public void print() { + checkWidget(); + Printer printer = new Printer(); + StyledTextPrintOptions options = new StyledTextPrintOptions(); + + options.printTextForeground = true; + options.printTextBackground = true; + options.printTextFontStyle = true; + options.printLineBackground = true; + new Printing(this, printer, options).run(); + printer.dispose(); +} +/** + * Returns a runnable that will print the widget's text + * to the specified printer. + * <p> + * The runnable may be run in a non-UI thread. + * </p> + * + * @param printer the printer to print to + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when printer is null</li> + * </ul> + */ +public Runnable print(Printer printer) { + checkWidget(); + StyledTextPrintOptions options = new StyledTextPrintOptions(); + options.printTextForeground = true; + options.printTextBackground = true; + options.printTextFontStyle = true; + options.printLineBackground = true; + if (printer == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + return print(printer, options); +} +/** + * Returns a runnable that will print the widget's text + * to the specified printer. + * <p> + * The runnable may be run in a non-UI thread. + * </p> + * + * @param printer the printer to print to + * @param options print options to use during printing + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when printer or options is null</li> + * </ul> + * @since 2.1 + */ +public Runnable print(Printer printer, StyledTextPrintOptions options) { + checkWidget(); + if (printer == null || options == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + return new Printing(this, printer, options); +} +/** + * Causes the entire bounds of the receiver to be marked + * as needing to be redrawn. The next time a paint request + * is processed, the control will be completely painted. + * <p> + * Recalculates the content width for all lines in the bounds. + * When a <code>LineStyleListener</code> is used a redraw call + * is the only notification to the widget that styles have changed + * and that the content width may have changed. + * </p> + * + * @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 Control#update + */ +public void redraw() { + int itemCount; + + super.redraw(); + itemCount = getPartialBottomIndex() - topIndex + 1; + lineCache.redrawReset(topIndex, itemCount, true); + lineCache.calculate(topIndex, itemCount); + setHorizontalScrollBar(); +} +/** + * Causes the rectangular area of the receiver specified by + * the arguments to be marked as needing to be redrawn. + * The next time a paint request is processed, that area of + * the receiver will be painted. If the <code>all</code> flag + * is <code>true</code>, any children of the receiver which + * intersect with the specified area will also paint their + * intersecting areas. If the <code>all</code> flag is + * <code>false</code>, the children will not be painted. + * <p> + * Marks the content width of all lines in the specified rectangle + * as unknown. Recalculates the content width of all visible lines. + * When a <code>LineStyleListener</code> is used a redraw call + * is the only notification to the widget that styles have changed + * and that the content width may have changed. + * </p> + * + * @param x the x coordinate of the area to draw + * @param y the y coordinate of the area to draw + * @param width the width of the area to draw + * @param height the height of the area to draw + * @param all <code>true</code> if children should redraw, and <code>false</code> otherwise + * + * @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 Control#update + */ +public void redraw(int x, int y, int width, int height, boolean all) { + super.redraw(x, y, width, height, all); + if (height > 0) { + int lineCount = content.getLineCount(); + int startLine = (getTopPixel() + y) / lineHeight; + int endLine = startLine + Compatibility.ceil(height, lineHeight); + int itemCount; + + // reset all lines in the redraw rectangle + startLine = Math.min(startLine, lineCount); + itemCount = Math.min(endLine, lineCount) - startLine; + lineCache.reset(startLine, itemCount, true); + // only calculate the visible lines + itemCount = getPartialBottomIndex() - topIndex + 1; + lineCache.calculate(topIndex, itemCount); + setHorizontalScrollBar(); + } +} +/** + * Redraws a text range in the specified lines + * <p> + * + * @param firstLine first line to redraw at the specified offset + * @param offsetInFirstLine offset in firstLine to start redrawing + * @param lastLine last line to redraw + * @param endOffset offset in the last where redrawing should stop + * @param clearBackground true=clear the background by invalidating + * the requested redraw range. If the redraw range includes the + * last character of a line (i.e., the entire line is redrawn) the + * line is cleared all the way to the right border of the widget. + * false=draw the foreground directly without invalidating the + * redraw range. + */ +void redrawLines(int firstLine, int offsetInFirstLine, int lastLine, int endOffset, boolean clearBackground) { + String line = content.getLine(firstLine); + int lineCount = lastLine - firstLine + 1; + int redrawY, redrawWidth; + int lineOffset = content.getOffsetAtLine(firstLine); + boolean fullLineRedraw; + Rectangle clientArea = getClientArea(); + + fullLineRedraw = ((getStyle() & SWT.FULL_SELECTION) != 0 && lastLine > firstLine); + // if redraw range includes last character on the first line, + // clear background to right widget border. fixes bug 19595. + if (clearBackground && endOffset - lineOffset >= line.length()) { + fullLineRedraw = true; + } + TextLayout layout = renderer.getTextLayout(line, lineOffset); + Rectangle rect = layout.getBounds(offsetInFirstLine, Math.min(endOffset, line.length()) - 1); + renderer.disposeTextLayout(layout); + rect.x -= horizontalScrollOffset; + rect.intersect(clientArea); + redrawY = firstLine * lineHeight - verticalScrollOffset; + redrawWidth = fullLineRedraw ? clientArea.width - leftMargin - rightMargin : rect.width; + draw(rect.x, redrawY, redrawWidth, lineHeight, clearBackground); + + // redraw last line if more than one line needs redrawing + if (lineCount > 1) { + lineOffset = content.getOffsetAtLine(lastLine); + int offsetInLastLine = endOffset - lineOffset; + // no redraw necessary if redraw offset is 0 + if (offsetInLastLine > 0) { + line = content.getLine(lastLine); + // if redraw range includes last character on the last line, + // clear background to right widget border. fixes bug 19595. + if (clearBackground && offsetInLastLine >= line.length()) { + fullLineRedraw = true; + } + line = content.getLine(lastLine); + layout = renderer.getTextLayout(line, lineOffset); + rect = layout.getBounds(0, offsetInLastLine - 1); + renderer.disposeTextLayout(layout); + rect.x -= horizontalScrollOffset; + rect.intersect(clientArea); + redrawY = lastLine * lineHeight - verticalScrollOffset; + redrawWidth = fullLineRedraw ? clientArea.width - leftMargin - rightMargin : rect.width; + draw(rect.x, redrawY, redrawWidth, lineHeight, clearBackground); + } + } +} +/** + * Fixes the widget to display a text change. + * Bit blitting and redrawing is done as necessary. + * <p> + * + * @param y y location of the text change + * @param newLineCount number of new lines. + * @param replacedLineCount number of replaced lines. + */ +void redrawMultiLineChange(int y, int newLineCount, int replacedLineCount) { + Rectangle clientArea = getClientArea(); + int lineCount = newLineCount - replacedLineCount; + int sourceY; + int destinationY; + + if (lineCount > 0) { + sourceY = Math.max(0, y + lineHeight); + destinationY = sourceY + lineCount * lineHeight; + } + else { + destinationY = Math.max(0, y + lineHeight); + sourceY = destinationY - lineCount * lineHeight; + } + scroll( + 0, destinationY, // destination x, y + 0, sourceY, // source x, y + clientArea.width, clientArea.height, true); + // Always redrawing causes the bottom line to flash when a line is + // deleted. This is because SWT merges the paint area of the scroll + // with the paint area of the redraw call below. + // To prevent this we could call update after the scroll. However, + // adding update can cause even more flash if the client does other + // redraw/update calls (ie. for syntax highlighting). + // We could also redraw only when a line has been added or when + // contents has been added to a line. This would require getting + // line index info from the content and is not worth the trouble + // (the flash is only on the bottom line and minor). + // Specifying the NO_MERGE_PAINTS style bit prevents the merged + // redraw but could cause flash/slowness elsewhere. + if (y + lineHeight > 0 && y <= clientArea.height) { + // redraw first changed line in case a line was split/joined + super.redraw(0, y, clientArea.width, lineHeight, true); + } + if (newLineCount > 0) { + int redrawStartY = y + lineHeight; + int redrawHeight = newLineCount * lineHeight; + + if (redrawStartY + redrawHeight > 0 && redrawStartY <= clientArea.height) { + // display new text + super.redraw(0, redrawStartY, clientArea.width, redrawHeight, true); + } + } +} +/** + * Redraws the specified text range. + * <p> + * + * @param start offset of the first character to redraw + * @param length number of characters to redraw + * @param clearBackground true if the background should be cleared as + * part of the redraw operation. If true, the entire redraw range will + * be cleared before anything is redrawn. If the redraw range includes + * the last character of a line (i.e., the entire line is redrawn) the + * line is cleared all the way to the right border of the widget. + * The redraw operation will be faster and smoother if clearBackground + * is set to false. Whether or not the flag can be set to false depends + * on the type of change that has taken place. If font styles or + * background colors for the redraw range have changed, clearBackground + * should be set to true. If only foreground colors have changed for + * the redraw range, clearBackground can be set to false. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when start and/or end are outside the widget content</li> + * </ul> + */ +public void redrawRange(int start, int length, boolean clearBackground) { + checkWidget(); + int end = start + length; + int contentLength = content.getCharCount(); + int firstLine; + int lastLine; + + if (start > end || start < 0 || end > contentLength) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + firstLine = content.getLineAtOffset(start); + lastLine = content.getLineAtOffset(end); + // reset all affected lines but let the redraw recalculate only + // those that are visible. + lineCache.reset(firstLine, lastLine - firstLine + 1, true); + internalRedrawRange(start, length, clearBackground); +} +/** + * Removes the specified bidirectional segment listener. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + * @since 2.0 + */ +public void removeBidiSegmentListener(BidiSegmentListener listener) { + checkWidget(); + if (listener == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + removeListener(LineGetSegments, listener); +} +/** + * Removes the specified extended modify listener. + * <p> + * + * @param extendedModifyListener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void removeExtendedModifyListener(ExtendedModifyListener extendedModifyListener) { + checkWidget(); + if (extendedModifyListener == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + removeListener(ExtendedModify, extendedModifyListener); +} +/** + * Removes the specified line background listener. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void removeLineBackgroundListener(LineBackgroundListener listener) { + checkWidget(); + if (listener == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + removeListener(LineGetBackground, listener); + // use default line styler if last user line styler was removed. + if (!isListening(LineGetBackground) && userLineBackground) { + StyledTextListener typedListener = new StyledTextListener(defaultLineStyler); + addListener(LineGetBackground, typedListener); + userLineBackground = false; + } +} +/** + * Removes the specified line style listener. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void removeLineStyleListener(LineStyleListener listener) { + checkWidget(); + if (listener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + removeListener(LineGetStyle, listener); + // use default line styler if last user line styler was removed. Fixes 1G7B1X2 + if (!isListening(LineGetStyle) && userLineStyle) { + StyledTextListener typedListener = new StyledTextListener(defaultLineStyler); + addListener(LineGetStyle, typedListener); + userLineStyle = false; + } +} +/** + * Removes the specified modify listener. + * <p> + * + * @param modifyListener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void removeModifyListener(ModifyListener modifyListener) { + checkWidget(); + if (modifyListener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + removeListener(SWT.Modify, modifyListener); +} +/** + * Removes the specified selection listener. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void removeSelectionListener(SelectionListener listener) { + checkWidget(); + if (listener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + removeListener(SWT.Selection, listener); +} +/** + * Removes the specified verify listener. + * <p> + * + * @param verifyListener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void removeVerifyListener(VerifyListener verifyListener) { + checkWidget(); + if (verifyListener == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + removeListener(SWT.Verify, verifyListener); +} +/** + * Removes the specified key verify listener. + * <p> + * + * @param listener the listener + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void removeVerifyKeyListener(VerifyKeyListener listener) { + if (listener == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + removeListener(VerifyKey, listener); +} +/** + * Replaces the styles in the given range with new styles. This method + * effectively deletes the styles in the given range and then adds the + * the new styles. + * <p> + * Should not be called if a LineStyleListener has been set since the + * listener maintains the styles. + * </p> + * + * @param start offset of first character where styles will be deleted + * @param length length of the range to delete styles in + * @param ranges StyleRange objects containing the new style information. + * The ranges should not overlap and should be within the specified start + * and length. The style rendering is undefined if the ranges do overlap + * or are ill-defined. Must not be null. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when either start or end is outside the valid range (0 <= offset <= getCharCount())</li> + * <li>ERROR_NULL_ARGUMENT when string is null</li> + * </ul> + * @since 2.0 + */ +public void replaceStyleRanges(int start, int length, StyleRange[] ranges) { + checkWidget(); + if (userLineStyle) { + return; + } + if (ranges == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + if (ranges.length == 0) { + setStyleRange(new StyleRange(start, length, null, null)); + return; + } + int end = start + length; + if (start > end || start < 0 || end > getCharCount()) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + int firstLine = content.getLineAtOffset(start); + int lastLine = content.getLineAtOffset(end); + + defaultLineStyler.replaceStyleRanges(start, length, ranges); + lineCache.reset(firstLine, lastLine - firstLine + 1, true); + + // if the area is not visible, there is no need to redraw + if (isAreaVisible(firstLine, lastLine)) { + int redrawY = firstLine * lineHeight - verticalScrollOffset; + int redrawStopY = (lastLine + 1) * lineHeight - verticalScrollOffset; + draw(0, redrawY, getClientArea().width, redrawStopY - redrawY, true); + } + + // make sure that the caret is positioned correctly. + // caret location may change if font style changes. + // fixes 1G8FODP + setCaretLocation(); +} +/** + * Replaces the given text range with new text. + * If the widget has the SWT.SINGLE style and "text" contains more than + * one line, only the first line is rendered but the text is stored + * unchanged. A subsequent call to getText will return the same text + * that was set. Note that only a single line of text should be set when + * the SWT.SINGLE style is used. + * <p> + * <b>NOTE:</b> During the replace operation the current selection is + * changed as follows: + * <ul> + * <li>selection before replaced text: selection unchanged + * <li>selection after replaced text: adjust the selection so that same text + * remains selected + * <li>selection intersects replaced text: selection is cleared and caret + * is placed after inserted text + * </ul> + * </p> + * + * @param start offset of first character to replace + * @param length number of characters to replace. Use 0 to insert text + * @param text new text. May be empty to delete text. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when either start or end is outside the valid range (0 <= offset <= getCharCount())</li> + * <li>ERROR_INVALID_ARGUMENT when either start or end is inside a multi byte line delimiter. + * Splitting a line delimiter for example by inserting text in between the CR and LF and deleting part of a line delimiter is not supported</li> + * <li>ERROR_NULL_ARGUMENT when string is null</li> + * </ul> + */ +public void replaceTextRange(int start, int length, String text) { + checkWidget(); + int contentLength = getCharCount(); + int end = start + length; + Event event = new Event(); + + if (start > end || start < 0 || end > contentLength) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + if (text == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + event.start = start; + event.end = end; + event.text = text; + modifyContent(event, false); +} +/** + * Resets the caret position, selection and scroll offsets. Recalculate + * the content width and scroll bars. Redraw the widget. + */ +void reset() { + ScrollBar verticalBar = getVerticalBar(); + ScrollBar horizontalBar = getHorizontalBar(); + caretOffset = 0; + topIndex = 0; + topOffset = 0; + verticalScrollOffset = 0; + horizontalScrollOffset = 0; + resetSelection(); + // discard any styles that may have been set by creating a + // new default line styler + if (defaultLineStyler != null) { + removeLineBackgroundListener(defaultLineStyler); + removeLineStyleListener(defaultLineStyler); + installDefaultLineStyler(); + } + calculateContentWidth(); + if (verticalBar != null) { + verticalBar.setSelection(0); + } + if (horizontalBar != null) { + horizontalBar.setSelection(0); + } + setScrollBars(); + setCaretLocation(); + super.redraw(); +} +/** + * Resets the selection. + */ +void resetSelection() { + selection.x = selection.y = caretOffset; + selectionAnchor = -1; +} +/** + * Scrolls the widget horizontally. + * <p> + * + * @param pixels number of pixels to scroll, > 0 = scroll left, + * < 0 scroll right + */ +void scrollHorizontal(int pixels) { + Rectangle clientArea; + + if (pixels == 0) { + return; + } + clientArea = getClientArea(); + if (pixels > 0) { + int sourceX = leftMargin + pixels; + int scrollWidth = clientArea.width - sourceX - rightMargin; + int scrollHeight = clientArea.height - topMargin - bottomMargin; + scroll( + leftMargin, topMargin, // destination x, y + sourceX, topMargin, // source x, y + scrollWidth, scrollHeight, true); + if (sourceX > scrollWidth) { + // redraw from end of scrolled area to beginning of scroll + // invalidated area + super.redraw( + leftMargin + scrollWidth, topMargin, + pixels - scrollWidth, scrollHeight, true); + } + } + else { + int destinationX = leftMargin - pixels; + int scrollWidth = clientArea.width - destinationX - rightMargin; + int scrollHeight = clientArea.height - topMargin - bottomMargin; + scroll( + destinationX, topMargin, // destination x, y + leftMargin, topMargin, // source x, y + scrollWidth, scrollHeight, true); + if (destinationX > scrollWidth) { + // redraw from end of scroll invalidated area to scroll + // destination + super.redraw( + leftMargin + scrollWidth, topMargin, + -pixels - scrollWidth, scrollHeight, true); + } + } + horizontalScrollOffset += pixels; + int oldColumnX = columnX - pixels; + setCaretLocation(); + // restore the original horizontal caret index + columnX = oldColumnX; +} +/** + * Scrolls the widget horizontally and adjust the horizontal scroll + * bar to reflect the new horizontal offset.. + * <p> + * + * @param pixels number of pixels to scroll, > 0 = scroll left, + * < 0 scroll right + * @return + * true=the widget was scrolled + * false=the widget was not scrolled, the given offset is not valid. + */ +boolean scrollHorizontalBar(int pixels) { + if (pixels == 0) { + return false; + } + ScrollBar horizontalBar = getHorizontalBar(); + if (horizontalBar != null) { + horizontalBar.setSelection(horizontalScrollOffset + pixels); + } + scrollHorizontal(pixels); + return true; +} +/** + * Selects all the text. + * <p> + * + * @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> + */ +public void selectAll() { + checkWidget(); + setSelection(0, Math.max(getCharCount(),0)); +} +/** + * Replaces/inserts text as defined by the event. + * <p> + * + * @param event the text change event. + * <ul> + * <li>event.start - the replace start offset</li> + * <li>event.end - the replace end offset</li> + * <li>event.text - the new text</li> + * </ul> + */ +void sendKeyEvent(Event event) { + if (editable) { + modifyContent(event, true); + } +} +void sendModifyEvent(Event event) { + Accessible accessible = getAccessible(); + if (event.text.length() == 0) { + accessible.textChanged(ACC.TEXT_DELETE, event.start, event.end - event.start); + } else { + if (event.start == event.end) { + accessible.textChanged(ACC.TEXT_INSERT, event.start, event.text.length()); + } else { + accessible.textChanged(ACC.TEXT_DELETE, event.start, event.end - event.start); + accessible.textChanged(ACC.TEXT_INSERT, event.start, event.text.length()); + } + } + notifyListeners(SWT.Modify, event); +} +/** + * Sends the specified selection event. + */ +void sendSelectionEvent() { + getAccessible().textSelectionChanged(); + Event event = new Event(); + event.x = selection.x; + event.y = selection.y; + notifyListeners(SWT.Selection, event); +} +/** + * Sets whether the widget wraps lines. + * This overrides the creation style bit SWT.WRAP. + * <p> + * + * @param wrap true=widget wraps lines, false=widget does not wrap lines + * @since 2.0 + */ +public void setWordWrap(boolean wrap) { + checkWidget(); + if ((getStyle() & SWT.SINGLE) != 0) return; + + if (wrap != wordWrap) { + ScrollBar horizontalBar = getHorizontalBar(); + + wordWrap = wrap; + if (wordWrap) { + logicalContent = content; + content = new WrappedContent(renderer, logicalContent); + } + else { + content = logicalContent; + } + calculateContentWidth(); + horizontalScrollOffset = 0; + if (horizontalBar != null) { + horizontalBar.setVisible(!wordWrap); + } + setScrollBars(); + setCaretLocation(); + super.redraw(); + } +} +/** + * Sets the receiver's caret. Set the caret's height and location. + * + * </p> + * @param caret the new caret for the receiver + * + * @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> + */ +public void setCaret(Caret caret) { + checkWidget (); + super.setCaret(caret); + caretDirection = SWT.NULL; + if (caret != null) { + setCaretLocation(); + } +} +/** + * @see org.eclipse.swt.widgets.Control#setBackground + */ +public void setBackground(Color color) { + checkWidget(); + background = color; + super.setBackground(getBackground()); + redraw(); +} +/** + * Sets the BIDI coloring mode. When true the BIDI text display + * algorithm is applied to segments of text that are the same + * color. + * + * @param mode the new coloring mode + * @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> + * <p> + * @deprecated use BidiSegmentListener instead. + * </p> + */ +public void setBidiColoring(boolean mode) { + checkWidget(); + bidiColoring = mode; +} +void setCaretLocation(int newCaretX, int line, int direction) { + Caret caret = getCaret(); + if (caret != null) { + boolean updateImage = caret == defaultCaret; + int imageDirection = direction; + if (isMirrored()) { + if (imageDirection == SWT.LEFT) { + imageDirection = SWT.RIGHT; + } else if (imageDirection == SWT.RIGHT) { + imageDirection = SWT.LEFT; + } + } + if (updateImage && imageDirection == SWT.RIGHT) { + newCaretX -= (caret.getSize().x - 1); + } + int newCaretY = line * lineHeight - verticalScrollOffset + topMargin; + caret.setLocation(newCaretX, newCaretY); + getAccessible().textCaretMoved(getCaretOffset()); + if (direction != caretDirection) { + caretDirection = direction; + if (updateImage) { + if (imageDirection == SWT.DEFAULT) { + defaultCaret.setImage(null); + } else if (imageDirection == SWT.LEFT) { + defaultCaret.setImage(leftCaretBitmap); + } else if (imageDirection == SWT.RIGHT) { + defaultCaret.setImage(rightCaretBitmap); + } + } + caret.setSize(caret.getSize().x, lineHeight); + if (caretDirection == SWT.LEFT) { + BidiUtil.setKeyboardLanguage(BidiUtil.KEYBOARD_NON_BIDI); + } else if (caretDirection == SWT.RIGHT) { + BidiUtil.setKeyboardLanguage(BidiUtil.KEYBOARD_BIDI); + } + } + } + columnX = newCaretX; +} +/** + * Moves the Caret to the current caret offset. + */ +void setCaretLocation() { + int lineIndex = getCaretLine(); + String line = content.getLine(lineIndex); + int lineOffset = content.getOffsetAtLine(lineIndex); + int offsetInLine = caretOffset - lineOffset; + int newCaretX = getXAtOffset(line, lineIndex, offsetInLine); + setCaretLocation(newCaretX, lineIndex, getCaretDirection()); +} +/** + * Sets the caret offset. + * + * @param offset caret offset, relative to the first character in the text. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT when either the start or the end of the selection range is inside a + * multi byte line delimiter (and thus neither clearly in front of or after the line delimiter) + * </ul> + */ +public void setCaretOffset(int offset) { + checkWidget(); + int length = getCharCount(); + + if (length > 0 && offset != caretOffset) { + if (offset < 0) { + caretOffset = 0; + } + else + if (offset > length) { + caretOffset = length; + } + else { + if (isLineDelimiter(offset)) { + // offset is inside a multi byte line delimiter. This is an + // illegal operation and an exception is thrown. Fixes 1GDKK3R + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + caretOffset = offset; + } + // clear the selection if the caret is moved. + // don't notify listeners about the selection change. + clearSelection(false); + } + // always update the caret location. fixes 1G8FODP + setCaretLocation(); +} +/** + * Copies the specified text range to the clipboard. The text will be placed + * in the clipboard in plain text format and RTF format. + * <p> + * + * @param start start index of the text + * @param length length of text to place in clipboard + * + * @exception SWTError, see Clipboard.setContents + * @see org.eclipse.swt.dnd.Clipboard#setContents + */ +void setClipboardContent(int start, int length, int clipboardType) throws SWTError { + if (clipboardType == DND.SELECTION_CLIPBOARD && !(IS_MOTIF || IS_GTK)) return; + TextTransfer plainTextTransfer = TextTransfer.getInstance(); + TextWriter plainTextWriter = new TextWriter(start, length); + String plainText = getPlatformDelimitedText(plainTextWriter); + Object[] data; + Transfer[] types; + if (clipboardType == DND.SELECTION_CLIPBOARD) { + data = new Object[]{plainText}; + types = new Transfer[]{plainTextTransfer}; + } else { + RTFTransfer rtfTransfer = RTFTransfer.getInstance(); + RTFWriter rtfWriter = new RTFWriter(start, length); + String rtfText = getPlatformDelimitedText(rtfWriter); + data = new Object[]{rtfText, plainText}; + types = new Transfer[]{rtfTransfer, plainTextTransfer}; + } + clipboard.setContents(data, types, clipboardType); +} +/** + * Sets the content implementation to use for text storage. + * <p> + * + * @param newContent StyledTextContent implementation to use for text storage. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * </ul> + */ +public void setContent(StyledTextContent newContent) { + checkWidget(); + if (newContent == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + if (content != null) { + content.removeTextChangeListener(textChangeListener); + } + logicalContent = newContent; + if (wordWrap) { + content = new WrappedContent(renderer, logicalContent); + } + else { + content = logicalContent; + } + content.addTextChangeListener(textChangeListener); + reset(); +} +/** + * Sets the receiver's cursor to the cursor specified by the + * argument. Overridden to handle the null case since the + * StyledText widget uses an ibeam as its default cursor. + * + * @see org.eclipse.swt.widgets.Control#setCursor + */ +public void setCursor (Cursor cursor) { + if (cursor == null) { + super.setCursor(ibeamCursor); + } else { + super.setCursor(cursor); + } +} +/** + * Sets whether the widget implements double click mouse behavior. + * </p> + * + * @param enable if true double clicking a word selects the word, if false + * double clicks have the same effect as regular mouse clicks. + * @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> + */ +public void setDoubleClickEnabled(boolean enable) { + checkWidget(); + doubleClickEnabled = enable; +} +/** + * Sets whether the widget content can be edited. + * </p> + * + * @param editable if true content can be edited, if false content can not be + * edited + * @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> + */ +public void setEditable(boolean editable) { + checkWidget(); + this.editable = editable; +} +/** + * Sets a new font to render text with. + * <p> + * <b>NOTE:</b> Italic fonts are not supported unless they have no overhang + * and the same baseline as regular fonts. + * </p> + * + * @param font new font + * @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> + */ +public void setFont(Font font) { + checkWidget(); + int oldLineHeight = lineHeight; + + super.setFont(font); + initializeRenderer(); + // keep the same top line visible. fixes 5815 + if (lineHeight != oldLineHeight) { + setVerticalScrollOffset(verticalScrollOffset * lineHeight / oldLineHeight, true); + claimBottomFreeSpace(); + } + calculateContentWidth(); + calculateScrollBars(); + if (isBidiCaret()) createCaretBitmaps(); + caretDirection = SWT.NULL; + // always set the caret location. Fixes 6685 + setCaretLocation(); + super.redraw(); +} +/** + * @see org.eclipse.swt.widgets.Control#setForeground + */ +public void setForeground(Color color) { + checkWidget(); + foreground = color; + super.setForeground(getForeground()); + redraw(); +} +/** + * Sets the horizontal scroll offset relative to the start of the line. + * Do nothing if there is no text set. + * <p> + * <b>NOTE:</b> The horizontal index is reset to 0 when new text is set in the + * widget. + * </p> + * + * @param offset horizontal scroll offset relative to the start + * of the line, measured in character increments starting at 0, if + * equal to 0 the content is not scrolled, if > 0 = the content is scrolled. + * @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> + */ +public void setHorizontalIndex(int offset) { + checkWidget(); + int clientAreaWidth = getClientArea().width; + if (getCharCount() == 0) { + return; + } + if (offset < 0) { + offset = 0; + } + offset *= getHorizontalIncrement(); + // allow any value if client area width is unknown or 0. + // offset will be checked in resize handler. + // don't use isVisible since width is known even if widget + // is temporarily invisible + if (clientAreaWidth > 0) { + int width = lineCache.getWidth(); + // prevent scrolling if the content fits in the client area. + // align end of longest line with right border of client area + // if offset is out of range. + if (offset > width - clientAreaWidth) { + offset = Math.max(0, width - clientAreaWidth); + } + } + scrollHorizontalBar(offset - horizontalScrollOffset); +} +/** + * Sets the horizontal pixel offset relative to the start of the line. + * Do nothing if there is no text set. + * <p> + * <b>NOTE:</b> The horizontal pixel offset is reset to 0 when new text + * is set in the widget. + * </p> + * + * @param pixel horizontal pixel offset relative to the start + * of the line. + * @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> + * @since 2.0 + */ +public void setHorizontalPixel(int pixel) { + checkWidget(); + int clientAreaWidth = getClientArea().width; + if (getCharCount() == 0) { + return; + } + if (pixel < 0) { + pixel = 0; + } + // allow any value if client area width is unknown or 0. + // offset will be checked in resize handler. + // don't use isVisible since width is known even if widget + // is temporarily invisible + if (clientAreaWidth > 0) { + int width = lineCache.getWidth(); + // prevent scrolling if the content fits in the client area. + // align end of longest line with right border of client area + // if offset is out of range. + if (pixel > width - clientAreaWidth) { + pixel = Math.max(0, width - clientAreaWidth); + } + } + scrollHorizontalBar(pixel - horizontalScrollOffset); +} +/** + * Adjusts the maximum and the page size of the horizontal scroll bar + * to reflect content width changes. + */ +void setHorizontalScrollBar() { + ScrollBar horizontalBar = getHorizontalBar(); + + if (horizontalBar != null && horizontalBar.getVisible()) { + final int INACTIVE = 1; + Rectangle clientArea = getClientArea(); + // only set the real values if the scroll bar can be used + // (ie. because the thumb size is less than the scroll maximum) + // avoids flashing on Motif, fixes 1G7RE1J and 1G5SE92 + if (clientArea.width < lineCache.getWidth()) { + horizontalBar.setValues( + horizontalBar.getSelection(), + horizontalBar.getMinimum(), + lineCache.getWidth(), // maximum + clientArea.width - leftMargin - rightMargin, // thumb size + horizontalBar.getIncrement(), + clientArea.width - leftMargin - rightMargin); // page size + } + else + if (horizontalBar.getThumb() != INACTIVE || horizontalBar.getMaximum() != INACTIVE) { + horizontalBar.setValues( + horizontalBar.getSelection(), + horizontalBar.getMinimum(), + INACTIVE, + INACTIVE, + horizontalBar.getIncrement(), + INACTIVE); + } + } +} +/** + * Sets the background color of the specified lines. + * The background color is drawn for the width of the widget. All + * line background colors are discarded when setText is called. + * The text background color if defined in a StyleRange overlays the + * line background color. Should not be called if a LineBackgroundListener + * has been set since the listener maintains the line backgrounds. + * <p> + * Line background colors are maintained relative to the line text, not the + * line index that is specified in this method call. + * During text changes, when entire lines are inserted or removed, the line + * background colors that are associated with the lines after the change + * will "move" with their respective text. An entire line is defined as + * extending from the first character on a line to the last and including the + * line delimiter. + * </p> + * <p> + * When two lines are joined by deleting a line delimiter, the top line + * background takes precedence and the color of the bottom line is deleted. + * For all other text changes line background colors will remain unchanged. + * </p> + * + * @param startLine first line the color is applied to, 0 based + * @param lineCount number of lines the color applies to. + * @param background line background color + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT when the specified line range is invalid</li> + * </ul> + */ +public void setLineBackground(int startLine, int lineCount, Color background) { + checkWidget(); + int partialBottomIndex = getPartialBottomIndex(); + + // this API can not be used if the client is providing the line background + if (userLineBackground) { + return; + } + if (startLine < 0 || startLine + lineCount > logicalContent.getLineCount()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + defaultLineStyler.setLineBackground(startLine, lineCount, background); + // do nothing if redraw range is completely invisible + if (startLine > partialBottomIndex || startLine + lineCount - 1 < topIndex) { + return; + } + // only redraw visible lines + if (startLine < topIndex) { + lineCount -= topIndex - startLine; + startLine = topIndex; + } + if (startLine + lineCount - 1 > partialBottomIndex) { + lineCount = partialBottomIndex - startLine + 1; + } + startLine -= topIndex; + super.redraw( + leftMargin, startLine * lineHeight + topMargin, + getClientArea().width - leftMargin - rightMargin, lineCount * lineHeight, true); +} +/** + * Flips selection anchor based on word selection direction. + */ +void setMouseWordSelectionAnchor() { + if (mouseDoubleClick) { + if (caretOffset < doubleClickSelection.x) { + selectionAnchor = doubleClickSelection.y; + } + else if (caretOffset > doubleClickSelection.y) { + selectionAnchor = doubleClickSelection.x; + } + } +} +/** + * Sets the orientation of the receiver, which must be one + * of the constants <code>SWT.LEFT_TO_RIGHT</code> or <code>SWT.RIGHT_TO_LEFT</code>. + * <p> + * + * @param orientation new orientation style + * + * @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> + * + * @since 2.1.2 + */ +public void setOrientation(int orientation) { + if ((orientation & (SWT.RIGHT_TO_LEFT | SWT.LEFT_TO_RIGHT)) == 0) { + return; + } + if ((orientation & SWT.RIGHT_TO_LEFT) != 0 && (orientation & SWT.LEFT_TO_RIGHT) != 0) { + return; + } + if ((orientation & SWT.RIGHT_TO_LEFT) != 0 && isMirrored()) { + return; + } + if ((orientation & SWT.LEFT_TO_RIGHT) != 0 && !isMirrored()) { + return; + } + if (!BidiUtil.setOrientation(handle, orientation)) { + return; + } + isMirrored = (orientation & SWT.RIGHT_TO_LEFT) != 0; + initializeRenderer(); + caretDirection = SWT.NULL; + setCaretLocation(); + keyActionMap.clear(); + createKeyBindings(); + super.redraw(); +} +/** + * Adjusts the maximum and the page size of the scroll bars to + * reflect content width/length changes. + */ +void setScrollBars() { + ScrollBar verticalBar = getVerticalBar(); + + if (verticalBar != null) { + Rectangle clientArea = getClientArea(); + final int INACTIVE = 1; + int maximum = content.getLineCount() * getVerticalIncrement(); + + // only set the real values if the scroll bar can be used + // (ie. because the thumb size is less than the scroll maximum) + // avoids flashing on Motif, fixes 1G7RE1J and 1G5SE92 + if (clientArea.height < maximum) { + verticalBar.setValues( + verticalBar.getSelection(), + verticalBar.getMinimum(), + maximum, + clientArea.height, // thumb size + verticalBar.getIncrement(), + clientArea.height); // page size + } + else + if (verticalBar.getThumb() != INACTIVE || verticalBar.getMaximum() != INACTIVE) { + verticalBar.setValues( + verticalBar.getSelection(), + verticalBar.getMinimum(), + INACTIVE, + INACTIVE, + verticalBar.getIncrement(), + INACTIVE); + } + } + setHorizontalScrollBar(); +} +/** + * Sets the selection to the given position and scrolls it into view. Equivalent to setSelection(start,start). + * <p> + * + * @param start new caret position + * @see #setSelection(int,int) + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT when either the start or the end of the selection range is inside a + * multi byte line delimiter (and thus neither clearly in front of or after the line delimiter) + * </ul> + */ +public void setSelection(int start) { + // checkWidget test done in setSelectionRange + setSelection(start, start); +} +/** + * Sets the selection and scrolls it into view. + * <p> + * Indexing is zero based. Text selections are specified in terms of + * caret positions. In a text widget that contains N characters, there are + * N+1 caret positions, ranging from 0..N + * </p> + * + * @param point x=selection start offset, y=selection end offset + * The caret will be placed at the selection start when x > y. + * @see #setSelection(int,int) + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when point is null</li> + * <li>ERROR_INVALID_ARGUMENT when either the start or the end of the selection range is inside a + * multi byte line delimiter (and thus neither clearly in front of or after the line delimiter) + * </ul> + */ +public void setSelection(Point point) { + checkWidget(); + if (point == null) SWT.error (SWT.ERROR_NULL_ARGUMENT); + setSelection(point.x, point.y); +} +/** + * Sets the receiver's selection background color to the color specified + * by the argument, or to the default system color for the control + * if the argument is null. + * + * @param color the new color (or null) + * + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT - if the argument has been disposed</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> + * @since 2.1 + */ +public void setSelectionBackground (Color color) { + checkWidget (); + if (color != null) { + if (color.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + selectionBackground = color; + redraw(); +} +/** + * Sets the receiver's selection foreground color to the color specified + * by the argument, or to the default system color for the control + * if the argument is null. + * + * @param color the new color (or null) + * + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT - if the argument has been disposed</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> + * @since 2.1 + */ +public void setSelectionForeground (Color color) { + checkWidget (); + if (color != null) { + if (color.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + selectionForeground = color; + redraw(); +} +/** + * Sets the selection and scrolls it into view. + * <p> + * Indexing is zero based. Text selections are specified in terms of + * caret positions. In a text widget that contains N characters, there are + * N+1 caret positions, ranging from 0..N + * </p> + * + * @param start selection start offset. The caret will be placed at the + * selection start when start > end. + * @param end selection end offset + * @see #setSelectionRange(int,int) + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT when either the start or the end of the selection range is inside a + * multi byte line delimiter (and thus neither clearly in front of or after the line delimiter) + * </ul> + */ +public void setSelection(int start, int end) { + // checkWidget test done in setSelectionRange + setSelectionRange(start, end - start); + showSelection(); +} +/** + * Sets the selection. The new selection may not be visible. Call showSelection to scroll + * the selection into view. A negative length places the caret at the visual start of the + * selection. <p> + * + * @param start offset of the first selected character + * @param length number of characters to select + * + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_ARGUMENT when either the start or the end of the selection range is inside a + * multi byte line delimiter (and thus neither clearly in front of or after the line delimiter) + * </ul> + */ +public void setSelectionRange(int start, int length) { + checkWidget(); + int contentLength = getCharCount(); + start = Math.max(0, Math.min (start, contentLength)); + int end = start + length; + if (end < 0) { + length = -start; + } else { + if (end > contentLength) length = contentLength - start; + } + if (isLineDelimiter(start) || isLineDelimiter(start + length)) { + // the start offset or end offset of the selection range is inside a + // multi byte line delimiter. This is an illegal operation and an exception + // is thrown. Fixes 1GDKK3R + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + internalSetSelection(start, length, false); + // always update the caret location. fixes 1G8FODP + setCaretLocation(); +} +/** + * Sets the selection. + * The new selection may not be visible. Call showSelection to scroll + * the selection into view. + * <p> + * + * @param start offset of the first selected character, start >= 0 must be true. + * @param length number of characters to select, 0 <= start + length + * <= getCharCount() must be true. + * A negative length places the caret at the selection start. + * @param sendEvent a Selection event is sent when set to true and when + * the selection is reset. + */ +void internalSetSelection(int start, int length, boolean sendEvent) { + int end = start + length; + + if (start > end) { + int temp = end; + end = start; + start = temp; + } + // is the selection range different or is the selection direction + // different? + if (selection.x != start || selection.y != end || + (length > 0 && selectionAnchor != selection.x) || + (length < 0 && selectionAnchor != selection.y)) { + clearSelection(sendEvent); + if (length < 0) { + selectionAnchor = selection.y = end; + caretOffset = selection.x = start; + } + else { + selectionAnchor = selection.x = start; + caretOffset = selection.y = end; + } + internalRedrawRange(selection.x, selection.y - selection.x, true); + } +} +/** + * Adds the specified style. The new style overwrites existing styles for the + * specified range. Existing style ranges are adjusted if they partially + * overlap with the new style, To clear an individual style, call setStyleRange + * with a StyleRange that has null attributes. + * <p> + * Should not be called if a LineStyleListener has been set since the + * listener maintains the styles. + * </p> + * + * @param range StyleRange object containing the style information. + * Overwrites the old style in the given range. May be null to delete + * all styles. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_INVALID_RANGE when the style range is outside the valid range (> getCharCount())</li> + * </ul> + */ +public void setStyleRange(StyleRange range) { + checkWidget(); + + // this API can not be used if the client is providing the line styles + if (userLineStyle) { + return; + } + // check the range, make sure it falls within the range of the text + if (range != null && range.start + range.length > content.getCharCount()) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + defaultLineStyler.setStyleRange(range); + if (range != null) { + int firstLine = content.getLineAtOffset(range.start); + int lastLine = content.getLineAtOffset(range.start + range.length); + lineCache.reset(firstLine, lastLine - firstLine + 1, true); + + // if the style is not visible, there is no need to redraw + if (isAreaVisible(firstLine, lastLine)) { + int redrawY = firstLine * lineHeight - verticalScrollOffset; + int redrawStopY = (lastLine + 1) * lineHeight - verticalScrollOffset; + draw(0, redrawY, getClientArea().width, redrawStopY - redrawY, true); + } + } else { + // clearing all styles + lineCache.reset(0, content.getLineCount(), false); + redraw(); + } + + // make sure that the caret is positioned correctly. + // caret location may change if font style changes. + // fixes 1G8FODP + setCaretLocation(); +} +/** + * Sets styles to be used for rendering the widget content. All styles + * in the widget will be replaced with the given set of styles. + * <p> + * Should not be called if a LineStyleListener has been set since the + * listener maintains the styles. + * </p> + * + * @param ranges StyleRange objects containing the style information. + * The ranges should not overlap. The style rendering is undefined if + * the ranges do overlap. Must not be null. The styles need to be in order. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when listener is null</li> + * <li>ERROR_INVALID_RANGE when the last of the style ranges is outside the valid range (> getCharCount())</li> + * </ul> + */ +public void setStyleRanges(StyleRange[] ranges) { + checkWidget(); + // this API can not be used if the client is providing the line styles + if (userLineStyle) { + return; + } + if (ranges == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + // check the last range, make sure it falls within the range of the + // current text + if (ranges.length != 0) { + StyleRange last = ranges[ranges.length-1]; + int lastEnd = last.start + last.length; + int firstLine = content.getLineAtOffset(ranges[0].start); + int lastLine; + if (lastEnd > content.getCharCount()) { + SWT.error(SWT.ERROR_INVALID_RANGE); + } + lastLine = content.getLineAtOffset(lastEnd); + // reset all lines affected by the style change + lineCache.reset(firstLine, lastLine - firstLine + 1, true); + } + else { + // reset all lines + lineCache.reset(0, content.getLineCount(), false); + } + defaultLineStyler.setStyleRanges(ranges); + redraw(); // should only redraw affected area to avoid flashing + // make sure that the caret is positioned correctly. + // caret location may change if font style changes. + // fixes 1G8FODP + setCaretLocation(); +} +/** + * Sets the tab width. + * <p> + * + * @param tabs tab width measured in characters. + * @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> + */ +public void setTabs(int tabs) { + checkWidget(); + tabLength = tabs; + renderer.setTabLength(tabLength); + if (caretOffset > 0) { + caretOffset = 0; + showCaret(); + clearSelection(false); + } + // reset all line widths when the tab width changes + lineCache.reset(0, content.getLineCount(), false); + redraw(); +} +/** + * Sets the widget content. + * If the widget has the SWT.SINGLE style and "text" contains more than + * one line, only the first line is rendered but the text is stored + * unchanged. A subsequent call to getText will return the same text + * that was set. + * <p> + * <b>Note:</b> Only a single line of text should be set when the SWT.SINGLE + * style is used. + * </p> + * + * @param text new widget content. Replaces existing content. Line styles + * that were set using StyledText API are discarded. The + * current selection is also discarded. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_NULL_ARGUMENT when string is null</li> + * </ul> + */ +public void setText(String text) { + checkWidget(); + Event event = new Event(); + + if (text == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + event.start = 0; + event.end = getCharCount(); + event.text = text; + event.doit = true; + notifyListeners(SWT.Verify, event); + if (event.doit) { + StyledTextEvent styledTextEvent = null; + + if (isListening(ExtendedModify)) { + styledTextEvent = new StyledTextEvent(logicalContent); + styledTextEvent.start = event.start; + styledTextEvent.end = event.start + event.text.length(); + styledTextEvent.text = content.getTextRange(event.start, event.end - event.start); + } + content.setText(event.text); + sendModifyEvent(event); + if (styledTextEvent != null) { + notifyListeners(ExtendedModify, styledTextEvent); + } + } +} +/** + * Sets the text limit to the specified number of characters. + * <p> + * The text limit specifies the amount of text that + * the user can type into the widget. + * </p> + * + * @param limit the new text limit. + * @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> + * @exception IllegalArgumentException <ul> + * <li>ERROR_CANNOT_BE_ZERO when limit is 0</li> + * </ul> + */ +public void setTextLimit(int limit) { + checkWidget(); + if (limit == 0) { + SWT.error(SWT.ERROR_CANNOT_BE_ZERO); + } + textLimit = limit; +} +/** + * Sets the top index. Do nothing if there is no text set. + * <p> + * The top index is the index of the line that is currently at the top + * of the widget. The top index changes when the widget is scrolled. + * Indexing starts from zero. + * Note: The top index is reset to 0 when new text is set in the widget. + * </p> + * + * @param topIndex new top index. Must be between 0 and + * getLineCount() - fully visible lines per page. If no lines are fully + * visible the maximum value is getLineCount() - 1. An out of range + * index will be adjusted accordingly. + * @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> + */ +public void setTopIndex(int topIndex) { + checkWidget(); + int lineCount = logicalContent.getLineCount(); + int pageSize = Math.max(1, Math.min(lineCount, getLineCountWhole())); + + if (getCharCount() == 0) { + return; + } + if (topIndex < 0) { + topIndex = 0; + } + else + if (topIndex > lineCount - pageSize) { + topIndex = lineCount - pageSize; + } + if (wordWrap) { + int logicalLineOffset = logicalContent.getOffsetAtLine(topIndex); + topIndex = content.getLineAtOffset(logicalLineOffset); + } + setVerticalScrollOffset(topIndex * getVerticalIncrement(), true); +} +/** + * Sets the top pixel offset. Do nothing if there is no text set. + * <p> + * The top pixel offset is the vertical pixel offset of the widget. The + * widget is scrolled so that the given pixel position is at the top. + * The top index is adjusted to the corresponding top line. + * Note: The top pixel is reset to 0 when new text is set in the widget. + * </p> + * + * @param pixel new top pixel offset. Must be between 0 and + * (getLineCount() - visible lines per page) / getLineHeight()). An out + * of range offset will be adjusted accordingly. + * @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> + * @since 2.0 + */ +public void setTopPixel(int pixel) { + checkWidget(); + int lineCount =content.getLineCount(); + int height = getClientArea().height; + int maxTopPixel = Math.max(0, lineCount * getVerticalIncrement() - height); + + if (getCharCount() == 0) { + return; + } + if (pixel < 0) { + pixel = 0; + } + else + if (pixel > maxTopPixel) { + pixel = maxTopPixel; + } + setVerticalScrollOffset(pixel, true); +} +/** + * Scrolls the widget vertically. + * <p> + * + * @param pixelOffset the new vertical scroll offset + * @param adjustScrollBar + * true= the scroll thumb will be moved to reflect the new scroll offset. + * false = the scroll thumb will not be moved + * @return + * true=the widget was scrolled + * false=the widget was not scrolled, the given offset is not valid. + */ +boolean setVerticalScrollOffset(int pixelOffset, boolean adjustScrollBar) { + Rectangle clientArea; + ScrollBar verticalBar = getVerticalBar(); + + if (pixelOffset == verticalScrollOffset) { + return false; + } + if (verticalBar != null && adjustScrollBar) { + verticalBar.setSelection(pixelOffset); + } + clientArea = getClientArea(); + scroll( + 0, 0, // destination x, y + 0, pixelOffset - verticalScrollOffset, // source x, y + clientArea.width, clientArea.height, true); + + verticalScrollOffset = pixelOffset; + calculateTopIndex(); + int oldColumnX = columnX; + setCaretLocation(); + // restore the original horizontal caret index + columnX = oldColumnX; + return true; +} +/** + * Scrolls the specified location into view. + * <p> + * + * @param x the x coordinate that should be made visible. + * @param line the line that should be made visible. Relative to the + * first line in the document. + * @return + * true=the widget was scrolled to make the specified location visible. + * false=the specified location is already visible, the widget was + * not scrolled. + */ +boolean showLocation(int x, int line) { + int clientAreaWidth = getClientArea().width - leftMargin; + int verticalIncrement = getVerticalIncrement(); + int horizontalIncrement = clientAreaWidth / 4; + boolean scrolled = false; + + if (x < leftMargin) { + // always make 1/4 of a page visible + x = Math.max(horizontalScrollOffset * -1, x - horizontalIncrement); + scrolled = scrollHorizontalBar(x); + } + else + if (x >= clientAreaWidth) { + // always make 1/4 of a page visible + x = Math.min(lineCache.getWidth() - horizontalScrollOffset, x + horizontalIncrement); + scrolled = scrollHorizontalBar(x - clientAreaWidth); + } + if (line < topIndex) { + scrolled = setVerticalScrollOffset(line * verticalIncrement, true); + } + else + if (line > getBottomIndex()) { + scrolled = setVerticalScrollOffset((line + 1) * verticalIncrement - getClientArea().height, true); + } + return scrolled; +} +/** + * Sets the caret location and scrolls the caret offset into view. + */ +void showCaret() { + int caretLine = content.getLineAtOffset(caretOffset); + + showCaret(caretLine); +} +/** + * Sets the caret location and scrolls the caret offset into view. + */ +void showCaret(int caretLine) { + int lineOffset = content.getOffsetAtLine(caretLine); + String line = content.getLine(caretLine); + int offsetInLine = caretOffset - lineOffset; + int newCaretX = getXAtOffset(line, caretLine, offsetInLine); + boolean scrolled = showLocation(newCaretX, caretLine); + boolean setWrapCaretLocation = false; + Caret caret = getCaret(); + + if (wordWrap && caret != null) { + int caretY = caret.getLocation().y; + if ((caretY + verticalScrollOffset) / getVerticalIncrement() - 1 != caretLine) { + setWrapCaretLocation = true; + } + } + if (!scrolled || setWrapCaretLocation) { + // set the caret location if a scroll operation did not set it (as a + // sideeffect of scrolling) or when in word wrap mode and the caret + // line was explicitly specified (i.e., because getWrapCaretLine does + // not return the desired line causing scrolling to not set it correctly) + setCaretLocation(newCaretX, caretLine, getCaretDirection()); + } +} +/** + * Scrolls the specified offset into view. + * <p> + * + * @param offset offset that should be scolled into view + */ +void showOffset(int offset) { + int line = content.getLineAtOffset(offset); + int lineOffset = content.getOffsetAtLine(line); + int offsetInLine = offset - lineOffset; + String lineText = content.getLine(line); + int xAtOffset = getXAtOffset(lineText, line, offsetInLine); + + showLocation(xAtOffset, line); +} +/** +/** + * Scrolls the selection into view. The end of the selection will be scrolled into + * view. Note that if a right-to-left selection exists, the end of the selection is the + * visual beginning of the selection (i.e., where the caret is located). + * <p> + * + * @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> + */ +public void showSelection() { + checkWidget(); + boolean selectionFits; + int startOffset, startLine, startX, endOffset, endLine, endX, offsetInLine; + + // is selection from right-to-left? + boolean rightToLeft = caretOffset == selection.x; + + if (rightToLeft) { + startOffset = selection.y; + endOffset = selection.x; + } else { + startOffset = selection.x; + endOffset = selection.y; + } + + // calculate the logical start and end values for the selection + startLine = content.getLineAtOffset(startOffset); + offsetInLine = startOffset - content.getOffsetAtLine(startLine); + startX = getXAtOffset(content.getLine(startLine), startLine, offsetInLine); + endLine = content.getLineAtOffset(endOffset); + offsetInLine = endOffset - content.getOffsetAtLine(endLine); + endX = getXAtOffset(content.getLine(endLine), endLine, offsetInLine); + + // can the selection be fully displayed within the widget's visible width? + int w = getClientArea().width; + if (rightToLeft) { + selectionFits = startX - endX <= w; + } else { + selectionFits = endX - startX <= w; + } + + if (selectionFits) { + // show as much of the selection as possible by first showing + // the start of the selection + showLocation(startX, startLine); + // endX value could change if showing startX caused a scroll to occur + endX = getXAtOffset(content.getLine(endLine), endLine, offsetInLine); + showLocation(endX, endLine); + } else { + // just show the end of the selection since the selection start + // will not be visible + showLocation(endX, endLine); + } +} +boolean isBidiCaret() { + return BidiUtil.isBidiPlatform(); +} +/** + * Updates the selection and caret position depending on the text change. + * If the selection intersects with the replaced text, the selection is + * reset and the caret moved to the end of the new text. + * If the selection is behind the replaced text it is moved so that the + * same text remains selected. If the selection is before the replaced text + * it is left unchanged. + * <p> + * + * @param startOffset offset of the text change + * @param replacedLength length of text being replaced + * @param newLength length of new text + */ +void updateSelection(int startOffset, int replacedLength, int newLength) { + if (selection.y <= startOffset) { + // selection ends before text change + return; + } + if (selection.x < startOffset) { + // clear selection fragment before text change + internalRedrawRange(selection.x, startOffset - selection.x, true); + } + if (selection.y > startOffset + replacedLength && selection.x < startOffset + replacedLength) { + // clear selection fragment after text change. + // do this only when the selection is actually affected by the + // change. Selection is only affected if it intersects the change (1GDY217). + int netNewLength = newLength - replacedLength; + int redrawStart = startOffset + newLength; + internalRedrawRange(redrawStart, selection.y + netNewLength - redrawStart, true); + } + if (selection.y > startOffset && selection.x < startOffset + replacedLength) { + // selection intersects replaced text. set caret behind text change + internalSetSelection(startOffset + newLength, 0, true); + // always update the caret location. fixes 1G8FODP + setCaretLocation(); + } + else { + // move selection to keep same text selected + internalSetSelection(selection.x + newLength - replacedLength, selection.y - selection.x, true); + // always update the caret location. fixes 1G8FODP + setCaretLocation(); + } +} +/** + * Rewraps all lines + * <p> + * + * @param oldClientAreaWidth client area width before resize + * occurred + */ +void wordWrapResize(int oldClientAreaWidth) { + WrappedContent wrappedContent = (WrappedContent) content; + int newTopIndex; + + // all lines are wrapped and no rewrap required if widget has already + // been visible, client area is now wider and visual (wrapped) line + // count equals logical line count. + if (oldClientAreaWidth != 0 && clientAreaWidth > oldClientAreaWidth && + wrappedContent.getLineCount() == logicalContent.getLineCount()) { + return; + } + wrappedContent.wrapLines(); + + // adjust the top index so that top line remains the same + newTopIndex = content.getLineAtOffset(topOffset); + // topOffset is the beginning of the top line. therefore it + // needs to be adjusted because in a wrapped line this is also + // the end of the preceeding line. + if (newTopIndex < content.getLineCount() - 1 && + topOffset == content.getOffsetAtLine(newTopIndex + 1)) { + newTopIndex++; + } + if (newTopIndex != topIndex) { + ScrollBar verticalBar = getVerticalBar(); + // adjust index and pixel offset manually instead of calling + // setVerticalScrollOffset because the widget does not actually need + // to be scrolled. causes flash otherwise. + verticalScrollOffset += (newTopIndex - topIndex) * getVerticalIncrement(); + // verticalScrollOffset may become negative if first line was + // partially visible and second line was top line. prevent this from + // happening to fix 8503. + if (verticalScrollOffset < 0) { + verticalScrollOffset = 0; + } + topIndex = newTopIndex; + topOffset = content.getOffsetAtLine(topIndex); + if (verticalBar != null) { + verticalBar.setSelection(verticalScrollOffset); + } + } + // caret may be on a different line after a rewrap. + // call setCaretLocation after fixing vertical scroll offset. + setCaretLocation(); + // word wrap may have changed on one of the visible lines + super.redraw(); +} +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/textStyler.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/textStyler.js new file mode 100644 index 00000000..b88fec85 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/textStyler.js @@ -0,0 +1,713 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*global document window navigator */ + +var examples = examples || {}; +examples.textview = examples.textview || {}; + +examples.textview.TextStyler = (function() { + + var JS_KEYWORDS = + ["break", "continue", "do", "for", /*"import",*/ "new", "this", /*"void",*/ + "case", "default", "else", "function", "in", "return", "typeof", "while", + "comment", "delete", "export", "if", /*"label",*/ "switch", "var", "with", + "abstract", "implements", "protected", /*"boolean",*/ /*"instanceOf",*/ "public", + /*"byte", "int", "short", "char",*/ "interface", "static", + /*"double", "long",*/ "synchronized", "false", /*"native",*/ "throws", + "final", "null", "transient", /*"float",*/ "package", "true", + "goto", "private", "catch", "enum", "throw", "class", "extends", "try", + "const", "finally", "debugger", "super", "undefined"]; + + var JAVA_KEYWORDS = + ["abstract", + "boolean", "break", "byte", + "case", "catch", "char", "class", "continue", + "default", "do", "double", + "else", "extends", + "false", "final", "finally", "float", "for", + "if", "implements", "import", "instanceof", "int", "interface", + "long", + "native", "new", "null", + "package", "private", "protected", "public", + "return", + "short", "static", "super", "switch", "synchronized", + "this", "throw", "throws", "transient", "true", "try", + "void", "volatile", + "while"]; + + var CSS_KEYWORDS = + ["color", "text-align", "text-indent", "text-decoration", + "font", "font-style", "font-family", "font-weight", "font-size", "font-variant", "line-height", + "background", "background-color", "background-image", "background-position", "background-repeat", "background-attachment", + "list-style", "list-style-image", "list-style-position", "list-style-type", + "outline", "outline-color", "outline-style", "outline-width", + "border", "border-left", "border-top", "border-bottom", "border-right", "border-color", "border-width", "border-style", + "border-bottom-color", "border-bottom-style", "border-bottom-width", + "border-left-color", "border-left-style", "border-left-width", + "border-top-color", "border-top-style", "border-top-width", + "border-right-color", "border-right-style", "border-right-width", + "padding", "padding-left", "padding-top", "padding-bottom", "padding-right", + "margin", "margin-left", "margin-top", "margin-bottom", "margin-right", + "width", "height", "left", "top", "right", "bottom", + "min-width", "max-width", "min-height", "max-height", + "display", "visibility", + "clip", "cursor", "overflow", "overflow-x", "overflow-y", "position", "z-index", + "vertical-align", "horizontal-align", + "float", "clear" + ]; + + // Scanner constants + var UNKOWN = 1; + var KEYWORD = 2; + var STRING = 3; + var COMMENT = 4; + var WHITE = 5; + var WHITE_TAB = 6; + var WHITE_SPACE = 7; + + // Styles + var isIE = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent) ? document.documentMode : undefined; + var commentStyle = {styleClass: "token_comment"}; + var javadocStyle = {styleClass: "token_javadoc"}; + var stringStyle = {styleClass: "token_string"}; + var keywordStyle = {styleClass: "token_keyword"}; + var spaceStyle = {styleClass: "token_space"}; + var tabStyle = {styleClass: "token_tab"}; + var bracketStyle = {styleClass: isIE < 9 ? "token_bracket" : "token_bracket_outline"}; + var caretLineStyle = {styleClass: "line_caret"}; + + var Scanner = (function() { + function Scanner (keywords, whitespacesVisible) { + this.keywords = keywords; + this.whitespacesVisible = whitespacesVisible; + this.setText(""); + } + + Scanner.prototype = { + getOffset: function() { + return this.offset; + }, + getStartOffset: function() { + return this.startOffset; + }, + getData: function() { + return this.text.substring(this.startOffset, this.offset); + }, + getDataLength: function() { + return this.offset - this.startOffset; + }, + _read: function() { + if (this.offset < this.text.length) { + return this.text.charCodeAt(this.offset++); + } + return -1; + }, + _unread: function(c) { + if (c !== -1) { this.offset--; } + }, + nextToken: function() { + this.startOffset = this.offset; + while (true) { + var c = this._read(); + switch (c) { + case -1: return null; + case 47: // SLASH -> comment + c = this._read(); + if (c === 47) { + while (true) { + c = this._read(); + if ((c === -1) || (c === 10)) { + this._unread(c); + return COMMENT; + } + } + } + this._unread(c); + return UNKOWN; + case 39: // SINGLE QUOTE -> char const + while(true) { + c = this._read(); + switch (c) { + case 39: + return STRING; + case -1: + this._unread(c); + return STRING; + case 92: // BACKSLASH + c = this._read(); + break; + } + } + break; + case 34: // DOUBLE QUOTE -> string + while(true) { + c = this._read(); + switch (c) { + case 34: // DOUBLE QUOTE + return STRING; + case -1: + this._unread(c); + return STRING; + case 92: // BACKSLASH + c = this._read(); + break; + } + } + break; + case 32: // SPACE + case 9: // TAB + if (this.whitespacesVisible) { + return c === 32 ? WHITE_SPACE : WHITE_TAB; + } + do { + c = this._read(); + } while(c === 32 || c === 9); + this._unread(c); + return WHITE; + default: + var isCSS = this.isCSS; + if ((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)) { //LETTER OR UNDERSCORE OR NUMBER + var off = this.offset - 1; + do { + c = this._read(); + } while((97 <= c && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57) || (0x2d === c && isCSS)); //LETTER OR UNDERSCORE OR NUMBER + this._unread(c); + var word = this.text.substring(off, this.offset); + //TODO slow + for (var i=0; i<this.keywords.length; i++) { + if (this.keywords[i] === word) { return KEYWORD; } + } + } + return UNKOWN; + } + } + }, + setText: function(text) { + this.text = text; + this.offset = 0; + this.startOffset = 0; + } + }; + return Scanner; + }()); + + var WhitespaceScanner = (function() { + function WhitespaceScanner () { + Scanner.call(this, null, true); + } + WhitespaceScanner.prototype = new Scanner(null); + WhitespaceScanner.prototype.nextToken = function() { + this.startOffset = this.offset; + while (true) { + var c = this._read(); + switch (c) { + case -1: return null; + case 32: // SPACE + return WHITE_SPACE; + case 9: // TAB + return WHITE_TAB; + default: + do { + c = this._read(); + } while(!(c === 32 || c === 9 || c === -1)); + this._unread(c); + return UNKOWN; + } + } + }; + + return WhitespaceScanner; + }()); + + function TextStyler (view, lang) { + this.commentStart = "/*"; + this.commentEnd = "*/"; + var keywords = []; + switch (lang) { + case "java": keywords = JAVA_KEYWORDS; break; + case "js": keywords = JS_KEYWORDS; break; + case "css": keywords = CSS_KEYWORDS; break; + } + this.whitespacesVisible = false; + this.highlightCaretLine = true; + this._scanner = new Scanner(keywords, this.whitespacesVisible); + //TODO this scanner is not the best/correct way to parse CSS + if (lang === "css") { + this._scanner.isCSS = true; + } + this._whitespaceScanner = new WhitespaceScanner(); + this.view = view; + this.commentOffset = 0; + this.commentOffsets = []; + this._currentBracket = undefined; + this._matchingBracket = undefined; + + view.addEventListener("Selection", this, this._onSelection); + view.addEventListener("ModelChanged", this, this._onModelChanged); + view.addEventListener("Destroy", this, this._onDestroy); + view.addEventListener("LineStyle", this, this._onLineStyle); + view.redrawLines(); + } + + TextStyler.prototype = { + destroy: function() { + var view = this.view; + if (view) { + view.removeEventListener("Selection", this, this._onSelection); + view.removeEventListener("ModelChanged", this, this._onModelChanged); + view.removeEventListener("Destroy", this, this._onDestroy); + view.removeEventListener("LineStyle", this, this._onLineStyle); + this.view = null; + } + }, + setHighlightCaretLine: function(highlight) { + this.highlightCaretLine = highlight; + }, + setWhitespacesVisible: function(visible) { + this.whitespacesVisible = visible; + this._scanner.whitespacesVisible = visible; + }, + _binarySearch: function(offsets, offset, low, high) { + while (high - low > 2) { + var index = (((high + low) >> 1) >> 1) << 1; + var end = offsets[index + 1]; + if (end > offset) { + high = index; + } else { + low = index; + } + } + return high; + }, + _computeComments: function(end) { + // compute comments between commentOffset and end + if (end <= this.commentOffset) { return; } + var model = this.view.getModel(); + var charCount = model.getCharCount(); + var e = end; + // Uncomment to compute all comments +// e = charCount; + var t = /*start == this.commentOffset && e == end ? text : */model.getText(this.commentOffset, e); + if (this.commentOffsets.length > 1 && this.commentOffsets[this.commentOffsets.length - 1] === charCount) { + this.commentOffsets.length--; + } + var offset = 0; + while (offset < t.length) { + var begin = (this.commentOffsets.length & 1) === 0; + var search = begin ? this.commentStart : this.commentEnd; + var index = t.indexOf(search, offset); + if (index !== -1) { + this.commentOffsets.push(this.commentOffset + (begin ? index : index + search.length)); + } else { + break; + } + offset = index + search.length; + } + if ((this.commentOffsets.length & 1) === 1) { this.commentOffsets.push(charCount); } + this.commentOffset = e; + }, + _getCommentRanges: function(start, end) { + this._computeComments (end); + var commentCount = this.commentOffsets.length; + var commentStart = this._binarySearch(this.commentOffsets, start, -1, commentCount); + if (commentStart >= commentCount) { return []; } + if (this.commentOffsets[commentStart] > end) { return []; } + var commentEnd = Math.min(commentCount - 2, this._binarySearch(this.commentOffsets, end, commentStart - 1, commentCount)); + if (this.commentOffsets[commentEnd] > end) { commentEnd = Math.max(commentStart, commentEnd - 2); } + return this.commentOffsets.slice(commentStart, commentEnd + 2); + }, + _getLineStyle: function(lineIndex) { + if (this.highlightCaretLine) { + var view = this.view; + var model = view.getModel(); + var selection = view.getSelection(); + if (selection.start === selection.end && model.getLineAtOffset(selection.start) === lineIndex) { + return caretLineStyle; + } + } + return null; + }, + _getStyles: function(text, start) { + var end = start + text.length; + var model = this.view.getModel(); + + // get comment ranges that intersect with range + var commentRanges = this._getCommentRanges (start, end); + var styles = []; + + // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc) + var offset = start; + for (var i = 0; i < commentRanges.length; i+= 2) { + var commentStart = commentRanges[i]; + if (offset < commentStart) { + this._parse(text.substring(offset - start, commentStart - start), offset, styles); + } + var style = commentStyle; + if ((commentRanges[i+1] - commentStart) > (this.commentStart.length + this.commentEnd.length)) { + var o = commentStart + this.commentStart.length; + if (model.getText(o, o + 1) === "*") { style = javadocStyle; } + } + if (this.whitespacesVisible) { + var s = Math.max(offset, commentStart); + var e = Math.min(end, commentRanges[i+1]); + this._parseWhitespace(text.substring(s - start, e - start), s, styles, style); + } else { + styles.push({start: commentRanges[i], end: commentRanges[i+1], style: style}); + } + offset = commentRanges[i+1]; + } + if (offset < end) { + this._parse(text.substring(offset - start, end - start), offset, styles); + } + return styles; + }, + _parse: function(text, offset, styles) { + var scanner = this._scanner; + scanner.setText(text); + var token; + while ((token = scanner.nextToken())) { + var tokenStart = scanner.getStartOffset() + offset; + var style = null; + if (tokenStart === this._matchingBracket) { + style = bracketStyle; + } else { + switch (token) { + case KEYWORD: style = keywordStyle; break; + case STRING: + if (this.whitespacesVisible) { + this._parseWhitespace(scanner.getData(), tokenStart, styles, stringStyle); + continue; + } else { + style = stringStyle; + } + break; + case COMMENT: + if (this.whitespacesVisible) { + this._parseWhitespace(scanner.getData(), tokenStart, styles, commentStyle); + continue; + } else { + style = commentStyle; + } + break; + case WHITE_TAB: + if (this.whitespacesVisible) { + style = tabStyle; + } + break; + case WHITE_SPACE: + if (this.whitespacesVisible) { + style = spaceStyle; + } + break; + } + } + styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style}); + } + }, + _parseWhitespace: function(text, offset, styles, s) { + var scanner = this._whitespaceScanner; + scanner.setText(text); + var token; + while ((token = scanner.nextToken())) { + var tokenStart = scanner.getStartOffset() + offset; + var style = s; + switch (token) { + case WHITE_TAB: + style = tabStyle; + break; + case WHITE_SPACE: + style = spaceStyle; + break; + } + styles.push({start: tokenStart, end: scanner.getOffset() + offset, style: style}); + } + }, + _findBrackets: function(bracket, closingBracket, text, textOffset, start, end) { + var result = []; + + // get comment ranges that intersect with range + var commentRanges = this._getCommentRanges (start, end); + + // for any sub range that is not a comment, parse code generating tokens (keywords, numbers, brackets, line comments, etc) + var offset = start, scanner = this._scanner, token, tokenData; + for (var i = 0; i < commentRanges.length; i+= 2) { + var commentStart = commentRanges[i]; + if (offset < commentStart) { + scanner.setText(text.substring(offset - start, commentStart - start)); + while ((token = scanner.nextToken())) { + if (scanner.getDataLength() !== 1) { continue; } + tokenData = scanner.getData(); + if (tokenData === bracket) { + result.push(scanner.getStartOffset() + offset - start + textOffset); + } + if (tokenData === closingBracket) { + result.push(-(scanner.getStartOffset() + offset - start + textOffset)); + } + } + } + offset = commentRanges[i+1]; + } + if (offset < end) { + scanner.setText(text.substring(offset - start, end - start)); + while ((token = scanner.nextToken())) { + if (scanner.getDataLength() !== 1) { continue; } + tokenData = scanner.getData(); + if (tokenData === bracket) { + result.push(scanner.getStartOffset() + offset - start + textOffset); + } + if (tokenData === closingBracket) { + result.push(-(scanner.getStartOffset() + offset - start + textOffset)); + } + } + } + return result; + }, + _onDestroy: function(e) { + this.destroy(); + }, + _onLineStyle: function (e) { + e.style = this._getLineStyle(e.lineIndex); + e.ranges = this._getStyles(e.lineText, e.lineStart); + }, + _onSelection: function(e) { + var oldSelection = e.oldValue; + var newSelection = e.newValue; + var view = this.view; + var model = view.getModel(); + var lineIndex; + if (this._matchingBracket !== undefined) { + lineIndex = model.getLineAtOffset(this._matchingBracket); + view.redrawLines(lineIndex, lineIndex + 1); + this._matchingBracket = this._currentBracket = undefined; + } + if (this.highlightCaretLine) { + var oldLineIndex = model.getLineAtOffset(oldSelection.start); + lineIndex = model.getLineAtOffset(newSelection.start); + var newEmpty = newSelection.start === newSelection.end; + var oldEmpty = oldSelection.start === oldSelection.end; + if (!(oldLineIndex === lineIndex && oldEmpty && newEmpty)) { + if (oldEmpty) { + view.redrawLines(oldLineIndex, oldLineIndex + 1); + } + if ((oldLineIndex !== lineIndex || !oldEmpty) && newEmpty) { + view.redrawLines(lineIndex, lineIndex + 1); + } + } + } + if (newSelection.start !== newSelection.end || newSelection.start === 0) { + return; + } + var caret = view.getCaretOffset(); + if (caret === 0) { return; } + var brackets = "{}()[]<>"; + var bracket = model.getText(caret - 1, caret); + var bracketIndex = brackets.indexOf(bracket, 0); + if (bracketIndex === -1) { return; } + var closingBracket; + if (bracketIndex & 1) { + closingBracket = brackets.substring(bracketIndex - 1, bracketIndex); + } else { + closingBracket = brackets.substring(bracketIndex + 1, bracketIndex + 2); + } + lineIndex = model.getLineAtOffset(caret); + var lineText = model.getLine(lineIndex); + var lineStart = model.getLineStart(lineIndex); + var lineEnd = model.getLineEnd(lineIndex); + brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd); + for (var i=0; i<brackets.length; i++) { + var sign = brackets[i] >= 0 ? 1 : -1; + if (brackets[i] * sign === caret - 1) { + var level = 1; + this._currentBracket = brackets[i] * sign; + if (bracketIndex & 1) { + i--; + for (; i>=0; i--) { + sign = brackets[i] >= 0 ? 1 : -1; + level += sign; + if (level === 0) { + this._matchingBracket = brackets[i] * sign; + view.redrawLines(lineIndex, lineIndex + 1); + return; + } + } + lineIndex -= 1; + while (lineIndex >= 0) { + lineText = model.getLine(lineIndex); + lineStart = model.getLineStart(lineIndex); + lineEnd = model.getLineEnd(lineIndex); + brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd); + for (var j=brackets.length - 1; j>=0; j--) { + sign = brackets[j] >= 0 ? 1 : -1; + level += sign; + if (level === 0) { + this._matchingBracket = brackets[j] * sign; + view.redrawLines(lineIndex, lineIndex + 1); + return; + } + } + lineIndex--; + } + } else { + i++; + for (; i<brackets.length; i++) { + sign = brackets[i] >= 0 ? 1 : -1; + level += sign; + if (level === 0) { + this._matchingBracket = brackets[i] * sign; + view.redrawLines(lineIndex, lineIndex + 1); + return; + } + } + lineIndex += 1; + var lineCount = model.getLineCount (); + while (lineIndex < lineCount) { + lineText = model.getLine(lineIndex); + lineStart = model.getLineStart(lineIndex); + lineEnd = model.getLineEnd(lineIndex); + brackets = this._findBrackets(bracket, closingBracket, lineText, lineStart, lineStart, lineEnd); + for (var k=0; k<brackets.length; k++) { + sign = brackets[k] >= 0 ? 1 : -1; + level += sign; + if (level === 0) { + this._matchingBracket = brackets[k] * sign; + view.redrawLines(lineIndex, lineIndex + 1); + return; + } + } + lineIndex++; + } + } + break; + } + } + }, + _onModelChanged: function(e) { + var start = e.start; + var removedCharCount = e.removedCharCount; + var addedCharCount = e.addedCharCount; + if (this._matchingBracket && start < this._matchingBracket) { this._matchingBracket += addedCharCount + removedCharCount; } + if (this._currentBracket && start < this._currentBracket) { this._currentBracket += addedCharCount + removedCharCount; } + if (start >= this.commentOffset) { return; } + var model = this.view.getModel(); + +// window.console.log("start=" + start + " added=" + addedCharCount + " removed=" + removedCharCount) +// for (var i=0; i< this.commentOffsets.length; i++) { +// window.console.log(i +"="+ this.commentOffsets[i]); +// } + + var commentCount = this.commentOffsets.length; + var extra = Math.max(this.commentStart.length - 1, this.commentEnd.length - 1); + if (commentCount === 0) { + this.commentOffset = Math.max(0, start - extra); + return; + } + var charCount = model.getCharCount(); + var oldCharCount = charCount - addedCharCount + removedCharCount; + var commentStart = this._binarySearch(this.commentOffsets, start, -1, commentCount); + var end = start + removedCharCount; + var commentEnd = this._binarySearch(this.commentOffsets, end, commentStart - 1, commentCount); +// window.console.log("s=" + commentStart + " e=" + commentEnd); + var ts; + if (commentStart > 0) { + ts = this.commentOffsets[--commentStart]; + } else { + ts = Math.max(0, Math.min(this.commentOffsets[commentStart], start) - extra); + --commentStart; + } + var te; + var redrawEnd = charCount; + if (commentEnd + 1 < this.commentOffsets.length) { + te = this.commentOffsets[++commentEnd]; + if (end > (te - this.commentEnd.length)) { + if (commentEnd + 2 < this.commentOffsets.length) { + commentEnd += 2; + te = this.commentOffsets[commentEnd]; + redrawEnd = te + 1; + if (redrawEnd > start) { redrawEnd += addedCharCount - removedCharCount; } + } else { + te = Math.min(oldCharCount, end + extra); + this.commentOffset = te; + } + } + } else { + te = Math.min(oldCharCount, end + extra); + this.commentOffset = te; + if (commentEnd > 0 && commentEnd === this.commentOffsets.length) { + commentEnd = this.commentOffsets.length - 1; + } + } + if (ts > start) { ts += addedCharCount - removedCharCount; } + if (te > start) { te += addedCharCount - removedCharCount; } + +// window.console.log("commentStart="+ commentStart + " commentEnd=" + commentEnd + " ts=" + ts + " te=" + te) + + if (this.commentOffsets.length > 1 && this.commentOffsets[this.commentOffsets.length - 1] === oldCharCount) { + this.commentOffsets.length--; + } + + var offset = 0; + var newComments = []; + var t = model.getText(ts, te); + if (this.commentOffset < te) { this.commentOffset = te; } + while (offset < t.length) { + var begin = ((commentStart + 1 + newComments.length) & 1) === 0; + var search = begin ? this.commentStart : this.commentEnd; + var index = t.indexOf(search, offset); + if (index !== -1) { + newComments.push(ts + (begin ? index : index + search.length)); + } else { + break; + } + offset = index + search.length; + } +// window.console.log("lengths=" + newComments.length + " " + (commentEnd - commentStart) + " t=<" + t + ">") +// for (var i=0; i< newComments.length; i++) { +// window.console.log(i +"=>"+ newComments[i]); +// } + var redraw = (commentEnd - commentStart) !== newComments.length; + if (!redraw) { + for (var i=0; i<newComments.length; i++) { + offset = this.commentOffsets[commentStart + 1 + i]; + if (offset > start) { offset += addedCharCount - removedCharCount; } + if (offset !== newComments[i]) { + redraw = true; + break; + } + } + } + + var args = [commentStart + 1, (commentEnd - commentStart)].concat(newComments); + Array.prototype.splice.apply(this.commentOffsets, args); + for (var k=commentStart + 1 + newComments.length; k< this.commentOffsets.length; k++) { + this.commentOffsets[k] += addedCharCount - removedCharCount; + } + + if ((this.commentOffsets.length & 1) === 1) { this.commentOffsets.push(charCount); } + + if (redraw) { +// window.console.log ("redraw " + (start + addedCharCount) + " " + redrawEnd); + this.view.redrawRange(start + addedCharCount, redrawEnd); + } + +// for (var i=0; i< this.commentOffsets.length; i++) { +// window.console.log(i +"="+ this.commentOffsets[i]); +// } + + } + }; + return TextStyler; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return examples.textview; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/textstyler.css b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/textstyler.css new file mode 100644 index 00000000..72f339c0 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/examples/textview/textstyler.css @@ -0,0 +1,41 @@ +.token_comment { + color: green; +} + +.token_javadoc { + color: #00008F; +} + +.token_string { + color: blue; +} + +.token_keyword { + color: darkred; + font-weight: bold; +} + +.token_bracket_outline { + outline: 1px solid red; +} + +.token_bracket { + color: white; + background-color: grey; +} + +.token_space { + background-image: url('/examples/textview/images/white_space.png'); + background-repeat: no-repeat; + background-position: center center; +} + +.token_tab { + background-image: url('/examples/textview/images/white_tab.png'); + background-repeat: no-repeat; + background-position: left center; +} + +.line_caret { + background-color: #EAF2FE; +}
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/contentAssist.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/contentAssist.js new file mode 100644 index 00000000..cd1a54b6 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/contentAssist.js @@ -0,0 +1,258 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +/*global eclipse:true dojo */ +/*jslint maxerr:150 browser:true devel:true */ + + +/** + * @namespace The global container for eclipse APIs. + */ + var orion = orion || {}; + orion.editor = orion.editor || {}; + +/** + * A ContentAssist will look for content assist providers in the service registry (if provided). + * Alternately providers can be registered directly by calling {@link #addProvider}. + * @name eclipse.ContentAssist + * @param {orion.editor.Editor} editor + * @param {String} contentAssistId + * @param {eclipse.ServiceRegistry} [serviceRegistry] If omitted, providers must be registered via {@link #addProvider}. + */ +orion.editor.ContentAssist = (function() { + function ContentAssist(editor, contentAssistId, serviceRegistry) { + this.editor = editor; + this.textView = editor.getTextView(); + this.contentAssistPanel = dojo.byId(contentAssistId); + this.active = false; + this.prefix = ""; + this.serviceRegistry = serviceRegistry; + this.contentAssistProviders = []; + this.activeServiceReferences = []; + this.activeContentAssistProviders = []; + this.contentAssistListener = { + onVerify: function(event){ + this.showContentAssist(false); + }, + onSelectionChanged: function() { + this.showContentAssist(false); + } + }; + this.init(); + } + ContentAssist.prototype = { + init: function() { + var isMac = navigator.platform.indexOf("Mac") !== -1; + this.textView.setKeyBinding(isMac ? new orion.textview.KeyBinding(' ', false, false, false, true) : new orion.textview.KeyBinding(' ', true), "Content Assist"); + this.textView.setAction("Content Assist", dojo.hitch(this, function() { + this.showContentAssist(true); + return true; + })); + dojo.connect(this.editor, "onInputChange", this, this.inputChanged); + }, + + inputChanged: function(fileName) { + if (this.serviceRegistry) { + // Filter the ServiceReferences + this.activeServiceReferences = []; + var serviceReferences = this.serviceRegistry.getServiceReferences("orion.edit.contentAssist"); + var serviceReference; + dojo.forEach(serviceReferences, dojo.hitch(this, function(serviceReference) { + var info = {}; + var propertyNames = serviceReference.getPropertyNames(); + for (var i = 0; i < propertyNames.length; i++) { + info[propertyNames[i]] = serviceReference.getProperty(propertyNames[i]); + } + if (new RegExp(info.pattern).test(fileName)) { + this.activeServiceReferences.push(serviceReference); + } + })); + } + // Filter the registered providers + for (var i=0; i < this.contentAssistProviders.length; i++) { + var provider = this.contentAssistProviders[i]; + if (new RegExp(provider.pattern).test(fileName)) { + this.activeContentAssistProviders.push(provider.provider); + } + } + }, + + cancel: function() { + this.showContentAssist(false); + }, + isActive: function() { + return this.active; + }, + lineUp: function() { + if (this.contentAssistPanel) { + var nodes = dojo.query('> div', this.contentAssistPanel); + var index = 0; + for (var i=0; i<nodes.length; i++) { + if (nodes[i].className === "selected") { + nodes[i].className = ""; + index = i; + break; + } + } + if (index > 0) { + nodes[index-1].className = "selected"; + } else { + nodes[nodes.length - 1].className = "selected"; + } + return true; + } + }, + lineDown: function() { + if (this.contentAssistPanel) { + var nodes = dojo.query('> div', this.contentAssistPanel); + var index = 0; + for (var i=0; i<nodes.length; i++) { + if (nodes[i].className === "selected") { + nodes[i].className = ""; + index = i; + break; + } + } + if (index < nodes.length - 1) { + nodes[index+1].className = "selected"; + } else { + nodes[0].className = "selected"; + } + return true; + } + }, + enter: function() { + if (this.contentAssistPanel) { + var proposal = dojo.query("> .selected", this.contentAssistPanel); + this.textView.setText(proposal[0].innerHTML.substring(this.prefix.length), this.textView.getCaretOffset(), this.textView.getCaretOffset()); + this.showContentAssist(false); + return true; + } + }, + showContentAssist: function(enable) { + if (!this.contentAssistPanel) { + return; + } + function createDiv(proposal, isSelected, parent) { + var attributes = {innerHTML: proposal, onclick: function(){alert(proposal);}}; + if (isSelected) { + attributes.className = "selected"; + } + dojo.create("div", attributes, parent, this); + } + if (!enable) { + this.textView.removeEventListener("Verify", this, this.contentAssistListener.onVerify); + this.textView.removeEventListener("Selection", this, this.contentAssistListener.onSelectionChanged); + this.active = false; + this.contentAssistPanel.style.display = "none"; + } else { + var offset = this.textView.getCaretOffset(); + var index = offset; + var c; + while (index > 0 && ((97 <= (c = this.textView.getText(index - 1, index).charCodeAt(0)) && c <= 122) || (65 <= c && c <= 90) || c === 95 || (48 <= c && c <= 57))) { //LETTER OR UNDERSCORE OR NUMBER + index--; + } + + // Show all proposals +// if (index === offset) { +// return; +// } + this.prefix = this.textView.getText(index, offset); + + var proposals = [], + buffer = this.textView.getText(), + selection = this.textView.getSelection(); + this.getKeywords(this.prefix, buffer, selection).then( + dojo.hitch(this, function(keywords) { + for (var i = 0; i < keywords.length; i++) { + var proposal = keywords[i]; + if (proposal.substr(0, this.prefix.length) === this.prefix) { + proposals.push(proposal); + } + } + if (proposals.length === 0) { + return; + } + + var caretLocation = this.textView.getLocationAtOffset(offset); + caretLocation.y += this.textView.getLineHeight(); + this.contentAssistPanel.innerHTML = ""; + for (i = 0; i<proposals.length; i++) { + createDiv(proposals[i], i===0, this.contentAssistPanel); + } + this.textView.convert(caretLocation, "document", "page"); + this.contentAssistPanel.style.position = "absolute"; + this.contentAssistPanel.style.left = caretLocation.x + "px"; + this.contentAssistPanel.style.top = caretLocation.y + "px"; + this.contentAssistPanel.style.display = "block"; + this.textView.addEventListener("Verify", this, this.contentAssistListener.onVerify); + this.textView.addEventListener("Selection", this, this.contentAssistListener.onSelectionChanged); + this.active = true; + })); + } + }, + /** + * @param {String} The string buffer.substring(w+1, c) where c is the caret offset and w is the index of the + * rightmost whitespace character preceding c. + * @param {String} buffer The entire buffer being edited + * @param {eclipse.Selection} selection The current textView selection. + * @returns {dojo.Deferred} A future that will provide the keywords. + */ + getKeywords: function(prefix, buffer, selection) { + var keywords = []; + + // Add keywords from directly registered providers + dojo.forEach(this.activeContentAssistProviders, function(provider) { + keywords = keywords.concat(provider.getKeywords() || []); + }); + + // Add keywords from providers registered through service registry + if (this.serviceRegistry) { + var keywordPromises = dojo.map(this.activeServiceReferences, dojo.hitch(this, function(serviceRef) { + return this.serviceRegistry.getService(serviceRef).then(function(service) { + return service.getKeywords(prefix, buffer, selection); + }); + })); + var dl = new dojo.DeferredList(keywordPromises); + return dl.then(function(results) { + for (var i=0; i < results.length; i++) { + var result = results[i]; + if (result[0]) { + var serviceKeywords = result[1]; + keywords = keywords.concat(serviceKeywords); + } + } + return keywords; + }); + } else { + var d = new dojo.Deferred(); + d.callback(keywords); + return d; + } + }, + /** + * Adds a content assist provider. + * @param {Object} provider The provider object. See {@link orion.contentAssist.CssContentAssistProvider} for an example. + * @param {String} name Name for this provider. + * @param {String} pattern The regex pattern matching filenames that provider can offer content assist for. + */ + addProvider: function(provider, name, pattern) { + this.contentAssistProviders = this.contentAssistProviders || []; + this.contentAssistProviders.push({name: name, pattern: pattern, provider: provider}); + } + }; + return ContentAssist; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define(['dojo', 'orion/textview/keyBinding', 'dojo/DeferredList'], function() { + return orion.editor; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/editor.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/editor.js new file mode 100644 index 00000000..91fee9eb --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/editor.js @@ -0,0 +1,352 @@ +/******************************************************************************* + * Copyright (c) 2009, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + + /*global window dojo orion:true eclipse:true handleGetAuthenticationError*/ + /*jslint maxerr:150 browser:true devel:true regexp:false*/ + +var orion = orion || {}; +orion.editor = orion.editor || {}; + +orion.editor.Editor = (function() { + function Editor(options) { + this._textViewFactory = options.textViewFactory; + this._undoStackFactory = options.undoStackFactory; + this._annotationFactory = options.annotationFactory; + this._lineNumberRulerFactory = options.lineNumberRulerFactory; + this._contentAssistFactory = options.contentAssistFactory; + this._keyBindingFactory = options.keyBindingFactory; + this._statusReporter = options.statusReporter; + this._domNode = options.domNode; + this._syntaxHighlightProviders = options.syntaxHighlightProviders; + + this._annotationsRuler = null; + this._overviewRuler = null; + this._dirty = false; + this._contentAssist = null; + this._keyModes = []; + } + Editor.prototype = { + getTextView: function() { + return this._textView; + }, + + reportStatus: function(message, isError) { + if (this._statusReporter) { + this._statusReporter(message, isError); + } else { + window.alert(isError ? "ERROR: " + message : message); + } + }, + + /** + * @static + * @param textView + * @param start + * @param end + */ + moveSelection: function(textView, start, end) { + end = end || start; + textView.setSelection(start, end, false); + var topPixel = textView.getTopPixel(); + var bottomPixel = textView.getBottomPixel(); + var line = textView.getModel().getLineAtOffset(start); + var linePixel = textView.getLinePixel(line); + if (linePixel < topPixel || linePixel > bottomPixel) { + var height = bottomPixel - topPixel; + var target = Math.max(0, linePixel- Math.floor((linePixel<topPixel?3:1)*height / 4)); + var a = new dojo.Animation({ + node: textView, + duration: 300, + curve: [topPixel, target], + onAnimate: function(x){ + textView.setTopPixel(Math.floor(x)); + }, + onEnd: function() { + textView.showSelection(); + textView.focus(); + } + }); + a.play(); + } else { + textView.showSelection(); + textView.focus(); + } + }, + getContents : function() { + if (this._textView) { + return this._textView.getText(); + } + }, + isDirty : function() { + return this._dirty; + }, + checkDirty : function() { + var dirty = !this._undoStack.isClean(); + if (this._dirty === dirty) { + return; + } + this.onDirtyChange(dirty); + }, + + getAnnotationsRuler : function() { + return this._annotationsRuler; + }, + + /** + * Helper for finding occurrences of str in the textView. + * @param str {String} + * @param startIndex {number} + * @param [ignoreCase] {boolean} Default is false + * @param [reverse] {boolean} Default is false + * @return {index: number, length: number} giving the match details, or null if no match found. + */ + doFind: function(str, startIndex, ignoreCase, reverse) { + var text = this._textView.getText(); + if (ignoreCase) { + str = str.toLowerCase(); + text = text.toLowerCase(); + } + + var i; + if (reverse) { + text = text.split("").reverse().join(""); + str = str.split("").reverse().join(""); + startIndex = text.length - startIndex - 1; + i = text.indexOf(str, startIndex); + if (i !== -1) { + return {index: text.length - str.length - i, length: str.length}; + } + } else { + i = text.indexOf(str, startIndex); + if (i !== -1) { + return {index: i, length: str.length}; + } + } + return null; + }, + + /** + * Helper for finding regexp matches in the textView. Use doFind() for simple string searches. + * @param pattern {String} A valid regexp pattern + * @param flags {String} Valid regexp flags: [is] + * @param [startIndex] {number} Default is false + * @param [reverse] {boolean} Default is false + * @return {index: number, length: number} giving the match details, or null if no match found. + */ + doFindRegExp: function(pattern, flags, startIndex, reverse) { + if (!pattern) { + return null; + } + + flags = flags || ""; + // 'g' makes exec() iterate all matches, 'm' makes ^$ work linewise + flags += (flags.indexOf("g") === -1 ? "g" : "") + (flags.indexOf("m") === -1 ? "m" : ""); + var regexp = new RegExp(pattern, flags); + var text = this._textView.getText(); + var result = null, + match = null; + if (reverse) { + while (true) { + result = regexp.exec(text); + if (result && result.index <= startIndex) { + match = {index: result.index, length: result[0].length}; + } else { + return match; + } + } + } else { + result = regexp.exec(text.substring(startIndex)); + return result && {index: result.index + startIndex, length: result[0].length}; + } + }, + + /** + * @param {String} Input string + * @return {pattern:String, flags:String} if str looks like a RegExp, or null otherwise + */ + parseRegExp: function(str) { + var regexp = /^\s*\/(.+)\/([gim]{0,3})\s*$/.exec(str); + if (regexp) { + return {pattern: regexp[1], flags: regexp[2]}; + } + return null; + }, + + installTextView : function() { + // Create textView and install optional features + this._textView = this._textViewFactory(); + if (this._undoStackFactory) { + this._undoStack = this._undoStackFactory.createUndoStack(this); + } + if (this._contentAssistFactory) { + this._contentAssist = this._contentAssistFactory(this); + this._keyModes.push(this._contentAssist); + } + + var editor = this, + textView = this._textView; + + // Set up keybindings + if (this._keyBindingFactory) { + this._keyBindingFactory(this, this._keyModes, this._undoStack, this._contentAssist); + } + + // Set keybindings for keys that apply to different modes + textView.setKeyBinding(new orion.textview.KeyBinding(27), "Cancel Current Mode"); + textView.setAction("Cancel Current Mode", dojo.hitch(this, function() { + for (var i=0; i<this._keyModes.length; i++) { + if (this._keyModes[i].isActive()) { + return this._keyModes[i].cancel(); + } + } + return false; + })); + + textView.setAction("lineUp", dojo.hitch(this, function() { + for (var i=0; i<this._keyModes.length; i++) { + if (this._keyModes[i].isActive()) { + return this._keyModes[i].lineUp(); + } + } + return false; + })); + textView.setAction("lineDown", dojo.hitch(this, function() { + for (var i=0; i<this._keyModes.length; i++) { + if (this._keyModes[i].isActive()) { + return this._keyModes[i].lineDown(); + } + } + return false; + })); + + /**@this {orion.editor.Editor} */ + function updateCursorStatus() { + var model = textView.getModel(); + var caretOffset = textView.getCaretOffset(); + var lineIndex = model.getLineAtOffset(caretOffset); + var lineStart = model.getLineStart(lineIndex); + var offsetInLine = caretOffset - lineStart; + // If we are in a mode, we will bail out from reporting the cursor position. + for (var i=0; i<this._keyModes.length; i++) { + if (this._keyModes[i].isActive()) { + return; + } + } + this.reportStatus("Line " + (lineIndex + 1) + " : Col " + offsetInLine); + } + + // Listener for dirty state + textView.addEventListener("ModelChanged", this, this.checkDirty); + + //Adding selection changed listener + textView.addEventListener("Selection", this, updateCursorStatus); + + // Create rulers + if (this._annotationFactory) { + var annotations = this._annotationFactory.createAnnotationRulers(); + this._annotationsRuler = annotations.annotationRuler; + + this._annotationsRuler.onClick = function(lineIndex, e) { + if (lineIndex === undefined) { return; } + if (lineIndex === -1) { return; } + var annotation = this.getAnnotation(lineIndex); + if (annotation === undefined) { return; } + editor.onGotoLine(annotation.line, annotation.column); + }; + + this._overviewRuler = annotations.overviewRuler; + this._overviewRuler.onClick = function(lineIndex, e) { + if (lineIndex === undefined) { return; } + editor.moveSelection(textView, textView.getModel().getLineStart(lineIndex)); + }; + + textView.addRuler(this._annotationsRuler); + textView.addRuler(this._overviewRuler); + } + + if (this._lineNumberRulerFactory) { + this._lineNumberRuler = this._lineNumberRulerFactory.createLineNumberRuler(); + textView.addRuler(this._lineNumberRuler); + } + }, + + showSelection : function(start, end, line, offset, length) { + // We use typeof because we need to distinguish the number 0 from an undefined or null parameter + if (typeof(start) === "number") { + if (typeof(end) !== "number") { + end = start; + } + this.moveSelection(this._textView, start, end); + } else if (typeof(line) === "number") { + var pos = this._textView.getModel().getLineStart(line-1); + if (typeof(offset) === "number") { + pos = pos + offset; + } + if (typeof(length) !== "number") { + length = 0; + } + this.moveSelection(this._textView, pos, pos+length); + } + }, + + onInputChange : function (title, message, contents, contentsSaved) { + if (contentsSaved && this._textView) { + // don't reset undo stack on save, just mark it clean so that we don't lose the undo past the save + this._undoStack.markClean(); + this.checkDirty(); + return; + } + if (this._textView) { + if (message) { + this._textView.setText(message); + } else { + if (contents !== null && contents !== undefined) { + this._textView.setText(contents); + } + } + this._undoStack.reset(); + this.checkDirty(); + this._textView.focus(); + } + }, + + onGotoLine : function (line, column, end) { + if (this._textView) { + var lineStart = this._textView.getModel().getLineStart(line); + if (typeof column === "string") { + var index = this._textView.getModel().getLine(line).indexOf(column); + if (index !== -1) { + end = index + column.length; + column = index; + } else { + column = 0; + } + } + var col = Math.min(this._textView.getModel().getLineEnd(line), column); + if (end===undefined) { + end = col; + } + var offset = lineStart + col; + this.moveSelection(this._textView, offset, lineStart + end); + } + }, + + onDirtyChange: function(isDirty) { + this._dirty = isDirty; + } + }; + return Editor; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define(['dojo', 'dijit', 'orion/textview/keyBinding', 'dijit/TitlePane', 'dijit/layout/ContentPane' ], function(){ + return orion.editor; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/editorFeatures.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/editorFeatures.js new file mode 100644 index 00000000..573c341b --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/editorFeatures.js @@ -0,0 +1,798 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +/*global window widgets eclipse:true orion:true serviceRegistry dojo dijit */ +/*jslint maxerr:150 browser:true devel:true regexp:false*/ + + +/** + * @namespace The global container for orion APIs. + */ + +var orion = orion || {}; +orion.editor = orion.editor || {}; + +orion.editor.UndoFactory = (function() { + function UndoFactory() { + } + UndoFactory.prototype = { + createUndoStack: function(editor) { + var undoStack = new orion.textview.UndoStack(editor.getTextView(), 200); + editor.getTextView().setKeyBinding(new orion.textview.KeyBinding('z', true), "Undo"); + editor.getTextView().setAction("Undo", function() { + undoStack.undo(); + return true; + }); + + var isMac = navigator.platform.indexOf("Mac") !== -1; + editor.getTextView().setKeyBinding(isMac ? new orion.textview.KeyBinding('z', true, true) : new orion.textview.KeyBinding('y', true), "Redo"); + editor.getTextView().setAction("Redo", function() { + undoStack.redo(); + return true; + }); + return undoStack; + } + }; + return UndoFactory; +}()); + +orion.editor.LineNumberRulerFactory = (function() { + function LineNumberRulerFactory() { + } + LineNumberRulerFactory.prototype = { + createLineNumberRuler: function() { + return new orion.textview.LineNumberRuler("left", {style: {backgroundColor: "#ffffff", textAlign: "right", borderLeft:"1px solid #ddd", borderRight:"1px solid #ddd"}}, {style: { backgroundColor: "#ffffff" }}, {style: { backgroundColor: "#ffffff" }}); + } + }; + return LineNumberRulerFactory; +}()); + + +orion.editor.AnnotationFactory = (function() { + function AnnotationFactory() { + } + AnnotationFactory.prototype = { + createAnnotationRulers: function() { + var rulerStyle = {style: { backgroundColor: "#ffffff" }}; + this.annotationRuler = new orion.textview.AnnotationRuler("left", rulerStyle, {html: "<img src='/images/problem.gif'></img>"}); + this.overviewRuler = new orion.textview.OverviewRuler("right", rulerStyle, this.annotationRuler); + return {annotationRuler: this.annotationRuler, overviewRuler: this.overviewRuler}; + }, + + showProblems : function(problems) { + var errors, i, k, escapedReason, functions; + errors = problems || []; + i = 0; + if (errors.length>0 && errors[errors.length - 1] === null) { + errors.pop(); + } + var ruler = this.annotationRuler; + if (!ruler) { + return; + } + ruler.clearAnnotations(); + var lastLine = -1; + for (k in errors) { + if (errors[k]) { + // escaping voodoo... we need to construct HTML that contains valid JavaScript. + escapedReason = errors[k].reason.replace(/'/g, "'").replace(/"/g, '"'); + // console.log(escapedReason); + var annotation = { + line: errors[k].line - 1, + column: errors[k].character, + html: "<img src='/images/problem.gif' title='" + escapedReason + "' alt='" + escapedReason + "'></img>", + overviewStyle: {style: {"backgroundColor": "lightcoral", "border": "1px solid red"}} + }; + + // only one error reported per line, unless we want to merge them. + // For now, just show the first one, and the next one will show when the first is fixed... + if (lastLine !== errors[k].line) { + // console.log("adding annotation at line " + errors[k].line); + ruler.setAnnotation(errors[k].line - 1, annotation); + lastLine = errors[k].line; + } + } + } + } + }; + return AnnotationFactory; +}()); + +/** + * TextCommands connects common text editing keybindings onto an editor. + */ +orion.editor.TextActions = (function() { + function TextActions(editor, undoStack) { + this.editor = editor; + this.textView = editor.getTextView(); + this.undoStack = undoStack; + this._incrementalFindActive = false; + this._incrementalFindSuccess = true; + this._incrementalFindIgnoreSelection = false; + this._incrementalFindPrefix = ""; + + this.init(); + } + TextActions.prototype = { + init: function() { + this._incrementalFindListener = { + onVerify: dojo.hitch(this, function(event){ + var prefix = this._incrementalFindPrefix, + txt = this.textView.getText(event.start, event.end), + match = prefix.match(new RegExp("^"+dojo.regexp.escapeString(txt), "i")); + if (match && match.length > 0) { + prefix = this._incrementalFindPrefix += event.text; + this.editor.reportStatus("Incremental find: " + prefix); + var ignoreCase = prefix.toLowerCase() === prefix; + var result = this.editor.doFind(prefix, this.textView.getSelection().start, ignoreCase); + if (result) { + this._incrementalFindSuccess = true; + this._incrementalFindIgnoreSelection = true; + this.editor.moveSelection(this.textView, result.index, result.index+result.length); + this._incrementalFindIgnoreSelection = false; + } else { + this.editor.reportStatus("Incremental find: " + prefix + " (not found)", true); + this._incrementalFindSuccess = false; + } + event.text = null; + } else { + } + }), + onSelection: dojo.hitch(this, function() { + if (!this._incrementalFindIgnoreSelection) { + this.toggleIncrementalFind(); + } + }) + }; + // Find actions + // These variables are used among the various find actions: + var searchString = "", + pattern, + flags; + this.textView.setKeyBinding(new orion.textview.KeyBinding("f", true), "Find..."); + this.textView.setAction("Find...", dojo.hitch(this, function() { + setTimeout(dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + if (selection.end > selection.start) { + searchString = this.textView.getText().substring(selection.start, selection.end); + } else { + searchString = ""; + } + searchString = prompt("Enter search term or /regex/:", searchString); + if (!searchString) { + return; + } + + var ignoreCase = searchString.toLowerCase() === searchString, + regexp = this.editor.parseRegExp(searchString), + result; + if (regexp) { + pattern = regexp.pattern; + flags = regexp.flags; + flags = flags + (ignoreCase && flags.indexOf("i") === -1 ? "i" : ""); + result = this.editor.doFindRegExp(pattern, flags, this.textView.getCaretOffset()); + } else { + pattern = null; + flags = null; + result = this.editor.doFind(searchString, this.textView.getCaretOffset(), ignoreCase); + } + + if (result) { + this.editor.moveSelection(this.textView, result.index, result.index+result.length); + } else { + this.editor.reportStatus("not found", true); + } + }), 0); + return true; + })); + this.textView.setKeyBinding(new orion.textview.KeyBinding("k", true), "Find Next Occurrence"); + this.textView.setAction("Find Next Occurrence", dojo.hitch(this, function() { + var result, ignoreCase, selection; + if (this._incrementalFindActive) { + var str = this._incrementalFindPrefix; + ignoreCase = str.toLowerCase() === str; + result = this.editor.doFind(str, this.textView.getCaretOffset(), ignoreCase); + } else if (pattern) { + // RegExp search + result = this.editor.doFindRegExp(pattern, flags, this.textView.getCaretOffset()); + } else { + // use selection if there is one, otherwise use last stored string. + // Since we aren't sure how/why text is highlighted, we will always ignore case. + // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=342334 + selection = this.textView.getSelection(); + if (selection.end > selection.start) { + searchString = this.textView.getText().substring(selection.start, selection.end); + } + result = this.editor.doFind(searchString, this.textView.getCaretOffset(), true); + } + + if (result) { + this._incrementalFindIgnoreSelection = true; + this.editor.moveSelection(this.textView, result.index, result.index+result.length); + this._incrementalFindIgnoreSelection = false; + } else { + this.editor.reportStatus("not found", true); + } + return true; + })); + this.textView.setKeyBinding(new orion.textview.KeyBinding("k", true, true), "Find Previous Occurrence"); + this.textView.setAction("Find Previous Occurrence", dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + var selectionSize = (selection.end > selection.start) ? selection.end - selection.start : 0; + var result, ignoreCase; + if (this._incrementalFindActive) { + var str = this._incrementalFindPrefix; + ignoreCase = str.toLowerCase() === str; + result = this.editor.doFind(str, this.textView.getCaretOffset() - selectionSize - 1, ignoreCase, true); + } else if (pattern) { + // RegExp search + result = this.editor.doFindRegExp(pattern, flags, this.textView.getCaretOffset() - selectionSize - 1, true); + } else { + if (selectionSize > 0) { + searchString = this.textView.getText().substring(selection.start, selection.end); + } + result = this.editor.doFind(searchString, this.textView.getCaretOffset() - selectionSize - 1, true, true); + } + + if (result) { + this._incrementalFindIgnoreSelection = true; + this.editor.moveSelection(this.textView, result.index, result.index+result.length); + this._incrementalFindIgnoreSelection = false; + } else { + this.editor.reportStatus("not found", true); + } + return true; + })); + this.textView.setKeyBinding(new orion.textview.KeyBinding("j", true), "Incremental Find"); + this.textView.setAction("Incremental Find", dojo.hitch(this, function() { + if (!this._incrementalFindActive) { + this.textView.setCaretOffset(this.textView.getCaretOffset()); + this.toggleIncrementalFind(); + } else { + var p = this._incrementalFindPrefix; + if (p.length !== 0) { + var start = this.textView.getSelection().start + 1; + if (this._incrementalFindSuccess === false) { + start = 0; + } + + var caseInsensitive = p.toLowerCase() === p; + var result = this.editor.doFind(p, start, caseInsensitive); + if (result) { + this._incrementalFindSuccess = true; + this._incrementalFindIgnoreSelection = true; + this.editor.moveSelection(this.textView, result.index, result.index + result.length); + this._incrementalFindIgnoreSelection = false; + this.editor.reportStatus("Incremental find: " + p); + } else { + this.editor.reportStatus("Incremental find: " + p + " (not found)", true); + this._incrementalFindSuccess = false; + } + } + } + return true; + })); + this.textView.setAction("deletePrevious", dojo.hitch(this, function() { + if (this._incrementalFindActive) { + var p = this._incrementalFindPrefix; + p = this._incrementalFindPrefix = p.substring(0, p.length-1); + if (p.length===0) { + this._incrementalFindSuccess = true; + this._incrementalFindIgnoreSelection = true; + this.textView.setCaretOffset(this.textView.getSelection().start); + this._incrementalFindIgnoreSelection = false; + this.toggleIncrementalFind(); + return true; + } + this.editor.reportStatus("Incremental find: " + p); + var index = this.textView.getText().lastIndexOf(p, this.textView.getCaretOffset() - p.length - 1); + if (index !== -1) { + this._incrementalFindSuccess = true; + this._incrementalFindIgnoreSelection = true; + this.editor.moveSelection(this.textView, index,index+p.length); + this._incrementalFindIgnoreSelection = false; + } else { + this.editor.reportStatus("Incremental find: " + p + " (not found)", true); + } + return true; + } else { + return false; + } + })); + + // Tab actions + this.textView.setAction("tab", dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + if (firstLine !== lastLine) { + var lines = []; + lines.push(""); + for (var i = firstLine; i <= lastLine; i++) { + lines.push(model.getLine(i, true)); + } + this.startUndo(); + var firstLineStart = model.getLineStart(firstLine); + this.textView.setText(lines.join("\t"), firstLineStart, model.getLineEnd(lastLine, true)); + this.textView.setSelection(firstLineStart===selection.start?selection.start:selection.start + 1, selection.end + (lastLine - firstLine + 1)); + this.endUndo(); + return true; + } + return false; + })); + this.textView.setKeyBinding(new orion.textview.KeyBinding(9, false, true), "Unindent Lines"); + this.textView.setAction("Unindent Lines", dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + var lines = []; + for (var i = firstLine; i <= lastLine; i++) { + var line = model.getLine(i, true); + if (line.indexOf("\t") !== 0) { return false; } + lines.push(line.substring(1)); + } + this.startUndo(); + var firstLineStart = model.getLineStart(firstLine); + var lastLineStart = model.getLineStart(lastLine); + this.textView.setText(lines.join(""), firstLineStart, model.getLineEnd(lastLine, true)); + this.textView.setSelection(firstLineStart===selection.start?selection.start:selection.start - 1, selection.end - (lastLine - firstLine + 1) + (selection.end===lastLineStart+1?1:0)); + this.endUndo(); + return true; + })); + + this.textView.setKeyBinding(new orion.textview.KeyBinding(38, false, false, true), "Move Lines Up"); + this.textView.setAction("Move Lines Up", dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var firstLine = model.getLineAtOffset(selection.start); + if (firstLine===0) { + return true; + } + this.startUndo(); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + var isMoveFromLastLine = model.getLineCount()-1===lastLine; + var lineStart = model.getLineStart(firstLine); + var lineEnd = isMoveFromLastLine?model.getCharCount():model.getLineStart(lastLine+1); + if (isMoveFromLastLine) { + // Move delimiter preceding selection to end + var delimiterStart = model.getLineEnd(firstLine-1); + var delimiterEnd = model.getLineEnd(firstLine-1, true); + var delimiter = model.getText(delimiterStart, delimiterEnd); + lineStart = delimiterStart; + model.setText(model.getText(delimiterEnd, lineEnd)+delimiter, lineStart, lineEnd); + } + var text = model.getText(lineStart, lineEnd); + model.setText("", lineStart, lineEnd); + var insertPos = model.getLineStart(firstLine-1); + model.setText(text, insertPos, insertPos); + var selectionEnd = insertPos+text.length-(isMoveFromLastLine?model.getLineDelimiter().length:0); + this.textView.setSelection(insertPos, selectionEnd); + this.endUndo(); + return true; + })); + + this.textView.setKeyBinding(new orion.textview.KeyBinding(40, false, false, true), "Move Lines Down"); + this.textView.setAction("Move Lines Down", dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + if (lastLine===model.getLineCount()-1) { + return true; + } + this.startUndo(); + var isMoveIntoLastLine = lastLine===model.getLineCount()-2; + var lineStart = model.getLineStart(firstLine); + var lineEnd = model.getLineStart(lastLine+1); + if (isMoveIntoLastLine) { + // Move delimiter following selection to front + var delimiterStart = model.getLineStart(lastLine+1)-model.getLineDelimiter().length; + var delimiterEnd = model.getLineStart(lastLine+1); + var delimiter = model.getText(delimiterStart, delimiterEnd); + model.setText(delimiter + model.getText(lineStart, delimiterStart), lineStart, lineEnd); + } + var text = model.getText(lineStart, lineEnd); + var insertPos = (isMoveIntoLastLine?model.getCharCount():model.getLineStart(lastLine+2))-(lineEnd-lineStart); + model.setText("", lineStart, lineEnd); + model.setText(text, insertPos, insertPos); + var selStart = insertPos+(isMoveIntoLastLine?model.getLineDelimiter().length:0); + var selEnd = insertPos+text.length; + this.textView.setSelection(selStart, selEnd); + this.endUndo(); + return true; + })); + + this.textView.setKeyBinding(new orion.textview.KeyBinding(38, true, false, true), "Copy Lines Up"); + this.textView.setAction("Copy Lines Up", dojo.hitch(this, function() { + this.startUndo(); + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var delimiter = model.getLineDelimiter(); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + var lineStart = model.getLineStart(firstLine); + var isCopyFromLastLine = model.getLineCount()-1===lastLine; + var lineEnd = isCopyFromLastLine?model.getCharCount():model.getLineStart(lastLine+1); + var text = model.getText(lineStart, lineEnd)+(isCopyFromLastLine?delimiter:""); //+ delimiter; + //var insertPos = model.getLineStart(firstLine - 1); + var insertPos = lineStart; + model.setText(text, insertPos, insertPos); + this.textView.setSelection(insertPos, insertPos+text.length-(isCopyFromLastLine?delimiter.length:0)); + this.endUndo(); + return true; + })); + + this.textView.setKeyBinding(new orion.textview.KeyBinding(40, true, false, true), "Copy Lines Down"); + this.textView.setAction("Copy Lines Down", dojo.hitch(this, function() { + this.startUndo(); + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var delimiter = model.getLineDelimiter(); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + var lineStart = model.getLineStart(firstLine); + var isCopyFromLastLine = model.getLineCount()-1===lastLine; + var lineEnd = isCopyFromLastLine?model.getCharCount():model.getLineStart(lastLine+1); + var text = (isCopyFromLastLine?delimiter:"")+model.getText(lineStart, lineEnd); + //model.setText("", lineStart, lineEnd); + //var insertPos = model.getLineStart(firstLine - 1); + var insertPos = lineEnd; + model.setText(text, insertPos, insertPos); + this.textView.setSelection(insertPos+(isCopyFromLastLine?delimiter.length:0), insertPos+text.length); + this.endUndo(); + return true; + })); + + this.textView.setKeyBinding(new orion.textview.KeyBinding('d', true, false, false), "Delete Selected Lines"); + this.textView.setAction("Delete Selected Lines", dojo.hitch(this, function() { + this.startUndo(); + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + var lineStart = model.getLineStart(firstLine); + var lineEnd = model.getLineCount()-1===lastLine?model.getCharCount():model.getLineStart(lastLine+1); + model.setText("", lineStart, lineEnd); + this.endUndo(); + return true; + })); + + // Go To Line action + this.textView.setKeyBinding(new orion.textview.KeyBinding("l", true), "Goto Line..."); + this.textView.setAction("Goto Line...", dojo.hitch(this, function() { + var line = this.textView.getModel().getLineAtOffset(this.textView.getCaretOffset()); + line = prompt("Go to line:", line + 1); + if (line) { + line = parseInt(line, 10); + this.editor.onGotoLine(line-1, 0); + } + return true; + })); + + }, + + toggleIncrementalFind: function() { + this._incrementalFindActive = !this._incrementalFindActive; + if (this._incrementalFindActive) { + this.editor.reportStatus("Incremental find: " + this._incrementalFindPrefix); + this.textView.addEventListener("Verify", this, this._incrementalFindListener.onVerify); + this.textView.addEventListener("Selection", this, this._incrementalFindListener.onSelection); + } else { + this._incrementalFindPrefix = ""; + this.editor.reportStatus(""); + this.textView.removeEventListener("Verify", this, this._incrementalFindListener.onVerify); + this.textView.removeEventListener("Selection", this, this._incrementalFindListener.onSelection); + this.textView.setCaretOffset(this.textView.getCaretOffset()); + } + }, + + startUndo: function() { + if (this.undoStack) { + this.undoStack.startCompoundChange(); + } + }, + + endUndo: function() { + if (this.undoStack) { + this.undoStack.endCompoundChange(); + } + }, + + cancel: function() { + this.toggleIncrementalFind(); + }, + + isActive: function() { + return this._incrementalFindActive; + }, + + lineUp: function() { + var index; + if (this._incrementalFindActive) { + var p = this._incrementalFindPrefix; + var start = this.textView.getCaretOffset() - p.length - 1; + if (this._incrementalFindSuccess === false) { + start = this.textView.getModel().getCharCount() - 1; + } + index = this.textView.getText().lastIndexOf(p, start); + if (index !== -1) { + this._incrementalFindSuccess = true; + this._incrementalFindIgnoreSelection = true; + this.editor.moveSelection(this.textView, index,index+p.length); + this._incrementalFindIgnoreSelection = false; + } else { + this.editor.reportStatus("Incremental find: " + p + " (not found)", true); + this._incrementalFindSuccess = false; + } + return true; + } + return false; + }, + lineDown: function() { + var index; + if (this._incrementalFindActive) { + var p = this._incrementalFindPrefix; + if (p.length===0) { + return; + } + var start = this.textView.getSelection().start + 1; + if (this._incrementalFindSuccess === false) { + start = 0; + } + index = this.textView.getText().indexOf(p, start); + if (index !== -1) { + this._incrementalFindSuccess = true; + this._incrementalFindIgnoreSelection = true; + this.editor.moveSelection(this.textView, index, index+p.length); + this._incrementalFindIgnoreSelection = false; + this.editor.reportStatus("Incremental find: " + p); + } else { + this.editor.reportStatus("Incremental find: " + p + " (not found)", true); + this._incrementalFindSuccess = false; + } + return true; + } + return false; + }, + enter: function() { + return false; + } + }; + return TextActions; +}()); + +orion.editor.SourceCodeActions = (function() { + function SourceCodeActions(editor, undoStack, contentAssist) { + this.editor = editor; + this.textView = editor.getTextView(); + this.undoStack = undoStack; + this.contentAssist = contentAssist; + this.init(); + } + SourceCodeActions.prototype = { + startUndo: function() { + if (this.undoStack) { + this.undoStack.startCompoundChange(); + } + }, + + endUndo: function() { + if (this.undoStack) { + this.undoStack.endCompoundChange(); + } + }, + init: function() { + + // Block comment operations + this.textView.setKeyBinding(new orion.textview.KeyBinding(191, true), "Toggle Line Comment"); + this.textView.setAction("Toggle Line Comment", dojo.hitch(this, function() { + this.startUndo(); + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end>selection.start?selection.end - 1:selection.end); + var uncomment = true; + var lineText; + for (var i = firstLine; i <= lastLine && uncomment; i++) { + lineText = this.textView.getModel().getLine(i); + var index = lineText.indexOf("//"); + if (index === -1) { + uncomment = false; + } else { + if (index !== 0) { + var j; + for (j=0; j<index; j++) { + var c = lineText.charCodeAt(j); + if (!(c === 32 || c === 9)) { + break; + } + } + uncomment = j === index; + } + } + } + var k, lines = []; + var firstLineStart = model.getLineStart(firstLine); + if (uncomment) { + var lastLineStart = model.getLineStart(lastLine); + for (k = firstLine; k <= lastLine; k++) { + var line = model.getLine(k, true); + var commentIndex = lineText.indexOf("//"); + lines.push(line.substring(0, commentIndex) + line.substring(commentIndex + 2)); + } + this.textView.setText(lines.join(""), firstLineStart, model.getLineEnd(lastLine, true)); + this.textView.setSelection(firstLineStart===selection.start?selection.start:selection.start - 2, selection.end - (2 * (lastLine - firstLine + 1)) + (selection.end===lastLineStart+1?2:0)); + } else { + lines.push(""); + for (k = firstLine; k <= lastLine; k++) { + lines.push(model.getLine(k, true)); + } + this.textView.setText(lines.join("//"), firstLineStart, model.getLineEnd(lastLine, true)); + this.textView.setSelection(firstLineStart===selection.start?selection.start:selection.start + 2, selection.end + (2 * (lastLine - firstLine + 1))); + } + this.endUndo(); + return true; + })); + + function findEnclosingComment(model, start, end) { + var open = "/*", close = "*/"; + var firstLine = model.getLineAtOffset(start); + var lastLine = model.getLineAtOffset(end); + var i, line, extent, openPos, closePos; + var commentStart, commentEnd; + for (i=firstLine; i >= 0; i--) { + line = model.getLine(i); + extent = (i === firstLine) ? start - model.getLineStart(firstLine) : line.length; + openPos = line.lastIndexOf(open, extent); + closePos = line.lastIndexOf(close, extent); + if (closePos > openPos) { + break; // not inside a comment + } else if (openPos !== -1) { + commentStart = model.getLineStart(i) + openPos; + break; + } + } + for (i=lastLine; i < model.getLineCount(); i++) { + line = model.getLine(i); + extent = (i === lastLine) ? end - model.getLineStart(lastLine) : 0; + openPos = line.indexOf(open, extent); + closePos = line.indexOf(close, extent); + if (openPos !== -1 && openPos < closePos) { + break; + } else if (closePos !== -1) { + commentEnd = model.getLineStart(i) + closePos; + break; + } + } + return {commentStart: commentStart, commentEnd: commentEnd}; + } + + this.textView.setKeyBinding(new orion.textview.KeyBinding(191, true, true), "Add Block Comment"); + this.textView.setAction("Add Block Comment", dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var open = "/*", close = "*/", commentTags = new RegExp("/\\*" + "|" + "\\*/", "g"); + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end); + + var result = findEnclosingComment(model, selection.start, selection.end); + if (result.commentStart !== undefined && result.commentEnd !== undefined) { + return true; // Already in a comment + } + + var text = model.getText(selection.start, selection.end); + if (text.length === 0) { return true; } + + var oldLength = text.length; + text = text.replace(commentTags, ""); + var newLength = text.length; + + this.startUndo(); + model.setText(open + text + close, selection.start, selection.end); + this.textView.setSelection(selection.start + open.length, selection.end + open.length + (newLength-oldLength)); + this.endUndo(); + return true; + })); + + this.textView.setKeyBinding(new orion.textview.KeyBinding(220, true, true), "Remove Block Comment"); + this.textView.setAction("Remove Block Comment", dojo.hitch(this, function() { + var selection = this.textView.getSelection(); + var model = this.textView.getModel(); + var open = "/*", close = "*/"; + var firstLine = model.getLineAtOffset(selection.start); + var lastLine = model.getLineAtOffset(selection.end); + + // Try to shrink selection to a comment block + var selectedText = model.getText(selection.start, selection.end); + var newStart, newEnd; + var i; + for(i=0; i < selectedText.length; i++) { + if (selectedText.substring(i, i + open.length) === open) { + newStart = selection.start + i; + break; + } + } + for (; i < selectedText.length; i++) { + if (selectedText.substring(i, i + close.length) === close) { + newEnd = selection.start + i; + break; + } + } + + this.startUndo(); + if (newStart !== undefined && newEnd !== undefined) { + model.setText(model.getText(newStart + open.length, newEnd), newStart, newEnd + close.length); + this.textView.setSelection(newStart, newEnd); + } else { + // Otherwise find enclosing comment block + var result = findEnclosingComment(model, selection.start, selection.end); + if (result.commentStart === undefined || result.commentEnd === undefined) { + return true; + } + + var text = model.getText(result.commentStart + open.length, result.commentEnd); + model.setText(text, result.commentStart, result.commentEnd + close.length); + this.textView.setSelection(selection.start - open.length, selection.end - close.length); + } + this.endUndo(); + return true; + })); + + //Auto indent + this.textView.setAction("enter", dojo.hitch(this, function() { + if (this.contentAssist && this.contentAssist.isActive()) { + return this.contentAssist.enter(); + } + var selection = this.textView.getSelection(); + if (selection.start === selection.end) { + var model = this.textView.getModel(); + var lineIndex = model.getLineAtOffset(selection.start); + var lineText = model.getLine(lineIndex); + var lineStart = model.getLineStart(lineIndex); + var index = 0, end = selection.start - lineStart, c; + while (index < end && ((c = lineText.charCodeAt(index)) === 32 || c === 9)) { index++; } + if (index > 0) { + var prefix = lineText.substring(0, index); + index = end; + while (index < lineText.length && ((c = lineText.charCodeAt(index++)) === 32 || c === 9)) { selection.end++; } + this.textView.setText(model.getLineDelimiter() + prefix, selection.start, selection.end); + return true; + } + } + return false; + })); + }, + + cancel: function() { + return false; + }, + isActive: function() { + return false; // we have no modal interactions + }, + lineUp: function() { + return false; + }, + lineDown: function() { + return false; + }, + enter: function() { + return false; + } + }; + return SourceCodeActions; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define(['dojo', 'orion/textview/undoStack', 'orion/textview/keyBinding', 'orion/textview/rulers'], function() { + return orion.editor; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/htmlGrammar.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/htmlGrammar.js new file mode 100644 index 00000000..bcbc5f51 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/htmlGrammar.js @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*jslint */ +/*global dojo orion:true*/ + +var orion = orion || {}; + +orion.editor = orion.editor || {}; + +/** + * Uses a grammar to provide some very rough syntax highlighting for HTML.<p> + * @class orion.syntax.HtmlGrammar + */ +orion.editor.HtmlGrammar = (function() { + var _fileTypes = [ "html", "htm" ]; + return { + /** + * What kind of highlight provider we are. + * @public + * @type "grammar"|"parser" + */ + type: "grammar", + + /** + * The file extensions that we provide rules for. + * @public + * @type String[] + */ + fileTypes: _fileTypes, + + /** + * Object containing the grammar rules. + * @public + * @type JSONObject + */ + grammar: { + "comment": "HTML syntax rules", + "name": "HTML", + "fileTypes": _fileTypes, + "scopeName": "source.html", + "uuid": "3B5C76FB-EBB5-D930-F40C-047D082CE99B", + "patterns": [ + // TODO unicode? + { + "match": "<!(doctype|DOCTYPE)[^>]+>", + "name": "entity.name.tag.doctype.html" + }, + { + "begin": "<!--", + "end": "-->", + "beginCaptures": { + "0": { "name": "punctuation.definition.comment.html" } + }, + "endCaptures": { + "0": { "name": "punctuation.definition.comment.html" } + }, + "patterns": [ + { + // For testing nested subpatterns + "match": "--", + "name": "invalid.illegal.badcomment.html" + } + ], + "contentName": "comment.block.html" + }, + { // startDelimiter + tagName + "match": "<[A-Za-z0-9_\\-:]+(?= ?)", + "name": "entity.name.tag.html" + }, + { "include": "#attrName" }, + { "include": "#qString" }, + { "include": "#qqString" }, + // TODO attrName, qString, qqString should be applied first while inside a tag + { // startDelimiter + slash + tagName + endDelimiter + "match": "</[A-Za-z0-9_\\-:]+>", + "name": "entity.name.tag.html" + }, + { // end delimiter of open tag + "match": ">", + "name": "entity.name.tag.html" + } ], + "repository": { + "attrName": { // attribute name + "match": "[A-Za-z\\-:]+(?=\\s*=\\s*['\"])", + "name": "entity.other.attribute.name.html" + }, + "qqString": { // double quoted string + "match": "(\")[^\"]+(\")", + "name": "string.quoted.double.html" + }, + "qString": { // single quoted string + "match": "(')[^']+(\')", + "name": "string.quoted.single.html" + } + } + } + }; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return orion.editor; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/textMateStyler.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/textMateStyler.js new file mode 100644 index 00000000..6baa8826 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/textMateStyler.js @@ -0,0 +1,1322 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*jslint regexp:false laxbreak:true*/ +/*global define dojo window*/ + +var orion = orion || {}; +orion.editor = orion.editor || {}; + +/** + * A styler that does nothing, but can be extended by concrete stylers. Extenders can call + * {@link orion.editor.AbstractStyler.extend} and provide their own {@link #_onSelection}, + * {@link #_onModelChanged}, {@link #_onDestroy} and {@link #_onLineStyle} methods. + * @class orion.editor.AbstractStyler + */ +orion.editor.AbstractStyler = (function() { + /** @inner */ + function AbstractStyler() { + } + AbstractStyler.prototype = /** @lends orion.editor.AbstractStyler.prototype */ { + /** + * Initializes this styler with a textView. Extenders <b>must</b> call this from their constructor. + * @param {orion.textview.TextView} textView + */ + initialize: function(textView) { + this.textView = textView; + + textView.addEventListener("Selection", this, this._onSelection); + textView.addEventListener("ModelChanged", this, this._onModelChanged); + textView.addEventListener("Destroy", this, this._onDestroy); + textView.addEventListener("LineStyle", this, this._onLineStyle); + textView.redrawLines(); + }, + + /** + * Destroys this styler and removes all listeners. Called by the editor. + */ + destroy: function() { + if (this.textView) { + this.textView.removeEventListener("Selection", this, this._onSelection); + this.textView.removeEventListener("ModelChanged", this, this._onModelChanged); + this.textView.removeEventListener("Destroy", this, this._onDestroy); + this.textView.removeEventListener("LineStyle", this, this._onLineStyle); + this.textView = null; + } + }, + + /** To be overridden by subclass. + * @public + */ + _onSelection: function(/**eclipse.SelectionEvent*/ e) {}, + + /** To be overridden by subclass. + * @public + */ + _onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) {}, + + /** To be overridden by subclass. + * @public + */ + _onDestroy: function(/**eclipse.DestroyEvent*/ e) {}, + + /** To be overridden by subclass. + * @public + */ + _onLineStyle: function(/**eclipse.LineStyleEvent*/ e) {} + }; + + return AbstractStyler; +}()); + +/** + * Helper for extending AbstractStyler. + * @methodOf orion.editor.AbstractStyler + * @static + * @param {Function} subCtor The constructor function for the subclass. + * @param {Object} [proto] Object to be mixed into the subclass's prototype. This object can contain your + * implementation of _onSelection, _onModelChanged, etc. + * @see orion.editor.TextMateStyler for example usage. + */ +orion.editor.AbstractStyler.extend = function(subCtor, proto) { + if (typeof(subCtor) !== "function") { throw new Error("Function expected"); } + subCtor.prototype = new orion.editor.AbstractStyler(); + subCtor.constructor = subCtor; + for (var p in proto) { + if (proto.hasOwnProperty(p)) { subCtor.prototype[p] = proto[p]; } + } +}; + +orion.editor.RegexUtil = { + // Rules to detect some unsupported Oniguruma features + unsupported: [ + {regex: /\(\?[ims\-]:/, func: function(match) { return "option on/off for subexp"; }}, + {regex: /\(\?<([=!])/, func: function(match) { return (match[1] === "=") ? "lookbehind" : "negative lookbehind"; }}, + {regex: /\(\?>/, func: function(match) { return "atomic group"; }} + ], + + /** + * @param {String} str String giving a regular expression pattern from a TextMate grammar. + * @param {String} [flags] [ismg]+ + * @returns {RegExp} + */ + toRegExp: function(str) { + function fail(feature, match) { + throw new Error("Unsupported regex feature \"" + feature + "\": \"" + match[0] + "\" at index: " + + match.index + " in " + match.input); + } + function getMatchingCloseParen(str, start) { + var depth = 0, + len = str.length, + xStop = -1; + for (var i=start; i < len && xStop === -1; i++) { + switch (str[i]) { + case "\\": + i += 1; // skip next char + break; + case "(": + depth++; + break; + case ")": + depth--; + if (depth === 0) { + xStop = i; + } + break; + } + } + return xStop; + } + // Turns an extended regex into a normal one + function normalize(/**String*/ str) { + var result = ""; + var insideCharacterClass = false; + var len = str.length; + for (var i=0; i < len; ) { + var chr = str[i]; + if (!insideCharacterClass && chr === "#") { + // skip to eol + while (i < len && chr !== "\r" && chr !== "\n") { + chr = str[++i]; + } + } else if (!insideCharacterClass && /\s/.test(chr)) { + // skip whitespace + while (i < len && /\s/.test(chr)) { + chr = str[++i]; + } + } else if (chr === "\\") { + result += chr; + if (!/\s/.test(str[i+1])) { + result += str[i+1]; + i += 1; + } + i += 1; + } else if (chr === "[") { + insideCharacterClass = true; + result += chr; + i += 1; + } else if (chr === "]") { + insideCharacterClass = false; + result += chr; + i += 1; + } else { + result += chr; + i += 1; + } + } + return result; + } + + var flags = ""; + var i; + + // Check for unsupported syntax + for (i=0; i < this.unsupported.length; i++) { + var match; + if ((match = this.unsupported[i].regex.exec(str))) { + fail(this.unsupported[i].func(match)); + } + } + + // Deal with "x" flag (whitespace/comments) + if (str.substring(0, 4) === "(?x)") { + // Leading (?x) term (means "x" flag applies to entire regex) + str = normalize(str.substring(4)); + } else if (str.substring(0, 4) === "(?x:") { + // Regex wrapped in a (?x: ...) -- again "x" applies to entire regex + var xStop = getMatchingCloseParen(str, 0); + if (xStop < str.length-1) { + throw new Error("Only a (?x:) group that encloses the entire regex is supported: " + str); + } + str = normalize(str.substring(4, xStop)); + } + // TODO: tolerate /(?iSubExp)/ -- eg. in PHP grammar (trickier) + return new RegExp(str, flags); + }, + + hasBackReference: function(/**RegExp*/ regex) { + return (/\\\d+/).test(regex.source); + }, + + /** @returns {RegExp} A regex made by substituting any backreferences in <tt>regex</tt> for the value of the property + * in <tt>sub</tt> with the same name as the backreferenced group number. */ + getSubstitutedRegex: function(/**RegExp*/ regex, /**Object*/ sub, /**Boolean*/ escape) { + escape = (typeof escape === "undefined") ? true : false; + var exploded = regex.source.split(/(\\\d+)/g); + var array = []; + for (var i=0; i < exploded.length; i++) { + var term = exploded[i]; + var backrefMatch = /\\(\d+)/.exec(term); + if (backrefMatch) { + var text = sub[backrefMatch[1]] || ""; + array.push(escape ? orion.editor.RegexUtil.escapeRegex(text) : text); + } else { + array.push(term); + } + } + return new RegExp(array.join("")); + }, + + /** @returns {String} The input string with regex special characters escaped. */ + escapeRegex: function(/**String*/ str) { + return str.replace(/([\\$\^*\/+?\.\(\)|{}\[\]])/g, "\\$&"); + }, + + /** + * Builds a version of <tt>regex</tt> with every non-capturing term converted into a capturing group. This is a workaround + * for JavaScript's lack of API to get the index at which a matched group begins in the input string.<p> + * Using the "groupified" regex, we can sum the lengths of matches from <i>consuming groups</i> 1..n-1 to obtain the + * starting index of group n. (A consuming group is a capturing group that is not inside a lookahead assertion).</p> + * Example: groupify(/(a+)x+(b+)/) === /(a+)(x+)(b+)/<br /> + * Example: groupify(/(?:x+(a+))b+/) === /(?:(x+)(a+))(b+)/ + * @param {RegExp} regex The regex to groupify. + * @param {Object} [backRefOld2NewMap] Optional. If provided, the backreference numbers in regex will be updated using the + * properties of this object rather than the new group numbers of regex itself. + * <ul><li>[0] {RegExp} The groupified version of the input regex.</li> + * <li>[1] {Object} A map containing old-group to new-group info. Each property is a capturing group number of <tt>regex</tt> + * and its value is the corresponding capturing group number of [0].</li> + * <li>[2] {Object} A map indicating which capturing groups of [0] are also consuming groups. If a group number is found + * as a property in this object, then it's a consuming group.</li></ul> + */ + groupify: function(regex, backRefOld2NewMap) { + var NON_CAPTURING = 1, + CAPTURING = 2, + LOOKAHEAD = 3, + NEW_CAPTURING = 4; + var src = regex.source, + len = src.length; + var groups = [], + lookaheadDepth = 0, + newGroups = [], + oldGroupNumber = 1, + newGroupNumber = 1; + var result = [], + old2New = {}, + consuming = {}; + for (var i=0; i < len; i++) { + var curGroup = groups[groups.length-1]; + var chr = src[i]; + switch (chr) { + case "(": + // If we're in new capturing group, close it since ( signals end-of-term + if (curGroup === NEW_CAPTURING) { + groups.pop(); + result.push(")"); + newGroups[newGroups.length-1].end = i; + } + var peek2 = (i + 2 < len) ? (src[i+1] + "" + src[i+2]) : null; + if (peek2 === "?:" || peek2 === "?=" || peek2 === "?!") { + // Found non-capturing group or lookahead assertion. Note that we preserve non-capturing groups + // as such, but any term inside them will become a new capturing group (unless it happens to + // also be inside a lookahead). + var groupType; + if (peek2 === "?:") { + groupType = NON_CAPTURING; + } else { + groupType = LOOKAHEAD; + lookaheadDepth++; + } + groups.push(groupType); + newGroups.push({ start: i, end: -1, type: groupType /*non capturing*/ }); + result.push(chr); + result.push(peek2); + i += peek2.length; + } else { + groups.push(CAPTURING); + newGroups.push({ start: i, end: -1, type: CAPTURING, oldNum: oldGroupNumber, num: newGroupNumber }); + result.push(chr); + if (lookaheadDepth === 0) { + consuming[newGroupNumber] = null; + } + old2New[oldGroupNumber] = newGroupNumber; + oldGroupNumber++; + newGroupNumber++; + } + break; + case ")": + var group = groups.pop(); + if (group === LOOKAHEAD) { lookaheadDepth--; } + newGroups[newGroups.length-1].end = i; + result.push(chr); + break; + case "*": + case "+": + case "?": + case "}": + // Unary operator. If it's being applied to a capturing group, we need to add a new capturing group + // enclosing the pair + var op = chr; + var prev = src[i-1], + prevIndex = i-1; + if (chr === "}") { + for (var j=i-1; src[j] !== "{" && j >= 0; j--) {} + prev = src[j-1]; + prevIndex = j-1; + op = src.substring(j, i+1); + } + var lastGroup = newGroups[newGroups.length-1]; + if (prev === ")" && (lastGroup.type === CAPTURING || lastGroup.type === NEW_CAPTURING)) { + // Shove in the new group's (, increment num/start in from [lastGroup.start .. end] + result.splice(lastGroup.start, 0, "("); + result.push(op); + result.push(")"); + var newGroup = { start: lastGroup.start, end: result.length-1, type: NEW_CAPTURING, num: lastGroup.num }; + for (var k=0; k < newGroups.length; k++) { + group = newGroups[k]; + if (group.type === CAPTURING || group.type === NEW_CAPTURING) { + if (group.start >= lastGroup.start && group.end <= prevIndex) { + group.start += 1; + group.end += 1; + group.num = group.num + 1; + if (group.type === CAPTURING) { + old2New[group.oldNum] = group.num; + } + } + } + } + newGroups.push(newGroup); + newGroupNumber++; + break; + } else { + // Fallthrough to default + } + default: + if (chr !== "|" && curGroup !== CAPTURING && curGroup !== NEW_CAPTURING) { + // Not in a capturing group, so make a new one to hold this term. + // Perf improvement: don't create the new group if we're inside a lookahead, since we don't + // care about them (nothing inside a lookahead actually consumes input so we don't need it) + if (lookaheadDepth === 0) { + groups.push(NEW_CAPTURING); + newGroups.push({ start: i, end: -1, type: NEW_CAPTURING, num: newGroupNumber }); + result.push("("); + consuming[newGroupNumber] = null; + newGroupNumber++; + } + } + result.push(chr); + if (chr === "\\") { + var peek = src[i+1]; + // Eat next so following iteration doesn't think it's a real special character + result.push(peek); + i += 1; + } + break; + } + } + while (groups.length) { + // Close any remaining new capturing groups + groups.pop(); + result.push(")"); + } + var newRegex = new RegExp(result.join("")); + + // Update backreferences so they refer to the new group numbers. Use backRefOld2NewMap if provided + var subst = {}; + backRefOld2NewMap = backRefOld2NewMap || old2New; + for (var prop in backRefOld2NewMap) { + if (backRefOld2NewMap.hasOwnProperty(prop)) { + subst[prop] = "\\" + backRefOld2NewMap[prop]; + } + } + newRegex = this.getSubstitutedRegex(newRegex, subst, false); + + return [newRegex, old2New, consuming]; + }, + + /** @returns {Boolean} True if the captures object assigns scope to a matching group other than "0". */ + complexCaptures: function(capturesObj) { + if (!capturesObj) { return false; } + for (var prop in capturesObj) { + if (capturesObj.hasOwnProperty(prop)) { + if (prop !== "0") { + return true; + } + } + } + return false; + } +}; + +/** + * A styler that knows how to apply a limited subset of the TextMate grammar format to style a line.<p> + * + * <h4>Styling from a grammar:</h4> + * Each scope name given in the grammar is converted to an array of CSS class names. For example + * a region of text with scope <tt>keyword.control.php</tt> will be assigned the CSS classes + * <pre>keyword, keyword-control, keyword-control-php</pre> + * + * A CSS file can give rules matching any of these class names to provide generic or more specific styling. + * For example, <pre>.keyword { font-color: blue; }</pre> colors all keywords blue, while + * <pre>.keyword-control-php { font-weight: bold; }</pre> bolds only PHP control keywords. + * + * This is useful when using grammars that adhere to TextMate's + * <a href="http://manual.macromates.com/en/language_grammars.html#naming_conventions">scope name conventions</a>, + * as a single CSS rule can provide consistent styling to similar constructs across different languages.<p> + * + * <h4>Supported top-level grammar features:</h4> + * <ul><li><tt>fileTypes, patterns, repository</tt> (but see below) are supported.</li> + * <li><tt>scopeName, firstLineMatch, foldingStartMarker, foldingStopMarker</tt> are <b>not</b> supported.</li> + * </ul> + * + * <p>TODO update this section</p> + * <del> + * <h4>Supported grammar rule features:</h4> + * <ul><li><tt>match</tt> patterns are supported.</li> + * <li><tt>name</tt> scope is supported.</li> + * <li><tt>captures</tt> is <b>not</b> supported. Any scopes given inside a <tt>captures</tt> object are not applied.</li> + * <li><tt>begin/end</tt> patterns are <b>not</b> supported and are ignored, along with their subrules. Consequently, + * matched constructs may <b>not</b> span multiple lines.</li> + * <li><tt>contentName, beginCaptures, endCaptures, applyEndPatternLast</tt> are <b>not</b> supported.</li> + * <li><tt>include</tt> is supported, but only when it references a rule in the current grammar's <tt>repository</tt>. + * Including <tt>$self</tt>, <tt>$base</tt>, or <tt>rule.from.another.grammar</tt> is <b>not</b> supported.</li> + * <li>The <tt>(?x)</tt> option ("extended" regex format) is supported, but only when it appears at the beginning of a regex pattern.</li> + * <li>Matching is done using native JavaScript {@link RegExp}s. As a result, many Oniguruma features are <b>not</b> supported. + * Unsupported features include: + * <ul><li>Named captures</li> + * <li>Setting flags inside groups (eg. <tt>(?i:a)b</tt>)</li> + * <li>Lookbehind and negative lookbehind</li> + * <li>Subexpression call</li> + * <li>etc.</li> + * </ul> + * </li> + * </ul> + * </del> + * + * @class orion.editor.TextMateStyler + * @extends orion.editor.AbstractStyler + * @param {orion.textview.TextView} textView The textView. + * @param {Object} grammar The TextMate grammar as a JavaScript object. You can use a plist-to-JavaScript conversion tool + * to produce this object. Note that some features of TextMate grammars are not supported. + */ +orion.editor.TextMateStyler = (function() { + /** @inner */ + function TextMateStyler(textView, grammar) { + this.initialize(textView); + // Copy the grammar since we'll mutate it + this.grammar = this.copy(grammar); + this._styles = {}; /* key: {String} scopeName, value: {String[]} cssClassNames */ + this._tree = null; + + this.preprocess(); + } + orion.editor.AbstractStyler.extend(TextMateStyler, /** @lends orion.editor.TextMateStyler.prototype */ { + copy: function(obj) { + return JSON.parse(JSON.stringify(obj)); + }, + preprocess: function() { + var stack = [this.grammar]; + for (; stack.length !== 0; ) { + var rule = stack.pop(); + if (rule._resolvedRule && rule._typedRule) { + continue; + } +// console.debug("Process " + (rule.include || rule.name)); + + // Look up include'd rule, create typed *Rule instance + rule._resolvedRule = this._resolve(rule); + rule._typedRule = this._createTypedRule(rule); + + // Convert the scope names to styles and cache them for later + this.addStyles(rule.name); + this.addStyles(rule.contentName); + this.addStylesForCaptures(rule.captures); + this.addStylesForCaptures(rule.beginCaptures); + this.addStylesForCaptures(rule.endCaptures); + + if (rule._resolvedRule !== rule) { + // Add include target + stack.push(rule._resolvedRule); + } + if (rule.patterns) { + // Add subrules + for (var i=0; i < rule.patterns.length; i++) { + stack.push(rule.patterns[i]); + } + } + } + }, + + /** + * Adds eclipse.Style objects for scope to our _styles cache. + * @param {String} scope A scope name, like "constant.character.php". + */ + addStyles: function(scope) { + if (scope && !this._styles[scope]) { + this._styles[scope] = dojo.map(scope.split("."), + function(segment, i, segments) { + return segments.slice(0, i+1).join("-"); + }); +// console.debug("add style for " + scope + " = [" + this._styles[scope].join(", ") + "]"); + } + }, + addStylesForCaptures: function(/**Object*/ captures) { + for (var prop in captures) { + if (captures.hasOwnProperty(prop)) { + var scope = captures[prop].name; + this.addStyles(scope); + } + } + }, + /** + * A rule that contains subrules ("patterns" in TextMate parlance) but has no "begin" or "end". + * Also handles top level of grammar. + * @private + */ + ContainerRule: (function() { + function ContainerRule(/**Object*/ rule) { + this.rule = rule; + this.subrules = rule.patterns; + } + ContainerRule.prototype.valueOf = function() { return "aa"; }; + return ContainerRule; + }()), + /** + * A rule that is delimited by "begin" and "end" matches, which may be separated by any number of + * lines. This type of rule may contain subrules, which apply only inside the begin .. end region. + * @private + */ + BeginEndRule: (function() { + function BeginEndRule(/**Object*/ rule) { + this.rule = rule; + // TODO: the TextMate blog claims that "end" is optional. + this.beginRegex = orion.editor.RegexUtil.toRegExp(rule.begin); + this.endRegex = orion.editor.RegexUtil.toRegExp(rule.end); + this.subrules = rule.patterns || []; + + this.endRegexHasBackRef = orion.editor.RegexUtil.hasBackReference(this.endRegex); + + // Deal with non-0 captures + var complexCaptures = orion.editor.RegexUtil.complexCaptures(rule.captures); + var complexBeginEnd = orion.editor.RegexUtil.complexCaptures(rule.beginCaptures) || orion.editor.RegexUtil.complexCaptures(rule.endCaptures); + this.isComplex = complexCaptures || complexBeginEnd; + if (this.isComplex) { + var bg = orion.editor.RegexUtil.groupify(this.beginRegex); + this.beginRegex = bg[0]; + this.beginOld2New = bg[1]; + this.beginConsuming = bg[2]; + + var eg = orion.editor.RegexUtil.groupify(this.endRegex, this.beginOld2New /*Update end's backrefs to begin's new group #s*/); + this.endRegex = eg[0]; + this.endOld2New = eg[1]; + this.endConsuming = eg[2]; + } + } + BeginEndRule.prototype.valueOf = function() { return this.beginRegex; }; + return BeginEndRule; + }()), + /** + * A rule with a "match" pattern. + * @private + */ + MatchRule: (function() { + function MatchRule(/**Object*/ rule) { + this.rule = rule; + this.matchRegex = orion.editor.RegexUtil.toRegExp(rule.match); + this.isComplex = orion.editor.RegexUtil.complexCaptures(rule.captures); + if (this.isComplex) { + var mg = orion.editor.RegexUtil.groupify(this.matchRegex); + this.matchRegex = mg[0]; + this.matchOld2New = mg[1]; + this.matchConsuming = mg[2]; + } + } + MatchRule.prototype.valueOf = function() { return this.matchRegex; }; + return MatchRule; + }()), + /** + * @param {Object} rule A rule from the grammar. + * @returns {MatchRule|BeginEndRule|ContainerRule} + */ + _createTypedRule: function(rule) { + if (rule.match) { + return new this.MatchRule(rule); + } else if (rule.begin) { + return new this.BeginEndRule(rule); + } else { + return new this.ContainerRule(rule); + } + }, + /** + * Resolves a rule from the grammar (which may be an include) into the real rule that it points to. + */ + _resolve: function(rule) { + var resolved = rule; + if (rule.include) { + if (rule.begin || rule.end || rule.match) { + throw new Error("Unexpected regex pattern in \"include\" rule " + rule.include); + } + var name = rule.include; + if (name.charAt(0) === "#") { + resolved = this.grammar.repository && this.grammar.repository[name.substring(1)]; + if (!resolved) { throw new Error("Couldn't find included rule " + name + " in grammar repository"); } + } else if (name === "$self") { + resolved = this.grammar; + } else if (name === "$base") { + // $base is only relevant when including rules from foreign grammars + throw new Error("Include \"$base\" is not supported"); + } else { + throw new Error("Include external rule \"" + name + "\" is not supported"); + } + } + return resolved; + }, + ContainerNode: (function() { + function ContainerNode(parent, rule) { + this.parent = parent; + this.rule = rule; + this.children = []; + + this.start = null; + this.end = null; + } + ContainerNode.prototype.addChild = function(child) { + this.children.push(child); + }; + ContainerNode.prototype.valueOf = function() { + var r = this.rule; + return "ContainerNode { " + (r.include || "") + " " + (r.name || "") + (r.comment || "") + "}"; + }; + return ContainerNode; + }()), + BeginEndNode: (function() { + function BeginEndNode(parent, rule, beginMatch) { + this.parent = parent; + this.rule = rule; + this.children = []; + + this.setStart(beginMatch); + this.end = null; // will be set eventually during parsing (may be EOF) + this.endMatch = null; // may remain null if we never match our "end" pattern + + // Build a new regex if the "end" regex has backrefs since they refer to matched groups of beginMatch + if (rule.endRegexHasBackRef) { + this.endRegexSubstituted = orion.editor.RegexUtil.getSubstitutedRegex(rule.endRegex, beginMatch); + } else { + this.endRegexSubstituted = null; + } + } + BeginEndNode.prototype.addChild = function(child) { + this.children.push(child); + }; + /** @return {Number} This node's index in its parent's "children" list */ + BeginEndNode.prototype.getIndexInParent = function(node) { + return this.parent ? this.parent.children.indexOf(this) : -1; + }; + /** @param {RegExp.match} beginMatch */ + BeginEndNode.prototype.setStart = function(beginMatch) { + this.start = beginMatch.index; + this.beginMatch = beginMatch; + }; + /** @param {RegExp.match|Number} endMatchOrLastChar */ + BeginEndNode.prototype.setEnd = function(endMatchOrLastChar) { + if (endMatchOrLastChar && typeof(endMatchOrLastChar) === "object") { + var endMatch = endMatchOrLastChar; + this.endMatch = endMatch; + this.end = endMatch.index + endMatch[0].length; + } else { + var lastChar = endMatchOrLastChar; + this.endMatch = null; + this.end = lastChar; + } + }; + BeginEndNode.prototype.shiftStart = function(amount) { + this.start += amount; + this.beginMatch.index += amount; + }; + BeginEndNode.prototype.shiftEnd = function(amount) { + this.end += amount; + if (this.endMatch) { this.endMatch.index += amount; } + }; + BeginEndNode.prototype.valueOf = function() { + return "{" + this.rule.beginRegex + " range=" + this.start + ".." + this.end + "}"; + }; + return BeginEndNode; + }()), + /** Pushes rules onto stack so that rules[startFrom] is on top */ + push: function(/**Array*/ stack, /**Array*/ rules) { + if (!rules) { return; } + for (var i = rules.length; i > 0; ) { + stack.push(rules[--i]); + } + }, + /** Execs regex on text, and returns the match object with its index offset by the given amount. */ + exec: function(/**RegExp*/ regex, /**String*/ text, /**Number*/ offset) { + var match = regex.exec(text); + if (match) { match.index += offset; } + regex.lastIndex = 0; // Just in case + return match; + }, + /** @returns {Number} The position immediately following the match. */ + afterMatch: function(/**RegExp.match*/ match) { + return match.index + match[0].length; + }, + /** @returns {RegExp.match} If node is a BeginEndNode and its rule's "end" pattern matches the text. */ + getEndMatch: function(/**Node*/ node, /**String*/ text, /**Number*/ offset) { + if (node instanceof this.BeginEndNode) { + var rule = node.rule; + var endRegex = node.endRegexSubstituted || rule.endRegex; + if (!endRegex) { return null; } + return this.exec(endRegex, text, offset); + } + return null; + }, + /** Called once when file is first loaded to build the parse tree. Tree is updated incrementally thereafter as buffer is modified */ + initialParse: function() { + var last = this.textView.getModel().getCharCount(); + // First time; make parse tree for whole buffer + var root = new this.ContainerNode(null, this.grammar._typedRule); + this._tree = root; + this.parse(this._tree, false, 0); + }, + _onModelChanged: function(/**eclipse.ModelChangedEvent*/ e) { + var addedCharCount = e.addedCharCount, + addedLineCount = e.addedLineCount, + removedCharCount = e.removedCharCount, + removedLineCount = e.removedLineCount, + start = e.start; + if (!this._tree) { + this.initialParse(); + } else { + var model = this.textView.getModel(); + var charCount = model.getCharCount(); + + // For rs, we must rewind to the line preceding the line 'start' is on. We can't rely on start's + // line since it may've been changed in a way that would cause a new beginMatch at its lineStart. + var rs = model.getLineEnd(model.getLineAtOffset(start) - 1); // may be < 0 + var fd = this.getFirstDamaged(rs, rs); + rs = rs === -1 ? 0 : rs; + var stoppedAt; + if (fd) { + // [rs, re] is the region we need to verify. If we find the structure of the tree + // has changed in that area, then we may need to reparse the rest of the file. + stoppedAt = this.parse(fd, true, rs, start, addedCharCount, removedCharCount); + } else { + // FIXME: fd == null ? + stoppedAt = charCount; + } + this.textView.redrawRange(rs, stoppedAt); + } + }, + /** @returns {BeginEndNode|ContainerNode} The result of taking the first (smallest "start" value) + * node overlapping [start,end] and drilling down to get its deepest damaged descendant (if any). + */ + getFirstDamaged: function(start, end) { + // If start === 0 we actually have to start from the root because there is no position + // we can rely on. (First index is damaged) + if (start < 0) { + return this._tree; + } + + var nodes = [this._tree]; + var result = null; + while (nodes.length) { + var n = nodes.pop(); + if (!n.parent /*n is root*/ || this.isDamaged(n, start, end)) { + // n is damaged by the edit, so go into its children + // Note: If a node is damaged, then some of its descendents MAY be damaged + // If a node is undamaged, then ALL of its descendents are undamaged + if (n instanceof this.BeginEndNode) { + result = n; + } + // Examine children[0] last + for (var i=0; i < n.children.length; i++) { + nodes.push(n.children[i]); + } + } + } + return result || this._tree; + }, + /** @returns true If n overlaps the interval [start,end] */ + isDamaged: function(/**BeginEndNode*/ n, start, end) { + // Note strict > since [2,5] doesn't overlap [5,7] + return (n.start <= end && n.end > start); + }, + /** + * Builds tree from some of the buffer content + * + * TODO cleanup params + * @param {BeginEndNode|ContainerNode} origNode The deepest node that overlaps [rs,rs], or the root. + * @param {Boolean} repairing + * @param {Number} rs See _onModelChanged() + * @param {Number} [editStart] Only used for repairing === true + * @param {Number} [addedCharCount] Only used for repairing === true + * @param {Number} [removedCharCount] Only used for repairing === true + */ + parse: function(origNode, repairing, rs, editStart, addedCharCount, removedCharCount) { + var model = this.textView.getModel(); + var lastLineStart = model.getLineStart(model.getLineCount() - 1); + var eof = model.getCharCount(); + var initialExpected = this.getInitialExpected(origNode, rs); + + // re is best-case stopping point; if we detect change to tree, we must continue past it + var re = -1; + if (repairing) { + origNode.repaired = true; + origNode.endNeedsUpdate = true; + var lastChild = origNode.children[origNode.children.length-1]; + var delta = addedCharCount - removedCharCount; + var lastChildLineEnd = lastChild ? model.getLineEnd(model.getLineAtOffset(lastChild.end + delta)) : -1; + var editLineEnd = model.getLineEnd(model.getLineAtOffset(editStart + removedCharCount)); + re = Math.max(lastChildLineEnd, editLineEnd); + } + re = (re === -1) ? eof : re; + + var expected = initialExpected; + var node = origNode; + var matchedChildOrEnd = false; + var pos = rs; + while (node && (!repairing || (pos < re))) { + var matchInfo = this.getNextMatch(model, node, pos); + if (!matchInfo) { + // Go to next line, if any + pos = (pos >= lastLineStart) ? eof : model.getLineStart(model.getLineAtOffset(pos) + 1); + } + var match = matchInfo && matchInfo.match, + rule = matchInfo && matchInfo.rule, + isSub = matchInfo && matchInfo.isSub, + isEnd = matchInfo && matchInfo.isEnd; + if (isSub) { + pos = this.afterMatch(match); + if (rule instanceof this.BeginEndRule) { + matchedChildOrEnd = true; + // Matched a child. Did we expect that? + if (repairing && rule === expected.rule && node === expected.parent) { + // Yes: matched expected child + var foundChild = expected; + foundChild.setStart(match); + // Note: the 'end' position for this node will either be matched, or fixed up by us post-loop + foundChild.repaired = true; + foundChild.endNeedsUpdate = true; + node = foundChild; // descend + expected = this.getNextExpected(expected, "begin"); + } else { + if (repairing) { + // No: matched unexpected child. + this.prune(node, expected); + repairing = false; + } + + // Add the new child (will replace 'expected' in node's children list) + var subNode = new this.BeginEndNode(node, rule, match); + node.addChild(subNode); + node = subNode; // descend + } + } else { + // Matched a MatchRule; no changes to tree required + } + } else if (isEnd || pos === eof) { + if (node instanceof this.BeginEndNode) { + if (match) { + matchedChildOrEnd = true; + node.setEnd(match); + pos = this.afterMatch(match); + // Matched node's end. Did we expect that? + if (repairing && node === expected && node.parent === expected.parent) { + // Yes: found the expected end of node + node.repaired = true; + delete node.endNeedsUpdate; + expected = this.getNextExpected(expected, "end"); + } else { + if (repairing) { + // No: found an unexpected end + this.prune(node, expected); + repairing = false; + } + } + } else { + // Force-ending a BeginEndNode that runs until eof + node.setEnd(eof); + delete node.endNeedsUpdate; + } + } + node = node.parent; // ascend + } + +// if (repairing && pos >= re && !matchedChildOrEnd) { +// // Reached re without matching any begin/end => initialExpected itself was removed => repair fail +// this.prune(origNode, initialExpected); +// repairing = false; +// } + } // end loop + // TODO: do this for every node we end? + this.removeUnrepairedChildren(origNode, repairing, rs); + + //console.debug("parsed " + (pos - rs) + " of " + model.getCharCount + "buf"); + this.cleanup(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount); + return pos; // where we stopped repairing/reparsing + }, + /** Helper for parse() in the repair case. To be called when ending a node, as any children that + * lie in [rs,node.end] and were not repaired must've been deleted. + */ + removeUnrepairedChildren: function(node, repairing, start) { + if (repairing) { + var children = node.children; + var removeFrom = -1; + for (var i=0; i < children.length; i++) { + var child = children[i]; + if (!child.repaired && this.isDamaged(child, start, Number.MAX_VALUE /*end doesn't matter*/)) { + removeFrom = i; + break; + } + } + if (removeFrom !== -1) { + node.children.length = removeFrom; + } + } + }, + /** Helper for parse() in the repair case */ + cleanup: function(repairing, origNode, rs, re, eof, addedCharCount, removedCharCount) { + var i, node, maybeRepairedNodes; + if (repairing) { + // The repair succeeded, so update stale begin/end indices by simple translation. + var delta = addedCharCount - removedCharCount; + // A repaired node's end can't exceed re, but it may exceed re-delta+1. + // TODO: find a way to guarantee disjoint intervals for repaired vs unrepaired, then stop using flag + var maybeUnrepairedNodes = this.getIntersecting(re-delta+1, eof); + maybeRepairedNodes = this.getIntersecting(rs, re); + // Handle unrepaired nodes. They are those intersecting [re-delta+1, eof] that don't have the flag + for (i=0; i < maybeUnrepairedNodes.length; i++) { + node = maybeUnrepairedNodes[i]; + if (!node.repaired && node instanceof this.BeginEndNode) { + node.shiftEnd(delta); + node.shiftStart(delta); + } + } + // Translate 'end' index of repaired node whose 'end' was not matched in loop (>= re) + for (i=0; i < maybeRepairedNodes.length; i++) { + node = maybeRepairedNodes[i]; + if (node.repaired && node.endNeedsUpdate) { + node.shiftEnd(delta); + } + delete node.endNeedsUpdate; + delete node.repaired; + } + } else { + // Clean up after ourself + maybeRepairedNodes = this.getIntersecting(rs, re); + for (i=0; i < maybeRepairedNodes.length; i++) { + delete maybeRepairedNodes[i].repaired; + } + } + }, + /** + * @param model {orion.textview.TextModel} + * @param node {Node} + * @param pos {Number} + * @param [matchRulesOnly] {Boolean} Optional, if true only "match" subrules will be considered. + * @returns {Object} A match info object with properties: + * {Boolean} isEnd + * {Boolean} isSub + * {RegExp.match} match + * {(Match|BeginEnd)Rule} rule + */ + getNextMatch: function(model, node, pos, matchRulesOnly) { + var lineIndex = model.getLineAtOffset(pos); + var lineEnd = model.getLineEnd(lineIndex); + var line = model.getText(pos, lineEnd); + + var stack = [], + expandedContainers = [], + subMatches = [], + subrules = []; + this.push(stack, node.rule.subrules); + while (stack.length) { + var next = stack.length ? stack.pop() : null; + var subrule = next && next._resolvedRule._typedRule; + if (subrule instanceof this.ContainerRule && expandedContainers.indexOf(subrule) === -1) { + // Expand ContainerRule by pushing its subrules on + expandedContainers.push(subrule); + this.push(stack, subrule.subrules); + continue; + } + if (subrule && matchRulesOnly && !(subrule.matchRegex)) { + continue; + } + var subMatch = subrule && this.exec(subrule.matchRegex || subrule.beginRegex, line, pos); + if (subMatch) { + subMatches.push(subMatch); + subrules.push(subrule); + } + } + + var bestSub = Number.MAX_VALUE, + bestSubIndex = -1; + for (var i=0; i < subMatches.length; i++) { + var match = subMatches[i]; + if (match.index < bestSub) { + bestSub = match.index; + bestSubIndex = i; + } + } + + if (!matchRulesOnly) { + // See if the "end" pattern of the active begin/end node matches. + // TODO: The active begin/end node may not be the same as the node that holds the subrules + var activeBENode = node; + var endMatch = this.getEndMatch(node, line, pos); + if (endMatch) { + var doEndLast = activeBENode.rule.applyEndPatternLast; + var endWins = bestSubIndex === -1 || (endMatch.index < bestSub) || (!doEndLast && endMatch.index === bestSub); + if (endWins) { + return {isEnd: true, rule: activeBENode.rule, match: endMatch}; + } + } + } + return bestSubIndex === -1 ? null : {isSub: true, rule: subrules[bestSubIndex], match: subMatches[bestSubIndex]}; + }, + /** + * Gets the node corresponding to the first match we expect to see in the repair. + * @param {BeginEndNode|ContainerNode} node The node returned via getFirstDamaged(rs,rs) -- may be the root. + * @param {Number} rs See _onModelChanged() + * Note that because rs is a line end (or 0, a line start), it will intersect a beginMatch or + * endMatch either at their 0th character, or not at all. (begin/endMatches can't cross lines). + * This is the only time we rely on the start/end values from the pre-change tree. After this + * we only look at node ordering, never use the old indices. + * @returns {Node} + */ + getInitialExpected: function(node, rs) { + // TODO: Kind of weird.. maybe ContainerNodes should have start & end set, like BeginEndNodes + var i, child; + if (node === this._tree) { + // get whichever of our children comes after rs + for (i=0; i < node.children.length; i++) { + child = node.children[i]; // BeginEndNode + if (child.start >= rs) { + return child; + } + } + } else if (node instanceof this.BeginEndNode) { + if (node.endMatch) { + // Which comes next after rs: our nodeEnd or one of our children? + var nodeEnd = node.endMatch.index; + for (i=0; i < node.children.length; i++) { + child = node.children[i]; // BeginEndNode + if (child.start >= rs) { + break; + } + } + if (child && child.start < nodeEnd) { + return child; // Expect child as the next match + } + } else { + // No endMatch => node goes until eof => it end should be the next match + } + } + return node; // We expect node to end, so it should be the next match + }, + /** + * Helper for repair() to tell us what kind of event we expect next. + * @param {Node} expected Last value returned by this method. + * @param {String} event "begin" if the last value of expected was matched as "begin", + * or "end" if it was matched as an end. + * @returns {Node} The next expected node to match, or null. + */ + getNextExpected: function(/**Node*/ expected, event) { + var node = expected; + if (event === "begin") { + var child = node.children[0]; + if (child) { + return child; + } else { + return node; + } + } else if (event === "end") { + var parent = node.parent; + if (parent) { + var nextSibling = parent.children[parent.children.indexOf(node) + 1]; + if (nextSibling) { + return nextSibling; + } else { + return parent; + } + } + } + return null; + }, + /** Helper for parse() when repairing. Prunes out the unmatched nodes from the tree so we can continue parsing. */ + prune: function(/**BeginEndNode|ContainerNode*/ node, /**Node*/ expected) { + var expectedAChild = expected.parent === node; + if (expectedAChild) { + // Expected child wasn't matched; prune it and all siblings after it + node.children.length = expected.getIndexInParent(); + } else if (node instanceof this.BeginEndNode) { + // Expected node to end but it didn't; set its end unknown and we'll match it eventually + node.endMatch = null; + node.end = null; + } + // Reparsing from node, so prune the successors outside of node's subtree + if (node.parent) { + node.parent.children.length = node.getIndexInParent() + 1; + } + }, + _onLineStyle: function(/**eclipse.LineStyleEvent*/ e) { + function byStart(r1, r2) { + return r1.start - r2.start; + } + + if (!this._tree) { + // In some cases it seems onLineStyle is called before onModelChanged, so we need to parse here + this.initialParse(); + } + var lineStart = e.lineStart, + model = this.textView.getModel(), + lineEnd = model.getLineEnd(e.lineIndex); + + var rs = model.getLineEnd(model.getLineAtOffset(lineStart) - 1); // may be < 0 + var node = this.getFirstDamaged(rs, rs); + + var scopes = this.getLineScope(model, node, lineStart, lineEnd); + e.ranges = this.toStyleRanges(scopes); +// // Editor requires StyleRanges must be in ascending order by 'start', or else some will be ignored + e.ranges.sort(byStart); + }, + // Run parse algorithm on [start, end] in the context of node, assigning scope as we find matches + getLineScope: function(model, node, start, end) { + var pos = start; + var expected = this.getInitialExpected(node, start); + var scopes = [], + gaps = []; + while (node && (pos < end)) { + var matchInfo = this.getNextMatch(model, node, pos); + if (!matchInfo) { + break; // line is over + } + var match = matchInfo && matchInfo.match, + rule = matchInfo && matchInfo.rule, + isSub = matchInfo && matchInfo.isSub, + isEnd = matchInfo && matchInfo.isEnd; + if (match.index !== pos) { + // gap [pos..match.index] + gaps.push({ start: pos, end: match.index, node: node}); + } + if (isSub) { + pos = this.afterMatch(match); + if (rule instanceof this.BeginEndRule) { + // Matched a "begin", assign its scope and descend into it + this.addBeginScope(scopes, match, rule); + node = expected; // descend + expected = this.getNextExpected(expected, "begin"); + } else { + // Matched a child MatchRule; + this.addMatchScope(scopes, match, rule); + } + } else if (isEnd) { + pos = this.afterMatch(match); + // Matched and "end", assign its end scope and go up + this.addEndScope(scopes, match, rule); + expected = this.getNextExpected(expected, "end"); + node = node.parent; // ascend + } + } + if (pos < end) { + gaps.push({ start: pos, end: end, node: node }); + } + var inherited = this.getInheritedLineScope(gaps, start, end); + return scopes.concat(inherited); + }, + getInheritedLineScope: function(gaps, start, end) { + var scopes = []; + for (var i=0; i < gaps.length; i++) { + var gap = gaps[i]; + var node = gap.node; + while (node) { + // if node defines a contentName or name, apply it + var rule = node.rule.rule; + var name = rule.name, + contentName = rule.contentName; + // TODO: if both are given, we don't resolve the conflict. contentName always wins + var scope = contentName || name; + if (scope) { + this.addScopeRange(scopes, gap.start, gap.end, scope); + break; + } + node = node.parent; + } + } + return scopes; + }, + addBeginScope: function(scopes, match, typedRule) { + var rule = typedRule.rule; + this.addCapturesScope(scopes, match, (rule.beginCaptures || rule.captures), typedRule.isComplex, typedRule.beginOld2New, typedRule.beginConsuming); + }, + addEndScope: function(scopes, match, typedRule) { + var rule = typedRule.rule; + this.addCapturesScope(scopes, match, (rule.endCaptures || rule.captures), typedRule.isComplex, typedRule.endOld2New, typedRule.endConsuming); + }, + addMatchScope: function(scopes, match, typedRule) { + var rule = typedRule.rule, + name = rule.name, + captures = rule.captures; + if (captures) { + // captures takes priority over name + this.addCapturesScope(scopes, match, captures, typedRule.isComplex, typedRule.matchOld2New, typedRule.matchConsuming); + } else { + this.addScope(scopes, match, name); + } + }, + addScope: function(scopes, match, name) { + if (!name) { return; } + scopes.push({start: match.index, end: this.afterMatch(match), scope: name }); + }, + addScopeRange: function(scopes, start, end, name) { + if (!name) { return; } + scopes.push({start: start, end: end, scope: name }); + }, + addCapturesScope: function(/**Array*/scopes, /*RegExp.match*/ match, /**Object*/captures, /**Boolean*/isComplex, /**Object*/old2New, /**Object*/consuming) { + if (!captures) { return; } + if (!isComplex) { + this.addScope(scopes, match, captures[0] && captures[0].name); + } else { + // apply scopes captures[1..n] to matching groups [1]..[n] of match + + // Sum up the lengths of preceding consuming groups to get the start offset for each matched group. + var newGroupStarts = {1: 0}; + var sum = 0; + for (var num = 1; match[num] !== undefined; num++) { + if (consuming[num] !== undefined) { + sum += match[num].length; + } + if (match[num+1] !== undefined) { + newGroupStarts[num + 1] = sum; + } + } + // Map the group numbers referred to in captures object to the new group numbers, and get the actual matched range. + var start = match.index; + for (var oldGroupNum = 1; captures[oldGroupNum]; oldGroupNum++) { + var scope = captures[oldGroupNum].name; + var newGroupNum = old2New[oldGroupNum]; + var groupStart = start + newGroupStarts[newGroupNum]; + // Not every capturing group defined in regex need match every time the regex is run. + // eg. (a)|b matches "b" but group 1 is undefined + if (typeof match[newGroupNum] !== "undefined") { + var groupEnd = groupStart + match[newGroupNum].length; + this.addScopeRange(scopes, groupStart, groupEnd, scope); + } + } + } + }, + /** @returns {Node[]} In depth-first order */ + getIntersecting: function(start, end) { + var result = []; + var nodes = this._tree ? [this._tree] : []; + while (nodes.length) { + var n = nodes.pop(); + var visitChildren = false; + if (n instanceof this.ContainerNode) { + visitChildren = true; + } else if (this.isDamaged(n, start, end)) { + visitChildren = true; + result.push(n); + } + if (visitChildren) { + var len = n.children.length; +// for (var i=len-1; i >= 0; i--) { +// nodes.push(n.children[i]); +// } + for (var i=0; i < len; i++) { + nodes.push(n.children[i]); + } + } + } + return result.reverse(); + }, + _onSelection: function(e) { + }, + _onDestroy: function(/**eclipse.DestroyEvent*/ e) { + this.grammar = null; + this._styles = null; + this._tree = null; + }, + /** + * Applies the grammar to obtain the {@link eclipse.StyleRange[]} for the given line. + * @returns eclipse.StyleRange[] + */ + toStyleRanges: function(/**ScopeRange[]*/ scopeRanges) { + var styleRanges = []; + for (var i=0; i < scopeRanges.length; i++) { + var scopeRange = scopeRanges[i]; + var classNames = this._styles[scopeRange.scope]; + if (!classNames) { throw new Error("styles not found for " + scopeRange.scope); } + var classNamesString = classNames.join(" "); + styleRanges.push({start: scopeRange.start, end: scopeRange.end, style: {styleClass: classNamesString}}); +// console.debug("{start " + styleRanges[i].start + ", end " + styleRanges[i].end + ", style: " + styleRanges[i].style.styleClass + "}"); + } + return styleRanges; + } + }); + return TextMateStyler; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define(['dojo'], function() { + return orion.editor; + }); +} + diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/webContentAssist.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/webContentAssist.js new file mode 100644 index 00000000..8d837767 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/editor/webContentAssist.js @@ -0,0 +1,82 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +/*global orion:true*/ + +/** @namespace */ +var orion = orion || {}; +orion.editor = orion.editor || {}; + +/** + * @class orion.contentAssist.CssContentAssistProvider + */ +orion.editor.CssContentAssistProvider = (function() { + /** @private */ + function CssContentAssistProvider() { + } + CssContentAssistProvider.prototype = /** @lends orion.contentAssist.CssContentAssistProvider.prototype */ { + /** + * @param {String} The string buffer.substring(w+1, c) where c is the caret offset and w is the index of the + * rightmost whitespace character preceding c. + * @param {String} buffer The entire buffer being edited + * @param {orion.editor.Selection} selection The current textView selection. + * @returns {dojo.Deferred} A future that will provide the keywords. + */ + getKeywords: function(prefix, buffer, selection) { + return [ "background", "background-attachment", "background-color", "background-image", + "background-position", "background-repeat", "border", "border-bottom", + "border-bottom-color", "border-bottom-style", "border-bottom-width", "border-color", + "border-left", "border-left-color", "border-left-style", "border-left-width", + "border-right", "border-right-color", "border-right-style", "border-right-width", + "border-style", "border-top", "border-top-color", "border-top-style", "border-top-width", + "border-width", "bottom", "clear", "clip", "color", "cursor", "display", "float", "font", + "font-family", "font-size", "font-style", "font-variant", "font-weight", "height", + "horizontal-align", "left", "line-height", "list-style", "list-style-image", + "list-style-position", "list-style-type", "margin", "margin-bottom", "margin-left", + "margin-right", "margin-top", "max-height", "max-width", "min-height", "min-width", + "outline", "outline-color", "outline-style", "outline-width", "overflow", "overflow-x", + "overflow-y", "padding", "padding-bottom", "padding-left", "padding-right", + "padding-top", "position", "right", "text-align", "text-decoration", "text-indent", + "top", "vertical-align", "visibility", "width", "z-index" ]; + } + }; + return CssContentAssistProvider; +}()); + +/** + * @class orion.contentAssist.JavaScriptContentAssistProvider + */ +orion.editor.JavaScriptContentAssistProvider = (function() { + /** @private */ + function JavaScriptContentAssistProvider() { + } + JavaScriptContentAssistProvider.prototype = /** @lends orion.contentAssist.JavaScriptContentAssistProvider.prototype */ { + /** + * @param {String} The string buffer.substring(w+1, c) where c is the caret offset and w is the index of the + * rightmost whitespace character preceding c. + * @param {String} buffer The entire buffer being edited + * @param {orion.editor.Selection} selection The current textView selection. + * @returns {dojo.Deferred} A future that will provide the keywords. + */ + getKeywords: function(prefix, buffer, selection) { + return [ "break", "case", "catch", "continue", "debugger", "default", "delete", "do", "else", + "finally", "for", "function", "if", "in", "instanceof", "new", "return", "switch", + "this", "throw", "try", "typeof", "var", "void", "while", "with" ]; + } + }; + return JavaScriptContentAssistProvider; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return orion.editor; + }); +} + diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/keyBinding.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/keyBinding.js new file mode 100644 index 00000000..252233b2 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/keyBinding.js @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + ******************************************************************************/ + +/*global window define */ + +/** + * @namespace The global container for Orion APIs. + */ +var orion = orion || {}; +orion.textview = orion.textview || {}; + +/** + * Constructs a new key binding with the given key code and modifiers. + * + * @param {String|Number} keyCode the key code. + * @param {Boolean} mod1 the primary modifier (usually Command on Mac and Control on other platforms). + * @param {Boolean} mod2 the secondary modifier (usually Shift). + * @param {Boolean} mod3 the third modifier (usually Alt). + * @param {Boolean} mod4 the fourth modifier (usually Control on the Mac). + * + * @class A KeyBinding represents of a key code and a modifier state that can be triggered by the user using the keyboard. + * @name orion.textview.KeyBinding + * + * @property {String|Number} keyCode The key code. + * @property {Boolean} mod1 The primary modifier (usually Command on Mac and Control on other platforms). + * @property {Boolean} mod2 The secondary modifier (usually Shift). + * @property {Boolean} mod3 The third modifier (usually Alt). + * @property {Boolean} mod4 The fourth modifier (usually Control on the Mac). + * + * @see orion.textview.TextView#setKeyBinding + */ +orion.textview.KeyBinding = (function() { + var isMac = window.navigator.platform.indexOf("Mac") !== -1; + /** @private */ + function KeyBinding (keyCode, mod1, mod2, mod3, mod4) { + if (typeof(keyCode) === "string") { + this.keyCode = keyCode.toUpperCase().charCodeAt(0); + } else { + this.keyCode = keyCode; + } + this.mod1 = mod1 !== undefined && mod1 !== null ? mod1 : false; + this.mod2 = mod2 !== undefined && mod2 !== null ? mod2 : false; + this.mod3 = mod3 !== undefined && mod3 !== null ? mod3 : false; + this.mod4 = mod4 !== undefined && mod4 !== null ? mod4 : false; + } + KeyBinding.prototype = /** @lends orion.textview.KeyBinding.prototype */ { + /** + * Returns whether this key binding matches the given key event. + * + * @param e the key event. + * @returns {Boolean} <code>true</code> whether the key binding matches the key event. + */ + match: function (e) { + if (this.keyCode === e.keyCode) { + var mod1 = isMac ? e.metaKey : e.ctrlKey; + if (this.mod1 !== mod1) { return false; } + if (this.mod2 !== e.shiftKey) { return false; } + if (this.mod3 !== e.altKey) { return false; } + if (isMac && this.mod4 !== e.ctrlKey) { return false; } + return true; + } + return false; + }, + /** + * Returns whether this key binding is the same as the given parameter. + * + * @param {orion.textview.KeyBinding} kb the key binding to compare with. + * @returns {Boolean} whether or not the parameter and the receiver describe the same key binding. + */ + equals: function(kb) { + if (!kb) { return false; } + if (this.keyCode !== kb.keyCode) { return false; } + if (this.mod1 !== kb.mod1) { return false; } + if (this.mod2 !== kb.mod2) { return false; } + if (this.mod3 !== kb.mod3) { return false; } + if (this.mod4 !== kb.mod4) { return false; } + return true; + } + }; + return KeyBinding; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return orion.textview; + }); +} + diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/rulers.css b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/rulers.css new file mode 100644 index 00000000..24becbc7 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/rulers.css @@ -0,0 +1,39 @@ +.ruler_annotation { + background-color: #e1ebfb; + width: 16px; +} + +.ruler_annotation_todo { +} + +.ruler_annotation_todo_overview { + background-color: lightgreen; + border: 1px solid green; +} + +.ruler_annotation_breakpoint { +} + +.ruler_annotation_breakpoint_overview { + background-color: lightblue; + border: 1px solid blue; +} + +.ruler_lines { + background-color: #e1ebfb; + border-right: 1px solid #b1badf; + text-align: right; +} + +.ruler_overview { + background-color: #e1ebfb; +} + +.ruler_lines_even { + background-color: #e1ebfb; +} + +.ruler_lines_odd { + background-color: white; +} + diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/rulers.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/rulers.js new file mode 100644 index 00000000..17c890b3 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/rulers.js @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*global window define */ + +/** + * @namespace The global container for Orion APIs. + */ +var orion = orion || {}; +orion.textview = orion.textview || {}; + +orion.textview.Ruler = (function() { + function Ruler (rulerLocation, rulerOverview, rulerStyle) { + this._location = rulerLocation || "left"; + this._overview = rulerOverview || "page"; + this._rulerStyle = rulerStyle; + this._view = null; + } + Ruler.prototype = { + setView: function (view) { + if (this._onModelChanged && this._view) { + this._view.removeEventListener("ModelChanged", this, this._onModelChanged); + } + this._view = view; + if (this._onModelChanged && this._view) { + this._view.addEventListener("ModelChanged", this, this._onModelChanged); + } + }, + getLocation: function() { + return this._location; + }, + getOverview: function(view) { + return this._overview; + } + }; + return Ruler; +}()); + +orion.textview.LineNumberRuler = (function() { + function LineNumberRuler (rulerLocation, rulerStyle, oddStyle, evenStyle) { + orion.textview.Ruler.call(this, rulerLocation, "page", rulerStyle); + this._oddStyle = oddStyle || {style: {backgroundColor: "white"}}; + this._evenStyle = evenStyle || {style: {backgroundColor: "white"}}; + this._numOfDigits = 0; + } + LineNumberRuler.prototype = new orion.textview.Ruler(); + LineNumberRuler.prototype.getStyle = function(lineIndex) { + if (lineIndex === undefined) { + return this._rulerStyle; + } else { + return lineIndex & 1 ? this._oddStyle : this._evenStyle; + } + }; + LineNumberRuler.prototype.getHTML = function(lineIndex) { + if (lineIndex === -1) { + var model = this._view.getModel(); + return model.getLineCount(); + } else { + return lineIndex + 1; + } + }; + LineNumberRuler.prototype._onModelChanged = function(e) { + var start = e.start; + var model = this._view.getModel(); + var lineCount = model.getLineCount(); + var numOfDigits = (lineCount+"").length; + if (this._numOfDigits !== numOfDigits) { + this._numOfDigits = numOfDigits; + var startLine = model.getLineAtOffset(start); + this._view.redrawLines(startLine, lineCount, this); + } + }; + return LineNumberRuler; +}()); + +orion.textview.AnnotationRuler = (function() { + function AnnotationRuler (rulerLocation, rulerStyle, defaultAnnotation) { + orion.textview.Ruler.call(this, rulerLocation, "page", rulerStyle); + this._defaultAnnotation = defaultAnnotation; + this._annotations = []; + } + AnnotationRuler.prototype = new orion.textview.Ruler(); + AnnotationRuler.prototype.clearAnnotations = function() { + this._annotations = []; + var lineCount = this._view.getModel().getLineCount(); + this._view.redrawLines(0, lineCount, this); + if (this._overviewRuler) { + this._view.redrawLines(0, lineCount, this._overviewRuler); + } + }; + AnnotationRuler.prototype.getAnnotation = function(lineIndex) { + return this._annotations[lineIndex]; + }; + AnnotationRuler.prototype.getAnnotations = function() { + return this._annotations; + }; + AnnotationRuler.prototype.getStyle = function(lineIndex) { + switch (lineIndex) { + case undefined: + return this._rulerStyle; + case -1: + return this._defaultAnnotation ? this._defaultAnnotation.style : null; + default: + return this._annotations[lineIndex] && this._annotations[lineIndex].style ? this._annotations[lineIndex].style : null; + } + }; + AnnotationRuler.prototype.getHTML = function(lineIndex) { + if (lineIndex === -1) { + return this._defaultAnnotation ? this._defaultAnnotation.html : ""; + } else { + return this._annotations[lineIndex] && this._annotations[lineIndex].html ? this._annotations[lineIndex].html : ""; + } + }; + AnnotationRuler.prototype.setAnnotation = function(lineIndex, annotation) { + if (lineIndex === undefined) { return; } + this._annotations[lineIndex] = annotation; + this._view.redrawLines(lineIndex, lineIndex + 1, this); + if (this._overviewRuler) { + this._view.redrawLines(lineIndex, lineIndex + 1, this._overviewRuler); + } + }; + AnnotationRuler.prototype._onModelChanged = function(e) { + var start = e.start; + var removedLineCount = e.removedLineCount; + var addedLineCount = e.addedLineCount; + var linesChanged = addedLineCount - removedLineCount; + if (linesChanged) { + var model = this._view.getModel(); + var startLine = model.getLineAtOffset(start); + var newLines = [], lines = this._annotations; + var changed = false; + for (var prop in lines) { + var i = prop >>> 0; + if (!(startLine < i && i < startLine + removedLineCount)) { + var newIndex = i; + if (i > startLine) { + newIndex += linesChanged; + changed = true; + } + newLines[newIndex] = lines[i]; + } else { + changed = true; + } + } + this._annotations = newLines; + if (changed) { + var lineCount = model.getLineCount(); + this._view.redrawLines(startLine, lineCount, this); + //TODO redraw overview (batch it for performance) + if (this._overviewRuler) { + this._view.redrawLines(0, lineCount, this._overviewRuler); + } + } + } + }; + return AnnotationRuler; +}()); + +orion.textview.OverviewRuler = (function() { + function OverviewRuler (rulerLocation, rulerStyle, annotationRuler) { + orion.textview.Ruler.call(this, rulerLocation, "document", rulerStyle); + this._annotationRuler = annotationRuler; + if (annotationRuler) { + annotationRuler._overviewRuler = this; + } + } + OverviewRuler.prototype = new orion.textview.Ruler(); + OverviewRuler.prototype.getAnnotations = function() { + var annotations = this._annotationRuler.getAnnotations(); + var lines = []; + for (var prop in annotations) { + var i = prop >>> 0; + if (annotations[i] !== undefined) { + lines.push(i); + } + } + return lines; + }; + OverviewRuler.prototype.getStyle = function(lineIndex) { + var result, style; + if (lineIndex === undefined) { + result = this._rulerStyle || {}; + style = result.style || (result.style = {}); + style.lineHeight = "1px"; + style.fontSize = "1px"; + style.width = "14px"; + } else { + if (lineIndex !== -1) { + var annotation = this._annotationRuler.getAnnotation(lineIndex); + result = annotation.overviewStyle || {}; + } else { + result = {}; + } + style = result.style || (result.style = {}); + style.cursor = "pointer"; + style.width = "8px"; + style.height = "3px"; + style.left = "2px"; + } + return result; + }; + OverviewRuler.prototype.getHTML = function(lineIndex) { + return " "; + }; + OverviewRuler.prototype.onClick = function(lineIndex, e) { + if (lineIndex === undefined) { return; } + this._view.setTopIndex(lineIndex); + }; + return OverviewRuler; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return orion.textview; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textModel.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textModel.js new file mode 100644 index 00000000..88921cbb --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textModel.js @@ -0,0 +1,458 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + ******************************************************************************/ + +/*global window define */ + +/** + * @namespace The global container for Orion APIs. + */ +var orion = orion || {}; +orion.textview = orion.textview || {}; + +/** + * Constructs a new TextModel with the given text and default line delimiter. + * + * @param {String} [text=""] the text that the model will store + * @param {String} [lineDelimiter=platform delimiter] the line delimiter used when inserting new lines to the model. + * + * @name orion.textview.TextModel + * @class The TextModel is an interface that provides text for the view. Applications may + * implement the TextModel interface to provide a custom store for the view content. The + * view interacts with its text model in order to access and update the text that is being + * displayed and edited in the view. This is the default implementation. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#setModel} + * </p> + */ +orion.textview.TextModel = (function() { + var isWindows = window.navigator.platform.indexOf("Win") !== -1; + + /** @private */ + function TextModel(text, lineDelimiter) { + this._listeners = []; + this._lineDelimiter = lineDelimiter ? lineDelimiter : (isWindows ? "\r\n" : "\n"); + this._lastLineIndex = -1; + this._text = [""]; + this._lineOffsets = [0]; + this.setText(text); + } + + TextModel.prototype = /** @lends orion.textview.TextModel.prototype */ { + /** + * Adds a listener to the model. + * + * @param {Object} listener the listener to add. + * @param {Function} [listener.onChanged] see {@link #onChanged}. + * @param {Function} [listener.onChanging] see {@link #onChanging}. + * + * @see removeListener + */ + addListener: function(listener) { + this._listeners.push(listener); + }, + /** + * Removes a listener from the model. + * + * @param {Object} listener the listener to remove + * + * @see #addListener + */ + removeListener: function(listener) { + for (var i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] === listener) { + this._listeners.splice(i, 1); + return; + } + } + }, + /** + * Returns the number of characters in the model. + * + * @returns {Number} the number of characters in the model. + */ + getCharCount: function() { + var count = 0; + for (var i = 0; i<this._text.length; i++) { + count += this._text[i].length; + } + return count; + }, + /** + * Returns the text of the line at the given index. + * <p> + * The valid indices are 0 to line count exclusive. Returns <code>null</code> + * if the index is out of range. + * </p> + * + * @param {Number} lineIndex the zero based index of the line. + * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter. + * @returns {String} the line text or <code>null</code> if out of range. + * + * @see #getLineAtOffset + */ + getLine: function(lineIndex, includeDelimiter) { + var lineCount = this.getLineCount(); + if (!(0 <= lineIndex && lineIndex < lineCount)) { + return null; + } + var start = this._lineOffsets[lineIndex]; + if (lineIndex + 1 < lineCount) { + var text = this.getText(start, this._lineOffsets[lineIndex + 1]); + if (includeDelimiter) { + return text; + } + var end = text.length, c; + while (((c = text.charCodeAt(end - 1)) === 10) || (c === 13)) { + end--; + } + return text.substring(0, end); + } else { + return this.getText(start); + } + }, + /** + * Returns the line index at the given character offset. + * <p> + * The valid offsets are 0 to char count inclusive. The line index for + * char count is <code>line count - 1</code>. Returns <code>-1</code> if + * the offset is out of range. + * </p> + * + * @param {Number} offset a character offset. + * @returns {Number} the zero based line index or <code>-1</code> if out of range. + */ + getLineAtOffset: function(offset) { + if (!(0 <= offset && offset <= this.getCharCount())) { + return -1; + } + var lineCount = this.getLineCount(); + var charCount = this.getCharCount(); + if (offset === charCount) { + return lineCount - 1; + } + var lineStart, lineEnd; + var index = this._lastLineIndex; + if (0 <= index && index < lineCount) { + lineStart = this._lineOffsets[index]; + lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount; + if (lineStart <= offset && offset < lineEnd) { + return index; + } + } + var high = lineCount; + var low = -1; + while (high - low > 1) { + index = Math.floor((high + low) / 2); + lineStart = this._lineOffsets[index]; + lineEnd = index + 1 < lineCount ? this._lineOffsets[index + 1] : charCount; + if (offset <= lineStart) { + high = index; + } else if (offset < lineEnd) { + high = index; + break; + } else { + low = index; + } + } + this._lastLineIndex = high; + return high; + }, + /** + * Returns the number of lines in the model. + * <p> + * The model always has at least one line. + * </p> + * + * @returns {Number} the number of lines. + */ + getLineCount: function() { + return this._lineOffsets.length; + }, + /** + * Returns the line delimiter that is used by the view + * when inserting new lines. New lines entered using key strokes + * and paste operations use this line delimiter. + * + * @return {String} the line delimiter that is used by the view when inserting new lines. + */ + getLineDelimiter: function() { + return this._lineDelimiter; + }, + /** + * Returns the end character offset for the given line. + * <p> + * The end offset is not inclusive. This means that when the line delimiter is included, the + * offset is either the start offset of the next line or char count. When the line delimiter is + * not included, the offset is the offset of the line delimiter. + * </p> + * <p> + * The valid indices are 0 to line count exclusive. Returns <code>-1</code> + * if the index is out of range. + * </p> + * + * @param {Number} lineIndex the zero based index of the line. + * @param {Boolean} [includeDelimiter=false] whether or not to include the line delimiter. + * @return {Number} the line end offset or <code>-1</code> if out of range. + * + * @see #getLineStart + */ + getLineEnd: function(lineIndex, includeDelimiter) { + var lineCount = this.getLineCount(); + if (!(0 <= lineIndex && lineIndex < lineCount)) { + return -1; + } + if (lineIndex + 1 < lineCount) { + var end = this._lineOffsets[lineIndex + 1]; + if (includeDelimiter) { + return end; + } + var text = this.getText(Math.max(this._lineOffsets[lineIndex], end - 2), end); + var i = text.length, c; + while (((c = text.charCodeAt(i - 1)) === 10) || (c === 13)) { + i--; + } + return end - (text.length - i); + } else { + return this.getCharCount(); + } + }, + /** + * Returns the start character offset for the given line. + * <p> + * The valid indices are 0 to line count exclusive. Returns <code>-1</code> + * if the index is out of range. + * </p> + * + * @param {Number} lineIndex the zero based index of the line. + * @return {Number} the line start offset or <code>-1</code> if out of range. + * + * @see #getLineEnd + */ + getLineStart: function(lineIndex) { + if (!(0 <= lineIndex && lineIndex < this.getLineCount())) { + return -1; + } + return this._lineOffsets[lineIndex]; + }, + /** + * Returns the text for the given range. + * <p> + * The end offset is not inclusive. This means that character at the end offset + * is not included in the returned text. + * </p> + * + * @param {Number} [start=0] the zero based start offset of text range. + * @param {Number} [end=char count] the zero based end offset of text range. + * + * @see #setText + */ + getText: function(start, end) { + if (start === undefined) { start = 0; } + if (end === undefined) { end = this.getCharCount(); } + var offset = 0, chunk = 0, length; + while (chunk<this._text.length) { + length = this._text[chunk].length; + if (start <= offset + length) { break; } + offset += length; + chunk++; + } + var firstOffset = offset; + var firstChunk = chunk; + while (chunk<this._text.length) { + length = this._text[chunk].length; + if (end <= offset + length) { break; } + offset += length; + chunk++; + } + var lastOffset = offset; + var lastChunk = chunk; + if (firstChunk === lastChunk) { + return this._text[firstChunk].substring(start - firstOffset, end - lastOffset); + } + var beforeText = this._text[firstChunk].substring(start - firstOffset); + var afterText = this._text[lastChunk].substring(0, end - lastOffset); + return beforeText + this._text.slice(firstChunk+1, lastChunk).join("") + afterText; + }, + /** + * Notifies all listeners that the text is about to change. + * <p> + * This notification is intended to be used only by the view. Application clients should + * use {@link orion.textview.TextView#event:onModelChanging}. + * </p> + * <p> + * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel + * as part of the implementation of {@link #setText}. This method is included in the public API for documentation + * purposes and to allow integration with other toolkit frameworks. + * </p> + * + * @param {String} text the text that is about to be inserted in the model. + * @param {Number} start the character offset in the model where the change will occur. + * @param {Number} removedCharCount the number of characters being removed from the model. + * @param {Number} addedCharCount the number of characters being added to the model. + * @param {Number} removedLineCount the number of lines being removed from the model. + * @param {Number} addedLineCount the number of lines being added to the model. + */ + onChanging: function(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + for (var i = 0; i < this._listeners.length; i++) { + var l = this._listeners[i]; + if (l && l.onChanging) { + l.onChanging(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + } + } + }, + /** + * Notifies all listeners that the text has changed. + * <p> + * This notification is intended to be used only by the view. Application clients should + * use {@link orion.textview.TextView#event:onModelChanged}. + * </p> + * <p> + * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel + * as part of the implementation of {@link #setText}. This method is included in the public API for documentation + * purposes and to allow integration with other toolkit frameworks. + * </p> + * + * @param {Number} start the character offset in the model where the change occurred. + * @param {Number} removedCharCount the number of characters removed from the model. + * @param {Number} addedCharCount the number of characters added to the model. + * @param {Number} removedLineCount the number of lines removed from the model. + * @param {Number} addedLineCount the number of lines added to the model. + */ + onChanged: function(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + for (var i = 0; i < this._listeners.length; i++) { + var l = this._listeners[i]; + if (l && l.onChanged) { + l.onChanged(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + } + } + }, + /** + * Replaces the text in the given range with the given text. + * <p> + * The end offset is not inclusive. This means that the character at the + * end offset is not replaced. + * </p> + * <p> + * The text model must notify the listeners before and after the + * the text is changed by calling {@link #onChanging} and {@link #onChanged} + * respectively. + * </p> + * + * @param {String} [text=""] the new text. + * @param {Number} [start=0] the zero based start offset of text range. + * @param {Number} [end=char count] the zero based end offset of text range. + * + * @see #getText + */ + setText: function(text, start, end) { + if (text === undefined) { text = ""; } + if (start === undefined) { start = 0; } + if (end === undefined) { end = this.getCharCount(); } + var startLine = this.getLineAtOffset(start); + var endLine = this.getLineAtOffset(end); + var eventStart = start; + var removedCharCount = end - start; + var removedLineCount = endLine - startLine; + var addedCharCount = text.length; + var addedLineCount = 0; + var lineCount = this.getLineCount(); + + var cr = 0, lf = 0, index = 0; + var newLineOffsets = []; + while (true) { + if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); } + if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); } + if (lf === -1 && cr === -1) { break; } + if (cr !== -1 && lf !== -1) { + if (cr + 1 === lf) { + index = lf + 1; + } else { + index = (cr < lf ? cr : lf) + 1; + } + } else if (cr !== -1) { + index = cr + 1; + } else { + index = lf + 1; + } + newLineOffsets.push(start + index); + addedLineCount++; + } + + this.onChanging(text, eventStart, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + + //TODO this should be done the loops below to avoid getText() + if (newLineOffsets.length === 0) { + var startLineOffset = this.getLineStart(startLine), endLineOffset; + if (endLine + 1 < lineCount) { + endLineOffset = this.getLineStart(endLine + 1); + } else { + endLineOffset = this.getCharCount(); + } + if (start !== startLineOffset) { + text = this.getText(startLineOffset, start) + text; + start = startLineOffset; + } + if (end !== endLineOffset) { + text = text + this.getText(end, endLineOffset); + end = endLineOffset; + } + } + + var changeCount = addedCharCount - removedCharCount; + for (var j = startLine + removedLineCount + 1; j < lineCount; j++) { + this._lineOffsets[j] += changeCount; + } + var args = [startLine + 1, removedLineCount].concat(newLineOffsets); + Array.prototype.splice.apply(this._lineOffsets, args); + + var offset = 0, chunk = 0, length; + while (chunk<this._text.length) { + length = this._text[chunk].length; + if (start <= offset + length) { break; } + offset += length; + chunk++; + } + var firstOffset = offset; + var firstChunk = chunk; + while (chunk<this._text.length) { + length = this._text[chunk].length; + if (end <= offset + length) { break; } + offset += length; + chunk++; + } + var lastOffset = offset; + var lastChunk = chunk; + var firstText = this._text[firstChunk]; + var lastText = this._text[lastChunk]; + var beforeText = firstText.substring(0, start - firstOffset); + var afterText = lastText.substring(end - lastOffset); + var params = [firstChunk, lastChunk - firstChunk + 1]; + if (beforeText) { params.push(beforeText); } + if (text) { params.push(text); } + if (afterText) { params.push(afterText); } + Array.prototype.splice.apply(this._text, params); + if (this._text.length === 0) { this._text = [""]; } + + this.onChanged(eventStart, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + } + }; + + return TextModel; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return orion.textview; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textView.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textView.js new file mode 100644 index 00000000..0bd5b27f --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textView.js @@ -0,0 +1,4827 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: + * Felipe Heidrich (IBM Corporation) - initial API and implementation + * Silenio Quarti (IBM Corporation) - initial API and implementation + * Mihai Sucan (Mozilla Foundation) - fix for Bugs 334583, 348471 + ******************************************************************************/ + +/*global window document navigator setTimeout clearTimeout XMLHttpRequest define */ + +/** + * @namespace The global container for Orion APIs. + */ +var orion = orion || {}; +orion.textview = orion.textview || {}; + +/** + * Constructs a new text view. + * + * @param options the view options. + * @param {String|DOMElement} options.parent the parent element for the view, it can be either a DOM element or an ID for a DOM element. + * @param {orion.textview.TextModel} [options.model] the text model for the view. If this options is not set the view creates an empty {@link orion.textview.TextModel}. + * @param {Boolean} [options.readonly=false] whether or not the view is read-only. + * @param {Boolean} [options.fullSelection=true] whether or not the view is in full selection mode. + * @param {String|String[]} [options.stylesheet] one or more stylesheet URIs for the view. + * @param {Number} [options.tabSize] The number of spaces in a tab. + * + * @class A TextView is a user interface for editing text. + * @name orion.textview.TextView + */ +orion.textview.TextView = (function() { + + /** @private */ + function addHandler(node, type, handler, capture) { + if (typeof node.addEventListener === "function") { + node.addEventListener(type, handler, capture === true); + } else { + node.attachEvent("on" + type, handler); + } + } + /** @private */ + function removeHandler(node, type, handler, capture) { + if (typeof node.removeEventListener === "function") { + node.removeEventListener(type, handler, capture === true); + } else { + node.detachEvent("on" + type, handler); + } + } + var isIE = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent) ? document.documentMode : undefined; + var isFirefox = parseFloat(navigator.userAgent.split("Firefox/")[1] || navigator.userAgent.split("Minefield/")[1]) || undefined; + var isOpera = navigator.userAgent.indexOf("Opera") !== -1; + var isChrome = navigator.userAgent.indexOf("Chrome") !== -1; + var isSafari = navigator.userAgent.indexOf("Safari") !== -1; + var isWebkit = navigator.userAgent.indexOf("WebKit") !== -1; + var isPad = navigator.userAgent.indexOf("iPad") !== -1; + var isMac = navigator.platform.indexOf("Mac") !== -1; + var isWindows = navigator.platform.indexOf("Win") !== -1; + var isLinux = navigator.platform.indexOf("Linux") !== -1; + var isW3CEvents = typeof window.document.documentElement.addEventListener === "function"; + var isRangeRects = (!isIE || isIE >= 9) && typeof window.document.createRange().getBoundingClientRect === "function"; + var platformDelimiter = isWindows ? "\r\n" : "\n"; + + /** + * Constructs a new Selection object. + * + * @class A Selection represents a range of selected text in the view. + * @name orion.textview.Selection + */ + var Selection = (function() { + /** @private */ + function Selection (start, end, caret) { + /** + * The selection start offset. + * + * @name orion.textview.Selection#start + */ + this.start = start; + /** + * The selection end offset. + * + * @name orion.textview.Selection#end + */ + this.end = end; + /** @private */ + this.caret = caret; //true if the start, false if the caret is at end + } + Selection.prototype = /** @lends orion.textview.Selection.prototype */ { + /** @private */ + clone: function() { + return new Selection(this.start, this.end, this.caret); + }, + /** @private */ + collapse: function() { + if (this.caret) { + this.end = this.start; + } else { + this.start = this.end; + } + }, + /** @private */ + extend: function (offset) { + if (this.caret) { + this.start = offset; + } else { + this.end = offset; + } + if (this.start > this.end) { + var tmp = this.start; + this.start = this.end; + this.end = tmp; + this.caret = !this.caret; + } + }, + /** @private */ + setCaret: function(offset) { + this.start = offset; + this.end = offset; + this.caret = false; + }, + /** @private */ + getCaret: function() { + return this.caret ? this.start : this.end; + }, + /** @private */ + toString: function() { + return "start=" + this.start + " end=" + this.end + (this.caret ? " caret is at start" : " caret is at end"); + }, + /** @private */ + isEmpty: function() { + return this.start === this.end; + }, + /** @private */ + equals: function(object) { + return this.caret === object.caret && this.start === object.start && this.end === object.end; + } + }; + return Selection; + }()); + + /** + * Constructs a new EventTable object. + * + * @class + * @name orion.textview.EventTable + * @private + */ + var EventTable = (function() { + /** @private */ + function EventTable(){ + this._listeners = {}; + } + EventTable.prototype = /** @lends EventTable.prototype */ { + /** @private */ + addEventListener: function(type, context, func, data) { + if (!this._listeners[type]) { + this._listeners[type] = []; + } + var listener = { + context: context, + func: func, + data: data + }; + this._listeners[type].push(listener); + }, + /** @private */ + sendEvent: function(type, event) { + var listeners = this._listeners[type]; + if (listeners) { + for (var i=0, len=listeners.length; i < len; i++){ + var l = listeners[i]; + if (l && l.context && l.func) { + l.func.call(l.context, event, l.data); + } + } + } + }, + /** @private */ + removeEventListener: function(type, context, func, data){ + var listeners = this._listeners[type]; + if (listeners) { + for (var i=0, len=listeners.length; i < len; i++){ + var l = listeners[i]; + if (l.context === context && l.func === func && l.data === data) { + listeners.splice(i, 1); + break; + } + } + } + } + }; + return EventTable; + }()); + + /** @private */ + function TextView (options) { + this._init(options); + } + + TextView.prototype = /** @lends orion.textview.TextView.prototype */ { + /** + * Adds an event listener to the text view. + * + * @param {String} type the event type. The supported events are: + * <ul> + * <li>"Modify" See {@link #onModify} </li> + * <li>"Selection" See {@link #onSelection} </li> + * <li>"Scroll" See {@link #onScroll} </li> + * <li>"Verify" See {@link #onVerify} </li> + * <li>"Destroy" See {@link #onDestroy} </li> + * <li>"LineStyle" See {@link #onLineStyle} </li> + * <li>"ModelChanging" See {@link #onModelChanging} </li> + * <li>"ModelChanged" See {@link #onModelChanged} </li> + * </ul> + * @param {Object} context the context of the function. + * @param {Function} func the function that will be executed when the event happens. + * The function should take an event as the first parameter and optional data as the second parameter. + * @param {Object} [data] optional data passed to the function. + * + * @see #removeEventListener + */ + addEventListener: function(type, context, func, data) { + this._eventTable.addEventListener(type, context, func, data); + }, + /** + * @class This interface represents a ruler for the text view. + * <p> + * A Ruler is a graphical element that is placed either on the left or on the right side of + * the view. It can be used to provide the view with per line decoration such as line numbering, + * bookmarks, breakpoints, folding disclosures, etc. + * </p><p> + * There are two types of rulers: page and document. A page ruler only shows the content for the lines that are + * visible, while a document ruler always shows the whole content. + * </p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#addRuler} + * </p> + * @name orion.textview.Ruler + * + */ + /** + * Returns the ruler overview type. + * + * @name getOverview + * @methodOf orion.textview.Ruler# + * @returns {String} the overview type, which is either "page" or "document". + * + * @see #getLocation + */ + /** + * Returns the ruler location. + * + * @name getLocation + * @methodOf orion.textview.Ruler# + * @returns {String} the ruler location, which is either "left" or "right". + */ + /** + * Returns the HTML content for the decoration of a given line. + * <p> + * If the line index is <code>-1</code>, the HTML content for the decoration + * that determines the width of the ruler should be returned. + * </p> + * + * @name getHTML + * @methodOf orion.textview.Ruler# + * @param {Number} lineIndex + * @returns {String} the HTML content for a given line, or generic line. + * + * @see #getStyle + */ + /** + * Returns the CSS styling information for the decoration of a given line. + * <p> + * If the line index is <code>-1</code>, the CSS styling information for the decoration + * that determines the width of the ruler should be returned. If the line is + * <code>undefined</code>, the ruler styling information should be returned. + * </p> + * + * @name getStyle + * @methodOf orion.textview.Ruler# + * @param {Number} lineIndex + * @returns {orion.textview.Style} the CSS styling for ruler, given line, or generic line. + * + * @see #getHTML + */ + /** + * Returns the indices of the lines that have decoration. + * <p> + * This function is only called for rulers with "document" overview type. + * </p> + * @name getAnnotations + * @methodOf orion.textview.Ruler# + * @returns {Number[]} an array of line indices. + */ + /** + * This event is sent when the user clicks a line decoration. + * + * @name onClick + * @event + * @methodOf orion.textview.Ruler# + * @param {Number} lineIndex the line index of the clicked decoration + * @param {DOMEvent} e the click event + */ + /** + * This event is sent when the user double clicks a line decoration. + * + * @name onDblClick + * @event + * @methodOf orion.textview.Ruler# + * @param {Number} lineIndex the line index of the double clicked decoration + * @param {DOMEvent} e the double click event + */ + /** + * Adds a ruler to the text view. + * + * @param {orion.textview.Ruler} ruler the ruler. + */ + addRuler: function (ruler) { + var document = this._frameDocument; + var body = document.body; + var side = ruler.getLocation(); + var rulerParent = side === "left" ? this._leftDiv : this._rightDiv; + if (!rulerParent) { + rulerParent = document.createElement("DIV"); + rulerParent.style.overflow = "hidden"; + rulerParent.style.MozUserSelect = "none"; + rulerParent.style.WebkitUserSelect = "none"; + if (isIE) { + rulerParent.attachEvent("onselectstart", function() {return false;}); + } + rulerParent.style.position = "absolute"; + rulerParent.style.top = "0px"; + rulerParent.style.cursor = "default"; + body.appendChild(rulerParent); + if (side === "left") { + this._leftDiv = rulerParent; + rulerParent.className = "viewLeftRuler"; + } else { + this._rightDiv = rulerParent; + rulerParent.className = "viewRightRuler"; + } + var table = document.createElement("TABLE"); + rulerParent.appendChild(table); + table.cellPadding = "0px"; + table.cellSpacing = "0px"; + table.border = "0px"; + table.insertRow(0); + var self = this; + addHandler(rulerParent, "click", function(e) { self._handleRulerEvent(e); }); + addHandler(rulerParent, "dblclick", function(e) { self._handleRulerEvent(e); }); + } + var div = document.createElement("DIV"); + div._ruler = ruler; + div.rulerChanged = true; + div.style.position = "relative"; + var row = rulerParent.firstChild.rows[0]; + var index = row.cells.length; + var cell = row.insertCell(index); + cell.vAlign = "top"; + cell.appendChild(div); + ruler.setView(this); + this._updatePage(); + }, + /** + * Converts the given rectangle from one coordinate spaces to another. + * <p>The supported coordinate spaces are: + * <ul> + * <li>"document" - relative to document, the origin is the top-left corner of first line</li> + * <li>"page" - relative to html page that contains the text view</li> + * <li>"view" - relative to text view, the origin is the top-left corner of the view container</li> + * </ul> + * </p> + * <p>All methods in the view that take or return a position are in the document coordinate space.</p> + * + * @param rect the rectangle to convert. + * @param rect.x the x of the rectangle. + * @param rect.y the y of the rectangle. + * @param rect.width the width of the rectangle. + * @param rect.height the height of the rectangle. + * @param {String} from the source coordinate space. + * @param {String} to the destination coordinate space. + * + * @see #getLocationAtOffset + * @see #getOffsetAtLocation + * @see #getTopPixel + * @see #setTopPixel + */ + convert: function(rect, from, to) { + var scroll = this._getScroll(); + var viewPad = this._getViewPadding(); + var frame = this._frame.getBoundingClientRect(); + var viewRect = this._viewDiv.getBoundingClientRect(); + switch(from) { + case "document": + if (rect.x !== undefined) { + rect.x += - scroll.x + viewRect.left + viewPad.left; + } + if (rect.y !== undefined) { + rect.y += - scroll.y + viewRect.top + viewPad.top; + } + break; + case "page": + if (rect.x !== undefined) { + rect.x += - frame.left; + } + if (rect.y !== undefined) { + rect.y += - frame.top; + } + break; + } + //At this point rect is in the widget coordinate space + switch (to) { + case "document": + if (rect.x !== undefined) { + rect.x += scroll.x - viewRect.left - viewPad.left; + } + if (rect.y !== undefined) { + rect.y += scroll.y - viewRect.top - viewPad.top; + } + break; + case "page": + if (rect.x !== undefined) { + rect.x += frame.left; + } + if (rect.y !== undefined) { + rect.y += frame.top; + } + break; + } + }, + /** + * Destroys the text view. + * <p> + * Removes the view from the page and frees all resources created by the view. + * Calling this function causes the "Destroy" event to be fire so that all components + * attached to view can release their references. + * </p> + * + * @see #onDestroy + */ + destroy: function() { + this._setGrab(null); + this._unhookEvents(); + + /* Destroy rulers*/ + var destroyRulers = function(rulerDiv) { + if (!rulerDiv) { + return; + } + var cells = rulerDiv.firstChild.rows[0].cells; + for (var i = 0; i < cells.length; i++) { + var div = cells[i].firstChild; + div._ruler.setView(null); + } + }; + destroyRulers (this._leftDiv); + destroyRulers (this._rightDiv); + + /* Destroy timers */ + if (this._autoScrollTimerID) { + clearTimeout(this._autoScrollTimerID); + this._autoScrollTimerID = null; + } + if (this._updateTimer) { + clearTimeout(this._updateTimer); + this._updateTimer = null; + } + + /* Destroy DOM */ + var parent = this._parent; + var frame = this._frame; + parent.removeChild(frame); + + if (isPad) { + parent.removeChild(this._touchDiv); + this._touchDiv = null; + this._selDiv1 = null; + this._selDiv2 = null; + this._selDiv3 = null; + this._textArea = null; + } + + var e = {}; + this.onDestroy(e); + + this._parent = null; + this._parentDocument = null; + this._model = null; + this._selection = null; + this._doubleClickSelection = null; + this._eventTable = null; + this._frame = null; + this._frameDocument = null; + this._frameWindow = null; + this._scrollDiv = null; + this._viewDiv = null; + this._clientDiv = null; + this._overlayDiv = null; + this._keyBindings = null; + this._actions = null; + }, + /** + * Gives focus to the text view. + */ + focus: function() { + /* + * Feature in Chrome. When focus is called in the clientDiv without + * setting selection the browser will set the selection to the first dom + * element, which can be above the client area. When this happen the + * browser also scrolls the window to show that element. + * The fix is to call _updateDOMSelection() before calling focus(). + */ + this._updateDOMSelection(); + if (isPad) { + this._textArea.focus(); + } else { + if (isOpera) { this._clientDiv.blur(); } + this._clientDiv.focus(); + } + /* + * Feature in Safari. When focus is called the browser selects the clientDiv + * itself. The fix is to call _updateDOMSelection() after calling focus(). + */ + this._updateDOMSelection(); + }, + /** + * Returns all action names defined in the text view. + * <p> + * There are two types of actions, the predefined actions of the view + * and the actions added by application code. + * </p> + * <p> + * The predefined actions are: + * <ul> + * <li>Navigation actions. These actions move the caret collapsing the selection.</li> + * <ul> + * <li>"lineUp" - moves the caret up by one line</li> + * <li>"lineDown" - moves the caret down by one line</li> + * <li>"lineStart" - moves the caret to beginning of the current line</li> + * <li>"lineEnd" - moves the caret to end of the current line </li> + * <li>"charPrevious" - moves the caret to the previous character</li> + * <li>"charNext" - moves the caret to the next character</li> + * <li>"pageUp" - moves the caret up by one page</li> + * <li>"pageDown" - moves the caret down by one page</li> + * <li>"wordPrevious" - moves the caret to the previous word</li> + * <li>"wordNext" - moves the caret to the next word</li> + * <li>"textStart" - moves the caret to the beginning of the document</li> + * <li>"textEnd" - moves the caret to the end of the document</li> + * </ul> + * <li>Selection actions. These actions move the caret extending the selection.</li> + * <ul> + * <li>"selectLineUp" - moves the caret up by one line</li> + * <li>"selectLineDown" - moves the caret down by one line</li> + * <li>"selectLineStart" - moves the caret to beginning of the current line</li> + * <li>"selectLineEnd" - moves the caret to end of the current line </li> + * <li>"selectCharPrevious" - moves the caret to the previous character</li> + * <li>"selectCharNext" - moves the caret to the next character</li> + * <li>"selectPageUp" - moves the caret up by one page</li> + * <li>"selectPageDown" - moves the caret down by one page</li> + * <li>"selectWordPrevious" - moves the caret to the previous word</li> + * <li>"selectWordNext" - moves the caret to the next word</li> + * <li>"selectTextStart" - moves the caret to the beginning of the document</li> + * <li>"selectTextEnd" - moves the caret to the end of the document</li> + * <li>"selectAll" - selects the entire document</li> + * </ul> + * <li>Edit actions. These actions modify the text view text</li> + * <ul> + * <li>"deletePrevious" - deletes the character preceding the caret</li> + * <li>"deleteNext" - deletes the charecter following the caret</li> + * <li>"deleteWordPrevious" - deletes the word preceding the caret</li> + * <li>"deleteWordNext" - deletes the word following the caret</li> + * <li>"tab" - inserts a tab character at the caret</li> + * <li>"enter" - inserts a line delimiter at the caret</li> + * </ul> + * <li>Clipboard actions.</li> + * <ul> + * <li>"copy" - copies the selected text to the clipboard</li> + * <li>"cut" - copies the selected text to the clipboard and deletes the selection</li> + * <li>"paste" - replaces the selected text with the clipboard contents</li> + * </ul> + * </ul> + * </p> + * + * @param {Boolean} [defaultAction=false] whether or not the predefined actions are included. + * @returns {String[]} an array of action names defined in the text view. + * + * @see #invokeAction + * @see #setAction + * @see #setKeyBinding + * @see #getKeyBindings + */ + getActions: function (defaultAction) { + var result = []; + var actions = this._actions; + for (var i = 0; i < actions.length; i++) { + if (!defaultAction && actions[i].defaultHandler) { continue; } + result.push(actions[i].name); + } + return result; + }, + /** + * Returns the bottom index. + * <p> + * The bottom index is the line that is currently at the bottom of the view. This + * line may be partially visible depending on the vertical scroll of the view. The parameter + * <code>fullyVisible</code> determines whether to return only fully visible lines. + * </p> + * + * @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the last fully visible line. This + * parameter is ignored if the view is not big enough to show one line. + * @returns {Number} the index of the bottom line. + * + * @see #getTopIndex + * @see #setTopIndex + */ + getBottomIndex: function(fullyVisible) { + return this._getBottomIndex(fullyVisible); + }, + /** + * Returns the bottom pixel. + * <p> + * The bottom pixel is the pixel position that is currently at + * the bottom edge of the view. This position is relative to the + * beginning of the document. + * </p> + * + * @returns {Number} the bottom pixel. + * + * @see #getTopPixel + * @see #setTopPixel + * @see #convert + */ + getBottomPixel: function() { + return this._getScroll().y + this._getClientHeight(); + }, + /** + * Returns the caret offset relative to the start of the document. + * + * @returns the caret offset relative to the start of the document. + * + * @see #setCaretOffset + * @see #setSelection + * @see #getSelection + */ + getCaretOffset: function () { + var s = this._getSelection(); + return s.getCaret(); + }, + /** + * Returns the client area. + * <p> + * The client area is the portion in pixels of the document that is visible. The + * client area position is relative to the beginning of the document. + * </p> + * + * @returns the client area rectangle {x, y, width, height}. + * + * @see #getTopPixel + * @see #getBottomPixel + * @see #getHorizontalPixel + * @see #convert + */ + getClientArea: function() { + var scroll = this._getScroll(); + return {x: scroll.x, y: scroll.y, width: this._getClientWidth(), height: this._getClientHeight()}; + }, + /** + * Returns the horizontal pixel. + * <p> + * The horizontal pixel is the pixel position that is currently at + * the left edge of the view. This position is relative to the + * beginning of the document. + * </p> + * + * @returns {Number} the horizontal pixel. + * + * @see #setHorizontalPixel + * @see #convert + */ + getHorizontalPixel: function() { + return this._getScroll().x; + }, + /** + * Returns all the key bindings associated to the given action name. + * + * @param {String} name the action name. + * @returns {orion.textview.KeyBinding[]} the array of key bindings associated to the given action name. + * + * @see #setKeyBinding + * @see #setAction + */ + getKeyBindings: function (name) { + var result = []; + var keyBindings = this._keyBindings; + for (var i = 0; i < keyBindings.length; i++) { + if (keyBindings[i].name === name) { + result.push(keyBindings[i].keyBinding); + } + } + return result; + }, + /** + * Returns the line height for a given line index. Returns the default line + * height if the line index is not specified. + * + * @param {Number} [lineIndex] the line index. + * @returns {Number} the height of the line in pixels. + * + * @see #getLinePixel + */ + getLineHeight: function(lineIndex) { + return this._getLineHeight(); + }, + /** + * Returns the top pixel position of a given line index relative to the beginning + * of the document. + * <p> + * Clamps out of range indices. + * </p> + * + * @param {Number} lineIndex the line index. + * @returns {Number} the pixel position of the line. + * + * @see #setTopPixel + * @see #convert + */ + getLinePixel: function(lineIndex) { + lineIndex = Math.min(Math.max(0, lineIndex), this._model.getLineCount()); + var lineHeight = this._getLineHeight(); + return lineHeight * lineIndex; + }, + /** + * Returns the {x, y} pixel location of the top-left corner of the character + * bounding box at the specified offset in the document. The pixel location + * is relative to the document. + * <p> + * Clamps out of range offsets. + * </p> + * + * @param {Number} offset the character offset + * @returns the {x, y} pixel location of the given offset. + * + * @see #getOffsetAtLocation + * @see #convert + */ + getLocationAtOffset: function(offset) { + var model = this._model; + offset = Math.min(Math.max(0, offset), model.getCharCount()); + var lineIndex = model.getLineAtOffset(offset); + var scroll = this._getScroll(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var viewPad = this._getViewPadding(); + var x = this._getOffsetToX(offset) + scroll.x - viewRect.left - viewPad.left; + var y = this.getLinePixel(lineIndex); + return {x: x, y: y}; + }, + /** + * Returns the text model of the text view. + * + * @returns {orion.textview.TextModel} the text model of the view. + */ + getModel: function() { + return this._model; + }, + /** + * Returns the character offset nearest to the given pixel location. The + * pixel location is relative to the document. + * + * @param x the x of the location + * @param y the y of the location + * @returns the character offset at the given location. + * + * @see #getLocationAtOffset + */ + getOffsetAtLocation: function(x, y) { + var model = this._model; + var scroll = this._getScroll(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var viewPad = this._getViewPadding(); + var lineIndex = this._getYToLine(y - scroll.y); + x += -scroll.x + viewRect.left + viewPad.left; + var offset = this._getXToOffset(lineIndex, x); + return offset; + }, + /** + * Returns the text view selection. + * <p> + * The selection is defined by a start and end character offset relative to the + * document. The character at end offset is not included in the selection. + * </p> + * + * @returns {orion.textview.Selection} the view selection + * + * @see #setSelection + */ + getSelection: function () { + var s = this._getSelection(); + return {start: s.start, end: s.end}; + }, + /** + * Returns the text for the given range. + * <p> + * The text does not include the character at the end offset. + * </p> + * + * @param {Number} [start=0] the start offset of text range. + * @param {Number} [end=char count] the end offset of text range. + * + * @see #setText + */ + getText: function(start, end) { + var model = this._model; + return model.getText(start, end); + }, + /** + * Returns the top index. + * <p> + * The top index is the line that is currently at the top of the view. This + * line may be partially visible depending on the vertical scroll of the view. The parameter + * <code>fullyVisible</code> determines whether to return only fully visible lines. + * </p> + * + * @param {Boolean} [fullyVisible=false] if <code>true</code>, returns the index of the first fully visible line. This + * parameter is ignored if the view is not big enough to show one line. + * @returns {Number} the index of the top line. + * + * @see #getBottomIndex + * @see #setTopIndex + */ + getTopIndex: function(fullyVisible) { + return this._getTopIndex(fullyVisible); + }, + /** + * Returns the top pixel. + * <p> + * The top pixel is the pixel position that is currently at + * the top edge of the view. This position is relative to the + * beginning of the document. + * </p> + * + * @returns {Number} the top pixel. + * + * @see #getBottomPixel + * @see #setTopPixel + * @see #convert + */ + getTopPixel: function() { + return this._getScroll().y; + }, + /** + * Executes the action handler associated with the given name. + * <p> + * The application defined action takes precedence over predefined actions unless + * the <code>defaultAction</code> paramater is <code>true</code>. + * </p> + * <p> + * If the application defined action returns <code>false</code>, the text view predefined + * action is executed if present. + * </p> + * + * @param {String} name the action name. + * @param {Boolean} [defaultAction] whether to always execute the predefined action. + * @returns {Boolean} <code>true</code> if the action was executed. + * + * @see #setAction + * @see #getActions + */ + invokeAction: function (name, defaultAction) { + var actions = this._actions; + for (var i = 0; i < actions.length; i++) { + var a = actions[i]; + if (a.name && a.name === name) { + if (!defaultAction && a.userHandler) { + if (a.userHandler()) { return; } + } + if (a.defaultHandler) { return a.defaultHandler(); } + return false; + } + } + return false; + }, + /** + * @class This is the event sent when the user right clicks or otherwise invokes the context menu of the view. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onContextMenu} + * </p> + * + * @name orion.textview.ContextMenuEvent + * + * @property {Number} x The pointer location on the x axis, relative to the document the user is editing. + * @property {Number} y The pointer location on the y axis, relative to the document the user is editing. + * @property {Number} screenX The pointer location on the x axis, relative to the screen. This is copied from the DOM contextmenu event.screenX property. + * @property {Number} screenY The pointer location on the y axis, relative to the screen. This is copied from the DOM contextmenu event.screenY property. + */ + /** + * This event is sent when the user invokes the view context menu. + * + * @event + * @param {orion.textview.ContextMenuEvent} contextMenuEvent the event + */ + onContextMenu: function(contextMenuEvent) { + this._eventTable.sendEvent("ContextMenu", contextMenuEvent); + }, + /** + * @class This is the event sent when the text view is destroyed. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onDestroy} + * </p> + * @name orion.textview.DestroyEvent + */ + /** + * This event is sent when the text view has been destroyed. + * + * @event + * @param {orion.textview.DestroyEvent} destroyEvent the event + * + * @see #destroy + */ + onDestroy: function(destroyEvent) { + this._eventTable.sendEvent("Destroy", destroyEvent); + }, + /** + * @class This object is used to define style information for the text view. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onLineStyle} + * </p> + * @name orion.textview.Style + * + * @property {String} styleClass A CSS class name. + * @property {Object} style An object with CSS properties. + */ + /** + * @class This object is used to style range. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onLineStyle} + * </p> + * @name orion.textview.StyleRange + * + * @property {Number} start The start character offset, relative to the document, where the style should be applied. + * @property {Number} end The end character offset (exclusive), relative to the document, where the style should be applied. + * @property {orion.textview.Style} style The style for the range. + */ + /** + * @class This is the event sent when the text view needs the style information for a line. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onLineStyle} + * </p> + * @name orion.textview.LineStyleEvent + * + * @property {Number} lineIndex The line index. + * @property {String} lineText The line text. + * @property {Number} lineStart The character offset, relative to document, of the first character in the line. + * @property {orion.textview.Style} style The style for the entire line (output argument). + * @property {orion.textview.StyleRange[]} ranges An array of style ranges for the line (output argument). + */ + /** + * This event is sent when the text view needs the style information for a line. + * + * @event + * @param {orion.textview.LineStyleEvent} lineStyleEvent the event + */ + onLineStyle: function(lineStyleEvent) { + this._eventTable.sendEvent("LineStyle", lineStyleEvent); + }, + /** + * @class This is the event sent when the text in the model has changed. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onModelChanged}<br/> + * {@link orion.textview.TextModel#onChanged} + * </p> + * @name orion.textview.ModelChangedEvent + * + * @property {Number} start The character offset in the model where the change has occurred. + * @property {Number} removedCharCount The number of characters removed from the model. + * @property {Number} addedCharCount The number of characters added to the model. + * @property {Number} removedLineCount The number of lines removed from the model. + * @property {Number} addedLineCount The number of lines added to the model. + */ + /** + * This event is sent when the text in the model has changed. + * + * @event + * @param {orion.textview.ModelChangingEvent} modelChangingEvent the event + */ + onModelChanged: function(modelChangedEvent) { + this._eventTable.sendEvent("ModelChanged", modelChangedEvent); + }, + /** + * @class This is the event sent when the text in the model is about to change. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onModelChanging}<br/> + * {@link orion.textview.TextModel#onChanging} + * </p> + * @name orion.textview.ModelChangingEvent + * + * @property {String} text The text that is about to be inserted in the model. + * @property {Number} start The character offset in the model where the change will occur. + * @property {Number} removedCharCount The number of characters being removed from the model. + * @property {Number} addedCharCount The number of characters being added to the model. + * @property {Number} removedLineCount The number of lines being removed from the model. + * @property {Number} addedLineCount The number of lines being added to the model. + */ + /** + * This event is sent when the text in the model is about to change. + * + * @event + * @param {orion.textview.ModelChangingEvent} modelChangingEvent the event + */ + onModelChanging: function(modelChangingEvent) { + this._eventTable.sendEvent("ModelChanging", modelChangingEvent); + }, + /** + * @class This is the event sent when the text is modified by the text view. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onModify} + * </p> + * @name orion.textview.ModifyEvent + */ + /** + * This event is sent when the text view has changed text in the model. + * <p> + * If the text is changed directly through the model API, this event + * is not sent. + * </p> + * + * @event + * @param {orion.textview.ModifyEvent} modifyEvent the event + */ + onModify: function(modifyEvent) { + this._eventTable.sendEvent("Modify", modifyEvent); + }, + /** + * @class This is the event sent when the selection changes in the text view. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onSelection} + * </p> + * @name orion.textview.SelectionEvent + * + * @property {orion.textview.Selection} oldValue The old selection. + * @property {orion.textview.Selection} newValue The new selection. + */ + /** + * This event is sent when the text view selection has changed. + * + * @event + * @param {orion.textview.SelectionEvent} selectionEvent the event + */ + onSelection: function(selectionEvent) { + this._eventTable.sendEvent("Selection", selectionEvent); + }, + /** + * @class This is the event sent when the text view scrolls. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onScroll} + * </p> + * @name orion.textview.ScrollEvent + * + * @property oldValue The old scroll {x,y}. + * @property newValue The new scroll {x,y}. + */ + /** + * This event is sent when the text view scrolls vertically or horizontally. + * + * @event + * @param {orion.textview.ScrollEvent} scrollEvent the event + */ + onScroll: function(scrollEvent) { + this._eventTable.sendEvent("Scroll", scrollEvent); + }, + /** + * @class This is the event sent when the text is about to be modified by the text view. + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * {@link orion.textview.TextView#event:onVerify} + * </p> + * @name orion.textview.VerifyEvent + * + * @property {String} text The text being inserted. + * @property {Number} start The start offset of the text range to be replaced. + * @property {Number} end The end offset (exclusive) of the text range to be replaced. + */ + /** + * This event is sent when the text view is about to change text in the model. + * <p> + * If the text is changed directly through the model API, this event + * is not sent. + * </p> + * <p> + * Listeners are allowed to change these parameters. Setting text to null + * or undefined stops the change. + * </p> + * + * @event + * @param {orion.textview.VerifyEvent} verifyEvent the event + */ + onVerify: function(verifyEvent) { + this._eventTable.sendEvent("Verify", verifyEvent); + }, + /** + * Redraws the text in the given line range. + * <p> + * The line at the end index is not redrawn. + * </p> + * + * @param {Number} [startLine=0] the start line + * @param {Number} [endLine=line count] the end line + */ + redrawLines: function(startLine, endLine, ruler) { + if (startLine === undefined) { startLine = 0; } + if (endLine === undefined) { endLine = this._model.getLineCount(); } + if (startLine === endLine) { return; } + var div = this._clientDiv; + if (ruler) { + var location = ruler.getLocation();//"left" or "right" + var divRuler = location === "left" ? this._leftDiv : this._rightDiv; + var cells = divRuler.firstChild.rows[0].cells; + for (var i = 0; i < cells.length; i++) { + if (cells[i].firstChild._ruler === ruler) { + div = cells[i].firstChild; + break; + } + } + } + if (ruler) { + div.rulerChanged = true; + } + if (!ruler || ruler.getOverview() === "page") { + var child = div.firstChild; + while (child) { + var lineIndex = child.lineIndex; + if (startLine <= lineIndex && lineIndex < endLine) { + child.lineChanged = true; + } + child = child.nextSibling; + } + } + if (!ruler) { + if (startLine <= this._maxLineIndex && this._maxLineIndex < endLine) { + this._maxLineIndex = -1; + this._maxLineWidth = 0; + } + } + this._queueUpdatePage(); + }, + /** + * Redraws the text in the given range. + * <p> + * The character at the end offset is not redrawn. + * </p> + * + * @param {Number} [start=0] the start offset of text range + * @param {Number} [end=char count] the end offset of text range + */ + redrawRange: function(start, end) { + var model = this._model; + if (start === undefined) { start = 0; } + if (end === undefined) { end = model.getCharCount(); } + if (start === end) { return; } + var startLine = model.getLineAtOffset(start); + var endLine = model.getLineAtOffset(Math.max(0, end - 1)) + 1; + this.redrawLines(startLine, endLine); + }, + /** + * Removes an event listener from the text view. + * <p> + * All the parameters must be the same ones used to add the listener. + * </p> + * + * @param {String} type the event type. + * @param {Object} context the context of the function. + * @param {Function} func the function that will be executed when the event happens. + * @param {Object} [data] optional data passed to the function. + * + * @see #addEventListener + */ + removeEventListener: function(type, context, func, data) { + this._eventTable.removeEventListener(type, context, func, data); + }, + /** + * Removes a ruler from the text view. + * + * @param {orion.textview.Ruler} ruler the ruler. + */ + removeRuler: function (ruler) { + ruler.setView(null); + var side = ruler.getLocation(); + var rulerParent = side === "left" ? this._leftDiv : this._rightDiv; + var row = rulerParent.firstChild.rows[0]; + var cells = row.cells; + for (var index = 0; index < cells.length; index++) { + var cell = cells[index]; + if (cell.firstChild._ruler === ruler) { break; } + } + if (index === cells.length) { return; } + row.cells[index]._ruler = undefined; + row.deleteCell(index); + this._updatePage(); + }, + /** + * Associates an application defined handler to an action name. + * <p> + * If the action name is a predefined action, the given handler executes before + * the default action handler. If the given handler returns <code>true</code>, the + * default action handler is not called. + * </p> + * + * @param {String} name the action name. + * @param {Function} handler the action handler. + * + * @see #getActions + * @see #invokeAction + */ + setAction: function(name, handler) { + if (!name) { return; } + var actions = this._actions; + for (var i = 0; i < actions.length; i++) { + var a = actions[i]; + if (a.name === name) { + a.userHandler = handler; + return; + } + } + actions.push({name: name, userHandler: handler}); + }, + /** + * Associates a key binding with the given action name. Any previous + * association with the specified key binding is overwriten. If the + * action name is <code>null</code>, the association is removed. + * + * @param {orion.textview.KeyBinding} keyBinding the key binding + * @param {String} name the action + */ + setKeyBinding: function(keyBinding, name) { + var keyBindings = this._keyBindings; + for (var i = 0; i < keyBindings.length; i++) { + var kb = keyBindings[i]; + if (kb.keyBinding.equals(keyBinding)) { + if (name) { + kb.name = name; + } else { + if (kb.predefined) { + kb.name = null; + } else { + var oldName = kb.name; + keyBindings.splice(i, 1); + var index = 0; + while (index < keyBindings.length && oldName !== keyBindings[index].name) { + index++; + } + if (index === keyBindings.length) { + /* <p> + * Removing all the key bindings associated to an user action will cause + * the user action to be removed. TextView predefined actions are never + * removed (so they can be reinstalled in the future). + * </p> + */ + var actions = this._actions; + for (var j = 0; j < actions.length; j++) { + if (actions[j].name === oldName) { + if (!actions[j].defaultHandler) { + actions.splice(j, 1); + } + } + } + } + } + } + return; + } + } + if (name) { + keyBindings.push({keyBinding: keyBinding, name: name}); + } + }, + /** + * Sets the caret offset relative to the start of the document. + * + * @param {Number} caret the caret offset relative to the start of the document. + * @param {Boolean} [show=true] if <code>true</coce>, the view will scroll if needed to show the caret location. + * + * @see #getCaretOffset + * @see #setSelection + * @see #getSelection + */ + setCaretOffset: function(offset, show) { + var charCount = this._model.getCharCount(); + offset = Math.max(0, Math.min (offset, charCount)); + var selection = new Selection(offset, offset, false); + this._setSelection (selection, show === undefined || show); + }, + /** + * Sets the horizontal pixel. + * <p> + * The horizontal pixel is the pixel position that is currently at + * the left edge of the view. This position is relative to the + * beginning of the document. + * </p> + * + * @param {Number} pixel the horizontal pixel. + * + * @see #getHorizontalPixel + * @see #convert + */ + setHorizontalPixel: function(pixel) { + pixel = Math.max(0, pixel); + this._scrollView(pixel - this._getScroll().x, 0); + }, + /** + * Sets the text model of the text view. + * + * @param {orion.textview.TextModel} model the text model of the view. + */ + setModel: function(model) { + if (!model) { return; } + this._model.removeListener(this._modelListener); + var oldLineCount = this._model.getLineCount(); + var oldCharCount = this._model.getCharCount(); + var newLineCount = model.getLineCount(); + var newCharCount = model.getCharCount(); + var newText = model.getText(); + var e = { + text: newText, + start: 0, + removedCharCount: oldCharCount, + addedCharCount: newCharCount, + removedLineCount: oldLineCount, + addedLineCount: newLineCount + }; + this.onModelChanging(e); + this.redrawRange(); + this._model = model; + e = { + start: 0, + removedCharCount: oldCharCount, + addedCharCount: newCharCount, + removedLineCount: oldLineCount, + addedLineCount: newLineCount + }; + this.onModelChanged(e); + this._model.addListener(this._modelListener); + this.redrawRange(); + }, + /** + * Sets the text view selection. + * <p> + * The selection is defined by a start and end character offset relative to the + * document. The character at end offset is not included in the selection. + * </p> + * <p> + * The caret is always placed at the end offset. The start offset can be + * greater than the end offset to place the caret at the beginning of the + * selection. + * </p> + * <p> + * Clamps out of range offsets. + * </p> + * + * @param {Number} start the start offset of the selection + * @param {Number} end the end offset of the selection + * @param {Boolean} [show=true] if <code>true</coce>, the view will scroll if needed to show the caret location. + * + * @see #getSelection + */ + setSelection: function (start, end, show) { + var caret = start > end; + if (caret) { + var tmp = start; + start = end; + end = tmp; + } + var charCount = this._model.getCharCount(); + start = Math.max(0, Math.min (start, charCount)); + end = Math.max(0, Math.min (end, charCount)); + var selection = new Selection(start, end, caret); + this._setSelection(selection, show === undefined || show); + }, + /** + * Replaces the text in the given range with the given text. + * <p> + * The character at the end offset is not replaced. + * </p> + * <p> + * When both <code>start</code> and <code>end</code> parameters + * are not specified, the text view places the caret at the beginning + * of the document and scrolls to make it visible. + * </p> + * + * @param {String} text the new text. + * @param {Number} [start=0] the start offset of text range. + * @param {Number} [end=char count] the end offset of text range. + * + * @see #getText + */ + setText: function (text, start, end) { + var reset = start === undefined && end === undefined; + if (start === undefined) { start = 0; } + if (end === undefined) { end = this._model.getCharCount(); } + this._modifyContent({text: text, start: start, end: end, _code: true}, !reset); + if (reset) { + this._columnX = -1; + this._setSelection(new Selection (0, 0, false), true); + + /* + * Bug in Firefox. For some reason, the caret does not show after the + * view is refreshed. The fix is to toggle the contentEditable state and + * force the clientDiv to loose and receive focus if the it is focused. + */ + if (isFirefox) { + var hasFocus = this._hasFocus; + var clientDiv = this._clientDiv; + if (hasFocus) { clientDiv.blur(); } + clientDiv.contentEditable = false; + clientDiv.contentEditable = true; + if (hasFocus) { clientDiv.focus(); } + } + } + }, + /** + * Sets the top index. + * <p> + * The top index is the line that is currently at the top of the text view. This + * line may be partially visible depending on the vertical scroll of the view. + * </p> + * + * @param {Number} topIndex the index of the top line. + * + * @see #getBottomIndex + * @see #getTopIndex + */ + setTopIndex: function(topIndex) { + var model = this._model; + if (model.getCharCount() === 0) { + return; + } + var lineCount = model.getLineCount(); + var lineHeight = this._getLineHeight(); + var pageSize = Math.max(1, Math.min(lineCount, Math.floor(this._getClientHeight () / lineHeight))); + if (topIndex < 0) { + topIndex = 0; + } else if (topIndex > lineCount - pageSize) { + topIndex = lineCount - pageSize; + } + var pixel = topIndex * lineHeight - this._getScroll().y; + this._scrollView(0, pixel); + }, + /** + * Sets the top pixel. + * <p> + * The top pixel is the pixel position that is currently at + * the top edge of the view. This position is relative to the + * beginning of the document. + * </p> + * + * @param {Number} pixel the top pixel. + * + * @see #getBottomPixel + * @see #getTopPixel + * @see #convert + */ + setTopPixel: function(pixel) { + var lineHeight = this._getLineHeight(); + var clientHeight = this._getClientHeight(); + var lineCount = this._model.getLineCount(); + pixel = Math.min(Math.max(0, pixel), lineHeight * lineCount - clientHeight); + this._scrollView(0, pixel - this._getScroll().y); + }, + /** + * Scrolls the selection into view if needed. + * + * @see #getSelection + * @see #setSelection + */ + showSelection: function() { + return this._showCaret(); + }, + + /**************************************** Event handlers *********************************/ + _handleBodyMouseDown: function (e) { + if (!e) { e = window.event; } + /* + * Prevent clicks outside of the view from taking focus + * away the view. Note that in Firefox and Opera clicking on the + * scrollbar also take focus from the view. Other browsers + * do not have this problem and stopping the click over the + * scrollbar for them causes mouse capture problems. + */ + var topNode = isOpera ? this._clientDiv : this._overlayDiv || this._viewDiv; + + var temp = e.target ? e.target : e.srcElement; + while (temp) { + if (topNode === temp) { + return; + } + temp = temp.parentNode; + } + if (e.preventDefault) { e.preventDefault(); } + if (e.stopPropagation){ e.stopPropagation(); } + if (!isW3CEvents) { + /* In IE 8 is not possible to prevent the default handler from running + * during mouse down event using usual API. The workaround is to use + * setCapture/releaseCapture. + */ + topNode.setCapture(); + setTimeout(function() { topNode.releaseCapture(); }, 0); + } + }, + _handleBlur: function (e) { + if (!e) { e = window.event; } + this._hasFocus = false; + /* + * Bug in IE 8 and earlier. For some reason when text is deselected + * the overflow selection at the end of some lines does not get redrawn. + * The fix is to create a DOM element in the body to force a redraw. + */ + if (isIE < 9) { + if (!this._getSelection().isEmpty()) { + var document = this._frameDocument; + var child = document.createElement("DIV"); + var body = document.body; + body.appendChild(child); + body.removeChild(child); + } + } + if (isFirefox || isIE) { + if (this._selDiv1) { + var color = isIE ? "transparent" : "#AFAFAF"; + this._selDiv1.style.background = color; + this._selDiv2.style.background = color; + this._selDiv3.style.background = color; + } + } + }, + _handleContextMenu: function (e) { + if (!e) { e = window.event; } + var scroll = this._getScroll(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var viewPad = this._getViewPadding(); + var x = e.clientX + scroll.x - viewRect.left - viewPad.left; + var y = e.clientY + scroll.y - viewRect.top - viewPad.top; + this.onContextMenu({x: x, y: y, screenX: e.screenX, screenY: e.screenY}); + if (e.preventDefault) { e.preventDefault(); } + return false; + }, + _handleCopy: function (e) { + if (this._ignoreCopy) { return; } + if (!e) { e = window.event; } + if (this._doCopy(e)) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleCut: function (e) { + if (!e) { e = window.event; } + if (this._doCut(e)) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleDataModified: function(e) { + this._startIME(); + }, + _handleDblclick: function (e) { + if (!e) { e = window.event; } + var time = e.timeStamp ? e.timeStamp : new Date().getTime(); + this._lastMouseTime = time; + if (this._clickCount !== 2) { + this._clickCount = 2; + this._handleMouse(e); + } + }, + _handleDragStart: function (e) { + if (!e) { e = window.event; } + if (e.preventDefault) { e.preventDefault(); } + return false; + }, + _handleDragOver: function (e) { + if (!e) { e = window.event; } + e.dataTransfer.dropEffect = "none"; + if (e.preventDefault) { e.preventDefault(); } + return false; + }, + _handleDrop: function (e) { + if (!e) { e = window.event; } + if (e.preventDefault) { e.preventDefault(); } + return false; + }, + _handleDocFocus: function (e) { + if (!e) { e = window.event; } + this._clientDiv.focus(); + }, + _handleFocus: function (e) { + if (!e) { e = window.event; } + this._hasFocus = true; + /* + * Feature in IE. The selection is not restored when the + * view gets focus and the caret is always placed at the + * beginning of the document. The fix is to update the DOM + * selection during the focus event. + */ + if (isIE) { + this._updateDOMSelection(); + } + if (isFirefox || isIE) { + if (this._selDiv1) { + var color = this._hightlightRGB; + this._selDiv1.style.background = color; + this._selDiv2.style.background = color; + this._selDiv3.style.background = color; + } + } + }, + _handleKeyDown: function (e) { + if (!e) { e = window.event; } + if (isPad) { + if (e.keyCode === 8) { + this._doBackspace({}); + e.preventDefault(); + } + return; + } + if (e.keyCode === 229) { + if (this.readonly) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + this._startIME(); + } else { + this._commitIME(); + } + /* + * Feature in Firefox. When a key is held down the browser sends + * right number of keypress events but only one keydown. This is + * unexpected and causes the view to only execute an action + * just one time. The fix is to ignore the keydown event and + * execute the actions from the keypress handler. + * Note: This only happens on the Mac and Linux (Firefox 3.6). + * + * Feature in Opera. Opera sends keypress events even for non-printable + * keys. The fix is to handle actions in keypress instead of keydown. + */ + if (((isMac || isLinux) && isFirefox < 4) || isOpera) { + this._keyDownEvent = e; + return true; + } + + if (this._doAction(e)) { + if (e.preventDefault) { + e.preventDefault(); + } else { + e.cancelBubble = true; + e.returnValue = false; + e.keyCode = 0; + } + return false; + } + }, + _handleKeyPress: function (e) { + if (!e) { e = window.event; } + /* + * Feature in Embedded WebKit. Embedded WekKit on Mac runs in compatibility mode and + * generates key press events for these Unicode values (Function keys). This does not + * happen in Safari or Chrome. The fix is to ignore these key events. + */ + if (isMac && isWebkit) { + if ((0xF700 <= e.keyCode && e.keyCode <= 0xF7FF) || e.keyCode === 13 || e.keyCode === 8) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + } + if (((isMac || isLinux) && isFirefox < 4) || isOpera) { + if (this._doAction(this._keyDownEvent)) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + } + var ctrlKey = isMac ? e.metaKey : e.ctrlKey; + if (e.charCode !== undefined) { + if (ctrlKey) { + switch (e.charCode) { + /* + * In Firefox and Safari if ctrl+v, ctrl+c ctrl+x is canceled + * the clipboard events are not sent. The fix to allow + * the browser to handles these key events. + */ + case 99://c + case 118://v + case 120://x + return true; + } + } + } + var ignore = false; + if (isMac) { + if (e.ctrlKey || e.metaKey) { ignore = true; } + } else { + if (isFirefox) { + //Firefox clears the state mask when ALT GR generates input + if (e.ctrlKey || e.altKey) { ignore = true; } + } else { + //IE and Chrome only send ALT GR when input is generated + if (e.ctrlKey ^ e.altKey) { ignore = true; } + } + } + if (!ignore) { + var key = isOpera ? e.which : (e.charCode !== undefined ? e.charCode : e.keyCode); + if (key !== 0) { + this._doContent(String.fromCharCode (key)); + if (e.preventDefault) { e.preventDefault(); } + return false; + } + } + }, + _handleKeyUp: function (e) { + if (!e) { e = window.event; } + + // don't commit for space (it happens during JP composition) + if (e.keyCode === 13) { + this._commitIME(); + } + }, + _handleMouse: function (e) { + var target = this._frameWindow; + if (isIE) { target = this._clientDiv; } + if (this._overlayDiv) { + var self = this; + setTimeout(function () { + self.focus(); + }, 0); + } + if (this._clickCount === 1) { + this._setGrab(target); + this._setSelectionTo(e.clientX, e.clientY, e.shiftKey); + } else { + /* + * Feature in IE8 and older, the sequence of events in the IE8 event model + * for a doule-click is: + * + * down + * up + * up + * dblclick + * + * Given that the mouse down/up events are not balanced, it is not possible to + * grab on mouse down and ungrab on mouse up. The fix is to grab on the first + * mouse down and ungrab on mouse move when the button 1 is not set. + */ + if (isW3CEvents) { this._setGrab(target); } + + this._doubleClickSelection = null; + this._setSelectionTo(e.clientX, e.clientY, e.shiftKey); + this._doubleClickSelection = this._getSelection(); + } + }, + _handleMouseDown: function (e) { + if (!e) { e = window.event; } + var left = e.which ? e.button === 0 : e.button === 1; + this._commitIME(); + if (left) { + this._isMouseDown = true; + var deltaX = Math.abs(this._lastMouseX - e.clientX); + var deltaY = Math.abs(this._lastMouseY - e.clientY); + var time = e.timeStamp ? e.timeStamp : new Date().getTime(); + if ((time - this._lastMouseTime) <= this._clickTime && deltaX <= this._clickDist && deltaY <= this._clickDist) { + this._clickCount++; + } else { + this._clickCount = 1; + } + this._lastMouseX = e.clientX; + this._lastMouseY = e.clientY; + this._lastMouseTime = time; + this._handleMouse(e); + if (isOpera) { + if (!this._hasFocus) { + this.focus(); + } + e.preventDefault(); + } + } + }, + _handleMouseMove: function (e) { + if (!e) { e = window.event; } + /* + * Feature in IE8 and older, the sequence of events in the IE8 event model + * for a doule-click is: + * + * down + * up + * up + * dblclick + * + * Given that the mouse down/up events are not balanced, it is not possible to + * grab on mouse down and ungrab on mouse up. The fix is to grab on the first + * mouse down and ungrab on mouse move when the button 1 is not set. + * + * In order to detect double-click and drag gestures, it is necessary to send + * a mouse down event from mouse move when the button is still down and isMouseDown + * flag is not set. + */ + if (!isW3CEvents) { + if (e.button === 0) { + this._setGrab(null); + return true; + } + if (!this._isMouseDown && e.button === 1 && (this._clickCount & 1) !== 0) { + this._clickCount = 2; + return this._handleMouse(e, this._clickCount); + } + } + + var x = e.clientX; + var y = e.clientY; + var viewPad = this._getViewPadding(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var width = this._getClientWidth (), height = this._getClientHeight(); + var leftEdge = viewRect.left + viewPad.left; + var topEdge = viewRect.top + viewPad.top; + var rightEdge = viewRect.left + viewPad.left + width; + var bottomEdge = viewRect.top + viewPad.top + height; + var model = this._model; + var caretLine = model.getLineAtOffset(this._getSelection().getCaret()); + if (y < topEdge && caretLine !== 0) { + this._doAutoScroll("up", x, y - topEdge); + } else if (y > bottomEdge && caretLine !== model.getLineCount() - 1) { + this._doAutoScroll("down", x, y - bottomEdge); + } else if (x < leftEdge) { + this._doAutoScroll("left", x - leftEdge, y); + } else if (x > rightEdge) { + this._doAutoScroll("right", x - rightEdge, y); + } else { + this._endAutoScroll(); + this._setSelectionTo(x, y, true); + /* + * Feature in IE. IE does redraw the selection background right + * away after the selection changes because of mouse move events. + * The fix is to call getBoundingClientRect() on the + * body element to force the selection to be redraw. Some how + * calling this method forces a redraw. + */ + if (isIE) { + var body = this._frameDocument.body; + body.getBoundingClientRect(); + } + } + }, + _handleMouseUp: function (e) { + if (!e) { e = window.event; } + this._endAutoScroll(); + var left = e.which ? e.button === 0 : e.button === 1; + if (left) { + this._isMouseDown=false; + + /* + * Feature in IE8 and older, the sequence of events in the IE8 event model + * for a doule-click is: + * + * down + * up + * up + * dblclick + * + * Given that the mouse down/up events are not balanced, it is not possible to + * grab on mouse down and ungrab on mouse up. The fix is to grab on the first + * mouse down and ungrab on mouse move when the button 1 is not set. + */ + if (isW3CEvents) { this._setGrab(null); } + } + }, + _handleMouseWheel: function (e) { + if (!e) { e = window.event; } + var lineHeight = this._getLineHeight(); + var pixelX = 0, pixelY = 0; + // Note: On the Mac the correct behaviour is to scroll by pixel. + if (isFirefox) { + var pixel; + if (isMac) { + pixel = e.detail * 3; + } else { + var limit = 256; + pixel = Math.max(-limit, Math.min(limit, e.detail)) * lineHeight; + } + if (e.axis === e.HORIZONTAL_AXIS) { + pixelX = pixel; + } else { + pixelY = pixel; + } + } else { + //Webkit + if (isMac) { + /* + * In Safari, the wheel delta is a multiple of 120. In order to + * convert delta to pixel values, it is necessary to divide delta + * by 40. + * + * In Chrome, the wheel delta depends on the type of the mouse. In + * general, it is the pixel value for Mac mice and track pads, but + * it is a multiple of 120 for other mice. There is no presise + * way to determine if it is pixel value or a multiple of 120. + * + * Note that the current approach does not calculate the correct + * pixel value for Mac mice when the delta is a multiple of 120. + */ + var denominatorX = 40, denominatorY = 40; + if (isChrome) { + if (e.wheelDeltaX % 120 !== 0) { denominatorX = 1; } + if (e.wheelDeltaY % 120 !== 0) { denominatorY = 1; } + } + pixelX = -e.wheelDeltaX / denominatorX; + if (-1 < pixelX && pixelX < 0) { pixelX = -1; } + if (0 < pixelX && pixelX < 1) { pixelX = 1; } + pixelY = -e.wheelDeltaY / denominatorY; + if (-1 < pixelY && pixelY < 0) { pixelY = -1; } + if (0 < pixelY && pixelY < 1) { pixelY = 1; } + } else { + pixelX = -e.wheelDeltaX; + var linesToScroll = 8; + pixelY = (-e.wheelDeltaY / 120 * linesToScroll) * lineHeight; + } + } + /* + * Feature in Safari. If the event target is removed from the DOM + * safari stops smooth scrolling. The fix is keep the element target + * in the DOM and remove it on a later time. + * + * Note: Using a timer is not a solution, because the timeout needs to + * be at least as long as the gesture (which is too long). + */ + if (isSafari) { + var lineDiv = e.target; + while (lineDiv && lineDiv.lineIndex === undefined) { + lineDiv = lineDiv.parentNode; + } + this._mouseWheelLine = lineDiv; + } + var oldScroll = this._getScroll(); + this._scrollView(pixelX, pixelY); + var newScroll = this._getScroll(); + if (isSafari) { this._mouseWheelLine = null; } + if (oldScroll.x !== newScroll.x || oldScroll.y !== newScroll.y) { + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handlePaste: function (e) { + if (this._ignorePaste) { return; } + if (!e) { e = window.event; } + if (this._doPaste(e)) { + if (isIE) { + /* + * Bug in IE, + */ + var self = this; + setTimeout(function() {self._updateDOMSelection();}, 0); + } + if (e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleResize: function (e) { + if (!e) { e = window.event; } + var element = this._frameDocument.documentElement; + var newWidth = element.clientWidth; + var newHeight = element.clientHeight; + if (this._frameWidth !== newWidth || this._frameHeight !== newHeight) { + this._frameWidth = newWidth; + this._frameHeight = newHeight; + this._updatePage(); + } + }, + _handleRulerEvent: function (e) { + if (!e) { e = window.event; } + var target = e.target ? e.target : e.srcElement; + var lineIndex = target.lineIndex; + var element = target; + while (element && !element._ruler) { + if (lineIndex === undefined && element.lineIndex !== undefined) { + lineIndex = element.lineIndex; + } + element = element.parentNode; + } + var ruler = element ? element._ruler : null; + if (isPad && lineIndex === undefined && ruler && ruler.getOverview() === "document") { + var buttonHeight = 17; + var clientHeight = this._getClientHeight (); + var lineHeight = this._getLineHeight (); + var viewPad = this._getViewPadding(); + var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight; + var pixels = this._model.getLineCount () * lineHeight; + this.setTopPixel(Math.floor((e.clientY - buttonHeight - lineHeight) * pixels / trackHeight)); + } + if (ruler) { + switch (e.type) { + case "click": + if (ruler.onClick) { ruler.onClick(lineIndex, e); } + break; + case "dblclick": + if (ruler.onDblClick) { ruler.onDblClick(lineIndex, e); } + break; + } + } + }, + _handleScroll: function () { + this._doScroll(this._getScroll()); + }, + _handleSelectStart: function (e) { + if (!e) { e = window.event; } + if (this._ignoreSelect) { + if (e && e.preventDefault) { e.preventDefault(); } + return false; + } + }, + _handleInput: function (e) { + var textArea = this._textArea; + this._doContent(textArea.value); + textArea.selectionStart = textArea.selectionEnd = 0; + textArea.value = ""; + e.preventDefault(); + }, + _handleTextInput: function (e) { + this._doContent(e.data); + e.preventDefault(); + }, + _touchConvert: function (touch) { + var rect = this._frame.getBoundingClientRect(); + var body = this._parentDocument.body; + return {left: touch.clientX - rect.left - body.scrollLeft, top: touch.clientY - rect.top - body.scrollTop}; + }, + _handleTouchStart: function (e) { + var touches = e.touches, touch, pt, sel; + this._touchMoved = false; + this._touchStartScroll = undefined; + if (touches.length === 1) { + touch = touches[0]; + var pageX = touch.pageX; + var pageY = touch.pageY; + this._touchStartX = pageX; + this._touchStartY = pageY; + this._touchStartTime = e.timeStamp; + this._touchStartScroll = this._getScroll(); + sel = this._getSelection(); + pt = this._touchConvert(touches[0]); + this._touchGesture = "none"; + if (!sel.isEmpty()) { + if (this._hitOffset(sel.end, pt.left, pt.top)) { + this._touchGesture = "extendEnd"; + } else if (this._hitOffset(sel.start, pt.left, pt.top)) { + this._touchGesture = "extendStart"; + } + } + if (this._touchGesture === "none") { + var textArea = this._textArea; + textArea.value = ""; + textArea.style.left = "-1000px"; + textArea.style.top = "-1000px"; + textArea.style.width = "3000px"; + textArea.style.height = "3000px"; + var self = this; + var f = function() { + self._touchTimeout = null; + self._clickCount = 1; + self._setSelectionTo(pt.left, pt.top, false); + }; + this._touchTimeout = setTimeout(f, 200); + } + } else if (touches.length === 2) { + this._touchGesture = "select"; + if (this._touchTimeout) { + clearTimeout(this._touchTimeout); + this._touchTimeout = null; + } + pt = this._touchConvert(touches[0]); + var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + pt = this._touchConvert(touches[1]); + var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + sel = this._getSelection(); + sel.setCaret(offset1); + sel.extend(offset2); + this._setSelection(sel, true, true); + } + //Cannot prevent to show maginifier +// e.preventDefault(); + }, + _handleTouchMove: function (e) { + this._touchMoved = true; + var touches = e.touches, pt, sel; + if (touches.length === 1) { + var touch = touches[0]; + var pageX = touch.pageX; + var pageY = touch.pageY; + var deltaX = this._touchStartX - pageX; + var deltaY = this._touchStartY - pageY; + pt = this._touchConvert(touch); + sel = this._getSelection(); + if (this._touchTimeout) { + clearTimeout(this._touchTimeout); + this._touchTimeout = null; + } + if (this._touchGesture === "none") { + if ((e.timeStamp - this._touchStartTime) < 200 && (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5)) { + this._touchGesture = "scroll"; + } else { + this._touchGesture = "caret"; + } + } + if (this._touchGesture === "select") { + if (this._hitOffset(sel.end, pt.left, pt.top)) { + this._touchGesture = "extendEnd"; + } else if (this._hitOffset(sel.start, pt.left, pt.top)) { + this._touchGesture = "extendStart"; + } else { + this._touchGesture = "caret"; + } + } + switch (this._touchGesture) { + case "scroll": + this._touchStartX = pageX; + this._touchStartY = pageY; + this._scrollView(deltaX, deltaY); + break; + case "extendStart": + case "extendEnd": + this._clickCount = 1; + var lineIndex = this._getYToLine(pt.top); + var offset = this._getXToOffset(lineIndex, pt.left); + sel.setCaret(this._touchGesture === "extendStart" ? sel.end : sel.start); + sel.extend(offset); + if (offset >= sel.end && this._touchGesture === "extendStart") { + this._touchGesture = "extendEnd"; + } + if (offset <= sel.start && this._touchGesture === "extendEnd") { + this._touchGesture = "extendStart"; + } + this._setSelection(sel, true, true); + break; + case "caret": + this._setSelectionTo(pt.left, pt.top, false); + break; + } + } else if (touches.length === 2) { + pt = this._touchConvert(touches[0]); + var offset1 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + pt = this._touchConvert(touches[1]); + var offset2 = this._getXToOffset(this._getYToLine(pt.top), pt.left); + sel = this._getSelection(); + sel.setCaret(offset1); + sel.extend(offset2); + this._setSelection(sel, true, true); + } + e.preventDefault(); + }, + _handleTouchEnd: function (e) { + if (!this._touchMoved) { + if (e.touches.length === 0 && e.changedTouches.length === 1 && this._touchTimeout) { + clearTimeout(this._touchTimeout); + this._touchTimeout = null; + var touch = e.changedTouches[0]; + this._clickCount = 1; + var pt = this._touchConvert(touch); + this._setSelectionTo(pt.left, pt.top, false); + } + } + if (e.touches.length === 0) { + var self = this; + setTimeout(function() { + var selection = self._getSelection(); + var text = self._model.getText(selection.start, selection.end); + var textArea = self._textArea; + textArea.value = text; + textArea.selectionStart = 0; + textArea.selectionEnd = text.length; + if (!selection.isEmpty()) { + var touchRect = self._touchDiv.getBoundingClientRect(); + var bounds = self._getOffsetBounds(selection.start); + textArea.style.left = (touchRect.width / 2) + "px"; + textArea.style.top = ((bounds.top > 40 ? bounds.top - 30 : bounds.top + 30)) + "px"; + } + }, 0); + } + e.preventDefault(); + }, + + /************************************ Actions ******************************************/ + _doAction: function (e) { + var keyBindings = this._keyBindings; + for (var i = 0; i < keyBindings.length; i++) { + var kb = keyBindings[i]; + if (kb.keyBinding.match(e)) { + if (kb.name) { + var actions = this._actions; + for (var j = 0; j < actions.length; j++) { + var a = actions[j]; + if (a.name === kb.name) { + if (a.userHandler) { + if (!a.userHandler()) { + if (a.defaultHandler) { + a.defaultHandler(); + } else { + return false; + } + } + } else if (a.defaultHandler) { + a.defaultHandler(); + } + break; + } + } + } + return true; + } + } + return false; + }, + _doBackspace: function (args) { + var selection = this._getSelection(); + if (selection.isEmpty()) { + var model = this._model; + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (caret === model.getLineStart(lineIndex)) { + if (lineIndex > 0) { + selection.extend(model.getLineEnd(lineIndex - 1)); + } + } else { + selection.extend(this._getOffset(caret, args.unit, -1)); + } + } + this._modifyContent({text: "", start: selection.start, end: selection.end}, true); + return true; + }, + _doContent: function (text) { + var selection = this._getSelection(); + this._modifyContent({text: text, start: selection.start, end: selection.end, _ignoreDOMSelection: true}, true); + }, + _doCopy: function (e) { + var selection = this._getSelection(); + if (!selection.isEmpty()) { + var text = this._model.getText(selection.start, selection.end); + return this._setClipboardText(text, e); + } + return true; + }, + _doCursorNext: function (args) { + if (!args.select) { + if (this._clearSelection("next")) { return true; } + } + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (caret === model.getLineEnd(lineIndex)) { + if (lineIndex + 1 < model.getLineCount()) { + selection.extend(model.getLineStart(lineIndex + 1)); + } + } else { + selection.extend(this._getOffset(caret, args.unit, 1)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doCursorPrevious: function (args) { + if (!args.select) { + if (this._clearSelection("previous")) { return true; } + } + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (caret === model.getLineStart(lineIndex)) { + if (lineIndex > 0) { + selection.extend(model.getLineEnd(lineIndex - 1)); + } + } else { + selection.extend(this._getOffset(caret, args.unit, -1)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doCut: function (e) { + var selection = this._getSelection(); + if (!selection.isEmpty()) { + var text = this._model.getText(selection.start, selection.end); + this._doContent(""); + return this._setClipboardText(text, e); + } + return true; + }, + _doDelete: function (args) { + var selection = this._getSelection(); + if (selection.isEmpty()) { + var model = this._model; + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (caret === model.getLineEnd (lineIndex)) { + if (lineIndex + 1 < model.getLineCount()) { + selection.extend(model.getLineStart(lineIndex + 1)); + } + } else { + selection.extend(this._getOffset(caret, args.unit, 1)); + } + } + this._modifyContent({text: "", start: selection.start, end: selection.end}, true); + return true; + }, + _doEnd: function (args) { + var selection = this._getSelection(); + var model = this._model; + if (args.ctrl) { + selection.extend(model.getCharCount()); + } else { + var lineIndex = model.getLineAtOffset(selection.getCaret()); + selection.extend(model.getLineEnd(lineIndex)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doEnter: function (args) { + var model = this._model; + this._doContent(model.getLineDelimiter()); + return true; + }, + _doHome: function (args) { + var selection = this._getSelection(); + var model = this._model; + if (args.ctrl) { + selection.extend(0); + } else { + var lineIndex = model.getLineAtOffset(selection.getCaret()); + selection.extend(model.getLineStart(lineIndex)); + } + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true); + return true; + }, + _doLineDown: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (lineIndex + 1 < model.getLineCount()) { + var x = this._columnX; + if (x === -1 || args.select) { + x = this._getOffsetToX(caret); + } + selection.extend(this._getXToOffset(lineIndex + 1, x)); + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true, true); + this._columnX = x;//fix x by scrolling + } + return true; + }, + _doLineUp: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var lineIndex = model.getLineAtOffset(caret); + if (lineIndex > 0) { + var x = this._columnX; + if (x === -1 || args.select) { + x = this._getOffsetToX(caret); + } + selection.extend(this._getXToOffset(lineIndex - 1, x)); + if (!args.select) { selection.collapse(); } + this._setSelection(selection, true, true); + this._columnX = x;//fix x by scrolling + } + return true; + }, + _doPageDown: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var caretLine = model.getLineAtOffset(caret); + var lineCount = model.getLineCount(); + if (caretLine < lineCount - 1) { + var clientHeight = this._getClientHeight(); + var lineHeight = this._getLineHeight(); + var lines = Math.floor(clientHeight / lineHeight); + var scrollLines = Math.min(lineCount - caretLine - 1, lines); + scrollLines = Math.max(1, scrollLines); + var x = this._columnX; + if (x === -1 || args.select) { + x = this._getOffsetToX(caret); + } + selection.extend(this._getXToOffset(caretLine + scrollLines, x)); + if (!args.select) { selection.collapse(); } + this._setSelection(selection, false, false); + + var verticalMaximum = lineCount * lineHeight; + var verticalScrollOffset = this._getScroll().y; + var scrollOffset = verticalScrollOffset + scrollLines * lineHeight; + if (scrollOffset + clientHeight > verticalMaximum) { + scrollOffset = verticalMaximum - clientHeight; + } + if (scrollOffset > verticalScrollOffset) { + this._scrollView(0, scrollOffset - verticalScrollOffset); + } else { + this._updateDOMSelection(); + } + this._columnX = x;//fix x by scrolling + } + return true; + }, + _doPageUp: function (args) { + var model = this._model; + var selection = this._getSelection(); + var caret = selection.getCaret(); + var caretLine = model.getLineAtOffset(caret); + if (caretLine > 0) { + var clientHeight = this._getClientHeight(); + var lineHeight = this._getLineHeight(); + var lines = Math.floor(clientHeight / lineHeight); + var scrollLines = Math.max(1, Math.min(caretLine, lines)); + var x = this._columnX; + if (x === -1 || args.select) { + x = this._getOffsetToX(caret); + } + selection.extend(this._getXToOffset(caretLine - scrollLines, x)); + if (!args.select) { selection.collapse(); } + this._setSelection(selection, false, false); + + var verticalScrollOffset = this._getScroll().y; + var scrollOffset = Math.max(0, verticalScrollOffset - scrollLines * lineHeight); + if (scrollOffset < verticalScrollOffset) { + this._scrollView(0, scrollOffset - verticalScrollOffset); + } else { + this._updateDOMSelection(); + } + this._columnX = x;//fix x by scrolling + } + return true; + }, + _doPaste: function(e) { + var text = this._getClipboardText(e); + if (text) { + this._doContent(text); + } + return text !== null; + }, + _doScroll: function (scroll) { + var oldX = this._hScroll; + var oldY = this._vScroll; + if (oldX !== scroll.x || oldY !== scroll.y) { + this._hScroll = scroll.x; + this._vScroll = scroll.y; + this._commitIME(); + this._updatePage(); + var e = { + oldValue: {x: oldX, y: oldY}, + newValue: scroll + }; + this.onScroll(e); + } + }, + _doSelectAll: function (args) { + var model = this._model; + var selection = this._getSelection(); + selection.setCaret(0); + selection.extend(model.getCharCount()); + this._setSelection(selection, false); + return true; + }, + _doTab: function (args) { + this._doContent("\t"); + return true; + }, + + /************************************ Internals ******************************************/ + _applyStyle: function(style, node) { + if (!style) { + return; + } + if (style.styleClass) { + node.className = style.styleClass; + } + var properties = style.style; + if (properties) { + for (var s in properties) { + if (properties.hasOwnProperty(s)) { + node.style[s] = properties[s]; + } + } + } + }, + _autoScroll: function () { + var selection = this._getSelection(); + var line; + var x = this._autoScrollX; + if (this._autoScrollDir === "up" || this._autoScrollDir === "down") { + var scroll = this._autoScrollY / this._getLineHeight(); + scroll = scroll < 0 ? Math.floor(scroll) : Math.ceil(scroll); + line = this._model.getLineAtOffset(selection.getCaret()); + line = Math.max(0, Math.min(this._model.getLineCount() - 1, line + scroll)); + } else if (this._autoScrollDir === "left" || this._autoScrollDir === "right") { + line = this._getYToLine(this._autoScrollY); + x += this._getOffsetToX(selection.getCaret()); + } + selection.extend(this._getXToOffset(line, x)); + this._setSelection(selection, true); + }, + _autoScrollTimer: function () { + this._autoScroll(); + var self = this; + this._autoScrollTimerID = setTimeout(function () {self._autoScrollTimer();}, this._AUTO_SCROLL_RATE); + }, + _calculateLineHeight: function() { + var parent = this._clientDiv; + var document = this._frameDocument; + var c = " "; + var line = document.createElement("DIV"); + line.style.position = "fixed"; + line.style.left = "-1000px"; + var span1 = document.createElement("SPAN"); + span1.appendChild(document.createTextNode(c)); + line.appendChild(span1); + var span2 = document.createElement("SPAN"); + span2.style.fontStyle = "italic"; + span2.appendChild(document.createTextNode(c)); + line.appendChild(span2); + var span3 = document.createElement("SPAN"); + span3.style.fontWeight = "bold"; + span3.appendChild(document.createTextNode(c)); + line.appendChild(span3); + var span4 = document.createElement("SPAN"); + span4.style.fontWeight = "bold"; + span4.style.fontStyle = "italic"; + span4.appendChild(document.createTextNode(c)); + line.appendChild(span4); + parent.appendChild(line); + var spanRect1 = span1.getBoundingClientRect(); + var spanRect2 = span2.getBoundingClientRect(); + var spanRect3 = span3.getBoundingClientRect(); + var spanRect4 = span4.getBoundingClientRect(); + var h1 = spanRect1.bottom - spanRect1.top; + var h2 = spanRect2.bottom - spanRect2.top; + var h3 = spanRect3.bottom - spanRect3.top; + var h4 = spanRect4.bottom - spanRect4.top; + var fontStyle = 0; + var lineHeight = h1; + if (h2 > h1) { + lineHeight = h2; + fontStyle = 1; + } + if (h3 > h2) { + lineHeight = h3; + fontStyle = 2; + } + if (h4 > h3) { + lineHeight = h4; + fontStyle = 3; + } + this._largestFontStyle = fontStyle; + parent.removeChild(line); + return lineHeight; + }, + _calculatePadding: function() { + var document = this._frameDocument; + var parent = this._clientDiv; + var pad = this._getPadding(this._viewDiv); + var div1 = document.createElement("DIV"); + div1.style.position = "fixed"; + div1.style.left = "-1000px"; + div1.style.paddingLeft = pad.left + "px"; + div1.style.paddingTop = pad.top + "px"; + div1.style.paddingRight = pad.right + "px"; + div1.style.paddingBottom = pad.bottom + "px"; + div1.style.width = "100px"; + div1.style.height = "100px"; + var div2 = document.createElement("DIV"); + div2.style.width = "100%"; + div2.style.height = "100%"; + div1.appendChild(div2); + parent.appendChild(div1); + var rect1 = div1.getBoundingClientRect(); + var rect2 = div2.getBoundingClientRect(); + parent.removeChild(div1); + pad = { + left: rect2.left - rect1.left, + top: rect2.top - rect1.top, + right: rect1.right - rect2.right, + bottom: rect1.bottom - rect2.bottom + }; + return pad; + }, + _clearSelection: function (direction) { + var selection = this._getSelection(); + if (selection.isEmpty()) { return false; } + if (direction === "next") { + selection.start = selection.end; + } else { + selection.end = selection.start; + } + this._setSelection(selection, true); + return true; + }, + _commitIME: function () { + if (this._imeOffset === -1) { return; } + // make the state of the IME match the state the view expects it be in + // when the view commits the text and IME also need to be committed + // this can be accomplished by changing the focus around + this._scrollDiv.focus(); + this._clientDiv.focus(); + + var model = this._model; + var lineIndex = model.getLineAtOffset(this._imeOffset); + var lineStart = model.getLineStart(lineIndex); + var newText = this._getDOMText(lineIndex); + var oldText = model.getLine(lineIndex); + var start = this._imeOffset - lineStart; + var end = start + newText.length - oldText.length; + if (start !== end) { + var insertText = newText.substring(start, end); + this._doContent(insertText); + } + this._imeOffset = -1; + }, + _convertDelimiter: function (text, addTextFunc, addDelimiterFunc) { + var cr = 0, lf = 0, index = 0, length = text.length; + while (index < length) { + if (cr !== -1 && cr <= index) { cr = text.indexOf("\r", index); } + if (lf !== -1 && lf <= index) { lf = text.indexOf("\n", index); } + var start = index, end; + if (lf === -1 && cr === -1) { + addTextFunc(text.substring(index)); + break; + } + if (cr !== -1 && lf !== -1) { + if (cr + 1 === lf) { + end = cr; + index = lf + 1; + } else { + end = cr < lf ? cr : lf; + index = (cr < lf ? cr : lf) + 1; + } + } else if (cr !== -1) { + end = cr; + index = cr + 1; + } else { + end = lf; + index = lf + 1; + } + addTextFunc(text.substring(start, end)); + addDelimiterFunc(); + } + }, + _createActions: function () { + var KeyBinding = orion.textview.KeyBinding; + //no duplicate keybindings + var bindings = this._keyBindings = []; + + // Cursor Navigation + bindings.push({name: "lineUp", keyBinding: new KeyBinding(38), predefined: true}); + bindings.push({name: "lineDown", keyBinding: new KeyBinding(40), predefined: true}); + bindings.push({name: "charPrevious", keyBinding: new KeyBinding(37), predefined: true}); + bindings.push({name: "charNext", keyBinding: new KeyBinding(39), predefined: true}); + bindings.push({name: "pageUp", keyBinding: new KeyBinding(33), predefined: true}); + bindings.push({name: "pageDown", keyBinding: new KeyBinding(34), predefined: true}); + if (isMac) { + bindings.push({name: "lineStart", keyBinding: new KeyBinding(37, true), predefined: true}); + bindings.push({name: "lineEnd", keyBinding: new KeyBinding(39, true), predefined: true}); + bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, null, null, true), predefined: true}); + bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, null, null, true), predefined: true}); + bindings.push({name: "textStart", keyBinding: new KeyBinding(36), predefined: true}); + bindings.push({name: "textEnd", keyBinding: new KeyBinding(35), predefined: true}); + bindings.push({name: "textStart", keyBinding: new KeyBinding(38, true), predefined: true}); + bindings.push({name: "textEnd", keyBinding: new KeyBinding(40, true), predefined: true}); + } else { + bindings.push({name: "lineStart", keyBinding: new KeyBinding(36), predefined: true}); + bindings.push({name: "lineEnd", keyBinding: new KeyBinding(35), predefined: true}); + bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, true), predefined: true}); + bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, true), predefined: true}); + bindings.push({name: "textStart", keyBinding: new KeyBinding(36, true), predefined: true}); + bindings.push({name: "textEnd", keyBinding: new KeyBinding(35, true), predefined: true}); + } + + // Select Cursor Navigation + bindings.push({name: "selectLineUp", keyBinding: new KeyBinding(38, null, true), predefined: true}); + bindings.push({name: "selectLineDown", keyBinding: new KeyBinding(40, null, true), predefined: true}); + bindings.push({name: "selectCharPrevious", keyBinding: new KeyBinding(37, null, true), predefined: true}); + bindings.push({name: "selectCharNext", keyBinding: new KeyBinding(39, null, true), predefined: true}); + bindings.push({name: "selectPageUp", keyBinding: new KeyBinding(33, null, true), predefined: true}); + bindings.push({name: "selectPageDown", keyBinding: new KeyBinding(34, null, true), predefined: true}); + if (isMac) { + bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(37, true, true), predefined: true}); + bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, true, true), predefined: true}); + bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, null, true, true), predefined: true}); + bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, null, true, true), predefined: true}); + bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, null, true), predefined: true}); + bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, null, true), predefined: true}); + bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(38, true, true), predefined: true}); + bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(40, true, true), predefined: true}); + } else { + bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(36, null, true), predefined: true}); + bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(35, null, true), predefined: true}); + bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, true, true), predefined: true}); + bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, true, true), predefined: true}); + bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, true, true), predefined: true}); + bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, true, true), predefined: true}); + } + + //Misc + bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8), predefined: true}); + bindings.push({name: "deletePrevious", keyBinding: new KeyBinding(8, null, true), predefined: true}); + bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46), predefined: true}); + bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true), predefined: true}); + bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, true, true), predefined: true}); + bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, true), predefined: true}); + bindings.push({name: "tab", keyBinding: new KeyBinding(9), predefined: true}); + bindings.push({name: "enter", keyBinding: new KeyBinding(13), predefined: true}); + bindings.push({name: "enter", keyBinding: new KeyBinding(13, null, true), predefined: true}); + bindings.push({name: "selectAll", keyBinding: new KeyBinding('a', true), predefined: true}); + if (isMac) { + bindings.push({name: "deleteNext", keyBinding: new KeyBinding(46, null, true), predefined: true}); + bindings.push({name: "deleteWordPrevious", keyBinding: new KeyBinding(8, null, null, true), predefined: true}); + bindings.push({name: "deleteWordNext", keyBinding: new KeyBinding(46, null, null, true), predefined: true}); + } + + /* + * Feature in IE/Chrome: prevent ctrl+'u', ctrl+'i', and ctrl+'b' from applying styles to the text. + * + * Note that Chrome applies the styles on the Mac with Ctrl instead of Cmd. + */ + var isMacChrome = isMac && isChrome; + bindings.push({name: null, keyBinding: new KeyBinding('u', !isMacChrome, false, false, isMacChrome), predefined: true}); + bindings.push({name: null, keyBinding: new KeyBinding('i', !isMacChrome, false, false, isMacChrome), predefined: true}); + bindings.push({name: null, keyBinding: new KeyBinding('b', !isMacChrome, false, false, isMacChrome), predefined: true}); + + if (isFirefox) { + bindings.push({name: "copy", keyBinding: new KeyBinding(45, true), predefined: true}); + bindings.push({name: "paste", keyBinding: new KeyBinding(45, null, true), predefined: true}); + bindings.push({name: "cut", keyBinding: new KeyBinding(46, null, true), predefined: true}); + } + + //1 to 1, no duplicates + var self = this; + this._actions = [ + {name: "lineUp", defaultHandler: function() {return self._doLineUp({select: false});}}, + {name: "lineDown", defaultHandler: function() {return self._doLineDown({select: false});}}, + {name: "lineStart", defaultHandler: function() {return self._doHome({select: false, ctrl:false});}}, + {name: "lineEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:false});}}, + {name: "charPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"character"});}}, + {name: "charNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"character"});}}, + {name: "pageUp", defaultHandler: function() {return self._doPageUp({select: false});}}, + {name: "pageDown", defaultHandler: function() {return self._doPageDown({select: false});}}, + {name: "wordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"word"});}}, + {name: "wordNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"word"});}}, + {name: "textStart", defaultHandler: function() {return self._doHome({select: false, ctrl:true});}}, + {name: "textEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:true});}}, + + {name: "selectLineUp", defaultHandler: function() {return self._doLineUp({select: true});}}, + {name: "selectLineDown", defaultHandler: function() {return self._doLineDown({select: true});}}, + {name: "selectLineStart", defaultHandler: function() {return self._doHome({select: true, ctrl:false});}}, + {name: "selectLineEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:false});}}, + {name: "selectCharPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"character"});}}, + {name: "selectCharNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"character"});}}, + {name: "selectPageUp", defaultHandler: function() {return self._doPageUp({select: true});}}, + {name: "selectPageDown", defaultHandler: function() {return self._doPageDown({select: true});}}, + {name: "selectWordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"word"});}}, + {name: "selectWordNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"word"});}}, + {name: "selectTextStart", defaultHandler: function() {return self._doHome({select: true, ctrl:true});}}, + {name: "selectTextEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:true});}}, + + {name: "deletePrevious", defaultHandler: function() {return self._doBackspace({unit:"character"});}}, + {name: "deleteNext", defaultHandler: function() {return self._doDelete({unit:"character"});}}, + {name: "deleteWordPrevious", defaultHandler: function() {return self._doBackspace({unit:"word"});}}, + {name: "deleteWordNext", defaultHandler: function() {return self._doDelete({unit:"word"});}}, + {name: "tab", defaultHandler: function() {return self._doTab();}}, + {name: "enter", defaultHandler: function() {return self._doEnter();}}, + {name: "selectAll", defaultHandler: function() {return self._doSelectAll();}}, + {name: "copy", defaultHandler: function() {return self._doCopy();}}, + {name: "cut", defaultHandler: function() {return self._doCut();}}, + {name: "paste", defaultHandler: function() {return self._doPaste();}} + ]; + }, + _createLine: function(parent, sibling, document, lineIndex, model) { + var lineText = model.getLine(lineIndex); + var lineStart = model.getLineStart(lineIndex); + var e = {lineIndex: lineIndex, lineText: lineText, lineStart: lineStart}; + this.onLineStyle(e); + var child = document.createElement("DIV"); + child.lineIndex = lineIndex; + this._applyStyle(e.style, child); + if (lineText.length !== 0) { + var start = 0; + var tabSize = this._tabSize; + if (tabSize && tabSize !== 8) { + var tabIndex = lineText.indexOf("\t"), ignoreChars = 0; + while (tabIndex !== -1) { + this._createRange(child, document, e.ranges, start, tabIndex, lineText, lineStart); + var spacesCount = tabSize - ((tabIndex + ignoreChars) % tabSize); + var spaces = "\u00A0"; + for (var i = 1; i < spacesCount; i++) { + spaces += " "; + } + var tabSpan = document.createElement("SPAN"); + tabSpan.appendChild(document.createTextNode(spaces)); + tabSpan.ignoreChars = spacesCount - 1; + ignoreChars += tabSpan.ignoreChars; + if (e.ranges) { + for (var j = 0; j < e.ranges.length; j++) { + var range = e.ranges[j]; + var styleStart = range.start - lineStart; + var styleEnd = range.end - lineStart; + if (styleStart > tabIndex) { break; } + if (styleStart <= tabIndex && tabIndex < styleEnd) { + this._applyStyle(range.style, tabSpan); + break; + } + } + } + child.appendChild(tabSpan); + start = tabIndex + 1; + tabIndex = lineText.indexOf("\t", start); + } + } + this._createRange(child, document, e.ranges, start, lineText.length, lineText, lineStart); + } + + /* + * Firefox, Opera and IE9 do not extend the selection at the end of the line + * when the line is fully selected. The fix is to add an extra space at the end + * of the line. + * + * Note: the height of a div with only an empty span is zero. The fix is + * the add a extra zero-width non-break space to preserve the default + * height in the line div. In Chrome this character shows a glyph, so the + * zero-width non-joiner character is used instead. + * + * Note: in order to support bold and italic fonts with fixed line + * height all lines need to have at least one span with the largest + * font. + */ + var span = document.createElement("SPAN"); + span.ignoreChars = 1; + if ((this._largestFontStyle & 1) !== 0) { + span.style.fontStyle = "italic"; + } + if ((this._largestFontStyle & 2) !== 0) { + span.style.fontWeight = "bold"; + } + var fullSelection = this._fullSelection; + var extendSelection = !fullSelection && (isFirefox || isOpera || isIE >= 9); + var c = extendSelection ? " " : (isWebkit || isFirefox ? "\u200C" : "\uFEFF"); + span.appendChild(document.createTextNode(c)); + child.appendChild(span); + + parent.insertBefore(child, sibling); + return child; + }, + _createRange: function(parent, document, ranges, start, end, text, lineStart) { + if (start >= end) { return; } + var span; + if (ranges) { + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i]; + if (range.end <= lineStart + start) { continue; } + var styleStart = Math.max(lineStart + start, range.start) - lineStart; + if (styleStart >= end) { break; } + var styleEnd = Math.min(lineStart + end, range.end) - lineStart; + if (styleStart < styleEnd) { + styleStart = Math.max(start, styleStart); + styleEnd = Math.min(end, styleEnd); + if (start < styleStart) { + span = document.createElement("SPAN"); + span.appendChild(document.createTextNode(text.substring(start, styleStart))); + parent.appendChild(span); + } + span = document.createElement("SPAN"); + span.appendChild(document.createTextNode(text.substring(styleStart, styleEnd))); + this._applyStyle(range.style, span); + parent.appendChild(span); + start = styleEnd; + } + } + } + if (start < end) { + span = document.createElement("SPAN"); + span.appendChild(document.createTextNode(text.substring(start, end))); + parent.appendChild(span); + } + }, + _doAutoScroll: function (direction, x, y) { + this._autoScrollDir = direction; + this._autoScrollX = x; + this._autoScrollY = y; + if (!this._autoScrollTimerID) { + this._autoScrollTimer(); + } + }, + _endAutoScroll: function () { + if (this._autoScrollTimerID) { clearTimeout(this._autoScrollTimerID); } + this._autoScrollDir = undefined; + this._autoScrollTimerID = undefined; + }, + _getBoundsAtOffset: function (offset) { + var model = this._model; + var document = this._frameDocument; + var clientDiv = this._clientDiv; + var lineIndex = model.getLineAtOffset(offset); + var dummy; + var child = this._getLineNode(lineIndex); + if (!child) { + child = dummy = this._createLine(clientDiv, null, document, lineIndex, model); + } + var result = null; + if (offset < model.getLineEnd(lineIndex)) { + var lineOffset = model.getLineStart(lineIndex); + var lineChild = child.firstChild; + while (lineChild) { + var textNode = lineChild.firstChild; + var nodeLength = textNode.length; + if (lineChild.ignoreChars) { + nodeLength -= lineChild.ignoreChars; + } + if (lineOffset + nodeLength > offset) { + var index = offset - lineOffset; + var range; + if (isRangeRects) { + range = document.createRange(); + range.setStart(textNode, index); + range.setEnd(textNode, index + 1); + result = range.getBoundingClientRect(); + } else if (isIE) { + range = document.body.createTextRange(); + range.moveToElementText(lineChild); + range.collapse(); + range.moveEnd("character", index + 1); + range.moveStart("character", index); + result = range.getBoundingClientRect(); + } else { + var text = textNode.data; + lineChild.removeChild(textNode); + lineChild.appendChild(document.createTextNode(text.substring(0, index))); + var span = document.createElement("SPAN"); + span.appendChild(document.createTextNode(text.substring(index, index + 1))); + lineChild.appendChild(span); + lineChild.appendChild(document.createTextNode(text.substring(index + 1))); + result = span.getBoundingClientRect(); + lineChild.innerHTML = ""; + lineChild.appendChild(textNode); + if (!dummy) { + /* + * Removing the element node that holds the selection start or end + * causes the selection to be lost. The fix is to detect this case + * and restore the selection. + */ + var s = this._getSelection(); + if ((lineOffset <= s.start && s.start < lineOffset + nodeLength) || (lineOffset <= s.end && s.end < lineOffset + nodeLength)) { + this._updateDOMSelection(); + } + } + } + if (isIE) { + var logicalXDPI = window.screen.logicalXDPI; + var deviceXDPI = window.screen.deviceXDPI; + result.left = result.left * logicalXDPI / deviceXDPI; + result.right = result.right * logicalXDPI / deviceXDPI; + } + break; + } + lineOffset += nodeLength; + lineChild = lineChild.nextSibling; + } + } + if (!result) { + var rect = this._getLineBoundingClientRect(child); + result = {left: rect.right, right: rect.right}; + } + if (dummy) { clientDiv.removeChild(dummy); } + return result; + }, + _getBottomIndex: function (fullyVisible) { + var child = this._bottomChild; + if (fullyVisible && this._getClientHeight() > this._getLineHeight()) { + var rect = child.getBoundingClientRect(); + var clientRect = this._clientDiv.getBoundingClientRect(); + if (rect.bottom > clientRect.bottom) { + child = this._getLinePrevious(child) || child; + } + } + return child.lineIndex; + }, + _getFrameHeight: function() { + return this._frameDocument.documentElement.clientHeight; + }, + _getFrameWidth: function() { + return this._frameDocument.documentElement.clientWidth; + }, + _getClientHeight: function() { + var viewPad = this._getViewPadding(); + return Math.max(0, this._viewDiv.clientHeight - viewPad.top - viewPad.bottom); + }, + _getClientWidth: function() { + var viewPad = this._getViewPadding(); + return Math.max(0, this._viewDiv.clientWidth - viewPad.left - viewPad.right); + }, + _getClipboardText: function (event) { + var delimiter = this._model.getLineDelimiter(); + var clipboadText, text; + if (this._frameWindow.clipboardData) { + //IE + clipboadText = []; + text = this._frameWindow.clipboardData.getData("Text"); + this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);}); + return clipboadText.join(""); + } + if (isFirefox) { + var window = this._frameWindow; + var document = this._frameDocument; + var child = document.createElement("PRE"); + child.style.position = "fixed"; + child.style.left = "-1000px"; + child.appendChild(document.createTextNode(" ")); + this._clientDiv.appendChild(child); + var range = document.createRange(); + range.selectNodeContents(child); + var sel = window.getSelection(); + if (sel.rangeCount > 0) { sel.removeAllRanges(); } + sel.addRange(range); + var self = this; + var cleanup = function() { + self._updateDOMSelection(); + self._clientDiv.removeChild(child); + }; + var _getText = function() { + /* + * Use the selection anchor to determine the end of the pasted text as it is possible that + * some browsers (like Firefox) add extra elements (<BR>) after the pasted text. + */ + var endNode = null; + if (sel.anchorNode.nodeType !== child.TEXT_NODE) { + endNode = sel.anchorNode.childNodes[sel.anchorOffset]; + } + var text = []; + var getNodeText = function(node) { + var nodeChild = node.firstChild; + while (nodeChild && nodeChild !== endNode) { + if (nodeChild.nodeType === child.TEXT_NODE) { + text.push(nodeChild !== sel.anchorNode ? nodeChild.data : nodeChild.data.substring(0, sel.anchorOffset)); + } else if (nodeChild.tagName === "BR") { + text.push(delimiter); + } else { + getNodeText(nodeChild); + } + nodeChild = nodeChild.nextSibling; + } + }; + getNodeText(child); + cleanup(); + return text.join(""); + }; + + /* Try execCommand first. Works on firefox with clipboard permission. */ + var result = false; + this._ignorePaste = true; + try { + result = document.execCommand("paste", false, null); + } catch (ex) {} + this._ignorePaste = false; + if (!result) { + /* + * Try native paste in DOM, works for firefox during the paste event. + */ + if (event) { + setTimeout(function() { + var text = _getText(); + if (text) { self._doContent(text); } + }, 0); + return null; + } else { + /* no event and no clipboard permission, paste can't be performed */ + cleanup(); + return ""; + } + } + return _getText(); + } + //webkit + if (event && event.clipboardData) { + /* + * Webkit (Chrome/Safari) allows getData during the paste event + * Note: setData is not allowed, not even during copy/cut event + */ + clipboadText = []; + text = event.clipboardData.getData("text/plain"); + this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);}); + return clipboadText.join(""); + } else { + //TODO try paste using extension (Chrome only) + } + return ""; + }, + _getDOMText: function(lineIndex) { + var child = this._getLineNode(lineIndex); + var lineChild = child.firstChild; + var text = ""; + while (lineChild) { + var textNode = lineChild.firstChild; + while (textNode) { + if (lineChild.ignoreChars) { + for (var i = 0; i < textNode.length; i++) { + var ch = textNode.data.substring(i, i + 1); + if (ch !== " ") { + text += ch; + } + } + } else { + text += textNode.data; + } + textNode = textNode.nextSibling; + } + lineChild = lineChild.nextSibling; + } + return text; + }, + _getViewPadding: function() { + return this._viewPadding; + }, + _getLineBoundingClientRect: function (child) { + var rect = child.getBoundingClientRect(); + var lastChild = child.lastChild; + //Remove any artificial trailing whitespace in the line + while (lastChild && lastChild.ignoreChars === lastChild.firstChild.length) { + lastChild = lastChild.previousSibling; + } + if (!lastChild) { + return {left: rect.left, top: rect.top, right: rect.left, bottom: rect.bottom}; + } + var lastRect = lastChild.getBoundingClientRect(); + return {left: rect.left, top: rect.top, right: lastRect.right, bottom: rect.bottom}; + }, + _getLineHeight: function() { + return this._lineHeight; + }, + _getLineNode: function (lineIndex) { + var clientDiv = this._clientDiv; + var child = clientDiv.firstChild; + while (child) { + if (lineIndex === child.lineIndex) { + return child; + } + child = child.nextSibling; + } + return undefined; + }, + _getLineNext: function (lineNode) { + var node = lineNode ? lineNode.nextSibling : this._clientDiv.firstChild; + while (node && node.lineIndex === -1) { + node = node.nextSibling; + } + return node; + }, + _getLinePrevious: function (lineNode) { + var node = lineNode ? lineNode.previousSibling : this._clientDiv.lastChild; + while (node && node.lineIndex === -1) { + node = node.previousSibling; + } + return node; + }, + _getOffset: function (offset, unit, direction) { + if (unit === "wordend") { + return this._getOffset_W3C(offset, unit, direction); + } + return isIE ? this._getOffset_IE(offset, unit, direction) : this._getOffset_W3C(offset, unit, direction); + }, + _getOffset_W3C: function (offset, unit, direction) { + function _isPunctuation(c) { + return (33 <= c && c <= 47) || (58 <= c && c <= 64) || (91 <= c && c <= 94) || c === 96 || (123 <= c && c <= 126); + } + function _isWhitespace(c) { + return c === 32 || c === 9; + } + if (unit === "word" || unit === "wordend") { + var model = this._model; + var lineIndex = model.getLineAtOffset(offset); + var lineText = model.getLine(lineIndex); + var lineStart = model.getLineStart(lineIndex); + var lineEnd = model.getLineEnd(lineIndex); + var lineLength = lineText.length; + var offsetInLine = offset - lineStart; + + + var c, previousPunctuation, previousLetterOrDigit, punctuation, letterOrDigit; + if (direction > 0) { + if (offsetInLine === lineLength) { return lineEnd; } + c = lineText.charCodeAt(offsetInLine); + previousPunctuation = _isPunctuation(c); + previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c); + offsetInLine++; + while (offsetInLine < lineLength) { + c = lineText.charCodeAt(offsetInLine); + punctuation = _isPunctuation(c); + if (unit === "wordend") { + if (!punctuation && previousPunctuation) { break; } + } else { + if (punctuation && !previousPunctuation) { break; } + } + letterOrDigit = !punctuation && !_isWhitespace(c); + if (unit === "wordend") { + if (!letterOrDigit && previousLetterOrDigit) { break; } + } else { + if (letterOrDigit && !previousLetterOrDigit) { break; } + } + previousLetterOrDigit = letterOrDigit; + previousPunctuation = punctuation; + offsetInLine++; + } + } else { + if (offsetInLine === 0) { return lineStart; } + offsetInLine--; + c = lineText.charCodeAt(offsetInLine); + previousPunctuation = _isPunctuation(c); + previousLetterOrDigit = !previousPunctuation && !_isWhitespace(c); + while (0 < offsetInLine) { + c = lineText.charCodeAt(offsetInLine - 1); + punctuation = _isPunctuation(c); + if (unit === "wordend") { + if (punctuation && !previousPunctuation) { break; } + } else { + if (!punctuation && previousPunctuation) { break; } + } + letterOrDigit = !punctuation && !_isWhitespace(c); + if (unit === "wordend") { + if (letterOrDigit && !previousLetterOrDigit) { break; } + } else { + if (!letterOrDigit && previousLetterOrDigit) { break; } + } + previousLetterOrDigit = letterOrDigit; + previousPunctuation = punctuation; + offsetInLine--; + } + } + return lineStart + offsetInLine; + } + return offset + direction; + }, + _getOffset_IE: function (offset, unit, direction) { + var document = this._frameDocument; + var model = this._model; + var lineIndex = model.getLineAtOffset(offset); + var clientDiv = this._clientDiv; + var dummy; + var child = this._getLineNode(lineIndex); + if (!child) { + child = dummy = this._createLine(clientDiv, null, document, lineIndex, model); + } + var result = 0, range, length; + var lineOffset = model.getLineStart(lineIndex); + if (offset === model.getLineEnd(lineIndex)) { + range = document.body.createTextRange(); + range.moveToElementText(child.lastChild); + length = range.text.length; + range.moveEnd(unit, direction); + result = offset + range.text.length - length; + } else if (offset === lineOffset && direction < 0) { + result = lineOffset; + } else { + var lineChild = child.firstChild; + while (lineChild) { + var textNode = lineChild.firstChild; + var nodeLength = textNode.length; + if (lineChild.ignoreChars) { + nodeLength -= lineChild.ignoreChars; + } + if (lineOffset + nodeLength > offset) { + range = document.body.createTextRange(); + if (offset === lineOffset && direction < 0) { + range.moveToElementText(lineChild.previousSibling); + } else { + range.moveToElementText(lineChild); + range.collapse(); + range.moveEnd("character", offset - lineOffset); + } + length = range.text.length; + range.moveEnd(unit, direction); + result = offset + range.text.length - length; + break; + } + lineOffset = nodeLength + lineOffset; + lineChild = lineChild.nextSibling; + } + } + if (dummy) { clientDiv.removeChild(dummy); } + return result; + }, + _getOffsetToX: function (offset) { + return this._getBoundsAtOffset(offset).left; + }, + _getPadding: function (node) { + var left,top,right,bottom; + if (node.currentStyle) { + left = node.currentStyle.paddingLeft; + top = node.currentStyle.paddingTop; + right = node.currentStyle.paddingRight; + bottom = node.currentStyle.paddingBottom; + } else if (this._frameWindow.getComputedStyle) { + var style = this._frameWindow.getComputedStyle(node, null); + left = style.getPropertyValue("padding-left"); + top = style.getPropertyValue("padding-top"); + right = style.getPropertyValue("padding-right"); + bottom = style.getPropertyValue("padding-bottom"); + } + return { + left: parseInt(left, 10), + top: parseInt(top, 10), + right: parseInt(right, 10), + bottom: parseInt(bottom, 10) + }; + }, + _getScroll: function() { + var viewDiv = this._viewDiv; + return {x: viewDiv.scrollLeft, y: viewDiv.scrollTop}; + }, + _getSelection: function () { + return this._selection.clone(); + }, + _getTopIndex: function (fullyVisible) { + var child = this._topChild; + if (fullyVisible && this._getClientHeight() > this._getLineHeight()) { + var rect = child.getBoundingClientRect(); + var viewPad = this._getViewPadding(); + var viewRect = this._viewDiv.getBoundingClientRect(); + if (rect.top < viewRect.top + viewPad.top) { + child = this._getLineNext(child) || child; + } + } + return child.lineIndex; + }, + _getXToOffset: function (lineIndex, x) { + var model = this._model; + var lineStart = model.getLineStart(lineIndex); + var lineEnd = model.getLineEnd(lineIndex); + if (lineStart === lineEnd) { + return lineStart; + } + var document = this._frameDocument; + var clientDiv = this._clientDiv; + var dummy; + var child = this._getLineNode(lineIndex); + if (!child) { + child = dummy = this._createLine(clientDiv, null, document, lineIndex, model); + } + var lineRect = this._getLineBoundingClientRect(child); + if (x < lineRect.left) { x = lineRect.left; } + if (x > lineRect.right) { x = lineRect.right; } + /* + * Bug in IE 8 and earlier. The coordinates of getClientRects() are relative to + * the browser window. The fix is to convert to the frame window before using it. + */ + var deltaX = 0, rects; + if (isIE < 9) { + rects = child.getClientRects(); + var minLeft = rects[0].left; + for (var i=1; i<rects.length; i++) { + minLeft = Math.min(rects[i].left, minLeft); + } + deltaX = minLeft - lineRect.left; + } + var scrollX = this._getScroll().x; + function _getClientRects(element) { + var rects, newRects, i, r; + if (!element._rectsCache) { + rects = element.getClientRects(); + newRects = [rects.length]; + for (i = 0; i<rects.length; i++) { + r = rects[i]; + newRects[i] = {left: r.left - deltaX + scrollX, top: r.top, right: r.right - deltaX + scrollX, bottom: r.bottom}; + } + element._rectsCache = newRects; + } + rects = element._rectsCache; + newRects = [rects.length]; + for (i = 0; i<rects.length; i++) { + r = rects[i]; + newRects[i] = {left: r.left - scrollX, top: r.top, right: r.right - scrollX, bottom: r.bottom}; + } + return newRects; + } + var logicalXDPI = isIE ? window.screen.logicalXDPI : 1; + var deviceXDPI = isIE ? window.screen.deviceXDPI : 1; + var offset = lineStart; + var lineChild = child.firstChild; + done: + while (lineChild) { + var textNode = lineChild.firstChild; + var nodeLength = textNode.length; + if (lineChild.ignoreChars) { + nodeLength -= lineChild.ignoreChars; + } + rects = _getClientRects(lineChild); + for (var j = 0; j < rects.length; j++) { + var rect = rects[j]; + if (rect.left <= x && x < rect.right) { + var range, start, end; + if (isIE || isRangeRects) { + range = isRangeRects ? document.createRange() : document.body.createTextRange(); + var high = nodeLength; + var low = -1; + while ((high - low) > 1) { + var mid = Math.floor((high + low) / 2); + start = low + 1; + end = mid === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : mid + 1; + if (isRangeRects) { + range.setStart(textNode, start); + range.setEnd(textNode, end); + } else { + range.moveToElementText(lineChild); + range.move("character", start); + range.moveEnd("character", end - start); + } + rects = range.getClientRects(); + var found = false; + for (var k = 0; k < rects.length; k++) { + rect = rects[k]; + var rangeLeft = rect.left * logicalXDPI / deviceXDPI - deltaX; + var rangeRight = rect.right * logicalXDPI / deviceXDPI - deltaX; + if (rangeLeft <= x && x < rangeRight) { + found = true; + break; + } + } + if (found) { + high = mid; + } else { + low = mid; + } + } + offset += high; + start = high; + end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : high + 1; + if (isRangeRects) { + range.setStart(textNode, start); + range.setEnd(textNode, end); + } else { + range.moveToElementText(lineChild); + range.move("character", start); + range.moveEnd("character", end - start); + } + rect = range.getClientRects()[0]; + //TODO test for character trailing (wrong for bidi) + if (x > ((rect.left * logicalXDPI / deviceXDPI - deltaX) + ((rect.right - rect.left) * logicalXDPI / deviceXDPI / 2))) { + offset++; + } + } else { + var newText = []; + for (var q = 0; q < nodeLength; q++) { + newText.push("<span>"); + if (q === nodeLength - 1) { + newText.push(textNode.data.substring(q)); + } else { + newText.push(textNode.data.substring(q, q + 1)); + } + newText.push("</span>"); + } + lineChild.innerHTML = newText.join(""); + var rangeChild = lineChild.firstChild; + while (rangeChild) { + rect = rangeChild.getBoundingClientRect(); + if (rect.left <= x && x < rect.right) { + //TODO test for character trailing (wrong for bidi) + if (x > rect.left + (rect.right - rect.left) / 2) { + offset++; + } + break; + } + offset++; + rangeChild = rangeChild.nextSibling; + } + if (!dummy) { + lineChild.innerHTML = ""; + lineChild.appendChild(textNode); + /* + * Removing the element node that holds the selection start or end + * causes the selection to be lost. The fix is to detect this case + * and restore the selection. + */ + var s = this._getSelection(); + if ((offset <= s.start && s.start < offset + nodeLength) || (offset <= s.end && s.end < offset + nodeLength)) { + this._updateDOMSelection(); + } + } + } + break done; + } + } + offset += nodeLength; + lineChild = lineChild.nextSibling; + } + if (dummy) { clientDiv.removeChild(dummy); } + return Math.min(lineEnd, Math.max(lineStart, offset)); + }, + _getYToLine: function (y) { + var viewPad = this._getViewPadding(); + var viewRect = this._viewDiv.getBoundingClientRect(); + y -= viewRect.top + viewPad.top; + var lineHeight = this._getLineHeight(); + var lineIndex = Math.floor((y + this._getScroll().y) / lineHeight); + var lineCount = this._model.getLineCount(); + return Math.max(0, Math.min(lineCount - 1, lineIndex)); + }, + _getOffsetBounds: function(offset) { + var model = this._model; + var lineIndex = model.getLineAtOffset(offset); + var lineHeight = this._getLineHeight(); + var scroll = this._getScroll(); + var viewPad = this._getViewPadding(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var bounds = this._getBoundsAtOffset(offset); + var left = bounds.left; + var right = bounds.right; + var top = (lineIndex * lineHeight) - scroll.y + viewRect.top + viewPad.top; + var bottom = top + lineHeight; + return {left: left, top: top, right: right, bottom: bottom}; + }, + _hitOffset: function (offset, x, y) { + var bounds = this._getOffsetBounds(offset); + var left = bounds.left; + var right = bounds.right; + var top = bounds.top; + var bottom = bounds.bottom; + var area = 20; + left -= area; + top -= area; + right += area; + bottom += area; + return (left <= x && x <= right && top <= y && y <= bottom); + }, + _hookEvents: function() { + var self = this; + this._modelListener = { + /** @private */ + onChanging: function(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + self._onModelChanging(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + }, + /** @private */ + onChanged: function(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + self._onModelChanged(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + } + }; + this._model.addListener(this._modelListener); + + this._mouseMoveClosure = function(e) { return self._handleMouseMove(e);}; + this._mouseUpClosure = function(e) { return self._handleMouseUp(e);}; + + var clientDiv = this._clientDiv; + var viewDiv = this._viewDiv; + var body = this._frameDocument.body; + var handlers = this._handlers = []; + var resizeNode = isIE < 9 ? this._frame : this._frameWindow; + var focusNode = isPad ? this._textArea : (isIE || isFirefox ? this._clientDiv: this._frameWindow); + handlers.push({target: resizeNode, type: "resize", handler: function(e) { return self._handleResize(e);}}); + handlers.push({target: focusNode, type: "blur", handler: function(e) { return self._handleBlur(e);}}); + handlers.push({target: focusNode, type: "focus", handler: function(e) { return self._handleFocus(e);}}); + handlers.push({target: viewDiv, type: "scroll", handler: function(e) { return self._handleScroll(e);}}); + if (isPad) { + var touchDiv = this._touchDiv; + var textArea = this._textArea; + handlers.push({target: textArea, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}}); + handlers.push({target: textArea, type: "input", handler: function(e) { return self._handleInput(e); }}); + handlers.push({target: textArea, type: "textInput", handler: function(e) { return self._handleTextInput(e); }}); + handlers.push({target: touchDiv, type: "touchstart", handler: function(e) { return self._handleTouchStart(e); }}); + handlers.push({target: touchDiv, type: "touchmove", handler: function(e) { return self._handleTouchMove(e); }}); + handlers.push({target: touchDiv, type: "touchend", handler: function(e) { return self._handleTouchEnd(e); }}); + } else { + var topNode = this._overlayDiv || this._clientDiv; + handlers.push({target: clientDiv, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}}); + handlers.push({target: clientDiv, type: "keypress", handler: function(e) { return self._handleKeyPress(e);}}); + handlers.push({target: clientDiv, type: "keyup", handler: function(e) { return self._handleKeyUp(e);}}); + handlers.push({target: clientDiv, type: "selectstart", handler: function(e) { return self._handleSelectStart(e);}}); + handlers.push({target: clientDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e);}}); + handlers.push({target: clientDiv, type: "copy", handler: function(e) { return self._handleCopy(e);}}); + handlers.push({target: clientDiv, type: "cut", handler: function(e) { return self._handleCut(e);}}); + handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}}); + handlers.push({target: topNode, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}}); + handlers.push({target: body, type: "mousedown", handler: function(e) { return self._handleBodyMouseDown(e);}}); + handlers.push({target: topNode, type: "dragstart", handler: function(e) { return self._handleDragStart(e);}}); + handlers.push({target: topNode, type: "dragover", handler: function(e) { return self._handleDragOver(e);}}); + handlers.push({target: topNode, type: "drop", handler: function(e) { return self._handleDrop(e);}}); + if (isIE) { + handlers.push({target: this._frameDocument, type: "activate", handler: function(e) { return self._handleDocFocus(e); }}); + } + if (isFirefox) { + handlers.push({target: this._frameDocument, type: "focus", handler: function(e) { return self._handleDocFocus(e); }}); + } + if (!isIE && !isOpera) { + var wheelEvent = isFirefox ? "DOMMouseScroll" : "mousewheel"; + handlers.push({target: this._viewDiv, type: wheelEvent, handler: function(e) { return self._handleMouseWheel(e); }}); + } + if (isFirefox && !isWindows) { + handlers.push({target: this._clientDiv, type: "DOMCharacterDataModified", handler: function (e) { return self._handleDataModified(e); }}); + } + if (this._overlayDiv) { + handlers.push({target: this._overlayDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e); }}); + } + if (!isW3CEvents) { + handlers.push({target: this._clientDiv, type: "dblclick", handler: function(e) { return self._handleDblclick(e); }}); + } + } + for (var i=0; i<handlers.length; i++) { + var h = handlers[i]; + addHandler(h.target, h.type, h.handler, h.capture); + } + }, + _init: function(options) { + var parent = options.parent; + if (typeof(parent) === "string") { + parent = window.document.getElementById(parent); + } + if (!parent) { throw "no parent"; } + this._parent = parent; + this._model = options.model ? options.model : new orion.textview.TextModel(); + this.readonly = options.readonly === true; + this._selection = new Selection (0, 0, false); + this._eventTable = new EventTable(); + this._maxLineWidth = 0; + this._maxLineIndex = -1; + this._ignoreSelect = true; + this._columnX = -1; + + /* Auto scroll */ + this._autoScrollX = null; + this._autoScrollY = null; + this._autoScrollTimerID = null; + this._AUTO_SCROLL_RATE = 50; + this._grabControl = null; + this._moseMoveClosure = null; + this._mouseUpClosure = null; + + /* Double click */ + this._lastMouseX = 0; + this._lastMouseY = 0; + this._lastMouseTime = 0; + this._clickCount = 0; + this._clickTime = 250; + this._clickDist = 5; + this._isMouseDown = false; + this._doubleClickSelection = null; + + /* Scroll */ + this._hScroll = 0; + this._vScroll = 0; + + /* IME */ + this._imeOffset = -1; + + /* Create elements */ + while (parent.hasChildNodes()) { parent.removeChild(parent.lastChild); } + var parentDocument = parent.document || parent.ownerDocument; + this._parentDocument = parentDocument; + var frame = parentDocument.createElement("IFRAME"); + this._frame = frame; + frame.frameBorder = "0px";//for IE, needs to be set before the frame is added to the parent + frame.style.width = "100%"; + frame.style.height = "100%"; + frame.scrolling = "no"; + frame.style.border = "0px"; + parent.appendChild(frame); + + var html = []; + html.push("<!DOCTYPE html>"); + html.push("<html>"); + html.push("<head>"); + if (isIE < 9) { + html.push("<meta http-equiv='X-UA-Compatible' content='IE=EmulateIE7'/>"); + } + html.push("<style>"); + html.push(".viewContainer {font-family: monospace; font-size: 10pt;}"); + html.push(".view {padding: 1px 2px;}"); + html.push(".viewContent {}"); + html.push("</style>"); + if (options.stylesheet) { + var stylesheet = typeof(options.stylesheet) === "string" ? [options.stylesheet] : options.stylesheet; + for (var i = 0; i < stylesheet.length; i++) { + try { + //Force CSS to be loaded synchronously so lineHeight can be calculated + var objXml = new XMLHttpRequest(); + if (objXml.overrideMimeType) { + objXml.overrideMimeType("text/css"); + } + objXml.open("GET", stylesheet[i], false); + objXml.send(null); + html.push("<style>"); + html.push(objXml.responseText); + html.push("</style>"); + } catch (e) { + html.push("<link rel='stylesheet' type='text/css' href='"); + html.push(stylesheet[i]); + html.push("'></link>"); + } + } + } + html.push("</head>"); + html.push("<body spellcheck='false'></body>"); + html.push("</html>"); + + var frameWindow = frame.contentWindow; + this._frameWindow = frameWindow; + var document = frameWindow.document; + this._frameDocument = document; + document.open(); + document.write(html.join("")); + document.close(); + + var body = document.body; + body.className = "viewContainer"; + body.style.margin = "0px"; + body.style.borderWidth = "0px"; + body.style.padding = "0px"; + + if (isPad) { + var touchDiv = parentDocument.createElement("DIV"); + this._touchDiv = touchDiv; + touchDiv.style.position = "absolute"; + touchDiv.style.border = "0px"; + touchDiv.style.padding = "0px"; + touchDiv.style.margin = "0px"; + touchDiv.style.zIndex = "2"; + touchDiv.style.overflow = "hidden"; + touchDiv.style.background="transparent"; + touchDiv.style.WebkitUserSelect = "none"; + parent.appendChild(touchDiv); + + var textArea = parentDocument.createElement("TEXTAREA"); + this._textArea = textArea; + textArea.style.position = "absolute"; + textArea.style.whiteSpace = "pre"; + textArea.style.left = "-1000px"; + textArea.tabIndex = 1; + textArea.autocapitalize = false; + textArea.autocorrect = false; + textArea.className = "viewContainer"; + textArea.style.background = "transparent"; + textArea.style.color = "transparent"; + textArea.style.border = "0px"; + textArea.style.padding = "0px"; + textArea.style.margin = "0px"; + textArea.style.borderRadius = "0px"; + textArea.style.WebkitAppearance = "none"; + textArea.style.WebkitTapHighlightColor = "transparent"; + touchDiv.appendChild(textArea); + } + + var viewDiv = document.createElement("DIV"); + viewDiv.className = "view"; + this._viewDiv = viewDiv; + viewDiv.id = "viewDiv"; + viewDiv.tabIndex = -1; + viewDiv.style.overflow = "auto"; + viewDiv.style.position = "absolute"; + viewDiv.style.top = "0px"; + viewDiv.style.borderWidth = "0px"; + viewDiv.style.margin = "0px"; + viewDiv.style.MozOutline = "none"; + viewDiv.style.outline = "none"; + body.appendChild(viewDiv); + + var scrollDiv = document.createElement("DIV"); + this._scrollDiv = scrollDiv; + scrollDiv.id = "scrollDiv"; + scrollDiv.style.margin = "0px"; + scrollDiv.style.borderWidth = "0px"; + scrollDiv.style.padding = "0px"; + viewDiv.appendChild(scrollDiv); + + this._fullSelection = options.fullSelection === undefined || options.fullSelection; + /* + * Bug in IE 8. For some reason, during scrolling IE does not reflow the elements + * that are used to compute the location for the selection divs. This causes the + * divs to be placed at the wrong location. The fix is to disabled full selection for IE8. + */ + if (isIE < 9) { + this._fullSelection = false; + } + if (isPad || (this._fullSelection && !isWebkit)) { + this._hightlightRGB = "Highlight"; + var selDiv1 = document.createElement("DIV"); + this._selDiv1 = selDiv1; + selDiv1.id = "selDiv1"; + selDiv1.style.position = "fixed"; + selDiv1.style.borderWidth = "0px"; + selDiv1.style.margin = "0px"; + selDiv1.style.padding = "0px"; + selDiv1.style.MozOutline = "none"; + selDiv1.style.outline = "none"; + selDiv1.style.background = this._hightlightRGB; + selDiv1.style.width="0px"; + selDiv1.style.height="0px"; + scrollDiv.appendChild(selDiv1); + var selDiv2 = document.createElement("DIV"); + this._selDiv2 = selDiv2; + selDiv2.id = "selDiv2"; + selDiv2.style.position = "fixed"; + selDiv2.style.borderWidth = "0px"; + selDiv2.style.margin = "0px"; + selDiv2.style.padding = "0px"; + selDiv2.style.MozOutline = "none"; + selDiv2.style.outline = "none"; + selDiv2.style.background = this._hightlightRGB; + selDiv2.style.width="0px"; + selDiv2.style.height="0px"; + scrollDiv.appendChild(selDiv2); + var selDiv3 = document.createElement("DIV"); + this._selDiv3 = selDiv3; + selDiv3.id = "selDiv3"; + selDiv3.style.position = "fixed"; + selDiv3.style.borderWidth = "0px"; + selDiv3.style.margin = "0px"; + selDiv3.style.padding = "0px"; + selDiv3.style.MozOutline = "none"; + selDiv3.style.outline = "none"; + selDiv3.style.background = this._hightlightRGB; + selDiv3.style.width="0px"; + selDiv3.style.height="0px"; + scrollDiv.appendChild(selDiv3); + + /* + * Bug in Firefox. The Highlight color is mapped to list selection + * background instead of the text selection background. The fix + * is to map known colors using a table or fallback to light blue. + */ + if (isFirefox && isMac) { + var style = frameWindow.getComputedStyle(selDiv3, null); + var rgb = style.getPropertyValue("background-color"); + switch (rgb) { + case "rgb(119, 141, 168)": rgb = "rgb(199, 208, 218)"; break; + case "rgb(127, 127, 127)": rgb = "rgb(198, 198, 198)"; break; + case "rgb(255, 193, 31)": rgb = "rgb(250, 236, 115)"; break; + case "rgb(243, 70, 72)": rgb = "rgb(255, 176, 139)"; break; + case "rgb(255, 138, 34)": rgb = "rgb(255, 209, 129)"; break; + case "rgb(102, 197, 71)": rgb = "rgb(194, 249, 144)"; break; + case "rgb(140, 78, 184)": rgb = "rgb(232, 184, 255)"; break; + default: rgb = "rgb(180, 213, 255)"; break; + } + this._hightlightRGB = rgb; + selDiv1.style.background = rgb; + selDiv2.style.background = rgb; + selDiv3.style.background = rgb; + var styleSheet = document.styleSheets[0]; + styleSheet.insertRule("::-moz-selection {background: " + rgb + "; }", 0); + } + } + + var clientDiv = document.createElement("DIV"); + clientDiv.className = "viewContent"; + this._clientDiv = clientDiv; + clientDiv.id = "clientDiv"; + clientDiv.style.whiteSpace = "pre"; + clientDiv.style.position = "fixed"; + clientDiv.style.borderWidth = "0px"; + clientDiv.style.margin = "0px"; + clientDiv.style.padding = "0px"; + clientDiv.style.MozOutline = "none"; + clientDiv.style.outline = "none"; + if (isPad) { + clientDiv.style.WebkitTapHighlightColor = "transparent"; + } + scrollDiv.appendChild(clientDiv); + + if (isFirefox) { + var overlayDiv = document.createElement("DIV"); + this._overlayDiv = overlayDiv; + overlayDiv.id = "overlayDiv"; + overlayDiv.style.position = clientDiv.style.position; + overlayDiv.style.borderWidth = clientDiv.style.borderWidth; + overlayDiv.style.margin = clientDiv.style.margin; + overlayDiv.style.padding = clientDiv.style.padding; + overlayDiv.style.cursor = "text"; + overlayDiv.style.zIndex = "1"; + scrollDiv.appendChild(overlayDiv); + } + if (!isPad) { + clientDiv.contentEditable = "true"; + } + this._lineHeight = this._calculateLineHeight(); + this._viewPadding = this._calculatePadding(); + if (isIE) { + body.style.lineHeight = this._lineHeight + "px"; + } + if (options.tabSize) { + if (isOpera) { + clientDiv.style.OTabSize = options.tabSize+""; + } else if (isFirefox >= 4) { + clientDiv.style.MozTabSize = options.tabSize+""; + } else if (options.tabSize !== 8) { + this._tabSize = options.tabSize; + } + } + this._createActions(); + this._hookEvents(); + }, + _modifyContent: function(e, updateCaret) { + if (this.readonly && !e._code) { + return; + } + + this.onVerify(e); + + if (e.text === null || e.text === undefined) { return; } + + var model = this._model; + if (e._ignoreDOMSelection) { this._ignoreDOMSelection = true; } + model.setText (e.text, e.start, e.end); + if (e._ignoreDOMSelection) { this._ignoreDOMSelection = false; } + + if (updateCaret) { + var selection = this._getSelection (); + selection.setCaret(e.start + e.text.length); + this._setSelection(selection, true); + } + this.onModify({}); + }, + _onModelChanged: function(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + var e = { + start: start, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onModelChanged(e); + + var selection = this._getSelection(); + if (selection.end > start) { + if (selection.end > start && selection.start < start + removedCharCount) { + // selection intersects replaced text. set caret behind text change + selection.setCaret(start + addedCharCount); + } else { + // move selection to keep same text selected + selection.start += addedCharCount - removedCharCount; + selection.end += addedCharCount - removedCharCount; + } + this._setSelection(selection, false, false); + } + + var model = this._model; + var startLine = model.getLineAtOffset(start); + var child = this._getLineNext(); + while (child) { + var lineIndex = child.lineIndex; + if (startLine <= lineIndex && lineIndex <= startLine + removedLineCount) { + child.lineChanged = true; + } + if (lineIndex > startLine + removedLineCount) { + child.lineIndex = lineIndex + addedLineCount - removedLineCount; + } + child = this._getLineNext(child); + } + if (startLine <= this._maxLineIndex && this._maxLineIndex <= startLine + removedLineCount) { + this._maxLineIndex = -1; + this._maxLineWidth = 0; + } + this._updatePage(); + }, + _onModelChanging: function(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + var e = { + text: newText, + start: start, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onModelChanging(e); + }, + _queueUpdatePage: function() { + if (this._updateTimer) { return; } + var self = this; + this._updateTimer = setTimeout(function() { + self._updateTimer = null; + self._updatePage(); + }, 0); + }, + _resizeTouchDiv: function() { + var viewRect = this._viewDiv.getBoundingClientRect(); + var parentRect = this._frame.getBoundingClientRect(); + var temp = this._frame; + while (temp) { + if (temp.style && temp.style.top) { break; } + temp = temp.parentNode; + } + var parentTop = parentRect.top; + if (temp) { + parentTop -= temp.getBoundingClientRect().top; + } else { + parentTop += this._parentDocument.body.scrollTop; + } + temp = this._frame; + while (temp) { + if (temp.style && temp.style.left) { break; } + temp = temp.parentNode; + } + var parentLeft = parentRect.left; + if (temp) { + parentLeft -= temp.getBoundingClientRect().left; + } else { + parentLeft += this._parentDocument.body.scrollLeft; + } + var touchDiv = this._touchDiv; + touchDiv.style.left = (parentLeft + viewRect.left) + "px"; + touchDiv.style.top = (parentTop + viewRect.top) + "px"; + touchDiv.style.width = viewRect.width + "px"; + touchDiv.style.height = viewRect.height + "px"; + }, + _scrollView: function (pixelX, pixelY) { + /* + * Always set _ensureCaretVisible to false so that the view does not scroll + * to show the caret when scrollView is not called from showCaret(). + */ + this._ensureCaretVisible = false; + + /* + * Scrolling is done only by setting the scrollLeft and scrollTop fields in the + * view div. This causes an updatePage from the scroll event. In some browsers + * this event is asynchromous and forcing update page to run synchronously + * (by calling doScroll) leads to redraw problems. On Chrome 11, the view + * stops redrawing at times when holding PageDown/PageUp key. + * On Firefox 4 for Linux, the view redraws the first page when holding + * PageDown/PageUp key, but it will not redraw again until the key is released. + */ + var viewDiv = this._viewDiv; + if (pixelX) { viewDiv.scrollLeft += pixelX; } + if (pixelY) { viewDiv.scrollTop += pixelY; } + }, + _setClipboardText: function (text, event) { + var clipboardText; + if (this._frameWindow.clipboardData) { + //IE + clipboardText = []; + this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);}); + return this._frameWindow.clipboardData.setData("Text", clipboardText.join("")); + } + /* Feature in Chrome, clipboardData.setData is no-op on Chrome even though it returns true */ + if (isChrome || isFirefox || !event) { + var window = this._frameWindow; + var document = this._frameDocument; + var child = document.createElement("PRE"); + child.style.position = "fixed"; + child.style.left = "-1000px"; + this._convertDelimiter(text, + function(t) { + child.appendChild(document.createTextNode(t)); + }, + function() { + child.appendChild(document.createElement("BR")); + } + ); + child.appendChild(document.createTextNode(" ")); + this._clientDiv.appendChild(child); + var range = document.createRange(); + range.setStart(child.firstChild, 0); + range.setEndBefore(child.lastChild); + var sel = window.getSelection(); + if (sel.rangeCount > 0) { sel.removeAllRanges(); } + sel.addRange(range); + var self = this; + var cleanup = function() { + self._clientDiv.removeChild(child); + self._updateDOMSelection(); + }; + var result = false; + /* + * Try execCommand first, it works on firefox with clipboard permission, + * chrome 5, safari 4. + */ + this._ignoreCopy = true; + try { + result = document.execCommand("copy", false, null); + } catch (e) {} + this._ignoreCopy = false; + if (!result) { + if (event) { + setTimeout(cleanup, 0); + return false; + } + } + /* no event and no permission, copy can not be done */ + cleanup(); + return true; + } + if (event && event.clipboardData) { + //webkit + clipboardText = []; + this._convertDelimiter(text, function(t) {clipboardText.push(t);}, function() {clipboardText.push(platformDelimiter);}); + return event.clipboardData.setData("text/plain", clipboardText.join("")); + } + }, + _setDOMSelection: function (startNode, startOffset, endNode, endOffset) { + var window = this._frameWindow; + var document = this._frameDocument; + var startLineNode, startLineOffset, endLineNode, endLineOffset; + var offset = 0; + var lineChild = startNode.firstChild; + var node, nodeLength, model = this._model; + var startLineEnd = model.getLine(startNode.lineIndex).length; + while (lineChild) { + node = lineChild.firstChild; + nodeLength = node.length; + if (lineChild.ignoreChars) { + nodeLength -= lineChild.ignoreChars; + } + if (offset + nodeLength > startOffset || offset + nodeLength >= startLineEnd) { + startLineNode = node; + startLineOffset = startOffset - offset; + if (lineChild.ignoreChars && nodeLength > 0 && startLineOffset === nodeLength) { + startLineOffset += lineChild.ignoreChars; + } + break; + } + offset += nodeLength; + lineChild = lineChild.nextSibling; + } + offset = 0; + lineChild = endNode.firstChild; + var endLineEnd = this._model.getLine(endNode.lineIndex).length; + while (lineChild) { + node = lineChild.firstChild; + nodeLength = node.length; + if (lineChild.ignoreChars) { + nodeLength -= lineChild.ignoreChars; + } + if (nodeLength + offset > endOffset || offset + nodeLength >= endLineEnd) { + endLineNode = node; + endLineOffset = endOffset - offset; + if (lineChild.ignoreChars && nodeLength > 0 && endLineOffset === nodeLength) { + endLineOffset += lineChild.ignoreChars; + } + break; + } + offset += nodeLength; + lineChild = lineChild.nextSibling; + } + + this._setDOMFullSelection(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd); + if (isPad) { return; } + + var range; + if (window.getSelection) { + //W3C + range = document.createRange(); + range.setStart(startLineNode, startLineOffset); + range.setEnd(endLineNode, endLineOffset); + var sel = window.getSelection(); + this._ignoreSelect = false; + if (sel.rangeCount > 0) { sel.removeAllRanges(); } + sel.addRange(range); + this._ignoreSelect = true; + } else if (document.selection) { + //IE < 9 + var body = document.body; + + /* + * Bug in IE. For some reason when text is deselected the overflow + * selection at the end of some lines does not get redrawn. The + * fix is to create a DOM element in the body to force a redraw. + */ + var child = document.createElement("DIV"); + body.appendChild(child); + body.removeChild(child); + + range = body.createTextRange(); + range.moveToElementText(startLineNode.parentNode); + range.moveStart("character", startLineOffset); + var endRange = body.createTextRange(); + endRange.moveToElementText(endLineNode.parentNode); + endRange.moveStart("character", endLineOffset); + range.setEndPoint("EndToStart", endRange); + this._ignoreSelect = false; + range.select(); + this._ignoreSelect = true; + } + }, + _setDOMFullSelection: function(startNode, startOffset, startLineEnd, endNode, endOffset, endLineEnd) { + var model = this._model; + if (this._selDiv1) { + var startLineBounds, l; + startLineBounds = this._getLineBoundingClientRect(startNode); + if (startOffset === 0) { + l = startLineBounds.left; + } else { + if (startOffset >= startLineEnd) { + l = startLineBounds.right; + } else { + this._ignoreDOMSelection = true; + l = this._getBoundsAtOffset(model.getLineStart(startNode.lineIndex) + startOffset).left; + this._ignoreDOMSelection = false; + } + } + var textArea = this._textArea; + if (textArea) { + textArea.selectionStart = textArea.selectionEnd = 0; + var rect = this._frame.getBoundingClientRect(); + var touchRect = this._touchDiv.getBoundingClientRect(); + var viewBounds = this._viewDiv.getBoundingClientRect(); + if (!(viewBounds.left <= l && l <= viewBounds.left + viewBounds.width && + viewBounds.top <= startLineBounds.top && startLineBounds.top <= viewBounds.top + viewBounds.height) || + !(startNode === endNode && startOffset === endOffset)) + { + textArea.style.left = "-1000px"; + } else { + textArea.style.left = (l - 4 + rect.left - touchRect.left) + "px"; + } + textArea.style.top = (startLineBounds.top + rect.top - touchRect.top) + "px"; + textArea.style.width = "6px"; + textArea.style.height = (startLineBounds.bottom - startLineBounds.top) + "px"; + } + + var selDiv = this._selDiv1; + selDiv.style.width = "0px"; + selDiv.style.height = "0px"; + selDiv = this._selDiv2; + selDiv.style.width = "0px"; + selDiv.style.height = "0px"; + selDiv = this._selDiv3; + selDiv.style.width = "0px"; + selDiv.style.height = "0px"; + if (!(startNode === endNode && startOffset === endOffset)) { + var handleWidth = isPad ? 2 : 0; + var handleBorder = handleWidth + "px blue solid"; + var viewPad = this._getViewPadding(); + var clientRect = this._clientDiv.getBoundingClientRect(); + var viewRect = this._viewDiv.getBoundingClientRect(); + var left = viewRect.left + viewPad.left; + var right = clientRect.right; + var top = viewRect.top + viewPad.top; + var bottom = clientRect.bottom; + var r; + var endLineBounds = this._getLineBoundingClientRect(endNode); + if (endOffset === 0) { + r = endLineBounds.left; + } else { + if (endOffset >= endLineEnd) { + r = endLineBounds.right; + } else { + this._ignoreDOMSelection = true; + r = this._getBoundsAtOffset(model.getLineStart(endNode.lineIndex) + endOffset).left; + this._ignoreDOMSelection = false; + } + } + var sel1Div = this._selDiv1; + var sel1Left = Math.min(right, Math.max(left, l)); + var sel1Top = Math.min(bottom, Math.max(top, startLineBounds.top)); + var sel1Right = right; + var sel1Bottom = Math.min(bottom, Math.max(top, startLineBounds.bottom)); + sel1Div.style.left = sel1Left + "px"; + sel1Div.style.top = sel1Top + "px"; + sel1Div.style.width = Math.max(0, sel1Right - sel1Left) + "px"; + sel1Div.style.height = Math.max(0, sel1Bottom - sel1Top) + (isPad ? 1 : 0) + "px"; + if (isPad) { + sel1Div.style.borderLeft = handleBorder; + sel1Div.style.borderRight = "0px"; + } + if (startNode === endNode) { + sel1Right = Math.min(r, right); + sel1Div.style.width = Math.max(0, sel1Right - sel1Left - handleWidth * 2) + "px"; + if (isPad) { + sel1Div.style.borderRight = handleBorder; + } + } else { + var sel3Left = left; + var sel3Top = Math.min(bottom, Math.max(top, endLineBounds.top)); + var sel3Right = Math.min(right, Math.max(left, r)); + var sel3Bottom = Math.min(bottom, Math.max(top, endLineBounds.bottom)); + var sel3Div = this._selDiv3; + sel3Div.style.left = sel3Left + "px"; + sel3Div.style.top = sel3Top + "px"; + sel3Div.style.width = Math.max(0, sel3Right - sel3Left - handleWidth) + "px"; + sel3Div.style.height = Math.max(0, sel3Bottom - sel3Top) + "px"; + if (isPad) { + sel3Div.style.borderRight = handleBorder; + } + if (sel3Top - sel1Bottom > 0) { + var sel2Div = this._selDiv2; + sel2Div.style.left = left + "px"; + sel2Div.style.top = sel1Bottom + "px"; + sel2Div.style.width = Math.max(0, right - left) + "px"; + sel2Div.style.height = Math.max(0, sel3Top - sel1Bottom) + (isPad ? 1 : 0) + "px"; + } + } + } + } + }, + _setGrab: function (target) { + if (target === this._grabControl) { return; } + if (target) { + addHandler(target, "mousemove", this._mouseMoveClosure); + addHandler(target, "mouseup", this._mouseUpClosure); + if (target.setCapture) { target.setCapture(); } + this._grabControl = target; + } else { + removeHandler(this._grabControl, "mousemove", this._mouseMoveClosure); + removeHandler(this._grabControl, "mouseup", this._mouseUpClosure); + if (this._grabControl.releaseCapture) { this._grabControl.releaseCapture(); } + this._grabControl = null; + } + }, + _setSelection: function (selection, scroll, update) { + if (selection) { + this._columnX = -1; + if (update === undefined) { update = true; } + var oldSelection = this._selection; + if (!oldSelection.equals(selection)) { + this._selection = selection; + var e = { + oldValue: {start:oldSelection.start, end:oldSelection.end}, + newValue: {start:selection.start, end:selection.end} + }; + this.onSelection(e); + } + /* + * Always showCaret(), even when the selection is not changing, to ensure the + * caret is visible. Note that some views do not scroll to show the caret during + * keyboard navigation when the selection does not chanage. For example, line down + * when the caret is already at the last line. + */ + if (scroll) { update = !this._showCaret(); } + + /* + * Sometimes the browser changes the selection + * as result of method calls or "leaked" events. + * The fix is to set the visual selection even + * when the logical selection is not changed. + */ + if (update) { this._updateDOMSelection(); } + } + }, + _setSelectionTo: function (x,y,extent) { + var model = this._model, offset; + var selection = this._getSelection(); + var lineIndex = this._getYToLine(y); + if (this._clickCount === 1) { + offset = this._getXToOffset(lineIndex, x); + selection.extend(offset); + if (!extent) { selection.collapse(); } + } else { + var word = (this._clickCount & 1) === 0; + var start, end; + if (word) { + offset = this._getXToOffset(lineIndex, x); + if (this._doubleClickSelection) { + if (offset >= this._doubleClickSelection.start) { + start = this._doubleClickSelection.start; + end = this._getOffset(offset, "wordend", +1); + } else { + start = this._getOffset(offset, "word", -1); + end = this._doubleClickSelection.end; + } + } else { + start = this._getOffset(offset, "word", -1); + end = this._getOffset(start, "wordend", +1); + } + } else { + if (this._doubleClickSelection) { + var doubleClickLine = model.getLineAtOffset(this._doubleClickSelection.start); + if (lineIndex >= doubleClickLine) { + start = model.getLineStart(doubleClickLine); + end = model.getLineEnd(lineIndex); + } else { + start = model.getLineStart(lineIndex); + end = model.getLineEnd(doubleClickLine); + } + } else { + start = model.getLineStart(lineIndex); + end = model.getLineEnd(lineIndex); + } + } + selection.setCaret(start); + selection.extend(end); + } + this._setSelection(selection, true, true); + }, + _showCaret: function () { + var model = this._model; + var selection = this._getSelection(); + var scroll = this._getScroll(); + var caret = selection.getCaret(); + var start = selection.start; + var end = selection.end; + var startLine = model.getLineAtOffset(start); + var endLine = model.getLineAtOffset(end); + var endInclusive = Math.max(Math.max(start, model.getLineStart(endLine)), end - 1); + var viewPad = this._getViewPadding(); + + var clientWidth = this._getClientWidth(); + var leftEdge = viewPad.left; + var rightEdge = viewPad.left + clientWidth; + var bounds = this._getBoundsAtOffset(caret === start ? start : endInclusive); + var left = bounds.left; + var right = bounds.right; + var minScroll = clientWidth / 4; + if (!selection.isEmpty() && startLine === endLine) { + bounds = this._getBoundsAtOffset(caret === end ? start : endInclusive); + var selectionWidth = caret === start ? bounds.right - left : right - bounds.left; + if ((clientWidth - minScroll) > selectionWidth) { + if (left > bounds.left) { left = bounds.left; } + if (right < bounds.right) { right = bounds.right; } + } + } + var viewRect = this._viewDiv.getBoundingClientRect(); + left -= viewRect.left; + right -= viewRect.left; + var pixelX = 0; + if (left < leftEdge) { + pixelX = Math.min(left - leftEdge, -minScroll); + } + if (right > rightEdge) { + var maxScroll = this._scrollDiv.scrollWidth - scroll.x - clientWidth; + pixelX = Math.min(maxScroll, Math.max(right - rightEdge, minScroll)); + } + + var pixelY = 0; + var topIndex = this._getTopIndex(true); + var bottomIndex = this._getBottomIndex(true); + var caretLine = model.getLineAtOffset(caret); + var clientHeight = this._getClientHeight(); + if (!(topIndex <= caretLine && caretLine <= bottomIndex)) { + var lineHeight = this._getLineHeight(); + var selectionHeight = (endLine - startLine) * lineHeight; + pixelY = caretLine * lineHeight; + pixelY -= scroll.y; + if (pixelY + lineHeight > clientHeight) { + pixelY -= clientHeight - lineHeight; + if (caret === start && start !== end) { + pixelY += Math.min(clientHeight - lineHeight, selectionHeight); + } + } else { + if (caret === end) { + pixelY -= Math.min (clientHeight - lineHeight, selectionHeight); + } + } + } + + if (pixelX !== 0 || pixelY !== 0) { + this._scrollView (pixelX, pixelY); + /* + * When the view scrolls it is possible that one of the scrollbars can show over the caret. + * Depending on the browser scrolling can be synchronous (Safari), in which case the change + * can be detected before showCaret() returns. When scrolling is asynchronous (most browsers), + * the detection is done during the next update page. + */ + if (clientHeight !== this._getClientHeight() || clientWidth !== this._getClientWidth()) { + this._showCaret(); + } else { + this._ensureCaretVisible = true; + } + return true; + } + return false; + }, + _startIME: function () { + if (this._imeOffset !== -1) { return; } + var selection = this._getSelection(); + if (!selection.isEmpty()) { + this._modifyContent({text: "", start: selection.start, end: selection.end}, true); + } + this._imeOffset = selection.start; + }, + _unhookEvents: function() { + this._model.removeListener(this._modelListener); + this._modelListener = null; + + this._mouseMoveClosure = null; + this._mouseUpClosure = null; + + for (var i=0; i<this._handlers.length; i++) { + var h = this._handlers[i]; + removeHandler(h.target, h.type, h.handler); + } + this._handlers = null; + }, + _updateDOMSelection: function () { + if (this._ignoreDOMSelection) { return; } + var selection = this._getSelection(); + var model = this._model; + var startLine = model.getLineAtOffset(selection.start); + var endLine = model.getLineAtOffset(selection.end); + var firstNode = this._getLineNext(); + /* + * Bug in Firefox. For some reason, after a update page sometimes the + * firstChild returns null incorrectly. The fix is to ignore show selection. + */ + if (!firstNode) { return; } + var lastNode = this._getLinePrevious(); + + var topNode, bottomNode, topOffset, bottomOffset; + if (startLine < firstNode.lineIndex) { + topNode = firstNode; + topOffset = 0; + } else if (startLine > lastNode.lineIndex) { + topNode = lastNode; + topOffset = 0; + } else { + topNode = this._getLineNode(startLine); + topOffset = selection.start - model.getLineStart(startLine); + } + + if (endLine < firstNode.lineIndex) { + bottomNode = firstNode; + bottomOffset = 0; + } else if (endLine > lastNode.lineIndex) { + bottomNode = lastNode; + bottomOffset = 0; + } else { + bottomNode = this._getLineNode(endLine); + bottomOffset = selection.end - model.getLineStart(endLine); + } + this._setDOMSelection(topNode, topOffset, bottomNode, bottomOffset); + }, + _updatePage: function() { + if (this._updateTimer) { + clearTimeout(this._updateTimer); + this._updateTimer = null; + } + var document = this._frameDocument; + var frameWidth = this._getFrameWidth(); + var frameHeight = this._getFrameHeight(); + document.body.style.width = frameWidth + "px"; + document.body.style.height = frameHeight + "px"; + + var viewDiv = this._viewDiv; + var clientDiv = this._clientDiv; + var viewPad = this._getViewPadding(); + + /* Update view height in order to have client height computed */ + viewDiv.style.height = Math.max(0, (frameHeight - viewPad.top - viewPad.bottom)) + "px"; + + var model = this._model; + var lineHeight = this._getLineHeight(); + var scrollY = this._getScroll().y; + var firstLine = Math.max(0, scrollY) / lineHeight; + var topIndex = Math.floor(firstLine); + var lineStart = Math.max(0, topIndex - 1); + var top = Math.round((firstLine - lineStart) * lineHeight); + var lineCount = model.getLineCount(); + var clientHeight = this._getClientHeight(); + var partialY = Math.round((firstLine - topIndex) * lineHeight); + var linesPerPage = Math.floor((clientHeight + partialY) / lineHeight); + var bottomIndex = Math.min(topIndex + linesPerPage, lineCount - 1); + var lineEnd = Math.min(bottomIndex + 1, lineCount - 1); + this._partialY = partialY; + + var lineIndex, lineWidth; + var child = clientDiv.firstChild; + while (child) { + lineIndex = child.lineIndex; + var nextChild = child.nextSibling; + if (!(lineStart <= lineIndex && lineIndex <= lineEnd) || child.lineChanged || child.lineIndex === -1) { + if (this._mouseWheelLine === child) { + child.style.display = "none"; + child.lineIndex = -1; + } else { + clientDiv.removeChild(child); + } + } + child = nextChild; + } + + child = this._getLineNext(); + var frag = document.createDocumentFragment(); + for (lineIndex=lineStart; lineIndex<=lineEnd; lineIndex++) { + if (!child || child.lineIndex > lineIndex) { + this._createLine(frag, null, document, lineIndex, model); + } else { + if (frag.firstChild) { + clientDiv.insertBefore(frag, child); + frag = document.createDocumentFragment(); + } + child = this._getLineNext(child); + } + } + if (frag.firstChild) { clientDiv.insertBefore(frag, child); } + + /* + * Feature in WekKit. Webkit limits the width of the lines + * computed below to the width of the client div. This causes + * the lines to be wrapped even though "pre" is set. The fix + * is to set the width of the client div to a larger number + * before computing the lines width. Note that this value is + * reset to the appropriate value further down. + */ + if (isWebkit) { + clientDiv.style.width = (0x7FFFF).toString() + "px"; + } + + child = this._getLineNext(); + while (child) { + lineWidth = child.lineWidth; + if (lineWidth === undefined) { + var rect = this._getLineBoundingClientRect(child); + lineWidth = child.lineWidth = rect.right - rect.left; + } + if (lineWidth >= this._maxLineWidth) { + this._maxLineWidth = lineWidth; + this._maxLineIndex = child.lineIndex; + } + if (child.lineIndex === topIndex) { this._topChild = child; } + if (child.lineIndex === bottomIndex) { this._bottomChild = child; } + child = this._getLineNext(child); + } + + // Update rulers + this._updateRuler(this._leftDiv, topIndex, bottomIndex); + this._updateRuler(this._rightDiv, topIndex, bottomIndex); + + var leftWidth = this._leftDiv ? this._leftDiv.scrollWidth : 0; + var rightWidth = this._rightDiv ? this._rightDiv.scrollWidth : 0; + viewDiv.style.left = leftWidth + "px"; + viewDiv.style.width = Math.max(0, frameWidth - leftWidth - rightWidth - viewPad.left - viewPad.right) + "px"; + if (this._rightDiv) { + this._rightDiv.style.left = (frameWidth - rightWidth) + "px"; + } + + var scrollDiv = this._scrollDiv; + /* Need to set the height first in order for the width to consider the vertical scrollbar */ + var scrollHeight = lineCount * lineHeight; + scrollDiv.style.height = scrollHeight + "px"; + var clientWidth = this._getClientWidth(); + var width = Math.max(this._maxLineWidth, clientWidth); + /* + * Except by IE 8 and earlier, all other browsers are not allocating enough space for the right padding + * in the scrollbar. It is possible this a bug since all other paddings are considered. + */ + var scrollWidth = width; + if (!isIE || isIE >= 9) { width += viewPad.right; } + scrollDiv.style.width = width + "px"; + + // Get the left scroll after setting the width of the scrollDiv as this can change the horizontal scroll offset. + var scroll = this._getScroll(); + var left = scroll.x; + var clipLeft = left; + var clipTop = top; + var clipRight = left + clientWidth; + var clipBottom = top + clientHeight; + if (clipLeft === 0) { clipLeft -= viewPad.left; } + if (clipTop === 0) { clipTop -= viewPad.top; } + if (clipRight === scrollWidth) { clipRight += viewPad.right; } + if (scroll.y + clientHeight === scrollHeight) { clipBottom += viewPad.bottom; } + clientDiv.style.clip = "rect(" + clipTop + "px," + clipRight + "px," + clipBottom + "px," + clipLeft + "px)"; + clientDiv.style.left = (-left + leftWidth + viewPad.left) + "px"; + clientDiv.style.top = (-top + viewPad.top) + "px"; + clientDiv.style.width = (isWebkit ? scrollWidth : clientWidth + left) + "px"; + clientDiv.style.height = (clientHeight + top) + "px"; + var overlayDiv = this._overlayDiv; + if (overlayDiv) { + overlayDiv.style.clip = clientDiv.style.clip; + overlayDiv.style.left = clientDiv.style.left; + overlayDiv.style.top = clientDiv.style.top; + overlayDiv.style.width = clientDiv.style.width; + overlayDiv.style.height = clientDiv.style.height; + } + function _updateRulerSize(divRuler) { + if (!divRuler) { return; } + var rulerHeight = clientHeight + viewPad.top + viewPad.bottom; + var cells = divRuler.firstChild.rows[0].cells; + for (var i = 0; i < cells.length; i++) { + var div = cells[i].firstChild; + var offset = lineHeight; + if (div._ruler.getOverview() === "page") { offset += partialY; } + div.style.top = -offset + "px"; + div.style.height = (rulerHeight + offset) + "px"; + div = div.nextSibling; + } + divRuler.style.height = rulerHeight + "px"; + } + _updateRulerSize(this._leftDiv); + _updateRulerSize(this._rightDiv); + if (isPad) { + var self = this; + setTimeout(function() {self._resizeTouchDiv();}, 0); + } + this._updateDOMSelection(); + + /* + * If the client height changed during the update page it means that scrollbar has either been shown or hidden. + * When this happens update page has to run again to ensure that the top and bottom lines div are correct. + * + * Note: On IE, updateDOMSelection() has to be called before getting the new client height because it + * forces the client area to be recomputed. + */ + var ensureCaretVisible = this._ensureCaretVisible; + this._ensureCaretVisible = false; + if (clientHeight !== this._getClientHeight()) { + this._updatePage(); + if (ensureCaretVisible) { + this._showCaret(); + } + } + }, + _updateRuler: function (divRuler, topIndex, bottomIndex) { + if (!divRuler) { return; } + var cells = divRuler.firstChild.rows[0].cells; + var lineHeight = this._getLineHeight(); + var parentDocument = this._frameDocument; + var viewPad = this._getViewPadding(); + for (var i = 0; i < cells.length; i++) { + var div = cells[i].firstChild; + var ruler = div._ruler, style; + if (div.rulerChanged) { + this._applyStyle(ruler.getStyle(), div); + } + + var widthDiv; + var child = div.firstChild; + if (child) { + widthDiv = child; + child = child.nextSibling; + } else { + widthDiv = parentDocument.createElement("DIV"); + widthDiv.style.visibility = "hidden"; + div.appendChild(widthDiv); + } + var lineIndex; + if (div.rulerChanged) { + if (widthDiv) { + lineIndex = -1; + this._applyStyle(ruler.getStyle(lineIndex), widthDiv); + widthDiv.innerHTML = ruler.getHTML(lineIndex); + widthDiv.lineIndex = lineIndex; + widthDiv.style.height = (lineHeight + viewPad.top) + "px"; + } + } + + var overview = ruler.getOverview(), lineDiv, frag; + if (overview === "page") { + while (child) { + lineIndex = child.lineIndex; + var nextChild = child.nextSibling; + if (!(topIndex <= lineIndex && lineIndex <= bottomIndex) || child.lineChanged) { + div.removeChild(child); + } + child = nextChild; + } + child = div.firstChild.nextSibling; + frag = document.createDocumentFragment(); + for (lineIndex=topIndex; lineIndex<=bottomIndex; lineIndex++) { + if (!child || child.lineIndex > lineIndex) { + lineDiv = parentDocument.createElement("DIV"); + this._applyStyle(ruler.getStyle(lineIndex), lineDiv); + lineDiv.innerHTML = ruler.getHTML(lineIndex); + lineDiv.lineIndex = lineIndex; + lineDiv.style.height = lineHeight + "px"; + frag.appendChild(lineDiv); + } else { + if (frag.firstChild) { + div.insertBefore(frag, child); + frag = document.createDocumentFragment(); + } + if (child) { + child = child.nextSibling; + } + } + } + if (frag.firstChild) { div.insertBefore(frag, child); } + } else { + var buttonHeight = 17; + var clientHeight = this._getClientHeight (); + var trackHeight = clientHeight + viewPad.top + viewPad.bottom - 2 * buttonHeight; + var lineCount = this._model.getLineCount (); + var divHeight = trackHeight / lineCount; + if (div.rulerChanged) { + var count = div.childNodes.length; + while (count > 1) { + div.removeChild(div.lastChild); + count--; + } + var lines = ruler.getAnnotations (); + frag = document.createDocumentFragment(); + for (var j = 0; j < lines.length; j++) { + lineIndex = lines[j]; + lineDiv = parentDocument.createElement("DIV"); + this._applyStyle(ruler.getStyle(lineIndex), lineDiv); + lineDiv.style.position = "absolute"; + lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineIndex * divHeight) + "px"; + lineDiv.innerHTML = ruler.getHTML(lineIndex); + lineDiv.lineIndex = lineIndex; + frag.appendChild(lineDiv); + } + div.appendChild(frag); + } else if (div._oldTrackHeight !== trackHeight) { + lineDiv = div.firstChild ? div.firstChild.nextSibling : null; + while (lineDiv) { + lineDiv.style.top = buttonHeight + lineHeight + Math.floor(lineDiv.lineIndex * divHeight) + "px"; + lineDiv = lineDiv.nextSibling; + } + } + div._oldTrackHeight = trackHeight; + } + div.rulerChanged = false; + div = div.nextSibling; + } + } + };//end prototype + + return TextView; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define(['orion/textview/textModel', 'orion/textview/keyBinding'], function() { + return orion.textview; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textview.css b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textview.css new file mode 100644 index 00000000..510648ca --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/textview.css @@ -0,0 +1,11 @@ +.view { + background-color: white; +} + +.viewContainer { + font-family: monospace; + font-size: 10pt; +} + +.viewContent { +}
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/undoStack.js b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/undoStack.js new file mode 100644 index 00000000..e56a8707 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/orion/orion/textview/undoStack.js @@ -0,0 +1,339 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*global window define */ + +/** + * @namespace The global container for Orion APIs. + */ +var orion = orion || {}; +orion.textview = orion.textview || {}; + +/** + * Constructs a new UndoStack on a text view. + * + * @param {orion.textview.TextView} view the text view for the undo stack. + * @param {Number} [size=100] the size for the undo stack. + * + * @name orion.textview.UndoStack + * @class The UndoStack is used to record the history of a text model associated to an view. Every + * change to the model is added to stack, allowing the application to undo and redo these changes. + * + * <p> + * <b>See:</b><br/> + * {@link orion.textview.TextView}<br/> + * </p> + */ +orion.textview.UndoStack = (function() { + /** + * Constructs a new Change object. + * + * @class + * @name orion.textview.Change + * @private + */ + var Change = (function() { + function Change(offset, text, previousText) { + this.offset = offset; + this.text = text; + this.previousText = previousText; + } + Change.prototype = { + undo: function (view, select) { + this._doUndoRedo(this.offset, this.previousText, this.text, view, select); + }, + redo: function (view, select) { + this._doUndoRedo(this.offset, this.text, this.previousText, view, select); + }, + _doUndoRedo: function(offset, text, previousText, view, select) { + view.setText(text, offset, offset + previousText.length); + if (select) { + view.setSelection(offset, offset + text.length); + } + } + }; + return Change; + }()); + + /** + * Constructs a new CompoundChange object. + * + * @class + * @name orion.textview.CompoundChange + * @private + */ + var CompoundChange = (function() { + function CompoundChange (selection, caret) { + this.selection = selection; + this.caret = caret; + this.changes = []; + } + CompoundChange.prototype = { + add: function (change) { + this.changes.push(change); + }, + undo: function (view, select) { + for (var i=this.changes.length - 1; i >= 0; i--) { + this.changes[i].undo(view, false); + } + if (select) { + var start = this.selection.start; + var end = this.selection.end; + view.setSelection(this.caret ? start : end, this.caret ? end : start); + } + }, + redo: function (view, select) { + for (var i = 0; i < this.changes.length; i++) { + this.changes[i].redo(view, false); + } + if (select) { + var start = this.selection.start; + var end = this.selection.end; + view.setSelection(this.caret ? start : end, this.caret ? end : start); + } + } + }; + return CompoundChange; + }()); + + /** @private */ + function UndoStack (view, size) { + this.view = view; + this.size = size !== undefined ? size : 100; + this.reset(); + view.addEventListener("ModelChanging", this, this._onModelChanging); + view.addEventListener("Destroy", this, this._onDestroy); + } + UndoStack.prototype = /** @lends orion.textview.UndoStack.prototype */ { + /** + * Adds a change to the stack. + * + * @param change the change to add. + * @param {Number} change.offset the offset of the change + * @param {String} change.text the new text of the change + * @param {String} change.previousText the previous text of the change + */ + add: function (change) { + if (this.compoundChange) { + this.compoundChange.add(change); + } else { + var length = this.stack.length; + this.stack.splice(this.index, length-this.index, change); + this.index++; + if (this.stack.length > this.size) { + this.stack.shift(); + this.index--; + this.cleanIndex--; + } + } + }, + /** + * Marks the current state of the stack as clean. + * + * <p> + * This function is typically called when the content of view associated with the stack is saved. + * </p> + * + * @see #isClean + */ + markClean: function() { + this.endCompoundChange(); + this._commitUndo(); + this.cleanIndex = this.index; + }, + /** + * Returns true if current state of stack is the same + * as the state when markClean() was called. + * + * <p> + * For example, the application calls markClean(), then calls undo() four times and redo() four times. + * At this point isClean() returns true. + * </p> + * <p> + * This function is typically called to determine if the content of the view associated with the stack + * has changed since the last time it was saved. + * </p> + * + * @return {Boolean} returns if the state is the same as the state when markClean() was called. + * + * @see #markClean + */ + isClean: function() { + return this.cleanIndex === this.getSize().undo; + }, + /** + * Returns true if there is at least one change to undo. + * + * @return {Boolean} returns true if there is at least one change to undo. + * + * @see #canRedo + * @see #undo + */ + canUndo: function() { + return this.getSize().undo > 0; + }, + /** + * Returns true if there is at least one change to redo. + * + * @return {Boolean} returns true if there is at least one change to redo. + * + * @see #canUndo + * @see #redo + */ + canRedo: function() { + return this.getSize().redo > 0; + }, + /** + * Finishes a compound change. + * + * @see #startCompoundChange + */ + endCompoundChange: function() { + this.compoundChange = undefined; + }, + /** + * Returns the sizes of the stack. + * + * @return {object} a object where object.undo is the number of changes that can be un-done, + * and object.redo is the number of changes that can be re-done. + * + * @see #canUndo + * @see #canRedo + */ + getSize: function() { + var index = this.index; + var length = this.stack.length; + if (this._undoStart !== undefined) { + index++; + } + return {undo: index, redo: (length - index)}; + }, + /** + * Undo the last change in the stack. + * + * @return {Boolean} returns true if a change was un-done. + * + * @see #redo + * @see #canUndo + */ + undo: function() { + this._commitUndo(); + if (this.index <= 0) { + return false; + } + var change = this.stack[--this.index]; + this._ignoreUndo = true; + change.undo(this.view, true); + this._ignoreUndo = false; + return true; + }, + /** + * Redo the last change in the stack. + * + * @return {Boolean} returns true if a change was re-done. + * + * @see #undo + * @see #canRedo + */ + redo: function() { + this._commitUndo(); + if (this.index >= this.stack.length) { + return false; + } + var change = this.stack[this.index++]; + this._ignoreUndo = true; + change.redo(this.view, true); + this._ignoreUndo = false; + return true; + }, + /** + * Reset the stack to its original state. All changes in the stack are thrown away. + */ + reset: function() { + this.index = this.cleanIndex = 0; + this.stack = []; + this._undoStart = undefined; + this._undoText = ""; + this._ignoreUndo = false; + this._compoundChange = undefined; + }, + /** + * Starts a compound change. + * <p> + * All changes added to stack from the time startCompoundChange() is called + * to the time that endCompoundChange() is called are compound on one change that can be un-done or re-done + * with one single call to undo() or redo(). + * </p> + * + * @see #endCompoundChange + */ + startCompoundChange: function() { + var change = new CompoundChange(this.view.getSelection(), this.view.getCaretOffset()); + this.add(change); + this.compoundChange = change; + }, + _commitUndo: function () { + if (this._undoStart !== undefined) { + if (this._undoStart < 0) { + this.add(new Change(-this._undoStart, "", this._undoText, "")); + } else { + this.add(new Change(this._undoStart, this._undoText, "")); + } + this._undoStart = undefined; + this._undoText = ""; + } + }, + _onDestroy: function() { + this.view.removeEventListener("ModelChanging", this, this._onModelChanging); + this.view.removeEventListener("Destroy", this, this._onDestroy); + }, + _onModelChanging: function(e) { + var newText = e.text; + var start = e.start; + var removedCharCount = e.removedCharCount; + var addedCharCount = e.addedCharCount; + if (this._ignoreUndo) { + return; + } + if (this._undoStart !== undefined && + !((addedCharCount === 1 && removedCharCount === 0 && start === this._undoStart + this._undoText.length) || + (addedCharCount === 0 && removedCharCount === 1 && (((start + 1) === -this._undoStart) || (start === -this._undoStart))))) + { + this._commitUndo(); + } + if (!this.compoundChange) { + if (addedCharCount === 1 && removedCharCount === 0) { + if (this._undoStart === undefined) { + this._undoStart = start; + } + this._undoText = this._undoText + newText; + return; + } else if (addedCharCount === 0 && removedCharCount === 1) { + var deleting = this._undoText.length > 0 && -this._undoStart === start; + this._undoStart = -start; + if (deleting) { + this._undoText = this._undoText + this.view.getText(start, start + removedCharCount); + } else { + this._undoText = this.view.getText(start, start + removedCharCount) + this._undoText; + } + return; + } + } + this.add(new Change(start, newText, this.view.getText(start, start + removedCharCount))); + } + }; + return UndoStack; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return orion.textview; + }); +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/plugin.xml b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/plugin.xml new file mode 100644 index 00000000..0b837eee --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/plugin.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?eclipse version="3.4"?> +<plugin> + + <extension + point="org.eclipse.ui.editors"> + <editor + class="org.eclipse.e4.examples.webintegration.orion.editor.plugin.Editor" + default="true" + extensions="js" + icon="orion-old/images/silk/page.png" + id="org.eclipse.e4.examples.webintegration.plugins.editors.orion" + name="Embedded Orion Editor"> + </editor> + </extension> +</plugin> diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/src/org/eclipse/e4/examples/webintegration/orion/editor/plugin/Editor.java b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/src/org/eclipse/e4/examples/webintegration/orion/editor/plugin/Editor.java new file mode 100644 index 00000000..b55626f6 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/src/org/eclipse/e4/examples/webintegration/orion/editor/plugin/Editor.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * 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: + * Dean Roberts, IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.e4.examples.webintegration.orion.editor.plugin; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.URL; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jface.action.IStatusLineManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.action.StatusLineContributionItem; +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.LocationEvent; +import org.eclipse.swt.browser.LocationListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorSite; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.part.EditorPart; + + +/** + * Example of hosting an editor in a BrowserWidget while + * implementing interesting parts of the Eclipse Workbench + * editor life cycle and properties: + * 1) Opening local content + * 2) Dirty indicator + * 3) Saving + * 4) Eclipse Status Bar (cursor position) + * + * Probably the most interesting part of this example is how + * the web application is implemented to allow the application + * to run in either a Browser or the Workbench with maximum integration + * using minimal code changes. + * + * The particular editor we embed is the Orion editor from http://eclipse.org/orion/ + * + * This example is discussed at http://deanoneclipse.wordpress.com + */ +public class Editor extends EditorPart { + private Browser browser; + private boolean isDirty = false; + private EditorService editorService; + private IStatusLineManager statusLineManager; + private StatusLineContributionItem position; + private StatusLineContributionItem keyMode; + private StatusLineContributionItem writeMode; + + // Create the editor widgets + public void createPartControl(Composite parent) { + // Use the system's default browser + browser = new Browser(parent, SWT.NONE); + + URL resource = Editor.class.getClassLoader().getResource("orion/examples/editor/embeddededitor.html"); + try { + URL resolved = FileLocator.resolve(resource); + String qualifiedPath = resolved.toExternalForm(); + browser.setUrl(qualifiedPath); + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Add a listener to register and unregister browser functions when pages load + browser.addLocationListener(getLocationListener()); + + // Create Eclipse status line contributions + createStatusLine(); + } + + /** + * Register our browser functions when the editor web page is loaded, unregister the browser + * functions when any other page is loaded. + */ + private LocationListener getLocationListener() { + return new LocationListener() { + public void changing(LocationEvent event) { + // Do nothing before the page loads + } + + public void changed(LocationEvent event) { + if (!event.top) return; + + if (event.location.contains("embeddededitor.html")) { + // Register browser functions that allow JavaScript to call into the Workbench + registerBrowserFunctions(); + } else { + unregisterBrowserFunctions(); + } + } + }; + } + + // Register browser functions that allow JavaScript to call into the Workbench + private void registerBrowserFunctions() { + editorService = new EditorService(browser, EditorService.EDITOR_SERVICE_HANDLER , this); + } + + private void unregisterBrowserFunctions() { + if (editorService != null && editorService instanceof BrowserFunction) { + editorService.dispose(); + } + } + + /** + * This method is part of the Eclipse editor save framework and will be called + * by Eclipse when a Save action is invoked (tool bar, menu, Eclipse key binding etc.) + * + * This implementation will call into the web application requesting a save, since + * only the web application knows what it means to save itself + * + * IMPORTANT: + * + * This mechanism is synchronous, while saving in a web world is typically asynchronous. + * Eclipse has an asynchronous saving mechanism through ISavelable. This example will + * be updated shortly to use that mechanism. + */ + public void doSave(IProgressMonitor monitor) { + try { + Object resultObj = browser.evaluate(EditorService.JAVA_SCRIPT_SAVE_FUNCTION); + + // If the call to the web application returns false, indicating the save failed, cancel the operation + if (!(resultObj instanceof Boolean && (Boolean) resultObj)) { + monitor.setCanceled(true); + } + } catch (SWTException e) { + // Either the script caused a javascript error or returned an unsupported type + e.printStackTrace(); + monitor.setCanceled(true); + } + } + + + /** + * This method is called by the web application's editor service to perform the local save. + * @param newContents The new contents to save as a string + * @return true if the save was successful, false otherwise + */ + protected boolean performSave(String newContents) { + if (getEditorInput() instanceof IFileEditorInput) { + IFileEditorInput input = (IFileEditorInput) getEditorInput(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(newContents.getBytes()); + try { + input.getFile().setContents(inputStream, IFile.KEEP_HISTORY, null); + return true; + } catch (CoreException e) { + // Save failed + e.printStackTrace(); + } + } + return false; + } + + public boolean isDirty() { + return isDirty; + } + + // Set by the web application's editor service when the dirty state changes + protected void setDirty(boolean newValue) { + if (isDirty != newValue) { + isDirty = newValue; + firePropertyChange(PROP_DIRTY); + } + } + + // Set by the web application's editor service when the cursor position changes + protected void setPositionStatus(String text) { + position.setText(text); + } + + /** + * Initialize the editor. + */ + public void init(IEditorSite site, IEditorInput input) throws PartInitException { + setSite(site); + setInput(input); + setPartName(input.getName()); + } + + public boolean isSaveAsAllowed() { + return false; + } + + public void doSaveAs() { + } + + private void createStatusLine() { + statusLineManager = getEditorSite().getActionBars().getStatusLineManager(); + position = new StatusLineContributionItem("position", 15); + keyMode = new StatusLineContributionItem("keyMode", 15); + writeMode = new StatusLineContributionItem("writeMode", 15); + statusLineManager.add(writeMode); + statusLineManager.add(new Separator()); + statusLineManager.add(keyMode); + statusLineManager.add(new Separator()); + statusLineManager.add(position); + + // Set the initial cursor position + position.setText("0 : 0"); + + // writeMode and keyMode values are faked at the moment since the Orion editor + // does not return actual values for these properties. + writeMode.setText("Writable"); + keyMode.setText("Smart Insert"); + } + + public void setFocus() { + // Nothing to do but contractually obligated to override this abstract method. + } +} diff --git a/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/src/org/eclipse/e4/examples/webintegration/orion/editor/plugin/EditorService.java b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/src/org/eclipse/e4/examples/webintegration/orion/editor/plugin/EditorService.java new file mode 100644 index 00000000..a1077985 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration.orion.editor.plugin/src/org/eclipse/e4/examples/webintegration/orion/editor/plugin/EditorService.java @@ -0,0 +1,213 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * 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: + * Dean Roberts, IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.examples.webintegration.orion.editor.plugin; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.core.resources.IStorage; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.swt.browser.Browser; +import org.eclipse.swt.browser.BrowserFunction; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IStorageEditorInput; + +/** + * This class implements the call in point for a web application. + * Through this single BrowserFunction a web application can request + * various actions. + * + * An alternate implementation could have a a single BrowserFunction subclass + * for each action a web application could call. But it was unclear that + * such an implementation adds any value. + */ +public class EditorService extends BrowserFunction { + // Action constants. Same values used by JavaScript + private static final int DIRTY_CHANGED = 1; + private static final int GET_CONTENT_NAME = 2; + private static final int GET_INITIAL_CONTENT = 3; + private static final int SAVE = 4; + private static final int STATUS_CHANGED = 5; + + // Name of the JavaScript variable containing the editor service + public static final String EDITOR_SERVICE_MAP = "editorService"; + + // Name of the JavaScript function for the editor service handler + public static final String EDITOR_SERVICE_HANDLER = "editorServiceHandler"; + + // Name of JavaScript save function + public static final String JAVA_SCRIPT_SAVE_FUNCTION = "return " + EDITOR_SERVICE_MAP + ".save()"; + + Editor editor; + + public EditorService(Browser browser, String name, Editor editor) { + super(browser, name); + this.editor = editor; + } + + /** + * This is the single function that is invoked by the JavaScript program. + * By specification of our implementation there is always one or more arguments + * and the first argument is the action id. + * Subsequent arguments, if present, are arguments for the given action. + */ + public Object function(Object[] arguments) { + super.function(arguments); + + if (arguments.length == 0 || !(arguments[0] instanceof Double)) { + return null; + } + + int action = ((Double) arguments[0]).intValue(); + switch (action) { + case DIRTY_CHANGED: + return doDirtyChanged(arguments); + + case GET_CONTENT_NAME: + return doGetContentName(arguments); + + case GET_INITIAL_CONTENT: + return doGetInitialContent(arguments); + + case SAVE: + return doSave(arguments); + + case STATUS_CHANGED: + return doStatusChanged(arguments); + + default: + return null; + } + } + + // Actions + + /** + * Return the initial content for the editor. Return an empty string on any error + */ + private Object doGetInitialContent(Object[] arguments) { + if (editor.getEditorInput() instanceof IStorageEditorInput) { + IStorageEditorInput input = (IStorageEditorInput) editor.getEditorInput(); + + BufferedInputStream inputStream = null; + + + try { + IStorage storage = input.getStorage(); + inputStream = new BufferedInputStream(storage.getContents()); + String contents = readInputStream(inputStream); + return contents; + } catch (CoreException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + return ""; + } + + /** + * Return the name of the file being edited. Return an empty string on any error + */ + private Object doGetContentName(Object[] arguments) { + if (editor.getEditorInput() instanceof IFileEditorInput) { + IFileEditorInput input = (IFileEditorInput) editor.getEditorInput(); + return input.getName(); + } + return ""; + } + + /** + * This method is called by the JavaScript editor whenever its dirty state changes. + * Set the dirty status of the Eclipse editor so the framework displays the + * appropriate dirty marker and save actions are enabled appropriately. + * + * Return the dirtyState after the event is processed + */ + + private Object doDirtyChanged(Object[] arguments) { + if (arguments.length == 2 && (arguments[1] instanceof Boolean)) { + editor.setDirty((Boolean) arguments[1]); + } + return editor.isDirty(); + } + + /** + * JavaScript has requested a save. Call the Eclipse editors save method. + * By contract, the JavaScript passes the new contents as an argument. + * @param arguments + * @return + */ + private Object doSave(Object[] arguments) { + boolean result = false; + if (arguments.length == 2 && (arguments[1] instanceof String)) { + String newContents = (String) arguments[1]; + result = editor.performSave(newContents); + } + return result; + } + + /** + * Update Eclipse status line. + * Currently we dig the cursor position out of the string that the Orion editor sends us. + * Clearly we would prefer Orion API that could just send us the position info ... but this is, after all, + * a web integration example and not an Orion example :-) + */ + private boolean doStatusChanged(Object[] arguments) { + if (arguments.length != 2 || !(arguments[1] instanceof String)) { + return false; + } + + String[] position = parsePosition((String) arguments[1]); + editor.setPositionStatus(position[0] + " : " + position[1]); + return true; + } + + // Boring utility methods + + // Read all the bytes from an InputStream and return them as a String + private String readInputStream(InputStream inputStream) throws IOException { + ByteArrayOutputStream buffer = null; + buffer = new ByteArrayOutputStream(); + byte[] bytes = new byte[1024]; + int bytesRead = 0; + while ((bytesRead = inputStream.read(bytes)) != -1) { + buffer.write(bytes, 0, bytesRead); + } + return buffer.toString(); + } + + // Really sleazy "parsing" code for "Line x : Col y". Lots of error cases ignored + private String[] parsePosition(String message) { + int start = message.indexOf("Line ") + "Line ".length(); + int end = message.indexOf(' ', start); + String line = message.substring(start, end); + + start = message.indexOf("Col ") + "Col ".length(); + end = message.indexOf(' ', start); + if (end == -1) { + end = message.length(); + } + + String col = message.substring(start, end); + return new String[] {line, col}; + } +} diff --git a/examples/org.eclipse.e4.examples.webintegration/.classpath b/examples/org.eclipse.e4.examples.webintegration/.classpath new file mode 100755 index 00000000..2d1a4302 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/>
+ <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/examples/org.eclipse.e4.examples.webintegration/.project b/examples/org.eclipse.e4.examples.webintegration/.project new file mode 100755 index 00000000..4de1f9cd --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/.project @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.eclipse.e4.examples.webintegration</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + </natures> +</projectDescription> diff --git a/examples/org.eclipse.e4.examples.webintegration/.settings/org.eclipse.jdt.core.prefs b/examples/org.eclipse.e4.examples.webintegration/.settings/org.eclipse.jdt.core.prefs new file mode 100755 index 00000000..d91c5b63 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Tue Apr 05 13:17:59 EDT 2011
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5
+org.eclipse.jdt.core.compiler.compliance=1.5
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.source=1.5
diff --git a/examples/org.eclipse.e4.examples.webintegration/META-INF/MANIFEST.MF b/examples/org.eclipse.e4.examples.webintegration/META-INF/MANIFEST.MF new file mode 100755 index 00000000..d624fa23 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/META-INF/MANIFEST.MF @@ -0,0 +1,10 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Web Application <-> Workbench Interactions +Bundle-SymbolicName: org.eclipse.e4.examples.webintegration;singleton:=true +Bundle-Version: 0.9.0.qualifier +Require-Bundle: org.eclipse.ui, + org.eclipse.core.runtime +Bundle-RequiredExecutionEnvironment: J2SE-1.5 +Bundle-ActivationPolicy: lazy +Bundle-Vendor: Eclipse.org diff --git a/examples/org.eclipse.e4.examples.webintegration/Web UI Integration Examples.launch b/examples/org.eclipse.e4.examples.webintegration/Web UI Integration Examples.launch new file mode 100644 index 00000000..7294cacb --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/Web UI Integration Examples.launch @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<launchConfiguration type="org.eclipse.pde.ui.RuntimeWorkbench"> +<booleanAttribute key="append.args" value="true"/> +<stringAttribute key="application" value="WebAppExamples.application"/> +<booleanAttribute key="askclear" value="true"/> +<booleanAttribute key="automaticAdd" value="false"/> +<booleanAttribute key="automaticValidate" value="false"/> +<stringAttribute key="bootstrap" value=""/> +<stringAttribute key="checked" value="[NONE]"/> +<booleanAttribute key="clearConfig" value="false"/> +<booleanAttribute key="clearws" value="false"/> +<booleanAttribute key="clearwslog" value="false"/> +<stringAttribute key="configLocation" value="${workspace_loc}/.metadata/.plugins/org.eclipse.pde.core/Web UI Integration Examples"/> +<booleanAttribute key="default" value="false"/> +<booleanAttribute key="includeOptional" value="false"/> +<stringAttribute key="location" value="${workspace_loc}/../runtime-WebUIIntegrationExamples"/> +<stringAttribute key="org.eclipse.jdt.launching.JRE_CONTAINER" value="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.5"/> +<stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-os ${target.os} -ws ${target.ws} -arch ${target.arch} -nl ${target.nl} -consoleLog"/> +<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.pde.ui.workbenchClasspathProvider"/> +<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xms40m -Xmx384m"/> +<stringAttribute key="org.eclipse.jdt.launching.WORKING_DIRECTORY" value="${workspace_loc:org.eclipse.e4.examples.webintegration}"/> +<booleanAttribute key="pde.generated.config" value="false"/> +<stringAttribute key="pde.version" value="3.3"/> +<stringAttribute key="product" value="EarlyUpdateTest.product"/> +<stringAttribute key="selected_target_plugins" value="com.ibm.icu@default:default,javax.servlet@default:default,org.eclipse.core.commands@default:default,org.eclipse.core.contenttype@default:default,org.eclipse.core.databinding.observable@default:default,org.eclipse.core.databinding.property@default:default,org.eclipse.core.databinding@default:default,org.eclipse.core.expressions@default:default,org.eclipse.core.filesystem@default:default,org.eclipse.core.jobs@default:default,org.eclipse.core.runtime@default:true,org.eclipse.core.variables@default:default,org.eclipse.equinox.app@default:default,org.eclipse.equinox.common@2:true,org.eclipse.equinox.preferences@default:default,org.eclipse.equinox.registry@default:default,org.eclipse.equinox.security@default:default,org.eclipse.help@default:default,org.eclipse.jface.databinding@default:default,org.eclipse.jface@default:default,org.eclipse.osgi.services@default:default,org.eclipse.osgi@-1:true,org.eclipse.swt.cocoa.macosx.x86_64@default:default,org.eclipse.swt@default:default,org.eclipse.ui.workbench@default:default,org.eclipse.ui@default:default"/> +<stringAttribute key="selected_workspace_plugins" value="WebAppExamples@default:default"/> +<booleanAttribute key="show_selected_only" value="false"/> +<stringAttribute key="timestamp" value="1303824967114"/> +<booleanAttribute key="tracing" value="false"/> +<booleanAttribute key="useCustomFeatures" value="false"/> +<booleanAttribute key="useDefaultConfig" value="true"/> +<booleanAttribute key="useDefaultConfigArea" value="true"/> +<booleanAttribute key="useProduct" value="false"/> +<booleanAttribute key="usefeatures" value="false"/> +</launchConfiguration> diff --git a/examples/org.eclipse.e4.examples.webintegration/build.properties b/examples/org.eclipse.e4.examples.webintegration/build.properties new file mode 100755 index 00000000..e4a04bb0 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/build.properties @@ -0,0 +1,6 @@ +source.. = src/
+output.. = bin/
+bin.includes = plugin.xml,\
+ META-INF/,\
+ .,\
+ static/
diff --git a/examples/org.eclipse.e4.examples.webintegration/plugin.xml b/examples/org.eclipse.e4.examples.webintegration/plugin.xml new file mode 100755 index 00000000..e5d254a7 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/plugin.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<?eclipse version="3.4"?>
+<plugin>
+
+ <extension
+ id="application"
+ point="org.eclipse.core.runtime.applications">
+ <application>
+ <run
+ class="org.eclipse.e4.examples.webintegration.application.Application">
+ </run>
+ </application>
+ </extension>
+
+ <!-- Add Perspectives. One for each example -->
+ <extension point="org.eclipse.ui.perspectives">
+ <perspective
+ name="Link Intercept"
+ class="org.eclipse.e4.examples.webintegration.application.Perspective"
+ id="WebUI.link.intercept.example.perspective">
+ </perspective>
+ </extension>
+
+ <extension
+ point="org.eclipse.ui.views">
+
+ <!-- Begin Link Intercept Views -->
+ <!-- View with browser -->
+ <view
+ class="org.eclipse.e4.examples.webintegration.links.BrowserView"
+ id="WebUI.link.intercept.browser.view"
+ name="Link Intercept"
+ restorable="true">
+ </view>
+
+ <!-- View invoked by intercepted link -->
+ <view
+ class="org.eclipse.e4.examples.webintegration.links.LinkView"
+ id="url.link.1"
+ name="Link View"
+ restorable="true">
+ </view>
+ <!-- End Link Intercept Views -->
+
+ <!-- View for displaying instructions about the example -->
+ <view
+ class="org.eclipse.e4.examples.webintegration.application.InstructionsView"
+ id="WebUI.instructions.view"
+ name="Instructions"
+ restorable="true">
+ </view>
+ </extension>
+
+ <extension point="org.eclipse.ui.perspectiveExtensions">
+
+ <!-- Add all perspectives to main part of perspective shortcut menu -->
+ <perspectiveExtension targetID="*">
+ <perspectiveShortcut id="WebUI.link.intercept.example.perspective"/>
+ </perspectiveExtension>
+
+ <!-- Link Intercept example perspective definition !-->
+ <perspectiveExtension targetID="WebUI.link.intercept.example.perspective">
+ <view
+ id="WebUI.link.intercept.browser.view"
+ minimized="false"
+ ratio="0.10"
+ relationship="top"
+ relative="org.eclipse.ui.editorss">
+ </view>
+
+ <view
+ id="WebUI.instructions.view"
+ minimized="false"
+ ratio="0.60"
+ relationship="bottom"
+ relative="WebUI.link.intercept.browser.view">
+ </view>
+ </perspectiveExtension>
+ </extension>
+</plugin>
diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/Application.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/Application.java new file mode 100755 index 00000000..b23b497d --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/Application.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * 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: + * Dean Roberts, IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.e4.examples.webintegration.application; + +import org.eclipse.equinox.app.IApplication; +import org.eclipse.equinox.app.IApplicationContext; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.PlatformUI; + +/** + * This is boiler plate code created by the New Plugin Wizard and does + * not have anything interesting in here in relation to the WebUI Integration examples + * + * This class controls all aspects of the application's execution + */ +public class Application implements IApplication { + + /* (non-Javadoc) + * @see org.eclipse.equinox.app.IApplication#start(org.eclipse.equinox.app.IApplicationContext) + */ + public Object start(IApplicationContext context) throws Exception { + Display display = PlatformUI.createDisplay(); + try { + int returnCode = PlatformUI.createAndRunWorkbench(display, new ApplicationWorkbenchAdvisor()); + if (returnCode == PlatformUI.RETURN_RESTART) + return IApplication.EXIT_RESTART; + else + return IApplication.EXIT_OK; + } finally { + display.dispose(); + } + + } + + /* (non-Javadoc) + * @see org.eclipse.equinox.app.IApplication#stop() + */ + public void stop() { + if (!PlatformUI.isWorkbenchRunning()) + return; + final IWorkbench workbench = PlatformUI.getWorkbench(); + final Display display = workbench.getDisplay(); + display.syncExec(new Runnable() { + public void run() { + if (!display.isDisposed()) + workbench.close(); + } + }); + } +} diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationActionBarAdvisor.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationActionBarAdvisor.java new file mode 100755 index 00000000..d3bb20dd --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationActionBarAdvisor.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * 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: + * Dean Roberts, IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.e4.examples.webintegration.application; + +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.application.ActionBarAdvisor; +import org.eclipse.ui.application.IActionBarConfigurer; + +/** + * This is boiler plate code created by the New Plugin Wizard and does + * not have anything interesting in here in relation to the WebUI Integration examples + */ +public class ApplicationActionBarAdvisor extends ActionBarAdvisor { + + public ApplicationActionBarAdvisor(IActionBarConfigurer configurer) { + super(configurer); + } + + protected void makeActions(IWorkbenchWindow window) { + } + + protected void fillMenuBar(IMenuManager menuBar) { + } + +} diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationWorkbenchAdvisor.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationWorkbenchAdvisor.java new file mode 100755 index 00000000..d3b9bf8f --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationWorkbenchAdvisor.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * 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: + * Dean Roberts, IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.e4.examples.webintegration.application; + +import org.eclipse.ui.application.IWorkbenchWindowConfigurer; +import org.eclipse.ui.application.WorkbenchAdvisor; +import org.eclipse.ui.application.WorkbenchWindowAdvisor; + +/** + * This is boiler plate code created by the New Plug-in Wizard and does + * not have anything interesting in here in relation to the WebUI Integration examples + */ +public class ApplicationWorkbenchAdvisor extends WorkbenchAdvisor { + + private static final String PERSPECTIVE_ID = "WebUI.link.intercept.example.perspective"; //$NON-NLS-1$ + + public WorkbenchWindowAdvisor createWorkbenchWindowAdvisor(IWorkbenchWindowConfigurer configurer) { + return new ApplicationWorkbenchWindowAdvisor(configurer); + } + + public String getInitialWindowPerspectiveId() { + return PERSPECTIVE_ID; + } +} diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationWorkbenchWindowAdvisor.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationWorkbenchWindowAdvisor.java new file mode 100755 index 00000000..a3f67fe6 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/ApplicationWorkbenchWindowAdvisor.java @@ -0,0 +1,43 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * 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: + * Dean Roberts, IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.e4.examples.webintegration.application; + +import org.eclipse.swt.graphics.Point; +import org.eclipse.ui.application.ActionBarAdvisor; +import org.eclipse.ui.application.IActionBarConfigurer; +import org.eclipse.ui.application.IWorkbenchWindowConfigurer; +import org.eclipse.ui.application.WorkbenchWindowAdvisor; + +/** + * This is boiler plate code created by the New Plugin Wizard and does + * not have anything interesting in here in relation to the WebUI Integration examples + */ +public class ApplicationWorkbenchWindowAdvisor extends WorkbenchWindowAdvisor { + + public ApplicationWorkbenchWindowAdvisor(IWorkbenchWindowConfigurer configurer) { + super(configurer); + } + + public ActionBarAdvisor createActionBarAdvisor(IActionBarConfigurer configurer) { + return new ApplicationActionBarAdvisor(configurer); + } + + public void preWindowOpen() { + IWorkbenchWindowConfigurer configurer = getWindowConfigurer(); + configurer.setInitialSize(new Point(800, 1000)); + configurer.setShowCoolBar(false); + configurer.setShowStatusLine(false); + configurer.setTitle("Web Application <-> Workbench Interactions"); //$NON-NLS-1$ + configurer.setShowPerspectiveBar(true); + configurer.setShowStatusLine(true); + } +} diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/InstructionsView.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/InstructionsView.java new file mode 100755 index 00000000..ba656519 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/InstructionsView.java @@ -0,0 +1,49 @@ +/*******************************************************************************
+ * Copyright (c) 2011 IBM Corporation and others.
+ * 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:
+ * Dean Roberts, IBM Corporation - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.e4.examples.webintegration.application;
+
+import java.io.File;
+import java.net.MalformedURLException;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.browser.Browser;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IPerspectiveDescriptor;
+import org.eclipse.ui.part.ViewPart;
+
+/**
+ * This view provides an area for displaying instructions germane
+ * to each example. The view itself is not interesting in regards
+ * to integrating WebUIs with an Eclipse Workbench
+ */
+public class InstructionsView extends ViewPart {
+
+ private Browser browser;
+
+ public void createPartControl(Composite parent) {
+ IPerspectiveDescriptor descriptor = getSite().getPage().getPerspective();
+ String instructionLocation = Perspective.getInstructionLocation(descriptor.getLabel());
+ browser = new Browser(parent, SWT.NONE);
+
+ String qualifiedPath = "";
+ try {
+ qualifiedPath = new File(instructionLocation).toURL().toExternalForm();
+ browser.setUrl(qualifiedPath);
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void setFocus() {
+ // Not used in this example
+ }
+}
diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/Perspective.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/Perspective.java new file mode 100755 index 00000000..7c8d05eb --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/application/Perspective.java @@ -0,0 +1,58 @@ +/******************************************************************************* + * Copyright (c) 2011 IBM Corporation and others. + * 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: + * Dean Roberts, IBM Corporation - initial API and implementation + *******************************************************************************/ + +package org.eclipse.e4.examples.webintegration.application; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IPerspectiveFactory; + +/** + * This Perspective is part of an example project that contains a set of simple examples + * for accomplishing better integration between WebUIs and the Eclipse Workbench. + * + * Each example topic will have its own instance of this Perspective. The implementation will + * be in its one example.* package. Look there for the interesting implementation details. + * + * The code contained in this class is generic and is not particularly illustrative of the + * examples themselves. + */ +public class Perspective implements IPerspectiveFactory { + + // Some public URLs that may be interesting for the various examples + public static final String gmailURL = "https://mail.google.com/mail/?hl=en&shva=1#inbox"; + public static final String pcFinancialURL = "https://www.txn.banking.pcfinancial.ca/a/banking/accounts/accountSummary.ams"; + public static final String pcFinancialURL2 = "https://www.pcfinancial.ca/"; + public static final String yahooURL = "https://mail.yahoo.com"; + private static final Map<String, String> instructionURLMap = new HashMap<String, String>(); + + // Initialize the instructions for each example + { + instructionURLMap.put("default", "static/default.html"); + instructionURLMap.put("Link Intercept", "static/link.intercept.example.instructions.html"); + } + + public void createInitialLayout(IPageLayout layout) { + layout.setEditorAreaVisible(false); + } + + // Return the appropriate instructions for each example + public static String getInstructionLocation(String label) { + + String result = instructionURLMap.get(label); + if (result == null) { + result = instructionURLMap.get("default"); + } + return result; + } +} diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/links/BrowserView.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/links/BrowserView.java new file mode 100755 index 00000000..b29aada1 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/links/BrowserView.java @@ -0,0 +1,106 @@ +/*******************************************************************************
+ * Copyright (c) 2011 IBM Corporation and others.
+ * 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:
+ * Dean Roberts, IBM Corporation - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.e4.examples.webintegration.links;
+
+
+import org.eclipse.e4.examples.webintegration.application.Perspective;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.browser.Browser;
+import org.eclipse.swt.browser.LocationEvent;
+import org.eclipse.swt.browser.LocationListener;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IViewPart;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.NewWizardAction;
+import org.eclipse.ui.part.ViewPart;
+
+
+/**
+ * Example of a BrowserWidget that can incercept links and perform Eclipse Workbench
+ * actions as desired.
+ *
+ * This example is discussed at http://deanoneclipse.wordpress.com
+ */
+public class BrowserView extends ViewPart {
+
+ private Browser browser;
+
+ public void createPartControl(Composite parent) {
+ browser = new Browser(parent, SWT.NONE);
+ browser.setUrl(Perspective.gmailURL);
+
+ // Hooks the link intercept code
+ browser.addLocationListener(new LinkInterceptListener());
+ }
+
+ /**
+ * Implement a LocationListener to intercept links and decide what to do.
+ */
+ private class LinkInterceptListener implements LocationListener {
+ // method called when the user clicks a link but before the link is opened.
+ public void changing(LocationEvent event) {
+ try {
+ // Call user code to process link as desired and return
+ // true if the link should be opened in place.
+ boolean shouldOpenLinkInPlace = !openView(event.location);
+
+ // Setting event.doit to false prevents the link from opening in place
+ event.doit = shouldOpenLinkInPlace;
+ } catch (PartInitException e) {
+ e.printStackTrace();
+ }
+ }
+
+ // method called after the link has been opened in place.
+ public void changed(LocationEvent event) {
+ // Not used in this example
+ }
+ }
+
+ /**
+ * User code:
+ *
+ * Examine the link and determine if we wish to intercept it. Perform appropriate actions for intercepted links, do
+ * nothing for links we want to be opened in place (default behaviour)
+ *
+ * Return true if we intercepted the link. Return false if we did not intercept the link and expect the browser to
+ * open the link in place.
+ */
+ private boolean openView(String location) throws PartInitException {
+
+ /**
+ * Certainly the if/else-if construct could be replaced with a more elegant lookup mechanism.
+ */
+
+ // Open a view
+ if (location.equals("http://www.google.com/intl/en_CA/mobile/mail/#utm_source=en_CA-cpp-g4mc-gmhp&utm_medium=cpp&utm_campaign=en_CA")) {
+ IViewPart newView = getViewSite().getPage().showView("url.link.1");
+ ((LinkView) newView).setURL(location);
+
+ return true;
+ // Open a wizard
+ } else if (location.contains("/accounts/recovery")) {
+ NewWizardAction action = new NewWizardAction(PlatformUI.getWorkbench().getActiveWorkbenchWindow());
+ action.run();
+
+ return true;
+ }
+
+ // Do not intercept link. Allow browser widget to open link in place
+ return false;
+ }
+
+ public void setFocus() {
+ // Not important for our example.
+ }
+}
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/links/LinkView.java b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/links/LinkView.java new file mode 100755 index 00000000..65f682dd --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/src/org/eclipse/e4/examples/webintegration/links/LinkView.java @@ -0,0 +1,37 @@ +/*******************************************************************************
+ * Copyright (c) 2011 IBM Corporation and others.
+ * 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:
+ * Dean Roberts, IBM Corporation - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.e4.examples.webintegration.links;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.browser.Browser;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.part.ViewPart;
+
+/**
+ * A simple view to be opened by BrowserView when certain links are intercepted
+ */
+public class LinkView extends ViewPart {
+
+ private Browser browser;
+
+ public void createPartControl(Composite parent) {
+ browser = new Browser(parent, SWT.NONE);
+ }
+
+ public void setFocus() {
+ }
+
+ public void setURL(String location) {
+ browser.setUrl(location);
+ }
+
+}
diff --git a/examples/org.eclipse.e4.examples.webintegration/static/default.html b/examples/org.eclipse.e4.examples.webintegration/static/default.html new file mode 100755 index 00000000..148d979e --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/static/default.html @@ -0,0 +1,13 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+<title>Default Instructions</title>
+<link rel="stylesheet" type="text/css" href="style.css" />
+</head>
+<body>
+<h1>
+The example implementor should have provided a description here.
+</h1>
+</body>
+</html>
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration/static/link.intercept.example.instructions.html b/examples/org.eclipse.e4.examples.webintegration/static/link.intercept.example.instructions.html new file mode 100755 index 00000000..3cbbe801 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/static/link.intercept.example.instructions.html @@ -0,0 +1,30 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+<title>Link Intercept Example Instructions</title>
+<link rel="stylesheet" type="text/css" href="style.css" />
+</head>
+<body>
+<h1>
+Description
+</h1>
+<p>
+This example demonstrates intercepting HTML links. Once intercepted the view implementor can decide which actions to take. Opening an Eclipse view or wizard for example.
+</p>
+<p>
+While this implementation does not require any changes to the HTML, care should be taken to define a consistent naming pattern for links to aid in the link to action mapping in the workbench code.
+</p>
+<h1>
+What to See
+</h1>
+<p>
+Various links in the browser window above do special things.
+</p>
+<ul>
+<li><a href="#null">Can't access your account?</a> opens a <b>wizard</b>.
+<li><a href="#null">Learn more</a> opens a new <b>view</b>.
+<li>All other links open in the target browser. You can use the back action from the context menu to return from one of these.
+</ul>
+</body>
+</html>
\ No newline at end of file diff --git a/examples/org.eclipse.e4.examples.webintegration/static/style.css b/examples/org.eclipse.e4.examples.webintegration/static/style.css new file mode 100755 index 00000000..118e1bc1 --- /dev/null +++ b/examples/org.eclipse.e4.examples.webintegration/static/style.css @@ -0,0 +1,13 @@ +body
+{
+ font-family:Arial,Helvetica,sans-serif;
+}
+
+p
+{
+ margin-left:20px;
+ font-size:0.875em;
+}
+
+h1 {font-size:1.875em;} /* 40px/16=2.5em */
+ul {font-size:0.875em;font-family:Arial,Helvetica,sans-serif;}
\ No newline at end of file |
