diff --git a/core/block_render_svg_horizontal.js b/core/block_render_svg_horizontal.js
index b9181f6e..51962a03 100644
--- a/core/block_render_svg_horizontal.js
+++ b/core/block_render_svg_horizontal.js
@@ -300,7 +300,7 @@ Blockly.BlockSvg.metricsAreEquivalent_ = function(first, second) {
  * Play some UI effects (sound) after a connection has been established.
  */
 Blockly.BlockSvg.prototype.connectionUiEffect = function() {
-  this.workspace.playAudio('click');
+  this.workspace.getAudioManager().play('click');
 };
 
 /**
diff --git a/core/block_svg.js b/core/block_svg.js
index 75a479dc..1a4040aa 100644
--- a/core/block_svg.js
+++ b/core/block_svg.js
@@ -28,6 +28,7 @@ goog.provide('Blockly.BlockSvg');
 
 goog.require('Blockly.Block');
 goog.require('Blockly.ContextMenu');
+goog.require('Blockly.Grid');
 goog.require('Blockly.RenderedConnection');
 goog.require('Blockly.Touch');
 goog.require('Blockly.utils');
@@ -482,11 +483,11 @@ Blockly.BlockSvg.prototype.snapToGrid = function() {
   if (this.isInFlyout) {
     return;  // Don't move blocks around in a flyout.
   }
-  if (!this.workspace.options.gridOptions ||
-      !this.workspace.options.gridOptions['snap']) {
+  var grid = this.workspace.getGrid();
+  if (!grid || !grid.shouldSnap()) {
     return;  // Config says no snapping.
   }
-  var spacing = this.workspace.options.gridOptions['spacing'];
+  var spacing = grid.getSpacing();
   var half = spacing / 2;
   var xy = this.getRelativeToSurfaceXY();
   var dx = Math.round((xy.x - half) / spacing) * spacing + half - xy.x;
@@ -910,7 +911,7 @@ Blockly.BlockSvg.prototype.dispose = function(healStack, animate) {
  * Play some UI effects (sound, animation) when disposing of a block.
  */
 Blockly.BlockSvg.prototype.disposeUiEffect = function() {
-  this.workspace.playAudio('delete');
+  this.workspace.getAudioManager().play('delete');
 
   var xy = this.workspace.getSvgXY(/** @type {!Element} */ (this.svgGroup_));
   // Deeply clone the current block.
@@ -930,7 +931,7 @@ Blockly.BlockSvg.prototype.disposeUiEffect = function() {
  * Play some UI effects (sound) after a connection has been established.
  */
 Blockly.BlockSvg.prototype.connectionUiEffect = function() {
-  this.workspace.playAudio('click');
+  this.workspace.getAudioManager().play('click');
 };
 
 /**
diff --git a/core/constants.js b/core/constants.js
index 97f659f1..323900ee 100644
--- a/core/constants.js
+++ b/core/constants.js
@@ -310,3 +310,19 @@ Blockly.VARIABLE_CATEGORY_NAME = 'VARIABLE';
  * @const {string}
  */
 Blockly.PROCEDURE_CATEGORY_NAME = 'PROCEDURE';
+
+/**
+ * String for use in the dropdown created in field_variable.
+ * This string indicates that this option in the dropdown is 'Rename
+ * variable...' and if selected, should trigger the prompt to rename a variable.
+ * @const {string}
+ */
+Blockly.RENAME_VARIABLE_ID = 'RENAME_VARIABLE_ID';
+
+/**
+ * String for use in the dropdown created in field_variable.
+ * This string indicates that this option in the dropdown is 'Delete the "%1"
+ * variable' and if selected, should trigger the prompt to delete a variable.
+ * @const {string}
+ */
+Blockly.DELETE_VARIABLE_ID = 'DELETE_VARIABLE_ID';
diff --git a/core/css.js b/core/css.js
index 7ec0dc2f..e4e63809 100644
--- a/core/css.js
+++ b/core/css.js
@@ -767,6 +767,10 @@ Blockly.Css.CONTENT = [
     'vertical-align: middle;',
   '}',
 
+  '.blocklyToolboxDelete .blocklyTreeLabel {',
+    'cursor: url("<<<PATH>>>/handdelete.cur"), auto;',
+  '}',
+
   '.blocklyTreeSelected .blocklyTreeLabel {',
     'color: #fff;',
   '}',
diff --git a/core/field_image.js b/core/field_image.js
index a13ea4ab..c9fd165c 100644
--- a/core/field_image.js
+++ b/core/field_image.js
@@ -162,6 +162,7 @@ Blockly.FieldImage.prototype.setText = function(alt) {
 Blockly.FieldImage.prototype.render_ = function() {
   // NOP
 };
+
 /**
  * Images are fixed width, no need to update.
  * @private
diff --git a/core/field_textinput.js b/core/field_textinput.js
index c5452042..f80e5833 100644
--- a/core/field_textinput.js
+++ b/core/field_textinput.js
@@ -489,7 +489,6 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
       }
     }
     thisField.setText(text);
-    // Rerender the field now that the text has changed.
     thisField.sourceBlock_.rendered && thisField.sourceBlock_.render();
     Blockly.unbindEvent_(htmlInput.onKeyDownWrapper_);
     Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
diff --git a/core/field_variable.js b/core/field_variable.js
index 2b10a016..880e1580 100644
--- a/core/field_variable.js
+++ b/core/field_variable.js
@@ -28,6 +28,7 @@ goog.provide('Blockly.FieldVariable');
 
 goog.require('Blockly.FieldDropdown');
 goog.require('Blockly.Msg');
+goog.require('Blockly.VariableModel');
 goog.require('Blockly.Variables');
 goog.require('goog.asserts');
 goog.require('goog.string');
@@ -50,21 +51,6 @@ Blockly.FieldVariable = function(varname, opt_validator) {
 };
 goog.inherits(Blockly.FieldVariable, Blockly.FieldDropdown);
 
-/**
- * The menu item index for the rename variable option.
- * @type {number}
- * @private
- */
-Blockly.FieldVariable.prototype.renameVarItemIndex_ = -1;
-
-/**
- * The menu item index for the delete variable option.
- * @type {number}
- * @private
- */
-Blockly.FieldVariable.prototype.deleteVarItemIndex_ = -1;
-
-
 /**
  * Install this dropdown on a block.
  */
@@ -135,37 +121,41 @@ Blockly.FieldVariable.prototype.setValue = function(newValue) {
  * @this {Blockly.FieldVariable}
  */
 Blockly.FieldVariable.dropdownCreate = function() {
-  var variableNameList = [];
-  if (this.sourceBlock_ && this.sourceBlock_.workspace) {
+  var variableModelList = [];
+  var name = this.getText();
+  // Don't create a new variable if there is nothing selected.
+  var createSelectedVariable = name ? true : false;
+  var workspace = null;
+  if (this.sourceBlock_) {
+    workspace = this.sourceBlock_.workspace;
+  }
+
+  if (workspace) {
     // Get a copy of the list, so that adding rename and new variable options
     // doesn't modify the workspace's list.
-
-    var variableModelList = this.sourceBlock_.workspace.getVariablesOfType('');
-    for (var i = 0; i < variableModelList.length; i++) {
-      variableNameList.push(variableModelList[i].name);
+    var variableModelList = workspace.getVariablesOfType('');
+    for (var i = 0; i < variableModelList.length; i++){
+      if (createSelectedVariable &&
+          goog.string.caseInsensitiveEquals(variableModelList[i].name, name)) {
+        createSelectedVariable = false;
+        break;
+      }
     }
   }
   // Ensure that the currently selected variable is an option.
-  var name = this.getText();
-  if (name && variableNameList.indexOf(name) == -1) {
-    variableNameList.push(name);
+  if (createSelectedVariable && workspace) {
+    var newVar = workspace.createVariable(name);
+    variableModelList.push(newVar);
   }
-  variableNameList.sort(goog.string.caseInsensitiveCompare);
-
-  this.renameVarItemIndex_ = variableNameList.length;
-  variableNameList.push(Blockly.Msg.RENAME_VARIABLE);
-
-  this.deleteVarItemIndex_ = variableNameList.length;
-  variableNameList.push(Blockly.Msg.DELETE_VARIABLE.replace('%1', name));
-  // Variables are not language-specific, use the name as both the user-facing
-  // text and the internal representation.
+  variableModelList.sort(Blockly.VariableModel.compareByName);
   var options = [];
-  for (var i = 0; i < variableNameList.length; i++) {
-    // TODO(marisaleung): Set options[i] to [name, uuid]. This requires
-    // changes where the variable gets set since the initialized value would be
-    // id.
-    options[i] = [variableNameList[i], variableNameList[i]];
+  for (var i = 0; i < variableModelList.length; i++) {
+    // Set the uuid as the internal representation of the variable.
+    options[i] = [variableModelList[i].name, variableModelList[i].getId()];
   }
+  options.push([Blockly.Msg.RENAME_VARIABLE, Blockly.RENAME_VARIABLE_ID]);
+  options.push([Blockly.Msg.DELETE_VARIABLE.replace('%1', name),
+               Blockly.DELETE_VARIABLE_ID]);
   return options;
 };
 
@@ -177,11 +167,18 @@ Blockly.FieldVariable.dropdownCreate = function() {
  * @param {!goog.ui.MenuItem} menuItem The MenuItem selected within menu.
  */
 Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
-  var itemText = menuItem.getValue();
-  if (this.sourceBlock_) {
+  var id = menuItem.getValue();
+  // TODO(marisaleung): change setValue() to take in an id as the parameter.
+  // Then remove itemText.
+  var itemText;
+  if (this.sourceBlock_ && this.sourceBlock_.workspace) {
     var workspace = this.sourceBlock_.workspace;
-    if (this.renameVarItemIndex_ >= 0 &&
-        menu.getChildAt(this.renameVarItemIndex_) === menuItem) {
+    var variable = workspace.getVariableById(id);
+    // If the item selected is a variable, set itemText to the variable name.
+    if (variable) {
+      itemText = variable.name;
+    }
+    else if (id == Blockly.RENAME_VARIABLE_ID) {
       // Rename variable.
       var oldName = this.getText();
       Blockly.hideChaff();
@@ -193,8 +190,7 @@ Blockly.FieldVariable.prototype.onItemSelected = function(menu, menuItem) {
             }
           });
       return;
-    } else if (this.deleteVarItemIndex_ >= 0 &&
-        menu.getChildAt(this.deleteVarItemIndex_) === menuItem) {
+    } else if (id == Blockly.DELETE_VARIABLE_ID) {
       // Delete variable.
       workspace.deleteVariable(this.getText());
       return;
diff --git a/core/gesture.js b/core/gesture.js
index 06cbd491..f89d2d81 100644
--- a/core/gesture.js
+++ b/core/gesture.js
@@ -77,13 +77,23 @@ Blockly.Gesture = function(e, creatorWorkspace) {
   this.startField_ = null;
 
   /**
-   * The block that the gesture started on, or null if it did not block on a
-   * field.
+   * The block that the gesture started on, or null if it did not start on a
+   * block.
    * @type {Blockly.BlockSvg}
    * @private
    */
   this.startBlock_ = null;
 
+  /**
+   * The block that this gesture targets.  If the gesture started on a
+   * shadow block, this is the first non-shadow parent of the block.  If the
+   * gesture started in the flyout, this is the root block of the block group
+   * that was clicked or dragged.
+   * @type {Blockly.BlockSvg}
+   * @private
+   */
+  this.targetBlock_ = null;
+
   /**
    * The workspace that the gesture started on.  There may be multiple
    * workspaces on a page; this is more accurate than using
@@ -213,6 +223,7 @@ Blockly.Gesture.prototype.dispose = function() {
 
   this.startField_ = null;
   this.startBlock_ = null;
+  this.targetBlock_ = null;
   this.startWorkspace_ = null;
   this.flyout_ = null;
 
@@ -274,13 +285,13 @@ Blockly.Gesture.prototype.updateDragDelta_ = function(currentXY) {
  * This function should be called on a mouse/touch move event the first time the
  * drag radius is exceeded.  It should be called no more than once per gesture.
  * If a block should be dragged from the flyout this function creates the new
- * block on the main workspace and updates startBlock_ and startWorkspace_.
+ * block on the main workspace and updates targetBlock_ and startWorkspace_.
  * @return {boolean} True if a block is being dragged from the flyout.
  * @private
  */
 Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() {
   // Disabled blocks may not be dragged from the flyout.
-  if (this.startBlock_.disabled) {
+  if (this.targetBlock_.disabled) {
     return false;
   }
   if (!this.flyout_.isScrollable() ||
@@ -292,8 +303,10 @@ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() {
     if (!Blockly.Events.getGroup()) {
       Blockly.Events.setGroup(true);
     }
-    this.startBlock_ = this.flyout_.createBlock(this.startBlock_);
-    this.startBlock_.select();
+    // The start block is no longer relevant, because this is a drag.
+    this.startBlock_ = null;
+    this.targetBlock_ = this.flyout_.createBlock(this.targetBlock_);
+    this.targetBlock_.select();
     return true;
   }
   return false;
@@ -309,13 +322,13 @@ Blockly.Gesture.prototype.updateIsDraggingFromFlyout_ = function() {
  * @private
  */
 Blockly.Gesture.prototype.updateIsDraggingBlock_ = function() {
-  if (!this.startBlock_) {
+  if (!this.targetBlock_) {
     return false;
   }
 
   if (this.flyout_) {
     this.isDraggingBlock_ = this.updateIsDraggingFromFlyout_();
-  } else if (this.startBlock_.isMovable()){
+  } else if (this.targetBlock_.isMovable()){
     this.isDraggingBlock_ = true;
   }
 
@@ -377,7 +390,7 @@ Blockly.Gesture.prototype.updateIsDragging_ = function() {
  * @private
  */
 Blockly.Gesture.prototype.startDraggingBlock_ = function() {
-  this.blockDragger_ = new Blockly.BlockDragger(this.startBlock_,
+  this.blockDragger_ = new Blockly.BlockDragger(this.targetBlock_,
       this.startWorkspace_);
   this.blockDragger_.startBlockDrag(this.currentDragDeltaXY_);
   this.blockDragger_.dragBlock(this.mostRecentEvent_,
@@ -407,12 +420,12 @@ Blockly.Gesture.prototype.doStart = function(e) {
   this.startWorkspace_.markFocused();
   this.mostRecentEvent_ = e;
 
-  // Hide chaff also hides the flyout by default.
+  // Hide chaff also hides the flyout, so don't do it if the click is in a flyout.
   Blockly.hideChaff(!!this.flyout_);
   Blockly.Tooltip.block();
 
-  if (this.startBlock_) {
-    this.startBlock_.select();
+  if (this.targetBlock_) {
+    this.targetBlock_.select();
   }
 
   if (Blockly.utils.isRightButton(e)) {
@@ -514,10 +527,10 @@ Blockly.Gesture.prototype.cancel = function() {
  * @package
  */
 Blockly.Gesture.prototype.handleRightClick = function(e) {
-  if (this.startBlock_) {
+  if (this.targetBlock_) {
     this.bringBlockToFront_();
     Blockly.hideChaff(this.flyout_);
-    this.startBlock_.showContextMenu_(e);
+    this.targetBlock_.showContextMenu_(e);
   } else if (this.startWorkspace_ && !this.flyout_) {
     Blockly.hideChaff();
     this.startWorkspace_.showContextMenu_(e);
@@ -595,7 +608,7 @@ Blockly.Gesture.prototype.doBlockClick_ = function() {
     if (!Blockly.Events.getGroup()) {
       Blockly.Events.setGroup(true);
     }
-    var newBlock = this.flyout_.createBlock(this.startBlock_);
+    var newBlock = this.flyout_.createBlock(this.targetBlock_);
     newBlock.scheduleSnapAndBump();
   } else {
     // A field is being edited if either the WidgetDiv or DropDownDiv is currently open.
@@ -635,8 +648,8 @@ Blockly.Gesture.prototype.doWorkspaceClick_ = function() {
  */
 Blockly.Gesture.prototype.bringBlockToFront_ = function() {
   // Blocks in the flyout don't overlap, so skip the work.
-  if (this.startBlock_ && !this.flyout_) {
-    this.startBlock_.bringToFront();
+  if (this.targetBlock_ && !this.flyout_) {
+    this.targetBlock_.bringToFront();
   }
 };
 
@@ -657,24 +670,37 @@ Blockly.Gesture.prototype.setStartField = function(field) {
 };
 
 /**
- * Record the block that a gesture started on.
- * If the block is a shadow, record the parent.  If the block is in the flyout,
- * use the root block from the block group.
+ * Record the block that a gesture started on, and set the target block
+ * appropriately.
  * @param {Blockly.BlockSvg} block The block the gesture started on.
  * @package
  */
 Blockly.Gesture.prototype.setStartBlock = function(block) {
   if (!this.startBlock_) {
-    if (block.isShadow()) {
-      this.setStartBlock(block.getParent());
-    } else if (block.isInFlyout && block != block.getRootBlock()) {
-      this.setStartBlock(block.getRootBlock());
+    this.startBlock_ = block;
+    if (block.isInFlyout && block != block.getRootBlock()) {
+      this.setTargetBlock_(block.getRootBlock());
     } else {
-      this.startBlock_ = block;
+      this.setTargetBlock_(block);
     }
   }
 };
 
+/**
+ * Record the block that a gesture targets, meaning the block that will be
+ * dragged if this turns into a drag.  If this block is a shadow, that will be
+ * its first non-shadow parent.
+ * @param {Blockly.BlockSvg} block The block the gesture targets.
+ * @private
+ */
+Blockly.Gesture.prototype.setTargetBlock_ = function(block) {
+  if (block.isShadow()) {
+    this.setTargetBlock_(block.getParent());
+  } else {
+    this.targetBlock_ = block;
+  }
+};
+
 /**
  * Record the workspace that a gesture started on.
  * @param {Blockly.WorkspaceSvg} ws The workspace the gesture started on.
diff --git a/core/grid.js b/core/grid.js
new file mode 100644
index 00000000..e87df602
--- /dev/null
+++ b/core/grid.js
@@ -0,0 +1,222 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2017 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Object for configuring and updating a workspace grid in
+ * Blockly.
+ * @author fenichel@google.com (Rachel Fenichel)
+ */
+'use strict';
+
+goog.provide('Blockly.Grid');
+
+goog.require('Blockly.utils');
+
+goog.require('goog.userAgent');
+
+
+/**
+ * Class for a workspace's grid.
+ * @param {!SVGElement} pattern The grid's SVG pattern, created during injection.
+ * @param {!Object} options A dictionary of normalized options for the grid.
+ *     See grid documentation:
+ *     https://developers.google.com/blockly/guides/configure/web/grid
+ * @constructor
+ */
+Blockly.Grid = function(pattern, options) {
+  /**
+   * The grid's SVG pattern, created during injection.
+   * @type {!SVGElement}
+   * @private
+   */
+  this.gridPattern_ = pattern;
+
+  /**
+   * The spacing of the grid lines (in px).
+   * @type {number}
+   * @private
+   */
+  this.spacing_ = options['spacing'];
+
+  /**
+   * How long the grid lines should be (in px).
+   * @type {number}
+   * @private
+   */
+  this.length_ = options['length'];
+
+  /**
+   * The horizontal grid line, if it exists.
+   * @type {SVGElement}
+   * @private
+   */
+  this.line1_ = pattern.firstChild;
+
+  /**
+   * The vertical grid line, if it exists.
+   * @type {SVGElement}
+   * @private
+   */
+  this.line2_ = this.line1_ && this.line1_.nextSibling;
+
+  /**
+   * Whether blocks should snap to the grid.
+   * @type {boolean}
+   * @private
+   */
+  this.snapToGrid_ = options['snap'];
+};
+
+/**
+ * The scale of the grid, used to set stroke width on grid lines.
+ * This should always be the same as the workspace scale.
+ * @type {number}
+ * @private
+ */
+Blockly.Grid.prototype.scale_ = 1;
+
+/**
+ * Dispose of this grid and unlink from the DOM.
+ * @package
+ */
+Blockly.Grid.prototype.dispose = function() {
+  this.gridPattern_ = null;
+};
+
+/**
+ * Whether blocks should snap to the grid, based on the initial configuration.
+ * @return {boolean} True if blocks should snap, false otherwise.
+ * @package
+ */
+Blockly.Grid.prototype.shouldSnap = function() {
+  return this.snapToGrid_;
+};
+
+/**
+ * Get the spacing of the grid points (in px).
+ * @return {number} The spacing of the grid points.
+ * @package
+ */
+Blockly.Grid.prototype.getSpacing = function() {
+  return this.spacing_;
+};
+
+/**
+ * Get the id of the pattern element, which should be randomized to avoid
+ * conflicts with other Blockly instances on the page.
+ * @return {string} The pattern id.
+ * @package
+ */
+Blockly.Grid.prototype.getPatternId = function() {
+  return this.gridPattern_.id;
+};
+
+/**
+ * Update the grid with a new scale.
+ * @param {number} scale The new workspace scale.
+ * @package
+ */
+Blockly.Grid.prototype.update = function(scale) {
+  this.scale_ = scale;
+  // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
+  var safeSpacing = (this.spacing_ * scale) || 100;
+
+  this.gridPattern_.setAttribute('width', safeSpacing);
+  this.gridPattern_.setAttribute('height', safeSpacing);
+
+  var half = Math.floor(this.spacing_ / 2) + 0.5;
+  var start = half - this.length_ / 2;
+  var end = half + this.length_ / 2;
+
+  half *= scale;
+  start *= scale;
+  end *= scale;
+
+  this.setLineAttributes_(this.line1_, scale, start, end, half, half);
+  this.setLineAttributes_(this.line2_, scale, half, half, start, end);
+};
+
+/**
+ * Set the attributes on one of the lines in the grid.  Use this to update the
+ * length and stroke width of the grid lines.
+ * @param {!SVGElement} line Which line to update.
+ * @param {number} width The new stroke size (in px).
+ * @param {number} x1 The new x start position of the line (in px).
+ * @param {number} x2 The new x end position of the line (in px).
+ * @param {number} y1 The new y start position of the line (in px).
+ * @param {number} y2 The new y end position of the line (in px).
+ * @private
+ */
+Blockly.Grid.prototype.setLineAttributes_ = function(line, width, x1, x2, y1, y2) {
+  if (line) {
+    line.setAttribute('stroke-width', width);
+    line.setAttribute('x1', x1);
+    line.setAttribute('y1', y1);
+    line.setAttribute('x2', x2);
+    line.setAttribute('y2', y2);
+  }
+};
+
+/**
+ * Move the grid to a new x and y position, and make sure that change is visible.
+ * @param {number} x The new x position of the grid (in px).
+ * @param {number} y The new y position ofthe grid (in px).
+ * @package
+ */
+Blockly.Grid.prototype.moveTo = function(x, y) {
+  this.gridPattern_.setAttribute('x', x);
+  this.gridPattern_.setAttribute('y', y);
+
+  if (goog.userAgent.IE || goog.userAgent.EDGE) {
+    // IE/Edge doesn't notice that the x/y offsets have changed.
+    // Force an update.
+    this.update(this.scale_);
+  }
+};
+
+/**
+ * Create the DOM for the grid described by options.
+ * @param {string} rnd A random ID to append to the pattern's ID.
+ * @param {!Object} gridOptions The object containing grid configuration.
+ * @param {!SVGElement} defs The root SVG element for this workspace's defs.
+ * @return {!SVGElement} The SVG element for the grid pattern.
+ * @package
+ */
+Blockly.Grid.createDom = function(rnd, gridOptions, defs) {
+  /*
+    <pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
+      <rect stroke="#888" />
+      <rect stroke="#888" />
+    </pattern>
+  */
+  var gridPattern = Blockly.utils.createSvgElement('pattern',
+      {'id': 'blocklyGridPattern' + rnd,
+       'patternUnits': 'userSpaceOnUse'}, defs);
+  if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) {
+    Blockly.utils.createSvgElement('line',
+        {'stroke': gridOptions['colour']}, gridPattern);
+    if (gridOptions['length'] > 1) {
+      Blockly.utils.createSvgElement('line',
+          {'stroke': gridOptions['colour']}, gridPattern);
+    }
+    // x1, y1, x1, x2 properties will be set later in update.
+  }
+  return gridPattern;
+};
diff --git a/core/inject.js b/core/inject.js
index 0a2eea8a..db344ad6 100644
--- a/core/inject.js
+++ b/core/inject.js
@@ -30,6 +30,7 @@ goog.require('Blockly.BlockDragSurfaceSvg');
 goog.require('Blockly.Css');
 goog.require('Blockly.constants');
 goog.require('Blockly.DropDownDiv');
+goog.require('Blockly.Grid');
 goog.require('Blockly.Options');
 goog.require('Blockly.WorkspaceSvg');
 goog.require('Blockly.WorkspaceDragSurfaceSvg');
@@ -183,27 +184,7 @@ Blockly.createDom_ = function(container, options) {
       {'d': 'M 0 0 L 10 10 M 10 0 L 0 10', 'stroke': '#cc0'}, disabledPattern);
   options.disabledPatternId = disabledPattern.id;
 
-  /*
-    <pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
-      <rect stroke="#888" />
-      <rect stroke="#888" />
-    </pattern>
-  */
-  var gridPattern = Blockly.utils.createSvgElement('pattern',
-      {'id': 'blocklyGridPattern' + rnd,
-       'patternUnits': 'userSpaceOnUse'}, defs);
-  if (options.gridOptions['length'] > 0 && options.gridOptions['spacing'] > 0) {
-    Blockly.utils.createSvgElement('line',
-        {'stroke': options.gridOptions['colour']},
-        gridPattern);
-    if (options.gridOptions['length'] > 1) {
-      Blockly.utils.createSvgElement('line',
-          {'stroke': options.gridOptions['colour']},
-          gridPattern);
-    }
-    // x1, y1, x1, x2 properties will be set later in updateGridPattern_.
-  }
-  options.gridPattern = gridPattern;
+  options.gridPattern = Blockly.Grid.createDom(rnd, options.gridOptions, defs);
   return svg;
 };
 
@@ -389,10 +370,15 @@ Blockly.inject.bindDocumentEvents_ = function() {
  * @private
  */
 Blockly.inject.loadSounds_ = function(pathToMedia, workspace) {
-  workspace.loadAudio_(
-      [pathToMedia + 'click.wav'], 'click');
-  workspace.loadAudio_(
-      [pathToMedia + 'delete.wav'], 'delete');
+  var audioMgr = workspace.getAudioManager();
+  audioMgr.load(
+      [pathToMedia + 'click.mp3',
+       pathToMedia + 'click.wav',
+       pathToMedia + 'click.ogg'], 'click');
+  audioMgr.load(
+      [pathToMedia + 'delete.mp3',
+       pathToMedia + 'delete.ogg',
+       pathToMedia + 'delete.wav'], 'delete');
 
   // Bind temporary hooks that preload the sounds.
   var soundBinds = [];
@@ -400,7 +386,7 @@ Blockly.inject.loadSounds_ = function(pathToMedia, workspace) {
     while (soundBinds.length) {
       Blockly.unbindEvent_(soundBinds.pop());
     }
-    workspace.preloadAudio_();
+    audioMgr.preload();
   };
 
   // opt_noCaptureIdentifier is true because this is an action to take on a
diff --git a/core/toolbox.js b/core/toolbox.js
index 5794d795..e0236d47 100644
--- a/core/toolbox.js
+++ b/core/toolbox.js
@@ -116,7 +116,6 @@ Blockly.Toolbox.prototype.init = function() {
   // Clicking on toolbox closes popups.
   Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this,
       function(e) {
-        Blockly.DropDownDiv.hide();
         if (Blockly.utils.isRightButton(e) || e.target == this.HtmlDiv) {
           // Close flyout.
           Blockly.hideChaff(false);
@@ -257,24 +256,6 @@ Blockly.Toolbox.prototype.removeDeleteStyle = function() {
                             'blocklyToolboxDelete');
 };
 
-/**
- * Adds styles on the toolbox indicating blocks will be deleted.
- * @package
- */
-Blockly.Toolbox.prototype.addDeleteStyle = function() {
-  Blockly.utils.addClass(/** @type {!Element} */ (this.HtmlDiv),
-                         'blocklyToolboxDelete');
-};
-
-/**
- * Remove styles from the toolbox that indicate blocks will be deleted.
- * @package
- */
-Blockly.Toolbox.prototype.removeDeleteStyle = function() {
-  Blockly.utils.removeClass(/** @type {!Element} */ (this.HtmlDiv),
-                            'blocklyToolboxDelete');
-};
-
 /**
  * Return the deletion rectangle for this toolbox.
  * @return {goog.math.Rect} Rectangle in which to delete.
diff --git a/core/utils.js b/core/utils.js
index 4c1ab572..9b088977 100644
--- a/core/utils.js
+++ b/core/utils.js
@@ -56,13 +56,6 @@ Blockly.utils.removeAttribute = function(element, attributeName) {
   }
 };
 
-/**
- * Cached value for whether 3D is supported
- * @type {boolean}
- * @private
- */
-Blockly.cache3dSupported_ = null;
-
 /**
  * Add a CSS class to a element.
  * Similar to Closure's goog.dom.classes.add, except it handles SVG elements.
diff --git a/core/variable_map.js b/core/variable_map.js
index 2cb16b71..522516db 100644
--- a/core/variable_map.js
+++ b/core/variable_map.js
@@ -28,7 +28,6 @@ goog.provide('Blockly.VariableMap');
 
 goog.require('Blockly.VariableModel');
 
-
 /**
  * Class for a variable map.  This contains a dictionary data structure with
  * variable types as keys and lists of variables as values.  The list of
diff --git a/core/variable_model.js b/core/variable_model.js
index 1a1f4fbb..b5ec897a 100644
--- a/core/variable_model.js
+++ b/core/variable_model.js
@@ -26,6 +26,9 @@
 
 goog.provide('Blockly.VariableModel');
 
+goog.require('goog.string');
+
+
 /**
  * Class for a variable model.
  * Holds information for the variable including name, id, and type.
@@ -73,3 +76,15 @@ Blockly.VariableModel = function(name, opt_type, opt_id) {
 Blockly.VariableModel.prototype.getId = function() {
   return this.id_;
 };
+
+/**
+ * A custom compare function for the VariableModel objects.
+ * @param {Blockly.VariableModel} var1 First variable to compare.
+ * @param {Blockly.VariableModel} var2 Second variable to compare.
+ * @return {number} -1 if name of var1 is less than name of var2, 0 if equal,
+ *     and 1 if greater.
+ * @package
+ */
+Blockly.VariableModel.compareByName = function(var1, var2) {
+  return goog.string.caseInsensitiveCompare(var1.name, var2.name);
+};
diff --git a/core/variables.js b/core/variables.js
index 679777a5..a8d89481 100644
--- a/core/variables.js
+++ b/core/variables.js
@@ -32,6 +32,7 @@ goog.provide('Blockly.Variables');
 
 goog.require('Blockly.Blocks');
 goog.require('Blockly.constants');
+goog.require('Blockly.VariableModel');
 goog.require('Blockly.Workspace');
 goog.require('goog.string');
 
@@ -109,12 +110,8 @@ Blockly.Variables.allVariables = function(root) {
  * @return {!Array.<!Element>} Array of XML block elements.
  */
 Blockly.Variables.flyoutCategory = function(workspace) {
-  var variableNameList = [];
   var variableModelList = workspace.getVariablesOfType('');
-  for (var i = 0; i < variableModelList.length; i++) {
-    variableNameList.push(variableModelList[i].name);
-  }
-  variableNameList.sort(goog.string.caseInsensitiveCompare);
+  variableModelList.sort(Blockly.VariableModel.compareByName);
 
   var xmlList = [];
   var button = goog.dom.createDom('button');
@@ -127,17 +124,19 @@ Blockly.Variables.flyoutCategory = function(workspace) {
 
   xmlList.push(button);
 
-  for (var i = 0; i < variableNameList.length; i++) {
+  for (var i = 0; i < variableModelList.length; i++) {
     if (Blockly.Blocks['data_variable']) {
       // <block type="data_variable">
-      //    <field name="VARIABLE">variablename</field>
+      //    <field name="VARIABLE" variableType="" id="">variablename</field>
       // </block>
       var block = goog.dom.createDom('block');
       block.setAttribute('type', 'data_variable');
       block.setAttribute('gap', 8);
 
-      var field = goog.dom.createDom('field', null, variableNameList[i]);
+      var field = goog.dom.createDom('field', null, variableModelList[i].name);
       field.setAttribute('name', 'VARIABLE');
+      field.setAttribute('variableType', variableModelList[i].type);
+      field.setAttribute('id', variableModelList[i].getId());
       block.appendChild(field);
 
       xmlList.push(block);
@@ -161,7 +160,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
       var block = goog.dom.createDom('block');
       block.setAttribute('type', 'data_setvariableto');
       block.setAttribute('gap', 8);
-      block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
+      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
       block.appendChild(Blockly.Variables.createTextDom_());
       xmlList.push(block);
     }
@@ -179,7 +178,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
       var block = goog.dom.createDom('block');
       block.setAttribute('type', 'data_changevariableby');
       block.setAttribute('gap', 8);
-      block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
+      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
       block.appendChild(Blockly.Variables.createMathNumberDom_());
       xmlList.push(block);
     }
@@ -192,7 +191,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
       var block = goog.dom.createDom('block');
       block.setAttribute('type', 'data_showvariable');
       block.setAttribute('gap', 8);
-      block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
+      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
       xmlList.push(block);
     }
     if (Blockly.Blocks['data_hidevariable']) {
@@ -203,7 +202,7 @@ Blockly.Variables.flyoutCategory = function(workspace) {
       // </block>
       var block = goog.dom.createDom('block');
       block.setAttribute('type', 'data_hidevariable');
-      block.appendChild(Blockly.Variables.createVariableDom_(variableNameList[0]));
+      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
       xmlList.push(block);
     }
   }
@@ -235,10 +234,10 @@ Blockly.Variables.createShadowDom_ = function(type) {
 
 /**
  * Create a dom element for value tag with a shadow variable inside.
- * @param {string} name The name of the variable to select.
+ * @param {Blockly.VariableModel} variableModel The variable to use.
  * @return {!Element} An XML element.
  */
-Blockly.Variables.createVariableDom_ = function(name) {
+Blockly.Variables.createVariableDom_ = function(variableModel) {
   //   <value name="VARIABLE">
   //     <shadow type="data_variablemenu">
   //       <field name="VARIABLE">variablename
@@ -247,8 +246,10 @@ Blockly.Variables.createVariableDom_ = function(name) {
   //   </value>
   var value = Blockly.Variables.createValueDom_('VARIABLE');
   var shadow = Blockly.Variables.createShadowDom_('data_variablemenu');
-  var field = goog.dom.createDom('field', null, name);
+  var field = goog.dom.createDom('field', null, variableModel.name);
   field.setAttribute('name', 'VARIABLE');
+  field.setAttribute('variableType', variableModel.type);
+  field.setAttribute('id', variableModel.getId());
   shadow.appendChild(field);
   value.appendChild(shadow);
   return value;
diff --git a/core/workspace.js b/core/workspace.js
index 39b1d7ec..f9403581 100644
--- a/core/workspace.js
+++ b/core/workspace.js
@@ -229,7 +229,7 @@ Blockly.Workspace.prototype.updateVariableStore = function(clear) {
     var tempVar = this.getVariable(name);
     if (tempVar) {
       varList.push({'name': tempVar.name, 'type': tempVar.type,
-                     'id': tempVar.getId()});
+                    'id': tempVar.getId()});
     }
     else {
       varList.push({'name': name, 'type': null, 'id': null});
@@ -415,6 +415,7 @@ Blockly.Workspace.prototype.deleteVariableInternal_ = function(variable) {
     uses[i].dispose(true, false);
   }
   Blockly.Events.setGroup(false);
+
   this.variableMap_.deleteVariable(variable);
 };
 
@@ -564,23 +565,6 @@ Blockly.Workspace.prototype.getBlockById = function(id) {
   return block || null;
 };
 
-/**
- * Checks whether all value and statement inputs in the workspace are filled
- * with blocks.
- * @param {boolean=} opt_shadowBlocksAreFilled An optional argument controlling
- *     whether shadow blocks are counted as filled. Defaults to true.
- * @return {boolean} True if all inputs are filled, false otherwise.
- */
-Blockly.Workspace.prototype.allInputsFilled = function(opt_shadowBlocksAreFilled) {
-  var blocks = this.getTopBlocks(false);
-  for (var i = 0, block; block = blocks[i]; i++) {
-    if (!block.allInputsFilled(opt_shadowBlocksAreFilled)) {
-      return false;
-    }
-  }
-  return true;
-};
-
 /**
  * Getter for the flyout associated with this workspace.  This is null in a
  * non-rendered workspace, but may be overriden by subclasses.
diff --git a/core/workspace_audio.js b/core/workspace_audio.js
new file mode 100644
index 00000000..8b746a36
--- /dev/null
+++ b/core/workspace_audio.js
@@ -0,0 +1,154 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2017 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @fileoverview Object in charge of loading, storing, and playing audio for a
+ *     workspace.
+ * @author fenichel@google.com (Rachel Fenichel)
+ */
+'use strict';
+
+goog.provide('Blockly.WorkspaceAudio');
+
+/**
+ * Class for loading, storing, and playing audio for a workspace.
+ * @param {Blockly.WorkspaceSvg} parentWorkspace The parent of the workspace
+ *     this audio object belongs to, or null.
+ */
+Blockly.WorkspaceAudio = function(parentWorkspace) {
+
+  /**
+   * The parent of the workspace this object belongs to, or null.  May be
+   * checked for sounds that this object can't find.
+   * @type {Blockly.WorkspaceSvg}
+   * @private
+   */
+  this.parentWorkspace_ = parentWorkspace;
+
+  /**
+   * Database of pre-loaded sounds.
+   * @private
+   * @const
+   */
+  this.SOUNDS_ = Object.create(null);
+};
+
+/**
+ * Time that the last sound was played.
+ * @type {Date}
+ * @private
+ */
+Blockly.WorkspaceAudio.prototype.lastSound_ = null;
+
+/**
+ * Dispose of this audio manager.
+ * @package
+ */
+Blockly.WorkspaceAudio.prototype.dispose = function() {
+  this.parentWorkspace_ = null;
+  this.SOUNDS_ = null;
+};
+
+/**
+ * Load an audio file.  Cache it, ready for instantaneous playing.
+ * @param {!Array.<string>} filenames List of file types in decreasing order of
+ *   preference (i.e. increasing size).  E.g. ['media/go.mp3', 'media/go.wav']
+ *   Filenames include path from Blockly's root.  File extensions matter.
+ * @param {string} name Name of sound.
+ * @package
+ */
+Blockly.WorkspaceAudio.prototype.load = function(filenames, name) {
+  if (!filenames.length) {
+    return;
+  }
+  try {
+    var audioTest = new window['Audio']();
+  } catch (e) {
+    // No browser support for Audio.
+    // IE can throw an error even if the Audio object exists.
+    return;
+  }
+  var sound;
+  for (var i = 0; i < filenames.length; i++) {
+    var filename = filenames[i];
+    var ext = filename.match(/\.(\w+)$/);
+    if (ext && audioTest.canPlayType('audio/' + ext[1])) {
+      // Found an audio format we can play.
+      sound = new window['Audio'](filename);
+      break;
+    }
+  }
+  if (sound && sound.play) {
+    this.SOUNDS_[name] = sound;
+  }
+};
+
+/**
+ * Preload all the audio files so that they play quickly when asked for.
+ * @package
+ */
+Blockly.WorkspaceAudio.prototype.preload = function() {
+  for (var name in this.SOUNDS_) {
+    var sound = this.SOUNDS_[name];
+    sound.volume = .01;
+    sound.play();
+    sound.pause();
+    // iOS can only process one sound at a time.  Trying to load more than one
+    // corrupts the earlier ones.  Just load one and leave the others uncached.
+    if (goog.userAgent.IPAD || goog.userAgent.IPHONE) {
+      break;
+    }
+  }
+};
+
+/**
+ * Play a named sound at specified volume.  If volume is not specified,
+ * use full volume (1).
+ * @param {string} name Name of sound.
+ * @param {number=} opt_volume Volume of sound (0-1).
+ */
+Blockly.WorkspaceAudio.prototype.play = function(name, opt_volume) {
+  var sound = this.SOUNDS_[name];
+  if (sound) {
+    // Don't play one sound on top of another.
+    var now = new Date;
+    if (this.lastSound_ != null &&
+        now - this.lastSound_ < Blockly.SOUND_LIMIT) {
+      return;
+    }
+    this.lastSound_ = now;
+    var mySound;
+    var ie9 = goog.userAgent.DOCUMENT_MODE &&
+              goog.userAgent.DOCUMENT_MODE === 9;
+    if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) {
+      // Creating a new audio node causes lag in IE9, Android and iPad. Android
+      // and IE9 refetch the file from the server, iPad uses a singleton audio
+      // node which must be deleted and recreated for each new audio tag.
+      mySound = sound;
+    } else {
+      mySound = sound.cloneNode();
+    }
+    mySound.volume = (opt_volume === undefined ? 1 : opt_volume);
+    mySound.play();
+  } else if (this.parentWorkspace_) {
+    // Maybe a workspace on a lower level knows about this sound.
+    this.parentWorkspace_.getAudioManager().play(name, opt_volume);
+  }
+};
diff --git a/core/workspace_svg.js b/core/workspace_svg.js
index c1cc7a90..6d9e782d 100644
--- a/core/workspace_svg.js
+++ b/core/workspace_svg.js
@@ -33,14 +33,15 @@ goog.require('Blockly.ConnectionDB');
 goog.require('Blockly.constants');
 goog.require('Blockly.DropDownDiv');
 goog.require('Blockly.Events');
-//goog.require('Blockly.HorizontalFlyout');
 goog.require('Blockly.Gesture');
+goog.require('Blockly.Grid');
 goog.require('Blockly.Options');
 goog.require('Blockly.ScrollbarPair');
 goog.require('Blockly.Touch');
 goog.require('Blockly.Trashcan');
 //goog.require('Blockly.VerticalFlyout');
 goog.require('Blockly.Workspace');
+goog.require('Blockly.WorkspaceAudio');
 goog.require('Blockly.WorkspaceDragSurfaceSvg');
 goog.require('Blockly.Xml');
 goog.require('Blockly.ZoomControls');
@@ -82,12 +83,6 @@ Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface
   this.useWorkspaceDragSurface_ =
       this.workspaceDragSurface_ && Blockly.utils.is3dSupported();
 
-  /**
-   * Database of pre-loaded sounds.
-   * @private
-   * @const
-   */
-  this.SOUNDS_ = Object.create(null);
   /**
    * List of currently highlighted blocks.  Block highlighting is often used to
    * visually mark blocks currently being executed.
@@ -96,6 +91,21 @@ Blockly.WorkspaceSvg = function(options, opt_blockDragSurface, opt_wsDragSurface
    */
   this.highlightedBlocks_ = [];
 
+  /**
+   * Object in charge of loading, storing, and playing audio for a workspace.
+   * @type {Blockly.WorkspaceAudio}
+   * @private
+   */
+  this.audioManager_ = new Blockly.WorkspaceAudio(options.parentWorkspace);
+
+  /**
+   * This workspace's grid object or null.
+   * @type {Blockly.Grid}
+   * @private
+   */
+  this.grid_ = this.options.gridPattern ?
+      new Blockly.Grid(options.gridPattern, options.gridOptions) : null;
+
   this.registerToolboxCategoryCallback(Blockly.VARIABLE_CATEGORY_NAME,
       Blockly.Variables.flyoutCategory);
   this.registerToolboxCategoryCallback(Blockly.PROCEDURE_CATEGORY_NAME,
@@ -227,13 +237,6 @@ Blockly.WorkspaceSvg.prototype.useWorkspaceDragSurface_ = false;
  */
 Blockly.WorkspaceSvg.prototype.isDragSurfaceActive_ = false;
 
-/**
- * Time that the last sound was played.
- * @type {Date}
- * @private
- */
-Blockly.WorkspaceSvg.prototype.lastSound_ = null;
-
 /**
  * Last known position of the page scroll.
  * This is used to determine whether we have recalculated screen coordinate
@@ -352,9 +355,9 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
         {'height': '100%', 'width': '100%', 'class': opt_backgroundClass},
         this.svgGroup_);
 
-    if (opt_backgroundClass == 'blocklyMainBackground') {
+    if (opt_backgroundClass == 'blocklyMainBackground' && this.grid_) {
       this.svgBackground_.style.fill =
-          'url(#' + this.options.gridPattern.id + ')';
+          'url(#' + this.grid_.getPatternId() + ')';
     }
   }
   /** @type {SVGElement} */
@@ -390,8 +393,9 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
      */
     this.toolbox_ = new Blockly.Toolbox(this);
   }
-  this.updateGridPattern_();
-  this.updateStackGlowScale_();
+  if (this.grid_) {
+    this.grid_.update(this.scale);
+  }
   this.recordDeleteAreas();
   return this.svgGroup_;
 };
@@ -434,6 +438,16 @@ Blockly.WorkspaceSvg.prototype.dispose = function() {
     this.zoomControls_ = null;
   }
 
+  if (this.audioManager_) {
+    this.audioManager_.dispose();
+    this.audioManager_ = null;
+  }
+
+  if (this.grid_) {
+    this.grid_.dispose();
+    this.grid_ = null;
+  }
+
   if (this.toolboxCategoryCallbacks_) {
     this.toolboxCategoryCallbacks_ = null;
   }
@@ -966,13 +980,15 @@ Blockly.WorkspaceSvg.prototype.renameVariable = function(oldName, newName) {
  *     variable immediately.
  * TODO: #468
  * @param {string} name The new variable's name.
+ * @return {?Blockly.VariableModel} The newly created variable.
  */
 Blockly.WorkspaceSvg.prototype.createVariable = function(name) {
-  Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name);
+  var newVar = Blockly.WorkspaceSvg.superClass_.createVariable.call(this, name);
   // Don't refresh the toolbox if there's a drag in progress.
   if (this.toolbox_ && this.toolbox_.flyout_ && !this.currentGesture_) {
     this.toolbox_.refreshSelection();
   }
+  return newVar;
 };
 
 /**
@@ -1308,96 +1324,6 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) {
   Blockly.ContextMenu.show(e, menuOptions, this.RTL);
 };
 
-/**
- * Load an audio file.  Cache it, ready for instantaneous playing.
- * @param {!Array.<string>} filenames List of file types in decreasing order of
- *   preference (i.e. increasing size).  E.g. ['media/go.mp3', 'media/go.wav']
- *   Filenames include path from Blockly's root.  File extensions matter.
- * @param {string} name Name of sound.
- * @private
- */
-Blockly.WorkspaceSvg.prototype.loadAudio_ = function(filenames, name) {
-  if (!filenames.length) {
-    return;
-  }
-  try {
-    var audioTest = new window['Audio']();
-  } catch(e) {
-    // No browser support for Audio.
-    // IE can throw an error even if the Audio object exists.
-    return;
-  }
-  var sound;
-  for (var i = 0; i < filenames.length; i++) {
-    var filename = filenames[i];
-    var ext = filename.match(/\.(\w+)$/);
-    if (ext && audioTest.canPlayType('audio/' + ext[1])) {
-      // Found an audio format we can play.
-      sound = new window['Audio'](filename);
-      break;
-    }
-  }
-  if (sound && sound.play) {
-    this.SOUNDS_[name] = sound;
-  }
-};
-
-/**
- * Preload all the audio files so that they play quickly when asked for.
- * @private
- */
-Blockly.WorkspaceSvg.prototype.preloadAudio_ = function() {
-  for (var name in this.SOUNDS_) {
-    var sound = this.SOUNDS_[name];
-    sound.volume = .01;
-    sound.play();
-    sound.pause();
-    // iOS can only process one sound at a time.  Trying to load more than one
-    // corrupts the earlier ones.  Just load one and leave the others uncached.
-    if (goog.userAgent.IPAD || goog.userAgent.IPHONE) {
-      break;
-    }
-  }
-};
-
-/**
- * Play a named sound at specified volume.  If volume is not specified,
- * use full volume (1).
- * @param {string} name Name of sound.
- * @param {number=} opt_volume Volume of sound (0-1).
- */
-Blockly.WorkspaceSvg.prototype.playAudio = function(name, opt_volume) {
-  // Send a UI event in case we wish to play the sound externally
-  var event = new Blockly.Events.Ui(null, 'sound', null, name);
-  event.workspaceId = this.id;
-  Blockly.Events.fire(event);
-  var sound = this.SOUNDS_[name];
-  if (sound) {
-    // Don't play one sound on top of another.
-    var now = new Date;
-    if (now - this.lastSound_ < Blockly.SOUND_LIMIT) {
-      return;
-    }
-    this.lastSound_ = now;
-    var mySound;
-    var ie9 = goog.userAgent.DOCUMENT_MODE &&
-              goog.userAgent.DOCUMENT_MODE === 9;
-    if (ie9 || goog.userAgent.IPAD || goog.userAgent.ANDROID) {
-      // Creating a new audio node causes lag in IE9, Android and iPad. Android
-      // and IE9 refetch the file from the server, iPad uses a singleton audio
-      // node which must be deleted and recreated for each new audio tag.
-      mySound = sound;
-    } else {
-      mySound = sound.cloneNode();
-    }
-    mySound.volume = (opt_volume === undefined ? 1 : opt_volume);
-    mySound.play();
-  } else if (this.options.parentWorkspace) {
-    // Maybe a workspace on a lower level knows about this sound.
-    this.options.parentWorkspace.playAudio(name, opt_volume);
-  }
-};
-
 /**
  * Modify the block tree on the existing toolbox.
  * @param {Node|string} tree DOM tree of blocks, or text representation of same.
@@ -1588,11 +1514,9 @@ Blockly.WorkspaceSvg.prototype.setScale = function(newScale) {
     newScale = this.options.zoomOptions.minScale;
   }
   this.scale = newScale;
-  this.updateStackGlowScale_();
-  this.updateGridPattern_();
-  // Hide the WidgetDiv without animation (zoom makes field out of place with div)
-  Blockly.WidgetDiv.hide(true);
-  Blockly.DropDownDiv.hideWithoutAnimation();
+  if (this.grid_) {
+    this.grid_.update(this.scale);
+  }
   if (this.scrollbar) {
     this.scrollbar.resize();
   } else {
@@ -1628,42 +1552,6 @@ Blockly.WorkspaceSvg.prototype.scroll = function(x, y) {
                      -y - metrics.contentTop);
 };
 
-/**
- * Updates the grid pattern.
- * @private
- */
-Blockly.WorkspaceSvg.prototype.updateGridPattern_ = function() {
-  if (!this.options.gridPattern) {
-    return;  // No grid.
-  }
-  // MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
-  var safeSpacing = (this.options.gridOptions['spacing'] * this.scale) || 100;
-  this.options.gridPattern.setAttribute('width', safeSpacing);
-  this.options.gridPattern.setAttribute('height', safeSpacing);
-  var half = Math.floor(this.options.gridOptions['spacing'] / 2) + 0.5;
-  var start = half - this.options.gridOptions['length'] / 2;
-  var end = half + this.options.gridOptions['length'] / 2;
-  var line1 = this.options.gridPattern.firstChild;
-  var line2 = line1 && line1.nextSibling;
-  half *= this.scale;
-  start *= this.scale;
-  end *= this.scale;
-  if (line1) {
-    line1.setAttribute('stroke-width', this.scale);
-    line1.setAttribute('x1', start);
-    line1.setAttribute('y1', half);
-    line1.setAttribute('x2', end);
-    line1.setAttribute('y2', half);
-  }
-  if (line2) {
-    line2.setAttribute('stroke-width', this.scale);
-    line2.setAttribute('x1', half);
-    line2.setAttribute('y1', start);
-    line2.setAttribute('x2', half);
-    line2.setAttribute('y2', end);
-  }
-};
-
 /**
  * Update the workspace's stack glow radius to be proportional to scale.
  * Ensures that stack glows always appear to be a fixed size.
@@ -1792,14 +1680,8 @@ Blockly.WorkspaceSvg.setTopLevelWorkspaceMetrics_ = function(xyRatio) {
   var x = this.scrollX + metrics.absoluteLeft;
   var y = this.scrollY + metrics.absoluteTop;
   this.translate(x, y);
-  if (this.options.gridPattern) {
-    this.options.gridPattern.setAttribute('x', x);
-    this.options.gridPattern.setAttribute('y', y);
-    if (goog.userAgent.IE || goog.userAgent.EDGE) {
-      // IE/Edge doesn't notice that the x/y offsets have changed.
-      // Force an update.
-      this.updateGridPattern_();
-    }
+  if (this.grid_) {
+    this.grid_.moveTo(x, y);
   }
 };
 
@@ -1952,6 +1834,24 @@ Blockly.WorkspaceSvg.prototype.cancelCurrentGesture = function() {
   }
 };
 
+/**
+ * Get the audio manager for this workspace.
+ * @return {Blockly.WorkspaceAudio} The audio manager for this workspace.
+ * @package
+ */
+Blockly.WorkspaceSvg.prototype.getAudioManager = function() {
+  return this.audioManager_;
+};
+
+/**
+ * Get the grid object for this workspace, or null if there is none.
+ * @return {Blockly.Grid} The grid object for this workspace.
+ * @package
+ */
+Blockly.WorkspaceSvg.prototype.getGrid = function() {
+  return this.grid_;
+};
+
 // Export symbols that would otherwise be renamed by Closure compiler.
 Blockly.WorkspaceSvg.prototype['setVisible'] =
     Blockly.WorkspaceSvg.prototype.setVisible;
diff --git a/core/xml.js b/core/xml.js
index 7bf91628..48225d30 100644
--- a/core/xml.js
+++ b/core/xml.js
@@ -43,6 +43,7 @@ goog.require('goog.userAgent');
  */
 Blockly.Xml.workspaceToDom = function(workspace, opt_noId) {
   var xml = goog.dom.createDom('xml');
+  xml.appendChild(Blockly.Xml.variablesToDom(workspace.getAllVariables()));
   var blocks = workspace.getTopBlocks(true);
   for (var i = 0, block; block = blocks[i]; i++) {
     xml.appendChild(Blockly.Xml.blockToDomWithXY(block, opt_noId));
@@ -50,6 +51,23 @@ Blockly.Xml.workspaceToDom = function(workspace, opt_noId) {
   return xml;
 };
 
+/**
+ * Encode a list of variables as XML.
+ * @param {!Array.<!Blockly.VariableModel>} variableList List of all variable
+ *     models.
+ * @return {!Element} List of XML elements.
+ */
+Blockly.Xml.variablesToDom = function(variableList) {
+  var variables = goog.dom.createDom('variables');
+  for (var i = 0, variable; variable = variableList[i]; i++) {
+    var element = goog.dom.createDom('variable', null, variable.name);
+    element.setAttribute('type', variable.type);
+    element.setAttribute('id', variable.getId());
+    variables.appendChild(element);
+  }
+  return variables;
+};
+
 /**
  * Encode a block subtree as XML with XY coordinates.
  * @param {!Blockly.Block} block The root block to encode.
@@ -92,6 +110,13 @@ Blockly.Xml.blockToDom = function(block, opt_noId) {
     if (field.name && field.EDITABLE) {
       var container = goog.dom.createDom('field', null, field.getValue());
       container.setAttribute('name', field.name);
+      if (field instanceof Blockly.FieldVariable) {
+        var variable = block.workspace.getVariable(field.getValue());
+        if (variable) {
+          container.setAttribute('id', variable.getId());
+          container.setAttribute('variableType', variable.type);
+        }
+      }
       element.appendChild(container);
     }
   }
@@ -325,6 +350,14 @@ Blockly.Xml.domToWorkspace = function(xml, workspace) {
       }
     } else if (name == 'shadow') {
       goog.asserts.fail('Shadow block cannot be a top-level block.');
+    }  else if (name == 'variables') {
+      if (i == 1) {
+        Blockly.Xml.domToVariables(xmlChild, workspace);
+      }
+      else {
+        throw Error('\'variables\' tag must be the first element in the' +
+          'workspace XML, but it was found in another location.');
+      }
     }
   }
   if (!existingGroup) {
@@ -463,6 +496,25 @@ Blockly.Xml.domToBlock = function(xmlBlock, workspace) {
   return topBlock;
 };
 
+/**
+ * Decode an XML list of variables and add the variables to the workspace.
+ * @param {!Element} xmlVariables List of XML variable elements.
+ * @param {!Blockly.Workspace} workspace The workspace to which the variable
+ *     should be added.
+ */
+Blockly.Xml.domToVariables = function(xmlVariables, workspace) {
+  for (var i = 0, xmlChild; xmlChild = xmlVariables.children[i]; i++) {
+    var type = xmlChild.getAttribute('type');
+    var id = xmlChild.getAttribute('id');
+    var name = xmlChild.textContent;
+
+    if (typeof(type) === undefined || type === null) {
+      throw Error('Variable with id, ' + id + ' is without a type');
+    }
+    workspace.createVariable(name, type, id);
+  }
+};
+
 /**
  * Decode an XML block tag and create a block (and possibly sub blocks) on the
  * workspace.
@@ -544,12 +596,32 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
         // Fall through.
       case 'field':
         var field = block.getField(name);
+        var text = xmlChild.textContent;
+        if (field instanceof Blockly.FieldVariable) {
+          // TODO (marisaleung): When we change setValue and getValue to
+          // interact with id's instead of names, update this so that we get
+          // the variable based on id instead of textContent.
+          var type = xmlChild.getAttribute('variabletype') || '';
+          var variable = workspace.getVariable(text);
+          if (!variable) {
+            variable = workspace.createVariable(text, type,
+              xmlChild.getAttribute(id));
+          }
+          if (typeof(type) !== undefined && type !== null) {
+            if (type !== variable.type) {
+              throw Error('Serialized variable type with id \'' +
+                variable.getId() + '\' had type ' + variable.type + ', and ' +
+                'does not match variable field that references it: ' +
+                Blockly.Xml.domToText(xmlChild) + '.');
+            }
+          }
+        }
         if (!field) {
           console.warn('Ignoring non-existent field ' + name + ' in block ' +
                        prototypeName);
           break;
         }
-        field.setValue(xmlChild.textContent);
+        field.setValue(text);
         break;
       case 'value':
       case 'statement':
@@ -629,9 +701,6 @@ Blockly.Xml.domToBlockHeadless_ = function(xmlBlock, workspace) {
       goog.asserts.assert(child.isShadow(),
                           'Shadow block not allowed non-shadow child.');
     }
-    // Ensure this block doesn't have any variable inputs.
-    goog.asserts.assert(block.getVars().length == 0,
-        'Shadow blocks cannot have variable fields.');
     block.setShadow(true);
   }
   return block;
diff --git a/dart_compressed.js b/dart_compressed.js
index 3ab9772e..bd2a74c9 100644
--- a/dart_compressed.js
+++ b/dart_compressed.js
@@ -6,7 +6,7 @@
 Blockly.Dart=new Blockly.Generator("Dart");Blockly.Dart.addReservedWords("assert,break,case,catch,class,const,continue,default,do,else,enum,extends,false,final,finally,for,if,in,is,new,null,rethrow,return,super,switch,this,throw,true,try,var,void,while,with,print,identityHashCode,identical,BidirectionalIterator,Comparable,double,Function,int,Invocation,Iterable,Iterator,List,Map,Match,num,Pattern,RegExp,Set,StackTrace,String,StringSink,Type,bool,DateTime,Deprecated,Duration,Expando,Null,Object,RuneIterator,Runes,Stopwatch,StringBuffer,Symbol,Uri,Comparator,AbstractClassInstantiationError,ArgumentError,AssertionError,CastError,ConcurrentModificationError,CyclicInitializationError,Error,Exception,FallThroughError,FormatException,IntegerDivisionByZeroException,NoSuchMethodError,NullThrownError,OutOfMemoryError,RangeError,StackOverflowError,StateError,TypeError,UnimplementedError,UnsupportedError");
 Blockly.Dart.ORDER_ATOMIC=0;Blockly.Dart.ORDER_UNARY_POSTFIX=1;Blockly.Dart.ORDER_UNARY_PREFIX=2;Blockly.Dart.ORDER_MULTIPLICATIVE=3;Blockly.Dart.ORDER_ADDITIVE=4;Blockly.Dart.ORDER_SHIFT=5;Blockly.Dart.ORDER_BITWISE_AND=6;Blockly.Dart.ORDER_BITWISE_XOR=7;Blockly.Dart.ORDER_BITWISE_OR=8;Blockly.Dart.ORDER_RELATIONAL=9;Blockly.Dart.ORDER_EQUALITY=10;Blockly.Dart.ORDER_LOGICAL_AND=11;Blockly.Dart.ORDER_LOGICAL_OR=12;Blockly.Dart.ORDER_IF_NULL=13;Blockly.Dart.ORDER_CONDITIONAL=14;
 Blockly.Dart.ORDER_CASCADE=15;Blockly.Dart.ORDER_ASSIGNMENT=16;Blockly.Dart.ORDER_NONE=99;
-Blockly.Dart.init=function(a){Blockly.Dart.definitions_=Object.create(null);Blockly.Dart.functionNames_=Object.create(null);Blockly.Dart.variableDB_?Blockly.Dart.variableDB_.reset():Blockly.Dart.variableDB_=new Blockly.Names(Blockly.Dart.RESERVED_WORDS_);var e=[];a=a.variableList;if(a.length){for(var b=0;b<a.length;b++)e[b]=Blockly.Dart.variableDB_.getName(a[b],Blockly.Variables.NAME_TYPE);Blockly.Dart.definitions_.variables="var "+e.join(", ")+";"}};
+Blockly.Dart.init=function(a){Blockly.Dart.definitions_=Object.create(null);Blockly.Dart.functionNames_=Object.create(null);Blockly.Dart.variableDB_?Blockly.Dart.variableDB_.reset():Blockly.Dart.variableDB_=new Blockly.Names(Blockly.Dart.RESERVED_WORDS_);var e=[];a=a.getAllVariables();if(a.length){for(var b=0;b<a.length;b++)e[b]=Blockly.Dart.variableDB_.getName(a[b].name,Blockly.Variables.NAME_TYPE);Blockly.Dart.definitions_.variables="var "+e.join(", ")+";"}};
 Blockly.Dart.finish=function(a){a&&(a=Blockly.Dart.prefixLines(a,Blockly.Dart.INDENT));a="main() {\n"+a+"}";var e=[],b=[],d;for(d in Blockly.Dart.definitions_){var c=Blockly.Dart.definitions_[d];c.match(/^import\s/)?e.push(c):b.push(c)}delete Blockly.Dart.definitions_;delete Blockly.Dart.functionNames_;Blockly.Dart.variableDB_.reset();return(e.join("\n")+"\n\n"+b.join("\n\n")).replace(/\n\n+/g,"\n\n").replace(/\n*$/,"\n\n\n")+a};Blockly.Dart.scrubNakedValue=function(a){return a+";\n"};
 Blockly.Dart.quote_=function(a){a=a.replace(/\\/g,"\\\\").replace(/\n/g,"\\\n").replace(/\$/g,"\\$").replace(/'/g,"\\'");return"'"+a+"'"};
 Blockly.Dart.scrub_=function(a,e){var b="";if(!a.outputConnection||!a.outputConnection.targetConnection){var d=a.getCommentText();(d=Blockly.utils.wrap(d,Blockly.Dart.COMMENT_WRAP-3))&&(b=a.getProcedureDef?b+Blockly.Dart.prefixLines(d+"\n","/// "):b+Blockly.Dart.prefixLines(d+"\n","// "));for(var c=0;c<a.inputList.length;c++)a.inputList[c].type==Blockly.INPUT_VALUE&&(d=a.inputList[c].connection.targetBlock())&&(d=Blockly.Dart.allNestedComments(d))&&(b+=Blockly.Dart.prefixLines(d,"// "))}c=a.nextConnection&&
diff --git a/generators/dart.js b/generators/dart.js
index 295d9002..83ef76cd 100644
--- a/generators/dart.js
+++ b/generators/dart.js
@@ -106,7 +106,7 @@ Blockly.Dart.init = function(workspace) {
   var variables = workspace.getAllVariables();
   if (variables.length) {
     for (var i = 0; i < variables.length; i++) {
-      defvars[i] = Blockly.Dart.variableDB_.getName(variables[i],
+      defvars[i] = Blockly.Dart.variableDB_.getName(variables[i].name,
           Blockly.Variables.NAME_TYPE);
     }
     Blockly.Dart.definitions_['variables'] =
diff --git a/generators/javascript.js b/generators/javascript.js
index b641d2dc..00b96bcc 100644
--- a/generators/javascript.js
+++ b/generators/javascript.js
@@ -156,7 +156,7 @@ Blockly.JavaScript.init = function(workspace) {
   var variables = workspace.getAllVariables();
   if (variables.length) {
     for (var i = 0; i < variables.length; i++) {
-      defvars[i] = Blockly.JavaScript.variableDB_.getName(variables[i],
+      defvars[i] = Blockly.JavaScript.variableDB_.getName(variables[i].name,
           Blockly.Variables.NAME_TYPE);
     }
     Blockly.JavaScript.definitions_['variables'] =
diff --git a/generators/python.js b/generators/python.js
index 3babbde6..e81a0423 100644
--- a/generators/python.js
+++ b/generators/python.js
@@ -163,7 +163,7 @@ Blockly.Python.init = function(workspace) {
   var defvars = [];
   var variables = workspace.getAllVariables();
   for (var i = 0; i < variables.length; i++) {
-    defvars[i] = Blockly.Python.variableDB_.getName(variables[i],
+    defvars[i] = Blockly.Python.variableDB_.getName(variables[i].name,
         Blockly.Variables.NAME_TYPE) + ' = None';
   }
   Blockly.Python.definitions_['variables'] = defvars.join('\n');
diff --git a/javascript_compressed.js b/javascript_compressed.js
index 46a840b1..729ff4f8 100644
--- a/javascript_compressed.js
+++ b/javascript_compressed.js
@@ -9,7 +9,7 @@ Blockly.JavaScript.ORDER_DIVISION=5.1;Blockly.JavaScript.ORDER_MULTIPLICATION=5.
 Blockly.JavaScript.ORDER_LOGICAL_AND=13;Blockly.JavaScript.ORDER_LOGICAL_OR=14;Blockly.JavaScript.ORDER_CONDITIONAL=15;Blockly.JavaScript.ORDER_ASSIGNMENT=16;Blockly.JavaScript.ORDER_COMMA=17;Blockly.JavaScript.ORDER_NONE=99;
 Blockly.JavaScript.ORDER_OVERRIDES=[[Blockly.JavaScript.ORDER_FUNCTION_CALL,Blockly.JavaScript.ORDER_MEMBER],[Blockly.JavaScript.ORDER_FUNCTION_CALL,Blockly.JavaScript.ORDER_FUNCTION_CALL],[Blockly.JavaScript.ORDER_MEMBER,Blockly.JavaScript.ORDER_MEMBER],[Blockly.JavaScript.ORDER_MEMBER,Blockly.JavaScript.ORDER_FUNCTION_CALL],[Blockly.JavaScript.ORDER_LOGICAL_NOT,Blockly.JavaScript.ORDER_LOGICAL_NOT],[Blockly.JavaScript.ORDER_MULTIPLICATION,Blockly.JavaScript.ORDER_MULTIPLICATION],[Blockly.JavaScript.ORDER_ADDITION,
 Blockly.JavaScript.ORDER_ADDITION],[Blockly.JavaScript.ORDER_LOGICAL_AND,Blockly.JavaScript.ORDER_LOGICAL_AND],[Blockly.JavaScript.ORDER_LOGICAL_OR,Blockly.JavaScript.ORDER_LOGICAL_OR]];
-Blockly.JavaScript.init=function(a){Blockly.JavaScript.definitions_=Object.create(null);Blockly.JavaScript.functionNames_=Object.create(null);Blockly.JavaScript.variableDB_?Blockly.JavaScript.variableDB_.reset():Blockly.JavaScript.variableDB_=new Blockly.Names(Blockly.JavaScript.RESERVED_WORDS_);var d=[];a=a.variableList;if(a.length){for(var b=0;b<a.length;b++)d[b]=Blockly.JavaScript.variableDB_.getName(a[b],Blockly.Variables.NAME_TYPE);Blockly.JavaScript.definitions_.variables="var "+d.join(", ")+
+Blockly.JavaScript.init=function(a){Blockly.JavaScript.definitions_=Object.create(null);Blockly.JavaScript.functionNames_=Object.create(null);Blockly.JavaScript.variableDB_?Blockly.JavaScript.variableDB_.reset():Blockly.JavaScript.variableDB_=new Blockly.Names(Blockly.JavaScript.RESERVED_WORDS_);var d=[];a=a.getAllVariables();if(a.length){for(var b=0;b<a.length;b++)d[b]=Blockly.JavaScript.variableDB_.getName(a[b].name,Blockly.Variables.NAME_TYPE);Blockly.JavaScript.definitions_.variables="var "+d.join(", ")+
 ";"}};Blockly.JavaScript.finish=function(a){var d=[],b;for(b in Blockly.JavaScript.definitions_)d.push(Blockly.JavaScript.definitions_[b]);delete Blockly.JavaScript.definitions_;delete Blockly.JavaScript.functionNames_;Blockly.JavaScript.variableDB_.reset();return d.join("\n\n")+"\n\n\n"+a};Blockly.JavaScript.scrubNakedValue=function(a){return a+";\n"};Blockly.JavaScript.quote_=function(a){a=a.replace(/\\/g,"\\\\").replace(/\n/g,"\\\n").replace(/'/g,"\\'");return"'"+a+"'"};
 Blockly.JavaScript.scrub_=function(a,d){var b="";if(!a.outputConnection||!a.outputConnection.targetConnection){var e=a.getCommentText();(e=Blockly.utils.wrap(e,Blockly.JavaScript.COMMENT_WRAP-3))&&(b=a.getProcedureDef?b+("/**\n"+Blockly.JavaScript.prefixLines(e+"\n"," * ")+" */\n"):b+Blockly.JavaScript.prefixLines(e+"\n","// "));for(var c=0;c<a.inputList.length;c++)a.inputList[c].type==Blockly.INPUT_VALUE&&(e=a.inputList[c].connection.targetBlock())&&(e=Blockly.JavaScript.allNestedComments(e))&&(b+=
 Blockly.JavaScript.prefixLines(e,"// "))}c=a.nextConnection&&a.nextConnection.targetBlock();c=Blockly.JavaScript.blockToCode(c);return b+d+c};
diff --git a/media/click.mp3 b/media/click.mp3
new file mode 100644
index 00000000..0c1b05cd
Binary files /dev/null and b/media/click.mp3 differ
diff --git a/media/click.ogg b/media/click.ogg
new file mode 100644
index 00000000..37535b86
Binary files /dev/null and b/media/click.ogg differ
diff --git a/media/delete.mp3 b/media/delete.mp3
new file mode 100644
index 00000000..a937a381
Binary files /dev/null and b/media/delete.mp3 differ
diff --git a/media/delete.ogg b/media/delete.ogg
new file mode 100644
index 00000000..e123af6b
Binary files /dev/null and b/media/delete.ogg differ
diff --git a/msg/js/en.js b/msg/js/en.js
index 656f6fd9..f4c4fc20 100644
--- a/msg/js/en.js
+++ b/msg/js/en.js
@@ -310,6 +310,7 @@ Blockly.Msg.PROCEDURES_MUTATORARG_TITLE = "input name:";
 Blockly.Msg.PROCEDURES_MUTATORARG_TOOLTIP = "Add an input to the function.";
 Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TITLE = "inputs";
 Blockly.Msg.PROCEDURES_MUTATORCONTAINER_TOOLTIP = "Add, remove, or reorder inputs to this function.";
+Blockly.Msg.PROCEDURE_ALREADY_EXISTS = "A procedure named '%1' already exists.";
 Blockly.Msg.REDO = "Redo";
 Blockly.Msg.REMOVE_COMMENT = "Remove Comment";
 Blockly.Msg.RENAME_VARIABLE = "Rename variable...";
diff --git a/msg/json/constants.json b/msg/json/constants.json
new file mode 100644
index 00000000..5b501035
--- /dev/null
+++ b/msg/json/constants.json
@@ -0,0 +1 @@
+{"MATH_HUE": "230", "LOOPS_HUE": "120", "LISTS_HUE": "260", "LOGIC_HUE": "210", "VARIABLES_HUE": "330", "TEXTS_HUE": "160", "PROCEDURES_HUE": "290", "COLOUR_HUE": "20"}
\ No newline at end of file
diff --git a/msg/json/en.json b/msg/json/en.json
index 7581b38b..bcb80266 100644
--- a/msg/json/en.json
+++ b/msg/json/en.json
@@ -1,7 +1,7 @@
 {
 	"@metadata": {
 		"author": "Ellen Spertus <ellen.spertus@gmail.com>",
-		"lastupdated": "2017-04-04 17:16:42.193032",
+		"lastupdated": "2017-05-25 07:58:37.810709",
 		"locale": "en",
 		"messagedocumentation" : "qqq"
 	},
@@ -31,6 +31,7 @@
 	"NEW_VARIABLE": "Create variable...",
 	"NEW_VARIABLE_TITLE": "New variable name:",
 	"VARIABLE_ALREADY_EXISTS": "A variable named '%1' already exists.",
+	"PROCEDURE_ALREADY_EXISTS": "A procedure named '%1' already exists.",
 	"DELETE_VARIABLE_CONFIRMATION": "Delete %1 uses of the '%2' variable?",
 	"CANNOT_DELETE_VARIABLE_PROCEDURE": "Can't delete the variable '%1' because it's part of the definition of the function '%2'",
 	"DELETE_VARIABLE": "Delete the '%1' variable",
diff --git a/python_compressed.js b/python_compressed.js
index 41bacd54..459dea12 100644
--- a/python_compressed.js
+++ b/python_compressed.js
@@ -7,7 +7,7 @@ Blockly.Python=new Blockly.Generator("Python");Blockly.Python.addReservedWords("
 Blockly.Python.ORDER_ATOMIC=0;Blockly.Python.ORDER_COLLECTION=1;Blockly.Python.ORDER_STRING_CONVERSION=1;Blockly.Python.ORDER_MEMBER=2.1;Blockly.Python.ORDER_FUNCTION_CALL=2.2;Blockly.Python.ORDER_EXPONENTIATION=3;Blockly.Python.ORDER_UNARY_SIGN=4;Blockly.Python.ORDER_BITWISE_NOT=4;Blockly.Python.ORDER_MULTIPLICATIVE=5;Blockly.Python.ORDER_ADDITIVE=6;Blockly.Python.ORDER_BITWISE_SHIFT=7;Blockly.Python.ORDER_BITWISE_AND=8;Blockly.Python.ORDER_BITWISE_XOR=9;Blockly.Python.ORDER_BITWISE_OR=10;
 Blockly.Python.ORDER_RELATIONAL=11;Blockly.Python.ORDER_LOGICAL_NOT=12;Blockly.Python.ORDER_LOGICAL_AND=13;Blockly.Python.ORDER_LOGICAL_OR=14;Blockly.Python.ORDER_CONDITIONAL=15;Blockly.Python.ORDER_LAMBDA=16;Blockly.Python.ORDER_NONE=99;
 Blockly.Python.ORDER_OVERRIDES=[[Blockly.Python.ORDER_FUNCTION_CALL,Blockly.Python.ORDER_MEMBER],[Blockly.Python.ORDER_FUNCTION_CALL,Blockly.Python.ORDER_FUNCTION_CALL],[Blockly.Python.ORDER_MEMBER,Blockly.Python.ORDER_MEMBER],[Blockly.Python.ORDER_MEMBER,Blockly.Python.ORDER_FUNCTION_CALL],[Blockly.Python.ORDER_LOGICAL_NOT,Blockly.Python.ORDER_LOGICAL_NOT],[Blockly.Python.ORDER_LOGICAL_AND,Blockly.Python.ORDER_LOGICAL_AND],[Blockly.Python.ORDER_LOGICAL_OR,Blockly.Python.ORDER_LOGICAL_OR]];
-Blockly.Python.init=function(a){Blockly.Python.PASS=this.INDENT+"pass\n";Blockly.Python.definitions_=Object.create(null);Blockly.Python.functionNames_=Object.create(null);Blockly.Python.variableDB_?Blockly.Python.variableDB_.reset():Blockly.Python.variableDB_=new Blockly.Names(Blockly.Python.RESERVED_WORDS_);var e=[];a=a.variableList;for(var b=0;b<a.length;b++)e[b]=Blockly.Python.variableDB_.getName(a[b],Blockly.Variables.NAME_TYPE)+" = None";Blockly.Python.definitions_.variables=e.join("\n")};
+Blockly.Python.init=function(a){Blockly.Python.PASS=this.INDENT+"pass\n";Blockly.Python.definitions_=Object.create(null);Blockly.Python.functionNames_=Object.create(null);Blockly.Python.variableDB_?Blockly.Python.variableDB_.reset():Blockly.Python.variableDB_=new Blockly.Names(Blockly.Python.RESERVED_WORDS_);var e=[];a=a.getAllVariables();for(var b=0;b<a.length;b++)e[b]=Blockly.Python.variableDB_.getName(a[b].name,Blockly.Variables.NAME_TYPE)+" = None";Blockly.Python.definitions_.variables=e.join("\n")};
 Blockly.Python.finish=function(a){var e=[],b=[],c;for(c in Blockly.Python.definitions_){var d=Blockly.Python.definitions_[c];d.match(/^(from\s+\S+\s+)?import\s+\S+/)?e.push(d):b.push(d)}delete Blockly.Python.definitions_;delete Blockly.Python.functionNames_;Blockly.Python.variableDB_.reset();return(e.join("\n")+"\n\n"+b.join("\n\n")).replace(/\n\n+/g,"\n\n").replace(/\n*$/,"\n\n\n")+a};Blockly.Python.scrubNakedValue=function(a){return a+"\n"};
 Blockly.Python.quote_=function(a){a=a.replace(/\\/g,"\\\\").replace(/\n/g,"\\\n").replace(/\%/g,"\\%");var e="'";-1!==a.indexOf("'")&&(-1===a.indexOf('"')?e='"':a=a.replace(/'/g,"\\'"));return e+a+e};
 Blockly.Python.scrub_=function(a,e){var b="";if(!a.outputConnection||!a.outputConnection.targetConnection){var c=a.getCommentText();(c=Blockly.utils.wrap(c,Blockly.Python.COMMENT_WRAP-3))&&(b=a.getProcedureDef?b+('"""'+c+'\n"""\n'):b+Blockly.Python.prefixLines(c+"\n","# "));for(var d=0;d<a.inputList.length;d++)a.inputList[d].type==Blockly.INPUT_VALUE&&(c=a.inputList[d].connection.targetBlock())&&(c=Blockly.Python.allNestedComments(c))&&(b+=Blockly.Python.prefixLines(c,"# "))}d=a.nextConnection&&a.nextConnection.targetBlock();
diff --git a/scripts/get_chromedriver.sh b/scripts/get_chromedriver.sh
index a16854dd..a0fbf3b7 100755
--- a/scripts/get_chromedriver.sh
+++ b/scripts/get_chromedriver.sh
@@ -5,6 +5,8 @@ if [ ! -d $chromedriver_dir ]; then
   mkdir $chromedriver_dir
 fi
 
+echo "downloading chromedriver"
+
 if [[ $os_name == 'Linux' && ! -f $chromedriver_dir/chromedriver ]]; then
   cd chromedriver  && curl -L https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip > tmp.zip &&  unzip -o tmp.zip && rm tmp.zip
   # wait until download finish
diff --git a/tests/jsunit/workspace_test.js b/tests/jsunit/workspace_test.js
index ac2d78f2..bf4618a8 100644
--- a/tests/jsunit/workspace_test.js
+++ b/tests/jsunit/workspace_test.js
@@ -62,12 +62,12 @@ function workspaceTest_tearDownWithMockBlocks() {
 
 /**
  * Create a test get_var_block.
- * @param {?string} variable The string to put into the variable field.
+ * @param {?string} variable_name The string to put into the variable field.
  * @return {!Blockly.Block} The created block.
  */
-function createMockBlock(variable) {
+function createMockBlock(variable_name) {
   var block = new Blockly.Block(workspace, 'get_var_block');
-  block.inputList[0].fieldRow[0].setValue(variable);
+  block.inputList[0].fieldRow[0].setValue(variable_name);
   return block;
 }
 
diff --git a/tests/jsunit/xml_test.js b/tests/jsunit/xml_test.js
index d77ed934..835ffbd4 100644
--- a/tests/jsunit/xml_test.js
+++ b/tests/jsunit/xml_test.js
@@ -19,6 +19,12 @@
  */
 'use strict';
 
+goog.require('goog.testing');
+goog.require('goog.testing.MockControl');
+
+var mockControl_;
+var saved_msg = Blockly.Msg.DELETE_VARIABLE;
+var workspace;
 var XML_TEXT = ['<xml xmlns="http://www.w3.org/1999/xhtml">',
   '  <block type="controls_repeat_ext" inline="true" x="21" y="23">',
   '    <value name="TIMES">',
@@ -46,6 +52,97 @@ var XML_TEXT = ['<xml xmlns="http://www.w3.org/1999/xhtml">',
   '  </block>',
   '</xml>'].join('\n');
 
+function xmlTest_setUp() {
+  workspace = new Blockly.Workspace();
+  mockControl_ = new goog.testing.MockControl();
+}
+
+function xmlTest_setUpWithMockBlocks() {
+  xmlTest_setUp();
+  Blockly.defineBlocksWithJsonArray([{
+    'type': 'field_variable_test_block',
+    'message0': '%1',
+    'args0': [
+      {
+        'type': 'field_variable',
+        'name': 'VAR',
+        'variable': 'item'
+      }
+    ],
+  }]);
+  // Need to define this because field_variable's dropdownCreate() calls replace
+  // on undefined value, Blockly.Msg.DELETE_VARIABLE. To fix this, define
+  // Blockly.Msg.DELETE_VARIABLE as %1 so the replace function finds the %1 it
+  // expects.
+  Blockly.Msg.DELETE_VARIABLE = '%1';
+}
+
+function xmlTest_tearDown() {
+  mockControl_.$tearDown();
+  workspace.dispose();
+}
+
+function xmlTest_tearDownWithMockBlocks() {
+  xmlTest_tearDown();
+  delete Blockly.Blocks.field_variable_test_block;
+  Blockly.Msg.DELETE_VARIABLE = saved_msg;
+}
+
+/**
+ * Check the values of the non variable field dom.
+ * @param {!Element} fieldDom The xml dom of the non variable field.
+ * @param {!string} name The expected name of the variable.
+ * @param {!string} text The expected text of the variable.
+ */
+function xmlTest_checkNonVariableField(fieldDom, name, text) {
+  assertEquals(text, fieldDom.textContent);
+  assertEquals(name, fieldDom.getAttribute('name'));
+  assertNull(fieldDom.getAttribute('id'));
+  assertNull(fieldDom.getAttribute('variableType'));
+}
+
+/**
+ * Check the values of the variable field DOM.
+ * @param {!Element} fieldDom The xml dom of the variable field.
+ * @param {!string} name The expected name of the variable.
+ * @param {!string} type The expected type of the variable.
+ * @param {!string} id The expected id of the variable.
+ * @param {!string} text The expected text of the variable.
+ */
+function xmlTest_checkVariableFieldDomValues(fieldDom, name, type, id, text) {
+  assertEquals(name, fieldDom.getAttribute('name'));
+  assertEquals(type, fieldDom.getAttribute('variableType'));
+  assertEquals(id, fieldDom.getAttribute('id'));
+  assertEquals(text, fieldDom.textContent);
+}
+
+/**
+ * Check the values of the variable DOM.
+ * @param {!Element} variableDom The xml dom of the variable.
+ * @param {!string} type The expected type of the variable.
+ * @param {!string} id The expected id of the variable.
+ * @param {!string} text The expected text of the variable.
+ */
+function xmlTest_checkVariableDomValues(variableDom, type, id, text) {
+  assertEquals(type, variableDom.getAttribute('type'));
+  assertEquals(id, variableDom.getAttribute('id'));
+  assertEquals(text, variableDom.textContent);
+}
+
+/**
+ * Check if a variable with the given values exists.
+ * @param {!string} name The expected name of the variable.
+ * @param {!string} type The expected type of the variable.
+ * @param {!string} id The expected id of the variable.
+ */
+function xmlTest_checkVariableValues(name, type, id) {
+  var variable = workspace.getVariable(name);
+  assertNotUndefined(variable);
+  assertEquals(name, variable.name);
+  assertEquals(type, variable.type);
+  assertEquals(id, variable.getId());
+}
+
 function test_textToDom() {
   var dom = Blockly.Xml.textToDom(XML_TEXT);
   assertEquals('XML tag', 'xml', dom.nodeName);
@@ -59,7 +156,131 @@ function test_domToText() {
       text.replace(/\s+/g, ''));
 }
 
-function test_domToWorkspace() {
+function test_domToWorkspace_BackwardCompatibility() {
+  // Expect that workspace still loads without serialized variables.
+  xmlTest_setUpWithMockBlocks();
+  var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid');
+  mockGenUid().$returns('1');
+  mockGenUid().$returns('1');
+  mockGenUid().$replay();
+  try {
+    var dom = Blockly.Xml.textToDom(
+        '<xml>' +
+        '  <block type="field_variable_test_block" id="block_id">' +
+        '    <field name="VAR">name1</field>' +
+        '  </block>' +
+        '</xml>');
+    Blockly.Xml.domToWorkspace(dom, workspace);
+    assertEquals('Block count', 1, workspace.getAllBlocks().length);
+    xmlTest_checkVariableValues('name1', '', '1');
+  } finally {
+    xmlTest_tearDownWithMockBlocks();
+  }
+}
+
+function test_domToWorkspace_VariablesAtTop() {
+  // Expect that unused variables are preserved.
+  xmlTest_setUpWithMockBlocks();
+  try {
+    var dom = Blockly.Xml.textToDom(
+        '<xml>' +
+        '  <variables>' +
+        '    <variable type="type1" id="id1">name1</variable>' +
+        '    <variable type="type2" id="id2">name2</variable>' +
+        '    <variable type="" id="id3">name3</variable>' +
+        '  </variables>' +
+        '  <block type="field_variable_test_block">' +
+        '    <field name="VAR" id="id3" variabletype="">name3</field>' +
+        '  </block>' +
+        '</xml>');
+    Blockly.Xml.domToWorkspace(dom, workspace);
+    assertEquals('Block count', 1, workspace.getAllBlocks().length);
+    xmlTest_checkVariableValues('name1', 'type1', 'id1');
+    xmlTest_checkVariableValues('name2', 'type2', 'id2');
+    xmlTest_checkVariableValues('name3', '', 'id3');
+  } finally {
+    xmlTest_tearDownWithMockBlocks();
+  }
+}
+
+function test_domToWorkspace_VariablesAtTop_DuplicateVariablesTag() {
+  // Expect thrown Error because of duplicate 'variables' tag
+  xmlTest_setUpWithMockBlocks();
+  try {
+    var dom = Blockly.Xml.textToDom(
+        '<xml>' +
+        '  <variables>' +
+        '  </variables>' +
+        '  <variables>' +
+        '  </variables>' +
+        '</xml>');
+    Blockly.Xml.domToWorkspace(dom, workspace);
+    fail();
+  }
+  catch (e) {
+    // expected
+  } finally {
+    xmlTest_tearDownWithMockBlocks();
+  }
+}
+
+function test_domToWorkspace_VariablesAtTop_MissingType() {
+  // Expect thrown error when a variable tag is missing the type attribute.
+  workspace = new Blockly.Workspace();
+  try {
+    var dom = Blockly.Xml.textToDom(
+        '<xml>' +
+        '  <variables>' +
+        '    <variable id="id1">name1</variable>' +
+        '  </variables>' +
+        '  <block type="field_variable_test_block">' +
+        '    <field name="VAR" id="id1" variabletype="">name3</field>' +
+        '  </block>' +
+        '</xml>');
+    Blockly.Xml.domToWorkspace(dom, workspace);
+    fail();
+  } catch (e) {
+    // expected
+  } finally {
+    workspace.dispose();
+  }
+}
+
+function test_domToWorkspace_VariablesAtTop_MismatchBlockType() {
+  // Expect thrown error when the serialized type of a variable does not match
+  // the type of a variable field that references it.
+  xmlTest_setUpWithMockBlocks();
+  try {
+    var dom = Blockly.Xml.textToDom(
+        '<xml>' +
+        '  <variables>' +
+        '    <variable type="type1" id="id1">name1</variable>' +
+        '  </variables>' +
+        '  <block type="field_variable_test_block">' +
+        '    <field name="VAR" id="id1" variabletype="">name1</field>' +
+        '  </block>' +
+        '</xml>');
+    Blockly.Xml.domToWorkspace(dom, workspace);
+    fail();
+  } catch (e) {
+    // expected
+  } finally {
+    xmlTest_tearDownWithMockBlocks();
+  }
+}
+
+function test_domToPrettyText() {
+  var dom = Blockly.Xml.textToDom(XML_TEXT);
+  var text = Blockly.Xml.domToPrettyText(dom);
+  assertEquals('Round trip', XML_TEXT.replace(/\s+/g, ''),
+      text.replace(/\s+/g, ''));
+}
+
+/**
+ * Tests the that appendDomToWorkspace works in a headless mode.
+ * Also see test_appendDomToWorkspace() in workspace_svg_test.js.
+ */
+function test_appendDomToWorkspace() {
   Blockly.Blocks.test_block = {
     init: function() {
       this.jsonInit({
@@ -75,20 +296,101 @@ function test_domToWorkspace() {
         '  <block type="test_block" inline="true" x="21" y="23">' +
         '  </block>' +
         '</xml>');
-    Blockly.Xml.domToWorkspace(dom, workspace);
+    workspace = new Blockly.Workspace();
+    Blockly.Xml.appendDomToWorkspace(dom, workspace);
     assertEquals('Block count', 1, workspace.getAllBlocks().length);
+    var newBlockIds = Blockly.Xml.appendDomToWorkspace(dom, workspace);
+    assertEquals('Block count', 2, workspace.getAllBlocks().length);
+    assertEquals('Number of new block ids',1,newBlockIds.length);
   } finally {
     delete Blockly.Blocks.test_block;
-
     workspace.dispose();
   }
 }
 
-function test_domToPrettyText() {
-  var dom = Blockly.Xml.textToDom(XML_TEXT);
-  var text = Blockly.Xml.domToPrettyText(dom);
-  assertEquals('Round trip', XML_TEXT.replace(/\s+/g, ''),
-      text.replace(/\s+/g, ''));
+function test_blockToDom_fieldToDom_trivial() {
+  xmlTest_setUpWithMockBlocks()
+  workspace.createVariable('name1', 'type1', 'id1');
+  var block = new Blockly.Block(workspace, 'field_variable_test_block');
+  block.inputList[0].fieldRow[0].setValue('name1');
+  var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
+  xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', 'type1', 'id1', 'name1')
+  xmlTest_tearDownWithMockBlocks()
+}
+
+function test_blockToDom_fieldToDom_defaultCase() {
+  xmlTest_setUpWithMockBlocks()
+  var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid');
+  mockGenUid().$returns('1');
+  mockGenUid().$replay();
+  workspace.createVariable('name1');
+  var block = new Blockly.Block(workspace, 'field_variable_test_block');
+  block.inputList[0].fieldRow[0].setValue('name1');
+  var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
+  // Expect type is '' and id is '1' since we don't specify type and id.
+  xmlTest_checkVariableFieldDomValues(resultFieldDom, 'VAR', '', '1', 'name1')
+  xmlTest_tearDownWithMockBlocks()
+}
+
+function test_blockToDom_fieldToDom_notAFieldVariable() {
+  Blockly.defineBlocksWithJsonArray([{
+    "type": "field_angle_test_block",
+    "message0": "%1",
+    "args0": [
+      {
+        "type": "field_angle",
+        "name": "VAR",
+        "angle": 90
+      }
+    ],
+  }]);
+  xmlTest_setUpWithMockBlocks()
+  var block = new Blockly.Block(workspace, 'field_angle_test_block');
+  var resultFieldDom = Blockly.Xml.blockToDom(block).childNodes[0];
+  xmlTest_checkNonVariableField(resultFieldDom, 'VAR', '90');
+  delete Blockly.Blocks.field_angle_block;
+  xmlTest_tearDownWithMockBlocks()
+}
+
+function test_variablesToDom_oneVariable() {
+  xmlTest_setUp();
+  var mockGenUid = mockControl_.createMethodMock(Blockly.utils, 'genUid');
+  mockGenUid().$returns('1');
+  mockGenUid().$replay();
+
+  workspace.createVariable('name1');
+  var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables());
+  assertEquals(1, resultDom.children.length);
+  var resultVariableDom = resultDom.children[0];
+  assertEquals('name1', resultVariableDom.textContent);
+  assertEquals('', resultVariableDom.getAttribute('type'));
+  assertEquals('1', resultVariableDom.getAttribute('id'));
+  xmlTest_tearDown();
+}
+
+function test_variablesToDom_twoVariables_oneBlock() {
+  xmlTest_setUpWithMockBlocks();
+
+  workspace.createVariable('name1', 'type1', 'id1');
+  workspace.createVariable('name2', 'type2', 'id2');
+  var block = new Blockly.Block(workspace, 'field_variable_test_block');
+  block.inputList[0].fieldRow[0].setValue('name1');
+
+  var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables());
+  assertEquals(2, resultDom.children.length);
+  xmlTest_checkVariableDomValues(resultDom.children[0], 'type1', 'id1',
+      'name1');
+  xmlTest_checkVariableDomValues(resultDom.children[1], 'type2', 'id2',
+      'name2');
+  xmlTest_tearDownWithMockBlocks();
+}
+
+function test_variablesToDom_noVariables() {
+  xmlTest_setUp();
+  workspace.createVariable('name1');
+  var resultDom = Blockly.Xml.variablesToDom(workspace.getAllVariables());
+  assertEquals(1, resultDom.children.length);
+  xmlTest_tearDown();
 }
 
 /**