Trevor Parscal has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/192738

Change subject: Context refactor
......................................................................

Context refactor

* Context items are no longer options in a select
* Context is now a group element, it's items are the context items
* Context items are registered with the context item factory
* ModeledFactory is a new mixin for factories of things that have static 
modelClasses properties
* If a selected model doesn't have a context item for it, the tool factory is 
queried for matching tools and then ToolContextItem objects are created to 
provide a fallback
* A context subclass can be configured to display context item descriptions 
instead of bodies, and the mobile context does so

Change-Id: Ibca8cf5cb8ee2f0a8f520461cef3af6fa6f2ac4d
---
M .docs/eg-iframe.html
M build/modules.json
M demos/ve/desktop.html
M demos/ve/mobile.html
A src/ui/contextitems/ve.ui.CommentContextItem.js
A src/ui/contextitems/ve.ui.LanguageContextItem.js
A src/ui/contextitems/ve.ui.LinkContextItem.js
A src/ui/contextitems/ve.ui.ToolContextItem.js
A src/ui/styles/contextitems/ve.ui.CommentContextItem.css
A src/ui/styles/contextitems/ve.ui.LanguageContextItem.css
A src/ui/styles/contextitems/ve.ui.LinkContextItem.css
A src/ui/styles/contextitems/ve.ui.ToolContextItem.css
M src/ui/styles/ve.ui.Context.css
A src/ui/styles/ve.ui.ContextItem.css
M src/ui/styles/ve.ui.DesktopContext.css
A src/ui/styles/ve.ui.MobileContext.css
D src/ui/styles/widgets/ve.ui.ContextOptionWidget.css
D src/ui/styles/widgets/ve.ui.ContextSelectWidget.css
D src/ui/styles/widgets/ve.ui.MobileContextOptionWidget.css
M src/ui/ve.ui.Context.js
A src/ui/ve.ui.ContextItem.js
A src/ui/ve.ui.ContextItemFactory.js
M src/ui/ve.ui.DesktopContext.js
M src/ui/ve.ui.MobileContext.js
A src/ui/ve.ui.ModeledFactory.js
M src/ui/ve.ui.TableContext.js
M src/ui/ve.ui.ToolFactory.js
M src/ui/ve.ui.js
D src/ui/widgets/ve.ui.ContextOptionWidget.js
D src/ui/widgets/ve.ui.ContextSelectWidget.js
D src/ui/widgets/ve.ui.MobileContextOptionWidget.js
M tests/index.html
32 files changed, 940 insertions(+), 433 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/VisualEditor/VisualEditor 
refs/changes/38/192738/1

diff --git a/.docs/eg-iframe.html b/.docs/eg-iframe.html
index c95f847..418aac0 100644
--- a/.docs/eg-iframe.html
+++ b/.docs/eg-iframe.html
@@ -28,8 +28,6 @@
                <link rel=stylesheet 
href="../src/ui/styles/tools/ve.ui.FormatTool.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.LanguageInputWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.LanguageSearchWidget.css" 
class="stylesheet-ve">
-               <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.ContextOptionWidget.css" 
class="stylesheet-ve">
-               <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.ContextSelectWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.DimensionsWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.MediaSizeWidget.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../src/ui/styles/inspectors/ve.ui.CommentInspector.css" 
class="stylesheet-ve">
@@ -37,6 +35,11 @@
                <link rel=stylesheet 
href="../src/ui/styles/inspectors/ve.ui.LinkInspector.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../src/ui/styles/inspectors/ve.ui.SpecialCharacterInspector.css" 
class="stylesheet-ve">
                <link rel=stylesheet href="../src/ui/styles/ve.ui.Context.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../src/ui/styles/ve.ui.ContextItem.css" class="stylesheet-ve">
+               <link rel=stylesheet 
href="../src/ui/styles/contextitems/ve.ui.CommentContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../src/ui/styles/contextitems/ve.ui.LanguageContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../src/ui/styles/contextitems/ve.ui.LinkContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../src/ui/styles/contextitems/ve.ui.ToolContextItem.css" 
class="stylesheet-ve">
                <link rel=stylesheet href="../src/ui/styles/ve.ui.Overlay.css" 
class="stylesheet-ve">
                <link rel=stylesheet href="../src/ui/styles/ve.ui.Surface.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../src/ui/styles/widgets/ve.ui.SurfaceWidget.css" class="stylesheet-ve">
@@ -290,6 +293,13 @@
                <script src="../src/ui/ve.ui.Overlay.js"></script>
                <script src="../src/ui/ve.ui.Surface.js"></script>
                <script src="../src/ui/ve.ui.Context.js"></script>
+               <script src="../src/ui/ve.ui.ModeledFactory.js"></script>
+               <script src="../src/ui/ve.ui.ContextItem.js"></script>
+               <script src="../src/ui/ve.ui.ContextItemFactory.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.CommentContextItem.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.LanguageContextItem.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.LinkContextItem.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.ToolContextItem.js"></script>
                <script src="../src/ui/ve.ui.TableContext.js"></script>
                <script src="../src/ui/ve.ui.Tool.js"></script>
                <script src="../src/ui/ve.ui.Toolbar.js"></script>
@@ -340,8 +350,6 @@
                <script 
src="../src/ui/widgets/ve.ui.LanguageInputWidget.js"></script>
                <script src="../src/ui/widgets/ve.ui.SurfaceWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.LinkTargetInputWidget.js"></script>
-               <script 
src="../src/ui/widgets/ve.ui.ContextSelectWidget.js"></script>
-               <script 
src="../src/ui/widgets/ve.ui.ContextOptionWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.DimensionsWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.MediaSizeWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.WhitespacePreservingTextInputWidget.js"></script>
diff --git a/build/modules.json b/build/modules.json
index 9b7126b..383a024 100644
--- a/build/modules.json
+++ b/build/modules.json
@@ -317,6 +317,13 @@
                        "src/ui/ve.ui.Overlay.js",
                        "src/ui/ve.ui.Surface.js",
                        "src/ui/ve.ui.Context.js",
+                       "src/ui/ve.ui.ModeledFactory.js",
+                       "src/ui/ve.ui.ContextItem.js",
+                       "src/ui/ve.ui.ContextItemFactory.js",
+                       "src/ui/contextitems/ve.ui.CommentContextItem.js",
+                       "src/ui/contextitems/ve.ui.LanguageContextItem.js",
+                       "src/ui/contextitems/ve.ui.LinkContextItem.js",
+                       "src/ui/contextitems/ve.ui.ToolContextItem.js",
                        "src/ui/ve.ui.TableContext.js",
                        "src/ui/ve.ui.Tool.js",
                        "src/ui/ve.ui.Toolbar.js",
@@ -368,8 +375,6 @@
                        "src/ui/widgets/ve.ui.LanguageInputWidget.js",
                        "src/ui/widgets/ve.ui.SurfaceWidget.js",
                        "src/ui/widgets/ve.ui.LinkTargetInputWidget.js",
-                       "src/ui/widgets/ve.ui.ContextSelectWidget.js",
-                       "src/ui/widgets/ve.ui.ContextOptionWidget.js",
                        "src/ui/widgets/ve.ui.DimensionsWidget.js",
                        "src/ui/widgets/ve.ui.MediaSizeWidget.js",
                        
"src/ui/widgets/ve.ui.WhitespacePreservingTextInputWidget.js",
@@ -412,8 +417,6 @@
                        "src/ui/styles/tools/ve.ui.FormatTool.css",
                        "src/ui/styles/widgets/ve.ui.LanguageInputWidget.css",
                        "src/ui/styles/widgets/ve.ui.LanguageSearchWidget.css",
-                       "src/ui/styles/widgets/ve.ui.ContextOptionWidget.css",
-                       "src/ui/styles/widgets/ve.ui.ContextSelectWidget.css",
                        "src/ui/styles/widgets/ve.ui.DimensionsWidget.css",
                        "src/ui/styles/widgets/ve.ui.MediaSizeWidget.css",
                        "src/ui/styles/inspectors/ve.ui.CommentInspector.css",
@@ -421,6 +424,11 @@
                        "src/ui/styles/inspectors/ve.ui.LinkInspector.css",
                        
"src/ui/styles/inspectors/ve.ui.SpecialCharacterInspector.css",
                        "src/ui/styles/ve.ui.Context.css",
+                       "src/ui/styles/ve.ui.ContextItem.css",
+                       
"src/ui/styles/contextitems/ve.ui.CommentContextItem.css",
+                       
"src/ui/styles/contextitems/ve.ui.LanguageContextItem.css",
+                       "src/ui/styles/contextitems/ve.ui.LinkContextItem.css",
+                       "src/ui/styles/contextitems/ve.ui.ToolContextItem.css",
                        "src/ui/styles/ve.ui.Overlay.css",
                        "src/ui/styles/ve.ui.Surface.css",
                        "src/ui/styles/widgets/ve.ui.SurfaceWidget.css",
@@ -561,12 +569,11 @@
                "scripts": [
                        "src/ui/ve.ui.MobileSurface.js",
                        "src/ui/ve.ui.MobileContext.js",
-                       "src/ui/windowmanagers/ve.ui.MobileWindowManager.js",
-                       "src/ui/widgets/ve.ui.MobileContextOptionWidget.js"
+                       "src/ui/windowmanagers/ve.ui.MobileWindowManager.js"
                ],
                "styles": [
-                       "src/ui/styles/ve.ui.MobileSurface.css",
-                       
"src/ui/styles/widgets/ve.ui.MobileContextOptionWidget.css"
+                       "src/ui/styles/ve.ui.MobileContext.css",
+                       "src/ui/styles/ve.ui.MobileSurface.css"
                ],
                "dependencies": [
                        "visualEditor.core.build"
diff --git a/demos/ve/desktop.html b/demos/ve/desktop.html
index 0b686e8..6d52fd2 100644
--- a/demos/ve/desktop.html
+++ b/demos/ve/desktop.html
@@ -42,8 +42,6 @@
                <link rel=stylesheet 
href="../../src/ui/styles/tools/ve.ui.FormatTool.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.LanguageInputWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.LanguageSearchWidget.css" 
class="stylesheet-ve">
-               <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.ContextOptionWidget.css" 
class="stylesheet-ve">
-               <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.ContextSelectWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.DimensionsWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.MediaSizeWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/inspectors/ve.ui.CommentInspector.css" 
class="stylesheet-ve">
@@ -51,6 +49,11 @@
                <link rel=stylesheet 
href="../../src/ui/styles/inspectors/ve.ui.LinkInspector.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/inspectors/ve.ui.SpecialCharacterInspector.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.Context.css" class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.ContextItem.css" class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.CommentContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.LanguageContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.LinkContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.ToolContextItem.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.Overlay.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.Surface.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.SurfaceWidget.css" 
class="stylesheet-ve">
@@ -307,6 +310,13 @@
                <script src="../../src/ui/ve.ui.Overlay.js"></script>
                <script src="../../src/ui/ve.ui.Surface.js"></script>
                <script src="../../src/ui/ve.ui.Context.js"></script>
+               <script src="../../src/ui/ve.ui.ModeledFactory.js"></script>
+               <script src="../../src/ui/ve.ui.ContextItem.js"></script>
+               <script src="../../src/ui/ve.ui.ContextItemFactory.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.CommentContextItem.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.LanguageContextItem.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.LinkContextItem.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.ToolContextItem.js"></script>
                <script src="../../src/ui/ve.ui.TableContext.js"></script>
                <script src="../../src/ui/ve.ui.Tool.js"></script>
                <script src="../../src/ui/ve.ui.Toolbar.js"></script>
@@ -358,8 +368,6 @@
                <script 
src="../../src/ui/widgets/ve.ui.LanguageInputWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.SurfaceWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.LinkTargetInputWidget.js"></script>
-               <script 
src="../../src/ui/widgets/ve.ui.ContextSelectWidget.js"></script>
-               <script 
src="../../src/ui/widgets/ve.ui.ContextOptionWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.DimensionsWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.MediaSizeWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.WhitespacePreservingTextInputWidget.js"></script>
diff --git a/demos/ve/mobile.html b/demos/ve/mobile.html
index ec72fa0..288f1a8 100644
--- a/demos/ve/mobile.html
+++ b/demos/ve/mobile.html
@@ -42,8 +42,6 @@
                <link rel=stylesheet 
href="../../src/ui/styles/tools/ve.ui.FormatTool.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.LanguageInputWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.LanguageSearchWidget.css" 
class="stylesheet-ve">
-               <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.ContextOptionWidget.css" 
class="stylesheet-ve">
-               <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.ContextSelectWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.DimensionsWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.MediaSizeWidget.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/inspectors/ve.ui.CommentInspector.css" 
class="stylesheet-ve">
@@ -51,6 +49,11 @@
                <link rel=stylesheet 
href="../../src/ui/styles/inspectors/ve.ui.LinkInspector.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/inspectors/ve.ui.SpecialCharacterInspector.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.Context.css" class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.ContextItem.css" class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.CommentContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.LanguageContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.LinkContextItem.css" 
class="stylesheet-ve">
+               <link rel=stylesheet 
href="../../src/ui/styles/contextitems/ve.ui.ToolContextItem.css" 
class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.Overlay.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.Surface.css" class="stylesheet-ve">
                <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.SurfaceWidget.css" 
class="stylesheet-ve">
@@ -59,8 +62,8 @@
                <link rel=stylesheet href="../../src/ui/styles/ve.ui.Icons.css" 
class="stylesheet-ve">
 
                <!-- visualEditor.mobile.build -->
+               <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.MobileContext.css">
                <link rel=stylesheet 
href="../../src/ui/styles/ve.ui.MobileSurface.css">
-               <link rel=stylesheet 
href="../../src/ui/styles/widgets/ve.ui.MobileContextOptionWidget.css">
 
                <!-- visualEditor.theme.mediawiki -->
                <link rel=stylesheet 
href="../../src/themes/mediawiki/dialogs.css">
@@ -308,6 +311,13 @@
                <script src="../../src/ui/ve.ui.Overlay.js"></script>
                <script src="../../src/ui/ve.ui.Surface.js"></script>
                <script src="../../src/ui/ve.ui.Context.js"></script>
+               <script src="../../src/ui/ve.ui.ModeledFactory.js"></script>
+               <script src="../../src/ui/ve.ui.ContextItem.js"></script>
+               <script src="../../src/ui/ve.ui.ContextItemFactory.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.CommentContextItem.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.LanguageContextItem.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.LinkContextItem.js"></script>
+               <script 
src="../../src/ui/contextitems/ve.ui.ToolContextItem.js"></script>
                <script src="../../src/ui/ve.ui.TableContext.js"></script>
                <script src="../../src/ui/ve.ui.Tool.js"></script>
                <script src="../../src/ui/ve.ui.Toolbar.js"></script>
@@ -359,8 +369,6 @@
                <script 
src="../../src/ui/widgets/ve.ui.LanguageInputWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.SurfaceWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.LinkTargetInputWidget.js"></script>
-               <script 
src="../../src/ui/widgets/ve.ui.ContextSelectWidget.js"></script>
-               <script 
src="../../src/ui/widgets/ve.ui.ContextOptionWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.DimensionsWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.MediaSizeWidget.js"></script>
                <script 
src="../../src/ui/widgets/ve.ui.WhitespacePreservingTextInputWidget.js"></script>
@@ -387,7 +395,6 @@
                <script src="../../src/ui/ve.ui.MobileSurface.js"></script>
                <script src="../../src/ui/ve.ui.MobileContext.js"></script>
                <script 
src="../../src/ui/windowmanagers/ve.ui.MobileWindowManager.js"></script>
-               <script 
src="../../src/ui/widgets/ve.ui.MobileContextOptionWidget.js"></script>
 
                <!-- visualEditor.standalone.demo -->
                <script src="../../demos/ve/demo.js"></script>
diff --git a/src/ui/contextitems/ve.ui.CommentContextItem.js 
b/src/ui/contextitems/ve.ui.CommentContextItem.js
new file mode 100644
index 0000000..9567411
--- /dev/null
+++ b/src/ui/contextitems/ve.ui.CommentContextItem.js
@@ -0,0 +1,54 @@
+
+/**
+ * @inheritdoc
+ *//*!
+ * VisualEditor CommentContextItem class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Context item for a comment.
+ *
+ * @param {ve.ui.Context} context Context item is in
+ * @param {ve.dm.Model} model Model item is related to
+ * @param {Object} config Configuration options
+ */
+ve.ui.CommentContextItem = function VeCommentContextItem( context, model, 
config ) {
+       // Parent constructor
+       ve.ui.CommentContextItem.super.call( this, context, model, config );
+
+       // Initialization
+       this.$element.addClass( 've-ui-commentContextItem' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.CommentContextItem, ve.ui.ContextItem );
+
+/* Static Properties */
+
+ve.ui.CommentContextItem.static.name = 'comment';
+
+ve.ui.CommentContextItem.static.icon = 'comment';
+
+ve.ui.CommentContextItem.static.label = OO.ui.deferMsg( 
'visualeditor-commentinspector-title' );
+
+ve.ui.CommentContextItem.static.modelClasses = [ ve.dm.CommentNode ];
+
+ve.ui.CommentContextItem.static.embeddable = false;
+
+ve.ui.CommentContextItem.static.commandName = 'comment';
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.CommentContextItem.prototype.getDescription = function () {
+       return this.model.getAttribute( 'text' ).trim();
+};
+
+/* Registration */
+
+ve.ui.contextItemFactory.register( ve.ui.CommentContextItem );
diff --git a/src/ui/contextitems/ve.ui.LanguageContextItem.js 
b/src/ui/contextitems/ve.ui.LanguageContextItem.js
new file mode 100644
index 0000000..64eae2d
--- /dev/null
+++ b/src/ui/contextitems/ve.ui.LanguageContextItem.js
@@ -0,0 +1,51 @@
+/*!
+ * VisualEditor LanguageContextItem class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Context item for a language.
+ *
+ * @param {ve.ui.Context} context Context item is in
+ * @param {ve.dm.Model} model Model item is related to
+ * @param {Object} config Configuration options
+ */
+ve.ui.LanguageContextItem = function VeLanguageContextItem( context, model, 
config ) {
+       // Parent constructor
+       ve.ui.LanguageContextItem.super.call( this, context, model, config );
+
+       // Initialization
+       this.$element.addClass( 've-ui-languageContextItem' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.LanguageContextItem, ve.ui.ContextItem );
+
+/* Static Properties */
+
+ve.ui.LanguageContextItem.static.name = 'language';
+
+ve.ui.LanguageContextItem.static.icon = 'language';
+
+ve.ui.LanguageContextItem.static.label = OO.ui.deferMsg( 
'visualeditor-languageinspector-title' );
+
+ve.ui.LanguageContextItem.static.modelClasses = [ ve.dm.LanguageAnnotation ];
+
+ve.ui.LanguageContextItem.static.embeddable = false;
+
+ve.ui.LanguageContextItem.static.commandName = 'language';
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.LanguageContextItem.prototype.getDescription = function () {
+       return ve.ce.LanguageAnnotation.static.getDescription( this.model );
+};
+
+/* Registration */
+
+ve.ui.contextItemFactory.register( ve.ui.LanguageContextItem );
diff --git a/src/ui/contextitems/ve.ui.LinkContextItem.js 
b/src/ui/contextitems/ve.ui.LinkContextItem.js
new file mode 100644
index 0000000..df28117
--- /dev/null
+++ b/src/ui/contextitems/ve.ui.LinkContextItem.js
@@ -0,0 +1,65 @@
+/*!
+ * VisualEditor LinkContextItem class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Context item for a link.
+ *
+ * @param {ve.ui.Context} context Context item is in
+ * @param {ve.dm.Model} model Model item is related to
+ * @param {Object} config Configuration options
+ */
+ve.ui.LinkContextItem = function VeLinkContextItem( context, model, config ) {
+       // Parent constructor
+       ve.ui.LinkContextItem.super.call( this, context, model, config );
+
+       // Initialization
+       this.$element.addClass( 've-ui-linkContextItem' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.LinkContextItem, ve.ui.ContextItem );
+
+/* Static Properties */
+
+ve.ui.LinkContextItem.static.name = 'link';
+
+ve.ui.LinkContextItem.static.icon = 'link';
+
+ve.ui.LinkContextItem.static.label = OO.ui.deferMsg( 
'visualeditor-linkinspector-title' );
+
+ve.ui.LinkContextItem.static.modelClasses = [ ve.dm.LinkAnnotation ];
+
+ve.ui.LinkContextItem.static.embeddable = false;
+
+ve.ui.LinkContextItem.static.commandName = 'link';
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.LinkContextItem.prototype.getDescription = function () {
+       return this.model.getHref();
+};
+
+/**
+ * @inheritdoc
+ */
+ve.ui.LinkContextItem.prototype.renderBody = function () {
+       this.$body.empty().append(
+               $( '<a>' )
+                       .text( this.getDescription() )
+                       .attr( {
+                               href: this.model.getHref(),
+                               target: '_blank'
+                       } )
+       );
+};
+
+/* Registration */
+
+ve.ui.contextItemFactory.register( ve.ui.LinkContextItem );
diff --git a/src/ui/contextitems/ve.ui.ToolContextItem.js 
b/src/ui/contextitems/ve.ui.ToolContextItem.js
new file mode 100644
index 0000000..2d0bff7
--- /dev/null
+++ b/src/ui/contextitems/ve.ui.ToolContextItem.js
@@ -0,0 +1,59 @@
+/*!
+ * VisualEditor ToolContextItem class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Context item for a tool.
+ *
+ * @param {ve.ui.Context} context Context item is in
+ * @param {ve.dm.Model} model Model the item is related to
+ * @param {Function} tool Tool class the item is based on
+ * @param {Object} config Configuration options
+ */
+ve.ui.ToolContextItem = function VeToolContextItem( context, model, tool, 
config ) {
+       // Parent constructor
+       ve.ui.ToolContextItem.super.call( this, context, model, config );
+
+       // Properties
+       this.tool = tool;
+
+       // Initialization
+       this.setIcon( tool.static.icon );
+       this.setLabel( tool.static.title );
+       this.$element.addClass( 've-ui-toolContextItem' );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.ToolContextItem, ve.ui.ContextItem );
+
+/* Methods */
+
+/**
+ * @inheritdoc
+ */
+ve.ui.ToolContextItem.prototype.getCommand = function () {
+       return ve.ui.commandRegistry.lookup( this.tool.static.commandName );
+};
+
+/**
+ * Get a description of the model.
+ *
+ * @return {string} Description of model
+ */
+ve.ui.ToolContextItem.prototype.getDescription = function () {
+       var description;
+
+       if ( this.model instanceof ve.dm.Annotation ) {
+               description = ve.ce.annotationFactory.getDescription( 
this.model );
+       } else if ( this.model instanceof ve.dm.Node ) {
+               description = ve.ce.nodeFactory.getDescription( this.model );
+       }
+       if ( !description ) {
+               description = this.tool.static.title;
+       }
+
+       return description;
+};
diff --git a/src/ui/styles/contextitems/ve.ui.CommentContextItem.css 
b/src/ui/styles/contextitems/ve.ui.CommentContextItem.css
new file mode 100644
index 0000000..ec60d2b
--- /dev/null
+++ b/src/ui/styles/contextitems/ve.ui.CommentContextItem.css
@@ -0,0 +1,13 @@
+/*!
+ * VisualEditor UserInterface CommentContextItem styles.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-commentContextItem .ve-ui-contextItem-body {
+       padding: 0.5em 1.5em 1.5em 1.5em;
+       font-family: 'Courier New', 'Courier', monospace;
+       white-space: pre-wrap;
+       line-height: 1em;
+       color: #555;
+}
diff --git a/src/ui/styles/contextitems/ve.ui.LanguageContextItem.css 
b/src/ui/styles/contextitems/ve.ui.LanguageContextItem.css
new file mode 100644
index 0000000..e939399
--- /dev/null
+++ b/src/ui/styles/contextitems/ve.ui.LanguageContextItem.css
@@ -0,0 +1,13 @@
+/*!
+ * VisualEditor UserInterface LanguageContextItem styles.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-languageContextItem .ve-ui-contextItem-body {
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+       font-size: 1.2em;
+       color: #555;
+}
diff --git a/src/ui/styles/contextitems/ve.ui.LinkContextItem.css 
b/src/ui/styles/contextitems/ve.ui.LinkContextItem.css
new file mode 100644
index 0000000..edb4c21
--- /dev/null
+++ b/src/ui/styles/contextitems/ve.ui.LinkContextItem.css
@@ -0,0 +1,12 @@
+/*!
+ * VisualEditor UserInterface LinkContextItem styles.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-linkContextItem .ve-ui-contextItem-body {
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+       font-size: 1.2em;
+}
diff --git a/src/ui/styles/contextitems/ve.ui.ToolContextItem.css 
b/src/ui/styles/contextitems/ve.ui.ToolContextItem.css
new file mode 100644
index 0000000..848e52c
--- /dev/null
+++ b/src/ui/styles/contextitems/ve.ui.ToolContextItem.css
@@ -0,0 +1,12 @@
+/*!
+ * VisualEditor UserInterface ToolContextItem styles.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-toolContextItem .ve-ui-contextItem-body {
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+       font-size: 1.2em;
+}
diff --git a/src/ui/styles/ve.ui.Context.css b/src/ui/styles/ve.ui.Context.css
index eebe9c2..7467016 100644
--- a/src/ui/styles/ve.ui.Context.css
+++ b/src/ui/styles/ve.ui.Context.css
@@ -6,5 +6,4 @@
 
 /* Hide context by default, only show programmatically */
 .ve-ui-context {
-       visibility: hidden;
 }
\ No newline at end of file
diff --git a/src/ui/styles/ve.ui.ContextItem.css 
b/src/ui/styles/ve.ui.ContextItem.css
new file mode 100644
index 0000000..288d39c
--- /dev/null
+++ b/src/ui/styles/ve.ui.ContextItem.css
@@ -0,0 +1,83 @@
+/*!
+ * VisualEditor UserInterface ContextItem styles.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-contextItem {
+       -webkit-user-select: none;
+       -moz-user-select: none;
+       -ms-user-select: none;
+       -o-user-select: none;
+       user-select: none;
+}
+
+.ve-ui-contextItem-head {
+       display: table;
+       width: 100%;
+}
+
+.ve-ui-contextItem-title {
+       display: table-cell;
+       width: 1%;
+       text-align: left;
+       padding: 0.5em 1em;
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+}
+
+.ve-ui-contextItem-icon {
+       display: inline-block;
+       vertical-align: middle;
+       width: 2em;
+       height: 2em;
+       background-position: center center;
+       background-repeat: no-repeat;
+}
+
+.ve-ui-contextItem-label {
+       min-width: 4em;
+       display: inline-block;
+       vertical-align: middle;
+       margin-left: 0.5em;
+}
+
+.ve-ui-contextItem-info {
+       display: table-cell;
+       position: relative;
+       width: 99%;
+}
+
+.ve-ui-contextItem-description {
+       position: absolute;
+       top: 0;
+       right: 0;
+       left: 0;
+       bottom: 0;
+       line-height: 3.6em;
+       overflow: hidden;
+       text-overflow: ellipsis;
+       white-space: nowrap;
+       color: #888;
+}
+
+.ve-ui-contextItem-actions {
+       display: table-cell;
+       width: 1%;
+       text-align: right;
+       padding: 0.5em;
+}
+
+.ve-ui-contextItem-editButton {
+       display: inline-block;
+       vertical-align: middle;
+}
+
+.ve-ui-contextItem-body {
+       padding: 0 1em 0.5em 1em;
+}
+
+.ve-ui-contextItem + .ve-ui-contextItem {
+       border-top: solid 1px #eee;
+}
diff --git a/src/ui/styles/ve.ui.DesktopContext.css 
b/src/ui/styles/ve.ui.DesktopContext.css
index 6fc0a1d..0af77bc 100644
--- a/src/ui/styles/ve.ui.DesktopContext.css
+++ b/src/ui/styles/ve.ui.DesktopContext.css
@@ -8,9 +8,14 @@
        position: absolute;
 }
 
+.ve-ui-desktopContext .ve-ui-contextItem-description {
+       display: none;
+}
+
 .ve-ui-desktopContext-menu {
        position: absolute;
        font-size: 0.8em;
+       width: 300px;
 }
 
 .ve-ui-desktopContext-menu .oo-ui-toolbar-bar {
diff --git a/src/ui/styles/ve.ui.MobileContext.css 
b/src/ui/styles/ve.ui.MobileContext.css
new file mode 100644
index 0000000..cfd479f
--- /dev/null
+++ b/src/ui/styles/ve.ui.MobileContext.css
@@ -0,0 +1,17 @@
+/*!
+ * VisualEditor UserInterface MobileSurface styles.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+.ve-ui-mobileContext {
+       font-size: 0.875em;
+}
+
+.ve-ui-mobileContext .ve-ui-contextItem-body {
+       display: none;
+}
+
+.ve-ui-mobileContext-menu {
+       border-top: solid 1px #eee;
+}
\ No newline at end of file
diff --git a/src/ui/styles/widgets/ve.ui.ContextOptionWidget.css 
b/src/ui/styles/widgets/ve.ui.ContextOptionWidget.css
deleted file mode 100644
index 151c7a5..0000000
--- a/src/ui/styles/widgets/ve.ui.ContextOptionWidget.css
+++ /dev/null
@@ -1,31 +0,0 @@
-/*!
- * VisualEditor UserInterface ContextOptionWidget styles.
- *
- * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
- */
-
-.ve-ui-contextOptionWidget {
-       padding: 0.5em 1em 0.5em 3em;
-}
-
-.ve-ui-contextOptionWidget .oo-ui-labelElement-label {
-       color: #666;
-       /* Size required to match menu and inspector widths */
-       max-width: 19.4em;
-       text-overflow: ellipsis;
-       overflow: hidden;
-}
-
-.ve-ui-contextOptionWidget .oo-ui-iconElement-icon {
-       opacity: 0.8;
-}
-
-.ve-ui-contextOptionWidget.oo-ui-optionWidget-highlighted,
-.ve-ui-contextOptionWidget.oo-ui-optionWidget-selected,
-.ve-ui-contextOptionWidget.oo-ui-optionWidget-pressed {
-       background-color: transparent;
-}
-
-.ve-ui-contextOptionWidget.oo-ui-optionWidget-highlighted 
.oo-ui-iconElement-icon {
-       opacity: 1;
-}
diff --git a/src/ui/styles/widgets/ve.ui.ContextSelectWidget.css 
b/src/ui/styles/widgets/ve.ui.ContextSelectWidget.css
deleted file mode 100644
index 679f6fc..0000000
--- a/src/ui/styles/widgets/ve.ui.ContextSelectWidget.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/*!
- * VisualEditor UserInterface ContextSelectWidget styles.
- *
- * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
- */
-
-.ve-ui-contextSelectWidget {
-       padding: 0;
-}
diff --git a/src/ui/styles/widgets/ve.ui.MobileContextOptionWidget.css 
b/src/ui/styles/widgets/ve.ui.MobileContextOptionWidget.css
deleted file mode 100644
index 488410c..0000000
--- a/src/ui/styles/widgets/ve.ui.MobileContextOptionWidget.css
+++ /dev/null
@@ -1,22 +0,0 @@
-/*!
- * VisualEditor UserInterface MobileContextOptionWidget styles.
- *
- * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
- */
-
-.ve-ui-mobileContextOptionWidget .oo-ui-labelElement-label {
-       max-width: none;
-       text-overflow: clip;
-}
-
-.ve-ui-mobileContextOptionWidget-label-primary {
-       display: block;
-       text-overflow: ellipsis;
-       overflow: hidden;
-}
-
-.ve-ui-mobileContextOptionWidget-label-secondary {
-       float: right;
-       margin-left: 1em;
-       color: #347bff;
-}
diff --git a/src/ui/ve.ui.Context.js b/src/ui/ve.ui.Context.js
index 15c8276..ac14982 100644
--- a/src/ui/ve.ui.Context.js
+++ b/src/ui/ve.ui.Context.js
@@ -10,6 +10,7 @@
  * @class
  * @abstract
  * @extends OO.ui.Element
+ * @mixins OO.ui.GroupElement
  *
  * @constructor
  * @param {ve.ui.Surface} surface
@@ -17,14 +18,17 @@
  */
 ve.ui.Context = function VeUiContext( surface, config ) {
        // Parent constructor
-       OO.ui.Element.call( this, config );
+       ve.ui.Context.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, config );
 
        // Properties
        this.surface = surface;
        this.visible = false;
+       this.choosing = false;
        this.inspector = null;
        this.inspectors = this.createInspectorWindowManager();
-       this.menu = new ve.ui.ContextSelectWidget( { $: this.$ } );
        this.lastSelectedNode = null;
        this.afterContextChangeTimeout = null;
        this.afterContextChangeHandler = this.afterContextChange.bind( this );
@@ -33,22 +37,38 @@
        // Events
        this.surface.getModel().connect( this, { contextChange: 
'onContextChange' } );
        this.inspectors.connect( this, { opening: 'onInspectorOpening' } );
-       this.menu.connect( this, { choose: 'onContextItemChoose' } );
 
        // Initialization
        // Hide element using a class, not this.toggle, as child implementations
        // of toggle may require the instance to be fully constructed before 
running.
+       this.$group.addClass( 've-ui-context-menu' );
        this.$element
-               .addClass( 've-ui-context oo-ui-element-hidden' );
-       this.menu.toggle( false );
+               .addClass( 've-ui-context oo-ui-element-hidden' )
+               .append( this.$group );
        this.inspectors.$element.addClass( 've-ui-context-inspectors' );
 };
 
 /* Inheritance */
 
 OO.inheritClass( ve.ui.Context, OO.ui.Element );
+OO.mixinClass( ve.ui.Context, OO.ui.GroupElement );
+
+/* Static Property */
+
+/**
+ * Instruct items to provide only a basic rendering.
+ *
+ * @static
+ * @inhertable
+ * @property {boolean}
+ */
+ve.ui.Context.static.basicRendering = false;
 
 /* Methods */
+
+ve.ui.Context.prototype.shouldUseBasicRendering = function () {
+       return this.constructor.static.basicRendering;
+};
 
 /**
  * Handle context change event.
@@ -75,8 +95,8 @@
                        this.afterContextChangeTimeout = setTimeout( 
this.afterContextChangeHandler );
                }
        }
-       // Purge available tools cache
-       this.availableTools = null;
+       // Purge related items cache
+       this.relatedSources = null;
 };
 
 /**
@@ -89,17 +109,21 @@
        this.afterContextChangeTimeout = null;
 
        if ( this.isVisible() ) {
-               if ( this.menu.isVisible() ) {
+               if ( !this.isEmpty() ) {
                        if ( this.isInspectable() ) {
                                // Change state: menu -> menu
-                               this.populateMenu();
+                               this.teardownMenuItems();
+                               this.setupMenuItems();
                                this.updateDimensionsDebounced();
                        } else {
                                // Change state: menu -> closed
-                               this.menu.toggle( false );
+                               this.toggleMenu( false );
                                this.toggle( false );
                        }
-               } else if ( this.inspector && ( !selectedNode || ( selectedNode 
!== this.lastSelectedNode ) ) ) {
+               } else if (
+                       this.inspector &&
+                       ( !selectedNode || ( selectedNode !== 
this.lastSelectedNode ) )
+               ) {
                        // Change state: inspector -> (closed|menu)
                        // Unless there is a selectedNode that hasn't changed 
(e.g. your inspector is editing a node)
                        this.inspector.close();
@@ -107,8 +131,7 @@
        } else {
                if ( this.isInspectable() ) {
                        // Change state: closed -> menu
-                       this.menu.toggle( true );
-                       this.populateMenu();
+                       this.toggleMenu( true );
                        this.toggle( true );
                }
        }
@@ -139,9 +162,9 @@
        opening
                .progress( function ( data ) {
                        if ( data.state === 'setup' ) {
-                               if ( context.menu.isVisible() ) {
+                               if ( !context.isEmpty() ) {
                                        // Change state: menu -> inspector
-                                       context.menu.toggle( false );
+                                       context.toggleMenu( false );
                                } else if ( !context.isVisible() ) {
                                        // Change state: closed -> inspector
                                        context.toggle( true );
@@ -152,7 +175,7 @@
                .always( function ( opened ) {
                        opened.always( function ( closed ) {
                                closed.always( function () {
-                                       var inspectable = 
!!context.getAvailableTools().length;
+                                       var inspectable = 
context.isInspectable();
 
                                        context.inspector = null;
 
@@ -161,8 +184,7 @@
 
                                        if ( inspectable ) {
                                                // Change state: inspector -> 
menu
-                                               context.menu.toggle( true );
-                                               context.populateMenu();
+                                               context.toggleMenu( true );
                                                
context.updateDimensionsDebounced();
                                        } else {
                                                // Change state: inspector -> 
closed
@@ -176,17 +198,6 @@
                                } );
                        } );
                } );
-};
-
-/**
- * Handle context item choose events.
- *
- * @param {ve.ui.ContextOptionWidget} item Chosen item
- */
-ve.ui.Context.prototype.onContextItemChoose = function ( item ) {
-       if ( item ) {
-               item.getCommand().execute( this.surface );
-       }
 };
 
 /**
@@ -204,7 +215,7 @@
  * @return {boolean} Content is inspectable
  */
 ve.ui.Context.prototype.isInspectable = function () {
-       return !!this.getAvailableTools().length;
+       return !!this.getRelatedSources().length;
 };
 
 /**
@@ -212,35 +223,63 @@
  *
  * @return {boolean} Content is inspectable
  */
-ve.ui.Context.prototype.hasInspector = function () {
-       var i, availableTools = this.getAvailableTools();
-       for ( i = availableTools.length - 1; i >= 0; i-- ) {
-               if ( availableTools[i].tool.prototype instanceof 
ve.ui.InspectorTool ) {
-                       return true;
+ve.ui.Context.prototype.isEmbeddable = function () {
+       var i, len,
+               sources = this.getRelatedSources();
+
+       for ( i = 0, len = sources.length; i < len; i++ ) {
+               if ( !sources[i].embedable ) {
+                       return false;
                }
        }
-       return false;
+
+       return true;
 };
 
 /**
- * Get available tools.
+ * Get related item sources.
  *
  * Result is cached, and cleared when the model or selection changes.
  *
- * @returns {Object[]} List of objects containing `tool` and `model` 
properties, representing each
- *   compatible tool and the node or annotation it is compatible with
+ * @returns {Object[]} List of objects containing `type`, `name` and `model` 
properties,
+ *   representing each compatible type (either `item` or `tool`), symbolic 
name of the item or tool
+ *   and the model the item or tool is compatible with
  */
-ve.ui.Context.prototype.getAvailableTools = function () {
-       if ( !this.availableTools ) {
+ve.ui.Context.prototype.getRelatedSources = function () {
+       var i, len, toolClass, items, tools, models,
+               selectedModels = 
this.surface.getModel().getFragment().getSelectedModels();
+
+       if ( !this.relatedSources ) {
+               this.relatedSources = [];
                if ( this.surface.getModel().getSelection() instanceof 
ve.dm.LinearSelection ) {
-                       this.availableTools = 
ve.ui.toolFactory.getToolsForFragment(
-                               this.surface.getModel().getFragment()
-                       );
-               } else {
-                       this.availableTools = [];
+                       models = [];
+                       items = ve.ui.contextItemFactory.getRelatedItems( 
selectedModels );
+                       for ( i = 0, len = items.length; i < len; i++ ) {
+                               models.push( items[i].model );
+                               this.relatedSources.push( {
+                                       type: 'item',
+                                       embedable: 
ve.ui.contextItemFactory.isEmbeddable( items[i].name ),
+                                       name: items[i].name,
+                                       model: items[i].model
+                               } );
+                       }
+                       tools = ve.ui.toolFactory.getRelatedItems( 
selectedModels );
+                       for ( i = 0, len = tools.length; i < len; i++ ) {
+                               if ( models.indexOf( tools[i].model ) === -1 ) {
+                                       toolClass = ve.ui.toolFactory.lookup( 
tools[i].name );
+                                       this.relatedSources.push( {
+                                               type: 'tool',
+                                               embedable: !toolClass ||
+                                                       !( toolClass.prototype 
instanceof ve.ui.InspectorTool ),
+                                               name: tools[i].name,
+                                               model: tools[i].model
+                                       } );
+                               }
+                       }
                }
        }
-       return this.availableTools;
+
+       return this.relatedSources;
 };
 
 /**
@@ -262,15 +301,6 @@
 };
 
 /**
- * Get context menu.
- *
- * @return {ve.ui.ContextSelectWidget}
- */
-ve.ui.Context.prototype.getMenu = function () {
-       return this.menu;
-};
-
-/**
  * Create a inspector window manager.
  *
  * @method
@@ -283,34 +313,72 @@
 };
 
 /**
- * Create a context item widget
+ * Toggle the menu.
  *
- * @param {Object} tool Object containing tool and model properties.
- * @return {ve.ui.ContextOptionWidget} Context item widget
+ * @param {boolean} [show] Show the menu, omit to toggle
+ * @chainable
  */
-ve.ui.Context.prototype.createItem = function ( tool ) {
-       return new ve.ui.ContextOptionWidget(
-               tool.tool, tool.model, { $: this.$, data: tool.tool.static.name 
}
-       );
+ve.ui.Context.prototype.toggleMenu = function ( show ) {
+       show = show === undefined ? !this.choosing : !!show;
+
+       if ( show !== this.choosing ) {
+               this.choosing = show;
+               this.$element.toggleClass( 've-ui-context-choosing', show );
+               if ( show ) {
+                       this.setupMenuItems();
+               } else {
+                       this.teardownMenuItems();
+               }
+       }
+
+       return this;
 };
 
 /**
- * Update the contents of the menu.
+ * Setup menu items.
  *
+ * @protected
  * @chainable
  */
-ve.ui.Context.prototype.populateMenu = function () {
-       var i, len,
-               items = [],
-               tools = this.getAvailableTools();
+ve.ui.Context.prototype.setupMenuItems = function () {
+       var i, len, source,
+               sources = this.getRelatedSources(),
+               items = [];
 
-       this.menu.clearItems();
-       if ( tools.length ) {
-               for ( i = 0, len = tools.length; i < len; i++ ) {
-                       items.push( this.createItem( tools[i] ) );
+       for ( i = 0, len = sources.length; i < len; i++ ) {
+               source = sources[i];
+               if ( source.type === 'item' ) {
+                       items.push( ve.ui.contextItemFactory.create(
+                               sources[i].name, this, sources[i].model, { $: 
this.$ }
+                       ) );
+               } else if ( source.type === 'tool' ) {
+                       items.push( new ve.ui.ToolContextItem(
+                               this, sources[i].model, 
ve.ui.toolFactory.lookup( sources[i].name ), { $: this.$ }
+                       ) );
                }
-               this.menu.addItems( items );
        }
+
+       this.addItems( items );
+       for ( i = 0, len = items.length; i < len; i++ ) {
+               items[i].setup();
+       }
+
+       return this;
+};
+
+/**
+ * Teardown menu items.
+ *
+ * @protected
+ * @chainable
+ */
+ve.ui.Context.prototype.teardownMenuItems = function () {
+       var i, len;
+
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               this.items[i].teardown();
+       }
+       this.clearItems();
 
        return this;
 };
@@ -347,7 +415,6 @@
        // Disconnect events
        this.surface.getModel().disconnect( this );
        this.inspectors.disconnect( this );
-       this.menu.disconnect( this );
 
        // Destroy inspectors WindowManager
        this.inspectors.destroy();
diff --git a/src/ui/ve.ui.ContextItem.js b/src/ui/ve.ui.ContextItem.js
new file mode 100644
index 0000000..be3228f
--- /dev/null
+++ b/src/ui/ve.ui.ContextItem.js
@@ -0,0 +1,179 @@
+/*!
+ * VisualEditor UserInterface ContextItem class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Item in a context.
+ *
+ * @extends OO.ui.Widget
+ * @mixins OO.ui.IconElement
+ * @mixins OO.ui.LabelElement
+ * @mixins OO.ui.PendingElement
+ *
+ * @param {ve.ui.Context} context Context item is in
+ * @param {ve.dm.Model} model Model item is related to
+ * @param {Object} config Configuration options
+ * @cfg {boolean} [basic] Render only basic information
+ */
+ve.ui.ContextItem = function ( context, model, config ) {
+       // Parent constructor
+       ve.ui.ContextItem.super.call( this, config );
+
+       // Mixin constructors
+       OO.ui.IconElement.call( this, config );
+       OO.ui.LabelElement.call( this, config );
+       OO.ui.PendingElement.call( this, config );
+
+       // Properties
+       this.context = context;
+       this.model = model;
+       this.$head = $( '<div>' );
+       this.$title = $( '<div>' );
+       this.$actions = $( '<div>' );
+       this.$body = $( '<div>' );
+       this.$info = $( '<div>' );
+       this.$description = $( '<div>' );
+       this.editButton = new OO.ui.ButtonWidget( {
+               label: 'Edit',
+               flags: [ 'progressive' ],
+               classes: [ 've-ui-contextItem-editButton' ]
+       } );
+
+       // Events
+       this.editButton.connect( this, { click: 'onEditButtonClick' } );
+       this.$element.on( 'mousedown', false );
+
+       // Initialization
+       this.$label.addClass( 've-ui-contextItem-label' );
+       this.$icon.addClass( 've-ui-contextItem-icon' );
+       this.$description.addClass( 've-ui-contextItem-description' );
+       this.$info
+               .addClass( 've-ui-contextItem-info' )
+               .append( this.$description );
+       this.$title
+               .addClass( 've-ui-contextItem-title' )
+               .append( this.$icon, this.$label );
+       this.$actions
+               .addClass( 've-ui-contextItem-actions' )
+               .append( this.editButton.$element );
+       this.$head
+               .addClass( 've-ui-contextItem-head' )
+               .append( this.$title, this.$info, this.$actions );
+       this.$body.addClass( 've-ui-contextItem-body' );
+       this.$element
+               .addClass( 've-ui-contextItem' )
+               .toggleClass( 've-ui-contextItem-basic', 
this.context.shouldUseBasicRendering() )
+               .append( this.$head, this.$body );
+};
+
+OO.inheritClass( ve.ui.ContextItem, OO.ui.Widget );
+OO.mixinClass( ve.ui.ContextItem, OO.ui.IconElement );
+OO.mixinClass( ve.ui.ContextItem, OO.ui.LabelElement );
+OO.mixinClass( ve.ui.ContextItem, OO.ui.PendingElement );
+
+/* Static Properties */
+
+ve.ui.ContextItem.static.editable = true;
+
+ve.ui.ContextItem.static.embeddable = true;
+
+ve.ui.ContextItem.static.commandName = null;
+
+/* Methods */
+
+/**
+ * Handle edit button click events.
+ *
+ * @localdoc Executes the command related to #static-commandName on the 
context's surface
+ *
+ * @protected
+ */
+ve.ui.ContextItem.prototype.onEditButtonClick = function () {
+       var command = this.getCommand();
+
+       if ( command ) {
+               command.execute( this.context.getSurface() );
+       }
+};
+
+/**
+ * Check if item is editable.
+ *
+ * @return {boolean} Item is editable
+ */
+ve.ui.ContextItem.prototype.isEditable = function () {
+       return this.constructor.static.editable;
+};
+
+/**
+ * Get the command for this item.
+ *
+ * @return {ve.ui.Command} Command
+ */
+ve.ui.ContextItem.prototype.getCommand = function () {
+       return ve.ui.commandRegistry.lookup( 
this.constructor.static.commandName );
+};
+
+/**
+ * Get the description.
+ *
+ * @localdoc Override for custom description content
+ * @return {string} Item description
+ */
+ve.ui.ContextItem.prototype.getDescription = function () {
+       return '';
+};
+
+/**
+ * Render the body.
+ *
+ * @localdoc Renders the result of #getDescription
+ * @localdoc Override for custom body rendering
+ */
+ve.ui.ContextItem.prototype.renderBody = function () {
+       this.$body.text( this.getDescription() );
+};
+
+/**
+ * Render the description.
+ *
+ * @localdoc Renders the result of #getDescription
+ * @localdoc Override for custom description rendering
+ */
+ve.ui.ContextItem.prototype.renderDescription = function () {
+       this.$description.text( this.getDescription() );
+};
+
+/**
+ * Setup the item.
+ *
+ * @localdoc Calls #renderDescription if the context suggests basic rendering 
or #renderBody if not
+ * @localdoc Override to start any async rendering common to the body and 
description
+ * @chainable
+ */
+ve.ui.ContextItem.prototype.setup = function () {
+       this.editButton.toggle( this.isEditable() );
+
+       if ( this.context.shouldUseBasicRendering() ) {
+               this.renderDescription();
+       } else {
+               this.renderBody();
+       }
+
+       return this;
+};
+
+/**
+ * Teardown the item.
+ *
+ * @localdoc Empties the description and body
+ * @localdox Override to abort any async rendering
+ * @chainable
+ */
+ve.ui.ContextItem.prototype.teardown = function () {
+       this.$description.empty();
+       this.$body.empty();
+       return this;
+};
diff --git a/src/ui/ve.ui.ContextItemFactory.js 
b/src/ui/ve.ui.ContextItemFactory.js
new file mode 100644
index 0000000..0761b87
--- /dev/null
+++ b/src/ui/ve.ui.ContextItemFactory.js
@@ -0,0 +1,46 @@
+/*!
+ * VisualEditor UserInterface ContextItemFactory class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Factory for context items.
+ *
+ * @class
+ * @extends OO.Factory
+ * @mixins ve.ui.ModeledFactory
+ *
+ * @constructor
+ */
+ve.ui.ContextItemFactory = function VeUiContextItemFactory() {
+       // Parent constructor
+       ve.ui.ContextItemFactory.super.call( this );
+
+       // Mixin constructors
+       ve.ui.ModeledFactory.call( this );
+};
+
+/* Inheritance */
+
+OO.inheritClass( ve.ui.ContextItemFactory, OO.Factory );
+OO.mixinClass( ve.ui.ContextItemFactory, ve.ui.ModeledFactory );
+
+/* Methods */
+
+/**
+ * Check if an item is embeddable.
+ *
+ * @param {string} name Symbolic item name
+ * @return {boolean} Item is embeddable
+ */
+ve.ui.ContextItemFactory.prototype.isEmbeddable = function ( name ) {
+       if ( Object.prototype.hasOwnProperty.call( this.registry, name ) ) {
+               return !!this.registry[name].static.embeddable;
+       }
+       throw new Error( 'Unrecognized symbolic name: ' + name );
+};
+
+/* Initialization */
+
+ve.ui.contextItemFactory = new ve.ui.ContextItemFactory();
diff --git a/src/ui/ve.ui.DesktopContext.js b/src/ui/ve.ui.DesktopContext.js
index 004b58e..6afee74 100644
--- a/src/ui/ve.ui.DesktopContext.js
+++ b/src/ui/ve.ui.DesktopContext.js
@@ -45,16 +45,9 @@
        this.$element
                .addClass( 've-ui-desktopContext' )
                .append( this.popup.$element );
-       this.menu.$element.addClass( 've-ui-desktopContext-menu' );
+       this.$group.addClass( 've-ui-desktopContext-menu' );
        this.inspectors.$element.addClass( 've-ui-desktopContext-inspectors' );
-       this.popup.$body.append( this.menu.$element, this.inspectors.$element );
-
-       // HACK: hide the popup with visibility: hidden; rather than display: 
none;, because
-       // the popup contains inspector iframes, and applying display: none; to 
those causes them to
-       // not load in Firefox
-       this.popup.$element
-               .css( { visibility: 'hidden' } )
-               .removeClass( 'oo-ui-element-hidden' );
+       this.popup.$body.append( this.$group, this.inspectors.$element );
 };
 
 /* Inheritance */
@@ -81,11 +74,10 @@
  */
 ve.ui.DesktopContext.prototype.onSuppress = function () {
        this.suppressed = true;
-
        if ( this.isVisible() ) {
-               if ( this.menu.isVisible() ) {
+               if ( !this.isEmpty() ) {
                        // Change state: menu -> closed
-                       this.menu.toggle( false );
+                       this.toggleMenu( false );
                        this.toggle( false );
                } else if ( this.inspector ) {
                        // Change state: inspector -> closed
@@ -98,14 +90,11 @@
  * Handle context unsuppression event.
  */
 ve.ui.DesktopContext.prototype.onUnsuppress = function () {
-       var inspectable = !!this.getAvailableTools().length;
-
        this.suppressed = false;
 
-       if ( inspectable ) {
+       if ( this.isInspectable() ) {
                // Change state: closed -> menu
-               this.menu.toggle( true );
-               this.populateMenu();
+               this.toggleMenu( true );
                this.toggle( true );
        }
 };
@@ -166,19 +155,13 @@
                return $.Deferred().resolve().promise();
        }
 
-       this.visible = show;
        this.transitioning = $.Deferred();
        promise = this.transitioning.promise();
 
        this.popup.toggle( show );
-       // HACK: make the context and popup visibility: hidden; instead of 
display: none; because
-       // they contain inspector iframes, and applying display: none; to those 
causes them to
-       // not load in Firefox
-       this.$element.add( this.popup.$element )
-               .removeClass( 'oo-ui-element-hidden' )
-               .css( {
-                       visibility: show ? 'visible' : 'hidden'
-               } );
+
+       // Parent method
+       ve.ui.DesktopContext.super.prototype.toggle.call( this, show );
 
        this.transitioning.resolve();
        this.transitioning = null;
@@ -216,9 +199,9 @@
                this.popup.toggleAnchor( true );
                this.popup.align = 'center';
        } else if ( focusedNode && !focusedNode.isContent() ) {
-               embeddable = !this.hasInspector() &&
-                       boundingRect.height > this.menu.$element.outerHeight() 
+ 5 &&
-                       boundingRect.width > this.menu.$element.outerWidth() + 
10;
+               embeddable = this.isEmbeddable() &&
+                       boundingRect.height > this.$group.outerHeight() + 5 &&
+                       boundingRect.width > this.$group.outerWidth() + 10;
                this.popup.toggleAnchor( !embeddable );
                if ( embeddable ) {
                        // Embedded context position depends on directionality
@@ -278,7 +261,7 @@
  * Resize the popup to match the size of its contents (menu or inspector).
  */
 ve.ui.DesktopContext.prototype.setPopupSize = function () {
-       var $container = this.inspector ? this.inspector.$frame : 
this.menu.$element;
+       var $container = this.inspector ? this.inspector.$frame : this.$group;
 
        // PopupWidget normally is clippable, suppress that to be able to 
resize and scroll it into view.
        // Needs to be repeated before every call, as it resets itself when the 
popup is shown or hidden.
diff --git a/src/ui/ve.ui.MobileContext.js b/src/ui/ve.ui.MobileContext.js
index 3f9cbdf..7db990c 100644
--- a/src/ui/ve.ui.MobileContext.js
+++ b/src/ui/ve.ui.MobileContext.js
@@ -28,11 +28,8 @@
        } );
 
        // Initialization
-       this.$element
-               .addClass( 've-ui-mobileContext' )
-               .append( this.menu.$element );
-       this.toggle( true );
-       this.menu.$element.addClass( 've-ui-mobileContext-menu' );
+       this.$element.addClass( 've-ui-mobileContext' );
+       this.$group.addClass( 've-ui-mobileContext-menu' );
        this.inspectors.$element.addClass( 've-ui-mobileContext-inspectors' );
        this.surface.getGlobalOverlay().$element.append( 
this.inspectors.$element );
 };
@@ -40,6 +37,10 @@
 /* Inheritance */
 
 OO.inheritClass( ve.ui.MobileContext, ve.ui.Context );
+
+/* Static Properties */
+
+ve.ui.MobileContext.static.basicRendering = true;
 
 /* Methods */
 
@@ -56,23 +57,13 @@
 /**
  * @inheritdoc
  */
-ve.ui.MobileContext.prototype.createItem = function ( tool ) {
-       return new ve.ui.MobileContextOptionWidget(
-               tool.tool, tool.model, { $: this.$, data: tool.tool.static.name 
}
-       );
-};
-
-/**
- * @inheritdoc
- */
 ve.ui.MobileContext.prototype.toggle = function ( show ) {
        var deferred = $.Deferred();
 
        show = show === undefined ? !this.visible : !!show;
        if ( show !== this.visible ) {
                this.visible = show;
-               this.$element
-                       .toggleClass( 'oo-ui-element-hidden', !show );
+               this.$element.toggleClass( 'oo-ui-element-hidden', !show );
                setTimeout( function () {
                        deferred.resolve();
                }, 300 );
diff --git a/src/ui/ve.ui.ModeledFactory.js b/src/ui/ve.ui.ModeledFactory.js
new file mode 100644
index 0000000..4f79aef
--- /dev/null
+++ b/src/ui/ve.ui.ModeledFactory.js
@@ -0,0 +1,90 @@
+/*!
+ * VisualEditor UserInterface ModeledFactory class.
+ *
+ * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
+ */
+
+/**
+ * Mixin for factories that relate to models.
+ *
+ * @class
+ * @extends OO.ui.ModeledFactory
+ *
+ * @constructor
+ */
+ve.ui.ModeledFactory = function VeUiModeledFactory() {
+       //
+};
+
+/* Inheritance */
+
+OO.initClass( ve.ui.ModeledFactory );
+
+/* Methods */
+
+/**
+ * Get a list of symbolic names for classes related to a list of models.
+ *
+ * The lowest compatible item in each inheritance chain will be used.
+ *
+ * @param {ve.dm.Model[]} models Models to find relationships with
+ * @returns {Object[]} List of objects containing `name` and `model` 
properties, representing
+ *   each compatible class's symbolic name and the model it is compatible with
+ */
+ve.ui.ModeledFactory.prototype.getRelatedItems = function ( models ) {
+       var i, iLen, j, jLen, name, classes, model,
+               registry = this.registry,
+               names = {},
+               matches = [];
+
+       /**
+        * Collect the most specific compatible classes for a model.
+        *
+        * @private
+        * @param {ve.dm.Model} model Model to find compatability with
+        * @returns {Function[]} List of compatible classes
+        */
+       function collect( model ) {
+               var i, len, name, candidate, add, modelClasses,
+                       candidates = [];
+
+               for ( name in registry ) {
+                       candidate = registry[name];
+                       modelClasses = candidate.static.modelClasses;
+                       if ( modelClasses && ve.isInstanceOfAny( model, 
modelClasses ) ) {
+                               add = true;
+                               for ( i = 0, len = candidates.length; i < len; 
i++ ) {
+                                       if ( candidate.prototype instanceof 
candidates[i] ) {
+                                               candidates.splice( i, 1, 
candidate );
+                                               add = false;
+                                               break;
+                                       } else if ( candidates[i].prototype 
instanceof candidate ) {
+                                               add = false;
+                                               break;
+                                       }
+                               }
+                               if ( add ) {
+                                       candidates.push( candidate );
+                               }
+                       }
+               }
+
+               return candidates;
+       }
+
+       // Collect compatible classes and the models they are specifically 
compatible with,
+       // discarding class's with duplicate symbolic names
+       for ( i = 0, iLen = models.length; i < iLen; i++ ) {
+               model = models[i];
+               classes = collect( model );
+               for ( j = 0, jLen = classes.length; j < jLen; j++ ) {
+                       name = classes[j].static.name;
+                       if ( !names[name] ) {
+                               matches.push( { name: name, model: model } );
+                       }
+                       names[name] = true;
+               }
+       }
+
+       return matches;
+};
diff --git a/src/ui/ve.ui.TableContext.js b/src/ui/ve.ui.TableContext.js
index 9943aae..657a0bb 100644
--- a/src/ui/ve.ui.TableContext.js
+++ b/src/ui/ve.ui.TableContext.js
@@ -11,6 +11,7 @@
  *
  * @class
  * @extends OO.ui.Element
+ * @mixins OO.ui.GroupElement
  *
  * @constructor
  * @param {ve.ce.TableNode} tableNode
@@ -24,6 +25,9 @@
        // Parent constructor
        ve.ui.TableContext.super.call( this, config );
 
+       // Mixin constructors
+       OO.ui.GroupElement.call( this, config );
+
        // Properties
        this.tableNode = tableNode;
        this.toolGroup = toolGroup;
@@ -34,7 +38,6 @@
                classes: ['ve-ui-tableContext-indicator'],
                indicator: config.indicator
        } );
-       this.menu = new ve.ui.ContextSelectWidget( { $: this.$ } );
        this.popup = new OO.ui.PopupWidget( {
                $: this.$,
                $container: this.surface.$element,
@@ -43,38 +46,40 @@
 
        // Events
        this.indicator.$element.on( 'mousedown', 
this.onIndicatorMouseDown.bind( this ) );
-       this.menu.connect( this, { choose: 'onContextItemChoose' } );
        this.onDocumentMouseDownHandler = this.onDocumentMouseDown.bind( this );
 
        // Initialization
-       this.populateMenu();
-       this.menu.$element.addClass( 've-ui-tableContext-menu' );
-       this.popup.$body.append( this.menu.$element );
+       this.toggleMenu( true );
+       this.$group.addClass( 've-ui-tableContext-menu' );
+       this.popup.$body.append( this.$group );
        this.$element.addClass( 've-ui-tableContext' ).append( 
this.indicator.$element, this.popup.$element );
 };
 
 /* Inheritance */
 
 OO.inheritClass( ve.ui.TableContext, OO.ui.Element );
+OO.mixinClass( ve.ui.TableContext, OO.ui.GroupElement );
 
 /* Methods */
 
 /**
  * Populate menu items.
  */
-ve.ui.TableContext.prototype.populateMenu = function () {
+ve.ui.TableContext.prototype.toggleMenu = function () {
+       /*
        var i, l, tool,
                items = [],
                toolList = ve.ui.toolFactory.getTools( [ { group: 
this.toolGroup } ] );
 
-       this.menu.clearItems();
+       this.clearItems();
        for ( i = 0, l = toolList.length; i < l; i++ ) {
                tool = ve.ui.toolFactory.lookup( toolList[i] );
                items.push( new ve.ui.ContextOptionWidget(
                        tool, this.tableNode.getModel(), { $: this.$, data: 
tool.static.name }
                ) );
        }
-       this.menu.addItems( items );
+       this.addItems( items );
+       */
 };
 
 /**
diff --git a/src/ui/ve.ui.ToolFactory.js b/src/ui/ve.ui.ToolFactory.js
index dd00a43..71c345a 100644
--- a/src/ui/ve.ui.ToolFactory.js
+++ b/src/ui/ve.ui.ToolFactory.js
@@ -9,84 +9,22 @@
  *
  * @class
  * @extends OO.ui.ToolFactory
+ * @mixins ve.ui.ModeledFactory
  *
  * @constructor
  */
-ve.ui.ToolFactory = function OoUiToolFactory() {
+ve.ui.ToolFactory = function VeUiToolFactory() {
        // Parent constructor
-       OO.ui.ToolFactory.call( this );
+       ve.ui.ToolFactory.super.call( this );
+
+       // Mixin constructors
+       ve.ui.ModeledFactory.call( this );
 };
 
 /* Inheritance */
 
 OO.inheritClass( ve.ui.ToolFactory, OO.ui.ToolFactory );
-
-/* Methods */
-
-/**
- * Get a list of tools for a fragment.
- *
- * The lowest compatible item in each inheritance chain will be used.
- *
- * @method
- * @param {ve.dm.SurfaceFragment} fragment Fragment to find compatible tools 
for
- * @returns {Object[]} List of objects containing `tool` and `model` 
properties, representing each
- *   compatible tool and the node or annotation it is compatible with
- */
-ve.ui.ToolFactory.prototype.getToolsForFragment = function ( fragment ) {
-       var i, iLen, j, jLen, name, tools, model,
-               models = fragment.getSelectedModels(),
-               names = {},
-               matches = [];
-
-       // Collect tool/model pairs, unique by tool name
-       for ( i = 0, iLen = models.length; i < iLen; i++ ) {
-               model = models[i];
-               tools = this.collectCompatibleTools( model );
-               for ( j = 0, jLen = tools.length; j < jLen; j++ ) {
-                       name = tools[j].static.name;
-                       if ( !names[name] ) {
-                               matches.push( { tool: tools[j], model: model } 
);
-                       }
-                       names[name] = true;
-               }
-       }
-
-       return matches;
-};
-
-/**
- * Collect the most specific compatible tools for an annotation or node.
- *
- * @param {ve.dm.Annotation|ve.dm.Node} model Annotation or node
- * @returns {Function[]} List of compatible tools
- */
-ve.ui.ToolFactory.prototype.collectCompatibleTools = function ( model ) {
-       var i, len, name, candidate, add,
-               candidates = [];
-
-       for ( name in this.registry ) {
-               candidate = this.registry[name];
-               if ( candidate.static.isCompatibleWith( model ) ) {
-                       add = true;
-                       for ( i = 0, len = candidates.length; i < len; i++ ) {
-                               if ( candidate.prototype instanceof 
candidates[i] ) {
-                                       candidates.splice( i, 1, candidate );
-                                       add = false;
-                                       break;
-                               } else if ( candidates[i].prototype instanceof 
candidate ) {
-                                       add = false;
-                                       break;
-                               }
-                       }
-                       if ( add ) {
-                               candidates.push( candidate );
-                       }
-               }
-       }
-
-       return candidates;
-};
+OO.mixinClass( ve.ui.ToolFactory, ve.ui.ModeledFactory );
 
 /* Initialization */
 
diff --git a/src/ui/ve.ui.js b/src/ui/ve.ui.js
index 26bfad1..d06b4c3 100644
--- a/src/ui/ve.ui.js
+++ b/src/ui/ve.ui.js
@@ -15,6 +15,7 @@
        // 'commandRegistry' instantiated in ve.ui.CommandRegistry.js
        // 'triggerRegistry' instantiated in ve.ui.TriggerRegistry.js
        // 'toolFactory' instantiated in ve.ui.ToolFactory.js
+       // 'contextItemFactory' instantiated in ve.ui.ContextItemFactory.js
        // 'dataTransferHandlerFactory' instantiated in 
ve.ui.DataTransferHandlerFactory.js
        windowFactory: new OO.Factory()
 };
diff --git a/src/ui/widgets/ve.ui.ContextOptionWidget.js 
b/src/ui/widgets/ve.ui.ContextOptionWidget.js
deleted file mode 100644
index 0175076..0000000
--- a/src/ui/widgets/ve.ui.ContextOptionWidget.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/*!
- * VisualEditor Context Item widget class.
- *
- * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
- */
-
-/**
- * Proxy for a tool, displaying information about the current context.
- *
- * Use with ve.ui.ContextSelectWidget.
- *
- * @class
- * @extends OO.ui.DecoratedOptionWidget
- *
- * @constructor
- * @param {Function} tool Tool item is a proxy for
- * @param {ve.dm.Node|ve.dm.Annotation} model Node or annotation item is 
related to
- * @param {Object} [config] Configuration options
- */
-ve.ui.ContextOptionWidget = function VeUiContextOptionWidget( tool, model, 
config ) {
-       // Config initialization
-       config = config || {};
-
-       // Parent constructor
-       ve.ui.ContextOptionWidget.super.call( this, config );
-
-       // Properties
-       this.tool = tool;
-       this.model = model;
-
-       // Initialization
-       this.$element.addClass( 've-ui-contextOptionWidget' );
-       this.setIcon( this.tool.static.icon );
-
-       this.setLabel( this.getDescription() );
-};
-
-/* Setup */
-
-OO.inheritClass( ve.ui.ContextOptionWidget, OO.ui.DecoratedOptionWidget );
-
-/* Methods */
-
-/**
- * Get a description of the model.
- *
- * @return {string} Description of model
- */
-ve.ui.ContextOptionWidget.prototype.getDescription = function () {
-       var description;
-
-       if ( this.model instanceof ve.dm.Annotation ) {
-               description = ve.ce.annotationFactory.getDescription( 
this.model );
-       } else if ( this.model instanceof ve.dm.Node ) {
-               description = ve.ce.nodeFactory.getDescription( this.model );
-       }
-       if ( !description ) {
-               description = this.tool.static.title;
-       }
-
-       return description;
-};
-
-/**
- * Get the command for this item.
- *
- * @return {ve.ui.Command} Command
- */
-ve.ui.ContextOptionWidget.prototype.getCommand = function () {
-       return ve.ui.commandRegistry.lookup( this.tool.static.commandName );
-};
diff --git a/src/ui/widgets/ve.ui.ContextSelectWidget.js 
b/src/ui/widgets/ve.ui.ContextSelectWidget.js
deleted file mode 100644
index 98b5960..0000000
--- a/src/ui/widgets/ve.ui.ContextSelectWidget.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/*!
- * VisualEditor Context Menu widget class.
- *
- * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
- */
-
-/**
- * Menu of items, each an inspectable attribute of the current context.
- *
- * Use with ve.ui.ContextOptionWidget.
- *
- * @class
- * @extends OO.ui.SelectWidget
- *
- * @constructor
- * @param {Object} [config] Configuration options
- */
-ve.ui.ContextSelectWidget = function VeUiContextSelectWidget( config ) {
-       // Config initialization
-       config = config || {};
-
-       // Parent constructor
-       ve.ui.ContextSelectWidget.super.call( this, config );
-
-       this.connect( this, { choose: 'onChooseItem' } );
-
-       // Initialization
-       this.$element.addClass( 've-ui-contextSelectWidget' );
-};
-
-/* Setup */
-
-OO.inheritClass( ve.ui.ContextSelectWidget, OO.ui.SelectWidget );
-
-/* Methods */
-
-/**
- * Handle choose item events.
- */
-ve.ui.ContextSelectWidget.prototype.onChooseItem = function () {
-       // Auto-deselect
-       this.selectItem( null );
-};
diff --git a/src/ui/widgets/ve.ui.MobileContextOptionWidget.js 
b/src/ui/widgets/ve.ui.MobileContextOptionWidget.js
deleted file mode 100644
index 3b3eee5..0000000
--- a/src/ui/widgets/ve.ui.MobileContextOptionWidget.js
+++ /dev/null
@@ -1,35 +0,0 @@
-/*!
- * VisualEditor Mobile Context Item widget class.
- *
- * @copyright 2011-2015 VisualEditor Team and others; see 
http://ve.mit-license.org
- */
-
-/**
- * Mobile version of context item widget
- *
- * @class
- * @extends ve.ui.ContextOptionWidget
- *
- * @constructor
- * @param {Function} tool
- * @param {ve.dm.Node|ve.dm.Annotation} model
- * @param {Object} [config]
- */
-ve.ui.MobileContextOptionWidget = function VeUiContextOptionWidget() {
-       // Parent constructor
-       ve.ui.MobileContextOptionWidget.super.apply( this, arguments );
-
-       this.$element.addClass( 've-ui-mobileContextOptionWidget' );
-       this.setLabel(
-               this.$( '<span>' ).addClass( 
've-ui-mobileContextOptionWidget-label-secondary' )
-                       .text( ve.msg( 
'visualeditor-contextitemwidget-label-secondary' ) )
-                       .add(
-                               this.$( '<span>' ).addClass( 
've-ui-mobileContextOptionWidget-label-primary' )
-                                       .text( this.getDescription() )
-                       )
-       );
-};
-
-/* Setup */
-
-OO.inheritClass( ve.ui.MobileContextOptionWidget, ve.ui.ContextOptionWidget );
diff --git a/tests/index.html b/tests/index.html
index 563f554..b028791 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -249,6 +249,13 @@
                <script src="../src/ui/ve.ui.Overlay.js"></script>
                <script src="../src/ui/ve.ui.Surface.js"></script>
                <script src="../src/ui/ve.ui.Context.js"></script>
+               <script src="../src/ui/ve.ui.ModeledFactory.js"></script>
+               <script src="../src/ui/ve.ui.ContextItem.js"></script>
+               <script src="../src/ui/ve.ui.ContextItemFactory.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.CommentContextItem.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.LanguageContextItem.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.LinkContextItem.js"></script>
+               <script 
src="../src/ui/contextitems/ve.ui.ToolContextItem.js"></script>
                <script src="../src/ui/ve.ui.TableContext.js"></script>
                <script src="../src/ui/ve.ui.Tool.js"></script>
                <script src="../src/ui/ve.ui.Toolbar.js"></script>
@@ -299,8 +306,6 @@
                <script 
src="../src/ui/widgets/ve.ui.LanguageInputWidget.js"></script>
                <script src="../src/ui/widgets/ve.ui.SurfaceWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.LinkTargetInputWidget.js"></script>
-               <script 
src="../src/ui/widgets/ve.ui.ContextSelectWidget.js"></script>
-               <script 
src="../src/ui/widgets/ve.ui.ContextOptionWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.DimensionsWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.MediaSizeWidget.js"></script>
                <script 
src="../src/ui/widgets/ve.ui.WhitespacePreservingTextInputWidget.js"></script>

-- 
To view, visit https://gerrit.wikimedia.org/r/192738
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: Ibca8cf5cb8ee2f0a8f520461cef3af6fa6f2ac4d
Gerrit-PatchSet: 1
Gerrit-Project: VisualEditor/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Trevor Parscal <tpars...@wikimedia.org>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to