Skip to content
This repository
Browse code

Spin off AwesomeBar HD from Home Dash.

Combine the location and search bar with category searches.
  • Loading branch information...
commit 89afdc206cfcc7c7a8b13f06c201b69fa1520790 1 parent 6be85dc
Edward Lee authored April 21, 2011
743  awesomeBarHD/bootstrap.js
... ...
@@ -0,0 +1,743 @@
  1
+/* ***** BEGIN LICENSE BLOCK *****
  2
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3
+ *
  4
+ * The contents of this file are subject to the Mozilla Public License Version
  5
+ * 1.1 (the "License"); you may not use this file except in compliance with
  6
+ * the License. You may obtain a copy of the License at
  7
+ * http://www.mozilla.org/MPL/
  8
+ *
  9
+ * Software distributed under the License is distributed on an "AS IS" basis,
  10
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11
+ * for the specific language governing rights and limitations under the
  12
+ * License.
  13
+ *
  14
+ * The Original Code is AwesomeBar HD.
  15
+ *
  16
+ * The Initial Developer of the Original Code is The Mozilla Foundation.
  17
+ * Portions created by the Initial Developer are Copyright (C) 2011
  18
+ * the Initial Developer. All Rights Reserved.
  19
+ *
  20
+ * Contributor(s):
  21
+ *   Edward Lee <edilee@mozilla.com>
  22
+ *
  23
+ * Alternatively, the contents of this file may be used under the terms of
  24
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
  25
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26
+ * in which case the provisions of the GPL or the LGPL are applicable instead
  27
+ * of those above. If you wish to allow use of your version of this file only
  28
+ * under the terms of either the GPL or the LGPL, and not to allow others to
  29
+ * use your version of this file under the terms of the MPL, indicate your
  30
+ * decision by deleting the provisions above and replace them with the notice
  31
+ * and other provisions required by the GPL or the LGPL. If you do not delete
  32
+ * the provisions above, a recipient may use your version of this file under
  33
+ * the terms of any one of the MPL, the GPL or the LGPL.
  34
+ *
  35
+ * ***** END LICENSE BLOCK ***** */
  36
+
  37
+"use strict";
  38
+const global = this;
  39
+
  40
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
  41
+Cu.import("resource://gre/modules/AddonManager.jsm");
  42
+Cu.import("resource://gre/modules/Services.jsm");
  43
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  44
+
  45
+// Keep a reference to the top level providers data
  46
+let allProviders;
  47
+
  48
+// Get and set preferences under the prospector pref branch
  49
+XPCOMUtils.defineLazyGetter(global, "prefs", function() {
  50
+  Cu.import("resource://services-sync/ext/Preferences.js");
  51
+  return new Preferences("extensions.prospector.awesomeBarHD.");
  52
+});
  53
+
  54
+// Remove existing Firefox UI and add in custom AwesomeBar HD
  55
+function addAwesomeBarHD(window) {
  56
+  let {async, change, createNode} = makeWindowHelpers(window);
  57
+  let {document, gBrowser, gIdentityHandler, gURLBar} = window;
  58
+
  59
+  // Get references to existing UI elements
  60
+  let origIdentity = gIdentityHandler._identityBox;
  61
+  let origInput = gURLBar.mInputField;
  62
+
  63
+  // Add an icon to indicate the active category
  64
+  let iconBox = createNode("box");
  65
+  iconBox.setAttribute("align", "center");
  66
+  iconBox.setAttribute("hidden", true);
  67
+  iconBox.setAttribute("id", "identity-box");
  68
+  origIdentity.parentNode.insertBefore(iconBox, origIdentity.nextSibling);
  69
+
  70
+  unload(function() {
  71
+    iconBox.parentNode.removeChild(iconBox);
  72
+  });
  73
+
  74
+  let providerIcon = createNode("image");
  75
+  providerIcon.setAttribute("id", "page-proxy-favicon");
  76
+  iconBox.appendChild(providerIcon);
  77
+
  78
+  // Add stuff around the original urlbar input box
  79
+  let urlbarStack = createNode("stack");
  80
+  origInput.parentNode.insertBefore(urlbarStack, origInput.nextSibling);
  81
+
  82
+  urlbarStack.setAttribute("flex", 1);
  83
+
  84
+  unload(function() {
  85
+    urlbarStack.parentNode.removeChild(urlbarStack);
  86
+  });
  87
+
  88
+  // Create a browser to prefetch search results
  89
+  let prefetcher = createNode("browser");
  90
+  prefetcher.setAttribute("autocompletepopup", gBrowser.getAttribute("autocompletepopup"));
  91
+  prefetcher.setAttribute("collapsed", true);
  92
+  prefetcher.setAttribute("contextmenu", gBrowser.getAttribute("contentcontextmenu"));
  93
+  prefetcher.setAttribute("tooltip", gBrowser.getAttribute("contenttooltip"));
  94
+  prefetcher.setAttribute("type", "content");
  95
+  gBrowser.appendChild(prefetcher);
  96
+
  97
+  // Save the prefetched page to a tab in the browser
  98
+  prefetcher.persistTo = function(targetTab) {
  99
+    let targetBrowser = targetTab.linkedBrowser;
  100
+    targetBrowser.stop();
  101
+
  102
+    // Unhook our progress listener
  103
+    let selectedIndex = targetTab._tPos;
  104
+    const filter = gBrowser.mTabFilters[selectedIndex];
  105
+    let tabListener = gBrowser.mTabListeners[selectedIndex];
  106
+    targetBrowser.webProgress.removeProgressListener(filter);
  107
+    filter.removeProgressListener(tabListener);
  108
+    let tabListenerBlank = tabListener.mBlank;
  109
+
  110
+    // Restore current registered open URI
  111
+    let previewURI = prefetcher.currentURI;
  112
+    let openPage = gBrowser._placesAutocomplete;
  113
+    if (targetBrowser.registeredOpenURI) {
  114
+      openPage.unregisterOpenPage(targetBrowser.registeredOpenURI);
  115
+      delete targetBrowser.registeredOpenURI;
  116
+    }
  117
+    openPage.registerOpenPage(previewURI);
  118
+    targetBrowser.registeredOpenURI = previewURI;
  119
+
  120
+    // Save the last history entry from the preview if it has loaded
  121
+    let history = prefetcher.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
  122
+    let lastEntry;
  123
+    if (history.count > 0) {
  124
+      lastEntry = history.getEntryAtIndex(history.index, false);
  125
+      history.PurgeHistory(history.count);
  126
+    }
  127
+
  128
+    // Copy over the history from the target browser if it's not empty
  129
+    let origHistory = targetBrowser.sessionHistory;
  130
+    for (let i = 0; i <= origHistory.index; i++) {
  131
+      let origEntry = origHistory.getEntryAtIndex(i, false);
  132
+      if (origEntry.URI.spec != "about:blank")
  133
+        history.addEntry(origEntry, true);
  134
+    }
  135
+
  136
+    // Add the last entry from the preview; in-progress preview will add itself
  137
+    if (lastEntry != null)
  138
+      history.addEntry(lastEntry, true);
  139
+
  140
+    // Swap the docshells then fix up various properties
  141
+    targetBrowser.swapDocShells(prefetcher);
  142
+    targetBrowser.webNavigation.sessionHistory = history;
  143
+    targetBrowser.attachFormFill();
  144
+    gBrowser.setTabTitle(targetTab);
  145
+    gBrowser.updateCurrentBrowser(true);
  146
+    gBrowser.useDefaultIcon(targetTab);
  147
+
  148
+    // Restore the progress listener
  149
+    tabListener = gBrowser.mTabProgressListener(targetTab, targetBrowser, tabListenerBlank);
  150
+    gBrowser.mTabListeners[selectedIndex] = tabListener;
  151
+    filter.addProgressListener(tabListener, Ci.nsIWebProgress.NOTIFY_ALL);
  152
+    targetBrowser.webProgress.addProgressListener(filter, Ci.nsIWebProgress.NOTIFY_ALL);
  153
+  };
  154
+
  155
+  unload(function() {
  156
+    prefetcher.parentNode.removeChild(prefetcher);
  157
+  });
  158
+
  159
+  // Prevent errors from browser.js/xul when it gets unexpected title changes
  160
+  prefetcher.addEventListener("DOMTitleChanged", function(event) {
  161
+    event.stopPropagation();
  162
+  }, true);
  163
+
  164
+  // Add an area to show a list of categories
  165
+  let categoryBox = createNode("hbox");
  166
+  urlbarStack.appendChild(categoryBox);
  167
+
  168
+  categoryBox.setAttribute("flex", 1);
  169
+  categoryBox.setAttribute("pack", "end");
  170
+
  171
+  categoryBox.style.cursor = "text";
  172
+  categoryBox.style.overflow = "hidden";
  173
+
  174
+  // Activate a category with an optional provider index
  175
+  categoryBox.activate = function(categoryLabel, index) {
  176
+    // Cycle through providers when re-activating the same category
  177
+    let {active} = categoryBox;
  178
+    if (active == categoryLabel) {
  179
+      let {defaultIndex, providers} = active.categoryData;
  180
+      index = (defaultIndex + 1) % providers.length;
  181
+    }
  182
+    else {
  183
+      // Keep track of the original text values
  184
+      let {selectionEnd, selectionStart, value} = hdInput;
  185
+
  186
+      // Remove any active query terms when activating another
  187
+      let query = value;
  188
+      if (categoryBox.active != goCategory)
  189
+        query = query.replace(/^[^:]+:\s*/, "");
  190
+
  191
+      // Update the text with the active keyword
  192
+      let {keyword} = categoryLabel.categoryData;
  193
+      let shortQuery = query.slice(0, selectionStart);
  194
+      if (keyword == "")
  195
+        hdInput.value = query;
  196
+      // Use the partially typed short keyword
  197
+      else if (selectionStart > 0 && shortQuery == keyword.slice(0, selectionStart))
  198
+        hdInput.value = shortQuery + ": " + query.slice(selectionStart);
  199
+      // Insert the full keyword
  200
+      else
  201
+        hdInput.value = keyword + query;
  202
+
  203
+      // Move the cursor to its original position
  204
+      let newLen = hdInput.value.length;
  205
+      let origLen = value.length;
  206
+      hdInput.selectionStart = newLen + selectionStart - origLen;
  207
+      hdInput.selectionEnd = newLen + selectionEnd - origLen;
  208
+    }
  209
+
  210
+    // Switch to a particular provider if necessary
  211
+    if (index != null)
  212
+      categoryLabel.categoryData.defaultIndex = index;
  213
+
  214
+    // Update the autocomplete results now that we've activated
  215
+    categoryBox.processInput();
  216
+    gURLBar.mController.handleText();
  217
+    hdInput.focus();
  218
+  };
  219
+
  220
+  // Look through the input to decide what category could be activated
  221
+  categoryBox.maybeHighlight = function() {
  222
+    categoryBox.highlight = null;
  223
+
  224
+    // See if there's any potential categories to highlight
  225
+    let {selectionStart, value} = hdInput;
  226
+    let shortValue = value.slice(0, selectionStart);
  227
+    let {length} = shortValue;
  228
+    if (length > 0) {
  229
+      Array.some(categoryBox.childNodes, function(label) {
  230
+        let {categoryData} = label;
  231
+        if (categoryData == null)
  232
+          return;
  233
+        let {keyword} = categoryData;
  234
+        if (keyword == "")
  235
+          return;
  236
+        if (shortValue == keyword.slice(0, length)) {
  237
+          categoryBox.highlight = label;
  238
+          return true;
  239
+        }
  240
+      });
  241
+    }
  242
+  };
  243
+
  244
+  // Figure out if the current input text is activating a category
  245
+  categoryBox.processInput = function() {
  246
+    // Figure out what's the active category based on the input
  247
+    let {value} = hdInput;
  248
+    let inputQuery = value;
  249
+    let inputParts = value.match(/^([^:]*):\s*(.*?)$/);
  250
+    categoryBox.active = goCategory;
  251
+    if (inputParts != null) {
  252
+      let inputKeyword = inputParts[1];
  253
+      Array.some(categoryBox.childNodes, function(label) {
  254
+        let {categoryData} = label;
  255
+        if (categoryData == null)
  256
+          return;
  257
+        let {keyword} = categoryData;
  258
+        if (keyword == "")
  259
+          return;
  260
+        if (inputKeyword == keyword.slice(0, inputKeyword.length)) {
  261
+          categoryBox.active = label;
  262
+          inputQuery = inputParts[2];
  263
+          return true;
  264
+        }
  265
+      });
  266
+    }
  267
+
  268
+    // Update the UI now that we've figured out various states
  269
+    categoryBox.maybeHighlight();
  270
+    categoryBox.updateLook();
  271
+
  272
+    // Convert the input into a url for the location bar
  273
+    let {active} = categoryBox;
  274
+    let {defaultIndex, providers} = active.categoryData;
  275
+    let url = providers[defaultIndex].url;
  276
+    const termRegex = /{search(.)terms}/;
  277
+    let spaceChar = url.match(termRegex)[1];
  278
+    url = url.replace(termRegex, inputQuery.replace(/ /g, spaceChar));
  279
+    gURLBar.value = url;
  280
+
  281
+    // Prefetch the search results if not going to a page
  282
+    if (active != goCategory)
  283
+      prefetcher.loadURI(url)
  284
+
  285
+    // Only show results for going to a history page
  286
+    gURLBar.popup.collapsed = active != goCategory;
  287
+
  288
+    // Save the input value to restore later if necessary
  289
+    gBrowser.selectedTab.HDinput = value;
  290
+  };
  291
+
  292
+  // Clear out various state of the current input
  293
+  categoryBox.reset = function() {
  294
+    categoryBox.active = null;
  295
+    categoryBox.highlight = null;
  296
+    categoryBox.hover = null;
  297
+    categoryBox.updateLook();
  298
+  };
  299
+
  300
+  // Differently color certain categories depending on state
  301
+  categoryBox.updateLook = function() {
  302
+    // Restore some UI like the identity box
  303
+    let {active, highlight, hover} = categoryBox;
  304
+    if (active == null) {
  305
+      gBrowser.selectedTab.HDinput = "";
  306
+      hdInput.value = "";
  307
+      origIdentity.hidden = false;
  308
+      iconBox.hidden = true;
  309
+    }
  310
+    // Prepare the UI for showing an active category
  311
+    else {
  312
+      origIdentity.hidden = true;
  313
+      iconBox.hidden = false;
  314
+
  315
+      let {defaultIndex, providers} = active.categoryData;
  316
+      let {icon} = providers[defaultIndex];
  317
+      if (icon == null)
  318
+        providerIcon.removeAttribute("src");
  319
+      else
  320
+        providerIcon.setAttribute("src", icon);
  321
+    }
  322
+
  323
+    // Go through each label and style it appropriately
  324
+    let doActive = gURLBar.hasAttribute("focused") || hdInput.value != "";
  325
+    Array.forEach(categoryBox.childNodes, function(label) {
  326
+      let color = "#999";
  327
+      if (label == active && doActive)
  328
+        color = "#090";
  329
+      else if (label == highlight || label == hover)
  330
+        color = "#00f";
  331
+      label.style.color = color;
  332
+
  333
+      label.style.textDecoration = label == hover ? "underline" : "";
  334
+    });
  335
+  };
  336
+
  337
+  // Pointing away removes the go category highlight
  338
+  categoryBox.addEventListener("mouseout", function(event) {
  339
+    if (event.target != categoryBox)
  340
+      return;
  341
+    if (gURLBar.hasAttribute("focused"))
  342
+      return;
  343
+    categoryBox.highlight = null;
  344
+    categoryBox.updateLook();
  345
+  }, false);
  346
+
  347
+  // Indicate the default behavior of a click is go
  348
+  categoryBox.addEventListener("mouseover", function(event) {
  349
+    if (event.target != categoryBox)
  350
+      return;
  351
+    if (gURLBar.hasAttribute("focused"))
  352
+      return;
  353
+    categoryBox.highlight = goCategory;
  354
+    categoryBox.updateLook();
  355
+  }, false);
  356
+
  357
+  // Select the text to edit for a website
  358
+  categoryBox.addEventListener("click", function(event) {
  359
+    if (event.target != categoryBox && event.target != goCategory)
  360
+      return;
  361
+    hdInput.focus();
  362
+    hdInput.select();
  363
+  }, false);
  364
+
  365
+  // Helper to add a category or comma
  366
+  function addLabel(text) {
  367
+    let label = createNode("label");
  368
+    categoryBox.appendChild(label);
  369
+
  370
+    label.setAttribute("value", text);
  371
+    label.style.margin = 0;
  372
+
  373
+    return label;
  374
+  }
  375
+
  376
+  // Create a category label
  377
+  function addCategory(categoryData) {
  378
+    let {category, keyword, providers, text} = categoryData;
  379
+
  380
+    let label = addLabel(text);
  381
+    label.categoryData = categoryData;
  382
+
  383
+    label.style.cursor = "pointer";
  384
+
  385
+    // For context-less, activate on plain click
  386
+    label.addEventListener("click", function() {
  387
+      categoryBox.activate(label);
  388
+    }, false);
  389
+
  390
+    // Handle the mouse moving in or out of the related labels
  391
+    function onMouse({type, relatedTarget}) {
  392
+      // Ignore events between the two related labels
  393
+      if (relatedTarget == label || relatedTarget == comma)
  394
+        return;
  395
+
  396
+      let hovering = type == "mouseover";
  397
+      categoryBox.hover = hovering ? label : null;
  398
+      categoryBox.updateLook();
  399
+
  400
+      if (!hovering)
  401
+        return;
  402
+
  403
+      if (context.state == "open")
  404
+        return;
  405
+      if (category == "go")
  406
+        return;
  407
+
  408
+      context.updateChecked();
  409
+      context.openPopup(label, "after_start");
  410
+    }
  411
+
  412
+    label.addEventListener("mouseout", onMouse, false);
  413
+    label.addEventListener("mouseover", onMouse, false);
  414
+
  415
+    // Add a comma after each category
  416
+    let comma = addLabel(", ");
  417
+    comma.addEventListener("mouseout", onMouse, false);
  418
+    comma.addEventListener("mouseover", onMouse, false);
  419
+
  420
+    // Prepare a popup to show category providers
  421
+    let context = createNode("menupopup");
  422
+    document.getElementById("mainPopupSet").appendChild(context);
  423
+
  424
+    // Add a menuitem that knows how to switch to the provider
  425
+    providers.forEach(function({icon, name}, index) {
  426
+      let provider = createNode("menuitem");
  427
+      provider.setAttribute("class", "menuitem-iconic");
  428
+      provider.setAttribute("image", icon);
  429
+      provider.setAttribute("label", name);
  430
+      context.appendChild(provider);
  431
+
  432
+      provider.addEventListener("command", function() {
  433
+        categoryBox.activate(label, index);
  434
+      }, false);
  435
+
  436
+      return provider;
  437
+    });
  438
+
  439
+    // Correctly mark which item is the default
  440
+    context.updateChecked = function() {
  441
+      let {defaultIndex} = categoryData;
  442
+      Array.forEach(context.childNodes, function(item, index) {
  443
+        if (index == defaultIndex)
  444
+          item.setAttribute("checked", true);
  445
+        else
  446
+          item.removeAttribute("checked");
  447
+      });
  448
+    };
  449
+
  450
+    context.updateChecked();
  451
+
  452
+    unload(function() {
  453
+      context.parentNode.removeChild(context);
  454
+    });
  455
+
  456
+    // Track when the menu disappears to maybe activate
  457
+    let unOver;
  458
+    context.addEventListener("popuphiding", function() {
  459
+      unOver();
  460
+      categoryBox.processInput();
  461
+      categoryBox.updateLook();
  462
+
  463
+      // Assume dismiss of the popup by clicking on the label is to activate
  464
+      // Windows sends both popuphiding and click events, so ignore this one
  465
+      if (!isWin && categoryBox.hover == label)
  466
+        categoryBox.activate(label);
  467
+    }, false);
  468
+
  469
+    // Keep the category highlighted and prepare to dismiss
  470
+    context.addEventListener("popupshowing", function() {
  471
+      categoryBox.highlight = label;
  472
+      categoryBox.updateLook();
  473
+
  474
+      // Automatically hide the popup when pointing away
  475
+      unOver = listen(window, window, "mouseover", function(event) {
  476
+        // Allow pointing at the category label
  477
+        switch (event.originalTarget) {
  478
+          case label:
  479
+          case comma:
  480
+            return;
  481
+        }
  482
+
  483
+        // Allow pointing at the menu
  484
+        let {target} = event;
  485
+        if (target == context)
  486
+          return;
  487
+
  488
+        // And the menu items
  489
+        if (target.parentNode == context)
  490
+          return;
  491
+
  492
+        // Must have pointed away allowed items, so dismiss
  493
+        context.hidePopup();
  494
+      });
  495
+    }, false);
  496
+
  497
+    return label;
  498
+  }
  499
+
  500
+  // Add each category to the UI and remember some special categories
  501
+  allProviders.forEach(addCategory);
  502
+  let goCategory = categoryBox.firstChild;
  503
+  let searchCategory = goCategory.nextSibling.nextSibling;
  504
+  categoryBox.removeChild(categoryBox.lastChild);
  505
+
  506
+  // Copy most of the original input field
  507
+  let hdInput = origInput.cloneNode(false);
  508
+  urlbarStack.appendChild(hdInput);
  509
+
  510
+  // Hide the original input
  511
+  change(origInput.style, "maxWidth", 0);
  512
+  change(origInput.style, "overflow", "hidden");
  513
+
  514
+  hdInput.removeAttribute("onblur");
  515
+  hdInput.removeAttribute("onfocus");
  516
+  hdInput.removeAttribute("placeholder");
  517
+
  518
+  hdInput.style.pointerEvents = "none";
  519
+
  520
+  // Use white shadows to cover up the category text
  521
+  let (shadow = []) {
  522
+    for (let i = -10; i <= 30; i += 5)
  523
+      for (let j = -6; j <= 3; j += 3)
  524
+        shadow.push(i + "px " + j + "px 5px white");
  525
+    hdInput.style.textShadow = shadow.join(", ");
  526
+  }
  527
+
  528
+  hdInput.addEventListener("blur", function() {
  529
+    categoryBox.updateLook();
  530
+  }, false);
  531
+
  532
+  hdInput.addEventListener("focus", function() {
  533
+    gURLBar.setAttribute("focused", true);
  534
+    categoryBox.processInput();
  535
+  }, false);
  536
+
  537
+  hdInput.addEventListener("input", function() {
  538
+    categoryBox.processInput();
  539
+  }, false);
  540
+
  541
+  // Allow escaping out of the input
  542
+  hdInput.addEventListener("keydown", function(event) {
  543
+    if (event.keyCode != event.DOM_VK_ESCAPE)
  544
+      return;
  545
+
  546
+    // Return focus to the browser if already empty
  547
+    if (hdInput.value == "")
  548
+      gBrowser.selectedBrowser.focus();
  549
+    // Empty out the input on first escape
  550
+    else {
  551
+      hdInput.value = "";
  552
+      categoryBox.processInput();
  553
+    }
  554
+  }, false);
  555
+
  556
+  // Update what gets highlighted when moving the cursor
  557
+  hdInput.addEventListener("keyup", function(event) {
  558
+    switch (event.keyCode) {
  559
+      case event.DOM_VK_LEFT:
  560
+      case event.DOM_VK_RIGHT:
  561
+        categoryBox.maybeHighlight();
  562
+        categoryBox.updateLook();
  563
+        break;
  564
+    }
  565
+  }, false);
  566
+
  567
+  // Detect tab switches to restore previous input
  568
+  listen(window, gBrowser.tabContainer, "TabSelect", function() {
  569
+    hdInput.value = gBrowser.selectedTab.HDinput || "";
  570
+    categoryBox.processInput();
  571
+  });
  572
+
  573
+  // Allow tab completion to activate
  574
+  listen(window, gURLBar.parentNode, "keypress", function(event) {
  575
+    if (event.keyCode != event.DOM_VK_TAB)
  576
+      return;
  577
+
  578
+    let {active, highlight} = categoryBox;
  579
+    if (active != goCategory)
  580
+      categoryBox.activate(active);
  581
+    else if (highlight != null)
  582
+      categoryBox.activate(highlight);
  583
+
  584
+    event.preventDefault();
  585
+    event.stopPropagation();
  586
+  });
  587
+
  588
+  // Activate the go category when dismissing the autocomplete results
  589
+  listen(window, gURLBar.popup, "popuphiding", function() {
  590
+    if (categoryBox.hover == goCategory)
  591
+      categoryBox.activate(goCategory);
  592
+  });
  593
+
  594
+  // Redirect focus from the original input to the new one
  595
+  listen(window, origInput, "focus", function(event) {
  596
+    origInput.blur();
  597
+    hdInput.focus();
  598
+  }, false);
  599
+
  600
+  // Hook into the user selecting a result
  601
+  change(gURLBar, "handleCommand", function(orig) {
  602
+    return function(event) {
  603
+      let isGo = categoryBox.active == goCategory;
  604
+      categoryBox.reset();
  605
+
  606
+      // Just load the page into the current tab
  607
+      if (isGo)
  608
+        return orig.call(this, event);
  609
+
  610
+      // Reuse the current tab if it's empty
  611
+      let targetTab = gBrowser.selectedTab;
  612
+      if (!window.isTabEmpty(targetTab))
  613
+        targetTab = gBrowser.addTab();
  614
+
  615
+      prefetcher.persistTo(targetTab);
  616
+      gBrowser.selectedTab = targetTab;
  617
+    };
  618
+  });
  619
+
  620
+  // Catch various existing browser commands to redirect to the dashboard
  621
+  let commandSet = document.getElementById("mainCommandSet");
  622
+  let commandWatcher = function(event) {
  623
+    // Figure out if it's a command we're stealing
  624
+    switch (event.target.id) {
  625
+      case "Browser:OpenLocation":
  626
+        // For power users, allow getting the current tab's location when empty
  627
+        if (hdInput.value == "") {
  628
+          let url = gBrowser.selectedBrowser.currentURI.spec;
  629
+          if (url != "about:blank")
  630
+            hdInput.value = url;
  631
+        }
  632
+
  633
+        hdInput.focus();
  634
+        hdInput.select();
  635
+        break;
  636
+
  637
+      case "Tools:Search":
  638
+        categoryBox.activate(searchCategory);
  639
+        break;
  640
+
  641
+      // Not something we care about, so nothing to do!
  642
+      default:
  643
+        return;
  644
+    }
  645
+
  646
+    // Prevent the original command from triggering
  647
+    event.stopPropagation();
  648
+  };
  649
+  commandSet.addEventListener("command", commandWatcher, true);
  650
+  unload(function() {
  651
+    commandSet.removeEventListener("command", commandWatcher, true);
  652
+  }, window);
  653
+
  654
+  // Always make the star visible to prevent text shifting
  655
+  let star = document.getElementById("star-button");
  656
+  star.setAttribute("style", "visibility: visible;");
  657
+  unload(function() {
  658
+    star.removeAttribute("style");
  659
+  });
  660
+
  661
+  // Remove the search bar when loading
  662
+  change(document.getElementById("search-container"), "hidden", true);
  663
+
  664
+  // Make sure the identity box is visible
  665
+  unload(function() {
  666
+    origIdentity.hidden = false;
  667
+  });
  668
+
  669
+  // Prepare the category box for first action!
  670
+  categoryBox.reset();
  671
+}
  672
+
  673
+/**
  674
+ * Handle the add-on being activated on install/enable
  675
+ */
  676
+function startup({id}) AddonManager.getAddonByID(id, function(addon) {
  677
+  // Load various javascript includes for helper functions
  678
+  ["helper", "providers", "utils"].forEach(function(fileName) {
  679
+    let fileURI = addon.getResourceURI("scripts/" + fileName + ".js");
  680
+    Services.scriptloader.loadSubScript(fileURI.spec, global);
  681
+  });
  682
+
  683
+  // Load in the provider data from preferences
  684
+  try {
  685
+    allProviders = JSON.parse(prefs.get("providers"));
  686
+  }
  687
+  catch(ex) {
  688
+    // Restore provider data with hardcoded defaults
  689
+    let categories = {};
  690
+    allProviders = [];
  691
+    PROVIDER_DATA.forEach(function([category, name, url, icon]) {
  692
+      // Add a new category and initialize with the current item
  693
+      let providers = categories[category];
  694
+      if (providers == null) {
  695
+        providers = categories[category] = [];
  696
+        allProviders.push({
  697
+          category: category,
  698
+          defaultIndex: 0,
  699
+          keyword: category == "go" ? "" : category + ": ",
  700
+          providers: providers,
  701
+          text: category == "go" ? "Go to a website" : category == "search" ? "search the web" : category,
  702
+        });
  703
+      }
  704
+
  705
+      // Save information about this provider for the category
  706
+      providers.push({
  707
+        icon: icon,
  708
+        name: name,
  709
+        url: url,
  710
+      });
  711
+    });
  712
+  }
  713
+
  714
+  // Combine location and search!
  715
+  watchWindows(addAwesomeBarHD);
  716
+})
  717
+
  718
+
  719
+/**
  720
+ * Handle the add-on being deactivated on uninstall/disable
  721
+ */
  722
+function shutdown(data, reason) {
  723
+  // Clean up with unloaders when we're deactivating
  724
+  if (reason != APP_SHUTDOWN)
  725
+    unload();
  726
+
  727
+  // Persist data across restarts and disables
  728
+  prefs.set("providers", JSON.stringify(allProviders));
  729
+}
  730
+
  731
+/**
  732
+ * Handle the add-on being installed
  733
+ */
  734
+function install(data, reason) {}
  735
+
  736
+/**
  737
+ * Handle the add-on being uninstalled
  738
+ */
  739
+function uninstall(data, reason) {
  740
+  // Clear out any persisted data when the user gets rid of the add-on
  741
+  if (reason == ADDON_UNINSTALL)
  742
+    prefs.resetBranch("");
  743
+}
24  awesomeBarHD/install.rdf
... ...
@@ -0,0 +1,24 @@
  1
+<?xml version="1.0" encoding="utf-8"?>
  2
+<r:RDF xmlns="http://www.mozilla.org/2004/em-rdf#"
  3
+       xmlns:r="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  4
+  <r:Description about="urn:mozilla:install-manifest">
  5
+    <creator>Mozilla Labs</creator>
  6
+    <description>Find pages from your history, on a the web, for a particular category.</description>
  7
+    <homepageURL>https://mozillalabs.com/prospector</homepageURL>
  8
+    <iconURL>http://mozillalabs.com/wp-content/themes/labs_project/img/prospector-header.png</iconURL>
  9
+    <id>awesomeBar.HD@prospector.labs.mozilla</id>
  10
+    <name>Mozilla Labs: Prospector - AwesomeBar HD</name>
  11
+    <version>1</version>
  12
+
  13
+    <bootstrap>true</bootstrap>
  14
+    <type>2</type>
  15
+
  16
+    <targetApplication>
  17
+      <r:Description>
  18
+        <id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</id>
  19
+        <minVersion>4.0</minVersion>
  20
+        <maxVersion>6.0a1</maxVersion>
  21
+      </r:Description>
  22
+    </targetApplication>
  23
+  </r:Description>
  24
+</r:RDF>
154  awesomeBarHD/scripts/helper.js
... ...
@@ -0,0 +1,154 @@
  1
+/* ***** BEGIN LICENSE BLOCK *****
  2
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3
+ *
  4
+ * The contents of this file are subject to the Mozilla Public License Version
  5
+ * 1.1 (the "License"); you may not use this file except in compliance with
  6
+ * the License. You may obtain a copy of the License at
  7
+ * http://www.mozilla.org/MPL/
  8
+ *
  9
+ * Software distributed under the License is distributed on an "AS IS" basis,
  10
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11
+ * for the specific language governing rights and limitations under the
  12
+ * License.
  13
+ *
  14
+ * The Original Code is Home Dash Helper Functions.
  15
+ *
  16
+ * The Initial Developer of the Original Code is The Mozilla Foundation.
  17
+ * Portions created by the Initial Developer are Copyright (C) 2011
  18
+ * the Initial Developer. All Rights Reserved.
  19
+ *
  20
+ * Contributor(s):
  21
+ *   Edward Lee <edilee@mozilla.com>
  22
+ *
  23
+ * Alternatively, the contents of this file may be used under the terms of
  24
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
  25
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26
+ * in which case the provisions of the GPL or the LGPL are applicable instead
  27
+ * of those above. If you wish to allow use of your version of this file only
  28
+ * under the terms of either the GPL or the LGPL, and not to allow others to
  29
+ * use your version of this file under the terms of the MPL, indicate your
  30
+ * decision by deleting the provisions above and replace them with the notice
  31
+ * and other provisions required by the GPL or the LGPL. If you do not delete
  32
+ * the provisions above, a recipient may use your version of this file under
  33
+ * the terms of any one of the MPL, the GPL or the LGPL.
  34
+ *
  35
+ * ***** END LICENSE BLOCK ***** */
  36
+
  37
+"use strict";
  38
+
  39
+const isMac = Services.appinfo.OS == "Darwin";
  40
+const isWin = Services.appinfo.OS == "WINNT";
  41
+
  42
+// Take a window and create various helper properties and functions
  43
+function makeWindowHelpers(window) {
  44
+  const XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
  45
+
  46
+  let {document, clearTimeout, gBrowser, setTimeout} = window;
  47
+
  48
+  // Call a function after waiting a little bit
  49
+  function async(callback, delay) {
  50
+    let timer = setTimeout(function() {
  51
+      stopTimer();
  52
+      callback();
  53
+    }, delay);
  54
+
  55
+    // Provide a way to stop an active timer
  56
+    function stopTimer() {
  57
+      if (timer == null)
  58
+        return;
  59
+      clearTimeout(timer);
  60
+      timer = null;
  61
+      unUnload();
  62
+    }
  63
+
  64
+    // Make sure to stop the timer when unloading
  65
+    let unUnload = unload(stopTimer, window);
  66
+
  67
+    // Give the caller a way to cancel the timer
  68
+    return stopTimer;
  69
+  }
  70
+
  71
+  // Replace a value with another value or a function of the original value
  72
+  function change(obj, prop, val) {
  73
+    let orig = obj[prop];
  74
+    obj[prop] = typeof val == "function" ? val(orig) : val;
  75
+    unload(function() obj[prop] = orig, window);
  76
+  }
  77
+
  78
+  // Create a XUL node that can get some extra functionality
  79
+  function createNode(nodeName, extend) {
  80
+    let node = document.createElementNS(XUL, nodeName);
  81
+
  82
+    // Only extend certain top-level nodes that want to be
  83
+    if (!extend)
  84
+      return node;
  85
+
  86
+    // Make a delayable function that uses a sharable timer
  87
+    let makeDelayable = function(timerName, func) {
  88
+      timerName += "Timer";
  89
+      return function(arg) {
  90
+        // Stop the shared timer if it's still waiting
  91
+        if (node[timerName] != null)
  92
+          node[timerName]();
  93
+
  94
+        // Pick out the arguments that the function wants
  95
+        let numArgs = func.length;
  96
+        let args;
  97
+        if (numArgs > 1)
  98
+          args = Array.slice(arguments, 0, func.length);
  99
+        function callFunc() {
  100
+          node[timerName] = null;
  101
+          if (numArgs == 0)
  102
+            func.call(global);
  103
+          else if (numArgs == 1)
  104
+            func.call(global, arg);
  105
+          else
  106
+            func.apply(global, args);
  107
+        }
  108
+
  109
+        // If we have some amount of time to wait, wait
  110
+        let delay = arguments[func.length];
  111
+        if (delay)
  112
+          node[timerName] = async(callFunc, delay);
  113
+        // Otherwise do it synchronously
  114
+        else {
  115
+          callFunc();
  116
+          node[timerName] = null;
  117
+        }
  118
+      };
  119
+    }
  120
+
  121
+    // Allow this node to be collapsed with a delay
  122
+    let slowHide = makeDelayable("showHide", function() node.collapsed = true);
  123
+    node.hide = function() {
  124
+      shown = false;
  125
+      slowHide.apply(global, arguments);
  126
+    };
  127
+
  128
+    // Set the opacity after a delay
  129
+    node.setOpacity = makeDelayable("opacity", function(val) {
  130
+      node.style.opacity = val;
  131
+    });
  132
+
  133
+    // Allow this node to be uncollapsed with a delay
  134
+    let slowShow  = makeDelayable("showHide", function() node.collapsed = false);
  135
+    node.show = function() {
  136
+      shown = true;
  137
+      slowShow.apply(global, arguments);
  138
+    };
  139
+
  140
+    // Indicate if the node should be shown even if it isn't visible yet
  141
+    let shown = true;
  142
+    Object.defineProperty(node, "shown", {
  143
+      get: function() shown
  144
+    });
  145
+
  146
+    return node;
  147
+  }
  148
+
  149
+  return {
  150
+    async: async,
  151
+    change: change,
  152
+    createNode: createNode,
  153
+  };
  154
+}
117  awesomeBarHD/scripts/providers.js
117 additions, 0 deletions not shown
310  awesomeBarHD/scripts/utils.js
... ...
@@ -0,0 +1,310 @@
  1
+/* ***** BEGIN LICENSE BLOCK *****
  2
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3
+ *
  4
+ * The contents of this file are subject to the Mozilla Public License Version
  5
+ * 1.1 (the "License"); you may not use this file except in compliance with
  6
+ * the License. You may obtain a copy of the License at
  7
+ * http://www.mozilla.org/MPL/
  8
+ *
  9
+ * Software distributed under the License is distributed on an "AS IS" basis,
  10
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11
+ * for the specific language governing rights and limitations under the
  12
+ * License.
  13
+ *
  14
+ * The Original Code is Home Dash Utility.
  15
+ *
  16
+ * The Initial Developer of the Original Code is The Mozilla Foundation.
  17
+ * Portions created by the Initial Developer are Copyright (C) 2011
  18
+ * the Initial Developer. All Rights Reserved.
  19
+ *
  20
+ * Contributor(s):
  21
+ *   Edward Lee <edilee@mozilla.com>
  22
+ *
  23
+ * Alternatively, the contents of this file may be used under the terms of
  24
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
  25
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  26
+ * in which case the provisions of the GPL or the LGPL are applicable instead
  27
+ * of those above. If you wish to allow use of your version of this file only
  28
+ * under the terms of either the GPL or the LGPL, and not to allow others to
  29
+ * use your version of this file under the terms of the MPL, indicate your
  30
+ * decision by deleting the provisions above and replace them with the notice
  31
+ * and other provisions required by the GPL or the LGPL. If you do not delete
  32
+ * the provisions above, a recipient may use your version of this file under
  33
+ * the terms of any one of the MPL, the GPL or the LGPL.
  34
+ *
  35
+ * ***** END LICENSE BLOCK ***** */
  36
+
  37
+"use strict";
  38
+
  39
+/**
  40
+ * Get a localized string with string replacement arguments filled in and
  41
+ * correct plural form picked if necessary.
  42
+ *
  43
+ * @note: Initialize the strings to use with getString.init(addon).
  44
+ *
  45
+ * @usage getString(name): Get the localized string for the given name.
  46
+ * @param [string] name: Corresponding string name in the properties file.
  47
+ * @return [string]: Localized string for the string name.
  48
+ *
  49
+ * @usage getString(name, arg): Replace %S references in the localized string.
  50
+ * @param [string] name: Corresponding string name in the properties file.
  51
+ * @param [any] arg: Value to insert for instances of %S.
  52
+ * @return [string]: Localized string with %S references replaced.
  53
+ *
  54
+ * @usage getString(name, args): Replace %1$S references in localized string.
  55
+ * @param [string] name: Corresponding string name in the properties file.
  56
+ * @param [array of any] args: Array of values to replace references like %1$S.
  57
+ * @return [string]: Localized string with %N$S references replaced.
  58
+ *
  59
+ * @usage getString(name, args, plural): Pick the correct plural form.
  60
+ * @param [string] name: Corresponding string name in the properties file.
  61
+ * @param [array of any] args: Array of values to replace references like %1$S.
  62
+ * @param [number] plural: Number to decide what plural form to use.
  63
+ * @return [string]: Localized string of the correct plural form.
  64
+ */
  65
+function getString(name, args, plural) {
  66
+  // Use the cached bundle to retrieve the string
  67
+  let str;
  68
+  try {
  69
+    str = getString.bundle.GetStringFromName(name);
  70
+  }
  71
+  // Use the fallback in-case the string isn't localized
  72
+  catch(ex) {
  73
+    str = getString.fallback.GetStringFromName(name);
  74
+  }
  75
+
  76
+  // Pick out the correct plural form if necessary
  77
+  if (plural != null)
  78
+    str = getString.plural(plural, str);
  79
+
  80
+  // Fill in the arguments if necessary
  81
+  if (args != null) {
  82
+    // Convert a string or something not array-like to an array
  83
+    if (typeof args == "string" || args.length == null)
  84
+      args = [args];
  85
+
  86
+    // Assume %S refers to the first argument
  87
+    str = str.replace(/%s/gi, args[0]);
  88
+
  89
+    // Replace instances of %N$S where N is a 1-based number
  90
+    Array.forEach(args, function(replacement, index) {
  91
+      str = str.replace(RegExp("%" + (index + 1) + "\\$S", "gi"), replacement);
  92
+    });
  93
+  }
  94
+
  95
+  return str;
  96
+}
  97
+
  98
+/**
  99
+ * Initialize getString() for the provided add-on.
  100
+ *
  101
+ * @usage getString.init(addon): Load properties file for the add-on.
  102
+ * @param [object] addon: Add-on object from AddonManager
  103
+ *
  104
+ * @usage getString.init(addon, getAlternate): Load properties with alternate.
  105
+ * @param [object] addon: Add-on object from AddonManager
  106
+ * @param [function] getAlternate: Convert a locale to an alternate locale
  107
+ */
  108
+getString.init = function(addon, getAlternate) {
  109
+  // Set a default get alternate function if it doesn't exist
  110
+  if (typeof getAlternate != "function")
  111
+    getAlternate = function() "en-US";
  112
+
  113
+  // Get the bundled properties file for the app's locale
  114
+  function getBundle(locale) {
  115
+    let propertyPath = "locales/" + locale + ".properties";
  116
+    let propertyFile = addon.getResourceURI(propertyPath);
  117
+
  118
+    // Get a bundle and test if it's able to do simple things
  119
+    try {
  120
+      // Avoid caching issues by always getting a new file
  121
+      let uniqueFileSpec = propertyFile.spec + "#" + Math.random();
  122
+      let bundle = Services.strings.createBundle(uniqueFileSpec);
  123
+      bundle.getSimpleEnumeration();
  124
+      return bundle;
  125
+    }
  126
+    catch(ex) {}
  127
+
  128
+    // The locale must not exist, so give nothing
  129
+    return null;
  130
+  }
  131
+
  132
+  // Use the current locale or the alternate as the primary bundle
  133
+  let locale = Cc["@mozilla.org/chrome/chrome-registry;1"].
  134
+    getService(Ci.nsIXULChromeRegistry).getSelectedLocale("global");
  135
+  getString.bundle = getBundle(locale) || getBundle(getAlternate(locale));
  136
+
  137
+  // Create a fallback in-case a string is missing
  138
+  getString.fallback = getBundle("en-US");
  139
+
  140
+  // Get the appropriate plural form getter
  141
+  Cu.import("resource://gre/modules/PluralForm.jsm");
  142
+  let rule = getString("pluralRule");
  143
+  [getString.plural] = PluralForm.makeGetter(rule);
  144
+}
  145
+
  146
+/**
  147
+ * Create a trigger that allows adding callbacks by default then triggering all
  148
+ * of them.
  149
+ */
  150
+function makeTrigger() {
  151
+  let callbacks = [];
  152
+
  153
+  // Provide the main function to add callbacks that can be removed
  154
+  function addCallback(callback) {
  155
+    callbacks.push(callback);
  156
+    return function() {
  157
+      let index = callbacks.indexOf(callback);
  158
+      if (index != -1)
  159
+        callbacks.splice(index, 1);
  160
+    };
  161
+  }
  162
+
  163
+  // Provide a way to clear out all the callbacks
  164
+  addCallback.reset = function() {
  165
+    callbacks.length = 0;
  166
+  };
  167
+
  168
+  // Run each callback in order ignoring failures
  169
+  addCallback.trigger = function(reason) {
  170
+    callbacks.slice().forEach(function(callback) {
  171
+      try {
  172
+        callback(reason);
  173
+      }
  174
+      catch(ex) {}
  175
+    });
  176
+  };
  177
+
  178
+  return addCallback;
  179
+}
  180
+
  181
+/**
  182
+ * Apply a callback to each open and new browser windows.
  183
+ *
  184
+ * @usage watchWindows(callback): Apply a callback to each browser window.
  185
+ * @param [function] callback: 1-parameter function that gets a browser window.
  186
+ */
  187
+function watchWindows(callback) {
  188
+  // Wrap the callback in a function that ignores failures
  189
+  function watcher(window) {
  190
+    try {
  191
+      callback(window);
  192
+    }
  193
+    catch(ex) {}
  194
+  }
  195
+
  196
+  // Wait for the window to finish loading before running the callback
  197
+  function runOnLoad(window) {
  198
+    // Listen for one load event before checking the window type
  199
+    window.addEventListener("load", function runOnce() {
  200
+      window.removeEventListener("load", runOnce, false);
  201
+
  202
+      // Now that the window has loaded, only handle browser windows
  203
+      let doc = window.document.documentElement;
  204
+      if (doc.getAttribute("windowtype") == "navigator:browser")
  205
+        watcher(window);
  206
+    }, false);
  207
+  }
  208
+
  209
+  // Add functionality to existing windows
  210
+  let browserWindows = Services.wm.getEnumerator("navigator:browser");
  211
+  while (browserWindows.hasMoreElements()) {
  212
+    // Only run the watcher immediately if the browser is completely loaded
  213
+    let browserWindow = browserWindows.getNext();
  214
+    if (browserWindow.document.readyState == "complete")
  215
+      watcher(browserWindow);
  216
+    // Wait for the window to load before continuing
  217
+    else
  218
+      runOnLoad(browserWindow);
  219
+  }
  220
+
  221
+  // Watch for new browser windows opening then wait for it to load
  222
+  function windowWatcher(subject, topic) {
  223
+    if (topic == "domwindowopened")
  224
+      runOnLoad(subject);
  225
+  }
  226
+  Services.ww.registerNotification(windowWatcher);
  227
+
  228
+  // Make sure to stop watching for windows if we're unloading
  229
+  unload(function() Services.ww.unregisterNotification(windowWatcher));
  230
+}
  231
+
  232
+/**
  233
+ * Save callbacks to run when unloading. Optionally scope the callback to a
  234
+ * container, e.g., window. Provide a way to run all the callbacks.
  235
+ *
  236
+ * @usage unload(): Run all callbacks and release them.