Fix bug 404900 - Add Accept/Decline to Calendar item's context menu. r=nomisvai
authorPhilipp Kewisch <mozilla@kewis.ch>
Sun, 13 Feb 2011 11:45:00 +0100
changeset 7214 c25fd0381b624928a52c74bb626fca6c03d3a5f0
parent 7213 87fb9473a51d27431fbf5a672e37061f7f2150e2
child 7215 d8ee7cd9a11c303ae2f33ce6f18ec0f738b4d089
push idunknown
push userunknown
push dateunknown
reviewersnomisvai
bugs404900
Fix bug 404900 - Add Accept/Decline to Calendar item's context menu. r=nomisvai
calendar/base/content/calendar-common-sets.js
calendar/base/content/calendar-common-sets.xul
calendar/base/content/calendar-item-editing.js
calendar/base/content/calendar-task-tree.xml
calendar/base/themes/pinstripe/calendar-views.css
calendar/base/themes/winstripe/calendar-views.css
calendar/locales/en-US/chrome/calendar/calendar.dtd
--- a/calendar/base/content/calendar-common-sets.js
+++ b/calendar/base/content/calendar-common-sets.js
@@ -94,16 +94,18 @@ var calendarController = {
         "calendar_priority-0_command": true,
         "calendar_priority-9_command": true,
         "calendar_priority-5_command": true,
         "calendar_priority-1_command": true,
         "calendar_general-priority_command": true,
         "calendar_general-progress_command": true,
         "calendar_task_category_command": true,
 
+        "calendar_attendance_command": true,
+
         // Pseudo commands
         "calendar_in_foreground": true,
         "calendar_in_background": true,
         "calendar_mode_calendar": true,
         "calendar_mode_task": true
     },
 
     updateCommands: function cC_updateCommands() {
@@ -180,16 +182,23 @@ var calendarController = {
                 return this.isInMode("calendar") &&
                        currentView().supportsWorkdaysOnly;
             case "calendar_publish_selected_events_command":
                 return this.item_selected;
 
             case "calendar_reload_remote_calendar":
                 return !this.no_network_calendars && !this.offline;
 
+            case "calendar_attendance_command":
+                // Small hack, we want to hide instead of disable.
+                let attendSel = this.item_selected && this.selected_events_invitation;
+                setBooleanAttribute("calendar_attendance_command", "hidden", !attendSel);
+                return attendSel;
+                break;
+
             // The following commands all just need the calendar in foreground,
             // make sure you take care when changing things here.
             case "calendar_view_next_command":
             case "calendar_view_prev_command":
             case "calendar_in_foreground":
                 return this.isCalendarInForeground();
             case "calendar_in_background":
                 return !this.isCalendarInForeground();
@@ -382,16 +391,19 @@ var calendarController = {
                 switchCalendarView("week", true);
                 break;
             case "calendar_multiweek-view_command":
                 switchCalendarView("multiweek", true);
                 break;
             case "calendar_month-view_command":
                 switchCalendarView("month", true);
                 break;
+            case "calendar_attendance_command":
+                // This command is actually handled inline, since it takes a value
+                break;
 
             default:
                 if (this.defaultController && !this.isCalendarInForeground()) {
                     // If calendar is not in foreground, let the default controller take
                     // care. If we don't have a default controller (i.e sunbird), just
                     // continue.
                     this.defaultController.doCommand(aCommand);
                     return;
@@ -420,50 +432,67 @@ var calendarController = {
                 return !isSunbird() && (gCurrentMode && gCurrentMode == "task");
        }
     },
 
     onSelectionChanged: function cC_onSelectionChanged(aEvent) {
         var selectedItems = aEvent.detail;
         calendarController.item_selected = selectedItems && (selectedItems.length > 0);
 
-        var selLength = (selectedItems === undefined ? 0 : selectedItems.length);
-        var selected_events_readonly = 0;
-        var selected_events_requires_network = 0;
+        let selLength = (selectedItems === undefined ? 0 : selectedItems.length);
+        let selected_events_readonly = 0;
+        let selected_events_requires_network = 0;
+        let selected_events_invitation = 0;
+
         if (selLength > 0) {
             for each (var item in selectedItems) {
                 if (item.calendar.readOnly) {
                     selected_events_readonly++;
                 }
                 if (item.calendar.getProperty("requiresNetwork")) {
                     selected_events_requires_network++;
                 }
+
+                if (cal.isInvitation(item)) {
+                    selected_events_invitation++;
+                } else if (item.organizer) {
+                    // If we are the organizer and there are attendees, then
+                    // this is likely also an invitation.
+                    let calOrgId = item.calendar.getProperty("organizerId");
+                    if (item.organizer.id == calOrgId && item.getAttendees({}).length) {
+                        selected_events_invitation++;
+                    }
+                }
             }
         }
 
         calendarController.selected_events_readonly =
               (selected_events_readonly == selLength);
 
         calendarController.selected_events_requires_network =
               (selected_events_requires_network == selLength);
+        calendarController.selected_events_invitation =
+              (selected_events_invitation == selLength);
+
         calendarController.updateCommands();
         calendarController2.updateCommands();
         if(!isSunbird()) {
             document.commandDispatcher.updateCommands('mail-toolbar');
         }
     },
 
     /**
      * Condition Helpers
      */
 
     // These attributes will be set up manually.
     item_selected: false,
     selected_events_readonly: false,
     selected_events_requires_network: false,
+    selected_events_invitation: false,
 
     /**
      * Returns a boolean indicating if its possible to write items to any
      * calendar.
      */
     get writable() {
         return !this.all_readonly &&
                (!this.offline || (this.has_local_calendars &&
@@ -777,16 +806,50 @@ function setupContextItemType(event, ite
         adaptModificationMenuItem("calendar-item-context-menu-delete-menuitem", "Event");
     } else if (items.length && isToDo(items[0])) {
         event.target.setAttribute("type", "todo");
         adaptModificationMenuItem("calendar-item-context-menu-delete-menuitem", "Task");
     } else {
         event.target.removeAttribute("type");
         adaptModificationMenuItem("calendar-item-context-menu-delete-menuitem", "Item");
     }
+
+    let menu = document.getElementById("calendar-item-context-menu-attendance-menu");
+    let allSingle = items.every(function(x) !x.recurrenceId);
+    setElementValue(menu, allSingle ? "single" : "recurring", "itemType");
+
+    // Set up the attendance menu
+    function getInvStat(item) {
+        let attendee = null;
+        if (cal.isInvitation(item)) {
+            attendee = cal.getInvitedAttendee(item);
+        } else if (item.organizer) {
+            let calOrgId = item.calendar.getProperty("organizerId");
+            if (calOrgId == item.organizer.id && item.getAttendees({}).length) {
+                attendee = item.organizer;
+            }
+        }
+        return attendee && attendee.participationStatus;
+    }
+
+    let firstStatusOccurrences = items.length && getInvStat(items[0]);
+    let firstStatusParents = items.length && getInvStat(items[0].parentItem);
+    let sameStatusOccurrences = items.every(function (x) getInvStat(x) == firstStatusOccurrences);
+    let sameStatusParents = items.every(function (x) getInvStat(x.parentItem) == firstStatusParents)
+
+    let occurrenceChildren = menu.getElementsByAttribute("value", firstStatusOccurrences);
+    let parentsChildren = menu.getElementsByAttribute("value", firstStatusParents);
+    if (sameStatusOccurrences && occurrenceChildren[0]) {
+        occurrenceChildren[0].setAttribute("checked", "true");
+    }
+
+    if (sameStatusParents && parentsChildren[1]) {
+        parentsChildren[1].setAttribute("checked", "true");
+    }
+
     return true;
 }
 
 /**
  * Shows the given date in the current view, if in calendar mode.
  *
  * XXX This function is misplaced, should go to calendar-views.js or a minimonth
  * specific js file.
--- a/calendar/base/content/calendar-common-sets.xul
+++ b/calendar/base/content/calendar-common-sets.xul
@@ -123,16 +123,18 @@
     <!-- this is a pseudo-command that is disabled when in calendar mode -->
     <command id="calendar_in_foreground"/>
     <!-- this is a pseudo-command that is disabled when not in calendar mode -->
     <command id="calendar_in_background"/>
 
     <!-- These commands are enabled when in calendar or task mode, respectively -->
     <command id="calendar_mode_calendar"/>
     <command id="calendar_mode_task"/>
+
+    <command id="calendar_attendance_command"/>
   </commandset>
 
   <keyset id="calendar-keys">
 
 
 // For linux tab switching reservers alt+number, where on windows that's ctrl.
 // Use the available modifiers for each platform.
 // Can't use the OPTION key on OSX, so we will use SHIFT+OPTION on the Mac.
@@ -235,16 +237,72 @@
         </menupopup>
       </menu>
       <menuseparator id="calendar-menuseparator-before-delete"/>
       <!-- the label and accesskey of the following menuitem is set during runtime,
            and depends on wether the item is a task or an event-->
       <menuitem id="calendar-item-context-menu-delete-menuitem"
                 key="calendar-delete-item-key"
                 observes="calendar_delete_event_command"/>
+      <menu id="calendar-item-context-menu-attendance-menu"
+            label="&calendar.context.attendance.menu.label;"
+            accesskey="&calendar.context.attendance.menu.accesskey;"
+            oncommand="setContextPartstat(event.target.value, event.target.getAttribute('scope'), currentView().getSelectedItems({}))"
+            observes="calendar_attendance_command">
+        <menupopup id="calendar-item-context-menu-attendance-menupopup">
+          <label id="calendar-item-context-attendance-thisoccurrence-label"
+                 class="calendar-context-heading-label"
+                 scope="all-occurrences"
+                 value="&calendar.context.attendance.occurrence.label;"/>
+          <menuitem id="calendar-item-context-menu-attend-accept-menuitem"
+                    type="radio"
+                    scope="this-occurrence"
+                    name="calendar-item-context-attendance"
+                    label="&read.only.accept.label;" value="ACCEPTED"/>
+          <menuitem id="calendar-item-context-menu-attend-tentative-menuitem"
+                    type="radio"
+                    scope="this-occurrence"
+                    name="calendar-item-context-attendance"
+                    label="&read.only.tentative.label;" value="TENTATIVE"/>
+          <menuitem id="calendar-item-context-menu-attend-declined-menuitem"
+                    type="radio"
+                    scope="this-occurrence"
+                    name="calendar-item-context-attendance"
+                    label="&read.only.decline.label;" value="DECLINED"/>
+          <menuitem id="calendar-item-context-menu-attend-needsaction-menuitem"
+                    type="radio"
+                    scope="this-occurrence"
+                    name="calendar-item-context-attendance"
+                    label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+          <label id="calendar-item-context-attendance-alloccurrence-label"
+                 class="calendar-context-heading-label"
+                 scope="all-occurrences"
+                 value="&calendar.context.attendance.all.label;"/>
+          <menuitem id="calendar-item-context-menu-attend-accept-all-menuitem"
+                    type="radio"
+                    scope="all-occurrences"
+                    name="calendar-item-context-attendance-all"
+                    label="&read.only.accept.label;" value="ACCEPTED"/>
+          <menuitem id="calendar-item-context-menu-attend-tentative-all-menuitem"
+                    type="radio"
+                    scope="all-occurrences"
+                    name="calendar-item-context-attendance-all"
+                    label="&read.only.tentative.label;" value="TENTATIVE"/>
+          <menuitem id="calendar-item-context-menu-attend-declined-all-menuitem"
+                    type="radio"
+                    scope="all-occurrences"
+                    name="calendar-item-context-attendance-all"
+                    label="&read.only.decline.label;" value="DECLINED"/>
+          <menuitem id="calendar-item-context-menu-attend-needsaction-all-menuitem"
+                    type="radio"
+                    scope="all-occurrences"
+                    name="calendar-item-context-attendance-all"
+                    label="&read.only.needs.action.label;" value="NEEDS-ACTION"/>
+        </menupopup>
+      </menu>
     </menupopup>
 
     <!-- CALENDAR VIEW CONTEXT MENU -->
     <menupopup id="calendar-view-context-menu">
       <menuitem id="calendar-view-context-menu-newevent"
                 label="&calendar.context.newevent.label;"
                 observes="calendar_new_event_context_command"
                 accesskey="&calendar.context.newevent.accesskey;"
--- a/calendar/base/content/calendar-item-editing.js
+++ b/calendar/base/content/calendar-item-editing.js
@@ -486,8 +486,49 @@ function canRedo() {
 
 /**
  * Update the undo and redo commands.
  */
 function updateUndoRedoMenu() {
     goUpdateCommand("cmd_undo");
     goUpdateCommand("cmd_redo");
 }
+
+function setContextPartstat(value, scope, items) {
+    startBatchTransaction();
+    try {
+        for each (let oldItem in items) {
+            if (scope == "all-occurrences") {
+                oldItem = oldItem.parentItem;
+            }
+            let attendee = null;
+            if (cal.isInvitation(oldItem)) {
+                // Check for the invited attendee first, this is more important
+                attendee = cal.getInvitedAttendee(oldItem);
+            } else if (oldItem.organizer && oldItem.getAttendees({}).length) {
+                // Now check the organizer. This should be done last.
+                let calOrgId = oldItem.calendar.getProperty("organizerId");
+                if (calOrgId == oldItem.organizer.id) {
+                    attendee = oldItem.organizer;
+                }
+            }
+
+            if (attendee) {
+                let newItem = oldItem.clone();
+                let newAttendee = attendee.clone();
+
+                newAttendee.participationStatus = value;
+                if (newAttendee.isOrganizer) {
+                    newItem.organizer = newAttendee;
+                } else {
+                    newItem.removeAttendee(attendee);
+                    newItem.addAttendee(newAttendee);
+                }
+
+                doTransaction('modify', newItem, newItem.calendar, oldItem, null);
+            }
+        }
+    } catch (e) {
+        cal.ERROR("Error settinge partstat: " + e);
+    } finally {
+        endBatchTransaction();
+    }
+}
--- a/calendar/base/content/calendar-task-tree.xml
+++ b/calendar/base/content/calendar-task-tree.xml
@@ -1137,17 +1137,19 @@
         ]]></body>
       </method>
 
     </implementation>
 
     <handlers>
       <handler event="select"><![CDATA[
         this.mTreeView.onSelect(event);
-        calendarController.onSelectionChanged({detail:this.selectedTasks});
+        if (calendarController.todo_tasktree_focused) {
+          calendarController.onSelectionChanged({detail:this.selectedTasks});
+        }
       ]]></handler>
       <handler event="dblclick" button="0"><![CDATA[
         this.mTreeView.onDoubleClick(event);
       ]]></handler>
       <handler event="focus"><![CDATA[
         calendarController.onSelectionChanged({detail:this.selectedTasks});
         calendarController.todo_tasktree_focused = true;
       ]]></handler>
--- a/calendar/base/themes/pinstripe/calendar-views.css
+++ b/calendar/base/themes/pinstripe/calendar-views.css
@@ -645,16 +645,24 @@ description.tooltipBody {
 #calendar-view-context-menu[type="mixed"] .todo-only,
 #calendar-item-context-menu[type="event"] .todo-only,
 #calendar-item-context-menu[type="todo"] .event-only,
 #calendar-item-context-menu[type="mixed"] .event-only,
 #calendar-item-context-menu[type="mixed"] .todo-only {
     display: none;
 }
 
+#calendar-item-context-menu-attendance-menu[itemType="single"] > menupopup > *[scope="all-occurrences"] {
+    display: none;
+}
+
+.calendar-context-heading-label {
+    font-weight: bold;
+}
+
 calendar-event-box,
 calendar-editable-item,
 calendar-month-day-box-item {
     opacity: 0.99;
     /* Do not change next line, since it would break item selection */
     -moz-user-focus: normal;
 }
 
--- a/calendar/base/themes/winstripe/calendar-views.css
+++ b/calendar/base/themes/winstripe/calendar-views.css
@@ -645,16 +645,24 @@ description.tooltipBody {
 #calendar-view-context-menu[type="mixed"] .todo-only,
 #calendar-item-context-menu[type="event"] .todo-only,
 #calendar-item-context-menu[type="todo"] .event-only,
 #calendar-item-context-menu[type="mixed"] .event-only,
 #calendar-item-context-menu[type="mixed"] .todo-only {
     display: none;
 }
 
+#calendar-item-context-menu-attendance-menu[itemType="single"] > menupopup > *[scope="all-occurrences"] {
+    display: none;
+}
+
+.calendar-context-heading-label {
+    font-weight: bold;
+}
+
 calendar-event-box,
 calendar-editable-item,
 calendar-month-day-box-item {
     opacity: 0.99;
     /* Do not change next line, since it would break item selection */
     -moz-user-focus: normal;
 }
 
--- a/calendar/locales/en-US/chrome/calendar/calendar.dtd
+++ b/calendar/locales/en-US/chrome/calendar/calendar.dtd
@@ -198,16 +198,20 @@
 <!ENTITY calendar.context.cutevent.label              "Cut">
 <!ENTITY calendar.context.cutevent.accesskey          "t">
 <!ENTITY calendar.context.copyevent.label             "Copy">
 <!ENTITY calendar.context.copyevent.accesskey         "C">
 <!ENTITY calendar.context.pasteevent.label            "Paste">
 <!ENTITY calendar.context.pasteevent.accesskey        "P">
 <!ENTITY calendar.context.button.label                "Today Pane" >
 <!ENTITY calendar.context.button.accesskey            "T" >
+<!ENTITY calendar.context.attendance.menu.label       "Attendance" >
+<!ENTITY calendar.context.attendance.menu.accesskey   "d" >
+<!ENTITY calendar.context.attendance.occurrence.label "This Occurrence" >
+<!ENTITY calendar.context.attendance.all.label        "All Occurrences" >
 
 <!-- Task Context Menu -->
 <!ENTITY calendar.context.progress.label              "Progress">
 <!ENTITY calendar.context.progress.accesskey          "P">
 <!ENTITY calendar.context.priority.label              "Priority">
 <!ENTITY calendar.context.priority.accesskey          "r">
 
 <!ENTITY percnt "&#38;#37;" ><!--=percent sign-->