Saturday, June 04, 2005

Code2HTML Widget

Update (06 - 06 - 2005 11:13pm) :Updated source with a few changes. Helps to alleviate the problem with white space and text-wrapping. There are a few more options now, including inserting spaces around braces, parentheses, and insertting spaces after commas. These changes help to take care of most, if not all, text-wrapping issues for most source code.

I finally got around to completing my widget. I worked for a while on trying to create the editing area as a contentEditable DIV, but finally gave up. There just isn't enough documentation on it. I got it work, but the scrolling didn't happen automatically like it does with the TEXTAREA, so I gave up, and broke Apple's user interface guidelines for widgets. Oh well, at least it functions.

When you create a widget for the Dashboard, there are a few files that must be created. These are the following:

Widget HTML - defines the layout using HTML
Widget CSS - defines the properties and stylistic elements
Widget Javascript - defines the behavior of the widget
Info.plist - widget bundle properties
Widget Background - image that comprises the body of the widget
Widget Preferences Background - image that comprises the body of the widget's preferences
Widget Icon - image for the icon in the widget tray of the dashboard

My widget, as you know if you are a regular reader, converts source code for inclusion in HTML markup. Its pretty simplistic right now, but functional enough for my needs. Currently, it supports converting spaces to non-breaking spaces, converting tabs to a variable number of non-breaking spaces, converting newlines to breaks, and converts the <, >, and & characters.

Ok, enough babbling. Here's the source for the files that make up this widget. Explanation of the source will follow in a future post.


"Code2HTML.html"
<html>

<head>
    <!-- The style sheet should be kept in a separate file; it contains the design for the widget -->
    <style type="text/css">
        @import "Code2HTML.css";
    </style>
    <!-- The JavaScript file contains the logic needed for this widget -->
    <script type='text/javascript' src='Code2HTML.js' charset='utf-8'></script>
    <script type='text/javascript' src='file:///System/Library/WidgetResources/button/genericButton.js' charset='utf-8'></script>
</head>

<body onload='setup ( ) ;'>
    <div id="front" onmousemove='mousemove ( event ) ;' onmouseout='mouseexit ( event ) ;'>
    <!-- front side here -->
    <img src='Default.png'>
    <textarea wrap="off" id="codeField"></textarea>
    <div class='flip' id='flip' onclick='showBack ( event ) ;' onmouseover='enterflip ( event ) ;' onmouseout='exitflip ( event ) ';></div>
    <div class="convertButton" id="convertButtonDiv"></div>
    <div class="revertButton" id="revertButtonDiv"></div>
    <div class='flip' id='fliprollie'></div>
    <!-- end front side -->
    </div>
    
    <div id="back">
    <!-- reverse side here -->
    <img src='Back.png'>       
    <input type="checkbox" id="convertNewlines" />
    <div id="convertNewlinesLabel">Convert Newlines To Breaks</div>
    <input type="checkbox" id="convertTabs" checked="checked"/>
    <div id="convertTabsLabel">Convert Tabs To</div>
    <input type="text" id="tabSize" value="4" size="2" maxlength="2" /><div id="tabSizeLabel">Spaces</div>
    <input type="checkbox" id="convertSpaces" checked="checked" /><div id="convertSpacesLabel">Convert Spaces To Non-Breaking Spaces</div>
    <input type="checkbox" id="convertParens" checked="checked"/>
    <div id="convertParensLabel">Insert Spaces Around Parentheses</div>
    <input type="checkbox" id="convertBraces" checked="checked"/>
    <div id="convertBracesLabel">Insert Spaces Around Braces</div>
    <input type="checkbox" id="convertCommas" checked="checked"/>
    <div id="convertCommasLabel">Insert Spaces After Commas</div>
    <div class="doneButton" id="doneButtonDiv"></div>
    <!-- end reverse side -->
    </div>
</body>

</html>


"Code2HTML.css"
body {
    margin: 0;
    font-family: verdana;
    font-size: 9pt;
}

.convertButton {
    position: absolute;
    bottom: 28px;
    left: 180px;
}

.revertButton {
    position: absolute;
    bottom: 28px;
    left: 180px;
    display: none;
}

#codeField {
    padding: 3px;
    position: absolute;
    top : 40px;
    left: 40px;
    width: 340px;
    height: 330px;
}

#convertNewlines {
    position: absolute;
    top: 50px;
    left: 30px;
}
#convertTabs {
    position: absolute;
    top: 100px;
    left: 30px;
}
#convertSpaces {
    position: absolute;
    top: 150px;
    left: 30px;
}

#tabSize {
    position: absolute;
    top: 95px;
    left: 150px;
}

#tabSizeLabel {
    position: absolute;
    top: 100px;
    left: 180px;
}

#convertTabsLabel {
    position: absolute;
    top: 100px;
    left: 50px;
}

#convertSpacesLabel {
    position: absolute;
    top: 150px;
    left: 50px;
}

#convertNewlinesLabel {
    position: absolute;
    top: 50px;
    left: 50px;
}

#convertParens {
    position: absolute;
    top: 180px;
    left: 30px;
}

#convertParensLabel {
    position: absolute;
    top: 180px;
    left: 50px;
}

#convertBraces {
    position: absolute;
    top: 210px;
    left: 30px;
}

#convertBracesLabel {
    position: absolute;
    top: 210px;
    left: 50px;
}

#convertCommas {
    position: absolute;
    top: 240px;
    left: 30px;
}

#convertCommasLabel {
    position: absolute;
    top: 240px;
    left: 50px;
}


.flip {
    position:absolute;
    bottom:30px;
    right:30px;
    width:13px;
    height:13px;
}
#flip {
    opacity:0;
    background:url ( file:///System/Library/WidgetResources/ibutton/white_i.png ) no-repeat top left;
    z-index:8000;
}
#fliprollie {
    display:none;
    opacity:0.25;
    background:url ( file:///System/Library/WidgetResources/ibutton/white_rollie.png ) no-repeat top left;
    z-index:7999;
}
#back {
    display:none;
}
.doneButton {
    position:absolute;
    bottom:30px;
    left:30px;
}

"Code2HTML.js"
var code = "";

function setup ( )
{
    createGenericButton ( document.getElementById ( 'convertButtonDiv' ) ,
        "Convert", convert ) ;  
    createGenericButton ( document.getElementById ( 'revertButtonDiv' ) ,
        "Revert", revert ) ;  
    createGenericButton ( document.getElementById ( 'doneButtonDiv' ) ,
        "Done", hideBack ) ;

    loadPrefs ( ) ;

    return 0;
}

function loadPrefs ( )
{
    if ( window.widget )
     {
        widget.setPreferenceForKey (
            document.getElementById ( 'tabSize' ) .value,
                "tabSize" ) ;
        widget.setPreferenceForKey (
            document.getElementById ( 'convertTabs' ) .checked,
                "convertTabs" ) ;
        widget.setPreferenceForKey (
            document.getElementById ( 'convertNewlines' ) .checked,
                "convertNewlines" ) ;
        widget.setPreferenceForKey (
            document.getElementById ( 'convertSpaces' ) .checked,
                "convertSpaces" ) ;
        widget.setPreferenceForKey (
            document.getElementById ( 'convertParens' ) .checked,
                "convertParens" ) ;
        widget.setPreferenceForKey (
            document.getElementById ( 'convertBraces' ) .checked,
                "convertBraces" ) ;
        widget.setPreferenceForKey (
            document.getElementById ( 'convertCommas' ) .checked,
                "convertCommas" ) ;
        
     }
}

function savePrefs ( )
{
    if ( window.widget )
     {
        document.getElementById ( 'tabSize' ) .value =
            widget.preferenceForKey (
                "tabSize" ) ;
        document.getElementById ( 'convertTabs' ) .checked,
            widget.preferenceForKey (
                "convertTabs" ) ;
        document.getElementById ( 'convertNewlines' ) .checked,
            widget.preferenceForKey (
                "convertNewlines" ) ;
        document.getElementById ( 'convertSpaces' ) .checked,
            widget.preferenceForKey (
                "convertSpaces" ) ;
        document.getElementById ( 'convertParens' ) .checked,
            widget.preferenceForKey (
                "convertParens" ) ;
        document.getElementById ( 'convertBracees' ) .checked,
            widget.preferenceForKey (
                "convertBraces" ) ;
        document.getElementById ( 'convertCommas' ) .checked,
            widget.preferenceForKey (
                "convertCommas" ) ;
     }
}

function revert ( )
{
    document.getElementById ( 'codeField' ) .value = code;
    document.getElementById ( 'codeField' ) .readOnly = false;
    document.getElementById ( 'revertButtonDiv' ) .style.display = "none";
    document.getElementById ( 'convertButtonDiv' ) .style.display = "block";
}

function convert ( )
{
    var html;
    code = document.getElementById ( 'codeField' ) .value;
    html = code.replace ( /&/g, "&amp;" ) ;
    html = html.replace ( /</g, "&lt;" ) ;
    html = html.replace ( />/g, "&gt;" ) ;
    
    if ( document.getElementById ( 'convertSpaces' ) .checked )
     {
        html = html.replace ( /\ \ /g, "&nbsp;&nbsp;" ) ;
        html = html.replace ( /\&nbsp;\ /g, "&nbsp;&nbsp;" ) ;
     }

    if ( document.getElementById ( 'convertCommas' ) .checked )
     {
        html = html.replace ( /\, /g, ", " ) ;
     }
    
    if ( document.getElementById ( 'convertParens' ) .checked )
     {
        html = html.replace ( /\ ( /g, " ( " ) ;
        html = html.replace ( /\ ) /g, " ) " ) ;
     }
    
    if ( document.getElementById ( 'convertBraces' ) .checked )
     {
        html = html.replace ( /\ { /g, " { " ) ;
        html = html.replace ( /\ } /g, " } " ) ;
     }

    if ( document.getElementById ( 'convertTabs' ) .checked )
     {
        var tabSpaces = "";
        for ( var i = 0; i < document.getElementById ( 'tabSize' ) .value; i++ )
            tabSpaces += "&nbsp;";
        html = html.replace ( /\t/g, tabSpaces ) ;
     }
    
    if ( document.getElementById ( 'convertNewlines' ) .checked )
        html = html.replace ( /\n/g, "<br/>\n" ) ;
    document.getElementById ( 'codeField' ) .value = html;
    document.getElementById ( 'convertButtonDiv' ) .style.display = "none";
    document.getElementById ( 'revertButtonDiv' ) .style.display = "block";
    document.getElementById ( 'codeField' ) .readOnly = true;
}

function showBack ( )
{
    var front = document.getElementById ( "front" ) ;
    var back = document.getElementById ( "back" ) ;
        
    if ( window.widget )
        widget.prepareForTransition ( "ToBack" ) ;
                
    front.style.display="none";
    back.style.display="block";
        
    if ( window.widget )
        setTimeout ( 'widget.performTransition ( ) ;', 0 ) ;  
}

function hideBack ( )
{
    var front = document.getElementById ( "front" ) ;
    var back = document.getElementById ( "back" ) ;
        
    if ( window.widget )
        widget.prepareForTransition ( "ToFront" ) ;
                
    back.style.display="none";
    front.style.display="block";
        
    if ( window.widget )
        setTimeout ( 'widget.performTransition ( ) ;', 0 ) ;

    savePrefs ( ) ;
}

var flipShown = false;
var animation = { duration:0, starttime:0, to:1.0, now:0.0, from:0.0, firstElement:null, timer:null } ;
function mousemove ( event )
{
    if ( !flipShown )
     {
        if ( animation.timer != null )
         {
            clearInterval ( animation.timer ) ;
            animation.timer  = null;
         }
                
        var starttime = ( new Date ) .getTime ( ) - 13;
                
        animation.duration = 500;
        animation.starttime = starttime;
        animation.firstElement = document.getElementById ( 'flip' ) ;
        animation.timer = setInterval ( "animate ( ) ;", 13 ) ;
        animation.from = animation.now;
        animation.to = 1.0;
        animate ( ) ;
        flipShown = true;
     }
}

function mouseexit ( event )
{
    if ( flipShown )
     {
        // fade in the info button
        if ( animation.timer != null )
         {
            clearInterval ( animation.timer ) ;
            animation.timer  = null;
         }
                
        var starttime = ( new Date ) .getTime ( ) - 13;
                
        animation.duration = 500;
        animation.starttime = starttime;
        animation.firstElement = document.getElementById ( 'flip' ) ;
        animation.timer = setInterval ( "animate ( ) ;", 13 ) ;
        animation.from = animation.now;
        animation.to = 0.0;
        animate ( ) ;
        flipShown = false;
     }
}

function animate ( )
{
    var T;
    var ease;
    var time = ( new Date ) .getTime ( ) ;
                
        
    T = limit_3 ( time-animation.starttime, 0, animation.duration ) ;
        
    if ( T >= animation.duration )
     {
        clearInterval ( animation.timer ) ;
        animation.timer = null;
        animation.now = animation.to;
     }
    else
     {
        ease = 0.5 - ( 0.5 * Math.cos ( Math.PI * T / animation.duration ) ) ;
        animation.now = computeNextFloat ( animation.from, animation.to, ease ) ;
     }
        
    animation.firstElement.style.opacity = animation.now;
}

function limit_3 ( a, b, c )
{
    return a < b ? b : ( a > c ? c : a ) ;
}

function computeNextFloat ( from, to, ease )
{
    return from + ( to - from ) * ease;
}

function enterflip ( event )
{
    document.getElementById ( 'fliprollie' ) .style.display = 'block';
}

function exitflip ( event )
{
    document.getElementById ( 'fliprollie' ) .style.display = 'none';
}

"Info.plist"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDisplayName</key>
    <string>Code2HTML</string>
    <key>CFBundleIdentifier</key>
    <string>net.4haks.code2html</string>
    <key>CFBundleName</key>
    <string>Code2HTML</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>CloseBoxInsetX</key>
    <integer>5</integer>
    <key>CloseBoxInsetY</key>
    <integer>5</integer>
    <key>MainHTML</key>
    <string>Code2HTML.html</string>
</dict>
</plist>

That's it for now. I'll try and go into some of the more interesting details of this code in a future post.

No comments: