Trevor Parscal has uploaded a new change for review.

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


Change subject: [WIP] Toolbar API
......................................................................

[WIP] Toolbar API

Objectives:

* Make it possible to add items to toolbars without having to have all toolbars 
know about the items in advance
* Make it possible to specialize an existing tool and have it be used instead 
of the base implementation

Approach:

* Tools are named using a path-style category/id/ext system, making them 
selectable, the latter component being used to differentiate extended tools 
from their base classes, but is ignored during selection
* Toolbars have ToolGroups, which include or exclude tools by category or 
category/id, and order them by promoting and demoting selections of tools by 
category or category/id

Future:

* Add a way to place available but not yet placed tools in an "overflow" group
* Add a mode to ToolGroup to make the tools a multi-column drop-down style list 
with labels so tools with less obvious icons are easier to identify - and 
probably use this as the overflow group

Change-Id: I7625f861435a99ce3d7a2b1ece9731aaab1776f8
---
M modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
M modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
M modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
M modules/ve-mw/ui/styles/ve.ui.Tool.css
M modules/ve-mw/ui/tools/buttons/ve.ui.MWLinkButtonTool.js
M modules/ve-mw/ui/tools/buttons/ve.ui.MWMathButtonTool.js
M modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaEditButtonTool.js
M modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaInsertButtonTool.js
M modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceButtonTool.js
M modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceListButtonTool.js
M modules/ve-mw/ui/tools/buttons/ve.ui.MWTransclusionButtonTool.js
M modules/ve-mw/ui/tools/dropdowns/ve.ui.MWFormatDropdownTool.js
M modules/ve/init/sa/ve.init.sa.Target.js
M modules/ve/init/ve.init.Target.js
M modules/ve/ui/styles/ve.ui.Tool.css
M modules/ve/ui/tools/buttons/ve.ui.BoldButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.BulletButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.ClearButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.CodeButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.IndentButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.ItalicButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.LanguageButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.LinkButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.NumberButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.OutdentButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.RedoButtonTool.js
M modules/ve/ui/tools/buttons/ve.ui.UndoButtonTool.js
M modules/ve/ui/tools/dropdowns/ve.ui.FormatDropdownTool.js
M modules/ve/ui/tools/ve.ui.DropdownTool.js
M modules/ve/ui/ve.ui.Context.js
M modules/ve/ui/ve.ui.Tool.js
M modules/ve/ui/ve.ui.ToolFactory.js
M modules/ve/ui/ve.ui.ToolGroup.js
M modules/ve/ui/ve.ui.Toolbar.js
34 files changed, 370 insertions(+), 181 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/VisualEditor 
refs/changes/12/78412/1

diff --git a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js 
b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
index 5856153..f0fd700 100644
--- a/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
+++ b/modules/ve-mw/init/targets/ve.init.mw.ViewPageTarget.js
@@ -167,18 +167,6 @@
        }
 };
 
-ve.init.mw.ViewPageTarget.static.toolbarTools = [
-       { 'items': [ 'undo', 'redo' ] },
-       { 'items': [ 'mwFormat' ] },
-       { 'items': [ 'bold', 'italic', 'mwLink', 'language', 'code', 'clear' ] 
},
-       { 'items': [ 'number', 'bullet', 'outdent', 'indent' ] },
-       { 'items': [ 'mwMediaInsert', 'mwReference', 'mwReferenceList', 
'mwTransclusion', 'mwMath' ] }
-];
-
-ve.init.mw.ViewPageTarget.static.surfaceCommands = [
-       'bold', 'italic', 'mwLink', 'undo', 'redo', 'indent', 'outdent', 'clear'
-];
-
 // TODO: Accessibility tooltips and logical tab order for prevButton and 
closeButton.
 ve.init.mw.ViewPageTarget.saveDialogTemplate = '\
        <div class="ve-init-mw-viewPageTarget-saveDialog-head">\
@@ -1887,7 +1875,7 @@
 ve.init.mw.ViewPageTarget.prototype.setUpToolbar = function () {
        this.toolbar = new ve.ui.SurfaceToolbar( this.surface, { 'shadow': 
true, 'actions': true } );
        this.toolbar.connect( this, { 'position': 'onToolbarPosition' } );
-       this.toolbar.setup( this.constructor.static.toolbarTools );
+       this.toolbar.setup( this.constructor.static.toolbarGroups );
        this.surface.addCommands( this.constructor.static.surfaceCommands );
        if ( !this.isMobileDevice ) {
                this.toolbar.enableFloatable();
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js 
b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
index 5f275aa..9873ccd 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWMediaEditDialog.js
@@ -33,16 +33,6 @@
 
 ve.ui.MWMediaEditDialog.static.icon = 'picture';
 
-ve.ui.MWMediaEditDialog.static.toolbarTools = [
-       { 'items': ['undo', 'redo'] },
-       { 'items': ['bold', 'italic', 'mwLink', 'clear'] },
-       { 'items': ['mwReference', 'mwTransclusion', 'mwMath'] }
-];
-
-ve.ui.MWMediaEditDialog.static.surfaceCommands = [
-       'bold', 'italic', 'mwLink', 'undo', 'redo', 'clear'
-];
-
 /* Methods */
 
 /** */
@@ -98,8 +88,8 @@
                new ve.dm.ElementLinearData( doc.getStore(), data ),
                {
                        '$$': this.frame.$$,
-                       'tools': this.constructor.static.toolbarTools,
-                       'commands': this.constructor.static.surfaceCommands
+                       'tools': ve.init.mw.Target.static.toolbarGroups,
+                       'commands': ve.init.mw.Target.static.surfaceCommands
                }
        );
 
diff --git a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js 
b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
index 9e12c40..93a7363 100644
--- a/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
+++ b/modules/ve-mw/ui/dialogs/ve.ui.MWReferenceDialog.js
@@ -33,15 +33,6 @@
 
 ve.ui.MWReferenceDialog.static.icon = 'reference';
 
-ve.ui.MWReferenceDialog.static.toolbarTools = [
-       { 'items': ['undo', 'redo'] },
-       { 'items': ['bold', 'italic', 'mwLink', 'clear', 'mwMediaInsert', 
'mwTransclusion'] }
-];
-
-ve.ui.MWReferenceDialog.static.surfaceCommands = [
-       'bold', 'italic', 'mwLink', 'undo', 'redo', 'clear'
-];
-
 /* Methods */
 
 /**
@@ -303,8 +294,8 @@
                new ve.dm.ElementLinearData( doc.getStore(), data ),
                {
                        '$$': this.frame.$$,
-                       'tools': this.constructor.static.toolbarTools,
-                       'commands': this.constructor.static.surfaceCommands
+                       'tools': ve.init.mw.Target.static.toolbarGroups,
+                       'commands': ve.init.mw.Target.static.surfaceCommands
                }
        );
 
diff --git a/modules/ve-mw/ui/styles/ve.ui.Tool.css 
b/modules/ve-mw/ui/styles/ve.ui.Tool.css
index 5c40410..5481102 100644
--- a/modules/ve-mw/ui/styles/ve.ui.Tool.css
+++ b/modules/ve-mw/ui/styles/ve.ui.Tool.css
@@ -5,6 +5,6 @@
  * @license The MIT License (MIT); see LICENSE.txt
  */
 
-.ve-ui-dropdownTool-mwFormat ul {
+.ve-ui-mwFormatDropdownTool ul {
        font-size: 80%;
 }
diff --git a/modules/ve-mw/ui/tools/buttons/ve.ui.MWLinkButtonTool.js 
b/modules/ve-mw/ui/tools/buttons/ve.ui.MWLinkButtonTool.js
index 613b8ca..8264a28 100644
--- a/modules/ve-mw/ui/tools/buttons/ve.ui.MWLinkButtonTool.js
+++ b/modules/ve-mw/ui/tools/buttons/ve.ui.MWLinkButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Members */
 
-ve.ui.MWLinkButtonTool.static.name = 'mwLink';
+ve.ui.MWLinkButtonTool.static.name = 'meta/link/mw';
 
 ve.ui.MWLinkButtonTool.static.inspector = 'mwLink';
 
@@ -35,10 +35,10 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwLink', ve.ui.MWLinkButtonTool );
+ve.ui.toolFactory.register( 'meta/link/mw', ve.ui.MWLinkButtonTool );
 
-ve.ui.commandRegistry.register( 'mwLink', 'inspector', 'open', 'mwLink' );
+ve.ui.commandRegistry.register( 'meta/link/mw', 'inspector', 'open', 'mwLink' 
);
 
 ve.ui.triggerRegistry.register(
-       'mwLink', { 'mac': new ve.ui.Trigger( 'cmd+k' ), 'pc': new 
ve.ui.Trigger( 'ctrl+k' ) }
+       'meta/link/mw', { 'mac': new ve.ui.Trigger( 'cmd+k' ), 'pc': new 
ve.ui.Trigger( 'ctrl+k' ) }
 );
diff --git a/modules/ve-mw/ui/tools/buttons/ve.ui.MWMathButtonTool.js 
b/modules/ve-mw/ui/tools/buttons/ve.ui.MWMathButtonTool.js
index c97e212..c4ae5e0 100644
--- a/modules/ve-mw/ui/tools/buttons/ve.ui.MWMathButtonTool.js
+++ b/modules/ve-mw/ui/tools/buttons/ve.ui.MWMathButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.MWMathButtonTool.static.name = 'mwMath';
+ve.ui.MWMathButtonTool.static.name = 'object/math/mw';
 
 ve.ui.MWMathButtonTool.static.icon = 'math';
 
@@ -37,4 +37,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwMath', ve.ui.MWMathButtonTool );
+ve.ui.toolFactory.register( 'object/math/mw', ve.ui.MWMathButtonTool );
diff --git a/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaEditButtonTool.js 
b/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaEditButtonTool.js
index 2e1fa5e..e4802c8 100644
--- a/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaEditButtonTool.js
+++ b/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaEditButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.MWMediaEditButtonTool.static.name = 'mwMediaEdit';
+ve.ui.MWMediaEditButtonTool.static.name = 'object/mediaEdit/mw';
 
 ve.ui.MWMediaEditButtonTool.static.icon = 'picture';
 
@@ -35,6 +35,8 @@
 
 ve.ui.MWMediaEditButtonTool.static.modelClasses = [ ve.dm.MWBlockImageNode ];
 
+ve.ui.MWMediaEditButtonTool.static.autoAdd = false;
+
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwMediaEdit', ve.ui.MWMediaEditButtonTool );
+ve.ui.toolFactory.register( 'object/mediaEdit/mw', ve.ui.MWMediaEditButtonTool 
);
diff --git a/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaInsertButtonTool.js 
b/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaInsertButtonTool.js
index c569fb6..d5a86f7 100644
--- a/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaInsertButtonTool.js
+++ b/modules/ve-mw/ui/tools/buttons/ve.ui.MWMediaInsertButtonTool.js
@@ -26,7 +26,7 @@
 
 /* Static Properties */
 
-ve.ui.MWMediaInsertButtonTool.static.name = 'mwMediaInsert';
+ve.ui.MWMediaInsertButtonTool.static.name = 'object/mediaInsert/mw';
 
 ve.ui.MWMediaInsertButtonTool.static.icon = 'picture';
 
@@ -36,4 +36,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwMediaInsert', ve.ui.MWMediaInsertButtonTool );
+ve.ui.toolFactory.register( 'object/mediaInsert/mw', 
ve.ui.MWMediaInsertButtonTool );
diff --git a/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceButtonTool.js 
b/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceButtonTool.js
index 837b326..b07bbfd 100644
--- a/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceButtonTool.js
+++ b/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceButtonTool.js
@@ -26,7 +26,7 @@
 
 /* Static Properties */
 
-ve.ui.MWReferenceButtonTool.static.name = 'mwReference';
+ve.ui.MWReferenceButtonTool.static.name = 'object/reference/mw';
 
 ve.ui.MWReferenceButtonTool.static.icon = 'reference';
 
@@ -38,4 +38,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwReference', ve.ui.MWReferenceButtonTool );
+ve.ui.toolFactory.register( 'object/reference/mw', ve.ui.MWReferenceButtonTool 
);
diff --git a/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceListButtonTool.js 
b/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceListButtonTool.js
index afe940e..6fbcd36 100644
--- a/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceListButtonTool.js
+++ b/modules/ve-mw/ui/tools/buttons/ve.ui.MWReferenceListButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.MWReferenceListButtonTool.static.name = 'mwReferenceList';
+ve.ui.MWReferenceListButtonTool.static.name = 'object/referenceList/mw';
 
 ve.ui.MWReferenceListButtonTool.static.icon = 'references';
 
@@ -37,4 +37,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwReferenceList', ve.ui.MWReferenceListButtonTool 
);
+ve.ui.toolFactory.register( 'object/referenceList/mw', 
ve.ui.MWReferenceListButtonTool );
diff --git a/modules/ve-mw/ui/tools/buttons/ve.ui.MWTransclusionButtonTool.js 
b/modules/ve-mw/ui/tools/buttons/ve.ui.MWTransclusionButtonTool.js
index f8d3757..8b88888 100644
--- a/modules/ve-mw/ui/tools/buttons/ve.ui.MWTransclusionButtonTool.js
+++ b/modules/ve-mw/ui/tools/buttons/ve.ui.MWTransclusionButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.MWTransclusionButtonTool.static.name = 'mwTransclusion';
+ve.ui.MWTransclusionButtonTool.static.name = 'object/transclusion/mw';
 
 ve.ui.MWTransclusionButtonTool.static.icon = 'template';
 
@@ -37,4 +37,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwTransclusion', ve.ui.MWTransclusionButtonTool );
+ve.ui.toolFactory.register( 'object/transclusion/mw', 
ve.ui.MWTransclusionButtonTool );
diff --git a/modules/ve-mw/ui/tools/dropdowns/ve.ui.MWFormatDropdownTool.js 
b/modules/ve-mw/ui/tools/dropdowns/ve.ui.MWFormatDropdownTool.js
index 504212e..86751c5 100644
--- a/modules/ve-mw/ui/tools/dropdowns/ve.ui.MWFormatDropdownTool.js
+++ b/modules/ve-mw/ui/tools/dropdowns/ve.ui.MWFormatDropdownTool.js
@@ -16,6 +16,9 @@
 ve.ui.MWFormatDropdownTool = function VeUiMwFormatDropdownTool( toolbar, 
config ) {
        // Parent constructor
        ve.ui.FormatDropdownTool.call( this, toolbar, config );
+
+       // Initialize
+       this.$.addClass( 've-ui-mwFormatDropdownTool' );
 };
 
 /* Inheritance */
@@ -24,9 +27,7 @@
 
 /* Static Properties */
 
-ve.ui.MWFormatDropdownTool.static.name = 'mwFormat';
-
-ve.ui.MWFormatDropdownTool.static.cssName = 'format 
ve-ui-dropdownTool-mwFormat';
+ve.ui.MWFormatDropdownTool.static.name = 'format/convert/mw';
 
 ve.ui.MWFormatDropdownTool.static.items[1].data.type = 'mwHeading';
 ve.ui.MWFormatDropdownTool.static.items[1].label = 
'visualeditor-formatdropdown-format-mw-heading1';
@@ -42,9 +43,11 @@
 ve.ui.MWFormatDropdownTool.static.items[6].label = 
'visualeditor-formatdropdown-format-mw-heading6';
 ve.ui.MWFormatDropdownTool.static.items[7].data.type = 'mwPreformatted';
 
-// Move the H1 (item 1 in the list) to the end (7) so as to make it less 
prominent and tempting to users
-ve.ui.MWFormatDropdownTool.static.items.splice( 7, 0, 
ve.ui.MWFormatDropdownTool.static.items.splice( 1, 1 )[0] );
+// Move H1 (index 0) to the end (index 7) so as to make it less prominent and 
tempting to users
+ve.ui.MWFormatDropdownTool.static.items.splice(
+       7, 0, ve.ui.MWFormatDropdownTool.static.items.splice( 1, 1 )[0]
+);
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'mwFormat', ve.ui.MWFormatDropdownTool );
+ve.ui.toolFactory.register( 'format/convert/mw', ve.ui.MWFormatDropdownTool );
diff --git a/modules/ve/init/sa/ve.init.sa.Target.js 
b/modules/ve/init/sa/ve.init.sa.Target.js
index ca4b965..c697dfb 100644
--- a/modules/ve/init/sa/ve.init.sa.Target.js
+++ b/modules/ve/init/sa/ve.init.sa.Target.js
@@ -30,7 +30,7 @@
 
        // Initialization
        this.toolbar.$.addClass( 've-init-sa-target-toolbar' );
-       this.toolbar.setup( this.constructor.static.toolbarTools );
+       this.toolbar.setup( this.constructor.static.toolbarGroups );
        this.toolbar.enableFloatable();
 
        this.$.append( this.toolbar.$, this.surface.$ );
diff --git a/modules/ve/init/ve.init.Target.js 
b/modules/ve/init/ve.init.Target.js
index 54db1d6..3459fb7 100644
--- a/modules/ve/init/ve.init.Target.js
+++ b/modules/ve/init/ve.init.Target.js
@@ -29,20 +29,46 @@
 
 /* Static Properties */
 
-ve.init.Target.static.toolbarTools = [
-       { 'items': ['undo', 'redo'] },
-       { 'items': ['format'] },
-       { 'items': ['bold', 'italic', 'link', 'code', 'language', 'clear'] },
-       { 'items': ['number', 'bullet', 'outdent', 'indent'] }
+ve.init.Target.static.toolbarGroups = [
+       {
+               'include': [ 'history' ],
+               'promote': [
+                       'history/undo',
+                       'history/redo'
+               ]
+       },
+       {
+               'include': [ 'format' ],
+               'promote': [ 'format/convert' ]
+       },
+       {
+               'include': [ 'textStyle', 'meta', 'utility/clear' ],
+               'promote': [
+                       'textStyle/bold',
+                       'textStyle/italic',
+                       'meta/link'
+               ],
+               'demote': [ 'utility/clear' ]
+       },
+       {
+               'include': [ 'structure' ],
+               'promote': [
+                       'structure/number',
+                       'structure/bullet',
+                       'structure/outdent',
+                       'structure/indent'
+               ]
+       },
+       { 'include': [ 'object' ] }
 ];
 
 ve.init.Target.static.surfaceCommands = [
-       'bold',
-       'italic',
-       'link',
-       //'language',
-       'undo',
-       'redo',
-       'indent',
-       'outdent'
+       'history/undo',
+       'history/redo',
+       'textStyle/bold',
+       'textStyle/italic',
+       'meta/link',
+       'utility/clear',
+       'structure/indent',
+       'structure/outdent'
 ];
diff --git a/modules/ve/ui/styles/ve.ui.Tool.css 
b/modules/ve/ui/styles/ve.ui.Tool.css
index ac83650..61c1f6a 100644
--- a/modules/ve/ui/styles/ve.ui.Tool.css
+++ b/modules/ve/ui/styles/ve.ui.Tool.css
@@ -32,6 +32,11 @@
        vertical-align: middle;
        border-radius: 0.25em;
        border: solid 1px transparent;
+       -webkit-transition: border-color 300ms;
+       -moz-transition: border-color 300ms;
+       -ms-transition: border-color 300ms;
+       -o-transition: border-color 300ms;
+       transition: border-color 300ms;
 }
 
 .ve-ui-toolGroup:hover {
@@ -148,45 +153,45 @@
 
 /* ve.ui.FormatDropdownTool */
 
-.ve-ui-dropdownTool-format {
+.ve-ui-formatDropdownTool {
        width: 8em;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="paragraph"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="paragraph"] 
.ve-ui-labeledElement-label {
        font-weight: normal;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="heading-1"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="heading-1"] 
.ve-ui-labeledElement-label {
        font-size: 188%;
        font-weight: normal;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="heading-2"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="heading-2"] 
.ve-ui-labeledElement-label {
        font-size: 150%;
        font-weight: normal;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="heading-3"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="heading-3"] 
.ve-ui-labeledElement-label {
        font-size: 132%;
        font-weight: bold;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="heading-4"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="heading-4"] 
.ve-ui-labeledElement-label {
        font-size: 116%;
        font-weight: bold;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="heading-5"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="heading-5"] 
.ve-ui-labeledElement-label {
        font-size: 100%;
        font-weight: bold;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="heading-6"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="heading-6"] 
.ve-ui-labeledElement-label {
        font-size: 80%;
        font-weight: bold;
 }
 
-.ve-ui-dropdownTool-format .ve-ui-optionWidget[rel="preformatted"] 
.ve-ui-labeledElement-label {
+.ve-ui-formatDropdownTool .ve-ui-optionWidget[rel="preformatted"] 
.ve-ui-labeledElement-label {
        font-family: monospace, "Courier New";
 }
 
diff --git a/modules/ve/ui/tools/buttons/ve.ui.BoldButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.BoldButtonTool.js
index cd6b950..0ef6570 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.BoldButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.BoldButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.BoldButtonTool.static.name = 'bold';
+ve.ui.BoldButtonTool.static.name = 'textStyle/bold';
 
 ve.ui.BoldButtonTool.static.icon = {
        'default': 'bold-a',
@@ -61,10 +61,11 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'bold', ve.ui.BoldButtonTool );
+ve.ui.toolFactory.register( 'textStyle/bold', ve.ui.BoldButtonTool );
 
-ve.ui.commandRegistry.register( 'bold', 'annotation', 'toggle', 
'textStyle/bold' );
+ve.ui.commandRegistry.register( 'textStyle/bold', 'annotation', 'toggle', 
'textStyle/bold' );
 
 ve.ui.triggerRegistry.register(
-       'bold', { 'mac': new ve.ui.Trigger( 'cmd+b' ), 'pc': new ve.ui.Trigger( 
'ctrl+b' ) }
+       'textStyle/bold',
+       { 'mac': new ve.ui.Trigger( 'cmd+b' ), 'pc': new ve.ui.Trigger( 
'ctrl+b' ) }
 );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.BulletButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.BulletButtonTool.js
index ea20ec1..07ca8d8 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.BulletButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.BulletButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.BulletButtonTool.static.name = 'bullet';
+ve.ui.BulletButtonTool.static.name = 'structure/bullet';
 
 ve.ui.BulletButtonTool.static.icon = 'bullet-list';
 
@@ -35,4 +35,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'bullet', ve.ui.BulletButtonTool );
+ve.ui.toolFactory.register( 'structure/bullet', ve.ui.BulletButtonTool );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.ClearButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.ClearButtonTool.js
index 763c3ff..451a4ff 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.ClearButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.ClearButtonTool.js
@@ -28,7 +28,7 @@
 
 /* Static Properties */
 
-ve.ui.ClearButtonTool.static.name = 'clear';
+ve.ui.ClearButtonTool.static.name = 'utility/clear';
 
 ve.ui.ClearButtonTool.static.icon = 'clear';
 
@@ -59,10 +59,11 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'clear', ve.ui.ClearButtonTool );
+ve.ui.toolFactory.register( 'utility/clear', ve.ui.ClearButtonTool );
 
-ve.ui.commandRegistry.register( 'clear', 'annotation', 'clearAll' );
+ve.ui.commandRegistry.register( 'utility/clear', 'annotation', 'clearAll' );
 
 ve.ui.triggerRegistry.register(
-       'clear', { 'mac': new ve.ui.Trigger( 'cmd+\\' ), 'pc': new 
ve.ui.Trigger( 'ctrl+\\' ) }
+       'utility/clear',
+       { 'mac': new ve.ui.Trigger( 'cmd+\\' ), 'pc': new ve.ui.Trigger( 
'ctrl+\\' ) }
 );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.CodeButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.CodeButtonTool.js
index ca462cc..a8a8eef 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.CodeButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.CodeButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.CodeButtonTool.static.name = 'code';
+ve.ui.CodeButtonTool.static.name = 'textStyle/code';
 
 ve.ui.CodeButtonTool.static.icon = 'code';
 
@@ -35,6 +35,6 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'code', ve.ui.CodeButtonTool );
+ve.ui.toolFactory.register( 'textStyle/code', ve.ui.CodeButtonTool );
 
-ve.ui.commandRegistry.register( 'code', 'annotation', 'toggle', 
'textStyle/code' );
+ve.ui.commandRegistry.register( 'textStyle/code', 'annotation', 'toggle', 
'textStyle/code' );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.IndentButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.IndentButtonTool.js
index 43018ea..1c35dd6 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.IndentButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.IndentButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.IndentButtonTool.static.name = 'indent';
+ve.ui.IndentButtonTool.static.name = 'structure/indent';
 
 ve.ui.IndentButtonTool.static.icon = 'indent-list';
 
@@ -35,8 +35,8 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'indent', ve.ui.IndentButtonTool );
+ve.ui.toolFactory.register( 'structure/indent', ve.ui.IndentButtonTool );
 
-ve.ui.commandRegistry.register( 'indent', 'indentation', 'increase' );
+ve.ui.commandRegistry.register( 'structure/indent', 'indentation', 'increase' 
);
 
-ve.ui.triggerRegistry.register( 'indent', new ve.ui.Trigger( 'tab' ) );
+ve.ui.triggerRegistry.register( 'structure/indent', new ve.ui.Trigger( 'tab' ) 
);
diff --git a/modules/ve/ui/tools/buttons/ve.ui.ItalicButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.ItalicButtonTool.js
index adcc886..1b016ed 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.ItalicButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.ItalicButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.ItalicButtonTool.static.name = 'italic';
+ve.ui.ItalicButtonTool.static.name = 'textStyle/italic';
 
 ve.ui.ItalicButtonTool.static.icon = {
        'default': 'italic-a',
@@ -61,12 +61,11 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'italic', ve.ui.ItalicButtonTool );
+ve.ui.toolFactory.register( 'textStyle/italic', ve.ui.ItalicButtonTool );
 
-ve.ui.commandRegistry.register(
-       'italic', 'annotation', 'toggle', 'textStyle/italic'
-);
+ve.ui.commandRegistry.register( 'textStyle/italic', 'annotation', 'toggle', 
'textStyle/italic' );
 
 ve.ui.triggerRegistry.register(
-       'italic', { 'mac': new ve.ui.Trigger( 'cmd+i' ), 'pc': new 
ve.ui.Trigger( 'ctrl+i' ) }
+       'textStyle/italic',
+       { 'mac': new ve.ui.Trigger( 'cmd+i' ), 'pc': new ve.ui.Trigger( 
'ctrl+i' ) }
 );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.LanguageButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.LanguageButtonTool.js
index b9397ff..53861eb 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.LanguageButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.LanguageButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.LanguageButtonTool.static.name = 'language';
+ve.ui.LanguageButtonTool.static.name = 'meta/language';
 
 ve.ui.LanguageButtonTool.static.icon = 'language';
 
@@ -37,6 +37,6 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'language', ve.ui.LanguageButtonTool );
+ve.ui.toolFactory.register( 'meta/language', ve.ui.LanguageButtonTool );
 
-ve.ui.commandRegistry.register( 'language', 'inspector', 'open', 'language' );
\ No newline at end of file
+ve.ui.commandRegistry.register( 'meta/language', 'inspector', 'open', 
'language' );
\ No newline at end of file
diff --git a/modules/ve/ui/tools/buttons/ve.ui.LinkButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.LinkButtonTool.js
index 4aa17d9..4f8af59 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.LinkButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.LinkButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.LinkButtonTool.static.name = 'link';
+ve.ui.LinkButtonTool.static.name = 'meta/link';
 
 ve.ui.LinkButtonTool.static.icon = 'link';
 
@@ -37,10 +37,11 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'link', ve.ui.LinkButtonTool );
+ve.ui.toolFactory.register( 'meta/link', ve.ui.LinkButtonTool );
 
-ve.ui.commandRegistry.register( 'link', 'inspector', 'open', 'link' );
+ve.ui.commandRegistry.register( 'meta/link', 'inspector', 'open', 'link' );
 
 ve.ui.triggerRegistry.register(
-       'link', { 'mac': new ve.ui.Trigger( 'cmd+k' ), 'pc': new ve.ui.Trigger( 
'ctrl+k' ) }
+       'meta/link',
+       { 'mac': new ve.ui.Trigger( 'cmd+k' ), 'pc': new ve.ui.Trigger( 
'ctrl+k' ) }
 );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.NumberButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.NumberButtonTool.js
index 9d49af2..9ec2d98 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.NumberButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.NumberButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.NumberButtonTool.static.name = 'number';
+ve.ui.NumberButtonTool.static.name = 'structure/number';
 
 ve.ui.NumberButtonTool.static.icon = 'number-list';
 
@@ -35,4 +35,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'number', ve.ui.NumberButtonTool );
+ve.ui.toolFactory.register( 'structure/number', ve.ui.NumberButtonTool );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.OutdentButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.OutdentButtonTool.js
index b8b093e..ac13547 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.OutdentButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.OutdentButtonTool.js
@@ -25,7 +25,7 @@
 
 /* Static Properties */
 
-ve.ui.OutdentButtonTool.static.name = 'outdent';
+ve.ui.OutdentButtonTool.static.name = 'structure/outdent';
 
 ve.ui.OutdentButtonTool.static.icon = 'outdent-list';
 
@@ -35,9 +35,9 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'outdent', ve.ui.OutdentButtonTool );
+ve.ui.toolFactory.register( 'structure/outdent', ve.ui.OutdentButtonTool );
 
 // TODO: Consistency between outdent and unindent.
-ve.ui.commandRegistry.register( 'outdent', 'indentation', 'decrease' );
+ve.ui.commandRegistry.register( 'structure/outdent', 'indentation', 'decrease' 
);
 
-ve.ui.triggerRegistry.register( 'outdent', new ve.ui.Trigger( 'shift+tab' ) );
+ve.ui.triggerRegistry.register( 'structure/outdent', new ve.ui.Trigger( 
'shift+tab' ) );
diff --git a/modules/ve/ui/tools/buttons/ve.ui.RedoButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.RedoButtonTool.js
index 8cee7ed..10941fd 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.RedoButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.RedoButtonTool.js
@@ -31,7 +31,7 @@
 
 /* Static Properties */
 
-ve.ui.RedoButtonTool.static.name = 'redo';
+ve.ui.RedoButtonTool.static.name = 'history/redo';
 
 ve.ui.RedoButtonTool.static.icon = 'redo';
 
@@ -62,13 +62,11 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'redo', ve.ui.RedoButtonTool );
+ve.ui.toolFactory.register( 'history/redo', ve.ui.RedoButtonTool );
 
-ve.ui.commandRegistry.register(
-       'redo', 'history', 'redo'
-);
+ve.ui.commandRegistry.register( 'history/redo', 'history', 'redo' );
 
 ve.ui.triggerRegistry.register(
-       'redo', { 'mac': new ve.ui.Trigger( 'cmd+shift+z' ), 'pc': new 
ve.ui.Trigger( 'ctrl+shift+z' ) }
+       'history/redo',
+       { 'mac': new ve.ui.Trigger( 'cmd+shift+z' ), 'pc': new ve.ui.Trigger( 
'ctrl+shift+z' ) }
 );
-
diff --git a/modules/ve/ui/tools/buttons/ve.ui.UndoButtonTool.js 
b/modules/ve/ui/tools/buttons/ve.ui.UndoButtonTool.js
index 21cb027..5865eae 100644
--- a/modules/ve/ui/tools/buttons/ve.ui.UndoButtonTool.js
+++ b/modules/ve/ui/tools/buttons/ve.ui.UndoButtonTool.js
@@ -31,7 +31,7 @@
 
 /* Static Properties */
 
-ve.ui.UndoButtonTool.static.name = 'undo';
+ve.ui.UndoButtonTool.static.name = 'history/undo';
 
 ve.ui.UndoButtonTool.static.icon = 'undo';
 
@@ -62,13 +62,11 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'undo', ve.ui.UndoButtonTool );
+ve.ui.toolFactory.register( 'history/undo', ve.ui.UndoButtonTool );
 
-ve.ui.commandRegistry.register(
-       'undo', 'history', 'undo'
-);
+ve.ui.commandRegistry.register( 'history/undo', 'history', 'undo' );
 
 ve.ui.triggerRegistry.register(
-       'undo', { 'mac': new ve.ui.Trigger( 'cmd+z' ), 'pc': new ve.ui.Trigger( 
'ctrl+z' ) }
+       'history/undo',
+       { 'mac': new ve.ui.Trigger( 'cmd+z' ), 'pc': new ve.ui.Trigger( 
'ctrl+z' ) }
 );
-
diff --git a/modules/ve/ui/tools/dropdowns/ve.ui.FormatDropdownTool.js 
b/modules/ve/ui/tools/dropdowns/ve.ui.FormatDropdownTool.js
index fae97e8..1ba1342 100644
--- a/modules/ve/ui/tools/dropdowns/ve.ui.FormatDropdownTool.js
+++ b/modules/ve/ui/tools/dropdowns/ve.ui.FormatDropdownTool.js
@@ -22,6 +22,7 @@
        ve.ui.DropdownTool.call( this, toolbar, config );
 
        // Initialization
+       this.$.addClass( 've-ui-formatDropdownTool' );
        for ( i = 0, len = items.length; i < len; i++ ) {
                item = items[i];
                items[i] = new ve.ui.MenuItemWidget(
@@ -37,7 +38,7 @@
 
 /* Static Properties */
 
-ve.ui.FormatDropdownTool.static.name = 'format';
+ve.ui.FormatDropdownTool.static.name = 'format/convert';
 
 ve.ui.FormatDropdownTool.static.titleMessage = 
'visualeditor-formatdropdown-title';
 
@@ -174,4 +175,4 @@
 
 /* Registration */
 
-ve.ui.toolFactory.register( 'format', ve.ui.FormatDropdownTool );
+ve.ui.toolFactory.register( 'format/convert', ve.ui.FormatDropdownTool );
diff --git a/modules/ve/ui/tools/ve.ui.DropdownTool.js 
b/modules/ve/ui/tools/ve.ui.DropdownTool.js
index 89e3377..47e5deb 100644
--- a/modules/ve/ui/tools/ve.ui.DropdownTool.js
+++ b/modules/ve/ui/tools/ve.ui.DropdownTool.js
@@ -36,10 +36,7 @@
        // Initialization
        this.$
                .append( this.$icon, this.$label, this.menu.$ )
-               .addClass(
-                       've-ui-dropdownTool ve-ui-dropdownTool-' +
-                       ( this.constructor.static.cssName || 
this.constructor.static.name )
-               );
+               .addClass( 've-ui-dropdownTool' );
        if ( this.constructor.static.titleMessage ) {
                this.$.attr( 'title', ve.msg( 
this.constructor.static.titleMessage ) );
        }
diff --git a/modules/ve/ui/ve.ui.Context.js b/modules/ve/ui/ve.ui.Context.js
index bfb96f8..5e6e454 100644
--- a/modules/ve/ui/ve.ui.Context.js
+++ b/modules/ve/ui/ve.ui.Context.js
@@ -222,7 +222,7 @@
                                this.toolbar.destroy();
                        }
                        this.toolbar = new ve.ui.SurfaceToolbar( this.surface );
-                       this.toolbar.setup( [ { 'items' : tools } ] );
+                       this.toolbar.setup( [ { 'include' : tools } ] );
                        this.$menu.append( this.toolbar.$ );
                        this.show();
                        this.toolbar.initialize();
diff --git a/modules/ve/ui/ve.ui.Tool.js b/modules/ve/ui/ve.ui.Tool.js
index 5ae3ab5..3ac601d 100644
--- a/modules/ve/ui/ve.ui.Tool.js
+++ b/modules/ve/ui/ve.ui.Tool.js
@@ -48,17 +48,6 @@
 ve.ui.Tool.static.name = '';
 
 /**
- * CSS class name, rendered as ve-ui-dropdownTool-cssName
- *
- * If this is left as null, static.name is used instead.
- *
- * @abstract
- * @static
- * @property {string}
- */
-ve.ui.Tool.static.cssName = null;
-
-/**
  * Message key for tool title.
  *
  * @abstract
@@ -66,6 +55,14 @@
  * @property {string}
  */
 ve.ui.Tool.static.titleMessage = null;
+
+/**
+ * Tool should be automatically added to toolbars.
+ *
+ * @static
+ * @property {boolean}
+ */
+ve.ui.Tool.static.autoAdd = true;
 
 /**
  * Check if this tool can be used on a model.
@@ -116,7 +113,6 @@
  * Combines trigger i18n with tooltip message if trigger exists.
  * Otherwise defaults to titleMessage value.
  *
- * @abstract
  * @method
  * @chainable
  */
@@ -131,3 +127,14 @@
        this.$.attr( 'title', labelText );
        return this;
 };
+
+/**
+ * Destory tool.
+ *
+ * @method
+ */
+ve.ui.Tool.prototype.destroy = function () {
+       this.toolbar.disconnect( this );
+       ve.ui.triggerRegistry.disconnect( this );
+       this.$.remove();
+};
diff --git a/modules/ve/ui/ve.ui.ToolFactory.js 
b/modules/ve/ui/ve.ui.ToolFactory.js
index dba608b..cd9e747 100644
--- a/modules/ve/ui/ve.ui.ToolFactory.js
+++ b/modules/ve/ui/ve.ui.ToolFactory.js
@@ -15,6 +15,9 @@
 ve.ui.ToolFactory = function VeUiToolFactory() {
        // Parent constructor
        ve.Factory.call( this );
+
+       // Properties
+       this.tools = {};
 };
 
 /* Inheritance */
@@ -24,6 +27,110 @@
 /* Methods */
 
 /**
+ * @inheritdoc
+ */
+ve.ui.ToolFactory.prototype.register = function ( name, constructor ) {
+       var parts = name.split( '/' ),
+               baseName = parts.slice( 0, 2 ).join( '/' );
+
+       if (
+               // First entry
+               !this.tools[baseName] ||
+               // Overriding entry
+               constructor.prototype instanceof 
this.registry[this.tools[baseName].name]
+       ) {
+               this.tools[baseName] = {
+                       'name': name,
+                       'type': parts[0],
+                       'id': parts[1],
+                       'ext': parts[2]
+               };
+       }
+
+       ve.Factory.prototype.register.call( this, name, constructor );
+};
+
+ve.ui.ToolFactory.prototype.getTools = function ( include, exclude, promote, 
demote ) {
+       var i, len, tool, parts, baseName,
+               tools = {},
+               promoted = [],
+               demoted = [],
+               auto = [];
+
+       // Collect included tools
+       for ( i = 0, len = include.length; i < len; i++ ) {
+               parts = include[i].split( '/' );
+               for ( baseName in this.tools ) {
+                       tool = this.tools[baseName];
+                       if (
+                               // Types match
+                               parts[0] === tool.type &&
+                               // Either no ID was specified and tool can be 
automatically added or IDs matche
+                               ( ( !parts[1] && 
this.registry[tool.name].static.autoAdd ) || parts[1] === tool.id )
+                       ) {
+                               tools[baseName] = tool;
+                       }
+
+               }
+       }
+
+       // Remove excluded tools
+       for ( i = 0, len = exclude.length; i < len; i++ ) {
+               parts = exclude[i].split( '/' );
+               for ( baseName in tools ) {
+                       tool = tools[baseName];
+                       if (
+                               // Types match
+                               parts[0] === tool.type &&
+                               // Either no ID was specified or IDs match
+                               ( !parts[1] || parts[1] === tool.id )
+                       ) {
+                               delete tools[baseName];
+                       }
+               }
+       }
+
+       // Promotion
+       for ( i = 0, len = promote.length; i < len; i++ ) {
+               parts = promote[i].split( '/' );
+               for ( baseName in tools ) {
+                       tool = tools[baseName];
+                       if (
+                               // Types match
+                               parts[0] === tool.type &&
+                               // Either no ID was specified or IDs match
+                               ( !parts[1] || parts[1] === tool.id )
+                       ) {
+                               promoted.push( tool.name );
+                               delete tools[baseName];
+                       }
+               }
+       }
+
+       for ( i = 0, len = demote.length; i < len; i++ ) {
+               parts = demote[i].split( '/' );
+               for ( baseName in tools ) {
+                       tool = tools[baseName];
+                       if (
+                               // Types match
+                               parts[0] === tool.type &&
+                               // Either no ID was specified or IDs match
+                               ( !parts[1] || parts[1] === tool.id )
+                       ) {
+                               demoted.push( tool.name );
+                               delete tools[baseName];
+                       }
+               }
+       }
+
+       for ( baseName in tools ) {
+               auto.push( tools[baseName].name );
+       }
+
+       return promoted.concat( auto.sort() ).concat( demoted );
+};
+
+/**
  * Get a list of tools from a set of annotations.
  *
  * The most specific tool will be chosen based on inheritance - mostly. The 
order of being added
diff --git a/modules/ve/ui/ve.ui.ToolGroup.js b/modules/ve/ui/ve.ui.ToolGroup.js
index 707887a..7835031 100644
--- a/modules/ve/ui/ve.ui.ToolGroup.js
+++ b/modules/ve/ui/ve.ui.ToolGroup.js
@@ -13,11 +13,22 @@
  * @extends ve.ui.Widget
  * @mixins ve.ui.GroupElement
  *
+ * Patterns can be either:
+ *  - All tools in a category: 'category'
+ *  - A specific tool: 'category/name'
+ *
  * @constructor
  * @param {ve.ui.Toolbar} toolbar
  * @param {Object} [config] Config options
+ * @param {string[]} [include=[]] Patterns of tools to automatically include
+ * @param {string[]} [exclude=[]] Patterns of tools to automatically exclude
+ * @param {string[]} [promote=[]] Patterns of tools to promote to the beginning
+ * @param {string[]} [demote=[]] Patterns of tools to demote to the end
  */
 ve.ui.ToolGroup = function VeUiToolGroup( toolbar, config ) {
+       // Configuration initialization
+       config = config || {};
+
        // Parent constructor
        ve.ui.Widget.call( this, config );
 
@@ -26,12 +37,19 @@
 
        // Properties
        this.toolbar = toolbar;
+       this.tools = {};
+       this.include = config.include || [];
+       this.exclude = config.exclude || [];
+       this.promote = config.promote || [];
+       this.demote = config.demote || [];
 
        // Events
        this.$.on( { 'mousedown': false } );
+       ve.ui.toolFactory.connect( this, { 'register': 'ontoolFactoryRegister' 
} );
 
        // Initialization
        this.$.addClass( 've-ui-toolGroup' );
+       this.populateTools();
 };
 
 /* Inheritance */
@@ -39,3 +57,60 @@
 ve.inheritClass( ve.ui.ToolGroup, ve.ui.Widget );
 
 ve.mixinClass( ve.ui.ToolGroup, ve.ui.GroupElement );
+
+/* Methods */
+
+/**
+ * Handle tool registry register events.
+ *
+ * If a tool is registered after the group is created, this handler will 
ensure the tool is included
+ * as if it were present at the time of the group being created.
+ *
+ * @param {string} name Symbolic name of tool
+ */
+ve.ui.ToolGroup.prototype.ontoolFactoryRegister = function () {
+       this.populateTools();
+};
+
+ve.ui.ToolGroup.prototype.populateTools = function () {
+       var i, len, name, tool,
+               names = {},
+               tools = [],
+               list = ve.ui.toolFactory.getTools(
+                       this.include, this.exclude, this.promote, this.demote
+               );
+
+       // Build a list of needed tools
+       for ( i = 0, len = list.length; i < len; i++ ) {
+               name = list[i];
+               tool = this.tools[name];
+               if ( !tool ) {
+                       // Auto-initialize tools on 
+                       tool = ve.ui.toolFactory.create( name, this.toolbar );
+                       this.tools[name] = tool;
+               }
+               tools.push( tool );
+               names[name] = true;
+       }
+       // Remove tools that are no longer needed
+       for ( name in this.tools ) {
+               if ( !names[name] ) {
+                       this.tools[name].destroy();
+                       this.removeItem( this.tools[name] );
+                       delete this.tools[name];
+               }
+       }
+       // Re-add tools (moving existing ones to new locations)
+       this.addItems( tools );
+};
+
+ve.ui.ToolGroup.prototype.destroy = function () {
+       var name;
+
+       this.clearItems();
+       ve.ui.toolFactory.disconnect( this );
+       for ( name in this.tools ) {
+               this.tools[name].destroy();
+       }
+       this.$.remove();
+};
diff --git a/modules/ve/ui/ve.ui.Toolbar.js b/modules/ve/ui/ve.ui.Toolbar.js
index 9b7be98..98cfd6b 100644
--- a/modules/ve/ui/ve.ui.Toolbar.js
+++ b/modules/ve/ui/ve.ui.Toolbar.js
@@ -11,6 +11,7 @@
  * @class
  * @extends ve.Element
  * @mixins ve.EventEmitter
+ * @mixins ve.ui.GroupElement
  *
  * @constructor
  * @param {Object} [config] Config options
@@ -26,21 +27,22 @@
 
        // Mixin constructors
        ve.EventEmitter.call( this );
+       ve.ui.GroupElement.call( this, this.$$( '<div>' ) );
 
        // Properties
+       this.groups = [];
        this.$bar = this.$$( '<div>' );
-       this.$tools = this.$$( '<div>' );
        this.$actions = this.$$( '<div>' );
        this.initialized = false;
 
        // Events
        this.$
-               .add( this.$bar ).add( this.$tools ).add( this.$actions )
+               .add( this.$bar ).add( this.$group ).add( this.$actions )
                .on( 'mousedown', false );
 
        // Initialization
-       this.$tools.addClass( 've-ui-toolbar-tools' );
-       this.$bar.addClass( 've-ui-toolbar-bar' ).append( this.$tools );
+       this.$group.addClass( 've-ui-toolbar-tools' );
+       this.$bar.addClass( 've-ui-toolbar-bar' ).append( this.$group );
        if ( options.actions ) {
                this.$actions.addClass( 've-ui-toolbar-actions' );
                this.$bar.append( this.$actions );
@@ -57,34 +59,9 @@
 ve.inheritClass( ve.ui.Toolbar, ve.Element );
 
 ve.mixinClass( ve.ui.Toolbar, ve.EventEmitter );
+ve.mixinClass( ve.ui.Toolbar, ve.ui.GroupElement );
 
 /* Methods */
-
-/**
- * Initialize all tools and groups.
- *
- * @method
- * @param {Object[]} config List of tool group configurations
- */
-ve.ui.Toolbar.prototype.setup = function ( config ) {
-       var i, j, group, tools;
-
-       for ( i = 0; i < config.length; i++ ) {
-               tools = config[i].items;
-               group = new ve.ui.ToolGroup( this, { '$$': this.$$ } );
-
-               // Add tools
-               for ( j = 0; j < tools.length; j++ ) {
-                       try {
-                               tools[j] = ve.ui.toolFactory.create( tools[j], 
this );
-                       } catch( e ) {}
-               }
-               group.addItems( tools );
-
-               // Append group
-               this.$tools.append( group.$ );
-       }
-};
 
 /**
  * Sets up handles and preloads required information for the toolbar to work.
@@ -95,10 +72,32 @@
 };
 
 /**
+ * Setup toolbar.
+ *
+ * @method
+ * @param {Object[]} groups List of tool group configurations
+ */
+ve.ui.Toolbar.prototype.setup = function ( groups ) {
+       var i, len,
+               items = [];
+
+       for ( i = 0, len = groups.length; i < len; i++ ) {
+               items.push( new ve.ui.ToolGroup( this, ve.extendObject( { '$$': 
this.$$ }, groups[i] ) ) );
+       }
+       this.addItems( items );
+};
+
+/**
  * Destroys toolbar, removing event handlers and DOM elements.
  *
  * Call this whenever you are done using a toolbar.
  */
 ve.ui.Toolbar.prototype.destroy = function () {
+       var i, len;
+
+       this.clearItems();
+       for ( i = 0, len = this.items.length; i < len; i++ ) {
+               this.items[i].destroy();
+       }
        this.$.remove();
 };

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

Gerrit-MessageType: newchange
Gerrit-Change-Id: I7625f861435a99ce3d7a2b1ece9731aaab1776f8
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/VisualEditor
Gerrit-Branch: master
Gerrit-Owner: Trevor Parscal <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to