From a9e10807caf861b63d3cc1af122602378a873eee Mon Sep 17 00:00:00 2001 From: Sam El-Husseini Date: Tue, 10 Mar 2020 10:30:13 -0700 Subject: [PATCH] Support extending themes with a base theme property. (#3731) * Support extending themes with a base theme property. --- core/options.js | 3 +- core/theme.js | 97 ++++++++++++++--------- core/theme/dark.js | 149 +++--------------------------------- core/toolbox.js | 2 +- core/utils/object.js | 18 +++++ tests/themes/test_themes.js | 10 ++- 6 files changed, 100 insertions(+), 179 deletions(-) diff --git a/core/options.js b/core/options.js index e78c25158..307129915 100644 --- a/core/options.js +++ b/core/options.js @@ -282,8 +282,7 @@ Blockly.Options.parseThemeOptions_ = function(options) { if (theme instanceof Blockly.Theme) { return /** @type {!Blockly.Theme} */ (theme); } - return new Blockly.Theme('builtin', - theme['blockStyles'], theme['categoryStyles'], theme['componentStyles']); + return Blockly.Theme.defineTheme('builtin', theme); }; /** diff --git a/core/theme.js b/core/theme.js index a9d7d5751..5ac44b501 100644 --- a/core/theme.js +++ b/core/theme.js @@ -13,27 +13,27 @@ goog.provide('Blockly.Theme'); goog.require('Blockly.utils'); goog.require('Blockly.utils.colour'); +goog.require('Blockly.utils.object'); /** * Class for a theme. * @param {string} name Theme name. - * @param {!Object.} blockStyles A map from - * style names (strings) to objects with style attributes for blocks. - * @param {!Object.} categoryStyles A map - * from style names (strings) to objects with style attributes for + * @param {!Object.=} opt_blockStyles A map + * from style names (strings) to objects with style attributes for blocks. + * @param {!Object.=} opt_categoryStyles A + * map from style names (strings) to objects with style attributes for * categories. * @param {!Object.=} opt_componentStyles A map of Blockly component * names to style value. * @constructor */ -Blockly.Theme = function(name, blockStyles, categoryStyles, +Blockly.Theme = function(name, opt_blockStyles, opt_categoryStyles, opt_componentStyles) { /** * The theme name. This can be used to reference a specific theme in CSS. * @type {string} - * @package */ this.name = name; @@ -42,36 +42,36 @@ Blockly.Theme = function(name, blockStyles, categoryStyles, * @type {!Object.} * @package */ - this.blockStyles = blockStyles; + this.blockStyles = opt_blockStyles || Object.create(null); /** * The category styles map. * @type {!Object.} * @package */ - this.categoryStyles = categoryStyles; + this.categoryStyles = opt_categoryStyles || Object.create(null); /** * The UI components styles map. * @type {!Object.} - * @private + * @package */ - this.componentStyles_ = opt_componentStyles || Object.create(null); + this.componentStyles = opt_componentStyles || Object.create(null); /** * The font style. - * @type {?Blockly.Theme.FontStyle} + * @type {Blockly.Theme.FontStyle} * @package */ - this.fontStyle = null; + this.fontStyle = /** @type {Blockly.Theme.FontStyle} */ (Object.create(null)); /** * Whether or not to add a 'hat' on top of all blocks with no previous or * output connections. - * @type {?boolean} + * @type {boolean} * @package */ - this.startHats = null; + this.startHats = false; }; /** @@ -122,6 +122,31 @@ Blockly.Theme.prototype.setCategoryStyle = function(categoryStyleName, this.categoryStyles[categoryStyleName] = categoryStyle; }; +/** + * Gets the style for a given Blockly UI component. If the style value is a + * string, we attempt to find the value of any named references. + * @param {string} componentName The name of the component. + * @return {?string} The style value. + */ +Blockly.Theme.prototype.getComponentStyle = function(componentName) { + var style = this.componentStyles[componentName]; + if (style && typeof propertyValue == 'string' && + this.getComponentStyle(/** @type {string} */ (style))) { + return this.getComponentStyle(/** @type {string} */ (style)); + } + return style ? String(style) : null; +}; + +/** + * Configure a specific Blockly UI component with a style value. + * @param {string} componentName The name of the component. + * @param {*} styleValue The style value. +*/ +Blockly.Theme.prototype.setComponentStyle = function(componentName, + styleValue) { + this.componentStyles[componentName] = styleValue; +}; + /** * Configure a theme's font style. * @param {Blockly.Theme.FontStyle} fontStyle The font style. @@ -140,26 +165,28 @@ Blockly.Theme.prototype.setStartHats = function(startHats) { }; /** - * Gets the style for a given Blockly UI component. If the style value is a - * string, we attempt to find the value of any named references. - * @param {string} componentName The name of the component. - * @return {?string} The style value. - */ -Blockly.Theme.prototype.getComponentStyle = function(componentName) { - var style = this.componentStyles_[componentName]; - if (style && typeof propertyValue == 'string' && - this.getComponentStyle(/** @type {string} */ (style))) { - return this.getComponentStyle(/** @type {string} */ (style)); - } - return style ? String(style) : null; -}; - -/** - * Configure a specific Blockly UI component with a style value. - * @param {string} componentName The name of the component. - * @param {*} styleValue The style value. + * Define a new Blockly theme. + * @param {string} name The name of the theme. + * @param {!Object} themeObj An object containing theme properties. + * @return {!Blockly.Theme} A new Blockly theme. */ -Blockly.Theme.prototype.setComponentStyle = function(componentName, - styleValue) { - this.componentStyles_[componentName] = styleValue; +Blockly.Theme.defineTheme = function(name, themeObj) { + var theme = new Blockly.Theme(name); + var base = themeObj['base']; + if (base && base instanceof Blockly.Theme) { + Blockly.utils.object.deepMerge(theme, base); + } + + Blockly.utils.object.deepMerge(theme.blockStyles, + themeObj['blockStyles']); + Blockly.utils.object.deepMerge(theme.categoryStyles, + themeObj['categoryStyles']); + Blockly.utils.object.deepMerge(theme.componentStyles, + themeObj['componentStyles']); + Blockly.utils.object.deepMerge(theme.fontStyle, + themeObj['fontStyle']); + if (themeObj['startHats'] != null) { + theme.startHats = themeObj['startHats']; + } + return theme; }; diff --git a/core/theme/dark.js b/core/theme/dark.js index d2573e51a..7f3c3bb31 100644 --- a/core/theme/dark.js +++ b/core/theme/dark.js @@ -12,143 +12,18 @@ goog.provide('Blockly.Themes.Dark'); -goog.require('Blockly.Css'); goog.require('Blockly.Theme'); - -// Temporary holding object. -Blockly.Themes.Dark = {}; - -Blockly.Themes.Dark.defaultBlockStyles = { - "colour_blocks": { - "colourPrimary": "#a5745b", - "colourSecondary": "#dbc7bd", - "colourTertiary": "#845d49" - }, - "list_blocks": { - "colourPrimary": "#745ba5", - "colourSecondary": "#c7bddb", - "colourTertiary": "#5d4984" - }, - "logic_blocks": { - "colourPrimary": "#5b80a5", - "colourSecondary": "#bdccdb", - "colourTertiary": "#496684" - }, - "loop_blocks": { - "colourPrimary": "#5ba55b", - "colourSecondary": "#bddbbd", - "colourTertiary": "#498449" - }, - "math_blocks": { - "colourPrimary": "#5b67a5", - "colourSecondary": "#bdc2db", - "colourTertiary": "#495284" - }, - "procedure_blocks": { - "colourPrimary": "#995ba5", - "colourSecondary": "#d6bddb", - "colourTertiary": "#7a4984" - }, - "text_blocks": { - "colourPrimary": "#5ba58c", - "colourSecondary": "#bddbd1", - "colourTertiary": "#498470" - }, - "variable_blocks": { - "colourPrimary": "#a55b99", - "colourSecondary": "#dbbdd6", - "colourTertiary": "#84497a" - }, - "variable_dynamic_blocks": { - "colourPrimary": "#a55b99", - "colourSecondary": "#dbbdd6", - "colourTertiary": "#84497a" - }, - "hat_blocks": { - "colourPrimary": "#a55b99", - "colourSecondary": "#dbbdd6", - "colourTertiary": "#84497a", - "hat": "cap" +Blockly.Themes.Dark = Blockly.Theme.defineTheme('dark', { + 'base': Blockly.Themes.Classic, + 'componentStyles': { + 'workspaceBackgroundColour': '#1e1e1e', + 'toolboxBackgroundColour': '#333', + 'toolboxForegroundColour': '#fff', + 'flyoutBackgroundColour': '#252526', + 'flyoutForegroundColour': '#ccc', + 'flyoutOpacity': 1, + 'scrollbarColour': '#797979', + 'scrollbarOpacity': 0.4 } -}; - -Blockly.Themes.Dark.categoryStyles = { - "colour_category": { - "colour": "#a5745b" - }, - "list_category": { - "colour": "#745ba5" - }, - "logic_category": { - "colour": "#5b80a5" - }, - "loop_category": { - "colour": "#5ba55b" - }, - "math_category": { - "colour": "#5b67a5" - }, - "procedure_category": { - "colour": "#995ba5" - }, - "text_category": { - "colour": "#5ba58c" - }, - "variable_category": { - "colour": "#a55b99" - }, - "variable_dynamic_category": { - "colour": "#a55b99" - } -}; - -// This style is still being fleshed out and may change. -Blockly.Themes.Dark = - new Blockly.Theme('dark', Blockly.Themes.Dark.defaultBlockStyles, - Blockly.Themes.Dark.categoryStyles); - -Blockly.Themes.Dark.setComponentStyle('workspaceBackgroundColour', '#1e1e1e'); -Blockly.Themes.Dark.setComponentStyle('toolboxBackgroundColour', '#333'); -Blockly.Themes.Dark.setComponentStyle('toolboxForegroundColour', '#fff'); -Blockly.Themes.Dark.setComponentStyle('flyoutBackgroundColour', '#252526'); -Blockly.Themes.Dark.setComponentStyle('flyoutForegroundColour', '#ccc'); -Blockly.Themes.Dark.setComponentStyle('flyoutOpacity', 1); -Blockly.Themes.Dark.setComponentStyle('scrollbarColour', '#797979'); -Blockly.Themes.Dark.setComponentStyle('scrollbarOpacity', 0.4); - -/** - * CSS for the dark theme. - * This registers CSS that is specific to this theme. It does so by prepending a - * ``.dark-theme`` selector before every CSS rule that we wish to override by - * this theme. - */ -(function() { - var selector = '.dark-theme'; - Blockly.Css.register([ - /* eslint-disable indent */ - // Toolbox hover - selector + ' .blocklyTreeRow:not(.blocklyTreeSelected):hover {', - 'background-color: #2a2d2e;', - '}', - // Dropdown and Widget div. - selector + '.blocklyWidgetDiv .goog-menu, ', - selector + '.blocklyDropDownDiv {', - 'background-color: #3c3c3c;', - '}', - selector + '.blocklyDropDownDiv {', - 'border-color: #565656;', - '}', - selector + '.blocklyWidgetDiv .goog-menuitem-content, ', - selector + '.blocklyDropDownDiv .goog-menuitem-content {', - 'color: #f0f0f0;', - '}', - selector + '.blocklyWidgetDiv .goog-menuitem-disabled', - ' .goog-menuitem-content,', - selector + '.blocklyDropDownDiv .goog-menuitem-disabled', - ' .goog-menuitem-content {', - 'color: #8a8a8a !important;', - '}', - /* eslint-enable indent */ - ]); -})(); +}); diff --git a/core/toolbox.js b/core/toolbox.js index cacd67d17..1060dd2a3 100644 --- a/core/toolbox.js +++ b/core/toolbox.js @@ -780,7 +780,7 @@ Blockly.Css.register([ '}', '.blocklyTreeRow:not(.blocklyTreeSelected):hover {', - 'background-color: #e4e4e4;', + 'background-color: rgba(255, 255, 255, 0.2);', '}', '.blocklyTreeSeparator {', diff --git a/core/utils/object.js b/core/utils/object.js index 8a5188c29..0212f3fdc 100644 --- a/core/utils/object.js +++ b/core/utils/object.js @@ -37,6 +37,24 @@ Blockly.utils.object.mixin = function(target, source) { } }; +/** + * Complete a deep merge of all members of a source object with a target object. + * @param {!Object} target Target. + * @param {!Object} source Source. + * @return {!Object} The resulting object. + */ +Blockly.utils.object.deepMerge = function(target, source) { + for (var x in source) { + if (typeof source[x] === 'object') { + target[x] = Blockly.utils.object.deepMerge( + target[x] || Object.create(null), source[x]); + } else { + target[x] = source[x]; + } + } + return target; +}; + /** * Returns an array of a given object's own enumerable property values. * @param {!Object} obj Object containing values. diff --git a/tests/themes/test_themes.js b/tests/themes/test_themes.js index 3f554c70f..30792ae9e 100644 --- a/tests/themes/test_themes.js +++ b/tests/themes/test_themes.js @@ -11,15 +11,17 @@ goog.provide('Blockly.TestThemes'); /** * A theme with classic colours but enables start hats. */ -Blockly.Themes.TestHats = new Blockly.Theme('testhats', - Blockly.Themes.Classic.blockStyles, Blockly.Themes.Classic.categoryStyles); +Blockly.Themes.TestHats = Blockly.Theme.defineTheme('testhats', { + 'base': Blockly.Themes.Classic +}); Blockly.Themes.TestHats.setStartHats(true); /** * A theme with classic colours but a different font. */ -Blockly.Themes.TestFont = new Blockly.Theme('testfont', - Blockly.Themes.Classic.blockStyles, Blockly.Themes.Classic.categoryStyles); +Blockly.Themes.TestFont = Blockly.Theme.defineTheme('testfont', { + 'base': Blockly.Themes.Classic +}); Blockly.Themes.TestFont.setFontStyle({ 'family': '"Times New Roman", Times, serif', 'weight': null, // Use default font-weight