summaryrefslogtreecommitdiffstats
path: root/comm/mail/modules/UIFontSize.jsm
blob: dd5a6db101a0002e321cf75fccc6196a7dcec38b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
/* 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/. */

"use strict";

const EXPORTED_SYMBOLS = ["UIFontSize"];

const { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

var langGroup = Services.prefs.getComplexValue(
  "font.language.group",
  Ci.nsIPrefLocalizedString
).data;

var registeredWindows = new Set();

/**
 * Update the font size of the registered window.
 *
 * @param {Window} win - The window to be registered.
 */
function updateWindow(win) {
  let tabmail = win.document.getElementById("tabmail");
  let browser =
    tabmail?.getBrowserForSelectedTab() ||
    win.document.getElementById("messagepane");

  if (
    UIFontSize.prefValue == UIFontSize.DEFAULT ||
    UIFontSize.prefValue == UIFontSize.user_value
  ) {
    win.document.documentElement.style.removeProperty("font-size");
    UIFontSize.updateMessageBrowser(browser);
    UIFontSize.updateAppMenuButton(win);
    return;
  }

  // Prevent any font update if the defined value can make the UI unusable.
  if (
    UIFontSize.prefValue < UIFontSize.MIN_VALUE ||
    UIFontSize.prefValue > UIFontSize.MAX_VALUE
  ) {
    // Reset to the default font size.
    UIFontSize.size = 0;
    Services.console.logStringMessage(
      `Unsupported font size: ${UIFontSize.prefValue}`
    );
    return;
  }

  // Add the font size to the HTML document element.
  win.document.documentElement.style.setProperty(
    "font-size",
    `${UIFontSize.prefValue}px`
  );

  UIFontSize.updateMessageBrowser(browser);
  UIFontSize.updateAppMenuButton(win);
}

/**
 * Loop through all registered windows and update the font size.
 */
function updateAllWindows() {
  for (let win of registeredWindows) {
    updateWindow(win);
  }
}

/**
 * The object controlling the global font size.
 */
var 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.
  user_value: 0,

  // Keeps track of the custom value while in safe mode.
  safe_mode_value: 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;
    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
    // return the default OS font size.
    return this.prefValue || this.user_value;
  },

  /**
   * 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) {
    if (isPlainText) {
      let monospaceSize = Services.prefs.getIntPref(
        "font.size.monospace." + langGroup,
        this.size
      );
      return monospaceSize + (this.size - this.user_value);
    }
    let variableSize = Services.prefs.getIntPref(
      "font.size.variable." + langGroup,
      this.size
    );
    return variableSize + (this.size - this.user_value);
  },

  /**
   * 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) {
    // Save the edited pref so we can restore it, and set the user value to the
    // default if the app is in safe mode to make sure we start from a clean
    // state.
    if (Services.appinfo.inSafeMode) {
      this.safe_mode_value = this.size;
      this.size = 0;
    }

    // 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.user_value) {
      let style = win
        .getComputedStyle(win.document.documentElement)
        .getPropertyValue("font-size");

      // Store the rounded default value.
      this.user_value = Math.round(parseFloat(style));
    }

    registeredWindows.add(win);
    win.addEventListener("unload", () => {
      registeredWindows.delete(win);
      // If we deregistered all the windows (application is getting closed) and
      // we're in safe mode, reset the font size value to the original one in
      // case the user edited the font size while in safe mode.
      if (!registeredWindows.size && Services.appinfo.inSafeMode) {
        Services.prefs.setIntPref("mail.uifontsize", this.safe_mode_value);
      }
    });
    updateWindow(win);
  },

  /**
   * 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) {
    let 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 = 0;
  },

  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.prefValue == this.DEFAULT || this.prefValue == this.user_value) {
      browser.contentDocument?.body?.style.removeProperty("font-size");
      // Update the state indicator here only after we cleared the font size
      // from the message browser.
      this.isEdited = false;
      return;
    }

    // Check if the current message is in plain text.
    let 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");
    }
  },

  observe(win, topic, data) {
    // Observe any new window or dialog that is opened and register it to
    // inherit the font sizing variation.
    switch (topic) {
      // FIXME! Temporarily disabled until we can properly manage all dialogs.
      // case "domwindowopened":
      //   win.addEventListener(
      //     "load",
      //     () => {
      //       this.registerWindow(win);
      //     },
      //     { once: true }
      //   );
      //   break;

      default:
        break;
    }
  },

  /**
   * Ensure the subdialogs are properly resized to fit larger font size
   * variations.
   * This is copied from SubDialog.jsm: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.prefValue == this.DEFAULT) {
      return;
    }
    let docEl = dialog._frame.contentDocument.documentElement;

    // These are deduced from styles which we don't change, so it's safe to get
    // them now:
    let boxHorizontalBorder =
      2 *
      parseFloat(dialog._window.getComputedStyle(dialog._box).borderLeftWidth);
    let frameHorizontalMargin =
      2 * parseFloat(dialog._window.getComputedStyle(dialog._frame).marginLeft);

    // Then determine and set a bunch of width stuff:
    let { scrollWidth } = docEl.ownerDocument.body || docEl;
    let frameMinWidth = docEl.style.width || scrollWidth + "px";
    let 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();
  },
};

/**
 * Bind the font size pref change to the updateAllWindows method.
 */
XPCOMUtils.defineLazyPreferenceGetter(
  UIFontSize,
  "prefValue",
  "mail.uifontsize",
  null,
  updateAllWindows
);

Services.ww.registerNotification(UIFontSize);