|
|
(10 intermediate revisions by the same user not shown) |
Line 1: |
Line 1: |
| ==Intro==
| |
|
| |
|
| The following sections describe the wizard workflow that is currently used in [http://kwwidgets.org KWWidgets]. This worfklow framework was created to help porting the EMSegmentation module (Polina Golland, Kilian Pohl) to [http://wiki.na-mic.org/Wiki/index.php/Slicer3 Slicer3]. This work is also part of the National Alliance for Medical Image Computing ([http://wiki.na-mic.org NAMIC]), funded by the National Institutes of Health through the NIH Roadmap for Medical Research, Grant U54 EB005149.
| |
|
| |
| Since a wizard is fundamentally a set of steps as well as additional logic and controls to navigate from one step to the other, we decided that a state machine or a Petri net would present a reasonable low-level basis for this framework. After some internal discussion at Kitware, we agreed that even a stab at a simple Petri net engine would not be a trivial task given the time frame and the resources allocated for this project. However, we chose to leverage and borrow a large part of the state machine engine design found in [http://www.igstk.org/index.htm IGSTK], while porting it to a more KWWidgets/VTK friendly environment. The resulting framework does not provide ITK-style C++ templating or extensive compile-time checks, but is compatible with VTK's coding style and wrapping technology.
| |
| {| border="0" align="center" cellpadding="20"
| |
| |-
| |
| | [[Image:KWWizardDialogExample.png|center|thumb|304px|KWWizardDialog Example]] || [[Image:EMSegmentationModuleWizard.png|center|thumb|240px|EMSegmentation Module]]
| |
| |}
| |
|
| |
| ==State Machine Engine==
| |
|
| |
| Nothing groundbreaking here. A few simple classes to represent states, inputs, and transitions that are triggered when the machine is in a particuliar state and receive a specific particuliar input, etc.
| |
|
| |
| * {{:kwwd|vtkKWStateMachineState}}: a state
| |
| * {{:kwwd|vtkKWStateMachineTransition}}: a transition between 2 states
| |
| * {{:kwwd|vtkKWStateMachineInput}}: an input (which triggers transitions)
| |
| * {{:kwwd|vtkKWStateMachine}}: a state machine
| |
|
| |
| A few additional classes are available.
| |
|
| |
| * {{:kwwd|vtkKWStateMachineCluster}}: a cluster (group) of states
| |
| * {{:kwwd|vtkKWStateMachineDOTWriter}}: a writer to Graphviz's DOT format
| |
| * {{:kwwd|vtkKWStateMachineWriter}}: a writer superclass
| |
|
| |
| ===vtkKWStateMachineState===
| |
|
| |
| The {{:kwwd|vtkKWStateMachineState}} class provides the representation for a state. The following members are available:
| |
| * Id (unique state ID)
| |
| * Name (string)
| |
| * Description (string)
| |
| * Enter/Leave (callbacks and events)
| |
|
| |
| The Enter/Leave callbacks and events are fired automatically by the state machine when it enters (respectively leaves) that state, independent of the transition to (respectively from) that state. This can be used, for example, to bring up the GUI of the wizard step associated to that state, or cleanup any resources that were used by that GUI.
| |
|
| |
| ==vtkKWStateMachineInput==
| |
|
| |
| An input in a state machine model:
| |
|
| |
| - [Get] Id: auto-generated unique-id (int).
| |
|
| |
| ==vtkKWStateMachineTransition==
| |
|
| |
| A transition between 2 states in a state machine model:
| |
|
| |
| - [Set/Get] Input (vtkKWStateMachineInput*)
| |
| - [Set/Get] State (vtkKWStateMachineState*)
| |
| - [Set/Get] NewState (vtkKWStateMachineState*)
| |
| - [Set/Get] Command (callback): (optional) callback fired by the transition
| |
|
| |
| ==vtkKWStateMachine==
| |
|
| |
| A state machine:
| |
|
| |
| - [Add] State (vtkKWStateMachineState*): add a state to the state machine
| |
| - [Add] Input (vtkKWStateMachineInput*): add an input to the state machine
| |
| - [Add] Transition (vtkKWStateMachineTransition*): add a transition to the state machine
| |
| - [Create] Transition (vtkKWStateMachineState*, vtkKWStateMachineInput*, vtkKWStateMachineState*, callback):
| |
| convenience method to create a transition and add it to the state machine
| |
| - [Set/Get[ CurrentState (vtkKWStateMachineState*): initial/current state
| |
| - PushInput(vtkKWStateMachineInput*): push a new input in the queue of inputs to be processed
| |
| - ProcessInputs(): perform the state transition and invoke the corresponding action for every
| |
| pending input stored in the input queue.
| |
|
| |
| ==vtkKWEMSegmentationWorkflow==
| |
|
| |
| A class that needs a state machine would declare states and inputs as class members, initialize the machine, add the states, add the inputs, then add the transitions (transitions objects don't really need to be created explicitly by the user). This would probably look like the example, for the first few steps of the EMseg project:
| |
| * "specify tree",
| |
| * "specify atlas",
| |
| * "specify target images"
| |
| In this specific example, the vtkKWEMSegmentationWorkflow is a class that embeds a state machine instance, create states and inputs as member variables, and has states and/or transitions callbacks point to its own methods. This provides a decent encapsulation of the whole wizard behavior inside a single class, but nothing would prevent people from splitting the wizard into several classes and have the callbacks point to different helper objects.
| |
|
| |
| vtkKWEMSegmentationWorkflow.h:
| |
| <pre>
| |
| class vtkKWEMSegmentationWorkflow:
| |
| public:
| |
| vtkKWStateMachineState *TreeStep;
| |
| vtkKWStateMachineState *AtlasStep;
| |
| vtkKWStateMachineState *TargetImagesStep;
| |
|
| |
| vtkKWStateMachineInput *InvalidTreeStepInput;
| |
| vtkKWStateMachineInput *GoToTreeStepInput;
| |
| vtkKWStateMachineInput *GoToAtlasStepInput;
| |
| vtkKWStateMachineInput *GoToTargetImagesStepInput;
| |
|
| |
| virtual void ShowTreeStep();
| |
| virtual void HideTreeStep();
| |
|
| |
| virtual void ShowAtlasStep();
| |
| virtual void HideAtlasStep();
| |
|
| |
| virtual void ShowTargetImagesStep();
| |
| virtual void HideTargetImagesStep();
| |
|
| |
| virtual void ReportTreeStepInvalidError();
| |
|
| |
| protected:
| |
| vtkKWStateMachine *StateMachine;
| |
| </pre>
| |
|
| |
| vtkKWEMSegmentationWorkflow.cxx:
| |
| <pre>
| |
| // Steps. The callbacks as used to bring up or hide the UIs
| |
|
| |
| this->TreeStep = vtkKWStateMachineState::New();
| |
| this->TreeStep->SetName("Specification of the tree");
| |
| this->TreeStep->SetEnterCommand(this, "ShowTreeStep");
| |
| this->TreeStep->SetLeaveCommand(this, "HideTreeStep");
| |
|
| |
| this->AtlasStep = vtkKWStateMachineState::New();
| |
| this->AtlasStep->SetName("Specification of the atlas");
| |
| this->AtlasStep->SetEnterCommand(this, "ShowAtlasStep");
| |
| this->AtlasStep->SetLeaveCommand(this, "HideAtlasStep");
| |
|
| |
| this->TargetImagesStep = vtkKWStateMachineState::New();
| |
| this->TargetImagesStep->SetName("Specification of the target images");
| |
| this->TargetImagesStep->SetEnterCommand(this, "ShowTargetImagesStep");
| |
| this->TargetImagesStep->SetLeaveCommand(this, "HideTargetImagesStep");
| |
|
| |
| // Inputs
| |
|
| |
| this->InvalidTreeStepInput = vtkKWStateMachineInput::New();
| |
| this->GoToTreeStepInput = vtkKWStateMachineInput::New();
| |
| this->GoToAtlasStepInput = vtkKWStateMachineInput::New();
| |
| this->GoToTargetImagesStepInput = vtkKWStateMachineInput::New();
| |
|
| |
| // State Machine
| |
|
| |
| this->StateMachine = vtkKWStateMachine::New();;
| |
|
| |
| // Add the states
| |
|
| |
| this->StateMachine->AddState(this->TreeStep);
| |
| this->StateMachine->AddState(this->AtlasStep);
| |
| this->StateMachine->AddState(this->TargetImagesStep);
| |
|
| |
| // Add the inputs
| |
|
| |
| this->StateMachine->AddInput(this->InvalidTreeStepInput);
| |
| this->StateMachine->AddInput(this->GoToTreeStepInput);
| |
| this->StateMachine->AddInput(this->GoToAtlasStepInput);
| |
| this->StateMachine->AddInput(this->GoToTargetImagesStepInput);
| |
|
| |
| // Add the transitions
| |
|
| |
| // Error control: loop over the 'Tree' step each time it is found
| |
| // to be invalid, and fire a method that could be used to display an
| |
| // error message.
| |
| // Say, pressing the "Next" button in a wizard from the 'Tree' step would
| |
| // first validate this step, and if it fails push the 'InvalidTreeStepInput'
| |
| // input to the state machine; the transition would invoke the
| |
| // 'ReportTreeStepInvalidError' method on 'this' object)
| |
|
| |
| this->StateMachine->AddTransition(this->TreeStep,
| |
| this->InvalidTreeStepInput,
| |
| this->TreeStep,
| |
| this, "ReportTreeStepInvalidError")
| |
|
| |
| // Go to the next step (no callback needed on the transition)
| |
| // Say, pressing the "Next" button in a wizard from the 'Tree' step would
| |
| // first validate this step, and if it succeeds push the 'GoToAtlasStepInput'
| |
| // input to the state machine, leading us to the 'Atlas' step.
| |
|
| |
| this->StateMachine->AddTransition(this->TreeStep,
| |
| this->GoToAtlasStepInput,
| |
| this->AtlasStep)
| |
|
| |
| // Go back the previous step (no callback needed on the transition)
| |
| // Say, pressing the "Back" button in a wizard from the 'Atlas' step would
| |
| // push the GoToTreeStepInput' input to the state machine, leading us back to
| |
| // the 'Tree' step.
| |
|
| |
| this->StateMachine->AddTransition(this->AtlasStep,
| |
| this->GoToTreeStepInput,
| |
| this->TreeStep)
| |
|
| |
| // Skip a step (no callback needed for the transition)
| |
| // Say, pressing the "Finish" button in a wizard from the 'Tree' step (or
| |
| // selecting a step directly from a pulldown menu listing all steps) would
| |
| // first validate this step, and if it succeeds push the GoToAtlasStepInput
| |
| // input to the state machine, leading us to the 'Target Images' step
| |
| // (it is considered to be the last one in this example). Now of course
| |
| // more logic is required here, for if you skip steps, you probably
| |
| // want to estimate good default values for the steps in between, etc)
| |
|
| |
| this->StateMachine->AddTransition(this->TreeStep,
| |
| this->GoToTargetImagesStepInput,
| |
| this->TargetImagesStep)
| |
| </pre>
| |
|
| |
| ==From State Machine to Wizard==
| |
|
| |
| How can we make things a little easier for the specific case of a simple Wizard? Here are a couple of thoughts:
| |
|
| |
| * a wizard is a most of the time a linear succession of steps (this will be our major assumption so far),
| |
| * you can navigate from one step to the next one other using a 'Next' and 'Back' button,
| |
| * you can navigate from one step to the previoous one using a 'Back' button,
| |
| * a 'Finish' may allow you to skip to the last step,
| |
| * a menu or a list of steps may allow you to go to a specific step explicitly,
| |
| * one need to make sure a step is valid before going to the next one,
| |
| * one probably do *not* need to make sure a step is valid before going to the previous one,
| |
| * one probably want to be able to guess a reasonable set of defaults values for a given step (ideally, values that make that step already 'valid'),
| |
| * one could be interested in knowing how much of a step is 'completed' to make it 'valid' (this could be used in the UI to display step names as 'green' or 'red' and locate visually which steps require to be completed in priority).
| |
|
| |
| Let's introduce the following subclasses:
| |
|
| |
| ==vtkKWWizardStep==
| |
|
| |
| A wizard step, subclass of vtkKWStateMachineState:
| |
|
| |
| - [Set/Get] ValidateCommand (callback): callback that will validate the step and push the
| |
| corresponding error inputs if needed (returns int)
| |
| - [Set/Get] EstimateDefaultsCommand (callback): callback that will attempt to estimate reasonable
| |
| defaults value for this step
| |
| - [Set/Get] EstimateCompletionCommand (callback): callback that can be used to estimate the completion
| |
| of that step (returns normalized double)
| |
| - [Get] GoToSelfInput (vtkKWStateMachineInput*): member variable that acts as an input specifying
| |
| "I want to go to that instance step"
| |
|
| |
| Note that GoToSelfInput is just a convenience member variable, an input that is automatically made available to you so that you can specify that you want to go to a specific step. Encapsulating the input inside the step you want to go to make it pretty clear from a notation/code point of view that you are referring to a specific step. This does not mean, however, that transitions have been created for you at this point, this is still up to you to explicitly create transitions that respond to that input, or explicitly call methods like vtkKWWizardWorkflow::AddLinearNavigationTransitions() (see below) to create some. Anyway, it is up to you to use GoToSelfInput or not, you can definitely ignore it actually.
| |
|
| |
| ==vtkKWWizardWorkflow==
| |
|
| |
| A wizard workflow, subclass of vtkKWStateMachine. The following class, vtkKWWizardWorkflow, is a specialization of a vtkKWStateMachine that offers a few convenience classes relying on the strong assumption that there exist a "linear" path of steps that were added as vtkKWWizardStep instances. As such, at given any step, the workflow can find the previous and next step (if any) and use that knowledge (see below). However, methods are not provided to retrieve the previous or next step in order not to bring the impression that a may start writing logic based on this methods, instead of programming the state machine correctly.
| |
|
| |
| - AddLinearNavigationTransitions(): assuming the states/steps were added as instance of vtkKWWizardStep, in
| |
| the right order, create transitions from one step to the next step (if any), and from one step to the previous
| |
| one (if any). This can be done by using each steps' GoToSelfInput member variable as input to the transitions.
| |
| For example, given steps A, B, C, this method would pretty much call:
| |
| this->AddTransition(A, B->GetGoToSelfInput(), B); // go from A to B
| |
| this->AddTransition(B, A->GetGoToSelfInput(), A); // go from B to A
| |
| this->AddTransition(B, C->GetGoToSelfInput(), C); // go from B to C
| |
| At this point, pushing B->GetGoToSelfInput() on the state machine, when the current state is A, will move it to
| |
| step B.
| |
| - AddDirectNavigationTransitions(): assuming the states/steps were added as instance of vtkKWWizardStep, in
| |
| the right order, create direct transitions from one step to all other steps (as a side effect, this also includes
| |
| adding all linear transitions in the process).
| |
| This can be done by using each steps' GoToSelfInput member variable as input to the transitions.
| |
| For example, given steps A, B, C, this method would pretty much call:
| |
| this->AddTransition(A, B->GetGoToSelfInput(), B); // go from A to B
| |
| this->AddTransition(A, C->GetGoToSelfInput(), C); // go from A to C
| |
| this->AddTransition(B, A->GetGoToSelfInput(), A); // go from B to A
| |
| this->AddTransition(B, C->GetGoToSelfInput(), C); // go from B to C
| |
| this->AddTransition(C, A->GetGoToSelfInput(), A); // go from C to A
| |
| this->AddTransition(C, B->GetGoToSelfInput(), B); // go from C to B
| |
|
| |
| Again, those are convenience methods, they probably probably more transitions than you would need, but you can also choose to ignore them and add transitions manually. The following methods are also for convenience and can be used to navigate from one step to the other, and as such can be associated to buttons like "Next" or "Back" in a Wizard UI:
| |
|
| |
| - RequestGoToNextStep(): given the current step 'current_step', try to go to the next step by
| |
| invoking 'current_step->ValidateCommand on the current step/state to check if we can leave the step
| |
| (i.e. its contents is valid), or if an error should be reported (its contents is not valid):
| |
| - if ValidateCommand fails (returns o), do nothing, as it is up to the ValidateCommand
| |
| to push error inputs that will in turn trigger transitions that can display error messgages and lead back
| |
| to the same 'current_step' for example.
| |
| - if ValidateCommand succeeds (returns 1), get the next step 'next_step' following 'current_step' and push a
| |
| next_step->GoToSelfInput to the state machine to go to 'next_step'.
| |
| - RequestGoToPreviousStep(): given the current step 'current_step', go to the previous step by
| |
| getting the previous step 'prev_step' preceeding 'current_step' (if any) and push a prev_step->GoToSelfInput to the
| |
| state machine to go to 'prev_step'.
| |
| - GoToStep(vtkKWWizardStep *step): given the current step 'current_step'
| |
| 'while' we have not reached 'step', validate current step,
| |
| - if it fails break the loop,
| |
| - if it succeeds get the next step, invoke EstimateDefaultsCommand and
| |
| push a next_step->GoToSelfInput
| |
| the loop will try to go as far as it can, as long as the step could be
| |
| validated with the defaults that were estimated.
| |
| - GoToLastStep(): self-explanatory
| |
|
| |
| Remember, those are convenience methods that are useful to drive the state machine from a UI. You can ignore them and still rely on the generic state machine model to program your wizard. The "linearity" of transition between the states is from a "step" point of view, you can add more states to the machine as instances of vtkKWStateMachineState, as long as only instances of vtkKWWizardStep are used to specificy a linear "path" among the states. This is a constraint/assumption.
| |
|
| |
| From a UI point of view, we probably also need something that can tell us *if* we can go to a given step, so that we can disable or grey out the corresponding "Next"/"Back" button, I'll leave that for another time.
| |
|
| |
| ==vtkKWEMSegmentationWorkflow revisited==
| |
|
| |
| vtkKWEMSegmentationWorkflow.h:
| |
| <pre>
| |
| class vtkKWEMSegmentationWorkflow:
| |
| public:
| |
| vtkKWWizardStep *TreeStep;
| |
| vtkKWWizardStep *AtlasStep;
| |
| vtkKWWizardStep *TargetImagesStep;
| |
|
| |
| vtkKWStateMachineInput *InvalidTreeStepInput;
| |
|
| |
| virtual void ShowTreeStep();
| |
| virtual void HideTreeStep();
| |
| virtual int ValidateTreeStep();
| |
| virtual void EstimateTreeStepDefaults();
| |
|
| |
| virtual void ShowAtlasStep();
| |
| virtual void HideAtlasStep();
| |
| virtual int ValidateAtlasStep();
| |
| virtual void EstimateAtlasStepDefaults();
| |
|
| |
| virtual void ShowTargetImagesStep();
| |
| virtual void HideTargetImagesStep();
| |
| virtual int ValidateTargetImagesStep();
| |
| virtual void EstimateTargetImagesStepDefaults();
| |
|
| |
| virtual void ReportTreeStepInvalidError();
| |
|
| |
| protected:
| |
| vtkKWWizardWorkflow *WizardWorkflow;
| |
| </pre>
| |
|
| |
| vtkKWEMSegmentationWorkflow.cxx:
| |
| <pre>
| |
| this->TreeStep = vtkKWWizardStep::New();
| |
| this->TreeStep->SetName("Specification of the tree");
| |
| this->TreeStep->SetEnterCommand(this, "ShowTreeStep");
| |
| this->TreeStep->SetLeaveCommand(this, "HideTreeStep");
| |
| this->TreeStep->SetValidateCommand(this, "ValidateTreeStep");
| |
| this->TreeStep->SetEstimateDefaultsCommand(this, "EstimateTreeStepDefaults");
| |
|
| |
| this->AtlasStep = vtkKWWizardStep::New();
| |
| this->AtlasStep->SetName("Specification of the atlas");
| |
| this->AtlasStep->SetEnterCommand(this, "ShowAtlasStep");
| |
| this->AtlasStep->SetLeaveCommand(this, "HideAtlasStep");
| |
| this->AtlasStep->SetValidateCommand(this, "ValidateAtlasStep");
| |
| this->AtlasStep->SetEstimateDefaultsCommand(this, "EstimateAtlasStepDefaults");
| |
|
| |
| this->TargetImagesStep = vtkKWWizardStep::New();
| |
| this->TargetImagesStep->SetName("Specification of the target images");
| |
| this->TargetImagesStep->SetEnterCommand(this, "ShowTargetImagesStep");
| |
| this->TargetImagesStep->SetLeaveCommand(this, "HideTargetImagesStep");
| |
| this->TargetImagesStep->SetValidateCommand(this, "ValidateTargetImagesStep");
| |
| this->TargetImagesStep->SetEstimateDefaultsCommand(this, "EstimateTargetImagesStepDefaults");
| |
|
| |
| // Inputs
| |
|
| |
| this->InvalidTreeStepInput = vtkKWStateMachineInput::New();
| |
|
| |
| // State Machine
| |
|
| |
| this->WizardWorkflow = vtkKWWizardWorkflow::New();;
| |
|
| |
| // Add the states
| |
|
| |
| this->WizardWorkflow->AddState(this->TreeStep);
| |
| this->WizardWorkflow->AddState(this->AtlasStep);
| |
| this->WizardWorkflow->AddState(this->TargetImagesStep);
| |
|
| |
| // Add the inputs
| |
|
| |
| this->WizardWorkflow->AddInput(this->InvalidTreeStepInput);
| |
|
| |
| // Add the transitions
| |
|
| |
| this->WizardWorkflow->AddNavigationTransitions();
| |
|
| |
| // Error control: loop over the 'Tree' step each time it is found
| |
| // to be invalid, and fire a method that could be used to display an
| |
| // error message.
| |
| // Say, pressing the "Next" button in a wizard from the 'Tree' step would
| |
| // first validate this step, and if it fails push the 'InvalidTreeStepInput'
| |
| // input to the state machine; the transition would invoke the
| |
| // 'ReportTreeStepInvalidError' method on 'this' object)
| |
|
| |
| this->WizardWorkflow->AddTransition(this->TreeStep,
| |
| this->InvalidTreeStepInput,
| |
| this->TreeStep,
| |
| this, "ReportTreeStepInvalidError")
| |
| </pre>
| |
|
| |
| Now at this point this vtkKWEMSegmentationWorkflow class could be a subclass of a vtkKWWizardWidget that presents some UI real-estate and "Next" / "Back" / "Finish" buttons at the bottom of the page associated to the GoToNextStep, GoToPreviousStep and GoToLastStep methods of the workflow instance...
| |
|
| |
| Note that this code:
| |
|
| |
| this->TreeStep = vtkKWWizardStep::New();
| |
| this->TreeStep->SetEnterCommand(this, "ShowTreeStep");
| |
| this->TreeStep->SetEstimateDefaultsCommand(this, "EstimateTreeStepDefaults");
| |
|
| |
| ...means that when the 'Tree' step/state is reached (whatever the transition), the ShowTreeStep callback is triggered, which we have set to:
| |
|
| |
| virtual void ShowTreeStep();
| |
|
| |
| In this ShowTreeStep callback, you could reuse the validation callback by doing something like:
| |
|
| |
| this->WizardWorkflow->GetCurrentStep()->InvokeEstimateDefaultsCommand();
| |
|
| |
| which would invoke:
| |
|
| |
| virtual void EstimateTreeStepDefaults();
| |