<?xml version="1.0"?>
<!-- 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/.
-->

<bindings id="calendar-multiday-view-bindings"
  xmlns="http://www.mozilla.org/xbl"
  xmlns:html="http://www.w3.org/1999/xhtml"
  xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="calendar-base-view">
    <resources>
      <stylesheet src="chrome://calendar/skin/calendar-alarms.css"/>
    </resources>
    <implementation>
      <field name="mWeekStartOffset">0</field>
      <field name="mRangeStartDate">null</field>;
      <field name="mRangeEndDate">null</field>;
      <field name="mWorkdaysOnly">false</field>
      <field name="mPendingRefreshJobs">null</field>
      <field name="mCalendar">null</field>
      <field name="mController">null</field>
      <field name="mStartDate">null</field>
      <field name="mEndDate">null</field>
      <field name="mTasksInView">false</field>
      <field name="mShowCompleted">false</field>
      <field name="mDisplayDaysOff">true</field>
      <field name="mDaysOffArray">[0,6]</field>
      <field name="mTimezone">null</field>
      <field name="mFlashingEvents">new Object()</field>
      <field name="mSelectedItems">[]</field>
      <field name="mLongWeekdayTotalPixels">-1</field>
      <field name="mResizeHandler">null</field>
      <field name="mDropShadowsLength">null</field>
      <field name="mShadowOffset">null</field>
      <field name="mDropShadows">null</field>
      <field name="mMagnifyAmount">0</field>
      <field name="mPixelScrollDelta">0</field>
      <field name="mViewStart">null</field>
      <field name="mViewEnd">null</field>
      <field name="mToggleStatus">0</field>
      <field name="mLog">null</field>
      <field name="mToggleStatusFlag"><![CDATA[
      ({ WorkdaysOnly:  1,
         TasksInView:   2,
         ShowCompleted: 4,
      })
      ]]></field>

      <field name="mPrefObserver"><![CDATA[
      ({ calView: this,
         observe: function calViewPrefChange(subj, topic, pref) {
             this.calView.handlePreference(subj, topic, pref);
             return;
         }
      })
      ]]></field>

      <field name="mObserver"><![CDATA[
        // the calIObserver, calICompositeObserver, and calIAlarmServiceObserver
        ({
            QueryInterface: XPCOMUtils.generateQI([
                Components.interfaces.calIObserver,
                Components.interfaces.calIAlarmServiceObserver,
                Components.interfaces.calICompositeObserver
            ]),

            calView: this,

            onStartBatch: function onStartBatch() {
            },
            onEndBatch: function onEndBatch() {
            },

            onLoad: function onLoad() {
                this.calView.refresh();
            },

            onAddItem: function onAddItem(aItem) {

                if (cal.isToDo(aItem)) {
                    if (!aItem.entryDate && !aItem.dueDate) {
                        return;
                    }
                    if(!this.calView.mTasksInView){
                        return;
                    }
                    if (aItem.isCompleted && !this.calView.mShowCompleted) {
                        return;
                    }
                }

                let occs = aItem.getOccurrencesBetween(this.calView.startDate,
                                                       this.calView.queryEndDate,
                                                       {});
                for each (let occ in occs) {
                    this.calView.doAddItem(occ);
                }
                return;
            },

            onModifyItem: function onModifyItem(aNewItem, aOldItem) {

                if (cal.isToDo(aNewItem) && cal.isToDo(aOldItem) &&
                    !this.calView.mTasksInView) {
                    return;
                }
                let occs;

                if (!cal.isToDo(aOldItem) || aOldItem.entryDate || aOldItem.dueDate) {
                    occs = aOldItem.getOccurrencesBetween(this.calView.startDate,
                                                          this.calView.queryEndDate,
                                                          {});
                    for each (let occ in occs) {
                        this.calView.doDeleteItem(occ);
                    }
                }
                if (cal.isToDo(aNewItem)) {
                    if (!aNewItem.entryDate && !aNewItem.dueDate || !this.calView.mTasksInView) {
                        return;
                    }
                    if (aNewItem.isCompleted && !this.calView.mShowCompleted) {
                        return;
                    }
                }

                occs = aNewItem.getOccurrencesBetween(this.calView.startDate,
                                                      this.calView.queryEndDate,
                                                      {});
                for each (let occ in occs) {
                    this.calView.doAddItem(occ);
                }
            },

            onDeleteItem: function onDeleteItem(aItem) {

                if (cal.isToDo(aItem)) {
                    if (!this.calView.mTasksInView) {
                        return;
                    }
                    if (!aItem.entryDate && !aItem.dueDate) {
                        return;
                    }
                    if (aItem.isCompleted && !this.calView.mShowCompleted) {
                        return;
                    }
                }

                let occs = aItem.getOccurrencesBetween(this.calView.startDate,
                                                       this.calView.queryEndDate,
                                                       {});
                for each (let occ in occs) {
                    this.calView.doDeleteItem(occ);
                }
            },

            onError: function onError(aCalendar, aErrNo, aMessage) { },

            onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
                switch (aName) {
                    case "suppressAlarms":
                        if (!Preferences.get("calendar.alarms.indicator.show", true) ||
                            aCalendar.getProperty("capabilities.alarms.popup.supported") === false ) {
                            break;
                        }
                        // else fall through
                    case "readOnly":
                    case "disabled":
                        // XXXvv we can be smarter about how we handle this stuff
                        this.calView.refresh();
                        break;
                }
            },

            onPropertyDeleting: function(aCalendar, aName) {
                // Values are not important here yet.
                this.onPropertyChanged(aCalendar, aName, null, null);
            },

            //
            // calIAlarmServiceObserver stuff
            //
            onAlarm: function onAlarm(aAlarmItem) {
                this.calView.flashAlarm(aAlarmItem, false);
            },

            onRemoveAlarmsByItem: function onRemoveAlarmsByItem(aItem) {
                // Stop the flashing for the item.
                this.calView.flashAlarm(aItem, true);
            },

            onRemoveAlarmsByCalendar: function onRemoveAlarmsByCalendar(aCalendar) {
                // Stop the flashing for all items of this calendar
                for each (let item in this.calView.mFlashingEvents) {
                    if (item.calendar.id == aCalendar.id) {
                        this.calView.flashAlarm(item, true);
                    }
                }
            },

            onAlarmsLoaded: function _onAlarmsLoaded(aCalendar) {},

            //
            // calICompositeObserver stuff
            // XXXvv we can be smarter about how we handle this stuff
            //
            onCalendarAdded: function onCalendarAdded(aCalendar) {
                if (!aCalendar.getProperty("disabled")) {
                    this.calView.addItemsFromCalendar(aCalendar);
                }
            },

            onCalendarRemoved: function onCalendarRemoved(aCalendar) {
                if (!aCalendar.getProperty("disabled")) {
                    this.calView.deleteItemsFromCalendar(aCalendar);
                }
            },

            onDefaultCalendarChanged:
            function onDefaultCalendarChanged(aNewDefaultCalendar) {
                // don't care, for now
            }
        })
      ]]></field>

      <constructor><![CDATA[
        Components.utils.import("resource://gre/modules/Services.jsm");
        Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
        Components.utils.import("resource://gre/modules/Preferences.jsm");
        Components.utils.import("resource:///modules/gloda/log4moz.js");

        const kWorkdaysCommand = "calendar_toggle_workdays_only_command";
        const kTasksInViewCommand = "calendar_toggle_tasks_in_view_command";
        const kShowCompleted = "calendar_toggle_show_completed_in_view_command";
        const kOrientation = "calendar_toggle_orientation_command";

        this.workdaysOnly = (document.getElementById(kWorkdaysCommand)
                                .getAttribute("checked") == "true");
        this.tasksInView = (document.getElementById(kTasksInViewCommand)
                                .getAttribute("checked") == "true");
        this.rotated = (document.getElementById(kOrientation)
                                .getAttribute("checked") == "true");
        this.showCompleted = (document.getElementById(kShowCompleted)
                                .getAttribute("checked") == "true");

        this.mTimezone = calendarDefaultTimezone();
        let alarmService = Components.classes['@mozilla.org/calendar/alarm-service;1']
                           .getService(Components.interfaces.calIAlarmService);
        alarmService.addObserver(this.mObserver);
        let self = this;
        this.setAttribute("type", this.type);
        this.mResizeHandler = function resizeHandler() {
            self.onResize(self); };
        this.viewBroadcaster.addEventListener(this.getAttribute("type") + "viewresized", this.mResizeHandler, true);
        // add a preference observer to monitor changes
        Services.prefs.addObserver("calendar.", this.mPrefObserver, false);
        this.weekStartOffset = Preferences.get("calendar.week.start", 0);
        this.updateDaysOffPrefs();
        this.mPendingRefreshJobs = new Map();
        this.mLog = Log4Moz.getConfiguredLogger("calBaseView");
      ]]></constructor>

      <destructor><![CDATA[
        Components.utils.import("resource://gre/modules/Services.jsm");

        if (this.mCalendar) {
            this.mCalendar.removeObserver(this.mObserver);
        }
        let alarmService = Components.classes['@mozilla.org/calendar/alarm-service;1']
                           .getService(Components.interfaces.calIAlarmService);
        alarmService.removeObserver(this.mObserver);
        this.viewBroadcaster.removeEventListener(this.getAttribute("type") + "viewresized", this.mResizeHandler, true);
        Services.prefs.removeObserver("calendar.", this.mPrefObserver);
      ]]></destructor>

      <property name="type" readonly="true">
        <getter><![CDATA[
            let typelist = this.id.split("-");
            return typelist[0];
        ]]></getter>
      </property>

      <property name="viewBroadcaster" readonly="true"
                onget="return document.getElementById('calendarviewBroadcaster')"/>

      <property name="labeldaybox" readonly="true"
                onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'labeldaybox')"/>

      <property name="rotated"
                onget="return (this.orient == 'horizontal')"
                onset="return (this.orient = (val ? 'horizontal' : 'vertical'))"/>
      <property name="supportsRotation" readonly="true"
                onget="return false"/>
      <property name="displayDaysOff"
                onget="return this.mDisplayDaysOff;"
                onset="return (this.mDisplayDaysOff = val);"/>
      <property name="controller"
                onget="return this.mController;"
                onset="return (this.mController = val);"/>
      <property name="daysOffArray"
                onget="return this.mDaysOffArray;"
                onset="return (this.mDaysOffArray = val);"/>
      <property name="tasksInView"
                onget="return this.mTasksInView;"
                onset="return (this.mTasksInView = val);"/>
      <property name="showCompleted"
                onget="return this.mShowCompleted;"
                onset="return (this.mShowCompleted = val);"/>
      <property name="timezone"
                onget="return this.mTimezone;"
                onset="return (this.mTimezone = val);"/>
      <property name="workdaysOnly"
                onget="return this.mWorkdaysOnly;"
                onset="return (this.mWorkdaysOnly = val);"/>
      <property name="supportsWorkdaysOnly" readonly="true"
                onget="return true;"/>
      <property name="supportsZoom" readonly="true"
                onget="return false;"/>
      <property name="selectionObserver" readonly="true"
                onget="return this.mSelectionObserver;"/>
      <property name="startDay" readonly="true"
                onget="return this.startDate;"/>
      <property name="endDay" readonly="true"
                onget="return this.endDate;"/>
      <property name="supportDisjointDates" readonly="true"
                onget="return false;"/>
      <property name="hasDisjointDates" readonly="true"
                onget="return false;"/>
      <property name="rangeStartDate"
                onget="return this.mRangeStartDate;"
                onset="return (this.mRangeStartDate = val);"/>
      <property name="rangeEndDate"
                onget="return this.mRangeEndDate;"
                onset="return (this.mRangeEndDate = val);"/>
      <property name="observerID" readonly="true"
                onget="return 'base-view-observer';"/>

      <property name="weekStartOffset">
        <getter><![CDATA[
           return this.mWeekStartOffset;
        ]]></getter>
        <setter><![CDATA[
           this.mWeekStartOffset = val;
           return val;
        ]]></setter>
      </property>

      <property name="displayCalendar">
        <getter><![CDATA[
          return this.mCalendar;
        ]]></getter>
        <setter><![CDATA[
          if (this.mCalendar)
              this.mCalendar.removeObserver(this.mObserver);
          this.mCalendar = val;
          this.mCalendar.addObserver(this.mObserver);
          this.refresh();
          return val;
        ]]></setter>
      </property>

      <property name="initialized">
        <getter><![CDATA[
            let retval;

            // Some views throw when accessing an uninitialized startDay
            try {
                retval = this.displayCalendar && this.startDay
                         && this.endDay;
            } catch(ex) {
                return false;
            }
            return retval;
        ]]></getter>
      </property>

      <method name="goToDay">
        <parameter name="aDate"/>
        <body><![CDATA[
            this.showDate(aDate);
        ]]></body>
      </method>

      <method name="getRangeDescription">
        <body><![CDATA[
            return getDateFormatter().formatInterval(this.rangeStartDate, this.rangeEndDate);
        ]]></body>
      </method>

      <!-- This function guarantees that the
       labels are clipped in the instance that the overflow occurrs,
       avoiding horizontal scrollbars from showing up for a short period. -->
      <method name="adjustWeekdayLength">
        <parameter name="forceShortName"/>
        <body><![CDATA[
          let useShortNames = false;
          let labeldayboxkids = this.labeldaybox.childNodes;
          if (!labeldayboxkids || !labeldayboxkids[0]) {
              useShortNames = true;
          } else if (forceShortName && forceShortName === true) {
              useShortNames = true;
          } else {
              let clientWidth = document.getAnonymousElementByAttribute(this, "anonid", "mainbox").clientWidth;
              let timespacer = document.getAnonymousElementByAttribute(this, "anonid", "headertimespacer");
              if (timespacer) {
                  clientWidth -= timespacer.clientWidth;
              }
              if (this.getLongWeekdayTotalPixels() > 0.95 * clientWidth) {
                  useShortNames = true;
              }
          }
          for (let i = 0; i < labeldayboxkids.length; i++) {
              labeldayboxkids[i].shortWeekNames = useShortNames;
          }
        ]]></body>
      </method>

      <method name="today">
        <body><![CDATA[
          var d = cal.jsDateToDateTime(new Date()).getInTimezone(this.mTimezone);
          d.isDate = true;
          return d;
        ]]></body>
      </method>

      <method name="isVisible">
        <body><![CDATA[
            return (this.nodeName == currentView().nodeName);
        ]]></body>
      </method>

      <method name="refresh">
        <body><![CDATA[
          if (this.isVisible()) {
              this.addItemsFromCalendar(this.mCalendar);
          }
        ]]></body>
      </method>

      <!-- force refresh visible and invisible views.
           This method is added because when only a preference is toggled, the start
           and end date of views are unchanged, therefore those views behind the
           "scene" might stay the same upon switch to them. -->
      <method name="forceRefresh">
        <body><![CDATA[
          this.addItemsFromCalendar(this.mCalendar);
        ]]></body>
      </method>

      <method name="addItemsFromCalendar">
        <parameter name="aCalendar"/>
        <body><![CDATA[
          let refreshJob = {
              calView: this,
              calendar: null,
              calId: null,
              operation: null,
              cancelled: false,

              onOperationComplete: function(aCalendar, aStatus, aOperationType, aId, aDateTime) {
                  this.calView.mLog.info("Refresh complete of calendar " + this.calId);
                  if (this.calView.mPendingRefreshJobs.has(this.calId)) {
                      this.calView.mPendingRefreshJobs.delete(this.calId);
                  }

                  if (!this.cancelled) {
                      // Fire event
                      this.calView.fireEvent('viewloaded', aOperationType);
                  }
              },

              onGetResult: function(aCalendar, aStatus, aItemType, aDetail, aCount, aItems) {
                  if (this.cancelled || !Components.isSuccessCode(aStatus)) {
                      return;
                  }

                  for (let item of aItems) {
                      if (!cal.isToDo(item) || item.entryDate || item.dueDate) {
                          this.calView.doAddItem(item);
                      }
                  }
              },

              cancel: function() {
                  this.calView.mLog.info("Refresh cancelled for calendar " + this.calId);
                  this.cancelled = true;
                  let op = cal.wrapInstance(this.operation, Components.interfaces.calIOperation);
                  if (op && op.isPending) {
                      op.cancel();
                      this.operation = null;
                  }
              },

              execute: function(aCalendar) {
                  if (!this.calView.startDate || !this.calView.endDate || !aCalendar) {
                      return;
                  }

                  if (aCalendar.type == "composite") {
                      // we're refreshing from the composite calendar, so we can cancel
                      // all other pending refresh jobs.
                      this.calView.mLog.info("Refreshing composite calendar, cancelling all pending refreshes");
                      this.calId = "composite";
                      for (let job of this.calView.mPendingRefreshJobs.values()) {
                          job.cancel();
                      }
                      this.calView.mPendingRefreshJobs.clear();
                      this.calView.relayout();
                  } else {
                      this.calId = aCalendar.id;
                      if (this.calView.mPendingRefreshJobs.has(this.calId)) {
                          this.calView.mPendingRefreshJobs.get(this.calId).cancel();
                          this.calView.mPendingRefreshJobs.delete(this.calId);
                      }
                  }
                  this.calendar = aCalendar;

                  // start our items query; for a disjoint date range
                  // we get all the items, and just filter out the ones we don't
                  // care about in addItem
                  let filter = this.calendar.ITEM_FILTER_CLASS_OCCURRENCES;
                  if (this.calView.mShowCompleted) {
                      filter |= this.calendar.ITEM_FILTER_COMPLETED_ALL;
                  } else {
                      filter |= this.calendar.ITEM_FILTER_COMPLETED_NO;
                  }

                  if (this.calView.mTasksInView) {
                      filter |= this.calendar.ITEM_FILTER_TYPE_ALL;
                  } else {
                      filter |= this.calendar.ITEM_FILTER_TYPE_EVENT;
                  }

                  let op = this.calendar.getItems(filter,
                                                  0,
                                                  this.calView.startDate,
                                                  this.calView.queryEndDate,
                                                  this);
                  op = cal.wrapInstance(op, Components.interfaces.calIOperation);
                  if (op && op.isPending) {
                      this.operation = op;
                      this.calView.mPendingRefreshJobs.set(this.calId, this);
                  }
              }
          };

          refreshJob.execute(aCalendar);
        ]]></body>
      </method>

      <method name="deleteItemsFromCalendar">
        <parameter name="aCalendar"/>
        <body><![CDATA[
            /* This method must be implemented in subclasses. */
            throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
        ]]></body>
      </method>

      <!-- the end date that should be used for getItems and similar queries -->
      <property name="queryEndDate" readonly="true">
        <getter><![CDATA[
          let end = this.endDate;
          if (!end) {
              return null;
          }
          end = end.clone();
          end.day += 1;
          end.isDate = true;
          return end;
        ]]></getter>
      </property>

      <method name="fireEvent">
        <parameter name="aEventName"/>
        <parameter name="aEventDetail"/>
        <body><![CDATA[
          let event = document.createEvent('Events');
          event.initEvent(aEventName, true, false);
          event.detail = aEventDetail;
          this.dispatchEvent(event);
        ]]></body>
      </method>

      <method name="removeDropShadows">
        <body><![CDATA[
            let dropbox = document.getAnonymousElementByAttribute(this, "dropbox", "true");
            if (dropbox && dropbox !== undefined) {
                dropbox.setAttribute("dropbox", "false");
            }
        ]]></body>
      </method>

      <method name="getLongWeekdayTotalPixels">
        <body><![CDATA[
          if (this.mLongWeekdayTotalPixels <= 0) {
              let maxDayWidth = 0;
              for each (let label in this.labeldaybox.childNodes) {
                  if (label.localName && label.localName == "calendar-day-label") {
                      label.shortWeekNames = false;
                      let curPixelLength = label.getLongWeekdayPixels();
                      maxDayWidth = Math.max(maxDayWidth, curPixelLength);
                  }
              }
              if (maxDayWidth > 0) {
                  this.mLongWeekdayTotalPixels = (maxDayWidth * this.labeldaybox.childNodes.length) + 10;
              }
          }
          return this.mLongWeekdayTotalPixels;
        ]]></body>
      </method>

      <!-- A preference handler which is called by the preference observer.
           Can be overwritten in derived bindings. -->
      <method name="handleCommonPreference">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aPreference"/>
        <body><![CDATA[
           // refresh view if categories seem to have changed
           if (aPreference.indexOf("calendar.category.color") == 0) {
               this.refreshView();
               return;
           }
           switch (aPreference) {
               case "calendar.week.d0sundaysoff":
               case "calendar.week.d1mondaysoff":
               case "calendar.week.d2tuesdaysoff":
               case "calendar.week.d3wednesdaysoff":
               case "calendar.week.d4thursdaysoff":
               case "calendar.week.d5fridaysoff":
               case "calendar.week.d6saturdaysoff":
                   this.updateDaysOffPrefs();
                   break;
               case "calendar.timezone.local":
                   this.timezone = calendarDefaultTimezone();
                   this.refreshView();
                   break;
               case "calendar.alarms.indicator.show":
                   //Break here to ensure the view is refreshed
                   break;
               case "calendar.week.start":
                   this.weekStartOffset = aSubject.getIntPref(aPreference);
                   break;
               case "calendar.date.format":
                   this.refreshView();
                   break;
               default:
                   return;
            }
            this.refreshView();
        ]]></body>
      </method>

      <method name="updateDaysOffPrefs">
        <body><![CDATA[
            const weekPrefix = "calendar.week.";
            const prefNames = ["d0sundaysoff", "d1mondaysoff", "d2tuesdaysoff",
                               "d3wednesdaysoff", "d4thursdaysoff",
                               "d5fridaysoff", "d6saturdaysoff"];
            const defaults = ["true", "false", "false", "false",
                              "false", "false", "true"];
            let daysOff = new Array();
            for (let i in prefNames) {
                if (Preferences.get(weekPrefix+prefNames[i])) {
                    daysOff.push(Number(i));
                }
            }
            this.daysOffArray = daysOff;
        ]]></body>
      </method>

      <method name="refreshView">
        <body><![CDATA[
          if (!this.startDay || !this.endDay) {
             // don't refresh if we're not initialized
             return;
          }
          // Just refresh, the goToDay function will notice
          this.goToDay(this.selectedDay);
          this.forceRefresh();
        ]]></body>
      </method>

      <!-- Default implementations follow, these make things easier for
            extensions that don't need certain features. -->
      <method name="handlePreference">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aPref"/>
        <body><![CDATA[
          // Do nothing by default
        ]]></body>
      </method>
      <method name="setDateRange">
        <parameter name="aStartDate"/>
        <parameter name="aEndDate"/>
        <body><![CDATA[
          cal.navigationBar.setDateRange(aStartDate, aEndDate);
        ]]></body>
      </method>

      <property name="selectedDay"
                onget="return this.startDate"
                onset="return this.startDate"/>
       
      <method name="getSelectedItems">
        <parameter name="aCount"/>
        <body><![CDATA[
          aCount.value = this.mSelectedItems.length;
          return this.mSelectedItems;
        ]]></body>
      </method>
      <method name="setSelectedItems">
        <parameter name="aCount"/>
        <parameter name="aItems"/>
        <body><![CDATA[
          this.mSelectedItems = aItems.concat([]);
          return this.mSelectedItems;
        ]]></body>
      </method>

      <method name="getDateList">
        <parameter name="aCount"/>
        <parameter name="aDates"/>
        <body><![CDATA[
          let start = this.startDate.clone();
          while (start.compare(this.endDate) <= 0) {
              dates.push(start);
              start.day++;
          }
          aCount.value = dates.length;
          return dates;
        ]]></body>
      </method>

      <method name="flashAlarm">
        <parameter name="aAlarmItem"/>
        <parameter name="aStop"/>
        <body><![CDATA[
          // Do nothing by default
        ]]></body>
      </method>

      <method name="zoomIn">
        <parameter name="aLevel"/>
        <body><![CDATA[
        ]]></body>
      </method>
      <method name="zoomOut">
        <parameter name="aLevel"/>
        <body><![CDATA[
        ]]></body>
      </method>
      <method name="zoomReset">
        <body><![CDATA[
        ]]></body>
      </method>
    </implementation>

    <handlers>
      <handler event="move">
        <![CDATA[
          this.moveView(event.detail);
        ]]>
      </handler>
      <handler event="keypress"><![CDATA[
        const kKE = Components.interfaces.nsIDOMKeyEvent;
        switch (event.keyCode) {
            case kKE.DOM_VK_PAGE_UP:
                this.moveView(-1);
                break;
            case kKE.DOM_VK_PAGE_DOWN:
                this.moveView(1);
                break;
        }
      ]]></handler>
      <handler event="wheel"><![CDATA[
        const pixelThreshold = 150;
        if (event.shiftKey && Preferences.get('calendar.view.mousescroll', true)){
            if (event.deltaMode == event.DOM_DELTA_LINE) {
                if (event.deltaY != 0) {
                    deltaView = event.deltaY < 0 ? -1 : 1;
                }
            } else if (event.deltaMode == event.DOM_DELTA_PIXEL) {
                this.mPixelScrollDelta += event.deltaY;
                if (this.mPixelScrollDelta > pixelThreshold){
                    deltaView = 1;
                    this.mPixelScrollDelta = 0;
                } else if (this.mPixelScrollDelta < -pixelThreshold) {
                    deltaView = -1;
                    this.mPixelScrollDelta = 0;
                }
            }

            if (deltaView != 0) {
                this.moveView(deltaView);
            }
        }

        // Prevent default scroll handling
        event.preventDefault();
      ]]></handler>
      <handler event="MozRotateGesture"><![CDATA[
        // Threshold for the minimum and maximum angle we should accept
        // rotation for. 90 degrees minimum is most logical, but 45 degrees
        // allows you to rotate with one hand.
        const MIN_ROTATE_ANGLE = 45;
        const MAX_ROTATE_ANGLE = 180;

        let absval = Math.abs(event.delta);
        if (this.supportsRotation &&
            absval >= MIN_ROTATE_ANGLE &&
            absval < MAX_ROTATE_ANGLE) {
            toggleOrientation();
            event.preventDefault();
        }
      ]]></handler>
      <handler event="MozMagnifyGestureStart"><![CDATA[
        this.mMagnifyAmount = 0;
      ]]></handler>
      <handler event="MozMagnifyGestureUpdate"><![CDATA[
        // Threshold as to how much magnification causes the zoom to happen
        const THRESHOLD = 30;

        if (this.supportsZoom) {
            this.mMagnifyAmount += event.delta;

            if (this.mMagnifyAmount > THRESHOLD) {
                this.zoomOut();
                this.mMagnifyAmount = 0;
            } else if (this.mMagnifyAmount < -THRESHOLD) {
                this.zoomIn();
                this.mMagnifyAmount = 0;
            }
            event.preventDefault();
        }
      ]]></handler>
      <handler event="MozSwipeGesture"><![CDATA[
        if ((event.direction == SimpleGestureEvent.DIRECTION_UP && !this.rotated) ||
            (event.direction == SimpleGestureEvent.DIRECTION_LEFT && this.rotated)) {
            this.moveView(-1);
        } else if ((event.direction == SimpleGestureEvent.DIRECTION_DOWN && !this.rotated) ||
                   (event.direction == SimpleGestureEvent.DIRECTION_RIGHT && this.rotated)) {
            this.moveView(1);
        }
      ]]></handler>
    </handlers>
  </binding>

  <binding id="calendar-day-label" extends="xul:box">
    <content flex="1" pack="center">
      <xul:label anonid="longWeekdayName" class="calendar-day-label-name" xbl:inherits="selected,relation"/>
      <xul:label anonid="shortWeekdayName" class="calendar-day-label-name" hidden="true" xbl:inherits="selected,relation"/>
    </content>
    <implementation>
      <field name="mWeekday">-1</field>
      <field name="longWeekdayPixels">0</field>
      <field name="mDate">null</field>
      <property name="longName" readonly="true"
                onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'longWeekdayName');"/>
      <property name="shortName" readonly="true"
                onget="return document.getAnonymousElementByAttribute(this, 'anonid', 'shortWeekdayName');"/>
      <property name="weekDay">
        <getter>return this.mWeekday;</getter>
        <setter><![CDATA[
          this.mWeekday = val % 7;
          this.longName.value = getDateFormatter().dayName(val);
          this.shortName.value = getDateFormatter().shortDayName(val);
          return this.mWeekday;
        ]]></setter>
      </property>

      <property name="date">
        <getter><![CDATA[
          return this.mDate;
        ]]></getter>
        <setter><![CDATA[
          this.mDate = val;
          let dateFormatter = cal.getDateFormatter();
          let label = cal.calGetString("calendar", "dayHeaderLabel",
                                       [dateFormatter.shortDayName(val.weekday),
                                        dateFormatter.formatDateWithoutYear(val)]);
          this.shortName.setAttribute("value", label);
          label = cal.calGetString("calendar", "dayHeaderLabel",
                                   [dateFormatter.dayName(val.weekday),
                                    dateFormatter.formatDateWithoutYear(val)]);
          this.longName.setAttribute("value", label);
          return val;
        ]]></setter>
      </property>

      <property name="shortWeekNames">
        <getter><![CDATA[
        ]]></getter>
        <setter><![CDATA[
          // cache before change, in case we are switching to short
          this.getLongWeekdayPixels();
          setBooleanAttribute(this.longName, "hidden", val);
          setBooleanAttribute(this.shortName, "hidden", !val);
          return val;
        ]]></setter>
      </property>

      <method name="getLongWeekdayPixels">
        <body><![CDATA[
          // Only do this if the long weekdays are visible and we haven't already cached.
          let longNameWidth = this.longName.boxObject.width;
          if (longNameWidth > 0) {
              this.longWeekdayPixels = longNameWidth +
                                       getSummarizedStyleValues(this.longName, ["margin-left", "margin-right"]);
              this.longWeekdayPixels += getSummarizedStyleValues(this, ["border-left-width",
                                                                        "padding-left", "padding-right"]);
              return this.longWeekdayPixels;
          } else {
              // in this case the weekdaypixels have not yet been layouted;
              // by definition 0 is returned
              return 0;
          }
        ]]></body>
      </method>
    </implementation>
  </binding>
</bindings>
