Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Updated statechart logic so that if a state's initialSubstate is *not…
…* assigned a value then the default state used will be an empty state (Ki.EmptyState). A root state must *always* have its initialSubstate property assigned an explicit value.
  • Loading branch information
mlcohen committed Jan 18, 2011
1 parent 8f5a001 commit 0601cf9
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 14 deletions.
57 changes: 45 additions & 12 deletions frameworks/foundation/system/state.js
Expand Up @@ -44,9 +44,14 @@ Ki.State = SC.Object.extend({
the state, the statechart will automatically change the property
to be a corresponding state object
The substate is only to be this state's immediate substates.
The substate is only to be this state's immediate substates. If
no initial substate is assigned then this states initial substate
will be an instance of an empty state (Ki.EmptyState).
@property {State}
Note that a statechart's root state must always have an explicity
initial substate value assigned else an error will be thrown.
@property {String|State}
*/
initialSubstate: null,

Expand Down Expand Up @@ -159,26 +164,28 @@ Ki.State = SC.Object.extend({
SC.Logger.error('Unable to set initial substate %@ since it did not match any of state\'s %@ substates'.fmt(initialSubstate, this));
}

this.set('substates', substates);
this.set('currentSubstates', []);

if (substates.length === 0) {
if (!SC.none(initialSubstate)) {
SC.Logger.warn('Unable to make %@ an initial substate since state %@ has no substates'.fmt(initialSubstate, this));
}
}
else if (substates.length > 0) {
if (SC.none(initialSubstate) && !substatesAreConcurrent) {
state = substates[0];
state = this.createEmptyState({ parentState: this, statechart: statechart });
this.set('initialSubstate', state);
SC.Logger.warn('state %@ has no initial substate defined. Will default to using %@ as initial substate'.fmt(this, state));
substates.push(state);
this[state.get('name')] = state;
state.initState();
SC.Logger.warn('state %@ has no initial substate defined. Will default to using an empty state as initial substate'.fmt(this));
}
else if (!SC.none(initialSubstate) && substatesAreConcurrent) {
this.set('initialSubstate', null);
SC.Logger.warn('Can not use %@ as initial substate since substates are all concurrent for state %@'.fmt(initialSubstate, this));
}
}

this.set('substates', substates);
this.set('currentSubstates', []);
this.set('stateIsInitialized', YES);
},

Expand All @@ -187,17 +194,23 @@ Ki.State = SC.Object.extend({
*/
createSubstate: function(state, attrs) {
if (!attrs) attrs = {};
state = state.create(attrs);
return state;
return state.create(attrs);
},

/**
Create a history state for this state
*/
createHistoryState: function(state, attrs) {
if (!attrs) attrs = {};
state = state.create(attrs);
return state;
return state.create(attrs);
},

/**
Create an empty state for this state's initial substate
*/
createEmptyState: function(attrs) {
if (!attrs) attrs = {};
return Ki.EmptyState.create(attrs);
},

/** @private
Expand Down Expand Up @@ -695,7 +708,8 @@ Ki.State = SC.Object.extend({
}.property('name', 'parentState').cacheable(),

toString: function() {
return "Ki.State<%@, %@>".fmt(this.get('fullPath'), SC.guidFor(this));
var className = SC._object_className(this.constructor);
return "%@<%@, %@>".fmt(className, this.get('fullPath'), SC.guidFor(this));
}

});
Expand Down Expand Up @@ -879,4 +893,23 @@ Ki.HistoryState = SC.Object.extend({
this.notifyPropertyChange('state');
}.observes('*parentState.historyState')

});

/**
The default name given to an empty state
*/
Ki.EMPTY_STATE_NAME = "__EMPTY_STATE__";

/**
Represents an empty state that gets assigned as a state's initial substate
if the state does not have an initial substate defined.
*/
Ki.EmptyState = Ki.State.extend({

name: Ki.EMPTY_STATE_NAME,

enterState: function() {
SC.Logger.warn("No initial substate was defined for state %@. Entering default empty state".fmt(this.get('parentState')));
}

});
14 changes: 12 additions & 2 deletions frameworks/foundation/system/statechart.js
Expand Up @@ -227,7 +227,8 @@ Ki.StatechartManager = {
}

var trace = this.get('trace'),
rootState = this.get('rootState');
rootState = this.get('rootState'),
msg;

if (trace) SC.Logger.info('BEGIN initialize statechart');

Expand All @@ -236,12 +237,21 @@ Ki.StatechartManager = {
}

if (!(SC.kindOf(rootState, Ki.State) && rootState.isClass)) {
throw "Unable to initialize statechart. Root state must be a state class";
msg = 'Unable to initialize statechart. Root state must be a state class';
SC.Logger.error(msg);
throw msg;
}

rootState = this.createRootState(rootState, { statechart: this, name: Ki.ROOT_STATE_NAME });
this.set('rootState', rootState);
rootState.initState();

if (SC.kindOf(rootState.get('initialSubstate'), Ki.EmptyState)) {
msg = 'Unable to initialize statechart. Root state must have an initial substate explicilty defined';
SC.Logger.error(msg);
throw msg;
}

this.set('statechartIsInitialized', YES);
this.gotoState(rootState);

Expand Down
71 changes: 71 additions & 0 deletions frameworks/foundation/tests/state/initial_substate.js
@@ -0,0 +1,71 @@
// ==========================================================================
// Ki.State Unit Test
// ==========================================================================
/*globals Ki externalState1 externalState2 */

var statechart, root, monitor, stateA, stateB, stateC, stateD, stateE, stateF;

// ..........................................................
// CONTENT CHANGING
//

module("Ki.Statechart: State Initial Substate Tests", {
setup: function() {

statechart = Ki.Statechart.create({

monitorIsActive: YES,

rootState: Ki.State.design({

initialSubstate: 'a',

a: Ki.State.design({
initialSubstate: 'c',
c: Ki.State.design(),
d: Ki.State.design()
}),

b: Ki.State.design({
e: Ki.State.design(),
f: Ki.State.design()
})

})

});

statechart.initStatechart();

root = statechart.get('rootState');
monitor = statechart.get('monitor');
stateA = statechart.getState('a');
stateB = statechart.getState('b');
stateC = statechart.getState('c');
stateD = statechart.getState('d');
stateE = statechart.getState('e');
stateF = statechart.getState('f');
},

teardown: function() {
statechart = root = stateA = stateB = stateC = stateD = stateE = stateF = null;
}
});

test("check initial substates", function() {
equals(root.get('initialSubstate'), stateA, "root state's initial substate should be state A");
equals(stateA.get('initialSubstate'), stateC, "state a's initial substate should be state c");
equals(stateC.get('initialSubstate'), null, "state c's initial substate should be null");
equals(stateD.get('initialSubstate'), null, "state d's initial substate should be null");
equals(SC.kindOf(stateB.get('initialSubstate'), Ki.EmptyState), true, "state b's initial substate should be an empty state");
equals(stateE.get('initialSubstate'), null, "state e's initial substate should be null");
equals(stateF.get('initialSubstate'), null, "state f's initial substate should be null");
});

test("go to state b and confirm current state is an empty state", function() {
equals(stateC.get('isCurrentState'), true);
monitor.reset();
statechart.gotoState(stateB);
ok(monitor.matchSequence().begin().exited(stateC, stateA).entered(stateB, stateB.get('initialSubstate')).end());
equals(stateB.getPath('initialSubstate.isCurrentState'), true, "state b\'s initial substate should be the current state");
});
Expand Up @@ -64,8 +64,12 @@ module("Ki.Statechart: No Concurrent States - Goto State Asynchronous Tests", {

c: Ki.State.design(StateMixin, {

initialSubstate: 'd',

d: Ki.State.design(StateMixin, {

initialSubstate: 'e',

e: Ki.State.design(StateMixin)

})
Expand Down

2 comments on commit 0601cf9

@erichocean
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good solution to the problem (identical to the one I had on my TODO list).

@FrozenCanuck
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great minds think alike :-)

Please sign in to comment.