Merge pull request #2157 from BeksOmega/feature/TrashcanContents

Added flyout to trashcan to "get back" deleted blocks.`
This commit is contained in:
Rachel Fenichel
2018-12-17 15:23:17 -08:00
committed by GitHub
5 changed files with 209 additions and 29 deletions

View File

@@ -326,8 +326,14 @@ Blockly.onContextMenu_ = function(e) {
Blockly.hideChaff = function(opt_allowToolbox) {
Blockly.Tooltip.hide();
Blockly.WidgetDiv.hide();
// For now the trashcan flyout always autocloses because it overlays the
// trashcan UI (no trashcan to click to close it)
var workspace = Blockly.getMainWorkspace();
if (workspace.trashcan &&
workspace.trashcan.flyout_) {
workspace.trashcan.flyout_.hide();
}
if (!opt_allowToolbox) {
var workspace = Blockly.getMainWorkspace();
if (workspace.toolbox_ &&
workspace.toolbox_.flyout_ &&
workspace.toolbox_.flyout_.autoClose) {

View File

@@ -218,6 +218,12 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface,
var flyout = mainWorkspace.addFlyout_('svg');
Blockly.utils.insertAfter(flyout, svg);
}
if (options.hasTrashcan) {
mainWorkspace.addTrashcan();
}
if (options.zoomOptions && options.zoomOptions.controls) {
mainWorkspace.addZoomControls();
}
// A null translation will also apply the correct initial scale.
mainWorkspace.translate(0, 0);
@@ -339,6 +345,14 @@ Blockly.init_ = function(mainWorkspace) {
}
}
var bottom = Blockly.Scrollbar.scrollbarThickness;
if (options.hasTrashcan) {
bottom = mainWorkspace.trashcan.init(bottom);
}
if (options.zoomOptions && options.zoomOptions.controls) {
mainWorkspace.zoomControls_.init(bottom);
}
if (options.hasScrollbars) {
mainWorkspace.scrollbar = new Blockly.ScrollbarPair(mainWorkspace);
mainWorkspace.scrollbar.resize();

View File

@@ -38,6 +38,19 @@ goog.require('goog.math.Rect');
*/
Blockly.Trashcan = function(workspace) {
this.workspace_ = workspace;
var flyoutWorkspaceOptions = {
scrollbars: true,
disabledPatternId: this.workspace_.options.disabledPatternId,
parentWorkspace: this.workspace_,
RTL: this.workspace_.RTL,
oneBasedIndex: workspace.options.oneBasedIndex,
// TODO: Add horizontal.
/*horizontalLayout: this.workspace_.horizontalLayout,*/
toolboxPosition: this.workspace_.RTL ? Blockly.TOOLBOX_AT_LEFT :
Blockly.TOOLBOX_AT_RIGHT
};
this.flyout_ = new Blockly.VerticalFlyout(flyoutWorkspaceOptions);
this.workspace_.addChangeListener(this.onDelete_());
};
/**
@@ -96,12 +109,50 @@ Blockly.Trashcan.prototype.SPRITE_LEFT_ = 0;
*/
Blockly.Trashcan.prototype.SPRITE_TOP_ = 32;
/**
* The openness of the lid when the trashcan contains blocks.
* (0.0 = closed, 1.0 = open)
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.HAS_BLOCKS_LID_ANGLE = 0.1;
/**
* The maximum number of blocks that can go in the trashcan's flyout. '0' turns
* turns off trashcan contents, 'Infinity' sets it to unlimited.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.MAX_CONTENTS = 32;
/**
* Current open/close state of the lid.
* @type {boolean}
*/
Blockly.Trashcan.prototype.isOpen = false;
/**
* The minimum openness of the lid. Used to indicate if the trashcan contains
* blocks.
* @type {number}
* @private
*/
Blockly.Trashcan.prototype.minOpenness_ = 0;
/**
* True if the trashcan contains blocks, otherwise false.
* @type {boolean}
* @private
*/
Blockly.Trashcan.prototype.hasBlocks_ = false;
/**
* A list of Xml (stored as strings) representing blocks "inside" the trashcan.
* @type {Array}
* @private
*/
Blockly.Trashcan.prototype.contents_ = [];
/**
* The SVG group containing the trash can.
* @type {Element}
@@ -207,6 +258,11 @@ Blockly.Trashcan.prototype.createDom = function() {
this.workspace_.options.pathToMedia + Blockly.SPRITE.url);
Blockly.bindEventWithChecks_(this.svgGroup_, 'mouseup', this, this.click);
// bindEventWithChecks_ quashes events too aggressively. See:
// https://groups.google.com/forum/#!topic/blockly/QF4yB9Wx00s
// Bind to body instead of this.svgGroup_ so that we don't get lid jitters
Blockly.bindEvent_(body, 'mouseover', this, this.mouseOver_);
Blockly.bindEvent_(body, 'mouseout', this, this.mouseOut_);
this.animateLid_();
return this.svgGroup_;
};
@@ -217,6 +273,10 @@ Blockly.Trashcan.prototype.createDom = function() {
* @return {number} Distance from workspace bottom to the top of trashcan.
*/
Blockly.Trashcan.prototype.init = function(bottom) {
Blockly.utils.insertAfter(this.flyout_.createDom('svg'),
this.workspace_.getParentSvg());
this.flyout_.init(this.workspace_);
this.bottom_ = this.MARGIN_BOTTOM_ + bottom;
this.setOpen_(false);
return this.bottom_ + this.BODY_HEIGHT_ + this.LID_HEIGHT_;
@@ -240,6 +300,10 @@ Blockly.Trashcan.prototype.dispose = function() {
* Move the trash can to the bottom-right corner.
*/
Blockly.Trashcan.prototype.position = function() {
// Not yet initialized.
if (!this.bottom_) {
return;
}
var metrics = this.workspace_.getMetrics();
if (!metrics) {
// There are no metrics available (workspace is probably not visible).
@@ -309,20 +373,28 @@ Blockly.Trashcan.prototype.setOpen_ = function(state) {
*/
Blockly.Trashcan.prototype.animateLid_ = function() {
this.lidOpen_ += this.isOpen ? 0.2 : -0.2;
this.lidOpen_ = Math.min(Math.max(this.lidOpen_, 0), 1);
var lidAngle = this.lidOpen_ * 45;
this.svgLid_.setAttribute('transform', 'rotate(' +
(this.workspace_.RTL ? -lidAngle : lidAngle) + ',' +
(this.workspace_.RTL ? 4 : this.WIDTH_ - 4) + ',' +
(this.LID_HEIGHT_ - 2) + ')');
this.lidOpen_ = Math.min(Math.max(this.lidOpen_, this.minOpenness_), 1);
this.setLidAngle_(this.lidOpen_ * 45);
// Linear interpolation between 0.4 and 0.8.
var opacity = 0.4 + this.lidOpen_ * (0.8 - 0.4);
this.svgGroup_.style.opacity = opacity;
if (this.lidOpen_ > 0 && this.lidOpen_ < 1) {
if (this.lidOpen_ > this.minOpenness_ && this.lidOpen_ < 1) {
this.lidTask_ = setTimeout(this.animateLid_.bind(this), 20);
}
};
/**
* Set the angle of the trashcan's lid.
* @param {!number} lidAngle The angle at which to set the lid.
* @private
*/
Blockly.Trashcan.prototype.setLidAngle_ = function(lidAngle) {
this.svgLid_.setAttribute('transform', 'rotate(' +
(this.workspace_.RTL ? -lidAngle : lidAngle) + ',' +
(this.workspace_.RTL ? 4 : this.WIDTH_ - 4) + ',' +
(this.LID_HEIGHT_ - 2) + ')');
};
/**
* Flip the lid shut.
* Called externally after a drag.
@@ -335,10 +407,107 @@ Blockly.Trashcan.prototype.close = function() {
* Inspect the contents of the trash.
*/
Blockly.Trashcan.prototype.click = function() {
var dx = this.workspace_.startScrollX - this.workspace_.scrollX;
var dy = this.workspace_.startScrollY - this.workspace_.scrollY;
if (Math.sqrt(dx * dx + dy * dy) > Blockly.DRAG_RADIUS) {
if (!this.hasBlocks_) {
return;
}
console.log('TODO: Inspect trash.');
var xml = [];
for (var i = 0, text; text = this.contents_[i]; i++) {
xml[i] = Blockly.Xml.textToDom(text).firstChild;
}
this.flyout_.show(xml);
};
/**
* Indicate that the trashcan can be clicked (by opening it) if it has blocks.
* @private
*/
Blockly.Trashcan.prototype.mouseOver_ = function() {
if (!this.hasBlocks_) {
return;
}
this.setOpen_(true);
};
/**
* Close the lid of the trashcan if it was open (Vis. it was indicating it had
* blocks).
* @private
*/
Blockly.Trashcan.prototype.mouseOut_ = function() {
// No need to do a .hasBlocks check here because if it doesn't the trashcan
// wont be open in the first place, and setOpen_ won't run.
this.setOpen_(false);
};
/**
* Handle a BLOCK_DELETE event. Adds deleted blocks oldXml to the content array.
* @returns {!Function} Function to call when a block is deleted.
* @private
*/
Blockly.Trashcan.prototype.onDelete_ = function() {
var trashcan = this;
return function(event) {
if (!trashcan.MAX_CONTENTS) {
return;
}
if (event.type == Blockly.Events.BLOCK_DELETE) {
var cleanedXML = trashcan.cleanBlockXML_(event.oldXml);
if (trashcan.contents_.indexOf(cleanedXML) != -1) {
return;
}
trashcan.contents_.unshift(cleanedXML);
if (trashcan.contents_.length > trashcan.MAX_CONTENTS) {
trashcan.contents_.splice(trashcan.MAX_CONTENTS,
trashcan.contents_.length - trashcan.MAX_CONTENTS);
}
trashcan.hasBlocks_ = true;
trashcan.minOpenness_ = trashcan.HAS_BLOCKS_LID_ANGLE;
trashcan.setLidAngle_(trashcan.minOpenness_ * 45);
}
};
};
/**
* Converts xml representing a block into text that can be stored in the
* content array.
* @param {!Element} xml An XML tree defining the block and any
* connected child blocks.
* @returns {!string} Text representing the XML tree, cleaned of all unnecessary
* attributes.
* @private
*/
Blockly.Trashcan.prototype.cleanBlockXML_ = function(xml) {
var xmlBlock = xml.cloneNode(true);
var node = xmlBlock;
while (node) {
// Things like text inside tags are still treated as nodes, but they
// don't have attributes (or the removeAttribute function) so we can
// skip removing attributes from them.
if (node.removeAttribute) {
node.removeAttribute('x');
node.removeAttribute('y');
node.removeAttribute('id');
}
// Try to go down the tree
var nextNode = node.firstChild || node.nextSibling;
// If we can't go down, try to go back up the tree.
if (!nextNode) {
nextNode = node.parentNode;
while (nextNode) {
// We are valid again!
if (nextNode.nextSibling) {
nextNode = nextNode.nextSibling;
break;
}
// Try going up again. If parentNode is null that means we have
// reached the top, and we will break out of both loops.
nextNode = nextNode.parentNode;
}
}
node = nextNode;
}
return '<xml>' + Blockly.Xml.domToText(xmlBlock) + '</xml>';
};

View File

@@ -466,13 +466,6 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
/** @type {SVGElement} */
this.svgBubbleCanvas_ = Blockly.utils.createSvgElement('g',
{'class': 'blocklyBubbleCanvas'}, this.svgGroup_);
var bottom = Blockly.Scrollbar.scrollbarThickness;
if (this.options.hasTrashcan) {
bottom = this.addTrashcan_(bottom);
}
if (this.options.zoomOptions && this.options.zoomOptions.controls) {
this.addZoomControls_(bottom);
}
if (!this.isFlyout) {
Blockly.bindEventWithChecks_(this.svgGroup_, 'mousedown', this,
@@ -582,30 +575,24 @@ Blockly.WorkspaceSvg.prototype.newBlock = function(prototypeName, opt_id) {
/**
* Add a trashcan.
* @param {number} bottom Distance from workspace bottom to bottom of trashcan.
* @return {number} Distance from workspace bottom to the top of trashcan.
* @private
* @package
*/
Blockly.WorkspaceSvg.prototype.addTrashcan_ = function(bottom) {
Blockly.WorkspaceSvg.prototype.addTrashcan = function() {
/** @type {Blockly.Trashcan} */
this.trashcan = new Blockly.Trashcan(this);
var svgTrashcan = this.trashcan.createDom();
this.svgGroup_.insertBefore(svgTrashcan, this.svgBlockCanvas_);
return this.trashcan.init(bottom);
};
/**
* Add zoom controls.
* @param {number} bottom Distance from workspace bottom to bottom of controls.
* @return {number} Distance from workspace bottom to the top of controls.
* @private
* @package
*/
Blockly.WorkspaceSvg.prototype.addZoomControls_ = function(bottom) {
Blockly.WorkspaceSvg.prototype.addZoomControls = function() {
/** @type {Blockly.ZoomControls} */
this.zoomControls_ = new Blockly.ZoomControls(this);
var svgZoomControls = this.zoomControls_.createDom();
this.svgGroup_.appendChild(svgZoomControls);
return this.zoomControls_.init(bottom);
};
/**

View File

@@ -132,6 +132,10 @@ Blockly.ZoomControls.prototype.dispose = function() {
* Move the zoom controls to the bottom-right corner.
*/
Blockly.ZoomControls.prototype.position = function() {
// Not yet initialized.
if (!this.bottom_) {
return;
}
var metrics = this.workspace_.getMetrics();
if (!metrics) {
// There are no metrics available (workspace is probably not visible).