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