Bug 393084 - Allow to cut/copy the series or a single occurrence for repeating items. r=philipp
authorMakeMyDay <makemyday@gmx-topmail.de>
Sun, 18 Feb 2018 20:38:33 +0100
changeset 31136 6bb845d0c8fcabdb4d3ba872c38f113f4caeaf56
parent 31135 cf8afa5f2ce8f0c3a259a2cbe7d8841311ebbc20
child 31137 4644f1f698d895f4f0fdc3db53e891f5479f9868
push id383
push userclokep@gmail.com
push dateMon, 07 May 2018 21:52:48 +0000
reviewersphilipp
bugs393084
Bug 393084 - Allow to cut/copy the series or a single occurrence for repeating items. r=philipp
calendar/base/content/calendar-clipboard.js
calendar/base/content/calendar-item-editing.js
calendar/base/content/dialogs/calendar-occurrence-prompt.xul
calendar/base/themes/common/calendar-occurrence-prompt.css
calendar/locales/en-US/chrome/calendar/calendar-occurrence-prompt.properties
--- a/calendar/base/content/calendar-clipboard.js
+++ b/calendar/base/content/calendar-clipboard.js
@@ -36,39 +36,47 @@ function canPaste() {
                                                      Components.interfaces.nsIClipboard.kGlobalClipboard);
 }
 
 /**
  * Copy the ics data of the current view's selected events to the clipboard and
  * deletes the events on success
  */
 function cutToClipboard() {
-    if (copyToClipboard()) {
+    if (copyToClipboard(null, true)) {
         deleteSelectedItems();
     }
 }
 
 /**
  * Copy the ics data of the items in calendarItemArray to the clipboard. Fills
  * both text/unicode and text/calendar mime types.
  *
- * @param calendarItemArray     (optional) an array of items to copy. If not
+ * @param aCalendarItemArray    (optional) an array of items to copy. If not
  *                                passed, the current view's selected items will
  *                                be used.
+ * @param aCutMode              (optional) set to true, if this is a cut operation
  * @return                      A boolean indicating if the operation succeeded.
  */
-function copyToClipboard(calendarItemArray) {
-    if (!calendarItemArray) {
-        calendarItemArray = getSelectedItems();
-    }
-
+function copyToClipboard(aCalendarItemArray=null, aCutMode=false) {
+    let calendarItemArray = aCalendarItemArray || getSelectedItems();
     if (!calendarItemArray.length) {
         cal.LOG("[calendar-clipboard] No items to copy.");
         return false;
     }
+    let [targetItems, , response] = promptOccurrenceModification(
+        calendarItemArray,
+        true,
+        aCutMode ? "cut" : "copy"
+    );
+    if (!response) {
+        // The user canceled the dialog, bail out
+        return false;
+    }
+    calendarItemArray = targetItems;
 
     let icsSerializer = Components.classes["@mozilla.org/calendar/ics-serializer;1"]
                                   .createInstance(Components.interfaces.calIIcsSerializer);
     icsSerializer.addItems(calendarItemArray, calendarItemArray.length);
     let icsString = icsSerializer.serializeToString();
 
     let clipboard = Services.clipboard;
     let trans = Components.classes["@mozilla.org/widget/transferable;1"]
--- a/calendar/base/content/calendar-item-editing.js
+++ b/calendar/base/content/calendar-item-editing.js
@@ -524,24 +524,27 @@ function openEventDialog(calendarItem, c
  * recurrence rules end (UNTIL) just before the given occurrence. If
  * aNeedsFuture is specified, a new item is made from the part that was stripped
  * off the passed item.
  *
  * EXDATEs and RDATEs that do not fit into the items recurrence are removed. If
  * the modified item or the future item only consist of a single occurrence,
  * they are changed to be single items.
  *
- * @param aItem                         The item to check.
+ * @param aItem                         The item or array of items to check.
  * @param aNeedsFuture                  If true, the future item is parsed.
  *                                        This parameter can for example be
  *                                        false if a deletion is being made.
  * @param aAction                       Either "edit" or "delete". Sets up
  *                                          the labels in the occurrence prompt
  * @return [modifiedItem, futureItem, promptResponse]
- *                                      If "this and all following" was chosen,
+ *                                      modifiedItem is a single item or array
+ *                                        of items depending on the past aItem
+ *
+ *                                        If "this and all following" was chosen,
  *                                        an array containing the item *until*
  *                                        the given occurrence (modifiedItem),
  *                                        and the item *after* the given
  *                                        occurrence (futureItem).
  *
  *                                        If any other option was chosen,
  *                                        futureItem is null  and the
  *                                        modifiedItem is either the parent item
@@ -552,56 +555,60 @@ function openEventDialog(calendarItem, c
  *                                        response of the dialog as a constant.
  */
 function promptOccurrenceModification(aItem, aNeedsFuture, aAction) {
     const CANCEL = 0;
     const MODIFY_OCCURRENCE = 1;
     const MODIFY_FOLLOWING = 2;
     const MODIFY_PARENT = 3;
 
-    let futureItem = false;
-    let pastItem;
+    let futureItems = false;
+    let pastItems = [];
+    let returnItem = null;
     let type = CANCEL;
+    let items = Array.isArray(aItem) ? aItem : [aItem];
 
     // Check if this actually is an instance of a recurring event
-    if (aItem == aItem.parentItem) {
+    if (items.every(item => item == item.parentItem)) {
         type = MODIFY_PARENT;
-    } else if (aItem.parentItem.recurrenceInfo.getExceptionFor(aItem.recurrenceId)) {
+    } else if (items.every(item => item.parentItem.recurrenceInfo.getExceptionFor(item.recurrenceId))) {
         // If the user wants to edit an occurrence which is already an exception
         // always edit this single item.
         // XXX  Why? I think its ok to ask also for exceptions.
         type = MODIFY_OCCURRENCE;
-    } else {
+    } else if (aItem && items.length) {
         // Prompt the user. Setting modal blocks the dialog until it is closed. We
         // use rv to pass our return value.
-        let rv = { value: CANCEL, item: aItem, action: aAction };
+        let rv = { value: CANCEL, items: items, action: aAction };
         window.openDialog("chrome://calendar/content/calendar-occurrence-prompt.xul",
                           "PromptOccurrenceModification",
                           "centerscreen,chrome,modal,titlebar",
                           rv);
         type = rv.value;
     }
 
     switch (type) {
         case MODIFY_PARENT:
-            pastItem = aItem.parentItem;
+            pastItems = items.map(item => item.parentItem);
             break;
         case MODIFY_FOLLOWING:
             // TODO tbd in a different bug
             throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
         case MODIFY_OCCURRENCE:
-            pastItem = aItem;
+            pastItems = items;
             break;
         case CANCEL:
             // Since we have not set past or futureItem, the return below will
             // take care.
             break;
     }
-
-    return [pastItem, futureItem, type];
+    if (aItem) {
+        returnItem = Array.isArray(aItem) ? pastItems : pastItems[0];
+    }
+    return [returnItem, futureItems, type];
 }
 
 // Undo/Redo code
 
 /**
  * Helper to return the transaction manager service.
  *
  * @return      The calITransactionManager service.
--- a/calendar/base/content/dialogs/calendar-occurrence-prompt.xul
+++ b/calendar/base/content/dialogs/calendar-occurrence-prompt.xul
@@ -14,53 +14,75 @@
         ondialogcancel="return exitOccurrenceDialog(0)"
         ondialogaccept="exitOccurrenceDialog(1)"
         onload="onLoad()"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         xmlns:xhtml2="http://www.w3.org/TR/xhtml2"
         xmlns:wairole="http://www.w3.org/2005/01/wai-rdf/GUIRoleTaxonomy#"
         xhtml2:role="wairole:alertdialog">
   <script type="application/javascript"><![CDATA[
-    ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
-    function exitOccurrenceDialog(aReturnValue) {
-      window.arguments[0].value = aReturnValue;
-      window.close();
-      return true;
-    }
+      ChromeUtils.import("resource://calendar/modules/calUtils.jsm");
+
+      function exitOccurrenceDialog(aReturnValue) {
+          window.arguments[0].value = aReturnValue;
+          window.close();
+          return true;
+      }
+
+      function getDString(aKey) {
+          return cal.calGetString("calendar-occurrence-prompt", aKey);
+      }
 
-    function getDialogString(key) {
-      return cal.calGetString("calendar-occurrence-prompt", key);
-    }
-
-    function onLoad() {
-      var action = window.arguments[0].action || "edit";
-      var itemType = (cal.item.isEvent(window.arguments[0].item) ? "event" : "task");
-
-      // Set up title
-      document.title = getDialogString("windowtitle." + itemType + "." + action);
-      document.getElementById("title-label").value = window.arguments[0].item.title;
+      function onLoad() {
+          let action = window.arguments[0].action || "edit";
+          // the calling code prevents sending no items
+          let multiple = (window.arguments[0].items.length == 1)
+                       ? "single" : "multiple";
+          let itemType;
+          for (let item of window.arguments[0].items) {
+              let type = cal.item.isEvent(item) ? "event" : "task";
+              if (itemType != type) {
+                  itemType = (itemType) ? "mixed" : type;
+              }
+          }
 
-      // Set up header
-      document.getElementById("isrepeating-label").value =
-        getDialogString("header.isrepeating." + itemType + ".label");
+          // Set up title and type label
+          document.title = getDString(`windowtitle.${itemType}.action`);
+          let title = document.getElementById("title-label");
+          if (multiple == "multiple") {
+              title.value = getDString("windowtitle.multipleitems");
+              document.getElementById("isrepeating-label").value = getDString(
+                  `header.containsrepeating.${itemType}.label`
+              );
+          } else {
+              title.value = window.arguments[0].items[0].title;
+              document.getElementById("isrepeating-label").value = getDString(
+                  `header.isrepeating.${itemType}.label`
+              );
+          }
 
-      // Set up buttons
-      document.getElementById("accept-buttons-box")
-              .setAttribute("action", action);
-      document.getElementById("accept-buttons-box")
-              .setAttribute("type", itemType);
+          // Set up buttons
+          document.getElementById("accept-buttons-box")
+                  .setAttribute("action", action);
+          document.getElementById("accept-buttons-box")
+                  .setAttribute("type", itemType);
+
+          document.getElementById("accept-occurrence-button").label = getDString(
+              `buttons.${multiple}.occurrence.${action}.label`
+          );
 
-      document.getElementById("accept-occurrence-button").label =
-        getDialogString("buttons.occurrence." + action + ".label");
+          document.getElementById("accept-allfollowing-button").label = getDString(
+              `buttons.${multiple}.allfollowing.${action}.label`
+          );
+          document.getElementById("accept-parent-button").label = getDString(
+              `buttons.${multiple}.parent.${action}.label`
+          );
 
-      document.getElementById("accept-allfollowing-button").label =
-        getDialogString("buttons.allfollowing." + action + ".label");
-      document.getElementById("accept-parent-button").label =
-        getDialogString("buttons.parent." + action + ".label");
-    }
+          window.sizeToContent();
+      }
   ]]></script>
 
   <vbox id="occurrence-prompt-header" pack="center">
     <label id="title-label" crop="end"/>
     <label id="isrepeating-label"/>
   </vbox>
 
   <vbox id="accept-buttons-box" flex="1" pack="center">
--- a/calendar/base/themes/common/calendar-occurrence-prompt.css
+++ b/calendar/base/themes/common/calendar-occurrence-prompt.css
@@ -36,20 +36,23 @@
   width: 18px;
   height: 18px;
 }
 
 .occurrence-accept-buttons > .button-box > .button-text {
   margin: 0 3px !important;
 }
 
+#accept-buttons-box[type="mixed"] > #accept-occurrence-button,
 #accept-buttons-box[type="event"] > #accept-occurrence-button {
   list-style-image: url(chrome://calendar-common/skin/calendar-occurrence.svg#event-single);
 }
 
+#accept-buttons-box[type="mixed"] > #accept-parent-button,
+#accept-buttons-box[type="mixed"] > #accept-allfollowing-button,
 #accept-buttons-box[type="event"] > #accept-parent-button,
 #accept-buttons-box[type="event"] > #accept-allfollowing-button {
   list-style-image: url(chrome://calendar-common/skin/calendar-occurrence.svg#event-all);
 }
 
 #accept-buttons-box[type="task"] > .occurrence-accept-buttons {
   list-style-image: url(chrome://calendar-common/skin/calendar-occurrence.svg#task-single);
 }
--- a/calendar/locales/en-US/chrome/calendar/calendar-occurrence-prompt.properties
+++ b/calendar/locales/en-US/chrome/calendar/calendar-occurrence-prompt.properties
@@ -1,20 +1,53 @@
 # 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/.
 
 header.isrepeating.event.label=is a repeating event
-header.isrepeating.task.label=is a repeating task 
+header.isrepeating.task.label=is a repeating task
+header.containsrepeating.event.label=contains repeating events
+header.containsrepeating.task.label=contains repeating tasks
+header.containsrepeating.mixed.label=contains repeating items of different type
 
+windowtitle.event.copy=Copy Repeating Event
+windowtitle.task.copy=Copy Repeating Task
+windowtitle.mixed.copy=Copy Repeating Items
+windowtitle.event.cut=Cut Repeating Event
+windowtitle.task.cut=Cut Repeating Task
+windowtitle.mixed.cut=Cut Repeating Items
 windowtitle.event.delete=Delete Repeating Event
 windowtitle.task.delete=Delete Repeating Task
+windowtitle.mixed.delete=Delete Repeating Items
 windowtitle.event.edit=Edit Repeating Event
 windowtitle.task.edit=Edit Repeating Task
+windowtitle.mixed.edit=Edit Repeating Items
+windowtitle.multipleitems=Selected items
 
-buttons.occurrence.delete.label=Delete just this occurrence
-buttons.occurrence.edit.label=Edit just this occurrence
+buttons.single.occurrence.copy.label=Copy only this occurrence
+buttons.single.occurrence.cut.label=Cut only this occurrence
+buttons.single.occurrence.delete.label=Delete only this occurrence
+buttons.single.occurrence.edit.label=Edit only this occurrence
+
+buttons.multiple.occurrence.copy.label=Copy only selected occurrences
+buttons.multiple.occurrence.cut.label=Cut only selected occurrences
+buttons.multiple.occurrence.delete.label=Delete only selected occurrences
+buttons.multiple.occurrence.edit.label=Edit only selected occurrences
 
-buttons.allfollowing.delete.label=Delete this and all future occurrences
-buttons.allfollowing.edit.label=Edit this and all future occurrences
+buttons.single.allfollowing.copy.label=Copy this and all future occurrences
+buttons.single.allfollowing.cut.label=Cut this and all future occurrences
+buttons.single.allfollowing.delete.label=Delete this and all future occurrences
+buttons.single.allfollowing.edit.label=Edit this and all future occurrences
+
+buttons.multiple.allfollowing.copy.label=Copy selected and all future occurrences
+buttons.multiple.allfollowing.cut.label=Cut selected and all future occurrences
+buttons.multiple.allfollowing.delete.label=Delete selcted and all future occurrences
+buttons.multiple.allfollowing.edit.label=Edit selcted and all future occurrences
 
-buttons.parent.delete.label=Delete all occurrences
-buttons.parent.edit.label=Edit all occurrences
+buttons.single.parent.copy.label=Copy all occurrences
+buttons.single.parent.cut.label=Cut all occurrences
+buttons.single.parent.delete.label=Delete all occurrences
+buttons.single.parent.edit.label=Edit all occurrences
+
+buttons.multiple.parent.copy.label=Copy all occurrences of selceted items
+buttons.multiple.parent.cut.label=Cut all occurrences of selceted items
+buttons.multiple.parent.delete.label=Delete all occurrences of selceted items
+buttons.multiple.parent.edit.label=Edit all occurrences of selceted items