diff --git a/packages/scratch-gui/src/components/gui/gui.jsx b/packages/scratch-gui/src/components/gui/gui.jsx index 1fa06119e2..c5da6c8778 100644 --- a/packages/scratch-gui/src/components/gui/gui.jsx +++ b/packages/scratch-gui/src/components/gui/gui.jsx @@ -34,7 +34,7 @@ import TelemetryModal from '../telemetry-modal/telemetry-modal.jsx'; import layout, {STAGE_SIZE_MODES} from '../../lib/layout-constants'; import {resolveStageSize} from '../../lib/screen-utils'; -import {themeMap} from '../../lib/themes'; +import {colorModeMap} from '../../lib/settings/color-mode/index.js'; import {AccountMenuOptionsPropTypes} from '../../lib/account-menu-options'; import styles from './gui.css'; @@ -67,6 +67,7 @@ const GUIComponent = props => { blocksTabVisible, cardsVisible, canChangeLanguage, + canChangeColorMode, canChangeTheme, canCreateNew, canEditTitle, @@ -129,6 +130,7 @@ const GUIComponent = props => { stageSizeMode, targetIsStage, telemetryModalVisible, + colorMode, theme, tipsLibraryVisible, useExternalPeripheralList, @@ -258,6 +260,7 @@ const GUIComponent = props => { authorThumbnailUrl={authorThumbnailUrl} authorUsername={authorUsername} canChangeLanguage={canChangeLanguage} + canChangeColorMode={canChangeColorMode} canChangeTheme={canChangeTheme} canCreateCopy={canCreateCopy} canCreateNew={canCreateNew} @@ -362,15 +365,15 @@ const GUIComponent = props => { ({ // This is the button's mode, as opposed to the actual current state blocksId: state.scratchGui.timeTravel.year.toString(), stageSizeMode: state.scratchGui.stageSize.stageSize, - theme: state.scratchGui.theme.theme + colorMode: state.scratchGui.settings.colorMode, + theme: state.scratchGui.settings.theme }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx index 58f955a1bb..d8f43cbc30 100644 --- a/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx +++ b/packages/scratch-gui/src/components/menu-bar/menu-bar.jsx @@ -452,8 +452,10 @@ class MenuBar extends React.Component { onClick={this.props.onClickLogo} /> - {(this.props.canChangeTheme || this.props.canChangeLanguage) && ( { + const item = props.item; + + return ( + +
+ + {item.icon && } + +
+
); +}; + +PreferenceItem.propTypes = { + isSelected: PropTypes.bool, + onClick: PropTypes.func, + item: PropTypes.shape({ + icon: PropTypes.string, + label: intlMessageShape.isRequired + }) +}; + +const PreferenceMenu = ({ + itemsMap, + onChange, + defaultMenuIconSrc, + submenuLabel, + selectedItemKey, + isRtl +}) => { + const [isOpen, setIsOpen] = useState(false); + const itemKeys = useMemo(() => Object.keys(itemsMap), [itemsMap]); + const selectedItem = useMemo(() => itemsMap[selectedItemKey], [itemsMap, selectedItemKey]); + const onClick = useCallback(() => { + setIsOpen(!isOpen); + }, [isOpen]); + return ( + +
+ + + + + +
+ + {itemKeys.map(itemKey => ( + onChange(itemKey)} + item={itemsMap[itemKey]} + />) + )} + +
+ ); +}; + +PreferenceMenu.propTypes = { + itemsMap: PropTypes.objectOf(PropTypes.shape({ + icon: PropTypes.string, + label: intlMessageShape.isRequired + })).isRequired, + onChange: PropTypes.func, + defaultMenuIconSrc: PropTypes.string, + submenuLabel: intlMessageShape.isRequired, + selectedItemKey: PropTypes.string, + isRtl: PropTypes.bool, + onRequestCloseSettings: PropTypes.func +}; + +const mapStateToProps = state => ({ + isRtl: state.locales.isRtl +}); + +export default connect( + mapStateToProps +)(PreferenceMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.css b/packages/scratch-gui/src/components/menu-bar/settings-menu.css index 4171aa1505..7d09671776 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.css +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.css @@ -2,7 +2,8 @@ width: 1.5rem; } -.theme-label { +/* Unused? */ +.color-mode-label { flex: 1; } diff --git a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx index 683f08a926..490bd44f4c 100644 --- a/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx +++ b/packages/scratch-gui/src/components/menu-bar/settings-menu.jsx @@ -1,65 +1,139 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {FormattedMessage} from 'react-intl'; +import {connect} from 'react-redux'; import LanguageMenu from './language-menu.jsx'; import MenuBarMenu from './menu-bar-menu.jsx'; -import ThemeMenu from './theme-menu.jsx'; import {MenuSection} from '../menu/menu.jsx'; +import PreferenceMenu from './preference-menu.jsx'; + +import {DEFAULT_MODE, HIGH_CONTRAST_MODE, colorModeMap} from '../../lib/settings/color-mode/index.js'; +import {themeMap} from '../../lib/settings/theme/index.js'; +import {persistColorMode} from '../../lib/settings/color-mode/persistence.js'; +import {persistTheme} from '../../lib/settings/theme/persistence.js'; +import {setColorMode, setTheme} from '../../reducers/settings.js'; import menuBarStyles from './menu-bar.css'; import styles from './settings-menu.css'; import dropdownCaret from './dropdown-caret.svg'; import settingsIcon from './icon--settings.svg'; +import themeIcon from '../../lib/assets/icon--theme.svg'; + +const enabledColorModes = [DEFAULT_MODE, HIGH_CONTRAST_MODE]; const SettingsMenu = ({ canChangeLanguage, + canChangeColorMode, canChangeTheme, isRtl, + activeColorMode, + onChangeColorMode, + activeTheme, + onChangeTheme, onRequestClose, onRequestOpen, settingsMenuOpen -}) => ( -
- - - - - - { + const enabledColorModesMap = useMemo(() => Object.keys(colorModeMap).reduce((acc, colorMode) => { + if (enabledColorModes.includes(colorMode)) { + acc[colorMode] = colorModeMap[colorMode]; + } + return acc; + }, {}), []); + + return ( +
- - {canChangeLanguage && } - {canChangeTheme && } - - -
-); + + + + + + + + {canChangeLanguage && } + {canChangeTheme && } + {canChangeColorMode && } + + +
+ ); +}; SettingsMenu.propTypes = { canChangeLanguage: PropTypes.bool, + canChangeColorMode: PropTypes.bool, canChangeTheme: PropTypes.bool, isRtl: PropTypes.bool, + activeColorMode: PropTypes.string, + onChangeColorMode: PropTypes.func, + activeTheme: PropTypes.string, + onChangeTheme: PropTypes.func, onRequestClose: PropTypes.func, onRequestOpen: PropTypes.func, settingsMenuOpen: PropTypes.bool }; -export default SettingsMenu; +const mapStateToProps = state => ({ + activeColorMode: state.scratchGui.settings.colorMode, + activeTheme: state.scratchGui.settings.theme +}); + +const mapDispatchToProps = (dispatch, ownProps) => ({ + onChangeColorMode: colorMode => { + dispatch(setColorMode(colorMode)); + ownProps.onRequestClose(); + persistColorMode(colorMode); + }, + onChangeTheme: theme => { + dispatch(setTheme(theme)); + ownProps.onRequestClose(); + persistTheme(theme); + } +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(SettingsMenu); diff --git a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx b/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx deleted file mode 100644 index e9ce24f1de..0000000000 --- a/packages/scratch-gui/src/components/menu-bar/theme-menu.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {FormattedMessage} from 'react-intl'; -import {connect} from 'react-redux'; - -import check from './check.svg'; -import {MenuItem, Submenu} from '../menu/menu.jsx'; -import {DEFAULT_THEME, HIGH_CONTRAST_THEME, themeMap} from '../../lib/themes'; -import {persistTheme} from '../../lib/themes/themePersistance'; -import {openThemeMenu, themeMenuOpen} from '../../reducers/menus.js'; -import {setTheme} from '../../reducers/theme.js'; - -import styles from './settings-menu.css'; - -import dropdownCaret from './dropdown-caret.svg'; - -const ThemeMenuItem = props => { - const themeInfo = themeMap[props.theme]; - - return ( - -
- - - -
-
); -}; - -ThemeMenuItem.propTypes = { - isSelected: PropTypes.bool, - onClick: PropTypes.func, - theme: PropTypes.string -}; - -const ThemeMenu = ({ - isRtl, - menuOpen, - onChangeTheme, - onRequestOpen, - theme -}) => { - const enabledThemes = [DEFAULT_THEME, HIGH_CONTRAST_THEME]; - const themeInfo = themeMap[theme]; - - return ( - -
- - - - - -
- - {enabledThemes.map(enabledTheme => ( - onChangeTheme(enabledTheme)} - theme={enabledTheme} - />) - )} - -
- ); -}; - -ThemeMenu.propTypes = { - isRtl: PropTypes.bool, - menuOpen: PropTypes.bool, - onChangeTheme: PropTypes.func, - // eslint-disable-next-line react/no-unused-prop-types - onRequestCloseSettings: PropTypes.func, - onRequestOpen: PropTypes.func, - theme: PropTypes.string -}; - -const mapStateToProps = state => ({ - isRtl: state.locales.isRtl, - menuOpen: themeMenuOpen(state), - theme: state.scratchGui.theme.theme -}); - -const mapDispatchToProps = (dispatch, ownProps) => ({ - onChangeTheme: theme => { - dispatch(setTheme(theme)); - ownProps.onRequestCloseSettings(); - persistTheme(theme); - }, - onRequestOpen: () => dispatch(openThemeMenu()) -}); - -export default connect( - mapStateToProps, - mapDispatchToProps -)(ThemeMenu); diff --git a/packages/scratch-gui/src/components/monitor/monitor.jsx b/packages/scratch-gui/src/components/monitor/monitor.jsx index d79e34fc9a..df2f337483 100644 --- a/packages/scratch-gui/src/components/monitor/monitor.jsx +++ b/packages/scratch-gui/src/components/monitor/monitor.jsx @@ -8,7 +8,7 @@ import DefaultMonitor from './default-monitor.jsx'; import LargeMonitor from './large-monitor.jsx'; import SliderMonitor from '../../containers/slider-monitor.jsx'; import ListMonitor from '../../containers/list-monitor.jsx'; -import {getColorsForTheme} from '../../lib/themes/index.js'; +import {getColorsForMode} from '../../lib/settings/color-mode/index.js'; import contextMenuStyles from '../context-menu/context-menu.css'; import {MenuItem, BorderedMenuItem} from '../context-menu/context-menu.jsx'; @@ -33,8 +33,8 @@ const modes = { list: ListMonitor }; -const getCategoryColor = (theme, category) => { - const colors = getColorsForTheme(theme); +const getCategoryColor = (colorMode, category) => { + const colors = getColorsForMode(colorMode); return { background: colors[categoryColorMap[category]].primary, text: colors.text @@ -62,7 +62,7 @@ const MonitorComponent = props => ( {React.createElement(modes[props.mode], { categoryColor: getCategoryColor( - props.theme, + props.colorMode, props.category ), ...props @@ -159,7 +159,7 @@ MonitorComponent.propTypes = { onSetModeToLarge: PropTypes.func, onSetModeToSlider: PropTypes.func, onSliderPromptOpen: PropTypes.func, - theme: PropTypes.string.isRequired + colorMode: PropTypes.string.isRequired }; MonitorComponent.defaultProps = { diff --git a/packages/scratch-gui/src/containers/blocks.jsx b/packages/scratch-gui/src/containers/blocks.jsx index f5ea9d561b..295dcf1d9f 100644 --- a/packages/scratch-gui/src/containers/blocks.jsx +++ b/packages/scratch-gui/src/containers/blocks.jsx @@ -19,8 +19,9 @@ 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 {DEFAULT_THEME, getColorsForTheme, themeMap} from '../lib/themes'; -import {injectExtensionBlockTheme, injectExtensionCategoryTheme} from '../lib/themes/blockHelpers'; +import {DEFAULT_MODE, getColorsForMode, colorModeMap} from '../lib/settings/color-mode'; +import {CAT_BLOCKS_THEME} from '../lib/settings/theme'; +import {injectExtensionBlockMode, injectExtensionCategoryMode} from '../lib/settings/color-mode/blockHelpers'; import {connect} from 'react-redux'; import {updateToolbox} from '../reducers/toolbox'; @@ -103,7 +104,7 @@ class Blocks extends React.Component { const workspaceConfig = defaultsDeep({}, Blocks.defaultOptions, this.props.options, - {rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForTheme(this.props.theme)} + {rtl: this.props.isRtl, toolbox: this.props.toolboxXML, colours: getColorsForMode(this.props.colorMode)} ); this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig); @@ -362,15 +363,15 @@ class Blocks extends React.Component { const stageCostumes = stage.getCostumes(); const targetCostumes = target.getCostumes(); const targetSounds = target.getSounds(); - const dynamicBlocksXML = injectExtensionCategoryTheme( + const dynamicBlocksXML = injectExtensionCategoryMode( this.props.vm.runtime.getBlocksXML(target), - this.props.theme + this.props.colorMode ); return makeToolboxXML(false, target.isStage, target.id, dynamicBlocksXML, targetCostumes[targetCostumes.length - 1].name, stageCostumes[stageCostumes.length - 1].name, targetSounds.length > 0 ? targetSounds[targetSounds.length - 1].name : '', - getColorsForTheme(this.props.theme) + getColorsForMode(this.props.colorMode) ); } catch { return null; @@ -455,7 +456,7 @@ class Blocks extends React.Component { if (blockInfo.info && blockInfo.info.isDynamic) { dynamicBlocksInfo.push(blockInfo); } else if (blockInfo.json) { - staticBlocksJson.push(injectExtensionBlockTheme(blockInfo.json, this.props.theme)); + staticBlocksJson.push(injectExtensionBlockMode(blockInfo.json, this.props.colorMode)); } // otherwise it's a non-block entry such as '---' }); @@ -652,7 +653,7 @@ Blocks.propTypes = { collapse: PropTypes.bool }), stageSize: PropTypes.oneOf(Object.keys(STAGE_DISPLAY_SIZES)).isRequired, - theme: PropTypes.oneOf(Object.keys(themeMap)), + colorMode: PropTypes.oneOf(Object.keys(colorModeMap)), toolboxXML: PropTypes.string, updateMetrics: PropTypes.func, updateToolboxState: PropTypes.func, @@ -684,7 +685,7 @@ Blocks.defaultOptions = { Blocks.defaultProps = { isVisible: true, options: Blocks.defaultOptions, - theme: DEFAULT_THEME + colorMode: DEFAULT_MODE }; const mapStateToProps = state => ({ @@ -699,7 +700,7 @@ const mapStateToProps = state => ({ toolboxXML: state.scratchGui.toolbox.toolboxXML, customProceduresVisible: state.scratchGui.customProcedures.active, workspaceMetrics: state.scratchGui.workspaceMetrics, - useCatBlocks: isTimeTravel2020(state) + useCatBlocks: isTimeTravel2020(state) || state.scratchGui.settings.theme === CAT_BLOCKS_THEME }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/scratch-gui/src/containers/monitor.jsx b/packages/scratch-gui/src/containers/monitor.jsx index 69489fc34a..110766ab2a 100644 --- a/packages/scratch-gui/src/containers/monitor.jsx +++ b/packages/scratch-gui/src/containers/monitor.jsx @@ -10,7 +10,7 @@ import {addMonitorRect, getInitialPosition, resizeMonitorRect, removeMonitorRect import {getVariable, setVariableValue} from '../lib/variable-utils'; import importCSV from '../lib/import-csv'; import downloadBlob from '../lib/download-blob'; -import {DEFAULT_THEME} from '../lib/themes'; +import {DEFAULT_MODE} from '../lib/settings/color-mode/index.js'; import SliderPrompt from './slider-prompt.jsx'; import {connect} from 'react-redux'; @@ -215,7 +215,7 @@ class Monitor extends React.Component { min={this.props.min} mode={this.props.mode} targetId={this.props.targetId} - theme={this.props.theme} + colorMode={this.props.colorMode} width={this.props.width} onDragEnd={this.handleDragEnd} onExport={isList ? this.handleExport : null} @@ -253,7 +253,7 @@ Monitor.propTypes = { resizeMonitorRect: PropTypes.func.isRequired, spriteName: PropTypes.string, targetId: PropTypes.string, - theme: PropTypes.string, + colorMode: PropTypes.string, toolboxXML: PropTypes.string, value: PropTypes.oneOfType([ PropTypes.string, @@ -269,11 +269,11 @@ Monitor.propTypes = { y: PropTypes.number }; Monitor.defaultProps = { - theme: DEFAULT_THEME + colorMode: DEFAULT_MODE }; const mapStateToProps = state => ({ monitorLayout: state.scratchGui.monitorLayout, - theme: state.scratchGui.theme.theme, + colorMode: state.scratchGui.settings.colorMode, // render on toolbox updates since changes to the blocks could affect monitor labels, i.e. updated locale toolboxXML: state.scratchGui.toolbox.toolboxXML, vm: state.scratchGui.vm diff --git a/packages/scratch-gui/src/lib/assets/icon--theme.svg b/packages/scratch-gui/src/lib/assets/icon--theme.svg new file mode 100644 index 0000000000..ae95a23fcd --- /dev/null +++ b/packages/scratch-gui/src/lib/assets/icon--theme.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/scratch-gui/src/lib/blocks.js b/packages/scratch-gui/src/lib/blocks.js index 14fafc92c9..2768ac06d7 100644 --- a/packages/scratch-gui/src/lib/blocks.js +++ b/packages/scratch-gui/src/lib/blocks.js @@ -5,7 +5,15 @@ * @returns {ScratchBlocks} ScratchBlocks connected with the vm */ export default function (vm, useCatBlocks) { - const ScratchBlocks = useCatBlocks ? require('cat-blocks') : require('scratch-blocks'); + const ScratchBlocks = require('scratch-blocks'); + + // TODO: Set theme from editor settings + if (useCatBlocks) { + ScratchBlocks.setTheme(ScratchBlocks.Themes.CAT_BLOCKS); + } else { + ScratchBlocks.setTheme(ScratchBlocks.Themes.CLASSIC); + } + const jsonForMenuBlock = function (name, menuOptionsFn, colors, start) { return { message0: '%1', diff --git a/packages/scratch-gui/src/lib/make-toolbox-xml.js b/packages/scratch-gui/src/lib/make-toolbox-xml.js index 90346b8b03..74faccf99c 100644 --- a/packages/scratch-gui/src/lib/make-toolbox-xml.js +++ b/packages/scratch-gui/src/lib/make-toolbox-xml.js @@ -1,5 +1,5 @@ import ScratchBlocks from 'scratch-blocks'; -import {defaultColors} from './themes'; +import {defaultColors} from './settings/color-mode'; const categorySeparator = ''; @@ -754,7 +754,7 @@ const xmlClose = ''; * @param {?string} costumeName - The name of the default selected costume dropdown. * @param {?string} backdropName - The name of the default selected backdrop dropdown. * @param {?string} soundName - The name of the default selected sound dropdown. - * @param {?object} colors - The colors for the theme. + * @param {?object} colors - The colors for the color mode. * @returns {string} - a ScratchBlocks-style XML document for the contents of the toolbox. */ const makeToolboxXML = function (isInitialSetup, isStage = true, targetId, categoriesXML = [], diff --git a/packages/scratch-gui/src/lib/themes/blockHelpers.js b/packages/scratch-gui/src/lib/settings/color-mode/blockHelpers.js similarity index 66% rename from packages/scratch-gui/src/lib/themes/blockHelpers.js rename to packages/scratch-gui/src/lib/settings/color-mode/blockHelpers.js index b201241eeb..56e5dd6129 100644 --- a/packages/scratch-gui/src/lib/themes/blockHelpers.js +++ b/packages/scratch-gui/src/lib/settings/color-mode/blockHelpers.js @@ -1,4 +1,4 @@ -import {DEFAULT_THEME, getColorsForTheme, themeMap} from '.'; +import {DEFAULT_MODE, getColorsForMode, colorModeMap} from '.'; const getBlockIconURI = extensionIcons => { if (!extensionIcons) return null; @@ -13,23 +13,23 @@ const getCategoryIconURI = extensionIcons => { }; // scratch-blocks colours has a pen property that scratch-gui uses for all extensions -const getExtensionColors = theme => getColorsForTheme(theme).pen; +const getExtensionColors = mode => getColorsForMode(mode).pen; /** - * Applies extension color theme to categories. - * No changes are applied if called with the default theme, allowing extensions to provide their own colors. + * Applies extension color mode to categories. + * No changes are applied if called with the default color mode, 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 + * @param {string} mode - Color Mode name * @returns {Array.} Dynamic block XML updated with colors. */ -const injectExtensionCategoryTheme = (dynamicBlockXML, theme) => { - // Don't do any manipulation for the default theme - if (theme === DEFAULT_THEME) return dynamicBlockXML; +const injectExtensionCategoryMode = (dynamicBlockXML, mode) => { + // Don't do any manipulation for the default mode + if (mode === DEFAULT_MODE) return dynamicBlockXML; - const extensionColors = getExtensionColors(theme); - const extensionIcons = themeMap[theme].extensions; + const extensionColors = getExtensionColors(mode); + const extensionIcons = colorModeMap[mode].extensions; const parser = new DOMParser(); const serializer = new XMLSerializer(); @@ -52,12 +52,12 @@ const injectExtensionCategoryTheme = (dynamicBlockXML, theme) => { }); }; -const injectBlockIcons = (blockInfoJson, theme) => { +const injectBlockIcons = (blockInfoJson, mode) => { // Block icons are the first element of `args0` if (!blockInfoJson.args0 || blockInfoJson.args0.length < 1 || blockInfoJson.args0[0].type !== 'field_image') return blockInfoJson; - const extensionIcons = themeMap[theme].extensions; + const extensionIcons = colorModeMap[mode].extensions; const extensionId = blockInfoJson.type.substring(0, blockInfoJson.type.indexOf('_')); const blockIconURI = getBlockIconURI(extensionIcons[extensionId]); @@ -77,20 +77,20 @@ const injectBlockIcons = (blockInfoJson, theme) => { }; /** - * 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. + * Applies extension color mode to static block json. + * No changes are applied if called with the default mode, allowing extensions to provide their own colors. * @param {object} blockInfoJson - Static block json - * @param {string} theme - Theme name + * @param {string} mode - Color Mode name * @returns {object} Block info json with updated colors. The original blockInfoJson is not modified. */ -const injectExtensionBlockTheme = (blockInfoJson, theme) => { - // Don't do any manipulation for the default theme - if (theme === DEFAULT_THEME) return blockInfoJson; +const injectExtensionBlockMode = (blockInfoJson, mode) => { + // Don't do any manipulation for the default mode + if (mode === DEFAULT_MODE) return blockInfoJson; - const extensionColors = getExtensionColors(theme); + const extensionColors = getExtensionColors(mode); return { - ...injectBlockIcons(blockInfoJson, theme), + ...injectBlockIcons(blockInfoJson, mode), colour: extensionColors.primary, colourSecondary: extensionColors.secondary, colourTertiary: extensionColors.tertiary, @@ -99,6 +99,6 @@ const injectExtensionBlockTheme = (blockInfoJson, theme) => { }; export { - injectExtensionBlockTheme, - injectExtensionCategoryTheme + injectExtensionBlockMode, + injectExtensionCategoryMode }; diff --git a/packages/scratch-gui/src/lib/themes/dark/__mocks__/index.js b/packages/scratch-gui/src/lib/settings/color-mode/dark/__mocks__/index.js similarity index 100% rename from packages/scratch-gui/src/lib/themes/dark/__mocks__/index.js rename to packages/scratch-gui/src/lib/settings/color-mode/dark/__mocks__/index.js diff --git a/packages/scratch-gui/src/lib/themes/dark/index.js b/packages/scratch-gui/src/lib/settings/color-mode/dark/index.js similarity index 100% rename from packages/scratch-gui/src/lib/themes/dark/index.js rename to packages/scratch-gui/src/lib/settings/color-mode/dark/index.js diff --git a/packages/scratch-gui/src/lib/themes/default/__mocks__/index.js b/packages/scratch-gui/src/lib/settings/color-mode/default/__mocks__/index.js similarity index 100% rename from packages/scratch-gui/src/lib/themes/default/__mocks__/index.js rename to packages/scratch-gui/src/lib/settings/color-mode/default/__mocks__/index.js diff --git a/packages/scratch-gui/src/lib/themes/default/icon.svg b/packages/scratch-gui/src/lib/settings/color-mode/default/icon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/default/icon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/default/icon.svg diff --git a/packages/scratch-gui/src/lib/themes/default/index.js b/packages/scratch-gui/src/lib/settings/color-mode/default/index.js similarity index 100% rename from packages/scratch-gui/src/lib/themes/default/index.js rename to packages/scratch-gui/src/lib/settings/color-mode/default/index.js diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/comment-arrow-down.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/comment-arrow-down.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/comment-arrow-down.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/comment-arrow-down.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/comment-arrow-up.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/comment-arrow-up.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/comment-arrow-up.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/comment-arrow-up.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/delete-x.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/delete-x.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/delete-x.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/delete-x.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/dropdown-arrow.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/dropdown-arrow.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/dropdown-arrow.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/dropdown-arrow.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/eyedropper.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/eyedropper.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/eyedropper.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/eyedropper.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/icons/arrow_button.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/icons/arrow_button.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/icons/arrow_button.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/icons/arrow_button.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/repeat.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/repeat.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/repeat.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/repeat.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/rotate-left.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/rotate-left.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/rotate-left.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/rotate-left.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/rotate-right.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/rotate-right.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/rotate-right.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/rotate-right.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/zoom-in.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/zoom-in.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/zoom-in.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/zoom-in.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/zoom-out.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/zoom-out.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/zoom-out.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/zoom-out.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/zoom-reset.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/zoom-reset.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/blocks-media/zoom-reset.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/blocks-media/zoom-reset.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/extensions/faceSensingIcon.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/faceSensingIcon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/extensions/faceSensingIcon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/faceSensingIcon.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/extensions/musicIcon.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/musicIcon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/extensions/musicIcon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/musicIcon.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/extensions/penIcon.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/penIcon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/extensions/penIcon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/penIcon.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/extensions/text2speechIcon.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/text2speechIcon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/extensions/text2speechIcon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/text2speechIcon.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/extensions/translateIcon.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/translateIcon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/extensions/translateIcon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/translateIcon.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/extensions/videoSensingIcon.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/videoSensingIcon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/extensions/videoSensingIcon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/extensions/videoSensingIcon.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/icon.svg b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/icon.svg similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/icon.svg rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/icon.svg diff --git a/packages/scratch-gui/src/lib/themes/high-contrast/index.js b/packages/scratch-gui/src/lib/settings/color-mode/high-contrast/index.js similarity index 100% rename from packages/scratch-gui/src/lib/themes/high-contrast/index.js rename to packages/scratch-gui/src/lib/settings/color-mode/high-contrast/index.js diff --git a/packages/scratch-gui/src/lib/themes/index.js b/packages/scratch-gui/src/lib/settings/color-mode/index.js similarity index 62% rename from packages/scratch-gui/src/lib/themes/index.js rename to packages/scratch-gui/src/lib/settings/color-mode/index.js index 673d453e74..a014d91b8e 100644 --- a/packages/scratch-gui/src/lib/themes/index.js +++ b/packages/scratch-gui/src/lib/settings/color-mode/index.js @@ -14,68 +14,68 @@ import {blockColors as defaultColors} from './default'; import defaultIcon from './default/icon.svg'; import highContrastIcon from './high-contrast/icon.svg'; -const DEFAULT_THEME = 'default'; -const HIGH_CONTRAST_THEME = 'high-contrast'; -const DARK_THEME = 'dark'; +const DEFAULT_MODE = 'default'; +const HIGH_CONTRAST_MODE = 'high-contrast'; +const DARK_MODE = 'dark'; const mergeWithDefaults = colors => defaultsDeep({}, colors, defaultColors); const messages = defineMessages({ - [DEFAULT_THEME]: { + [DEFAULT_MODE]: { id: 'gui.theme.default', defaultMessage: 'Original', - description: 'label for original theme' + description: 'label for original color mode' }, - [DARK_THEME]: { + [DARK_MODE]: { id: 'gui.theme.dark', defaultMessage: 'Dark', - description: 'label for dark mode theme' + description: 'label for dark mode' }, - [HIGH_CONTRAST_THEME]: { + [HIGH_CONTRAST_MODE]: { id: 'gui.theme.highContrast', defaultMessage: 'High Contrast', - description: 'label for high theme' + description: 'label for high contrast mode' } }); -const themeMap = { - [DEFAULT_THEME]: { +const colorModeMap = { + [DEFAULT_MODE]: { blocksMediaFolder: 'blocks-media/default', colors: defaultColors, extensions: {}, - label: messages[DEFAULT_THEME], + label: messages[DEFAULT_MODE], icon: defaultIcon }, - [DARK_THEME]: { + [DARK_MODE]: { blocksMediaFolder: 'blocks-media/default', colors: mergeWithDefaults(darkModeBlockColors), extensions: darkModeExtensions, - label: messages[DARK_THEME] + label: messages[DARK_MODE] }, - [HIGH_CONTRAST_THEME]: { + [HIGH_CONTRAST_MODE]: { blocksMediaFolder: 'blocks-media/high-contrast', colors: mergeWithDefaults(highContrastBlockColors), extensions: highContrastExtensions, - label: messages[HIGH_CONTRAST_THEME], + label: messages[HIGH_CONTRAST_MODE], icon: highContrastIcon } }; -const getColorsForTheme = theme => { - const themeInfo = themeMap[theme]; +const getColorsForMode = colorMode => { + const modeInfo = colorModeMap[colorMode]; - if (!themeInfo) { - throw new Error(`Undefined theme ${theme}`); + if (!modeInfo) { + throw new Error(`Undefined color mode ${colorMode}`); } - return themeInfo.colors; + return modeInfo.colors; }; export { - DEFAULT_THEME, - DARK_THEME, - HIGH_CONTRAST_THEME, + DEFAULT_MODE, + DARK_MODE, + HIGH_CONTRAST_MODE, defaultColors, - getColorsForTheme, - themeMap + getColorsForMode, + colorModeMap }; diff --git a/packages/scratch-gui/src/lib/settings/color-mode/persistence.js b/packages/scratch-gui/src/lib/settings/color-mode/persistence.js new file mode 100644 index 0000000000..a26e79186d --- /dev/null +++ b/packages/scratch-gui/src/lib/settings/color-mode/persistence.js @@ -0,0 +1,47 @@ +import cookie from 'cookie'; + +import {DEFAULT_MODE, HIGH_CONTRAST_MODE} from '.'; + +const PREFERS_HIGH_CONTRAST_QUERY = '(prefers-contrast: more)'; +// Technically what we are persisting is the color mode, but for historical reasons, +// we should continue using 'scratchtheme' as the cookie key. +const COOKIE_KEY = 'scratchtheme'; + +// Dark mode isn't enabled yet +const isValidColorMode = colorMode => [DEFAULT_MODE, HIGH_CONTRAST_MODE].includes(colorMode); + +const systemPreferencesColorMode = () => { + if (window.matchMedia && window.matchMedia(PREFERS_HIGH_CONTRAST_QUERY).matches) return HIGH_CONTRAST_MODE; + + return DEFAULT_MODE; +}; + +const detectColorMode = () => { + const obj = cookie.parse(document.cookie) || {}; + const colorModeCookie = obj.scratchtheme; + + if (isValidColorMode(colorModeCookie)) return colorModeCookie; + + // No cookie set. Fall back to system preferences + return systemPreferencesColorMode(); +}; + +const persistColorMode = mode => { + if (!isValidColorMode(mode)) { + throw new Error(`Invalid color mode: ${mode}`); + } + + if (systemPreferencesColorMode() === mode) { + // Clear the cookie to represent using the system preferences + document.cookie = `${COOKIE_KEY}=;path=/`; + return; + } + + const expires = new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString(); + document.cookie = `${COOKIE_KEY}=${mode};expires=${expires};path=/`; +}; + +export { + detectColorMode, + persistColorMode +}; diff --git a/packages/scratch-gui/src/lib/settings/theme/index.js b/packages/scratch-gui/src/lib/settings/theme/index.js new file mode 100644 index 0000000000..dfd8d03300 --- /dev/null +++ b/packages/scratch-gui/src/lib/settings/theme/index.js @@ -0,0 +1,34 @@ +import defaultsDeep from 'lodash.defaultsdeep'; +import {defineMessages} from 'react-intl'; + +const DEFAULT_THEME = 'default'; +const CAT_BLOCKS_THEME = 'cat-blocks'; + +const messages = defineMessages({ + [DEFAULT_THEME]: { + id: 'gui.blockTheme.default', + defaultMessage: 'Default', + description: 'label for default theme' + }, + [CAT_BLOCKS_THEME]: { + id: 'gui.blockTheme.catBlocks', + defaultMessage: 'Cat Blocks', + description: 'label for cat blocks theme' + } +}); + +// Keeping this as a map for consistency with the color modes +const themeMap = { + [DEFAULT_THEME]: { + label: messages[DEFAULT_THEME] + }, + [CAT_BLOCKS_THEME]: { + label: messages[CAT_BLOCKS_THEME] + } +}; + +export { + DEFAULT_THEME, + CAT_BLOCKS_THEME, + themeMap +}; diff --git a/packages/scratch-gui/src/lib/settings/theme/persistence.js b/packages/scratch-gui/src/lib/settings/theme/persistence.js new file mode 100644 index 0000000000..7d6a146c6a --- /dev/null +++ b/packages/scratch-gui/src/lib/settings/theme/persistence.js @@ -0,0 +1,31 @@ +import cookie from 'cookie'; + +import {DEFAULT_THEME, CAT_BLOCKS_THEME} from '.'; + +const COOKIE_KEY = 'scratchblockstheme'; + +const isValidTheme = theme => [DEFAULT_THEME, CAT_BLOCKS_THEME].includes(theme); + +// TODO: This should also depend on membership status +const detectTheme = () => { + const obj = cookie.parse(document.cookie) || {}; + const themeCookie = obj[COOKIE_KEY]; + + if (isValidTheme(themeCookie)) return themeCookie; + + return DEFAULT_THEME; +}; + +const persistTheme = theme => { + if (!isValidTheme(theme)) { + throw new Error(`Invalid theme: ${theme}`); + } + + const expires = new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString(); + document.cookie = `${COOKIE_KEY}=${theme};expires=${expires};path=/`; +}; + +export { + detectTheme, + persistTheme +}; diff --git a/packages/scratch-gui/src/lib/system-preferences-hoc.jsx b/packages/scratch-gui/src/lib/system-preferences-hoc.jsx index 2c9a4e251c..99ce3aa56e 100644 --- a/packages/scratch-gui/src/lib/system-preferences-hoc.jsx +++ b/packages/scratch-gui/src/lib/system-preferences-hoc.jsx @@ -2,8 +2,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {setTheme} from '../reducers/theme'; -import {detectTheme} from './themes/themePersistance'; +import {setColorMode} from '../reducers/settings'; +import {detectColorMode} from './settings/color-mode/persistence'; // Dark mode is not yet supported // const prefersDarkQuery = '(prefers-color-scheme: dark)'; @@ -12,7 +12,7 @@ const prefersHighContrastQuery = '(prefers-contrast: more)'; const systemPreferencesHOC = function (WrappedComponent) { class SystemPreferences extends React.Component { componentDidMount () { - this.preferencesListener = () => this.props.onSetTheme(detectTheme()); + this.preferencesListener = () => this.props.onSetColorMode(detectColorMode()); if (window.matchMedia) { this.highContrastMatchMedia = window.matchMedia(prefersHighContrastQuery); @@ -39,7 +39,7 @@ const systemPreferencesHOC = function (WrappedComponent) { render () { const { - onSetTheme, + onSetColorMode, ...props } = this.props; @@ -48,11 +48,11 @@ const systemPreferencesHOC = function (WrappedComponent) { } SystemPreferences.propTypes = { - onSetTheme: PropTypes.func + onSetColorMode: PropTypes.func }; const mapDispatchToProps = dispatch => ({ - onSetTheme: theme => dispatch(setTheme(theme)) + onSetColorMode: mode => dispatch(setColorMode(mode)) }); return connect( diff --git a/packages/scratch-gui/src/lib/themes/themePersistance.js b/packages/scratch-gui/src/lib/themes/themePersistance.js deleted file mode 100644 index 6056708651..0000000000 --- a/packages/scratch-gui/src/lib/themes/themePersistance.js +++ /dev/null @@ -1,45 +0,0 @@ -import cookie from 'cookie'; - -import {DEFAULT_THEME, HIGH_CONTRAST_THEME} from '.'; - -const PREFERS_HIGH_CONTRAST_QUERY = '(prefers-contrast: more)'; -const COOKIE_KEY = 'scratchtheme'; - -// Dark mode isn't enabled yet -const isValidTheme = theme => [DEFAULT_THEME, HIGH_CONTRAST_THEME].includes(theme); - -const systemPreferencesTheme = () => { - if (window.matchMedia && window.matchMedia(PREFERS_HIGH_CONTRAST_QUERY).matches) return HIGH_CONTRAST_THEME; - - return DEFAULT_THEME; -}; - -const detectTheme = () => { - const obj = cookie.parse(document.cookie) || {}; - const themeCookie = obj.scratchtheme; - - if (isValidTheme(themeCookie)) return themeCookie; - - // No cookie set. Fall back to system preferences - return systemPreferencesTheme(); -}; - -const persistTheme = theme => { - if (!isValidTheme(theme)) { - throw new Error(`Invalid theme: ${theme}`); - } - - if (systemPreferencesTheme() === theme) { - // Clear the cookie to represent using the system preferences - document.cookie = `${COOKIE_KEY}=;path=/`; - return; - } - - const expires = new Date(new Date().setYear(new Date().getFullYear() + 1)).toUTCString(); - document.cookie = `${COOKIE_KEY}=${theme};expires=${expires};path=/`; -}; - -export { - detectTheme, - persistTheme -}; diff --git a/packages/scratch-gui/src/reducers/gui.ts b/packages/scratch-gui/src/reducers/gui.ts index 327b483689..516b651ac4 100644 --- a/packages/scratch-gui/src/reducers/gui.ts +++ b/packages/scratch-gui/src/reducers/gui.ts @@ -22,7 +22,7 @@ import fontsLoadedReducer, {fontsLoadedInitialState} from './fonts-loaded'; import restoreDeletionReducer, {restoreDeletionInitialState} from './restore-deletion'; import stageSizeReducer, {stageSizeInitialState} from './stage-size'; import targetReducer, {targetsInitialState} from './targets'; -import themeReducer, {themeInitialState} from './theme'; +import settingsReducer, {settingsInitialState} from './settings'; import timeoutReducer, {timeoutInitialState} from './timeout'; import timeTravelReducer, {timeTravelInitialState} from './time-travel'; import toolboxReducer, {toolboxInitialState} from './toolbox'; @@ -61,7 +61,7 @@ const buildInitialState = (config: GUIConfig) => ({ fontsLoaded: fontsLoadedInitialState, restoreDeletion: restoreDeletionInitialState, targets: targetsInitialState, - theme: themeInitialState, + settings: settingsInitialState, timeout: timeoutInitialState, timeTravel: timeTravelInitialState, toolbox: toolboxInitialState, @@ -170,7 +170,7 @@ const guiReducer = combineReducers({ fontsLoaded: fontsLoadedReducer, restoreDeletion: restoreDeletionReducer, targets: targetReducer, - theme: themeReducer, + settings: settingsReducer, timeout: timeoutReducer, timeTravel: timeTravelReducer, toolbox: toolboxReducer, diff --git a/packages/scratch-gui/src/reducers/menus.js b/packages/scratch-gui/src/reducers/menus.js index 4fec54b8a6..f07ab0eab1 100644 --- a/packages/scratch-gui/src/reducers/menus.js +++ b/packages/scratch-gui/src/reducers/menus.js @@ -9,6 +9,7 @@ const MENU_LANGUAGE = 'languageMenu'; const MENU_LOGIN = 'loginMenu'; const MENU_MODE = 'modeMenu'; const MENU_SETTINGS = 'settingsMenu'; +const MENU_COLOR_MODE = 'colorModeMenu'; const MENU_THEME = 'themeMenu'; class Menu { @@ -51,6 +52,7 @@ const rootMenu = new Menu('root') .addChild( new Menu(MENU_SETTINGS) .addChild(new Menu(MENU_LANGUAGE)) + .addChild(new Menu(MENU_COLOR_MODE)) .addChild(new Menu(MENU_THEME)) ) .addChild(new Menu(MENU_FILE)) @@ -70,6 +72,7 @@ const initialState = { [MENU_LOGIN]: false, [MENU_MODE]: false, [MENU_SETTINGS]: false, + [MENU_COLOR_MODE]: false, [MENU_THEME]: false }; @@ -142,10 +145,6 @@ const openSettingsMenu = () => openMenu(MENU_SETTINGS); const closeSettingsMenu = () => closeMenu(MENU_SETTINGS); const settingsMenuOpen = state => state.scratchGui.menus[MENU_SETTINGS]; -const openThemeMenu = () => openMenu(MENU_THEME); -const closeThemeMenu = () => closeMenu(MENU_THEME); -const themeMenuOpen = state => state.scratchGui.menus[MENU_THEME]; - export { reducer as default, initialState as menuInitialState, @@ -172,8 +171,5 @@ export { modeMenuOpen, openSettingsMenu, closeSettingsMenu, - settingsMenuOpen, - openThemeMenu, - closeThemeMenu, - themeMenuOpen + settingsMenuOpen }; diff --git a/packages/scratch-gui/src/reducers/settings.js b/packages/scratch-gui/src/reducers/settings.js new file mode 100644 index 0000000000..bf520ae58c --- /dev/null +++ b/packages/scratch-gui/src/reducers/settings.js @@ -0,0 +1,38 @@ +import {detectColorMode} from '../lib/settings/color-mode/persistence'; +import {detectTheme} from '../lib/settings/theme/persistence'; + +const SET_COLOR_MODE = 'scratch-gui/settings/SET_COLOR_MODE'; +const SET_THEME = 'scratch-gui/settings/SET_THEME'; + +const initialState = { + colorMode: detectColorMode(), + theme: detectTheme() +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case SET_COLOR_MODE: + return {...state, colorMode: action.colorMode}; + case SET_THEME: + return {...state, theme: action.theme}; + default: + return state; + } +}; + +const setColorMode = colorMode => ({ + type: SET_COLOR_MODE, + colorMode +}); + +const setTheme = theme => ({ + type: SET_THEME, + theme +}); + +export { + reducer as default, + initialState as settingsInitialState, + setColorMode, + setTheme +}; diff --git a/packages/scratch-gui/src/reducers/theme.js b/packages/scratch-gui/src/reducers/theme.js deleted file mode 100644 index 4330f94dad..0000000000 --- a/packages/scratch-gui/src/reducers/theme.js +++ /dev/null @@ -1,27 +0,0 @@ -import {detectTheme} from '../lib/themes/themePersistance'; - -const SET_THEME = 'scratch-gui/theme/SET_THEME'; - -const initialState = { - theme: detectTheme() -}; - -const reducer = (state = initialState, action) => { - switch (action.type) { - case SET_THEME: - return {...state, theme: action.theme}; - default: - return state; - } -}; - -const setTheme = theme => ({ - type: SET_THEME, - theme -}); - -export { - reducer as default, - initialState as themeInitialState, - setTheme -}; diff --git a/packages/scratch-gui/test/integration/menu-bar.test.js b/packages/scratch-gui/test/integration/menu-bar.test.js index 46103bf793..1927246177 100644 --- a/packages/scratch-gui/test/integration/menu-bar.test.js +++ b/packages/scratch-gui/test/integration/menu-bar.test.js @@ -95,7 +95,7 @@ describe('Menu bar settings', () => { await findByText('project1-sprite'); }); - test('Theme picker shows themes', async () => { + test('Color mode picker shows color modes', async () => { await loadUri(uri); await clickXpath(SETTINGS_MENU_XPATH); await clickText('Color Mode', scope.menuBar); @@ -104,13 +104,13 @@ describe('Menu bar settings', () => { expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true); }); - test('Theme picker switches to high contrast', async () => { + test('Color mode picker switches to high contrast', async () => { await loadUri(uri); await clickXpath(SETTINGS_MENU_XPATH); await clickText('Color Mode', scope.menuBar); await clickText('High Contrast', scope.menuBar); - // There is a tiny delay for the color theme to be applied to the categories. + // There is a tiny delay for the color color mode to be applied to the categories. await driver.wait(async () => { const motionCategoryDiv = await findByXpath( '//div[contains(@class, "scratchCategoryMenuItem") and ' + @@ -121,20 +121,20 @@ describe('Menu bar settings', () => { // returns the value. Locally I am seeing 'rgba(128, 181, 255, 1)', // but this is a bit flexible just in case. return /128,\s?181,\s?255/.test(color) || color.includes('80B5FF'); - }, 5000, 'Motion category color does not match high contrast theme'); + }, 5000, 'Motion category color does not match high contrast color mode'); }); test('Settings menu switches between submenus', async () => { await loadUri(uri); await clickXpath(SETTINGS_MENU_XPATH); - // Language and theme options not visible yet + // Language and color mode options not visible yet expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(false); expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false); await clickText('Color Mode', scope.menuBar); - // Only theme options visible + // Only color mode options visible expect(await (await findByText('High Contrast', scope.menuBar)).isDisplayed()).toBe(true); expect(await (await findByText('Esperanto', scope.menuBar)).isDisplayed()).toBe(false); diff --git a/packages/scratch-gui/test/unit/components/menu-bar.test.jsx b/packages/scratch-gui/test/unit/components/menu-bar.test.jsx index 37ec9b61e8..483251cb31 100644 --- a/packages/scratch-gui/test/unit/components/menu-bar.test.jsx +++ b/packages/scratch-gui/test/unit/components/menu-bar.test.jsx @@ -3,7 +3,7 @@ import {renderWithIntl} from '../../helpers/intl-helpers.jsx'; import MenuBar from '../../../src/components/menu-bar/menu-bar'; import {menuInitialState} from '../../../src/reducers/menus'; import {LoadingState} from '../../../src/reducers/project-state'; -import {DEFAULT_THEME} from '../../../src/lib/themes'; +import {DEFAULT_MODE} from '../../../src/lib/settings/color-mode'; import {fireEvent} from '@testing-library/react'; import {PLATFORM} from '../../../src/lib/platform'; @@ -23,8 +23,8 @@ describe('MenuBar Component', () => { projectState: { loadingState: LoadingState.NOT_LOADED }, - theme: { - theme: DEFAULT_THEME + settings: { + colorMode: DEFAULT_MODE }, timeTravel: { year: 'NOW' diff --git a/packages/scratch-gui/test/unit/components/monitor-list.test.jsx b/packages/scratch-gui/test/unit/components/monitor-list.test.jsx index 51edd6caaa..8f60e5cb9b 100644 --- a/packages/scratch-gui/test/unit/components/monitor-list.test.jsx +++ b/packages/scratch-gui/test/unit/components/monitor-list.test.jsx @@ -4,7 +4,7 @@ import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; import {renderWithIntl} from '../../helpers/intl-helpers.jsx'; import MonitorList from '../../../src/components/monitor-list/monitor-list.jsx'; -import {DEFAULT_THEME} from '../../../src/lib/themes'; +import {DEFAULT_MODE} from '../../../src/lib/settings/color-mode'; describe('MonitorListComponent', () => { const store = configureStore()({ @@ -13,8 +13,8 @@ describe('MonitorListComponent', () => { monitors: {}, savedMonitorPositions: {} }, - theme: { - theme: DEFAULT_THEME + settings: { + colorMode: DEFAULT_MODE }, toolbox: { toolboxXML: '' diff --git a/packages/scratch-gui/test/unit/components/monitor.test.jsx b/packages/scratch-gui/test/unit/components/monitor.test.jsx index 7a5d0b0c28..853510adcc 100644 --- a/packages/scratch-gui/test/unit/components/monitor.test.jsx +++ b/packages/scratch-gui/test/unit/components/monitor.test.jsx @@ -1,10 +1,10 @@ import React from 'react'; import {render} from '@testing-library/react'; import Monitor from '../../../src/components/monitor/monitor'; -import {DARK_THEME, DEFAULT_THEME} from '../../../src/lib/themes'; +import {DARK_MODE, DEFAULT_MODE} from '../../../src/lib/settings/color-mode'; -jest.mock('../../../src/lib/themes/default'); -jest.mock('../../../src/lib/themes/dark'); +jest.mock('../../../src/lib/settings/color-mode/default'); +jest.mock('../../../src/lib/settings/color-mode/dark'); describe('Monitor Component', () => { const noop = jest.fn(); @@ -22,19 +22,19 @@ describe('Monitor Component', () => { onNextMode: noop }; - test('it selects the correct colors based on default theme', () => { + test('it selects the correct colors based on default color mode', () => { const {container} = render(); expect(container.firstChild).toMatchSnapshot(); }); - test('it selects the correct colors based on dark mode theme', () => { + test('it selects the correct colors based on dark mode', () => { const {container} = render(); expect(container.firstChild).toMatchSnapshot(); diff --git a/packages/scratch-gui/test/unit/util/themes.test.js b/packages/scratch-gui/test/unit/util/color-modes.test.js similarity index 66% rename from packages/scratch-gui/test/unit/util/themes.test.js rename to packages/scratch-gui/test/unit/util/color-modes.test.js index 35d8fa25b9..ba0e315daa 100644 --- a/packages/scratch-gui/test/unit/util/themes.test.js +++ b/packages/scratch-gui/test/unit/util/color-modes.test.js @@ -1,32 +1,32 @@ import { - DARK_THEME, + DARK_MODE, defaultColors, - DEFAULT_THEME, - getColorsForTheme, - HIGH_CONTRAST_THEME -} from '../../../src/lib/themes'; -import {injectExtensionBlockTheme, injectExtensionCategoryTheme} from '../../../src/lib/themes/blockHelpers'; -import {detectTheme, persistTheme} from '../../../src/lib/themes/themePersistance'; + DEFAULT_MODE, + getColorsForMode, + HIGH_CONTRAST_MODE +} from '../../../src/lib/settings/color-mode'; +import {injectExtensionBlockMode, injectExtensionCategoryMode} from '../../../src/lib/settings/color-mode/blockHelpers'; +import {detectColorMode, persistColorMode} from '../../../src/lib/settings/color-mode/persistence'; -jest.mock('../../../src/lib/themes/default'); -jest.mock('../../../src/lib/themes/dark'); +jest.mock('../../../src/lib/settings/color-mode/default'); +jest.mock('../../../src/lib/settings/color-mode/dark'); -describe('themes', () => { +describe('color modes', () => { let serializeToString; describe('core functionality', () => { - test('provides the default theme colors', () => { + test('provides the default color mode colors', () => { expect(defaultColors.motion.primary).toEqual('#111111'); }); test('returns the dark mode', () => { - const colors = getColorsForTheme(DARK_THEME); + const colors = getColorsForMode(DARK_MODE); expect(colors.motion.primary).toEqual('#AAAAAA'); }); - test('uses default theme colors when not specified', () => { - const colors = getColorsForTheme(DARK_THEME); + test('uses default color mode colors when not specified', () => { + const colors = getColorsForMode(DARK_MODE); expect(colors.motion.secondary).toEqual('#222222'); }); @@ -45,7 +45,7 @@ describe('themes', () => { global.XMLSerializer = XMLSerializer; }); - test('updates extension block colors based on theme', () => { + test('updates extension block colors based on color mode', () => { const blockInfoJson = { type: 'dummy_block', colour: '#0FBD8C', @@ -53,7 +53,7 @@ describe('themes', () => { colourTertiary: '#0B8E69' }; - const updated = injectExtensionBlockTheme(blockInfoJson, DARK_THEME); + const updated = injectExtensionBlockMode(blockInfoJson, DARK_MODE); expect(updated).toEqual({ type: 'dummy_block', @@ -65,7 +65,7 @@ describe('themes', () => { expect(blockInfoJson.colour).toBe('#0FBD8C'); }); - test('updates extension block icon based on theme', () => { + test('updates extension block icon based on color mode', () => { const blockInfoJson = { type: 'pen_block', args0: [ @@ -79,7 +79,7 @@ describe('themes', () => { colourTertiary: '#0B8E69' }; - const updated = injectExtensionBlockTheme(blockInfoJson, DARK_THEME); + const updated = injectExtensionBlockMode(blockInfoJson, DARK_MODE); expect(updated).toEqual({ type: 'pen_block', @@ -97,7 +97,7 @@ describe('themes', () => { expect(blockInfoJson.args0[0].src).toBe('original'); }); - test('bypasses updates if using the default theme', () => { + test('bypasses updates if using the default color mode', () => { const blockInfoJson = { type: 'dummy_block', colour: '#0FBD8C', @@ -105,7 +105,7 @@ describe('themes', () => { colourTertiary: '#0B8E69' }; - const updated = injectExtensionBlockTheme(blockInfoJson, DEFAULT_THEME); + const updated = injectExtensionBlockMode(blockInfoJson, DEFAULT_MODE); expect(updated).toEqual({ type: 'dummy_block', @@ -115,7 +115,7 @@ describe('themes', () => { }); }); - test('updates extension category based on theme', () => { + test('updates extension category based on color mode', () => { const dynamicBlockXML = [ { id: 'pen', @@ -123,7 +123,7 @@ describe('themes', () => { } ]; - injectExtensionCategoryTheme(dynamicBlockXML, DARK_THEME); + injectExtensionCategoryMode(dynamicBlockXML, DARK_MODE); // XMLSerializer is not available outside the browser. // Verify the mocked XMLSerializer.serializeToString is called with updated colors. @@ -133,35 +133,35 @@ describe('themes', () => { }); }); - describe('theme persistance', () => { - test('returns the theme stored in a cookie', () => { - window.document.cookie = `scratchtheme=${HIGH_CONTRAST_THEME}`; + describe('color mode persistence', () => { + test('returns the color mode stored in a cookie', () => { + window.document.cookie = `scratchtheme=${HIGH_CONTRAST_MODE}`; - const theme = detectTheme(); + const colorMode = detectColorMode(); - expect(theme).toEqual(HIGH_CONTRAST_THEME); + expect(colorMode).toEqual(HIGH_CONTRAST_MODE); }); - test('returns the system theme when no cookie', () => { + test('returns the system color mode when no cookie', () => { window.document.cookie = 'scratchtheme='; - const theme = detectTheme(); + const colorMode = detectColorMode(); - expect(theme).toEqual(DEFAULT_THEME); + expect(colorMode).toEqual(DEFAULT_MODE); }); - test('persists theme to cookie', () => { + test('persists color mode to cookie', () => { window.document.cookie = 'scratchtheme='; - persistTheme(HIGH_CONTRAST_THEME); + persistColorMode(HIGH_CONTRAST_MODE); - expect(window.document.cookie).toEqual(`scratchtheme=${HIGH_CONTRAST_THEME}`); + expect(window.document.cookie).toEqual(`scratchtheme=${HIGH_CONTRAST_MODE}`); }); - test('clears theme when matching system preferences', () => { - window.document.cookie = `scratchtheme=${HIGH_CONTRAST_THEME}`; + test('clears color mode when matching system preferences', () => { + window.document.cookie = `scratchtheme=${HIGH_CONTRAST_MODE}`; - persistTheme(DEFAULT_THEME); + persistColorMode(DEFAULT_MODE); expect(window.document.cookie).toEqual('scratchtheme='); }); diff --git a/packages/scratch-gui/webpack.config.js b/packages/scratch-gui/webpack.config.js index 34e2eda2e3..57168477a3 100644 --- a/packages/scratch-gui/webpack.config.js +++ b/packages/scratch-gui/webpack.config.js @@ -77,7 +77,7 @@ const baseConfig = new ScratchWebpackConfigBuilder( { // overwrite some of the default block media with high-contrast versions // this entry must come after copying scratch-blocks/media into the high-contrast directory - from: 'src/lib/themes/high-contrast/blocks-media', + from: 'src/lib/settings/color-mode/high-contrast/blocks-media', to: 'static/blocks-media/high-contrast', force: true },