Introduction: Customize Your Mac's "Unit Converter" Widget

Customizing a Mac OS X Widget might seem a daunting task. It's not really. It's a mater of understanding what the Widget does and then add to that.

In the case of the Unit Converter, what it does, it does beautifully. It makes just about any conversion from and to related units. Ever wonder how many BTUs (British Thermal Units) make up a Calorie? Just pull up the Unit Converter, select the Energy catagory, the BTU and Calories sub-catagories and enter '1' into the BTU area. You're immediately told that it's 251.99576 calories. Yea, it could have rounded this to 252 calories, but that would end up corrupting the data if you chose to then convert those Calories to Joules and then back to BTUs. With any rounding, you'd end up with screwed up results. Just accept the exact results and do your own rounding as needed.

Now, where it's lacking, is that it doesn't do Numeric conversions. I'm confronted frequently by people that just can't understand why that 10GB Hard drive they just purchased has less than 9.5GB. Well, let's see if the Unit Converter can answer that...

Step 1: Get Apple's Dashcode Beta Utility

Apple has just released the beta of Dashcode. This will be a utility included with OS X 10.5 (Panther), but you can get it right now! If you're not already registered as an Apple Developer, do so now. It's free and gives you access to some neat things like this very beta!

Click this link to get Dashcode. Register now as a Developer to get to it.

Proceed to install the utility. It will be available in the folder /Developer/Applications/Dashcode. If you haven't already, install the Developer package from your OS X 10.4 DVD.

Yes. Tiger (10.4) is required for this Instructable.

Step 2: Understand the Target

The first step is to understand what the target Widget does and how it does it.

Use the widget. Play with it. See how it responds to your actions. If it does something other than what you expected when you make a change, make a note of it. Really understand the program and how it works.

In this case, open and play with the Unit Converter Widget...

Step 3: Import Into Your New Widget

Open Dashcode, and click the Import Button. Navigate to /Library/Widgets/Unit Converter and open it. Now, select "Widget Attributes" in the left column, change the "Widget Identifier" to indicate your new Widget's name and Save it with that new name.

Step 4: Examine and Understand

What we now have before us is an exact duplicate of Apple's original Unit Converter Widget with a new name. Never (And I mean NEVER!) work with the original. Always import a new copy.

Before we start messing with the code, it's a good idea to try to understand what we're messing with. Take your time and examine everything in the Widget. Examine how the various JavaScript (js) files interact. Also take note of the other support files and what they contain. Don't make any changes yet. Just try to understand how the program does what it does.

Under the 'View' menu icon, select the 'Files' option. This will open a new devision in the window showing us the files that make up this widget. Notice the files with the same name as our widget, "Unit Converter +". They include an web document (html), a style sheet (css) and a script (js). These are really all that are necessary for your basic widget. They contain the general layout of the widget as defined in the GUI section of Dashcode. Any changes we make in that area, will reflect in these files.

A more complex widget, such as this one, will contain additional scripts for complex operations and, of course, all the cool graphics that make it pretty. The additional scripts in this widget are "parser.js" and Conversions.js." The parser scripts are for interpreting our human input into a format the math equations can use and then translating the results into a format that you and I can read. The Conversions scripts are the actual equations used to make the conversions.

Step 5: Where to Make Our Changes

In this case, we want an additional conversion that will convert numbers according to the suffix used. IE: 40G = ?K. Since numbers are already handled well in this utility, we can probably assume we don't need to make changes in parser.js. The only place we will need to add code is in Conversions.js.

Select the Conversions.js in the files area and note that the Source Code automatically appears in the lower right pane. As you scroll through the listing, you'll notice a few short routines and a long list of variables starting with 'var'. These variables contain the information necessary to make the conversions in each of the conversion selections. Each section contains one item that is given the value of '1.0'. This is the base value. All the other items in that array is given a value representing the difference from the base value. The two forms indicate wether we need to multiply or divide to get there and if that item is the From or To value.

Keep scrolling to find the end of Time (sounds like a SciFi novel... Doesn't it?). This is where we will insert our Numeric conversions...

Step 6: Make the Changes

Here, you may prefer to copy and paste to avoid syntax errors. You can also just type in the data as presented. Either way, do closely examine how the data is formated and why they will make the appropriate conversions.

var Numeric = [	{name:"Yotta base2 (Y)", toBase:linearForm(1208925819614629174706176.0), fromBase:invLinForm(1208925819614629174706176.0)},	{name:"Yotta (Y)",	 toBase:linearForm(1000000000000000000000000.0), fromBase:invLinForm(1000000000000000000000000.0)},	{name:"Zeta base2 (Z)",	 toBase:linearForm(1180591620717411303424.0),	 fromBase:invLinForm(1180591620717411303424.0)},	{name:"Zetta (Z)",	 toBase:linearForm(1000000000000000000000.0),	 fromBase:invLinForm(1000000000000000000000.0)},	{name:"Exa base2 (E)",	 toBase:linearForm(1152921504606846976.0),	 fromBase:invLinForm(1152921504606846976.0)},	{name:"Exa (E)",	 toBase:linearForm(1000000000000000000.0),	 fromBase:invLinForm(1000000000000000000.0)},	{name:"Peta base2 (P)",	 toBase:linearForm(1125899906842624.0),		 fromBase:invLinForm(1125899906842624.0)},	{name:"Peta (P)",	 toBase:linearForm(1000000000000000.0),		 fromBase:invLinForm(1000000000000000.0)},	{name:"Tera base2 (T)",	 toBase:linearForm(1099511627776.0),		 fromBase:invLinForm(1099511627776.0)},	{name:"Tera (T)",	 toBase:linearForm(1000000000000.0),		 fromBase:invLinForm(1000000000000.0)},	{name:"Giga base2 (G)",	 toBase:linearForm(1073741824.0),		 fromBase:invLinForm(1073741824.0)},	{name:"Giga (G)",	 toBase:linearForm(1000000000.0),		 fromBase:invLinForm(1000000000.0)},	{name:"Mega base2 (M)",	 toBase:linearForm(1048576.0),			 fromBase:invLinForm(1048576.0)},	{name:"Mega (M)",	 toBase:linearForm(1000000.0),			 fromBase:invLinForm(1000000.0)},	{name:"Kilo base2 (K)",	 toBase:linearForm(1024.0),			 fromBase:invLinForm(1024.0)},	{name:"Kilo (k or K)",	 toBase:linearForm(1000.0),			 fromBase:invLinForm(1000.0)},	{name:"Hecto (h)",	 toBase:linearForm(100.0),			 fromBase:invLinForm(100.0)},	{name:"Deka (D)",	 toBase:linearForm(10.0),			 fromBase:invLinForm(10.0)},	{name:"Real",		 toBase:linearForm(1.0),			 fromBase:linearForm(1.0)},	{name:"Deci (d)",	 toBase:invLinForm(10.0),			 fromBase:linearForm(10.0)},	{name:"Centi (c)",	 toBase:invLinForm(100.0),			 fromBase:linearForm(100.0)},	{name:"Milli (m)",	 toBase:invLinForm(1000.0),			 fromBase:linearForm(1000.0)},	{name:"Micro (µ or u)",	 toBase:invLinForm(1000000.0),			 fromBase:linearForm(1000000.0)},	{name:"Pico (p)",	 toBase:invLinForm(1000000000.0),		 fromBase:linearForm(1000000000.0)},	{name:"Femto (f)",	 toBase:invLinForm(1000000000000.0),		 fromBase:linearForm(1000000000000.0)},	{name:"Atto (a)",	 toBase:invLinForm(1000000000000000.0),		 fromBase:linearForm(1000000000000000.0)},	{name:"Zepto (z)",	 toBase:invLinForm(1000000000000000000.0),	 fromBase:linearForm(1000000000000000000.0)},	{name:"Yocto (y)",	 toBase:invLinForm(1000000000000000000000.0),	 fromBase:linearForm(1000000000000000000000.0)}];

We've now defined out new conversion set. Next we need to add the menu item. Scroll down to the Catagories variable at the end and add our Numeric entry:

	{name:'Numeric',array:Numeric,defaultFrom: 12, defaultTo: 13, precision:8}

Note that there is already a comma after the squiggly-bracket at the end of the Volume entry. That was an uncritical syntax error in the original code. It's okay though, because we need it there now. It tells Javascript that another entry follows.

All of the above code is formated for Web display. I've attached a text file with the lines to paste into the Javascript so you don't need to mess with tabs to format it back into a readable format.

Save your work by Selecting "Save" under the File menu. We're not done yet, but it's always a good idea to save as we go...

Step 7: Localization

Most Mac applications have a system to aid is language translations. Widgets are no different. We need to add our new words into the localization files so they display nicely. In this case, to add additional languages, there should be much translating needed. The suffixes use the same Greco-Roman names almost universally. In this case, because I happen to be in the USA, were only going to edit the English set. I leave it as an exercise to add translations for other languages to you if you need it.

Under the View menu icon, go ahead and close the Files and Source Code areas by re-selecting them. Now, in the left column, select the Widget Attributes icon. Here you will see the Localization section. Select the English section and scroll the Key window to the end.

Simply enter the following Keys by pressing the '+' button and pasting them in one at a time. Dashcode will automatically fill in the Value area with a duplicate of the Key when you press Return. Normally this is all we need. If there's any translation necessary, make it to the Value area only.

NumericYotta base2 (Y)Yotta (Y)Zeta base2 (Z)Zetta (Z)Exa base2 (E)Exa (E)Peta base2 (P)Peta (P)Tera base2 (T)Tera (T)Giga base2 (G)Giga (G)Mega base2 (M)Mega (M)Kilo base2 (K)Kilo (k or K)Hecto (h)Deka (D)RealDeci (d)Centi (c)Milli (m)Micro (µ or u)Pico (p)Femto (f)Atto (a)Zepto (z)Yocto (y)

Note that these Keys have to be entered exactly or they will not appear in the popup menus. Yes... It's very time consuming and redundant, but it's also very necessary. Every variable that appears in a widget must have a translation, even if there's nothing to translate.

Now, save your work. We're almost done.

Step 8: Test and Debug

The Dashboard environment in the Dashcode beta isn't perfect, but it does effectively verify syntax and operation of the Widget. What doesn't work is what should be occurring in the background. To test our changes, click the "Run" button in the upper left window. If everything was entered correctly, you should see the Widget running in front of you.

If you made a mistake somewhere, you'll see a bit of activity in Dashcode's frames and the widget will pause at a seemingly random place in the main code. An area we didn't even touch! The reason for this is that we've added variables only into this program. An error in a variable will cause a problem in the actual code though. To find the error, first press the Stop button (where the Run button was) and select, Run Log under the View menu icon. Here, you'll see a red symbol prior to the text "SyntaxError - Parse error" or something like that. If you click on that text line, it will show you the line with the error in the lower section. Even though it looks fine and you already checked it three times, there's a flaw there. Accept it and find it. A little hint? A semi-colon (;) can look just like a colon (:). Also, did you forget the comma in the previous line?

If it runs properly, you'll see "(Running") to the right of the widget's title at the top of the window. The widget will have appeared in the middle of your screen. Let's test it!

Set the conversion to "Numeric" (Last item) and then select "Real" for the first field. Enter 4096 into this field. If Dashcode were working perfectly, you would instantly see a result on the right side. Instead we need to get around this bug, so change the second field's popup to "Kilo Base2" or anything else. You should see the correct conversion for anything you select.

Step 9: Finishing Touches

Notice the little question mark in a blue box at the top. This is there because we don't have a graphic representing this category! I've taken the liberty of creating a graphic file to represent Numeric conversions. Just download, expand, and drag the graphic file "Numeric.png" into the Images folder in the Dashcode window. Now, Stop (if you didn't already) and re-run the Widget to test.

Step 10: Build Your Finished Widget!

Well done! Now all we need to do is build out new Widget and install it!

If this were a widget we would be distributing to others, we would select "Deploy Widget" under the File menu. This would create the Widget file anywhere we want it and we would proceed to upload/email that widget to all the world. In this case, we just want to use it in Dashboard. So were going to select "Deploy Widget To Dashboard" under the File menu to bypass the whole double-click/install process.

Now you can explain to your grandkids why their brand-new 10YB (Yotta-Bytes) crystal appears as only 8,470ZB (Zeta-Bytes Base2) and exactly how your antique 2.3GHz (Giga-Hertz Base2) computer compares to their new 3EHz (Exa-Hertz) iPhone Earbud*.

For those that would rather just replace the files that need editing, sorry... I can't help you. The documents throughout this widget are copyrighted by Apple, Inc. I can't distribute original or edited copies. You'll have to make the changes as described.

*Okay... We're stretching here... You think current cell phones might cause problems due to RF radiation... EHz would literally be X-Raying your head!

Step 11: A Bonus!!!

For those that actually read through this Instructable or returned again, here's a bonus: The Volume conversion list in the original Unit Converter was sorely lacking. I've gone through the trouble of adding as many liquid measurements as I could find to make it much more usable.

Just replace the original list in the Conversion.js file following:
var Volume = [	{name:'Cubic Meter',		toBase:linearForm(1000.0), 		fromBase:invLinForm(1000.0)},	{name:'Barrel',			toBase:linearForm(117.347766),		fromBase:invLinForm(117.347766)},	{name:'Bushel (Imperial)',	toBase:linearForm(36.368735),		fromBase:invLinForm(36.368735)},	{name:'Bushel (US)',		toBase:linearForm(35.239072),		fromBase:invLinForm(35.239072)},	{name:'Cubic Feet', 		toBase:linearForm(28.316846592), 	fromBase:invLinForm(28.316846592)},	{name:'Peck (Imperial)',	toBase:linearForm(9.09218376),		fromBase:invLinForm(9.09218376)},	{name:'Peck	(US)',		toBase:linearForm(8.809768),		fromBase:invLinForm(8.809768)},	{name:'Gallon (Imperial)', 	toBase:linearForm(4.54609), 		fromBase:invLinForm(4.54609)},	{name:'Gallon (US)', 		toBase:linearForm(3.785411784), 	fromBase:invLinForm(3.785411784)},	{name:'Quart (Imperial)',	toBase:linearForm(1.13652297),		fromBase:invLinForm(1.13652297)},	{name:'Liter', 			toBase:linearForm(1.0), 		fromBase:invLinForm(1.0)},	{name:'Quart (US)', 		toBase:invLinForm(1.0566882),		fromBase:linearForm(1.0566882)},	{name:'Pint (Imperial)',	toBase:invLinForm(1.75975326),		fromBase:linearForm(1.75975326)},	{name:'Pint (US)', 		toBase:invLinForm(2.11337641886519), 	fromBase:linearForm(2.11337641886519)},	{name:'Cup (US)',		toBase:invLinForm(4.22675282),		fromBase:linearForm(4.22675282)},	{name:'Fluid Ounce (US)',	toBase:invLinForm(33.81402270184300), 	fromBase:linearForm(33.81402270184300)},	{name:'Fluid Ounce (Imp)',	toBase:invLinForm(35.1950652),		fromBase:linearForm(35.1950652)},	{name:'Tablespoon (Imp)',	toBase:invLinForm(56.3121043),		fromBase:linearForm(56.3121043)},	{name:'Cubic Inch',		toBase:invLinForm(61.023758990325284),	fromBase:linearForm(61.023758990325284)},	{name:'Tablespoon (US)',	toBase:invLinForm(67.6280451),		fromBase:linearForm(67.6280451)},	{name:'Teaspoon (Imperial)',	toBase:invLinForm(168.936313),		fromBase:linearForm(168.936313)},	{name:'Teaspoon (US)',		toBase:invLinForm(202.884135),		fromBase:linearForm(202.884135)},	{name:'Dram (US)', 		toBase:invLinForm(270.51218161474401), 	fromBase:linearForm(270.51218161474401)},	{name:'Cubic Centimeter',	toBase:invLinForm(1000.0),		fromBase:linearForm(1000.0)},	{name:'Drop',			toBase:invLinForm(12173.048),		fromBase:linearForm(12173.048)}];