diff --git a/blocks/loops.ts b/blocks/loops.ts
index 8729619a467..50e4fcff96f 100644
--- a/blocks/loops.ts
+++ b/blocks/loops.ts
@@ -334,6 +334,11 @@ export type ControlFlowInLoopBlock = Block & ControlFlowInLoopMixin;
interface ControlFlowInLoopMixin extends ControlFlowInLoopMixinType {}
type ControlFlowInLoopMixinType = typeof CONTROL_FLOW_IN_LOOP_CHECK_MIXIN;
+/**
+ * The language-neutral ID for when the reason why a block is disabled is
+ * because the block is only valid inside of a loop.
+ */
+const CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON = 'CONTROL_FLOW_NOT_IN_LOOP';
/**
* This mixin adds a check to make sure the 'controls_flow_statements' block
* is contained in a loop. Otherwise a warning is added to the block.
@@ -380,7 +385,10 @@ const CONTROL_FLOW_IN_LOOP_CHECK_MIXIN = {
const group = Events.getGroup();
// Makes it so the move and the disable event get undone together.
Events.setGroup(e.group);
- this.setDisabledReason(!enabled, 'CONTROL_FLOW_NOT_IN_LOOP');
+ this.setDisabledReason(
+ !enabled,
+ CONTROL_FLOW_NOT_IN_LOOP_DISABLED_REASON,
+ );
Events.setGroup(group);
}
},
diff --git a/blocks/procedures.ts b/blocks/procedures.ts
index d82c2056e9c..32139264429 100644
--- a/blocks/procedures.ts
+++ b/blocks/procedures.ts
@@ -763,6 +763,13 @@ type CallExtraState = {
params?: string[];
};
+/**
+ * The language-neutral ID for when the reason why a block is disabled is
+ * because the block's corresponding procedure definition is disabled.
+ */
+const DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON =
+ 'DISABLED_PROCEDURE_DEFINITION';
+
/**
* Common properties for the procedure_callnoreturn and
* procedure_callreturn blocks.
@@ -1124,7 +1131,10 @@ const PROCEDURE_CALL_COMMON = {
}
Events.setGroup(event.group);
const valid = def.isEnabled();
- this.setDisabledReason(!valid, 'DISABLED_PROCEDURE_DEFINITION');
+ this.setDisabledReason(
+ !valid,
+ DISABLED_PROCEDURE_DEFINITION_DISABLED_REASON,
+ );
this.setWarningText(
valid
? null
@@ -1217,6 +1227,12 @@ interface IfReturnMixin extends IfReturnMixinType {
}
type IfReturnMixinType = typeof PROCEDURES_IFRETURN;
+/**
+ * The language-neutral ID for when the reason why a block is disabled is
+ * because the block is only valid inside of a procedure body.
+ */
+const UNPARENTED_IFRETURN_DISABLED_REASON = 'UNPARENTED_IFRETURN';
+
const PROCEDURES_IFRETURN = {
/**
* Block for conditionally returning a value from a procedure.
@@ -1317,7 +1333,7 @@ const PROCEDURES_IFRETURN = {
const group = Events.getGroup();
// Makes it so the move and the disable event get undone together.
Events.setGroup(e.group);
- this.setDisabledReason(!legal, 'UNPARENTED_IFRETURN');
+ this.setDisabledReason(!legal, UNPARENTED_IFRETURN_DISABLED_REASON);
Events.setGroup(group);
}
},
diff --git a/core/contextmenu_items.ts b/core/contextmenu_items.ts
index d5616c01d69..1c8d98cdb08 100644
--- a/core/contextmenu_items.ts
+++ b/core/contextmenu_items.ts
@@ -467,7 +467,14 @@ export function registerDisable() {
block!.workspace.options.disable &&
block!.isEditable()
) {
- if (block!.getInheritedDisabled()) {
+ // Determine whether this block is currently disabled for any reason
+ // other than the manual reason that this context menu item controls.
+ const disabledReasons = block!.getDisabledReasons();
+ const isDisabledForOtherReason =
+ disabledReasons.size >
+ (disabledReasons.has(constants.MANUALLY_DISABLED) ? 1 : 0);
+
+ if (block!.getInheritedDisabled() || isDisabledForOtherReason) {
return 'disabled';
}
return 'enabled';
diff --git a/core/events/utils.ts b/core/events/utils.ts
index 39da9a756c5..eacf0490673 100644
--- a/core/events/utils.ts
+++ b/core/events/utils.ts
@@ -188,6 +188,12 @@ export const COMMENT_COLLAPSE = 'comment_collapse';
*/
export const FINISHED_LOADING = 'finished_loading';
+/**
+ * The language-neutral ID for when the reason why a block is disabled is
+ * because the block is not descended from a root block.
+ */
+const ORPHANED_BLOCK_DISABLED_REASON = 'ORPHANED_BLOCK';
+
/**
* Type of events that cause objects to be bumped back into the visible
* portion of the workspace.
@@ -522,7 +528,6 @@ export function get(
* @param event Custom data for event.
*/
export function disableOrphans(event: Abstract) {
- const disabledReason = 'ORPHANED_BLOCK';
if (event.type === MOVE || event.type === CREATE) {
const blockEvent = event as BlockMove | BlockCreate;
if (!blockEvent.workspaceId) {
@@ -541,17 +546,20 @@ export function disableOrphans(event: Abstract) {
try {
recordUndo = false;
const parent = block.getParent();
- if (parent && !parent.hasDisabledReason(disabledReason)) {
+ if (
+ parent &&
+ !parent.hasDisabledReason(ORPHANED_BLOCK_DISABLED_REASON)
+ ) {
const children = block.getDescendants(false);
for (let i = 0, child; (child = children[i]); i++) {
- child.setDisabledReason(false, disabledReason);
+ child.setDisabledReason(false, ORPHANED_BLOCK_DISABLED_REASON);
}
} else if (
(block.outputConnection || block.previousConnection) &&
!eventWorkspace.isDragging()
) {
do {
- block.setDisabledReason(true, disabledReason);
+ block.setDisabledReason(true, ORPHANED_BLOCK_DISABLED_REASON);
block = block.getNextBlock();
} while (block);
}
diff --git a/core/flyout_base.ts b/core/flyout_base.ts
index 10bd4c39cde..ce1959a377f 100644
--- a/core/flyout_base.ts
+++ b/core/flyout_base.ts
@@ -43,6 +43,13 @@ enum FlyoutItemType {
BUTTON = 'button',
}
+/**
+ * The language-neutral ID for when the reason why a block is disabled is
+ * because the workspace is at block capacity.
+ */
+const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON =
+ 'WORKSPACE_AT_BLOCK_CAPACITY';
+
/**
* Class for a flyout.
*/
@@ -1239,7 +1246,10 @@ export abstract class Flyout
common.getBlockTypeCounts(block),
);
while (block) {
- block.setDisabledReason(!enable, 'WORKSPACE_AT_BLOCK_CAPACITY');
+ block.setDisabledReason(
+ !enable,
+ WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON,
+ );
block = block.getNextBlock();
}
}
diff --git a/core/serialization/blocks.ts b/core/serialization/blocks.ts
index 6e945c378d6..7a59ca0bc36 100644
--- a/core/serialization/blocks.ts
+++ b/core/serialization/blocks.ts
@@ -527,7 +527,7 @@ function loadAttributes(block: Block, state: State) {
'enabled',
'v11',
'v12',
- 'disabledReasons to ["MANUALLY_DISABLED"]',
+ 'disabledReasons with the value ["' + constants.MANUALLY_DISABLED + '"]',
);
block.setDisabledReason(true, constants.MANUALLY_DISABLED);
}
diff --git a/core/xml.ts b/core/xml.ts
index 64df838365f..3f90b7390d8 100644
--- a/core/xml.ts
+++ b/core/xml.ts
@@ -1027,7 +1027,7 @@ function domToBlockHeadless(
'disabled',
'v11',
'v12',
- 'disabled-reasons to "MANUALLY_DISABLED"',
+ 'disabled-reasons with the value "' + constants.MANUALLY_DISABLED + '"',
);
block.setDisabledReason(
disabled === 'true' || disabled === 'disabled',
diff --git a/tests/mocha/jso_serialization_test.js b/tests/mocha/jso_serialization_test.js
index 9a28e31c7de..72a74ad3d6a 100644
--- a/tests/mocha/jso_serialization_test.js
+++ b/tests/mocha/jso_serialization_test.js
@@ -86,20 +86,31 @@ suite('JSO Serialization', function () {
});
});
- suite('Enabled', function () {
- test('False', function () {
+ suite('DisabledReasons', function () {
+ test('One reason', function () {
const block = this.workspace.newBlock('row_block');
block.setDisabledReason(true, 'test reason');
const jso = Blockly.serialization.blocks.save(block);
assertProperty(jso, 'disabledReasons', ['test reason']);
});
- test('True', function () {
+ test('Zero reasons', function () {
const block = this.workspace.newBlock('row_block');
block.setDisabledReason(false, 'test reason');
const jso = Blockly.serialization.blocks.save(block);
assertNoProperty(jso, 'disabledReasons');
});
+
+ test('Multiple reasons', function () {
+ const block = this.workspace.newBlock('row_block');
+ block.setDisabledReason(true, 'test reason 1');
+ block.setDisabledReason(true, 'test reason 2');
+ const jso = Blockly.serialization.blocks.save(block);
+ assertProperty(jso, 'disabledReasons', [
+ 'test reason 1',
+ 'test reason 2',
+ ]);
+ });
});
suite('Inline', function () {
diff --git a/tests/mocha/serializer_test.js b/tests/mocha/serializer_test.js
index 35fcd0c9096..b10f48df515 100644
--- a/tests/mocha/serializer_test.js
+++ b/tests/mocha/serializer_test.js
@@ -84,6 +84,12 @@ Serializer.Attributes.Disabled = new SerializerTestCase(
'' +
'',
);
+Serializer.Attributes.DisabledWithEncodedComma = new SerializerTestCase(
+ 'DisabledWithEncodedComma',
+ '' +
+ '' +
+ '',
+);
Serializer.Attributes.NotDeletable = new SerializerTestCase(
'Deletable',
'' +
@@ -106,6 +112,7 @@ Serializer.Attributes.testCases = [
Serializer.Attributes.Basic,
Serializer.Attributes.Collapsed,
Serializer.Attributes.Disabled,
+ Serializer.Attributes.DisabledWithEncodedComma,
Serializer.Attributes.NotDeletable,
Serializer.Attributes.NotMovable,
Serializer.Attributes.NotEditable,