The Theme Editor

Introduction

screenshot
Figure 1: The Theme Editor with
a Completed Theme

This document describes the "Theme Editor" which was one component of a larger demo done at Tangis Corporation, called the Context Awareness (CA) demo. Tangis was founded by Dan Newell with the express purpose of developing software for wearable computers, and CA was his proposal for a software system for an always-with-you and always-on wearable computer, kind of like the Palm Pilot carried to its logical conclusion. Dan has graciously permitted me to keep and show the Theme Editor to prospective employers.

Most of the CA demo already existed when I started at Tangis, but the Theme Editor only existed at the concept level, and my first job was to design and implement it, based on a very high level description.

Overview

screenshot
Figure 2: A Stand-alone Theme

The Theme Editor is a tool for authoring "themes". To explain what a theme is, I need to delve a little bit into Context Awareness. The notion behind CA was that a person would be carrying an always-on wearable computer. This computer would be aware of its wearer's "context" -- it would know where the wearer was (via GPS, for example), it would know the time of day, it would know what other people were around, it would know what other devices were around, and it would probably know all sorts of other useful information as well. Based on this context the wearable computer could present an amalgam of the most useful information to the wearer on a head mounted display.

In this system a theme was a particular assembly of information for a particular context. Short of an artificial intelligence system, the only way a particular theme could be generated was manually, by the user. That's where the Theme Editor came in. A different part of the system was used to associate themes with particular contexts, but that's beyond the scope of this document.

The Theme Editor pretends to be a "real" program, but it's actually implemented entirely on top of Internet Explorer using scripting languages. It does a good job of demonstrating the concept, but really it has only the most basic functionality. Of course this is exactly how a good demo is supposed to work.

Theme Editor Screenshots and Commentary

Following is a sequence of screenshots showing the Theme Editor in action. These screenshots show the step-by-step construction of a theme.

Opening the Theme Editor

screenshot

This is the Theme Editor immediately after opening. The panel bay is on the left and the theme bay is the large white area to the right.

The panel bay is populated mostly by panel icons, but also by the divider icons and the delete frame icon. Panels can be introduced into the theme bay by dragging and dropping panel icons. The theme bay can also be subdivided into frames by dragging and dropping dividers. Finally frames can be deleted by dragging and dropping the delete frame icon on them.

Notes:

Dividing the Theme with a Vertical Divider

screenshot

The user has dragged and dropped a vertical divider into the theme bay. Panels could be dropped into either frame, or they could be further subdivided.

Notes:

Subdividing the Right Frame with a Horizontal Divider

screenshot

The user has dragged a horizontal divider into the right frame, further subdividing it.

Populating the Upper Right Frame with an Outlook Contacts Panel

screenshot

The user has dragged an Outlook Contacts panel to the upper-right frame. This panel uses Outlook automation to query the user's contacts.

Notes:

Poulating the Lower Right Frame with an Outlook Tasks Panel

screenshot

Now the user has dragged an Outlook Tasks panel to the lower right frame (astute readers will notice that I am using the "task" list as a shopping list).

Notes:

Populating the Left Frame with a Web Browser Panel

screenshot

The user now drags a web page panel to the left frame, fully populating this theme.

Notes:

Demonstrating Panel Functionality

screenshot

These panels are fully functional. Here the user has selected a different contact (Magnolia T.V.) in the contacts panel and a different URL (http://news.google.com) in the web page panel.

Saving the Theme

screenshot

The user clicks on the Save button in the upper right corner in order to save the new theme.

Notes:

Displaying a Stand-alone Theme

screenshot

The user has now opened the new theme directly. Note that the contacts panel and the web page panel retain the state they were in when the theme was saved in the Theme Editor.

Demonstrating Panel Functionality in a Stand-alone Theme

screenshot

The panels in the stand-alone theme are still functional. A new contact has been selected in the contacts panel and a new web page has been selected in the web page panel.

Notes:

Notes on Implementation

The Theme Editor as a whole consists of roughly 100 files. A little more than 20 of these files might be considered "source" files. Most of these files are HTML files, and most are fairly small. Nevertheless, there is a considerable amount of complexity collectively spread out over these files.

I invested a considerable amount of effort into minimizing duplication of logic (in code) and structure (in HTML). I found the most effective way of achieving the latter was a combination of XML and XSLT. I only arrived at this solution half or two-thirds of the way through the project, so I never fully exploited the capabilities it presented. In particular an XML/XSLT approach could have simplified the implementation of individual panels quite a bit.

There was a considerable pay-off in minimizing duplication in the project. The Theme Editor has surprisingly general behavior and is a lot more robust than I expected or planned for.

Along with this document I have attached three files: theme-editor.xml, theme-editor.xsl, and theme-editor.js. These files provide an incomplete picture of the Theme Editor implmentation, of course, but they provide a concrete demonstration of my coding abilities. This code is not heavily documented, but I would hope that even a superficial examination would indicate that I write really clean code, in the sense that I am scrupulous about the formatting and organization of code and especially the naming of identifiers. This attention to detail is really important for making any complex application work.

Appendix A: theme-editor.xml

<?xml version="1.0" encoding="utf-8" ?>
<?xml:stylesheet type="text/xsl" href="theme-editor.xsl" ?>
<html>
<head>
    <title>Theme Editor</title>

    <script for="window" event="onload" src="theme-editor.js"/>

    <style>

        body                
        { 
            background-color:lightgrey;
        }

        table.main
        {
            width:100%;
            height:100%;
            border-collapse:collapse;
            cell-spacing:0px;
        }

        table.main td
        {
            padding-right:10px; padding-bottom:8px
        }

        td.header    
        {
            width:100%; 
            height:0px;
            padding-left:10px;
        }

        table.header
        {
            width:100%;
            margin:0px;
            border-bottom: solid black 3px;
            border-collapse:collapse;
        }

        table.header td
        {
            padding:0px;
        }

        table.header td.title
        {
            font-family:serif; font-size:24pt; font-weight:bold;
        }

        table.header td.buttons
        {
            text-align:right;
            vertical-align:bottom;
        }

        #divSave
        {
            width:0pt;
            background-color:#B0B0B0;
            margin-bottom:2pt;
            padding:2pt;
            font:bold 10pt sans-serif;
            cursor:default;


        }

        div.panel-bay
        {
            height:100%;
            width:180px;
            overflow-y:hidden;
            margin-left:10px;
        }

        div.panel-bay select.category
        {
            width:184px;
            margin-bottom:2px;
            font-weight:bold;
        }

        div.panel-list
        {
            height:100%;
            background-color:white;
            border:inset 2px;
        }

        div.panel
        {
            padding-left:2px; padding-top:1px; padding-right:2px; padding-bottom:1px; 
            border-bottom:dotted lightgrey 1px
        }

        div.panel table.master-layout
        {
            width:99%;
            border-collapse:collapse;
        }

    </style>

</head>
<body leftmargin="0" topmargin="0" rightmargin="0" bottommargin="0">

<!--++++++++++++++++++++++++++++++++++++++++++++++++-->
<!--+++++++++++++++++ master table +++++++++++++++++-->
<!--++++++++++++++++++++++++++++++++++++++++++++++++-->

<table class="main">
    <tr>
        <td class="header" colspan="99">

            <table class="header">
                <tr>
                    <td class="title">Theme Editor</td>
                    <td class="buttons">
                        <div id="divSave">Save</div>
                    </td>
                </tr>
            </table>
        </td>
    </tr>

    <tr>
        <td ID="pagebay" class="pagebay" valign="bottom" style="width:25%">

            <panels>

                <panel-group title="Frames">

                    <panel src="theme-editor-s/hpair.bmp" href="hpair.htm" title="vertical divider"/>

                    <panel src="theme-editor-s/vpair.bmp" href="vpair.htm" title="horizontal divider"/>

                    <panel src="theme-editor-s/delete.bmp" href="panels/drop.htm?DELETE" title="delete frame"/>

                </panel-group>

                <panel-group title="Basics">

                    <panel src="theme-editor-s/world.bmp" href="panels/webpage.htm" title="web page"/>

                    <panel src="theme-editor-s/Calend.gif" href="panels/outlook/tasks.htm" title="tasks"/>

                    <panel src="theme-editor-s/contacts.gif" href="panels/outlook/contacts.htm" title="contacts"/>

                    <panel src="theme-editor-s/binoculars.bmp" href="panels/directions.htm" title="directions"/>

                    <panel src="theme-editor-s/binoculars.bmp" href="panels/traffic.htm" title="traffic"/>

                    <panel src="theme-editor-s/binoculars.bmp" href="panels/restaurant-search.htm" title="nearby restaurants"/>

                    <panel src="theme-editor-s/Wheel.gif" href="panels/car.htm" title="car"/>

                    <panel src="theme-editor-s/RICOHDC3Blue.gif" href="panels/projector.htm" title="digital projector"/>

                    <panel src="theme-editor-s/BeScript.gif" href="panels/meeting.htm" title="meeting notes"/>

                    <panel src="theme-editor-s/Dad.gif" href="panels/person.htm" title="person info"/>

                </panel-group>

            </panels>

        </td>

        <td class="framebay">
            <iframe name="framebay" src="theme.htm" scrolling="no" style="width:100%;height:100%"></iframe>
        </td>
    </tr>
</table>

</body>
</html>

Appendix B: theme-editor.xsl

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <!--++++++++++++++++++++++++++++++++++++++++++++++++-->
    <!--++++++++++++++ arbitrary elements ++++++++++++++-->
    <!--++++++++++++++++++++++++++++++++++++++++++++++++-->

    <xsl:template match="*">
        <xsl:copy>
            <xsl:copy-of select="@*"/>
            <xsl:apply-templates/>
        </xsl:copy>
    </xsl:template>

    <!--++++++++++++++++++++++++++++++++++++++++++++++++-->
    <!--++++++++++++++++ panel elements ++++++++++++++++-->
    <!--++++++++++++++++++++++++++++++++++++++++++++++++-->

    <!--+++++++++++++++++++++++++++++++ panels template -->

    <xsl:template match="panels">
        <div class="panel-bay" >
            <xsl:apply-templates select="." mode="drop-down"/>
            <div class="panel-list">
                <xsl:apply-templates/>
                <xsl:apply-templates select="." mode="script"/>
            </div>
        </div>
    </xsl:template>

    <!--+++++++++++++++++++++++++++++++ panels template (drop-down) -->

    <xsl:template match="panels" mode="drop-down">
        <select class="category" onchange="top.SelectPanel(this)">
            <option>All</option>
            <xsl:apply-templates mode="drop-down"/>
        </select>
    </xsl:template>

    <!--+++++++++++++++++++++++++++++++ panel-group template (drop-down) -->

    <xsl:template match="panel-group" mode="drop-down">
        <option id="optionPanelGroup_{count(preceding-sibling::panel-group)}"><xsl:value-of select="@title"/></option>    
    </xsl:template>

    <!--+++++++++++++++++++++++++++++++ panels template (script) -->

    <xsl:template match="panels" mode="script">
        <script>
            <xsl:apply-templates mode="script"/>
        </script>
    </xsl:template>

    <!--+++++++++++++++++++++++++++++++ panel-group template (script) -->

    <xsl:template match="panel-group" mode="script">
        <xsl:variable name="index" select="count(preceding-sibling::panel-group)"/>
        optionPanelGroup_<xsl:value-of select="$index"/>.myElement = divPanelGroup_<xsl:value-of select="$index"/>;
    </xsl:template>

    <!--+++++++++++++++++++++++++++++++ panel-group template -->

    <xsl:template match="panel-group">
        <div id="divPanelGroup_{count(preceding-sibling::panel-group)}" class="panel-group">
            <xsl:apply-templates/>
        </div>
    </xsl:template>

    <!--+++++++++++++++++++++++++++++++ panel -->

    <xsl:template match="panel">
        <div class="panel">
            <table class="master-layout">
                <tr valign="middle">
                    <td align="center" style="width:40px;height:35px;padding:0px">
                        <a href="{@href}" onclick="return false">
                            <img src="{@src}" border="0"/>
                        </a>
                    </td>
                    <td style="padding-top:5px;font-family:arial;font-weight:bold;font-size:10pt">
                        <xsl:value-of select="@title"/>
                    </td>
                </tr>
            </table>
        </div>
    </xsl:template>

</xsl:stylesheet>

Appendix C: theme-editor.js

/* File: theme-editor.js */

/*
 * This file contains JScript for theme-editor.xml.
 */

/* ------------------------------------------------------------ */
/* ----------------- functions (save document) ---------------- */
/* ------------------------------------------------------------ */

/* ---------------------------------------- RemoveTrailingBackslashes */

function RemoveTrailingBackslashes(path)
{
    return path.replace(/\\+$/, "");
}

/* ---------------------------------------- PathToParentFolder */

function PathToParentFolder(absPath)
{
    if (absPath.indexOf("\\") == -1) return null;
    absPath = RemoveTrailingBackslashes(absPath);
    absPath = absPath.replace(/\\[^\\]*$/, "");
    return absPath;
}

/* ---------------------------------------- CreateFolderIfNecessary */

function CreateFolderIfNecessary(absPathToFolder)
{
    var fso = top.createObject("Scripting.FileSystemObject");
    if (!fso.FolderExists(absPathToFolder))
    {
        fso.CreateFolder(absPathToFolder);
    }
}

/* ---------------------------------------- RemoveExtensionFromPath */

function RemoveExtensionFromPath(path)
{
    return path.replace(/\.[^\.\\]*$/, "");
}

/* ---------------------------------------- ExtractRootNameFromPath */

function ExtractRootNameFromPath(path)
{
    path = RemoveTrailingBackslashes(path);
    return path.replace(/.*\\/, "");
}

/* ---------------------------------------- ConcatPaths */

function ConcatPaths(folderPath, path)
{
    if (folderPath == null) return path;
    if (path == null) return folderPath;
    if (path == "") return folderPath;
    if (path.charAt(0) == "\\") return path;    // it's absolute
    if (path.indexOf(":") >= 0) return path;    // it's absolute (or malformed)
    var resultPath = folderPath + "\\" + path;
    return resultPath.replace(/\\+/g, "\\");
}

/* ---------------------------------------- ConcatURLAndRelativeURL */

function ConcatURLAndRelativeURL(folderURL, relativeURL)
{
    var resultURL = folderURL;
    if (relativeURL != null) resultURL += "/" + relativeURL;
    return resultURL .replace(/\/+/g, "/");
}

/* ---------------------------------------- RemoveQueryStringFromURL */

function RemoveQueryStringFromURL(url)
{
    return url.replace(/\?.*$/, "");
}

/* ---------------------------------------- ExtractRootFileNameFromURL */

function ExtractRootFileNameFromURL(url)
{
    var urlSansQueryString = RemoveQueryStringFromURL(url);
    return urlSansQueryString.replace(/.*\//, "");
}


/* ---------------------------------------- GetFileSystemPathToDocumentFolder */

function GetFileSystemPathToDocumentFolder(doc)
{
    var location = doc.parentWindow.location;
    if (location.protocol != "file:") return null;
    var absPathToDocFile = 
        unescape(location.pathname).replace(/^\/*/, "");
    var absPathToDocFileParentFolder = 
        PathToParentFolder(absPathToDocFile);
    return absPathToDocFileParentFolder;
}

/* ---------------------------------------- TranslateURLToPath */

function TranslateURLToPath(url)
{
    url = RemoveQueryStringFromURL(url);
    url = url.replace(/^file:\/\/\//, "");
    if (url.indexOf("://") >= 0) return null;
    var path = unescape(url).replace(/\//g, "\\");
    return path;
}

/* ---------------------------------------- CloneDoc */

function CloneDoc(doc)
{
    // For each SCRIPT element: Save the script text as an attribute.
    // ... This is a kludge to get around an IE bug where cloning
    // ... SCRIPT elements fails to clone the embedded script.
    for (var i = 0; i < doc.all.length; i++)
    {
        var elem = doc.all[i];
        if (elem.tagName == "SCRIPT")
        {
            elem.myOriginalText = elem.text;
        }
    }

    // Actually clone the document.
    var clonedDoc = doc.cloneNode(true);

    // Fix up the SCRIPT elements in the cloned document.
    for (var i = clonedDoc.all.length - 1; i >= 0; i--)
    {
        var elem = clonedDoc.all[i];
        if (elem.tagName == "SCRIPT")
        {
            elem.text = elem.myOriginalText;
            elem.removeAttribute("myOriginalText");
        }
    }

    // Return the clone.
    return clonedDoc;
}

/* ---------------------------------------- CreateFrameInfoList */

function CreateFrameInfoList(doc)
{
    // Get the list of all FRAME/IFRAME windows.
    var frameWindowList = doc.parentWindow.frames;

    // Create a list of all the FRAME/IFRAME elements in the document.
    // ... Note that we have to do it the long-winded way because 
    // ... the "frame elements" provided by doc.frames don't work.
    var frameElemList = new Array();
    for (var i = 0; i < doc.all.length; i++)
    {
        var elem = doc.all[i];
        if (elem.tagName == "FRAME" || elem.tagName == "IFRAME")
        {
            frameElemList[frameElemList.length] = elem;
        }
    }

    // Verify that we have the same number of frame elements as windows.
    var frameInfoList = new Array();
    if (frameElemList.length != frameWindowList.length)
    {
        var msg = "";
        msg += "[error]      # frame elements != # frame windows!\n";
        msg += "[document]   " + doc.location.href + "\n";
        msg += "[resolution] frames skipped.\n"
        alert(msg);
        return frameInfoList;
    }


    // Create the list of frame info objects.
    var pageDict = new Object();
    for (var i = 0; i < frameWindowList.length; i++)
    {
        frameWindow = frameWindowList[i];
        frameElem = frameElemList[i];

        // Get the page name if the frame doc is local -- otherwise just skip to the next frame.
        var href = null;    // the following try statment catches "permission denied" errors.
        try { href = frameWindow.location.href; } catch (e) { continue; }
        var pathToFrameDocFile = TranslateURLToPath(href);
        if (pathToFrameDocFile == null) continue;    // frame wasn't local -- skip it.
        var framePageName = ExtractRootNameFromPath(pathToFrameDocFile);

        // Make sure the page name ends in ".htm".
        // ... This prevents a bug that occurs when a frame points directly at an image.
        framePageName = RemoveExtensionFromPath(framePageName) + ".htm";

        // Uniquify the page name, if necessary.
        var pageIndex = pageDict[framePageName];
        if (pageIndex == null) pageIndex = 0;
        pageDict[framePageName] = pageIndex + 1;
        if (pageIndex > 0) 
        {
            var lastDotIndex = framePageName.lastIndexOf(".");
            if (lastDotIndex == -1) lastDotIndex = framePageName.length;
            var framePageNameHead = framePageName.substring(0, lastDotIndex);
            var framePageNameTail = framePageName.substring(lastDotIndex);
            framePageName = framePageNameHead + "-" + pageIndex + framePageNameTail;
        }

        // Build the frame info object.
        frameInfo = new Object();
        frameInfo.frameWindow = frameWindow;
        frameInfo.frameElem = frameElem;
        frameInfo.framePageName = framePageName;
        frameInfoList[frameInfoList.length] = frameInfo;
    }

    // Return the result.
    return frameInfoList;
}

/* ---------------------------------------- CopyFile */

function CopyFile(absPathToSourceFile, absPathToDestFile)
{
    try
    {
        // If the dest file already exists, make sure it's writable.
        var fso = top.createObject("Scripting.FileSystemObject");
        if (fso.FileExists(absPathToDestFile))
        {
            var destFileObject = fso.GetFile(absPathToDestFile);
            destFileObject.Attributes &= ~1;
        }

        // Actually copy the file.
        fso.CopyFile(absPathToSourceFile, absPathToDestFile, true);
    }
    catch (e)
    {
        var msg = "";
        msg += "[error]       " + e.description + "\n";
        msg += "[source path] " + absPathToSourceFile + "\n";
        msg += "[dest path]   " + absPathToDestFile + "\n";
        msg += "[resolution]  file skipped.";
        alert(msg);
    }
}

/* ---------------------------------------- SaveLinkedFile */

function SaveLinkedFile(
    elem, 
    hrefAttribName, 
    absPathToDocFileParentFolder, 
    absPathToChildFolder, 
    relURLToChildFolder
)

{
    href = elem[hrefAttribName];
    if (href == null) return;
    if (href == "") return;
    var pathToLinkedFile = TranslateURLToPath(href);
    if (pathToLinkedFile == null) return;
    var absPathToLinkedFile = 
        ConcatPaths(absPathToDocFileParentFolder, pathToLinkedFile);
    var linkedFileName = ExtractRootFileNameFromURL(href);
    CreateFolderIfNecessary(absPathToChildFolder);
    var absPathToNewLinkedFile = 
        ConcatPaths(absPathToChildFolder, linkedFileName);
    CopyFile(absPathToLinkedFile, absPathToNewLinkedFile);
    var relPathToCopiedLinkedFile = 
        ConcatURLAndRelativeURL(relURLToChildFolder, linkedFileName);
    elem[hrefAttribName] = relPathToCopiedLinkedFile;
}

/* ---------------------------------------- SaveReferencedFiles */

function SaveReferencedFiles(doc, absPathToChildFolder, relURLToChildFolder)
{
    var fso = top.createObject("Scripting.FileSystemObject");
    var absPathToDocFileParentFolder = GetFileSystemPathToDocumentFolder(doc);
    if (absPathToDocFileParentFolder == null) return;
    for (var i = 0; i < doc.all.length; i++)
    {
        var elem = doc.all[i];
        if (elem.tagName == "LINK")
        {
            SaveLinkedFile(
                elem, 
                "href", 
                absPathToDocFileParentFolder, 
                absPathToChildFolder, 
                relURLToChildFolder
            );
        }

        else if (elem.tagName == "SCRIPT")
        {
            SaveLinkedFile(
                elem, 
                "src", 
                absPathToDocFileParentFolder, 
                absPathToChildFolder, 
                relURLToChildFolder
            );
        }

        else if (elem.tagName == "IMG")
        {
            SaveLinkedFile(
                elem, 
                "src", 
                absPathToDocFileParentFolder, 
                absPathToChildFolder, 
                relURLToChildFolder
            );
        }
    }
}

/* ---------------------------------------- Save */

function Save(saveDoc, absPathToFile)
{
    // Work on a copy of the document, not the original.
    saveDoc = CloneDoc(saveDoc);

    // Make sure the directory we're trying to save to exists.
    var absPathToParentFolder = PathToParentFolder(absPathToFile);
    CreateFolderIfNecessary(absPathToParentFolder);

    // If we have frames or other dependent files, where do we put them?
    var absPathToChildFolder = RemoveExtensionFromPath(absPathToFile) + "_files";
    var childFolderName = ExtractRootNameFromPath(absPathToChildFolder);

    // Save child frames/iframes, if any.
    var frameInfoList = CreateFrameInfoList(saveDoc);
    for (var i = 0; i < frameInfoList.length; i++)
    {
        // Get the information about this particular frame;
        var frameInfo = frameInfoList[i];
        var frameWindow = frameInfo.frameWindow;
        var frameElem = frameInfo.frameElem;
        var framePageName = frameInfo.framePageName;

        // Recursively save the document for this frame.
        var absPathToFrameDocFile = 
            ConcatPaths(absPathToChildFolder, framePageName);
        Save(frameWindow.document, absPathToFrameDocFile);

        // Update the frame element with the relative location of the child file.
        var relURLToFrameDocFile = 
            ConcatURLAndRelativeURL(childFolderName, framePageName);
        frameElem.src = relURLToFrameDocFile;
    }

    // Save any other referenced files.
    SaveReferencedFiles(saveDoc, absPathToChildFolder, childFolderName);


    // Now we can save this document.
    var fso = top.createObject("Scripting.FileSystemObject");
    if (fso.FileExists(absPathToFile))
    {
        var fileObject = fso.GetFile(absPathToFile);
        fileObject.Attributes &= ~1;
    }
    var stream = fso.CreateTextFile(absPathToFile, true);
    stream.Write(saveDoc.documentElement.outerHTML);
    stream.Close();
}

/* ------------------------------------------------------------ */
/* -------------------- functions (save UI) ------------------- */
/* ------------------------------------------------------------ */

/* ---------------------------------------- divSave onmouseover */

divSave.onmouseover = function()
{
    this.style.backgroundColor = "white";
}

/* ---------------------------------------- divSave onmouseout */

divSave.onmouseout = function()
{
    this.style.backgroundColor = "#B0B0B0";
}

/* ---------------------------------------- divSave onmousedown */

divSave.onmousedown = function()
{
    this.style.backgroundColor = "white";
}

/* ---------------------------------------- divSave onmouseup */

divSave.onmouseup = function()
{
    this.style.backgroundColor = "white";
}

/* ---------------------------------------- divSave onclick */

divSave.onclick = function()
{
    // Determine out default directory.
    var absPathToThemeFolder = "c:\\dev\\JCADemo\\Themes";

    // Create the common dialog control and set its properties.
    var dialog = top.createObject("MSComDlg.CommonDialog");
    var cdlOFNOverwritePrompt = 0x2;
    dialog.MaxFileSize = 1024;    // actually means max-path-size
    dialog.DialogTitle = "Save As";
    dialog.InitDir = absPathToThemeFolder;
    dialog.FileName = "Foo.htm"
    dialog.Filter = "Themes (*.htm)|*.htm";
    dialog.Flags = cdlOFNOverwritePrompt;

    // Show the dialog; exit if the user cancels.
    try
    {
        dialog.CancelError = true;
        dialog.ShowSave();
    }
    catch (e)
    {
        if (e.description.toLowerCase().indexOf("cancel") == -1)
        {
            alert(e.description);
        }
        return;
    }

    // Do the Save.
    Save(framebay.document, dialog.FileName);
}

/* ------------------------------------------------------------ */
/* ---------------- functions (select panel UI) --------------- */
/* ------------------------------------------------------------ */

/* ---------------------------------------- SelectPanel */

function top.SelectPanel(selectElem)
{
    var optionSelected = selectElem.options[selectElem.selectedIndex];
    var defaultDisplay = "none";
    if (optionSelected.innerText.toLowerCase() == "all") defaultDisplay = "inline";


    for (var i = 0; i < selectElem.options.length; i++)
    {
        var optionCurr = selectElem.options[i];
        var display = defaultDisplay;
        if (optionCurr == optionSelected) display = "inline";
        var elemOptionSubject = optionCurr.myElement;
        if (elemOptionSubject != null) elemOptionSubject.style.display = display;
    }
}