Merge branch 'issue/8' into develop
diff --git a/weinre.server/interfaces/WeinreClientCommands.idl b/weinre.server/interfaces/WeinreClientCommands.idl
index 35d5a74..b912cdf 100644
--- a/weinre.server/interfaces/WeinreClientCommands.idl
+++ b/weinre.server/interfaces/WeinreClientCommands.idl
@@ -18,6 +18,8 @@
         void connectTarget(in string clientId, in string targetId);
         void disconnectTarget(in string clientId);
         
+        void getExtensions(out string[] extensions)
+        
         void logDebug(   in string message );
         void logInfo(    in string message );
         void logWarning( in string message );
diff --git a/weinre.server/src/weinre/server/ExtensionManager.java b/weinre.server/src/weinre/server/ExtensionManager.java
new file mode 100644
index 0000000..f050904
--- /dev/null
+++ b/weinre.server/src/weinre/server/ExtensionManager.java
@@ -0,0 +1,91 @@
+/*
+ * weinre is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ * 
+ * Copyright (c) 2011 IBM Corporation
+ */
+
+package weinre.server;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jetty.util.resource.Resource;
+
+//-------------------------------------------------------------------
+public class ExtensionManager {
+    static private File     weinreHomeDir = null;     
+    static private File     weinreExtDir  = null;    
+    static private long     weinreExtDate = 0L;
+    static private String[] extensions    = null;
+    
+    static private String[] EMPTY_STRING_ARRAY = {};
+
+    //---------------------------------------------------------------
+    static {
+        initExtensions();
+    }
+    
+    //---------------------------------------------------------------
+    static public String[] getExtensions() {
+        if (weinreExtDate != weinreExtDir.lastModified()) {
+            initExtensions();
+        }
+        
+        return extensions;
+    }
+    
+    //---------------------------------------------------------------
+    // path: /client/extensions/weinre-ext-sample/extension.html
+    static public Resource getResource(String path) throws MalformedURLException {
+        File resourceFile = new File(weinreExtDir, path.substring(18));
+        if (!resourceFile.exists()) return null;
+        
+        try {
+            return Resource.newResource(resourceFile.toURI().toURL().toExternalForm(), false);
+        } 
+        catch (IOException e) {
+            throw new MalformedURLException();
+        }
+    }
+    
+    //---------------------------------------------------------------
+    static private void initExtensions() {
+        
+        weinreHomeDir = new File(System.getProperty("user.home"), ".weinre");
+        if (!weinreHomeDir.isDirectory()) {
+            Main.info("extensions not enabled: ~/.weinre is not a directory");
+            return;
+        }
+
+        weinreExtDir  = new File(weinreHomeDir, "extensions");
+        weinreExtDate = weinreExtDir.lastModified();
+        if (!weinreExtDir.isDirectory()) {
+            Main.info("extensions not enabled: ~/.weinre/extensions is not a directory");
+            return;
+        }
+        
+        extensions = EMPTY_STRING_ARRAY;
+            
+        List<String> extensionList = new ArrayList<String>();
+        
+        String[] entries = weinreExtDir.list();
+        for (String entry: entries) {
+            if (entry.startsWith(".")) continue;
+            
+            File extDir = new File(weinreExtDir, entry);
+            if (!extDir.isDirectory()) continue;
+            
+            File extensionHtml = new File(extDir, "extension.html");
+            if (!extensionHtml.isFile()) continue;
+            
+            extensionList.add(entry);
+        }
+        
+        extensions = extensionList.toArray(EMPTY_STRING_ARRAY);
+    }
+    
+}
diff --git a/weinre.server/src/weinre/server/http/ClassPathResourceHandler.java b/weinre.server/src/weinre/server/http/ClassPathResourceHandler.java
index 13b0072..f6eccda 100644
--- a/weinre.server/src/weinre/server/http/ClassPathResourceHandler.java
+++ b/weinre.server/src/weinre/server/http/ClassPathResourceHandler.java
@@ -14,6 +14,8 @@
 import org.eclipse.jetty.server.handler.ResourceHandler;
 import org.eclipse.jetty.util.resource.Resource;
 
+import weinre.server.ExtensionManager;
+
 //-------------------------------------------------------------------
 public class ClassPathResourceHandler extends ResourceHandler {
 
@@ -32,6 +34,11 @@
             throw new MalformedURLException(path);
         }
 
+        // handle extensions
+        if (path.startsWith("/client/extensions/")) {
+            return ExtensionManager.getResource(path);
+        }
+        
         path = pathPrefix + path;
         URL url = getClass().getClassLoader().getResource(path);
         if (url != null)
diff --git a/weinre.server/src/weinre/server/service/WeinreClientCommands.java b/weinre.server/src/weinre/server/service/WeinreClientCommands.java
index 76d08fa..7dcf1a8 100644
--- a/weinre.server/src/weinre/server/service/WeinreClientCommands.java
+++ b/weinre.server/src/weinre/server/service/WeinreClientCommands.java
@@ -17,6 +17,7 @@
 import weinre.server.Channel;
 import weinre.server.Client;
 import weinre.server.ConnectionManager;
+import weinre.server.ExtensionManager;
 import weinre.server.Main;
 import weinre.server.Target;
 
@@ -58,6 +59,26 @@
     }
 
     //---------------------------------------------------------------
+    public void getExtensions(Channel channel, String callbackId) throws IOException {
+        String[]  extensions = ExtensionManager.getExtensions();
+        JSONArray result     = new JSONArray();
+        
+        try {
+            for (String extension: extensions) {
+                JSONObject extensionObject = new JSONObject();
+                extensionObject.put("startPage", "extensions/" + extension + "/extension.html");
+                
+                result.add(extensionObject);
+            }
+        }
+        catch(JSONException e) {
+            throw new RuntimeException(e);
+        }
+        
+        channel.sendCallback("WeinreClientEvents", callbackId, result);
+    }
+
+    //---------------------------------------------------------------
     public void connectTarget(Channel channel, String clientId, String targetId, String callbackId) {
         Client client = ConnectionManager.$.getClient(clientId);
         if (client == null) return;
diff --git a/weinre.server/src/weinre/server/service/WeinreTargetCommands.java b/weinre.server/src/weinre/server/service/WeinreTargetCommands.java
index 2b3a12f..80e9a22 100644
--- a/weinre.server/src/weinre/server/service/WeinreTargetCommands.java
+++ b/weinre.server/src/weinre/server/service/WeinreTargetCommands.java
@@ -10,7 +10,6 @@
 import java.io.IOException;
 import java.util.List;
 
-import org.apache.wink.json4j.JSONException;
 import org.apache.wink.json4j.JSONObject;
 
 import weinre.server.Channel;
diff --git a/weinre.web/client/ExtensionRegistryStub.js b/weinre.web/client/ExtensionRegistryStub.js
new file mode 100644
index 0000000..8e30a7e
--- /dev/null
+++ b/weinre.web/client/ExtensionRegistryStub.js
@@ -0,0 +1,11 @@
+/*
+ * weinre is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ * 
+ * Copyright (c) 2011 IBM Corporation
+ */
+
+(function() {
+    var ExtensionRegistryImpl = require("weinre/client/ExtensionRegistryImpl").getClass()
+    window.InspectorExtensionRegistry = new ExtensionRegistryImpl()
+})()
diff --git a/weinre.web/client/web-inspector-API.js b/weinre.web/client/web-inspector-API.js
new file mode 100644
index 0000000..e7fd7b0
--- /dev/null
+++ b/weinre.web/client/web-inspector-API.js
@@ -0,0 +1,8 @@
+/*
+ * weinre is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ * 
+ * Copyright (c) 2011 IBM Corporation
+ */
+ 
+eval(window.top.installWebInspectorAPIsource())
diff --git a/weinre.web/modules/weinre/client/Client.scoop b/weinre.web/modules/weinre/client/Client.scoop
index 584d0d3..f1368d8 100644
--- a/weinre.web/modules/weinre/client/Client.scoop
+++ b/weinre.web/modules/weinre/client/Client.scoop
@@ -13,6 +13,7 @@
 requireClass ../common/Weinre
 requireClass ../common/MessageDispatcher
 requireClass ../common/Binding
+requireClass ../common/IDGenerator
 
 requireClass ./InspectorBackendImpl
 requireClass ./InspectorFrontendHostImpl
@@ -80,6 +81,7 @@
     
     var toolbar = document.getElementById("toolbar")
     WebInspector.addPanelToolbarIcon(toolbar, panel, toolbar.childNodes[1])
+    WebInspector.panelOrder.unshift(WebInspector.panelOrder.pop())
     
     WebInspector.currentPanel = panel
     
@@ -129,6 +131,27 @@
     }, 1000)
 
 //-----------------------------------------------------------------------------
+function installWebInspectorAPIsource
+
+    if ("webInspector" in window) return
+    
+    var extensionAPI = window.parent.InspectorFrontendHost.getExtensionAPI()
+    extensionAPI = extensionAPI.replace("location.hostname + location.port", "location.hostname + ':' + location.port")
+    
+    // parms are: InjectedScriptHost, inspectedWindow, injectedScriptId
+    // InjectedScriptHost is not directly referenced
+    // inspectedWindow is not directly referenced
+    // injectedScriptId is used to scope object references, appears that
+    //     each extension should have a unique value for this
+    
+    var id = IDGenerator.next()
+    console.log("installing webInspector with injectedScriptId: " + id)
+    extensionAPI += "(null,null," + id + ")"
+    return extensionAPI
+
+//-----------------------------------------------------------------------------
 static method main
     Weinre.client = new Client()
     Weinre.client.initialize()
+
+    window.installWebInspectorAPIsource = installWebInspectorAPIsource
\ No newline at end of file
diff --git a/weinre.web/modules/weinre/client/ConnectorList.scoop b/weinre.web/modules/weinre/client/ConnectorList.scoop
index bb0746d..b2ad6a5 100644
--- a/weinre.web/modules/weinre/client/ConnectorList.scoop
+++ b/weinre.web/modules/weinre/client/ConnectorList.scoop
@@ -44,6 +44,10 @@
     }
 
 //-----------------------------------------------------------------------------
+method get(channel)
+    return this.connectors[channel]
+
+//-----------------------------------------------------------------------------
 method getNewestConnectorChannel(ignoring)
     var newest = 0
     
diff --git a/weinre.web/modules/weinre/client/ExtensionRegistryImpl.scoop b/weinre.web/modules/weinre/client/ExtensionRegistryImpl.scoop
new file mode 100644
index 0000000..bee7c42
--- /dev/null
+++ b/weinre.web/modules/weinre/client/ExtensionRegistryImpl.scoop
@@ -0,0 +1,35 @@
+
+/*
+ * weinre is available under *either* the terms of the modified BSD license *or* the
+ * MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
+ * 
+ * Copyright (c) 2011 IBM Corporation
+ */
+
+requireClass ../common/Ex
+requireClass ../common/Binding
+requireClass ../common/Weinre
+
+//-----------------------------------------------------------------------------
+class ExtensionRegistryImpl
+
+//-----------------------------------------------------------------------------
+init
+    var extensions = []
+
+//-----------------------------------------------------------------------------
+method getExtensionsAsync
+    if (extensions.length) return
+    
+    Weinre.WeinreClientCommands.getExtensions(Binding(this, this._cb_getExtensions))
+        
+//-----------------------------------------------------------------------------
+method _cb_getExtensions(extensionsResult)
+    extensions = extensionsResult
+    this._installExtensions()
+    
+//-----------------------------------------------------------------------------
+method _installExtensions    
+    WebInspector.addExtensions(extensions)
+    
+    
\ No newline at end of file
diff --git a/weinre.web/modules/weinre/client/InspectorFrontendHostImpl.scoop b/weinre.web/modules/weinre/client/InspectorFrontendHostImpl.scoop
index bb5093e..cbdb6c1 100644
--- a/weinre.web/modules/weinre/client/InspectorFrontendHostImpl.scoop
+++ b/weinre.web/modules/weinre/client/InspectorFrontendHostImpl.scoop
@@ -15,6 +15,10 @@
     this._getPlatformAndPort()
 
 //-----------------------------------------------------------------------------
+init
+    var _extensionAPI
+    
+//-----------------------------------------------------------------------------
 method loaded
 
 //-----------------------------------------------------------------------------
@@ -44,6 +48,17 @@
     Weinre.logInfo(arguments.callee.signature + "(" + JSON.stringify(object,null,4) + ")")
 
 //-----------------------------------------------------------------------------
+method setExtensionAPI(extensionAPI)
+    _extensionAPI = extensionAPI
+
+//-----------------------------------------------------------------------------
+method getExtensionAPI
+    return _extensionAPI
+
+//-----------------------------------------------------------------------------
+method inspectedURLChanged
+
+//-----------------------------------------------------------------------------
 method _getPlatformAndPort
     this._platform       = "weinre"
     this._platformFlavor = "weinre"
diff --git a/weinre.web/modules/weinre/client/RemotePanel.scoop b/weinre.web/modules/weinre/client/RemotePanel.scoop
index 5e0ec93..fe7c5b8 100644
--- a/weinre.web/modules/weinre/client/RemotePanel.scoop
+++ b/weinre.web/modules/weinre/client/RemotePanel.scoop
@@ -64,6 +64,9 @@
 method addTarget(target) 
     this.targetList.add(target)
 
+method getTarget(channel)
+    return this.targetList.get(channel)
+    
 method removeClient(channel) 
     this.clientList.remove(channel)
     
diff --git a/weinre.web/modules/weinre/client/WeinreClientEventsImpl.scoop b/weinre.web/modules/weinre/client/WeinreClientEventsImpl.scoop
index 8f925e4..459373d 100644
--- a/weinre.web/modules/weinre/client/WeinreClientEventsImpl.scoop
+++ b/weinre.web/modules/weinre/client/WeinreClientEventsImpl.scoop
@@ -14,6 +14,13 @@
     this.client = client
 
 //-----------------------------------------------------------------------------
+init
+    var titleNotConnected    = "weinre: target not connected"
+    var titleConnectedPrefix = "weinre: "
+    
+    document.title = titleNotConnected
+     
+//-----------------------------------------------------------------------------
 method clientRegistered(clientDescription)
     if (this.client.uiAvailable()) {
         WebInspector.panels.remote.addClient(clientDescription)
@@ -54,15 +61,23 @@
     WebInspector.panels.elements.reset()
     WebInspector.panels.timeline._clearPanel()
     WebInspector.panels.resources.reset()
+    
+    var target = WebInspector.panels.remote.getTarget(targetChannel)
+    document.title = titleConnectedPrefix + target.url
+    
+    WebInspector.inspectedURLChanged(target.url)
 
 //-----------------------------------------------------------------------------
 method connectionDestroyed(/*int*/ clientChannel, /*int*/ targetChannel)
+
     if (this.client.uiAvailable()) {
         WebInspector.panels.remote.setClientState(clientChannel, "not-connected") 
         WebInspector.panels.remote.setTargetState(targetChannel, "not-connected")
     }
     
     if (clientChannel != Weinre.messageDispatcher.channel) return
+
+    document.title = titleNotConnected
     
     if (!Weinre.client.autoConnect()) return
     if (!this.client.uiAvailable()) return
diff --git a/weinre.web/modules/weinre/common/IDGenerator.scoop b/weinre.web/modules/weinre/common/IDGenerator.scoop
index 4829267..5af26cd 100644
--- a/weinre.web/modules/weinre/common/IDGenerator.scoop
+++ b/weinre.web/modules/weinre/common/IDGenerator.scoop
@@ -11,8 +11,8 @@
 
 //-----------------------------------------------------------------------------
 init
-    var nextId = 1
-    var idName = "__weinre__id"
+    var nextIdValue = 1
+    var idName      = "__weinre__id"
 
 //-----------------------------------------------------------------------------
 static method checkId(object)
@@ -23,7 +23,7 @@
     var id = IDGenerator.checkId(object)
     
     if (!id) {
-        id = next()
+        id = nextId()
         
         // note:
         // attempted to use Object.defineProperty() to make
@@ -40,10 +40,14 @@
     }
     
     return id
-    
+
 //-----------------------------------------------------------------------------
-function next
-    var result = nextId
-    nextId += 1
+static method next
+    return nextId()
+
+//-----------------------------------------------------------------------------
+function nextId
+    var result = nextIdValue
+    nextIdValue += 1
     return result
 
diff --git a/weinre.web/modules/weinre/target/Target.scoop b/weinre.web/modules/weinre/target/Target.scoop
index 38ff60b..0a4d81d 100644
--- a/weinre.web/modules/weinre/target/Target.scoop
+++ b/weinre.web/modules/weinre/target/Target.scoop
@@ -90,6 +90,8 @@
 
 //-----------------------------------------------------------------------------
 method initialize()
+    var self = this
+
     this.setWeinreServerURLFromScriptSrc()
     this.setWeinreServerIdFromScriptSrc()
     
@@ -101,6 +103,18 @@
     Weinre.injectedScript = injectedScriptConstructor(injectedScriptHost, window, 0, "?")
     
     window.addEventListener("load", Binding(this, "onLoaded"), false)
+    document.addEventListener("DOMContentLoaded", Binding(this, "onDOMContent"), false)
+    
+    this._startTime = currentTime()
+    
+    if (document.readyState == "loaded") {
+        setTimeout(function() { self.onDOMContent() }, 10)
+    }
+    
+    if (document.readyState == "complete") {
+        setTimeout(function() { self.onDOMContent() }, 10)
+        setTimeout(function() { self.onLoaded() }, 20)
+    }
 
     var messageDispatcher = new MessageDispatcher(window.WeinreServerURL + "ws/target", window.WeinreServerId)
     Weinre.messageDispatcher = messageDispatcher
@@ -160,7 +174,12 @@
     Weinre.targetDescription    = targetDescription
 
 //-----------------------------------------------------------------------------
-method onLoaded()
+method onLoaded
+    Weinre.wi.InspectorNotify.loadEventFired(currentTime() - this._startTime)
+
+//-----------------------------------------------------------------------------
+method onDOMContent
+    Weinre.wi.InspectorNotify.domContentEventFired(currentTime() - this._startTime)
 
 //-----------------------------------------------------------------------------
 method setDocument()
@@ -170,3 +189,8 @@
     var nodeData = Weinre.nodeStore.getNodeData(nodeId, 2)
 
     Weinre.wi.DOMNotify.setDocument(nodeData)
+    
+//-----------------------------------------------------------------------------
+// WebKit's currentTime() seems to return time in seconds
+function currentTime()
+    return (new Date().getMilliseconds()) / 1000.0
\ No newline at end of file