diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx index cb998b929..14e9677c3 100644 --- a/src/containers/blocks.jsx +++ b/src/containers/blocks.jsx @@ -18,7 +18,8 @@ import {BLOCKS_DEFAULT_SCALE, STAGE_DISPLAY_SIZES} from '../lib/layout-constants import DropAreaHOC from '../lib/drop-area-hoc.jsx'; import DragConstants from '../lib/drag-constants'; import defineDynamicBlock from '../lib/define-dynamic-block'; -import {getColorsForTheme, themes} from '../lib/themes'; +import {DEFAULT_THEME, getColorsForTheme, themes} from '../lib/themes'; +import {injectExtensionBlockColors, injectExtensionCategoryColors} from '../lib/themes/blockHelpers'; import {connect} from 'react-redux'; import {updateToolbox} from '../reducers/toolbox'; @@ -348,7 +349,10 @@ class Blocks extends React.Component { const stageCostumes = stage.getCostumes(); const targetCostumes = target.getCostumes(); const targetSounds = target.getSounds(); - const dynamicBlocksXML = this.props.vm.runtime.getBlocksXML(target); + const dynamicBlocksXML = injectExtensionCategoryColors( + this.props.vm.runtime.getBlocksXML(target), + this.props.theme + ); return makeToolboxXML(false, target.isStage, target.id, dynamicBlocksXML, targetCostumes[targetCostumes.length - 1].name, stageCostumes[stageCostumes.length - 1].name, @@ -432,7 +436,7 @@ class Blocks extends React.Component { if (blockInfo.info && blockInfo.info.isDynamic) { dynamicBlocksInfo.push(blockInfo); } else if (blockInfo.json) { - staticBlocksJson.push(blockInfo.json); + staticBlocksJson.push(injectExtensionBlockColors(blockInfo.json, this.props.theme)); } // otherwise it's a non-block entry such as '---' }); @@ -655,7 +659,7 @@ Blocks.defaultOptions = { Blocks.defaultProps = { isVisible: true, options: Blocks.defaultOptions, - theme: 'standard' + theme: DEFAULT_THEME }; const mapStateToProps = state => ({ diff --git a/src/lib/themes/__mocks__/dark-mode.js b/src/lib/themes/__mocks__/dark-mode.js index c3faf8d54..793270dd6 100644 --- a/src/lib/themes/__mocks__/dark-mode.js +++ b/src/lib/themes/__mocks__/dark-mode.js @@ -2,6 +2,11 @@ const blockColors = { motion: { primary: '#AAAAAA' }, + pen: { + primary: '#FFFFFF', + secondary: '#EEEEEE', + tertiary: '#DDDDDD' + }, text: '#BBBBBB' }; diff --git a/src/lib/themes/__mocks__/default-colors.js b/src/lib/themes/__mocks__/default-colors.js index f382f38aa..f5e7aa48c 100644 --- a/src/lib/themes/__mocks__/default-colors.js +++ b/src/lib/themes/__mocks__/default-colors.js @@ -4,6 +4,11 @@ const blockColors = { secondary: '#222222', tertiary: '#333333' }, + pen: { + primary: '#121212', + secondary: '#232323', + tertiary: '#343434' + }, text: '#444444', workspace: '#555555' }; diff --git a/src/lib/themes/blockHelpers.js b/src/lib/themes/blockHelpers.js new file mode 100644 index 000000000..4071dfb59 --- /dev/null +++ b/src/lib/themes/blockHelpers.js @@ -0,0 +1,61 @@ +import {DEFAULT_THEME, getColorsForTheme} from '.'; + +// scratch-blocks colours has a pen property that scratch-gui uses for all extensions +const getExtensionColors = theme => getColorsForTheme(theme).pen; + +/** + * Applies extension color theme to categories. + * No changes are applied if called with the default theme, allowing extensions to provide their own colors. + * These colors are not seen if the category provides a blockIconURI. + * @param {Array.} dynamicBlockXML - XML for each category of extension blocks, returned from getBlocksXML + * in the vm runtime. + * @param {string} theme - Theme name + * @returns {Array.} Dynamic block XML updated with colors. + */ +const injectExtensionCategoryColors = (dynamicBlockXML, theme) => { + // Don't do any manipulation for the default theme + if (theme === DEFAULT_THEME) return dynamicBlockXML; + + const extensionColors = getExtensionColors(theme); + const parser = new DOMParser(); + const serializer = new XMLSerializer(); + + return dynamicBlockXML.map(extension => { + const dom = parser.parseFromString(extension.xml, 'text/xml'); + + dom.documentElement.setAttribute('colour', extensionColors.primary); + // Note: the category's secondaryColour matches up with the blocks' tertiary color, both used for border color. + dom.documentElement.setAttribute('secondaryColour', extensionColors.tertiary); + + return { + ...extension, + xml: serializer.serializeToString(dom) + }; + }); +}; + +/** + * Applies extension color theme to static block json. + * No changes are applied if called with the default theme, allowing extensions to provide their own colors. + * @param {object} blockInfoJson - Static block json + * @param {string} theme - Theme name + * @returns {object} Block info json with updated colors. The original blockInfoJson is not modified. + */ +const injectExtensionBlockColors = (blockInfoJson, theme) => { + // Don't do any manipulation for the default theme + if (theme === DEFAULT_THEME) return blockInfoJson; + + const extensionColors = getExtensionColors(theme); + + return { + ...blockInfoJson, + colour: extensionColors.primary, + colourSecondary: extensionColors.secondary, + colourTertiary: extensionColors.tertiary + }; +}; + +export { + injectExtensionBlockColors, + injectExtensionCategoryColors +}; diff --git a/src/lib/themes/index.js b/src/lib/themes/index.js index e1fa87f71..bd1edcb5b 100644 --- a/src/lib/themes/index.js +++ b/src/lib/themes/index.js @@ -4,12 +4,13 @@ import darkMode from './dark-mode'; import highContrast from './high-contrast'; import defaultColors from './default-colors'; +const DEFAULT_THEME = 'standard'; const mergeWithDefaults = colors => defaultsDeep({}, colors, defaultColors); const themeMap = { 'dark-mode': mergeWithDefaults(darkMode), 'high-contrast': mergeWithDefaults(highContrast), - 'standard': defaultColors + [DEFAULT_THEME]: defaultColors }; const getColorsForTheme = theme => { @@ -25,6 +26,7 @@ const getColorsForTheme = theme => { const themes = Object.keys(themeMap); export { + DEFAULT_THEME, defaultColors, getColorsForTheme, themes diff --git a/src/reducers/theme.js b/src/reducers/theme.js index 5d05eff00..63ddabc0e 100644 --- a/src/reducers/theme.js +++ b/src/reducers/theme.js @@ -1,7 +1,9 @@ +import {DEFAULT_THEME} from '../lib/themes'; + const SET_THEME = 'scratch-gui/theme/SET_THEME'; const initialState = { - theme: 'standard' + theme: DEFAULT_THEME }; const reducer = (state = initialState, action) => { diff --git a/test/unit/util/themes.test.js b/test/unit/util/themes.test.js index d281add0c..8bf70ebe5 100644 --- a/test/unit/util/themes.test.js +++ b/test/unit/util/themes.test.js @@ -1,4 +1,5 @@ -import {defaultColors, getColorsForTheme} from '../../../src/lib/themes'; +import {defaultColors, DEFAULT_THEME, getColorsForTheme} from '../../../src/lib/themes'; +import {injectExtensionBlockColors} from '../../../src/lib/themes/blockHelpers'; jest.mock('../../../src/lib/themes/default-colors'); jest.mock('../../../src/lib/themes/dark-mode'); @@ -19,4 +20,42 @@ describe('themes', () => { expect(colors.motion.secondary).toEqual('#222222'); }); + + test('updates extension blocks based on theme', () => { + const blockInfoJson = { + type: 'dummy_block', + colour: '#0FBD8C', + colourSecondary: '#0DA57A', + colourTertiary: '#0B8E69' + }; + + const updated = injectExtensionBlockColors(blockInfoJson, 'dark-mode'); + + expect(updated).toEqual({ + type: 'dummy_block', + colour: '#FFFFFF', + colourSecondary: '#EEEEEE', + colourTertiary: '#DDDDDD' + }); + // The original value was not modified + expect(blockInfoJson.colour).toBe('#0FBD8C'); + }); + + test('bypasses updates if using the default theme', () => { + const blockInfoJson = { + type: 'dummy_block', + colour: '#0FBD8C', + colourSecondary: '#0DA57A', + colourTertiary: '#0B8E69' + }; + + const updated = injectExtensionBlockColors(blockInfoJson, DEFAULT_THEME); + + expect(updated).toEqual({ + type: 'dummy_block', + colour: '#0FBD8C', + colourSecondary: '#0DA57A', + colourTertiary: '#0B8E69' + }); + }); });