Bug 386228 - "Unify back and forward tab history and provide only one drop-down button (IE7 style)" [p=zeniko@gmail.com (Simon Bünzli) ui-r=beltzner r=gavin a=blocking-firefox3+]
authorreed@reedloden.com
Thu, 24 Jan 2008 02:52:05 -0800
changeset 10623 b21c1e599758a1361b40148091f57daf08c7087a
parent 10622 2a103284ea4fea8ebee0b2dcc685e96b41b0f6fa
child 10624 23f3e0b8846edda7f2a18688380696f97bf30159
push idunknown
push userunknown
push dateunknown
reviewersbeltzner, gavin, blocking-firefox3
bugs386228
milestone1.9b3pre
Bug 386228 - "Unify back and forward tab history and provide only one drop-down button (IE7 style)" [p=zeniko@gmail.com (Simon Bünzli) ui-r=beltzner r=gavin a=blocking-firefox3+]
browser/base/content/bindings.xml
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/jar.mn
browser/locales/en-US/chrome/browser/browser.properties
new file mode 100644
--- /dev/null
+++ b/browser/base/content/bindings.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0"?>
+
+# -*- Mode: HTML -*-
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org browser.
+#
+# The Initial Developer of the Original Code is
+# Simon Bünzli <zeniko@gmail.com>
+# Portions created by the Initial Developer are Copyright (C) 2007 - 2008
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either the GNU General Public License Version 2 or later (the "GPL"), or
+# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+<!DOCTYPE bindings SYSTEM "chrome://global/locale/global.dtd">
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+  xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+  xmlns:xbl="http://www.mozilla.org/xbl"
+>
+  <binding id="unified-back-forward-button-wrapper" display="xul:menu"
+           extends="chrome://global/content/bindings/toolbarbutton.xml#menu">
+    <content>
+      <children includes="toolbarbutton|observes|template|menupopup|tooltip"/>
+      <xul:dropmarker type="menu-button"
+        class="toolbarbutton-menubutton-dropmarker"
+        chromedir="&locale.dir;"
+        xbl:inherits="align,dir,pack,orient,disabled,toolbarmode,buttonstyle"
+      />
+    </content>
+
+    <implementation>
+      <constructor><![CDATA[
+        this._updateUnifiedState();
+      ]]></constructor>
+
+      <method name="_updateUnifiedState">
+        <body><![CDATA[
+          var canGoBack = !document.getElementById("Browser:Back").hasAttribute("disabled");
+          var canGoForward = !document.getElementById("Browser:Forward").hasAttribute("disabled");
+          
+          if (canGoBack || canGoForward)
+            this.removeAttribute("disabled");
+          else
+            this.setAttribute("disabled", "true");
+        ]]></body>
+      </method>
+    </implementation>
+
+    <handlers>
+      <!-- observing state changes of the child buttons -->
+      <handler event="broadcast" action="this._updateUnifiedState();"/>
+
+      <handler event="mousedown"><![CDATA[
+        // a click on the disabled half of the unified button opens the history drop-down
+        if (event.button == 0 && !this.hasAttribute("disabled") &&
+            event.originalTarget.hasAttribute("disabled"))
+          this.open = true;
+      ]]></handler>
+    </handlers>
+  </binding>
+</bindings>
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -18,16 +18,27 @@ toolbar[printpreview="true"] {
 #PopupAutoComplete {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup");
 }
 
 #PopupAutoCompleteRichResult {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup");
 }
 
+/* ::::: Unified Back-/Forward Button ::::: */
+#unified-back-forward-button {
+  -moz-binding: url("chrome://browser/content/bindings.xml#unified-back-forward-button-wrapper");
+}
+#unified-back-forward-button > toolbarbutton > dropmarker {
+  display: none; /* we provide our own */
+}
+.unified-nav-current {
+  font-weight: bold;
+}
+
 menuitem.spell-suggestion {
   font-weight: bold;
 }
 
 #sidebar-box toolbarbutton.tabs-closebutton {
   -moz-user-focus: normal;
 }
 
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -165,16 +165,55 @@ function UpdateBackForwardCommands(aWebN
   if (forwardDisabled == aWebNavigation.canGoForward) {
     if (forwardDisabled)
       forwardBroadcaster.removeAttribute("disabled");
     else
       forwardBroadcaster.setAttribute("disabled", true);
   }
 }
 
+var UnifiedBackForwardButton = {
+  unify: function() {
+    var backButton = document.getElementById("back-button");
+    if (!backButton || !backButton.nextSibling || backButton.nextSibling.id != "forward-button")
+      return; // back and forward buttons aren't adjacent
+
+    var wrapper = document.createElement("toolbaritem");
+    wrapper.id = "unified-back-forward-button";
+    wrapper.className = "chromeclass-toolbar-additional";
+    wrapper.setAttribute("context", "backMenu");
+    
+    var toolbar = backButton.parentNode;
+    toolbar.insertBefore(wrapper, backButton);
+    
+    var forwardButton = backButton.nextSibling;
+    wrapper.appendChild(backButton);
+    wrapper.appendChild(forwardButton);
+    
+    var popup = backButton.getElementsByTagName("menupopup")[0].cloneNode(true);
+    wrapper.appendChild(popup);
+    
+    this._unified = true;
+  },
+
+  separate: function() {
+    if (!this._unified)
+      return;
+    
+    var wrapper = document.getElementById("unified-back-forward-button");
+    var toolbar = wrapper.parentNode;
+    
+    toolbar.insertBefore(wrapper.firstChild, wrapper); // Back button
+    toolbar.insertBefore(wrapper.firstChild, wrapper); // Forward button
+    toolbar.removeChild(wrapper);
+    
+    this._unified = false;
+  }
+};
+
 #ifdef XP_MACOSX
 /**
  * Click-and-Hold implementation for the Back and Forward buttons
  * XXXmano: should this live in toolbarbutton.xml?
  */
 function ClickAndHoldMouseDownCallback(aButton)
 {
   aButton.open = true;
@@ -904,16 +943,17 @@ function delayedStartup()
   Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
 
   if (gMustLoadSidebar) {
     var sidebar = document.getElementById("sidebar");
     var sidebarBox = document.getElementById("sidebar-box");
     sidebar.setAttribute("src", sidebarBox.getAttribute("src"));
   }
 
+  UnifiedBackForwardButton.unify();
   UpdateUrlbarSearchSplitterState();
   
   try {
     placesMigrationTasks();
   } catch(ex) {}
   initBookmarksToolbar();
   PlacesStarButton.init();
 
@@ -1413,22 +1453,24 @@ function BrowserHandleShiftBackspace()
   case 1:
     goDoCommand("cmd_scrollPageDown");
     break;
   }
 }
 
 function BrowserBackMenu(event)
 {
-  return FillHistoryMenu(event.target, "back");
+  var menuType = UnifiedBackForwardButton._unified ? "unified" : "back";
+  return FillHistoryMenu(event.target, menuType);
 }
 
 function BrowserForwardMenu(event)
 {
-  return FillHistoryMenu(event.target, "forward");
+  var menuType = UnifiedBackForwardButton._unified ? "unified" : "forward";
+  return FillHistoryMenu(event.target, menuType);
 }
 
 function BrowserStop()
 {
   try {
     const stopFlags = nsIWebNavigation.STOP_ALL;
     getWebNavigation().stop(stopFlags);
   }
@@ -2889,45 +2931,77 @@ const BrowserSearch = {
 
 function FillHistoryMenu(aParent, aMenu)
   {
     // Remove old entries if any
     deleteHistoryItems(aParent);
 
     var webNav = getWebNavigation();
     var sessionHistory = webNav.sessionHistory;
+    var bundle_browser = document.getElementById("bundle_browser");
 
     var count = sessionHistory.count;
     var index = sessionHistory.index;
     var end;
     var j;
     var entry;
 
     switch (aMenu)
       {
         case "back":
           end = (index > MAX_HISTORY_MENU_ITEMS) ? index - MAX_HISTORY_MENU_ITEMS : 0;
           if ((index - 1) < end) return false;
           for (j = index - 1; j >= end; j--)
             {
               entry = sessionHistory.getEntryAtIndex(j, false);
               if (entry)
-                createMenuItem(aParent, j, entry.title);
+                createMenuItem(aParent, j, entry.title || entry.URI.spec,
+                               bundle_browser.getString("tabHistory.goBack"));
             }
           break;
         case "forward":
           end  = ((count-index) > MAX_HISTORY_MENU_ITEMS) ? index + MAX_HISTORY_MENU_ITEMS : count - 1;
           if ((index + 1) > end) return false;
           for (j = index + 1; j <= end; j++)
             {
               entry = sessionHistory.getEntryAtIndex(j, false);
               if (entry)
-                createMenuItem(aParent, j, entry.title);
+                createMenuItem(aParent, j, entry.title || entry.URI.spec,
+                               bundle_browser.getString("tabHistory.goForward"));
             }
           break;
+        case "unified":
+          if (count <= 1) // don't display the popup for a single item
+            return false;
+          
+          var half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2);
+          var start = Math.max(index - half_length, 0);
+          end = Math.min(start == 0 ? MAX_HISTORY_MENU_ITEMS : index + half_length + 1, count);
+          if (end == count)
+            start = Math.max(count - MAX_HISTORY_MENU_ITEMS, 0);
+          
+          var tooltips = [
+            bundle_browser.getString("tabHistory.goBack"),
+            bundle_browser.getString("tabHistory.current"),
+            bundle_browser.getString("tabHistory.goForward")
+          ];
+          var classNames = ["unified-nav-back", "unified-nav-current", "unified-nav-forward"];
+          
+          for (var j = end - 1; j >= start; j--) {
+            entry = sessionHistory.getEntryAtIndex(j, false);
+            var tooltip = tooltips[j < index ? 0 : j == index ? 1 : 2];
+            var className = classNames[j < index ? 0 : j == index ? 1 : 2];
+            var item = createMenuItem(aParent, j, entry.title || entry.URI.spec, tooltip, className);
+            
+            if (j == index) { // mark the current history item
+              item.setAttribute("type", "radio");
+              item.setAttribute("checked", "true");
+            }
+          }
+          break;
       }
 
     return true;
   }
 
 function addToUrlbarHistory(aUrlToAdd)
 {
   if (!aUrlToAdd)
@@ -2939,22 +3013,26 @@ function addToUrlbarHistory(aUrlToAdd)
      if (aUrlToAdd.indexOf(" ") == -1) {
        PlacesUtils.markPageAsTyped(aUrlToAdd);
      }
    }
    catch(ex) {
    }
 }
 
-function createMenuItem( aParent, aIndex, aLabel)
+function createMenuItem(aParent, aIndex, aLabel, aTooltipText, aClassName)
   {
     var menuitem = document.createElement( "menuitem" );
     menuitem.setAttribute( "label", aLabel );
     menuitem.setAttribute( "index", aIndex );
-    aParent.appendChild( menuitem );
+    if (aTooltipText)
+      menuitem.setAttribute("tooltiptext", aTooltipText);
+    if (aClassName)
+      menuitem.className = aClassName;
+    return aParent.appendChild(menuitem);
   }
 
 function deleteHistoryItems(aParent)
 {
   var children = aParent.childNodes;
   for (var i = children.length - 1; i >= 0; --i)
     {
       var index = children[i].getAttribute("index");
@@ -3037,16 +3115,18 @@ function BrowserCustomizeToolbar()
   // Disable the toolbar context menu items
   var menubar = document.getElementById("main-menubar");
   for (var i = 0; i < menubar.childNodes.length; ++i)
     menubar.childNodes[i].setAttribute("disabled", true);
 
   var cmd = document.getElementById("cmd_CustomizeToolbars");
   cmd.setAttribute("disabled", "true");
 
+  UnifiedBackForwardButton.separate();
+
   var splitter = document.getElementById("urlbar-search-splitter");
   if (splitter)
     splitter.parentNode.removeChild(splitter);
 
 #ifdef TOOLBAR_CUSTOMIZATION_SHEET
   var sheetFrame = document.getElementById("customizeToolbarSheetIFrame");
   sheetFrame.hidden = false;
   // XXXmano: there's apparently no better way to get this when the iframe is
@@ -3075,16 +3155,17 @@ function BrowserToolboxCustomizeDone(aTo
     gProxyButton = document.getElementById("page-proxy-button");
     gProxyFavIcon = document.getElementById("page-proxy-favicon");
     gProxyDeck = document.getElementById("page-proxy-deck");
     gHomeButton.updateTooltip();
     gIdentityHandler._cacheElements();
     window.XULBrowserWindow.init();
   }
 
+  UnifiedBackForwardButton.unify();
   UpdateUrlbarSearchSplitterState();
 
   // Update the urlbar
   if (gURLBar) {
     URLBarSetURI();
     XULBrowserWindow.asyncUpdateUI();
     PlacesStarButton.updateState();
   }
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -8,16 +8,17 @@ browser.jar:
 #endif
 %  overlay chrome://global/content/viewSource.xul chrome://browser/content/viewSourceOverlay.xul
 %  overlay chrome://global/content/viewPartialSource.xul chrome://browser/content/viewSourceOverlay.xul
 %  style chrome://global/content/customizeToolbar.xul chrome://browser/content/browser.css
 %  style chrome://global/content/customizeToolbar.xul chrome://browser/skin/
 *       content/browser/aboutDialog.xul               (content/aboutDialog.xul)
 *       content/browser/aboutDialog.js                (content/aboutDialog.js)
         content/browser/aboutDialog.css               (content/aboutDialog.css)
+*       content/browser/bindings.xml                  (content/bindings.xml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/credits.xhtml                 (content/credits.xhtml)
 *       content/browser/EULA.js                       (content/EULA.js)
 *       content/browser/EULA.xhtml                    (content/EULA.xhtml)
 *       content/browser/EULA.xul                      (content/EULA.xul)
 *       content/browser/metaData.js                   (content/metaData.js)
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -76,16 +76,21 @@ updatesItem_pendingFallback=Apply Downloaded Update Now…
 feedNoFeeds=Page has no feeds
 feedShowFeedNew=Subscribe to '%S'…
 feedHasFeedsNew=Subscribe to this page…
 
 # History menu
 menuOpenAllInTabs.label=Open All in Tabs
 menuOpenAllInTabs.accesskey=o
 
+# Unified Back-/Forward Popup
+tabHistory.current=Stay on this page
+tabHistory.goBack=Go back to this page
+tabHistory.goForward=Go forward to this page
+
 # Block autorefresh
 refreshBlocked.goButton=Allow
 refreshBlocked.goButton.accesskey=A
 refreshBlocked.refreshLabel=%S prevented this page from automatically reloading.
 refreshBlocked.redirectLabel=%S prevented this page from automatically redirecting to another page.
 
 # Star button
 starButtonOn.tooltip=Edit this bookmark