/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; const registeredWindows = new Set(); /** * Loop through all registered windows and update the font size. */ function updateAllWindows() { for (const win of registeredWindows) { UIFontSize.updateWindow(win); } // After we've updated all the windows we can update isEdited. UIFontSize.isEdited = !UIFontSize.isDefault; } /** * The object controlling the global font size. */ export const UIFontSize = { /** * Default value is 0 so we know the font wasn't changed. */ DEFAULT: 0, /** * Font size limit to avoid unusable UI. */ MIN_VALUE: 9, MAX_VALUE: 30, /** * The default font size of the user's OS, rounded to integer. We use this in * order to prevent issues in case the user has a float default font size * (e.g.: 14.345px). By rounding to an INT, we can always go back the original * default font size and the rounding doesn't affect the actual sizing but * only the value shown to the user. */ osValue: 0, /** * Keep track of the state of the custom font size. We use this instead of the * size attribute because we need to keep track of when things changed back to * a default state, and using the size attribute wouldn't be accurate. */ isEdited: false, /** * Set the font size. * * @param {integer} size - The new size value. */ set size(size) { this.isEdited = true; if (Services.appinfo.inSafeMode) { this.prefValue = size; updateAllWindows(); return; } Services.prefs.setIntPref("mail.uifontsize", size); }, /** * Get the font size. * * @returns {integer} - The current font size defined in the pref or the value * defined by the OS, extracted from the messenger window computed style. */ get size() { // If the pref is set to 0, it means the user never changed font size so we // should use the default OS font size. const size = this.prefValue || this.osValue; // Fail safe to avoid UI lockdown in case the OS value is wrongly calculated // and the value returned is smaller or bigger than the defined thresholds. if (size < this.MIN_VALUE) { return this.MIN_VALUE; } if (size > this.MAX_VALUE) { return this.MAX_VALUE; } return size; }, /** * True if the current font size matches the default value or the actual OS * size. * * @type {boolean} */ get isDefault() { return this.prefValue === this.DEFAULT || this.prefValue === this.osValue; }, /** * Get the font size to be applied to the message browser. * * @param {boolean} isPlainText - If the current message is in plain text. * @returns {int} - The font size to apply to the message, changed relative to * the default preferences. */ browserSize(isPlainText) { const langGroup = Services.locale.fontLanguageGroup; if (isPlainText) { const monospaceSize = Services.prefs.getIntPref( "font.size.monospace." + langGroup, this.size ); return Math.max( Math.min(monospaceSize, this.size), monospaceSize + (this.size - this.osValue) ); } const variableSize = Services.prefs.getIntPref( "font.size.variable." + langGroup, this.size ); return Math.max( Math.min(variableSize, this.size), variableSize + (this.size - this.osValue) ); }, /** * Register a window to be updated if the size ever changes. The current * value is applied to the window. Deregistration is automatic. * * @param {Window} win - The window to be registered. */ registerWindow(win) { if (registeredWindows.has(win)) { console.error( "Trying to register the same window twice", win.location.pathname ); return; } // Fetch the default font size defined by the OS as soon as we register the // first window. Don't do it again if we already have a value. if (!this.osValue) { if (win.document.readyState == "loading") { console.error( "Trying to register a window before it is fully loaded", win.location.pathname ); return; } const style = win .getComputedStyle(win.document.documentElement) .getPropertyValue("font-size"); // Store the rounded default value. this.osValue = Math.round(parseFloat(style)); if (!this.isDefault) { this.isEdited = true; } } registeredWindows.add(win); win.addEventListener( "unload", () => { registeredWindows.delete(win); }, { once: true } ); this.updateWindow(win); }, /** * Update the font size of the registered window. * * @param {Window} win - The window to be updated. */ updateWindow(win) { const tabmail = win.document.getElementById("tabmail"); const browser = tabmail?.getBrowserForSelectedTab() || win.document.getElementById("messagepane"); if (this.isDefault) { win.document.documentElement.style.removeProperty("font-size"); this.updateMessageBrowser(browser); this.updateAppMenuButton(win); win.dispatchEvent(new win.CustomEvent("uifontsizechange")); return; } // Add the font size to the HTML document element. win.document.documentElement.style.setProperty( "font-size", `${this.size}px` ); this.updateMessageBrowser(browser); this.updateAppMenuButton(win); win.dispatchEvent(new win.CustomEvent("uifontsizechange")); }, /** * Update the label of the PanelUI app menu to reflect the current font size. * * @param {Window} win - The window from where the app menu is visible. */ updateAppMenuButton(win) { const panelButton = win.document.getElementById( "appMenu-fontSizeReset-button" ); if (panelButton) { win.document.l10n.setAttributes( panelButton, "appmenuitem-font-size-reset", { size: this.size, } ); } win.document .getElementById("appMenu-fontSizeReduce-button") ?.toggleAttribute("disabled", this.size <= this.MIN_VALUE); win.document .getElementById("appMenu-fontSizeEnlarge-button") ?.toggleAttribute("disabled", this.size >= this.MAX_VALUE); }, reduceSize() { if (this.size <= this.MIN_VALUE) { return; } this.size--; }, resetSize() { this.size = this.DEFAULT; }, increaseSize() { if (this.size >= this.MAX_VALUE) { return; } this.size++; }, /** * Update the font size of the document body element of a browser content. * This is used primarily for each loaded message in the message pane. * * @param {XULElement} browser - The message browser element. */ updateMessageBrowser(browser) { // Bail out if the font size wasn't changed, or we don't have a browser. // This might happen if the method is called before the message browser is // available in the DOM. if (!this.isEdited || !browser) { return; } if (this.isDefault) { browser.contentDocument?.body?.style.removeProperty("font-size"); return; } // Check if the current message is in plain text. const isPlainText = browser.contentDocument?.querySelector( ".moz-text-plain, .moz-text-flowed" ); browser.contentDocument?.body?.style.setProperty( "font-size", `${UIFontSize.browserSize(isPlainText)}px` ); // We need to remove the inline font size written in the div wrapper of the // body content in order to let our inline style take effect. if (isPlainText) { isPlainText.style.removeProperty("font-size"); } }, /** * Ensure the subdialogs are properly resized to fit larger font size * variations. * This is copied from SubDialog.sys.mjs:resizeDialog(), and we need to do that * because that method triggers again the `resizeCallback` and `dialogopen` * Event, which we use to detect the opening of a dialog, therefore calling * the `resizeDialog()` method would cause an infinite loop. * * @param {SubDialog} dialog - The dialog prototype. */ resizeSubDialog(dialog) { // No need to update the dialog size if the font size wasn't changed. if (this.isDefault) { return; } const docEl = dialog._frame.contentDocument.documentElement; // These are deduced from styles which we don't change, so it's safe to get // them now: const boxHorizontalBorder = 2 * parseFloat(dialog._window.getComputedStyle(dialog._box).borderLeftWidth); const frameHorizontalMargin = 2 * parseFloat(dialog._window.getComputedStyle(dialog._frame).marginLeft); // Then determine and set a bunch of width stuff: const { scrollWidth } = docEl.ownerDocument.body || docEl; const frameMinWidth = docEl.style.width || scrollWidth + "px"; const frameWidth = docEl.getAttribute("width") ? docEl.getAttribute("width") + "px" : frameMinWidth; if (dialog._box.getAttribute("sizeto") != "available") { dialog._frame.style.width = frameWidth; } let boxMinWidth = `calc(${ boxHorizontalBorder + frameHorizontalMargin }px + ${frameMinWidth})`; // Temporary fix to allow parent chrome to collapse properly to min width. // See Bug 1658722. if (dialog._window.isChromeWindow) { boxMinWidth = `min(80vw, ${boxMinWidth})`; } dialog._box.style.minWidth = boxMinWidth; dialog.resizeVertically(); }, }; if (Services.appinfo.inSafeMode) { UIFontSize.prefValue = UIFontSize.DEFAULT; } else { /** * Bind the font size pref change to the updateAllWindows method. */ XPCOMUtils.defineLazyPreferenceGetter( UIFontSize, "prefValue", "mail.uifontsize", UIFontSize.DEFAULT, updateAllWindows ); }