diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 1d2a67f81a03..bffd0ee97006 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1831,7 +1831,8 @@ function gotoHistoryIndex(aEvent) { } // Modified click. Go there in a new tab/window. - duplicateTabIn(gBrowser.selectedTab, where, index - gBrowser.sessionHistory.index); + let historyindex = aEvent.target.getAttribute("historyindex"); + duplicateTabIn(gBrowser.selectedTab, where, Number(historyindex)); return true; } @@ -3753,66 +3754,108 @@ function FillHistoryMenu(aParent) { } // Remove old entries if any - var children = aParent.childNodes; + let children = aParent.childNodes; for (var i = children.length - 1; i >= 0; --i) { if (children[i].hasAttribute("index")) aParent.removeChild(children[i]); } - var webNav = gBrowser.webNavigation; - var sessionHistory = webNav.sessionHistory; + const MAX_HISTORY_MENU_ITEMS = 15; - var count = sessionHistory.count; - if (count <= 1) // don't display the popup for a single item + const tooltipBack = gNavigatorBundle.getString("tabHistory.goBack"); + const tooltipCurrent = gNavigatorBundle.getString("tabHistory.current"); + const tooltipForward = gNavigatorBundle.getString("tabHistory.goForward"); + + function updateSessionHistory(sessionHistory, initial) + { + let count = sessionHistory.entries.length; + + if (!initial) { + if (count <= 1) { + // if there is only one entry now, close the popup. + aParent.hidePopup(); + return; + } else if (!aParent.parentNode.open) { + // if the popup wasn't open before, but now needs to be, reopen the menu. + // It should trigger FillHistoryMenu again. + aParent.parentNode.open = true; + return; + } + } + + let index = sessionHistory.index; + let half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2); + let start = Math.max(index - half_length, 0); + let 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); + } + + let existingIndex = 0; + + for (let j = end - 1; j >= start; j--) { + let entry = sessionHistory.entries[j]; + let uri = entry.url; + + let item = existingIndex < children.length ? + children[existingIndex] : document.createElement("menuitem"); + + let entryURI = BrowserUtils.makeURI(entry.url, entry.charset, null); + item.setAttribute("uri", uri); + item.setAttribute("label", entry.title || uri); + item.setAttribute("index", j); + + // Cache this so that gotoHistoryIndex doesn't need the original index + item.setAttribute("historyindex", j - index); + + if (j != index) { + PlacesUtils.favicons.getFaviconURLForPage(entryURI, function (aURI) { + if (aURI) { + let iconURL = PlacesUtils.favicons.getFaviconLinkForIcon(aURI).spec; + iconURL = PlacesUtils.getImageURLForResolution(window, iconURL); + item.style.listStyleImage = "url(" + iconURL + ")"; + } + }); + } + + if (j < index) { + item.className = "unified-nav-back menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipBack); + } else if (j == index) { + item.setAttribute("type", "radio"); + item.setAttribute("checked", "true"); + item.className = "unified-nav-current"; + item.setAttribute("tooltiptext", tooltipCurrent); + } else { + item.className = "unified-nav-forward menuitem-iconic menuitem-with-favicon"; + item.setAttribute("tooltiptext", tooltipForward); + } + + if (!item.parentNode) { + aParent.appendChild(item); + } + + existingIndex++; + } + + if (!initial) { + let existingLength = children.length; + while (existingIndex < existingLength) { + aParent.removeChild(aParent.lastChild); + existingIndex++; + } + } + } + + let sessionHistory = SessionStore.getSessionHistory(gBrowser.selectedTab, updateSessionHistory); + if (!sessionHistory) return false; - const MAX_HISTORY_MENU_ITEMS = 15; - var index = sessionHistory.index; - var half_length = Math.floor(MAX_HISTORY_MENU_ITEMS / 2); - var start = Math.max(index - half_length, 0); - var 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); + // don't display the popup for a single item + if (sessionHistory.entries.length <= 1) + return false; - var tooltipBack = gNavigatorBundle.getString("tabHistory.goBack"); - var tooltipCurrent = gNavigatorBundle.getString("tabHistory.current"); - var tooltipForward = gNavigatorBundle.getString("tabHistory.goForward"); - - for (var j = end - 1; j >= start; j--) { - let item = document.createElement("menuitem"); - let entry = sessionHistory.getEntryAtIndex(j, false); - let uri = entry.URI.spec; - let entryURI = BrowserUtils.makeURIFromCPOW(entry.URI); - - item.setAttribute("uri", uri); - item.setAttribute("label", entry.title || uri); - item.setAttribute("index", j); - - if (j != index) { - PlacesUtils.favicons.getFaviconURLForPage(entryURI, function (aURI) { - if (aURI) { - let iconURL = PlacesUtils.favicons.getFaviconLinkForIcon(aURI).spec; - iconURL = PlacesUtils.getImageURLForResolution(window, iconURL); - item.style.listStyleImage = "url(" + iconURL + ")"; - } - }); - } - - if (j < index) { - item.className = "unified-nav-back menuitem-iconic menuitem-with-favicon"; - item.setAttribute("tooltiptext", tooltipBack); - } else if (j == index) { - item.setAttribute("type", "radio"); - item.setAttribute("checked", "true"); - item.className = "unified-nav-current"; - item.setAttribute("tooltiptext", tooltipCurrent); - } else { - item.className = "unified-nav-forward menuitem-iconic menuitem-with-favicon"; - item.setAttribute("tooltiptext", tooltipForward); - } - - aParent.appendChild(item); - } + updateSessionHistory(sessionHistory, true); return true; } diff --git a/browser/base/content/test/general/browser_bug647886.js b/browser/base/content/test/general/browser_bug647886.js index 6b5bbef49446..6c28c465c103 100644 --- a/browser/base/content/test/general/browser_bug647886.js +++ b/browser/base/content/test/general/browser_bug647886.js @@ -20,6 +20,21 @@ add_task(function* () { ok(true, "history menu opened"); + // Wait for the session data to be flushed before continuing the test + yield new Promise(resolve => SessionStore.getSessionHistory(gBrowser.selectedTab, resolve)); + + is(event.target.children.length, 2, "Two history items"); + + let node = event.target.firstChild; + is(node.getAttribute("uri"), "http://example.com/2.html", "first item uri"); + is(node.getAttribute("index"), "1", "first item index"); + is(node.getAttribute("historyindex"), "0", "first item historyindex"); + + node = event.target.lastChild; + is(node.getAttribute("uri"), "http://example.com/", "second item uri"); + is(node.getAttribute("index"), "0", "second item index"); + is(node.getAttribute("historyindex"), "-1", "second item historyindex"); + event.target.hidePopup(); gBrowser.removeTab(gBrowser.selectedTab); }); diff --git a/browser/components/sessionstore/SessionHistory.jsm b/browser/components/sessionstore/SessionHistory.jsm index 2a00d9efd507..95a196bb8ef2 100644 --- a/browser/components/sessionstore/SessionHistory.jsm +++ b/browser/components/sessionstore/SessionHistory.jsm @@ -123,6 +123,8 @@ let SessionHistoryInternal = { entry.subframe = true; } + entry.charset = shEntry.URI.originCharset; + let cacheKey = shEntry.cacheKey; if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0) { @@ -289,7 +291,7 @@ let SessionHistoryInternal = { var shEntry = Cc["@mozilla.org/browser/session-history-entry;1"]. createInstance(Ci.nsISHEntry); - shEntry.setURI(Utils.makeURI(entry.url)); + shEntry.setURI(Utils.makeURI(entry.url, entry.charset)); shEntry.setTitle(entry.title || entry.url); if (entry.subframe) shEntry.setIsSubFrame(entry.subframe || false); diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm index 4a0edd6e8c81..0849214cca72 100644 --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -312,6 +312,10 @@ this.SessionStore = { navigateAndRestore(tab, loadArguments, historyIndex) { return SessionStoreInternal.navigateAndRestore(tab, loadArguments, historyIndex); + }, + + getSessionHistory(tab, updatedCallback) { + return SessionStoreInternal.getSessionHistory(tab, updatedCallback); } }; @@ -2262,6 +2266,34 @@ let SessionStoreInternal = { }); }, + /** + * Retrieves the latest session history information for a tab. The cached data + * is returned immediately, but a callback may be provided that supplies + * up-to-date data when or if it is available. The callback is passed a single + * argument with data in the same format as the return value. + * + * @param tab tab to retrieve the session history for + * @param updatedCallback function to call with updated data as the single argument + * @returns a object containing 'index' specifying the current index, and an + * array 'entries' containing an object for each history item. + */ + getSessionHistory(tab, updatedCallback) { + if (updatedCallback) { + TabStateFlusher.flush(tab.linkedBrowser).then(() => { + let sessionHistory = this.getSessionHistory(tab); + if (sessionHistory) { + updatedCallback(sessionHistory); + } + }); + } + + // Don't continue if the tab was closed before TabStateFlusher.flush resolves. + if (tab.linkedBrowser) { + let tabState = TabState.collect(tab); + return { index: tabState.index - 1, entries: tabState.entries } + } + }, + /** * See if aWindow is usable for use when restoring a previous session via * restoreLastSession. If usable, prepare it for use.