Bug 308175 - Visual indicator in day and week view to show current time (slot). r+a=philipp
authorDecathlon <bv1578@gmail.com>
Mon, 06 Feb 2012 22:22:26 +0100
changeset 10564 55ae7a6b32acac77633f27f219c5ce685b28f94b
parent 10563 0e082f56031d32bbd4be9015897d3c86003efd1d
child 10565 a4536456e92a28aa5f6d783734adb8efc5ca4f4e
push id402
push userbugzilla@standard8.plus.com
push dateTue, 13 Mar 2012 21:17:18 +0000
treeherdercomm-beta@d080a8ebf16a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs308175
Bug 308175 - Visual indicator in day and week view to show current time (slot). r+a=philipp
calendar/base/content/calendar-month-view.xml
calendar/base/content/calendar-multiday-view.xml
calendar/base/content/calendar-views.js
calendar/base/content/calendar-views.xml
calendar/base/themes/gnomestripe/calendar-views.css
calendar/base/themes/pinstripe/calendar-views.css
calendar/base/themes/winstripe/calendar-views.css
calendar/lightning/content/lightning.js
--- a/calendar/base/content/calendar-month-view.xml
+++ b/calendar/base/content/calendar-month-view.xml
@@ -651,16 +651,18 @@
           this.selectedDay = aDate;
         ]]></body>
       </method>
 
       <method name="onResize">
       <parameter name="aBinding"/>
         <body><![CDATA[
           aBinding.adjustWeekdayLength();
+          // Delete the timer for the time indicator in day/week view.
+          timeIndicator.cancel();
         ]]></body>
       </method>
 
       <method name="setDateRange">
         <parameter name="aStartDate"/>
         <parameter name="aEndDate"/>
         <body><![CDATA[
           this.rangeStartDate = aStartDate;
--- a/calendar/base/content/calendar-multiday-view.xml
+++ b/calendar/base/content/calendar-multiday-view.xml
@@ -57,18 +57,20 @@
   xmlns:xbl="http://www.mozilla.org/xbl">
 
   <!--
      - This is the time bar that displays time divisions to the side
      - or top of a multiday view.
     -->
   <binding id="calendar-time-bar">
     <content>
-      <xul:box xbl:inherits="orient,width,height" flex="1" anonid="topbox">
-      </xul:box>
+      <xul:stack anonid="timebarboxstack" style="display: block; position: absolute" xbl:inherits="orient,width,height" flex="1">
+        <xul:box anonid="topbox" xbl:inherits="orient,width,height" flex="1"/>
+        <xul:box anonid="timeIndicatorBoxTimeBar" class="timeIndicator-timeBar" xbl:inherits="orient" hidden="true"/>
+      </xul:stack>
     </content>
 
     <implementation>
       <field name="mPixPerMin">0.6</field>
       <field name="mStartMin">0*60</field>
       <field name="mEndMin">24*60</field>
       <field name="mDayStartHour">0</field>
       <field name="mDayEndHour">24</field>
@@ -265,17 +267,19 @@
   <!--
      - A column for displaying event boxes in.  One column per
      - day; it manages the layout of the events given via add/deleteEvent.
     -->
   <binding id="calendar-event-column">
     <content>
       <xul:stack anonid="boxstack" flex="1" class="multiday-column-box-stack" style="min-width: 1px; min-height: 1px">
         <xul:box anonid="bgbox" flex="1" class="multiday-column-bg-box" style="min-width: 1px; min-height: 1px"/>
-        <xul:box xbl:inherits="context" anonid="topbox" flex="1" equalsize="always" class="multiday-column-top-box" style="min-width: 1px; min-height: 1px" mousethrough="always"/>
+        <xul:box anonid="topbox" class="multiday-column-top-box" flex="1" style="min-width: 1px; min-height: 1px"
+                 xbl:inherits="context" equalsize="always" mousethrough="always"/>
+        <xul:box anonid="timeIndicatorBox" xbl:inherits="orient" class="timeIndicator" mousethrough="always" hidden="true"/>
         <xul:box anonid="fgbox" flex="1" class="fgdragcontainer" style="min-width: 1px; min-height: 1px; overflow:hidden;">
           <xul:box anonid="fgdragspacer" style="display: inherit; overflow: hidden;">
             <xul:spacer flex="1"/>
             <xul:label anonid="fgdragbox-startlabel" class="fgdragbox-label"/>
           </xul:box>
           <xul:box anonid="fgdragbox" class="fgdragbox" />
           <xul:label anonid="fgdragbox-endlabel" class="fgdragbox-label"/>
         </xul:box>
@@ -416,16 +420,23 @@
                   startlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-startlabel"),
                   endlabel: document.getAnonymousElementByAttribute(this, "anonid", "fgdragbox-endlabel")
               };
           }
           return this.mFgboxes;
         ]]></getter>
       </property>
 
+      <property name="timeIndicatorBox"
+        readonly="true">
+        <getter><![CDATA[
+          return document.getAnonymousElementByAttribute(this, "anonid", "timeIndicatorBox");
+        ]]></getter>
+      </property>
+
       <property
         name="events"
         readonly="true"
         onget="return this.methods"/>
 
       <field name="mDayOff">false</field>
       <property name="dayOff">
         <getter><![CDATA[
@@ -2443,88 +2454,199 @@
 
         var self = this;
         // set the flex attribute at the scrollbox-innerbox
         // (this can be removed, after Bug 343555 is fixed)
         let scrollbox = document.getAnonymousElementByAttribute(
                        this, "anonid", "scrollbox");
         document.getAnonymousElementByAttribute(
             scrollbox, "class", "box-inherit scrollbox-innerbox").flex = "1";
+
+        // set the time interval for the time indicator timer
+        this.setTimeIndicatorInterval(getPrefSafe("calendar.view.timeIndicatorInterval", 15));
+        this.enableTimeIndicator();
+
         this.reorient();
       ]]></constructor>
 
       <property name="daysInView" readonly="true">
         <getter><![CDATA[
           return this.labeldaybox.childNodes && this.labeldaybox.childNodes.length;
         ]]></getter>
       </property>
 
       <property name="supportsZoom" readonly="true"
                 onget="return true;"/>
       <property name="supportsRotation" readonly="true"
                 onget="return true"/>
 
+      <method name="setTimeIndicatorInterval">
+        <parameter name="aPrefInterval"/>
+        <body><![CDATA[
+          // If the preference just edited by the user is outside the valid
+          // range [0, 1440], we change it into the nearest limit (0 or 1440).
+          let newTimeInterval = Math.max(0, Math.min(1440, aPrefInterval));
+          if (newTimeInterval != aPrefInterval) {
+              cal.setPref("calendar.view.timeIndicatorInterval", newTimeInterval);
+          }
+
+          if (newTimeInterval != this.mTimeIndicatorInterval) {
+              this.mTimeIndicatorInterval = newTimeInterval;
+          }
+          if (this.mTimeIndicatorInterval == 0) {
+              timeIndicator.cancel();
+          }
+        ]]></body>
+      </method>
+
+      <method name="enableTimeIndicator">
+        <body><![CDATA[
+          // Hide or show the time indicator if the preference becomes 0 or greater than 0.
+          let hideIndicator = this.mTimeIndicatorInterval == 0;
+          setBooleanAttribute(this.timeBarTimeIndicator, "hidden", hideIndicator)
+          let todayColumn = this.findColumnForDate(this.today());
+          if (todayColumn) {
+              setBooleanAttribute(todayColumn.column.timeIndicatorBox, "hidden", hideIndicator)
+          }
+          // Update the timer but only under some circumstances, otherwise
+          // it will update the wrong view or it will start without need.
+          let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+          let currView = currentView().type;
+          if (currentMode == "calendar" && currView == this.type && !hideIndicator
+              && (currView == "day" || currView == "week")) {
+              this.updateTimeIndicatorPosition(true);
+          }
+        ]]></body>
+      </method>
+
+      <method name="updateTimeIndicatorPosition">
+        <parameter name="aUpdateTheTimer"/>
+        <parameter name="aPpmChanged"/>
+        <parameter name="aViewChanged"/>
+        <body><![CDATA[
+          let now = cal.now();
+          let nowMinutes = now.hour * 60 + now.minute;
+          if (aUpdateTheTimer) {
+              let prefInt = this.mTimeIndicatorInterval;
+              if (prefInt == 0) {
+                  timeIndicator.cancel()
+                  return;
+              }
+
+              // Increase the update interval if pixels per minute is small.
+              let oldPrefInt = prefInt;
+              if (aPpmChanged && this.mPixPerMin < 0.6) {
+                  prefInt = Math.round(prefInt / this.mPixPerMin);
+              }
+              if (!aPpmChanged || aViewChanged || oldPrefInt != prefInt) {
+                  // Synchronize the timer with a multiple of the interval.
+                  let firstInterval = (prefInt - nowMinutes % prefInt) * 60 - now.second;
+                  if (timeIndicator.timer) {
+                      timeIndicator.cancel();
+                  }
+                  timeIndicator.lastView = this.id;
+                  let self = this;
+                  timeIndicator.timer = setTimeout(function() {self.updateTimeIndicatorPosition(false);
+                                                               timeIndicator.start(prefInt * 60, self);},
+                                                   firstInterval * 1000);
+                  // Set the time for the first positioning of the indicator.
+                  let time = Math.floor(nowMinutes / prefInt) * prefInt;
+                  document.getElementById("day-view").mTimeIndicatorMinutes = time;
+                  document.getElementById("week-view").mTimeIndicatorMinutes = time;
+              }
+          } else if (aUpdateTheTimer === false) {
+              // Set the time for every positioning after the first
+              document.getElementById("day-view").mTimeIndicatorMinutes = nowMinutes;
+              document.getElementById("week-view").mTimeIndicatorMinutes = nowMinutes;
+          }
+          // Update the position of the indicator.
+          let position = Math.round(this.mPixPerMin * this.mTimeIndicatorMinutes) - 1;
+          let posAttr = (this.orient == "vertical" ? "top: " : "left: ");
+          this.timeBarTimeIndicator.setAttribute("style", posAttr + position + "px;");
+          let todayColumn = this.findColumnForDate(this.today());
+          if (todayColumn) {
+              todayColumn.column.timeIndicatorBox.setAttribute("style", "margin-" + posAttr + position + "px;");
+          }
+        ]]></body>
+      </method>
+
       <method name="handlePreference">
         <parameter name="aSubject"/>
         <parameter name="aTopic"/>
         <parameter name="aPreference"/>
         <body><![CDATA[
             aSubject.QueryInterface(Components.interfaces.nsIPrefBranch2);
             switch (aPreference) {
 
                 case "calendar.view.daystarthour":
                     this.setDayStartEndMinutes(aSubject.getIntPref(aPreference) * 60,
                                                this.mDayEndMin);
-
                     this.refreshView();
                     break;
 
                 case "calendar.view.dayendhour":
                     this.setDayStartEndMinutes(this.mDayStartMin,
                                                aSubject.getIntPref(aPreference) * 60);
                     this.refreshView();
                     break;
 
                 case "calendar.view.visiblehours":
                     this.setVisibleMinutes(aSubject.getIntPref(aPreference) * 60);
                     this.refreshView();
                     break;
 
+                case "calendar.view.timeIndicatorInterval":
+                    this.setTimeIndicatorInterval(aSubject.getIntPref(aPreference));
+                    this.enableTimeIndicator();
+                    break;
+
                 default:
                     this.handleCommonPreference(aSubject, aTopic, aPreference);
                     break;
             }
             return;
         ]]></body>
       </method>
 
       <method name="onResize">
         <parameter name="aRealSelf"/>
         <body><![CDATA[
-          var self = this;
+          let self = this;
+          let isARelayout = true;
           if (aRealSelf) {
-            self = aRealSelf;
+              self = aRealSelf;
+              isARelayout = false;
           }
           let scrollbox = document.getAnonymousElementByAttribute(self, "anonid", "scrollbox");
-          var size;
+          let size;
           if (self.orient == "horizontal") {
-            size = scrollbox.boxObject.width;
+              size = scrollbox.boxObject.width;
           } else {
-            size = scrollbox.boxObject.height;
+              size = scrollbox.boxObject.height;
           }
-          var ppm = size / self.mVisibleMinutes;
+          let ppm = size / self.mVisibleMinutes;
           ppm = Math.floor(ppm * 1000) / 1000;
           if (ppm < self.mMinPixelsPerMinute) {
-            ppm = self.mMinPixelsPerMinute;
+              ppm = self.mMinPixelsPerMinute;
           }
+          let ppmChanged = (self.pixelsPerMinute != ppm);
           self.pixelsPerMinute = ppm;
           setTimeout(function(){self.scrollToMinute(self.mFirstVisibleMinute)}, 0);
 
           // Fit the weekday labels while scrolling.
           self.adjustWeekdayLength(self.getAttribute("orient") == "horizontal");
+
+          // Adjust the time indicator position and the related timer.
+          if (this.mTimeIndicatorInterval != 0) {
+              let viewChanged = isARelayout && (timeIndicator.lastView != this.id);
+              let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+              if (currentMode == "calendar" && (!timeIndicator.timer || ppmChanged || viewChanged)) {
+                  self.updateTimeIndicatorPosition(true, ppmChanged, viewChanged);
+              }
+          }
         ]]></body>
       </method>
 
       <!-- mDateList will always be sorted before being set -->
       <field name="mDateList">null</field>
       <!-- array of { date: calIDatetime, column: colbox, header: hdrbox }  -->
       <field name="mDateColumns">null</field>
       <field name="mPixPerMin">0.6</field>
@@ -2532,16 +2654,19 @@
       <field name="mSelectedDayCol">null</field>
       <field name="mSelectedDay">null</field>
       <field name="mStartMin">0*60</field>
       <field name="mEndMin">24*60</field>
       <field name="mDayStartMin">0</field>
       <field name="mDayEndMin">0</field>
       <field name="mVisibleMinutes">9*60</field>
       <field name="mClickedTime">null</field>
+      <field name="mTimeIndicatorInterval">15</field>
+      <field name="mModeHandler">null</field>
+      <field name="mTimeIndicatorMinutes">0</field>
 
       <method name="flashAlarm">
         <parameter name="aAlarmItem"/>
         <parameter name="aStop"/>
         <body><![CDATA[
           var showIndicator = cal.getPrefSafe("calendar.alarms.indicator.show", true);
           var totaltime = cal.getPrefSafe("calendar.alarms.indicator.totaltime", 3600);
 
@@ -2993,16 +3118,23 @@
         ]]></getter>
       </property>
 
       <property name="orient">
         <getter><![CDATA[return (this.getAttribute("orient") || "vertical");]]></getter>
         <setter><![CDATA[this.setAttribute("orient", val); return val;]]></setter>
       </property>
 
+      <property name="timeBarTimeIndicator" readonly="true">
+        <getter><![CDATA[
+          let timebar = document.getAnonymousElementByAttribute(this, "anonid", "timebar");
+          return document.getAnonymousElementByAttribute(timebar, "anonid", "timeIndicatorBoxTimeBar");
+        ]]></getter>
+      </property>
+
       <method name="setAttribute">
         <parameter name="aAttr"/>
         <parameter name="aVal"/>
         <body><![CDATA[
           var needsreorient = false;
           var needsrelayout = false;
           if (aAttr == "orient") {
               if (this.getAttribute("orient") != aVal)
@@ -3132,16 +3264,17 @@
 
 
           // get today's date
           var today = this.today();
           var counter = 0;
           var dayboxkids = daybox.childNodes;
           var headerboxkids = headerdaybox.childNodes;
           let labelboxkids = this.labeldaybox.childNodes;
+          let updateTimeIndicator = false;
 
           for each (var d in computedDateList) {
               var dayEventsBox;
               if (counter < dayboxkids.length) {
                   dayEventsBox = dayboxkids[counter];
                   dayEventsBox.removeAttribute("relation");
                   dayEventsBox.mEventInfos = new Array();
               } else {
@@ -3180,31 +3313,34 @@
                   labelbox = labelboxkids[counter];
                   labelbox.date = d;
               } else {
                   labelbox = createXULElement("calendar-day-label");
                   labelbox.setAttribute("orient", otherorient);
                   this.labeldaybox.appendChild(labelbox);
                   labelbox.date = d;
               }
-              // Set attributes for date relations.
+              // Set attributes for date relations and for the time indicator.
               let headerDayBox = document.getAnonymousElementByAttribute(
                                      this, "anonid", "headerdaybox");
               headerDayBox.removeAttribute("todaylastinview");
+              dayEventsBox.timeIndicatorBox.setAttribute("hidden", "true");
               switch (d.compare(today)) {
                   case -1:
                       dayHeaderBox.setAttribute("relation", "past");
                       dayEventsBox.setAttribute("relation", "past");
                       labelbox.setAttribute("relation", "past");
                       break;
                   case 0:
                       let relation_ = this.numVisibleDates == 1 ? "today1day" : "today";
                       dayHeaderBox.setAttribute("relation", relation_);
                       dayEventsBox.setAttribute("relation", relation_);
                       labelbox.setAttribute("relation", relation_);
+                      setBooleanAttribute(dayEventsBox.timeIndicatorBox, "hidden", this.mTimeIndicatorInterval == 0)
+                      updateTimeIndicator = true;
 
                       // Due to equalsize=always being set on the dayboxes
                       // parent, there are a few issues showing the border of
                       // the last daybox correctly. To work around this, we're
                       // setting an attribute we can use in CSS. For more
                       // information about this hack, see bug 455045
                       if (dayHeaderBox == headerdaybox.childNodes[headerdaybox.childNodes.length - 1] &&
                           this.numVisibleDates > 1) {
@@ -3231,16 +3367,20 @@
               while (counter < elem.childNodes.length) {
                   elem.removeChild(elem.childNodes[counter]);
               }
           }
           removeExtraKids(daybox);
           removeExtraKids(headerdaybox);
           removeExtraKids(this.labeldaybox);
 
+          if (updateTimeIndicator) {
+              this.updateTimeIndicatorPosition();
+          }
+
           // fix pixels-per-minute
           this.onResize();
           for each (let col in this.mDateColumns) {
                col.column.endLayoutBatchChange();
           }
 
           // Adjust scrollbar spacers
           this.adjustScrollBarSpacers();
--- a/calendar/base/content/calendar-views.js
+++ b/calendar/base/content/calendar-views.js
@@ -834,8 +834,26 @@ cal.navigationBar = {
         if (document.getElementById("modeBroadcaster").getAttribute("mode") == "calendar") {
             document.title = (docTitle ? docTitle + " - " : "") +
                 calGetString("brand", "brandFullName", null, "branding");
         }
         let viewTabs = document.getElementById("view-tabs");
         viewTabs.selectedIndex = getViewDeck().selectedIndex;
     }
 };
+
+/*
+ * Timer for the time indicator in day and week view.
+ */
+var timeIndicator = {
+    timer: null,
+    start: function(aInterval, aThis) {
+        timeIndicator.timer = setInterval(function() {aThis.updateTimeIndicatorPosition(false);},
+                                          aInterval * 1000);
+    },
+    cancel: function() {
+        if (timeIndicator.timer) {
+            clearTimeout(timeIndicator.timer);
+            timeIndicator.timer = null;
+        }
+    },
+    lastView: null
+}
--- a/calendar/base/content/calendar-views.xml
+++ b/calendar/base/content/calendar-views.xml
@@ -82,22 +82,46 @@
                 ]]></body>
             </method>
         </implementation>
     </binding>
 
     <binding id="calendar-week-view"
              extends="chrome://calendar/content/calendar-multiday-view.xml#calendar-multiday-view">
         <implementation implements="calICalendarView">
+            <constructor><![CDATA[
+                // add a listener for the mode change
+                let self = this;
+                this.mModeHandler = function modeHandler(event) {
+                    if (event.attrName == "mode") {
+                        self.onModeChanged(event);
+                    }
+                };
+                document.getElementById("modeBroadcaster").addEventListener("DOMAttrModified", this.mModeHandler, true);
+            ]]></constructor>
+            <destructor><![CDATA[
+                document.getElementById("modeBroadcaster").removeEventListener("DOMAttrModified", this.mModeHandler, true);
+            ]]></destructor>
+
             <property name="observerID">
                 <getter><![CDATA[
                     return "week-view-observer";
                 ]]></getter>
             </property>
 
+            <method name="onModeChanged">
+              <parameter name="aEvent"/>
+              <body><![CDATA[
+                let currentMode = document.getElementById("modeBroadcaster").getAttribute("mode");
+                if (currentMode != "calendar") {
+                    timeIndicator.cancel();
+                }
+              ]]></body>
+            </method>
+
             <!--Public methods-->
             <method name="goToDay">
                 <parameter name="aDate"/>
                 <body><![CDATA[
                     this.displayDaysOff = !this.mWorkdaysOnly;
 
                     aDate = aDate.getInTimezone(this.timezone);
                     var d1 = getWeekInfoService().getStartOfWeek(aDate);
--- a/calendar/base/themes/gnomestripe/calendar-views.css
+++ b/calendar/base/themes/gnomestripe/calendar-views.css
@@ -420,16 +420,52 @@ window[systemcolors] .fgdragbox[dragging
     text-align: center;
     overflow: hidden;
 }
 
 window[systemcolors] .fgdragbox-label {
 	color: GrayText;
 }
 
+.timeIndicator[orient="vertical"] {
+    min-width: 1px;
+    -moz-margin-start: -1px;
+    -moz-margin-end: -1px;
+    border-top: 2px solid red;
+    opacity: 0.7;
+}
+
+.timeIndicator[orient="horizontal"] {
+    min-height: 1px;
+    margin-top: -1px;
+    margin-bottom: -1px;
+    border-left: 2px solid red;
+    opacity: 0.7;
+}
+
+.timeIndicator-timeBar {
+    background-color: red;
+    position: absolute;
+    border-radius: 2px;
+}
+
+.timeIndicator-timeBar[orient="vertical"] {
+    margin-top: -1px;
+    height: 4px;
+    width: 8px;
+    right: 0px;
+}
+
+.timeIndicator-timeBar[orient="horizontal"] {
+    margin-left: -1px;
+    height: 8px;
+    width: 4px;
+    bottom: 0px;
+}
+
 .calendar-event-box-container {
     padding: 0;
     overflow: hidden;
     margin: 1px;
 }
 
 .calendar-event-box-container[categories] {
     -moz-margin-end: 0px;
@@ -540,30 +576,32 @@ window[systemcolors] .calendar-time-bar-
     -moz-border-right-colors: -moz-Field ThreeDShadow;
 }
 
 .calendar-time-bar-box-odd[orient="horizontal"],
 .calendar-time-bar-box-even[orient="horizontal"] {
     border-right: 1px solid #D2D2D2;
     border-top: 1px solid #D2D2D2;
     -moz-border-right-colors: none;
+    height: 40px; /* the same as the calendar-time-bar element */
 }
 
 window[systemcolors] .calendar-time-bar-box-odd[orient="horizontal"],
 window[systemcolors] .calendar-time-bar-box-even[orient="horizontal"] {
     border-right: 1px solid ThreeDShadow;
     border-top: 1px solid ThreeDShadow;
     -moz-border-right-colors: none;
 }
 
 .calendar-time-bar-box-odd[orient="vertical"],
 .calendar-time-bar-box-even[orient="vertical"] {
     border-bottom: 1px  transparent !important;
     border-right: 2px solid #D2D2D2;
     -moz-border-right-colors:  #FFFFFF #D2D2D2;
+    width: 10ex; /* the same as the calendar-time-bar element */
 }
 
 window[systemcolors] .calendar-time-bar-box-odd[orient="vertical"],
 window[systemcolors] .calendar-time-bar-box-even[orient="vertical"] {
     border-right: 2px solid ThreeDShadow;
     -moz-border-right-colors: -moz-Field ThreeDShadow;
 }
 
--- a/calendar/base/themes/pinstripe/calendar-views.css
+++ b/calendar/base/themes/pinstripe/calendar-views.css
@@ -286,16 +286,52 @@ calendar-header-container[weekend="true"
 }
 
 .fgdragbox-label {
     font-weight: bold;
     text-align: center;
     overflow: hidden;
 }
 
+.timeIndicator[orient="vertical"] {
+    min-width: 1px;
+    -moz-margin-start: -1px;
+    -moz-margin-end: -1px;
+    border-top: 2px solid red;
+    opacity: 0.7;
+}
+
+.timeIndicator[orient="horizontal"] {
+    min-height: 1px;
+    margin-top: -1px;
+    margin-bottom: -1px;
+    border-left: 2px solid red;
+    opacity: 0.7;
+}
+
+.timeIndicator-timeBar {
+    background-color: red;
+    position: absolute;
+    border-radius: 2px;
+}
+
+.timeIndicator-timeBar[orient="vertical"] {
+    margin-top: -1px;
+    height: 4px;
+    width: 8px;
+    right: 0px;
+}
+
+.timeIndicator-timeBar[orient="horizontal"] {
+    margin-left: -1px;
+    height: 8px;
+    width: 4px;
+    bottom: 0px;
+}
+
 .calendar-event-box-container {
     padding: 0;
     overflow: hidden;
     margin: 1px;
 }
 
 .calendar-event-box-container[categories] {
     -moz-margin-end: 0px;
@@ -394,23 +430,25 @@ calendar-time-bar[orient="horizontal"] {
     -moz-border-right-colors:  #FFFFFF #D2D2D2;
 }
 
 .calendar-time-bar-box-odd[orient="horizontal"],
 .calendar-time-bar-box-even[orient="horizontal"] {
     border-right: 1px solid #D2D2D2;
     border-top: 1px solid #D2D2D2;
     -moz-border-right-colors: none;
+    height: 40px; /* the same as the calendar-time-bar element */
 }
 
 .calendar-time-bar-box-odd[orient="vertical"],
 .calendar-time-bar-box-even[orient="vertical"] {
     border-bottom: 1px  transparent !important;
     border-right: 2px solid #D2D2D2;
     -moz-border-right-colors:  #FFFFFF #D2D2D2;
+    width: 10ex; /* the same as the calendar-time-bar element */
 }
 
 /** End time bar **/
 
 calendar-multiday-view {
     background-color: #FFFFFF;
     padding: 0px;
 }
--- a/calendar/base/themes/winstripe/calendar-views.css
+++ b/calendar/base/themes/winstripe/calendar-views.css
@@ -420,16 +420,52 @@ window[systemcolors] .fgdragbox[dragging
     text-align: center;
     overflow: hidden;
 }
 
 window[systemcolors] .fgdragbox-label {
 	color: GrayText;
 }
 
+.timeIndicator[orient="vertical"] {
+    min-width: 1px;
+    -moz-margin-start: -1px;
+    -moz-margin-end: -1px;
+    border-top: 2px solid red;
+    opacity: 0.7;
+}
+
+.timeIndicator[orient="horizontal"] {
+    min-height: 1px;
+    margin-top: -1px;
+    margin-bottom: -1px;
+    border-left: 2px solid red;
+    opacity: 0.7;
+}
+
+.timeIndicator-timeBar {
+    background-color: red;
+    position: absolute;
+    border-radius: 2px;
+}
+
+.timeIndicator-timeBar[orient="vertical"] {
+    margin-top: -1px;
+    height: 4px;
+    width: 8px;
+    right: 0px;
+}
+
+.timeIndicator-timeBar[orient="horizontal"] {
+    margin-left: -1px;
+    height: 8px;
+    width: 4px;
+    bottom: 0px;
+}
+
 .calendar-event-box-container {
     padding: 0;
     overflow: hidden;
     margin: 1px;
 }
 
 .calendar-event-box-container[categories] {
     -moz-margin-end: 0px;
@@ -540,30 +576,32 @@ window[systemcolors] .calendar-time-bar-
     -moz-border-right-colors: -moz-Field ThreeDShadow;
 }
 
 .calendar-time-bar-box-odd[orient="horizontal"],
 .calendar-time-bar-box-even[orient="horizontal"] {
     border-right: 1px solid #D2D2D2;
     border-top: 1px solid #D2D2D2;
     -moz-border-right-colors: none;
+    height: 40px; /* the same as the calendar-time-bar element */
 }
 
 window[systemcolors] .calendar-time-bar-box-odd[orient="horizontal"],
 window[systemcolors] .calendar-time-bar-box-even[orient="horizontal"] {
     border-right: 1px solid ThreeDShadow;
     border-top: 1px solid ThreeDShadow;
     -moz-border-right-colors: none;
 }
 
 .calendar-time-bar-box-odd[orient="vertical"],
 .calendar-time-bar-box-even[orient="vertical"] {
     border-bottom: 1px  transparent !important;
     border-right: 2px solid #D2D2D2;
     -moz-border-right-colors:  #FFFFFF #D2D2D2;
+    width: 10ex; /* the same as the calendar-time-bar element */
 }
 
 window[systemcolors] .calendar-time-bar-box-odd[orient="vertical"],
 window[systemcolors] .calendar-time-bar-box-even[orient="vertical"] {
     border-right: 2px solid ThreeDShadow;
     -moz-border-right-colors: -moz-Field ThreeDShadow;
 }
 
--- a/calendar/lightning/content/lightning.js
+++ b/calendar/lightning/content/lightning.js
@@ -117,16 +117,19 @@ pref("calendar.week.d6saturdaysoff", tru
 
 // start and end work hour for day and week views
 pref("calendar.view.daystarthour", 8);
 pref("calendar.view.dayendhour", 17);
 
 // number of visible hours for day and week views
 pref("calendar.view.visiblehours", 9);
 
+// time indicator update interval in minutes (0 = no indicator)
+pref("calendar.view.timeIndicatorInterval", 15);
+
 // If true, mouse scrolling via shift+wheel will be enabled
 pref("calendar.view.mousescroll", true);
 
 // Do not set this!  If it's not there, then we guess the system timezone
 //pref("calendar.timezone.local", "");
 
 // categories settings
 // XXX One day we might want to move this to a locale specific file