Horizontal toolbox layout with positioning at start or end.

This commit is contained in:
rachel-fenichel 2016-03-17 15:46:22 -07:00
parent cea4c0a733
commit 18a1550285
10 changed files with 1045 additions and 191 deletions

View file

@ -445,7 +445,7 @@ Blockly.getMainWorkspaceMetrics_ = function() {
var bottomEdge = topEdge + blockBox.height;
}
var absoluteLeft = 0;
if (!this.RTL && this.toolbox_) {
if (this.toolbox_ && this.toolbox_.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
absoluteLeft = this.toolbox_.width;
}
var metrics = {

View file

@ -162,3 +162,28 @@ Blockly.OPPOSITE_TYPE[Blockly.INPUT_VALUE] = Blockly.OUTPUT_VALUE;
Blockly.OPPOSITE_TYPE[Blockly.OUTPUT_VALUE] = Blockly.INPUT_VALUE;
Blockly.OPPOSITE_TYPE[Blockly.NEXT_STATEMENT] = Blockly.PREVIOUS_STATEMENT;
Blockly.OPPOSITE_TYPE[Blockly.PREVIOUS_STATEMENT] = Blockly.NEXT_STATEMENT;
/**
* ENUM for toolbox and flyout at top of screen.
* @const
*/
Blockly.TOOLBOX_AT_TOP = 0;
/**
* ENUM for toolbox and flyout at bottom of screen.
* @const
*/
Blockly.TOOLBOX_AT_BOTTOM = 1;
/**
* ENUM for toolbox and flyout at left of screen.
* @const
*/
Blockly.TOOLBOX_AT_LEFT = 2;
/**
* ENUM for toolbox and flyout at right of screen.
* @const
*/
Blockly.TOOLBOX_AT_RIGHT = 3;

View file

@ -430,6 +430,16 @@ Blockly.Css.CONTENT = [
'white-space: nowrap;',
'}',
'.blocklyHorizontalTree {',
'float: left;',
'margin: 1px 5px 8px 0px;',
'}',
'.blocklyHorizontalTreeRtl {',
'float: right;',
'margin: 1px 0px 8px 5px;',
'}',
'.blocklyToolboxDiv[dir="RTL"] .blocklyTreeRow {',
'margin-left: 8px;',
'}',
@ -444,6 +454,14 @@ Blockly.Css.CONTENT = [
'margin: 5px 0;',
'}',
'.blocklyTreeSeparatorHorizontal {',
'border-right: solid #e5e5e5 1px;',
'width: 0px;',
'padding: 5px 0;',
'margin: 0 5px;',
'}',
'.blocklyTreeIcon {',
'background-image: url(<<<PATH>>>/sprites.png);',
'height: 16px;',

View file

@ -56,6 +56,18 @@ Blockly.Flyout = function(workspaceOptions) {
*/
this.RTL = !!workspaceOptions.RTL;
/**
* Flyout should be laid out horizontally vs vertically.
* @type {boolean}
*/
this.horizontalLayout_ = workspaceOptions.horizontalLayout;
/**
* Position of the toolbox and flyout relative to the workspace.
* @type {number}
*/
this.toolboxPosition_ = workspaceOptions.toolboxPosition;
/**
* Opaque data that can be passed to Blockly.unbindEvent_.
* @type {!Array.<!Array>}
@ -120,6 +132,13 @@ Blockly.Flyout.prototype.width_ = 0;
*/
Blockly.Flyout.prototype.height_ = 0;
/**
* Vertical offset of flyout.
* @type {number}
* @private
*/
Blockly.Flyout.prototype.verticalOffset_ = 0;
/**
* Creates the flyout's DOM. Only needs to be called once.
* @return {!Element} The flyout's SVG group.
@ -148,7 +167,8 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) {
this.targetWorkspace_ = targetWorkspace;
this.workspace_.targetWorkspace = targetWorkspace;
// Add scrollbar.
this.scrollbar_ = new Blockly.Scrollbar(this.workspace_, false, false);
this.scrollbar_ = new Blockly.Scrollbar(this.workspace_,
this.horizontalLayout_, false);
this.hide();
@ -197,9 +217,12 @@ Blockly.Flyout.prototype.dispose = function() {
* .viewHeight: Height of the visible rectangle,
* .viewWidth: Width of the visible rectangle,
* .contentHeight: Height of the contents,
* .contentWidth: Width of the contents,
* .viewTop: Offset of top edge of visible rectangle from parent,
* .contentTop: Offset of the top-most content from the y=0 coordinate,
* .absoluteTop: Top-edge of view.
* .viewLeft: Offset of the left edge of visible rectangle from parent,
* .contentLeft: Offset of the left-most content from the x=0 coordinate,
* .absoluteLeft: Left-edge of view.
* @return {Object} Contains size and position metrics of the flyout.
* @private
@ -209,44 +232,77 @@ Blockly.Flyout.prototype.getMetrics_ = function() {
// Flyout is hidden.
return null;
}
var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
var viewWidth = this.width_;
try {
var optionBox = this.workspace_.getCanvas().getBBox();
} catch (e) {
// Firefox has trouble with hidden elements (Bug 528969).
var optionBox = {height: 0, y: 0};
}
return {
var absoluteTop = this.verticalOffset_ + this.SCROLLBAR_PADDING
if (this.horizontalLayout_) {
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
absoluteTop = 0;
}
var viewHeight = this.height_;
var viewWidth = this.width_ - 2 * this.SCROLLBAR_PADDING;
} else {
var viewHeight = this.height_ - 2 * this.SCROLLBAR_PADDING;
var viewWidth = this.width_;
}
var metrics = {
viewHeight: viewHeight,
viewWidth: viewWidth,
contentHeight: (optionBox.height + optionBox.y) * this.workspace_.scale,
contentHeight: (optionBox.height) * this.workspace_.scale,
contentWidth: (optionBox.width) * this.workspace_.scale,
viewTop: -this.workspace_.scrollY,
contentTop: 0,
absoluteTop: this.SCROLLBAR_PADDING,
absoluteLeft: 0
viewLeft: -this.workspace_.scrollX,
contentTop: optionBox.y,
contentLeft: 0,
absoluteTop: absoluteTop,
absoluteLeft: this.SCROLLBAR_PADDING
};
return metrics;
};
/**
* Sets the Y translation of the flyout to match the scrollbars.
* @param {!Object} yRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling.
* Sets the translation of the flyout to match the scrollbars.
* @param {!Object} xyRatio Contains a y property which is a float
* between 0 and 1 specifying the degree of scrolling and a
* similar x property.
* @private
*/
Blockly.Flyout.prototype.setMetrics_ = function(yRatio) {
Blockly.Flyout.prototype.setMetrics_ = function(xyRatio) {
var metrics = this.getMetrics_();
// This is a fix to an apparent race condition.
if (!metrics) {
return;
}
if (goog.isNumber(yRatio.y)) {
if (!this.horizontalLayout_ && goog.isNumber(xyRatio.y)) {
this.workspace_.scrollY =
-metrics.contentHeight * yRatio.y - metrics.contentTop;
-metrics.contentHeight * xyRatio.y - metrics.contentTop;
} else if (this.horizontalLayout_ && goog.isNumber(xyRatio.x)) {
if (this.RTL) {
this.workspace_.scrollX =
-metrics.contentWidth * xyRatio.x + metrics.contentLeft;
} else {
this.workspace_.scrollX =
-metrics.contentWidth * xyRatio.x - metrics.contentLeft;
}
}
this.workspace_.translate(0, this.workspace_.scrollY + metrics.absoluteTop);
var translateX = this.horizontalLayout_ && this.RTL ?
metrics.absoluteLeft + metrics.viewWidth - this.workspace_.scrollX :
this.workspace_.scrollX + metrics.absoluteLeft;
this.workspace_.translate(translateX,
this.workspace_.scrollY + metrics.absoluteTop);
};
Blockly.Flyout.prototype.setVerticalOffset = function(verticalOffset) {
this.verticalOffset_ = verticalOffset;
}
/**
* Move the toolbox to the edge of the workspace.
*/
@ -259,47 +315,144 @@ Blockly.Flyout.prototype.position = function() {
// Hidden components will return null.
return;
}
var edgeWidth = this.width_ - this.CORNER_RADIUS;
if (this.RTL) {
var edgeWidth = this.horizontalLayout_ ? metrics.viewWidth : this.width_;
edgeWidth -= this.CORNER_RADIUS;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
edgeWidth *= -1;
}
var path = ['M ' + (this.RTL ? this.width_ : 0) + ',0'];
path.push('h', edgeWidth);
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
this.RTL ? 0 : 1,
this.RTL ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
this.CORNER_RADIUS);
path.push('v', Math.max(0, metrics.viewHeight - this.CORNER_RADIUS * 2));
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
this.RTL ? 0 : 1,
this.RTL ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
this.CORNER_RADIUS);
path.push('h', -edgeWidth);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
this.setBackgroundPath_(edgeWidth,
this.horizontalLayout_ ? this.height_ + this.verticalOffset_ : metrics.viewHeight);
var x = metrics.absoluteLeft;
if (this.RTL) {
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
x += metrics.viewWidth;
x -= this.width_;
}
this.svgGroup_.setAttribute('transform',
'translate(' + x + ',' + metrics.absoluteTop + ')');
// Record the height for Blockly.Flyout.getMetrics_.
this.height_ = metrics.viewHeight;
var y = metrics.absoluteTop;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
y += metrics.viewHeight;
y -= this.height_;
}
this.svgGroup_.setAttribute('transform',
'translate(' + x + ',' + y + ')');
// Record the height for Blockly.Flyout.getMetrics_, or width if the layout is
// horizontal.
if (this.horizontalLayout_) {
this.width_ = metrics.viewWidth;
} else {
this.height_ = metrics.viewHeight;
}
// Update the scrollbar (if one exists).
if (this.scrollbar_) {
this.scrollbar_.resize();
}
// The blocks need to be visible in order to be laid out and measured
// correctly, but we don't want the flyout to show up until it's properly
// sized. Opacity is set to zero in show().
this.svgGroup_.style.opacity = 1;
};
/**
* Create and set the path for the visible boundaries of the toolbox.
* @param {number} width The width of the toolbox, not including the
* rounded corners.
* @param {number} height The height of the toolbox, not including
* rounded corners.
* @private
*/
Blockly.Flyout.prototype.setBackgroundPath_ = function(width, height) {
if (this.horizontalLayout_) {
this.setBackgroundPathHorizontal_(width, height);
} else {
this.setBackgroundPathVertical_(width, height);
}
};
/**
* Create and set the path for the visible boundaries of the toolbox in vertical mode.
* @param {number} width The width of the toolbox, not including the
* rounded corners.
* @param {number} height The height of the toolbox, not including
* rounded corners.
* @private
*/
Blockly.Flyout.prototype.setBackgroundPathVertical_ = function(width, height) {
var atRight = this.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT;
// Decide whether to start on the left or right.
var path = ['M ' + (atRight ? this.width_ : 0) + ',0'];
// Top.
path.push('h', width);
// Rounded corner.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
atRight ? 0 : 1,
atRight ? -this.CORNER_RADIUS : this.CORNER_RADIUS,
this.CORNER_RADIUS);
// Side closest to workspace.
path.push('v', Math.max(0, height - this.CORNER_RADIUS * 2));
// Rounded corner.
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0,
atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
this.CORNER_RADIUS);
// Bottom.
path.push('h', -width);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Create and set the path for the visible boundaries of the toolbox in horizontal mode.
* @param {number} width The width of the toolbox, not including the
* rounded corners.
* @param {number} height The height of the toolbox, not including
* rounded corners.
* @private
*/
Blockly.Flyout.prototype.setBackgroundPathHorizontal_ = function(width, height) {
var atTop = this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP;
// start at top left.
var path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
// top
path.push('h', width + this.CORNER_RADIUS);
// right
path.push('v', height);
// bottom
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('h', -1 * (width - this.CORNER_RADIUS));
// left
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
-this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('z');
} else {
// top
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, -this.CORNER_RADIUS);
path.push('h', width - this.CORNER_RADIUS);
// right
path.push('a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, 1,
this.CORNER_RADIUS, this.CORNER_RADIUS);
path.push('v', height - this.CORNER_RADIUS);
// bottom
path.push('h', -width - this.CORNER_RADIUS);
// left
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
Blockly.Flyout.prototype.scrollToStart = function() {
this.scrollbar_.set(0);
this.scrollbar_.set((this.horizontalLayout_ && this.RTL) ? 1000000000 : 0);
};
/**
@ -308,6 +461,10 @@ Blockly.Flyout.prototype.scrollToStart = function() {
* @private
*/
Blockly.Flyout.prototype.wheel_ = function(e) {
// Don't scroll sideways.
if (this.horizontalLayout_) {
return;
}
var delta = e.deltaY;
if (delta) {
if (goog.userAgent.GECKO) {
@ -405,7 +562,14 @@ Blockly.Flyout.prototype.show = function(xmlList) {
}
}
// Lay out the blocks vertically.
// The blocks need to be visible in order to be laid out and measured
// correctly, but we don't want the flyout to show up until it's properly
// sized. Opacity is reset at the end of position().
this.svgGroup_.style.opacity = 0;
this.svgGroup_.style.display = 'block';
// Lay out the blocks.
var cursorX = margin / this.workspace_.scale + Blockly.BlockSvg.TAB_WIDTH;
var cursorY = margin;
for (var i = 0, block; block = blocks[i]; i++) {
var allBlocks = block.getDescendants();
@ -418,10 +582,12 @@ Blockly.Flyout.prototype.show = function(xmlList) {
block.render();
var root = block.getSvgRoot();
var blockHW = block.getHeightWidth();
var x = this.RTL ? 0 : margin / this.workspace_.scale +
Blockly.BlockSvg.TAB_WIDTH;
block.moveBy(x, cursorY);
cursorY += blockHW.height + gaps[i];
block.moveBy((this.horizontalLayout_ && this.RTL) ? -cursorX : cursorX, cursorY);
if (this.horizontalLayout_) {
cursorX += blockHW.width + gaps[i];
} else {
cursorY += blockHW.height + gaps[i];
}
// Create an invisible rectangle under the block to act as a button. Just
// using the block as a button is poor, since blocks have holes in them.
@ -431,6 +597,44 @@ Blockly.Flyout.prototype.show = function(xmlList) {
block.flyoutRect_ = rect;
this.buttons_[i] = rect;
this.addBlockListeners_(root, block, rect);
}
// IE 11 is an incompetant browser that fails to fire mouseout events.
// When the mouse is over the background, deselect all blocks.
var deselectAll = function(e) {
var blocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
block.removeSelect();
}
};
this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover',
this, deselectAll));
if (this.horizontalLayout_) {
this.height_ = 0;
} else {
this.width_ = 0;
}
this.reflow();
this.filterForCapacity_();
// Fire a resize event to update the flyout's scrollbar.
Blockly.fireUiEventNow(window, 'resize');
this.reflowWrapper_ = this.reflow.bind(this);
this.workspace_.addChangeListener(this.reflowWrapper_);
};
/**
* Add listeners to a block that has been added to the flyout.
* @param {Element} root The root node of the SVG group the block is in.
* @param {!Blockly.Block} block The block to add listeners for.
* @param {!Element} rect The invisible rectangle under the block that acts as
* a button for that block.
* @private
*/
Blockly.Flyout.prototype.addBlockListeners_ = function(root, block, rect) {
if (this.autoClose) {
this.listeners_.push(Blockly.bindEvent_(root, 'mousedown', null,
this.createBlockFunc_(block)));
@ -448,76 +652,6 @@ Blockly.Flyout.prototype.show = function(xmlList) {
block.addSelect));
this.listeners_.push(Blockly.bindEvent_(rect, 'mouseout', block,
block.removeSelect));
}
// IE 11 is an incompetant browser that fails to fire mouseout events.
// When the mouse is over the background, deselect all blocks.
var deselectAll = function(e) {
var blocks = this.workspace_.getTopBlocks(false);
for (var i = 0, block; block = blocks[i]; i++) {
block.removeSelect();
}
};
this.listeners_.push(Blockly.bindEvent_(this.svgBackground_, 'mouseover',
this, deselectAll));
this.width_ = 0;
this.reflow();
this.filterForCapacity_();
// Fire a resize event to update the flyout's scrollbar.
Blockly.fireUiEventNow(window, 'resize');
this.reflowWrapper_ = this.reflow.bind(this);
this.workspace_.addChangeListener(this.reflowWrapper_);
};
/**
* Compute width of flyout. Position button under each block.
* For RTL: Lay out the blocks right-aligned.
*/
Blockly.Flyout.prototype.reflow = function() {
this.workspace_.scale = this.targetWorkspace_.scale;
var flyoutWidth = 0;
var margin = this.CORNER_RADIUS;
var blocks = this.workspace_.getTopBlocks(false);
for (var x = 0, block; block = blocks[x]; x++) {
var width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= Blockly.BlockSvg.TAB_WIDTH;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
flyoutWidth += Blockly.BlockSvg.TAB_WIDTH;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += margin * 1.5 + Blockly.Scrollbar.scrollbarThickness;
if (this.width_ != flyoutWidth) {
for (var x = 0, block; block = blocks[x]; x++) {
var blockHW = block.getHeightWidth();
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
var oldX = block.getRelativeToSurfaceXY().x;
var dx = flyoutWidth - margin;
dx /= this.workspace_.scale;
dx -= Blockly.BlockSvg.TAB_WIDTH;
block.moveBy(dx - oldX, 0);
}
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('width', blockHW.width);
block.flyoutRect_.setAttribute('height', blockHW.height);
// Blocks with output tabs are shifted a bit.
var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
block.flyoutRect_.setAttribute('y', blockXY.y);
}
}
// Record the width for .getMetrics_ and .position.
this.width_ = flyoutWidth;
// Fire a resize event to update the flyout's scrollbar.
Blockly.fireUiEvent(window, 'resize');
}
};
/**
@ -578,13 +712,23 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) {
* @private
*/
Blockly.Flyout.prototype.onMouseMove_ = function(e) {
var dy = e.clientY - this.startDragMouseY_;
this.startDragMouseY_ = e.clientY;
var metrics = this.getMetrics_();
var y = metrics.viewTop - dy;
y = Math.min(y, metrics.contentHeight - metrics.viewHeight);
y = Math.max(y, 0);
this.scrollbar_.set(y);
if (this.horizontalLayout_) {
var dx = e.clientX - this.startDragMouseX_;
this.startDragMouseX_ = e.clientX;
var metrics = this.getMetrics_();
var x = metrics.viewLeft - dx;
x = Math.min(x, metrics.contentWidth - metrics.viewWidth);
x = Math.max(x, 0);
this.scrollbar_.set(x);
} else {
var dy = e.clientY - this.startDragMouseY_;
this.startDragMouseY_ = e.clientY;
var metrics = this.getMetrics_();
var y = metrics.viewTop - dy;
y = Math.min(y, metrics.contentHeight - metrics.viewHeight);
y = Math.max(y, 0);
this.scrollbar_.set(y);
}
};
/**
@ -644,7 +788,7 @@ Blockly.Flyout.prototype.createBlockFunc_ = function(originBlock) {
}
var xyOld = Blockly.getSvgXY_(svgRootOld, workspace);
// Scale the scroll (getSvgXY_ did not do this).
if (flyout.RTL) {
if (flyout.toolboxPosition_ == Blockly.TOOLBOX_AT_RIGHT) {
var width = workspace.getMetrics().viewWidth - flyout.width_;
xyOld.x += width / workspace.scale - width;
} else {
@ -708,13 +852,24 @@ Blockly.Flyout.prototype.getClientRect = function() {
// area are still deleted. Must be larger than the largest screen size,
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
var BIG_NUM = 1000000000;
if (this.RTL) {
var width = flyoutRect.left + flyoutRect.width + BIG_NUM;
return new goog.math.Rect(flyoutRect.left, -BIG_NUM, width, BIG_NUM * 2);
var x = flyoutRect.left;
var y = flyoutRect.top;
var width = flyoutRect.width;
var height = flyoutRect.height;
if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_TOP) {
return new goog.math.Rect(-BIG_NUM, y - BIG_NUM, BIG_NUM * 2,
BIG_NUM + height);
} else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_BOTTOM) {
return new goog.math.Rect(-BIG_NUM, y + this.verticalOffset_, BIG_NUM * 2,
BIG_NUM + height);
} else if (this.toolboxPosition_ == Blockly.TOOLBOX_AT_LEFT) {
return new goog.math.Rect(x - BIG_NUM, -BIG_NUM, BIG_NUM + width,
BIG_NUM * 2);
} else { // Right
return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width,
BIG_NUM * 2);
}
// LTR
var width = BIG_NUM + flyoutRect.width + flyoutRect.left;
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, width, BIG_NUM * 2);
};
/**
@ -742,3 +897,93 @@ Blockly.Flyout.terminateDrag_ = function() {
Blockly.Flyout.startBlock_ = null;
Blockly.Flyout.startFlyout_ = null;
};
/**
* Compute height of flyout. Position button under each block.
* For RTL: Lay out the blocks right-aligned.
*/
Blockly.Flyout.prototype.reflowHorizontal = function() {
this.workspace_.scale = this.targetWorkspace_.scale;
var flyoutHeight = 0;
var margin = this.CORNER_RADIUS;
var blocks = this.workspace_.getTopBlocks(false);
for (var x = 0, block; block = blocks[x]; x++) {
var height = block.getHeightWidth().height;
flyoutHeight = Math.max(flyoutHeight, height);
}
flyoutHeight *= this.workspace_.scale;
flyoutHeight += margin * 1.5 + Blockly.Scrollbar.scrollbarThickness;
if (this.height_ != flyoutHeight) {
for (var x = 0, block; block = blocks[x]; x++) {
var blockHW = block.getHeightWidth();
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('width', blockHW.width);
block.flyoutRect_.setAttribute('height', blockHW.height);
// Blocks with output tabs are shifted a bit.
var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('y', blockXY.y);
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
}
}
// Record the width for .getMetrics_ and .position.
this.height_ = flyoutHeight;
}
};
/**
* Compute width of flyout. Position button under each block.
* For RTL: Lay out the blocks right-aligned.
*/
Blockly.Flyout.prototype.reflowVertical = function() {
this.workspace_.scale = this.targetWorkspace_.scale;
var flyoutWidth = 0;
var margin = this.CORNER_RADIUS;
var blocks = this.workspace_.getTopBlocks(false);
for (var x = 0, block; block = blocks[x]; x++) {
var width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= Blockly.BlockSvg.TAB_WIDTH;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
flyoutWidth += Blockly.BlockSvg.TAB_WIDTH;
flyoutWidth *= this.workspace_.scale;
flyoutWidth += margin * 1.5 + Blockly.Scrollbar.scrollbarThickness;
if (this.width_ != flyoutWidth) {
for (var x = 0, block; block = blocks[x]; x++) {
var blockHW = block.getHeightWidth();
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
var oldX = block.getRelativeToSurfaceXY().x;
var dx = flyoutWidth - margin;
dx /= this.workspace_.scale;
dx -= Blockly.BlockSvg.TAB_WIDTH;
block.moveBy(dx - oldX, 0);
}
if (block.flyoutRect_) {
block.flyoutRect_.setAttribute('width', blockHW.width);
block.flyoutRect_.setAttribute('height', blockHW.height);
// Blocks with output tabs are shifted a bit.
var tab = block.outputConnection ? Blockly.BlockSvg.TAB_WIDTH : 0;
var blockXY = block.getRelativeToSurfaceXY();
block.flyoutRect_.setAttribute('x',
this.RTL ? blockXY.x - blockHW.width + tab : blockXY.x - tab);
block.flyoutRect_.setAttribute('y', blockXY.y);
}
}
// Record the width for .getMetrics_ and .position.
this.width_ = flyoutWidth;
}
};
Blockly.Flyout.prototype.reflow = function() {
if (this.horizontalLayout_) {
this.reflowHorizontal();
} else {
this.reflowVertical();
}
// Fire a resize event to update the flyout's scrollbar.
Blockly.fireUiEvent(window, 'resize');
};

View file

@ -276,7 +276,7 @@ Blockly.init_ = function(mainWorkspace) {
mainWorkspace.flyout_.show(options.languageTree.childNodes);
// Translate the workspace sideways to avoid the fixed flyout.
mainWorkspace.scrollX = mainWorkspace.flyout_.width_;
if (options.RTL) {
if (options.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
mainWorkspace.scrollX *= -1;
}
mainWorkspace.translate(mainWorkspace.scrollX, 0);

View file

@ -69,6 +69,26 @@ Blockly.Options = function(options) {
hasSounds = true;
}
}
var rtl = !!options['rtl'];
var horizontalLayout = options['horizontalLayout'];
if (horizontalLayout === undefined) {
horizontalLayout = false;
}
var toolboxAtStart = options['toolboxPosition'];
if (toolboxAtStart === 'end') {
toolboxAtStart = false;
} else {
toolboxAtStart = true;
}
if (horizontalLayout) {
var toolboxPosition = toolboxAtStart ?
Blockly.TOOLBOX_AT_TOP : Blockly.TOOLBOX_AT_BOTTOM;
} else {
var toolboxPosition = (toolboxAtStart == rtl) ?
Blockly.TOOLBOX_AT_RIGHT : Blockly.TOOLBOX_AT_LEFT;
}
var hasScrollbars = options['scrollbars'];
if (hasScrollbars === undefined) {
hasScrollbars = hasCategories;
@ -85,21 +105,28 @@ Blockly.Options = function(options) {
pathToMedia = options['path'] + 'media/';
}
this.RTL = !!options['rtl'];
var enableRealtime = !!options['realtime'];
var realtimeOptions = enableRealtime ? options['realtimeOptions'] : undefined;
this.RTL = rtl;
this.collapse = hasCollapse;
this.comments = hasComments;
this.disable = hasDisable;
this.readOnly = readOnly;
this.maxBlocks = options['maxBlocks'] || Infinity;
this.maxBlocks = options['maxBlocks'] || Infinity;
this.pathToMedia = pathToMedia;
this.hasCategories = hasCategories;
this.hasScrollbars = hasScrollbars;
this.hasTrashcan = hasTrashcan;
this.hasSounds = hasSounds;
this.hasCss = hasCss;
this.horizontalLayout = horizontalLayout;
this.languageTree = languageTree;
this.gridOptions = Blockly.Options.parseGridOptions_(options);
this.zoomOptions = Blockly.Options.parseZoomOptions_(options);
this.enableRealtime = enableRealtime;
this.realtimeOptions = realtimeOptions;
this.toolboxPosition = toolboxPosition;
};
/**

View file

@ -31,6 +31,7 @@ goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeStyle');
goog.require('goog.math.Rect');
goog.require('goog.style');
goog.require('goog.ui.tree.TreeControl');
@ -50,14 +51,78 @@ Blockly.Toolbox = function(workspace) {
* @private
*/
this.workspace_ = workspace;
/**
* Is RTL vs LTR.
* @type {boolean}
*/
this.RTL = workspace.options.RTL;
/**
* Whether the toolbox should be laid out horizontally.
* @type {boolean}
* @private
*/
this.horizontalLayout_ = workspace.options.horizontalLayout;
/**
* Position of the toolbox and flyout relative to the workspace.
* @type {number}
*/
this.toolboxPosition = workspace.options.toolboxPosition;
/**
* Configuration constants for Closure's tree UI.
* @type {Object.<string,*>}
* @private
*/
this.config_ = {
indentWidth: 19,
cssRoot: 'blocklyTreeRoot',
cssHideRoot: 'blocklyHidden',
cssItem: '',
cssTreeRow: 'blocklyTreeRow',
cssItemLabel: 'blocklyTreeLabel',
cssTreeIcon: 'blocklyTreeIcon',
cssExpandedFolderIcon: 'blocklyTreeIconOpen',
cssFileIcon: 'blocklyTreeIconNone',
cssSelectedRow: 'blocklyTreeSelected'
};
/**
* Configuration constants for tree separator.
* @type {Object.<string,*>}
* @private
*/
this.treeSeparatorConfig_ = {
cssTreeRow: 'blocklyTreeSeparator'
};
if (this.horizontalLayout_) {
this.config_['cssTreeRow'] =
this.config_['cssTreeRow'] +
(workspace.RTL ? ' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree');
this.treeSeparatorConfig_['cssTreeRow'] =
'blocklyTreeSeparatorHorizontal' +
(workspace.RTL ? ' blocklyHorizontalTreeRtl' : ' blocklyHorizontalTree');
this.config_['cssTreeIcon'] = '';
}
};
/**
* Width of the toolbox.
* Width of the toolbox, which changes only in vertical layout.
* @type {number}
*/
Blockly.Toolbox.prototype.width = 0;
/**
* Height of the toolbox, which changes only in horizontal layout.
* @type {number}
*/
Blockly.Toolbox.prototype.height = 0;
/**
* The SVG group currently selected.
* @type {SVGGElement}
@ -72,25 +137,6 @@ Blockly.Toolbox.prototype.selectedOption_ = null;
*/
Blockly.Toolbox.prototype.lastCategory_ = null;
/**
* Configuration constants for Closure's tree UI.
* @type {Object.<string,*>}
* @const
* @private
*/
Blockly.Toolbox.prototype.CONFIG_ = {
indentWidth: 19,
cssRoot: 'blocklyTreeRoot',
cssHideRoot: 'blocklyHidden',
cssItem: '',
cssTreeRow: 'blocklyTreeRow',
cssItemLabel: 'blocklyTreeLabel',
cssTreeIcon: 'blocklyTreeIcon',
cssExpandedFolderIcon: 'blocklyTreeIconOpen',
cssFileIcon: 'blocklyTreeIconNone',
cssSelectedRow: 'blocklyTreeSelected'
};
/**
* Initializes the toolbox.
*/
@ -116,7 +162,9 @@ Blockly.Toolbox.prototype.init = function() {
var workspaceOptions = {
disabledPatternId: workspace.options.disabledPatternId,
parentWorkspace: workspace,
RTL: workspace.RTL
RTL: workspace.RTL,
horizontalLayout: workspace.horizontalLayout,
toolboxPosition: workspace.options.toolboxPosition
};
/**
* @type {!Blockly.Flyout}
@ -126,10 +174,10 @@ Blockly.Toolbox.prototype.init = function() {
goog.dom.insertSiblingAfter(this.flyout_.createDom(), workspace.svgGroup_);
this.flyout_.init(workspace);
this.CONFIG_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif';
this.CONFIG_['cssCollapsedFolderIcon'] =
this.config_['cleardotPath'] = workspace.options.pathToMedia + '1x1.gif';
this.config_['cssCollapsedFolderIcon'] =
'blocklyTreeIconClosed' + (workspace.RTL ? 'Rtl' : 'Ltr');
var tree = new Blockly.Toolbox.TreeControl(this, this.CONFIG_);
var tree = new Blockly.Toolbox.TreeControl(this, this.config_);
this.tree_ = tree;
tree.setShowRootNode(false);
tree.setShowLines(false);
@ -164,18 +212,33 @@ Blockly.Toolbox.prototype.position = function() {
var svg = this.workspace_.getParentSvg();
var svgPosition = goog.style.getPageOffset(svg);
var svgSize = Blockly.svgSize(svg);
if (this.workspace_.RTL) {
treeDiv.style.left =
(svgPosition.x + svgSize.width - treeDiv.offsetWidth) + 'px';
} else {
if (this.horizontalLayout_) {
treeDiv.style.left = svgPosition.x + 'px';
}
treeDiv.style.height = svgSize.height + 'px';
treeDiv.style.top = svgPosition.y + 'px';
this.width = treeDiv.offsetWidth;
if (!this.workspace_.RTL) {
// For some reason the LTR toolbox now reports as 1px too wide.
this.width -= 1;
treeDiv.style.height = 'auto';
treeDiv.style.width = svgSize.width + 'px';
this.height = treeDiv.offsetHeight;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) { // Top
treeDiv.style.top = svgPosition.y + 'px';
this.flyout_.setVerticalOffset(treeDiv.offsetHeight);
} else { // Bottom
var topOfToolbox = svgPosition.y + svgSize.height;
treeDiv.style.top = topOfToolbox + 'px';
this.flyout_.setVerticalOffset(topOfToolbox);
}
} else {
if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) { // Right
treeDiv.style.left =
(svgPosition.x + svgSize.width - treeDiv.offsetWidth) + 'px';
} else { // Left
treeDiv.style.left = svgPosition.x + 'px';
}
treeDiv.style.height = svgSize.height + 'px';
treeDiv.style.top = svgPosition.y + 'px';
this.width = treeDiv.offsetWidth;
if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
// For some reason the LTR toolbox now reports as 1px too wide.
this.width -= 1;
}
}
this.flyout_.position();
};
@ -187,10 +250,11 @@ Blockly.Toolbox.prototype.position = function() {
*/
Blockly.Toolbox.prototype.populate_ = function(newTree) {
var rootOut = this.tree_;
var that = this;
rootOut.removeChildren(); // Delete any existing content.
rootOut.blocks = [];
var hasColours = false;
function syncTrees(treeIn, treeOut) {
function syncTrees(treeIn, treeOut, pathToMedia) {
var lastElement = null;
for (var i = 0, childIn; childIn = treeIn.childNodes[i]; i++) {
if (!childIn.tagName) {
@ -201,13 +265,17 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
case 'CATEGORY':
var childOut = rootOut.createNode(childIn.getAttribute('name'));
childOut.blocks = [];
treeOut.add(childOut);
if (that.horizontalLayout_) {
treeOut.add(childOut);
} else {
treeOut.addChildAt(childOut, 0);
}
var custom = childIn.getAttribute('custom');
if (custom) {
// Variables and procedures are special dynamic categories.
childOut.blocks = custom;
} else {
syncTrees(childIn, childOut);
syncTrees(childIn, childOut, pathToMedia);
}
var colour = childIn.getAttribute('colour');
if (goog.isString(colour)) {
@ -235,7 +303,13 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
if (lastElement.tagName.toUpperCase() == 'CATEGORY') {
// Separator between two categories.
// <sep></sep>
treeOut.add(new Blockly.Toolbox.TreeSeparator());
if (that.horizontalLayout_) {
treeOut.add(new Blockly.Toolbox.TreeSeparator(
that.treeSeparatorConfig_));
} else {
treeOut.addChildAt(new Blockly.Toolbox.TreeSeparator(
that.treeSeparatorConfig_), 0);
}
} else {
// Change the gap between two blocks.
// <sep gap="36"></sep>
@ -259,7 +333,7 @@ Blockly.Toolbox.prototype.populate_ = function(newTree) {
}
}
}
syncTrees(newTree, this.tree_);
syncTrees(newTree, this.tree_, this.workspace_.options.pathToMedia);
this.hasColours_ = hasColours;
if (rootOut.blocks.length) {
@ -313,16 +387,26 @@ Blockly.Toolbox.prototype.getClientRect = function() {
// area are still deleted. Must be smaller than Infinity, but larger than
// the largest screen size.
var BIG_NUM = 10000000;
var toolboxRect = this.HtmlDiv.getBoundingClientRect();
var x = toolboxRect.left;
var y = toolboxRect.top;
var width = toolboxRect.width;
var height = toolboxRect.height;
// Assumes that the toolbox is on the SVG edge. If this changes
// (e.g. toolboxes in mutators) then this code will need to be more complex.
var toolboxRect = this.HtmlDiv.getBoundingClientRect();
if (this.workspace_.RTL) {
var width = toolboxRect.left + toolboxRect.width + BIG_NUM;
return new goog.math.Rect(toolboxRect.left, -BIG_NUM, width, BIG_NUM * 2);
if (this.toolboxPosition == Blockly.TOOLBOX_AT_LEFT) {
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, BIG_NUM + x + width,
2 * BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_RIGHT) {
return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + width, 2 * BIG_NUM);
} else if (this.toolboxPosition == Blockly.TOOLBOX_AT_TOP) {
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, 2 * BIG_NUM,
BIG_NUM + y + height);
} else { // Bottom
return new goog.math.Rect(0, y, 2 * BIG_NUM, BIG_NUM + width);
}
// LTR
var width = BIG_NUM + toolboxRect.width + toolboxRect.left;
return new goog.math.Rect(-BIG_NUM, -BIG_NUM, width, BIG_NUM * 2);
};
// Extending Closure's Tree UI.
@ -495,18 +579,7 @@ Blockly.Toolbox.TreeNode.prototype.onDoubleClick_ = function(e) {
* @constructor
* @extends {Blockly.Toolbox.TreeNode}
*/
Blockly.Toolbox.TreeSeparator = function() {
Blockly.Toolbox.TreeNode.call(this, null, '',
Blockly.Toolbox.TreeSeparator.CONFIG_);
Blockly.Toolbox.TreeSeparator = function(config) {
Blockly.Toolbox.TreeNode.call(this, null, '', config);
};
goog.inherits(Blockly.Toolbox.TreeSeparator, Blockly.Toolbox.TreeNode);
/**
* Configuration constants for tree separator.
* @type {Object.<string,*>}
* @const
* @private
*/
Blockly.Toolbox.TreeSeparator.CONFIG_ = {
cssTreeRow: 'blocklyTreeSeparator'
};

View file

@ -43,6 +43,8 @@ Blockly.Workspace = function(opt_options) {
this.options = opt_options || {};
/** @type {boolean} */
this.RTL = !!this.options.RTL;
/** @type {boolean} */
this.horizontalLayout = !!this.options.horizontalLayout;
/** @type {!Array.<!Blockly.Block>} */
this.topBlocks_ = [];
/** @type {!Array.<!Function>} */

View file

@ -287,7 +287,9 @@ Blockly.WorkspaceSvg.prototype.addFlyout_ = function() {
var workspaceOptions = {
disabledPatternId: this.options.disabledPatternId,
parentWorkspace: this,
RTL: this.RTL
RTL: this.RTL,
horizontalLayout: this.horizontalLayout,
toolboxPosition: this.options.toolboxPosition,
};
/** @type {Blockly.Flyout} */
this.flyout_ = new Blockly.Flyout(workspaceOptions);

462
tests/multi_playground.html Normal file
View file

@ -0,0 +1,462 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Multi-toolbox Playground</title>
<script src="../blockly_uncompressed.js"></script>
<script src="../msg/messages.js"></script>
<script src="../blocks/logic.js"></script>
<script src="../blocks/loops.js"></script>
<script src="../blocks/math.js"></script>
<script src="../blocks/text.js"></script>
<script src="../blocks/lists.js"></script>
<script src="../blocks/colour.js"></script>
<script src="../blocks/variables.js"></script>
<script src="../blocks/procedures.js"></script>
<script>
'use strict';
function start() {
startBlocklyInstance('VertStartLTR', false, false, 'start');
startBlocklyInstance('VertStartRTL', true, false, 'start');
startBlocklyInstance('VertEndLTR', false, false, 'end');
startBlocklyInstance('VertEndRTL', true, false, 'end');
startBlocklyInstance('HorizontalStartLTR', false, true, 'start');
startBlocklyInstance('HorizontalStartRTL', true, true, 'start');
startBlocklyInstance('HorizontalEndLTR', false, true, 'end');
startBlocklyInstance('HorizontalEndRTL', true, true, 'end');
}
function startBlocklyInstance(suffix, rtl, horizontalLayout, position) {
var toolbox = document.getElementById('toolbox_alwaysOpen');
var options = {
comments: false,
disable: false,
collapse: false,
maxBlocks: Infinity,
media: '../media/',
readOnly: false,
rtl: rtl,
scrollbars: true,
toolbox: toolbox,
trashcan: true,
horizontalLayout: horizontalLayout,
toolboxPosition: position,
zoom: {
controls: true,
wheel: false,
startScale: 1.0,
maxScale: 4,
minScale: 0.25,
scaleSpeed: 1.1
},
};
Blockly.inject('blocklyDiv' + suffix, options);
}
</script>
<style>
html, body {
height: 100%;
}
body {
background-color: #fff;
font-family: sans-serif;
}
h1 {
font-weight: normal;
font-size: 140%;
}
#blocklyDiv {
float: right;
height: 95%;
width: 70%;
}
#collaborators {
float: right;
width: 30px;
margin-left: 10px;
}
#collaborators > img {
margin-right: 5px;
height: 30px;
padding-bottom: 5px;
width: 30px;
border-radius: 3px;
}
#importExport {
font-family: monospace;
}
</style>
</head>
<body onload="start()">
<div id="collaborators"></div>
<table>
<tr>
<td/>
<td>LTR</td>
<td>RTL</td>
</tr>
<tr>
<td>Vertical layout; toolbox at start</td>
<td>
<div id="blocklyDivVertStartLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivVertStartRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
<tr>
<td>Vertical layout; toolbox at end</td>
<td>
<div id="blocklyDivVertEndLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivVertEndRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
<tr>
<td>Horizontal layout; toolbox at start</td>
<td>
<div id="blocklyDivHorizontalStartLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivHorizontalStartRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
<tr>
<td>Horizontal layout; toolbox at end</td>
<td>
<div id="blocklyDivHorizontalEndLTR" style="height: 480px; width: 600px;"></div>
</td>
<td>
<div id="blocklyDivHorizontalEndRTL" style="height: 480px; width: 600px;"></div>
</td>
</tr>
</table>
<xml id="toolbox_alwaysOpen" style="display: none">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<!-- <block type="control_repeat"></block> -->
<block type="logic_operation"></block>
<block type="controls_repeat_ext">
<value name="TIMES">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
</xml>
<xml id="toolbox_categoriesScroll" style="display: none">
<category name="Logic" colour="210">
<block type="controls_if"></block>
<block type="logic_compare"></block>
<block type="logic_operation"></block>
<block type="logic_negate"></block>
<block type="logic_boolean"></block>
<block type="logic_null" disabled="true"></block>
<block type="logic_ternary"></block>
</category>
<category name="Loops" colour="120">
<block type="controls_repeat_ext">
<value name="TIMES">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="controls_repeat" disabled="true"></block>
<block type="controls_whileUntil"></block>
<block type="controls_for">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
<value name="BY">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="controls_forEach"></block>
<block type="controls_flow_statements"></block>
</category>
<category name="Math" colour="230">
<block type="math_number" gap="32"></block>
<block type="math_arithmetic">
<value name="A">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="B">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="math_single">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">9</field>
</shadow>
</value>
</block>
<block type="math_trig">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">45</field>
</shadow>
</value>
</block>
<block type="math_constant"></block>
<block type="math_number_property">
<value name="NUMBER_TO_CHECK">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="math_change">
<value name="DELTA">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
</block>
<block type="math_round">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">3.1</field>
</shadow>
</value>
</block>
<block type="math_on_list"></block>
<block type="math_modulo">
<value name="DIVIDEND">
<shadow type="math_number">
<field name="NUM">64</field>
</shadow>
</value>
<value name="DIVISOR">
<shadow type="math_number">
<field name="NUM">10</field>
</shadow>
</value>
</block>
<block type="math_constrain">
<value name="VALUE">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="LOW">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="HIGH">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_int">
<value name="FROM">
<shadow type="math_number">
<field name="NUM">1</field>
</shadow>
</value>
<value name="TO">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
</block>
<block type="math_random_float"></block>
</category>
<category name="Text" colour="160">
<block type="text"></block>
<block type="text_join"></block>
<block type="text_append">
<value name="TEXT">
<shadow type="text"></shadow>
</value>
</block>
<block type="text_length">
<value name="VALUE">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_isEmpty">
<value name="VALUE">
<shadow type="text">
<field name="TEXT"></field>
</shadow>
</value>
</block>
<block type="text_indexOf">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
<value name="FIND">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_charAt">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
</block>
<block type="text_getSubstring">
<value name="STRING">
<block type="variables_get">
<field name="VAR">text</field>
</block>
</value>
</block>
<block type="text_changeCase">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_trim">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_print">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
<block type="text_prompt_ext">
<value name="TEXT">
<shadow type="text">
<field name="TEXT">abc</field>
</shadow>
</value>
</block>
</category>
<category name="Lists" colour="260">
<block type="lists_create_with">
<mutation items="0"></mutation>
</block>
<block type="lists_create_with"></block>
<block type="lists_repeat">
<value name="NUM">
<shadow type="math_number">
<field name="NUM">5</field>
</shadow>
</value>
</block>
<block type="lists_length"></block>
<block type="lists_isEmpty"></block>
<block type="lists_indexOf">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_getIndex">
<value name="VALUE">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_setIndex">
<value name="LIST">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_getSublist">
<value name="LIST">
<block type="variables_get">
<field name="VAR">list</field>
</block>
</value>
</block>
<block type="lists_split">
<value name="DELIM">
<shadow type="text">
<field name="TEXT">,</field>
</shadow>
</value>
</block>
</category>
<category name="Colour" colour="20">
<block type="colour_picker"></block>
<block type="colour_random"></block>
<block type="colour_rgb">
<value name="RED">
<shadow type="math_number">
<field name="NUM">100</field>
</shadow>
</value>
<value name="GREEN">
<shadow type="math_number">
<field name="NUM">50</field>
</shadow>
</value>
<value name="BLUE">
<shadow type="math_number">
<field name="NUM">0</field>
</shadow>
</value>
</block>
<block type="colour_blend">
<value name="COLOUR1">
<shadow type="colour_picker">
<field name="COLOUR">#ff0000</field>
</shadow>
</value>
<value name="COLOUR2">
<shadow type="colour_picker">
<field name="COLOUR">#3333ff</field>
</shadow>
</value>
<value name="RATIO">
<shadow type="math_number">
<field name="NUM">0.5</field>
</shadow>
</value>
</block>
</category>
<sep></sep>
<category name="Variables" colour="330" custom="VARIABLE"></category>
<category name="Functions" colour="290" custom="PROCEDURE"></category>
</xml>
</body>
</html>