Commits

Michael Gruenewald  committed fdd233d

recipes extension basics (can open dialog and search for tags)

  • Participants
  • Parent commits 12c8155

Comments (0)

Files changed (10)

File chrome.p.manifest

+# #if MODE == "dev"
+content recipes content/ xpcnativewrappers=yes
+skin recipes classic/1.0 skin/
+locale recipes en-US locale/en-US/
+# #else
+content recipes jar:recipes.jar!/content/ xpcnativewrappers=yes
+skin recipes classic/1.0 jar:recipes.jar!/skin/
+locale recipes en-US jar:recipes.jar!/locale/en-US/
+# #endif
+overlay chrome://komodo/content/komodo.xul chrome://recipes/content/overlay.xul

File components/koIRecipes.idl

+/* Copyright (c) 2009 ActiveState Software Inc.
+   See the file LICENSE.txt for licensing information. */
+
+#include "nsISupports.idl"
+#include "nsITreeView.idl"
+#include "nsIDOMElement.idl"
+#include "koIProject.idl"
+#include "koIViews.idl"
+
+
+/* An object that represents a selection in the recipes dialog. */
+[scriptable, uuid(ba08e849-62bf-4d5c-882f-66df0fe599b2)]
+interface koIRecipesHit: nsISupports {
+    readonly attribute wstring label;
+};
+
+/* Handler for tree showing results in the Recipes dialog.
+ */
+[scriptable, uuid(3e08a0fa-ae73-4319-92fa-8bd282812ce3)]
+interface koIRecipesTreeView: nsITreeView {
+    void getSelectedHits(out PRUint32 count,
+        [retval, array, size_is(count)] out koIRecipesHit hits);
+};
+
+
+/* UI Driver API for the Recipes dialog.
+ * This is a component implemented in the dialog's JavaScript.
+ */
+[scriptable, uuid(569e18be-4e8b-4af8-aeb9-91ad1bf6bb08)]
+interface koIRecipesUIDriver: nsISupports {
+    void setTreeView(in koIRecipesTreeView view);
+    void searchStarted();
+    void searchAborted();
+    void searchCompleted();
+};
+
+
+/* Driver API for the Recipes dialog. */
+[scriptable, uuid(7dcdfd2e-674b-4e71-a9ca-d4ea4edaabb5)]
+interface koIRecipesSession: nsISupports {
+    readonly attribute koIRecipesUIDriver uiDriver;
+    
+    // Start a search for files with the given query string. This
+    // will cancel a previous `findFiles` call. Results are returned
+    // by calling the `uiDriver`.
+    void findRecipes(in wstring query);
+    
+    // Abort a current search (if any).
+    void abortSearch();
+};
+
+[scriptable, uuid(be7d96c3-02bb-4716-9271-f935b5d0c68e)]
+interface koIRecipesService: nsISupports {
+    koIRecipesSession getSession(in koIRecipesUIDriver uiDriver);
+};

File components/koRecipes.py

+#!python
+# Copyright (c) 2003-2006 ActiveState Software Inc.
+# See the file LICENSE.txt for licensing information.
+
+"""The main PyXPCOM module for Komodo's 'fast open' feature, i.e. the
+"Go to File" dialog.
+"""
+
+import os
+from os.path import (expanduser, basename, split, dirname, splitext, join,
+    abspath)
+import sys
+import string
+import re
+import threading
+import Queue
+import logging
+import types
+from glob import glob
+from pprint import pprint
+from collections import defaultdict
+
+from xpcom import components, nsError, ServerException, COMException
+from xpcom._xpcom import PROXY_SYNC, PROXY_ALWAYS, PROXY_ASYNC, getProxyForObject
+from xpcom.server import UnwrapObject
+from koTreeView import TreeView
+
+try:
+    import recipeslib.recipesapi
+except ImportError:
+    # PyXPCOM registration doesn't put extension 'pylib' dirs on sys.path, so
+    # we put it in (HACK).
+    pylib_dir = join(dirname(dirname(abspath(__file__))), "pylib")
+    sys.path.insert(0, pylib_dir)
+    import recipeslib.recipesapi
+
+
+#---- globals
+
+log = logging.getLogger("recipes")
+log.setLevel(logging.DEBUG)
+
+
+
+#---- fastopen backend
+
+class KoRecipesTreeView(TreeView):
+    _com_interfaces_ = [components.interfaces.koIRecipesTreeView]
+    _reg_clsid_ = "{3e08a0fa-ae73-4319-92fa-8bd282812ce3}"
+    _reg_contractid_ = "@activestate.com/koRecipesTreeView;1"
+    _reg_desc_ = "Recipes Results Tree View"
+
+    _tree = None
+    _rows = None
+
+    def __init__(self, uiDriver):
+        self.uiDriver = uiDriver
+        TreeView.__init__(self)
+        self._tree = None
+        self._selectionProxy = None
+        self.resetHits()
+
+    _last_num_hits = 0
+
+    # Batch update the tree every n rows - bug 82962.
+    def _updateTreeView(self, force=False):
+        """Update tree view when forced, or num rows changed significantly."""
+
+        num_hits = len(self._rows)
+        prev_num_hits = self._last_num_hits
+        if (not force and num_hits < (prev_num_hits + 1000)) or not self._tree:
+            # Don't update the tree yet.
+            return
+        self._last_num_hits = num_hits
+        try:
+            self._tree.beginUpdateBatch()
+            try:
+                num_rows_changed = num_hits - prev_num_hits
+                if num_rows_changed < 0:
+                    self._tree.rowCountChanged(num_hits, num_rows_changed)
+                    #self._tree.invalidate()
+                else:
+                    self._tree.rowCountChanged(prev_num_hits, num_rows_changed)
+                    #self._tree.invalidateRange(num_hits, num_rows_changed)
+            finally:
+                self._tree.endUpdateBatch()
+        except AttributeError:
+            pass # ignore `self._tree` going away
+        if prev_num_hits == 0 and len(self._rows):  # i.e. added first row
+            self._selectionProxy.select(0)
+
+    def resetHits(self):
+        self._rows = []
+        self._updateTreeView(force=True)
+
+    def addHit(self, hit):
+        self._rows.append(hit)
+        self._updateTreeView()
+
+    def addHits(self, hits):
+        """Batch add multiple hits."""
+        self._rows += hits
+        self._updateTreeView()
+    
+    # Dev Note: These are just on the koIRecipesTreeView to relay to the
+    # koIRecipesUIDriver because only the former is passed to the backend
+    # `recipes.Driver` thread. That is kind of lame.
+    def searchStarted(self):
+        self.uiDriver.searchStarted()
+        self._timer = threading.Timer(0.5, self._updateTreeView,
+                                      kwargs={'force': True})
+        self._timer.setDaemon(True)
+        self._timer.start()
+    def searchAborted(self):
+        self._timer.cancel()
+        self.uiDriver.searchAborted()
+    def searchCompleted(self):
+        self._timer.cancel()
+        self.uiDriver.searchCompleted()
+        self._updateTreeView(force=True)
+    
+    def getSelectedHits(self): 
+        hits = []
+        for i in range(self.selection.getRangeCount()):
+            start, end = self.selection.getRangeAt(i)
+            for row_idx in range(start, end+1):
+                hits.append(self._rows[row_idx])
+        return hits
+ 
+    #---- nsITreeView methods
+
+    def setTree(self, tree):
+        if tree is None:
+            self._rawTree = self._tree = self._selectionProxy = None
+        else:
+            self._rawTree = tree
+            self._tree = getProxyForObject(None,
+                components.interfaces.nsITreeBoxObject,
+                self._rawTree, PROXY_ALWAYS | PROXY_SYNC)
+            self._selectionProxy = getProxyForObject(None,
+                components.interfaces.nsITreeSelection,
+                self.selection, PROXY_ALWAYS | PROXY_SYNC)
+
+    def get_rowCount(self):
+        try:
+            return len(self._rows)
+        except TypeError: # self._rows is None
+            return 0
+
+    def getCellText(self, row, column):
+        try:
+            return self._rows[row].label
+        except IndexError:
+            pass
+
+class KoRecipesSession(object):
+    _com_interfaces_ = [components.interfaces.koIRecipesSession]
+    _reg_desc_ = "Fast Open search session"
+    _reg_clsid_ = "{7dcdfd2e-674b-4e71-a9ca-d4ea4edaabb5}"
+    _reg_contractid_ = "@activestate.com/koRecipesSession;1"
+
+    # Number of secs to wait for previous search to stop.
+    SEARCH_STOP_TIMEOUT = 3
+
+    resultsView = None
+    uiDriver = None
+    
+    def __init__(self, driver, uiDriver):
+        self.driver = driver
+        self.uiDriver = uiDriver
+        self.resultsView = KoRecipesTreeView(uiDriver)
+        self.uiDriver.setTreeView(self.resultsView)
+    
+    def findRecipes(self, query):
+        self.driver.search(query, self.resultsView)
+
+    def abortSearch(self):
+        self.driver.abortSearch()
+
+class KoRecipesService(object):
+    _com_interfaces_ = [components.interfaces.koIRecipesService,
+                        components.interfaces.nsIObserver]
+    _reg_desc_ = "Recipes service"
+    _reg_clsid_ = "{696308a0-aa8c-4d9e-bcc1-5808d15f7381}"
+    _reg_contractid_ = "@activestate.com/koRecipesService;1"
+
+
+    _driverCache = None
+    @property
+    def driver(self):
+        if self._driverCache is None:
+            self._driverCache = RecipesDriver()
+            obsSvc = components.classes["@mozilla.org/observer-service;1"].\
+                getService(components.interfaces.nsIObserverService)
+            obsSvc.addObserver(self, "xpcom-shutdown", 1)
+        return self._driverCache
+
+    def getSession(self, uiDriver):
+        return KoRecipesSession(self.driver, uiDriver)
+
+    def observe(self, subject, topic, data):
+        if topic == "xpcom-shutdown":
+            if self._driverCache:
+                self._driverCache.stop()
+            obsSvc = components.classes["@mozilla.org/observer-service;1"].\
+                getService(components.interfaces.nsIObserverService)
+            obsSvc.removeObserver(self, "xpcom-shutdown")
+
+
+class Request(object):
+    """Virtual base class for requests put on the `Driver` queue."""
+    pass
+
+class StopRequest(Request):
+    """Request to stop the driver thread altogether."""
+    pass
+
+class SearchRequest(Request):
+    """A light object to represent a fastopen search request.
+    
+    One of these is returned by `Driver.search(...)` and it has a `.wait()`
+    method on which one can wait for the request to complete.
+    """
+    def __init__(self, query, resultsView):
+        self.query = query
+        self.resultsView = resultsView
+        self._event = threading.Event()
+    def wait(self, timeout=None):
+        self._event.wait(timeout)
+    def complete(self):
+        self._event.set()
+
+class AbortSearchRequest(Request):
+    """Request to abort a running search."""
+    pass
+
+
+class RecipesDriver(threading.Thread):
+    def __init__(self):
+        threading.Thread.__init__(self, name="recipes driver")
+        self._queue = Queue.Queue()  # internal queue of search requests
+        self.start()
+    
+    def __del__(self):
+        self.stop(False)
+    
+    def stop(self, wait=False):
+        """Stop the driver thread.
+        
+        @param wait {bool} Whether to block on the driver thread completing.
+            Default is false.
+        """
+        if self.isAlive():
+            self._queue.put(StopRequest())
+            if wait:
+                self.join()
+    
+    def abortSearch(self):
+        self._queue.put(AbortSearchRequest())
+    
+    def search(self, query, resultsView):
+        request = SearchRequest(query, resultsView)
+        self._queue.put(request)
+    
+    def run(self):
+        while True:
+            # Get the latest request on the queue (last one wins, with the
+            # exception that a "StopRequest" trumps).
+            request = self._queue.get()
+            if isinstance(request, StopRequest):
+                return
+            while True:
+                try:
+                    request = self._queue.get_nowait()
+                except Queue.Empty:
+                    break
+                if isinstance(request, StopRequest):
+                    return
+            
+            if isinstance(request, SearchRequest):
+                self._handleSearchRequest(request)
+            elif isinstance(request, AbortSearchRequest):
+                pass
+    
+    def _handleSearchRequest(self, request):
+        try:
+            resultsView = request.resultsView
+            resultsView.searchStarted()
+            resultsView.resetHits()
+            
+            api = recipeslib.recipesapi.RecipesAPI()
+            recipes = api.iterquery(tags=request.query.split())
+            resultsView.addHits(RecipesHit(recipe) for recipe in recipes)
+            
+            resultsView.searchCompleted()
+        finally:
+            request.complete()
+
+
+class RecipesHit(object):
+    _com_interfaces_ = [components.interfaces.koIRecipesHit]
+    
+    def __init__(self, recipe):
+        self.recipe = recipe
+    
+    @property
+    def label(self):
+        return "Recipe %i: %s" % (self.recipe.id, self.recipe.title)

File content/insertrecipes.js

+/* Copyright (c) 2009 ActiveState Software Inc.
+   See the file LICENSE.txt for licensing information. */
+
+//---- globals
+
+var log = ko.logging.getLogger("recipes");
+
+var gWidgets = null;
+var gUIDriver = null;  // RecipesUIDriver instance (implements koIRecipesUIDriver)
+var gSession = null;   // koIRecipesSession
+
+
+//---- routines called by dialog
+
+function onLoad()
+{
+    log.warn("Z");
+    //try {
+        gWidgets = {
+            query: document.getElementById("query"),
+            throbber: document.getElementById("throbber"),
+            results: document.getElementById("results"),
+            statusbarPath: document.getElementById("statusbar-path")
+        }
+        log.warn("X");
+        var foSvc = Components.classes["@activestate.com/koRecipesService;1"]
+            .getService(Components.interfaces.koIRecipesService);
+        log.warn(foSvc);
+        gSession = foSvc.getSession(new RecipesUIDriver());
+        
+        // Configure the session.
+
+        gWidgets.query.focus();
+        findRecipes(gWidgets.query.value);
+    //} catch(ex) {
+    //    log.exception(ex, "error loading recipes dialog");
+    //}
+    window.getAttention();
+}
+
+function onUnload()
+{
+    try {
+        if (gSession) {
+            gSession.abortSearch();
+        }
+    } catch(ex) {
+        log.exception(ex, "error unloading recipes dialog");
+    }
+}
+ 
+
+function handleDblClick() {
+    if (_insertRecipes()) {
+        window.close();
+    }
+}
+
+
+function handleWindowKeyPress(event) {
+    if (event.keyCode == KeyEvent.DOM_VK_ENTER
+        || event.keyCode == KeyEvent.DOM_VK_RETURN)
+    {
+        if (_insertRecipes()) {
+            window.close();
+        }
+    } else if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
+        window.close();
+    }
+}
+
+var _gIgnoreNextFindRecipes = false;
+function handleQueryKeyPress(event) {
+    var index;
+    var keyCode = event.keyCode;
+    if (keyCode == KeyEvent.DOM_VK_ENTER
+        || keyCode == KeyEvent.DOM_VK_RETURN)
+    {
+        // Can't turn off <Enter> firing oncommand on the <textbox type="timed">,
+        // therefore tell the handler to ignore the coming one.
+        _gIgnoreNextFindRecipes = true;
+    } else if (keyCode == KeyEvent.DOM_VK_UP && event.shiftKey) {
+        index = gWidgets.results.currentIndex - 1;
+        if (index >= 0) {
+            _extendSelectTreeRow(gWidgets.results, index);
+        }
+        event.preventDefault();
+    } else if (keyCode == KeyEvent.DOM_VK_DOWN && event.shiftKey) {
+        index = gWidgets.results.currentIndex + 1;
+        if (index < 0) {
+            index = 0;
+        }
+        if (index < gWidgets.results.view.rowCount) {
+            _extendSelectTreeRow(gWidgets.results, index);
+        }
+        event.preventDefault();
+    } else if (keyCode == KeyEvent.DOM_VK_UP
+            /* Ctrl+p for Emacs-y people. */
+            || (event.ctrlKey && event.charCode === 112)) {
+        index = gWidgets.results.currentIndex - 1;
+        if (index >= 0) {
+            _selectTreeRow(gWidgets.results, index);
+        }
+        event.preventDefault();
+    } else if (keyCode == KeyEvent.DOM_VK_DOWN
+            /* Ctrl+n for Emacs-y people. */
+            || (event.ctrlKey && event.charCode === 110)) {
+        index = gWidgets.results.currentIndex + 1;
+        if (index < 0) {
+            index = 0;
+        }
+        if (index < gWidgets.results.view.rowCount) {
+            _selectTreeRow(gWidgets.results, index);
+        }
+        event.preventDefault();
+    }
+}
+
+function findRecipes(query) {
+    if (_gIgnoreNextFindRecipes) {
+        _gIgnoreNextFindRecipes = false;
+    } else {
+        gSession.findRecipes(query);
+    }
+}
+
+
+
+//---- UI driver class
+
+function RecipesUIDriver() {
+}
+RecipesUIDriver.prototype.constructor = RecipesUIDriver;
+
+RecipesUIDriver.prototype.QueryInterface = function (iid) {
+    if (!iid.equals(Components.interfaces.koIRecipesUIDriver) &&
+        !iid.equals(Components.interfaces.nsISupports)) {
+        throw Components.results.NS_ERROR_NO_INTERFACE;
+    }
+    return this;
+}
+
+RecipesUIDriver.prototype.setTreeView = function(view) {
+    gWidgets.results.treeBoxObject.view = view;
+}
+
+
+RecipesUIDriver.prototype.searchStarted = function() {
+    _startThrobber();
+}
+RecipesUIDriver.prototype.searchAborted = function() {
+    if (window.document) {
+        _stopThrobber();
+    }
+}
+RecipesUIDriver.prototype.searchCompleted = function() {
+    if (window.document) {
+        _stopThrobber();
+    }
+}
+
+
+
+//---- internal support routines
+
+function _startThrobber() {
+    gWidgets.throbber.setAttribute("busy", "true");
+}
+
+function _stopThrobber() {
+    gWidgets.throbber.removeAttribute("busy");
+}
+
+function _extendSelectTreeRow(tree, index) {
+    tree.view.selection.rangedSelect(index, index, true);
+    tree.treeBoxObject.ensureRowIsVisible(index);
+}
+
+function _selectTreeRow(tree, index) {
+    tree.view.selection.select(index);
+    tree.treeBoxObject.ensureRowIsVisible(index);
+}
+
+/* Open the views/paths selected in the results tree.
+ * @returns {Boolean} True iff successfully opened/switched-to files/tabs.
+ *      For example, this returns false for a selected dir -- which isn't opened
+ *      but set into the query box.
+ */
+function _insertRecipes() {
+    /*
+    var hits = gWidgets.results.view.getSelectedHits(new Object());
+    var hit, viewType, tabGroup;
+    for (var i in hits) {
+        var hit = hits[i];
+        if (hit.isdir) {
+            // Selecting a dir should just enter that dir into the filter box.
+            gWidgets.query.value = gOsPath.join(
+                gOsPath.dirname(gWidgets.query.value), hit.base) + gSep;
+            findFiles(gWidgets.query.value);
+            return false;
+        } else if (hit.type == "open-view") {
+            hit.view.makeCurrent();
+        } else {
+            //TODO: For history hits could perhaps restore viewType. Would need
+            // changes to the back end for that.
+            var viewType = "editor";
+            // Note: Komodo APIs are mixing "tabGroupId" (also "tabbedViewId") to
+            // mean either the full DOM element id (e.g. "view-1") or just the
+            // number (e.g. 1). TODO: fix this.
+            var tabGroup = null;
+            var uri = ko.uriparse.pathToURI(hit.path);
+            opener.ko.views.manager.openViewAsync(viewType, uri, tabGroup);
+        }
+    }
+    */
+    return true;
+}

File content/insertrecipes.xul

+<?xml version="1.0"?>
+<!-- Copyright (c) 2000-2007 ActiveState Software Inc.
+     See the file LICENSE.txt for licensing information. -->
+
+<!DOCTYPE window SYSTEM "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" [
+  <!ENTITY % recipesDTD SYSTEM "chrome://recipes/locale/recipes.dtd">
+  %recipesDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
+<?xml-stylesheet href="chrome://komodo/skin/platform.css" type="text/css"?>
+<?xml-stylesheet href="chrome://recipes/skin/recipes.css" type="text/css"?>
+
+<window id="dialog-insert-recipe"
+        window_type="recipes_insert"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+        title="&recipes.insertWindow.title;"
+        onload="onLoad()"
+        onunload="onUnload()"
+        orient="vertical"
+        width="500"
+        height="400"
+        style="min-height: 350px; min-width: 35em; max-width: 55em;"
+        persist="width height"
+        onkeypress="handleWindowKeyPress(event)">
+
+  <script src="chrome://komodo/content/library/logging.js" type="application/x-javascript;version=1.7"/>
+  <script src="chrome://komodo/content/library/windowManager.js" type="application/x-javascript;version=1.7"/>
+  <script src="chrome://komodo/content/library/uriparse.js" type="application/x-javascript;version=1.7"/>
+  <script src="chrome://recipes/content/insertrecipes.js" type="application/x-javascript;version=1.7"/>
+    
+  <vbox flex="1">
+    <hbox style="margin: 10px;">
+      <textbox id="query" type="timed" timeout="200"
+          oncommand="findRecipes(this.value)"
+          onkeypress="handleQueryKeyPress(event)"
+          class="search-box"
+          flex="1" />
+      <!-- This hbox is used to stop the throbber image from stretching. -->
+      <hbox align="center">
+        <image id="throbber" class="throbber" busy="true" />
+      </hbox>
+    </hbox>
+
+    <tree id="results" flex="1" hidecolumnpicker="true"
+        ondblclick="handleDblClick(event)"
+        onselect="this.view.selectionChanged()"
+        style="margin: 0px;">
+      <treecols>
+          <treecol id="results-file" hideheader="true" flex="1"/>
+      </treecols>
+      <treechildren />
+    </tree>
+  
+    <statusbar id="statusbar">
+      <statusbarpanel id="statusbar-path" crop="center" flex="1" />
+    </statusbar>
+  </vbox>
+</window>
+

File content/overlay.xul

+<?xml version="1.0"?>
+
+<!DOCTYPE overlay PUBLIC "-//MOZILLA//DTD XUL V1.0//EN" "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" [
+  <!ENTITY % recipesDTD SYSTEM "chrome://recipes/locale/recipes.dtd">
+  %recipesDTD;
+]>
+
+<overlay id="recipes-overlay"
+         xmlns:html="http://www.w3.org/1999/xhtml"
+         xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  
+  <script src="chrome://recipes/content/recipes.js" type="application/x-javascript;version=1.7"/>
+  
+  <commandset id="allcommands">
+    <command id="cmd_insertRecipe"
+             oncommand="ko.recipes.open_insert_dialog();"
+             desc="&recipes.insertDesc;" />
+  </commandset>
+
+  <menupopup id="popup_tools">
+    <menuitem id="menu_insertRecipe"
+              key="key_cmd_insertRecipe"
+              label="&recipes.insertMenuItem;"
+              observes="cmd_insertRecipe" />
+  </menupopup>
+</overlay>

File content/recipes.js

+/* Copyright (c) 2009 ActiveState Software Inc.
+   See the file LICENSE.txt for licensing information. */
+
+/* Recipes functionality.
+ *
+ * Defines the "ko.recipes" namespace.
+ */
+
+if (typeof(ko) == 'undefined') {
+    var ko = {};
+}
+
+ko.recipes = {};
+(function() {
+
+    var log = ko.logging.getLogger("recipes");
+
+
+    //---- public interface
+
+    this.open_insert_dialog = function open_insert_dialog() {
+        var obj = new Object();
+        ko.windowManager.openDialog("chrome://recipes/content/insertrecipes.xul",
+            "dialog-insert-recipe",
+            "chrome,modal,centerscreen,titlebar,resizable=yes",
+            obj);
+    }
+    
+}).apply(ko.recipes);
+

File locale/en-US/recipes.dtd

+<!ENTITY recipes.insertDesc "Inserts a recipe from ActiveState Code">
+<!ENTITY recipes.insertMenuItem "Insert recipe...">
+
+<!ENTITY recipes.insertWindow.title "Insert recipe from ActiveState Code...">
   <boolean id="import_recursive">1</boolean>
   <string id="import_type">useFolders</string>
 </preference-set>
+<preference-set idref="dfb999cc-0214-471f-8d16-728c9f5665a9/components/koRecipes.py">
+<preference-set id="Invocations">
+<preference-set id="default">
+  <string id="cookieparams"></string>
+  <string id="cwd"></string>
+  <string id="documentRoot"></string>
+  <string id="executable-params"></string>
+  <string relative="path" id="filename">components/koRecipes.py</string>
+  <string id="getparams"></string>
+  <string id="language">Python</string>
+  <string id="mpostparams"></string>
+  <string id="params"></string>
+  <string id="postparams"></string>
+  <string id="posttype">application/x-www-form-urlencoded</string>
+  <string id="request-method">GET</string>
+  <boolean id="show-dialog">1</boolean>
+  <boolean id="sim-cgi">0</boolean>
+  <boolean id="use-console">0</boolean>
+  <string id="userCGIEnvironment"></string>
+  <string id="userEnvironment"></string>
+</preference-set>
+</preference-set>
+  <string id="lastInvocation">default</string>
+</preference-set>
 </project>

File skin/recipes.css

+
+/* Spinning busy throbber. */
+.throbber {
+    list-style-image: none;
+    width: 16px;
+    height: 16px;
+    margin-top: 5px;
+    margin-bottom: 5px;
+    -moz-margin-start: 5px;
+    -moz-margin-end: 2px;
+}
+
+.throbber[busy="true"] {
+    list-style-image: url("chrome://global/skin/icons/loading_16.png");
+}
+
+
+treechildren::-moz-tree-image {
+    margin-right: 2px;
+}
+
+
+/* Styling of results view rows in the 'Go to File' dialog's <tree>.
+ * Note: This is all disabled because can't find something that works well.
+*/
+
+/*
+treechildren::-moz-tree-row(blockStart) {
+    border-top: 1px solid #eee;
+    padding-top: 5px;
+}
+
+// lame
+treechildren::-moz-tree-row(open-view) { border-right: 3px solid red; }
+treechildren::-moz-tree-row(path) { border-right: 3px solid blue; }
+treechildren::-moz-tree-row(project-path) { border-right: 3px solid yellow; }
+treechildren::-moz-tree-row(history-uri) { border-right: 3px solid green; }
+
+// So far can't get the selected/focus ones to clean up.
+treechildren::-moz-tree-row(odd_block) {
+    background-color: #eee;
+}
+treechildren::-moz-tree-row(odd_block, selected) {
+    background-color: threedface;
+}
+treechildren::-moz-tree-row(odd_block, selected, focus) {
+    background-color: highlight;
+}
+*/