Chapter 4. Implementing the User Interface

The classes making up your user interface should encapsulate their own behavior and appearance. View Components may require data or the occasional method invocation from the outside in order to have something to display or to know they need to change to another visual state. But they should be capable of making those visual transitions themselves once given the input and impetus. When they have something to communicate to the rest of the application, they should do this solely by dispatching events or setting properties and making method invocations on their child components. In the context of a PureMVC application, a View Component should never know anything about the PureMVC framework classes or their subclasses.

One reason it is a good idea to build the View Components after building the Value Objects, and before getting deeply into the PureMVC apparatus, is that you do not yet have the ability to access those Mediator, Proxy, and Command classes since they have not yet been created. Since we have already created our Value Objects and Enums, we can populate our View Components with dummy VOs as we build them without having to have all the rest of the system in place to feed them to us. In fact, you should be able to farm out the development of the View Components to a separate company, team, or team member that knows nothing at all about PureMVC and still be successful. If the team is at another company, you might want to repackage your Model classes into a separate library for their use while they build the View Components.

The main thing to remember in your implementation of the View Components is that they should expose an API of events, properties, and methods for interacting with them. This API should hide the internal implementation of the components. For example, it should not be expected that the caller will reach into the component and set the dataProvider property of a DataGrid instance declared within. Instead a bindable, public property named something like displayItems should be exposed. The DataGrid instance’s dataProvider property should then be bound to the displayItems property. This means that the Mediator subclass that will tend the component does not know or care how the component is implemented, it just knows that there is a displayItems property to be set for displaying the data. The View Component could later be refactored to use a drop-down list with a different ID and the Mediator would be completely unaffected by the change.

Also, as with any class in an OOP application, try not to heap too many responsibilities onto any given View Component. Once you see a great many nested structures declared in one place and lots of associated logic and variables, you should consider creating custom components to replace large chunks of MXML. While this can be argued to create complexity (in the form of more classes), it actually simplifies the View Components, clarifies their roles, and makes them more reusable as you will see in this chapter.

A Tale of Two Views

After much work at the whiteboard considering how our goals for the Story Architect application should be translated into a user interface, it was determined that there will be two primary views to the application: a Chooser and an Editor. When you open up the application, you have a number of options available with regard to creating and managing your Stories and Series. You could choose a Story and export it as a file, delete it, change its name, etc. Or you could choose to write, and that leads you to the Editor, where you will be able to do just that. The Editor will provide you with the functionality for attaching descriptions and notes to any part of the Story, as well as a timeline for navigating and extending the Story.

While the functionality for dealing with a Series is a big part of the application, it is far less important to the primary goal of writing a Story, as is management of Cast and Milieu. This book will focus discussion on the primary Story-related use cases and user interface elements. Remember, our major goal for the first iteration is to create and persist a Story, edit and extend its structure, name any part of the structure, and add descriptions and notes to it.

Now we will examine some of the more important pieces of the user interface. In addition to the main application and components, we will define one bubbling event called AppEvent that can carry data, and a class similar to a Value Object called SelectionContext for sharing information about the current selection among the View Components.

With regard to collaborations, notice that all of these View Components basically know only their direct child components, the AppEvent class, the SelectionContext class, Enum and its subclasses, and ValueObject and its subclasses. There are no PureMVC Facade, Mediator, Command, Proxy, or Notification classes referenced anywhere within the View Components (with the exception of the main application itself, which must know the Facade to bootstrap the startup process). View Components expose properties to receive data, send events to communicate user intentions, observe and conform to the shared selection context, and interact only with their direct children and the data objects they are fed (either from a parent component or from a Mediator).

In the first view, upon opening the application, you can add or choose a Story or Series and access various functionalities associated with your selection. This is called the Chooser View and is shown in Figure 4-1. Functions like creating and managing the Story will open pop-ups to collect and apply the user input.

UI—The Chooser View
Figure 4-1. UI—The Chooser View

When you have created or chosen a Story from the list, then you are taken to the Editor View (see Figure 4-2), where you can immediately begin writing the current draft of the last scene in your Story, however the scenes happen to be grouped. You can also reveal the timeline and details components for navigating the Story and extending and annotating.

UI—The Editor View
Figure 4-2. UI—The Editor View

The Application

Class

StoryArchitect.mxml

Responsibilities

  • Declare and layout the Chooser and Editor components

  • Initialize the PureMVC Facade

  • Trigger PureMVC startup process, passing a reference to the app for mediation

  • Define three display modes for Starting, Chooser, and Editor

  • Expose a bindable public property for setting the word count

  • Expose a bindable public property for setting the display mode

  • Expose a bindable public property for setting the SelectionContext

  • Provide the SelectionContext object to children that require it

  • Control visibility and layout inclusion of subcomponents based on display mode

  • Display mode in the status bar (and additionally word count when in Editor mode)

  • Listen to the Editor component for ReportWordCount AppEvents, updating the wordCount property when they occur

Collaborations

At runtime, Flash builds the main application (StoryArchitect) first, initializing its display list and variables. This makes it the perfect place to kick off the initialization of the PureMVC apparatus. Thus, StoryArchitect knows the ApplicationFacade, for the purpose of initializing it and triggering the startup process. The main application is the exception to the rule about View Components not knowing the PureMVC Facade. Interaction with the Facade by the application should be limited to fulfilling these two responsibilities. No other View Component ever has a reason to know about the Facade.

StoryArchitect also knows the SelectionContext class, a construct created specifically for this application that allows components to be informed about the current selection. When the SelectionContext is set on StoryArchitect, it is passed to its children via Flex binding. You will note that many of the View Components in our UI have this bindable property and pass it on to their children. It allows us to, for example, have the Timeline component automatically open up to the current Draft of the last Scene in the last Chapter of the last part of a Story. We will describe this class in more detail shortly.

StoryArchitect knows and controls its direct children, the Chooser and Editor components.

StoryArchitect is known by the ApplicationFacade, which exposes a convenience method for passing the application to the StartupCommand, which references it briefly to register the ApplicationMediator, who will mediate communications between StoryArchitect and the rest of the system.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- STORY ARCHITECT APPLICATION -->
<s:WindowedApplication xmlns:editor="com.futurescale.sa.view.component.editor.*"
                       xmlns:chooser="com.futurescale.sa.view.component.chooser.*" 
                       xmlns:s="library://ns.adobe.com/flex/spark"
                       xmlns:fx="http://ns.adobe.com/mxml/2009" 
                       applicationComplete="facade.startup(this); // startup app"
                       minWidth="800" minHeight="600">
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.ApplicationFacade;
            import com.futurescale.sa.view.context.SelectionContext;
            
            public static const MODE_STARTING:String = "Starting...";
            public static const MODE_CHOOSER:String  = "Chooser";
            public static const MODE_EDITOR:String   = "Editor";
            
            // Selection context shared between View Components.
            [Bindable] public var context:SelectionContext;
                        
            // Word Count (displayed on status bar in Editor mode)
            [Bindable] public var wordCount:String = "";
            
            // Display Mode (Chooser/Editor)
            [Bindable] private var mode:String = MODE_STARTING;
            
            // Initialize the PureMVC Facade
            private var facade:ApplicationFacade = ApplicationFacade.getInstance();
            
            /**
             * Set the application display mode, and if showing 
             * the Editor, initialize the word count from the 
             * selected Story.
             */ 
            public function setMode( mode:String ):void
            {
                this.mode=mode;
                if (mode == MODE_EDITOR && context.story) {
                    wordCount = context.story.wordCount.toString();
                } else {
                    wordCount = "";
                }
            }
        
    </fx:Script>
    
    <!-- LAYOUT -->
    <s:layout>
        <s:VerticalLayout horizontalAlign="center"/>
    </s:layout>
    
    <!-- STATUS BAR -->
    <s:status>{mode}{(wordCount != "")?" | "+wordCount+" words":""}</s:status>
    
    <!-- CHOOSER -->
    <chooser:Chooser id="chooser" width="100%" height="100%" 
                     includeInLayout="{mode == MODE_CHOOSER}" 
                     visible="{mode == MODE_CHOOSER}"/>

    <!-- EDITOR -->
    <editor:Editor id="editor" width="100%" height="100%" 
                   includeInLayout="{mode == MODE_EDITOR}" 
                   visible="{mode == MODE_EDITOR}"
                   reportWordCount="wordCount=String(event.data)"
                   context="{context}"/>

</s:WindowedApplication>

The Chooser

Class

Chooser.mxml

Responsibilities

  • Declare and layout the StoryChooser and SeriesChooser components

  • Expose a bindable public property for setting the list of StoryVOs

  • Expose a bindable public property for setting the list of SeriesVOs

  • Provide the Story and Series lists to children

Collaborations

The Chooser only knows and controls its direct children, the StoryChooser and SeriesChooser components. Note that the Chooser does not require access to the SelectionContext object, as its children only display simple lists and dispatch events to be processed when a selection is made and a button pressed.

The Chooser is known by its parent, the StoryArchitect application, and the StartupCommand, which references it briefly to register the ChooserMediator. The ChooserMediator who knows and mediates communications between the rest of the system and the Chooser (and indirectly its StoryChooser and SeriesChooser child components). The Story and Series lists that populate the children will be set on the Chooser by the ChooserMediator. The bubbling events that are dispatched from the children will be handled by the ChooserMediator who will set listeners on the Chooser.

Code

<?xml version="1.0" encoding="utf-8"?>
<s:HGroup xmlns:s="library://ns.adobe.com/flex/spark"
          xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:chooser="com.futurescale.sa.view.component.chooser.*" 
          verticalAlign="middle" horizontalAlign="center"
          paddingLeft="5" paddingRight="5" width="100%" 
          paddingBottom="5" paddingTop="5" height="100%"
         >
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayCollection;
            
            // The list of stories
            [Bindable] public var storyList:ArrayCollection;

            // The list of series
            [Bindable] public var seriesList:ArrayCollection;
        
    </fx:Script>
    
    <!-- STORY CHOOSER -->
    <chooser:StoryChooser id="storyChooser" storyList="{storyList}"
                          width="50%" height="100%"/>
    
    <!-- SERIES CHOOSER -->
    <chooser:SeriesChooser id="seriesChooser" seriesList="{seriesList}"
                           width="50%" height="100%"/>

</s:HGroup>

The Story Chooser

UI - The Story Chooser
Figure 4-3. UI - The Story Chooser

Class

StoryChooser.mxml

Responsibilities

  • Declare a list for displaying the StoryVOs for selection

  • Declare and layout the various buttons for acting on a selection

  • Control visibility and layout inclusion for buttons based on selection

  • Declare a prominent “Add” (+) button for adding a new Story

  • Dispatch appropriate events when buttons are pressed

Collaborations

The StoryChooser knows and controls its direct children: Flex Button, Label, and List components. It also knows the AppEvent class, which it constructs and dispatches in response to certain button presses.

The StoryChooser is known only by its parent, the Chooser component.

Code

<?xml version="1.0" encoding="utf-8"?>
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          minWidth="390" minHeight="290">
    
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.view.event.AppEvent;
            
            import mx.collections.ArrayCollection;
            
            // The list of StoryVOs
            [Bindable] public var storyList:ArrayCollection;

            // Create and dispatch an AppEvent
            private function sendEvent( type:String, data:Object=null ):void
            {
                dispatchEvent( new AppEvent(type,data ) );
            }            
        
    </fx:Script>

    <!-- CHOOSER HEADER -->
    <s:HGroup fontSize="24" verticalAlign="middle"
              width="100%" height="50">
        
        <!-- TITLE -->
        <s:Label text="Story" width="100%"/>
        
        <!-- ADD BUTTON-->
        <s:Button label="+" width="50" height="100%"
                   click="sendEvent( AppEvent.ADD_STORY )" />
        
    </s:HGroup>
    
    <!-- STORY LIST-->
    <s:List labelField="name" id="lstStories"
            dataProvider="{storyList}" fontSize="16" 
            width="100%" height="100%"/>

    <!-- CONTROLS -->
    <s:HGroup fontSize="16" verticalAlign="middle" 
              visible="{lstStories.selectedItem != null}"
              includeInLayout="{lstStories.selectedItem != null}"
              width="100%" height="100">
        
        <!-- CAST / MILIEU -->
        <s:VGroup height="100%" width="25%">
            <s:Button label="Cast"   height="50%" width="100%"/>
            <s:Button label="Milieu" height="50%" width="100%"/>
        </s:VGroup>

        <!-- NOTES / OUTLINE -->
        <s:VGroup height="100%" width="25%">
            <s:Button label="Notes"   height="50%" width="100%"/>
            <s:Button label="Outline" height="50%" width="100%"/>
        </s:VGroup>

        <!-- MANAGE / EXPORT -->
        <s:VGroup height="100%" width="25%">
            
            <!-- MANAGE -->
            <s:Button label="Manage" height="50%" width="100%"
                      click="sendEvent( AppEvent.MANAGE_STORY, 
                                          lstStories.selectedItem )" />
            <!-- EXPORT-->
            <s:Button label="Export" height="50%" width="100%"/>
        </s:VGroup>

        <!-- WRITE -->
        <s:Button label="Write" fontSize="24" 
                  click="sendEvent( AppEvent.EDIT_STORY, 
                                      lstStories.selectedItem )"
                  height="100%" width="25%"/>

    </s:HGroup>
    
</s:VGroup>

The Editor

Class

Editor.mxml

Responsibilities

  • Declare and layout a TextArea for writing or displaying read-only, aggregated text

  • Declare and layout the Controls subcomponent

  • Expose a bindable public property for setting the SelectionContext

  • Provide the SelectionContext object to children that require it

  • Provide a public function for setting focus to the text editor

  • Update the text of the selected DraftVO and when text is edited

  • Dispatch an event reporting the current word count when text is edited

  • Control the TextArea’s font size and percentage width with values from the Controls component

  • Declare Flex metadata indicating that the component will dispatch ReportWordCount AppEvents

  • Listen to the Control instance for SelectScene and SelectDraft AppEvents and set focus to the TextArea when they occur (the AppEvents will still bubble and be handled by a Mediator)

Collaborations

The Editor knows and controls its direct children: the Flex TextArea and Controls custom component. It also knows the AppEvent class, which it constructs and dispatches in response to text editing. It also knows the SelectionContext class, which it references in methods and binding expressions.

The Editor is known by its parent the StoryArchitect application and by the StartupCommand who references it briefly in order to register the EditorMediator. The EditorMediator will mediate communications between the Editor and the rest of the system.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- EDITOR -->
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          xmlns:mx="library://ns.adobe.com/flex/mx" 
          xmlns:editor="com.futurescale.sa.view.component.editor.*"
          paddingBottom="5" paddingTop="5" horizontalAlign="center"
          paddingLeft="5" paddingRight="5"
          minWidth="400" minHeight="300" >

    <fx:Metadata>
        [Event(name="reportWordCount", type="com.futurescale.sa.view.event.AppEvent")]
    </fx:Metadata>
    
   <fx:Script>
        <![CDATA[
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            // Selection Context            
            [Bindable] public var context:SelectionContext;
            
            // Called when the user types in the text editor 
            private function textEdit():void
            {
                if (context.draft) {
                    // Update draft with latest text from the editor
                    context.draft.text = textEditor.text;
                    
                    // Dispatch an event reporting the current wordcount of the story.
                    var event:AppEvent = new AppEvent( AppEvent.REPORT_WORDCOUNT, 
                                                       context.story.wordCount );
                    dispatchEvent( event );
                }
            }
        
    </fx:Script>
    
    <!-- TEXT EDITOR -->
    <s:TextArea id="textEditor" borderVisible="{context.draft != null}" 
                percentWidth="{100 - controls.editMarginPct}" height="100%" 
                visible="true" editable="{context.draft != null}" 
                text="{context.selectedText}" change="textEdit()"
                fontSize="{controls.editFontSize}" fontFamily="serif"
                paddingLeft="10" paddingRight="10" 
                paddingTop="10" paddingBottom="10"/>
                    
    <!-- CONTROLS-->
    <editor:Controls id="controls" width="100%"
                     selectDraft="textEditor.setFocus()"
                     selectScene="textEditor.setFocus()"
                     context="{context}"/>
</s:VGroup>

The Editor Controls

UI—The Editor Controls
Figure 4-4. UI—The Editor Controls

Class

Controls.mxml

Responsibilities

  • Base on Panel with vertical layout showing the name of the selected StoryVO for the title

  • In the control bar, declare buttons for discarding or saving edits

  • In the control bar, declare buttons for revealing the Timeline and Details components

  • In the control bar, declare sliders for controlling font size and margins for the text editor

  • Declare the Timeline and Details components and control their visibility and layout inclusion based on the buttons

  • When the “Timeline” and “Details” buttons are not selected, only the Panel header and control bar should be visible

  • Expose bindable public properties for the font size and margin percentage

  • Expose a bindable public property for setting the SelectionContext

  • Provide the SelectionContext object to children that require it

  • Dispatch the appropriate AppEvent when the “Discard” or “Save” buttons are pressed

  • Declare Flex metadata indicating that the component will dispatch SelectScene and SelectDraft AppEvents

Collaborations

The Controls component knows and controls its direct children: the Timeline and Details custom components as well as Flex Panel, Button, Label, and HSlider components declared in its control bar area. It also knows the AppEvent class, which it constructs and dispatches in response to text editing. It also knows the SelectionContext class, which it references in methods and binding expressions.

The Controls component is known only by its parent, the Editor component.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- CONTROLS -->
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009" 
         xmlns:s="library://ns.adobe.com/flex/spark" 
         xmlns:timeline="com.futurescale.sa.view.component.timeline.*"
         xmlns:details="com.futurescale.sa.view.component.details.*" 
         title="{(context.series) ? context.series.name : context.story.name }"
         width="100%" minHeight="0">
	
    <fx:Metadata>
        [Event(name="selectScene", type="com.futurescale.sa.view.event.AppEvent")]
        [Event(name="selectDraft", type="com.futurescale.sa.view.event.AppEvent")]
    </fx:Metadata>
	
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            [Bindable] public var editFontSize:Number=16;
            [Bindable] public var editMarginPct:Number=0;
            
            // Selection Context            
            [Bindable] public var context:SelectionContext;
            
            private function discardChanges():void
            {
                var event:AppEvent;
                if ( context.story ) {
                    event = new AppEvent( AppEvent.DISCARD_STORY, context.story );
                } else if ( context.series ) {
                    event = new AppEvent( AppEvent.DISCARD_SERIES, context.series );
                }
                dispatchEvent( event );
            }
            
            private function saveChanges():void
            {
                var event:AppEvent;
                if ( context.story ) {
                    event = new AppEvent( AppEvent.SAVE_STORY, context.story );
                } else if ( context.series ) {
                    event = new AppEvent( AppEvent.SAVE_SERIES, context.series );
                }
                dispatchEvent( event );
            }
        
    </fx:Script>

    <!-- DETAILS -->
    <details:Details id="details"
                     context="{context}"
                     width="100%" height="100%"
                     visible="{detailsButton.selected}"
                     includeInLayout="{detailsButton.selected}"/>
    
    <!-- TIMELINE -->
    <timeline:Timeline id="timeline" 
                       context="{context}"
                       story="{context.story}" 
                       width="100%" height="100%"
                       visible="{timelineButton.selected}"
                       includeInLayout="{timelineButton.selected}"/>
    
    <!-- CONTROL BAR -->
    <s:controlBarContent>
        
        <!-- TOGGLE TIMELINE -->
        <s:ToggleButton id="timelineButton" label="Timeline"/>
        
        <!-- TOGGLE DETAILS -->
        <s:ToggleButton id="detailsButton" label="Details"/>

        <!-- SPACER -->
        <s:Spacer width="100%"/>

        <!-- FONT SIZE -->
        <s:Label text="Font Size"/>
        <s:HSlider id="fontSlider" change="editFontSize=fontSlider.value"
                   minimum="12" maximum="48" value="{editFontSize}"/>
        
        <!-- MARGIN SIZE -->
        <s:Label text="Margins"/>
        <s:HSlider id="marginSlider" change="editMarginPct=marginSlider.value"
                   minimum="0" maximum="75" value="{editMarginPct}"/>
        
        <!-- SPACER -->
        <s:Spacer width="100%"/>
        
        <!-- DISCARD OR SAVE CHANGES -->
        <s:Button label="Discard" click="discardChanges()"/>
        <s:Button label="Save" click="saveChanges()"/>
        
    </s:controlBarContent>        

    <!-- PANEL LAYOUT -->
    <s:layout>
        <s:VerticalLayout gap="0"/>
    </s:layout>
    
    <!-- CONTROL BAR LAYOUT -->
    <s:controlBarLayout>
        <s:HorizontalLayout verticalAlign="middle" 
                            paddingLeft="5" paddingRight="5"
                            paddingBottom="5" paddingTop="5"/>
    </s:controlBarLayout>

</s:Panel>

The Details Component

UI—The Editor Controls showing the Details Component
Figure 4-5. UI—The Editor Controls showing the Details Component

Class

Details.mxml

Responsibilities

  • Declare the ItemInfo and Notes components

  • Expose a bindable public property for setting the SelectionContext

  • Provide the SelectionContext object to children that require it

Collaborations

The Details component knows and controls its direct children, the ItemInfo and Notes custom components. It also knows the SelectionContext class, which it references in methods and binding expressions.

The Details component is known only by its parent, the Controls component. In a future iteration, it will also be known by the Chooser component, which will allow modification of item info and the ability to add notes to the top-level VOs when selected without having to enter the Editor to do so.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- DETAILS -->
<s:HGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
         xmlns:s="library://ns.adobe.com/flex/spark" 
         xmlns:details="com.futurescale.sa.view.component.details.*"
         width="100%" height="100%">
    
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.view.context.SelectionContext;
            
            //Selection context
            [Bindable] public var context:SelectionContext;
            
        
    </fx:Script>

    <!-- ITEM INFO -->
    <details:ItemInfo context="{context}" width="50%" height="100%" />

    <!-- NOTES -->
    <details:Notes context="{context}" width="50%" height="100%" />
    
</s:HGroup>

The Item Info Component

UI—The Item Info Component
Figure 4-6. UI—The Item Info Component

Class

ItemInfo.mxml

Responsibilities

  • Declare a TextInput and TextArea for the name and description of the selected item (a ValueObject)

  • Update the selected item’s name and description in response to user edits

  • Declare buttons for deleting and reordering the selected in the list of its siblings (functionality deferred to a later iteration)

  • Only show the “Move” and “Delete” buttons when the selected item is not a top-level VO (CastVO, MilieuVO, StoryVO, or SeriesVO)

  • Expose a bindable public property for setting the SelectionContext

Collaborations

The ItemInfo component knows and controls its direct children, the Flex Button, TextInput, and TextArea components. It also knows the SelectionContext class, which it references in methods and binding expressions. And it knows the top-level VOs (CastVO, MilieuVO, StoryVO, or SeriesVO), which it must hide the “Move” and “Delete” buttons for.

The ItemInfo component is known only by its parent, the Details component.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- ITEM INFO -->
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          xmlns:details="com.futurescale.sa.view.component.details.*"
          width="100%" height="100%" paddingTop="5" paddingBottom="5"
          paddingLeft="5" paddingRight="5">
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.CastVO;
            import com.futurescale.sa.model.vo.MilieuVO;
            import com.futurescale.sa.model.vo.SeriesVO;
            import com.futurescale.sa.model.vo.StoryVO;
            import com.futurescale.sa.model.vo.ValueObject;
            import com.futurescale.sa.view.context.SelectionContext;
            
            //Selection context
            [Bindable] public var context:SelectionContext;

            // Update the selected item's name when edited
            private function nameEdit():void
            {
                if (context.selectedItem) context.selectedItem.name = itemName.text;
            }

            // Update the selected item's description when edited
            private function descEdit():void
            {
                if (context.selectedItem) context.selectedItem.description = itemDesc.text;
            }
            
            // Show or hide the move and delete buttons based the selection
            private function showButtons( vo:ValueObject ):Boolean
            {
                return (!(vo is StoryVO) &&
                        !(vo is SeriesVO) &&
                        !(vo is CastVO) &&
                        !(vo is MilieuVO) );
            }
        
    </fx:Script>

    <s:HGroup width="100%">
        
        <!-- ITEM NAME -->
        <s:TextInput id="itemName" width="100%" height="30" prompt="Name" 
                     change="nameEdit()" text="{context.selectedItem.name}"/>
        
        <!-- MOVE ITEM LEFT -->
        <s:Button label="&lt; Move" fontSize="16" height="100%" 
                  visible="{showButtons( context.selectedItem )}"
                  includeInLayout="{showButtons( context.selectedItem )}"/>        
        
        <!-- MOVE ITEM RIGHT -->
        <s:Button label="Move &gt;" fontSize="16" height="100%" 
                  visible="{showButtons( context.selectedItem )}"
                  includeInLayout="{showButtons( context.selectedItem )}"/>
        
        <!-- DELETE ITEM -->
        <s:Button label="Delete" fontSize="16" height="100%" 
                  visible="{showButtons( context.selectedItem )}"
                  includeInLayout="{showButtons( context.selectedItem )}"/>

        
    </s:HGroup>
    
    <!-- ITEM DESCRIPTION -->
    <s:TextArea id="itemDesc" width="100%" height="100%" 
                change="descEdit()" prompt="Description"
                text="{context.selectedItem.description}"/>
        
</s:VGroup>

The Notes Component

UI—The Notes Component
Figure 4-7. UI—The Notes Component

Class

Notes.mxml

Responsibilities

  • Declare a Flex Label and List for displaying the selected item’s Note list

  • Declare Flex Buttons for adding and removing Notes

  • Declare a Flex TextArea and TextInput for editing the selected Note

  • Declare a Flex Button for launching a browser to view the selected Note’s URL

  • Provide a label function to supply names for the Notes in the List, since Notes do not have a name field

  • Populate the form fields upon selection of a Note in the list

  • Update the selected Note’s text and url in response to user edits

  • Dispatch appropriate events when the “Add Note” (+) button is pressed (remove function deferred to a later iteration)

  • Expose a bindable public property for setting the SelectionContext

  • Wrap the selected item’s Vector of NoteVOs in an ArrayCollection for the List dataProvider

Collaborations

The Notes component knows and controls its direct children, the various Flex Button, TextInput, List, and TextArea components. It also knows the SelectionContext class, which it references in methods and binding expressions. And it knows the NoteVO, ValueObject, and AppEvent classes. It also dispatches AppEvents for adding and selecting a Note.

The Notes component is known only by its parent, the Details component.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- NOTES -->
<s:HGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          xmlns:details="com.futurescale.sa.view.component.details.*"
          width="100%" height="100%">
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.NoteVO;
            import com.futurescale.sa.model.vo.ValueObject;
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            import mx.collections.ArrayCollection;
            
             // Selection context
            [Bindable] public var context:SelectionContext;
            
            // Wrap the vector in a collection for the list.
            public function wrapNotes( vo:ValueObject ):ArrayCollection
            {
                var notes:Array = new Array();
                for each (var note:NoteVO in vo.notes) {
                    notes.push(note);
                }
                return new ArrayCollection( notes );
            }
            
            // Add a note to the selected item
            private function addNote():void
            {
                var event:AppEvent = new AppEvent( AppEvent.ADD_NOTE );
                event.data = context.selectedItem;
                dispatchEvent( event );
            }
            
            // Select a note from the selected item's note list
            private function selectNote():void
            {
                var event:AppEvent = new AppEvent( AppEvent.SELECT_NOTE );
                event.data = noteList.selectedItem;
                dispatchEvent( event );
            }
            
            // Provide a label for notes in the list
            private function noteLabelFunction(item:Object):String
            {
                var label:String = "Note ";
                var notes:Vector.<NoteVO> = context.selectedItem.notes;
                for ( var i:int = 0; i<notes.length; i++ ) {
                    if ( notes[i].xml === NoteVO(item).xml ) {
                        label += String(i+1); 
                        break;
                    }
                }
                return label;
            }
                        
            // Update selected note text when edited
            private function textEdit():void
            {
                if (context.note) context.note.text = noteText.text;
            }
            
            // Update selected note URL when edited
            private function urlEdit():void
            {
                if (context.note) context.note.url = noteURL.text;
            }
            
            // Open a browser to view the selected note URL
            private function openURL():void
            {
                var urlRequest:URLRequest = new URLRequest(context.note.url);
                navigateToURL(urlRequest);
            }
        
    </fx:Script>

    <!-- NOTES LIST MANAGEMENT -->
    <s:VGroup height="100%"
              paddingRight="0" paddingLeft="0"
              paddingBottom="5" paddingTop="5">

        <!-- LABEL -->
        <s:HGroup height="30" width="100%" verticalAlign="bottom" >
            <s:Label text="Notes" fontSize="16"/>
        </s:HGroup>
    
        <!-- NOTE LIST -->
        <s:List id="noteList" width="100%" height="100%"
                dataProvider="{ wrapNotes( context.selectedItem ) }" 
                change="selectNote()"
                labelFunction="noteLabelFunction"/>
        
        <!-- NOTE BUTTONS -->
        <s:HGroup width="100%">
            <s:Button label="-"  width="50%"
                      enabled="{noteList.selectedItem != null}"/>
            <s:Button label="+" width="50%" click="addNote()"/>
        </s:HGroup>

    </s:VGroup>

    <!-- NOTE FORM -->
    <s:VGroup height="100%" width="100%" enabled="{context.note != null}"
              paddingRight="5" paddingLeft="5"
              paddingBottom="5" paddingTop="5">

        <!-- NOTE TEXT -->
        <s:TextArea id="noteText" text="{context.note.text}"  
                    prompt="Note Text..." change="textEdit()" 
                    width="100%" height="100%"/>
        
        <!-- NOTE URL -->
        <s:HGroup width="100%" 
                  paddingRight="0" paddingLeft="0"
                  paddingBottom="0" paddingTop="0">

            <!-- URL INPUT -->
            <s:TextInput id="noteURL" width="100%" prompt="Note URL..."
                         text="{context.note.url}" change="urlEdit()"  />
            
            <!-- GO BUTTON -->
            <s:Button label="Go" click="openURL()"
                      enabled="{context.note.url.length != 0}" />    
    
        </s:HGroup>        
        
    </s:VGroup>

The Timeline Component

UI—The Editor Controls showing the Timeline Component
Figure 4-8. UI—The Editor Controls showing the Timeline Component

Class

Timeline.mxml

Responsibilities

  • Base on Flex Scroller class

  • Declare Flex HGroup for containing the scrollable content

  • Expose a public property for setting the StoryVO to be displayed

  • Create a StoryTile and replace any existing children of the HGroup with it when the StoryVO is set

  • Provide a label function to supply names for the notes in the List, since Notes do not have a name field

  • Expose a bindable public property for setting the SelectionContext

  • Provide the displayed StoryTile with the SelectionContext object

Collaborations

The Timeline component knows and controls its direct children, the Flex HGroup and custom StoryTile components. It also knows the SelectionContext class, which it passes to the StoryTile, and the StoryVO, which it uses to create a StoryTile.

The Timeline component is known only by its parent, the Controls component.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- TIMELINE -->
<s:Scroller xmlns:fx="http://ns.adobe.com/mxml/2009" 
         xmlns:s="library://ns.adobe.com/flex/spark">
    
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.StoryVO;
            import com.futurescale.sa.view.context.SelectionContext;
            
            // Selection context
            [Bindable] public var context:SelectionContext;
            
            // The Story
            public function set story( storyVO:StoryVO ):void {
                _story = storyVO;
                storyGroup.removeAllElements();
                if (storyVO) {
                    var storyTile:StoryTile = new StoryTile();
                    storyTile.context       = context;
                    storyTile.story         = story;
                    storyGroup.addElement( storyTile );
                }
            }
            public function get story( ):StoryVO  {
                return _story;
            }
            private var _story:StoryVO;
        
    </fx:Script>

    <!-- STORY GROUP -->
    <s:HGroup id="storyGroup" width="100%" height="100%"
              paddingRight="5" paddingBottom="5"
              paddingLeft="5"/>

</s:Scroller>

The Story Tile

UI—The Story Tile (Simple Story)
Figure 4-9. UI—The Story Tile (Simple Story)
UI—The Story Tile (Normal Story)
Figure 4-10. UI—The Story Tile (Normal Story)
UI—The Story Tile (Complex Story)
Figure 4-11. UI—The Story Tile (Complex Story)

Class

StoryTile.mxml

Responsibilities

  • Base on Flex VGroup class

  • Declare a Flex HGroup for containing PartTile, ChapterTile, or SceneTile instances

  • Declare a Flex ToggleButton for selecting the StoryVO

  • Expose a public property for setting the StoryVO to be displayed

  • When StoryVO is selected, create appropriate PartTile, ChapterTile, or SceneTile instances, replacing any existing children of the HGroup

  • When the appropriate tile components are added to the HGroup, add an AddTile to the end, set to dispatch the appropriate event for adding another child

  • Removing and recreating all tiles when the StoryVO is selected or deselected will have the effect of expanding or collapsing those tiles

  • Expose a bindable public property for setting the SelectionContext

  • Provide the displayed PartTile, ChapterTile, or SceneTile instances with the SelectionContext object

Collaborations

The StoryTile component knows and controls its direct children, the Flex HGroup, and custom PartTile, ChapterTile, SceneTile, and AddTile components. It knows the SelectionContext class, which it passes to the tile components, and the StoryVO, PartVO, ChapterVO, and SceneVO, which it uses to create the appropriate tile components depending on the Story type.

The StoryTile component is known only by its parent, the Timeline component. In future iterations, it will also be known by the SeasonTile component when Series functionality is added to the Timeline component.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- STORY TILE -->
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          height="100%" width="100%">

    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.ChapterVO;
            import com.futurescale.sa.model.vo.PartVO;
            import com.futurescale.sa.model.vo.SceneVO;
            import com.futurescale.sa.model.vo.StoryVO;
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            // Selection context
            [Bindable] public var context:SelectionContext;
            
            // The Story.
            [Bindable] public function set story( storyVO:StoryVO ):void
            {
                _story = storyVO;
            }
            public function get story():StoryVO
            {
                return _story;
            }
            private var _story:StoryVO;

            // Remove or add tiles according to the selected story
            private function changeStorySelection( selection:Boolean ):Boolean
            {
                if (!selection) {
                    removeTiles();
                } else {
                    createTiles();
                }
                return selection;
            }
            
            // Create Tiles 
            private function createTiles():void
            {
                removeTiles();
                var addTile:AddTile = new AddTile();
                
                if ( story.useScenes ) 
                { 
                    // Create the SceneTiles
                    var scenes:Vector.<SceneVO> = story.scenes;
                    for (var s:int=0; s< scenes.length; s++ ) {
                        var sceneTile:SceneTile = new SceneTile();
                        sceneTile.context = context;
                        sceneTile.scene   = scenes[s];
                        tileGroup.addElement( sceneTile );
                    }
                    
                    // Create the 'Add Scene' tile
                    addTile.addType     = AppEvent.ADD_SCENE;
                    addTile.addTarget   = story; 
                } 
                else if ( story.useChapters ) 
                { 
                    // Create the ChapterTiles
                    var chapters:Vector.<ChapterVO> = story.chapters;
                    for (var c:int=0; c<chapters.length; c++ ) {
                        var chapterTile:ChapterTile = new ChapterTile();
                        chapterTile.context = context;
                        chapterTile.chapter = chapters[c];
                        tileGroup.addElement( chapterTile );
                    }
                    
                    // Create the 'Add Chapter' tile
                    addTile.addType     = AppEvent.ADD_CHAPTER;
                    addTile.addTarget   = story; 
                    
                } 
                else if ( story.useParts ) 
                { 
                    // Create the PartTiles
                    var parts:Vector.<PartVO> = story.parts;
                    for (var p:int=0; p< parts.length; p++ ) {
                        var partTile:PartTile = new PartTile();
                        partTile.context = context;
                        partTile.part    = parts[p];
                        tileGroup.addElement( partTile );
                    }
                    
                    // Create the 'Add Part' tile
                    addTile.addType     = AppEvent.ADD_PART;
                    addTile.addTarget   = story; 
                } 
                tileGroup.addElement( addTile );
                tileGroup.percentHeight=100;
            }
            
            // Remove any existing tiles
            private function removeTiles():void
            {
                tileGroup.removeAllElements();
                tileGroup.percentHeight=0;
            }
            
            // Select the story
            private function selectStory():void
            {
                var appEvent:AppEvent = new AppEvent(AppEvent.SELECT_STORY);
                appEvent.data         = story;
                dispatchEvent(appEvent);
            }
            
        
    </fx:Script>

    <!-- TILE GROUP -->
    <s:HGroup id="tileGroup" width="100%"/>
    
    <!-- STORY BUTTON -->
    <s:ToggleButton id="storyButton" click="selectStory()"
                    selected="{changeStorySelection(context.story.uid == story.uid)}"
                    height="100%" width="100%" minWidth="150" minHeight="25" 
                    label="{story.name}"/>
    
</s:VGroup>

The Part Tile

UI—The Part Tile
Figure 4-12. UI—The Part Tile

Class

PartTile.mxml

Responsibilities

  • Base on Flex VGroup class

  • Declare a Flex HGroup for containing ChapterTile instances

  • Declare a Flex ToggleButton for selecting the PartVO

  • Expose a public property for setting the PartVO to be displayed

  • When PartVO is selected create ChapterTile instances, replacing any existing children of the HGroup

  • When the appropriate tile components are added to the HGroup, add an AddTile to the end, set to dispatch the appropriate event for adding another child

  • Removing or recreating all tiles when the PartVO is selected or deselected will have the effect of expanding or collapsing those tiles

  • Expose a bindable public property for setting the SelectionContext

  • Provide the displayed ChapterTile instances with the SelectionContext object

Collaborations

The PartTile component knows and controls its direct children, the Flex HGroup, and custom ChapterTile and AddTile components. It also knows the SelectionContext class, which it passes to the tile components, and the PartVO and ChapterVO, which it uses to create the appropriate tile components. Finally, it knows the AppEvent class, which it uses to inform the AddTile of the appropriate event to dispatch.

The PartTile component is known only by its parent, the StoryTile component.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- PART TILE -->
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          xmlns:mx="library://ns.adobe.com/flex/mx"
          height="100%" width="100%">

    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.ChapterVO;
            import com.futurescale.sa.model.vo.PartVO;
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            // Selection context
            [Bindable] public var context:SelectionContext;
            
            // The Part
            [Bindable] public var part:PartVO;
            
            // Remove or add tiles according to the selected Part
            private function changePartSelection( selection:Boolean ):Boolean
            {
                if (!selection) {
                    removeTiles();
                } else {
                    createTiles();
                }
                return selection;
            }
            
            // Create Tiles 
            private function createTiles():void
            {
                // Create the Chapter tiles
                removeTiles();
                var chapters:Vector.<ChapterVO> = part.chapters;
                for (var i:int=0; i<chapters.length; i++ ) {
                    var chapterTile:ChapterTile = new ChapterTile();
                    chapterTile.context = context;
                    chapterTile.chapter = chapters[i];
                    chapterGroup.addElement( chapterTile );
                }
                
                // Create the 'Add Chapter' tile
                var addTile:AddTile = new AddTile();
                addTile.addType     = AppEvent.ADD_CHAPTER;
                addTile.addTarget   = part;
                chapterGroup.addElement( addTile );
                chapterGroup.percentHeight=100;
            }            

            // Remove Tiles 
            private function removeTiles():void
            {
                chapterGroup.removeAllElements();
                chapterGroup.percentHeight=0;
            }
            
            // Toggle the Part selection 
            private function selectPart():void
            {
                var event:AppEvent;
                if (partButton.selected) {
                    event = new AppEvent(AppEvent.SELECT_PART);
                } else {
                    event = new AppEvent(AppEvent.DESELECT_PART);
                    removeTiles();
                }
                event.data    = part;
                event.related = context.story;
                dispatchEvent(event);
            }
            
        
    </fx:Script>
    
    <!-- CHAPTER GROUP -->
    <s:HGroup id="chapterGroup" width="100%"/>
    
    <!-- PART BUTTON -->
    <s:ToggleButton id="partButton" click="selectPart()"
                    selected="{changePartSelection(context.part.uid == part.uid)}"
                    height="100%" width="100%" minWidth="150" minHeight="25"
                    label="{part.name}"/>

</s:VGroup>

The Chapter Tile

UI—The Chapter Tile
Figure 4-13. UI—The Chapter Tile

Class

ChapterTile.mxml

Responsibilities

  • Base on Flex VGroup class

  • Declare a Flex HGroup for containing SceneTile instances

  • Declare a Flex ToggleButton for selecting the ChapterVO

  • Expose a public property for setting the ChapterVO to be displayed

  • When ChapterVO is selected, create SceneTile instances, replacing any existing children of the HGroup

  • When the appropriate tile components are added to the HGroup, add an AddTile to the end, set to dispatch the appropriate event for adding another child

  • Removing or recreating all tiles when the ChapterVO is selected or deselected will have the effect of expanding or collapsing those tiles

  • Expose a bindable public property for setting the SelectionContext

  • Provide the displayed SceneTile instances with the SelectionContext object

Collaborations

The ChapterTile component knows and controls its direct children, the Flex HGroup, and custom SceneTile and AddTile components. It also knows the SelectionContext class, which it passes to the tile components, and the SceneVO and ChapterVO, which it uses to create the appropriate tile components. Finally, it knows the AppEvent class, which it uses to inform the AddTile of the appropriate event to dispatch.

The ChapterTile component is known only by its two possible parents, the StoryTile and PartTile components.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- CHAPTER TILE -->
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          xmlns:mx="library://ns.adobe.com/flex/mx"
          height="100%" width="100%">

    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.ChapterVO;
            import com.futurescale.sa.model.vo.SceneVO;
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            // Selection context
            [Bindable] public var context:SelectionContext;
            
            // The Chapter
            [Bindable] public var chapter:ChapterVO;
            
            // Remove or add tiles according to the selected Chapter
            private function changeChapterSelection( selection:Boolean ):Boolean
            {
                if (!selection) {
                    removeTiles();
                } else {
                    createTiles();
                }
                return selection;
            }
            
            // Create Tiles
            private function createTiles():void
            {
                // Create SceneTiles
                removeTiles();
                var scenes:Vector.<SceneVO> = chapter.scenes;
                for (var i:int=0; i<scenes.length; i++ ) {
                    var sceneTile:SceneTile = new SceneTile();
                    sceneTile.context = context;
                    sceneTile.scene   = scenes[i];
                    sceneGroup.addElement( sceneTile );
                }
                
                // Create 'Add Scene' Tile
                var addTile:AddTile = new AddTile();
                addTile.addType     = AppEvent.ADD_SCENE;
                addTile.addTarget   = chapter;
                addTile.addArgument = (context.part)?context.part:context.story;
                sceneGroup.addElement( addTile );
                sceneGroup.percentHeight=100;
            }

            // Remove tiles
            private function removeTiles():void
            {
                sceneGroup.removeAllElements();
                sceneGroup.percentHeight=0;
            }
            
            // Toggle the Chapter selection 
            private function selectChapter():void
            {
                var event:AppEvent;
                if (chapterButton.selected) {
                    event = new AppEvent(AppEvent.SELECT_CHAPTER);
                } else {
                    event = new AppEvent(AppEvent.DESELECT_CHAPTER);
                    removeTiles();
                }
                event.data    = chapter;
                event.related = (context.part)?context.part:context.story;
                dispatchEvent(event);
            }
            
        
    </fx:Script>

    <!-- SCENE GROUP -->
    <s:HGroup id="sceneGroup" width="100%"/>
    
    <!-- CHAPTER BUTTON -->
    <s:ToggleButton id="chapterButton" click="selectChapter()"
                    selected="{changeChapterSelection(context.chapter.uid == chapter.uid)}"
                    height="100%" width="100%" minWidth="150" minHeight="25"
                    label="{chapter.name}"/>

</s:VGroup>

The Scene Tile

UI—The Scene Tile
Figure 4-14. UI—The Scene Tile

Class

SceneTile.mxml

Responsibilities

  • Base on Flex VGroup class

  • Declare a Flex HGroup for containing DraftTile instances

  • Declare a Flex ToggleButton for selecting the SceneVO

  • Expose a public property for setting the SceneVO to be displayed

  • When SceneVO is selected, create DraftTile instances, replacing any existing children of the HGroup

  • When the appropriate tile components are added to the HGroup, add an AddTile to the end, set to dispatch the appropriate event for adding another child

  • Removing or recreating all tiles when the SceneVO is selected or deselected will have the effect of expanding or collapsing those tiles

  • Expose a bindable public property for setting the SelectionContext

  • Provide the displayed DraftTile instances with the SelectionContext object

Collaborations

The SceneTile component knows and controls its direct children, the Flex HGroup, and custom DraftTile and AddTile components. It also knows the SelectionContext class, which it passes to the tile components, and the SceneVO and DraftVO, which it uses to create the appropriate tile components. Finally, it knows the AppEvent class, which it uses to inform the AddTile of the appropriate event to dispatch.

The SceneTile component is known only by its three possible parents: the StoryTile, PartTile, and ChaperTile components.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- SCENE TILE -->
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          height="100%" width="100%">
    
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.DraftVO;
            import com.futurescale.sa.model.vo.SceneVO;
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            //Selection context
            [Bindable] public var context:SelectionContext;
            
            // The Scene
            [Bindable] public var scene:SceneVO;

            // Remove or add tiles according to the selected Scene
            private function changeSceneSelection( selection:Boolean ):Boolean
            {
                if (!selection) {
                    removeTiles();
                } else {
                    createTiles();
                }
                return selection;
            }
            
            // Create Tiles
            private function createTiles():void
            {
                // Create the Draft tiles
                removeTiles();
                var drafts:Vector.<DraftVO> = scene.drafts;
                for (var i:int=0; i<drafts.length; i++ ) {
                    var draftTile:DraftTile = new DraftTile();
                    draftTile.context = context;
                    draftTile.draft   = drafts[i];
                    draftGroup.addElement( draftTile );
                }
                
                // Create the 'Add Draft' tile
                var addTile:AddTile = new AddTile();
                addTile.addType     = AppEvent.ADD_DRAFT;
                addTile.addTarget   = scene; 
                draftGroup.addElement( addTile );
                draftGroup.percentHeight=100;
            }

            // Remove tiles
            private function removeTiles():void
            {
                draftGroup.removeAllElements();
                draftGroup.percentHeight=0;
            }
            
            // Toggle the Scene selection 
            private function selectScene():void
            {
                var event:AppEvent;
                if (sceneButton.selected) {
                    event = new AppEvent(AppEvent.SELECT_SCENE);
                } else {
                    event = new AppEvent(AppEvent.DESELECT_SCENE);
                    removeTiles();
                }
                event.data    = scene;
                event.related = (context.chapter)?context.chapter:context.story;
                dispatchEvent(event);
            }
        
    </fx:Script>
    
    <!-- DRAFT GROUP -->
    <s:HGroup id="draftGroup" width="100%"/>
    
    <!-- SCENE BUTTON -->
    <s:ToggleButton id="sceneButton" click="selectScene()"
                    selected="{changeSceneSelection(context.scene.uid == scene.uid)}"
                    height="100%" width="100%" minWidth="150" minHeight="25"
                    label="{scene.name}"/>
    
</s:VGroup>

The Draft Tile

UI—The Draft Tile
Figure 4-15. UI—The Draft Tile

Class

DraftTile.mxml

Responsibilities

  • Base on Flex VGroup class

  • Declare a Flex HGroup for padding to exact height of other tiles, since this tile has no children

  • Declare a Flex ToggleButton for selecting the DraftVO

  • Expose a public property for setting the DraftVO to be displayed

  • Clicking the ToggleButton should set the selected SceneVO’s currentDraft and dispatch the appropriate event to select the DraftVO

  • Expose a bindable public property for setting the SelectionContext

  • Provide the displayed DraftTile instances with the SelectionContext object

Collaborations

The DraftTile component knows and controls its direct children: the Flex HGroup and Button components. It also knows the SelectionContext class, the DraftVO class, and the AppEvent class.

The DraftTile component is known only by its parent, the SceneTile component.

Code

<?xml version="1.0" encoding="utf-8"?>
<!-- DRAFT TILE -->
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009" 
          xmlns:s="library://ns.adobe.com/flex/spark" 
          xmlns:component="com.futurescale.sa.view.component.*"
          height="100%" width="100%">
    
    <fx:Script>
        <![CDATA[
            import com.futurescale.sa.model.vo.DraftVO;
            import com.futurescale.sa.model.vo.SceneVO;
            import com.futurescale.sa.view.context.SelectionContext;
            import com.futurescale.sa.view.event.AppEvent;
            
            // Selection context
            [Bindable] public var context:SelectionContext;
            
            // The Draft
            [Bindable] public var draft:DraftVO;

            // Toggle the Draft selection 
            public function selectDraft():void
            {
                context.scene.currentDraft = draft;
                var appEvent:AppEvent = new AppEvent( AppEvent.SELECT_DRAFT );
                appEvent.data = draft;
                appEvent.related = context.scene;
                dispatchEvent(appEvent);
            }

        
    </fx:Script>

    <!-- PADDING GROUP -->
    <s:HGroup width="100%"/>
    
    <!-- DRAFT BUTTON -->
    <s:ToggleButton id="draftButton" click="selectDraft()"
                    selected="{context.draft.uid == draft.uid}"
                    height="100%" width="100%" minWidth="75" minHeight="25"
                    label="{draft.name}"/>
    
</s:VGroup>

The Selection Context

Class

SelectionContext.as

Responsibilities

  • Expose public methods for selecting various ValueObject subclasses. These methods should ensure the deselection of related items or items below in the Story or Series hierarchy. For example, selecting a Part should clear the current Chapter, Scene, Draft, and Note selections, but not the Story, Season, or Series selections or the Cast and Milieu selections. Selecting a Story would also select its Cast and Milieu and clear the Part, Chapter, Scene, Draft, and Note selections.

  • A bindable public ValueObject typed selectedItem property should always be set to the item being selected by a selection method. Thus, selectStory( story:StoryVO ) would not only set the story property to the inbound StoryVO, but also set the selectedItem:ValueObject property as well. The selectedItem property is used by the ItemInfo and Notes View Components, which work with any ValueObject.

  • Expose bindable public properties for reading the currently selected CastVO, ChapterVO, CharacterVO, DraftVO, CastVO, MilieuVO, NoteVO, PartVO, SceneVO, SeasonVO, SeriesVO, SettingVO, StoryVO, and ValueObject.

Collaborations

The SelectionContext class knows CastVO, ChapterVO, CharacterVO, DraftVO, CastVO, MilieuVO, NoteVO, PartVO, SceneVO, SeasonVO, SeriesVO, SettingVO, StoryVO, and ValueObject classes, for which it maintains the entire system’s notion of selected items for operations.

The SelectionContext class is known by most all View Components, which look to its properties to determine their selection-related behaviors. The SelectionContext class is also known by various Commands, including AddItemCommand, DeleteItemCommand, ApplySelectionCommand, RemoveSelectionCommand, ApplyChangesCommand, DiscardChangesCommand, StartupCommand, etc.

Code

package com.futurescale.sa.view.context
{
    import com.futurescale.sa.model.vo.CastVO;
    import com.futurescale.sa.model.vo.ChapterVO;
    import com.futurescale.sa.model.vo.CharacterVO;
    import com.futurescale.sa.model.vo.DraftVO;
    import com.futurescale.sa.model.vo.MilieuVO;
    import com.futurescale.sa.model.vo.NoteVO;
    import com.futurescale.sa.model.vo.PartVO;
    import com.futurescale.sa.model.vo.SceneVO;
    import com.futurescale.sa.model.vo.SeasonVO;
    import com.futurescale.sa.model.vo.SeriesVO;
    import com.futurescale.sa.model.vo.SettingVO;
    import com.futurescale.sa.model.vo.StoryVO;
    import com.futurescale.sa.model.vo.ValueObject;

    /**
     * The currently selected items in the UI. 
     * 
     * When calling the select methods, items 
     * below the selection in the Series/Story 
     * hierarchy as well as the selected Note,
     * Cast and Milieu are automatically nulled
     * as appropriate.
     */
    [Bindable] public class SelectionContext
    {
        public static const NAME:String = "SelectionContext";
        
        public function selectSeries( series:SeriesVO ):void
        {
            this.selectedItem = series;
            this.series  = series;
            this.season  = null;
            this.story   = null;
            this.part    = null;
            this.chapter = null;
            this.scene   = null;
            this.draft   = null;
            this.note    = null;
            if ( series ) {
                this.cast   = series.cast;
                this.milieu = series.milieu;
                this.setting   = null;
                this.character = null;
            }
        }
        
        public function selectSeason( season:SeasonVO ):void
        {
            this.selectedItem = season;
            this.season  = season;
            this.story   = null;
            this.part    = null;
            this.chapter = null;
            this.scene   = null;
            this.draft   = null;
            this.note    = null;
        }
        
        public function selectStory( story:StoryVO ):void
        {
            this.selectedItem = story;
            this.story   = story;
            this.part    = null;
            this.chapter = null;
            this.scene   = null;
            this.draft   = null;
            this.note    = null;
            if ( story ) {
                this.cast   = story.cast;
                this.milieu = story.milieu;
                this.setting   = null;
                this.character = null;
            }
            selectedText = (story)? story.getText(true) : "";
        }
        
        public function selectPart( part:PartVO ):void
        {
            this.selectedItem = part;
            this.part    = part;
            this.chapter = null;
            this.scene   = null;
            this.draft   = null;
            this.note    = null;
            selectedText = (part)? part.getText(true) : "";
        }
        
        public function selectChapter( chapter:ChapterVO ):void
        {
            this.selectedItem = chapter;
            this.chapter = chapter;
            this.scene   = null;
            this.draft   = null;
            this.note    = null;
            selectedText = (chapter)? chapter.getText(true) : "";
        }
        
        public function selectScene( scene:SceneVO ):void
        {
            this.selectedItem = scene;
            this.scene   = scene;
            this.draft   = null;
            this.note    = null;
            selectedText = (scene)? scene.getText(true) : "";
            if ( scene && scene.currentDraft ) selectDraft( scene.currentDraft ); 
        }
        
        public function selectDraft( draft:DraftVO ):void
        {
            this.scene.currentDraft = draft;
            this.draft   = draft;
            this.note    = null;
            selectedText = (draft)? draft.text : "";
        }
        
        public function selectNote( note:NoteVO ):void
        {
            this.note    = note;
        }
        
        public function selectCharacter( character:CharacterVO ):void
        {
            this.selectedItem = character;
            this.character = character;
            this.note   = null;
        }
        
        public function selectSetting( setting:SettingVO ):void
        {
            this.selectedItem = setting;
            this.setting = setting;
            this.note   = null;
        }
        
        public var selectedText:String;
        public var selectedItem:ValueObject;
        
        public var series:SeriesVO;
        public var season:SeasonVO;
        public var story:StoryVO;
        public var part:PartVO;
        public var chapter:ChapterVO;
        public var scene:SceneVO;
        public var draft:DraftVO;
        public var note:NoteVO;
        public var cast:CastVO;
        public var milieu:MilieuVO;
        public var character:CharacterVO;
        public var setting:SettingVO;
    }

The App Event

Class

AppEvent.as

Responsibilities

Some people like to build separate classes for every Event their system will use, and that is a perfectly valid approach. In this application, we will have one custom Event class with many possible types.

With the AppEvent, we are creating one part of a two-part protocol. The constants defined on the AppEvent class represent, in one place, all of the intentions that will arise from the user interface to be processed somewhere, either higher up in the display list, or deeper in the system by Mediators and Commands. The other part of that two-part protocol is the AppConstants file, where we will define, in a similar way, all of the notification names that will be shared by the View and Controller tiers. There will often be duplication between the two files, leading one to ponder whether they should be combined into a single file, and the answer is no. There will not always be a one-to-one relationship between event names and notification names, as you will shortly see. It is best to separate event and notification names and let the Mediators take care of translation. This two-part protocol approach is in keeping with the promise made earlier about being able to farm out the building of the entire UI to a developer or team that does not know anything about PureMVC. It lets the UI developer decide what events to dispatch and the PureMVC developers decide on how best to mediate those events.

Also notice we take the approach of defining verbs and nouns as private constants and then combine them in public constants that are used for AppEvent types. This is not necessary, but it can have a clarifying effect on your event naming.

  • Extend the Flash Event class

  • Define public static constants for valid AppEvent types

  • Define public Object typed properties for data and related data

  • Accept type, data, and related arguments on the constructor, setting them to the associated public properties

  • Constructor should call superclass constructor with the passed in type, a true bubbling argument, and a true cancelable argument

Collaborations

The AppEvent class knows only the Flash Event class that it extends, and the ActionScript Object class that it has public properties for.

The AppEvent class is known by most all View Components, which dispatches events defined by its public, static type constants. The AppEvent class is also known by various Mediators, who listen to their View Components for them, and many Commands, including AddItemCommand, DeleteItemCommand, ApplySelectionCommand, RemoveSelectionCommand, ApplyChangesCommand, DiscardChangesCommand, which interpret the AppEvents relayed to them inside Notifications by Mediators in order to complete their business logic.

Code

package com.futurescale.sa.view.event
{
    import flash.events.Event;
    
    public class AppEvent extends Event
    {
        // Components of the AppEvent types 
        private static const ADD:String             = "add";
        private static const DELETE:String          = "delete";
        private static const MANAGE:String          = "manage";
        private static const SELECT:String          = "delect";
        private static const DESELECT:String        = "deselect";
        private static const EXPORT:String          = "export";
        private static const OUTLINE:String         = "outline";
        private static const EDIT:String            = "edit";
        private static const DISCARD:String         = "discard";
        private static const SAVE:String            = "save";
        private static const REPORT:String          = "report";
        private static const MOVE:String            = "move";
        
        private static const SERIES:String          = "Series";
        private static const SEASON:String          = "Season";
        private static const EPISODE:String         = "Episode";
        private static const STORY:String           = "Story";
        private static const PART:String            = "Part";
        private static const CHAPTER:String         = "Chapter";
        private static const SCENE:String           = "Scene";
        private static const DRAFT:String           = "Draft";
        private static const CAST:String            = "Cast";
        private static const CHARACTER:String       = "Character";
        private static const MILIEU:String          = "Milieu";
        private static const SETTING:String         = "Setting";
        private static const NOTE:String            = "Note";
        private static const WORDCOUNT:String       = "WordCount";
        private static const AHEAD:String           = "Ahead";
        private static const BACK:String            = "Back";

        // AppEvent types
        public static const ADD_SERIES:String       = ADD+SERIES;
        public static const ADD_SEASON:String       = ADD+SEASON;
        public static const ADD_EPISODE:String      = ADD+EPISODE;
        public static const ADD_STORY:String        = ADD+STORY;
        public static const ADD_PART:String         = ADD+PART;
        public static const ADD_CHAPTER:String      = ADD+CHAPTER;
        public static const ADD_SCENE:String        = ADD+SCENE;
        public static const ADD_DRAFT:String        = ADD+DRAFT;
        public static const ADD_CHARACTER:String    = ADD+CHARACTER;
        public static const ADD_SETTING:String      = ADD+SETTING;
        public static const ADD_NOTE:String         = ADD+NOTE;

        public static const DELETE_SERIES:String    = DELETE+SERIES;
        public static const DELETE_SEASON:String    = DELETE+SEASON;
        public static const DELETE_EPISODE:String   = DELETE+EPISODE;
        public static const DELETE_STORY:String     = DELETE+STORY;
        public static const DELETE_PART:String      = DELETE+PART;
        public static const DELETE_CHAPTER:String   = DELETE+CHAPTER;
        public static const DELETE_SCENE:String     = DELETE+SCENE;
        public static const DELETE_DRAFT:String     = DELETE+DRAFT;
        public static const DELETE_CHARACTER:String = DELETE+CHARACTER;
        public static const DELETE_SETTING:String   = DELETE+SETTING;
        public static const DELETE_NOTE:String      = DELETE+NOTE;
        
        public static const DISCARD_STORY:String    = DISCARD+STORY;
        public static const DISCARD_SERIES:String   = DISCARD+SERIES;
        
        public static const EXPORT_SERIES:String    = EXPORT+SERIES;
        public static const EXPORT_STORY:String     = EXPORT+STORY;
        
        public static const EDIT_SEREIES:String     = EDIT+SERIES;
        public static const EDIT_STORY:String       = EDIT+STORY;
        
        public static const SAVE_STORY:String       = SAVE+STORY;
        public static const SAVE_SERIES:String      = SAVE+SERIES;

        public static const MANAGE_SERIES:String    = MANAGE+SERIES;
        public static const MANAGE_STORY:String     = MANAGE+STORY;
        public static const MANAGE_CAST:String      = MANAGE+CAST;
        public static const MANAGE_MILIEU:String    = MANAGE+MILIEU;

        public static const MOVE_AHEAD:String       = MOVE+AHEAD;
        public static const MOVE_BACK:String        = MOVE+BACK;
        
        public static const OUTLINE_SERIES:String   = OUTLINE+SERIES;
        public static const OUTLINE_STORY:String    = OUTLINE+STORY;
        
        public static const REPORT_WORDCOUNT:String = REPORT+WORDCOUNT;
        
        public static const SELECT_SERIES:String    = SELECT+SERIES;
        public static const SELECT_SEASON:String    = SELECT+SEASON;
        public static const SELECT_STORY:String     = SELECT+STORY;
        public static const SELECT_PART:String      = SELECT+PART;
        public static const SELECT_CHAPTER:String   = SELECT+CHAPTER;
        public static const SELECT_SCENE:String     = SELECT+SCENE;
        public static const SELECT_DRAFT:String     = SELECT+DRAFT;
        public static const SELECT_CHARACTER:String = SELECT+CHARACTER;
        public static const SELECT_SETTING:String   = SELECT+SETTING;
        public static const SELECT_NOTE:String      = SELECT+NOTE;
        
        public static const DESELECT_PART:String    = DESELECT+PART;
        public static const DESELECT_CHAPTER:String = DESELECT+CHAPTER;
        public static const DESELECT_SCENE:String   = DESELECT+SCENE;
        
        public var data:Object;     // optional data object
        public var related:Object;  // optional related data object

        public function AppEvent( type:String, data:Object=null, related:Object=null )
        {
            super(type, true, true);
            this.data = data;
            this.related = related;
        }
    }
}

Get ActionScript Developer's Guide to PureMVC now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.