summaryrefslogtreecommitdiff
path: root/blockly/demos/blocklyfactory/workspacefactory/wfactory_controller.js
diff options
context:
space:
mode:
Diffstat (limited to 'blockly/demos/blocklyfactory/workspacefactory/wfactory_controller.js')
-rw-r--r--blockly/demos/blocklyfactory/workspacefactory/wfactory_controller.js1344
1 files changed, 1344 insertions, 0 deletions
diff --git a/blockly/demos/blocklyfactory/workspacefactory/wfactory_controller.js b/blockly/demos/blocklyfactory/workspacefactory/wfactory_controller.js
new file mode 100644
index 0000000..faf3c09
--- /dev/null
+++ b/blockly/demos/blocklyfactory/workspacefactory/wfactory_controller.js
@@ -0,0 +1,1344 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2016 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 Contains the controller code for workspace factory. Depends
+ * on the model and view objects (created as internal variables) and interacts
+ * with previewWorkspace and toolboxWorkspace (internal references stored to
+ * both). Also depends on standard_categories.js for standard Blockly
+ * categories. Provides the functionality for the actions the user can initiate:
+ * - adding and removing categories
+ * - switching between categories
+ * - printing and downloading configuration xml
+ * - updating the preview workspace
+ * - changing a category name
+ * - moving the position of a category.
+ *
+ * @author Emma Dauterman (evd2014)
+ */
+
+ goog.require('FactoryUtils');
+ goog.require('StandardCategories');
+
+/**
+ * Class for a WorkspaceFactoryController
+ * @constructor
+ *
+ * @param {!string} toolboxName Name of workspace toolbox XML.
+ * @param {!string} toolboxDiv Name of div to inject toolbox workspace in.
+ * @param {!string} previewDiv Name of div to inject preview workspace in.
+ */
+WorkspaceFactoryController = function(toolboxName, toolboxDiv, previewDiv) {
+ // Toolbox XML element for the editing workspace.
+ this.toolbox = document.getElementById(toolboxName);
+
+ // Workspace for user to drag blocks in for a certain category.
+ this.toolboxWorkspace = Blockly.inject(toolboxDiv,
+ {grid:
+ {spacing: 25,
+ length: 3,
+ colour: '#ccc',
+ snap: true},
+ media: '../../media/',
+ toolbox: this.toolbox
+ });
+
+ // Workspace for user to preview their changes.
+ this.previewWorkspace = Blockly.inject(previewDiv,
+ {grid:
+ {spacing: 25,
+ length: 3,
+ colour: '#ccc',
+ snap: true},
+ media: '../../media/',
+ toolbox: '<xml></xml>',
+ zoom:
+ {controls: true,
+ wheel: true}
+ });
+
+ // Model to keep track of categories and blocks.
+ this.model = new WorkspaceFactoryModel();
+ // Updates the category tabs.
+ this.view = new WorkspaceFactoryView();
+ // Generates XML for categories.
+ this.generator = new WorkspaceFactoryGenerator(this.model);
+ // Tracks which editing mode the user is in. Toolbox mode on start.
+ this.selectedMode = WorkspaceFactoryController.MODE_TOOLBOX;
+ // True if key events are enabled, false otherwise.
+ this.keyEventsEnabled = true;
+ // True if there are unsaved changes in the toolbox, false otherwise.
+ this.hasUnsavedToolboxChanges = false;
+ // True if there are unsaved changes in the preloaded blocks, false otherwise.
+ this.hasUnsavedPreloadChanges = false;
+};
+
+// Toolbox editing mode. Changes the user makes to the workspace updates the
+// toolbox.
+WorkspaceFactoryController.MODE_TOOLBOX = 'toolbox';
+// Pre-loaded workspace editing mode. Changes the user makes to the workspace
+// udpates the pre-loaded blocks.
+WorkspaceFactoryController.MODE_PRELOAD = 'preload';
+
+/**
+ * Currently prompts the user for a name, checking that it's valid (not used
+ * before), and then creates a tab and switches to it.
+ */
+WorkspaceFactoryController.prototype.addCategory = function() {
+ // Transfers the user's blocks to a flyout if it's the first category created.
+ this.transferFlyoutBlocksToCategory();
+
+ // After possibly creating a category, check again if it's the first category.
+ var isFirstCategory = !this.model.hasElements();
+ // Get name from user.
+ name = this.promptForNewCategoryName('Enter the name of your new category: ');
+ if (!name) { //Exit if cancelled.
+ return;
+ }
+ // Create category.
+ this.createCategory(name);
+ // Switch to category.
+ this.switchElement(this.model.getCategoryIdByName(name));
+
+ // Sets the default options for injecting the workspace
+ // when there are categories if adding the first category.
+ if (isFirstCategory) {
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ }
+ // Update preview.
+ this.updatePreview();
+};
+
+/**
+ * Helper method for addCategory. Adds a category to the view given a name, ID,
+ * and a boolean for if it's the first category created. Assumes the category
+ * has already been created in the model. Does not switch to category.
+ *
+ * @param {!string} name Name of category being added.
+ * @param {!string} id The ID of the category being added.
+ */
+WorkspaceFactoryController.prototype.createCategory = function(name) {
+ // Create empty category
+ var category = new ListElement(ListElement.TYPE_CATEGORY, name);
+ this.model.addElementToList(category);
+ // Create new category.
+ var tab = this.view.addCategoryRow(name, category.id);
+ this.addClickToSwitch(tab, category.id);
+};
+
+/**
+ * Given a tab and a ID to be associated to that tab, adds a listener to
+ * that tab so that when the user clicks on the tab, it switches to the
+ * element associated with that ID.
+ *
+ * @param {!Element} tab The DOM element to add the listener to.
+ * @param {!string} id The ID of the element to switch to when tab is clicked.
+ */
+WorkspaceFactoryController.prototype.addClickToSwitch = function(tab, id) {
+ var self = this;
+ var clickFunction = function(id) { // Keep this in scope for switchElement
+ return function() {
+ self.switchElement(id);
+ };
+ };
+ this.view.bindClick(tab, clickFunction(id));
+};
+
+/**
+ * Transfers the blocks in the user's flyout to a new category if
+ * the user is creating their first category and their workspace is not
+ * empty. Should be called whenever it is possible to switch from single flyout
+ * to categories (not including importing).
+ */
+WorkspaceFactoryController.prototype.transferFlyoutBlocksToCategory =
+ function() {
+ // Saves the user's blocks from the flyout in a category if there is no
+ // toolbox and the user has dragged in blocks.
+ if (!this.model.hasElements() &&
+ this.toolboxWorkspace.getAllBlocks().length > 0) {
+ // Create the new category.
+ this.createCategory('Category 1', true);
+ // Set the new category as selected.
+ var id = this.model.getCategoryIdByName('Category 1');
+ this.model.setSelectedById(id);
+ this.view.setCategoryTabSelection(id, true);
+ // Allow user to use the default options for injecting with categories.
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ // Update preview here in case exit early.
+ this.updatePreview();
+ }
+};
+
+/**
+ * Attached to "-" button. Checks if the user wants to delete
+ * the current element. Removes the element and switches to another element.
+ * When the last element is removed, it switches to a single flyout mode.
+ *
+ */
+WorkspaceFactoryController.prototype.removeElement = function() {
+ // Check that there is a currently selected category to remove.
+ if (!this.model.getSelected()) {
+ return;
+ }
+
+ // Check if user wants to remove current category.
+ var check = confirm('Are you sure you want to delete the currently selected '
+ + this.model.getSelected().type + '?');
+ if (!check) { // If cancelled, exit.
+ return;
+ }
+
+ var selectedId = this.model.getSelectedId();
+ var selectedIndex = this.model.getIndexByElementId(selectedId);
+ // Delete element visually.
+ this.view.deleteElementRow(selectedId, selectedIndex);
+ // Delete element in model.
+ this.model.deleteElementFromList(selectedIndex);
+
+ // Find next logical element to switch to.
+ var next = this.model.getElementByIndex(selectedIndex);
+ if (!next && this.model.hasElements()) {
+ next = this.model.getElementByIndex(selectedIndex - 1);
+ }
+ var nextId = next ? next.id : null;
+
+ // Open next element.
+ this.clearAndLoadElement(nextId);
+
+ // If no element to switch to, display message, clear the workspace, and
+ // set a default selected element not in toolbox list in the model.
+ if (!nextId) {
+ alert('You currently have no categories or separators. All your blocks' +
+ ' will be displayed in a single flyout.');
+ this.toolboxWorkspace.clear();
+ this.toolboxWorkspace.clearUndo();
+ this.model.createDefaultSelectedIfEmpty();
+ }
+ // Update preview.
+ this.updatePreview();
+};
+
+/**
+ * Gets a valid name for a new category from the user.
+ *
+ * @param {!string} promptString Prompt for the user to enter a name.
+ * @return {string} Valid name for a new category, or null if cancelled.
+ */
+WorkspaceFactoryController.prototype.promptForNewCategoryName =
+ function(promptString) {
+ do {
+ var name = prompt(promptString);
+ if (!name) { // If cancelled.
+ return null;
+ }
+ } while (this.model.hasCategoryByName(name));
+ return name;
+}
+
+/**
+ * Switches to a new tab for the element given by ID. Stores XML and blocks
+ * to reload later, updates selected accordingly, and clears the workspace
+ * and clears undo, then loads the new element.
+ *
+ * @param {!string} id ID of tab to be opened, must be valid element ID.
+ */
+WorkspaceFactoryController.prototype.switchElement = function(id) {
+ // Disables events while switching so that Blockly delete and create events
+ // don't update the preview repeatedly.
+ Blockly.Events.disable();
+ // Caches information to reload or generate xml if switching to/from element.
+ // Only saves if a category is selected.
+ if (this.model.getSelectedId() != null && id != null) {
+ this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
+ }
+ // Load element.
+ this.clearAndLoadElement(id);
+ // Enable Blockly events again.
+ Blockly.Events.enable();
+};
+
+/**
+ * Switches to a new tab for the element by ID. Helper for switchElement.
+ * Updates selected, clears the workspace and clears undo, loads a new element.
+ *
+ * @param {!string} id ID of category to load
+ */
+WorkspaceFactoryController.prototype.clearAndLoadElement = function(id) {
+ // Unselect current tab if switching to and from an element.
+ if (this.model.getSelectedId() != null && id != null) {
+ this.view.setCategoryTabSelection(this.model.getSelectedId(), false);
+ }
+
+ // If switching to another category, set category selection in the model and
+ // view.
+ if (id != null) {
+ // Set next category.
+ this.model.setSelectedById(id);
+
+ // Clears workspace and loads next category.
+ this.clearAndLoadXml_(this.model.getSelectedXml());
+
+ // Selects the next tab.
+ this.view.setCategoryTabSelection(id, true);
+
+ // Order blocks as shown in flyout.
+ this.toolboxWorkspace.cleanUp();
+
+ // Update category editing buttons.
+ this.view.updateState(this.model.getIndexByElementId
+ (this.model.getSelectedId()), this.model.getSelected());
+ } else {
+ // Update category editing buttons for no categories.
+ this.view.updateState(-1, null);
+ }
+};
+
+/**
+ * Tied to "Export" button. Gets a file name from the user and downloads
+ * the corresponding configuration xml to that file.
+ *
+ * @param {!string} exportMode The type of file to export
+ * (WorkspaceFactoryController.MODE_TOOLBOX for the toolbox configuration,
+ * and WorkspaceFactoryController.MODE_PRELOAD for the pre-loaded workspace
+ * configuration)
+ */
+WorkspaceFactoryController.prototype.exportXmlFile = function(exportMode) {
+ // Get file name.
+ var fileName = prompt('File Name for ' + (exportMode ==
+ WorkspaceFactoryController.MODE_TOOLBOX ? 'toolbox XML: ' :
+ 'pre-loaded workspace XML: '));
+ if (!fileName) { // If cancelled
+ return;
+ }
+
+ // Generate XML.
+ if (exportMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // Export the toolbox XML.
+
+ var configXml = Blockly.Xml.domToPrettyText
+ (this.generator.generateToolboxXml());
+ this.hasUnsavedToolboxChanges = false;
+ } else if (exportMode == WorkspaceFactoryController.MODE_PRELOAD) {
+ // Export the pre-loaded block XML.
+
+ var configXml = Blockly.Xml.domToPrettyText
+ (this.generator.generateWorkspaceXml());
+ this.hasUnsavedPreloadChanges = false;
+ } else {
+ // Unknown mode. Throw error.
+ throw new Error ("Unknown export mode: " + exportMode);
+ }
+
+ // Download file.
+ var data = new Blob([configXml], {type: 'text/xml'});
+ this.view.createAndDownloadFile(fileName, data);
+ };
+
+/**
+ * Export the options object to be used for the Blockly inject call. Gets a
+ * file name from the user and downloads the options object to that file.
+ */
+WorkspaceFactoryController.prototype.exportInjectFile = function() {
+ var fileName = prompt('File Name for starter Blockly workspace code: ');
+ if (!fileName) { // If cancelled.
+ return;
+ }
+ // Generate new options to remove toolbox XML from options object (if
+ // necessary).
+ this.generateNewOptions();
+ var printableOptions = this.generator.generateInjectString()
+ var data = new Blob([printableOptions], {type: 'text/javascript'});
+ this.view.createAndDownloadFile(fileName, data);
+};
+
+/**
+ * Tied to "Print" button. Mainly used for debugging purposes. Prints
+ * the configuration XML to the console.
+ */
+WorkspaceFactoryController.prototype.printConfig = function() {
+ // Capture any changes made by user before generating XML.
+ this.saveStateFromWorkspace();
+ // Print XML.
+ window.console.log(Blockly.Xml.domToPrettyText
+ (this.generator.generateToolboxXml()));
+};
+
+/**
+ * Updates the preview workspace based on the toolbox workspace. If switching
+ * from no categories to categories or categories to no categories, reinjects
+ * Blockly with reinjectPreview, otherwise just updates without reinjecting.
+ * Called whenever a list element is created, removed, or modified and when
+ * Blockly move and delete events are fired. Do not call on create events
+ * or disabling will cause the user to "drop" their current blocks. Make sure
+ * that no changes have been made to the workspace since updating the model
+ * (if this might be the case, call saveStateFromWorkspace).
+ */
+WorkspaceFactoryController.prototype.updatePreview = function() {
+ // Disable events to stop updatePreview from recursively calling itself
+ // through event handlers.
+ Blockly.Events.disable();
+
+ // Only update the toolbox if not in read only mode.
+ if (!this.model.options['readOnly']) {
+ // Get toolbox XML.
+ var tree = Blockly.Options.parseToolboxTree
+ (this.generator.generateToolboxXml());
+
+ // No categories, creates a simple flyout.
+ if (tree.getElementsByTagName('category').length == 0) {
+ // No categories, creates a simple flyout.
+ if (this.previewWorkspace.toolbox_) {
+ this.reinjectPreview(tree); // Switch to simple flyout, expensive.
+ } else {
+ this.previewWorkspace.updateToolbox(tree);
+ }
+ } else {
+ // Uses categories, creates a toolbox.
+ if (!this.previewWorkspace.toolbox_) {
+ this.reinjectPreview(tree); // Create a toolbox, expensive.
+ } else {
+ // Close the toolbox before updating it so that the user has to reopen
+ // the flyout and see their updated toolbox (open flyout doesn't update)
+ this.previewWorkspace.toolbox_.clearSelection();
+ this.previewWorkspace.updateToolbox(tree);
+ }
+ }
+ }
+
+ // Update pre-loaded blocks in the preview workspace.
+ this.previewWorkspace.clear();
+ Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(),
+ this.previewWorkspace);
+
+ // Reenable events.
+ Blockly.Events.enable();
+};
+
+/**
+ * Saves the state from the workspace depending on the current mode. Should
+ * be called after making changes to the workspace.
+ */
+WorkspaceFactoryController.prototype.saveStateFromWorkspace = function() {
+ if (this.selectedMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // If currently editing the toolbox.
+ // Update flags if toolbox has been changed.
+ if (this.model.getSelectedXml() !=
+ Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) {
+ this.hasUnsavedToolboxChanges = true;
+ }
+
+ this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
+
+ } else if (this.selectedMode == WorkspaceFactoryController.MODE_PRELOAD) {
+ // If currently editing the pre-loaded workspace.
+ // Update flags if preloaded blocks have been changed.
+ if (this.model.getPreloadXml() !=
+ Blockly.Xml.workspaceToDom(this.toolboxWorkspace)) {
+ this.hasUnsavedPreloadChanges = true;
+ }
+
+ this.model.savePreloadXml
+ (Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
+ }
+};
+
+/**
+ * Used to completely reinject the preview workspace. This should be used only
+ * when switching from simple flyout to categories, or categories to simple
+ * flyout. More expensive than simply updating the flyout or toolbox.
+ *
+ * @param {!Element} tree of xml elements
+ * @package
+ */
+WorkspaceFactoryController.prototype.reinjectPreview = function(tree) {
+ this.previewWorkspace.dispose();
+ var injectOptions = this.readOptions_();
+ injectOptions['toolbox'] = Blockly.Xml.domToPrettyText(tree);
+ this.previewWorkspace = Blockly.inject('preview_blocks', injectOptions);
+ Blockly.Xml.domToWorkspace(this.generator.generateWorkspaceXml(),
+ this.previewWorkspace);
+};
+
+/**
+ * Tied to "change name" button. Changes the name of the selected category.
+ * Continues prompting the user until they input a category name that is not
+ * currently in use, exits if user presses cancel.
+ */
+WorkspaceFactoryController.prototype.changeCategoryName = function() {
+ // Return if a category is not selected.
+ if (this.model.getSelected().type != ListElement.TYPE_CATEGORY) {
+ return;
+ }
+ // Get new name from user.
+ var newName = this.promptForNewCategoryName('What do you want to change this'
+ + ' category\'s name to?');
+ if (!newName) { // If cancelled.
+ return;
+ }
+ // Change category name.
+ this.model.getSelected().changeName(newName);
+ this.view.updateCategoryName(newName, this.model.getSelectedId());
+ // Update preview.
+ this.updatePreview();
+};
+
+/**
+ * Tied to arrow up and arrow down buttons. Swaps with the element above or
+ * below the currently selected element (offset categories away from the
+ * current element). Updates state to enable the correct element editing
+ * buttons.
+ *
+ * @param {int} offset The index offset from the currently selected element
+ * to swap with. Positive if the element to be swapped with is below, negative
+ * if the element to be swapped with is above.
+ */
+WorkspaceFactoryController.prototype.moveElement = function(offset) {
+ var curr = this.model.getSelected();
+ if (!curr) { // Return if no selected element.
+ return;
+ }
+ var currIndex = this.model.getIndexByElementId(curr.id);
+ var swapIndex = this.model.getIndexByElementId(curr.id) + offset;
+ var swap = this.model.getElementByIndex(swapIndex);
+ if (!swap) { // Return if cannot swap in that direction.
+ return;
+ }
+ // Move currently selected element to index of other element.
+ // Indexes must be valid because confirmed that curr and swap exist.
+ this.moveElementToIndex(curr, swapIndex, currIndex);
+ // Update element editing buttons.
+ this.view.updateState(swapIndex, this.model.getSelected());
+ // Update preview.
+ this.updatePreview();
+};
+
+/**
+ * Moves a element to a specified index and updates the model and view
+ * accordingly. Helper functions throw an error if indexes are out of bounds.
+ *
+ * @param {!Element} element The element to move.
+ * @param {int} newIndex The index to insert the element at.
+ * @param {int} oldIndex The index the element is currently at.
+ */
+WorkspaceFactoryController.prototype.moveElementToIndex = function(element,
+ newIndex, oldIndex) {
+ this.model.moveElementToIndex(element, newIndex, oldIndex);
+ this.view.moveTabToIndex(element.id, newIndex, oldIndex);
+};
+
+/**
+ * Changes the color of the selected category. Return if selected element is
+ * a separator.
+ *
+ * @param {!string} color The color to change the selected category. Must be
+ * a valid CSS string.
+ */
+WorkspaceFactoryController.prototype.changeSelectedCategoryColor =
+ function(color) {
+ // Return if category is not selected.
+ if (this.model.getSelected().type != ListElement.TYPE_CATEGORY) {
+ return;
+ }
+ // Change color of selected category.
+ this.model.getSelected().changeColor(color);
+ this.view.setBorderColor(this.model.getSelectedId(), color);
+ this.updatePreview();
+};
+
+/**
+ * Tied to the "Standard Category" dropdown option, this function prompts
+ * the user for a name of a standard Blockly category (case insensitive) and
+ * loads it as a new category and switches to it. Leverages StandardCategories.
+ */
+WorkspaceFactoryController.prototype.loadCategory = function() {
+ // Prompt user for the name of the standard category to load.
+ do {
+ var name = prompt('Enter the name of the category you would like to import '
+ + '(Logic, Loops, Math, Text, Lists, Colour, Variables, or Functions)');
+ if (!name) {
+ return; // Exit if cancelled.
+ }
+ } while (!this.isStandardCategoryName(name));
+
+ // Load category.
+ this.loadCategoryByName(name);
+};
+
+/**
+ * Loads a Standard Category by name and switches to it. Leverages
+ * StandardCategories. Returns if cannot load standard category.
+ *
+ * @param {string} name Name of the standard category to load.
+ */
+WorkspaceFactoryController.prototype.loadCategoryByName = function(name) {
+ // Check if the user can load that standard category.
+ if (!this.isStandardCategoryName(name)) {
+ return;
+ }
+ if (this.model.hasVariables() && name.toLowerCase() == 'variables') {
+ alert('A Variables category already exists. You cannot create multiple' +
+ ' variables categories.');
+ return;
+ }
+ if (this.model.hasProcedures() && name.toLowerCase() == 'functions') {
+ alert('A Functions category already exists. You cannot create multiple' +
+ ' functions categories.');
+ return;
+ }
+ // Check if the user can create a category with that name.
+ var standardCategory = StandardCategories.categoryMap[name.toLowerCase()]
+ if (this.model.hasCategoryByName(standardCategory.name)) {
+ alert('You already have a category with the name ' + standardCategory.name
+ + '. Rename your category and try again.');
+ return;
+ }
+ // Transfers current flyout blocks to a category if it's the first category
+ // created.
+ this.transferFlyoutBlocksToCategory();
+
+ var isFirstCategory = !this.model.hasElements();
+ // Copy the standard category in the model.
+ var copy = standardCategory.copy();
+
+ // Add it to the model.
+ this.model.addElementToList(copy);
+
+ // Update the copy in the view.
+ var tab = this.view.addCategoryRow(copy.name, copy.id);
+ this.addClickToSwitch(tab, copy.id);
+ // Color the category tab in the view.
+ if (copy.color) {
+ this.view.setBorderColor(copy.id, copy.color);
+ }
+ // Switch to loaded category.
+ this.switchElement(copy.id);
+ // Convert actual shadow blocks to user-generated shadow blocks.
+ this.convertShadowBlocks();
+ // Save state from workspace before updating preview.
+ this.saveStateFromWorkspace();
+ if (isFirstCategory) {
+ // Allow the user to use the default options for injecting the workspace
+ // when there are categories.
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ }
+ // Update preview.
+ this.updatePreview();
+};
+
+/**
+ * Loads the standard Blockly toolbox into the editing space. Should only
+ * be called when the mode is set to toolbox.
+ */
+WorkspaceFactoryController.prototype.loadStandardToolbox = function() {
+ this.loadCategoryByName('Logic');
+ this.loadCategoryByName('Loops');
+ this.loadCategoryByName('Math');
+ this.loadCategoryByName('Text');
+ this.loadCategoryByName('Lists');
+ this.loadCategoryByName('Colour');
+ this.addSeparator();
+ this.loadCategoryByName('Variables');
+ this.loadCategoryByName('Functions');
+}
+
+/**
+ * Given the name of a category, determines if it's the name of a standard
+ * category (case insensitive).
+ *
+ * @param {string} name The name of the category that should be checked if it's
+ * in StandardCategories categoryMap
+ * @return {boolean} True if name is a standard category name, false otherwise.
+ */
+WorkspaceFactoryController.prototype.isStandardCategoryName = function(name) {
+ for (var category in StandardCategories.categoryMap) {
+ if (name.toLowerCase() == category) {
+ return true;
+ }
+ }
+ return false;
+};
+
+/**
+ * Connected to the "add separator" dropdown option. If categories already
+ * exist, adds a separator to the model and view. Does not switch to select
+ * the separator, and updates the preview.
+ */
+WorkspaceFactoryController.prototype.addSeparator = function() {
+ // If adding the first element in the toolbox, transfers the user's blocks
+ // in a flyout to a category.
+ this.transferFlyoutBlocksToCategory();
+ // Create the separator in the model.
+ var separator = new ListElement(ListElement.TYPE_SEPARATOR);
+ this.model.addElementToList(separator);
+ // Create the separator in the view.
+ var tab = this.view.addSeparatorTab(separator.id);
+ this.addClickToSwitch(tab, separator.id);
+ // Switch to the separator and update the preview.
+ this.switchElement(separator.id);
+ this.updatePreview();
+};
+
+/**
+ * Connected to the import button. Given the file path inputted by the user
+ * from file input, if the import mode is for the toolbox, this function loads
+ * that toolbox XML to the workspace, creating category and separator tabs as
+ * necessary. If the import mode is for pre-loaded blocks in the workspace,
+ * this function loads that XML to the workspace to be edited further. This
+ * function switches mode to whatever the import mode is. Catches errors from
+ * file reading and prints an error message alerting the user.
+ *
+ * @param {string} file The path for the file to be imported into the workspace.
+ * Should contain valid toolbox XML.
+ * @param {!string} importMode The mode corresponding to the type of file the
+ * user is importing (WorkspaceFactoryController.MODE_TOOLBOX or
+ * WorkspaceFactoryController.MODE_PRELOAD).
+ */
+WorkspaceFactoryController.prototype.importFile = function(file, importMode) {
+ // Exit if cancelled.
+ if (!file) {
+ return;
+ }
+
+ Blockly.Events.disable();
+ var controller = this;
+ var reader = new FileReader();
+
+ // To be executed when the reader has read the file.
+ reader.onload = function() {
+ // Try to parse XML from file and load it into toolbox editing area.
+ // Print error message if fail.
+ try {
+ var tree = Blockly.Xml.textToDom(reader.result);
+ if (importMode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // Switch mode.
+ controller.setMode(WorkspaceFactoryController.MODE_TOOLBOX);
+
+ // Confirm that the user wants to override their current toolbox.
+ var hasToolboxElements = controller.model.hasElements() ||
+ controller.getAllBlocks().length > 0;
+ if (hasToolboxElements &&
+ !confirm('Are you sure you want to import? You will lose your '
+ + 'current toolbox. ')) {
+ return;
+ }
+ // Import toolbox XML.
+ controller.importToolboxFromTree_(tree);
+
+ } else if (importMode == WorkspaceFactoryController.MODE_PRELOAD) {
+ // Switch mode.
+ controller.setMode(WorkspaceFactoryController.MODE_PRELOAD);
+
+ // Confirm that the user wants to override their current blocks.
+ if (controller.toolboxWorkspace.getAllBlocks().length > 0 &&
+ !confirm('Are you sure you want to import? You will lose your '
+ + 'current workspace blocks. ')) {
+ return;
+ }
+
+ // Import pre-loaded workspace XML.
+ controller.importPreloadFromTree_(tree);
+ } else {
+ // Throw error if invalid mode.
+ throw new Error("Unknown import mode: " + importMode);
+ }
+ } catch(e) {
+ alert('Cannot load XML from file.');
+ console.log(e);
+ } finally {
+ Blockly.Events.enable();
+ }
+ }
+
+ // Read the file asynchronously.
+ reader.readAsText(file);
+};
+
+/**
+ * Given a XML DOM tree, loads it into the toolbox editing area so that the
+ * user can continue editing their work. Assumes that tree is in valid toolbox
+ * XML format. Assumes that the mode is MODE_TOOLBOX.
+ * @private
+ *
+ * @param {!Element} tree XML tree to be loaded to toolbox editing area.
+ */
+WorkspaceFactoryController.prototype.importToolboxFromTree_ = function(tree) {
+ // Clear current editing area.
+ this.model.clearToolboxList();
+ this.view.clearToolboxTabs();
+
+ if (tree.getElementsByTagName('category').length == 0) {
+ // No categories present.
+ // Load all the blocks into a single category evenly spaced.
+ Blockly.Xml.domToWorkspace(tree, this.toolboxWorkspace);
+ this.toolboxWorkspace.cleanUp();
+
+ // Convert actual shadow blocks to user-generated shadow blocks.
+ this.convertShadowBlocks();
+
+ // Add message to denote empty category.
+ this.view.addEmptyCategoryMessage();
+
+ } else {
+ // Categories/separators present.
+ for (var i = 0, item; item = tree.children[i]; i++) {
+
+ if (item.tagName == 'category') {
+ // If the element is a category, create a new category and switch to it.
+ this.createCategory(item.getAttribute('name'), false);
+ var category = this.model.getElementByIndex(i);
+ this.switchElement(category.id);
+
+ // Load all blocks in that category to the workspace to be evenly
+ // spaced and saved to that category.
+ for (var j = 0, blockXml; blockXml = item.children[j]; j++) {
+ Blockly.Xml.domToBlock(blockXml, this.toolboxWorkspace);
+ }
+
+ // Evenly space the blocks.
+ this.toolboxWorkspace.cleanUp();
+
+ // Convert actual shadow blocks to user-generated shadow blocks.
+ this.convertShadowBlocks();
+
+ // Set category color.
+ if (item.getAttribute('colour')) {
+ category.changeColor(item.getAttribute('colour'));
+ this.view.setBorderColor(category.id, category.color);
+ }
+ // Set any custom tags.
+ if (item.getAttribute('custom')) {
+ this.model.addCustomTag(category, item.getAttribute('custom'));
+ }
+ } else {
+ // If the element is a separator, add the separator and switch to it.
+ this.addSeparator();
+ this.switchElement(this.model.getElementByIndex(i).id);
+ }
+ }
+ }
+ this.view.updateState(this.model.getIndexByElementId
+ (this.model.getSelectedId()), this.model.getSelected());
+
+ this.saveStateFromWorkspace();
+
+ // Set default configuration options for a single flyout or multiple
+ // categories.
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+
+ this.updatePreview();
+};
+
+/**
+ * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
+ * Assumes that tree is in valid XML format and that the selected mode is
+ * MODE_PRELOAD.
+ *
+ * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
+ * area.
+ */
+WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) {
+ this.clearAndLoadXml_(tree);
+ this.model.savePreloadXml(tree);
+ this.updatePreview();
+}
+
+/**
+ * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
+ * Assumes that tree is in valid XML format and that the selected mode is
+ * MODE_PRELOAD.
+ *
+ * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
+ * area.
+ */
+WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) {
+ this.clearAndLoadXml_(tree);
+ this.model.savePreloadXml(tree);
+ this.saveStateFromWorkspace();
+ this.updatePreview();
+}
+
+/**
+ * Given a XML DOM tree, loads it into the pre-loaded workspace editing area.
+ * Assumes that tree is in valid XML format and that the selected mode is
+ * MODE_PRELOAD.
+ *
+ * @param {!Element} tree XML tree to be loaded to pre-loaded block editing
+ * area.
+ */
+WorkspaceFactoryController.prototype.importPreloadFromTree_ = function(tree) {
+ this.clearAndLoadXml_(tree);
+ this.model.savePreloadXml(tree);
+ this.saveStateFromWorkspace();
+ this.updatePreview();
+}
+
+/**
+ * Clears the editing area completely, deleting all categories and all
+ * blocks in the model and view and all pre-loaded blocks. Tied to the
+ * "Clear" button.
+ */
+WorkspaceFactoryController.prototype.clearAll = function() {
+ if (!confirm('Are you sure you want to clear all of your work in Workspace' +
+ ' Factory?')) {
+ return;
+ }
+ var hasCategories = this.model.hasElements();
+ this.model.clearToolboxList();
+ this.view.clearToolboxTabs();
+ this.model.savePreloadXml(Blockly.Xml.textToDom('<xml></xml>'));
+ this.view.addEmptyCategoryMessage();
+ this.view.updateState(-1, null);
+ this.toolboxWorkspace.clear();
+ this.toolboxWorkspace.clearUndo();
+ this.saveStateFromWorkspace();
+ this.hasUnsavedToolboxChanges = false;
+ this.hasUnsavedPreloadChanges = false;
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ this.updatePreview();
+};
+
+/*
+ * Makes the currently selected block a user-generated shadow block. These
+ * blocks are not made into real shadow blocks, but recorded in the model
+ * and visually marked as shadow blocks, allowing the user to move and edit
+ * them (which would be impossible with actual shadow blocks). Updates the
+ * preview when done.
+ *
+ */
+WorkspaceFactoryController.prototype.addShadow = function() {
+ // No block selected to make a shadow block.
+ if (!Blockly.selected) {
+ return;
+ }
+ // Clear any previous warnings on the block (would only have warnings on
+ // a non-shadow block if it was nested inside another shadow block).
+ Blockly.selected.setWarningText(null);
+ // Set selected block and all children as shadow blocks.
+ this.addShadowForBlockAndChildren_(Blockly.selected);
+
+ // Save and update the preview.
+ this.saveStateFromWorkspace();
+ this.updatePreview();
+};
+
+/**
+ * Sets a block and all of its children to be user-generated shadow blocks,
+ * both in the model and view.
+ * @private
+ *
+ * @param {!Blockly.Block} block The block to be converted to a user-generated
+ * shadow block.
+ */
+WorkspaceFactoryController.prototype.addShadowForBlockAndChildren_ =
+ function(block) {
+ // Convert to shadow block.
+ this.view.markShadowBlock(block);
+ this.model.addShadowBlock(block.id);
+
+ if (FactoryUtils.hasVariableField(block)) {
+ block.setWarningText('Cannot make variable blocks shadow blocks.');
+ }
+
+ // Convert all children to shadow blocks recursively.
+ var children = block.getChildren();
+ for (var i = 0; i < children.length; i++) {
+ this.addShadowForBlockAndChildren_(children[i]);
+ }
+};
+
+/**
+ * If the currently selected block is a user-generated shadow block, this
+ * function makes it a normal block again, removing it from the list of
+ * shadow blocks and loading the workspace again. Updates the preview again.
+ *
+ */
+WorkspaceFactoryController.prototype.removeShadow = function() {
+ // No block selected to modify.
+ if (!Blockly.selected) {
+ return;
+ }
+ this.model.removeShadowBlock(Blockly.selected.id);
+ this.view.unmarkShadowBlock(Blockly.selected);
+
+ // If turning invalid shadow block back to normal block, remove warning.
+ Blockly.selected.setWarningText(null);
+
+ this.saveStateFromWorkspace();
+ this.updatePreview();
+};
+
+/**
+ * Given a unique block ID, uses the model to determine if a block is a
+ * user-generated shadow block.
+ *
+ * @param {!string} blockId The unique ID of the block to examine.
+ * @return {boolean} True if the block is a user-generated shadow block, false
+ * otherwise.
+ */
+WorkspaceFactoryController.prototype.isUserGenShadowBlock = function(blockId) {
+ return this.model.isShadowBlock(blockId);
+}
+
+/**
+ * Call when importing XML containing real shadow blocks. This function turns
+ * all real shadow blocks loaded in the workspace into user-generated shadow
+ * blocks, meaning they are marked as shadow blocks by the model and appear as
+ * shadow blocks in the view but are still editable and movable.
+ */
+WorkspaceFactoryController.prototype.convertShadowBlocks = function() {
+ var blocks = this.toolboxWorkspace.getAllBlocks();
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (block.isShadow()) {
+ block.setShadow(false);
+ // Delete the shadow DOM attached to the block so that the shadow block
+ // does not respawn. Dependent on implementation details.
+ var parentConnection = block.outputConnection ?
+ block.outputConnection.targetConnection :
+ block.previousConnection.targetConnection;
+ if (parentConnection) {
+ parentConnection.setShadowDom(null);
+ }
+ this.model.addShadowBlock(block.id);
+ this.view.markShadowBlock(block);
+ }
+ }
+};
+
+/**
+ * Sets the currently selected mode that determines what the toolbox workspace
+ * is being used to edit. Updates the view and then saves and loads XML
+ * to and from the toolbox and updates the help text.
+ *
+ * @param {!string} tab The type of tab being switched to
+ * (WorkspaceFactoryController.MODE_TOOLBOX or
+ * WorkspaceFactoryController.MODE_PRELOAD).
+ */
+WorkspaceFactoryController.prototype.setMode = function(mode) {
+ // No work to change mode that's currently set.
+ if (this.selectedMode == mode) {
+ return;
+ }
+
+ // No work to change mode that's currently set.
+ if (this.selectedMode == mode) {
+ return;
+ }
+
+ // Set tab selection and display appropriate tab.
+ this.view.setModeSelection(mode);
+
+ // Update selected tab.
+ this.selectedMode = mode;
+
+ // Update help text above workspace.
+ this.view.updateHelpText(mode);
+
+ if (mode == WorkspaceFactoryController.MODE_TOOLBOX) {
+ // Open the toolbox editing space.
+ this.model.savePreloadXml
+ (Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
+ this.clearAndLoadXml_(this.model.getSelectedXml());
+ this.view.disableWorkspace(this.view.shouldDisableWorkspace
+ (this.model.getSelected()));
+ } else {
+ // Open the pre-loaded workspace editing space.
+ if (this.model.getSelected()) {
+ this.model.getSelected().saveFromWorkspace(this.toolboxWorkspace);
+ }
+ this.clearAndLoadXml_(this.model.getPreloadXml());
+ this.view.disableWorkspace(false);
+ }
+};
+
+/**
+ * Clears the toolbox workspace and loads XML to it, marking shadow blocks
+ * as necessary.
+ * @private
+ *
+ * @param {!Element} xml The XML to be loaded to the workspace.
+ */
+WorkspaceFactoryController.prototype.clearAndLoadXml_ = function(xml) {
+ this.toolboxWorkspace.clear();
+ this.toolboxWorkspace.clearUndo();
+ Blockly.Xml.domToWorkspace(xml, this.toolboxWorkspace);
+ this.view.markShadowBlocks(this.model.getShadowBlocksInWorkspace
+ (this.toolboxWorkspace.getAllBlocks()));
+ this.warnForUndefinedBlocks_();
+};
+
+/**
+ * Sets the standard default options for the options object and updates
+ * the preview workspace. The default values depends on if categories are
+ * present.
+ */
+WorkspaceFactoryController.prototype.setStandardOptionsAndUpdate = function() {
+ this.view.setBaseOptions();
+ this.view.setCategoryOptions(this.model.hasElements());
+ this.generateNewOptions();
+ };
+
+/**
+ * Generates a new options object for injecting a Blockly workspace based
+ * on user input. Should be called every time a change has been made to
+ * an input field. Updates the model and reinjects the preview workspace.
+ */
+WorkspaceFactoryController.prototype.generateNewOptions = function() {
+ this.model.setOptions(this.readOptions_());
+
+ this.reinjectPreview(Blockly.Options.parseToolboxTree
+ (this.generator.generateToolboxXml()));
+};
+
+/**
+ * Generates a new options object for injecting a Blockly workspace based on
+ * user input.
+ * @private
+ *
+ * @return {!Object} Blockly injection options object.
+ */
+WorkspaceFactoryController.prototype.readOptions_ = function() {
+ var optionsObj = Object.create(null);
+
+ // Add all standard options to the options object.
+ // Use parse int to get numbers from value inputs.
+ optionsObj['collapse'] =
+ document.getElementById('option_collapse_checkbox').checked;
+ optionsObj['comments'] =
+ document.getElementById('option_comments_checkbox').checked;
+ optionsObj['css'] = document.getElementById('option_css_checkbox').checked;
+ optionsObj['disable'] =
+ document.getElementById('option_disable_checkbox').checked;
+ if (document.getElementById('option_infiniteBlocks_checkbox').checked) {
+ optionsObj['maxBlocks'] = Infinity;
+ } else {
+ var maxBlocksValue =
+ document.getElementById('option_maxBlocks_number').value;
+ optionsObj['maxBlocks'] = typeof maxBlocksValue == 'string' ?
+ parseInt(maxBlocksValue) : maxBlocksValue;
+ }
+ optionsObj['media'] = document.getElementById('option_media_text').value;
+ optionsObj['readOnly'] =
+ document.getElementById('option_readOnly_checkbox').checked;
+ optionsObj['rtl'] = document.getElementById('option_rtl_checkbox').checked;
+ optionsObj['scrollbars'] =
+ document.getElementById('option_scrollbars_checkbox').checked;
+ optionsObj['sounds'] =
+ document.getElementById('option_sounds_checkbox').checked;
+ if (!optionsObj['readOnly']) {
+ optionsObj['trashcan'] =
+ document.getElementById('option_trashcan_checkbox').checked;
+ }
+
+ // If using a grid, add all grid options.
+ if (document.getElementById('option_grid_checkbox').checked) {
+ var grid = Object.create(null);
+ var spacingValue =
+ document.getElementById('gridOption_spacing_number').value;
+ grid['spacing'] = typeof spacingValue == 'string' ?
+ parseInt(spacingValue) : spacingValue;
+ var lengthValue = document.getElementById('gridOption_length_number').value;
+ grid['length'] = typeof lengthValue == 'string' ?
+ parseInt(lengthValue) : lengthValue;
+ grid['colour'] = document.getElementById('gridOption_colour_text').value;
+ grid['snap'] = document.getElementById('gridOption_snap_checkbox').checked;
+ optionsObj['grid'] = grid;
+ }
+
+ // If using zoom, add all zoom options.
+ if (document.getElementById('option_zoom_checkbox').checked) {
+ var zoom = Object.create(null);
+ zoom['controls'] =
+ document.getElementById('zoomOption_controls_checkbox').checked;
+ zoom['wheel'] =
+ document.getElementById('zoomOption_wheel_checkbox').checked;
+ var startScaleValue =
+ document.getElementById('zoomOption_startScale_number').value;
+ zoom['startScale'] = typeof startScaleValue == 'string' ?
+ parseFloat(startScaleValue) : startScaleValue;
+ var maxScaleValue =
+ document.getElementById('zoomOption_maxScale_number').value;
+ zoom['maxcale'] = typeof maxScaleValue == 'string' ?
+ parseFloat(maxScaleValue) : maxScaleValue;
+ var minScaleValue =
+ document.getElementById('zoomOption_minScale_number').value;
+ zoom['minScale'] = typeof minScaleValue == 'string' ?
+ parseFloat(minScaleValue) : minScaleValue;
+ var scaleSpeedValue =
+ document.getElementById('zoomOption_scaleSpeed_number').value;
+ zoom['startScale'] = typeof startScaleValue == 'string' ?
+ parseFloat(scaleSpeedValue) : scaleSpeedValue;
+ optionsObj['zoom'] = zoom;
+ }
+
+ return optionsObj;
+};
+
+/**
+ * Imports blocks from a file, generating a category in the toolbox workspace
+ * to allow the user to use imported blocks in the toolbox and in pre-loaded
+ * blocks.
+ *
+ * @param {!File} file File object for the blocks to import.
+ * @param {!string} format The format of the file to import, either 'JSON' or
+ * 'JavaScript'.
+ */
+WorkspaceFactoryController.prototype.importBlocks =
+ function(file, format) {
+ // Generate category name from file name.
+ var categoryName = file.name;
+
+ var controller = this;
+ var reader = new FileReader();
+
+ // To be executed when the reader has read the file.
+ reader.onload = function() {
+ try {
+ // Define blocks using block types from file.
+ var blockTypes = FactoryUtils.defineAndGetBlockTypes(reader.result,
+ format);
+
+ // If an imported block type is already defined, check if the user wants
+ // to override the current block definition.
+ if (controller.model.hasDefinedBlockTypes(blockTypes) &&
+ !confirm('An imported block uses the same name as a block '
+ + 'already in your toolbox. Are you sure you want to override the '
+ + 'currently defined block?')) {
+ return;
+ }
+
+ var blocks = controller.generator.getDefinedBlocks(blockTypes);
+ // Generate category XML and append to toolbox.
+ var categoryXml = FactoryUtils.generateCategoryXml(blocks, categoryName);
+ // Get random color for category between 0 and 360. Gives each imported
+ // category a different color.
+ var randomColor = Math.floor(Math.random() * 360);
+ categoryXml.setAttribute('colour', randomColor);
+ controller.toolbox.appendChild(categoryXml);
+ controller.toolboxWorkspace.updateToolbox(controller.toolbox);
+ // Update imported block types.
+ controller.model.addImportedBlockTypes(blockTypes);
+ // Reload current category to possibly reflect any newly defined blocks.
+ controller.clearAndLoadXml_
+ (Blockly.Xml.workspaceToDom(controller.toolboxWorkspace));
+ } catch (e) {
+ alert('Cannot read blocks from file.');
+ window.console.log(e);
+ }
+ }
+
+ // Read the file asynchronously.
+ reader.readAsText(file);
+};
+
+/*
+ * Updates the block library category in the toolbox workspace toolbox.
+ *
+ * @param {!Element} categoryXml XML for the block library category.
+ * @param {!Array<!string>} libBlockTypes Array of block types from the block
+ * library.
+ */
+WorkspaceFactoryController.prototype.setBlockLibCategory =
+ function(categoryXml, libBlockTypes) {
+ var blockLibCategory = document.getElementById('blockLibCategory');
+
+ // Set category id so that it can be easily replaced, and set a standard,
+ // arbitrary block library color.
+ categoryXml.setAttribute('id', 'blockLibCategory');
+ categoryXml.setAttribute('colour', 260);
+
+ // Update the toolbox and toolboxWorkspace.
+ this.toolbox.replaceChild(categoryXml, blockLibCategory);
+ this.toolboxWorkspace.toolbox_.clearSelection();
+ this.toolboxWorkspace.updateToolbox(this.toolbox);
+
+ // Update the block library types.
+ this.model.updateLibBlockTypes(libBlockTypes);
+
+ // Reload XML on page to account for blocks now defined or undefined in block
+ // library.
+ this.clearAndLoadXml_(Blockly.Xml.workspaceToDom(this.toolboxWorkspace));
+};
+
+/**
+ * Return the block types used in the custom toolbox and pre-loaded workspace.
+ *
+ * @return {!Array.<!string>} Block types used in the custom toolbox and
+ * pre-loaded workspace.
+ */
+WorkspaceFactoryController.prototype.getAllUsedBlockTypes = function() {
+ return this.model.getAllUsedBlockTypes();
+};
+
+/**
+ * Determines if a block loaded in the workspace has a definition (if it
+ * is a standard block, is defined in the block library, or has a definition
+ * imported).
+ *
+ * @param {!Blockly.Block} block The block to examine.
+ */
+WorkspaceFactoryController.prototype.isDefinedBlock = function(block) {
+ return this.model.isDefinedBlockType(block.type);
+};
+
+/**
+ * Sets a warning on blocks loaded to the workspace that are not defined.
+ * @private
+ */
+WorkspaceFactoryController.prototype.warnForUndefinedBlocks_ = function() {
+ var blocks = this.toolboxWorkspace.getAllBlocks();
+ for (var i = 0, block; block = blocks[i]; i++) {
+ if (!this.isDefinedBlock(block)) {
+ block.setWarningText(block.type + ' is not defined (it is not a standard '
+ + 'block, \nin your block library, or an imported block)');
+ }
+ }
+};
+
+/*
+ * Determines if a standard variable category is in the custom toolbox.
+ *
+ * @return {boolean} True if a variables category is in use, false otherwise.
+ */
+WorkspaceFactoryController.prototype.hasVariablesCategory = function() {
+ return this.model.hasVariables();
+};
+
+/**
+ * Determines if a standard procedures category is in the custom toolbox.
+ *
+ * @return {boolean} True if a procedures category is in use, false otherwise.
+ */
+WorkspaceFactoryController.prototype.hasProceduresCategory = function() {
+ return this.model.hasProcedures();
+};
+
+/**
+ * Determines if there are any unsaved changes in workspace factory.
+ *
+ * @return {boolean} True if there are unsaved changes, false otherwise.
+ */
+WorkspaceFactoryController.prototype.hasUnsavedChanges = function() {
+ return this.hasUnsavedToolboxChanges || this.hasUnsavedPreloadChanges;
+};