diff --git a/i18n/strings_to_json_mapping.json b/i18n/strings_to_json_mapping.json
index ef779237..c990302f 100644
--- a/i18n/strings_to_json_mapping.json
+++ b/i18n/strings_to_json_mapping.json
@@ -275,6 +275,10 @@
"TEXT_BLOCK_LANGUAGE_FROM_CAMERA": "${formula_editor_function_text_block_from_camera}",
"DELETE": "${am_delete}",
"DELETE_BLOCK": "${brick_context_dialog_delete_brick}",
+ "BRICK_CONTEXT_DIALOG_COMMENT_OUT_SCRIPT": "${brick_context_dialog_comment_out_script}",
+ "BRICK_CONTEXT_DIALOG_COMMENT_IN_SCRIPT": "${brick_context_dialog_comment_in_script}",
+ "BRICK_CONTEXT_DIALOG_COMMENT_OUT": "${brick_context_dialog_comment_out}",
+ "BRICK_CONTEXT_DIALOG_COMMENT_IN": "${brick_context_dialog_comment_in}",
"HELP": "${main_menu_help}",
"EMBROIDERY_STITCH": "${brick_stitch}",
"EMBRIODERY_SEW_UP": "${brick_sew_up}",
diff --git a/src/intern/ts/Testing.ts b/src/intern/ts/Testing.ts
index 126324ec..8efea195 100644
--- a/src/intern/ts/Testing.ts
+++ b/src/intern/ts/Testing.ts
@@ -22,6 +22,9 @@ class MockAndroid implements IAndroid {
public duplicateBrick(brickID: string): string {
throw new Error('Method not implemented.');
}
+ public commentOutBrick(brickID: string): string {
+ throw new Error('Method not implemented.');
+ }
public getCurrentProject(): string {
throw new Error('Method not implemented.');
}
diff --git a/src/library/js/integration/catroid.js b/src/library/js/integration/catroid.js
index b71e459e..b9898303 100644
--- a/src/library/js/integration/catroid.js
+++ b/src/library/js/integration/catroid.js
@@ -19,7 +19,9 @@ import {
advancedModeAddParentheses,
advancedModeAddCurlyBrackets,
getFieldByCatroidFieldID,
- advancedModeRemoveWhiteSpacesInFormulas
+ advancedModeRemoveWhiteSpacesInFormulas,
+ updateAdvancedModeCommentOutBricks,
+ updateAdvancedModeUncommentOutBricks
} from './utils';
import { CatBlocksMsgs } from '../../ts/i18n/CatBlocksMsgs';
import advancedTheme from '../advanced_theme.json';
@@ -122,6 +124,82 @@ export class Catroid {
return 'hidden';
};
+ Blockly.ContextMenuRegistry.registry.getItem('blockComment').callback = scope => {
+ const block = scope.block;
+ // TODO: Why should it be possible to disable a NoteBrick? (It is possible in 1D-View)
+ if (block && block.id) {
+ Android.commentOutBrick(scope.block.id);
+ if (block.disable) {
+ block.disable = false;
+ if (this.config.advancedMode) {
+ updateAdvancedModeUncommentOutBricks(block);
+ }
+ Blockly.utils.dom.removeClass(block.pathObject.svgRoot, 'catblocks-blockly-disabled');
+ if (Array.from(getBrickScriptMapping().values()).includes(block.type)) {
+ this.enableDisableAllBlocksAndNestedStatements(null, block.getChildren()[0], false);
+ }
+ if (block.statementInputCount > 0) {
+ const child = Array.from(block.getChildren().filter(c => c !== block.nextConnection.targetBlock()));
+ this.enableDisableAllBlocksAndNestedStatements(block, child, false);
+ }
+ } else {
+ block.disable = true;
+ if (this.config.advancedMode) {
+ updateAdvancedModeCommentOutBricks(block);
+ }
+ Blockly.utils.dom.addClass(block.pathObject.svgRoot, 'catblocks-blockly-disabled');
+ // Change CSS class of all blocks inside the script
+ if (Array.from(getBrickScriptMapping().values()).includes(block.type)) {
+ this.enableDisableAllBlocksAndNestedStatements(null, block.getChildren()[0], true);
+ }
+ if (block.statementInputCount > 0) {
+ // get all child of block, except the nextConnection
+ const child = Array.from(block.getChildren().filter(c => c !== block.nextConnection.targetBlock()));
+ this.enableDisableAllBlocksAndNestedStatements(block, child, true);
+ }
+ }
+ } else {
+ console.log('could not disable or enable brick - might be that it is a NoteBrick');
+ }
+ };
+
+ Blockly.ContextMenuRegistry.registry.getItem('blockComment').displayText = function (scope) {
+ const block = scope.block;
+ if (!block.isInFlyout && block.isDeletable() && block.isMovable()) {
+ if (Array.from(getBrickScriptMapping().values()).includes(block.type)) {
+ if (!block.disable) {
+ return CatBlocksMsgs.getCurrentLocaleValues()['BRICK_CONTEXT_DIALOG_COMMENT_OUT_SCRIPT'];
+ } else {
+ return CatBlocksMsgs.getCurrentLocaleValues()['BRICK_CONTEXT_DIALOG_COMMENT_IN_SCRIPT'];
+ }
+ }
+ if (!block.disable) {
+ return CatBlocksMsgs.getCurrentLocaleValues()['BRICK_CONTEXT_DIALOG_COMMENT_OUT'];
+ }
+ return CatBlocksMsgs.getCurrentLocaleValues()['BRICK_CONTEXT_DIALOG_COMMENT_IN'];
+ }
+ };
+
+ Blockly.ContextMenuRegistry.registry.getItem('blockComment').preconditionFn = function (scope) {
+ const block = scope.block;
+
+ if (
+ (block.type && block.type.endsWith('_UDB_CATBLOCKS_DEF')) ||
+ block.type === 'UserDefinedScript' ||
+ block.type === 'NoteBrick'
+ ) {
+ return 'hidden';
+ }
+
+ if (!block.isInFlyout && block.isDeletable() && block.isMovable()) {
+ if (block.isDuplicatable()) {
+ return 'enabled';
+ }
+ return 'disabled';
+ }
+ return 'hidden';
+ };
+
Blockly.ContextMenuRegistry.registry.getItem('blockHelp').callback = function (scope) {
Android.helpBrick(scope.block.id);
};
@@ -695,4 +773,42 @@ export class Catroid {
this.workspace.scroll(this.workspace.scrollX - boundingRect.x, this.workspace.scrollY - boundingRect.y);
}
}
+
+ enableDisableAllBlocksAndNestedStatements(block, child, disable) {
+ if (block) {
+ child.forEach(child => {
+ child.disable = disable;
+ if (disable) {
+ if (this.config.advancedMode && child.type !== 'NoteBrick') {
+ updateAdvancedModeCommentOutBricks(child);
+ }
+ Blockly.utils.dom.addClass(child.pathObject.svgRoot, 'catblocks-blockly-disabled');
+ } else {
+ if (this.config.advancedMode && child.type !== 'NoteBrick') {
+ updateAdvancedModeUncommentOutBricks(child);
+ }
+ Blockly.utils.dom.removeClass(child.pathObject.svgRoot, 'catblocks-blockly-disabled');
+ }
+ child.getChildren().forEach(c => {
+ this.enableDisableAllBlocksAndNestedStatements(null, c, disable);
+ });
+ });
+ } else {
+ child.disable = disable;
+ if (disable) {
+ if (this.config.advancedMode && child.type !== 'NoteBrick') {
+ updateAdvancedModeCommentOutBricks(child);
+ }
+ Blockly.utils.dom.addClass(child.pathObject.svgRoot, 'catblocks-blockly-disabled');
+ } else {
+ if (this.config.advancedMode && child.type !== 'NoteBrick') {
+ updateAdvancedModeUncommentOutBricks(child);
+ }
+ Blockly.utils.dom.removeClass(child.pathObject.svgRoot, 'catblocks-blockly-disabled');
+ }
+ child.getChildren().forEach(c => {
+ this.enableDisableAllBlocksAndNestedStatements(null, c, disable);
+ });
+ }
+ }
}
diff --git a/src/library/js/integration/utils.js b/src/library/js/integration/utils.js
index 8ff60992..b5b6e6a8 100644
--- a/src/library/js/integration/utils.js
+++ b/src/library/js/integration/utils.js
@@ -433,6 +433,7 @@ export const renderBrick = (parentBrick, jsonBrick, brickListType, workspace, re
Blockly.utils.dom.addClass(childBrick.pathObject.svgRoot, 'catblockls-blockly-invisible');
} else if (jsonBrick.commentedOut) {
Blockly.utils.dom.addClass(childBrick.pathObject.svgRoot, 'catblocks-blockly-disabled');
+ childBrick.disable = true;
if (workspace.getTheme().name.toLowerCase() === 'advanced') {
childBrick.setStyle('disabled');
}
@@ -913,6 +914,77 @@ function advancedModeCommentOutBricks(childBrick) {
}
}
+export function updateAdvancedModeCommentOutBricks(brick) {
+ if (!brick.inputList[0].fieldRow[0].getValue().startsWith('// ')) {
+ brick.inputList[0].fieldRow[0].setValue('// ' + brick.inputList[0].fieldRow[0].getValue());
+ }
+ if (
+ brick.type === 'IfLogicBeginBrick' ||
+ brick.type === 'PhiroIfLogicBeginBrick' ||
+ brick.type === 'RaspiIfLogicBeginBrick'
+ ) {
+ if (!brick.inputList[2].fieldRow[0].getValue().startsWith('// ')) {
+ brick.inputList[2].fieldRow[0].setValue('// ' + brick.inputList[2].fieldRow[0].getValue());
+ brick.inputList[4].fieldRow[0].setValue('// ' + brick.inputList[4].fieldRow[0].getValue());
+ }
+ }
+ if (brick.inputList.length === 3 && !brick.inputList[2].fieldRow[0].getValue().startsWith('// ')) {
+ brick.inputList[2].fieldRow[0].setValue('// ' + brick.inputList[2].fieldRow[0].getValue());
+ }
+
+ const brickElements = document.getElementById(brick.pathObject.svgRoot.id).childNodes;
+ let count = 1;
+ while (
+ count < brickElements.length &&
+ (brickElements[count].id.includes(brick.getSvgRoot().id) || !brickElements[count].id)
+ ) {
+ if (
+ brickElements[count].classList[0] !== 'blocklyNonEditableText' &&
+ brickElements[count].classList[0] !== 'blocklyEditableText'
+ ) {
+ brickElements[count].style.opacity = 0.5;
+ }
+ count++;
+ }
+}
+
+export function updateAdvancedModeUncommentOutBricks(brick) {
+ if (brick.inputList[0].fieldRow[0].getValue().startsWith('// ')) {
+ brick.inputList[0].fieldRow[0].setValue(brick.inputList[0].fieldRow[0].getValue().slice(3));
+ }
+ if (
+ brick.type === 'IfLogicBeginBrick' ||
+ brick.type === 'PhiroIfLogicBeginBrick' ||
+ brick.type === 'RaspiIfLogicBeginBrick'
+ ) {
+ if (
+ brick.inputList[2].fieldRow[0].getValue().startsWith('// ') &&
+ brick.inputList[4].fieldRow[0].getValue().startsWith('// ')
+ ) {
+ brick.inputList[2].fieldRow[0].setValue(brick.inputList[2].fieldRow[0].getValue().slice(3));
+ brick.inputList[4].fieldRow[0].setValue(brick.inputList[4].fieldRow[0].getValue().slice(3));
+ }
+ }
+ if (brick.inputList.length === 3 && brick.inputList[2].fieldRow[0].getValue().startsWith('// ')) {
+ brick.inputList[2].fieldRow[0].setValue(brick.inputList[2].fieldRow[0].getValue().slice(3));
+ }
+
+ const brickElements = document.getElementById(brick.pathObject.svgRoot.id).childNodes;
+ let count = 1;
+ while (
+ count < brickElements.length &&
+ (brickElements[count].id.includes(brick.getSvgRoot().id) || !brickElements[count].id)
+ ) {
+ if (
+ brickElements[count].classList[0] !== 'blocklyNonEditableText' &&
+ brickElements[count].classList[0] !== 'blocklyEditableText'
+ ) {
+ brickElements[count].style.opacity = 1;
+ }
+ count++;
+ }
+}
+
export const getFieldByCatroidFieldID = (brick, fieldID) => {
for (const input of brick.inputList) {
for (const field of input.fieldRow) {
diff --git a/src/library/ts/IAndroid.ts b/src/library/ts/IAndroid.ts
index fb7a6009..d7ff459e 100644
--- a/src/library/ts/IAndroid.ts
+++ b/src/library/ts/IAndroid.ts
@@ -2,6 +2,7 @@
interface IAndroid {
switchTo1D(brickID: string): void;
duplicateBrick(brickID: string): string;
+ commentOutBrick(brickID: string): void;
getCurrentProject(): string; // returns coeXML
updateScriptPosition(brickID: string, newX: number, newY: number): void;
helpBrick(brickID: string): void;
diff --git a/test/jsunit/catroid/context-menu.test.js b/test/jsunit/catroid/context-menu.test.js
new file mode 100644
index 00000000..1fa587b8
--- /dev/null
+++ b/test/jsunit/catroid/context-menu.test.js
@@ -0,0 +1,258 @@
+/* global Test, page */
+/* eslint no-global-assign:0 */
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+describe('Catroid Integration Advanced Mode tests', () => {
+ beforeAll(async () => {
+ await page.goto('http://localhost:8080', {
+ waitUntil: 'networkidle0'
+ });
+ const programXML = fs.readFileSync(path.resolve(__dirname, '../../programs/binding_of_krishna_1_12.xml'), 'utf8');
+ const language = 'en';
+ const rtl = false;
+ const advancedMode = false;
+ await page.evaluate(
+ async (pLanguage, pRTL, pAdvancedMode) => {
+ try {
+ await Test.CatroidCatBlocks.init({
+ container: 'catblocks-container',
+ renderSize: 0.75,
+ language: pLanguage,
+ rtl: pRTL,
+ i18n: '/i18n',
+ shareRoot: '',
+ media: 'media/',
+ noImageFound: 'No_Image_Available.jpg',
+ renderLooks: false,
+ renderSounds: false,
+ readOnly: false,
+ advancedMode: pAdvancedMode
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ language,
+ rtl,
+ advancedMode
+ );
+ await page.evaluate(async pProgramXML => {
+ await Test.CatroidCatBlocks.render(
+ pProgramXML,
+ 'Introduction',
+ 'Caption',
+ '7fc239bb-d330-4226-b075-0c4c545198e2'
+ );
+ }, programXML);
+
+ await page.evaluate(() => {
+ // function to JSON.stringify circular objects
+ window.shallowJSON = (obj, indent = 2) => {
+ let cache = [];
+ const retVal = JSON.stringify(
+ obj,
+ (key, value) =>
+ typeof value === 'object' && value !== null
+ ? cache.includes(value)
+ ? undefined // Duplicate reference found, discard key
+ : cache.push(value) && value // Store value in our collection
+ : value,
+ indent
+ );
+ cache = null;
+ return retVal;
+ };
+ });
+ });
+
+ test('Context menu item disable block text test', async () => {
+ const brickID = await page.evaluate(() => {
+ const blocks = document.querySelectorAll('.blocklyText');
+ const removedBlocks = Array.from(blocks).filter(
+ block => block !== null && block !== undefined && block.getAttribute('id') !== null
+ );
+ const setVariableBlock = Array.from(removedBlocks).find(block =>
+ block.getAttribute('id').includes('SetVariableBrick-0')
+ );
+ return setVariableBlock.id;
+ });
+
+ const mItems = [];
+ let isBlocklyContextMenu = false;
+ while (!isBlocklyContextMenu) {
+ await page.waitForSelector(`[id="${brickID}"]`, { visible: true });
+ await page.click(`[id="${brickID}"]`, { button: 'right' });
+
+ isBlocklyContextMenu = await page.evaluate(() => {
+ const contextMenu = document.querySelector('.blocklyMenu');
+ if (contextMenu) {
+ const menuItems = contextMenu.querySelectorAll('.blocklyMenuItemContent');
+ if (menuItems) {
+ return true;
+ }
+ }
+ return false;
+ });
+
+ if (isBlocklyContextMenu) {
+ const menuItems = await page.evaluate(() => {
+ const items = [];
+ const menuItems = document.querySelectorAll('.blocklyMenuItemContent');
+ for (const item of menuItems) {
+ items.push(item.innerText);
+ }
+ return items;
+ });
+ for (const i of menuItems) {
+ mItems.push(i);
+ }
+ }
+ }
+
+ expect(mItems).toContain('Disable brick');
+ }, 10000);
+
+ test('Context menu item enable brick block text test', async () => {
+ const brickID = await page.evaluate(() => {
+ const blocks = document.querySelectorAll('.blocklyText');
+ const removedBlocks = Array.from(blocks).filter(
+ block => block !== null && block !== undefined && block.getAttribute('id') !== null
+ );
+ const showVariableBlock = Array.from(removedBlocks).find(block =>
+ block.getAttribute('id').includes('ShowTextColorSizeAlignmentBrick-6')
+ );
+ return showVariableBlock.id;
+ });
+
+ const mItems = [];
+ let isBlocklyContextMenu = false;
+ while (!isBlocklyContextMenu) {
+ await page.waitForSelector(`[id="${brickID}"]`, { visible: true });
+ await page.click(`[id="${brickID}"]`, { button: 'right' });
+
+ isBlocklyContextMenu = await page.evaluate(() => {
+ const contextMenu = document.querySelector('.blocklyMenu');
+ if (contextMenu) {
+ const menuItems = contextMenu.querySelectorAll('.blocklyMenuItemContent');
+ if (menuItems) {
+ return true;
+ }
+ }
+ return false;
+ });
+
+ if (isBlocklyContextMenu) {
+ const menuItems = await page.evaluate(() => {
+ const items = [];
+ const menuItems = document.querySelectorAll('.blocklyMenuItemContent');
+ for (const item of menuItems) {
+ items.push(item.innerText);
+ }
+ return items;
+ });
+ for (const i of menuItems) {
+ mItems.push(i);
+ }
+ }
+ }
+
+ expect(mItems).toContain('Enable brick');
+ }, 10000);
+
+ test('Context menu item disable script block text test', async () => {
+ const scriptID = await page.evaluate(() => {
+ const blocks = document.querySelectorAll('.blocklyText');
+ const removedBlocks = Array.from(blocks).filter(
+ block => block !== null && block !== undefined && block.getAttribute('id') !== null
+ );
+ const startBlock = Array.from(removedBlocks).find(block => block.getAttribute('id').includes('StartScript'));
+ return startBlock.id;
+ });
+
+ const mItems = [];
+ let isBlocklyContextMenu = false;
+ while (!isBlocklyContextMenu) {
+ await page.waitForSelector(`[id="${scriptID}"]`, { visible: true });
+ await page.click(`[id="${scriptID}"]`, { button: 'right' });
+
+ isBlocklyContextMenu = await page.evaluate(() => {
+ const contextMenu = document.querySelector('.blocklyMenu');
+ if (contextMenu) {
+ const menuItems = contextMenu.querySelectorAll('.blocklyMenuItemContent');
+ if (menuItems) {
+ return true;
+ }
+ }
+ return false;
+ });
+
+ if (isBlocklyContextMenu) {
+ const menuItems = await page.evaluate(() => {
+ const items = [];
+ const menuItems = document.querySelectorAll('.blocklyMenuItemContent');
+ for (const item of menuItems) {
+ items.push(item.innerText);
+ }
+ return items;
+ });
+ for (const i of menuItems) {
+ mItems.push(i);
+ }
+ }
+ }
+
+ expect(mItems).toContain('Disable script');
+ }, 10000);
+
+ test('Context menu item enable script block text test', async () => {
+ const scriptID = await page.evaluate(() => {
+ const blocks = document.querySelectorAll('.blocklyText');
+
+ // Remove all null and undefined values from array
+ const removedBlocks = Array.from(blocks).filter(
+ block => block !== null && block !== undefined && block.getAttribute('id') !== null
+ );
+ const broadcastBlock = Array.from(removedBlocks).find(block =>
+ block.getAttribute('id').includes('BroadcastScript')
+ );
+ return broadcastBlock.id;
+ });
+
+ const mItems = [];
+ let isBlocklyContextMenu = false;
+ while (!isBlocklyContextMenu) {
+ await page.waitForSelector(`[id="${scriptID}"]`, { visible: true });
+ await page.click(`[id="${scriptID}"]`, { button: 'right' });
+
+ isBlocklyContextMenu = await page.evaluate(() => {
+ const contextMenu = document.querySelector('.blocklyMenu');
+ if (contextMenu) {
+ const menuItems = contextMenu.querySelectorAll('.blocklyMenuItemContent');
+ if (menuItems) {
+ return true;
+ }
+ }
+ return false;
+ });
+
+ if (isBlocklyContextMenu) {
+ const menuItems = await page.evaluate(() => {
+ const items = [];
+ const menuItems = document.querySelectorAll('.blocklyMenuItemContent');
+ for (const item of menuItems) {
+ items.push(item.innerText);
+ }
+ return items;
+ });
+ for (const i of menuItems) {
+ mItems.push(i);
+ }
+ }
+ }
+
+ expect(mItems).toContain('Enable script');
+ });
+}, 10000);
diff --git a/test/programs/binding_of_krishna_1_12.xml b/test/programs/binding_of_krishna_1_12.xml
index 2035772b..8a7bb709 100644
--- a/test/programs/binding_of_krishna_1_12.xml
+++ b/test/programs/binding_of_krishna_1_12.xml
@@ -14096,7 +14096,7 @@ Mori Shohei
- false
+ true
819b7038-1a2f-4c71-b741-37043a9acbd6
nextcaption