mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
Added Max Instances Property to Workspace Options (#2130)
* Added Max Instances property to Blocks * eslint cleanup * eslint cleanup 2 * Moved maxInstances property from block to workspace (as a map of block type to max instances). isDuplicate() changed to correctly handle siblings/branches. * eslint cleanup * Changed checking types to map. Added hasBlockLimits. Fixed Nits. * Added limit_instances test block. eslint fixes. * fixup! Added limit_instances test block. eslint fixes. * Changed sorting objects to a private static function of the workspace. Fixed nits. Undeleted .eslintrc * Reverted .gitignore file. * Added getBlockTypeCounts() to utils. Added isCapacityAvailable() to workspace. Changed clipboard to save typeCountsMap rather than object.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,4 +11,4 @@ npm-debug.log
|
||||
tests/compile/main_compressed.js
|
||||
tests/compile/*compiler*.jar
|
||||
local_build/*compiler*.jar
|
||||
local_build/local_blockly_compressed.js
|
||||
local_build/local_blockly_compressed.js
|
||||
@@ -164,6 +164,7 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
|
||||
}
|
||||
|
||||
workspace.addTopBlock(this);
|
||||
workspace.addTypedBlock(this);
|
||||
|
||||
// Call an initialization function, if it exists.
|
||||
if (typeof this.init == 'function') {
|
||||
@@ -255,6 +256,7 @@ Blockly.Block.prototype.dispose = function(healStack) {
|
||||
// Remove this block from the workspace's list of top-most blocks.
|
||||
if (this.workspace) {
|
||||
this.workspace.removeTopBlock(this);
|
||||
this.workspace.removeTypedBlock(this);
|
||||
// Remove from block database.
|
||||
delete this.workspace.blockDB_[this.id];
|
||||
this.workspace = null;
|
||||
@@ -668,6 +670,21 @@ Blockly.Block.prototype.setMovable = function(movable) {
|
||||
this.movable_ = movable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get whether is block is duplicatable or not. If duplicating this block and
|
||||
* descendants will put this block over the workspace's capacity this block is
|
||||
* not duplicatable. If duplicating this block and descendants will put any
|
||||
* type over their maxInstances this block is not duplicatable.
|
||||
* @return {boolean} True if duplicatable.
|
||||
*/
|
||||
Blockly.Block.prototype.isDuplicatable = function() {
|
||||
if (!this.workspace.hasBlockLimits()) {
|
||||
return true;
|
||||
}
|
||||
return this.workspace.isCapacityAvailable(
|
||||
Blockly.utils.getBlockTypeCounts(this, true));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get whether this block is a shadow block or not.
|
||||
* @return {boolean} True if a shadow.
|
||||
|
||||
@@ -98,6 +98,13 @@ Blockly.clipboardXml_ = null;
|
||||
*/
|
||||
Blockly.clipboardSource_ = null;
|
||||
|
||||
/**
|
||||
* Map of types to type counts for the clipboard object and descendants.
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
Blockly.clipboardTypeCounts_ = null;
|
||||
|
||||
/**
|
||||
* Cached value for whether 3D is supported.
|
||||
* @type {!boolean}
|
||||
@@ -227,15 +234,18 @@ Blockly.onKeyDown_ = function(e) {
|
||||
if (e.keyCode == 86) {
|
||||
// 'v' for paste.
|
||||
if (Blockly.clipboardXml_) {
|
||||
Blockly.Events.setGroup(true);
|
||||
// Pasting always pastes to the main workspace, even if the copy
|
||||
// started in a flyout workspace.
|
||||
var workspace = Blockly.clipboardSource_;
|
||||
if (workspace.isFlyout) {
|
||||
workspace = workspace.targetWorkspace;
|
||||
}
|
||||
workspace.paste(Blockly.clipboardXml_);
|
||||
Blockly.Events.setGroup(false);
|
||||
if (Blockly.clipboardTypeCounts_ &&
|
||||
workspace.isCapacityAvailable(Blockly.clipboardTypeCounts_)) {
|
||||
Blockly.Events.setGroup(true);
|
||||
workspace.paste(Blockly.clipboardXml_);
|
||||
Blockly.Events.setGroup(false);
|
||||
}
|
||||
}
|
||||
} else if (e.keyCode == 90) {
|
||||
// 'z' for undo 'Z' is for redo.
|
||||
@@ -273,6 +283,8 @@ Blockly.copy_ = function(toCopy) {
|
||||
}
|
||||
Blockly.clipboardXml_ = xml;
|
||||
Blockly.clipboardSource_ = toCopy.workspace;
|
||||
Blockly.clipboardTypeCounts_ = toCopy.isComment ? null :
|
||||
Blockly.utils.getBlockTypeCounts(toCopy, true);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -260,11 +260,7 @@ Blockly.ContextMenu.blockHelpOption = function(block) {
|
||||
* @package
|
||||
*/
|
||||
Blockly.ContextMenu.blockDuplicateOption = function(block) {
|
||||
var enabled = true;
|
||||
if (block.getDescendants(false).length >
|
||||
block.workspace.remainingCapacity()) {
|
||||
enabled = false;
|
||||
}
|
||||
var enabled = block.isDuplicatable();
|
||||
var duplicateOption = {
|
||||
text: Blockly.Msg['DUPLICATE_BLOCK'],
|
||||
enabled: enabled,
|
||||
|
||||
@@ -228,6 +228,7 @@ Blockly.Flyout.prototype.createDom = function(tagName) {
|
||||
Blockly.Flyout.prototype.init = function(targetWorkspace) {
|
||||
this.targetWorkspace_ = targetWorkspace;
|
||||
this.workspace_.targetWorkspace = targetWorkspace;
|
||||
|
||||
// Add scrollbar.
|
||||
this.scrollbar_ = new Blockly.Scrollbar(this.workspace_,
|
||||
this.horizontalLayout_, false, 'blocklyFlyoutScrollbar');
|
||||
@@ -729,7 +730,8 @@ Blockly.Flyout.prototype.filterForCapacity_ = function() {
|
||||
for (var i = 0, block; block = blocks[i]; i++) {
|
||||
if (this.permanentlyDisabled_.indexOf(block) == -1) {
|
||||
var allBlocks = block.getDescendants(false);
|
||||
block.setDisabled(allBlocks.length > remainingCapacity);
|
||||
block.setDisabled(allBlocks.length > remainingCapacity ||
|
||||
this.targetWorkspace_.remainingCapacityOfType(block.type) <= 0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -119,6 +119,7 @@ Blockly.Options = function(options) {
|
||||
this.disable = hasDisable;
|
||||
this.readOnly = readOnly;
|
||||
this.maxBlocks = options['maxBlocks'] || Infinity;
|
||||
this.maxInstances = options['maxInstances'];
|
||||
this.pathToMedia = pathToMedia;
|
||||
this.hasCategories = hasCategories;
|
||||
this.hasScrollbars = hasScrollbars;
|
||||
|
||||
@@ -974,3 +974,32 @@ Blockly.utils.containsNode = function(parent, descendant) {
|
||||
return !!(parent.compareDocumentPosition(descendant) &
|
||||
Node.DOCUMENT_POSITION_CONTAINED_BY);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a map of all the block's descendants mapping their type to the number of
|
||||
* children with that type.
|
||||
* @param {!Blockly.Block} block The block to map.
|
||||
* @param {boolean=} opt_stripFollowing Optionally ignore all following
|
||||
* statements (blocks that are not inside a value or statement input
|
||||
* of the block).
|
||||
* @returns {!Object} Map of types to type counts for descendants of the bock.
|
||||
*/
|
||||
Blockly.utils.getBlockTypeCounts = function(block, opt_stripFollowing) {
|
||||
var typeCountsMap = Object.create(null);
|
||||
var descendants = block.getDescendants(true);
|
||||
if (opt_stripFollowing) {
|
||||
var nextBlock = block.getNextBlock();
|
||||
if (nextBlock) {
|
||||
var index = descendants.indexOf(nextBlock);
|
||||
descendants.splice(index, descendants.length - index);
|
||||
}
|
||||
}
|
||||
for (var i = 0, checkBlock; checkBlock = descendants[i]; i++) {
|
||||
if (typeCountsMap[checkBlock.type]) {
|
||||
typeCountsMap[checkBlock.type]++;
|
||||
} else {
|
||||
typeCountsMap[checkBlock.type] = 1;
|
||||
}
|
||||
}
|
||||
return typeCountsMap;
|
||||
};
|
||||
|
||||
@@ -85,6 +85,11 @@ Blockly.Workspace = function(opt_options) {
|
||||
* @private
|
||||
*/
|
||||
this.blockDB_ = Object.create(null);
|
||||
/**
|
||||
* @type {!Object}
|
||||
* @private
|
||||
*/
|
||||
this.typedBlocksDB_ = Object.create(null);
|
||||
|
||||
/**
|
||||
* A map from variable type to list of variable names. The lists contain all
|
||||
@@ -147,6 +152,24 @@ Blockly.Workspace.prototype.dispose = function() {
|
||||
*/
|
||||
Blockly.Workspace.SCAN_ANGLE = 3;
|
||||
|
||||
/**
|
||||
* Compare function for sorting objects (blocks, comments, etc) by position;
|
||||
* top to bottom (with slight LTR or RTL bias).
|
||||
* @param {!Blockly.Block | !Blockly.WorkspaceComment} a The first object to
|
||||
* compare.
|
||||
* @param {!Blockly.Block | !Blockly.WorkspaceComment} b The second object to
|
||||
* compare.
|
||||
* @returns {number} The comparison value. This tells Array.sort() how to change
|
||||
* object a's index.
|
||||
* @private
|
||||
*/
|
||||
Blockly.Workspace.prototype.sortObjects_ = function(a, b) {
|
||||
var aXY = a.getRelativeToSurfaceXY();
|
||||
var bXY = b.getRelativeToSurfaceXY();
|
||||
return (aXY.y + Blockly.Workspace.prototype.sortObjects_.offset * aXY.x) -
|
||||
(bXY.y + Blockly.Workspace.prototype.sortObjects_.offset * bXY.x);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a block to the list of top blocks.
|
||||
* @param {!Blockly.Block} block Block to add.
|
||||
@@ -175,16 +198,58 @@ Blockly.Workspace.prototype.getTopBlocks = function(ordered) {
|
||||
// Copy the topBlocks_ list.
|
||||
var blocks = [].concat(this.topBlocks_);
|
||||
if (ordered && blocks.length > 1) {
|
||||
var offset =
|
||||
this.sortObjects_.offset =
|
||||
Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE));
|
||||
if (this.RTL) {
|
||||
offset *= -1;
|
||||
this.sortObjects_.offset *= -1;
|
||||
}
|
||||
blocks.sort(function(a, b) {
|
||||
var aXY = a.getRelativeToSurfaceXY();
|
||||
var bXY = b.getRelativeToSurfaceXY();
|
||||
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
|
||||
});
|
||||
blocks.sort(this.sortObjects_);
|
||||
}
|
||||
return blocks;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a block to the list of blocks keyed by type.
|
||||
* @param {!Blockly.Block} block Block to add.
|
||||
*/
|
||||
Blockly.Workspace.prototype.addTypedBlock = function(block) {
|
||||
if (!this.typedBlocksDB_[block.type]) {
|
||||
this.typedBlocksDB_[block.type] = [];
|
||||
}
|
||||
this.typedBlocksDB_[block.type].push(block);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove a block from the list of blocks keyed by type.
|
||||
* @param {!Blockly.Block} block Block to remove.
|
||||
*/
|
||||
Blockly.Workspace.prototype.removeTypedBlock = function(block) {
|
||||
this.typedBlocksDB_[block.type].splice(this.typedBlocksDB_[block.type]
|
||||
.indexOf(block), 1);
|
||||
if (!this.typedBlocksDB_[block.type].length) {
|
||||
delete this.typedBlocksDB_[block.type];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Finds the blocks with the associated type and returns them. Blocks are
|
||||
* optionally sorted by position; top to bottom (with slight LTR or RTL bias).
|
||||
* @param {string} type The type of block to search for.
|
||||
* @param {boolean} ordered Sort the list if true.
|
||||
* @return {!Array.<!Blockly.Block>} The blocks of the given type.
|
||||
*/
|
||||
Blockly.Workspace.prototype.getBlocksByType = function(type, ordered) {
|
||||
if (!this.typedBlocksDB_[type]) {
|
||||
return [];
|
||||
}
|
||||
var blocks = this.typedBlocksDB_[type].slice(0);
|
||||
if (ordered && blocks.length > 1) {
|
||||
this.sortObjects_.offset =
|
||||
Math.sign(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE));
|
||||
if (this.RTL) {
|
||||
this.sortObjects_.offset *= -1;
|
||||
}
|
||||
blocks.sort(this.sortObjects_);
|
||||
}
|
||||
return blocks;
|
||||
};
|
||||
@@ -232,16 +297,12 @@ Blockly.Workspace.prototype.getTopComments = function(ordered) {
|
||||
// Copy the topComments_ list.
|
||||
var comments = [].concat(this.topComments_);
|
||||
if (ordered && comments.length > 1) {
|
||||
var offset =
|
||||
this.sortObjects_.offset =
|
||||
Math.sin(Blockly.utils.toRadians(Blockly.Workspace.SCAN_ANGLE));
|
||||
if (this.RTL) {
|
||||
offset *= -1;
|
||||
this.sortObjects_.offset *= -1;
|
||||
}
|
||||
comments.sort(function(a, b) {
|
||||
var aXY = a.getRelativeToSurfaceXY();
|
||||
var bXY = b.getRelativeToSurfaceXY();
|
||||
return (aXY.y + offset * aXY.x) - (bXY.y + offset * bXY.x);
|
||||
});
|
||||
comments.sort(this.sortObjects_);
|
||||
}
|
||||
return comments;
|
||||
};
|
||||
@@ -455,6 +516,56 @@ Blockly.Workspace.prototype.remainingCapacity = function() {
|
||||
return this.options.maxBlocks - this.getAllBlocks().length;
|
||||
};
|
||||
|
||||
/**
|
||||
* The number of blocks of the given type that may be added to the workspace
|
||||
* before reaching the maxInstances allowed for that type.
|
||||
* @param {string} type Type of block to return capacity for.
|
||||
* @return {number} Number of blocks of type left.
|
||||
*/
|
||||
Blockly.Workspace.prototype.remainingCapacityOfType = function(type) {
|
||||
if (!this.options.maxInstances) {
|
||||
return Infinity;
|
||||
}
|
||||
return (this.options.maxInstances[type] || Infinity) -
|
||||
this.getBlocksByType(type).length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is remaining capacity for blocks of the given counts to be
|
||||
* created. If the total number of blocks represented by the map is more than
|
||||
* the total remaining capacity, it returns false. If a type count is more
|
||||
* than the remaining capacity for that type, it returns false.
|
||||
* @param {!Object} typeCountsMap A map of types to counts (usually representing
|
||||
* blocks to be created).
|
||||
* @returns {boolean} True if there is capacity for the given map,
|
||||
* false otherwise.
|
||||
*/
|
||||
Blockly.Workspace.prototype.isCapacityAvailable = function(typeCountsMap) {
|
||||
if (!this.hasBlockLimits()) {
|
||||
return true;
|
||||
}
|
||||
var copyableBlocksCount = 0;
|
||||
for (var type in typeCountsMap) {
|
||||
if (typeCountsMap[type] > this.remainingCapacityOfType(type)) {
|
||||
return false;
|
||||
}
|
||||
copyableBlocksCount += typeCountsMap[type];
|
||||
}
|
||||
if (copyableBlocksCount > this.remainingCapacity()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the workspace has any limits on the maximum number of blocks,
|
||||
* or the maximum number of blocks of specific types.
|
||||
* @returns {boolean} True if it has block limits, false otherwise.
|
||||
*/
|
||||
Blockly.Workspace.prototype.hasBlockLimits = function() {
|
||||
return this.options.maxBlocks != Infinity || !!this.options.maxInstances;
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo or redo the previous action.
|
||||
* @param {boolean} redo False if undo, true if redo.
|
||||
|
||||
@@ -44,6 +44,22 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
|
||||
"output": null,
|
||||
"colour": 230
|
||||
},
|
||||
{
|
||||
"type": "limit_instances",
|
||||
"message0": "limit 3 instances %1 %2",
|
||||
"args0": [
|
||||
{
|
||||
"type": "input_dummy"
|
||||
},
|
||||
{
|
||||
"type": "input_statement",
|
||||
"name": "STATEMENT"
|
||||
}
|
||||
],
|
||||
"previousStatement": null,
|
||||
"nextStatement": null,
|
||||
"colour": 230,
|
||||
},
|
||||
{
|
||||
"type": "example_dropdown_long",
|
||||
"message0": "long: %1",
|
||||
@@ -145,17 +161,17 @@ Blockly.defineBlocksWithJsonArray([ // BEGIN JSON EXTRACT
|
||||
"type": "example_angle",
|
||||
"message0": "angle: %1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "field_angle",
|
||||
"name": "FIELDNAME",
|
||||
"angle": "90",
|
||||
"alt":
|
||||
{
|
||||
"type": "field_label",
|
||||
"text": "NO ANGLE FIELD"
|
||||
}
|
||||
}
|
||||
]
|
||||
{
|
||||
"type": "field_angle",
|
||||
"name": "FIELDNAME",
|
||||
"angle": "90",
|
||||
"alt":
|
||||
{
|
||||
"type": "field_label",
|
||||
"text": "NO ANGLE FIELD"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "example_date",
|
||||
|
||||
@@ -103,6 +103,7 @@ function start() {
|
||||
},
|
||||
horizontalLayout: side == 'top' || side == 'bottom',
|
||||
maxBlocks: Infinity,
|
||||
maxInstances: {'limit_instances': 3},
|
||||
media: '../media/',
|
||||
oneBasedIndex: true,
|
||||
readOnly: false,
|
||||
@@ -1080,6 +1081,7 @@ h1 {
|
||||
<block type="empty_block_with_mutator"></block>
|
||||
<block type="value_to_stack"></block>
|
||||
<block type="value_to_statement"></block>
|
||||
<block type="limit_instances"></block>
|
||||
</category>
|
||||
<category name="Drag">
|
||||
<label text="Drag each to the workspace"></label>
|
||||
|
||||
Reference in New Issue
Block a user