Bug 1561528 - Use CSS variables for calendar category colours rather than dynamically adding CSS rules. r=philipp
authorGeoff Lankow <geoff@darktrojan.net>
Wed, 26 Jun 2019 20:13:49 +1200
changeset 35998 796d020bab06cde6baf0882a39f90d2560ebae2a
parent 35997 e0edf02808494b9f1f3f36d18097c7adc2666a60
child 35999 b02d7fcf98333e52269e6f83f9c03f65b0770505
push id392
push userclokep@gmail.com
push dateMon, 02 Sep 2019 20:17:19 +0000
reviewersphilipp
bugs1561528
Bug 1561528 - Use CSS variables for calendar category colours rather than dynamically adding CSS rules. r=philipp
calendar/base/content/calendar-chrome-startup.js
calendar/base/content/calendar-view-core.xml
calendar/base/content/calendar-views-utils.js
calendar/base/modules/utils/calViewUtils.jsm
--- a/calendar/base/content/calendar-chrome-startup.js
+++ b/calendar/base/content/calendar-chrome-startup.js
@@ -45,19 +45,16 @@ async function commonInitCalendar() {
     getViewDeck().addEventListener("dayselect", observeViewDaySelect);
     getViewDeck().addEventListener("itemselect", calendarController.onSelectionChanged, true);
 
     // Start alarm service
     Cc["@mozilla.org/calendar/alarm-service;1"].getService(Ci.calIAlarmService).startup();
     document.getElementById("calsidebar_splitter").addEventListener("command", onCalendarViewResize);
     window.addEventListener("resize", onCalendarViewResize, true);
 
-    // Set up the category colors
-    categoryManagement.initCategories();
-
     // Set calendar color CSS on this window
     cal.view.colorTracker.registerWindow(window);
 
     // Set up window pref observers
     calendarWindowPrefs.init();
 
     // Set up the available modifiers for each platform.
     let keys = document.querySelectorAll("#calendar-keys > key");
@@ -93,19 +90,16 @@ function commonFinishCalendar() {
     removeCalendarCommandController();
 
     // Tear down calendar appmenus.
     tearDownCalendarAppMenus();
 
     document.getElementById("calsidebar_splitter").removeEventListener("command", onCalendarViewResize);
     window.removeEventListener("resize", onCalendarViewResize, true);
 
-    // Clean up the category colors
-    categoryManagement.cleanupCategories();
-
     // Clean up window pref observers
     calendarWindowPrefs.cleanup();
 }
 
 /**
  * Handler function to create |viewtype + "viewresized"| events that are
  * dispatched through the calendarviewBroadcaster.
  *
--- a/calendar/base/content/calendar-view-core.xml
+++ b/calendar/base/content/calendar-view-core.xml
@@ -166,16 +166,18 @@
             let item = this.mOccurrence;
             let cssSafeId = cal.view.formatStringForCSSRule(item.calendar.id);
             this.style.setProperty("--item-backcolor", `var(--calendar-${cssSafeId}-backcolor)`);
             this.style.setProperty("--item-forecolor", `var(--calendar-${cssSafeId}-forecolor)`);
             let categoriesArray = item.getCategories({});
             if (categoriesArray.length > 0) {
                 let cssClassesArray = categoriesArray.map(cal.view.formatStringForCSSRule);
                 this.setAttribute("categories", cssClassesArray.join(" "));
+                let categoriesBox = document.getAnonymousElementByAttribute(this, "anonid", "category-box");
+                categoriesBox.style.backgroundColor = `var(--category-${cssClassesArray[0]}-color)`;
             }
 
             // Add alarm icons as needed.
             let alarms = item.getAlarms({});
             if (alarms.length && Services.prefs.getBoolPref("calendar.alarms.indicator.show", true)) {
                 let iconsBox = document.getAnonymousElementByAttribute(this, "anonid", "alarm-icons-box");
                 cal.alarms.addReminderImages(iconsBox, alarms);
 
--- a/calendar/base/content/calendar-views-utils.js
+++ b/calendar/base/content/calendar-views-utils.js
@@ -371,110 +371,16 @@ function scheduleMidnightUpdate(refreshC
             Services.obs.removeObserver(wakeObserver, "wake_notification");
         });
         gMidnightTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     }
     gMidnightTimer.initWithCallback(udCallback, msUntilTomorrow, gMidnightTimer.TYPE_ONE_SHOT);
 }
 
 /**
- * Returns a cached copy of the view stylesheet.
- *
- * @return      The view stylesheet object.
- */
-function getViewStyleSheet() {
-    if (!getViewStyleSheet.sheet) {
-        const cssUri = "chrome://calendar/content/calendar-view-bindings.css";
-        for (let sheet of document.styleSheets) {
-            if (sheet.href == cssUri) {
-                getViewStyleSheet.sheet = sheet;
-                break;
-            }
-        }
-    }
-    return getViewStyleSheet.sheet;
-}
-
-/**
- * Category preferences observer. Used to update the stylesheets for category
- * colors.
- *
- * Note we need to keep the categoryPrefBranch variable outside of
- * initCategories since branch observers only live as long as the branch object
- * is alive, and out of categoryManagement to avoid cyclic references.
- */
-var categoryPrefBranch;
-var categoryManagement = {
-    QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
-
-    initCategories: function() {
-        categoryPrefBranch = Services.prefs.getBranch("calendar.category.color.");
-        let categories = categoryPrefBranch.getChildList("");
-
-        // Fix illegally formatted category prefs.
-        for (let i in categories) {
-            let category = categories[i];
-            if (category.search(/[^_0-9a-z-]/) != -1) {
-                let categoryFix = cal.view.formatStringForCSSRule(category);
-                if (categoryPrefBranch.prefHasUserValue(categoryFix)) {
-                    categories.splice(i, 1); // remove illegal name
-                } else {
-                    let color = categoryPrefBranch.getCharPref(category);
-                    categoryPrefBranch.setCharPref(categoryFix, color);
-                    categoryPrefBranch.clearUserPref(category); // not usable
-                    categories[i] = categoryFix;  // replace illegal name
-                }
-            }
-        }
-
-        // Add color information to the stylesheets.
-        categories.forEach(categoryManagement.updateStyleSheetForCategory,
-                           categoryManagement);
-        categoryPrefBranch.addObserver("", categoryManagement);
-    },
-
-    cleanupCategories: function() {
-        categoryPrefBranch = Services.prefs.getBranch("calendar.category.color.");
-        categoryPrefBranch.removeObserver("", categoryManagement);
-    },
-
-    observe: function(subject, topic, prefName) {
-        this.updateStyleSheetForCategory(prefName);
-        // TODO Currently, the only way to find out if categories are removed is
-        // to initially grab the calendar.categories.names preference and then
-        // observe changes to it. it would be better if we had hooks for this,
-        // so we could delete the rule from our style cache and also remove its
-        // color preference.
-    },
-
-    categoryStyleCache: {},
-
-    updateStyleSheetForCategory: function(catName) {
-        if (!(catName in this.categoryStyleCache)) {
-            // We haven't created a rule for this category yet, do so now.
-            let sheet = getViewStyleSheet();
-            let ruleString = '.category-color-box[categories~="' + catName + '"] {} ';
-
-            try {
-                let ruleIndex = sheet.insertRule(ruleString, sheet.cssRules.length);
-                this.categoryStyleCache[catName] = sheet.cssRules[ruleIndex];
-            } catch (ex) {
-                sheet.ownerNode.addEventListener("load",
-                                                 () => this.updateStyleSheetForCategory(catName),
-                                                 { once: true });
-                return;
-            }
-        }
-
-        let color = Services.prefs.getStringPref("calendar.category.color." + catName, "");
-        this.categoryStyleCache[catName].style.backgroundColor = color;
-    }
-};
-
-/**
  * Handler function to set the selected day in the minimonth to the currently
  * selected day in the current view.
  *
  * @param event     The "dayselect" event emitted from the views.
  *
  */
 function observeViewDaySelect(event) {
     let date = event.detail;
--- a/calendar/base/modules/utils/calViewUtils.jsm
+++ b/calendar/base/modules/utils/calViewUtils.jsm
@@ -1,12 +1,13 @@
 /* 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/. */
 
+var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 var { XPCOMUtils } = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "cal", "resource://calendar/modules/calUtils.jsm", "cal");
 
 /*
  * View and DOM related helper functions
  */
 
@@ -289,76 +290,111 @@ var calview = {
 };
 
 /**
  * Adds CSS variables for each calendar to registered windows for coloring
  * UI elements. Automatically tracks calendar creation, changes, and deletion.
  */
 calview.colorTracker = {
     calendars: null,
+    categoryBranch: null,
     windows: new Set(),
     QueryInterface: cal.generateQI([
         Ci.calICalendarManagerObserver,
         Ci.calIObserver
     ]),
 
     // The only public method. Deregistration is not required.
     registerWindow(aWindow) {
         if (this.calendars === null) {
             let manager = cal.getCalendarManager();
             this.calendars = new Set(manager.getCalendars({}));
             manager.addObserver(this);
             manager.addCalendarObserver(this);
+            this.categoryBranch = Services.prefs.getBranch("calendar.category.color.");
+            this.categoryBranch.addObserver("", this);
+            Services.obs.addObserver(this, "xpcom-shutdown");
         }
 
         this.windows.add(aWindow);
         aWindow.addEventListener("unload", () => this.windows.delete(aWindow));
         for (let calendar of this.calendars) {
-            this._addToWindow(aWindow, calendar);
+            this._addCalendarToWindow(aWindow, calendar);
         }
+        this._addAllCategoriesToWindow(aWindow);
     },
-    _addToWindow(aWindow, aCalendar) {
-        let cssSafeId = cal.view.formatStringForCSSRule(aCalendar.id);
+    _addCalendarToWindow(aWindow, aCalendar) {
+        let cssSafeId = calview.formatStringForCSSRule(aCalendar.id);
         let style = aWindow.document.documentElement.style;
         let backColor = aCalendar.getProperty("color") || "#a8c2e1";
         let foreColor = calview.getContrastingTextColor(backColor);
         style.setProperty(`--calendar-${cssSafeId}-backcolor`, backColor);
         style.setProperty(`--calendar-${cssSafeId}-forecolor`, foreColor);
     },
-    _removeFromWindow(aWindow, aCalendar) {
-        let cssSafeId = cal.view.formatStringForCSSRule(aCalendar.id);
+    _removeCalendarFromWindow(aWindow, aCalendar) {
+        let cssSafeId = calview.formatStringForCSSRule(aCalendar.id);
         let style = aWindow.document.documentElement.style;
         style.removeProperty(`--calendar-${cssSafeId}-backcolor`);
         style.removeProperty(`--calendar-${cssSafeId}-forecolor`);
     },
+    _addCategoryToWindow(aWindow, aCategoryName) {
+        if (/[^\w-]/.test(aCategoryName)) {
+            return;
+        }
+
+        let cssSafeName = calview.formatStringForCSSRule(aCategoryName);
+        let style = aWindow.document.documentElement.style;
+        let color = this.categoryBranch.getStringPref(aCategoryName, "transparent");
+        style.setProperty(`--category-${cssSafeName}-color`, color);
+    },
+    _addAllCategoriesToWindow(aWindow) {
+        for (let categoryName of this.categoryBranch.getChildList("")) {
+            this._addCategoryToWindow(aWindow, categoryName);
+        }
+    },
 
     // calICalendarManagerObserver methods
     onCalendarRegistered(aCalendar) {
         this.calendars.add(aCalendar);
         for (let window of this.windows) {
-            this._addToWindow(window, aCalendar);
+            this._addCalendarToWindow(window, aCalendar);
         }
     },
     onCalendarUnregistering(aCalendar) {
         this.calendars.delete(aCalendar);
         for (let window of this.windows) {
-            this._removeFromWindow(window, aCalendar);
+            this._removeCalendarFromWindow(window, aCalendar);
         }
     },
     onCalendarDeleting(aCalendar) {},
 
     // calIObserver methods
     onStartBatch() {},
     onEndBatch() {},
     onLoad() {},
     onAddItem(aItem) {},
     onModifyItem(aNewItem, aOldItem) {},
     onDeleteItem(aDeletedItem) {},
     onError(aCalendar, aErrNo, aMessage) {},
     onPropertyChanged(aCalendar, aName, aValue, aOldValue) {
         if (aName == "color") {
             for (let window of this.windows) {
-                this._addToWindow(window, aCalendar);
+                this._addCalendarToWindow(window, aCalendar);
             }
         }
     },
     onPropertyDeleting(aCalendar, aName) {},
+
+    // nsIObserver method
+    observe(aSubject, aTopic, aData) {
+        if (aTopic == "nsPref:changed") {
+            for (let window of this.windows) {
+                this._addCategoryToWindow(window, aData);
+            }
+            // TODO Currently, the only way to find out if categories are removed is
+            // to initially grab the calendar.categories.names preference and then
+            // observe changes to it. It would be better if we had hooks for this.
+        } else if (aTopic == "xpcom-shutdown") {
+            this.categoryBranch.removeObserver("", this);
+            Services.obs.removeObserver(this, "xpcom-shutdown");
+        }
+    }
 };