Bug 401914 - Improve Editor Theming
diff --git a/bundles/org.eclipse.orion.client.editor/web/examples/editor/demoSetup.js b/bundles/org.eclipse.orion.client.editor/web/examples/editor/demoSetup.js
index ccfa25a..2f99418 100644
--- a/bundles/org.eclipse.orion.client.editor/web/examples/editor/demoSetup.js
+++ b/bundles/org.eclipse.orion.client.editor/web/examples/editor/demoSetup.js
@@ -18,6 +18,7 @@
 		"orion/editor/annotations", 
 		"orion/editor/projectionTextModel", 
 		"orion/editor/textView", 
+		"orion/editor/textTheme", 
 		"orion/editor/textDND", 
 		"orion/editor/rulers",
 		"orion/editor/undoStack",
@@ -26,13 +27,12 @@
 		"orion/editor/htmlGrammar",
 		"examples/editor/textStyler",
 		"orion/util"
-], function(require, mKeyBinding, mTextModel, mAnnotations, mProjectionTextModel, mTextView, mTextDND, mRulers, mUndoStack, mEventTarget, mTextMateStyler, mHtmlGrammar, mTextStyler, util) {
+], function(require, mKeyBinding, mTextModel, mAnnotations, mProjectionTextModel, mTextView, mTextTheme, mTextDND, mRulers, mUndoStack, mEventTarget, mTextMateStyler, mHtmlGrammar, mTextStyler, util) {
 
 	var exports = {};
 	var view = null;
 	var styler = null;
 	var annotationStyler = null;
-	var loadedThemes = [];
 	
 	var AnnotationType = mAnnotations.AnnotationType;
 		
@@ -48,28 +48,10 @@
 	}
 	exports.getFile = getFile;
 	
-	function loadTheme(theme) {
-		if (theme) {
-			for (var i=0; i<loadedThemes.length; i++) {
-				if (theme === loadedThemes[i]) {
-					return;
-				}
-			}
-			loadedThemes.push(theme);
-			require(["text!examples/editor/themes/" + theme + ".css"], function(cssText) { //$NON-NLS-1$ //$NON-NLS-0$
-				var stylesheet;
-				var document = view.getOptions("parent").ownerDocument; //$NON-NLS-0$
-				if (document.createStyleSheet) {
-					stylesheet = document.createStyleSheet();
-					stylesheet.cssText = cssText;
-				} else {
-					stylesheet = util.createElement(document, "style"); //$NON-NLS-0$
-					var head = document.getElementsByTagName("head")[0] || document.documentElement; //$NON-NLS-0$
-					stylesheet.appendChild(document.createTextNode(cssText));
-					head.appendChild(stylesheet);
-				}
-				view.update(true);
-			});
+	function loadTheme(themeClass) {
+		if (themeClass) {
+			var theme = mTextTheme.TextTheme.getTheme();
+			theme.setThemeClass(themeClass, {href: "examples/editor/themes/" + themeClass}); //$NON-NLS-0$
 		}
 	}
 	
diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/textTheme.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/textTheme.js
index 9ccfcc1..e4cc33d 100644
--- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/textTheme.js
+++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/textTheme.js
@@ -10,13 +10,16 @@
  *     IBM Corporation - initial API and implementation

  *******************************************************************************/

  

-/*globals define document*/

+/*globals define*/

 

-define("orion/editor/textTheme", ['orion/editor/eventTarget', 'orion/util'], function(mEventTarget, util) { 

-	var THEME_PREFIX = "orion-theme-";

-	

-	var DefaultTheme;

-	

+define("orion/editor/textTheme", //$NON-NLS-0$

+[

+	'require', //$NON-NLS-0$

+	'orion/editor/eventTarget', //$NON-NLS-0$

+	'orion/util' //$NON-NLS-0$

+], function(require, mEventTarget, util) {

+	var THEME_PREFIX = "orion-theme-"; //$NON-NLS-0$

+

 	/**

 	 * Constructs a new text theme.

 	 * 

@@ -26,10 +29,12 @@
 	 * @borrows orion.editor.EventTarget#removeEventListener as #removeEventListener

 	 * @borrows orion.editor.EventTarget#dispatchEvent as #dispatchEvent

 	 */

-	function TextTheme() {

-		

+	function TextTheme(options) {

+		options = options || {};

+		this._document = options.document || document;

 	}

 	

+	var DefaultTheme;

 	TextTheme.getTheme = function() {

 		if (!DefaultTheme) {

 			//TODO: Load a default sheet somehow

@@ -39,30 +44,137 @@
 	};

 

 	TextTheme.prototype = /** @lends orion.editor.TextTheme.prototype */ {

-		load: function (className, styleSheet) {

-			//check to see if ID exists already

-			var node = document.getElementById(THEME_PREFIX + className);

+		/**

+		 *

+		 */

+		buildStyleSheet: function(themeCLass, settings) {

+			

+			var result = [];

+			result.push("");

+			

+			//view container

+			var family = settings.fontFamily;

+			if (family === "sans serif") { //$NON-NLS-0$

+				family = '"Menlo", "Consolas", "Vera Mono", "monospace"'; //$NON-NLS-0$

+			} else {

+				family = 'monospace'; //$NON-NLS-0$

+			}	

+			

+			result.push("." + themeCLass + " {"); //$NON-NLS-1$ //$NON-NLS-0$

+			result.push("\tfont-family: " + family + ";"); //$NON-NLS-1$ //$NON-NLS-0$

+			result.push("\tfont-size: " + settings.fontSize + ";"); //$NON-NLS-1$ //$NON-NLS-0$

+			result.push("\tcolor: " + settings.text + ";"); //$NON-NLS-1$ //$NON-NLS-0$

+			result.push("}"); //$NON-NLS-0$

+			

+			//From textview.css

+			result.push("." + themeCLass + ".textview {"); //$NON-NLS-1$ //$NON-NLS-0$

+			result.push("\tbackground-color: " + settings.background + ";"); //$NON-NLS-1$ //$NON-NLS-0$

+			result.push("}"); //$NON-NLS-0$

+			

+			function defineRule(className, value, isBackground) {

+				result.push("." + themeCLass + " ." + className + " {"); //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$

+				result.push("\t" + (isBackground ? "background-color" : "color") + ": " + value + ";"); //$NON-NLS-4$ //$NON-NLS-3$ //$NON-NLS-2$ //$NON-NLS-1$ //$NON-NLS-0$

+				result.push("}"); //$NON-NLS-0$

+			}

+			

+			//From rulers.css

+			defineRule("ruler.annotations", settings.annotationRuler, true); //$NON-NLS-0$

+			defineRule("ruler.lines", settings.annotationRuler, true); //$NON-NLS-0$

+			defineRule("ruler.folding", settings.annotationRuler, true); //$NON-NLS-0$

+			defineRule("ruler.overview", settings.overviewRuler, true); //$NON-NLS-0$

+			defineRule("rulerLines", settings.lineNumber, false); //$NON-NLS-0$

+			defineRule("rulerLines.even", settings.lineNumberEven, false); //$NON-NLS-0$

+			defineRule("rulerLines.odd", settings.lineNumberOdd, false); //$NON-NLS-0$

+			

+			//From annotations.css

+			defineRule("annotationLine.currentLine", settings.currentLine, true); //$NON-NLS-0$

+			

+			//From default-theme.css

+			defineRule("entity-name-tag", settings.keyword, false); //$NON-NLS-0$

+			defineRule("entity-other-attribute-name", settings.attribute, false); //$NON-NLS-0$

+			defineRule("string-quoted", settings.string, false); //$NON-NLS-0$

+			

+			//From textstyler.css

+			defineRule("token_keyword", settings.keyword, false); //$NON-NLS-0$

+			defineRule("token_string", settings.string, false); //$NON-NLS-0$

+			defineRule("token_singleline_comment", settings.comment, false); //$NON-NLS-0$

+			defineRule("token_multiline_comment", settings.comment, false); //$NON-NLS-0$

+			defineRule("token_doc_comment", settings.comment, false); //$NON-NLS-0$

+			defineRule("token_doc_html_markup", settings.comment, false); //$NON-NLS-0$

+			

+			return result.join("\n"); //$NON-NLS-0$

+		},

+		_createStyle: function(className, styleSheet, callback, link) {

+			var document = this._document;

+			var id = THEME_PREFIX + className;

+			var node = document.getElementById(id);

 			if (node) {

-				//TODO: Check if contents are the same, if so return

+				if (link || node.firstChild.data === styleSheet) {

+					return;

+				}

 				node.removeChild(node.firstChild);

 				node.appendChild(document.createTextNode(styleSheet));

 			} else {

-				node = util.createElement(document, "style");

-				node.appendChild(document.createTextNode(styleSheet));

-				var head = document.getElementsByTagName("head")[0] || document.documentElement;	

+				if (link) {

+					node = util.createElement(document, "link"); //$NON-NLS-0$

+					node.rel = "stylesheet"; //$NON-NLS-0$

+					node.type = "text/css"; //$NON-NLS-0$

+					node.href = styleSheet;

+					node.addEventListener("load", function() { //$NON-NLS-0$

+						callback();

+					});

+				} else {

+					node = util.createElement(document, "style"); //$NON-NLS-0$

+					node.appendChild(document.createTextNode(styleSheet));

+				}

+				node.id = id;

+				var head = document.getElementsByTagName("head")[0] || document.documentElement; //$NON-NLS-0$

 				head.appendChild(node);

-			}	

+			}

+			if (!link) {

+				callback();

+			}

 		},

-		setThemeClass: function(className, styleSheet) {

-			//TODO: Also need to check the styleSheet contents before determining if it is the same

-			//if (className === this._themeClass) { return; }

-			this._themeClass = className;

-			this.load(className, styleSheet);

-			this.onThemeChanged({type: "ThemeChanged"});

-		},

+		/**

+		 *

+		 */

 		getThemeClass: function() {

 			return this._themeClass;

 		},

+		_load: function (className, styleSheet, callback) {

+			if (typeof styleSheet === "string") { //$NON-NLS-0$

+				this._createStyle(className, styleSheet, callback);

+				return;

+			}

+			var href = styleSheet.href;

+			var extension = ".css"; //$NON-NLS-0$

+			if (href.substring(href.length - extension.length) !== extension) {

+				href += extension;

+			}

+			if (/^https?:\/\//i.test(href)) {

+				this._createStyle(className, require.toUrl(href), callback, true);

+			} else {

+				var self = this;

+				require(["text!" + href], function(cssText) { //$NON-NLS-0$

+					self._createStyle(className, cssText, callback, false);

+				});

+			}

+		},

+		/**

+		 *

+		 */

+		setThemeClass: function(className, styleSheet) {

+			var self = this;

+			this._load(className, styleSheet, function() {

+				var themeClass = self._themeClass;

+				self._themeClass = className;

+				self.onThemeChanged({

+					type: "ThemeChanged", //$NON-NLS-0$

+					oldValue: themeClass,

+					newValue: self.getThemeClass()

+				});

+			});

+		},

 		/**

 		 * @class This is the event sent when the current theme has changed.

 		 * <p>

@@ -71,6 +183,9 @@
 		 * {@link orion.editor.TextTheme#event:onThemeChanged}

 		 * </p>

 		 * @name orion.editor.ThemeChangedEvent

+		 * 

+		 * @property {String} oldValue The old selection.

+		 * @property {String} newValue The new selection.

 		 */

 		/**

 		 * This event is sent when the current theme has changed.

@@ -80,64 +195,6 @@
 		 */

 		onThemeChanged: function(themeChangedEvent) {

 			return this.dispatchEvent(themeChangedEvent);

-		},

-		buildStyleSheet: function(settings, theme ){

-			

-			var result = [];

-			result.push("");

-			

-			//view container

-			var family = settings.fontFamily;

-			if(family === "sans serif"){

-				family = '"Menlo", "Consolas", "Vera Mono", "monospace"';

-			}else{

-				family = 'monospace';

-			}	

-			

-			result.push("." + theme + " {");

-			result.push("\tfont-family: " + family + ";");

-			result.push("\tfont-size: " + settings.fontSize + ";");

-			

-			result.push("\tcolor: " + settings.text + ";");

-			result.push("}");

-			

-			//From textview.css

-			result.push("." + theme + ".textview {");

-			result.push("\tbackground-color: " + settings.background + ";");

-			result.push("}");

-			

-			function defineRule(className, value, isBackground) {

-				result.push("." + theme + " ." + className + " {");

-				result.push("\t" + (isBackground ? "background-color" : "color") + ": " + value + ";");

-				result.push("}");

-			}

-			

-			//From rulers.css

-			defineRule("ruler.annotations", settings.annotationRuler, true);

-			defineRule("ruler.lines", settings.annotationRuler, true);

-			defineRule("ruler.folding", settings.annotationRuler, true);

-			defineRule("ruler.overview", settings.overviewRuler, true);

-			defineRule("rulerLines", settings.lineNumber, false);

-			defineRule("rulerLines.even", settings.lineNumberEven, false);

-			defineRule("rulerLines.odd", settings.lineNumberOdd, false);

-			

-			//From annotations.css

-			defineRule("annotationLine.currentLine", settings.currentLine, true);

-			

-			//From default-theme.css

-			defineRule("entity-name-tag", settings.keyword, false);

-			defineRule("entity-other-attribute-name", settings.attribute, false);

-			defineRule("string-quoted", settings.string, false);

-			

-			//From textstyler.css

-			defineRule("token_keyword", settings.keyword, false);

-			defineRule("token_string", settings.string, false);

-			defineRule("token_singleline_comment", settings.comment, false);

-			defineRule("token_multiline_comment", settings.comment, false);

-			defineRule("token_doc_comment", settings.comment, false);

-			defineRule("token_doc_html_markup", settings.comment, false);

-			

-			return result.join("\n");

 		}

 	};

 	mEventTarget.EventTarget.addMixin(TextTheme.prototype);

@@ -145,5 +202,4 @@
 	return {

 		TextTheme: TextTheme

 	};

-	

 });

diff --git a/bundles/org.eclipse.orion.client.editor/web/orion/editor/textView.js b/bundles/org.eclipse.orion.client.editor/web/orion/editor/textView.js
index c31f599..8865aae 100644
--- a/bundles/org.eclipse.orion.client.editor/web/orion/editor/textView.js
+++ b/bundles/org.eclipse.orion.client.editor/web/orion/editor/textView.js
@@ -1029,6 +1029,7 @@
 	 * @property {Boolean} [fullSelection=true] whether or not the view is in full selection mode.
 	 * @property {Boolean} [tabMode=true] whether or not the tab keypress is consumed by the view or is used for focus traversal.
 	 * @property {Boolean} [expandTab=false] whether or not the tab key inserts white spaces.
+	 * @property {orion.editor.TextTheme} [theme=orion.editor.TextTheme.getTheme()] the TextTheme manager. TODO more info on this
 	 * @property {String} [themeClass] the CSS class for the view theming.
 	 * @property {Number} [tabSize=8] The number of spaces in a tab.
 	 * @property {Boolean} [wrapMode=false] whether or not the view wraps lines.
@@ -1045,7 +1046,7 @@
 	 * @borrows orion.editor.EventTarget#dispatchEvent as #dispatchEvent
 	 */
 	function TextView (options) {
-		this._init(options);
+		this._init(options || {});
 	}
 	
 	TextView.prototype = /** @lends orion.editor.editor.prototype */ {
@@ -1179,6 +1180,7 @@
 
 			this._parent = null;
 			this._model = null;
+			this._theme = null;
 			this._selection = null;
 			this._doubleClickSelection = null;
 			this._keyBindings = null;
@@ -4541,6 +4543,7 @@
 				tabSize: {value: 8, update: this._setTabSize},
 				expandTab: {value: false, update: null},
 				wrapMode: {value: false, update: this._setWrapMode},
+				theme: {value: mTextTheme.TextTheme.getTheme(), update: this._setTheme},
 				themeClass: {value: undefined, update: this._setThemeClass}
 			};
 		},
@@ -4950,13 +4953,12 @@
 			this._model.addEventListener("preChanging", this._modelListener.onChanging); //$NON-NLS-0$
 			this._model.addEventListener("postChanged", this._modelListener.onChanged); //$NON-NLS-0$
 			
-			var theme = mTextTheme.TextTheme.getTheme();
 			this._themeListener = {
 				onChanged: function(themeChangedEvent) {
-					self._setThemeClass(theme.getThemeClass());
+					self._setThemeClass(self._themeClass);
 				}
 			};
-			theme.addEventListener("ThemeChanged", this._themeListener.onChanged); //$NON-NLS-0$
+			this._theme.addEventListener("ThemeChanged", this._themeListener.onChanged); //$NON-NLS-0$
 			
 			var handlers = this._handlers = [];
 			var clientDiv = this._clientDiv, viewDiv = this._viewDiv, rootDiv = this._rootDiv;
@@ -5757,10 +5759,22 @@
 				this._resetLineWidth();
 			}
 		},
+		_setTheme: function(theme) {
+			if (this._theme) {
+				this._theme.removeEventListener("ThemeChanged", this._themeListener.onChanged); //$NON-NLS-0$
+			}
+			this._theme = theme;
+			if (this._theme) {
+				this._theme.addEventListener("ThemeChanged", this._themeListener.onChanged); //$NON-NLS-0$
+			}
+			this._setThemeClass(this._themeClass);
+		},
 		_setThemeClass: function (themeClass, init) {
 			this._themeClass = themeClass;
 			var viewContainerClass = "textview"; //$NON-NLS-0$
-			if (this._themeClass) { viewContainerClass += " " + this._themeClass; } //$NON-NLS-0$
+			var globalThemeClass = this._theme.getThemeClass();
+			if (globalThemeClass) { viewContainerClass += " " + globalThemeClass; } //$NON-NLS-0$
+			if (this._themeClass && globalThemeClass !== this._themeClass) { viewContainerClass += " " + this._themeClass; } //$NON-NLS-0$
 			this._rootDiv.className = viewContainerClass;
 			this._updateStyle(init);
 		},
@@ -5870,6 +5884,7 @@
 		_unhookEvents: function() {
 			this._model.removeEventListener("preChanging", this._modelListener.onChanging); //$NON-NLS-0$
 			this._model.removeEventListener("postChanged", this._modelListener.onChanged); //$NON-NLS-0$
+			this._theme.removeEventListener("ThemeChanged", this._themeListener.onChanged); //$NON-NLS-0$
 			this._modelListener = null;
 			for (var i=0; i<this._handlers.length; i++) {
 				var h = this._handlers[i];
diff --git a/bundles/org.eclipse.orion.client.ui/web/orion/widgets/themes/editor/MiniThemeChooser.js b/bundles/org.eclipse.orion.client.ui/web/orion/widgets/themes/editor/MiniThemeChooser.js
index 36dc0fa..19acfaf 100644
--- a/bundles/org.eclipse.orion.client.ui/web/orion/widgets/themes/editor/MiniThemeChooser.js
+++ b/bundles/org.eclipse.orion.client.ui/web/orion/widgets/themes/editor/MiniThemeChooser.js
@@ -140,8 +140,9 @@
 		MiniThemeChooser.prototype.selectTheme = selectTheme;

 		

 		function setThemeData( settings ){

+			var themeClass = "editorTheme";

 			var theme = mTextTheme.TextTheme.getTheme();

-			theme.setThemeClass("userTheme", theme.buildStyleSheet(settings, "userTheme"));

+			theme.setThemeClass(themeClass, theme.buildStyleSheet(themeClass, settings));

 		}

 		

 		MiniThemeChooser.prototype.setThemeData = setThemeData;