/* This file contains proprietary software owned by Motorola Mobility, Inc.
No rights, expressed or implied, whatsoever to this software are provided by Motorola Mobility, Inc. hereunder.
(c) Copyright 2011 Motorola Mobility, Inc. All Rights Reserved.
*/ /** @module montage/core/state-chart @requires montage */ var Montage = require("montage").Montage; /** @class module:montage/core/state-chart.State @extends module:montage/core/core.Montage */ var State = exports.State = Montage.create(Montage, /** @lends module:montage/core/state-chart.State# */{ _stateChart: { enumerable: false, value: null }, /** Initializes a State object with a set of options. @function @param {String} options The options for the new State. @returns {State} */ init: { value: function(options) { this.substates = {}; this.enterState = null; this.exitState = null; var keys = Object.keys(options), i = 0, iKey, iOption; for (; (iKey = keys[i]); i++) { iOption = options[iKey]; if (iOption.prototype === State.prototype) { iOption.name = iKey; iOption.parentState = this; this.substates[iKey] = iOption; } // TODO is it worth guarding against any old property being added to this state? if (typeof iOption === "string" && "initialSubstate" !== iKey) { // this is a simple gotoState transition syntax this[iKey] = this._encloseGotoState(iOption); } else { this[iKey] = iOption; } } return this; } }, /** The name of the state. @type {Property} @default {String} null */ name: { enumerable: false, value: null }, _initialSubstate: { enumerable: false, value: null }, /** The intitial substate. @type {Function} @default {String} null */ initialSubstate: { get: function() { if (typeof this._initialSubstate === "string") { this._initialSubstate = this[this._initialSubstate]; } return this._initialSubstate; }, set: function(value) { this._initialSubstate = value; } }, /** The set of substates. @type {Property} @default {String} null */ substates: { enumerable: false, value: null }, /** The state's parent state. @type {Property} @default {String} null */ parentState: { enumerable: false, value: null }, _path: { enumerable: false, value: null }, /** @type {Function} @default {String} null */ path: { // TODO add dependency on parentState, remember to clear cached value enumerable: false, get: function() { if (!this._path) { if (this.parentState && this.parentState.path) { this._path = this.parentState.path + "." + this.name; } else { this._path = this.name; } } return this._path; } }, /** @type {Property} @default {String} null */ enterState: { enumerable: false, value: null }, /** @type {Property} @default {String} null */ exitState: { enumerable: false, value: null }, /** @function @param {String} otherState @returns !!this.path.match(new RegExp(".?" + otherState + ".?")) */ isInState: { enumerable: false, value: function(otherState) { if (typeof otherState !== "string") { otherState = otherState.name; } return !!this.path.match(new RegExp(".?" + otherState + ".?")); } }, /** @private */ _encloseGotoState: { value: function(state) { return (function(stateChart, owner) { // Not relying on original implementation to save an extra function call, despite duplicated code return this._stateChart._gotoState(state, owner); }); } }, /** @function @param {Property} state @param {Property} owner @returns this._stateChart._gotoState(state, owner) */ gotoState: { value: function(state, owner) { return this._stateChart._gotoState(state, owner); } }, /** @private */ _performAction: { enumerable: null, value: function(actionName, stateChart, owner) { if (this[actionName]) { // TODO what should the context be inside the action function: state or stateChart? // state makes sense but requires that authors know that when building a stateChart // it's easy to get annoyed with having to remember which one you're in when building // a relatively complex stateChart // we could just .call(this._stateChart, args) etc... this[actionName](stateChart, owner); } else if (this.parentState) { this.parentState._performAction(actionName, stateChart, owner); } else { throw "Action '" + actionName + "' not available"; } } }, /** @function @returns "[State " + this.path + " ]" */ toString: { enumerable: false, value: function() { return "[State " + this.path + " ]"; } } }); /** @class module:montage/core/state-chart.StateChart */ var StateChart = exports.StateChart = Montage.create(Montage,/** @lends module:montage/core/state-chart.StateChart# */ { /** @type {Property} @default {String} null */ delegate: { enumerable: false, value: null }, // When the ownerStateProperty is set, we expect all operations of this stateChart to rely on the owner property. // That is, many objects can rely on this stateChart to manage the flow of state but keep track of their own // currentState (exposed as ownerStateProperty and recorded at "_" + ownerStateProperty); // When this is set, the statechart itself is completely stateless and an object must take ownership of it // prior to performing any actions /** @type {Property} @default {String} null */ ownerStateProperty: { enumerable: false, value: null }, /** @type {Property} @default {String} null */ rootState: { enumerable: false, value: null }, _currentState: { enumerable: false, value: null }, /** The current state. @function @returns The current state. */ currentState: { get: function() { return this.ownerStateProperty ? null : this._currentState; } }, /** Initializes a StateChart with a State object, and returns the StateChart. @function @param {String} state TODO @returns {StateChart} */ initWithState: { value: function(state) { this._states = {}; this.rootState = state; this.rootState._stateChart = this; this._prepareState(this.rootState); this.enterDefaultState(); return this; } }, _defaultState: { enumerable: false, value: null }, /** The default state. @function @returns this._defaultState */ defaultState: { enumerable: false, get: function() { if (!this._defaultState) { var deepestState, nextState; deepestState = nextState = this.rootState; while ((nextState = nextState.initialSubstate)) { deepestState = nextState; } this._defaultState = deepestState; } return this._defaultState; } }, /** @function @returns this.defaultState */ enterDefaultState: { enumerable: false, value: function() { if (this.ownerStateProperty && !this.owner) { throw "This stateChart has been configured to require an owner to execute this function"; } var owner = this.ownerStateProperty ? this.owner : this, // Using internal ownerStateProperty normally reserved for writing, seeing as many // components trigger this enterDefaultState, when their ownerStateProperty.get is invoked the first // time. So we need to stop the infinite loop currentState = this.ownerStateProperty ? owner["_" + this.ownerStateProperty] : owner.currentState; if (currentState) { throw "Cannot enter default state from '" + currentState.name + "'"; } // We could probably use gotoState for this but I want to minimize the callbacks // as we're not really transitioning for the first setup of the state // we're just getting there, I don't think we need to make it look like // a "transition" to the delegate var deepestState, nextState; deepestState = nextState = this.rootState; while ((nextState = nextState.initialSubstate)) { if (deepestState.enterState) { deepestState.enterState(this, owner); } deepestState = nextState; if (nextState.initialSubstate && deepestState.exitState) { deepestState.exitState(this, owner); } } if (this.ownerStateProperty) { owner["_" + this.ownerStateProperty] = this.defaultState; } else { this._currentState = this.defaultState; } return this.defaultState; } }, /** @private */ _prepareState: { enumerable: false, value: function(state) { state._stateChart = this; // Keep a record of all states other than the root if (state.name) { this._states[state.name] = state; } var substateName; for (substateName in state.substates) { this._prepareState(state.substates[substateName]); } } }, /** @private */ _states: { enumerable: false, value: null }, /** @function @param {Property} stateName TODO @returns {Array} this._states[stateName] */ stateWithName: { enumerable: false, value: function(stateName) { return this._states[stateName]; } }, /** @function @param {String} action TODO @param {String} owner TODO */ performAction: { value: function(action, owner) { if (this.ownerStateProperty && !owner) { throw "This stateChart has been configured to require an owner to execute this function"; } owner = this.ownerStateProperty ? owner : this; var currentState = this.ownerStateProperty ? owner[this.ownerStateProperty] : owner.currentState; if (!currentState) { throw "Cannot perform action '" + action + "' without a currentState"; } currentState._performAction(action, this, owner); // After performing the action, possibly with state transitions, clear out the owner this.owner = null; } }, /** @private */ _gotoState: { value: function(state, owner) { if (this.ownerStateProperty && !owner) { throw "This stateChart has been configured to require an owner to execute this function"; } owner = this.ownerStateProperty ? owner : this; var fromState = this.ownerStateProperty ? owner[this.ownerStateProperty] : owner.currentState, fromStateName = fromState.name, stateName = state, currentPath, destinationPath, i, interiorDestinationPath, interiorDestinationPathCount, lastCommonIndex, nextIndex, destinationPathCount, searchCount, iState, oldState, delegateWillExit = false, delegateWillEnter = false, delegateDidExit = false, delegateDidEnter = false; if (typeof stateName === "string") { state = this._states[state]; } else { stateName = state.name; } //same state if (stateName === fromStateName) { return; } if (this.delegate) { delegateWillExit = typeof this.delegate.stateChartWillExitState === "function"; delegateWillEnter = typeof this.delegate.stateChartWillEnterState === "function"; delegateDidExit = typeof this.delegate.stateChartDidExitState === "function"; delegateDidEnter = typeof this.delegate.stateChartDidEnterState === "function"; } if (this.delegate && typeof this.delegate.stateChartShouldGoFromStateToState === "function") { if (!this.delegate.stateChartShouldGoFromStateToState(this, fromState, state)) { return; } } if (this.delegate && typeof this.delegate.stateChartWillGoFromStateToState === "function") { this.delegate.stateChartWillGoFromStateToState(this, fromState, state); } currentPath = fromState.path; destinationPath = state.path; //state is inside this state if ((new RegExp(currentPath)).test(destinationPath)) { interiorDestinationPath = destinationPath.replace(new RegExp(currentPath + ".?"), "").split("."); interiorDestinationPathCount = interiorDestinationPath.length; i = 0; for (; i < interiorDestinationPathCount; i++) { iState = this._states[interiorDestinationPath[i]]; if (delegateWillEnter) { this.delegate.stateChartWillEnterState(this, iState); } if (typeof iState.enterState === "function") { iState.enterState(this, owner); } if (delegateDidEnter) { this.delegate.stateChartDidEnterState(this, iState); } } } // state is outside this state, need to find where they fork else { currentPath = currentPath.split("."); destinationPath = destinationPath.split("."); lastCommonIndex = -1; destinationPathCount = destinationPath.length; searchCount = Math.min(currentPath.length, destinationPathCount); while (lastCommonIndex < searchCount) { nextIndex = lastCommonIndex + 1; if (currentPath[nextIndex] !== destinationPath[nextIndex]) { break; } lastCommonIndex++; } // exit from the currentState to just before the common path for (i = currentPath.length - 1; i > lastCommonIndex; i--) { iState = this._states[currentPath[i]]; if (delegateWillExit) { this.delegate.stateChartWillExitState(this, iState); } if (typeof iState.exitState === "function") { iState.exitState(this, owner); } if (delegateDidExit) { this.delegate.stateChartDidExitState(this, iState); } } // We don't want to enter the root state lastCommonIndex = lastCommonIndex < 0 ? 0 : lastCommonIndex; // enter from the common path to the new state at the end for (i = lastCommonIndex; i < destinationPathCount; i++) { iState = this._states[destinationPath[i]]; if (delegateWillEnter) { this.delegate.stateChartWillEnterState(this, iState); } // Update the currentState as we enter each state // TODO is this really necessary, is it best done at this point? what about exit? if (this.ownerStateProperty) { owner["_" + this.ownerStateProperty] = iState; } else { this._currentState = iState; } if (typeof iState.enterState === "function") { iState.enterState(this, owner); } if (delegateDidEnter) { this.delegate.stateChartDidEnterState(this, iState); } } } oldState = fromState; if (this.delegate && typeof this.delegate.stateChartDidGoFromStateToState === "function") { this.delegate.stateChartDidGoFromStateToState(this, oldState, state); } if (typeof owner.transitionedFromStateToState === "function") { owner.transitionedFromStateToState(this, oldState, state); } } } });