AboutPreferences.jsm 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  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. "use strict";
  5. const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
  6. const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
  7. ChromeUtils.defineModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm");
  8. const {actionTypes: at} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm");
  9. XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
  10. const HTML_NS = "http://www.w3.org/1999/xhtml";
  11. const PREFERENCES_LOADED_EVENT = "home-pane-loaded";
  12. // These "section" objects are formatted in a way to be similar to the ones from
  13. // SectionsManager to construct the preferences view.
  14. const PREFS_BEFORE_SECTIONS = [
  15. {
  16. id: "search",
  17. pref: {
  18. feed: "showSearch",
  19. titleString: "prefs_search_header",
  20. },
  21. icon: "chrome://browser/skin/search-glass.svg",
  22. },
  23. {
  24. id: "topsites",
  25. pref: {
  26. feed: "feeds.topsites",
  27. titleString: "settings_pane_topsites_header",
  28. descString: "prefs_topsites_description",
  29. },
  30. icon: "topsites",
  31. maxRows: 4,
  32. rowsPref: "topSitesRows",
  33. },
  34. ];
  35. const PREFS_AFTER_SECTIONS = [
  36. {
  37. id: "snippets",
  38. pref: {
  39. feed: "feeds.snippets",
  40. titleString: "settings_pane_snippets_header",
  41. descString: "prefs_snippets_description",
  42. },
  43. icon: "info",
  44. },
  45. ];
  46. // This CSS is added to the whole about:preferences page
  47. const CUSTOM_CSS = `
  48. #homeContentsGroup checkbox[src] .checkbox-icon {
  49. -moz-context-properties: fill;
  50. fill: currentColor;
  51. margin-inline-end: 8px;
  52. margin-inline-start: 4px;
  53. width: 16px;
  54. }
  55. #homeContentsGroup [data-subcategory] {
  56. margin-top: 14px;
  57. }
  58. #homeContentsGroup [data-subcategory] .section-checkbox {
  59. font-weight: 600;
  60. }
  61. #homeContentsGroup [data-subcategory] > vbox menulist {
  62. margin-top: 0;
  63. margin-bottom: 0;
  64. }
  65. `;
  66. this.AboutPreferences = class AboutPreferences {
  67. init() {
  68. Services.obs.addObserver(this, PREFERENCES_LOADED_EVENT);
  69. }
  70. uninit() {
  71. Services.obs.removeObserver(this, PREFERENCES_LOADED_EVENT);
  72. }
  73. onAction(action) {
  74. switch (action.type) {
  75. case at.INIT:
  76. this.init();
  77. break;
  78. case at.UNINIT:
  79. this.uninit();
  80. break;
  81. case at.SETTINGS_OPEN:
  82. action._target.browser.ownerGlobal.openPreferences("paneHome");
  83. break;
  84. // This is used to open the web extension settings page for an extension
  85. case at.OPEN_WEBEXT_SETTINGS:
  86. action._target.browser.ownerGlobal.BrowserOpenAddonsMgr(`addons://detail/${encodeURIComponent(action.data)}`);
  87. break;
  88. }
  89. }
  90. handleDiscoverySettings(sections) {
  91. // Deep copy object to not modify original Sections state in store
  92. let sectionsCopy = JSON.parse(JSON.stringify(sections));
  93. sectionsCopy.forEach(obj => {
  94. if (obj.id === "highlights") {
  95. obj.shouldHidePref = true;
  96. }
  97. if (obj.id === "topstories") {
  98. obj.rowsPref = "";
  99. }
  100. });
  101. return sectionsCopy;
  102. }
  103. async observe(window) {
  104. const discoveryStreamConfig = this.store.getState().DiscoveryStream.config;
  105. let sections = this.store.getState().Sections;
  106. if (discoveryStreamConfig.enabled) {
  107. sections = this.handleDiscoverySettings(sections);
  108. }
  109. this.renderPreferences(window, await this.strings, [...PREFS_BEFORE_SECTIONS,
  110. ...sections, ...PREFS_AFTER_SECTIONS]);
  111. }
  112. /**
  113. * Get strings from a js file that the content page would have loaded. The
  114. * file should be a single variable assignment of a JSON/JS object of strings.
  115. */
  116. get strings() {
  117. return this._strings || (this._strings = new Promise(async resolve => {
  118. let data = {};
  119. try {
  120. const locale = Cc["@mozilla.org/browser/aboutnewtab-service;1"]
  121. .getService(Ci.nsIAboutNewTabService).activityStreamLocale;
  122. const request = await fetch(`resource://activity-stream/prerendered/${locale}/activity-stream-strings.js`);
  123. const text = await request.text();
  124. const [json] = text.match(/{[^]*}/);
  125. data = JSON.parse(json);
  126. } catch (ex) {
  127. Cu.reportError("Failed to load strings for Activity Stream about:preferences");
  128. }
  129. resolve(data);
  130. }));
  131. }
  132. /**
  133. * Render preferences to an about:preferences content window with the provided
  134. * strings and preferences structure.
  135. */
  136. renderPreferences({document, Preferences, gHomePane}, strings, prefStructure) {
  137. // Helper to create a new element and append it
  138. const createAppend = (tag, parent, options) => parent.appendChild(
  139. document.createXULElement(tag, options));
  140. // Helper to get strings and format with values if necessary
  141. const formatString = id => {
  142. if (typeof id !== "object") {
  143. return strings[id] || id;
  144. }
  145. let string = strings[id.id] || JSON.stringify(id);
  146. if (id.values) {
  147. Object.entries(id.values).forEach(([key, val]) => {
  148. string = string.replace(new RegExp(`{${key}}`, "g"), val);
  149. });
  150. }
  151. return string;
  152. };
  153. // Helper to link a UI element to a preference for updating
  154. const linkPref = (element, name, type) => {
  155. const fullPref = `browser.newtabpage.activity-stream.${name}`;
  156. element.setAttribute("preference", fullPref);
  157. Preferences.add({id: fullPref, type});
  158. // Prevent changing the UI if the preference can't be changed
  159. element.disabled = Preferences.get(fullPref).locked;
  160. };
  161. // Add in custom styling
  162. document.insertBefore(document.createProcessingInstruction("xml-stylesheet",
  163. `href="data:text/css,${encodeURIComponent(CUSTOM_CSS)}" type="text/css"`),
  164. document.documentElement);
  165. // Insert a new group immediately after the homepage one
  166. const homeGroup = document.getElementById("homepageGroup");
  167. const contentsGroup = homeGroup.insertAdjacentElement("afterend", homeGroup.cloneNode());
  168. contentsGroup.id = "homeContentsGroup";
  169. contentsGroup.setAttribute("data-subcategory", "contents");
  170. createAppend("label", contentsGroup)
  171. .appendChild(document.createElementNS(HTML_NS, "h2"))
  172. .textContent = formatString("prefs_home_header");
  173. createAppend("description", contentsGroup)
  174. .textContent = formatString("prefs_home_description");
  175. // Add preferences for each section
  176. prefStructure.forEach(sectionData => {
  177. const {
  178. id,
  179. pref: prefData,
  180. icon = "webextension",
  181. maxRows,
  182. rowsPref,
  183. shouldHidePref,
  184. } = sectionData;
  185. const {
  186. feed: name,
  187. titleString,
  188. descString,
  189. nestedPrefs = [],
  190. } = prefData || {};
  191. // Don't show any sections that we don't want to expose in preferences UI
  192. if (shouldHidePref) {
  193. return;
  194. }
  195. // Use full icon spec for certain protocols or fall back to packaged icon
  196. const iconUrl = !icon.search(/^(chrome|moz-extension|resource):/) ? icon :
  197. `resource://activity-stream/data/content/assets/glyph-${icon}-16.svg`;
  198. // Add the main preference for turning on/off a section
  199. const sectionVbox = createAppend("vbox", contentsGroup);
  200. sectionVbox.setAttribute("data-subcategory", id);
  201. const checkbox = createAppend("checkbox", sectionVbox);
  202. checkbox.classList.add("section-checkbox");
  203. checkbox.setAttribute("label", formatString(titleString));
  204. checkbox.setAttribute("src", iconUrl);
  205. linkPref(checkbox, name, "bool");
  206. // Specially add a link for stories
  207. if (id === "topstories") {
  208. const sponsoredHbox = createAppend("hbox", sectionVbox);
  209. sponsoredHbox.setAttribute("align", "center");
  210. sponsoredHbox.appendChild(checkbox);
  211. checkbox.classList.add("tail-with-learn-more");
  212. const link = createAppend("label", sponsoredHbox, {is: "text-link"});
  213. link.classList.add("learn-sponsored");
  214. link.setAttribute("href", sectionData.learnMore.link.href);
  215. link.textContent = formatString(sectionData.learnMore.link.id);
  216. }
  217. // Add more details for the section (e.g., description, more prefs)
  218. const detailVbox = createAppend("vbox", sectionVbox);
  219. detailVbox.classList.add("indent");
  220. if (descString) {
  221. const label = createAppend("label", detailVbox);
  222. label.classList.add("indent");
  223. label.textContent = formatString(descString);
  224. // Add a rows dropdown if we have a pref to control and a maximum
  225. if (rowsPref && maxRows) {
  226. const detailHbox = createAppend("hbox", detailVbox);
  227. detailHbox.setAttribute("align", "center");
  228. label.setAttribute("flex", 1);
  229. detailHbox.appendChild(label);
  230. // Add box so the search tooltip is positioned correctly
  231. const tooltipBox = createAppend("hbox", detailHbox);
  232. // Add appropriate number of localized entries to the dropdown
  233. const menulist = createAppend("menulist", tooltipBox);
  234. menulist.setAttribute("crop", "none");
  235. const menupopup = createAppend("menupopup", menulist);
  236. for (let num = 1; num <= maxRows; num++) {
  237. const plurals = formatString({id: "prefs_section_rows_option", values: {num}});
  238. const item = createAppend("menuitem", menupopup);
  239. item.setAttribute("label", PluralForm.get(num, plurals));
  240. item.setAttribute("value", num);
  241. }
  242. linkPref(menulist, rowsPref, "int");
  243. }
  244. }
  245. // Add a checkbox pref for any nested preferences
  246. nestedPrefs.forEach(nested => {
  247. const subcheck = createAppend("checkbox", detailVbox);
  248. subcheck.classList.add("indent");
  249. subcheck.setAttribute("label", formatString(nested.titleString));
  250. linkPref(subcheck, nested.name, "bool");
  251. });
  252. });
  253. // Update the visibility of the Restore Defaults btn based on checked prefs
  254. gHomePane.toggleRestoreDefaultsBtn();
  255. }
  256. };
  257. this.PREFERENCES_LOADED_EVENT = PREFERENCES_LOADED_EVENT;
  258. const EXPORTED_SYMBOLS = ["AboutPreferences", "PREFERENCES_LOADED_EVENT"];