Workspace theme (#3093)

* Move the theme object so it's on the workspace.

* Add support for subscribing UI elements to theme component styles and changes.
This commit is contained in:
Sam El-Husseini
2019-09-26 16:52:17 -07:00
committed by GitHub
parent b587ad71c5
commit 870824bc3e
23 changed files with 566 additions and 130 deletions

View File

@@ -120,8 +120,10 @@ goog.addDependency("../../core/requires.js", ['Blockly.requires'], ['Blockly', '
goog.addDependency("../../core/scrollbar.js", ['Blockly.Scrollbar', 'Blockly.ScrollbarPair'], ['Blockly.Touch', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom']);
goog.addDependency("../../core/theme.js", ['Blockly.Theme'], []);
goog.addDependency("../../core/theme/classic.js", ['Blockly.Themes.Classic'], ['Blockly.Theme']);
goog.addDependency("../../core/theme/dark.js", ['Blockly.Themes.Dark'], ['Blockly.Theme']);
goog.addDependency("../../core/theme/highcontrast.js", ['Blockly.Themes.HighContrast'], ['Blockly.Theme']);
goog.addDependency("../../core/theme/modern.js", ['Blockly.Themes.Modern'], ['Blockly.Theme']);
goog.addDependency("../../core/theme_manager.js", ['Blockly.ThemeManager'], ['Blockly.Theme']);
goog.addDependency("../../core/toolbox.js", ['Blockly.Toolbox'], ['Blockly.Events', 'Blockly.Events.Ui', 'Blockly.navigation', 'Blockly.Touch', 'Blockly.tree.TreeControl', 'Blockly.tree.TreeNode', 'Blockly.utils', 'Blockly.utils.aria', 'Blockly.utils.colour', 'Blockly.utils.dom', 'Blockly.utils.object', 'Blockly.utils.Rect']);
goog.addDependency("../../core/tooltip.js", ['Blockly.Tooltip'], ['Blockly.utils.string']);
goog.addDependency("../../core/touch.js", ['Blockly.Touch'], ['Blockly.utils', 'Blockly.utils.global', 'Blockly.utils.string']);
@@ -153,7 +155,7 @@ goog.addDependency("../../core/variables.js", ['Blockly.Variables'], ['Blockly.B
goog.addDependency("../../core/variables_dynamic.js", ['Blockly.VariablesDynamic'], ['Blockly.Variables', 'Blockly.Blocks', 'Blockly.Msg', 'Blockly.utils.xml', 'Blockly.VariableModel', 'Blockly.Xml']);
goog.addDependency("../../core/warning.js", ['Blockly.Warning'], ['Blockly.Bubble', 'Blockly.Events', 'Blockly.Events.Ui', 'Blockly.Icon', 'Blockly.utils.dom', 'Blockly.utils.object']);
goog.addDependency("../../core/widgetdiv.js", ['Blockly.WidgetDiv'], ['Blockly.Css', 'Blockly.utils.style']);
goog.addDependency("../../core/workspace.js", ['Blockly.Workspace'], ['Blockly.Cursor', 'Blockly.MarkerCursor', 'Blockly.Events', 'Blockly.Themes.Classic', 'Blockly.utils', 'Blockly.utils.math', 'Blockly.VariableMap', 'Blockly.WorkspaceComment']);
goog.addDependency("../../core/workspace.js", ['Blockly.Workspace'], ['Blockly.Cursor', 'Blockly.MarkerCursor', 'Blockly.Events', 'Blockly.ThemeManager', 'Blockly.utils', 'Blockly.utils.math', 'Blockly.VariableMap', 'Blockly.WorkspaceComment']);
goog.addDependency("../../core/workspace_audio.js", ['Blockly.WorkspaceAudio'], ['Blockly.utils', 'Blockly.utils.global', 'Blockly.utils.userAgent']);
goog.addDependency("../../core/workspace_comment.js", ['Blockly.WorkspaceComment'], ['Blockly.Events', 'Blockly.Events.CommentChange', 'Blockly.Events.CommentCreate', 'Blockly.Events.CommentDelete', 'Blockly.Events.CommentMove', 'Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.xml']);
goog.addDependency("../../core/workspace_comment_render_svg.js", ['Blockly.WorkspaceCommentSvg.render'], ['Blockly.utils', 'Blockly.utils.Coordinate', 'Blockly.utils.dom', 'Blockly.WorkspaceCommentSvg']);
@@ -247,7 +249,9 @@ goog.require('Blockly.RenderedConnection');
goog.require('Blockly.Scrollbar');
goog.require('Blockly.ScrollbarPair');
goog.require('Blockly.Theme');
goog.require('Blockly.ThemeManager');
goog.require('Blockly.Themes.Classic');
goog.require('Blockly.Themes.Dark');
goog.require('Blockly.Themes.HighContrast');
goog.require('Blockly.Themes.Modern');
goog.require('Blockly.Toolbox');

View File

@@ -1006,11 +1006,7 @@ Blockly.Block.prototype.setColour = function(colour) {
* @throws {Error} if the block style does not exist.
*/
Blockly.Block.prototype.setStyle = function(blockStyleName) {
var theme = Blockly.getTheme();
if (!theme) {
throw Error('Trying to set block style to ' + blockStyleName +
' before theme was defined via Blockly.setTheme().');
}
var theme = this.workspace.getTheme();
var blockStyle = theme.getBlockStyle(blockStyleName);
this.styleName_ = blockStyleName;

View File

@@ -117,13 +117,6 @@ Blockly.clipboardTypeCounts_ = null;
*/
Blockly.cache3dSupported_ = null;
/**
* Holds all Blockly style attributes.
* @type {Blockly.Theme}
* @private
*/
Blockly.theme_ = null;
/**
* Returns the dimensions of the specified SVG image.
* @param {!Element} svg SVG image.
@@ -684,72 +677,3 @@ Blockly.checkBlockColourConstant_ = function(
console.warn(warning);
}
};
/**
* Sets the theme for Blockly and refreshes all blocks in the toolbox and
* workspace.
* @param {!Blockly.Theme} theme Theme for Blockly.
*/
Blockly.setTheme = function(theme) {
Blockly.theme_ = theme;
var ws = Blockly.getMainWorkspace();
if (ws) {
Blockly.refreshTheme_(ws);
}
};
/**
* Refresh the theme for all items on the workspace.
* @param {!Blockly.Workspace} ws Blockly workspace to refresh theme on.
* @private
*/
Blockly.refreshTheme_ = function(ws) {
// Update all blocks in workspace that have a style name.
Blockly.updateBlockStyles_(ws.getAllBlocks().filter(
function(block) {
return block.getStyleName() !== undefined;
}
));
// Update blocks in the flyout.
if (!ws.toolbox_ && ws.flyout_ && ws.flyout_.workspace_) {
Blockly.updateBlockStyles_(ws.flyout_.workspace_.getAllBlocks());
} else {
ws.refreshToolboxSelection();
}
// Update colours on the categories.
if (ws.toolbox_) {
ws.toolbox_.updateColourFromTheme();
}
var event = new Blockly.Events.Ui(null, 'theme');
event.workspaceId = ws.id;
Blockly.Events.fire(event);
};
/**
* Updates all the blocks with new style.
* @param {!Array.<!Blockly.Block>} blocks List of blocks to update the style
* on.
* @private
*/
Blockly.updateBlockStyles_ = function(blocks) {
for (var i = 0, block; block = blocks[i]; i++) {
var blockStyleName = block.getStyleName();
block.setStyle(blockStyleName);
if (block.mutator) {
block.mutator.updateBlockStyle(blockStyleName);
}
}
};
/**
* Gets the theme.
* @return {Blockly.Theme} Theme for Blockly.
*/
Blockly.getTheme = function() {
return Blockly.theme_;
};

View File

@@ -230,6 +230,8 @@ Blockly.Flyout.prototype.createDom = function(tagName) {
this.svgBackground_ = Blockly.utils.dom.createSvgElement('path',
{'class': 'blocklyFlyoutBackground'}, this.svgGroup_);
this.svgGroup_.appendChild(this.workspace_.createDom());
this.workspace_.getThemeManager().subscribe(this.svgBackground_, 'flyout', 'fill');
this.workspace_.getThemeManager().subscribe(this.svgBackground_, 'flyoutOpacity', 'fill-opacity');
return this.svgGroup_;
};
@@ -286,6 +288,7 @@ Blockly.Flyout.prototype.dispose = function() {
this.scrollbar_ = null;
}
if (this.workspace_) {
this.workspace_.getThemeManager().unsubscribe(this.svgBackground_);
this.workspace_.targetWorkspace = null;
this.workspace_.dispose();
this.workspace_ = null;

View File

@@ -156,6 +156,10 @@ Blockly.FlyoutButton.prototype.createDom = function() {
},
this.svgGroup_);
svgText.textContent = Blockly.utils.replaceMessageReferences(this.text_);
if (this.isLabel_) {
this.svgText_ = svgText;
this.workspace_.getThemeManager().subscribe(this.svgText_, 'flyoutText', 'fill');
}
this.width = Blockly.utils.dom.getTextWidth(svgText);
this.height = 20; // Can't compute it :(
@@ -235,6 +239,9 @@ Blockly.FlyoutButton.prototype.dispose = function() {
Blockly.utils.dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
}
if (this.svgText_) {
this.workspace_.getThemeManager().unsubscribe(this.svgText_);
}
this.workspace_ = null;
this.targetWorkspace_ = null;
};

View File

@@ -72,7 +72,6 @@ Blockly.inject = function(container, opt_options) {
var workspace = Blockly.createMainWorkspace_(svg, options, blockDragSurface,
workspaceDragSurface);
Blockly.setTheme(options.theme);
Blockly.user.keyMap.setKeyMap(options.keyMap);
Blockly.init_(workspace);
@@ -231,6 +230,8 @@ Blockly.createMainWorkspace_ = function(svg, options, blockDragSurface,
if (options.zoomOptions && options.zoomOptions.controls) {
mainWorkspace.addZoomControls();
}
// Register the workspace svg as a UI component.
mainWorkspace.getThemeManager().subscribe(svg, 'workspace', 'background-color');
// A null translation will also apply the correct initial scale.
mainWorkspace.translate(0, 0);

View File

@@ -157,7 +157,8 @@ Blockly.Mutator.prototype.createEditor_ = function() {
Blockly.TOOLBOX_AT_LEFT,
horizontalLayout: false,
getMetrics: this.getFlyoutMetrics_.bind(this),
setMetrics: null
setMetrics: null,
renderer: this.block_.workspace.options.renderer
};
this.workspace_ = new Blockly.WorkspaceSvg(workspaceOptions);
this.workspace_.isMutator = true;

View File

@@ -26,7 +26,6 @@
goog.provide('Blockly.Options');
goog.require('Blockly.Themes.Classic');
goog.require('Blockly.utils.userAgent');
goog.require('Blockly.Xml');
@@ -118,7 +117,7 @@ Blockly.Options = function(options) {
} else {
var oneBasedIndex = !!options['oneBasedIndex'];
}
var theme = options['theme'] || Blockly.Themes.Classic;
var theme = options['theme'];
var keyMap = options['keyMap'] || Blockly.user.keyMap.createDefaultKeyMap();
var renderer = options['renderer'] || 'geras';

View File

@@ -351,7 +351,10 @@ Blockly.Scrollbar.prototype.dispose = function() {
this.outerSvg_ = null;
this.svgGroup_ = null;
this.svgBackground_ = null;
this.svgHandle_ = null;
if (this.svgHandle_) {
this.workspace_.getThemeManager().unsubscribe(this.svgHandle_);
this.svgHandle_ = null;
}
this.workspace_ = null;
};
@@ -625,6 +628,8 @@ Blockly.Scrollbar.prototype.createDom_ = function(opt_class) {
'ry': radius
},
this.svgGroup_);
this.workspace_.getThemeManager().subscribe(this.svgHandle_, 'scrollbar', 'fill');
this.workspace_.getThemeManager().subscribe(this.svgHandle_, 'scrollbarOpacity', 'fill-opacity');
Blockly.utils.dom.insertAfter(this.outerSvg_, this.workspace_.getParentSvg());
};

View File

@@ -28,16 +28,33 @@ goog.provide('Blockly.Theme');
/**
* Class for a theme.
* @param {!Object.<string, Blockly.Theme.BlockStyle>} blockStyles A map from style
* names (strings) to objects with style attributes relating to blocks.
* @param {!Object.<string, Blockly.Theme.CategoryStyle>} categoryStyles A map from
* style names (strings) to objects with style attributes relating to
* @param {!Object.<string, Blockly.Theme.BlockStyle>} blockStyles A map from
* style names (strings) to objects with style attributes for blocks.
* @param {!Object.<string, Blockly.Theme.CategoryStyle>} categoryStyles A map
* from style names (strings) to objects with style attributes for
* categories.
* @param {!Object.<string, *>=} opt_componentStyles A map of Blockly component
* names to style value.
* @constructor
*/
Blockly.Theme = function(blockStyles, categoryStyles) {
Blockly.Theme = function(blockStyles, categoryStyles, opt_componentStyles) {
/**
* The block styles map.
* @type {!Object.<string, Blockly.Theme.BlockStyle>}
*/
this.blockStyles_ = blockStyles;
/**
* The category styles map.
* @type {!Object.<string, Blockly.Theme.CategoryStyle>}
*/
this.categoryStyles_ = categoryStyles;
/**
* The UI components styles map.
* @type {!Object.<string, *>}
*/
this.componentStyles_ = opt_componentStyles || Object.create(null);
};
/**
@@ -114,3 +131,28 @@ Blockly.Theme.prototype.setCategoryStyle = function(categoryStyleName,
categoryStyle) {
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;
};

131
core/theme/dark.js Normal file
View File

@@ -0,0 +1,131 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2019 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Dark theme.
* @author samelh@google.com (Sam El-Husseini)
*/
'use strict';
goog.provide('Blockly.Themes.Dark');
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.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(Blockly.Themes.Dark.defaultBlockStyles,
Blockly.Themes.Dark.categoryStyles);
Blockly.Themes.Dark.setComponentStyle('workspace', '#1e1e1e');
Blockly.Themes.Dark.setComponentStyle('toolbox', '#333');
Blockly.Themes.Dark.setComponentStyle('toolboxText', '#fff');
Blockly.Themes.Dark.setComponentStyle('flyout', '#252526');
Blockly.Themes.Dark.setComponentStyle('flyoutText', '#ccc');
Blockly.Themes.Dark.setComponentStyle('flyoutOpacity', 1);
Blockly.Themes.Dark.setComponentStyle('scrollbar', '#797979');
Blockly.Themes.Dark.setComponentStyle('scrollbarOpacity', 0.4);

196
core/theme_manager.js Normal file
View File

@@ -0,0 +1,196 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2019 Google Inc.
* https://developers.google.com/blockly/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Object in charge of storing and updating a workspace theme
* and UI components.
* @author aschmiedt@google.com (Abby Schmiedt)
* @author samelh@google.com (Sam El-Husseini)
*/
'use strict';
goog.provide('Blockly.ThemeManager');
goog.require('Blockly.Theme');
/**
* Class for storing and updating a workspace's theme and UI components.
* @param {!Blockly.Theme} theme The workspace theme.
* @constructor
* @package
*/
Blockly.ThemeManager = function(theme) {
/**
* The Blockly theme to use.
* @type {!Blockly.Theme}
* @private
*/
this.theme_ = theme;
/**
* A list of workspaces that are subscribed to this theme.
* @type {!Array.<Blockly.Workspace>}
* @private
*/
this.subscribedWorkspaces_ = [];
/**
* A map of subscribed UI components, keyed by component name.
* @type {!Object.<string, !Array.<!Blockly.ThemeManager.Component>>}
* @private
*/
this.componentDB_ = Object.create(null);
};
/**
* A Blockly UI component type.
* @typedef {{
* element:!Element,
* propertyName:string
* }}
*/
Blockly.ThemeManager.Component;
/**
* Get the workspace theme.
* @return {!Blockly.Theme} The workspace theme.
* @package
*/
Blockly.ThemeManager.prototype.getTheme = function() {
return this.theme_;
};
/**
* Set the workspace theme, and refresh the workspace and all components.
* @param {!Blockly.Theme} theme The workspace theme.
* @package
*/
Blockly.ThemeManager.prototype.setTheme = function(theme) {
if (this.theme_ === theme) {
// No change.
return;
}
this.theme_ = theme;
// Refresh all subscribed workspaces.
for (var i = 0, workspace; (workspace = this.subscribedWorkspaces_[i]); i++) {
workspace.refreshTheme();
}
// Refresh all registered Blockly UI components.
for (var i = 0, keys = Object.keys(this.componentDB_),
key; key = keys[i]; i++) {
for (var j = 0, component; (component = this.componentDB_[key][j]); j++) {
var element = component.element;
var propertyName = component.propertyName;
var style = this.theme_ && this.theme_.getComponentStyle(key);
element.style[propertyName] = style || '';
}
}
};
/**
* Subscribe a workspace to changes to the selected theme. If a new theme is
* set, the workspace is called to refresh its blocks.
* @param {!Blockly.Workspace} workspace The workspace to subscribe.
* @package
*/
Blockly.ThemeManager.prototype.subscribeWorkspace = function(workspace) {
this.subscribedWorkspaces_.push(workspace);
};
/**
* Unsubscribe a workspace to changes to the selected theme.
* @param {!Blockly.Workspace} workspace The workspace to unsubscribe.
* @package
*/
Blockly.ThemeManager.prototype.unsubscribeWorkspace = function(workspace) {
var index = this.subscribedWorkspaces_.indexOf(workspace);
if (index < 0) {
throw Error('Cannot unsubscribe a workspace that hasn\'t been subscribed.');
}
this.subscribedWorkspaces_.splice(index, 1);
};
/**
* Subscribe an element to changes to the selected theme. If a new theme is
* selected, the element's style is refreshed with the new theme's style.
* @param {!Element} element The element to subscribe.
* @param {string} componentName The name used to identify the component. This
* must be the same name used to configure the style in the Theme object.
* @param {string} propertyName The inline style property name to update.
* @package
*/
Blockly.ThemeManager.prototype.subscribe = function(element, componentName,
propertyName) {
if (!this.componentDB_[componentName]) {
this.componentDB_[componentName] = [];
}
// Add the element to our component map.
this.componentDB_[componentName].push({
element: element,
propertyName: propertyName
});
// Initialize the element with its corresponding theme style.
var style = this.theme_ && this.theme_.getComponentStyle(componentName);
element.style[propertyName] = style || '';
};
/**
* Unsubscribe an element to changes to the selected theme.
* @param {Element} element The element to unsubscribe.
* @package
*/
Blockly.ThemeManager.prototype.unsubscribe = function(element) {
if (!element) {
return;
}
// Go through all component, and remove any references to this element.
var componentNames = Object.keys(this.componentDB_);
for (var c = 0, componentName; (componentName = componentNames[c]); c++) {
var elements = this.componentDB_[componentName];
for (var i = elements.length - 1; i >= 0; i--) {
if (elements[i].element === element) {
elements.splice(i, 1);
}
}
// Clean up the component map entry if the list is empty.
if (!this.componentDB_[componentName].length) {
delete this.componentDB_[componentName];
}
}
};
/**
* Dispose of this theme manager.
* @package
* @suppress {checkTypes}
*/
Blockly.ThemeManager.prototype.dispose = function() {
this.owner_ = null;
this.theme_ = null;
this.subscribedWorkspaces_ = null;
this.componentDB_ = null;
};

View File

@@ -156,6 +156,9 @@ Blockly.Toolbox.prototype.init = function() {
this.HtmlDiv.className = 'blocklyToolboxDiv blocklyNonSelectable';
this.HtmlDiv.setAttribute('dir', workspace.RTL ? 'RTL' : 'LTR');
svg.parentNode.insertBefore(this.HtmlDiv, svg);
var themeManager = workspace.getThemeManager();
themeManager.subscribe(this.HtmlDiv, 'toolbox', 'background-color');
themeManager.subscribe(this.HtmlDiv, 'toolboxText', 'color');
// Clicking on toolbox closes popups.
Blockly.bindEventWithChecks_(this.HtmlDiv, 'mousedown', this,
@@ -350,6 +353,7 @@ Blockly.Toolbox.prototype.onBlocklyAction = function(action) {
Blockly.Toolbox.prototype.dispose = function() {
this.flyout_.dispose();
this.tree_.dispose();
this.workspace_.getThemeManager().unsubscribe(this.HtmlDiv);
Blockly.utils.dom.removeNode(this.HtmlDiv);
this.workspace_ = null;
this.lastCategory_ = null;
@@ -533,8 +537,9 @@ Blockly.Toolbox.prototype.setColour_ = function(colourValue, childOut,
Blockly.Toolbox.prototype.setColourFromStyle_ = function(
styleName, childOut, categoryName) {
childOut.styleName = styleName;
if (styleName && Blockly.getTheme()) {
var style = Blockly.getTheme().getCategoryStyle(styleName);
var theme = this.workspace_.getTheme();
if (styleName && theme) {
var style = theme.getCategoryStyle(styleName);
if (style && style.colour) {
this.setColour_(style.colour, childOut, categoryName);
} else {
@@ -566,6 +571,7 @@ Blockly.Toolbox.prototype.updateColourFromTheme_ = function(opt_tree) {
/**
* Updates the category colours and background colour of selected categories.
* @package
*/
Blockly.Toolbox.prototype.updateColourFromTheme = function() {
var tree = this.tree_;

View File

@@ -29,6 +29,7 @@ goog.provide('Blockly.Workspace');
goog.require('Blockly.Cursor');
goog.require('Blockly.MarkerCursor');
goog.require('Blockly.Events');
goog.require('Blockly.ThemeManager');
goog.require('Blockly.Themes.Classic');
goog.require('Blockly.utils');
goog.require('Blockly.utils.math');
@@ -131,11 +132,16 @@ Blockly.Workspace = function(opt_options) {
*/
this.marker_ = new Blockly.MarkerCursor();
// Set the default theme. This is for headless workspaces. This will get
// overwritten by the theme passed into the inject call for rendered workspaces.
if (!Blockly.getTheme()) {
Blockly.setTheme(Blockly.Themes.Classic);
}
/**
* Object in charge of storing and updating the workspace theme.
* @type {!Blockly.ThemeManager}
* @protected
*/
this.themeManager_ = this.options.parentWorkspace ?
this.options.parentWorkspace.getThemeManager() :
new Blockly.ThemeManager(this.options.theme || Blockly.Themes.Classic);
this.themeManager_.subscribeWorkspace(this);
};
/**
@@ -198,16 +204,78 @@ Blockly.Workspace.prototype.getMarker = function() {
return this.marker_;
};
/**
* Get the workspace theme object.
* @return {!Blockly.Theme} The workspace theme object.
*/
Blockly.Workspace.prototype.getTheme = function() {
return this.themeManager_.getTheme();
};
/**
* Set the workspace theme object.
* If no theme is passed, default to the `Blockly.Themes.Classic` theme.
* @param {Blockly.Theme} theme The workspace theme object.
*/
Blockly.Workspace.prototype.setTheme = function(theme) {
if (!theme) {
theme = /** @type {!Blockly.Theme} */ (Blockly.Themes.Classic);
}
this.themeManager_.setTheme(theme);
};
/**
* Refresh all blocks on the workspace after a theme update.
* @package
*/
Blockly.Workspace.prototype.refreshTheme = function() {
// Update all blocks in workspace that have a style name.
this.updateBlockStyles_(this.getAllBlocks().filter(
function(block) {
return block.getStyleName() !== undefined;
}
));
var event = new Blockly.Events.Ui(null, 'theme', null, null);
event.workspaceId = this.id;
Blockly.Events.fire(event);
};
/**
* Updates all the blocks with new style.
* @param {!Array.<!Blockly.Block>} blocks List of blocks to update the style
* on.
* @private
*/
Blockly.Workspace.prototype.updateBlockStyles_ = function(blocks) {
for (var i = 0, block; block = blocks[i]; i++) {
var blockStyleName = block.getStyleName();
block.setStyle(blockStyleName);
if (block.mutator) {
block.mutator.updateBlockStyle(blockStyleName);
}
}
};
/**
* Dispose of this workspace.
* Unlink from all DOM elements to prevent memory leaks.
* @suppress {checkTypes}
*/
Blockly.Workspace.prototype.dispose = function() {
this.listeners_.length = 0;
this.clear();
// Remove from workspace database.
delete Blockly.Workspace.WorkspaceDB_[this.id];
if (this.themeManager_) {
this.themeManager_.unsubscribeWorkspace(this);
this.themeManager_.unsubscribe(this.svgBackground_);
if (!this.options.parentWorkspace) {
this.themeManager_.dispose();
this.themeManager_ = null;
}
}
};
/**
@@ -810,3 +878,12 @@ Blockly.Workspace.getAll = function() {
}
return workspaces;
};
/**
* Get the theme manager for this workspace.
* @return {!Blockly.ThemeManager} The theme manager for this workspace.
* @package
*/
Blockly.Workspace.prototype.getThemeManager = function() {
return this.themeManager_;
};

View File

@@ -97,7 +97,7 @@ Blockly.WorkspaceSvg = function(options,
/**
* Object in charge of loading, storing, and playing audio for a workspace.
* @type {Blockly.WorkspaceAudio}
* @type {!Blockly.WorkspaceAudio}
* @private
*/
this.audioManager_ = new Blockly.WorkspaceAudio(options.parentWorkspace);
@@ -636,6 +636,8 @@ Blockly.WorkspaceSvg.prototype.createDom = function(opt_backgroundClass) {
if (opt_backgroundClass == 'blocklyMainBackground' && this.grid_) {
this.svgBackground_.style.fill =
'url(#' + this.grid_.getPatternId() + ')';
} else {
this.themeManager_.subscribe(this.svgBackground_, 'workspace', 'fill');
}
}
/** @type {SVGElement} */
@@ -686,7 +688,6 @@ Blockly.WorkspaceSvg.prototype.dispose = function() {
if (this.currentGesture_) {
this.currentGesture_.cancel();
}
Blockly.WorkspaceSvg.superClass_.dispose.call(this);
if (this.svgGroup_) {
Blockly.utils.dom.removeNode(this.svgGroup_);
this.svgGroup_ = null;
@@ -732,6 +733,11 @@ Blockly.WorkspaceSvg.prototype.dispose = function() {
this.grid_ = null;
}
if (this.themeManager_) {
this.themeManager_.unsubscribe(this.svgBackground_);
}
Blockly.WorkspaceSvg.superClass_.dispose.call(this);
this.connectionDBList = null;
this.toolboxCategoryCallbacks_ = null;
@@ -2508,7 +2514,7 @@ Blockly.WorkspaceSvg.prototype.cancelCurrentGesture = function() {
/**
* Get the audio manager for this workspace.
* @return {Blockly.WorkspaceAudio} The audio manager for this workspace.
* @return {!Blockly.WorkspaceAudio} The audio manager for this workspace.
*/
Blockly.WorkspaceSvg.prototype.getAudioManager = function() {
return this.audioManager_;
@@ -2522,3 +2528,18 @@ Blockly.WorkspaceSvg.prototype.getAudioManager = function() {
Blockly.WorkspaceSvg.prototype.getGrid = function() {
return this.grid_;
};
/**
* Refresh all blocks on the workspace, toolbox and flyout after a theme update.
* @package
* @override
*/
Blockly.WorkspaceSvg.prototype.refreshTheme = function() {
Blockly.WorkspaceSvg.superClass_.refreshTheme.call(this);
// Update current toolbox selection.
this.refreshToolboxSelection();
if (this.toolbox_) {
this.toolbox_.updateColourFromTheme();
}
};

View File

@@ -90,7 +90,7 @@
function setRandomStyle() {
var blocks = workspace.getAllBlocks();
var styles = Object.keys(Blockly.getTheme().getAllBlockStyles());
var styles = Object.keys(workspace.getTheme().getAllBlockStyles());
styles.splice(styles.indexOf(blocks[0].getStyleName()), 1);
var style = styles[Math.floor(Math.random() * styles.length)];
for(var i = 0, block; block = blocks[i]; i++) {

View File

@@ -267,7 +267,7 @@ function test_set_style() {
};
}
};
mockControl_ = setUpMockMethod(Blockly, 'getTheme', null, [styleStub]);
mockControl_ = setUpMockMethod(workspace, 'getTheme', null, [styleStub]);
var blockA = workspace.newBlock('row_block');
blockA.setStyle('styleOne');
@@ -285,7 +285,7 @@ function test_set_style_throw_exception() {
return null;
}
};
mockControl_ = setUpMockMethod(Blockly, 'getTheme', null, [styleStub]);
mockControl_ = setUpMockMethod(workspace, 'getTheme', null, [styleStub]);
var blockA = workspace.newBlock('row_block');
try {
blockA.setStyle('styleOne');

View File

@@ -131,10 +131,10 @@ function test_setTheme() {
var mockControl_ = setUpMockMethod(Blockly, 'getMainWorkspace', null, [workspace]);
Blockly.setTheme(blockStyles);
workspace.setTheme(blockStyles);
//Checks that the theme was set correctly on Blockly namespace
stringifyAndCompare(Blockly.getTheme(), blockStyles);
stringifyAndCompare(workspace.getTheme(), blockStyles);
//Checks that the setTheme function was called on the block
assertEquals(blockA.getStyleName(), 'styleTwo');

View File

@@ -23,12 +23,12 @@ goog.require('Blockly.Msg');
suite('Procedures', function() {
setup(function() {
Blockly.setTheme(new Blockly.Theme({
this.workspace = new Blockly.Workspace();
this.workspace.setTheme(new Blockly.Theme({
"procedure_blocks": {
"colourPrimary": "290"
}
}));
this.workspace = new Blockly.Workspace();
this.callForAllTypes = function(func, startName) {
var typesArray = [
@@ -252,7 +252,9 @@ suite('Procedures', function() {
});
test('Multiple Workspaces', function() {
this.callForAllTypes(function() {
var workspace = new Blockly.Workspace();
var workspace = new Blockly.Workspace({
theme: this.workspace.getTheme()
});
var def2 = new Blockly.Block(workspace, this.defType);
def2.setFieldValue('name', 'NAME');
var caller2 = new Blockly.Block(workspace, this.callType);
@@ -294,7 +296,9 @@ suite('Procedures', function() {
});
test('Multiple Workspaces', function() {
this.callForAllTypes(function() {
var workspace = new Blockly.Workspace();
var workspace = new Blockly.Workspace({
theme: this.workspace.getTheme()
});
var def2 = new Blockly.Block(workspace, this.defType);
def2.setFieldValue('name', 'NAME');
var caller2 = new Blockly.Block(workspace, this.callType);
@@ -321,8 +325,10 @@ suite('Procedures', function() {
});
suite('Composition', function() {
suite('Statements', function() {
function setStatementValue(defBlock, value) {
var mutatorWorkspace = new Blockly.Workspace();
function setStatementValue(mainWorkspace, defBlock, value) {
var mutatorWorkspace = new Blockly.Workspace({
parentWorkspace: mainWorkspace
});
defBlock.decompose(mutatorWorkspace);
var containerBlock = mutatorWorkspace.getTopBlocks()[0];
var statementField = containerBlock.getField('STATEMENTS');
@@ -331,12 +337,12 @@ suite('Procedures', function() {
}
test('Has Statements', function() {
var defBlock = new Blockly.Block(this.workspace, 'procedures_defreturn');
setStatementValue(defBlock, true);
setStatementValue(this.workspace, defBlock, true);
chai.assert.isTrue(defBlock.hasStatements_);
});
test('Has No Statements', function() {
var defBlock = new Blockly.Block(this.workspace, 'procedures_defreturn');
setStatementValue(defBlock, false);
setStatementValue(this.workspace, defBlock, false);
chai.assert.isFalse(defBlock.hasStatements_);
});
test('Saving Statements', function() {
@@ -348,9 +354,9 @@ suite('Procedures', function() {
'</block>'
);
var defBlock = Blockly.Xml.domToBlock(blockXml, this.workspace);
setStatementValue(defBlock, false);
setStatementValue(this.workspace, defBlock, false);
chai.assert.isNull(defBlock.getInput('STACK'));
setStatementValue(defBlock, true);
setStatementValue(this.workspace, defBlock, true);
chai.assert.isNotNull(defBlock.getInput('STACK'));
var statementBlocks = defBlock.getChildren();
chai.assert.equal(statementBlocks.length, 1);
@@ -361,7 +367,9 @@ suite('Procedures', function() {
});
suite('Untyped Arguments', function() {
function createMutator(argArray) {
this.mutatorWorkspace = new Blockly.Workspace();
this.mutatorWorkspace = new Blockly.Workspace({
parentWorkspace: this.workspace
});
this.containerBlock = this.defBlock.decompose(this.mutatorWorkspace);
this.connection = this.containerBlock.getInput('STACK').connection;
for (var i = 0; i < argArray.length; i++) {
@@ -496,7 +504,9 @@ suite('Procedures', function() {
suite('Statements', function() {
test('Has Statement Input', function() {
this.callForAllTypes(function() {
var mutatorWorkspace = new Blockly.Workspace();
var mutatorWorkspace = new Blockly.Workspace({
parentWorkspace: this.workspace
});
this.defBlock.decompose(mutatorWorkspace);
var statementInput = mutatorWorkspace.getTopBlocks()[0]
.getInput('STATEMENT_INPUT');
@@ -510,7 +520,9 @@ suite('Procedures', function() {
test('Has Statements', function() {
var defBlock = new Blockly.Block(this.workspace, 'procedures_defreturn');
defBlock.hasStatements_ = true;
var mutatorWorkspace = new Blockly.Workspace();
var mutatorWorkspace = new Blockly.Workspace({
parentWorkspace: this.workspace
});
defBlock.decompose(mutatorWorkspace);
var statementValue = mutatorWorkspace.getTopBlocks()[0]
.getField('STATEMENTS').getValueBoolean();
@@ -519,7 +531,9 @@ suite('Procedures', function() {
test('No Has Statements', function() {
var defBlock = new Blockly.Block(this.workspace, 'procedures_defreturn');
defBlock.hasStatements_ = false;
var mutatorWorkspace = new Blockly.Workspace();
var mutatorWorkspace = new Blockly.Workspace({
parentWorkspace: this.workspace
});
defBlock.decompose(mutatorWorkspace);
var statementValue = mutatorWorkspace.getTopBlocks()[0]
.getField('STATEMENTS').getValueBoolean();
@@ -529,7 +543,9 @@ suite('Procedures', function() {
suite('Untyped Arguments', function() {
function assertArguments(argumentsArray) {
this.defBlock.arguments_ = argumentsArray;
var mutatorWorkspace = new Blockly.Workspace();
var mutatorWorkspace = new Blockly.Workspace({
parentWorkspace: this.workspace
});
this.defBlock.decompose(mutatorWorkspace);
var argBlocks = mutatorWorkspace.getBlocksByType('procedures_mutatorarg');
chai.assert.equal(argBlocks.length, argumentsArray.length);

View File

@@ -137,10 +137,10 @@ suite('Theme', function() {
var stub = sinon.stub(Blockly, "getMainWorkspace").returns(workspace);
Blockly.setTheme(blockStyles);
workspace.setTheme(blockStyles);
// Checks that the theme was set correctly on Blockly namespace
stringifyAndCompare(Blockly.getTheme(), blockStyles);
stringifyAndCompare(workspace.getTheme(), blockStyles);
// Checks that the setTheme function was called on the block
assertEquals(blockA.getStyleName(), 'styleTwo');

View File

@@ -19,6 +19,7 @@
*/
suite("Trashcan", function() {
var themeManager = new Blockly.ThemeManager(Blockly.Themes.Classic);
var workspace = {
addChangeListener: function(func) {
this.listener = func;
@@ -26,6 +27,9 @@ suite("Trashcan", function() {
triggerListener: function(event) {
this.listener(event);
},
getThemeManager: function() {
return themeManager;
},
options: {
maxTrashcanContents: Infinity
}

View File

@@ -24,12 +24,12 @@ goog.require('Blockly.Msg');
suite('Procedures XML', function() {
suite('Deserialization', function() {
setup(function() {
Blockly.setTheme(new Blockly.Theme({
this.workspace = new Blockly.Workspace();
this.workspace.setTheme(new Blockly.Theme({
"procedure_blocks": {
"colourPrimary": "290"
}
}));
this.workspace = new Blockly.Workspace();
this.callForAllTypes = function(func) {
var typesArray = [

View File

@@ -185,7 +185,7 @@ function addToolboxButtonCallbacks() {
};
var setRandomStyle = function(button) {
var blocks = button.workspace_.getAllBlocks();
var styles = Object.keys(Blockly.getTheme().getAllBlockStyles());
var styles = Object.keys(workspace.getTheme().getAllBlockStyles());
styles.splice(styles.indexOf(blocks[0].getStyleName()), 1);
var style = styles[Math.floor(Math.random() * styles.length)];
for(var i = 0, block; block = blocks[i]; i++) {
@@ -314,11 +314,13 @@ function addRenderDebugOptionsCheckboxes() {
function changeTheme() {
var theme = document.getElementById('themeChanger');
if (theme.value === "modern") {
Blockly.setTheme(Blockly.Themes.Modern);
Blockly.getMainWorkspace().setTheme(Blockly.Themes.Modern);
} else if (theme.value === "dark") {
Blockly.getMainWorkspace().setTheme(Blockly.Themes.Dark);
} else if (theme.value === "high_contrast") {
Blockly.setTheme(Blockly.Themes.HighContrast);
Blockly.getMainWorkspace().setTheme(Blockly.Themes.HighContrast);
} else {
Blockly.setTheme(Blockly.Themes.Classic);
Blockly.getMainWorkspace().setTheme(Blockly.Themes.Classic);
}
}
@@ -564,6 +566,7 @@ var spaghettiXml = [
<select id="themeChanger" name="theme" onchange="changeTheme()">
<option value="classic">Classic</option>
<option value="modern">Modern</option>
<option value="dark">Dark</option>
<option value="high_contrast">High Contrast</option>
</select>
<p>