Jonathan's profileJon's Vista Gadgets BlogBlogLists Tools Help
No list items have been added yet.

Jon's Vista Gadgets Blog

June 03

Power Plan Gadget

I've recently posted a "Work in Progress" Gadget on AeroXperience, that changes the current Vista Power plan based on currently running processes.  I wrote the Gadget because whilst playing Assassins Creed, I noticed Vista was lowering the CPU to 50% - which was dropping the frame rate by around 15FPS.

I normally leave the power plan on Balanced to save power, but got fed up having to manually change it to Performance whenever I played AC.

drag

Download here.

April 24

HOW TO: Debugging Gadgets

Debugging Gadgets can be a little frustrating, not only whilst coding; but also when trying to get useful feedback from users who've reported issues.

The simplest way to achieve this, is to log debug entries out to a file.  In the example below, we check to see if "debug.txt" exists in the Gadget folder and if it does write debugging information to it.

The first step is to see if the file exists and open it.  In the following code, we create a BOOLEAN variable "bDebug" which we can use later to output more detailed information, beyond simply errors:

var bDebug = oFSO.FileExists(gadgetPath+"\\debug.txt");
try{
if (bDebug)
  var debugLogFile = oFSO.OpenTextFile(gadgetPath+"\\debug.txt", 2);
} catch(err) {bDebug = false; debugLog("Open debug.txt error: "+err.name+" - "+err.message)}

//log a debug entry
function debugLog(str) {
    try{
        System.Debug.outputString(str);
        if (bDebug) debugLogFile.WriteLine(str);
    } catch(err) {}
}

It's good practice to get into the habit of wrapping all your functions in a try/catch loop, to prevent Runtime errors from popping up:

try{
  // your code
} catch (err) {debugLog(err.name+ " - "+err.message)}

It's also a good idea to put in tracking information, so you know where you are in the code.  For example:

if(bDebug) debugLog("Register ActiveX");
if(bDebug) debugLog("  trying HKCU");
if (!activateXDLL("HKCU")) {
    if(bDebug) debugLog("  trying HKLM");
    if (!activateDLL("HKLM"))
        debugLog("Error creating ActiveX object");
}

To enable "debug mode", simply create a blank text file in your Gadget folder called "debug.txt".

Personally, I put this code into all Gadgets and leave "debug.txt" out of the Gadget package.  When a user reports an issue, I ask them to create the file and eMail it back to me once they've reproduced the problem.

 

Whilst your developing your code, I'd also advise running Microsoft's Debug View; which will allow you to watch all debug entries as they occur.

In a future article, I'll detail how to write out debugging information from VB and C++ ActiveX DLL's.

HOW TO: Implementing Drag / Drop in Gadgets


Allowing drag/drop

First, you need to set up the HTML events to allow drag/drop. By default they're disabled, so you need to allow them. You do this by cancelling two drag events on the <BODY> tag:

<BODY
ondragenter="java script:event.returnValue = false"
ondragover="java script:event.returnValue = false"
>



The next thing you need to do, is set up a function to handle the drag/drop action. This is also done on the <BODY> tag with the ondrop event. In this example, the function is fileDragDropped(), so your final <BODY> tag should be:

<BODY
ondragenter="java script:event.returnValue = false"
ondragover="java script:event.returnValue = false"
ondrop="fileDragDropped"
>




Handling File drag/drop from Explorer

Files are passed through event.dataTransfer as an object with a collection of items inside. To extract each entry you need to use System.Shell.itemFromFileDrop(<object>, <n>). To avoid errors, you need to loop whilst the object is not null.

function fileDragDropped() {
    var  sFile;

    for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
    {
        // get the file name and path
        sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

        // do something with sFile
    }
}




Trapping for certain file types

In most cases, you'll want to handle only certain file types. So you need to check the file extension and ignore files that are not supported. The best way to do this is through a string that contains all your supported extensions, you then check if the file extension is in that string. supportedFileTypes contains all your extensions padded with a space either side, to stop it catching truncated extensions (eg ".doc" but not ".document"). In this example, we're checking for .doc, .xls and .ppt files:

var supportedFileTypes = " .doc .xls .ppt ";

function fileDragDropped() {
    var sFile, sFiletype;

    for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
    {
        // get the file name and path
        sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

        // get the extension
        sFiletype = " " + sFile.substring(sFile.lastIndexOf(".")).toLowerCase() + " ";

        if (supportedFileTypes.indexOf(sFiletype) > -1) {
            // do something with sFile
        }
    }
}





Handling URL drag/drop from Internet Explorer

Internet Explorer passes information slightly differently, URL's are passed as text, so you need to change the code slightly to handle these. The URL you end up with is a string and to ensure we don't pick up files being drag/dropped, we need to check if that string is null:

function fileDragDropped() {
    var url = event.dataTransfer.getData("text");
    if (url != null) {
        // do something with the url
    }
}





Putting it all together

Combining both IE and file/drag drop together, your final function will be:

var supportedFileTypes = " .doc .xls .ppt ";

function fileDragDropped() {
    var sFile, sFiletype;

    // is it a URL from IE?
    var url = event.dataTransfer.getData("text");

    if (url != null) {
        // do something with the url
    } else {
        for (var i=0; System.Shell.itemFromFileDrop(event.dataTransfer, i) != null; i++)
        {
            // get the file name and path
            sFile = System.Shell.itemFromFileDrop(event.dataTransfer, i).path;

            // get the extension
            sFiletype = " " + sFile.substring(sFile.lastIndexOf(".")).toLowerCase() + " ";
   
            // is the extension supported
            if (supportedFileTypes.indexOf(sFiletype) > -1) {
                //do something with sFile
            }
        }
    }
}
October 03

HOW TO: Skin selection

You want to provide your users with a visual skin selector in your settings, but don't know where to start.  Here's all the code you'll need to do it.  The end result will look something like this:

skinSelection

 

File structure

Firstly, create a file structure to support skins.  You'll be placing all the skins in subfolders under a themes folder.

<Gadget Folder>
    images
    themes
        <skin1 folder>
        <skin2 folder>

This keeps all your skins out of the way, and easy to distinguish from the rest of the Gadget.  Each skin subfolder should be named as you'd like the skin name to appear to the user.  ie Default, Blue, Black etc.  And each folder needs to contain a folder.png file, which should be a screenshot of the Gadget with the skin applied.  This image should ideally be scaled and no larger than 100x100 pixels.

 

Images

You'll need some images for the previous/next buttons, so save the following images in the images folder created above.

left.png: Left leftdown.png: LeftDown lefthover.png: LeftHover
right.png: Right rightdown.png: RightDown righthover.png: RightHover

 

Settings.html

The settings file needs to search the themes subfolder, store the options and select the current skin.

Without going into too much detail, it will create an array of the subfolders under themes and using the gSkin stored setting, select the currently selected skin.  The only entry you need to change is the default skin name, which is the first line of the script.

<html>
 <style>
     * {
      font-family:Segoe UI;
      font-size:12px;
     }

  body{
   width:300px;
   height:250px;
   margin:0px;
  }
 </style>
 <head>
  <script language="javascript" type="text/javascript">
   var defaultSkin = "Default";   // default skin name
   var maxSkins = 256;  // max number of skins allowed

   System.Gadget.onSettingsClosing = settingsClosing;

   var oFSO = new ActiveXObject("Scripting.FileSystemObject");
   var aSkins = new Array(maxSkins);
   var skinCount = 0;        // number of skins found
   var currentSkinNo = 0;    // the current skin selected

   var currentSetting = System.Gadget.Settings.readString("gSkin");
   if (currentSetting == "")
    currentSetting = defaultSkin;

   
   function init() {
       getSkins();
       updateSkin();
   }
   
   function settingsClosing(event) {
       try{
            if (event.closeAction == event.Action.commit) {
               //write settings to INI
               System.Gadget.Settings.write("gSkin", aSkins[currentSkinNo].substring(aSkins[currentSkinNo].lastIndexOf("\\") + 1, aSkins[currentSkinNo].length));
   
               // update the Gadget if the skin has changed
               if (currentSetting != System.Gadget.Settings.readString("gSkin"))
                   updateGadget();
           }
       } catch(err) {System.Debug.outputString("settingsClosing: " + err.name+" - "+err.message)}
   
       event.cancel = false;
   }
   
   function updateSkin() {
       imgSkin.src = aSkins[currentSkinNo] + "\\folder.png";
       divCount.innerText = (currentSkinNo + 1) + "/" + skinCount;
       divName.innerText = aSkins[currentSkinNo].substring(aSkins[currentSkinNo].lastIndexOf("\\") + 1, aSkins[currentSkinNo].length);
   }
   
   // Read skin folders
   function getSkins() {
       var oFolder = oFSO.GetFolder(System.Gadget.path + "\\themes");
       var oSubFolders = new Enumerator(oFolder.SubFolders);
   
       try{
           for (; !oSubFolders.atEnd() && skinCount < maxSkins; oSubFolders.moveNext())
           {
               aSkins[skinCount] = String(oSubFolders.item());
   
               // have we found the current skin
               if(aSkins[skinCount].toLowerCase() == (System.Gadget.path.toLowerCase() + "\\themes\\" + currentSetting).toLowerCase())
                   currentSkinNo = skinCount;
   
               skinCount++;
           }
       } catch(err) {System.Debug.outputString("getSkins: " + err.name + " - " + err.message)}
   }
   
   // Display the next Skin
   function nextSkin() {
       currentSkinNo = (currentSkinNo+1) % skinCount;
       updateSkin();
   }
   
   // Display the previous Skin
   function previousSkin() {
       currentSkinNo = (currentSkinNo-1+skinCount) % skinCount;
       updateSkin();
   }
   
   // Change a IMG source
   function changeIMG(imagesource, imagename) {
       imagesource.src= "images\\" + imagename;
   }
  </script>
 </head>
 <body onload="init()">
  <div style="position:static; width:100%; height:160px;">
   <div style="position:absolute; top:0px; left:0px; width:100%; height:100px; text-align:center;">
    <div style="width:128px; height:100%; background-color:white; border-style: solid; border-width:1px; border-color:rgb(180,180,180); overflow:hidden;">
     <div style="position:absolute; top:50%; left:50%;">
      <img id="imgSkin" src="images/blank.png" style="position:relative; top:-50%; left:-50%"/>
     </div>
    </div>
   </div>
   <div style="position:absolute; width:100%; height:50px; top:110px; left:0px; text-align:center;">
    <div style="position:absolute; top:0px; left:50%;">
     <div style="position:absolute; left:-35px;">
        <img id="imgPreviousSkin" title="Previous" src="images/left.png"
       style="position:relative; left:-50%;"
       onMouseOver="javascript:changeIMG(imgPreviousSkin,'LeftHover.png')"
       onMouseOut="javascript:changeIMG(imgPreviousSkin,'Left.png')"
       onClick="previousSkin()"
      />
     </div>
       <div style="position:absolute; left:35px;">
          <img id="imgNextSkin" title="Next" src="images/right.png"
           style="position:relative; left:-50%;"
       onMouseOver="javascript:changeIMG(imgNextSkin,'RightHover.png')"
       onMouseOut="javascript:changeIMG(imgNextSkin,'Right.png')"
       onClick="nextSkin()"
      />
     </div>
    </div>
    <div style="position:absolute; top:2px; left:50%;">
     <div id="divCount" style="position:relative; left:-50%;"></DIV>
    </div>
    <div style="position:absolute; top:22px; left:50%;">
     <B><DIV id="divName" style="position:relative; left:-50%;"></DIV></B>
    </div>
   </div>
  </div>
   </body>
</html>

 

The only function not coded above is updateGadget(), which needs to apply the skin to the Gadget.  An example would be to reload the Gadget:

function updateGadget() {
    try{
        System.Gadget.document.parentWindow.location.href = System.Gadget.document.parentWindow.location.href;
    } catch(err) {}
}

Or, call the parent function that loads the skin (see Loading the Gadget skin):

function updateGadget() {
    try{
        System.Gadget.document.parentWindow.loadSkin();
    } catch(err) {}
}

 

Loading the Gadget skin

This is the tricky bit and depends very much on how complicated you want your skins to be.  It can also get very complicated if you're supporting docked/undocked states and left/right handed sidebars.  For sanity, I've assumed we have only one Gadget state in the following examples.

Option 1 -  The skin contains graphics only, in which case you just need to update the URL's of your images. eg.

var defaultSkin = "Default";
function loadSkin() {
    // use the default skin if there's no saved setting
    if (System.Gadget.Settings.readString("gSkin") != "")
        var skinPath = "themes/" + System.Gadget.Settings.readString("gSkin") + "/";
    else
        var skinPath = "themes/" + defaultSkin + "/";

    // update the Gadget background and images
    System.Gadget.background = "url(" + skinPath + "background.png)";
    img1.src = skinPath + "img1.png";
    img2.src = skinPath + "img2.png";
    ...
}

 

Option 2 - As well as containing images, the skin also has a stylesheet called skin.css. To see this in action, look at Media Player

var defaultSkin = "Default";
function loadSkin() {
    // use the default skin if there's no saved setting
    if (System.Gadget.Settings.readString("gSkin") != "")
        var skinPath = "themes/" + System.Gadget.Settings.readString("gSkin") + "/";
    else
        var skinPath = "themes/" + defaultSkin + "/";

    // create the stylesheet if it doesn't exist
    var x = document.getElementsByTagName('link');
    if (x.length > 0)
        x[0].href = skinPath + "skin.css";
    else {
        var oLink = document.createElement("link");
        oLink.href = skinPath + "skin.css";
        oLink.rel = "stylesheet";
        oLink.type = "text/css";
        document.body.appendChild(oLink);
    }
    // update the Gadget background and images
    System.Gadget.background = "url(" + skinPath + "background.png)";
    img1.src = skinPath + "img1.png";
    img2.src = skinPath + "img2.png";
    ...
}

Note that when applying a stylesheet, due to bugs in Sidebar, several style settings aren't applied.  Body background images, body width and body height to name but a few.  If you skins change the Gadget size, you'll have to code around this by reading and applying the stylesheet settings yourself.

 

Option 3 - The skin contains code.  To see this in action, look at Spectrum Analyser.

This is ironically the easiest option, as you can leave the skin to initialise the Gadget's graphics etc.  Simply append the following code immediately before the closing </html> statement in your gadget.html file:

<script language="javascript" type="text/javascript">
    try{
        if (System.Gadget.Settings.readString("gSkin") != "")
            var skinPath = "themes/" + System.Gadget.Settings.readString("gSkin") + "/";
        else
            var skinPath = "themes/" + defaultSkin + "/";

        var scriptInclude = "<script src=\"" + skinPath + "skin.js\" language=\"javascript\" type=\"text/javascript\"></script" + ">";
        document.body.appendChild(document.createElement(scriptInclude));
    } catch(err) {System.Debug.outputString("loadSkin: " + err.name + " - " + err.message)}
</script>

Your skins should then use the skinPath variable for all local file references.

September 27

Spectrum Analyser updated

There's a new version of Spectrum Analyser available on AeroXP.

drag