UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

927 lines (829 loc) 28.9 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2006, 2007, 2011 Derrell Lipman License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * Derrell Lipman (derrell) ************************************************************************ */ /** * Create a new state which may be added to a finite state machine. */ qx.Class.define("qx.util.fsm.State", { extend : qx.core.Object, /** * @param stateName {String} * The name of this state. This is the name which may be referenced in * objects of class qx.util.fsm.Transition, when passing of * the transition's predicate means transition to this state. * * @param stateInfo {Map} * <pre> * An object containing any of the following properties: * * context - * A context in which all of the following functions should be run. * * onentry - * A function which is called upon entry to the state. Its signature * is function(fsm, event) and it is saved in the onentry property of * the state object. (This function is called after the Transition's * action function and after the previous state's onexit function.) * * In the onentry function: * * fsm - * The finite state machine object to which this state is attached. * * event - * The event that caused the finite state machine to run * * onexit - * A function which is called upon exit from the state. Its signature * is function(fsm, event) and it is saved in the onexit property of * the state object. (This function is called after the Transition's * action function and before the next state's onentry function.) * * In the onexit function: * * fsm - * The finite state machine object to which this state is attached. * * event - * The event that caused the finite state machine to run * * autoActionsBeforeOnentry - * autoActionsAfterOnentry - * autoActionsBeforeOnexit - * autoActionsAfterOnexit - * Automatic actions which take place at the time specified by the * property name. In all cases, the action takes place immediately * before or after the specified function. * * The property value for each of these properties is an object which * describes some number of functions to invoke on a set of specified * objects (typically widgets). * * An example, using autoActionsBeforeOnentry, might look like this: * * "autoActionsBeforeOnentry" : * { * // The name of a function. * "setEnabled" : * [ * { * // The parameter value, thus "setEnabled(true);" * "parameters" : [ true ], * * // The function would be called on each object: * // this.getObject("obj1").setEnabled(true); * // this.getObject("obj2").setEnabled(true); * "objects" : [ "obj1", "obj2" ], * * // And similarly for each object in each specified group. * "groups" : [ "group1", "group2" ] * } * ], * * // The name of another function. * "setVisible" : * [ * { * // The parameter value, thus "setVisible(false);" * "parameters" : [ false ], * * // The function would be called on each object and group, as * // described above. * "objects" : [ "obj3", "obj4" ], * "groups" : [ "group3", "group4" ] * } * ] * }; * * events (required) - * A description to the finite state machine of how to handle a * particular event, optionally associated with a specific target * object on which the event was dispatched. This should be an object * containing one property for each event which is either handled or * blocked. The property name should be the event name. The property * value should be one of: * * (a) qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE * * (b) qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED * * (c) a string containing the name of an explicit Transition to use * * (d) an object where each property name is the Friendly Name of an * object (meaning that this rule applies if both the event and * the event's target object's Friendly Name match), and its * property value is one of (a), (b) or (c), above. * * This object is saved in the events property of the state object. * * Additional properties may be provided in stateInfo. They will not be * used by the finite state machine, but will be available via * this.getUserData("<propertyName>") during the state's onentry and * onexit functions. * </pre> * * @throws {Error} If the state info is not a valid object. * @throws {Error} If the events object is not provided in new state info. * */ construct : function(stateName, stateInfo) { var context; // Call our superclass' constructor this.base(arguments); // Save the state name this.setName(stateName); // Ensure they passed in an object if (typeof (stateInfo) != "object") { throw new Error("State info must be an object"); } // If a context was specified, retrieve it. context = stateInfo.context || window; // Save it for future use this.setUserData("context", context); // Save data from the stateInfo object for (var field in stateInfo) { // If we find one of our properties, call its setter. switch(field) { case "onentry": this.setOnentry( this.__bindIfFunction(stateInfo[field], context)); break; case "onexit": this.setOnexit( this.__bindIfFunction(stateInfo[field], context)); break; case "autoActionsBeforeOnentry": this.setAutoActionsBeforeOnentry(stateInfo[field]); break; case "autoActionsAfterOnentry": this.setAutoActionsAfterOnentry(stateInfo[field]); break; case "autoActionsBeforeOnexit": this.setAutoActionsBeforeOnexit(stateInfo[field]); break; case "autoActionsAfterOnexit": this.setAutoActionsAfterOnexit(stateInfo[field]); break; case "events": this.setEvents(stateInfo[field]); break; case "context": // already handled break; default: // Anything else is user-provided data for their own use. Save it. this.setUserData(field, stateInfo[field]); // Log it in case it was a typo and they intended a built-in field this.debug("State " + stateName + ": " + "Adding user-provided field to state: " + field); break; } } // Check for required but missing properties if (!this.getEvents()) { throw new Error("The events object must be provided in new state info"); } // Initialize the transition list this.transitions = {}; }, statics : { /** * Common function for checking the value provided for * auto actions. * * Auto-action property values passed to us look akin to: * * <pre class='javascript'> * { * // The name of a function. * "setEnabled" : * [ * { * // The parameter value(s), thus "setEnabled(true);" * "parameters" : [ true ], * * // The function would be called on each object: * // this.getObject("obj1").setEnabled(true); * // this.getObject("obj2").setEnabled(true); * "objects" : [ "obj1", "obj2" ] * * // And similarly for each object in each specified group. * "groups" : [ "group1", "group2" ], * } * ]; * * "setTextColor" : * [ * { * "parameters" : [ "blue" ] * "groups" : [ "group3", "group4" ], * "objects" : [ "obj3", "obj4" ] * } * ]; * }; * </pre> * * * @param actionType {String} * The name of the action being validated (for debug messages) * * @param value {Object} * The property value which is being validated * * @param context {Object} * The object to which the created function should be bound. * * @return {Function} * Function that implements calls to each of the requested automatic * actions * * @throws {Error} If the value has an invalid type. * @throws {Error} If the function type is not an array. * @throws {Error} If the function request parameter type is not valid. * @throws {Error} If the function parameters are not valid. * @throws {Error} If 'objects' list is invalid. * @throws {Error} If a name in the 'objects' list is not valid. * @throws {Error} If the 'groups' list is not valid. */ _commonTransformAutoActions : function(actionType, value, context) { // Validate that we received an object property value if (typeof (value) != "object") { throw new Error("Invalid " + actionType + " value: " + typeof (value)); } // We'll create a function to do the requested actions. Initialize the // string into which we'll generate the common fragment added to the // function for each object. var funcFragment; // Here, we'll keep the function body. Initialize a try block. var func = "try" + "{"; var param; var objectAndGroupList; // Retrieve the function request, e.g. // "enabled" : for (var f in value) { // Get the function request value object, e.g. // "setEnabled" : // [ // { // "parameters" : [ true ], // "objects" : [ "obj1", "obj2" ] // "groups" : [ "group1", "group2" ], // } // ]; var functionRequest = value[f]; // The function request value should be an object if (!functionRequest instanceof Array) { throw new Error("Invalid function request type: " + "expected array, found " + typeof (functionRequest)); } // For each function request... for (var i=0; i<functionRequest.length; i++) { // Retrieve the object and group list object objectAndGroupList = functionRequest[i]; // The object and group list should be an object, e.g. // { // "parameters" : [ true ], // "objects" : [ "obj1", "obj2" ] // "groups" : [ "group1", "group2" ], // } if (typeof (objectAndGroupList) != "object") { throw new Error("Invalid function request parameter type: " + "expected object, found " + typeof (functionRequest[param])); } // Retrieve the parameter list var params = objectAndGroupList["parameters"]; // If it didn't exist, ... if (!params) { // ... use an empty array. params = []; } else { // otherwise, ensure we got an array if (!params instanceof Array) { throw new Error("Invalid function parameters: " + "expected array, found " + typeof (params)); } } // Create the function to call on each object. The object on which // the function is called will be prepended later. funcFragment = f + "("; // For each parameter... for (var j=0; j<params.length; j++) { // If this isn't the first parameter, add a separator if (j != 0) { funcFragment += ","; } if (typeof (params[j]) == "function") { // If the parameter is a function, arrange for it to be called // at run time. funcFragment += "(" + params[j] + ")(fsm)"; } else if (typeof (params[j]) == "string") { // If the parameter is a string, quote it. funcFragment += '"' + params[j] + '"'; } else { // Otherwise, just add the parameter's literal value funcFragment += params[j]; } } // Complete the function call funcFragment += ")"; // Get the "objects" list, e.g. // "objects" : [ "obj1", "obj2" ] var a = objectAndGroupList["objects"]; // Was there an "objects" list? if (!a) { // Nope. Simplify code by creating an empty array. a = []; } else if (!a instanceof Array) { throw new Error("Invalid 'objects' list: expected array, got " + typeof (a)); } for (var j=0; j<a.length; j++) { // Ensure we got a string if (typeof (a[j]) != "string") { throw new Error("Invalid friendly name in 'objects' list: " + a[j]); } func += " fsm.getObject('" + a[j] + "')." + funcFragment + ";"; } // Get the "groups" list, e.g. // "groups" : [ "group1, "group2" ] var g = objectAndGroupList["groups"]; // Was a "groups" list found? if (g) { // Yup. Ensure it's an array. if (!g instanceof Array) { throw new Error("Invalid 'groups' list: expected array, got " + typeof (g)); } for (j=0; j<g.length; j++) { // Arrange to call the function on each object in each group func += " var groupObjects = " + " fsm.getGroupObjects('" + g[j] + "');" + " for (var i = 0; i < groupObjects.length; i++)" + " {" + " var objName = groupObjects[i];" + " fsm.getObject(objName)." + funcFragment + ";" + " }"; } } } } // Terminate the try block for function invocations func += "}" + "catch(ex)" + "{" + " fsm.debug(ex);" + "}"; // We've now built the entire body of a function that implements calls // to each of the requested automatic actions. Create and return the // function, which will become the property value. return qx.lang.Function.bind(new Function("fsm", func), context); } }, properties : { /** * The name of this state. This name may be used as a Transition's * nextState value, or an explicit next state in the 'events' handling * list in a State. */ name : { transform : "__transformName", nullable : true }, /** * The onentry function for this state. This is documented in the * constructor, and is typically provided through the constructor's * stateInfo object, but it is also possible (but highly NOT recommended) * to change this dynamically. */ onentry : { transform : "__transformOnentry", nullable : true, init : function(fsm, event) {} }, /** * The onexit function for this state. This is documented in the * constructor, and is typically provided through the constructor's * stateInfo object, but it is also possible (but highly NOT recommended) * to change this dynamically. */ onexit : { transform : "__transformOnexit", nullable : true, init : function(fsm, event) {} }, /** * Automatic actions to take prior to calling the state's onentry function. * * The value passed to setAutoActionsBeforeOnentry() should like something * akin to: * * <pre class='javascript'> * "autoActionsBeforeOnentry" : * { * // The name of a function. This would become "setEnabled(" * "enabled" : * [ * { * // The parameter value, thus "setEnabled(true);" * "parameters" : [ true ], * * // The function would be called on each object: * // this.getObject("obj1").setEnabled(true); * // this.getObject("obj2").setEnabled(true); * "objects" : [ "obj1", "obj2" ] * * // And similarly for each object in each specified group. * "groups" : [ "group1", "group2" ], * } * ]; * }; * </pre> */ autoActionsBeforeOnentry : { transform : "__transformAutoActionsBeforeOnentry", nullable : true, init : function(fsm, event) {} }, /** * Automatic actions to take after return from the state's onentry * function. * * The value passed to setAutoActionsAfterOnentry() should like something * akin to: * * <pre class='javascript'> * "autoActionsAfterOnentry" : * { * // The name of a function. This would become "setEnabled(" * "enabled" : * [ * { * // The parameter value, thus "setEnabled(true);" * "parameters" : [ true ], * * // The function would be called on each object: * // this.getObject("obj1").setEnabled(true); * // this.getObject("obj2").setEnabled(true); * "objects" : [ "obj1", "obj2" ] * * // And similarly for each object in each specified group. * "groups" : [ "group1", "group2" ], * } * ]; * }; * </pre> */ autoActionsAfterOnentry : { transform : "__transformAutoActionsAfterOnentry", nullable : true, init : function(fsm, event) {} }, /** * Automatic actions to take prior to calling the state's onexit function. * * The value passed to setAutoActionsBeforeOnexit() should like something * akin to: * * <pre class='javascript'> * "autoActionsBeforeOnexit" : * { * // The name of a function. This would become "setEnabled(" * "enabled" : * [ * { * // The parameter value, thus "setEnabled(true);" * "parameters" : [ true ], * * // The function would be called on each object: * // this.getObject("obj1").setEnabled(true); * // this.getObject("obj2").setEnabled(true); * "objects" : [ "obj1", "obj2" ] * * // And similarly for each object in each specified group. * "groups" : [ "group1", "group2" ], * } * ]; * }; * </pre> */ autoActionsBeforeOnexit : { transform : "__transformAutoActionsBeforeOnexit", nullable : true, init : function(fsm, event) {} }, /** * Automatic actions to take after returning from the state's onexit * function. * * The value passed to setAutoActionsAfterOnexit() should like something * akin to: * * <pre class='javascript'> * "autoActionsBeforeOnexit" : * { * // The name of a function. This would become "setEnabled(" * "enabled" : * [ * { * // The parameter value, thus "setEnabled(true);" * "parameters" : [ true ], * * // The function would be called on each object: * // this.getObject("obj1").setEnabled(true); * // this.getObject("obj2").setEnabled(true); * "objects" : [ "obj1", "obj2" ] * * // And similarly for each object in each specified group. * "groups" : [ "group1", "group2" ], * } * ]; * }; * </pre> */ autoActionsAfterOnexit : { transform : "__transformAutoActionsAfterOnexit", nullable : true, init : function(fsm, event) {} }, /** * The object representing handled and blocked events for this state. * This is documented in the constructor, and is typically provided * through the constructor's stateInfo object, but it is also possible * (but highly NOT recommended) to change this dynamically. */ events : { transform : "__transformEvents", nullable : true } }, members : { /** * Internal transform method * * @param value {var} Value passed to setter * @return {var} the final value * @throws {Error} when an invalid value is detected */ __transformName : function(value) { // Ensure that we got a valid state name if (typeof (value) != "string" || value.length < 1) { throw new Error("Invalid state name"); } return value; }, /** * Internal transform method * * @param value {var} Current value * @return {var} the final value * @throws {Error} when an invalid value is detected */ __transformOnentry : function(value) { // Validate the onentry function switch(typeof (value)) { case "undefined": // None provided. Convert it to a null function return function(fsm, event) {}; case "function": // We're cool. No changes required return qx.lang.Function.bind(value, this.getUserData("context")); default: throw new Error("Invalid onentry type: " + typeof (value)); } }, /** * Internal transform method * * @param value {var} Current value * @return {var} the final value * @throws {Error} when an invalid value is detected */ __transformOnexit : function(value) { // Validate the onexit function switch(typeof (value)) { case "undefined": // None provided. Convert it to a null function return function(fsm, event) {}; case "function": // We're cool. No changes required return qx.lang.Function.bind(value, this.getUserData("context")); default: throw new Error("Invalid onexit type: " + typeof (value)); } }, /** * Internal transform method * * @param value {var} Current value * @return {var} the final value * @throws {Error} when an invalid value is detected */ __transformEvents : function(value) { // Validate that events is an object if (typeof (value) != "object") { throw new Error("events must be an object"); } // Confirm that each property is a valid value // The property value should be one of: // // (a) qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE // // (b) qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED // // (c) a string containing the name of an explicit Transition to use // // (d) an object where each property name is the Friendly Name of an // object (meaning that this rule applies if both the event and // the event's target object's Friendly Name match), and its // property value is one of (a), (b) or (c), above. for (var e in value) { var action = value[e]; if (typeof (action) == "number" && action != qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE && action != qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED) { throw new Error("Invalid numeric value in events object: " + e + ": " + action); } else if (typeof (action) == "object") { for (var action_e in action) { if (typeof (action[action_e]) == "number" && action[action_e] != qx.util.fsm.FiniteStateMachine.EventHandling.PREDICATE && action[action_e] != qx.util.fsm.FiniteStateMachine.EventHandling.BLOCKED) { throw new Error("Invalid numeric value in events object " + "(" + e + "): " + action_e + ": " + action[action_e]); } else if (typeof (action[action_e]) != "string" && typeof (action[action_e]) != "number") { throw new Error("Invalid value in events object " + "(" + e + "): " + action_e + ": " + action[action_e]); } } } else if (typeof (action) != "string" && typeof (action) != "number") { throw new Error("Invalid value in events object: " + e + ": " + value[e]); } } // We're cool. No changes required. return value; }, /** * Internal transform method * * @param value {var} Current value * @return {var} the final value */ __transformAutoActionsBeforeOnentry : function(value) { return qx.util.fsm.State._commonTransformAutoActions( "autoActionsBeforeOnentry", value, this.getUserData("context")); }, /** * Internal transform method * * @param value {var} Current value * @return {var} the final value */ __transformAutoActionsAfterOnentry : function(value) { return qx.util.fsm.State._commonTransformAutoActions( "autoActionsAfterOnentry", value, this.getUserData("context")); }, /** * Internal transform method * * @param value {var} Current value * @return {var} the final value */ __transformAutoActionsBeforeOnexit : function(value) { return qx.util.fsm.State._commonTransformAutoActions( "autoActionsBeforeOnexit", value, this.getUserData("context")); }, /** * Internal transform method * * @param value {var} Current value * @return {var} the final value */ __transformAutoActionsAfterOnexit : function(value) { return qx.util.fsm.State._commonTransformAutoActions( "autoActionsAfterOnexit", value, this.getUserData("context")); }, /** * If given a function, bind it to a specified context. * * @param f {Function|var} * The (possibly) function to be bound to the specified context. * * @param context {Object} * The context to bind the function to. * * @return {Function} * If f was a function, the return value is f wrapped such that it will * be called in the specified context. Otherwise, f is returned * unaltered. */ __bindIfFunction : function(f, context) { // Is the first parameter a function? if (typeof(f) == "function") { // Yup. Bind it to the specified context. f = qx.lang.Function.bind(f, context); } return f; }, /** * Add a transition to a state * * * @param trans {qx.util.fsm.Transition} * An object of class qx.util.fsm.Transition representing a transition * which is to be a part of this state. * */ addTransition : function(trans) { // Ensure that we got valid transition info if (!trans instanceof qx.util.fsm.Transition) { throw new Error("Invalid transition: not an instance of " + "qx.util.fsm.Transition"); } // Add the new transition object to the state this.transitions[trans.getName()] = trans; } } });