Monday, August 04, 2008

Jump to the last (or any) level in MOTAS

I found out recently that a new level of MOTAS (Mistery Of Time And Space) has been released, so I thought like great, let's play it! Unfortunately, I changed the computer since I played MOTAS last time, so my cookie with the saved game was lost. I really didn't felt like replaying the whole game for the second or third time in order to reach to level 20.

The Problem: The game doesn't offer any option to jump to a specific level without playing the whole game up to that level. I played the first 10 or 12 levels 3-4 years ago and last year the first 19. I tried to find the cookie where MOTAS stores the current level, but there was none from the host gamesplanet.be in my browser. Weird ...

The Solution: Actually the saved game isn't stored in the browser's cookies, but in Flash's. Flash stores its cookies separately from the browser. MOTAS' cookie is located in

C:\Documents and Settings\[your username]\Application Data\Macromedia\Flash Player\#SharedObjects\[some random name]\gamesplanet.be

There should be a .sol file, but it's a binary file and you cannot edit it directly. Download .sol Editor (Flash Shared Object), close your browser and open the .sol file with it. You should see a string variable named savedLevel. Change its value to 21 and hit Save.


Now open your browser, go to MOTAS homepage and click on Continue game. From the game menu, choose Load game. There should be the full list of levels available. Click any of them and wait (it took several minutes in my connection) for the level to load. Enjoy the 20th level!

Wednesday, July 30, 2008

Map a keyboard shortcut to automatically search selected text in Firefox

I use the searchbox in Firefox quite often. But dragging and dropping text on it or right-clicking and choosing search... from the context menu can get annoying, especially if you want to search more phrases from a page. And let's face it, nobody uses the F1 key for Help, so ...

To map the F1 key (or another shortcut) to automatically search selected text in Firefox, you'll have to install the keyconfig extension. After restarting Firefox, go to Tools | Keyconfig. For each of the following code snippets, add a new key, choose a name, check the global checkbox and paste the code in the code section.


// Search (using the current search engine) in background tab
var ss = Cc["@mozilla.org/browser/search-service;1"].getService(Ci.nsIBrowserSearchService);
var submission = ss.currentEngine.getSubmission(getBrowserSelection(), null);
gBrowser.loadOneTab(submission.uri.spec, null, null, submission.postData, true, false);

// Search (using the current search engine) in foreground tab
var ss = Cc["@mozilla.org/browser/search-service;1"].getService(Ci.nsIBrowserSearchService);
var submission = ss.currentEngine.getSubmission(getBrowserSelection(), null);
gBrowser.loadOneTab(submission.uri.spec, null, null, submission.postData, false, false);

// Search (using the default search engine) in background tab
var ss = Cc["@mozilla.org/browser/search-service;1"].getService(Ci.nsIBrowserSearchService);
var submission = ss.defaultEngine.getSubmission(getBrowserSelection(), null);
gBrowser.loadOneTab(submission.uri.spec, null, null, submission.postData, true, false);

// Search (using the default search engine) in foreground tab
var ss = Cc["@mozilla.org/browser/search-service;1"].getService(Ci.nsIBrowserSearchService);
var submission = ss.defaultEngine.getSubmission(getBrowserSelection(), null);
gBrowser.loadOneTab(submission.uri.spec, null, null, submission.postData, false, false);


I found this code here. After creating these actions, you'll need to associate a keyboard shortcut to them. I chose F1, F2, Shift+F1 and Shift+F2 for them.

Restart Firefox, select some text and hit F1 or F2. It should open a new tab in the background (or in the foreground) with the results of the currently selected search engine.

Saturday, June 07, 2008

Writing An Exchange Rates Google Gadget

There are many exchange rate gadgets for iGoogle, but i wanted one as small and simple as possible, so i decided to write my own. There's a lot of useful documentation on Google Gadgets Developer website. Data is retreived from the European Central Bank website. Beware that several features (like settings) aren't available in the embedded version.



You can use any editor to write a Google Gadget, but the Google Editor Gadget is very useful. You should also use the Google Developer Gadget to easily add a gadget and disable caching for it. Here is my iGoogle shared tab with these two gadgets.

A gadget consists of some xml, some html and some javascript to do the work. A Hello World gadget looks like this:


<?xml version="1.0" encoding="UTF-8"?>
<Module>
<ModulePrefs title="hello world example" />
<Content type="html"><![CDATA[
Hello, world!
]]></Content>
</Module>


Module parameters, such as the title, are declared in the "ModulePrefs" tag. If the gadget requires special features, they should be declared here. I wanted my gadget to adjust its height according to the length of the content, so I requested the "dynamic-height" feature.


<ModulePrefs title="__MSG_mp_title__" title_url="http://www.ecb.int/stats/exchange/eurofxref/html/index.en.html"">
<Require feature="dynamic-height"/>
<Locale messages="http://1984.hosting.googlepages.com/ecb-exchange-rates.ALL_ALL.xml"/>
<Locale lang="ro" messages="http://1984.hosting.googlepages.com/ecb-exchange-rates.ro_ALL.xml"/>
<Locale lang="de" messages="http://1984.hosting.googlepages.com/ecb-exchange-rates.de_ALL.xml"/>
</ModulePrefs>


Beside English, I added localization for German and Romanian. The language used depends on the language set in iGoogle. If the iGoogle language is not one of these two, it will fallback to the default language, English. The structure of a localization file is very simple:


<messagebundle>
<msg name="mp_title">Euro Wechselkurs (EZB)</msg>
<msg name="up_currencyCodes">Währungs-Abkürzung</msg>
<msg name="up_numberOfColumns">Anzeige auf # Spalten</msg>
<msg name="invalid_data">Ungültige Daten.</msg>
<msg name="on">Am</msg>
</messagebundle>


The "name" attribute of the "msg" element can be used in the gadget to retrieve the value of the element (here the German translation of the strings I used).


<UserPref name="currencyCodes" display_name="__MSG_up_currencyCodes__" datatype="list" default_value="USD|GBP|JPY|CAD" />
<UserPref name="numberOfColumns" display_name="__MSG_up_numberOfColumns__" default_value="2" datatype="enum" >
<EnumValue value="1" />
<EnumValue value="2" />
<EnumValue value="3" />
<EnumValue value="4" />
<EnumValue value="5" />
<EnumValue value="6" />
<EnumValue value="7" />
<EnumValue value="8" />
</UserPref>


These are the settings for my gadget. I used a list setting to store the currencies the user wants to display, and an enum setting to store the number of columns the data should be displayed on. I initiliazed both settings with a default value. The items in a list are separeated with a "|" character. You can retrieve this settings both as an array or as a string. Strings like __MSG_up_numberOfColumns__ will be replaced with the appropriate message from the localization files.

All the html and javascript goes into the "Content" element. The type attribute of the Content element can can be "url" or "html". If you want to create a gadget from a webpage you should set the attribute to "url". I speciefied the content of my gadget inline, so I set "html". All the html and javascript should be written in a CDATA section, to avoid html tags screwing your xml.


<script type="text/javascript">
function receivedData(response)
{
//do something
}

function requestData()
{
var url = "http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml";
_IG_FetchXmlContent(url, receivedData);
}

_IG_RegisterOnloadHandler(requestData);
</script>


The content_div element will act as a container for the output. _IG_RegisterOnloadHandler is self-explaining: it registers a function to run when the module loads. _IG_FetchXmlContent loads asynchronously a XML file from the European Central Bank website. It registers the function receivedData to run when the data arrives. These functions are written in the reverse order because unlike languages like C#, in javascript you must declare a function before using it.


function receivedData(response)
{
//get settings
var prefs = new _IG_Prefs(__MODULE_ID__);
var currencyCodesArray = prefs.getArray("currencyCodes");
var currencyCodesString = prefs.getString("currencyCodes");
var useTwoColumns = prefs.getBool("useTwoColumns");
var numberOfColumns = prefs.getInt("numberOfColumns");


When data arrives error checking should be the first thing to do. But since we support more languages, we should get the settings to be able to provide the error message in the correct language. Settings are initialized with a call to new _IG_Prefs(__MODULE_ID__). I read the currencyCodes list both as a an array and a string.


//check for errors
if (response == null || typeof(response) != "object" || response.firstChild == null)
{
_gel("content_div").innerHTML = "<em>" + prefs.getMsg("invalid_data") + "</em>";
return;
}


Some basic error checking: if data isn't available, notify the user and return. Localized messages are retrieved using the getMsg function of the preferences object.


//get data
var exchangeRatesDate = response.getElementsByTagName("gesmes:Envelope").item(0).getElementsByTagName("Cube").item(0).getElementsByTagName("Cube").item(0).getAttribute("time");
var itemList = response.getElementsByTagName("gesmes:Envelope").item(0).getElementsByTagName("Cube").item(0).getElementsByTagName("Cube").item(0).getElementsByTagName("Cube");
var rowsArray = new Object();
for (var i = 0; i < itemList.length; i++)
{
var currencyCode = itemList.item(i).getAttribute("currency");
if(currencyCodesString.search(currencyCode) != -1)
{
rowsArray[currencyCode] = new Object();
rowsArray[currencyCode]["code"] = itemList.item(i).getAttribute("currency");
rowsArray[currencyCode]["rate"] = itemList.item(i).getAttribute("rate");
rowsArray[currencyCode]["mult"] = itemList.item(i).getAttribute("multiplier") != null ? currencyNode.getAttribute("multiplier") : "1";
}
}


The data is retrieved from the received XML. For each currency item in the XML, we test if the user has added it in his settings. That is why I retrieved the currencyCodes list as both an array and a string: it's easier to search in a string than iterating through an array. If the currency is found in that string, a new associative array with information like the currency code, the exchange rate and the multiplicator is added in as an element in another associative array, the key being the currency code.


//sort data
var sortedRowsArray = new Array();
for(var i = 0; i < currencyCodesArray.length; i++ )
if(rowsArray[currencyCodesArray[i]] != undefined)
sortedRowsArray.push(rowsArray[currencyCodesArray[i]]);


What happens if the user adds a wrong currency code in his list? We iterate through the currencyCodesArray and test if the rowsArray contains information about the currencies. Beside that, I wanted to 'sort' the currencies in the same order they were added.


//output html
var html = "";
html += "<span class='head'><span class='date'> " + prefs.getMsg("on") + " " + exchangeRatesDate + " </span>";
html += "<img class='flag' alt='EUR flag' src='http://1984.hosting.googlepages.com/EUR.png'> 1 EUR <span class='date'>=</span></span>";
html += "<table class='currencyTable' align='center' summary='Exchange Rates'>";
for(var i = 0; i < sortedRowsArray.length; i++)
{
if(i % numberOfColumns == 0) html += "<tr>";
html += "<td><img class='flag' alt='' src='http://1984.hosting.googlepages.com/" + sortedRowsArray[i]["code"] + ".png'></td>";
html += "<td class='number'>" + sortedRowsArray[i]["mult"] * sortedRowsArray[i]["rate"] + "</td>";
html += "<td>" + sortedRowsArray[i]["code"] + "</td>";
if(i % numberOfColumns == numberOfColumns - 1 || i == sortedRowsArray.length - 1) html += "</tr>";
else html += "<td> </td>";
}
html += "</table>";

_gel("content_div").innerHTML = html;
_IG_AdjustIFrameHeight();
}


Finally, the code handling the HTML output. Because most people will watch less than 4-5 currencies, it would be a waste of space on your homepage to leave the gadget 200px high. A call to _IG_AdjustIFrameHeight will shrink the gadget to an optimum height (remember i requested the dynamic height feature).

If you want to have a look at the code, here it is.

If you like, you can add the gadget to your iGoogle page by clicking on this button. Add to Google

Monday, February 11, 2008

Word Automation

Did you ever wanted to convert a batch of several Microsoft Word documents to another format? Did you ever wanted to read some documents in the subway on your mobile phone but your mobile phone didn't supported .doc or .docx files you had to save all those files as HTML?

You can solve this boring task by writing a small program that takes advantage of Word Automation. The basic steps of saving a Word document as HTML are:

  • Run Word
  • Load a document
  • Format the document to be more readable on your phone
  • Save the document
  • Exit Word
The steps to programmatically converting a Word document to HTML are the same. Create a new project in Visual Studio and add a reference to the Microsoft.Office.Interop.Word assembly. If you don't find the assembly in the list you can get it from here.

First we'll get all .doc or .docx files from o user specified directory.


string[] documentFiles = Directory.GetFiles(inputDirectory, "*.doc");


Then we'll have to create a Word application. This is similar to opening Word. We'll set the Visible property to false because we don't want a Word window to pop up.


Microsoft.Office.Interop.Word.Application wordApp = new Microsoft.Office.Interop.Word.Application();
Microsoft.Office.Interop.Word.Document doc;
wordApp.Visible = false;


We'll load each selected document in the Word application. All methods from the Microsoft.Office.Interop.Word assembly require you to pass the parameters (even the value-typed) as an ref Object. Some parameters are optional and you'll have to pass a reference to a Type.Missing object in place of them.


object m = Type.Missing;
object documentFile = documentFiles[i];
doc = wordApp.Documents.Open(ref documentFile, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m);


Some documents I wanted to convert had the font size 10, others 14. I find documents easier to read if they use the same font family and font size. To format the text, you have to create a range and set its Font and Size properties. I created a range containing the whole document.


object oStart = 0;
object oEnd = doc.Characters.Count;
Microsoft.Office.Interop.Word.Range range = doc.Range(ref oStart, ref oEnd);

range.Font.Size = 10;
range.Font.Name = "Arial";


Now we have to save the document. In Word there are three HTML-related options:
  • Single File Web Page (*.mht, *.mhtml) - this saves the document as a web archive, including images and text, in a single file. Images are stored in their original size, so the size can be considerable.
  • Web Page (*.htm, *.html) - saves the document in a .html file and the resources in a folder. However, the resulted size can also be quite large, because Word saves the markup and the images in their original sizes to be able to edit the document at a later time.
  • Web Page, Filtered (*.htm, *.html) - this option is quite similar to the former, with the exception that Word markup isn't stored and images are optimized when saved. If you paste in Word an image 1000x1000 but resize it to 200x200, when you save it with this option the resulting image will be only 200x200, saving you a lot of space and resources.



object wdFormatFilteredHTML = Microsoft.Office.Interop.Word.WdSaveFormat.wdFormatFilteredHTML;
doc.SaveAs(ref documentFile, ref wdFormatFilteredHTML, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m);


Finally, we have to close the document and the application.


doc.Close(ref m, ref m, ref m);
doc = null;
wordApp.Quit(ref m, ref m, ref m);
wordApp = null;

Roundup

I created a very basic Windows Application: I added on the Form a FolderBrowserDialog, a Button, a RichTextBox and a ProgressBar. I've also created an index page for all those html files. Here is the source code:


public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
if (folderBrowserDialog1.ShowDialog() == DialogResult.OK)
{
string inputDirectory = folderBrowserDialog1.SelectedPath;
string outputDirectory = Path.Combine(inputDirectory, "output");

Convert(inputDirectory, outputDirectory);
GenerateHtmlIndex(outputDirectory);
}
}

private void Convert(string inputDirectory, string outputDirectory)
{
richTextBox1.Text = "starting...\n";
richTextBox1.Update(); ;
richTextBox1.Update();
progressBar1.Minimum = 0;
progressBar1.Value = 0;

if (Directory.Exists(outputDirectory))
Directory.Delete(outputDirectory, true);

Directory.CreateDirectory(outputDirectory);

object m = Type.Missing;
object wdFormatFilteredHTML = Microsoft.Office.Interop.Word.WdSaveFormat.wdFormatFilteredHTML;

string[] documentFiles = Directory.GetFiles(inputDirectory, "*.doc");

progressBar1.Maximum = documentFiles.Length;

Microsoft.Office.Interop.Word.Application wordApp = new Microsoft.Office.Interop.Word.Application();
Microsoft.Office.Interop.Word.Document doc;
wordApp.Visible = false;

for (int i = 0; i < documentFiles.Length; i++)
{
object documentFile = documentFiles[i];
doc = wordApp.Documents.Open(ref documentFile, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m);

object oStart = 0;
object oEnd = doc.Characters.Count;
Microsoft.Office.Interop.Word.Range range = doc.Range(ref oStart, ref oEnd);

range.Font.Size = 10;
range.Font.Name = "Arial";

documentFiles[i] = Path.Combine(outputDirectory, Path.GetFileNameWithoutExtension(documentFiles[i]) + ".htm");
documentFile = documentFiles[i];

doc.SaveAs(ref documentFile, ref wdFormatFilteredHTML, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m, ref m);

doc.Close(ref m, ref m, ref m);
doc = null;

richTextBox1.AppendText(String.Format("[{0} of {1}] {2}\n", i + 1, documentFiles.Length, Path.GetFileName(documentFiles[i])));
richTextBox1.Update();
progressBar1.Increment(1);
}

wordApp.Quit(ref m, ref m, ref m);
wordApp = null;

richTextBox1.AppendText("finished.\n");
richTextBox1.Update();
}

private void GenerateHtmlIndex(string directory)
{
string[] htmFiles = Directory.GetFiles(directory, "*.htm");
StringBuilder indexFile = new StringBuilder();
indexFile.Append(
String.Format("<html><head><title>{0}</title></head><body>",
Path.GetDirectoryName(directory)));
for (int i = 0; i < htmFiles.Length; i++)
indexFile.Append(String.Format("<a href=\"{0}\">{0}</a><br />", Path.GetFileName(htmFiles[i])));
indexFile.Append("</body></html>");
File.WriteAllText(Path.Combine(directory, "_index.htm"), indexFile.ToString());
}
}