Merge pull request #52 from rachel-fenichel/feature/horizontal_flyout

Horizontal flyout rendering
This commit is contained in:
rachel-fenichel 2016-02-11 11:03:05 -08:00
commit 0ccdf83e18
3 changed files with 274 additions and 126 deletions

View file

@ -56,6 +56,12 @@ Blockly.Flyout = function(workspaceOptions) {
*/
this.RTL = !!workspaceOptions.RTL;
/**
* Flyout should be laid out horizontally vs vertically.
* @type {boolean}
*/
this.horizontalLayout_ = false;
/**
* Opaque data that can be passed to Blockly.unbindEvent_.
* @type {!Array.<!Array>}
@ -148,7 +154,7 @@ 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();
@ -159,7 +165,7 @@ Blockly.Flyout.prototype.init = function(targetWorkspace) {
Blockly.bindEvent_(this.targetWorkspace_.getCanvas(),
'blocklyWorkspaceChange', this, this.filterForCapacity_));
}
// Dragging the flyout up and down.
// Dragging the flyout up and down (or left and right).
Array.prototype.push.apply(this.eventWrappers_,
Blockly.bindEvent_(this.svgGroup_, 'mousedown', this, this.onMouseDown_));
};
@ -194,9 +200,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
@ -206,42 +215,64 @@ 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 {
if (this.horizontalLayout_) {
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,
contentWidth: (optionBox.width) * this.workspace_.scale,
viewTop: -this.workspace_.scrollY,
contentTop: 0,
viewLeft: -this.workspace_.scrollX,
contentTop: optionBox.y,
contentLeft: 0,
absoluteTop: this.verticalOffset_ + this.SCROLLBAR_PADDING,
absoluteLeft: 0
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);
this.workspace_.translate(this.workspace_.scrollX + metrics.absoluteLeft,
this.workspace_.scrollY + metrics.absoluteTop);
};
Blockly.Flyout.prototype.setVerticalOffset = function(verticalOffset) {
@ -260,24 +291,14 @@ Blockly.Flyout.prototype.position = function() {
// Hidden components will return null.
return;
}
var edgeWidth = this.width_ - this.CORNER_RADIUS;
var edgeWidth = this.horizontalLayout_ ? metrics.viewWidth : this.width_;
edgeWidth -= this.CORNER_RADIUS;
if (this.RTL) {
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) {
@ -287,8 +308,13 @@ Blockly.Flyout.prototype.position = function() {
this.svgGroup_.setAttribute('transform',
'translate(' + x + ',' + metrics.absoluteTop + ')');
// Record the height for Blockly.Flyout.getMetrics_.
this.height_ = metrics.viewHeight;
// 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_) {
@ -296,10 +322,41 @@ Blockly.Flyout.prototype.position = function() {
}
};
/**
* 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) {
// Decide whether to start on the left or right.
var path = ['M ' + (this.RTL ? this.width_ : 0) + ',0'];
// Top.
path.push('h', width);
// Rounded corner.
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);
// 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,
this.RTL ? 0 : 1,
this.RTL ? this.CORNER_RADIUS : -this.CORNER_RADIUS,
this.CORNER_RADIUS);
// Bottom.
path.push('h', -width);
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
};
/**
* Scroll the flyout to the top.
*/
Blockly.Flyout.prototype.scrollToTop = function() {
Blockly.Flyout.prototype.scrollToStart = function() {
this.scrollbar_.set(0);
};
@ -309,6 +366,10 @@ Blockly.Flyout.prototype.scrollToTop = 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) {
@ -401,7 +462,11 @@ Blockly.Flyout.prototype.show = function(xmlList) {
}
}
// Lay out the blocks vertically.
// Lay out the blocks.
var cursorX = margin / this.workspace_.scale + Blockly.BlockSvg.TAB_WIDTH;
if (this.RTL) {
cursorX = this.width_ - cursorX;
}
var cursorY = margin;
for (var i = 0, block; block = blocks[i]; i++) {
var allBlocks = block.getDescendants();
@ -414,10 +479,16 @@ 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(cursorX, cursorY);
if (this.horizontalLayout_) {
if (this.RTL) {
cursorX -= (blockHW.width + gaps[i]);
} else {
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.
@ -427,6 +498,43 @@ 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_();
this.reflowWrapper_ = Blockly.bindEvent_(this.workspace_.getCanvas(),
'blocklyWorkspaceChange', this, this.reflow);
this.workspace_.fireChangeEvent();
};
/**
* 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)));
@ -444,77 +552,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_ = Blockly.bindEvent_(this.workspace_.getCanvas(),
'blocklyWorkspaceChange', this, this.reflow);
this.workspace_.fireChangeEvent();
};
/**
* 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');
}
};
/**
@ -550,7 +587,7 @@ Blockly.Flyout.prototype.blockMouseDown_ = function(block) {
};
/**
* Mouse down on the flyout background. Start a vertical scroll drag.
* Mouse down on the flyout background. Start a scroll drag.
* @param {!Event} e Mouse down event.
* @private
*/
@ -561,6 +598,7 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) {
Blockly.hideChaff(true);
Blockly.Flyout.terminateDrag_();
this.startDragMouseY_ = e.clientY;
this.startDragMouseX_ = e.clientX;
Blockly.Flyout.onMouseMoveWrapper_ = Blockly.bindEvent_(document, 'mousemove',
this, this.onMouseMove_);
Blockly.Flyout.onMouseUpWrapper_ = Blockly.bindEvent_(document, 'mouseup',
@ -571,18 +609,28 @@ Blockly.Flyout.prototype.onMouseDown_ = function(e) {
};
/**
* Handle a mouse-move to vertically drag the flyout.
* Handle a mouse-move to drag the flyout.
* @param {!Event} e Mouse move event.
* @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);
}
};
/**
@ -699,14 +747,21 @@ Blockly.Flyout.prototype.getRect = function() {
// but be smaller than half Number.MAX_SAFE_INTEGER (not available on IE).
var BIG_NUM = 1000000000;
var mainWorkspace = Blockly.mainWorkspace;
var x = Blockly.getSvgXY_(this.svgGroup_, mainWorkspace).x;
if (!this.RTL) {
x -= BIG_NUM;
}
// Fix scale if nested in zoomed workspace.
var scale = this.targetWorkspace_ == mainWorkspace ? 1 : mainWorkspace.scale;
return new goog.math.Rect(x, -BIG_NUM,
BIG_NUM + this.width_ * scale, BIG_NUM * 2);
var x = Blockly.getSvgXY_(this.svgGroup_, mainWorkspace).x;
if (this.horizontalLayout_) {
var y = Blockly.getSvgXY_(this.svgGroup_, mainWorkspace).y - BIG_NUM;
return new goog.math.Rect(-BIG_NUM, y, BIG_NUM * 2,
BIG_NUM + this.height_ * scale);
} else {
if (!this.RTL) {
x -= BIG_NUM;
}
return new goog.math.Rect(x, -BIG_NUM, BIG_NUM + this.width_ * scale,
BIG_NUM * 2);
}
};
/**
@ -734,3 +789,94 @@ 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 += Blockly.BlockSvg.TAB_WIDTH;
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

@ -497,8 +497,9 @@ Blockly.Scrollbar.prototype.onScroll_ = function() {
* @param {number} value The distance from the top/left end of the bar.
*/
Blockly.Scrollbar.prototype.set = function(value) {
var ratio = this.ratio_ == this.ratio_ || 0;
// Move the scrollbar slider.
this.svgKnob_.setAttribute(this.horizontal_ ? 'x' : 'y', value * this.ratio_);
this.svgKnob_.setAttribute(this.horizontal_ ? 'x' : 'y', value * ratio);
this.onScroll_();
};

View file

@ -65,6 +65,7 @@ Blockly.Toolbox = function(workspace) {
* @private
*/
this.horizontalLayout_ = false;
if (this.horizontalLayout_) {
this.CONFIG_['cssTreeRow'] =
this.CONFIG_['cssTreeRow']
@ -449,9 +450,9 @@ Blockly.Toolbox.TreeControl.prototype.setSelectedItem = function(node) {
goog.ui.tree.TreeControl.prototype.setSelectedItem.call(this, node);
if (node && node.blocks && node.blocks.length) {
toolbox.flyout_.show(node.blocks);
// Scroll the flyout to the top if the category has changed.
// Scroll the flyout to the start if the category has changed.
if (toolbox.lastCategory_ != node) {
toolbox.flyout_.scrollToTop();
toolbox.flyout_.scrollToStart();
}
} else {
// Hide the flyout.