1 // ========================================================================== 2 // Project: SC.Statechart - A Statechart Framework for SproutCore 3 // Copyright: ©2010, 2011 Michael Cohen, and contributors. 4 // Portions @2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /*global SC */ 9 10 sc_require('system/state'); 11 sc_require('mixins/statechart_delegate'); 12 13 /** 14 @class 15 16 The startchart manager mixin allows an object to be a statechart. By becoming a statechart, the 17 object can then be manage a set of its own states. 18 19 This implementation of the statechart manager closely follows the concepts stated in D. Harel's 20 original paper "Statecharts: A Visual Formalism For Complex Systems" 21 (www.wisdom.weizmann.ac.il/~harel/papers/Statecharts.pdf). 22 23 The statechart allows for complex state heircharies by nesting states within states, and 24 allows for state orthogonality based on the use of concurrent states. 25 26 At minimum, a statechart must have one state: The root state. All other states in the statechart 27 are a decendents (substates) of the root state. 28 29 The following example shows how states are nested within a statechart: 30 31 MyApp.Statechart = SC.Object.extend(SC.StatechartManager, { 32 rootState: SC.State.design({ 33 initialSubstate: 'stateA', 34 35 stateA: SC.State.design({ 36 // ... can continue to nest further states 37 }), 38 39 stateB: SC.State.design({ 40 // ... can continue to nest further states 41 }) 42 }) 43 }); 44 45 Note how in the example above, the root state as an explicit initial substate to enter into. If no 46 initial substate is provided, then the statechart will default to the the state's first substate. 47 48 You can also defined states without explicitly defining the root state. To do so, simply create properties 49 on your object that represents states. Upon initialization, a root state will be constructed automatically 50 by the mixin and make the states on the object substates of the root state. As an example: 51 52 MyApp.Statechart = SC.Object.extend(SC.StatechartManager, { 53 initialState: 'stateA', 54 55 stateA: SC.State.design({ 56 // ... can continue to nest further states 57 }), 58 59 stateB: SC.State.design({ 60 // ... can continue to nest further states 61 }) 62 }); 63 64 If you liked to specify a class that should be used as the root state but using the above method to defined 65 states, you can set the rootStateExample property with a class that extends from SC.State. If the 66 rootStateExample property is not explicitly assigned the then default class used will be SC.State. 67 68 To provide your statechart with orthogonality, you use concurrent states. If you use concurrent states, 69 then your statechart will have multiple current states. That is because each concurrent state represents an 70 independent state structure from other concurrent states. The following example shows how to provide your 71 statechart with concurrent states: 72 73 MyApp.Statechart = SC.Object.extend(SC.StatechartManager, { 74 rootState: SC.State.design({ 75 substatesAreConcurrent: YES, 76 77 stateA: SC.State.design({ 78 // ... can continue to nest further states 79 }), 80 81 stateB: SC.State.design({ 82 // ... can continue to nest further states 83 }) 84 }) 85 }); 86 87 Above, to indicate that a state's substates are concurrent, you just have to set the substatesAreConcurrent to 88 YES. Once done, then stateA and stateB will be independent of each other and each will manage their 89 own current substates. The root state will then have more then one current substate. 90 91 To define concurrent states directly on the object without explicitly defining a root, you can do the 92 following: 93 94 MyApp.Statechart = SC.Object.extend(SC.StatechartManager, { 95 statesAreConcurrent: YES, 96 97 stateA: SC.State.design({ 98 // ... can continue to nest further states 99 }), 100 101 stateB: SC.State.design({ 102 // ... can continue to nest further states 103 }) 104 }); 105 106 Remember that a startchart can have a mixture of nested and concurrent states in order for you to 107 create as complex of statecharts that suite your needs. Here is an example of a mixed state structure: 108 109 MyApp.Statechart = SC.Object.extend(SC.StatechartManager, { 110 rootState: SC.State.design({ 111 initialSubstate: 'stateA', 112 113 stateA: SC.State.design({ 114 substatesAreConcurrent: YES, 115 116 stateM: SC.State.design({ ... }) 117 stateN: SC.State.design({ ... }) 118 stateO: SC.State.design({ ... }) 119 }), 120 121 stateB: SC.State.design({ 122 initialSubstate: 'stateX', 123 124 stateX: SC.State.design({ ... }) 125 stateY: SC.State.design({ ... }) 126 }) 127 }) 128 }); 129 130 Depending on your needs, a statechart can have lots of states, which can become hard to manage all within 131 one file. To modularize your states and make them easier to manage and maintain, you can plug-in states 132 into other states. Let's say we are using the statechart in the last example above, and all the code is 133 within one file. We could update the code and split the logic across two or more files like so: 134 135 // state_a.js 136 137 MyApp.StateA = SC.State.extend({ 138 substatesAreConcurrent: YES, 139 140 stateM: SC.State.design({ ... }) 141 stateN: SC.State.design({ ... }) 142 stateO: SC.State.design({ ... }) 143 }); 144 145 // state_b.js 146 147 MyApp.StateB = SC.State.extend({ 148 substatesAreConcurrent: YES, 149 150 stateM: SC.State.design({ ... }) 151 stateN: SC.State.design({ ... }) 152 stateO: SC.State.design({ ... }) 153 }); 154 155 // statechart.js 156 157 MyApp.Statechart = SC.Object.extend(SC.StatechartManager, { 158 rootState: SC.State.design({ 159 initialSubstate: 'stateA', 160 stateA: SC.State.plugin('MyApp.StateA'), 161 stateB: SC.State.plugin('MyApp.StateB') 162 }) 163 }); 164 165 Using state plug-in functionality is optional. If you use the plug-in feature you can break up your statechart 166 into as many files as you see fit. 167 168 @author Michael Cohen 169 */ 170 171 SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{ 172 173 //@if(debug) 174 /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */ 175 176 /** @private @property */ 177 allowStatechartTracing: function () { 178 var key = this.get('statechartTraceKey'); 179 return this.get(key); 180 }.property().cacheable(), 181 182 /** @private */ 183 _statechartTraceDidChange: function () { 184 this.notifyPropertyChange('allowStatechartTracing'); 185 }, 186 187 /** 188 @property 189 190 Returns an object containing current detailed information about 191 the statechart. This is primarily used for diagnostic/debugging 192 purposes. 193 194 Detailed information includes: 195 196 - current states 197 - state transition information 198 - event handling information 199 200 NOTE: This is only available in debug mode! 201 202 @returns {Hash} 203 */ 204 details: function () { 205 var details = { 206 'initialized': this.get('statechartIsInitialized') 207 }; 208 209 if (this.get('name')) { 210 details.name = this.get('name'); 211 } 212 213 if (!this.get('statechartIsInitialized')) { 214 return details; 215 } 216 217 details['current-states'] = []; 218 this.get('currentStates').forEach(function (state) { 219 details['current-states'].push(state.get('fullPath')); 220 }); 221 222 var stateTransition = { 223 active: this.get('gotoStateActive'), 224 suspended: this.get('gotoStateSuspended') 225 }; 226 227 if (this._gotoStateActions) { 228 stateTransition['transition-sequence'] = []; 229 var actions = this._gotoStateActions, 230 actionToStr = function (action) { 231 var actionName = action.action === SC.ENTER_STATE ? "enter" : "exit"; 232 return "%@ %@".fmt(actionName, action.state.get('fullPath')); 233 }; 234 235 actions.forEach(function (action) { 236 stateTransition['transition-sequence'].push(actionToStr(action)); 237 }); 238 239 stateTransition['current-transition'] = actionToStr(this._currentGotoStateAction); 240 } 241 242 details['state-transition'] = stateTransition; 243 244 if (this._stateHandleEventInfo) { 245 var info = this._stateHandleEventInfo; 246 details['handling-event'] = { 247 state: info.state.get('fullPath'), 248 event: info.event, 249 handler: info.handler 250 }; 251 } else { 252 details['handling-event'] = false; 253 } 254 255 return details; 256 }.property(), 257 258 /** 259 Returns a formatted string of detailed information about this statechart. Useful 260 for diagnostic/debugging purposes. 261 262 @returns {String} 263 264 NOTE: This is only available in debug mode! 265 266 @see #details 267 */ 268 toStringWithDetails: function () { 269 var str = "", 270 header = this.toString(), 271 details = this.get('details'); 272 273 str += header + "\n"; 274 str += this._hashToString(details, 2); 275 276 return str; 277 }, 278 279 /** @private */ 280 _hashToString: function (hash, indent) { 281 var str = ""; 282 283 for (var key in hash) { 284 var value = hash[key]; 285 if (value instanceof Array) { 286 str += this._arrayToString(key, value, indent) + "\n"; 287 } 288 else if (value instanceof Object) { 289 str += "%@%@:\n".fmt(' '.mult(indent), key); 290 str += this._hashToString(value, indent + 2); 291 } 292 else { 293 str += "%@%@: %@\n".fmt(' '.mult(indent), key, value); 294 } 295 } 296 297 return str; 298 }, 299 300 /** @private */ 301 _arrayToString: function (key, array, indent) { 302 if (array.length === 0) { 303 return "%@%@: []".fmt(' '.mult(indent), key); 304 } 305 306 var str = "%@%@: [\n".fmt(' '.mult(indent), key); 307 308 array.forEach(function (item, idx) { 309 str += "%@%@\n".fmt(' '.mult(indent + 2), item); 310 }, this); 311 312 str += ' '.mult(indent) + "]"; 313 314 return str; 315 }, 316 317 /** 318 Indicates whether to use a monitor to monitor that statechart's activities. If true then 319 the monitor will be active, otherwise the monitor will not be used. Useful for debugging 320 purposes. 321 322 NOTE: This is only available in debug mode! 323 324 @type Boolean 325 */ 326 monitorIsActive: NO, 327 328 /** 329 A statechart monitor that can be used to monitor this statechart. Useful for debugging purposes. 330 A monitor will only be used if monitorIsActive is true. 331 332 NOTE: This is only available in debug mode! 333 334 @property {SC.StatechartMonitor} 335 */ 336 monitor: null, 337 338 /** 339 Used to specify what property (key) on the statechart should be used as the trace property. By 340 default the property is 'trace'. 341 342 NOTE: This is only available in debug mode! 343 344 @type String 345 */ 346 statechartTraceKey: 'trace', 347 348 /** 349 Indicates whether to trace the statecharts activities. If true then the statechart will output 350 its activites to the browser's JS console. Useful for debugging purposes. 351 352 NOTE: This is only available in debug mode! 353 354 @see #statechartTraceKey 355 356 @type Boolean 357 */ 358 trace: NO, 359 360 /** 361 Used to log a statechart trace message 362 363 NOTE: This is only available in debug mode! 364 */ 365 statechartLogTrace: function (msg, style) { 366 if (style) { 367 SC.Logger.log("%c%@: %@".fmt(this.get('statechartLogPrefix'), msg), style); 368 } else { 369 SC.Logger.info("%@: %@".fmt(this.get('statechartLogPrefix'), msg)); 370 } 371 }, 372 373 /* END DEBUG ONLY PROPERTIES AND METHODS */ 374 //@endif 375 376 // Walk like a duck 377 isResponderContext: YES, 378 379 // Walk like a duck 380 isStatechart: YES, 381 382 /** 383 Indicates if this statechart has been initialized 384 385 @type Boolean 386 */ 387 statechartIsInitialized: NO, 388 389 /** 390 Optional name you can provide the statechart with. If set this will be included 391 in tracing and error output as well as detail output. Useful for 392 debugging/diagnostic purposes 393 */ 394 name: null, 395 396 /** 397 The root state of this statechart. All statecharts must have a root state. 398 399 If this property is left unassigned then when the statechart is initialized 400 it will used the rootStateExample, initialState, and statesAreConcurrent 401 properties to construct a root state. 402 403 @see #rootStateExample 404 @see #initialState 405 @see #statesAreConcurrent 406 407 @property {SC.State} 408 */ 409 rootState: null, 410 411 /** 412 Represents the class used to construct a class that will be the root state for 413 this statechart. The class assigned must derive from SC.State. 414 415 This property will only be used if the rootState property is not assigned. 416 417 @see #rootState 418 419 @property {SC.State} 420 */ 421 rootStateExample: SC.State, 422 423 /** 424 Indicates what state should be the initial state of this statechart. The value 425 assigned must be the name of a property on this object that represents a state. 426 As well, the statesAreConcurrent must be set to NO. 427 428 This property will only be used if the rootState property is not assigned. 429 430 @see #rootState 431 432 @type String 433 */ 434 initialState: null, 435 436 /** 437 Indicates if properties on this object representing states are concurrent to each other. 438 If YES then they are concurrent, otherwise they are not. If the YES, then the 439 initialState property must not be assigned. 440 441 This property will only be used if the rootState property is not assigned. 442 443 @see #rootState 444 445 @type Boolean 446 */ 447 statesAreConcurrent: NO, 448 449 /** 450 Used to specify what property (key) on the statechart should be used as the owner property. By 451 default the property is 'owner'. 452 453 @type String 454 */ 455 statechartOwnerKey: 'owner', 456 457 /** 458 Sets who the owner is of this statechart. If null then the owner is this object otherwise 459 the owner is the assigned object. 460 461 @see #statechartOwnerKey 462 463 @type SC.Object 464 */ 465 owner: null, 466 467 /** 468 Indicates if the statechart should be automatically initialized by this 469 object after it has been created. If YES then initStatechart will be 470 called automatically, otherwise it will not. 471 472 @type Boolean 473 */ 474 autoInitStatechart: YES, 475 476 /** 477 If yes, any warning messages produced by the statechart or any of its states will 478 not be logged, otherwise all warning messages will be logged. 479 480 While designing and debugging your statechart, it's best to keep this value false. 481 In production you can then suppress the warning messages. 482 483 @type Boolean 484 */ 485 suppressStatechartWarnings: NO, 486 487 /** 488 A statechart delegate used by the statechart and the states that the statechart 489 manages. The value assigned must adhere to the {@link SC.StatechartDelegate} mixin. 490 491 @type SC.Object 492 493 @see SC.StatechartDelegate 494 */ 495 delegate: null, 496 497 /** 498 Computed property that returns an objects that adheres to the 499 {@link SC.StatechartDelegate} mixin. If the {@link #delegate} is not 500 assigned then this object is the default value returned. 501 502 @see SC.StatechartDelegate 503 @see #delegate 504 */ 505 statechartDelegate: function () { 506 var del = this.get('delegate'); 507 return this.delegateFor('isStatechartDelegate', del); 508 }.property('delegate'), 509 510 initMixin: function () { 511 if (this.get('autoInitStatechart')) { 512 this.initStatechart(); 513 } 514 }, 515 516 destroyMixin: function () { 517 var root = this.get('rootState'); 518 519 //@if(debug) 520 var traceKey = this.get('statechartTraceKey'); 521 522 this.removeObserver(traceKey, this, '_statechartTraceDidChange'); 523 //@endif 524 525 root.destroy(); 526 this.set('rootState', null); 527 this.notifyPropertyChange('currentStates'); 528 }, 529 530 /** 531 Initializes the statechart. By initializing the statechart, it will create all the states and register 532 them with the statechart. Once complete, the statechart can be used to go to states and send events to. 533 */ 534 initStatechart: function () { 535 if (this.get('statechartIsInitialized')) return; 536 537 this._gotoStateLocked = NO; 538 this._sendEventLocked = NO; 539 this._pendingStateTransitions = []; 540 this._pendingSentEvents = []; 541 542 this.sendAction = this.sendEvent; 543 544 //@if(debug) 545 if (this.get('monitorIsActive')) { 546 this.set('monitor', SC.StatechartMonitor.create({ statechart: this })); 547 } 548 549 var traceKey = this.get('statechartTraceKey'), 550 trace = this.get('allowStatechartTracing'); 551 552 this.addObserver(traceKey, this, '_statechartTraceDidChange'); 553 this._statechartTraceDidChange(); 554 555 if (trace) this.statechartLogTrace("BEGIN initialize statechart", SC.TRACE_STATECHART_STYLE.init); 556 //@endif 557 558 var rootState = this.get('rootState'), 559 msg; 560 561 // If no root state was explicitly defined then try to construct 562 // a root state class 563 if (!rootState) { 564 rootState = this._constructRootStateClass(); 565 } 566 else if (SC.typeOf(rootState) === SC.T_FUNCTION && rootState.statePlugin) { 567 rootState = rootState.apply(this); 568 } 569 570 if (!(SC.kindOf(rootState, SC.State) && rootState.isClass)) { 571 msg = "Unable to initialize statechart. Root state must be a state class"; 572 this.statechartLogError(msg); 573 SC.throw(msg); 574 } 575 576 rootState = this.createRootState(rootState, { 577 statechart: this, 578 name: SC.ROOT_STATE_NAME 579 }); 580 581 this.set('rootState', rootState); 582 rootState.initState(); 583 584 if (SC.kindOf(rootState.get('initialSubstate'), SC.EmptyState)) { 585 msg = "Unable to initialize statechart. Root state must have an initial substate explicitly defined"; 586 this.statechartLogError(msg); 587 SC.throw(msg); 588 } 589 590 if (!SC.empty(this.get('initialState'))) { 591 var key = 'initialState'; 592 this.set(key, rootState.get(this.get(key))); 593 } 594 595 this.set('statechartIsInitialized', YES); 596 this.gotoState(rootState); 597 598 //@if(debug) 599 if (trace) this.statechartLogTrace("END initialize statechart", SC.TRACE_STATECHART_STYLE.init); 600 //@endif 601 }, 602 603 /** 604 Will create a root state for the statechart 605 */ 606 createRootState: function (state, attrs) { 607 if (!attrs) attrs = {}; 608 state = state.create(attrs); 609 return state; 610 }, 611 612 /** 613 Returns an array of all the current states for this statechart 614 615 @returns {Array} the current states 616 */ 617 currentStates: function () { 618 return this.getPath('rootState.currentSubstates'); 619 }.property().cacheable(), 620 621 /** 622 Returns the first current state for this statechart. 623 624 @return {SC.State} 625 */ 626 firstCurrentState: function () { 627 var cs = this.get('currentStates'); 628 return cs ? cs.objectAt(0) : null; 629 }.property('currentStates').cacheable(), 630 631 /** 632 Returns the count of the current states for this statechart 633 634 @returns {Number} the count 635 */ 636 currentStateCount: function () { 637 return this.getPath('currentStates.length'); 638 }.property('currentStates').cacheable(), 639 640 /** 641 Checks if a given state is a current state of this statechart. 642 643 @param state {State|String} the state to check 644 @returns {Boolean} true if the state is a current state, otherwise fals is returned 645 */ 646 stateIsCurrentState: function (state) { 647 return this.get('rootState').stateIsCurrentSubstate(state); 648 }, 649 650 /** 651 Returns an array of all the states that are currently entered for 652 this statechart. 653 654 @returns {Array} the currently entered states 655 */ 656 enteredStates: function () { 657 return this.getPath('rootState.enteredSubstates'); 658 }.property().cacheable(), 659 660 /** 661 Checks if a given state is a currently entered state of this statechart. 662 663 @param state {State|String} the state to check 664 @returns {Boolean} true if the state is a currently entered state, otherwise false is returned 665 */ 666 stateIsEntered: function (state) { 667 return this.get('rootState').stateIsEnteredSubstate(state); 668 }, 669 670 /** 671 Checks if the given value represents a state is this statechart 672 673 @param value {State|String} either a state object or the name of a state 674 @returns {Boolean} true if the state does belong ot the statechart, otherwise false is returned 675 */ 676 doesContainState: function (value) { 677 return !SC.none(this.getState(value)); 678 }, 679 680 /** 681 Gets a state from the statechart that matches the given value 682 683 @param value {State|String} either a state object of the name of a state 684 @returns {State} if a match then the matching state is returned, otherwise null is returned 685 */ 686 getState: function (state) { 687 var root = this.get('rootState'); 688 return root === state ? root : root.getSubstate(state); 689 }, 690 691 /** 692 When called, the statechart will proceed with making state transitions in the statechart starting from 693 a current state that meet the statechart conditions. When complete, some or all of the statechart's 694 current states will be changed, and all states that were part of the transition process will either 695 be exited or entered in a specific order. 696 697 The state that is given to go to will not necessarily be a current state when the state transition process 698 is complete. The final state or states are dependent on factors such an initial substates, concurrent 699 states, and history states. 700 701 Because the statechart can have one or more current states, it may be necessary to indicate what current state 702 to start from. If no current state to start from is provided, then the statechart will default to using 703 the first current state that it has; depending of the make up of the statechart (no concurrent state vs. 704 with concurrent states), the outcome may be unexpected. For a statechart with concurrent states, it is best 705 to provide a current state in which to start from. 706 707 When using history states, the statechart will first make transitions to the given state and then use that 708 state's history state and recursively follow each history state's history state until there are no 709 more history states to follow. If the given state does not have a history state, then the statechart 710 will continue following state transition procedures. 711 712 Method can be called in the following ways: 713 714 // With one argument. 715 gotoState(<state>) 716 717 // With two argument. 718 gotoState(<state>, <state | boolean | hash>) 719 720 // With three argument. 721 gotoState(<state>, <state>, <boolean | hash>) 722 gotoState(<state>, <boolean>, <hash>) 723 724 // With four argument. 725 gotoState(<state>, <state>, <boolean>, <hash>) 726 727 where <state> is either a SC.State object or a string and <hash> is a regular JS hash object. 728 729 @param state {SC.State|String} the state to go to (may not be the final state in the transition process) 730 @param fromCurrentState {SC.State|String} Optional. The current state to start the transition process from. 731 @param useHistory {Boolean} Optional. Indicates whether to include using history states in the transition process 732 @param context {Hash} Optional. A context object that will be passed to all exited and entered states 733 */ 734 gotoState: function (state, fromCurrentState, useHistory, context) { 735 if (!this.get('statechartIsInitialized')) { 736 this.statechartLogError("can not go to state %@. statechart has not yet been initialized".fmt(state)); 737 return; 738 } 739 740 if (this.get('isDestroyed')) { 741 this.statechartLogError("can not go to state %@. statechart is destroyed".fmt(this)); 742 return; 743 } 744 745 // Fast arguments access. 746 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 747 var args = new Array(arguments.length); // SC.$A(arguments) 748 for (var i = 0, len = args.length; i < len; i++) { args[i] = arguments[i]; } 749 750 args = this._processGotoStateArgs(args); 751 752 state = args.state; 753 fromCurrentState = args.fromCurrentState; 754 useHistory = args.useHistory; 755 context = args.context; 756 757 var pivotState = null, 758 exitStates = [], 759 enterStates = [], 760 paramState = state, 761 paramFromCurrentState = fromCurrentState, 762 msg; 763 764 state = this.getState(state); 765 766 if (SC.none(state)) { 767 this.statechartLogError("Can not to goto state %@. Not a recognized state in statechart".fmt(paramState)); 768 return; 769 } 770 771 if (this._gotoStateLocked) { 772 // There is a state transition currently happening. Add this requested state 773 // transition to the queue of pending state transitions. The request will 774 // be invoked after the current state transition is finished. 775 this._pendingStateTransitions.push({ 776 state: state, 777 fromCurrentState: fromCurrentState, 778 useHistory: useHistory, 779 context: context 780 }); 781 782 return; 783 } 784 785 // Lock the current state transition so that no other requested state transition 786 // interferes. 787 this._gotoStateLocked = YES; 788 789 if (fromCurrentState) { 790 // Check to make sure the current state given is actually a current state of this statechart 791 fromCurrentState = this.getState(fromCurrentState); 792 if (SC.none(fromCurrentState) || !fromCurrentState.get('isCurrentState')) { 793 msg = "Can not to goto state %@. %@ is not a recognized current state in statechart"; 794 this.statechartLogError(msg.fmt(paramState, paramFromCurrentState)); 795 this._gotoStateLocked = NO; 796 return; 797 } 798 } 799 else { 800 // No explicit current state to start from; therefore, need to find a current state 801 // to transition from. 802 fromCurrentState = state.findFirstRelativeCurrentState(); 803 if (!fromCurrentState) fromCurrentState = this.get('firstCurrentState'); 804 } 805 806 //@if(debug) 807 var trace = this.get('allowStatechartTracing'); 808 if (trace) { 809 this.statechartLogTrace("BEGIN gotoState: %@".fmt(state), SC.TRACE_STATECHART_STYLE.gotoState); 810 msg = " starting from current state: %@"; 811 msg = msg.fmt(fromCurrentState ? fromCurrentState : '-- none --'); 812 this.statechartLogTrace(msg, SC.TRACE_STATECHART_STYLE.gotoStateInfo); 813 814 var len = this.getPath('currentStates.length'); 815 // For many states, we list each on its own line. 816 if (len > 2) { 817 msg = "current states before:\n%@"; 818 msg = msg.fmt(this.get('currentStates').getEach('fullPath').join('\n')); 819 } 820 // For a few states, all on one line. 821 else if (len > 0) { 822 msg = " current states before: %@"; 823 msg = msg.fmt(this.get('currentStates').getEach('fullPath').join(', ')); 824 } 825 // For no states, no states. 826 else { 827 msg = " current states before: --none--"; 828 } 829 this.statechartLogTrace(msg, SC.TRACE_STATECHART_STYLE.gotoStateInfo); 830 } 831 //@endif 832 833 // If there is a current state to start the transition process from, then determine what 834 // states are to be exited 835 if (!SC.none(fromCurrentState)) { 836 exitStates = this._createStateChain(fromCurrentState); 837 } 838 839 // Now determine the initial states to be entered 840 enterStates = this._createStateChain(state); 841 842 // Get the pivot state to indicate when to go from exiting states to entering states 843 pivotState = this._findPivotState(exitStates, enterStates); 844 845 if (pivotState) { 846 //@if(debug) 847 if (trace) this.statechartLogTrace(" pivot state = %@".fmt(pivotState), SC.TRACE_STATECHART_STYLE.gotoStateInfo); 848 //@endif 849 if (pivotState.get('substatesAreConcurrent') && pivotState !== state) { 850 this.statechartLogError("Can not go to state %@ from %@. Pivot state %@ has concurrent substates.".fmt(state, fromCurrentState, pivotState)); 851 this._gotoStateLocked = NO; 852 return; 853 } 854 } 855 856 // Collect what actions to perform for the state transition process 857 var gotoStateActions = []; 858 859 860 // Go ahead and find states that are to be exited 861 this._traverseStatesToExit(exitStates.shift(), exitStates, pivotState, gotoStateActions); 862 863 // Now go find states that are to be entered 864 if (pivotState !== state) { 865 this._traverseStatesToEnter(enterStates.pop(), enterStates, pivotState, useHistory, gotoStateActions); 866 } else { 867 this._traverseStatesToExit(pivotState, [], null, gotoStateActions); 868 this._traverseStatesToEnter(pivotState, null, null, useHistory, gotoStateActions); 869 } 870 871 // Collected all the state transition actions to be performed. Now execute them. 872 this._gotoStateActions = gotoStateActions; 873 this._executeGotoStateActions(state, gotoStateActions, null, context); 874 }, 875 876 /** 877 Indicates if the statechart is in an active goto state process 878 */ 879 gotoStateActive: function () { 880 return this._gotoStateLocked; 881 }.property(), 882 883 /** 884 Indicates if the statechart is in an active goto state process 885 that has been suspended 886 */ 887 gotoStateSuspended: function () { 888 return this._gotoStateLocked && !!this._gotoStateSuspendedPoint; 889 }.property(), 890 891 /** 892 Resumes an active goto state transition process that has been suspended. 893 */ 894 resumeGotoState: function () { 895 if (!this.get('gotoStateSuspended')) { 896 this.statechartLogError("Can not resume goto state since it has not been suspended"); 897 return; 898 } 899 900 var point = this._gotoStateSuspendedPoint; 901 this._executeGotoStateActions(point.gotoState, point.actions, point.marker, point.context); 902 }, 903 904 /** @private */ 905 _executeGotoStateActions: function (gotoState, actions, marker, context) { 906 var action = null, 907 len = actions.length, 908 actionResult = null; 909 910 marker = SC.none(marker) ? 0 : marker; 911 912 for (; marker < len; marker += 1) { 913 this._currentGotoStateAction = action = actions[marker]; 914 switch (action.action) { 915 case SC.EXIT_STATE: 916 actionResult = this._exitState(action.state, context); 917 break; 918 919 case SC.ENTER_STATE: 920 actionResult = this._enterState(action.state, action.currentState, context); 921 break; 922 } 923 924 // 925 // Check if the state wants to perform an asynchronous action during 926 // the state transition process. If so, then we need to first 927 // suspend the state transition process and then invoke the 928 // asynchronous action. Once called, it is then up to the state or something 929 // else to resume this statechart's state transition process by calling the 930 // statechart's resumeGotoState method. 931 // 932 if (SC.kindOf(actionResult, SC.Async)) { 933 this._gotoStateSuspendedPoint = { 934 gotoState: gotoState, 935 actions: actions, 936 marker: marker + 1, 937 context: context 938 }; 939 940 actionResult.tryToPerform(action.state); 941 return; 942 } 943 } 944 945 this.beginPropertyChanges(); 946 this.notifyPropertyChange('currentStates'); 947 this.notifyPropertyChange('enteredStates'); 948 this.endPropertyChanges(); 949 950 //@if(debug) 951 if (this.get('allowStatechartTracing')) { 952 if (this.getPath('currentStates.length') > 2) { 953 this.statechartLogTrace(" current states after:\n%@".fmt(this.get('currentStates').getEach('fullPath').join(' \n')), SC.TRACE_STATECHART_STYLE.gotoStateInfo); 954 } else { 955 this.statechartLogTrace(" current states after: %@".fmt(this.get('currentStates').getEach('fullPath').join(', ')), SC.TRACE_STATECHART_STYLE.gotoStateInfo); 956 } 957 this.statechartLogTrace("END gotoState: %@".fmt(gotoState), SC.TRACE_STATECHART_STYLE.gotoState); 958 } 959 //@endif 960 961 this._cleanupStateTransition(); 962 }, 963 964 /** @private */ 965 _cleanupStateTransition: function () { 966 this._currentGotoStateAction = null; 967 this._gotoStateSuspendedPoint = null; 968 this._gotoStateActions = null; 969 this._gotoStateLocked = NO; 970 this._flushPendingStateTransition(); 971 // Check the flags so we only flush if the events will actually get sent. 972 if (!this._sendEventLocked && !this._gotoStateLocked) { this._flushPendingSentEvents(); } 973 }, 974 975 /** @private */ 976 _exitState: function (state, context) { 977 var parentState; 978 979 if (state.get('currentSubstates').indexOf(state) >= 0) { 980 parentState = state.get('parentState'); 981 while (parentState) { 982 parentState.get('currentSubstates').removeObject(state); 983 parentState.notifyPropertyChange('currentSubstates'); 984 parentState = parentState.get('parentState'); 985 } 986 } 987 988 parentState = state; 989 while (parentState) { 990 parentState.get('enteredSubstates').removeObject(state); 991 parentState.notifyPropertyChange('enteredSubstates'); 992 parentState = parentState.get('parentState'); 993 } 994 995 //@if(debug) 996 if (this.get('allowStatechartTracing')) { 997 this.statechartLogTrace("<-- exiting state: %@".fmt(state), SC.TRACE_STATECHART_STYLE.exit); 998 } 999 //@endif 1000 1001 state.set('currentSubstates', []); 1002 state.notifyPropertyChange('currentSubstates'); 1003 1004 state.stateWillBecomeExited(context); 1005 var result = this.exitState(state, context); 1006 state.stateDidBecomeExited(context); 1007 1008 //@if(debug) 1009 if (this.get('monitorIsActive')) this.get('monitor').pushExitedState(state); 1010 //@endif 1011 1012 state._traverseStatesToExit_skipState = NO; 1013 1014 return result; 1015 }, 1016 1017 /** 1018 What will actually invoke a state's exitState method. 1019 1020 Called during the state transition process whenever the gotoState method is 1021 invoked. 1022 1023 @param state {SC.State} the state whose enterState method is to be invoked 1024 @param context {Hash} a context hash object to provide the enterState method 1025 */ 1026 exitState: function (state, context) { 1027 return state.exitState(context); 1028 }, 1029 1030 /** @private */ 1031 _enterState: function (state, current, context) { 1032 var parentState = state.get('parentState'); 1033 if (parentState && !state.get('isConcurrentState')) parentState.set('historyState', state); 1034 1035 if (current) { 1036 parentState = state; 1037 while (parentState) { 1038 parentState.get('currentSubstates').pushObject(state); 1039 parentState.notifyPropertyChange('currentSubstates'); 1040 parentState = parentState.get('parentState'); 1041 } 1042 } 1043 1044 parentState = state; 1045 while (parentState) { 1046 parentState.get('enteredSubstates').pushObject(state); 1047 parentState.notifyPropertyChange('enteredSubstates'); 1048 parentState = parentState.get('parentState'); 1049 } 1050 1051 //@if(debug) 1052 if (this.get('allowStatechartTracing')) { 1053 if (state.enterStateByRoute && SC.kindOf(context, SC.StateRouteHandlerContext)) { 1054 this.statechartLogTrace("--> entering state (by route): %@".fmt(state), SC.TRACE_STATECHART_STYLE.enter); 1055 } else { 1056 this.statechartLogTrace("--> entering state: %@".fmt(state), SC.TRACE_STATECHART_STYLE.enter); 1057 } 1058 } 1059 //@endif 1060 1061 state.stateWillBecomeEntered(context); 1062 var result = this.enterState(state, context); 1063 state.stateDidBecomeEntered(context); 1064 1065 //@if(debug) 1066 if (this.get('monitorIsActive')) this.get('monitor').pushEnteredState(state); 1067 //@endif 1068 1069 return result; 1070 }, 1071 1072 /** 1073 What will actually invoke a state's enterState method. 1074 1075 Called during the state transition process whenever the gotoState method is 1076 invoked. 1077 1078 If the context provided is a state route context object 1079 ({@link SC.StateRouteContext}), then if the given state has a enterStateByRoute 1080 method, that method will be invoked, otherwise the state's enterState method 1081 will be invoked by default. The state route context object will be supplied to 1082 both enter methods in either case. 1083 1084 @param state {SC.State} the state whose enterState method is to be invoked 1085 @param context {Hash} a context hash object to provide the enterState method 1086 */ 1087 enterState: function (state, context) { 1088 if (state.enterStateByRoute && SC.kindOf(context, SC.StateRouteHandlerContext)) { 1089 return state.enterStateByRoute(context); 1090 } else { 1091 return state.enterState(context); 1092 } 1093 }, 1094 1095 /** 1096 When called, the statechart will proceed to make transitions to the given state then follow that 1097 state's history state. 1098 1099 You can either go to a given state's history recursively or non-recursively. To go to a state's history 1100 recursively means to following each history state's history state until no more history states can be 1101 followed. Non-recursively means to just to the given state's history state but do not recusively follow 1102 history states. If the given state does not have a history state, then the statechart will just follow 1103 normal procedures when making state transitions. 1104 1105 Because a statechart can have one or more current states, depending on if the statechart has any concurrent 1106 states, it is optional to provided current state in which to start the state transition process from. If no 1107 current state is provided, then the statechart will default to the first current state that it has; which, 1108 depending on the make up of that statechart, can lead to unexpected outcomes. For a statechart with concurrent 1109 states, it is best to explicitly supply a current state. 1110 1111 Method can be called in the following ways: 1112 1113 // With one arguments. 1114 gotoHistoryState(<state>) 1115 1116 // With two arguments. 1117 gotoHistoryState(<state>, <state | boolean | hash>) 1118 1119 // With three arguments. 1120 gotoHistoryState(<state>, <state>, <boolean | hash>) 1121 gotoHistoryState(<state>, <boolean>, <hash>) 1122 1123 // With four argumetns 1124 gotoHistoryState(<state>, <state>, <boolean>, <hash>) 1125 1126 where <state> is either a SC.State object or a string and <hash> is a regular JS hash object. 1127 1128 @param state {SC.State|String} the state to go to and follow it's history state 1129 @param fromCurrentState {SC.State|String} Optional. the current state to start the state transition process from 1130 @param recursive {Boolean} Optional. whether to follow history states recursively. 1131 */ 1132 gotoHistoryState: function (state, fromCurrentState, recursive, context) { 1133 if (!this.get('statechartIsInitialized')) { 1134 this.statechartLogError("can not go to state %@'s history state. Statechart has not yet been initialized".fmt(state)); 1135 return; 1136 } 1137 1138 // Fast arguments access. 1139 // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly. 1140 var args = new Array(arguments.length); // SC.$A(arguments) 1141 for (var i = 0, len = args.length; i < len; i++) { args[i] = arguments[i]; } 1142 1143 args = this._processGotoStateArgs(args); 1144 1145 state = args.state; 1146 fromCurrentState = args.fromCurrentState; 1147 recursive = args.useHistory; 1148 context = args.context; 1149 1150 state = this.getState(state); 1151 1152 if (!state) { 1153 this.statechartLogError("Can not to goto state %@'s history state. Not a recognized state in statechart".fmt(state)); 1154 return; 1155 } 1156 1157 var historyState = state.get('historyState'); 1158 1159 if (!recursive) { 1160 if (historyState) { 1161 this.gotoState(historyState, fromCurrentState, context); 1162 } else { 1163 this.gotoState(state, fromCurrentState, context); 1164 } 1165 } else { 1166 this.gotoState(state, fromCurrentState, YES, context); 1167 } 1168 }, 1169 1170 /** 1171 Sends a given event to all the statechart's current states. 1172 1173 If a current state does can not respond to the sent event, then the current state's parent state 1174 will be tried. This process is recursively done until no more parent state can be tried. 1175 1176 Note that a state will only be checked once if it can respond to an event. Therefore, if 1177 there is a state S that handles event foo and S has concurrent substates, then foo will 1178 only be invoked once; not as many times as there are substates. 1179 1180 @param event {String} name of the event 1181 @param arg1 {Object} optional argument 1182 @param arg2 {Object} optional argument 1183 @returns {SC.Responder} the responder that handled it or null 1184 1185 @see #stateWillTryToHandleEvent 1186 @see #stateDidTryToHandleEvent 1187 */ 1188 sendEvent: function (event, arg1, arg2) { 1189 1190 if (this.get('isDestroyed')) { 1191 this.statechartLogError("can not send event %@. statechart is destroyed".fmt(event)); 1192 return; 1193 } 1194 1195 var statechartHandledEvent = NO, 1196 result = this, 1197 eventHandled = NO, 1198 currentStates = this.get('currentStates').slice(), 1199 checkedStates = {}, 1200 len = 0, 1201 i = 0, 1202 state = null; 1203 1204 if (this._sendEventLocked || this._gotoStateLocked) { 1205 // Want to prevent any actions from being processed by the states until 1206 // they have had a chance to handle the most immediate action or completed 1207 // a state transition 1208 this._pendingSentEvents.push({ 1209 event: event, 1210 arg1: arg1, 1211 arg2: arg2 1212 }); 1213 1214 return; 1215 } 1216 1217 this._sendEventLocked = YES; 1218 1219 //@if(debug) 1220 var trace = this.get('allowStatechartTracing'); 1221 if (trace) { 1222 this.statechartLogTrace("BEGIN sendEvent: '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.action); 1223 } 1224 //@endif 1225 1226 len = currentStates.get('length'); 1227 for (; i < len; i += 1) { 1228 eventHandled = NO; 1229 state = currentStates[i]; 1230 if (!state.get('isCurrentState')) continue; 1231 while (!eventHandled && state) { 1232 if (!checkedStates[state.get('fullPath')]) { 1233 eventHandled = state.tryToHandleEvent(event, arg1, arg2); 1234 checkedStates[state.get('fullPath')] = YES; 1235 } 1236 if (!eventHandled) state = state.get('parentState'); 1237 else statechartHandledEvent = YES; 1238 } 1239 } 1240 1241 // Now that all the states have had a chance to process the 1242 // first event, we can go ahead and flush any pending sent events. 1243 this._sendEventLocked = NO; 1244 1245 //@if(debug) 1246 if (trace) { 1247 if (!statechartHandledEvent) this.statechartLogTrace("No state was able to handle event %@".fmt(event), SC.TRACE_STATECHART_STYLE.action); 1248 this.statechartLogTrace("END sendEvent: '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.action); 1249 } 1250 //@endif 1251 1252 // Check if the flags are unlocked. These means any pending events 1253 // will successfully send, so go ahead and flush. Otherwise, events 1254 // would become out of order since the first event would get shifted, 1255 // then pushed. 1256 if (!this._sendEventLocked && !this._gotoStateLocked) { 1257 result = this._flushPendingSentEvents(); 1258 } 1259 1260 return statechartHandledEvent ? this : (result ? this : null); 1261 }, 1262 1263 /** 1264 Used to notify the statechart that a state will try to handle event that has been passed 1265 to it. 1266 1267 @param {SC.State} state the state that will try to handle the event 1268 @param {String} event the event the state will try to handle 1269 @param {String} handler the name of the method on the state that will try to handle the event 1270 */ 1271 stateWillTryToHandleEvent: function (state, event, handler) { 1272 this._stateHandleEventInfo = { 1273 state: state, 1274 event: event, 1275 handler: handler 1276 }; 1277 }, 1278 1279 /** 1280 Used to notify the statechart that a state did try to handle event that has been passed 1281 to it. 1282 1283 @param {SC.State} state the state that did try to handle the event 1284 @param {String} event the event the state did try to handle 1285 @param {String} handler the name of the method on the state that did try to handle the event 1286 @param {Boolean} handled indicates if the handler was able to handle the event 1287 */ 1288 stateDidTryToHandleEvent: function (state, event, handler, handled) { 1289 this._stateHandleEventInfo = null; 1290 }, 1291 1292 /** @private 1293 1294 Creates a chain of states from the given state to the greatest ancestor state (the root state). Used 1295 when perform state transitions. 1296 */ 1297 _createStateChain: function (state) { 1298 var chain = []; 1299 1300 while (state) { 1301 chain.push(state); 1302 state = state.get('parentState'); 1303 } 1304 1305 return chain; 1306 }, 1307 1308 /** @private 1309 1310 Finds a pivot state from two given state chains. The pivot state is the state indicating when states 1311 go from being exited to states being entered during the state transition process. The value 1312 returned is the fist matching state between the two given state chains. 1313 */ 1314 _findPivotState: function (stateChain1, stateChain2) { 1315 if (stateChain1.length === 0 || stateChain2.length === 0) return null; 1316 1317 var pivot = stateChain1.find(function (state, index) { 1318 if (stateChain2.indexOf(state) >= 0) return YES; 1319 }); 1320 1321 return pivot; 1322 }, 1323 1324 /** @private 1325 1326 Recursively follow states that are to be exited during a state transition process. The exit 1327 process is to start from the given state and work its way up to when either all exit 1328 states have been reached based on a given exit path or when a stop state has been reached. 1329 1330 @param state {State} the state to be exited 1331 @param exitStatePath {Array} an array representing a path of states that are to be exited 1332 @param stopState {State} an explicit state in which to stop the exiting process 1333 */ 1334 _traverseStatesToExit: function (state, exitStatePath, stopState, gotoStateActions) { 1335 if (!state || state === stopState) return; 1336 1337 // This state has concurrent substates. Therefore we have to make sure we 1338 // exit them up to this state before we can go any further up the exit chain. 1339 if (state.get('substatesAreConcurrent')) { 1340 var i = 0, 1341 currentSubstates = state.get('currentSubstates'), 1342 len = currentSubstates.length, 1343 currentState = null; 1344 1345 for (; i < len; i += 1) { 1346 currentState = currentSubstates[i]; 1347 if (currentState._traverseStatesToExit_skipState === YES) continue; 1348 var chain = this._createStateChain(currentState); 1349 this._traverseStatesToExit(chain.shift(), chain, state, gotoStateActions); 1350 } 1351 } 1352 1353 gotoStateActions.push({ action: SC.EXIT_STATE, state: state }); 1354 if (state.get('isCurrentState')) state._traverseStatesToExit_skipState = YES; 1355 this._traverseStatesToExit(exitStatePath.shift(), exitStatePath, stopState, gotoStateActions); 1356 }, 1357 1358 /** @private 1359 1360 Recursively follow states that are to be entered during the state transition process. The 1361 enter process is to start from the given state and work its way down a given enter path. When 1362 the end of enter path has been reached, then continue entering states based on whether 1363 an initial substate is defined, there are concurrent substates or history states are to be 1364 followed; when none of those condition are met then the enter process is done. 1365 1366 @param state {State} the sate to be entered 1367 @param enterStatePath {Array} an array representing an initial path of states that are to be entered 1368 @param pivotState {State} The state pivoting when to go from exiting states to entering states 1369 @param useHistory {Boolean} indicates whether to recursively follow history states 1370 */ 1371 _traverseStatesToEnter: function (state, enterStatePath, pivotState, useHistory, gotoStateActions) { 1372 if (!state) return; 1373 1374 // We do not want to enter states in the enter path until the pivot state has been reached. After 1375 // the pivot state has been reached, then we can go ahead and actually enter states. 1376 if (pivotState) { 1377 if (state !== pivotState) { 1378 this._traverseStatesToEnter(enterStatePath.pop(), enterStatePath, pivotState, useHistory, gotoStateActions); 1379 } else { 1380 this._traverseStatesToEnter(enterStatePath.pop(), enterStatePath, null, useHistory, gotoStateActions); 1381 } 1382 } 1383 1384 // If no more explicit enter path instructions, then default to enter states based on 1385 // other criteria 1386 else if (!enterStatePath || enterStatePath.length === 0) { 1387 var gotoStateAction = { action: SC.ENTER_STATE, state: state, currentState: NO }; 1388 gotoStateActions.push(gotoStateAction); 1389 1390 var initialSubstate = state.get('initialSubstate'), 1391 historyState = state.get('historyState'); 1392 1393 // State has concurrent substates. Need to enter all of the substates 1394 if (state.get('substatesAreConcurrent')) { 1395 this._traverseConcurrentStatesToEnter(state.get('substates'), null, useHistory, gotoStateActions); 1396 } 1397 1398 // State has substates and we are instructed to recursively follow the state's 1399 // history state if it has one. 1400 else if (state.get('hasSubstates') && historyState && useHistory) { 1401 this._traverseStatesToEnter(historyState, null, null, useHistory, gotoStateActions); 1402 } 1403 1404 // State has an initial substate to enter 1405 else if (initialSubstate) { 1406 if (SC.kindOf(initialSubstate, SC.HistoryState)) { 1407 if (!useHistory) useHistory = initialSubstate.get('isRecursive'); 1408 initialSubstate = initialSubstate.get('state'); 1409 } 1410 this._traverseStatesToEnter(initialSubstate, null, null, useHistory, gotoStateActions); 1411 } 1412 1413 // Looks like we hit the end of the road. Therefore the state has now become 1414 // a current state of the statechart. 1415 else { 1416 gotoStateAction.currentState = YES; 1417 } 1418 } 1419 1420 // Still have an explicit enter path to follow, so keep moving through the path. 1421 else if (enterStatePath.length > 0) { 1422 gotoStateActions.push({ action: SC.ENTER_STATE, state: state }); 1423 var nextState = enterStatePath.pop(); 1424 this._traverseStatesToEnter(nextState, enterStatePath, null, useHistory, gotoStateActions); 1425 1426 // We hit a state that has concurrent substates. Must go through each of the substates 1427 // and enter them 1428 if (state.get('substatesAreConcurrent')) { 1429 this._traverseConcurrentStatesToEnter(state.get('substates'), nextState, useHistory, gotoStateActions); 1430 } 1431 } 1432 }, 1433 1434 /** @override 1435 1436 Returns YES if the named value translates into an executable function on 1437 any of the statechart's current states or the statechart itself. 1438 1439 @param event {String} the property name to check 1440 @returns {Boolean} 1441 */ 1442 respondsTo: function (event) { 1443 // Fast path! 1444 if (this.get('isDestroyed')) { 1445 this.statechartLogError("can not respond to event %@. statechart is destroyed".fmt(event)); 1446 return false; 1447 } 1448 1449 var currentStates = this.get('currentStates'), 1450 len = currentStates.get('length'), 1451 i = 0, state = null; 1452 1453 for (; i < len; i += 1) { 1454 state = currentStates.objectAt(i); 1455 while (state) { 1456 if (state.respondsToEvent(event)) return true; 1457 state = state.get('parentState'); 1458 } 1459 } 1460 1461 // None of the current states can respond. Now check the statechart itself 1462 return SC.typeOf(this[event]) === SC.T_FUNCTION; 1463 }, 1464 1465 /** @override 1466 1467 Attempts to handle a given event against any of the statechart's current states and the 1468 statechart itself. If any current state can handle the event or the statechart itself can 1469 handle the event then YES is returned, otherwise NO is returned. 1470 1471 @param event {String} what to perform 1472 @param arg1 {Object} Optional 1473 @param arg2 {Object} Optional 1474 @returns {Boolean} YES if handled, NO if not handled 1475 */ 1476 tryToPerform: function (event, arg1, arg2) { 1477 if (!this.respondsTo(event)) return NO; 1478 1479 if (SC.typeOf(this[event]) === SC.T_FUNCTION) { 1480 var result = this[event](arg1, arg2); 1481 if (result !== NO) return YES; 1482 } 1483 1484 return !!this.sendEvent(event, arg1, arg2); 1485 }, 1486 1487 /** 1488 Used to invoke a method on current states. If the method can not be executed 1489 on a current state, then the state's parent states will be tried in order 1490 of closest ancestry. 1491 1492 A few notes: 1493 1494 1. Calling this is not the same as calling sendEvent or sendAction. 1495 Rather, this should be seen as calling normal methods on a state that 1496 will *not* call gotoState or gotoHistoryState. 1497 2. A state will only ever be invoked once per call. So if there are two 1498 or more current states that have the same parent state, then that parent 1499 state will only be invoked once if none of the current states are able 1500 to invoke the given method. 1501 1502 When calling this method, you are able to supply zero ore more arguments 1503 that can be pass onto the method called on the states. As an example 1504 1505 invokeStateMethod('render', context, firstTime); 1506 1507 The above call will invoke the render method on the current states 1508 and supply the context and firstTime arguments to the method. 1509 1510 Because a statechart can have more than one current state and the method 1511 invoked may return a value, the addition of a callback function may be provided 1512 in order to handle the returned value for each state. As an example, let's say 1513 we want to call a calculate method on the current states where the method 1514 will return a value when invoked. We can handle the returned values like so: 1515 1516 invokeStateMethod('calculate', value, function (state, result) { 1517 // .. handle the result returned from calculate that was invoked 1518 // on the given state 1519 }) 1520 1521 If the method invoked does not return a value and a callback function is 1522 supplied, then result value will simply be undefined. In all cases, if 1523 a callback function is given, it must be the last value supplied to this 1524 method. 1525 1526 invokeStateMethod will return a value if only one state was able to have 1527 the given method invoked on it, otherwise no value is returned. 1528 1529 @param methodName {String} methodName a method name 1530 @param args {Object...} Optional. any additional arguments 1531 @param func {Function} Optional. a callback function. Must be the last 1532 value supplied if provided. 1533 1534 @returns a value if the number of current states is one, otherwise undefined 1535 is returned. The value is the result of the method that got invoked 1536 on a state. 1537 */ 1538 invokeStateMethod: function (methodName, args, func) { 1539 if (methodName === 'unknownEvent') { 1540 this.statechartLogError("can not invoke method unkownEvent"); 1541 return; 1542 } 1543 1544 args = SC.A(arguments); 1545 args.shift(); 1546 1547 var len = args.length, 1548 arg = len > 0 ? args[len - 1] : null, 1549 callback = SC.typeOf(arg) === SC.T_FUNCTION ? args.pop() : null, 1550 currentStates = this.get('currentStates'), 1551 i = 0, state = null, 1552 checkedStates = {}, 1553 method, result, 1554 calledStates = 0; 1555 1556 len = currentStates.get('length'); 1557 1558 for (; i < len; i += 1) { 1559 state = currentStates.objectAt(i); 1560 while (state) { 1561 if (checkedStates[state.get('fullPath')]) break; 1562 checkedStates[state.get('fullPath')] = YES; 1563 method = state[methodName]; 1564 if (SC.typeOf(method) === SC.T_FUNCTION && !method.isEventHandler) { 1565 result = method.apply(state, args); 1566 if (callback) callback.call(this, state, result); 1567 calledStates += 1; 1568 break; 1569 } 1570 state = state.get('parentState'); 1571 } 1572 } 1573 1574 return calledStates === 1 ? result : undefined; 1575 }, 1576 1577 /** @private 1578 1579 Iterate over all the given concurrent states and enter them 1580 */ 1581 _traverseConcurrentStatesToEnter: function (states, exclude, useHistory, gotoStateActions) { 1582 var i = 0, 1583 len = states.length, 1584 state = null; 1585 1586 for (; i < len; i += 1) { 1587 state = states[i]; 1588 if (state !== exclude) this._traverseStatesToEnter(state, null, null, useHistory, gotoStateActions); 1589 } 1590 }, 1591 1592 /** @private 1593 1594 Called by gotoState to flush a pending state transition at the front of the 1595 pending queue. 1596 */ 1597 _flushPendingStateTransition: function () { 1598 if (!this._pendingStateTransitions) { 1599 this.statechartLogError("Unable to flush pending state transition. _pendingStateTransitions is invalid"); 1600 return; 1601 } 1602 var pending = this._pendingStateTransitions.shift(); 1603 if (!pending) return; 1604 this.gotoState(pending.state, pending.fromCurrentState, pending.useHistory, pending.context); 1605 }, 1606 1607 /** @private 1608 1609 Called by sendEvent to flush a pending actions at the front of the pending 1610 queue 1611 */ 1612 _flushPendingSentEvents: function () { 1613 var pending = this._pendingSentEvents.shift(); 1614 if (!pending) return null; 1615 return this.sendEvent(pending.event, pending.arg1, pending.arg2); 1616 }, 1617 1618 /** @private */ 1619 //@if(debug) 1620 _monitorIsActiveDidChange: function () { 1621 if (this.get('monitorIsActive') && SC.none(this.get('monitor'))) { 1622 this.set('monitor', SC.StatechartMonitor.create()); 1623 } 1624 }.observes('monitorIsActive'), 1625 //@endif 1626 1627 /** @private 1628 Will process the arguments supplied to the gotoState method. 1629 1630 TODO: Come back to this and refactor the code. It works, but it 1631 could certainly be improved 1632 */ 1633 _processGotoStateArgs: function (args) { 1634 var processedArgs = { 1635 state: null, 1636 fromCurrentState: null, 1637 useHistory: false, 1638 context: null 1639 }, 1640 len = null, 1641 value = null; 1642 1643 args = args.filter(function (item) { 1644 return item !== undefined; 1645 }); 1646 len = args.length; 1647 1648 if (len < 1) return processedArgs; 1649 1650 processedArgs.state = args[0]; 1651 1652 if (len === 2) { 1653 value = args[1]; 1654 switch (SC.typeOf(value)) { 1655 case SC.T_BOOL: 1656 processedArgs.useHistory = value; 1657 break; 1658 case SC.T_HASH: 1659 case SC.T_OBJECT: 1660 if (!SC.kindOf(value, SC.State)) { 1661 processedArgs.context = value; 1662 } 1663 break; 1664 default: 1665 processedArgs.fromCurrentState = value; 1666 } 1667 } 1668 else if (len === 3) { 1669 value = args[1]; 1670 if (SC.typeOf(value) === SC.T_BOOL) { 1671 processedArgs.useHistory = value; 1672 processedArgs.context = args[2]; 1673 } else { 1674 processedArgs.fromCurrentState = value; 1675 value = args[2]; 1676 if (SC.typeOf(value) === SC.T_BOOL) { 1677 processedArgs.useHistory = value; 1678 } else { 1679 processedArgs.context = value; 1680 } 1681 } 1682 } 1683 else { 1684 processedArgs.fromCurrentState = args[1]; 1685 processedArgs.useHistory = args[2]; 1686 processedArgs.context = args[3]; 1687 } 1688 1689 return processedArgs; 1690 }, 1691 1692 /** @private 1693 1694 Will return a newly constructed root state class. The root state will have substates added to 1695 it based on properties found on this state that derive from a SC.State class. For the 1696 root state to be successfully built, the following much be met: 1697 1698 - The rootStateExample property must be defined with a class that derives from SC.State 1699 - Either the initialState or statesAreConcurrent property must be set, but not both 1700 - There must be one or more states that can be added to the root state 1701 1702 */ 1703 _constructRootStateClass: function () { 1704 var rsExampleKey = 'rootStateExample', 1705 rsExample = this.get(rsExampleKey), 1706 initialState = this.get('initialState'), 1707 statesAreConcurrent = this.get('statesAreConcurrent'), 1708 stateCount = 0, 1709 key, value, valueIsFunc, attrs = {}; 1710 1711 if (SC.typeOf(rsExample) === SC.T_FUNCTION && rsExample.statePlugin) { 1712 rsExample = rsExample.apply(this); 1713 } 1714 1715 if (!(SC.kindOf(rsExample, SC.State) && rsExample.isClass)) { 1716 this._logStatechartCreationError("Invalid root state example"); 1717 return null; 1718 } 1719 1720 if (statesAreConcurrent && !SC.empty(initialState)) { 1721 this._logStatechartCreationError("Can not assign an initial state when states are concurrent"); 1722 } else if (statesAreConcurrent) { 1723 attrs.substatesAreConcurrent = YES; 1724 } else if (SC.typeOf(initialState) === SC.T_STRING) { 1725 attrs.initialSubstate = initialState; 1726 } else { 1727 this._logStatechartCreationError("Must either define initial state or assign states as concurrent"); 1728 return null; 1729 } 1730 1731 for (key in this) { 1732 if (key === rsExampleKey) continue; 1733 1734 value = this[key]; 1735 valueIsFunc = SC.typeOf(value) === SC.T_FUNCTION; 1736 1737 if (valueIsFunc && value.statePlugin) { 1738 value = value.apply(this); 1739 } 1740 1741 if (SC.kindOf(value, SC.State) && value.isClass && this[key] !== this.constructor) { 1742 attrs[key] = value; 1743 stateCount += 1; 1744 } 1745 } 1746 1747 if (stateCount === 0) { 1748 this._logStatechartCreationError("Must define one or more states"); 1749 return null; 1750 } 1751 1752 return rsExample.extend(attrs); 1753 }, 1754 1755 /** @private */ 1756 _logStatechartCreationError: function (msg) { 1757 SC.Logger.error("Unable to create statechart for %@: %@.".fmt(this, msg)); 1758 }, 1759 1760 /** 1761 Used to log a statechart error message 1762 */ 1763 statechartLogError: function (msg) { 1764 SC.Logger.error("ERROR %@: %@".fmt(this.get('statechartLogPrefix'), msg)); 1765 }, 1766 1767 /** 1768 Used to log a statechart warning message 1769 */ 1770 statechartLogWarning: function (msg) { 1771 if (this.get('suppressStatechartWarnings')) return; 1772 SC.Logger.warn("WARN %@: %@".fmt(this.get('statechartLogPrefix'), msg)); 1773 }, 1774 1775 /** @property */ 1776 statechartLogPrefix: function () { 1777 var className = SC._object_className(this.constructor), 1778 name = this.get('name'), prefix; 1779 1780 if (SC.empty(name)) prefix = "%@<%@>".fmt(className, SC.guidFor(this)); 1781 else prefix = "%@<%@, %@>".fmt(className, name, SC.guidFor(this)); 1782 1783 return prefix; 1784 }.property().cacheable() 1785 1786 }; 1787 1788 SC.mixin(SC.StatechartManager, SC.StatechartDelegate, SC.DelegateSupport); 1789 1790 /** 1791 The default name given to a statechart's root state 1792 */ 1793 SC.ROOT_STATE_NAME = "__ROOT_STATE__"; 1794 1795 /** 1796 Constants used during the state transition process 1797 */ 1798 SC.EXIT_STATE = 0; 1799 SC.ENTER_STATE = 1; 1800 1801 /** 1802 A Statechart class. 1803 */ 1804 SC.Statechart = SC.Object.extend(SC.StatechartManager, { 1805 autoInitStatechart: NO 1806 }); 1807 1808 SC.Statechart.design = SC.Statechart.extend; 1809