You are hereAria User Guide / Section V: Case Studies / Case Study: Wizards
Case Study: Wizards
- Wizard application
Introduction
This wizard application which will be examined in this section is for generating product selection applications for SMEs. It is a fully functional application and is in use in production environments by some of Formaria's clients. It's intended audience is small to medium companies who wish to manage their product listings and to make those products available within a simple and easy to use application. The typical user would not be particularly technical and with that in mind spreadsheets were chosen as the preferred format for storing product information. The wizard is only the first step in creating the application for the end user and can be used to setup and manage any number of projects.
This application makes use of many of the features of Aria including page navigation, framesets, component registration, localization, validations, the synth look and feel and data binding amongst others.
![]() |
Framesets and pages
The application consists of a Aria frameset which contains four different target areas. The title frame and navigation frame both remain in the same position with the same content throughout the lifetime of the application. A status frame on the left is initially hidden and is then shown when the user decides on a wizard route. The main content frame takes up the full width of the application when it is first started and contains the WelcomePage Page. Once the status frame appears the width of the main content page is the width of the application less the width of the status frame. The frameset file is defined below.
frames.xml
<FrameSet> <Frame name="banner" constraint="NORTH" content="banner" width="640" height="70"/> <Frame name="wizardstatus" constraint="WEST" content="wizardstatus" width="150" height="500"/> <Frame name="content" constraint="CENTER" content="welcome" width="490" height="324"/> <Frame name="navPanel" constraint="SOUTH" content="navPanel" width="640" height="70"/> </FrameSet>
From the welcome page, it is possible to select one of four radio buttons which will determine the route through the wizard.
The four radio buttons and their declared events
... <RadioButton name="createRad" x="150" y="100" w="250" h="20" style="welcome/prompt" content="WEL_CREATE_PROJ"/> <RadioButton name="generateRad" x="150" y="120" w="250" h="20" style="welcome/prompt" content="WEL_GEN_PROJ"/> <RadioButton name="buildRad" x="150" y="140" w="250" h="20" style="welcome/prompt" content="WEL_BUILD_PROJ"/> <RadioButton name="hotspotRad" x="150" y="160" w="250" h="20" style="welcome/prompt" content="WEL_EDIT_HOTSPOT"/> <Label name="infoLbl" x="100" y="190" w="300" h="80" style="Infotext" alignment="Center" opaque="false"/> ... <Event target="createRad" type="ItemHandler" method="showInfo"/> <Event target="generateRad" type="ItemHandler" method="showInfo"/> <Event target="buildRad" type="ItemHandler" method="showInfo"/> <Event target="hotspotRad" type="ItemHandler" method="showInfo"/> ...
The XML shows the four declarations for each of the radio buttons. The application is completely localized so the content attribute is set to the key for the current resource bundle. The four event declarations are all setup to call the same method showInfo which is shown below. There is also a declaration for a label which will show information for each of the selections and, again, this component uses the current language resource bundle to populate its text. So now the information label needs to be populated depending on the selected radio button.
The Welcome class
... RadioButton createRad, generateRad, buildRad, hotspotRad; Label infoLbl; public void pageCreated() { createRad = ( RadioButton )findComponent( "createRad" ); generateRad = ( RadioButton )findComponent( "generateRad" ); buildRad = ( RadioButton )findComponent( "buildRad" ); hotspotRad = ( RadioButton )findComponent( "hotspotRad" ); infoLbl = ( Label ) findComponent( "infoLbl" ); ... } public void showInfo() { NavPanel navPanel = ( NavPanel ) pageMgr.getPage( "navPanel" ); String text = ""; if ( createRad.isSelected() ) { text = "WEL_CREATE_TEXT"; navPanel.setNavPath( "wizard" ); } else if ( generateRad.isSelected() ) { text = "WEL_GEN_TEXT"; navPanel.setNavPath( "generator" ); } else if ( buildRad.isSelected() ) { text = "WEL_PACK_TEXT"; navPanel.setNavPath( "builder" ); } else if ( hotspotRad.isSelected() ) { text = "WEL_HOTSPOT_GEN"; navPanel.setNavPath( "hotspot" ); } infoLbl.setText( Translator.translate( text ) ); itemSelected = true; } ...
The pageCreated method obtains references to the components which will be used in this operation and the showInfo function sets the translated text in the label for the selected radio button. This application uses a specialized Translator class which will be explained further on.
The NavPanel class is the navigation Page which was declared last in the frames.xml file above. The setNavPath call sets the navigation path which needs to be taken for the selected radio button and that will be looked at next.
Navigation
The application consists of multiple pages and navigation between pages is provided by a common class. In the screenshot below the page navigation is initiated by the buttons at the bottom right of the page.
![]() |
The NavPanel class controls the navigation of the wizard and is shown below.
The NavPanel class
public class NavPanel extends BasePage { Button prevBtn, nextBtn; BasePage currentPage; NavigationManager wizardNavigator, generatorNavigator, builderNavigator, hotspotNavigator, currentNavigator; public void pageCreated() { prevBtn = ( Button ) findComponent( "prevBtn" ); nextBtn = ( Button ) findComponent( "nextBtn" ); setCursors(); wizardNavigator = new WizardNavigator( nextBtn, prevBtn, "navigation/wizardpages" ); generatorNavigator = new NavigationManager( nextBtn, prevBtn, "navigation/generationpages" ); builderNavigator = new NavigationManager( nextBtn, prevBtn, "navigation/builderpages" ); hotspotNavigator = new NavigationManager( nextBtn, prevBtn, "navigation/hotspotpages" ); currentNavigator = wizardNavigator; } public void setNavPath( String name ) { if ( name.compareTo( "wizard" ) == 0 ) currentNavigator = wizardNavigator; else if ( name.compareTo( "generator" ) == 0 ) currentNavigator = generatorNavigator; else if ( name.compareTo( "builder" ) == 0 ) currentNavigator = builderNavigator; else if ( name.compareTo( "hotspot" ) == 0 ) currentNavigator = hotspotNavigator; } private void setCursors() { prevBtn.setCursor( new Cursor( Cursor.HAND_CURSOR ) ); nextBtn.setCursor( new Cursor( Cursor.HAND_CURSOR ) ); } public void prevPage() { currentNavigator.showPrevPage(); } public void nextPage() { currentNavigator.showNextPage(); } public void goHome() { currentNavigator.showHomePage(); } public void home() { goHome(); } }
The pages of this application are using a custom BasePage class which is derived from the Page. The reasons for doing this are irrelevant to this discussion. The setNavPath function which is called from the Welcome Page can be seen. This function assigns one of four NavigationManager instances to the currentNavigator variable. Each of the NavigationManagers accepts the next and previous buttons as contructor arguments along with the path to be followed. This allows the NavigationManagers to control the enabled or visible state of the buttons depending on where the current page of the application.
The navigation paths should be explained now. This is the path into some predefined nodes in the datamodel which can be seen in the wizardpages.xml file below. This page needs to be referenced from the datasource file as referred to by the ModelData startup property.
wizardpages.xml
<Datasets> <dataset id="navigation"> <dataset id="wizardpages"> <data value="welcome"/> <data value="projectinit" desc="WIZ_PROJ_SET"/> <data value="languagepage" desc="WIZ_PROJ_LANG"/> <data value="colourscheme" desc="WIZ_PROJ_LAYOUT"/> <data value="categories" desc="WIZ_PROJ_CATS"/> <data value="categorysetup" desc="WIZ_PROJ_CAT_DET"/> <data value="promptspage" desc="WIZ_PROJ_PROMPTS"/> <data value="constantspage" desc="WIZ_PROJ_CNST"/> <data value="finish" desc="WIZ_PROJ_FINISH"/> </dataset> <dataset id="generationpages"> <data value="welcome"/> <data value="projectgenerate" desc="WIZ_GEN"/> </dataset> <dataset id="builderpages"> <data value="welcome"/> <data value="projectbuilder" desc="WIZ_BUILD"/> </dataset> <dataset id="hotspotpages"> <data value="welcome"/> <data value="hotspotsetup" desc="WIZ_HTSPT_SPEC"/> <data value="hotspotedit" desc="WIZ_HTSPT_EDIT"/> </dataset> </dataset> </Datasets>
Now the NavigationManager can take care of displaying the correct page depending on whether the next or previous button was clicked.
NavigationManager class
public class NavigationManager { BaseModel navMdl; protected int currentNode = 0; BasePage currentPage; Button nextBtn, prevBtn; WizardStatus wizStatus; PageManager pageMgr; public NavigationManager( Button nextBtn, Button prevBtn, String navPath ) { Project project = ProjectManager.getCurrentProject(); pageMgr = project.getPageManager(); navMdl = ( BaseModel )project.getModel().get( navPath ); this.nextBtn = nextBtn; this.prevBtn = prevBtn; wizStatus = ( WizardStatus )pageMgr.showPage( "wizardstatus", "wizardstatus" ); setWizardPanelSize( 0 ); } public void showPrevPage() { nextBtn.setVisible(true); currentNode--; showpage(); if (currentNode == 0) prevBtn.setVisible(false); } public void showNextPage() { if ( currentPage == null ) { BaseModel mdl = (BaseModel) navMdl.get( currentNode ); String pagename = (String) mdl.get(); currentPage = (BasePage) pageMgr.getPage( pagename ); wizStatus.setNavModel( navMdl ); } if ( currentPage.checkValidations() == Validator.LEVEL_IGNORE ) { prevBtn.setVisible(true); currentNode++; showpage(); if (currentNode == ( navMdl.getNumChildren() - 1 ) ) nextBtn.setVisible(false); } } public void showHomePage() { currentPage = null; currentNode = 0; nextBtn.setVisible( true ); prevBtn.setVisible( false ); BaseModel mdl = (BaseModel) navMdl.get( currentNode ); String pagename = (String) mdl.get(); currentPage = (BasePage) pageMgr.showPage( pagename ); setWizardPanelSize( 0 ); } private void showpage() { BaseModel mdl = ( BaseModel ) navMdl.get( currentNode ); String pagename = ( String ) mdl.get(); currentPage = ( BasePage ) pageMgr.showPage( pagename ); setWizardPanelSize( 100 ); wizStatus.setCurrentItem( currentNode ); } protected void setWizardPanelSize( int width ) { Container c = wizStatus.getParent(); c.setVisible( width == 0 ? false : true ); c.getParent().doLayout(); } }
The constructor obtains a reference to the model node specified by the path parameter. This node contains all of the child nodes which define the pages for this wizard route. The constructor also obtains a reference to the wizardstatus page which is declared second in the frames.xml file. and sets its width to zero and also makes it invisible.
The showNextPage function first checks to see if the currentPage is null. If it is null it retrieves the name of the first page node in the navMdl and passes the navMdl node to the WizardStatus page. It then checks the currentpage for validations and if the validations pass the previous button is set to visible and the next page in the route is displayed. If the page displayed is the last page in the route the next button is made invisible.
The showPage function takes care is displaying the current page, making the WizardStatus page visible and passing the current node to the WizardStatus page.
The showHomePage function simply resets the variables and sets the appropriate visibility for the navigation buttons.
The WizardStatus page can now be looked at.
The WizardStatus class
public class WizardStatus extends BasePage { int currentNode = -1; BaseModel navMdl; /** Creates a new instance of WizardStatus */ public WizardStatus() { } public void setNavModel( BaseModel mdl ) { navMdl = mdl; repaint(); } public void setCurrentItem( int item ) { currentNode = item; repaint(); } public void paint( Graphics g ) { super.paint( g ); g.setColor( new Color( 136, 136, 136 ) ); if ( navMdl != null ) { int currentY = 50; for ( int i = 1; i < navMdl.getNumChildren(); i++ ) { if ( i == currentNode ) { g.setColor( new Color( 255, 58, 0 ) ); } else if ( i > currentNode ) { g.setColor( new Color( 136, 136, 0 ) ); } BaseModel itemMdl = ( BaseModel ) navMdl.get( i ); String text = Translator.translate( ModelHelper.getAttrib( itemMdl, "desc" )); g.drawString( text, 10, currentY ); currentY += 20; } } } }
The setNavModel function as referred to from the NavigationManager class can be seen taking the path node as a parameter. The setCurrentItem function stores the index of the current page in the path in a class variable. The paint function then simply iterates all of the page nodes in the navigation model and outputs the translated description of the page at intervals of twenty pixels. When the current node is reached the color of the text is changed to indicate the progress of the wizard.
One final scenario needs to be mentioned before leaving this topic. The WizardNavigator as defined in the NavPanel class is derived from the NavigationManager class and needs to do some specialized navigation. This navigation class overloads some of the NavigationManager functions in order to repeat specific pages in the navigation path. There is little need to go into the specifics of that navigation here but it's worth noting to show how custom NavigationManagers can be created.
Component Registration
There are several custom components included in the wizard application each designed to carry out a specific task. This section will look at a single component, the FileLocater, which can be seen in the screenshot above. There are two of these components on the first screen on the main navigation path to specify the project location and the icon path.
FileLocator constructor, setAttribute and actionPerformed methods
public class FileLocater extends JPanel implements AttributedComponent, ActionListener, TextHolder { StyleFactory fact; Label promptLbl; Edit fileEdit; Button locateBtn; public FileLocater() { setLayout( null ); fact = new StyleFactory( ProjectManager.getCurrentProject(), "org.formaria.swing" ); fact.setParentComponent( this ); promptLbl = ( Label ) fact.addComponent( "LABEL", 0, 0, 30, 20 ); fileEdit = ( Edit ) fact.addComponent( "EDIT", 30, 0, 30, 20 ); locateBtn = ( Button ) fact.addComponent( "BUTTON", 0, 0, 30, 20, "..." ); locateBtn.addActionListener( this ); } public void setAttribute( String name, Object objValue ) { String value = ( String )objValue; if ( name.compareTo( "content" ) == 0 ) setContent( value ); else if ( name.compareTo( "promptWidth" ) == 0 ) setPromptWidth( value ); else if ( name.compareTo( "dataStyle" ) == 0 ) setDataStyle( value ); else if ( name.compareTo( "style" ) == 0 ) fact.applyStyle( promptLbl, value ); } public void actionPerformed( ActionEvent ae ) { JFileChooser dlg = new JFileChooser( fileEdit.getText() ); dlg.setFileSelectionMode( javax.swing.JFileChooser.FILES_AND_DIRECTORIES); int returnVal = dlg.showOpenDialog( this ); if(returnVal == JFileChooser.APPROVE_OPTION) { fileEdit.setText( dlg.getSelectedFile().getAbsolutePath() ); } } public void setContent( String value ) { promptLbl.setText( Translator.translate( value ) ); } public void setPromptWidth( String value ) { promptLbl.setSize( Integer.parseInt( value ), promptLbl.getSize().width ); doLayout(); } public void setDataStyle( String value ) { fact.applyStyle( fileEdit, value ); }
The constructor sets the layout of the panel to null. It then constructs a new StyleFactory which will use the org.formaria.swing widget set to construct it's components. The factory is then used to create the label, text box and the locate button. An action listener is added to the button. The setAttribute function as defined in the AttribedComponent interface is called from the AriaBuilder when an unknown component attribute is found. The attributes being looked for by the component are content, promptWidth, dataStyle and style. Each of these properties are set using the component functions. It can be seen how the StyleFactory can apply a style to a component with the simple applyStyle call.
The actionPerformed function shows a JFileChooser being created with it's default path set to the text of the text component. If a new path is selected by the user the text component is once more set.
The remainder of the component can now be completed as below.
Remainder of the FileLocater class
public void doLayout() { fileEdit.setLocation( promptLbl.getSize().width, 0 ); fileEdit.setSize( getWidth() - promptLbl.getWidth() - 30, 20 ); locateBtn.setLocation( getWidth() - 30, 0 ); setOpaque( false ); super.doLayout(); } public String getText() { return fileEdit.getText(); } public void setText( String text ) { fileEdit.setText( text == null ? "" : text ); } public File getFile() { return new File( getText() ); } public Edit getEditComponent() { return fileEdit; } public void setBackground( Color c ) { super.setBackground( null ); super.setOpaque( false ); }
The doLayout function sets the location of the text and button components relative to the width of the text component and sets the panel to be transparent.
This component can now be declared in a component registration file as shown below.
components.xml
<Components> <Component class="org.formaria.quotation.FileLocater" name="FileLocater"> <Property type="set" name="content" /> <Property type="set" name="promptWidth" /> <Property type="set" name="dataStyle" /> </Component> ... <Components>
The following startup properties need to be set. The RegisteredComponentFactory will assume a filename of components.xml
startup properties
NumComponentFactories=2 ComponentFactory0=org.formaria.swing.SwingComponentFactory ComponentFactory1=org.formaria.registry.RegisteredComponentFactory
The custom component is now ready to be used within the Aria pages. This can now be done very simply by creating an element whose name corresponds to the definition of the component in the components.xml file.
ProjectInit XML page declaration
<Components> <FileLocater name="pathEdit" x="10" y="10" w="430" h="20" content="PROJINIT_PROJ_DIR" promptWidth="160" style="base/prompt" dataStyle="base/data"/> <FileLocater name="iconEdit" x="10" y="30" w="430" h="20" content="PROJINIT_PROJ_ICON" promptWidth="160" style="base/prompt" dataStyle="base/data"/> ...
The location, dimension and name attributes are used as usual and the custom attributes as expected by the setAttribute function can be seen. This example gives an impression of how the StyleManager and styles in general can be used to customize the application in general.
Validations
This application requires the use of custom validations which go beyond the abilities of the basic validations built into the Aria core libraries. This requires the use of a custom validation factory which can be specified in the startup properties file as below.
Specifying the custom validation factory
... ValidationFactory=org.formaria.quotation.validation.ValidationFactory ...
The custom validation factory is shown below.
The ValidationFactory class
public class ValidationFactory extends ValidationFactory { public ValidationFactory( Project project ) { super( project ); } public ValidationFactory( Reader reader ) { super ( reader ); } public Validator getValidation( String validationName, Method m, int mask, Object page, XmlElement pageEle ) { XmlElement ele = ( XmlElement ) validations.get( validationName ); if ( BuildProperties.DEBUG ) { if ( ele == null ) { DebugLogger.logError( "Cannot find the validation rule: " + validationName ); return null; } } String type = ele.getAttribute( "type" ); if ( type.compareTo( "integer" ) == 0 ) { IntegerValidation validator = new IntegerValidation(); validator.setName( validationName ); validator.setMask( mask ); validator.setup( ele ); return validator; } else if ( type.compareTo( "number" ) == 0 ) { NumberValidation validator = new NumberValidation(); validator.setName(validationName); validator.setMask(mask); validator.setup(ele); return validator; } else if ( type.compareTo( "function" ) == 0 ) { FunctionValidation validator = new FunctionValidation(); validator.setName( validationName ); validator.setup( ele ); return validator; } else { BaseValidator validator = ( BaseValidator ) super.getValidation( validationName, m, mask, page pageEle ); validator.setup( ele ); return validator; } } }
This custom ValidationFactory class is aware of three types of custom validations, integer , number and function . If the validation being created is none of these then the super getValidation function is called in order to create the validation and is returned. Two of the custom validations are shown below. The first:
The NumberValidation class
public class NumberValidation extends BaseValidator { public NumberValidation( Project project ) { super( project ); } public void validate( Object c, boolean forceMandatory ) throws java.lang.Exception { if ( forceMandatory ) { String text = getText(c); try { Double.parseDouble( text ); } catch ( Exception ex ) { errorLevel = LEVEL_ERROR; throwException(); } } } }
And the second:
The FunctionValidation class
public class FunctionValidation extends BaseValidator { public FunctionValidation( Project project ) { super( project ); } public void validate( Object c, boolean forceMandatory ) throws java.lang.Exception { Integer ret = ( Integer ) invokeMethod(); if ( ret.intValue() > LEVEL_IGNORE ) { errorLevel = ret.intValue(); throwException(); } } public void setup( XmlElement element ) { String function = element.getAttribute( "method" ); super.setup( element ); } }
Both classes extend the BaseValidator class and overload the validate method. The NumberValidation class simply attempts to parse the component text and if that fails the errorLevel variable of the super class is set and an exception is thrown. The FunctionValidation class overloads the setup function and retrieves the name of the method which needs to be called in order to carry out the validation.
The validations need to be declared in the validations.xml file some of which are shown below.
Validations.xml
<validation name="overwriteFile" type="function" msg="VAL_OVERWRITE_FILE"/> <validation name="rate" type="number" msg="VAL_QTY_NUM" mandatory="true"/> ...
And the validation rules themselves are as follows:
The validations defined in a page
<Validation rule="rate" target="taxEdit"/> <Validation rule="overwriteFile" target="finishBtn" method="fileExists"/>
The rate validation checks that the text entered into the taxEdit text component is numeric. The overwriteFile function calls the fileExists function in the Page as shown below.
fileExists function
public Integer fileExists() { File f = new File( getPath() + File.separatorChar + "xlintquote.xls" ); return new Integer( f.exists() ? BaseValidator.LEVEL_WARNING : BaseValidator.LEVEL_IGNORE ); }
The fileExists function checks for the existence of a named file and if exists the BaseValidator LEVEL_WARNING is returned in an Integer object. If it does not exist the LEVEL_IGNORE value is returned.
One of the functions of the BasePage class is to set a custom ExceptionHandler for each of the pages as shown below.
BasePage constructor
public class BasePage extends Page implements ModelListener { public BasePage() { setExceptionHandler( new ExceptionHandler( this ) ); }
The custom exception handler follows. The ExceptionHandler class
public class ExceptionHandler implements ExceptionHandler { boolean pageValidation = false; String validationText = ""; private Page currentPage; Vector errors, warnings; public ExceptionHandler( Page page ) { currentPage = page; } public boolean handleException( Object comp, Exception ex, Object xvalidator ) { Validator validator = ( Validator ) xvalidator; if ( ( validator.getLevel() == validator.LEVEL_ERROR ) && ( ! pageValidation ) ){ currentPage.showMessage( "Input error", ex.getMessage() ); return true; } String msg = validator.getMessage(); if ( validationText.length()>0 ) validationText += "\n"; validationText += msg; if ( validator.getLevel() == validator.LEVEL_ERROR ) errors.add( msg ); else warnings.add( msg ); return true; } public int accumulateMessages( boolean start, int level ) { pageValidation = start; if ( pageValidation ){ errors = new Vector(); warnings = new Vector(); validationText = ""; } else { if ( validationText.length()>0 ) { if ( level == Validator.LEVEL_WARNING ) { ErrorMessage dlg = new ErrorMessage( errors, warnings ); dlg.setSaveOnClose( false ); int r = dlg.showDialog( currentPage ); return r == 0 ? 0 : level; } else { ErrorMessage dlg = new ErrorMessage( errors, warnings ); dlg.setSaveOnClose( false ); dlg.showDialog( currentPage ); return BaseValidator.LEVEL_ERROR; } } } return level; } }
The accumulateMessages function is called when the validations for a page begins and is passed true in the start parameter. This value is recorded in the pageValidation variable. The handleException function is called whenever a validation triggers a problem and depending on value returned from the getValue function of the Validator object the validation message is added to the appropriate Vector. When all of the validations are finished the accumulateMessages function is called once more and this time the start parameter is false . In this case an Dialog ErrorMessage is created in two possible ways. If there are errors the messages will be displayed with only a close button. If there are only warnings generated then the dialog will give the option of cancelling or continuing, disregarding the warnings. This functionality can be seen in the value returned from the showDialog function. This will be explored in more detail in the section on dialogs.
The custom FileLocater component needs to have several validations applied to it as shown below.
The FileLocator validations
... <Validation rule="projectpath" target="pathEdit"/> <Validation rule="projectexists" target="pathEdit" method="projectExists"/> <Validation rule="projectfolder" target="pathEdit" method="projectFolder"/> <Validation rule="folderexists" target="pathEdit" method="folderExists"/> ...
These validations are declared in the validations file as below.
The validations.xml declarations
... <validation name="projectpath" type="mandatory" msg="VAL_PATH_CANNOT_EMPTY"/> <validation name="projectexists" type="function" msg="VAL_PATH_NOT_EMPTY"/> <validation name="projectfolder" type="function" msg="VAL_NOT_DIR"/> <validation name="folderexists" type="function" msg="VAL_PATH_NOT_EXIST"/> ...</P> </TD> </TR> </CAPTION> </TABLE> <P> The <EM> projectPath</EM> is a simple built in mandatory validation and the functions for the <EM> FunctionValidations</EM> follows.</P> <p><i> The FunctionValidations functions</i></p><java> public Integer projectExists() { File f = pathEdit.getFile(); boolean ret = ( f.list() == null ) || ( f.list().length == 0 ); return new Integer( ret ? BaseValidator.LEVEL_IGNORE : BaseValidator.LEVEL_WARNING ); } public Integer folderExists() { return new Integer( pathEdit.getFile().exists() ? BaseValidator.LEVEL_IGNORE : BaseValidator.LEVEL_ERROR ); } public Integer projectFolder() { return new Integer( pathEdit.getFile().isDirectory() ? BaseValidator.LEVEL_IGNORE : BaseValidator.LEVEL_ERROR ); }
Data binding
The data bindings for the wizard application are on the whole pretty straightforward and there are some which deserve particular attention.
The projectinit page
<Components> <Panel constraint="Center" painter="org.formaria.swing.LogoBackground" style="synthPanelLight"> <Label x="20" y="10" w="500" h="30" style="base/title" content="PROJINIT_TITLE" opaque="false"/> <Panel x="100" y="100" w="450" h="250" border="1" painter="org.formaria.swing.LogoBackground" style="synthPanelLight"> <FileLocater name="pathEdit" x="10" y="10" w="430" h="20" content="PROJINIT_PROJ_DIR" promptWidth="160" style="base/prompt" dataStyle="base/data"/> <FileLocater name="iconEdit" x="10" y="30" w="430" h="20" content="PROJINIT_PROJ_ICON" promptWidth="160" style="base/prompt" dataStyle="base/data"/> <Label x="10" y="50" w="160" h="20" style="base/prompt" content="PROJINIT_PROJ_TITLE"/> <Edit name="titleEdit" x="170" y="50" w="240" h="20" style="base/data"/> <Label x="10" y="70" w="160" h="20" style="base/prompt" content="PROJINIT_PROJ_WEL_TITLE"/> <Edit name="welcomeTitleEdit" x="170" y="70" w="240" h="20" style="base/data"/> <Label x="10" y="90" w="160" h="20" style="base/prompt" content="PROJINIT_PROJ_WEL_MES"/> <ScrollPane x="170" y="90" w="240" h="100"> <TextArea name="welcomeText" x="0" y="0" w="250" h="100" style="base/data"/> </ScrollPane> <Label x="10" y="200" w="160" h="20" style="base/prompt" content="PROJINIT_PROJ_WEL_INFO"/> <Edit name="infoEdit" x="170" y="200" w="240" h="20" style="base/data"/> </Panel> </Panel> </Components> <Data> <Bind target="pathEdit" source="/project/projectPath" class="org.formaria.quotation.data.FileLoaderBinding"/> <Bind target="iconEdit" source="/project/projectIcon" class="org.formaria.quotation.data.FileLoaderBinding"/> <Bind target="titleEdit" source="/project/projectTitle"/> <Bind target="welcomeTitleEdit" source="/project/welcomeTitle"/> <Bind target="welcomeText" source="/project/welcomeMessage"/> <Bind target="infoEdit" source="/project/companyInfo"/> </Data>
The last four bindings are straightforward text bindings and are bound to text components. The first two bindings are bould to the custom FileLocator components created earlier. The type attribute specifies the use of a custom binding and the class attribute provides the name of the class which will carry out the binding. The FileLoaderBinding is shown below.
FileLoaderBinding class
public class FileLoaderBinding extends TextBinding { public FileLoaderBinding() { } public void setup( Project project, Object c, Hashtable bindingConfig, Hashtable instanceConfig ) { comp = ( ( FileLocater ) c ).getEditComponent(); srcPath = ( String )instanceConfig.get( "source" ); BaseModel srcModel = ( BaseModel ) project.getModel().get( srcPath ); outputPath = DataModel.prefixOutputPath( srcPath ); if (( srcModel == null ) && ( srcPath != null )) sourceModel = ( DataModel )project.getModel().get( srcPath ); else sourceModel = srcModel; comp = c; attribStr = ( String )instanceConfig.( "attrib" ); } }
The custom binding extends the TextBinding class and implements the setup function. The passed component is cast to a FileLocater component and the text component is retrieved from it.
The wizard allows the user to create applications with whatever languages they require. This is done through the languages page on the main wizard path.
![]() |
The master list component contains a list of all of the languages which are available from the wizard. The selected languages list contains a list of languages which have been selected to date. The master list is populated from a predefined set of languages defined in a datasets file as shown below.
languages.xml
<Datasets> <dataset id="languagelist"> <lang id="Chinese" res="LST_LANG_ZH" code="zh" requiresutf="yes" locallang="æ±?è¯"/> <lang id="English" res="LST_LANG_EN" code="en" include="true" locallang="English"/> <lang id="French" res="LST_LANG_FR" code="fr" locallang="Français"/> <lang id="German" res="LST_LANG_DE" code="de" locallang="Deutsch"/> </dataset> <dataset id="wizardlanguages"> <lang id="English" value="English" code="en"/> <lang id="æ±?è¯" value="æ±?è¯" code="zh" encoding="UTF-8"/> </dataset> </Datasets>
This datasets definition show how the model can be used to store much more than just ids and values for a node. In the languageList node each language node has information about the localized language name the language code and whether the language relies on Unicode.
In the LanguagePage class the master list is populated in the code below.
Some of the LanguagePage class
List masterLst, languageLst; LanguageModel masterListModel, listModel; Checkbox defaultChk; public void pageCreated() { langListMdl = ( BaseModel ) rootModel.get( "languagelist" ); languageLst = ( List ) findComponent( "languageLst" ); masterLst = ( List ) findComponent( "masterLst" ); defaultChk = ( Checkbox ) findComponent( "defaultChk" ); listModel = new LanguageModel( "selectedLanguages" ); languageLst.setModel( listModel ); masterListModel = new LanguageModel(); masterLst.setModel( masterListModel ); populateMasterLangs(); populateDefaultLangs(); updateLists(); } private void populateMasterLangs() { for ( int i = 0; i < langListMdl.getNumChildren(); i++ ) { BaseModel langMdl = ( BaseModel ) langListMdl.get( i ); String include = ModelHelper.getAttrib( langMdl, "include" ); if ( ( include == null ) || ( include.compareTo( "true" ) != 0 ) ) { masterListModel.addElement( langMdl ); } } } private void populateDefaultLangs() { for ( int i = 0; i < langListMdl.getNumChildren(); i++ ) { BaseModel langMdl = ( BaseModel ) langListMdl.get( i ); String include = ModelHelper.getAttrib( langMdl, "include" ); if ( ( include != null ) && ( include.compareTo( "true" ) == 0 ) ) { listModel.addElement( langMdl ); } } } private void updateLists() { masterLst.updateUI(); languageLst.updateUI(); }
The pageCreated method shows references being obtained to the page components which will be used in the class. New LanguageModel classes are created which are used as models for each of the lists. Taking the masterListModel , the populateMasterLangs function iterates all of the languages in the languagelist dataset and adds those elements whose include attribute is set to false . The converse is done for the listModel within the populateDefaultLangs function. This means that English will appear by default in the selected languages list when the page first appears. The updateLists function needs to be called whenever the list data changes so as to render the changes correctly. The LanguageModel class follows.
The LanguageModel class
public class LanguageModel implements ListModel { BaseModel langMdl; Project project; public LanguageModel() { langMdl = new BaseModel(); project = ProjectManager.getCurrentProject(); } public LanguageModel( String path ) { langMdl = ( BaseModel ) project.getModel().get( path ); } public void setDefault( String name ) { ModelHelper.setAttrib( langMdl, "default", name ); } public String getDefault() { return ModelHelper.getAttrib( langMdl, "default" ); } public void addElement( Object ele ) { langMdl.append( ( BaseModel ) ele ); } public void removeElement( Object ele ) { langMdl.remove((BaseModel)langMdl.get(((Integer)ele).intValue())); } public BaseModel getModelAt( int i ) { return ( BaseModel )langMdl.get( i ); } public int getSize() { return langMdl.getNumChildren(); } public Object getElementAt( int i ) { BaseModel mdl = ( BaseModel )langMdl.get( i ); String res = ModelHelper.getAttrib( mdl, "res" ); return Translator.translate( res ); } public void addListDataListener( ListDataListener listener ){} public void removeListDataListener( ListDataListener listener ){} }
The LanguageModel class implements the ListModel interface and as such needs to implement the required methods such as addElement and removeElement . This class is now effectively acting as a wrapper for the language models and makes it easy to move the model nodes around. This class also takes care of the default language and uses the Translator object to translate the language names to the current application language.
Localization
The application has been completely translated into Simplified Chinese and required some specific techniques to take account of the Unicode characters needed to render the language correctly. The results of that localization can be seen below.
![]() |
The default language for the wizard is English and that is specified in the startup properties as follows.
The language startup property
... Language=en ...
This means that the text for the language will be stored in a file called en.properies some of which is shown below. For each page in the application which is to be localized automatically by Aria the resource attribute needs to be set in the page XML.
The Page resource attribute
<Page resource=""> ...
Section of the en.properties file
... WIZ_PROJ_PROMPTS=Prompts PROMPT_DESC=Description\: DATA_LST_NAV_CFG=Configurator navigation DATA_LST_SCR_DRILL=Simple drilldown WEL_HOTSPOT_GEN=Create or edit a hotspot file COL_APP_TYPE=Application type\: PROJ_BUILD_LOC=Locate the spreadsheet you wish to generate WEL_CLICK_CONT=Click the next button to continue ...
This is a typical resource file for a Java application.
The language list on the welcome page is populated from the wizardlanguages dataset declared in the languages.xml file which was shown in the previous section.
Some of the welcome.xml page
... <Panel laf="true" name="langPanel" x="0" y="0" w="200" h="30" border="0" style="header/light" opaque="false"> <Label x="5" y="5" w="80" h="20" style="welcome/prompt" content="WEL_LANG" opaque="false"/> <Combo name="langlist" x="85" y="5" w="100" h="20" style="welcome/data/utf"/> </Panel> ... <Events> <Event target="langlist" type="FocusHandler" method="changeClicked"/> <Event target="langlist" type="ActionHandler" method="changeLanguage"/> ... <Data> <Bind target="langlist" source="wizardlanguages" output="wizardlanguages"/> </Data> ...
The langlist combobox is bound to the wizardlanguages node of the data model and will appear with a list of its children from which the user can select. There are two events attached to the language list changeClicked and changeLanguage and these are shown below.
Some of the WelcomePage class
... ComboBox langlist; boolean changeClicked = false; public void changeClicked() { changeClicked = true; } public void changeLanguage() { if ( changeClicked ) { String selected = (String) langlist.getSelectedObject(); BaseModel mdl = (BaseModel)rootModel.get( "wizardlanguages/" + selected ); String langCode = ModelHelper.getAttrib( mdl, "code" ); String langEncoding = ModelHelper.getAttrib(mdl, "encoding"); project.setStartupParam("Language", langCode); Translator.reset( langEncoding == null ? false : true ); pageMgr.reset(); pageMgr.showPage( "welcome", "content" ); pageMgr.showPage( "navPanel", "navPanel" ); pageMgr.showPage( "banner", "banner" ); } } ...
The changeClicked boolean variable is set to true only when the user clicks on the languages combobox. This ensures that the code in the changeLanguage function is not executed as the list in populated by the binding. If the variable is true and the user selects a new language then the changeLanguage code is invoked. The selected language is obtained from the list and the corresponding language node is retrieved from the model. The code and encoding attributes are then retrieved from the language node. The Language startup parameter is then set for the application and the Translator is reset to use language encoding or not. Now the page manager is reset and the startup pages are reloaded into their respective target frames.
At this point its worth looking at the Translator object and how it manages the necessary fonts.
The Translator Singleton class
public class Translator { protected static ResourceBundle languageResourceBundle; private static Translator translator; private static boolean UTF_Encoding = false; private static String defaultFont = "NSimSun"; private Translator() { Project proj = ProjectManager.getCurrentProject(); languageResourceBundle = proj.getResourceBundle( proj.getStartupParam( "Language" )); } public static Translator getInstance() { if ( translator == null ) translator = new Translator(); return translator; } public static String translate( String key ) { if ( languageResourceBundle == null ) getInstance(); if (( key != null ) && ( languageResourceBundle != null )) { try { String trans = languageResourceBundle.getString( key ); if ( trans != null ) return trans; } catch ( Exception ex ) { if ( BuildProperties.DEBUG ) DebugLogger.logWarning( "No translation found for the key: " + key ); } } return key; } public static void reset() { reset( false ); } public static void setUseUTF( boolean use ) { UTF_Encoding = use; } public static void reset( boolean useUTF ) { translator = null; getInstance(); translator.setUseUTF( useUTF ); UTF_Encoding = useUTF; } public static boolean useUTF() { return UTF_Encoding; } public static String getDefaultFont() { return defaultFont; } }
The Translator is a singleton class. The call to the reset function with the useUTF parameter informs it whether or not the application is running with a language which requires Unicode encoding. If so the NSimSun font is returned from the getDefaultFont function. This font could just as easily be parameterized. Now each of the StyleFactory in each of the Pages needs to make use of the Translator class in order to construct its components with the correct font so a new StyleManager class is introduced which extends the basic StyleManager class as shown below.
The StyleManager class
public class StyleManager extends org.formaria.style.StyleManager { public Font getFont( String style ) { Font f = super.getFont( style ); if ( Translator.useUTF() ) { f = new Font( Translator.getDefaultFont(), f.getStyle(), f.getSize() ); } return f; } public Font getFont( Style style ) { Font f = super.getFont( style ); if ( Translator.useUTF() ) { f = new Font( Translator.getDefaultFont(), f.getStyle(), f.getSize() ); } return f; } }
The custom StyleManager class overloads the two getFont functions of the base class and checks the Translator object to see if Unicode is being used. If it is then the Translator's default font is used to construct a new font with the same style and size as specified in the styles file. It's now simply a matter of telling the Aria to use this style manager which is done through the startup properties as follows.
The DefaultStyleManager startup property
... DefaultStylemanager=org.formaria.quotation.ui.StyleManager ...
The Synth Look & Feel
The look and feel of the application which can be see from the screenshots in the preceding sections is achieved through use of synth. This look and feel is only available since JDK 1.5
In order to specify the use of the synth look and feel some startup parameters needs to be set as follows.
Synth look and feel startup properties
... LAF=org.formaria.laf.SynthLafInstaller SynthConfigFile=aria.xsynth SynthResourceLoader=org.formaria.controls.swing.Welcome ...
In order to use the look and feel for panels a painter attribute needs to be specified as follows. Specifying the synth logo painter for panels
<Panel x="100" y="80" w="390" h="320" border="1" painter="org.formaria.swing.LogoBackground" style="synthPanelLight"> <Panel x="5" y="5" w="160" h="150" border="1" painter="org.formaria.swing.LogoBackground" style="synthPanelLight"> <Label x="5" y="5" w="80" h="20" style="base/prompt" content="LANG_MASTER"/> <List name="masterLst" x="5" y="25" w="150" h="90" style="base/data"/> <Button name="addBtn" x="5" y="120" w="150" h="25" content="PROMPT_ADD" style="base/data"/> </Panel> <Panel x="170" y="5" w="210" h="150" border="1" painter="org.formaria.swing.LogoBackground" style="synthPanelLight"> <Label x="5" y="5" w="125" h="20" style="base/prompt" content="LANG_OTHER"/> <Label x="5" y="35" w="110" h="20" style="base/prompt" content="LANG_CODE"/> <Edit name="languageCode" x="120" y="35" w="80" h="20" style="base/data"/> ...
The aria.synth file needs to be available on the classpath and in this case was placed in the resources folder. The aria.synth file specifies component borders and offsets and also refers to images which it uses to render the components. For example it refers to the following two images to render the buttons. The orange version is for mouse over events giving a very interactive application.
![]() |
- Printer-friendly version
- Login or register to post comments




