Bug 1449464 - Improvements to the calendar list tree. r=Taraman
authorPhilipp Kewisch <mozilla@kewis.ch>
Sun, 18 Mar 2018 21:20:16 +0100
changeset 30563 2d0822c5a149c89e047b2e368d91083fbe8badda
parent 30562 52d3403860a3b8c923f13189f64a02662aba5d28
child 30564 33a67b0129b390a6b1fe235155f3a526a36bf4a7
push id2172
push usermozilla@kewis.ch
push dateSun, 15 Apr 2018 05:33:14 +0000
treeherdercomm-beta@33a67b0129b3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersTaraman
bugs1449464
Bug 1449464 - Improvements to the calendar list tree. r=Taraman MozReview-Commit-ID: EyEO4GKTFrM
calendar/base/content/dialogs/calendar-providerUninstall-dialog.xul
calendar/base/content/widgets/calendar-list-tree.xml
calendar/base/themes/common/calendar-management.css
calendar/base/themes/linux/calendar-management.css
calendar/base/themes/osx/calendar-management.css
calendar/base/themes/windows/calendar-management.css
--- a/calendar/base/content/dialogs/calendar-providerUninstall-dialog.xul
+++ b/calendar/base/content/dialogs/calendar-providerUninstall-dialog.xul
@@ -27,11 +27,11 @@
 
   <description id="pre-name-description">&providerUninstall.preName.label;</description>
   <label id="provider-name-label"/>
   <description id="post-name-description">&providerUninstall.postName.label;</description>
   <description id="reinstall-note-description">&providerUninstall.reinstallNote.label;</description>
 
   <calendar-list-tree id="calendar-list-tree"
                       hidecolumnpicker="true"
-                      ignoredisabledstate="true"
+                      disabledstate="ignore"
                       flex="1"/>
 </dialog>
--- a/calendar/base/content/widgets/calendar-list-tree.xml
+++ b/calendar/base/content/widgets/calendar-list-tree.xml
@@ -222,16 +222,17 @@
       <field name="mCompositeCalendar">null</field>
       <field name="tree">null</field>
       <field name="treebox">null</field>
       <field name="ruleCache">null</field>
       <field name="mCachedSheet">null</field>
 
       <field name="mCycleCalendarFlag">null</field>
       <field name="mCycleTimer">null</field>
+      <field name="cycleDebounce">200</field>
 
       <constructor><![CDATA[
           ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
           ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
           this.tree.view = this;
           this.ruleCache = {};
           this.mCycleCalendarFlag = {};
       ]]></constructor>
@@ -340,19 +341,27 @@
         ]]></getter>
       </property>
 
       <property name="calendars">
         <getter><![CDATA[
             return this.mCalendarList;
         ]]></getter>
         <setter><![CDATA[
-            this.mCalendarList = val;
-            this.mCalendarList.forEach(this.addCalendar, this);
-            return this.mCalendarList;
+            this.treebox.beginUpdateBatch();
+            try {
+                this.clear();
+                this.mCalendarList = [];
+                for (let calendar of val) {
+                    this.addCalendar(calendar);
+                }
+                return this.mCalendarList;
+            } finally {
+                this.treebox.endUpdateBatch();
+            }
         ]]></setter>
       </property>
 
       <property name="compositeCalendar">
         <getter><![CDATA[
             if (!this.mCompositeCalendar) {
                 this.mCompositeCalendar =
                     Components.classes["@mozilla.org/calendar/calendar;1?type=composite"]
@@ -379,19 +388,19 @@
                 readonly="true"
                 onget="return this.compositeCalendar.getCalendars({});"/>
       <property name="allowDrag"
                 onget="return (this.getAttribute('allowdrag') == 'true');"
                 onset="return setBooleanAttribute(this, 'allowdrag', val);"/>
       <property name="writable"
                 onget="return (this.getAttribute('writable') == 'true');"
                 onset="return setBooleanAttribute(this, 'writable', val);"/>
-      <property name="ignoreDisabledState"
-                onget="return (this.getAttribute('ignoredisabledstate') == 'true');"
-                onset="return setBooleanAttribute(this, 'ignoredisabledstate', val);"/>
+      <property name="disabledState"
+                onget="return this.getAttribute('disabledstate') || 'disabled';"
+                onset="return this.setAttribute('disabledstate', val);"/>
 
       <method name="sortOrderChanged">
         <parameter name=""/>
         <body><![CDATA[
             if (this.mAddingFromComposite) {
                 return;
             }
             let event = document.createEvent("Events");
@@ -443,16 +452,24 @@
                 if (this.mCalendarList[i].id == aId) {
                     return i;
                 }
             }
             return -1;
         ]]></body>
       </method>
 
+      <method name="selectCalendarById">
+        <parameter name="aId"/>
+        <body><![CDATA[
+            let index = this.findIndexById(aId);
+            this.tree.view.selection.select(index);
+        ]]></body>
+      </method>
+
       <method name="addCalendar">
         <!--
           - Add a calendar to the calendar list
           -
           - @param aCalendar     The calendar to add.
           -->
         <parameter name="aCalendar"/>
         <body><![CDATA[
@@ -511,42 +528,54 @@
         <parameter name="aCalendar"/>
         <body><![CDATA[
             let index = this.findIndexById(aCalendar.id);
             if (index < 0) {
                 return;
             }
 
             this.mCalendarList.splice(index, 1);
+            this.treebox.rowCountChanged(index, -1);
+
             if (index == this.rowCount) {
                 index--;
             }
 
             this.tree.view.selection.select(index + 1);
-            this.treebox.rowCountChanged(index, -1);
 
             aCalendar.removeObserver(this.calObserver);
 
             // Make sure the calendar is removed from the composite calendar
             this.compositeCalendar.removeCalendar(aCalendar);
 
             // Remove the css style rule from the sheet.
             let sheet = this.sheet;
             for (let i = 0; i < sheet.cssRules.length; i++) {
-                if (sheet.cssRules[i] == this.ruleCache[aCalendar.id]) {
+                if (sheet.cssRules[i] == this.ruleCache[aCalendar.id][0] ||
+                    sheet.cssRules[i] == this.ruleCache[aCalendar.id][1]) {
                     sheet.deleteRule(i);
-                    delete this.ruleCache[aCalendar.id];
-                    break;
                 }
             }
+            delete this.ruleCache[aCalendar.id];
 
             this.sortOrderChanged();
         ]]></body>
       </method>
 
+      <method name="clear">
+        <body><![CDATA[
+            this.treebox.beginUpdateBatch();
+            try {
+                this.mCalendarList.forEach(this.removeCalendar, this);
+            } finally {
+                this.treebox.endUpdateBatch();
+            }
+        ]]></body>
+      </method>
+
       <method name="updateCalendar">
         <!--
           - Update a calendar's tree row (to refresh the color and such)
           -
           - @param aCalendar     The calendar to update.
           -->
         <parameter name="aCalendar"/>
         <body><![CDATA[
@@ -564,20 +593,40 @@
         <body><![CDATA[
             let color = aCalendar.getProperty("color") || "#a8c2e1";
             let sheet = this.sheet;
             if (!(aCalendar.id in this.ruleCache)) {
                 let ruleString = "calendar-list-tree > tree > treechildren" +
                                  "::-moz-tree-cell(color-treecol, id-" +
                                  aCalendar.id + ") {}";
 
+                let disabledRuleString = "calendar-list-tree > tree > treechildren" +
+                                         "::-moz-tree-cell(color-treecol, id-" +
+                                         aCalendar.id + ", disabled) {}";
+
                 let ruleIndex = sheet.insertRule(ruleString, sheet.cssRules.length);
-                this.ruleCache[aCalendar.id] = sheet.cssRules[ruleIndex];
+                let disabledIndex = sheet.insertRule(disabledRuleString, sheet.cssRules.length);
+                this.ruleCache[aCalendar.id] = [sheet.cssRules[ruleIndex], sheet.cssRules[disabledIndex]];
             }
-            this.ruleCache[aCalendar.id].style.backgroundColor = color;
+
+            let [enabledRule, disabledRule] = this.ruleCache[aCalendar.id];
+            enabledRule.style.backgroundColor = color;
+
+            let colorMatch = color.match(/#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})?/);
+            if (colorMatch && this.disabledState == "disabled") {
+                let gray = (
+                    0.2126 * parseInt(colorMatch[1], 16) +
+                    0.7152 * parseInt(colorMatch[2], 16) +
+                    0.0722 * parseInt(colorMatch[3], 16)
+                );
+                let alpha = colorMatch[4] ? parseInt(colorMatch[4], 16) : 255;
+                disabledRule.style.backgroundColor = `rgba(${gray}, ${gray}, ${gray}, ${alpha})`;
+            } else {
+                disabledRule.style.backgroundColor = color;
+            }
         ]]></body>
       </method>
 
       <method name="getCalendarFromEvent">
         <!--
           - Get the calendar from the given DOM event. This can be a Mouse event or a
           - keyboard event.
           -
@@ -643,19 +692,16 @@
 
       <method name="getRowProperties">
         <parameter name="aRow"/>
         <body><![CDATA[
             let properties = [];
             let calendar = this.getCalendar(aRow);
             let composite = this.compositeCalendar;
 
-            // Set up the composite calendar status
-            properties.push(composite.getCalendarById(calendar.id) ? "checked" : "unchecked");
-
             // Set up the calendar id
             properties.push("id-" + calendar.id);
 
             // Get the calendar color
             let color = (calendar.getProperty("color") || "").substr(1);
 
             // Set up the calendar color (background)
             properties.push("color-" + (color || "default"));
@@ -668,19 +714,30 @@
                 // 'readfailed' is supposed to "win" over 'readonly', meaning that
                 // if reading from a calendar fails there is no further need to also display
                 // information about 'readonly' status
                 properties.push("readfailed");
             } else if (calendar.readOnly) {
                 properties.push("readonly");
             }
 
-            // Set up the disabled state
-            properties.push(!this.ignoreDisabledState && calendar.getProperty("disabled") ?
-                            "disabled" : "enabled");
+            // Set up the composite calendar status and disabled state
+            let checkedState = composite.getCalendarById(calendar.id) ? "checked" : "unchecked";
+            let isDisabled = calendar.getProperty("disabled");
+            let disabledState = isDisabled ? "disabled" : "enabled";
+
+            if (isDisabled && this.disabledState == "ignore") {
+                disabledState = "enabled";
+            } else if (isDisabled && this.disabledState == "checked") {
+                checkedState = "checked";
+            } else if (isDisabled) {
+                checkedState = "unchecked";
+            }
+
+            properties.push(disabledState, checkedState);
 
             return properties.join(" ");
         ]]></body>
       </method>
 
       <method name="getColumnProperties">
         <parameter name="aCol"/>
         <body><![CDATA[
@@ -932,45 +989,55 @@
         ]]></body>
       </method>
 
       <method name="cycleCell">
         <parameter name="aRow"/>
         <parameter name="aCol"/>
         <body><![CDATA[
             let calendar = this.getCalendar(aRow);
+            if (this.disabledState != "ignore" && calendar.getProperty("disabled")) {
+                return;
+            }
+
             if (this.mCycleCalendarFlag[calendar.id]) {
                 delete this.mCycleCalendarFlag[calendar.id];
             } else {
-                this.mCycleCalendarFlag[calendar.id] = calendar;
+                this.mCycleCalendarFlag[calendar.id] = [calendar, aRow];
             }
 
-            if (this.mCycleTimer) {
-                clearTimeout(this.mCycleTimer);
+            if (this.cycleDebounce) {
+                if (this.mCycleTimer) {
+                    clearTimeout(this.mCycleTimer);
+                }
+                this.mCycleTimer = setTimeout(this.cycleCellCommit.bind(this), 200);
+            } else {
+                this.cycleCellCommit();
             }
-            this.treebox.invalidateRow(aRow);
-            this.mCycleTimer = setTimeout(this.cycleCellCommit.bind(this), 200);
         ]]></body>
       </method>
 
       <method name="cycleCellCommit">
         <body><![CDATA[
             let composite = this.compositeCalendar;
+            this.treebox.beginUpdateBatch();
+            composite.startBatch();
             try {
-                composite.startBatch();
-                for (let id in this.mCycleCalendarFlag) {
+                for (let [id, [calendar, row]] of Object.entries(this.mCycleCalendarFlag)) {
                     if (composite.getCalendarById(id)) {
-                        composite.removeCalendar(this.mCycleCalendarFlag[id]);
+                        composite.removeCalendar(calendar);
                     } else {
-                        composite.addCalendar(this.mCycleCalendarFlag[id]);
+                        composite.addCalendar(calendar);
                     }
-                    delete this.mCycleCalendarFlag[id];
+                    this.treebox.invalidateRow(row);
                 }
+                this.mCycleCalendarFlag = {};
             } finally {
                 composite.endBatch();
+                this.treebox.endUpdateBatch();
             }
         ]]></body>
       </method>
 
       <method name="isEditable">
         <parameter name="aRow"/>
         <parameter name="aCol"/>
         <body><![CDATA[
@@ -1041,16 +1108,17 @@
               event.preventDefault();
           }
       ]]></handler>
 
       <!-- use key=" " since keycode="VK_SPACE" doesn't work -->
       <handler event="keypress" key=" "><![CDATA[
           if (this.tree.currentIndex > -1) {
               this.cycleCell(this.tree.currentIndex, this.getColumn("checkbox-treecol"));
+              this.treebox.invalidateRow(this.tree.currentIndex);
               event.preventDefault();
           }
       ]]></handler>
 
       <handler event="keypress" keycode="VK_DOWN" modifiers="control"><![CDATA[
           if (!this.allowDrag) {
               return;
           }
--- a/calendar/base/themes/common/calendar-management.css
+++ b/calendar/base/themes/common/calendar-management.css
@@ -19,16 +19,20 @@ calendar-list-tree > tree > treechildren
     -moz-image-region: rect(0, 14px, 14px, 0);
 }
 
 calendar-list-tree > tree > treechildren::-moz-tree-image(status-treecol, readfailed) {
     list-style-image: url(chrome://calendar-common/skin/calendar-status.png);
     -moz-image-region: rect(0px, 28px, 14px, 14px);
 }
 
+calendar-list-tree > tree > treechildren::-moz-tree-cell-text(disabled) {
+    color: GrayText;
+}
+
 calendar-list-tree > tree {
     padding: 0;
     margin: 4px 0;
     -moz-appearance: none;
 }
 
 calendar-list-tree > tree > treecols > treecol[hideheader="true"],
 calendar-list-tree > tree > treecols > treecol[hideheader="true"] {
--- a/calendar/base/themes/linux/calendar-management.css
+++ b/calendar/base/themes/linux/calendar-management.css
@@ -15,11 +15,11 @@ calendar-list-tree > tree > treechildren
 calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked) {
     -moz-image-region: rect(0 26px 13px 13px);
 }
 
 calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, disabled) {
     -moz-image-region: rect(0 39px 13px 26px);
 }
 
-calendar-list-tree > tree > treechildren::-moz-tree-cell-text(disabled) {
-    color: GrayText;
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked, disabled) {
+    -moz-image-region: rect(0 52px 13px 39px);
 }
--- a/calendar/base/themes/osx/calendar-management.css
+++ b/calendar/base/themes/osx/calendar-management.css
@@ -23,16 +23,20 @@ calendar-list-tree > tree > treechildren
 calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked) {
     -moz-image-region: rect(0 64px 32px 32px);
 }
 
 calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, disabled) {
     -moz-image-region: rect(0 96px 32px 64px);
 }
 
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked, disabled) {
+    -moz-image-region: rect(0 128px 32px 96px);
+}
+
 calendar-list-tree > tree > treechildren::-moz-tree-cell(checkbox-treecol) {
     padding: 0;
 }
 
 calendar-list-tree treechildren::-moz-tree-row(selected) {
     background: url(chrome://calendar/skin/sidebar-item.png) 0 0 repeat-x #90A0C0;
 }
 
--- a/calendar/base/themes/windows/calendar-management.css
+++ b/calendar/base/themes/windows/calendar-management.css
@@ -15,11 +15,11 @@ calendar-list-tree > tree > treechildren
 calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked) {
     -moz-image-region: rect(0 26px 13px 13px);
 }
 
 calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, disabled) {
     -moz-image-region: rect(0 39px 13px 26px);
 }
 
-calendar-list-tree > tree > treechildren::-moz-tree-cell-text(disabled) {
-    color: GrayText;
+calendar-list-tree > tree > treechildren::-moz-tree-image(checkbox-treecol, checked, disabled) {
+    -moz-image-region: rect(0 52px 13px 39px);
 }