1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5var EXPORTED_SYMBOLS = ["RecentlyClosedTabsAndWindowsMenuUtils"];
6
7const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
8
9ChromeUtils.defineModuleGetter(
10  this,
11  "PluralForm",
12  "resource://gre/modules/PluralForm.jsm"
13);
14ChromeUtils.defineModuleGetter(
15  this,
16  "SessionStore",
17  "resource:///modules/sessionstore/SessionStore.jsm"
18);
19
20var navigatorBundle = Services.strings.createBundle(
21  "chrome://browser/locale/browser.properties"
22);
23
24var RecentlyClosedTabsAndWindowsMenuUtils = {
25  /**
26   * Builds up a document fragment of UI items for the recently closed tabs.
27   * @param   aWindow
28   *          The window that the tabs were closed in.
29   * @param   aTagName
30   *          The tag name that will be used when creating the UI items.
31   * @param   aPrefixRestoreAll (defaults to false)
32   *          Whether the 'restore all tabs' item is suffixed or prefixed to the list.
33   *          If suffixed (the default) a separator will be inserted before it.
34   * @param   aRestoreAllLabel (defaults to "appmenu-reopen-all-tabs")
35   *          Which localizable string to use for the 'restore all tabs' item.
36   * @returns A document fragment with UI items for each recently closed tab.
37   */
38  getTabsFragment(
39    aWindow,
40    aTagName,
41    aPrefixRestoreAll = false,
42    aRestoreAllLabel = "appmenu-reopen-all-tabs"
43  ) {
44    let doc = aWindow.document;
45    let fragment = doc.createDocumentFragment();
46    if (SessionStore.getClosedTabCount(aWindow) != 0) {
47      let closedTabs = SessionStore.getClosedTabData(aWindow, false);
48      for (let i = 0; i < closedTabs.length; i++) {
49        createEntry(
50          aTagName,
51          false,
52          i,
53          closedTabs[i],
54          doc,
55          closedTabs[i].title,
56          fragment
57        );
58      }
59
60      createRestoreAllEntry(
61        doc,
62        fragment,
63        aPrefixRestoreAll,
64        false,
65        aRestoreAllLabel,
66        closedTabs.length,
67        aTagName
68      );
69    }
70    return fragment;
71  },
72
73  /**
74   * Builds up a document fragment of UI items for the recently closed windows.
75   * @param   aWindow
76   *          A window that can be used to create the elements and document fragment.
77   * @param   aTagName
78   *          The tag name that will be used when creating the UI items.
79   * @param   aPrefixRestoreAll (defaults to false)
80   *          Whether the 'restore all windows' item is suffixed or prefixed to the list.
81   *          If suffixed (the default) a separator will be inserted before it.
82   * @param   aRestoreAllLabel (defaults to "appmenu-reopen-all-windows")
83   *          Which localizable string to use for the 'restore all windows' item.
84   * @returns A document fragment with UI items for each recently closed window.
85   */
86  getWindowsFragment(
87    aWindow,
88    aTagName,
89    aPrefixRestoreAll = false,
90    aRestoreAllLabel = "appmenu-reopen-all-windows"
91  ) {
92    let closedWindowData = SessionStore.getClosedWindowData(false);
93    let doc = aWindow.document;
94    let fragment = doc.createDocumentFragment();
95    if (closedWindowData.length) {
96      let menuLabelString = navigatorBundle.GetStringFromName(
97        "menuUndoCloseWindowLabel"
98      );
99      let menuLabelStringSingleTab = navigatorBundle.GetStringFromName(
100        "menuUndoCloseWindowSingleTabLabel"
101      );
102
103      for (let i = 0; i < closedWindowData.length; i++) {
104        let undoItem = closedWindowData[i];
105        let otherTabsCount = undoItem.tabs.length - 1;
106        let label =
107          otherTabsCount == 0
108            ? menuLabelStringSingleTab
109            : PluralForm.get(otherTabsCount, menuLabelString);
110        let menuLabel = label
111          .replace("#1", undoItem.title)
112          .replace("#2", otherTabsCount);
113        let selectedTab = undoItem.tabs[undoItem.selected - 1];
114
115        createEntry(aTagName, true, i, selectedTab, doc, menuLabel, fragment);
116      }
117
118      createRestoreAllEntry(
119        doc,
120        fragment,
121        aPrefixRestoreAll,
122        true,
123        aRestoreAllLabel,
124        closedWindowData.length,
125        aTagName
126      );
127    }
128    return fragment;
129  },
130
131  /**
132   * Re-open a closed tab and put it to the end of the tab strip.
133   * Used for a middle click.
134   * @param aEvent
135   *        The event when the user clicks the menu item
136   */
137  _undoCloseMiddleClick(aEvent) {
138    if (aEvent.button != 1) {
139      return;
140    }
141
142    aEvent.view.undoCloseTab(aEvent.originalTarget.getAttribute("value"));
143    aEvent.view.gBrowser.moveTabToEnd();
144    let ancestorPanel = aEvent.target.closest("panel");
145    if (ancestorPanel) {
146      ancestorPanel.hidePopup();
147    }
148  },
149
150  get strings() {
151    delete this.strings;
152    return (this.strings = new Localization(
153      ["branding/brand.ftl", "browser/menubar.ftl", "browser/appmenu.ftl"],
154      true
155    ));
156  },
157};
158
159function setImage(aItem, aElement) {
160  let iconURL = aItem.image;
161  // don't initiate a connection just to fetch a favicon (see bug 467828)
162  if (/^https?:/.test(iconURL)) {
163    iconURL = "moz-anno:favicon:" + iconURL;
164  }
165
166  aElement.setAttribute("image", iconURL);
167}
168
169/**
170 * Create a UI entry for a recently closed tab or window.
171 * @param aTagName
172 *        the tag name that will be used when creating the UI entry
173 * @param aIsWindowsFragment
174 *        whether or not this entry will represent a closed window
175 * @param aIndex
176 *        the index of the closed tab
177 * @param aClosedTab
178 *        the closed tab
179 * @param aDocument
180 *        a document that can be used to create the entry
181 * @param aMenuLabel
182 *        the label the created entry will have
183 * @param aFragment
184 *        the fragment the created entry will be in
185 */
186function createEntry(
187  aTagName,
188  aIsWindowsFragment,
189  aIndex,
190  aClosedTab,
191  aDocument,
192  aMenuLabel,
193  aFragment
194) {
195  let element = aDocument.createXULElement(aTagName);
196
197  element.setAttribute("label", aMenuLabel);
198  if (aClosedTab.image) {
199    setImage(aClosedTab, element);
200  }
201  if (!aIsWindowsFragment) {
202    element.setAttribute("value", aIndex);
203  }
204
205  if (aTagName == "menuitem") {
206    element.setAttribute(
207      "class",
208      "menuitem-iconic bookmark-item menuitem-with-favicon"
209    );
210  }
211
212  element.setAttribute(
213    "oncommand",
214    "undoClose" + (aIsWindowsFragment ? "Window" : "Tab") + "(" + aIndex + ");"
215  );
216
217  // Set the targetURI attribute so it will be shown in tooltip.
218  // SessionStore uses one-based indexes, so we need to normalize them.
219  let tabData;
220  tabData = aIsWindowsFragment ? aClosedTab : aClosedTab.state;
221  let activeIndex = (tabData.index || tabData.entries.length) - 1;
222  if (activeIndex >= 0 && tabData.entries[activeIndex]) {
223    element.setAttribute("targetURI", tabData.entries[activeIndex].url);
224  }
225
226  // Windows don't open in new tabs and menuitems dispatch command events on
227  // middle click, so we only need to manually handle middle clicks for
228  // toolbarbuttons.
229  if (!aIsWindowsFragment && aTagName != "menuitem") {
230    element.addEventListener(
231      "click",
232      RecentlyClosedTabsAndWindowsMenuUtils._undoCloseMiddleClick
233    );
234  }
235
236  if (aIndex == 0) {
237    element.setAttribute(
238      "key",
239      "key_undoClose" + (aIsWindowsFragment ? "Window" : "Tab")
240    );
241  }
242
243  aFragment.appendChild(element);
244}
245
246/**
247 * Create an entry to restore all closed windows or tabs.
248 * @param aDocument
249 *        a document that can be used to create the entry
250 * @param aFragment
251 *        the fragment the created entry will be in
252 * @param aPrefixRestoreAll
253 *        whether the 'restore all windows' item is suffixed or prefixed to the list
254 *        If suffixed a separator will be inserted before it.
255 * @param aIsWindowsFragment
256 *        whether or not this entry will represent a closed window
257 * @param aRestoreAllLabel
258 *        which localizable string to use for the entry
259 * @param aEntryCount
260 *        the number of elements to be restored by this entry
261 * @param aTagName
262 *        the tag name that will be used when creating the UI entry
263 */
264function createRestoreAllEntry(
265  aDocument,
266  aFragment,
267  aPrefixRestoreAll,
268  aIsWindowsFragment,
269  aRestoreAllLabel,
270  aEntryCount,
271  aTagName
272) {
273  let restoreAllElements = aDocument.createXULElement(aTagName);
274  restoreAllElements.classList.add("restoreallitem");
275
276  // We cannot use aDocument.l10n.setAttributes because the menubar label is not
277  // updated in time and displays a blank string (see Bug 1691553).
278  restoreAllElements.setAttribute(
279    "label",
280    RecentlyClosedTabsAndWindowsMenuUtils.strings.formatValueSync(
281      aRestoreAllLabel
282    )
283  );
284
285  restoreAllElements.setAttribute(
286    "oncommand",
287    "for (var i = 0; i < " +
288      aEntryCount +
289      "; i++) undoClose" +
290      (aIsWindowsFragment ? "Window" : "Tab") +
291      "();"
292  );
293  if (aPrefixRestoreAll) {
294    aFragment.insertBefore(restoreAllElements, aFragment.firstChild);
295  } else {
296    aFragment.appendChild(aDocument.createXULElement("menuseparator"));
297    aFragment.appendChild(restoreAllElements);
298  }
299}
300