Calculator Application
One powerful feature of the Scaffold framework is extensibility through layered properties files. Every Scaffold application can be declared in a single properties file, but in practice it is often prudent to organize the application into multiple files that are merged (at runtime) to create extended works.
The inspiration for the Calculator comes from the F3 blog (subsequently rebranded as JavaFX, apparently), which was inspired by an app for the Mac OSX implemented in HTML/CSS/Javascript.
The web is everywhere, and RIA is vying for ubiquity. A goal of Scaffold is to make Java a more viable RIA client platform, and making simple, attractive apps like the Calculator simple to implement is one way to achieve that.
Step 1 - Run the Application
Because we can!
java -cp Scaffold.jar \ com.meldcraft.application.AppManager \ com.meldcraft.calculator.CalculatorApp
Hard to believe this scrunched up little JFrame will morph into an elegant Calculator...
Step 2 - Swing GUI
Here is the entire GUI using plain old Swing components declared in a single properties file - no Java required.
- Application Files
-
com/meldcraft/calculator/CalculatorApp.properties
- com/meldcraft/calculator/CalculatorApp.properties
-
Application.name = Calculator Application.UIRoot.elementProperties = undecorated=true Application.rootContent = CalcPanel CalcPanel.baseResource = com.meldcraft.application.guis.SSFactory CalcPanel.type = panel CalcPanel.elementRows = NumberField ,\ MAdd | MSub | MRec | MClear | Div ,\ NumberPanel <<< | Mul ,\ ^ | Sub ,\ ^ | Add ,\ ^ | [fill:-1 12] ,\ ^ | Equal NumberField.baseResource = com.meldcraft.application.guis.SSFactory NumberField.type = textField NumberField.elementProperties = editable=false, background=white,\ horizontalAlignment=RIGHT,\ text=0 NumberPanel.baseResource = com.meldcraft.application.guis.SSFactory NumberPanel.type = panel NumberPanel.elementGrid = NP7 | NP8 | NP9 ,\ NP4 | NP5 | NP6 ,\ NP1 | NP2 | NP3 ,\ NP0 | NPDot | NPClear KeyRep.{1}.name = {2} KeyRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\ preferredSize=28,28 KeyRep.replicateElement = MAdd;m+,MSub;m-,MRec;mr,MClear;mc OpRep.{1}.name = {2} OpRep.{1}.accelerator = {3} OpRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\ preferredSize=28,28 OpRep.replicateElement = Div;\u00f7;DIVIDE | SLASH,\ Mul;\u00d7;MULTIPLY | ASTERISK | shift 8,\ Add;+;PLUS | ADD | shift EQUALS,\ Sub;-;MINUS | SUBTRACT,\ Equal;=;EQUALS | ENTER,\ NPDot;.;DECIMAL | PERIOD,\ NPClear;c;C Equal.elementProperties = focusable=false, margin=0,0,0,0, preferredSize=28,56 RadioRep.{1}.type = radioOp RadioRep.{1}.buttonStyle = toggle RadioRep.replicateElement = Div,Mul,Add,Sub NPrep.NP{1}.name = {1} NPrep.NP{1}.accelerator = {1} | NUMPAD{1} NPrep.NP{1}.elementProperties = focusable=false, margin=0,0,0,0,\ preferredSize=38,38 NPrep.replicateElement = 0,1,2,3,4,5,6,7,8,9 Application.contextListeners = NPClear.action=ClearOpInvoker,\ Equal.action=ClearOpInvoker ClearOpInvoker.forward = Op.selectValue=<null>
Compile the application (just copying files at this point), and run it:
java -cp Scaffold.jar;ScaffoldGUIS.jar;Calculator/classes \ com.meldcraft.application.AppManager \ com.meldcraft.calculator.CalculatorApp
Mostly functional - the buttons work, the operators hold toggle state. The buttons are aligned neatly, and the undesired frame decorations are configured away.
Step 3 - Business Logic
Next, we add some Java code to implement the calculator logic. Just a POJO based on some code found on the internet. The key method is calculate(), which takes a String argument (the key press), and returns the text that should be displayed.
A few more lines in the properties file hook up the GUI to the calculate() method.
- Application Files
-
com/meldcraft/calculator/CalculatorApp.properties com/meldcraft/calculator/Calculator.java
- com/meldcraft/calculator/Calculator.java
-
package com.meldcraft.calculator; public class Calculator { /** * Issues command and returns the display text. The command * is any key on the calculator. * * @param command * @return */ public synchronized String calculate(String command) { String ret = null; boolean handled = false; String pre = "calc."; if (command.startsWith(pre) && (command.length() > pre.length())) { if (doCalculate(command.substring(pre.length()))) { handled = true; ret = text; } } if (!handled) { throw new RuntimeException("Unknown command: " + command); } return ret; } /* * Calculator implementation based on http://www.yamaza.com/java/Calc.java, * http://www.geocities.com/entity05/java/myjava.html */ private double dReg1; private double dReg2; private String sOperator; private String text = "0"; private boolean isFixReg = true; private String sText2; private String sText1; private double dMem; private boolean doCalculate(String arg) { boolean ret = true; // // numeric key input // if ("c".equals(arg)) { dReg1 = 0.0d; dReg2 = 0.0d; sOperator = ""; text = "0"; isFixReg = true; } else if (("0".equals(arg)) | ("1".equals(arg)) | ("2".equals(arg)) | ("3".equals(arg)) | ("4".equals(arg)) | ("5".equals(arg)) | ("6".equals(arg)) | ("7".equals(arg)) | ("8".equals(arg)) | ("9".equals(arg)) | (".".equals(arg))) { if (isFixReg) sText2 = (String) arg; else { if (!".".equals(arg) || sText2.indexOf(".") == -1) sText2 = text + arg; } text = sText2; isFixReg = false; } // // operations // else if (("+".equals(arg)) | ("-".equals(arg)) | ("\u00d7".equals(arg)) | ("\u00f7".equals(arg)) | ("=".equals(arg))) { sText1 = text; dReg2 = (Double.valueOf(sText1)).doubleValue(); dReg1 = calcValue(sOperator, dReg1, dReg2); Double dTemp = new Double(dReg1); sText2 = dTemp.toString(); text = sText2; sOperator = (String) arg; isFixReg = true; } // // memory clear operation // else if ("mc".equals(arg)) { dMem = 0; } // // memory read operation // else if ("mr".equals(arg)) { Double dTemp = new Double(dMem); sText2 = dTemp.toString(); text = sText2; sOperator = ""; isFixReg = true; } // // memory add operation // else if ("m+".equals(arg)) { sText1 = text; dReg2 = (Double.valueOf(sText1)).doubleValue(); dReg1 = calcValue(sOperator, dReg1, dReg2); Double dTemp = new Double(dReg1); sText2 = dTemp.toString(); text = sText2; dMem = dMem + dReg1; sOperator = ""; isFixReg = true; } // // memory sub operation // else if ("m-".equals(arg)) { sText1 = text; dReg2 = (Double.valueOf(sText1)).doubleValue(); dReg1 = calcValue(sOperator, dReg1, dReg2); Double dTemp = new Double(dReg1); sText2 = dTemp.toString(); text = sText2; dMem = dMem - dReg1; sOperator = ""; isFixReg = true; } else { ret = false; } return ret; } //------------- // Calculation //------------- private double calcValue(String sOperator, double dReg1, double dReg2) { if ("+".equals(sOperator)) dReg1 = dReg1 + dReg2; else if ("-".equals(sOperator)) dReg1 = dReg1 - dReg2; else if ("\u00d7".equals(sOperator)) dReg1 = dReg1 * dReg2; else if ("\u00f7".equals(sOperator)) dReg1 = dReg1 / dReg2; else dReg1 = dReg2; return dReg1; } }
- com/meldcraft/calculator/CalculatorApp.properties
-
Application.name = Calculator target = CalculatorPOJO CalculatorPOJO.className = com.meldcraft.calculator.Calculator Application.UIRoot.elementProperties = undecorated=true Application.rootContent = CalcPanel Application.mainactions = Exit Exit.name = Exit Exit.target = Application Exit.method = applicationClose Exit.accelerator = shift X | X CalcPanel.baseResource = com.meldcraft.application.guis.SSFactory CalcPanel.type = panel CalcPanel.elementRows = NumberField ,\ MAdd | MSub | MRec | MClear | Div ,\ NumberPanel <<< | Mul ,\ ^ | Sub ,\ ^ | Add ,\ ^ | [fill:-1 12] ,\ ^ | Equal NumberField.baseResource = com.meldcraft.application.guis.SSFactory NumberField.type = textField NumberField.elementProperties = editable=false, background=white,\ horizontalAlignment=RIGHT,\ text=0 NumberPanel.baseResource = com.meldcraft.application.guis.SSFactory NumberPanel.type = panel NumberPanel.elementGrid = NP7 | NP8 | NP9 ,\ NP4 | NP5 | NP6 ,\ NP1 | NP2 | NP3 ,\ NP0 | NPDot | NPClear KeyRep.{1}.name = {2} KeyRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\ preferredSize=28,28 KeyRep.{1}.method = calculate KeyRep.{1}.method.arg = calc.{2} KeyRep.{1}.invokeReturnContextKey = calcText KeyRep.replicateElement = MAdd;m+,MSub;m-,MRec;mr,MClear;mc OpRep.{1}.name = {2} OpRep.{1}.accelerator = {3} OpRep.{1}.elementProperties = focusable=false, margin=0,0,0,0,\ preferredSize=28,28 OpRep.{1}.method = calculate OpRep.{1}.method.arg = calc.{2} OpRep.{1}.invokeReturnContextKey = calcText OpRep.replicateElement = Div;\u00f7;DIVIDE | SLASH,\ Mul;\u00d7;MULTIPLY | ASTERISK | shift 8,\ Add;+;PLUS | ADD | shift EQUALS,\ Sub;-;MINUS | SUBTRACT,\ Equal;=;EQUALS | ENTER,\ NPDot;.;DECIMAL | PERIOD,\ NPClear;c;C Equal.elementProperties = focusable=false, margin=0,0,0,0, preferredSize=28,56 RadioRep.{1}.type = radioOp RadioRep.{1}.buttonStyle = toggle RadioRep.replicateElement = Div,Mul,Add,Sub NPrep.NP{1}.name = {1} NPrep.NP{1}.accelerator = {1} | NUMPAD{1} NPrep.NP{1}.elementProperties = focusable=false, margin=0,0,0,0,\ preferredSize=38,38 NPrep.NP{1}.method = calculate NPrep.NP{1}.method.arg = calc.{1} NPrep.NP{1}.invokeReturnContextKey = calcText NPrep.replicateElement = 0,1,2,3,4,5,6,7,8,9 Application.contextListeners = calcText.invokeReturn=CalcTextInvoker,\ NPClear.action=ClearOpInvoker,\ Equal.action=ClearOpInvoker ClearOpInvoker.forward = Op.selectValue=<null> CalcTextInvoker.target = NumberField CalcTextInvoker.method = setText
Compile and run the program.
OK! A fully functioning calculator with the basic essence of the target application. Note that the calculator responds to key presses (per the Action accelerators); pressing "X" closes the calculator (and exits the VM).
Still a bit far from beautiful...
Step 4 - More Style
The F3 Calculator uses images for the buttons, and a nice LCD font. This time, instead of just augmenting CalculatorApp.properties, we'll leave that file in tact (so we can run it again to remind us how it all started, perhaps), and add the GUI tweaks to a new properties file which extends CalculatorApp.properties.
And, of course we need to add all of those icon images. Note the special naming of the icons precludes them from having to be specified in the properties file. E.g. ai0.png is used for the 0 button, and ai0p.png is used for the 0 button in the pressed state (the "ai" prefix stands for "Action Icon").
- Application Files
-
com/meldcraft/calculator/CalculatorApp.properties com/meldcraft/calculator/Calculator.java com/meldcraft/calculator/fancy/CalculatorFancy.properties com/meldcraft/calculator/fancy/LCD-N___.TTF com/meldcraft/calculator/fancy/ai0.png com/meldcraft/calculator/fancy/ai0p.png com/meldcraft/calculator/fancy/ai1.png com/meldcraft/calculator/fancy/ai1p.png com/meldcraft/calculator/fancy/ai2.png com/meldcraft/calculator/fancy/ai2p.png com/meldcraft/calculator/fancy/ai3.png com/meldcraft/calculator/fancy/ai3p.png com/meldcraft/calculator/fancy/ai4.png com/meldcraft/calculator/fancy/ai4p.png com/meldcraft/calculator/fancy/ai5.png com/meldcraft/calculator/fancy/ai5p.png com/meldcraft/calculator/fancy/ai6.png com/meldcraft/calculator/fancy/ai6p.png com/meldcraft/calculator/fancy/ai7.png com/meldcraft/calculator/fancy/ai7p.png com/meldcraft/calculator/fancy/ai8.png com/meldcraft/calculator/fancy/ai8p.png com/meldcraft/calculator/fancy/ai9.png com/meldcraft/calculator/fancy/ai9p.png com/meldcraft/calculator/fancy/aiAdd.png com/meldcraft/calculator/fancy/aiAdds.png com/meldcraft/calculator/fancy/aiDiv.png com/meldcraft/calculator/fancy/aiDivs.png com/meldcraft/calculator/fancy/aiEqual.png com/meldcraft/calculator/fancy/aiEqualp.png com/meldcraft/calculator/fancy/aiMul.png com/meldcraft/calculator/fancy/aiMuls.png com/meldcraft/calculator/fancy/aiNPDot.png com/meldcraft/calculator/fancy/aiNPDotp.png com/meldcraft/calculator/fancy/aiSub.png com/meldcraft/calculator/fancy/aiSubs.png com/meldcraft/calculator/fancy/aic.png com/meldcraft/calculator/fancy/aicp.png com/meldcraft/calculator/fancy/aim+.png com/meldcraft/calculator/fancy/aim+p.png com/meldcraft/calculator/fancy/aim-.png com/meldcraft/calculator/fancy/aim-p.png com/meldcraft/calculator/fancy/aimc.png com/meldcraft/calculator/fancy/aimcp.png com/meldcraft/calculator/fancy/aimr.png com/meldcraft/calculator/fancy/aimrp.png
- com/meldcraft/calculator/fancy/CalculatorFancy.properties
-
baseResourceLayers = com.meldcraft.calculator.CalculatorApp NumberField.elementProperties+= ,font=<element:LCDFont>, preferredSize=38,38,\ foreground=0,0,0,0.4 LCDFont.className = com.meldcraft.application.guis.SSSupport LCDFont.type = font LCDFont.resource = /com/meldcraft/calculator/fancy/LCD-N___.TTF LCDFont.size = 20 buttonattrs = borderPainted=false, background=<null>, opaque=false,\ hideActionText=true KeyRep.{1}.elementProperties+= ,<propertyRef:buttonattrs> OpRep.{1}.elementProperties+= ,<propertyRef:buttonattrs> Equal.elementProperties+= ,<propertyRef:buttonattrs> NPrep.NP{1}.elementProperties+=,<propertyRef:buttonattrs>
Compile and run the program.
java -cp Scaffold.jar;ScaffoldGUIS.jar;Calculator/classes \ com.meldcraft.application.AppManager \ com.meldcraft.calculator.fancy.CalculatorFancy
Better! Much closer to the look of the F3 Calculator.
One downside to this GUI is that even though the buttons look round, they function as squares. E.g. pressing the mouse in the blank space activates the nearest button. We'll fix this, and the background image in the next step.
Step 5 - Polish and Shine
Lets extend the fancy calculator with another new properties file to finish the job (and add a background image, too).
- Application Files
-
com/meldcraft/calculator/CalculatorApp.properties com/meldcraft/calculator/Calculator.java com/meldcraft/calculator/fancy/CalculatorFancy.properties com/meldcraft/calculator/fancy/LCD-N___.TTF com/meldcraft/calculator/fancy/ai0.png com/meldcraft/calculator/fancy/ai0p.png com/meldcraft/calculator/fancy/ai1.png com/meldcraft/calculator/fancy/ai1p.png com/meldcraft/calculator/fancy/ai2.png com/meldcraft/calculator/fancy/ai2p.png com/meldcraft/calculator/fancy/ai3.png com/meldcraft/calculator/fancy/ai3p.png com/meldcraft/calculator/fancy/ai4.png com/meldcraft/calculator/fancy/ai4p.png com/meldcraft/calculator/fancy/ai5.png com/meldcraft/calculator/fancy/ai5p.png com/meldcraft/calculator/fancy/ai6.png com/meldcraft/calculator/fancy/ai6p.png com/meldcraft/calculator/fancy/ai7.png com/meldcraft/calculator/fancy/ai7p.png com/meldcraft/calculator/fancy/ai8.png com/meldcraft/calculator/fancy/ai8p.png com/meldcraft/calculator/fancy/ai9.png com/meldcraft/calculator/fancy/ai9p.png com/meldcraft/calculator/fancy/aiAdd.png com/meldcraft/calculator/fancy/aiAdds.png com/meldcraft/calculator/fancy/aiDiv.png com/meldcraft/calculator/fancy/aiDivs.png com/meldcraft/calculator/fancy/aiEqual.png com/meldcraft/calculator/fancy/aiEqualp.png com/meldcraft/calculator/fancy/aiMul.png com/meldcraft/calculator/fancy/aiMuls.png com/meldcraft/calculator/fancy/aiNPDot.png com/meldcraft/calculator/fancy/aiNPDotp.png com/meldcraft/calculator/fancy/aiSub.png com/meldcraft/calculator/fancy/aiSubs.png com/meldcraft/calculator/fancy/aic.png com/meldcraft/calculator/fancy/aicp.png com/meldcraft/calculator/fancy/aim+.png com/meldcraft/calculator/fancy/aim+p.png com/meldcraft/calculator/fancy/aim-.png com/meldcraft/calculator/fancy/aim-p.png com/meldcraft/calculator/fancy/aimc.png com/meldcraft/calculator/fancy/aimcp.png com/meldcraft/calculator/fancy/aimr.png com/meldcraft/calculator/fancy/aimrp.png com/meldcraft/calculator/skinned/Calculator.png com/meldcraft/calculator/skinned/CalculatorSkinned.properties
- com/meldcraft/calculator/skinned/CalculatorSkinned.properties
-
baseResourceLayers = com.meldcraft.calculator.fancy.CalculatorFancy Application.rootContent = LayerPanel LayerPanel.className = com.meldcraft.application.guis.SSFactory LayerPanel.type = panel LayerPanel.elementLayers = MouseMask, CalcPanel, CalcBackground MouseMask.className = com.meldcraft.application.guis.SSFactory MouseMask.type = mouseMask MouseMask.maskedElement = CalcPanel CalcBackground.className = com.meldcraft.application.guis.SSFactory CalcBackground.type = label CalcBackground.resourceImage = com/meldcraft/calculator/skinned/Calculator CalcPanel.elementProperties = opaque=false,\ preferredSize=<element:CalcBackground>.preferredSize CalcPanel.border = 3,0,0,0 NumberPanel.elementProperties = opaque=false NumberField.elementProperties+= ,opaque=false NumberField.border = 0,0,5,0
Compile and run the program.
java -cp Scaffold.jar;ScaffoldGUIS.jar;Calculator/classes \ com.meldcraft.application.AppManager \ com.meldcraft.calculator.skinned.CalculatorSkinned
Voila! An artsy looking calculator written with plain old Swing components (only the MouseMask component is a specialized JComponent provided by the Scaffold framework).
And, not that anyone would want to, but we can still run each version of the calculator independently. The property layering feature of the Scaffold framework substantially facilitates branding and extending applications.